2023-03-14 15:22:52 +00:00
package config
import (
2023-03-24 13:41:08 +00:00
"fmt"
2023-12-27 16:24:52 +00:00
"io"
2023-07-03 14:31:12 +00:00
"net/url"
2023-03-14 15:22:52 +00:00
"os"
"path/filepath"
2023-06-07 18:50:30 +00:00
"reflect"
2024-01-04 04:11:46 +00:00
"slices"
2023-03-24 13:41:08 +00:00
"strconv"
"strings"
2024-09-09 09:50:05 +00:00
"time"
2023-06-18 15:01:27 +00:00
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/utils"
"gopkg.in/yaml.v3"
2023-03-14 15:22:52 +00:00
)
2024-04-27 00:53:48 +00:00
var OpengistVersion = ""
2023-03-14 15:22:52 +00:00
var C * config
2024-10-24 21:23:00 +00:00
var SecretKey [ ] byte
2023-04-06 23:52:56 +00:00
// Not using nested structs because the library
// doesn't support dot notation in this case sadly
2023-03-14 15:22:52 +00:00
type config struct {
2024-10-31 13:50:13 +00:00
SecretKey string ` yaml:"secret-key" env:"OG_SECRET_KEY" `
2023-06-07 18:50:30 +00:00
LogLevel string ` yaml:"log-level" env:"OG_LOG_LEVEL" `
2023-12-27 16:24:52 +00:00
LogOutput string ` yaml:"log-output" env:"OG_LOG_OUTPUT" `
2023-06-07 18:50:30 +00:00
ExternalUrl string ` yaml:"external-url" env:"OG_EXTERNAL_URL" `
OpengistHome string ` yaml:"opengist-home" env:"OG_OPENGIST_HOME" `
2024-09-20 14:01:09 +00:00
DBUri string ` yaml:"db-uri" env:"OG_DB_URI" `
DBFilename string ` yaml:"db-filename" env:"OG_DB_FILENAME" ` // deprecated
2024-01-04 02:38:15 +00:00
IndexEnabled bool ` yaml:"index.enabled" env:"OG_INDEX_ENABLED" `
IndexDirname string ` yaml:"index.dirname" env:"OG_INDEX_DIRNAME" `
2023-06-07 18:50:30 +00:00
2023-12-27 16:30:42 +00:00
GitDefaultBranch string ` yaml:"git.default-branch" env:"OG_GIT_DEFAULT_BRANCH" `
2023-06-09 13:25:41 +00:00
SqliteJournalMode string ` yaml:"sqlite.journal-mode" env:"OG_SQLITE_JOURNAL_MODE" `
2023-09-18 16:06:27 +00:00
HttpHost string ` yaml:"http.host" env:"OG_HTTP_HOST" `
HttpPort string ` yaml:"http.port" env:"OG_HTTP_PORT" `
HttpGit bool ` yaml:"http.git-enabled" env:"OG_HTTP_GIT_ENABLED" `
2023-06-07 18:50:30 +00:00
SshGit bool ` yaml:"ssh.git-enabled" env:"OG_SSH_GIT_ENABLED" `
SshHost string ` yaml:"ssh.host" env:"OG_SSH_HOST" `
SshPort string ` yaml:"ssh.port" env:"OG_SSH_PORT" `
SshExternalDomain string ` yaml:"ssh.external-domain" env:"OG_SSH_EXTERNAL_DOMAIN" `
SshKeygen string ` yaml:"ssh.keygen-executable" env:"OG_SSH_KEYGEN_EXECUTABLE" `
GithubClientKey string ` yaml:"github.client-key" env:"OG_GITHUB_CLIENT_KEY" `
GithubSecret string ` yaml:"github.secret" env:"OG_GITHUB_SECRET" `
2023-12-18 00:35:44 +00:00
GitlabClientKey string ` yaml:"gitlab.client-key" env:"OG_GITLAB_CLIENT_KEY" `
GitlabSecret string ` yaml:"gitlab.secret" env:"OG_GITLAB_SECRET" `
GitlabUrl string ` yaml:"gitlab.url" env:"OG_GITLAB_URL" `
2024-02-19 01:20:03 +00:00
GitlabName string ` yaml:"gitlab.name" env:"OG_GITLAB_NAME" `
2023-12-18 00:35:44 +00:00
2023-06-07 18:50:30 +00:00
GiteaClientKey string ` yaml:"gitea.client-key" env:"OG_GITEA_CLIENT_KEY" `
GiteaSecret string ` yaml:"gitea.secret" env:"OG_GITEA_SECRET" `
GiteaUrl string ` yaml:"gitea.url" env:"OG_GITEA_URL" `
2024-02-19 01:20:03 +00:00
GiteaName string ` yaml:"gitea.name" env:"OG_GITEA_NAME" `
2023-09-15 21:56:14 +00:00
OIDCClientKey string ` yaml:"oidc.client-key" env:"OG_OIDC_CLIENT_KEY" `
OIDCSecret string ` yaml:"oidc.secret" env:"OG_OIDC_SECRET" `
OIDCDiscoveryUrl string ` yaml:"oidc.discovery-url" env:"OG_OIDC_DISCOVERY_URL" `
2024-01-20 22:46:47 +00:00
2024-04-02 15:12:54 +00:00
CustomLogo string ` yaml:"custom.logo" env:"OG_CUSTOM_LOGO" `
CustomFavicon string ` yaml:"custom.favicon" env:"OG_CUSTOM_FAVICON" `
StaticLinks [ ] StaticLink ` yaml:"custom.static-links" env:"OG_CUSTOM_STATIC_LINK" `
}
type StaticLink struct {
Name string ` yaml:"name" env:"OG_CUSTOM_STATIC_LINK_#_NAME" `
Path string ` yaml:"path" env:"OG_CUSTOM_STATIC_LINK_#_PATH" `
2023-03-14 15:22:52 +00:00
}
2023-03-15 00:36:46 +00:00
func configWithDefaults ( ) ( * config , error ) {
c := & config { }
2024-10-31 13:50:13 +00:00
c . SecretKey = ""
2023-04-06 23:52:56 +00:00
c . LogLevel = "warn"
2023-12-27 16:24:52 +00:00
c . LogOutput = "stdout,file"
2023-10-31 06:23:15 +00:00
c . OpengistHome = ""
2024-09-20 14:01:09 +00:00
c . DBUri = "opengist.db"
2024-01-04 02:38:15 +00:00
c . IndexEnabled = true
c . IndexDirname = "opengist.index"
2023-03-15 00:36:46 +00:00
2023-06-09 13:25:41 +00:00
c . SqliteJournalMode = "WAL"
2023-04-06 23:52:56 +00:00
c . HttpHost = "0.0.0.0"
c . HttpPort = "6157"
c . HttpGit = true
2023-03-15 09:37:17 +00:00
2023-04-06 23:52:56 +00:00
c . SshGit = true
c . SshHost = "0.0.0.0"
c . SshPort = "2222"
c . SshKeygen = "ssh-keygen"
2023-03-15 00:36:46 +00:00
2024-02-19 01:20:03 +00:00
c . GitlabName = "GitLab"
2023-12-18 00:35:44 +00:00
c . GiteaUrl = "https://gitea.com"
2024-02-19 01:20:03 +00:00
c . GiteaName = "Gitea"
2023-04-17 17:11:32 +00:00
2023-03-15 00:36:46 +00:00
return c , nil
}
2024-01-29 23:07:57 +00:00
func InitConfig ( configPath string , out io . Writer ) error {
2023-04-06 23:52:56 +00:00
// Default values
2023-03-15 00:36:46 +00:00
c , err := configWithDefaults ( )
if err != nil {
return err
}
2024-01-29 23:07:57 +00:00
if err = loadConfigFromYaml ( c , configPath , out ) ; err != nil {
2023-06-07 18:50:30 +00:00
return err
2023-03-14 15:22:52 +00:00
}
2024-01-29 23:07:57 +00:00
if err = loadConfigFromEnv ( c , out ) ; err != nil {
2023-06-07 18:50:30 +00:00
return err
2023-03-14 15:22:52 +00:00
}
2023-04-06 23:52:56 +00:00
2023-10-31 06:23:15 +00:00
if c . OpengistHome == "" {
homeDir , err := os . UserHomeDir ( )
if err != nil {
return fmt . Errorf ( "opengist home directory is not set and current user home directory could not be determined; please specify the opengist home directory manually via the configuration" )
}
c . OpengistHome = filepath . Join ( homeDir , ".opengist" )
}
2023-07-03 14:31:12 +00:00
if err = checks ( c ) ; err != nil {
return err
}
2023-03-14 15:22:52 +00:00
C = c
2024-10-31 13:50:13 +00:00
if err = migrateConfig ( ) ; err != nil {
return err
}
2024-10-24 21:23:00 +00:00
2024-01-23 19:24:01 +00:00
if err = os . Setenv ( "OG_OPENGIST_HOME_INTERNAL" , GetHomeDir ( ) ) ; err != nil {
return err
}
2023-03-14 15:22:52 +00:00
return nil
}
func InitLog ( ) {
if err := os . MkdirAll ( filepath . Join ( GetHomeDir ( ) , "log" ) , 0755 ) ; err != nil {
panic ( err )
}
var level zerolog . Level
2023-12-27 16:24:52 +00:00
level , err := zerolog . ParseLevel ( C . LogLevel )
2023-03-14 15:22:52 +00:00
if err != nil {
level = zerolog . InfoLevel
}
2023-12-27 16:24:52 +00:00
var logWriters [ ] io . Writer
logOutputTypes := utils . RemoveDuplicates [ string ] (
strings . Split ( strings . ToLower ( C . LogOutput ) , "," ) ,
)
2024-09-09 09:50:05 +00:00
consoleWriter := zerolog . NewConsoleWriter (
func ( w * zerolog . ConsoleWriter ) {
w . TimeFormat = time . TimeOnly
w . FormatCaller = func ( i interface { } ) string {
file := i . ( string )
index := strings . Index ( file , "internal" )
if index == - 1 {
return file
}
return file [ index : ]
}
} ,
)
2023-12-27 16:24:52 +00:00
for _ , logOutputType := range logOutputTypes {
logOutputType = strings . TrimSpace ( logOutputType )
2024-01-04 04:11:46 +00:00
if ! slices . Contains ( [ ] string { "stdout" , "file" } , logOutputType ) {
2023-12-27 16:24:52 +00:00
defer func ( ) { log . Warn ( ) . Msg ( "Invalid log output type: " + logOutputType ) } ( )
continue
}
switch logOutputType {
case "stdout" :
2024-09-09 09:50:05 +00:00
logWriters = append ( logWriters , consoleWriter )
2023-12-27 16:24:52 +00:00
defer func ( ) { log . Debug ( ) . Msg ( "Logging to stdout" ) } ( )
case "file" :
file , err := os . OpenFile ( filepath . Join ( GetHomeDir ( ) , "log" , "opengist.log" ) , os . O_APPEND | os . O_CREATE | os . O_WRONLY , 0644 )
if err != nil {
panic ( err )
}
logWriters = append ( logWriters , file )
defer func ( ) { log . Debug ( ) . Msg ( "Logging to file: " + file . Name ( ) ) } ( )
}
}
if len ( logWriters ) == 0 {
2024-09-09 09:50:05 +00:00
logWriters = append ( logWriters , consoleWriter )
2023-12-27 16:24:52 +00:00
defer func ( ) { log . Warn ( ) . Msg ( "No valid log outputs, defaulting to stdout" ) } ( )
}
multi := zerolog . MultiLevelWriter ( logWriters ... )
2024-09-09 09:50:05 +00:00
log . Logger = zerolog . New ( multi ) . Level ( level ) . With ( ) . Caller ( ) . Timestamp ( ) . Logger ( )
2023-06-09 13:25:41 +00:00
2024-09-09 09:50:05 +00:00
if ! slices . Contains ( [ ] string { "debug" , "info" , "warn" , "error" , "fatal" } , strings . ToLower ( C . LogLevel ) ) {
2023-06-09 13:25:41 +00:00
log . Warn ( ) . Msg ( "Invalid log level: " + C . LogLevel )
}
2023-03-14 15:22:52 +00:00
}
2023-03-24 13:41:08 +00:00
func CheckGitVersion ( version string ) ( bool , error ) {
versionParts := strings . Split ( version , "." )
if len ( versionParts ) < 2 {
return false , fmt . Errorf ( "invalid version string" )
}
major , err := strconv . Atoi ( versionParts [ 0 ] )
if err != nil {
return false , fmt . Errorf ( "invalid major version number" )
}
minor , err := strconv . Atoi ( versionParts [ 1 ] )
if err != nil {
return false , fmt . Errorf ( "invalid minor version number" )
}
2023-12-27 16:30:42 +00:00
// Check if version is prior to 2.28
if major < 2 || ( major == 2 && minor < 28 ) {
2023-03-24 13:41:08 +00:00
return false , nil
}
return true , nil
}
2023-03-14 15:22:52 +00:00
func GetHomeDir ( ) string {
absolutePath , _ := filepath . Abs ( C . OpengistHome )
return filepath . Clean ( absolutePath )
}
2023-06-07 18:50:30 +00:00
2024-10-31 13:50:13 +00:00
func SetupSecretKey ( ) {
if C . SecretKey == "" {
path := filepath . Join ( GetHomeDir ( ) , "opengist-secret.key" )
SecretKey , _ = utils . GenerateSecretKey ( path )
} else {
SecretKey = [ ] byte ( C . SecretKey )
}
}
2024-01-29 23:07:57 +00:00
func loadConfigFromYaml ( c * config , configPath string , out io . Writer ) error {
2023-06-07 18:50:30 +00:00
if configPath != "" {
absolutePath , _ := filepath . Abs ( configPath )
absolutePath = filepath . Clean ( absolutePath )
file , err := os . Open ( absolutePath )
if err != nil {
if ! os . IsNotExist ( err ) {
return err
}
2024-01-29 23:07:57 +00:00
_ , _ = fmt . Fprintln ( out , "No YAML config file found at " + absolutePath )
2023-06-07 18:50:30 +00:00
} else {
2024-01-29 23:07:57 +00:00
_ , _ = fmt . Fprintln ( out , "Using YAML config file: " + absolutePath )
2023-06-07 18:50:30 +00:00
// Override default values with values from config.yml
d := yaml . NewDecoder ( file )
if err = d . Decode ( & c ) ; err != nil {
return err
}
defer file . Close ( )
}
} else {
2024-01-29 23:07:57 +00:00
_ , _ = fmt . Fprintln ( out , "No YAML config file specified." )
2023-06-07 18:50:30 +00:00
}
return nil
}
2024-01-29 23:07:57 +00:00
func loadConfigFromEnv ( c * config , out io . Writer ) error {
2023-06-07 18:50:30 +00:00
v := reflect . ValueOf ( c ) . Elem ( )
var envVars [ ] string
for i := 0 ; i < v . NumField ( ) ; i ++ {
tag := v . Type ( ) . Field ( i ) . Tag . Get ( "env" )
if tag == "" {
continue
}
envValue := os . Getenv ( strings . ToUpper ( tag ) )
2024-04-02 15:12:54 +00:00
if envValue == "" && v . Field ( i ) . Kind ( ) != reflect . Slice {
2023-06-07 18:50:30 +00:00
continue
}
switch v . Field ( i ) . Kind ( ) {
case reflect . String :
v . Field ( i ) . SetString ( envValue )
2024-04-02 15:12:54 +00:00
envVars = append ( envVars , tag )
2023-06-07 18:50:30 +00:00
case reflect . Bool :
boolVal , err := strconv . ParseBool ( envValue )
if err != nil {
return err
}
v . Field ( i ) . SetBool ( boolVal )
2024-04-02 15:12:54 +00:00
envVars = append ( envVars , tag )
case reflect . Slice :
if v . Type ( ) . Field ( i ) . Type . Elem ( ) . Kind ( ) == reflect . Struct {
prefix := strings . ToUpper ( tag ) + "_"
var sliceValue reflect . Value
elemType := v . Type ( ) . Field ( i ) . Type . Elem ( )
for index := 0 ; ; index ++ {
allFieldsPresent := true
elemValue := reflect . New ( elemType ) . Elem ( )
for j := 0 ; j < elemValue . NumField ( ) && allFieldsPresent ; j ++ {
elemField := elemValue . Type ( ) . Field ( j )
envName := fmt . Sprintf ( "%s%d_%s" , prefix , index , strings . ToUpper ( elemField . Name ) )
envValue , present := os . LookupEnv ( envName )
if ! present {
allFieldsPresent = false
break
}
envVars = append ( envVars , envName )
elemValue . Field ( j ) . SetString ( envValue )
}
if ! allFieldsPresent {
break
}
if sliceValue . Kind ( ) != reflect . Slice {
sliceValue = reflect . MakeSlice ( v . Type ( ) . Field ( i ) . Type , 0 , index + 1 )
}
sliceValue = reflect . Append ( sliceValue , elemValue )
}
if sliceValue . IsValid ( ) {
v . Field ( i ) . Set ( sliceValue )
}
}
default :
return fmt . Errorf ( "unsupported type: %s" , v . Field ( i ) . Kind ( ) )
2023-06-07 18:50:30 +00:00
}
}
if len ( envVars ) > 0 {
2024-01-29 23:07:57 +00:00
_ , _ = fmt . Fprintln ( out , "Using environment variables config: " + strings . Join ( envVars , ", " ) )
2023-06-07 18:50:30 +00:00
} else {
2024-01-29 23:07:57 +00:00
_ , _ = fmt . Fprintln ( out , "No environment variables config specified." )
2023-06-07 18:50:30 +00:00
}
return nil
}
2023-07-03 14:31:12 +00:00
func checks ( c * config ) error {
if _ , err := url . Parse ( c . ExternalUrl ) ; err != nil {
return err
}
if _ , err := url . Parse ( c . GiteaUrl ) ; err != nil {
return err
}
2023-09-25 14:08:26 +00:00
if _ , err := url . Parse ( c . OIDCDiscoveryUrl ) ; err != nil {
2023-09-15 21:56:14 +00:00
return err
}
2023-07-03 14:31:12 +00:00
return nil
}