diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 24f80cc35..6afc2a4ba 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -352,12 +352,22 @@ func buildUpdateNodeRequest( // startWithJDLifecycle initializes all components required for the JD lifecycle manager and starts it. func (b *Bootstrapper) startWithJDLifecycle(ctx context.Context) error { - db, err := connectToDB(ctx, b.config.DB.URL) + dbURL, err := b.config.DB.GetURL() + if err != nil { + return fmt.Errorf("invalid db config: %w", err) + } + + db, err := connectToDB(ctx, dbURL) if err != nil { return fmt.Errorf("failed to connect to bootstrapper database: %w", err) } - keyStore, csaSigner, err := initializeKeystore(ctx, b.lggr, db, b.config.Keystore.Password, b.keys) + keyStorePass, err := b.config.Keystore.GetPassword() + if err != nil { + return fmt.Errorf("invalid keystore config: %w", err) + } + + keyStore, csaSigner, err := initializeKeystore(ctx, b.lggr, db, keyStorePass, b.keys) if err != nil { return fmt.Errorf("failed to initialize keystore: %w", err) } diff --git a/bootstrap/config.go b/bootstrap/config.go index 2c7a53ebc..1a69db153 100644 --- a/bootstrap/config.go +++ b/bootstrap/config.go @@ -35,28 +35,74 @@ func (c *JDConfig) validate() error { // KeystoreConfig is the configuration for the keystore. type KeystoreConfig struct { - // Password is the password to the keystore. + // Password is the password to the keystore. Has precedence over PasswordEnvVar. Password string `toml:"password"` + // PasswordEnvVar is the name of an environment variable containing the keystore password. + // Only used if Password is not defined. + PasswordEnvVar string `toml:"password_env_var"` } -func (c *KeystoreConfig) validate() error { - if c.Password == "" { - return fmt.Errorf("field 'password' is required") +// GetPassword returns the keystore password, following the precedence rule: +// Password (direct config) takes precedence over PasswordEnvVar (environment variable). +// Returns an error if neither source is defined or if PasswordEnvVar references a non-existent variable. +func (c *KeystoreConfig) GetPassword() (string, error) { + // If Password is defined, use it (it has precedence) + if c.Password != "" { + return c.Password, nil } - return nil + + // If Password is not defined, try to load from PasswordEnvVar + if c.PasswordEnvVar != "" { + envPassword := os.Getenv(c.PasswordEnvVar) + if envPassword == "" { + return "", fmt.Errorf("field 'password' is empty and environment variable %q (from 'password_env_var') is not set or is empty", c.PasswordEnvVar) + } + return envPassword, nil + } + + // Neither Password nor PasswordEnvVar is defined + return "", fmt.Errorf("field 'password' is required (either 'password' or 'password_env_var' must be defined)") +} + +func (c *KeystoreConfig) validate() error { + _, err := c.GetPassword() + return err } // DBConfig is the configuration for the bootstrap database. type DBConfig struct { - // URL is the URL to use for saving jobs and the keystore. + // URL is the database URL. Has precedence over URLEnvVar. URL string `toml:"url"` + // URLEnvVar is the name of an environment variable containing the database URL. + // Only used if URL is not defined. + URLEnvVar string `toml:"url_env_var"` } -func (c *DBConfig) validate() error { - if c.URL == "" { - return fmt.Errorf("field 'url' is required") +// GetURL returns the database URL, following the precedence rule: +// URL (direct config) takes precedence over URLEnvVar (environment variable). +// Returns an error if neither source is defined or if URLEnvVar references a non-existent variable. +func (c *DBConfig) GetURL() (string, error) { + // If URL is defined, use it (it has precedence) + if c.URL != "" { + return c.URL, nil } - return nil + + // If URL is not defined, try to load from URLEnvVar + if c.URLEnvVar != "" { + envURL := os.Getenv(c.URLEnvVar) + if envURL == "" { + return "", fmt.Errorf("field 'url' is empty and environment variable %q (from 'url_env_var') is not set or is empty", c.URLEnvVar) + } + return envURL, nil + } + + // Neither URL nor URLEnvVar is defined + return "", fmt.Errorf("field 'url' is required (either 'url' or 'url_env_var' must be defined)") +} + +func (c *DBConfig) validate() error { + _, err := c.GetURL() + return err } // ServerConfig is the configuration for the HTTP info server. diff --git a/bootstrap/config_test.go b/bootstrap/config_test.go index c7915a834..09c7da18e 100644 --- a/bootstrap/config_test.go +++ b/bootstrap/config_test.go @@ -91,27 +91,97 @@ func TestJDConfig_validate(t *testing.T) { } } +func TestKeystoreConfig_GetPassword(t *testing.T) { + tests := []struct { + name string + config *KeystoreConfig + envVars map[string]string + wantPassword string + wantErr bool + errContains []string + }{ + { + name: "returns direct password", + config: &KeystoreConfig{Password: "direct_secret"}, + wantPassword: "direct_secret", + wantErr: false, + }, + { + name: "password precedence over env var", + config: &KeystoreConfig{Password: "direct", PasswordEnvVar: "TEST_PASSWORD_ENV_VAR"}, + envVars: map[string]string{"TEST_PASSWORD_ENV_VAR": "from_env"}, + wantPassword: "direct", + wantErr: false, + }, + { + name: "returns env var when password empty", + config: &KeystoreConfig{Password: "", PasswordEnvVar: "TEST_PASSWORD_ENV_VAR"}, + envVars: map[string]string{"TEST_PASSWORD_ENV_VAR": "env_secret"}, + wantPassword: "env_secret", + wantErr: false, + }, + { + name: "error when env var not set", + config: &KeystoreConfig{Password: "", PasswordEnvVar: "MISSING_ENV_VAR"}, + envVars: map[string]string{}, + wantErr: true, + errContains: []string{"field 'password' is empty", "MISSING_ENV_VAR", "not set"}, + }, + { + name: "error when neither source defined", + config: &KeystoreConfig{Password: "", PasswordEnvVar: ""}, + wantErr: true, + errContains: []string{"field 'password' is required"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set up environment variables for this test + for key, value := range tt.envVars { + t.Setenv(key, value) + } + + password, err := tt.config.GetPassword() + if tt.wantErr { + require.Error(t, err) + for _, sub := range tt.errContains { + require.Contains(t, err.Error(), sub) + } + } else { + require.NoError(t, err) + require.Equal(t, tt.wantPassword, password) + } + }) + } +} + func TestKeystoreConfig_validate(t *testing.T) { tests := []struct { name string config *KeystoreConfig + envVars map[string]string wantErr bool errContains []string }{ { - name: "valid", + name: "valid with password", config: &KeystoreConfig{Password: "secret"}, wantErr: false, }, { - name: "missing password", - config: &KeystoreConfig{Password: ""}, + name: "neither password nor env var defined", + config: &KeystoreConfig{Password: "", PasswordEnvVar: ""}, wantErr: true, errContains: []string{"field 'password' is required"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + // Set up environment variables for this test + for key, value := range tt.envVars { + t.Setenv(key, value) + } + err := tt.config.validate() if tt.wantErr { require.Error(t, err) @@ -125,10 +195,75 @@ func TestKeystoreConfig_validate(t *testing.T) { } } +func TestDBConfig_GetURL(t *testing.T) { + tests := []struct { + name string + config *DBConfig + envVars map[string]string + wantURL string + wantErr bool + errContains []string + }{ + { + name: "returns direct url", + config: &DBConfig{URL: "postgres://localhost:5432/mydb"}, + wantURL: "postgres://localhost:5432/mydb", + wantErr: false, + }, + { + name: "url precedence over env var", + config: &DBConfig{URL: "postgres://direct", URLEnvVar: "TEST_DATABASE_URL"}, + envVars: map[string]string{"TEST_DATABASE_URL": "postgres://from_env"}, + wantURL: "postgres://direct", + wantErr: false, + }, + { + name: "returns env var when url empty", + config: &DBConfig{URL: "", URLEnvVar: "TEST_DATABASE_URL"}, + envVars: map[string]string{"TEST_DATABASE_URL": "postgres://env_db"}, + wantURL: "postgres://env_db", + wantErr: false, + }, + { + name: "error when env var not set", + config: &DBConfig{URL: "", URLEnvVar: "MISSING_DB_URL"}, + envVars: map[string]string{}, + wantErr: true, + errContains: []string{"field 'url' is empty", "MISSING_DB_URL", "not set"}, + }, + { + name: "error when neither source defined", + config: &DBConfig{URL: "", URLEnvVar: ""}, + wantErr: true, + errContains: []string{"field 'url' is required"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set up environment variables for this test + for key, value := range tt.envVars { + t.Setenv(key, value) + } + + url, err := tt.config.GetURL() + if tt.wantErr { + require.Error(t, err) + for _, sub := range tt.errContains { + require.Contains(t, err.Error(), sub) + } + } else { + require.NoError(t, err) + require.Equal(t, tt.wantURL, url) + } + }) + } +} + func TestDBConfig_validate(t *testing.T) { tests := []struct { name string config *DBConfig + envVars map[string]string wantErr bool errContains []string }{ @@ -138,14 +273,19 @@ func TestDBConfig_validate(t *testing.T) { wantErr: false, }, { - name: "missing url", - config: &DBConfig{URL: ""}, + name: "neither url nor env var defined", + config: &DBConfig{URL: "", URLEnvVar: ""}, wantErr: true, errContains: []string{"field 'url' is required"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + // Set up environment variables for this test + for key, value := range tt.envVars { + t.Setenv(key, value) + } + err := tt.config.validate() if tt.wantErr { require.Error(t, err)