package config import ( "crypto/rand" "crypto/tls" "encoding/base64" "errors" "flag" "fmt" "github.com/getsentry/sentry-go" "github.com/openHPI/poseidon/pkg/dto" "github.com/openHPI/poseidon/pkg/logging" "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" "k8s.io/client-go/rest" "net/url" "os" "reflect" "strconv" "strings" ) const ( defaultPoseidonPort = 7200 defaultNomadPort = 4646 defaultMemoryUsageAlertThreshold = 1_000 ) // Config contains the default configuration of Poseidon. var ( Config = &configuration{ Server: server{ Address: "127.0.0.1", Port: defaultPoseidonPort, SystemdSocketActivation: false, Token: "", TLS: TLS{ Active: false, CAFile: "", CertFile: "", KeyFile: "", }, InteractiveStderr: true, TemplateJobFile: "", Alert: alert{ PrewarmingPoolThreshold: 0, PrewarmingPoolReloadTimeout: 0, }, LoggingFilterToken: randomFilterToken(), }, AWS: AWS{ Enabled: false, Endpoint: "", Functions: []string{}, }, Kubernetes: Kubernetes{ Enabled: false, Address: "", Port: 0, Token: "", }, Logger: Logger{ Level: "INFO", Formatter: dto.FormatterText, }, Profiling: Profiling{ MemoryThreshold: defaultMemoryUsageAlertThreshold, }, Sentry: sentry.ClientOptions{ AttachStacktrace: true, }, InfluxDB: InfluxDB{ URL: "", Token: "", Organization: "", Bucket: "", Stage: "", }, } configurationFilePath = "./configuration.yaml" configurationInitialized = false log = logging.GetLogger("config") TLSConfig = &tls.Config{ MinVersion: tls.VersionTLS13, CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, } ErrConfigInitialized = errors.New("configuration is already initialized") ) type alert struct { PrewarmingPoolThreshold float64 PrewarmingPoolReloadTimeout uint } // server configures the Poseidon webserver. type server struct { Address string Port int SystemdSocketActivation bool Token string TLS TLS InteractiveStderr bool TemplateJobFile string Alert alert LoggingFilterToken string } // URL returns the URL of the Poseidon webserver. func (s *server) URL() *url.URL { return parseURL(s.Address, s.Port, s.TLS.Active) } // AWS configures the AWS Lambda usage. type AWS struct { Enabled bool Endpoint string Functions []string } type Kubernetes struct { Enabled bool Namespace string Config *rest.Config Images []string } // TLS configures TLS on a connection. type TLS struct { Active bool CAFile string CertFile string KeyFile string } // Logger configures the used Logger. type Logger struct { Formatter dto.Formatter Level string } // Profiling configures the usage of a runtime profiler to create optimized binaries. type Profiling struct { CPUEnabled bool CPUFile string MemoryInterval uint MemoryThreshold uint } // InfluxDB configures the usage of an Influx db monitoring. type InfluxDB struct { URL string Token string Organization string Bucket string Stage string } // configuration contains the complete configuration of Poseidon. type configuration struct { Server server AWS AWS Kubernetes Kubernetes Logger Logger Profiling Profiling Sentry sentry.ClientOptions InfluxDB InfluxDB } // InitConfig merges configuration options from environment variables and // a configuration file into the default configuration. Calls of InitConfig // after the first call have no effect and return an error. InitConfig // should be called directly after starting the program. func InitConfig() error { if configurationInitialized { return ErrConfigInitialized } configurationInitialized = true content := readConfigFile() Config.mergeYaml(content) Config.mergeEnvironmentVariables() return nil } func parseURL(address string, port int, tlsEnabled bool) *url.URL { scheme := "http" if tlsEnabled { scheme = "https" } return &url.URL{ Scheme: scheme, Host: fmt.Sprintf("%s:%d", address, port), } } func readConfigFile() []byte { parseFlags() data, err := os.ReadFile(configurationFilePath) if err != nil { log.WithError(err).Info("Using default configuration...") return nil } return data } func parseFlags() { if flag.Lookup("config") == nil { flag.StringVar(&configurationFilePath, "config", configurationFilePath, "path of the yaml config file") } flag.Parse() } func (c *configuration) mergeYaml(content []byte) { if err := yaml.Unmarshal(content, c); err != nil { log.WithError(err).Fatal("Could not parse configuration file") } } func (c *configuration) mergeEnvironmentVariables() { readFromEnvironment("POSEIDON", reflect.ValueOf(c).Elem()) } func readFromEnvironment(prefix string, value reflect.Value) { logEntry := log.WithField("prefix", prefix) // if value was not derived from a pointer, it is not possible to alter its contents if !value.CanSet() { logEntry.Warn("Cannot overwrite struct field that can not be set") return } if value.Kind() != reflect.Struct { loadValue(prefix, value, logEntry) } else { for i := 0; i < value.NumField(); i++ { fieldName := value.Type().Field(i).Name newPrefix := fmt.Sprintf("%s_%s", prefix, strings.ToUpper(fieldName)) readFromEnvironment(newPrefix, value.Field(i)) } } } func loadValue(prefix string, value reflect.Value, logEntry *logrus.Entry) { content, ok := os.LookupEnv(prefix) if !ok { return } logEntry = logEntry.WithField("content", content) switch value.Kind() { case reflect.String: value.SetString(content) case reflect.Int: integer, err := strconv.Atoi(content) if err != nil { logEntry.Warn("Could not parse environment variable as integer") return } value.SetInt(int64(integer)) case reflect.Bool: boolean, err := strconv.ParseBool(content) if err != nil { logEntry.Warn("Could not parse environment variable as boolean") return } value.SetBool(boolean) case reflect.Slice: if content != "" && content[0] == '"' && content[len(content)-1] == '"' { content = content[1 : len(content)-1] // remove wrapping quotes } parts := strings.Fields(content) value.Set(reflect.AppendSlice(value, reflect.ValueOf(parts))) default: // ignore this field logEntry.WithField("type", value.Type().Name()). Warn("Setting configuration option via environment variables is not supported") } } func randomFilterToken() string { const tokenLength = 32 randomBytes := make([]byte, tokenLength) //nolint:all // length required to be filled by rand.Read. n, err := rand.Read(randomBytes) if n != tokenLength || err != nil { log.WithError(err).WithField("byteCount", n).Fatal("Failed to generate random token") } return base64.URLEncoding.EncodeToString(randomBytes) }