446 lines
13 KiB
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)
|
|
}
|