diff --git a/go/internal/database/manager.go b/go/internal/database/manager.go index baf2fa9b9..1f9a1ce55 100644 --- a/go/internal/database/manager.go +++ b/go/internal/database/manager.go @@ -2,6 +2,8 @@ package database import ( "fmt" + "os" + "strings" "sync" "github.com/glebarez/sqlite" @@ -30,7 +32,8 @@ type SqliteConfig struct { } type PostgresConfig struct { - URL string + URL string + URLFile string } type Config struct { @@ -63,7 +66,15 @@ func NewManager(config *Config) (*Manager, error) { TranslateError: true, }) case DatabaseTypePostgres: - db, err = gorm.Open(postgres.Open(config.PostgresConfig.URL), &gorm.Config{ + url := config.PostgresConfig.URL + if config.PostgresConfig.URLFile != "" { + resolved, resolveErr := resolveURLFile(config.PostgresConfig.URLFile) + if resolveErr != nil { + return nil, fmt.Errorf("failed to resolve postgres URL from file: %w", resolveErr) + } + url = resolved + } + db, err = gorm.Open(postgres.Open(url), &gorm.Config{ Logger: logger.Default.LogMode(logLevel), TranslateError: true, }) @@ -142,6 +153,20 @@ func (m *Manager) Reset(recreateTables bool) error { return nil } +// resolveURLFile reads a database connection URL from a file and returns the +// trimmed contents. Returns an error if the file cannot be read or is empty. +func resolveURLFile(path string) (string, error) { + content, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("reading URL file: %w", err) + } + url := strings.TrimSpace(string(content)) + if url == "" { + return "", fmt.Errorf("URL file %s is empty or contains only whitespace", path) + } + return url, nil +} + // Close closes the database connection func (m *Manager) Close() error { if m.db == nil { diff --git a/go/internal/database/manager_test.go b/go/internal/database/manager_test.go new file mode 100644 index 000000000..20f0e3eaf --- /dev/null +++ b/go/internal/database/manager_test.go @@ -0,0 +1,60 @@ +package database + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResolveURLFile(t *testing.T) { + tests := []struct { + name string + fileContent string + wantUrl string + wantErr bool + }{ + { + name: "reads URL from file", + fileContent: "postgres://testuser:testpass@host:5432/testdb", + wantUrl: "postgres://testuser:testpass@host:5432/testdb", + }, + { + name: "trims whitespace and newlines", + fileContent: " postgres://user:pass@host:5432/db\n", + wantUrl: "postgres://user:pass@host:5432/db", + }, + { + name: "empty file returns error", + fileContent: "", + wantErr: true, + }, + { + name: "whitespace-only file returns error", + fileContent: " \n\t\n ", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpFile := filepath.Join(t.TempDir(), "db-url") + err := os.WriteFile(tmpFile, []byte(tt.fileContent), 0600) + assert.NoError(t, err) + + url, err := resolveURLFile(tmpFile) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.wantUrl, url) + }) + } + + t.Run("missing file returns error", func(t *testing.T) { + _, err := resolveURLFile("/nonexistent/path/db-url") + assert.Error(t, err) + }) +} diff --git a/go/pkg/app/app.go b/go/pkg/app/app.go index 5387956a7..fe7f06b13 100644 --- a/go/pkg/app/app.go +++ b/go/pkg/app/app.go @@ -123,9 +123,10 @@ type Config struct { WatchNamespaces string A2ABaseUrl string Database struct { - Type string - Path string - Url string + Type string + Path string + Url string + UrlFile string } } @@ -156,6 +157,7 @@ func (cfg *Config) SetFlags(commandLine *flag.FlagSet) { commandLine.StringVar(&cfg.Database.Type, "database-type", "sqlite", "The type of the database to use. Supported values: sqlite, postgres.") commandLine.StringVar(&cfg.Database.Path, "sqlite-database-path", "./kagent.db", "The path to the SQLite database file.") commandLine.StringVar(&cfg.Database.Url, "postgres-database-url", "postgres://postgres:kagent@db.kagent.svc.cluster.local:5432/crud", "The URL of the PostgreSQL database.") + commandLine.StringVar(&cfg.Database.UrlFile, "postgres-database-url-file", "", "Path to a file containing the PostgreSQL database URL. Takes precedence over --postgres-database-url.") commandLine.StringVar(&cfg.WatchNamespaces, "watch-namespaces", "", "The namespaces to watch for .") @@ -350,7 +352,8 @@ func Start(getExtensionConfig GetExtensionConfig) { DatabasePath: cfg.Database.Path, }, PostgresConfig: &database.PostgresConfig{ - URL: cfg.Database.Url, + URL: cfg.Database.Url, + URLFile: cfg.Database.UrlFile, }, }) if err != nil { diff --git a/go/pkg/app/app_test.go b/go/pkg/app/app_test.go index ffb6ea2d7..ead8242bc 100644 --- a/go/pkg/app/app_test.go +++ b/go/pkg/app/app_test.go @@ -248,6 +248,23 @@ func TestLoadFromEnvDurationFlags(t *testing.T) { } } +func TestDatabaseUrlFileFlag(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ContinueOnError) + cfg := Config{} + cfg.SetFlags(fs) + + // Verify the flag exists and has the right default + f := fs.Lookup("postgres-database-url-file") + assert.NotNil(t, f, "postgres-database-url-file flag should be registered") + assert.Equal(t, "", f.DefValue, "default should be empty string") + + // Verify env var loading works for the new flag + t.Setenv("POSTGRES_DATABASE_URL_FILE", "/etc/credentials/db-url") + err := LoadFromEnv(fs) + assert.NoError(t, err) + assert.Equal(t, "/etc/credentials/db-url", cfg.Database.UrlFile) +} + func TestLoadFromEnvIntegration(t *testing.T) { envVars := map[string]string{ "METRICS_BIND_ADDRESS": ":9090", diff --git a/helm/kagent/templates/controller-configmap.yaml b/helm/kagent/templates/controller-configmap.yaml index 531ba55ff..f0a289119 100644 --- a/helm/kagent/templates/controller-configmap.yaml +++ b/helm/kagent/templates/controller-configmap.yaml @@ -48,9 +48,13 @@ data: {{- end }} {{- if eq .Values.database.type "sqlite" }} SQLITE_DATABASE_PATH: /sqlite-volume/{{ .Values.database.sqlite.databaseName }} - {{- else if and (eq .Values.database.type "postgres") (not (eq .Values.database.postgres.url "")) }} + {{- else if eq .Values.database.type "postgres" }} + {{- if not (eq .Values.database.postgres.urlFile "") }} + POSTGRES_DATABASE_URL_FILE: {{ .Values.database.postgres.urlFile | quote }} + {{- else if not (eq .Values.database.postgres.url "") }} POSTGRES_DATABASE_URL: {{ .Values.database.postgres.url | quote }} {{- end }} + {{- end }} STREAMING_INITIAL_BUF_SIZE: {{ .Values.controller.streaming.initialBufSize | quote }} STREAMING_MAX_BUF_SIZE: {{ .Values.controller.streaming.maxBufSize | quote }} STREAMING_TIMEOUT: {{ .Values.controller.streaming.timeout | quote }} diff --git a/helm/kagent/values.yaml b/helm/kagent/values.yaml index c6bd29397..fb84495f9 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -56,6 +56,9 @@ database: databaseName: kagent.db postgres: url: postgres://postgres:kagent@pgsql-postgresql.kagent.svc.cluster.local:5432/postgres + # Path to a file containing the database URL. + # Takes precedence over url when set. + urlFile: "" # ============================================================================== # CONTROLLER CONFIGURATION