package config import "errors" import "fmt" import "os" import "path/filepath" import "strconv" import "strings" import "time" import "unicode" import yaml "github.com/goccy/go-yaml" type Config struct { App AppConfig `yaml:"app"` CTL CTLConfig `yaml:"ctl"` PublicBaseURL string `yaml:"public-base-url"` DataDir string `yaml:"data-dir"` FrontendDir string `yaml:"frontend-dir"` DBDriver string `yaml:"db-driver"` DBDSN string `yaml:"db-dsn"` SessionTTL Duration `yaml:"session-ttl"` AuthMode string `yaml:"auth-mode"` LDAPURL string `yaml:"ldap-url"` LDAPBindDN string `yaml:"ldap-bind-dn"` LDAPBindPassword string `yaml:"ldap-bind-password"` LDAPUserBaseDN string `yaml:"ldap-user-base-dn"` LDAPUserFilter string `yaml:"ldap-user-filter"` LDAPTLSInsecureSkipVerify bool `yaml:"ldap-tls-insecure-skip-verify"` OIDCClientID string `yaml:"oidc-client-id"` OIDCClientSecret string `yaml:"oidc-client-secret"` OIDCAuthorizeURL string `yaml:"oidc-authorize-url"` OIDCTokenURL string `yaml:"oidc-token-url"` OIDCUserInfoURL string `yaml:"oidc-userinfo-url"` OIDCRedirectURL string `yaml:"oidc-redirect-url"` OIDCScopes string `yaml:"oidc-scopes"` OIDCEnabled bool `yaml:"oidc-enabled"` OIDCTLSInsecureSkipVerify bool `yaml:"oidc-tls-insecure-skip-verify"` DockerHTTPPrefix string `yaml:"docker-http-prefix"` GitHTTPPrefix string `yaml:"git-http-prefix"` RPMHTTPPrefix string `yaml:"rpm-http-prefix"` } type CTLConfig struct { Service CTLServiceConfig `yaml:"service"` TLS CTLTLSConfig `yaml:"tls"` } type CTLServiceConfig struct { Addrs []string `yaml:"addresses"` } type CTLTLSConfig struct { Enabled bool `yaml:"enabled"` ServerCertSource string `yaml:"server-cert-source"` CertFile string `yaml:"cert-file"` KeyFile string `yaml:"key-file"` PKIServerCertID string `yaml:"pki-server-cert-id"` ClientAuth string `yaml:"client-auth"` ClientCAFile string `yaml:"client-ca-file"` PKIClientCAID string `yaml:"pki-client-ca-id"` MinVersion string `yaml:"min-version"` } type AppConfig struct { LogMask []string `yaml:"log-mask"` LogFile string `yaml:"log-file"` LogMaxSize int64 `yaml:"log-max-size"` LogRotate int `yaml:"log-rotate"` SiteName string `yaml:"site-name"` } const DefaultEnvPrefix string = "CODIT_" type Duration time.Duration func Load(path string) (Config, error) { return LoadWithEnvPrefix(path, DefaultEnvPrefix) } func LoadWithEnvPrefix(path string, envPrefix string) (Config, error) { return LoadFilesWithEnvPrefix([]string{path}, envPrefix) } func LoadFiles(paths []string) (Config, error) { return LoadFilesWithEnvPrefix(paths, DefaultEnvPrefix) } func LoadFilesWithEnvPrefix(paths []string, envPrefix string) (Config, error) { var cfg Config var serverID string var dataDir string var err error serverID = ServerIdFromEnvPrefix(envPrefix) dataDir = "./" + serverID + "-data" cfg = Config{ App: AppConfig{LogMask: []string{}}, CTL: CTLConfig{ //Service: CTLServiceConfig{Addrs: []string{":1080"}}, TLS: CTLTLSConfig{ServerCertSource: "files", ClientAuth: "none", MinVersion: "1.2"}, }, DataDir: dataDir, FrontendDir: filepath.Join("..", "frontend", "dist"), DBDriver: "sqlite", DBDSN: "file:" + dataDir + "/" + serverID + ".db", SessionTTL: Duration(24 * time.Hour), AuthMode: "db", LDAPUserFilter: "(uid={username})", OIDCScopes: "openid profile email", DockerHTTPPrefix: "/v2", GitHTTPPrefix: "/git", RPMHTTPPrefix: "/rpm", } err = loadConfigFiles(&cfg, paths) if err != nil { return cfg, err } override(&cfg, envPrefix) normalizeCTLConfig(&cfg) cfg.AuthMode = strings.ToLower(strings.TrimSpace(cfg.AuthMode)) cfg.DockerHTTPPrefix = NormalizeHTTPPrefix(cfg.DockerHTTPPrefix, "/v2") cfg.GitHTTPPrefix = NormalizeHTTPPrefix(cfg.GitHTTPPrefix, "/git") cfg.RPMHTTPPrefix = NormalizeHTTPPrefix(cfg.RPMHTTPPrefix, "/rpm") if cfg.DBDSN == "" { return cfg, errors.New("db dsn is required") } return cfg, nil } func normalizeCTLConfig(cfg *Config) { cfg.CTL.Service.Addrs = NormalizeHTTPAddrs(cfg.CTL.Service.Addrs) cfg.CTL.TLS.ServerCertSource = strings.ToLower(strings.TrimSpace(cfg.CTL.TLS.ServerCertSource)) cfg.CTL.TLS.CertFile = strings.TrimSpace(cfg.CTL.TLS.CertFile) cfg.CTL.TLS.KeyFile = strings.TrimSpace(cfg.CTL.TLS.KeyFile) cfg.CTL.TLS.PKIServerCertID = strings.TrimSpace(cfg.CTL.TLS.PKIServerCertID) cfg.CTL.TLS.ClientAuth = strings.ToLower(strings.TrimSpace(cfg.CTL.TLS.ClientAuth)) cfg.CTL.TLS.ClientCAFile = strings.TrimSpace(cfg.CTL.TLS.ClientCAFile) cfg.CTL.TLS.PKIClientCAID = strings.TrimSpace(cfg.CTL.TLS.PKIClientCAID) cfg.CTL.TLS.MinVersion = strings.TrimSpace(cfg.CTL.TLS.MinVersion) } func loadConfigFiles(cfg *Config, paths []string) error { var expanded []string var path string var i int var err error expanded, err = expandConfigFiles(paths) if err != nil { return err } for i = 0; i < len(expanded); i++ { path = expanded[i] err = loadConfigFile(cfg, path) if err != nil { return fmt.Errorf("load config file %s - %w", path, err) } } return nil } func loadConfigFile(cfg *Config, path string) error { var f *os.File var yd *yaml.Decoder var err error if strings.TrimSpace(path) == "" { return nil } f, err = os.Open(path) if err != nil { return err } yd = yaml.NewDecoder(f, yaml.AllowDuplicateMapKey(), yaml.DisallowUnknownField()) err = yd.Decode(cfg) f.Close() return err } func expandConfigFiles(paths []string) ([]string, error) { var out []string var matches []string var path string var i int var j int var err error for i = 0; i < len(paths); i++ { path = strings.TrimSpace(paths[i]) if path == "" { continue } if hasGlobMeta(path) { matches, err = filepath.Glob(path) if err != nil { return nil, err } for j = 0; j < len(matches); j++ { out = append(out, matches[j]) } } else { out = append(out, path) } } return out, nil } func hasGlobMeta(path string) bool { return strings.ContainsAny(path, "*?[") } func override(cfg *Config, envPrefix string) { var v string v = getEnv(envPrefix, "LOG_MASK") if v != "" { cfg.App.LogMask = splitCSV(v) } v = getEnv(envPrefix, "LOG_FILE") if v != "" { cfg.App.LogFile = v } v = getEnv(envPrefix, "LOG_MAX_SIZE") if v != "" { cfg.App.LogMaxSize, _ = strconv.ParseInt(v, 10, 64) } v = getEnv(envPrefix, "LOG_ROTATE") if v != "" { cfg.App.LogRotate, _ = strconv.Atoi(v) } v = getEnv(envPrefix, "SITE_NAME") if v != "" { cfg.App.SiteName = v } v = getEnv(envPrefix, "CTL_SERVICE_ADDRESSES") if v != "" { cfg.CTL.Service.Addrs = splitCSV(v) } v = getEnv(envPrefix, "CTL_TLS_ENABLED") if v != "" { cfg.CTL.TLS.Enabled = parseEnvBool(v) } v = getEnv(envPrefix, "CTL_TLS_SERVER_CERT_SOURCE") if v != "" { cfg.CTL.TLS.ServerCertSource = v } v = getEnv(envPrefix, "CTL_TLS_CERT_FILE") if v != "" { cfg.CTL.TLS.CertFile = v } v = getEnv(envPrefix, "CTL_TLS_KEY_FILE") if v != "" { cfg.CTL.TLS.KeyFile = v } v = getEnv(envPrefix, "CTL_TLS_PKI_SERVER_CERT_ID") if v != "" { cfg.CTL.TLS.PKIServerCertID = v } v = getEnv(envPrefix, "CTL_TLS_CLIENT_AUTH") if v != "" { cfg.CTL.TLS.ClientAuth = v } v = getEnv(envPrefix, "CTL_TLS_CLIENT_CA_FILE") if v != "" { cfg.CTL.TLS.ClientCAFile = v } v = getEnv(envPrefix, "CTL_TLS_PKI_CLIENT_CA_ID") if v != "" { cfg.CTL.TLS.PKIClientCAID = v } v = getEnv(envPrefix, "CTL_TLS_MIN_VERSION") if v != "" { cfg.CTL.TLS.MinVersion = v } v = getEnv(envPrefix, "PUBLIC_BASE_URL") if v != "" { cfg.PublicBaseURL = v } v = getEnv(envPrefix, "DATA_DIR") if v != "" { cfg.DataDir = v } v = getEnv(envPrefix, "FRONTEND_DIR") if v != "" { cfg.FrontendDir = v } v = getEnv(envPrefix, "DB_DRIVER") if v != "" { cfg.DBDriver = v } v = getEnv(envPrefix, "DB_DSN") if v != "" { cfg.DBDSN = v } v = getEnv(envPrefix, "AUTH_MODE") if v != "" { cfg.AuthMode = v } v = getEnv(envPrefix, "LDAP_URL") if v != "" { cfg.LDAPURL = v } v = getEnv(envPrefix, "LDAP_BIND_DN") if v != "" { cfg.LDAPBindDN = v } v = getEnv(envPrefix, "LDAP_BIND_PASSWORD") if v != "" { cfg.LDAPBindPassword = v } v = getEnv(envPrefix, "LDAP_USER_BASE_DN") if v != "" { cfg.LDAPUserBaseDN = v } v = getEnv(envPrefix, "LDAP_USER_FILTER") if v != "" { cfg.LDAPUserFilter = v } v = getEnv(envPrefix, "LDAP_TLS_INSECURE_SKIP_VERIFY") if v != "" { cfg.LDAPTLSInsecureSkipVerify = parseEnvBool(v) } v = getEnv(envPrefix, "OIDC_CLIENT_ID") if v != "" { cfg.OIDCClientID = v } v = getEnv(envPrefix, "OIDC_CLIENT_SECRET") if v != "" { cfg.OIDCClientSecret = v } v = getEnv(envPrefix, "OIDC_AUTHORIZE_URL") if v != "" { cfg.OIDCAuthorizeURL = v } v = getEnv(envPrefix, "OIDC_TOKEN_URL") if v != "" { cfg.OIDCTokenURL = v } v = getEnv(envPrefix, "OIDC_USERINFO_URL") if v != "" { cfg.OIDCUserInfoURL = v } v = getEnv(envPrefix, "OIDC_REDIRECT_URL") if v != "" { cfg.OIDCRedirectURL = v } v = getEnv(envPrefix, "OIDC_SCOPES") if v != "" { cfg.OIDCScopes = v } v = getEnv(envPrefix, "OIDC_ENABLED") if v != "" { cfg.OIDCEnabled = parseEnvBool(v) } v = getEnv(envPrefix, "OIDC_TLS_INSECURE_SKIP_VERIFY") if v != "" { cfg.OIDCTLSInsecureSkipVerify = parseEnvBool(v) } v = getEnv(envPrefix, "DOCKER_HTTP_PREFIX") if v != "" { cfg.DockerHTTPPrefix = v } v = getEnv(envPrefix, "GIT_HTTP_PREFIX") if v != "" { cfg.GitHTTPPrefix = v } v = getEnv(envPrefix, "RPM_HTTP_PREFIX") if v != "" { cfg.RPMHTTPPrefix = v } } func getEnv(prefix string, name string) string { if prefix == "" { return os.Getenv(name) } return os.Getenv(prefix + name) } func (d Duration) Duration() time.Duration { return time.Duration(d) } func (d *Duration) UnmarshalYAML(b []byte) error { var raw string var asNumber int64 var err error var parsed time.Duration raw = strings.TrimSpace(string(b)) raw = strings.Trim(raw, "'\"") parsed, err = time.ParseDuration(raw) if err == nil { *d = Duration(parsed) return nil } asNumber, err = strconv.ParseInt(raw, 10, 64) if err == nil { *d = Duration(time.Duration(asNumber)) return nil } return errors.New("invalid duration format") } func parseEnvBool(v string) bool { var lowered string var parsed bool var err error lowered = strings.ToLower(strings.TrimSpace(v)) if lowered == "true" || lowered == "yes" || lowered == "y" || lowered == "on" { return true } if lowered == "false" || lowered == "no" || lowered == "n" || lowered == "off" { return false } parsed, err = strconv.ParseBool(lowered) if err == nil { return parsed } return false } func splitCSV(v string) []string { var parts []string var out []string var i int var p string parts = strings.Split(v, ",") for i = 0; i < len(parts); i++ { p = strings.TrimSpace(parts[i]) if p == "" { continue } out = append(out, p) } return out } func NormalizeHTTPAddrs(values []string) []string { var out []string var i int var v string for i = 0; i < len(values); i++ { v = strings.TrimSpace(values[i]) if v == "" { continue } out = append(out, v) } return out } func NormalizeHTTPPrefix(value string, fallback string) string { value = strings.TrimSpace(value) if value == "" { value = fallback } if !strings.HasPrefix(value, "/") { value = "/" + value } value = strings.TrimRight(value, "/") if value == "" { return "/" } return value } func NormalizeServerId(value string) string { value = strings.ToLower(strings.TrimSpace(value)) value = strings.ReplaceAll(value, "_", "-") value = strings.Trim(value, "-") if value == "" { return "codit" } return value } func EnvPrefixFromServerId(value string) string { var prefix string prefix = strings.ToUpper(strings.ReplaceAll(NormalizeServerId(value), "-", "_")) if !strings.HasSuffix(prefix, "_") { prefix = prefix + "_" } return prefix } func TitleFromServerId(value string) string { var id string var parts []string var i int id = NormalizeServerId(value) parts = strings.Split(id, "-") for i = 0; i < len(parts); i++ { parts[i] = titleWord(parts[i]) } return strings.Join(parts, " ") } func ServerIdFromEnvPrefix(envPrefix string) string { var value string value = strings.TrimSpace(envPrefix) value = strings.TrimSuffix(value, "_") return NormalizeServerId(value) } func titleWord(value string) string { var runes []rune var i int runes = []rune(strings.ToLower(strings.TrimSpace(value))) if len(runes) == 0 { return "" } runes[0] = unicode.ToTitle(runes[0]) for i = 1; i < len(runes); i++ { runes[i] = unicode.ToLower(runes[i]) } return string(runes) }