From cd6bb233a2c839ec213bbafcbbc60a23f661f374 Mon Sep 17 00:00:00 2001 From: Alan Moran Date: Mon, 19 Aug 2024 19:08:39 +0200 Subject: [PATCH] WIP --- "\\" | 355 ++++++++++++++++++ .../metricsforwarder/config/config.go | 69 ++-- .../metricsforwarder/config/config_test.go | 61 ++- 3 files changed, 439 insertions(+), 46 deletions(-) create mode 100644 "\\" diff --git "a/\\" "b/\\" new file mode 100644 index 0000000000..e980a29bcf --- /dev/null +++ "b/\\" @@ -0,0 +1,355 @@ +package config_test + +import ( + "os" + "time" + + "code.cloudfoundry.org/app-autoscaler/src/autoscaler/db" + . "code.cloudfoundry.org/app-autoscaler/src/autoscaler/metricsforwarder/config" + "code.cloudfoundry.org/app-autoscaler/src/autoscaler/models" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func bytesToFile(b []byte) string { + if len(b) == 0 { + return "" + } + + file, err := os.CreateTemp("", "") + Expect(err).NotTo(HaveOccurred()) + _, err = file.Write(b) + Expect(err).NotTo(HaveOccurred()) + return file.Name() +} + +var _ = Describe("Config", func() { + var ( + conf *Config + err error + configBytes []byte + configFile string + ) + + Describe("LoadConfig", func() { + JustBeforeEach(func() { + configFile = bytesToFile(configBytes) + conf, err = LoadConfig(configFile) + }) + + When("config is read from env", func() { + + BeforeEach(func() { + configFile = "" + }) + + When("PORT env variable is set", func() { + AfterEach(func() { + os.Unsetenv("PORT") + }) + + When("PORT env is a number", func() { + BeforeEach(func() { + os.Setenv("PORT", "3333") + }) + + It("sets env variable over config file", func() { + Expect(conf.Server.Port).To(Equal(3333)) + }) + }) + + When("PORT env is not number", func() { + BeforeEach(func() { + os.Setenv("PORT", "NAN") + }) + + It("return invalid port error", func() { + Expect(err).To(MatchError(ErrReadEnvironment)) + Expect(err).To(MatchError(MatchRegexp("parsing \"NAN\": invalid syntax"))) + }) + }) + }) + + When("VCAP_SERVICES is set", func() { + BeforeEach(func() { + // vcap services has a postgres service provisioned by + // a service broker binding + vcapServices := `{ + "autoscaler": [ + { + "credentials": { + "uri":"postgres://foo:bar@postgres.example.com:5432/policy_db" + }, + "label": "postgres", + "name": "policy_db", + "syslog_drain_url": "", + "tags": ["postgres","postgresql","relational"] + } + ] + " + }` // #nosec G101 + + os.Setenv("VCAP_APPLICATION", "{}") + os.Setenv("VCAP_SERVICES", vcapServices) + }) + + It("loads the db config from VCAP_SERVICES", func() { + expectedDbConfig := db.DatabaseConfig{ + URL: "postgres://foo:bar@postgres.example.com:5432/policy_db", + } + + Expect(err).NotTo(HaveOccurred()) + Expect(conf.Db[db.PolicyDb]).To(Equal(expectedDbConfig)) + }) + + AfterEach(func() { + os.Unsetenv("VCAP_SERVICES") + os.Unsetenv("VCAP_APPLICATION") + }) + }) + }) + + When("config is read from file", func() { + AfterEach(func() { + Expect(os.Remove(configFile)).To(Succeed()) + }) + + Context("with invalid yaml", func() { + BeforeEach(func() { + configBytes = []byte(` + server: + port: 8081 + logging: + level: info + +loggregator + metron_address: 127.0.0.1:3457 + tls: + cert_file: "../testcerts/ca.crt" +`) + }) + + It("returns an error", func() { + Expect(err).To(MatchError(MatchRegexp("yaml: .*"))) + }) + }) + Context("with valid yaml", func() { + BeforeEach(func() { + configBytes = []byte(` +server: + port: 8081 +logging: + level: debug +loggregator: + metron_address: 127.0.0.1:3457 + tls: + ca_file: "../testcerts/ca.crt" + cert_file: "../testcerts/client.crt" + key_file: "../testcerts/client.key" +db: + policy_db: + url: "postgres://pqgotest:password@localhost/pqgotest" + max_open_connections: 10 + max_idle_connections: 5 + connection_max_lifetime: 60s +health: + port: 9999 +cred_helper_impl: default +`) + }) + + It("returns the config", func() { + Expect(conf.Server.Port).To(Equal(8081)) + Expect(conf.Logging.Level).To(Equal("debug")) + Expect(conf.Health.Port).To(Equal(9999)) + Expect(conf.LoggregatorConfig.MetronAddress).To(Equal("127.0.0.1:3457")) + Expect(conf.Db[db.PolicyDb]).To(Equal( + db.DatabaseConfig{ + URL: "postgres://pqgotest:password@localhost/pqgotest", + MaxOpenConnections: 10, + MaxIdleConnections: 5, + ConnectionMaxLifetime: 60 * time.Second, + })) + Expect(conf.CredHelperImpl).To(Equal("default")) + }) + + }) + Context("with partial config", func() { + BeforeEach(func() { + configBytes = []byte(` +loggregator: + tls: + ca_file: "../testcerts/ca.crt" + cert_file: "../testcerts/client.crt" + key_file: "../testcerts/client.key" +db: + policy_db: + url: "postgres://pqgotest:password@localhost/pqgotest" + max_open_connections: 10 + max_idle_connections: 5 + connection_max_lifetime: 60s +health: + port: 8081 +`) + }) + + It("returns default values", func() { + Expect(err).NotTo(HaveOccurred()) + Expect(conf.Server.Port).To(Equal(6110)) + Expect(conf.Logging.Level).To(Equal("info")) + Expect(conf.LoggregatorConfig.MetronAddress).To(Equal(DefaultMetronAddress)) + Expect(conf.CacheTTL).To(Equal(DefaultCacheTTL)) + Expect(conf.CacheCleanupInterval).To(Equal(DefaultCacheCleanupInterval)) + Expect(conf.Health.Port).To(Equal(8081)) + }) + }) + + }) + + }) + + Describe("Validate", func() { + BeforeEach(func() { + conf = &Config{} + conf.Server.Port = 8081 + conf.Logging.Level = "debug" + conf.Health.Port = 8081 + conf.LoggregatorConfig.MetronAddress = "127.0.0.1:3458" + conf.LoggregatorConfig.TLS.CACertFile = "../testcerts/ca.crt" + conf.LoggregatorConfig.TLS.CertFile = "../testcerts/client.crt" + conf.LoggregatorConfig.TLS.KeyFile = "../testcerts/client.crt" + conf.Db = make(map[string]db.DatabaseConfig) + conf.Db[db.PolicyDb] = db.DatabaseConfig{ + URL: "postgres://pqgotest:password@localhost/pqgotest", + MaxOpenConnections: 10, + MaxIdleConnections: 5, + ConnectionMaxLifetime: 60 * time.Second, + } + conf.RateLimit.MaxAmount = 10 + conf.RateLimit.ValidDuration = 1 * time.Second + + conf.CredHelperImpl = "path/to/plugin" + }) + + JustBeforeEach(func() { + err = conf.Validate() + }) + + When("syslog is available", func() { + BeforeEach(func() { + conf.SyslogConfig = SyslogConfig{ + ServerAddress: "localhost", + Port: 514, + TLS: models.TLSCerts{ + CACertFile: "../testcerts/ca.crt", + CertFile: "../testcerts/client.crt", + KeyFile: "../testcerts/client.crt", + }, + } + conf.LoggregatorConfig = LoggregatorConfig{} + }) + + It("should not error", func() { + Expect(err).NotTo(HaveOccurred()) + }) + + When("SyslogServer CACert is not set", func() { + BeforeEach(func() { + conf.SyslogConfig.TLS.CACertFile = "" + }) + + It("should error", func() { + Expect(err).To(MatchError(MatchRegexp("Configuration error: SyslogServer Loggregator CACert is empty"))) + }) + }) + + When("SyslogServer CertFile is not set", func() { + BeforeEach(func() { + conf.SyslogConfig.TLS.KeyFile = "" + }) + + It("should error", func() { + Expect(err).To(MatchError(MatchRegexp("Configuration error: SyslogServer ClientKey is empty"))) + }) + }) + + When("SyslogServer ClientCert is not set", func() { + BeforeEach(func() { + conf.SyslogConfig.TLS.CertFile = "" + }) + + It("should error", func() { + Expect(err).To(MatchError(MatchRegexp("Configuration error: SyslogServer ClientCert is empty"))) + }) + }) + }) + + When("all the configs are valid", func() { + It("should not error", func() { + Expect(err).NotTo(HaveOccurred()) + }) + }) + + When("policy db url is not set", func() { + BeforeEach(func() { + conf.Db[db.PolicyDb] = db.DatabaseConfig{URL: ""} + }) + + It("should error", func() { + Expect(err).To(MatchError(MatchRegexp("Configuration error: Policy DB url is empty"))) + }) + }) + + When("Loggregator CACert is not set", func() { + BeforeEach(func() { + conf.LoggregatorConfig.TLS.CACertFile = "" + }) + + It("should error", func() { + Expect(err).To(MatchError(MatchRegexp("Configuration error: Loggregator CACert is empty"))) + }) + }) + + When("Loggregator ClientCert is not set", func() { + BeforeEach(func() { + conf.LoggregatorConfig.TLS.CertFile = "" + }) + + It("should error", func() { + Expect(err).To(MatchError(MatchRegexp("Configuration error: Loggregator ClientCert is empty"))) + }) + }) + + When("Loggregator ClientKey is not set", func() { + BeforeEach(func() { + conf.LoggregatorConfig.TLS.KeyFile = "" + }) + + It("should error", func() { + Expect(err).To(MatchError(MatchRegexp("Configuration error: Loggregator ClientKey is empty"))) + }) + }) + + When("rate_limit.max_amount is <= zero", func() { + BeforeEach(func() { + conf.RateLimit.MaxAmount = 0 + }) + + It("should err", func() { + Expect(err).To(MatchError(MatchRegexp("Configuration error: RateLimit.MaxAmount is equal or less than zero"))) + + }) + }) + + When("rate_limit.valid_duration is <= 0 ns", func() { + BeforeEach(func() { + conf.RateLimit.ValidDuration = 0 * time.Nanosecond + }) + + It("should err", func() { + Expect(err).To(MatchError(MatchRegexp("Configuration error: RateLimit.ValidDuration is equal or less than zero nanosecond"))) + }) + }) + }) +}) diff --git a/src/autoscaler/metricsforwarder/config/config.go b/src/autoscaler/metricsforwarder/config/config.go index 984c4a6196..3e0193fe99 100644 --- a/src/autoscaler/metricsforwarder/config/config.go +++ b/src/autoscaler/metricsforwarder/config/config.go @@ -1,6 +1,7 @@ package config import ( + "encoding/json" "errors" "fmt" "os" @@ -22,6 +23,7 @@ import ( // - ErrReadVCAPEnvironment var ErrReadYaml = errors.New("failed to read config file") +var ErrReadJson = errors.New("failed to read vcap_services json") var ErrReadEnvironment = errors.New("failed to read environment variables") var ErrReadVCAPEnvironment = errors.New("failed to read VCAP environment variables") @@ -79,7 +81,15 @@ type SyslogConfig struct { TLS models.TLSCerts `yaml:"tls"` } -func DecodeYamlFile(filepath string, c *Config) error { +func decodeJson(data []byte, c *Config) error { + err := yaml.Unmarshal(data, c) + if err != nil { + return fmt.Errorf("%w: %w", ErrReadJson, err) + } + return nil +} + +func decodeYamlFile(filepath string, c *Config) error { r, err := os.Open(filepath) if err != nil { @@ -120,7 +130,7 @@ func LoadConfig(filepath string) (*Config, error) { } if filepath != "" { - err = DecodeYamlFile(filepath, &conf) + err = decodeYamlFile(filepath, &conf) if err != nil { return nil, err } @@ -135,9 +145,31 @@ func LoadConfig(filepath string) (*Config, error) { conf.Server.Port = portNumber } - err = loadVCAPEnvs(&conf) - if err != nil { - return nil, fmt.Errorf("%w: %w", ErrReadVCAPEnvironment, err) + if os.Getenv("VCAP_SERVICES") != "" { + appEnv, err := cfenv.Current() + if err != nil { + return nil, err + } + + err = readDbFromVCAP(appEnv, &conf) + if err != nil { + return nil, err + } + + configVcapService, err := appEnv.Services.WithName("config") + if err != nil { + return nil, err + } + + data := configVcapService.Credentials["metricsforwarder"] + rawJSON, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrReadJson, err) + } + err = decodeJson(rawJSON, &conf) + if err != nil { + return nil, err + } } return &conf, nil @@ -248,31 +280,6 @@ func readDbFromVCAP(appEnv *cfenv.App, c *Config) error { return nil } -func readConfigFromVCAP(appEnv *cfenv.App, c *Config) error { - fmt.Println(appEnv.Services) - return nil -} - -func loadVCAPEnvs(c *Config) error { - if os.Getenv("VCAP_APPLICATION") == "" || os.Getenv("VCAP_SERVICES") == "" { - return nil - } - - // panic here - appEnv, err := cfenv.Current() - if err != nil { - return err - } - - err = readDbFromVCAP(appEnv, c) - if err != nil { - return err - } - - err = readConfigFromVCAP(appEnv, c) - if err != nil { - return err - } - +func readConfigFromVCAP(c *Config) error { return nil } diff --git a/src/autoscaler/metricsforwarder/config/config_test.go b/src/autoscaler/metricsforwarder/config/config_test.go index fb5fdefec8..fcf132e01b 100644 --- a/src/autoscaler/metricsforwarder/config/config_test.go +++ b/src/autoscaler/metricsforwarder/config/config_test.go @@ -74,29 +74,60 @@ var _ = Describe("Config", func() { When("VCAP_SERVICES has service config", func() { BeforeEach(func() { - + // VCAP_SERVICES={"user-provided":[ + //{"label":"user-provided", + // "name":"config", + // "tags":[], + // "instance_guid":"444c838e-17d9-429d-a1ea-660904db9841", + // "instance_name":"config", + // "binding_guid":"2cb523a1-773a-4fa4-ba05-3a76cc488ff7", + // "binding_name":null, + // "credentials":{ + // "cache_cleanup_interval":"6h", + // "cache_ttl":"900s", + // "cred_helper_impl":"default", + // "db":null, + // "health":{"password":"health-password","username":"health-user"}, + // "logging":{"level":"info"}, + // "policy_poller_interval":"60s", + // "rate_limit":{"max_amount":10,"valid_duration":"1s"}, + // "syslog":{ + // "port":6067, + // "server_address":"log-cache.service.cf.internal", + // "tls":{"ca_file":"/var/vcap/jobs/metricsforwarder/config/certs/syslog_client/ca.crt","cert_file":"/var/vcap/jobs/metricsforwarder/config/certs/syslog_client/client.crt","key_file":"/var/vcap/jobs/metricsforwarder/config/certs/syslog_client/client.key"} + // }, + // } + //}, + // "syslog_drain_url":null, + // "volume_mounts":[]}]} + // vcapServicesJson = `{ - "config": { - "logging": { + "user-provided": [ { + "label":"user-provided", + "name": "config", + "credentials": { + "metricsforwarder": { + "logging": { "level": "debug" - }, - "loggregator": { + }, + "loggregator": { "metron_address": "127.0.0.1:3457", "tls": { - "ca_file": "../testcerts/ca.crt", - "cert_file": "../testcerts/client.crt", - "key_file": "../testcerts/client.key" + "ca_file": "../testcerts/ca.crt", + "cert_file": "../testcerts/client.crt", + "key_file": "../testcerts/client.key" } - }, - "cred_helper_impl": "default" }, - "autoscaler": [ - { + "cred_helper_impl": "default" + } + } + }], + "autoscaler": [ { + "name": "policy_db", + "label": "postgres", "credentials": { "uri":"postgres://foo:bar@postgres.example.com:5432/policy_db" }, - "label": "postgres", - "name": "policy_db", "syslog_drain_url": "", "tags": ["postgres","postgresql","relational"] } @@ -104,7 +135,7 @@ var _ = Describe("Config", func() { }` // #nosec G101 }) - FIt("loads the config from VCAP_SERVICES", func() { + It("loads the config from VCAP_SERVICES", func() { Expect(err).NotTo(HaveOccurred()) Expect(conf.Logging.Level).To(Equal("debug")) })