diff --git a/go.mod b/go.mod index c510ed745..f078d27d6 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/icinga/icingadb go 1.22 require ( + github.com/caarlos0/env/v11 v11.0.1 github.com/creasty/defaults v1.7.0 github.com/go-sql-driver/mysql v1.8.1 github.com/goccy/go-yaml v1.11.3 diff --git a/go.sum b/go.sum index 306c502b7..64b7f5f26 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/caarlos0/env/v11 v11.0.1 h1:A8dDt9Ub9ybqRSUF3fQc/TA/gTam2bKT4Pit+cwrsPs= +github.com/caarlos0/env/v11 v11.0.1/go.mod h1:2RC3HQu8BQqtEK3V4iHPxj0jOdWdbPpWJ6pOueeU1xM= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA= diff --git a/internal/command/command.go b/internal/command/command.go index a4c76ab8d..c6eb75270 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -35,7 +35,13 @@ func New() *Command { os.Exit(0) } - cfg, err := config.FromYAMLFile(flags.Config) + var cfg *config.Config + if flags.ConfigFromEnv { + cfg, err = config.FromEnv() + } else { + cfg, err = config.FromYAMLFile(flags.Config) + } + if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(2) diff --git a/pkg/config/config.go b/pkg/config/config.go index 744f4c31f..95ccbf1a7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -3,6 +3,7 @@ package config import ( "crypto/tls" "crypto/x509" + "github.com/caarlos0/env/v11" "github.com/creasty/defaults" "github.com/goccy/go-yaml" "github.com/jessevdk/go-flags" @@ -12,10 +13,10 @@ import ( // Config defines Icinga DB config. type Config struct { - Database Database `yaml:"database"` - Redis Redis `yaml:"redis"` - Logging Logging `yaml:"logging"` - Retention Retention `yaml:"retention"` + Database Database `yaml:"database" envPrefix:"DATABASE_"` + Redis Redis `yaml:"redis" envPrefix:"REDIS_"` + Logging Logging `yaml:"logging" envPrefix:"LOGGING_"` + Retention Retention `yaml:"retention" envPrefix:"RETENTION_"` } // Validate checks constraints in the supplied configuration and returns an error if they are violated. @@ -40,6 +41,8 @@ func (c *Config) Validate() error { type Flags struct { // Version decides whether to just print the version and exit. Version bool `long:"version" description:"print version and exit"` + // ConfigFromEnv decides whether to load the config from environment variables. + ConfigFromEnv bool `long:"config-from-env" description:"load config from env vars"` // Config is the path to the config file Config string `short:"c" long:"config" description:"path to config file" required:"true" default:"/etc/icingadb/config.yml"` } @@ -70,6 +73,25 @@ func FromYAMLFile(name string) (*Config, error) { return c, nil } +// FromEnv returns a new Config value created from environment variables. +func FromEnv() (*Config, error) { + c := &Config{} + + if err := defaults.Set(c); err != nil { + return nil, errors.Wrap(err, "can't set config defaults") + } + + if err := env.ParseWithOptions(c, env.Options{Prefix: "ICINGADB_"}); err != nil { + return nil, errors.Wrap(err, "can't parse env vars") + } + + if err := c.Validate(); err != nil { + return nil, errors.Wrap(err, "invalid configuration") + } + + return c, nil +} + // ParseFlags parses CLI flags and // returns a Flags value created from them. func ParseFlags() (*Flags, error) { @@ -85,11 +107,11 @@ func ParseFlags() (*Flags, error) { // TLS provides TLS configuration options for Redis and Database. type TLS struct { - Enable bool `yaml:"tls"` - Cert string `yaml:"cert"` - Key string `yaml:"key"` - Ca string `yaml:"ca"` - Insecure bool `yaml:"insecure"` + Enable bool `yaml:"tls" env:"TLS"` + Cert string `yaml:"cert" env:"CERT"` + Key string `yaml:"key" env:"KEY"` + Ca string `yaml:"ca" env:"CA"` + Insecure bool `yaml:"insecure" env:"INSECURE"` } // MakeConfig assembles a tls.Config from t and serverName. diff --git a/pkg/config/database.go b/pkg/config/database.go index 0895d26c9..2169876f3 100644 --- a/pkg/config/database.go +++ b/pkg/config/database.go @@ -22,14 +22,14 @@ import ( // Database defines database client configuration. type Database struct { - Type string `yaml:"type" default:"mysql"` - Host string `yaml:"host"` - Port int `yaml:"port"` - Database string `yaml:"database"` - User string `yaml:"user"` - Password string `yaml:"password"` + Type string `yaml:"type" env:"TYPE" default:"mysql"` + Host string `yaml:"host" env:"HOST"` + Port int `yaml:"port" env:"PORT"` + Database string `yaml:"database" env:"DATABASE"` + User string `yaml:"user" env:"USER"` + Password string `yaml:"password" env:"PASSWORD"` TlsOptions TLS `yaml:",inline"` - Options icingadb.Options `yaml:"options"` + Options icingadb.Options `yaml:"options" envPrefix:"OPTIONS_"` } // Open prepares the DSN string and driver configuration, diff --git a/pkg/config/history_retention.go b/pkg/config/history_retention.go index d4373b708..6fe006077 100644 --- a/pkg/config/history_retention.go +++ b/pkg/config/history_retention.go @@ -8,11 +8,11 @@ import ( // Retention defines configuration for history retention. type Retention struct { - HistoryDays uint64 `yaml:"history-days"` - SlaDays uint64 `yaml:"sla-days"` - Interval time.Duration `yaml:"interval" default:"1h"` - Count uint64 `yaml:"count" default:"5000"` - Options history.RetentionOptions `yaml:"options"` + HistoryDays uint64 `yaml:"history-days" env:"HISTORY_DAYS"` + SlaDays uint64 `yaml:"sla-days" env:"SLA_DAYS"` + Interval time.Duration `yaml:"interval" env:"INTERVAL" default:"1h"` + Count uint64 `yaml:"count" env:"COUNT" default:"5000"` + Options history.RetentionOptions `yaml:"options" env:"OPTIONS"` } // Validate checks constraints in the supplied retention configuration and diff --git a/pkg/config/logging.go b/pkg/config/logging.go index 9ccd35e01..669b1cdd8 100644 --- a/pkg/config/logging.go +++ b/pkg/config/logging.go @@ -11,12 +11,12 @@ import ( // Logging defines Logger configuration. type Logging struct { // zapcore.Level at 0 is for info level. - Level zapcore.Level `yaml:"level" default:"0"` - Output string `yaml:"output"` + Level zapcore.Level `yaml:"level" env:"LEVEL" default:"0"` + Output string `yaml:"output" env:"OUTPUT"` // Interval for periodic logging. - Interval time.Duration `yaml:"interval" default:"20s"` + Interval time.Duration `yaml:"interval" env:"INTERVAL" default:"20s"` - logging.Options `yaml:"options"` + logging.Options `yaml:"options" env:"OPTIONS"` } // Validate checks constraints in the supplied Logging configuration and returns an error if they are violated. diff --git a/pkg/config/redis.go b/pkg/config/redis.go index ad8b31a60..4af61ae0e 100644 --- a/pkg/config/redis.go +++ b/pkg/config/redis.go @@ -19,11 +19,11 @@ import ( // Redis defines Redis client configuration. type Redis struct { - Host string `yaml:"host"` - Port int `yaml:"port" default:"6380"` - Password string `yaml:"password"` + Host string `yaml:"host" env:"HOST"` + Port int `yaml:"port" env:"PORT" default:"6380"` + Password string `yaml:"password" env:"PASSWORD"` TlsOptions TLS `yaml:",inline"` - Options icingaredis.Options `yaml:"options"` + Options icingaredis.Options `yaml:"options" envPrefix:"OPTIONS_"` } type ctxDialerFunc = func(ctx context.Context, network, addr string) (net.Conn, error) diff --git a/pkg/icingadb/db.go b/pkg/icingadb/db.go index 47940af9e..51de3be74 100644 --- a/pkg/icingadb/db.go +++ b/pkg/icingadb/db.go @@ -37,29 +37,29 @@ type DB struct { // Options define user configurable database options. type Options struct { // Maximum number of open connections to the database. - MaxConnections int `yaml:"max_connections" default:"16"` + MaxConnections int `yaml:"max_connections" env:"MAX_CONNECTIONS" default:"16"` // Maximum number of connections per table, // regardless of what the connection is actually doing, // e.g. INSERT, UPDATE, DELETE. - MaxConnectionsPerTable int `yaml:"max_connections_per_table" default:"8"` + MaxConnectionsPerTable int `yaml:"max_connections_per_table" env:"MAX_CONNECTIONS_PER_TABLE" default:"8"` // MaxPlaceholdersPerStatement defines the maximum number of placeholders in an // INSERT, UPDATE or DELETE statement. Theoretically, MySQL can handle up to 2^16-1 placeholders, // but this increases the execution time of queries and thus reduces the number of queries // that can be executed in parallel in a given time. // The default is 2^13, which in our tests showed the best performance in terms of execution time and parallelism. - MaxPlaceholdersPerStatement int `yaml:"max_placeholders_per_statement" default:"8192"` + MaxPlaceholdersPerStatement int `yaml:"max_placeholders_per_statement" env:"MAX_PLACEHOLDERS_PER_STATEMENT" default:"8192"` // MaxRowsPerTransaction defines the maximum number of rows per transaction. // The default is 2^13, which in our tests showed the best performance in terms of execution time and parallelism. - MaxRowsPerTransaction int `yaml:"max_rows_per_transaction" default:"8192"` + MaxRowsPerTransaction int `yaml:"max_rows_per_transaction" env:"MAX_ROWS_PER_TRANSACTION" default:"8192"` // WsrepSyncWait enforces Galera cluster nodes to perform strict cluster-wide causality checks // before executing specific SQL queries determined by the number you provided. // Please refer to the below link for a detailed description. // https://icinga.com/docs/icinga-db/latest/doc/03-Configuration/#galera-cluster - WsrepSyncWait int `yaml:"wsrep_sync_wait" default:"7"` + WsrepSyncWait int `yaml:"wsrep_sync_wait" env:"WSREP_SYNC_WAIT" default:"7"` } // Validate checks constraints in the supplied database options and returns an error if they are violated. diff --git a/pkg/icingaredis/client.go b/pkg/icingaredis/client.go index c494f95d0..8f9efea44 100644 --- a/pkg/icingaredis/client.go +++ b/pkg/icingaredis/client.go @@ -28,12 +28,12 @@ type Client struct { // Options define user configurable Redis options. type Options struct { - BlockTimeout time.Duration `yaml:"block_timeout" default:"1s"` - HMGetCount int `yaml:"hmget_count" default:"4096"` - HScanCount int `yaml:"hscan_count" default:"4096"` - MaxHMGetConnections int `yaml:"max_hmget_connections" default:"8"` - Timeout time.Duration `yaml:"timeout" default:"30s"` - XReadCount int `yaml:"xread_count" default:"4096"` + BlockTimeout time.Duration `yaml:"block_timeout" env:"BLOCK_TIMEOUT" default:"1s"` + HMGetCount int `yaml:"hmget_count" env:"HMGET_COUNT" default:"4096"` + HScanCount int `yaml:"hscan_count" env:"HSCAN_COUNT" default:"4096"` + MaxHMGetConnections int `yaml:"max_hmget_connections" env:"MAX_HMGET_CONNECTIONS" default:"8"` + Timeout time.Duration `yaml:"timeout" env:"TIMEOUT" default:"30s"` + XReadCount int `yaml:"xread_count" env:"XREAD_COUNT" default:"4096"` } // Validate checks constraints in the supplied Redis options and returns an error if they are violated.