Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Comment thread
Copilot marked this conversation as resolved.

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)
}
Comment thread
Copilot marked this conversation as resolved.

keyStore, csaSigner, err := initializeKeystore(ctx, b.lggr, db, keyStorePass, b.keys)
if err != nil {
return fmt.Errorf("failed to initialize keystore: %w", err)
}
Expand Down
66 changes: 56 additions & 10 deletions bootstrap/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Comment thread
nicolasgnr marked this conversation as resolved.
}

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"`
Comment thread
nicolasgnr marked this conversation as resolved.
}

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.
Expand Down
150 changes: 145 additions & 5 deletions bootstrap/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Comment thread
nicolasgnr marked this conversation as resolved.
{
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)
Expand All @@ -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
}{
Expand All @@ -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)
Expand Down
Loading