diff --git a/README.md b/README.md index 32ed119..ce6fe2b 100644 --- a/README.md +++ b/README.md @@ -802,7 +802,21 @@ If the `databases` array is empty or not provided for a metric, that metric will ### Using OCI Vault -The exporter will read the password from a secret stored in OCI Vault if you set these two environment variables: +Each database in the config file may be configured to use OCI Vault. To load the database username and/or password from OCI Vault, set the `vault.oci` property to contain the OCI Vault OCID, and secret names for the database username/password: + +```yaml +databases: + mydb: + vault: + oci: + id: + usernameSecret: + passwordSecret: +``` + +#### OCI Vault CLI Configuration + +If using the default database with CLI parameters, the exporter will read the password from a secret stored in OCI Vault if you set these two environment variables: - `OCI_VAULT_ID` should be set to the OCID of the OCI vault that you wish to use - `OCI_VAULT_SECRET_NAME` should be set to the name of the secret in the OCI vault which contains the database password @@ -811,7 +825,21 @@ The exporter will read the password from a secret stored in OCI Vault if you set ### Using Azure Vault -The exporter will read the database username and password from secrets stored in Azure Key Vault if you set these environment variables: +Each database in the config file may be configured to use Azure Vault. To load the database username and/or password from Azure Vault, set the `vault.azure` property to contain the Azure Vault ID, and secret names for the database username/password: + +```yaml +databases: + mydb: + vault: + azure: + id: + usernameSecret: + passwordSecret: +``` + +#### Azure Vault CLI Configuration + +If using the default database with CLI parameters, the exporter will read the database username and password from secrets stored in Azure Key Vault if you set these environment variables: - `AZ_VAULT_ID` should be set to the ID of the Azure Key Vault that you wish to use - `AZ_VAULT_USERNAME_SECRET` should be set to the name of the secret in the Azure Key Vault which contains the database username diff --git a/collector/config.go b/collector/config.go index e49c358..c6b93e4 100644 --- a/collector/config.go +++ b/collector/config.go @@ -9,7 +9,6 @@ import ( "github.com/oracle/oracle-db-appdev-monitoring/ocivault" "gopkg.in/yaml.v2" "log/slog" - "maps" "os" "strings" "time" @@ -27,6 +26,7 @@ type DatabaseConfig struct { Password string URL string `yaml:"url"` ConnectConfig `yaml:",inline"` + Vault *VaultConfig `yaml:"vault,omitempty"` } type ConnectConfig struct { @@ -41,6 +41,25 @@ type ConnectConfig struct { QueryTimeout *int `yaml:"queryTimeout"` } +type VaultConfig struct { + // OCI if present, OCI vault will be used to load username and/or password. + OCI *OCIVault `yaml:"oci"` + // Azure if present, Azure vault will be used to load username and/or password. + Azure *AZVault `yaml:"azure"` +} + +type OCIVault struct { + ID string `yaml:"id"` + UsernameSecret string `yaml:"usernameSecret"` + PasswordSecret string `yaml:"passwordSecret"` +} + +type AZVault struct { + ID string `yaml:"id"` + UsernameSecret string `yaml:"usernameSecret"` + PasswordSecret string `yaml:"passwordSecret"` +} + type MetricsFilesConfig struct { Default string Custom []string @@ -115,6 +134,32 @@ func (c ConnectConfig) GetQueryTimeout() int { return *c.QueryTimeout } +func (d DatabaseConfig) GetUsername() string { + if d.Vault == nil { + return d.Username + } + if d.Vault.OCI != nil { + return ocivault.GetVaultSecret(d.Vault.OCI.ID, d.Vault.OCI.UsernameSecret) + } + if d.Vault.Azure != nil { + return azvault.GetVaultSecret(d.Vault.Azure.ID, d.Vault.Azure.UsernameSecret) + } + return "" +} + +func (d DatabaseConfig) GetPassword() string { + if d.Vault == nil { + return d.Password + } + if d.Vault.OCI != nil { + return ocivault.GetVaultSecret(d.Vault.OCI.ID, d.Vault.OCI.PasswordSecret) + } + if d.Vault.Azure != nil { + return azvault.GetVaultSecret(d.Vault.Azure.ID, d.Vault.Azure.PasswordSecret) + } + return "" +} + func LoadMetricsConfiguration(logger *slog.Logger, cfg *Config, path string) (*MetricsConfiguration, error) { m := &MetricsConfiguration{} if len(cfg.ConfigFile) > 0 { @@ -127,16 +172,12 @@ func LoadMetricsConfiguration(logger *slog.Logger, cfg *Config, path string) (*M return m, yerr } } else { + logger.Warn("Configuring default database from CLI parameters is deprecated. Use of the '--config.file' argument is preferred. See https://github.com/oracle/oracle-db-appdev-monitoring?tab=readme-ov-file#standalone-binary") m.Databases = make(map[string]DatabaseConfig) m.Databases["default"] = m.defaultDatabase(cfg) } m.merge(cfg, path) - - // TODO: rework vault support for multi-database. - // Currently, the vault user/password is applied for every database. - // It must be configurable at the database level for true multi-database support. - m.setKeyVaultUserPassword(logger) return m, nil } @@ -172,8 +213,10 @@ func (m *MetricsConfiguration) mergeMetricsConfig(cfg *Config) { } } +// defaultDatabase creates a database named "default" if CLI arguments are used. It is for backwards compatibility when the exporter +// was only configurable through CLI arguments for a single database instance. func (m *MetricsConfiguration) defaultDatabase(cfg *Config) DatabaseConfig { - return DatabaseConfig{ + dbconfig := DatabaseConfig{ Username: cfg.User, Password: cfg.Password, URL: cfg.ConnectString, @@ -189,38 +232,25 @@ func (m *MetricsConfiguration) defaultDatabase(cfg *Config) DatabaseConfig { QueryTimeout: &cfg.QueryTimeout, }, } -} - -func (m *MetricsConfiguration) setKeyVaultUserPassword(logger *slog.Logger) { - if user, password, ok := getKeyVaultUserPassword(logger); ok { - for dbname := range maps.Keys(m.Databases) { - db := m.Databases[dbname] - db.Password = password - if len(user) > 0 { - db.Username = user - } - m.Databases[dbname] = db + // Vault ID lookup through environment variables is the historic method of loading vault metadata. + // These semantics are preserved if the "default" database from CLI config is requested. + if ociVaultID, useOciVault := os.LookupEnv("OCI_VAULT_ID"); useOciVault { + dbconfig.Vault = &VaultConfig{ + OCI: &OCIVault{ + ID: ociVaultID, + // For the CLI, only the password may be loaded from a secret. If you need to load + // both the username and password from OCI Vault, use the exporter configuration file. + PasswordSecret: os.Getenv("OCI_VAULT_SECRET_NAME"), + }, + } + } else if azVaultID, useAzVault := os.LookupEnv("AZ_VAULT_ID"); useAzVault { + dbconfig.Vault = &VaultConfig{ + Azure: &AZVault{ + ID: azVaultID, + UsernameSecret: os.Getenv("AZ_VAULT_USERNAME_SECRET"), + PasswordSecret: os.Getenv("AZ_VAULT_PASSWORD_SECRET"), + }, } } -} - -func getKeyVaultUserPassword(logger *slog.Logger) (user string, password string, ok bool) { - ociVaultID, useOciVault := os.LookupEnv("OCI_VAULT_ID") - if useOciVault { - - logger.Info("OCI_VAULT_ID env var is present so using OCI Vault", "vaultOCID", ociVaultID) - password = ocivault.GetVaultSecret(ociVaultID, os.Getenv("OCI_VAULT_SECRET_NAME")) - return "", password, true - } - - azVaultID, useAzVault := os.LookupEnv("AZ_VAULT_ID") - if useAzVault { - - logger.Info("AZ_VAULT_ID env var is present so using Azure Key Vault", "VaultID", azVaultID) - logger.Info("Using the environment variables AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET to authentication with Azure.") - user = azvault.GetVaultSecret(azVaultID, os.Getenv("AZ_VAULT_USERNAME_SECRET")) - password = azvault.GetVaultSecret(azVaultID, os.Getenv("AZ_VAULT_PASSWORD_SECRET")) - return user, password, true - } - return user, password, ok + return dbconfig } diff --git a/collector/database.go b/collector/database.go index 2a3702b..6058cc0 100644 --- a/collector/database.go +++ b/collector/database.go @@ -67,8 +67,10 @@ func connect(logger *slog.Logger, dbname string, dbconfig DatabaseConfig) (*sql. logger.Debug("Launching connection to "+maskDsn(dbconfig.URL), "database", dbname) var P godror.ConnectionParams - // If password is not specified, externalAuth will be true and we'll ignore user input - dbconfig.ExternalAuth = dbconfig.Password == "" + password := dbconfig.GetPassword() + username := dbconfig.GetUsername() + // If password is not specified, externalAuth will be true, and we'll ignore user input + dbconfig.ExternalAuth = password == "" logger.Debug(fmt.Sprintf("external authentication set to %t", dbconfig.ExternalAuth), "database", dbname) msg := "Using Username/Password Authentication." if dbconfig.ExternalAuth { @@ -80,7 +82,7 @@ func connect(logger *slog.Logger, dbname string, dbconfig DatabaseConfig) (*sql. Bool: dbconfig.ExternalAuth, Valid: true, } - P.Username, P.Password, P.ConnectString, P.ExternalAuth = dbconfig.Username, godror.NewPassword(dbconfig.Password), dbconfig.URL, externalAuth + P.Username, P.Password, P.ConnectString, P.ExternalAuth = username, godror.NewPassword(password), dbconfig.URL, externalAuth if dbconfig.GetPoolIncrement() > 0 { logger.Debug(fmt.Sprintf("set pool increment to %d", dbconfig.PoolIncrement), "database", dbname) diff --git a/main.go b/main.go index 05f5fb2..9600f3f 100644 --- a/main.go +++ b/main.go @@ -198,7 +198,7 @@ func main() { } }() } - + // start the main server thread server := &http.Server{} if err := web.ListenAndServe(server, toolkitFlags, logger); err != nil { diff --git a/ocivault/ocivault.go b/ocivault/ocivault.go index 1384bdf..526ed0a 100755 --- a/ocivault/ocivault.go +++ b/ocivault/ocivault.go @@ -8,34 +8,20 @@ import ( b64 "encoding/base64" "strings" - "github.com/prometheus/common/promslog" - "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/example/helpers" "github.com/oracle/oci-go-sdk/v65/secrets" ) func GetVaultSecret(vaultId string, secretName string) string { - promLogConfig := &promslog.Config{} - logger := promslog.New(promLogConfig) - client, err := secrets.NewSecretsClientWithConfigurationProvider(common.DefaultConfigProvider()) helpers.FatalIfError(err) - tenancyID, err := common.DefaultConfigProvider().TenancyOCID() - helpers.FatalIfError(err) - region, err := common.DefaultConfigProvider().Region() - helpers.FatalIfError(err) - logger.Info("OCI_VAULT_ID env var is present so using OCI Vault", "Region", region) - logger.Info("OCI_VAULT_ID env var is present so using OCI Vault", "tenancyOCID", tenancyID) - req := secrets.GetSecretBundleByNameRequest{ SecretName: common.String(secretName), VaultId: common.String(vaultId)} - resp, err := client.GetSecretBundleByName(context.Background(), req) helpers.FatalIfError(err) - rawSecret := getSecretFromBase64(resp) return strings.TrimRight(rawSecret, "\r\n") // make sure a \r and/or \n didn't make it into the secret }