Skip to content
Merged
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
37 changes: 32 additions & 5 deletions go/pkg/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"crypto/tls"
"flag"
"fmt"
"net/http"
"net/http/pprof"
"os"
Expand All @@ -29,6 +30,7 @@ import (

"github.com/gorilla/mux"

"github.com/hashicorp/go-multierror"
"github.com/kagent-dev/kagent/go/internal/version"

"k8s.io/apimachinery/pkg/api/resource"
Expand Down Expand Up @@ -147,6 +149,30 @@ func (cfg *Config) SetFlags(commandLine *flag.FlagSet) {
commandLine.Var(&cfg.Streaming.MaxBufSize, "streaming-max-buf-size", "The maximum size of the streaming buffer.")
commandLine.Var(&cfg.Streaming.InitialBufSize, "streaming-initial-buf-size", "The initial size of the streaming buffer.")
commandLine.DurationVar(&cfg.Streaming.Timeout, "streaming-timeout", 60*time.Second, "The timeout for the streaming connection.")

commandLine.StringVar(&agent_translator.DefaultImageConfig.Registry, "image-registry", agent_translator.DefaultImageConfig.Registry, "The registry to use for the image.")
commandLine.StringVar(&agent_translator.DefaultImageConfig.Tag, "image-tag", agent_translator.DefaultImageConfig.Tag, "The tag to use for the image.")
commandLine.StringVar(&agent_translator.DefaultImageConfig.PullPolicy, "image-pull-policy", agent_translator.DefaultImageConfig.PullPolicy, "The pull policy to use for the image.")
commandLine.StringVar(&agent_translator.DefaultImageConfig.PullSecret, "image-pull-secret", "", "The pull secret name for the agent image.")
commandLine.StringVar(&agent_translator.DefaultImageConfig.Repository, "image-repository", agent_translator.DefaultImageConfig.Repository, "The repository to use for the agent image.")
}

// 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 {
var loadErr error

fs.VisitAll(func(f *flag.Flag) {
envName := strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_"))

if envVal := os.Getenv(envName); envVal != "" {
if err := f.Value.Set(envVal); err != nil {
loadErr = multierror.Append(loadErr, fmt.Errorf("failed to set flag %s from env %s=%s: %w", f.Name, envName, envVal, err))
}
}
})

return loadErr
}

type BootstrapConfig struct {
Expand Down Expand Up @@ -174,16 +200,17 @@ func Start(getExtensionConfig GetExtensionConfig) {
ctx := context.Background()

cfg.SetFlags(flag.CommandLine)
flag.StringVar(&agent_translator.DefaultImageConfig.Registry, "image-registry", agent_translator.DefaultImageConfig.Registry, "The registry to use for the image.")
flag.StringVar(&agent_translator.DefaultImageConfig.Tag, "image-tag", agent_translator.DefaultImageConfig.Tag, "The tag to use for the image.")
flag.StringVar(&agent_translator.DefaultImageConfig.PullPolicy, "image-pull-policy", agent_translator.DefaultImageConfig.PullPolicy, "The pull policy to use for the image.")
flag.StringVar(&agent_translator.DefaultImageConfig.PullSecret, "image-pull-secret", "", "The pull secret name for the agent image.")
flag.StringVar(&agent_translator.DefaultImageConfig.Repository, "image-repository", agent_translator.DefaultImageConfig.Repository, "The repository to use for the agent image.")

opts := zap.Options{}
opts.BindFlags(flag.CommandLine)
flag.Parse()

// Load configuration from environment variables (overrides flags)
if err := LoadFromEnv(flag.CommandLine); err != nil {
setupLog.Error(err, "failed to load configuration from environment variables")
os.Exit(1)
}

logger := zap.New(zap.UseFlagOptions(&opts))
ctrl.SetLogger(logger)

Expand Down
219 changes: 219 additions & 0 deletions go/pkg/app/app_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package app

import (
"flag"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/api/resource"
)

func TestFilterValidNamespaces(t *testing.T) {
Expand Down Expand Up @@ -112,3 +115,219 @@ func TestConfigureNamespaceWatching(t *testing.T) {
})
}
}

func TestLoadFromEnv(t *testing.T) {
tests := []struct {
name string
envVars map[string]string
flagName string
flagDefault string
wantValue string
}{
{
name: "string flag with hyphen",
envVars: map[string]string{
"METRICS_BIND_ADDRESS": ":9090",
},
flagName: "metrics-bind-address",
flagDefault: ":8080",
wantValue: ":9090",
},
{
name: "flag without env var uses default",
envVars: map[string]string{
"OTHER_FLAG": "value",
},
flagName: "test-flag",
flagDefault: "default",
wantValue: "default",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set environment variables
for k, v := range tt.envVars {
t.Setenv(k, v)
}

// Create a new flag set for testing
fs := flag.NewFlagSet("test", flag.ContinueOnError)
var testVar string
fs.StringVar(&testVar, tt.flagName, tt.flagDefault, "test flag")

// Load from environment
if err := LoadFromEnv(fs); err != nil {
t.Fatalf("LoadFromEnv() error = %v", err)
}

// Check the value
if testVar != tt.wantValue {
t.Errorf("flag value = %v, want %v", testVar, tt.wantValue)
}
})
}
}

func TestLoadFromEnvBoolFlags(t *testing.T) {
tests := []struct {
name string
envValue string
wantValue bool
wantErr bool
}{
{
name: "true value",
envValue: "true",
wantValue: true,
wantErr: false,
},
{
name: "false value",
envValue: "false",
wantValue: false,
wantErr: false,
},
{
name: "1 value",
envValue: "1",
wantValue: true,
wantErr: false,
},
{
name: "0 value",
envValue: "0",
wantValue: false,
wantErr: false,
},
{
name: "invalid value",
envValue: "invalid",
wantValue: false,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
envName := "TEST_BOOL"
t.Setenv(envName, tt.envValue)

fs := flag.NewFlagSet("test", flag.ContinueOnError)
var testVar bool
fs.BoolVar(&testVar, "test-bool", false, "test bool flag")

err := LoadFromEnv(fs)
if (err != nil) != tt.wantErr {
t.Errorf("LoadFromEnv() error = %v, wantErr %v", err, tt.wantErr)
return
}

if !tt.wantErr && testVar != tt.wantValue {
t.Errorf("flag value = %v, want %v", testVar, tt.wantValue)
}
})
}
}

func TestLoadFromEnvDurationFlags(t *testing.T) {
envName := "TEST_DURATION"
t.Setenv(envName, "5m")

fs := flag.NewFlagSet("test", flag.ContinueOnError)
var testVar time.Duration
fs.DurationVar(&testVar, "test-duration", 1*time.Second, "test duration flag")

if err := LoadFromEnv(fs); err != nil {
t.Fatalf("LoadFromEnv() error = %v", err)
}

wantValue := 5 * time.Minute
if testVar != wantValue {
t.Errorf("flag value = %v, want %v", testVar, wantValue)
}
}

func TestLoadFromEnvIntegration(t *testing.T) {
envVars := map[string]string{
"METRICS_BIND_ADDRESS": ":9090",
"HEALTH_PROBE_BIND_ADDRESS": ":8081",
"LEADER_ELECT": "true",
"METRICS_SECURE": "false",
"ENABLE_HTTP2": "true",
"DEFAULT_MODEL_CONFIG_NAME": "custom-model",
"DEFAULT_MODEL_CONFIG_NAMESPACE": "custom-ns",
"HTTP_SERVER_ADDRESS": ":9000",
"A2A_BASE_URL": "http://example.com:9000",
"DATABASE_TYPE": "postgres",
"POSTGRES_DATABASE_URL": "postgres://localhost:5432/testdb",
"WATCH_NAMESPACES": "ns1,ns2,ns3",
"STREAMING_TIMEOUT": "120s",
"STREAMING_MAX_BUF_SIZE": "2Mi",
"STREAMING_INITIAL_BUF_SIZE": "8Ki",
}

for k, v := range envVars {
t.Setenv(k, v)
}

fs := flag.NewFlagSet("test", flag.ContinueOnError)
cfg := Config{}
cfg.SetFlags(fs) // Sets flags and defaults

if err := LoadFromEnv(fs); err != nil {
t.Fatalf("LoadFromEnv() error = %v", err)
}

// Verify values - env vars should override default flags
if cfg.Metrics.Addr != ":9090" {
t.Errorf("Metrics.Addr = %v, want :9090", cfg.Metrics.Addr)
}
if cfg.ProbeAddr != ":8081" {
t.Errorf("ProbeAddr = %v, want :8081", cfg.ProbeAddr)
}
if !cfg.LeaderElection {
t.Errorf("LeaderElection = false, want true")
}
if cfg.SecureMetrics {
t.Errorf("SecureMetrics = true, want false")
}
if !cfg.EnableHTTP2 {
t.Errorf("EnableHTTP2 = false, want true")
}
if cfg.DefaultModelConfig.Name != "custom-model" {
t.Errorf("DefaultModelConfig.Name = %v, want custom-model", cfg.DefaultModelConfig.Name)
}
if cfg.DefaultModelConfig.Namespace != "custom-ns" {
t.Errorf("DefaultModelConfig.Namespace = %v, want custom-ns", cfg.DefaultModelConfig.Namespace)
}
if cfg.HttpServerAddr != ":9000" {
t.Errorf("HttpServerAddr = %v, want :9000", cfg.HttpServerAddr)
}
if cfg.A2ABaseUrl != "http://example.com:9000" {
t.Errorf("A2ABaseUrl = %v, want http://example.com:9000", cfg.A2ABaseUrl)
}
if cfg.Database.Type != "postgres" {
t.Errorf("Database.Type = %v, want postgres", cfg.Database.Type)
}
if cfg.Database.Url != "postgres://localhost:5432/testdb" {
t.Errorf("Database.Url = %v, want postgres://localhost:5432/testdb", cfg.Database.Url)
}
if cfg.WatchNamespaces != "ns1,ns2,ns3" {
t.Errorf("WatchNamespaces = %v, want ns1,ns2,ns3", cfg.WatchNamespaces)
}
if cfg.Streaming.Timeout != 120*time.Second {
t.Errorf("Streaming.Timeout = %v, want 120s", cfg.Streaming.Timeout)
}

// Check quantity values
expectedMaxBuf := resource.MustParse("2Mi")
if cfg.Streaming.MaxBufSize.Cmp(expectedMaxBuf) != 0 {
t.Errorf("Streaming.MaxBufSize = %v, want 2Mi", cfg.Streaming.MaxBufSize)
}

expectedInitBuf := resource.MustParse("8Ki")
if cfg.Streaming.InitialBufSize.Cmp(expectedInitBuf) != 0 {
t.Errorf("Streaming.InitialBufSize = %v, want 8Ki", cfg.Streaming.InitialBufSize)
}
}
Comment on lines +251 to +333
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

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

The integration test TestLoadFromEnvIntegration doesn't test the image-related configuration flags that were added to SetFlags (lines 155-159 in app.go): IMAGE_REGISTRY, IMAGE_TAG, IMAGE_PULL_POLICY, IMAGE_PULL_SECRET, and IMAGE_REPOSITORY. Consider adding test cases for these environment variables to ensure they are properly loaded and set on agent_translator.DefaultImageConfig.

Copilot uses AI. Check for mistakes.
36 changes: 36 additions & 0 deletions helm/kagent/templates/controller-configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "kagent.fullname" . }}-controller
namespace: {{ include "kagent.namespace" . }}
labels:
{{- include "kagent.controller.labels" . | nindent 4 }}
data:
DATABASE_TYPE: {{ .Values.database.type | quote }}
DEFAULT_MODEL_CONFIG_NAME: {{ include "kagent.defaultModelConfigName" . | quote }}
IMAGE_PULL_POLICY: {{ .Values.controller.agentImage.pullPolicy | default .Values.imagePullPolicy | quote }}
{{- if and .Values.controller.agentImage.pullSecret (not (eq .Values.controller.agentImage.pullSecret "")) }}
IMAGE_PULL_SECRET: {{ .Values.controller.agentImage.pullSecret | quote }}
{{- end }}
IMAGE_REGISTRY: {{ .Values.controller.agentImage.registry | default .Values.registry | quote }}
IMAGE_REPOSITORY: {{ .Values.controller.agentImage.repository | quote }}
IMAGE_TAG: {{ coalesce .Values.controller.agentImage.tag .Values.tag .Chart.Version | quote }}
OTEL_EXPORTER_OTLP_ENDPOINT: {{ .Values.otel.tracing.exporter.otlp.endpoint | quote }}
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: {{ .Values.otel.logging.exporter.otlp.endpoint | quote }}
OTEL_EXPORTER_OTLP_LOGS_INSECURE: {{ .Values.otel.logging.exporter.otlp.insecure | quote }}
OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: {{ .Values.otel.logging.exporter.otlp.timeout | quote }}
OTEL_EXPORTER_OTLP_TRACES_INSECURE: {{ .Values.otel.tracing.exporter.otlp.insecure | quote }}
OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: {{ .Values.otel.tracing.exporter.otlp.timeout | quote }}
OTEL_LOGGING_ENABLED: {{ .Values.otel.logging.enabled | quote }}
OTEL_TRACING_ENABLED: {{ .Values.otel.tracing.enabled | quote }}
OTEL_TRACING_EXPORTER_OTLP_ENDPOINT: {{ .Values.otel.tracing.exporter.otlp.endpoint | quote }}
{{- 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 "")) }}
POSTGRES_DATABASE_URL: {{ .Values.database.postgres.url | quote }}
{{- 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 }}
WATCH_NAMESPACES: {{ include "kagent.watchNamespaces" . | quote }}
ZAP_LOG_LEVEL: {{ .Values.controller.loglevel | quote }}
Loading
Loading