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
46 changes: 40 additions & 6 deletions go/core/internal/database/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@ import (
// PostgresConfig holds the connection parameters for a Postgres database.
// URL must be a resolved connection string — use ResolveURL to resolve from
// a file path before constructing this config.
//
// Pool fields are optional: nil leaves the corresponding pgxpool.Config value
// from ParseConfig unchanged (pgx library defaults).
type PostgresConfig struct {
URL string
VectorEnabled bool
URL string
VectorEnabled bool
MaxConns *int32
MinConns *int32
MaxConnIdleTime *time.Duration
MaxConnLifetime *time.Duration
}

const (
Expand All @@ -30,21 +37,48 @@ const (
// Connect opens a Postgres connection pool using cfg and retries Ping with
// exponential backoff until the connection succeeds or defaultMaxTimeout elapses.
func Connect(ctx context.Context, cfg *PostgresConfig) (*pgxpool.Pool, error) {
return retryDBConnection(ctx, cfg.URL, cfg.VectorEnabled)
return retryDBConnection(ctx, cfg)
}

// applyPoolConfig copies non-nil pool settings from cfg onto config and
// validates the resulting pool bounds.
func applyPoolConfig(config *pgxpool.Config, cfg *PostgresConfig) error {
if cfg.MaxConns != nil {
config.MaxConns = *cfg.MaxConns
}
if cfg.MinConns != nil {
config.MinConns = *cfg.MinConns
}
if cfg.MaxConnIdleTime != nil {
config.MaxConnIdleTime = *cfg.MaxConnIdleTime
}
if cfg.MaxConnLifetime != nil {
config.MaxConnLifetime = *cfg.MaxConnLifetime
}
if config.MaxConns < 1 {
return fmt.Errorf("db maxConns must be >= 1, got %d", config.MaxConns)
}
if config.MinConns > config.MaxConns {
return fmt.Errorf("db minConns (%d) cannot be greater than maxConns (%d)", config.MinConns, config.MaxConns)
}
return nil
}

// retryDBConnection opens a pgxpool connection, registering pgvector types when
// vectorEnabled is true, and retries Ping with exponential backoff until the
// connection succeeds or defaultMaxTimeout elapses.
func retryDBConnection(ctx context.Context, url string, vectorEnabled bool) (*pgxpool.Pool, error) {
func retryDBConnection(ctx context.Context, cfg *PostgresConfig) (*pgxpool.Pool, error) {
ctx, cancel := context.WithTimeout(ctx, defaultMaxTimeout)
defer cancel()

config, err := pgxpool.ParseConfig(url)
config, err := pgxpool.ParseConfig(cfg.URL)
if err != nil {
return nil, fmt.Errorf("failed to parse database URL: %w", err)
}
if vectorEnabled {
if err := applyPoolConfig(config, cfg); err != nil {
return nil, err
}
if cfg.VectorEnabled {
config.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
return pgvectorpgx.RegisterTypes(ctx, conn)
}
Expand Down
38 changes: 37 additions & 1 deletion go/core/internal/database/connect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,53 @@ import (
"testing"
"time"

"github.com/jackc/pgx/v5/pgxpool"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestRetryDBConnection_DeadlineExceeded(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

_, err := retryDBConnection(ctx, "postgres://user:pass@localhost:1/nodb?connect_timeout=1", false)
_, err := retryDBConnection(ctx, &PostgresConfig{
URL: "postgres://user:pass@localhost:1/nodb?connect_timeout=1",
})
assert.ErrorIs(t, err, context.DeadlineExceeded)
}

func TestApplyPoolConfig(t *testing.T) {
base, err := pgxpool.ParseConfig("postgres://user:pass@localhost:5432/db")
require.NoError(t, err)

t.Run("unset leaves pgx defaults", func(t *testing.T) {
config := base.Copy()
applyPoolConfig(config, &PostgresConfig{})
assert.Equal(t, base.MaxConns, config.MaxConns)
assert.Equal(t, int32(0), config.MinConns)
assert.Equal(t, 30*time.Minute, config.MaxConnIdleTime)
assert.Equal(t, time.Hour, config.MaxConnLifetime)
})

t.Run("set fields override", func(t *testing.T) {
config := base.Copy()
maxConns := int32(8)
minConns := int32(0)
idle := time.Minute
lifetime := 10 * time.Minute
applyPoolConfig(config, &PostgresConfig{
MaxConns: &maxConns,
MinConns: &minConns,
MaxConnIdleTime: &idle,
MaxConnLifetime: &lifetime,
})
assert.Equal(t, int32(8), config.MaxConns)
assert.Equal(t, int32(0), config.MinConns)
assert.Equal(t, time.Minute, config.MaxConnIdleTime)
assert.Equal(t, 10*time.Minute, config.MaxConnLifetime)
})
}

func TestResolveURLFile(t *testing.T) {
tests := []struct {
name string
Expand Down
47 changes: 39 additions & 8 deletions go/core/pkg/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,14 @@ type Config struct {
// that originates TLS upstream. Off by default;
MCPEgressPlaintext bool
Database struct {
Url string
UrlFile string
VectorEnabled bool
SkipMigrations bool
Url string
UrlFile string
VectorEnabled bool
SkipMigrations bool
MaxConns int // 0 = unset (pgx default)
MinConns int // -1 = unset (pgx default); 0 is a valid value
MaxConnIdleTime time.Duration // 0 = unset (pgx default)
MaxConnLifetime time.Duration // 0 = unset (pgx default)
}
Substrate struct {
AteAPIEndpoint string
Expand Down Expand Up @@ -183,6 +187,10 @@ func (cfg *Config) SetFlags(commandLine *flag.FlagSet) {
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.BoolVar(&cfg.Database.VectorEnabled, "database-vector-enabled", true, "Enable pgvector extension and memory table. Requires pgvector to be installed on the PostgreSQL server.")
commandLine.BoolVar(&cfg.Database.SkipMigrations, "skip-migrations", false, "Do not run database migrations at startup; instead verify the database is already migrated and fail if it is not. Migrations must be applied out-of-band (e.g. from a pipeline or pre-upgrade hook). Settable via the SKIP_MIGRATIONS env var.")
commandLine.IntVar(&cfg.Database.MaxConns, "db-max-conns", 0, "Maximum number of connections in the Postgres pool. 0 leaves the pgx default.")
commandLine.IntVar(&cfg.Database.MinConns, "db-min-conns", -1, "Minimum number of connections in the Postgres pool. -1 leaves the pgx default.")
commandLine.DurationVar(&cfg.Database.MaxConnIdleTime, "db-max-conn-idle-time", 0, "Maximum idle time before a Postgres pool connection is closed. 0 leaves the pgx default (30m).")
commandLine.DurationVar(&cfg.Database.MaxConnLifetime, "db-max-conn-lifetime", 0, "Maximum lifetime of a Postgres pool connection. 0 leaves the pgx default (1h).")

commandLine.StringVar(&cfg.WatchNamespaces, "watch-namespaces", "", "The namespaces to watch for .")

Expand Down Expand Up @@ -230,6 +238,32 @@ func (cfg *Config) SetFlags(commandLine *flag.FlagSet) {
commandLine.StringVar(&agent_translator.DefaultAgentBindHost, "default-agent-bind-host", agent_translator.DefaultAgentBindHost, "Default host address for agent pods to bind to. Use '0.0.0.0' for IPv4 only or '::' for dual-stack (IPv4+IPv6).")
}

// postgresConfigFromApp builds a database.PostgresConfig from app flags.
// Zero/unset flag values leave the corresponding pool field nil so pgx defaults apply.
func postgresConfigFromApp(dbURL string, cfg *Config) *database.PostgresConfig {

@iplay88keys iplay88keys Aug 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should definitely validate that minConns is greater than maxConns if minConns is set.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair point, added the check

pgCfg := &database.PostgresConfig{
URL: dbURL,
VectorEnabled: cfg.Database.VectorEnabled,
}
if cfg.Database.MaxConns > 0 {
v := int32(cfg.Database.MaxConns)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For each of these, we should probably have an upper bound as well. At least since an int converted to an int32 can truncate. Probably not an issue in practice, though, as that is an incredibly large number.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that would be a realistic config though

pgCfg.MaxConns = &v
}
if cfg.Database.MinConns >= 0 {
v := int32(cfg.Database.MinConns)
pgCfg.MinConns = &v
}
if cfg.Database.MaxConnIdleTime > 0 {
v := cfg.Database.MaxConnIdleTime
pgCfg.MaxConnIdleTime = &v
}
if cfg.Database.MaxConnLifetime > 0 {
v := cfg.Database.MaxConnLifetime
pgCfg.MaxConnLifetime = &v
}
return pgCfg
}

// LoadFromEnv loads configuration values from environment variables.
// Flag names are converted to uppercase with underscores (e.g., metrics-bind-address -> METRICS_BIND_ADDRESS).
func LoadFromEnv(fs *flag.FlagSet) error {
Expand Down Expand Up @@ -512,10 +546,7 @@ func Start(getExtensionConfig GetExtensionConfig, extraSources []migrations.Sour
}

// Connect to database
db, err := database.Connect(ctx, &database.PostgresConfig{
URL: dbURL,
VectorEnabled: cfg.Database.VectorEnabled,
})
db, err := database.Connect(ctx, postgresConfigFromApp(dbURL, &cfg))
if err != nil {
setupLog.Error(err, "unable to connect to database")
os.Exit(1)
Expand Down
42 changes: 42 additions & 0 deletions go/core/pkg/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestFilterValidNamespaces(t *testing.T) {
Expand Down Expand Up @@ -264,6 +265,47 @@ func TestDatabaseUrlFileFlag(t *testing.T) {
assert.Equal(t, "/etc/credentials/db-url", cfg.Database.UrlFile)
}

func TestDatabasePoolFlags(t *testing.T) {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
cfg := Config{}
cfg.SetFlags(fs)

assert.Equal(t, "0", fs.Lookup("db-max-conns").DefValue)
assert.Equal(t, "-1", fs.Lookup("db-min-conns").DefValue)
assert.Equal(t, "0s", fs.Lookup("db-max-conn-idle-time").DefValue)
assert.Equal(t, "0s", fs.Lookup("db-max-conn-lifetime").DefValue)

t.Setenv("DB_MAX_CONNS", "4")
t.Setenv("DB_MIN_CONNS", "0")
t.Setenv("DB_MAX_CONN_IDLE_TIME", "1m")
t.Setenv("DB_MAX_CONN_LIFETIME", "10m")
assert.NoError(t, LoadFromEnv(fs))
assert.Equal(t, 4, cfg.Database.MaxConns)
assert.Equal(t, 0, cfg.Database.MinConns)
assert.Equal(t, time.Minute, cfg.Database.MaxConnIdleTime)
assert.Equal(t, 10*time.Minute, cfg.Database.MaxConnLifetime)

pgCfg := postgresConfigFromApp("postgres://localhost/db", &cfg)
require.NotNil(t, pgCfg.MaxConns)
assert.Equal(t, int32(4), *pgCfg.MaxConns)
require.NotNil(t, pgCfg.MinConns)
assert.Equal(t, int32(0), *pgCfg.MinConns)
require.NotNil(t, pgCfg.MaxConnIdleTime)
assert.Equal(t, time.Minute, *pgCfg.MaxConnIdleTime)
require.NotNil(t, pgCfg.MaxConnLifetime)
assert.Equal(t, 10*time.Minute, *pgCfg.MaxConnLifetime)
}

func TestPostgresConfigFromAppUnset(t *testing.T) {
cfg := Config{}
cfg.Database.MinConns = -1 // flag default
pgCfg := postgresConfigFromApp("postgres://localhost/db", &cfg)
assert.Nil(t, pgCfg.MaxConns)
assert.Nil(t, pgCfg.MinConns)
assert.Nil(t, pgCfg.MaxConnIdleTime)
assert.Nil(t, pgCfg.MaxConnLifetime)
}

func TestSubstrateAteAPITokenFileFlag(t *testing.T) {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
cfg := Config{}
Expand Down
14 changes: 14 additions & 0 deletions helm/kagent/templates/controller-configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ data:
{{- end }}
DATABASE_VECTOR_ENABLED: {{ .Values.database.postgres.vectorEnabled | quote }}
SKIP_MIGRATIONS: {{ .Values.database.postgres.skipMigrations | default false | quote }}
{{- with .Values.database.postgres.pool }}
{{- if and (hasKey . "maxConns") (ne .maxConns nil) }}
DB_MAX_CONNS: {{ .maxConns | quote }}
{{- end }}
{{- if and (hasKey . "minConns") (ne .minConns nil) }}
DB_MIN_CONNS: {{ .minConns | quote }}
{{- end }}
{{- if .maxConnIdleTime }}
DB_MAX_CONN_IDLE_TIME: {{ .maxConnIdleTime | quote }}
{{- end }}
{{- if .maxConnLifetime }}
DB_MAX_CONN_LIFETIME: {{ .maxConnLifetime | quote }}
{{- end }}
{{- end }}
WATCH_NAMESPACES: {{ include "kagent.watchNamespaces" . | quote }}
MCP_EGRESS_PLAINTEXT: {{ .Values.controller.mcpEgressPlaintext | default false | quote }}
{{- if .Values.controller.a2aClientTimeout }}
Expand Down
36 changes: 36 additions & 0 deletions helm/kagent/tests/controller-deployment_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,42 @@ tests:
path: data.DATABASE_VECTOR_ENABLED
value: "true"

- it: should not set DB pool env vars by default
template: controller-configmap.yaml
asserts:
- notExists:
path: data.DB_MAX_CONNS
- notExists:
path: data.DB_MIN_CONNS
- notExists:
path: data.DB_MAX_CONN_IDLE_TIME
- notExists:
path: data.DB_MAX_CONN_LIFETIME

- it: should set DB pool env vars when pool is configured
template: controller-configmap.yaml
set:
database:
postgres:
pool:
maxConns: 4
minConns: 0
maxConnIdleTime: 1m
maxConnLifetime: 10m
asserts:
- equal:
path: data.DB_MAX_CONNS
value: "4"
- equal:
path: data.DB_MIN_CONNS
value: "0"
- equal:
path: data.DB_MAX_CONN_IDLE_TIME
value: "1m"
- equal:
path: data.DB_MAX_CONN_LIFETIME
value: "10m"

- it: should not set POSTGRES_DATABASE_URL in configmap
template: controller-configmap.yaml
asserts:
Expand Down
11 changes: 11 additions & 0 deletions helm/kagent/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ database:
# The controller instead verifies the database is already migrated and fails if it is not.
# Migrations must be applied out-of-band (e.g. from a CI/CD pipeline) before install/upgrade.
skipMigrations: false
# -- Optional pgxpool settings. Leave unset/null to keep pgx library defaults
# (MaxConns≈max(4,NumCPU), MinConns=0, MaxConnIdleTime=30m, MaxConnLifetime=1h).
# For Aurora Serverless / scale-to-zero, recommended:
# minConns: 0
# maxConnIdleTime: 1m
# maxConns: 4
pool:
maxConns: null
minConns: null
maxConnIdleTime: ""
maxConnLifetime: ""
# -- Bundled PostgreSQL instance — for development and evaluation only.
# Not suitable for production. Deployed when enabled is true and url/urlFile are not set.
bundled:
Expand Down
Loading