From 8a4a82716d9f5898da96fdb2575082b1a0314abc Mon Sep 17 00:00:00 2001 From: Matteo Mori Date: Fri, 20 Feb 2026 16:53:47 +0000 Subject: [PATCH 1/4] feat: add --postgres-database-url-file flag for file-based DB credentials Support reading the PostgreSQL connection URL from a file on disk. When set, takes precedence over --postgres-database-url. Useful for credential injection systems that write secrets to files rather than environment variables. Signed-off-by: Matteo Mori Co-Authored-By: Claude Opus 4.6 --- go/pkg/app/app.go | 37 ++++++++-- go/pkg/app/app_test.go | 70 +++++++++++++++++++ .../templates/controller-configmap.yaml | 6 +- helm/kagent/values.yaml | 3 + 4 files changed, 110 insertions(+), 6 deletions(-) diff --git a/go/pkg/app/app.go b/go/pkg/app/app.go index 5387956a7..fe647daf6 100644 --- a/go/pkg/app/app.go +++ b/go/pkg/app/app.go @@ -122,10 +122,11 @@ type Config struct { HttpServerAddr string WatchNamespaces string A2ABaseUrl string - Database struct { - Type string - Path string - Url string + Database struct { + 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 .") @@ -190,6 +192,21 @@ func LoadFromEnv(fs *flag.FlagSet) error { return loadErr } +// resolvePostgresURLFile reads a PostgreSQL connection URL from a file and +// returns the trimmed contents. It returns an error if the file cannot be read +// or if the file is empty/whitespace-only. +func resolvePostgresURLFile(path string) (string, error) { + content, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("reading postgres database URL file: %w", err) + } + url := strings.TrimSpace(string(content)) + if url == "" { + return "", fmt.Errorf("postgres database URL file %s is empty or contains only whitespace", path) + } + return url, nil +} + type BootstrapConfig struct { Ctx context.Context Manager manager.Manager @@ -230,7 +247,17 @@ func Start(getExtensionConfig GetExtensionConfig) { logger := zap.New(zap.UseFlagOptions(&opts)) ctrl.SetLogger(logger) - setupLog.Info("Starting KAgent Controller", "version", Version, "git_commit", GitCommit, "build_date", BuildDate, "config", cfg) + // If a URL file is specified and the database type is postgres, read the URL from it (takes precedence over --postgres-database-url) + if cfg.Database.UrlFile != "" && cfg.Database.Type == "postgres" { + url, err := resolvePostgresURLFile(cfg.Database.UrlFile) + if err != nil { + setupLog.Error(err, "failed to resolve postgres database URL from file", "path", cfg.Database.UrlFile) + os.Exit(1) + } + cfg.Database.Url = url + } + + setupLog.Info("Starting KAgent Controller", "version", Version, "git_commit", GitCommit, "build_date", BuildDate) goruntime.SetMaxProcs(logger) diff --git a/go/pkg/app/app_test.go b/go/pkg/app/app_test.go index ffb6ea2d7..4149792ea 100644 --- a/go/pkg/app/app_test.go +++ b/go/pkg/app/app_test.go @@ -2,6 +2,8 @@ package app import ( "flag" + "os" + "path/filepath" "strings" "testing" "time" @@ -248,6 +250,74 @@ func TestLoadFromEnvDurationFlags(t *testing.T) { } } +func TestResolvePostgresURLFile(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 := resolvePostgresURLFile(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 := resolvePostgresURLFile("/nonexistent/path/db-url") + assert.Error(t, err) + }) +} + +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 From d720010ddfaa90b3f23ff023a8221352786561f1 Mon Sep 17 00:00:00 2001 From: Matteo Mori Date: Mon, 23 Feb 2026 13:40:35 +0000 Subject: [PATCH 2/4] fix: align Database struct field with gofmt formatting Signed-off-by: Matteo Mori Co-Authored-By: Claude Opus 4.6 --- go/pkg/app/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/pkg/app/app.go b/go/pkg/app/app.go index fe647daf6..1c09d3304 100644 --- a/go/pkg/app/app.go +++ b/go/pkg/app/app.go @@ -122,7 +122,7 @@ type Config struct { HttpServerAddr string WatchNamespaces string A2ABaseUrl string - Database struct { + Database struct { Type string Path string Url string From b81dacb14f775ab93963bddb3af12740e1c2255c Mon Sep 17 00:00:00 2001 From: Matteo Mori Date: Mon, 23 Feb 2026 15:22:58 +0000 Subject: [PATCH 3/4] refactor: move postgres URL file resolution into database manager Signed-off-by: Matteo Mori Co-Authored-By: Claude Opus 4.6 --- go/internal/database/manager.go | 29 +++++++++++++- go/internal/database/manager_test.go | 60 ++++++++++++++++++++++++++++ go/pkg/app/app.go | 28 +------------ go/pkg/app/app_test.go | 53 ------------------------ 4 files changed, 89 insertions(+), 81 deletions(-) create mode 100644 go/internal/database/manager_test.go 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 1c09d3304..616bbe63b 100644 --- a/go/pkg/app/app.go +++ b/go/pkg/app/app.go @@ -192,21 +192,6 @@ func LoadFromEnv(fs *flag.FlagSet) error { return loadErr } -// resolvePostgresURLFile reads a PostgreSQL connection URL from a file and -// returns the trimmed contents. It returns an error if the file cannot be read -// or if the file is empty/whitespace-only. -func resolvePostgresURLFile(path string) (string, error) { - content, err := os.ReadFile(path) - if err != nil { - return "", fmt.Errorf("reading postgres database URL file: %w", err) - } - url := strings.TrimSpace(string(content)) - if url == "" { - return "", fmt.Errorf("postgres database URL file %s is empty or contains only whitespace", path) - } - return url, nil -} - type BootstrapConfig struct { Ctx context.Context Manager manager.Manager @@ -247,16 +232,6 @@ func Start(getExtensionConfig GetExtensionConfig) { logger := zap.New(zap.UseFlagOptions(&opts)) ctrl.SetLogger(logger) - // If a URL file is specified and the database type is postgres, read the URL from it (takes precedence over --postgres-database-url) - if cfg.Database.UrlFile != "" && cfg.Database.Type == "postgres" { - url, err := resolvePostgresURLFile(cfg.Database.UrlFile) - if err != nil { - setupLog.Error(err, "failed to resolve postgres database URL from file", "path", cfg.Database.UrlFile) - os.Exit(1) - } - cfg.Database.Url = url - } - setupLog.Info("Starting KAgent Controller", "version", Version, "git_commit", GitCommit, "build_date", BuildDate) goruntime.SetMaxProcs(logger) @@ -377,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 4149792ea..ead8242bc 100644 --- a/go/pkg/app/app_test.go +++ b/go/pkg/app/app_test.go @@ -2,8 +2,6 @@ package app import ( "flag" - "os" - "path/filepath" "strings" "testing" "time" @@ -250,57 +248,6 @@ func TestLoadFromEnvDurationFlags(t *testing.T) { } } -func TestResolvePostgresURLFile(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 := resolvePostgresURLFile(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 := resolvePostgresURLFile("/nonexistent/path/db-url") - assert.Error(t, err) - }) -} - func TestDatabaseUrlFileFlag(t *testing.T) { fs := flag.NewFlagSet("test", flag.ContinueOnError) cfg := Config{} From a4e9e5782a54ded04a7eec64f254def4d3bb3026 Mon Sep 17 00:00:00 2001 From: Matteo Mori Date: Tue, 24 Feb 2026 09:05:43 +0000 Subject: [PATCH 4/4] fix: restore config logging on controller startup Signed-off-by: Matteo Mori Co-Authored-By: Claude Opus 4.6 --- go/pkg/app/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/pkg/app/app.go b/go/pkg/app/app.go index 616bbe63b..fe7f06b13 100644 --- a/go/pkg/app/app.go +++ b/go/pkg/app/app.go @@ -232,7 +232,7 @@ func Start(getExtensionConfig GetExtensionConfig) { logger := zap.New(zap.UseFlagOptions(&opts)) ctrl.SetLogger(logger) - setupLog.Info("Starting KAgent Controller", "version", Version, "git_commit", GitCommit, "build_date", BuildDate) + setupLog.Info("Starting KAgent Controller", "version", Version, "git_commit", GitCommit, "build_date", BuildDate, "config", cfg) goruntime.SetMaxProcs(logger)