Files
codit/backend/config/config.go

446 lines
13 KiB
Go

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)
}