diff --git a/go.mod b/go.mod index fdcb3dc7..031755cd 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( replace github.com/go-openapi/validate => github.com/flant/go-openapi-validate v0.19.12-flant.0 require ( + github.com/caarlos0/env/v11 v11.3.1 github.com/deckhouse/module-sdk v0.10.4 github.com/gojuno/minimock/v3 v3.4.7 github.com/itchyny/gojq v0.12.17 diff --git a/go.sum b/go.sum index eef281b4..76d7b260 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= +github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= diff --git a/pkg/app/app_config.go b/pkg/app/app_config.go new file mode 100644 index 00000000..95ecacb6 --- /dev/null +++ b/pkg/app/app_config.go @@ -0,0 +1,253 @@ +package app + +import ( + "fmt" + "time" + + env "github.com/caarlos0/env/v11" + + "github.com/deckhouse/deckhouse/pkg/log" +) + +type appConfig struct { + HooksDir string `env:"HOOKS_DIR"` + TmpDir string `env:"TMP_DIR"` + ListenAddress string `env:"LISTEN_ADDRESS"` + ListenPort string `env:"LISTEN_PORT"` + PrometheusMetricsPrefix string `env:"PROMETHEUS_METRICS_PREFIX"` + // unused? + HooksMetricsListenPort string `env:"HOOK_METRICS_LISTEN_PORT"` + Namespace string `env:"NAMESPACE"` +} + +func newAppConfig() *appConfig { + return &appConfig{} +} + +type debugConfig struct { + HTTPServerAddress string `env:"HTTP_SERVER_ADDR"` + KeepTemporaryFiles string `env:"KEEP_TMP_FILES"` + KubernetesAPI bool `env:"KUBERNETES_API"` + UnixSocket string `env:"UNIX_SOCKET"` +} + +func newDebugConfig() *debugConfig { + return &debugConfig{} +} + +type kubeConfig struct { + // Settings for Kubernetes connection. + ContextName string `env:"CONTEXT"` + ConfigPath string `env:"CONFIG"` + ServerAddress string `env:"SERVER"` + // Rate limit settings for 'main' kube client + ClientQPS float32 `env:"CLIENT_QPS"` + ClientBurst int `env:"CLIENT_BURST"` +} + +func newKubeConfig() *kubeConfig { + return &kubeConfig{} +} + +type objectPatcherConfig struct { + // Settings for 'object_patcher' kube client + KubeClientQPS float32 `env:"KUBE_CLIENT_QPS"` + KubeClisntBurst int `env:"KUBE_CLIENT_BURST"` + KubeClientTimeout time.Duration `env:"KUBE_CLIENT_TIMEOUT"` +} + +func newObjectPatcherConfig() *objectPatcherConfig { + return &objectPatcherConfig{} +} + +type validatingWebhookConfig struct { + ConfigurationName string `env:"CONFIGURATION_NAME"` + ServiceName string `env:"SERVICE_NAME"` + ServerCert string `env:"SERVER_CERT"` + ServerKey string `env:"SERVER_KEY"` + CA string `env:"CA"` + // check separator? + ClientCA []string `env:"CLIENT_CA" envSeparator:","` + // enum "Fail" || "Ignore" + FailurePolicy string `env:"FAILURE_POLICY"` + ListenPort string `env:"LISTEN_PORT"` + ListenAddress string `env:"LISTEN_ADDRESS"` +} + +func newValidatingWebhookConfig() *validatingWebhookConfig { + return &validatingWebhookConfig{} +} + +type conversionWebhookConfig struct { + ServiceName string `env:"SERVICE_NAME"` + ServerCert string `env:"SERVER_CERT"` + ServerKey string `env:"SERVER_KEY"` + CA string `env:"CA"` + // check separator? + ClientCA []string `env:"CLIENT_CA" envSeparator:","` + ListenPort string `env:"LISTEN_PORT"` + ListenAddress string `env:"LISTEN_ADDRESS"` +} + +func newConversionWebhookConfig() *conversionWebhookConfig { + return &conversionWebhookConfig{} +} + +type logConfig struct { + Level string `env:"LEVEL"` + Type string `env:"TYPE"` + NoTime bool `env:"NO_TIME"` + ProxyHookJson bool `env:"PROXY_HOOK_JSON"` +} + +func newLogConfig() *logConfig { + return &logConfig{} +} + +type Config struct { + AppConfig *appConfig `envPrefix:"SHELL_OPERATOR_"` + KubeConfig *kubeConfig `envPrefix:"KUBE_"` + ObjectPatcherConfig *objectPatcherConfig `envPrefix:"OBJECT_PATCHER_"` + ValidatingWebhookConfig *validatingWebhookConfig `envPrefix:"VALIDATING_WEBHOOK_"` + ConversionWebhookConfig *conversionWebhookConfig `envPrefix:"CONVERSION_WEBHOOK_"` + + DebugConfig *debugConfig `envPrefix:"DEBUG_"` + + LogConfig *logConfig `envPrefix:"LOG_"` + LogLevel log.Level `env:"-"` + + ready bool +} + +func NewConfig() *Config { + return &Config{ + AppConfig: newAppConfig(), + KubeConfig: newKubeConfig(), + ObjectPatcherConfig: newObjectPatcherConfig(), + ValidatingWebhookConfig: newValidatingWebhookConfig(), + ConversionWebhookConfig: newConversionWebhookConfig(), + DebugConfig: newDebugConfig(), + LogConfig: newLogConfig(), + } +} + +func (cfg *Config) Parse() error { + if cfg.IsReady() { + return nil + } + + opts := env.Options{ + Prefix: "", + } + + err := env.ParseWithOptions(cfg, opts) + if err != nil { + return fmt.Errorf("failed to parse config: %w", err) + } + + cfg.LogLevel = log.LogLevelFromStr(cfg.LogConfig.Level) + + return nil +} + +func (cfg *Config) SetupGlobalVars() { + if cfg.IsReady() { + return + } + + setIfNotEmpty(&HooksDir, cfg.AppConfig.HooksDir) + setIfNotEmpty(&TempDir, cfg.AppConfig.TmpDir) + setIfNotEmpty(&ListenAddress, cfg.AppConfig.ListenAddress) + setIfNotEmpty(&ListenPort, cfg.AppConfig.ListenPort) + setIfNotEmpty(&PrometheusMetricsPrefix, cfg.AppConfig.PrometheusMetricsPrefix) + setIfNotEmpty(&Namespace, cfg.AppConfig.Namespace) + + setIfNotEmpty(&DebugHttpServerAddr, cfg.DebugConfig.HTTPServerAddress) + setIfNotEmpty(&DebugKeepTmpFiles, cfg.DebugConfig.KeepTemporaryFiles == "true" || cfg.DebugConfig.KeepTemporaryFiles == "yes") + setIfNotEmpty(&DebugKubernetesAPI, cfg.DebugConfig.KubernetesAPI) + setIfNotEmpty(&DebugUnixSocket, cfg.DebugConfig.UnixSocket) + + setIfNotEmpty(&KubeContext, cfg.KubeConfig.ContextName) + setIfNotEmpty(&KubeConfig, cfg.KubeConfig.ConfigPath) + setIfNotEmpty(&KubeServer, cfg.KubeConfig.ServerAddress) + setIfNotEmpty(&KubeClientQps, cfg.KubeConfig.ClientQPS) + setIfNotEmpty(&KubeClientBurst, cfg.KubeConfig.ClientBurst) + + setIfNotEmpty(&ObjectPatcherKubeClientQps, cfg.ObjectPatcherConfig.KubeClientQPS) + setIfNotEmpty(&ObjectPatcherKubeClientBurst, cfg.ObjectPatcherConfig.KubeClisntBurst) + setIfNotEmpty(&ObjectPatcherKubeClientTimeout, cfg.ObjectPatcherConfig.KubeClientTimeout) + + setIfNotEmpty(&LogLevel, cfg.LogConfig.Level) + setIfNotEmpty(&LogNoTime, cfg.LogConfig.NoTime) + setIfNotEmpty(&LogType, cfg.LogConfig.Type) + setIfNotEmpty(&LogProxyHookJSON, cfg.LogConfig.ProxyHookJson) + + setIfNotEmpty(&ValidatingWebhookConfigurationName, cfg.ValidatingWebhookConfig.ConfigurationName) + setIfNotEmpty(&ValidatingWebhookServiceName, cfg.ValidatingWebhookConfig.ServiceName) + setIfNotEmpty(&ValidatingWebhookServerCert, cfg.ValidatingWebhookConfig.ServerCert) + setIfNotEmpty(&ValidatingWebhookServerKey, cfg.ValidatingWebhookConfig.ServerKey) + setIfNotEmpty(&ValidatingWebhookCA, cfg.ValidatingWebhookConfig.CA) + setSliceIfNotEmpty(&ValidatingWebhookClientCA, cfg.ValidatingWebhookConfig.ClientCA) + setIfNotEmpty(&ValidatingWebhookFailurePolicy, cfg.ValidatingWebhookConfig.FailurePolicy) + setIfNotEmpty(&ValidatingWebhookListenPort, cfg.ValidatingWebhookConfig.ListenPort) + setIfNotEmpty(&ValidatingWebhookListenAddress, cfg.ValidatingWebhookConfig.ListenAddress) + + setIfNotEmpty(&ConversionWebhookServiceName, cfg.ConversionWebhookConfig.ServiceName) + setIfNotEmpty(&ConversionWebhookServerCert, cfg.ConversionWebhookConfig.ServerCert) + setIfNotEmpty(&ConversionWebhookServerKey, cfg.ConversionWebhookConfig.ServerKey) + setIfNotEmpty(&ConversionWebhookCA, cfg.ConversionWebhookConfig.CA) + setSliceIfNotEmpty(&ConversionWebhookClientCA, cfg.ConversionWebhookConfig.ClientCA) + setIfNotEmpty(&ConversionWebhookListenPort, cfg.ConversionWebhookConfig.ListenPort) + setIfNotEmpty(&ConversionWebhookListenAddress, cfg.ConversionWebhookConfig.ListenAddress) +} + +func (cfg *Config) IsReady() bool { + return cfg.ready +} + +func (cfg *Config) SetReady() { + cfg.ready = true +} + +var configInstance *Config + +func MustGetConfig() *Config { + cfg, err := GetConfig() + if err != nil { + panic(err) + } + + return cfg +} + +func GetConfig() (*Config, error) { + if configInstance != nil { + return configInstance, nil + } + + cfg := NewConfig() + err := cfg.Parse() + if err != nil { + return nil, err + } + + configInstance = cfg + + return configInstance, nil +} + +func setIfNotEmpty[T comparable](v *T, env T) { + if !isZero(env) { + *v = env + } +} + +func setSliceIfNotEmpty[T any](v *[]T, env []T) { + if len(env) != 0 { + *v = env + } +} + +func isZero[T comparable](v T) bool { + return v == *new(T) +}