From b54ae331e73c8aa5fb4c213d6a5b2c40531914a9 Mon Sep 17 00:00:00 2001 From: QuentinBisson Date: Mon, 15 Jun 2026 19:43:01 +0200 Subject: [PATCH 1/3] feat(controller): make Go (ADK) runtime agent image independently configurable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds DefaultGoImageConfig (registry, repository, pullPolicy) with per-field fallback to DefaultImageConfig and the existing golang-adk derivation logic. Exposed as --go-image-{registry,repository,pull-policy} flags (auto-mapped to GO_IMAGE_* env vars via LoadFromEnv) and as controller.goAgentImage.* Helm values. Fixes registry layouts where the Python repository is a flat name that cannot produce a valid path via last-segment replacement (e.g. kagent-golang-adk). Tag is not configurable — Go runtime images are digest-pinned at link time. Closes #2018 Signed-off-by: QuentinBisson --- .../translator/agent/adk_api_translator.go | 7 + .../translator/agent/deployments.go | 37 ++- .../translator/agent/runtime_test.go | 277 ++++++++++++++++++ go/core/pkg/app/app.go | 3 + .../templates/controller-configmap.yaml | 9 + helm/kagent/values.yaml | 10 + 6 files changed, 329 insertions(+), 14 deletions(-) diff --git a/go/core/internal/controller/translator/agent/adk_api_translator.go b/go/core/internal/controller/translator/agent/adk_api_translator.go index 74490cd673..87c9a9b96a 100644 --- a/go/core/internal/controller/translator/agent/adk_api_translator.go +++ b/go/core/internal/controller/translator/agent/adk_api_translator.go @@ -122,6 +122,13 @@ var PythonADKImageDigest string var GoADKImageDigest string var GoADKFullImageDigest string +// DefaultGoImageConfig overrides the image used for the Go (ADK) runtime agent. +// Any field left empty falls back to DefaultImageConfig for registry/pullPolicy, or to +// the derived repository (last segment of DefaultImageConfig.Repository replaced with +// "golang-adk") when Repository is empty. Tag is unused — Go runtime images are +// digest-pinned at controller link time via GoADKImageDigest / GoADKFullImageDigest. +var DefaultGoImageConfig = ImageConfig{} + // DefaultSkillsInitImageConfig is the image config for the skills-init container // that clones skill repositories from Git and pulls OCI skill images. var DefaultSkillsInitImageConfig = ImageConfig{ diff --git a/go/core/internal/controller/translator/agent/deployments.go b/go/core/internal/controller/translator/agent/deployments.go index 2a55255697..9b23d600d7 100644 --- a/go/core/internal/controller/translator/agent/deployments.go +++ b/go/core/internal/controller/translator/agent/deployments.go @@ -81,28 +81,24 @@ func getDefaultLabels(agentName string, incoming map[string]string) map[string]s } // getRuntimeImageRepository returns the image repository for a given runtime. -// It respects DefaultImageConfig.Repository for the Python runtime, and derives -// the Go runtime repository by replacing the last path segment with "golang-adk". -// This ensures custom repository configurations (e.g., --image-repository flag) work correctly. +// For the Go runtime: returns DefaultGoImageConfig.Repository when set; otherwise derives +// it from DefaultImageConfig.Repository by replacing the last path segment with "golang-adk". +// For the Python runtime (and default): returns DefaultImageConfig.Repository. func getRuntimeImageRepository(runtime v1alpha2.DeclarativeRuntime) string { switch runtime { case v1alpha2.DeclarativeRuntime_Go: - // Derive Go runtime repository from the default Python repository - // by replacing the last segment (typically "app") with "golang-adk". - // This respects any custom repository configuration. + if DefaultGoImageConfig.Repository != "" { + return DefaultGoImageConfig.Repository + } pythonRepo := DefaultImageConfig.Repository lastSlash := strings.LastIndex(pythonRepo, "/") if lastSlash == -1 { - // No slash found, repository is just the image name return "golang-adk" } - baseRepo := pythonRepo[:lastSlash] - return baseRepo + "/golang-adk" + return pythonRepo[:lastSlash] + "/golang-adk" case v1alpha2.DeclarativeRuntime_Python: - // Use the configured Python repository as-is return DefaultImageConfig.Repository default: - // Default to Python (should never happen due to enum validation) return DefaultImageConfig.Repository } } @@ -175,8 +171,21 @@ func resolveInlineDeployment(agent v1alpha2.AgentObject, mdd *modelDeploymentDat // Determine runtime (defaults to python if not set; substrate SandboxAgents use Go). runtime := v1alpha2.EffectiveDeclarativeRuntimeForAgent(agent) - // Get registry - registry := DefaultImageConfig.Registry + // Resolve base registry and pull policy; Go runtime uses DefaultGoImageConfig fields + // when set, falling back to DefaultImageConfig for any unset field. + baseRegistry := DefaultImageConfig.Registry + basePullPolicy := DefaultImageConfig.PullPolicy + if runtime == v1alpha2.DeclarativeRuntime_Go { + if DefaultGoImageConfig.Registry != "" { + baseRegistry = DefaultGoImageConfig.Registry + } + if DefaultGoImageConfig.PullPolicy != "" { + basePullPolicy = DefaultGoImageConfig.PullPolicy + } + } + + // Per-agent spec overrides take precedence over all defaults. + registry := baseRegistry if spec.ImageRegistry != "" { registry = spec.ImageRegistry } @@ -198,7 +207,7 @@ func resolveInlineDeployment(agent v1alpha2.AgentObject, mdd *modelDeploymentDat } } - imagePullPolicy := corev1.PullPolicy(DefaultImageConfig.PullPolicy) + imagePullPolicy := corev1.PullPolicy(basePullPolicy) if spec.ImagePullPolicy != "" { imagePullPolicy = corev1.PullPolicy(spec.ImagePullPolicy) } diff --git a/go/core/internal/controller/translator/agent/runtime_test.go b/go/core/internal/controller/translator/agent/runtime_test.go index f196415ed8..cad05d27ef 100644 --- a/go/core/internal/controller/translator/agent/runtime_test.go +++ b/go/core/internal/controller/translator/agent/runtime_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" schemev1 "k8s.io/client-go/kubernetes/scheme" @@ -487,3 +488,279 @@ func TestRuntime_CustomRepositoryPath_WithSkillsUsesFullTag(t *testing.T) { assert.Contains(t, container.Image, "my-registry.com/custom/golang-adk", "Image should use custom repository with golang-adk") assert.Contains(t, container.Image, "@sha256:test-go-full", "Go runtime with skills should use digest-pinned golang-adk-full image") } + +// withGoImageConfig sets DefaultGoImageConfig for the duration of a test and restores it +// via t.Cleanup. +func withGoImageConfig(t *testing.T, cfg translator.ImageConfig) { + t.Helper() + original := translator.DefaultGoImageConfig + translator.DefaultGoImageConfig = cfg + t.Cleanup(func() { translator.DefaultGoImageConfig = original }) +} + +// TestRuntime_GoImageConfig_FlatRepository tests that DefaultGoImageConfig.Repository is +// used verbatim, enabling flat-name registry layouts where the last-segment derivation +// would produce the wrong name (e.g. "kagent-golang-adk" instead of ".../golang-adk"). +func TestRuntime_GoImageConfig_FlatRepository(t *testing.T) { + withGoRuntimeDigests(t) + withGoImageConfig(t, translator.ImageConfig{Repository: "kagent-golang-adk"}) + + ctx := context.Background() + agent := &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "flat-repo-agent", Namespace: "test"}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + Runtime: v1alpha2.DeclarativeRuntime_Go, + SystemMessage: "test", + ModelConfig: "test-model", + }, + }, + } + modelConfig := &v1alpha2.ModelConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "test-model", Namespace: "test"}, + Spec: v1alpha2.ModelConfigSpec{Provider: "OpenAI", Model: "gpt-4o"}, + } + + scheme := schemev1.Scheme + require.NoError(t, v1alpha2.AddToScheme(scheme)) + kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(agent, modelConfig).Build() + translatorInstance := translator.NewAdkApiTranslator(kubeClient, types.NamespacedName{Namespace: "test", Name: "test-model"}, nil, "", nil) + + result, err := translator.TranslateAgent(ctx, translatorInstance, agent) + require.NoError(t, err) + + var deployment *appsv1.Deployment + for _, obj := range result.Manifest { + if dep, ok := obj.(*appsv1.Deployment); ok { + deployment = dep + break + } + } + require.NotNil(t, deployment) + + img := deployment.Spec.Template.Spec.Containers[0].Image + assert.Contains(t, img, "kagent-golang-adk@", "Image should use the explicit flat repository") + assert.NotContains(t, img, "/golang-adk@", "Derived path must not appear when repository is explicitly set") + assert.Contains(t, img, "@sha256:test-go-base") +} + +// TestRuntime_GoImageConfig_ExplicitRegistry tests that DefaultGoImageConfig.Registry is +// applied to the Go runtime while leaving the Python runtime's registry unaffected. +func TestRuntime_GoImageConfig_ExplicitRegistry(t *testing.T) { + withGoRuntimeDigests(t) + withPythonRuntimeDigest(t) + withGoImageConfig(t, translator.ImageConfig{Registry: "my.registry.io"}) + + ctx := context.Background() + goAgent := &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "go-agent", Namespace: "test"}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + Runtime: v1alpha2.DeclarativeRuntime_Go, + SystemMessage: "go", + ModelConfig: "test-model", + }, + }, + } + pythonAgent := &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "python-agent", Namespace: "test"}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + Runtime: v1alpha2.DeclarativeRuntime_Python, + SystemMessage: "python", + ModelConfig: "test-model", + }, + }, + } + modelConfig := &v1alpha2.ModelConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "test-model", Namespace: "test"}, + Spec: v1alpha2.ModelConfigSpec{Provider: "OpenAI", Model: "gpt-4o"}, + } + + scheme := schemev1.Scheme + require.NoError(t, v1alpha2.AddToScheme(scheme)) + kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(goAgent, pythonAgent, modelConfig).Build() + tn := types.NamespacedName{Namespace: "test", Name: "test-model"} + translatorInstance := translator.NewAdkApiTranslator(kubeClient, tn, nil, "", nil) + + goResult, err := translator.TranslateAgent(ctx, translatorInstance, goAgent) + require.NoError(t, err) + pythonResult, err := translator.TranslateAgent(ctx, translatorInstance, pythonAgent) + require.NoError(t, err) + + goImage, pythonImage := "", "" + for _, obj := range goResult.Manifest { + if dep, ok := obj.(*appsv1.Deployment); ok { + goImage = dep.Spec.Template.Spec.Containers[0].Image + break + } + } + for _, obj := range pythonResult.Manifest { + if dep, ok := obj.(*appsv1.Deployment); ok { + pythonImage = dep.Spec.Template.Spec.Containers[0].Image + break + } + } + + assert.Contains(t, goImage, "my.registry.io/", "Go image should use the explicit Go registry") + assert.NotContains(t, pythonImage, "my.registry.io/", "Python image must not use the Go-specific registry") +} + +// TestRuntime_GoImageConfig_FlatRepository_WithSkillsUsesFullDigest verifies that the +// golang-adk-full digest is still selected when DefaultGoImageConfig.Repository is set +// explicitly and the agent uses skills (which requires the full image). +func TestRuntime_GoImageConfig_FlatRepository_WithSkillsUsesFullDigest(t *testing.T) { + withGoRuntimeDigests(t) + withGoImageConfig(t, translator.ImageConfig{Repository: "kagent-golang-adk"}) + + ctx := context.Background() + agent := &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "flat-repo-skills-agent", Namespace: "test"}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + Runtime: v1alpha2.DeclarativeRuntime_Go, + SystemMessage: "test", + ModelConfig: "test-model", + }, + Skills: &v1alpha2.SkillForAgent{Refs: []string{"example.com/skill:latest"}}, + }, + } + modelConfig := &v1alpha2.ModelConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "test-model", Namespace: "test"}, + Spec: v1alpha2.ModelConfigSpec{Provider: "OpenAI", Model: "gpt-4o"}, + } + + scheme := schemev1.Scheme + require.NoError(t, v1alpha2.AddToScheme(scheme)) + kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(agent, modelConfig).Build() + translatorInstance := translator.NewAdkApiTranslator(kubeClient, types.NamespacedName{Namespace: "test", Name: "test-model"}, nil, "", nil) + + result, err := translator.TranslateAgent(ctx, translatorInstance, agent) + require.NoError(t, err) + + var deployment *appsv1.Deployment + for _, obj := range result.Manifest { + if dep, ok := obj.(*appsv1.Deployment); ok { + deployment = dep + break + } + } + require.NotNil(t, deployment) + + img := deployment.Spec.Template.Spec.Containers[0].Image + assert.Contains(t, img, "kagent-golang-adk", "Image should use the explicit repository") + assert.Contains(t, img, "@sha256:test-go-full", "Skills agent should use the full digest") +} + +// TestRuntime_GoImageConfig_EmptyFallsBackToDerivation ensures backward compatibility: +// when DefaultGoImageConfig is all-empty the Go runtime image is derived exactly as before. +func TestRuntime_GoImageConfig_EmptyFallsBackToDerivation(t *testing.T) { + withGoRuntimeDigests(t) + withGoImageConfig(t, translator.ImageConfig{}) // all empty + + ctx := context.Background() + agent := &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "go-fallback-agent", Namespace: "test"}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + Runtime: v1alpha2.DeclarativeRuntime_Go, + SystemMessage: "test", + ModelConfig: "test-model", + }, + }, + } + modelConfig := &v1alpha2.ModelConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "test-model", Namespace: "test"}, + Spec: v1alpha2.ModelConfigSpec{Provider: "OpenAI", Model: "gpt-4o"}, + } + + scheme := schemev1.Scheme + require.NoError(t, v1alpha2.AddToScheme(scheme)) + kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(agent, modelConfig).Build() + translatorInstance := translator.NewAdkApiTranslator(kubeClient, types.NamespacedName{Namespace: "test", Name: "test-model"}, nil, "", nil) + + result, err := translator.TranslateAgent(ctx, translatorInstance, agent) + require.NoError(t, err) + + var deployment *appsv1.Deployment + for _, obj := range result.Manifest { + if dep, ok := obj.(*appsv1.Deployment); ok { + deployment = dep + break + } + } + require.NotNil(t, deployment) + + img := deployment.Spec.Template.Spec.Containers[0].Image + assert.Contains(t, img, "golang-adk", "Fallback should still produce the derived golang-adk repository") + assert.Contains(t, img, "@sha256:test-go-base") +} + +// TestRuntime_GoImageConfig_ExplicitPullPolicy tests that DefaultGoImageConfig.PullPolicy +// is applied to the Go runtime and does not affect the Python runtime. +func TestRuntime_GoImageConfig_ExplicitPullPolicy(t *testing.T) { + withGoRuntimeDigests(t) + withPythonRuntimeDigest(t) + withGoImageConfig(t, translator.ImageConfig{PullPolicy: "Always"}) + + ctx := context.Background() + goAgent := &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "go-pullpolicy-agent", Namespace: "test"}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + Runtime: v1alpha2.DeclarativeRuntime_Go, + SystemMessage: "test", + ModelConfig: "test-model", + }, + }, + } + pythonAgent := &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "python-pullpolicy-agent", Namespace: "test"}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + Runtime: v1alpha2.DeclarativeRuntime_Python, + SystemMessage: "test", + ModelConfig: "test-model", + }, + }, + } + modelConfig := &v1alpha2.ModelConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "test-model", Namespace: "test"}, + Spec: v1alpha2.ModelConfigSpec{Provider: "OpenAI", Model: "gpt-4o"}, + } + + scheme := schemev1.Scheme + require.NoError(t, v1alpha2.AddToScheme(scheme)) + kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(goAgent, pythonAgent, modelConfig).Build() + tn := types.NamespacedName{Namespace: "test", Name: "test-model"} + translatorInstance := translator.NewAdkApiTranslator(kubeClient, tn, nil, "", nil) + + goResult, err := translator.TranslateAgent(ctx, translatorInstance, goAgent) + require.NoError(t, err) + pythonResult, err := translator.TranslateAgent(ctx, translatorInstance, pythonAgent) + require.NoError(t, err) + + goPullPolicy, pythonPullPolicy := corev1.PullPolicy(""), corev1.PullPolicy("") + for _, obj := range goResult.Manifest { + if dep, ok := obj.(*appsv1.Deployment); ok { + goPullPolicy = dep.Spec.Template.Spec.Containers[0].ImagePullPolicy + break + } + } + for _, obj := range pythonResult.Manifest { + if dep, ok := obj.(*appsv1.Deployment); ok { + pythonPullPolicy = dep.Spec.Template.Spec.Containers[0].ImagePullPolicy + break + } + } + + assert.Equal(t, corev1.PullAlways, goPullPolicy, "Go runtime should use the explicit pull policy") + assert.NotEqual(t, corev1.PullAlways, pythonPullPolicy, "Python runtime must not inherit the Go-specific pull policy") +} diff --git a/go/core/pkg/app/app.go b/go/core/pkg/app/app.go index 3781eefab8..a5be703400 100644 --- a/go/core/pkg/app/app.go +++ b/go/core/pkg/app/app.go @@ -218,6 +218,9 @@ func (cfg *Config) SetFlags(commandLine *flag.FlagSet) { commandLine.StringVar(&agent_translator.DefaultSkillsInitImageConfig.Tag, "skills-init-image-tag", agent_translator.DefaultSkillsInitImageConfig.Tag, "The tag to use for the skills init image.") commandLine.StringVar(&agent_translator.DefaultSkillsInitImageConfig.PullPolicy, "skills-init-image-pull-policy", agent_translator.DefaultSkillsInitImageConfig.PullPolicy, "The pull policy to use for the skills init image.") commandLine.StringVar(&agent_translator.DefaultSkillsInitImageConfig.Repository, "skills-init-image-repository", agent_translator.DefaultSkillsInitImageConfig.Repository, "The repository to use for the skills init image.") + commandLine.StringVar(&agent_translator.DefaultGoImageConfig.Registry, "go-image-registry", agent_translator.DefaultGoImageConfig.Registry, "The registry to use for the Go (ADK) runtime agent image. When empty, falls back to --image-registry.") + commandLine.StringVar(&agent_translator.DefaultGoImageConfig.Repository, "go-image-repository", agent_translator.DefaultGoImageConfig.Repository, "The repository to use for the Go (ADK) runtime agent image. When empty, derived from --image-repository by replacing the last path segment with \"golang-adk\".") + commandLine.StringVar(&agent_translator.DefaultGoImageConfig.PullPolicy, "go-image-pull-policy", agent_translator.DefaultGoImageConfig.PullPolicy, "The pull policy to use for the Go (ADK) runtime agent image. When empty, falls back to --image-pull-policy.") commandLine.StringVar(&cfg.Openshell.GatewayURL, "openshell-gateway-url", "", "gRPC target for the OpenShell sandbox gateway (e.g. dns:///openshell.openshell.svc:443). When empty, the Sandbox controller is disabled.") commandLine.StringVar(&cfg.Openshell.Token, "openshell-token", "", "Static bearer token for the OpenShell gateway. Prefer --openshell-token-file for secrets.") diff --git a/helm/kagent/templates/controller-configmap.yaml b/helm/kagent/templates/controller-configmap.yaml index 3f3dc04539..2e20686e6f 100644 --- a/helm/kagent/templates/controller-configmap.yaml +++ b/helm/kagent/templates/controller-configmap.yaml @@ -20,6 +20,15 @@ data: SKILLS_INIT_IMAGE_REGISTRY: {{ .Values.controller.skillsInitImage.registry | default .Values.registry | quote }} SKILLS_INIT_IMAGE_REPOSITORY: {{ .Values.controller.skillsInitImage.repository | quote }} SKILLS_INIT_IMAGE_TAG: {{ coalesce .Values.controller.skillsInitImage.tag .Values.tag .Chart.Version | quote }} + {{- with .Values.controller.goAgentImage.registry }} + GO_IMAGE_REGISTRY: {{ . | quote }} + {{- end }} + {{- with .Values.controller.goAgentImage.repository }} + GO_IMAGE_REPOSITORY: {{ . | quote }} + {{- end }} + {{- with .Values.controller.goAgentImage.pullPolicy }} + GO_IMAGE_PULL_POLICY: {{ . | quote }} + {{- end }} LEADER_ELECT: {{ include "kagent.leaderElectionEnabled" . | quote }} # OpenTelemetry Configuration OTEL_TRACING_ENABLED: {{ .Values.otel.tracing.enabled | quote }} diff --git a/helm/kagent/values.yaml b/helm/kagent/values.yaml index 3cc24721fd..bfe77fc208 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -187,6 +187,16 @@ controller: repository: kagent-dev/kagent/skills-init tag: "" # Will default to global, then Chart version pullPolicy: "" + # -- The image used for the Go (ADK) runtime agent. + # Each field is optional: when empty, the controller falls back to the corresponding + # agentImage value (registry/pullPolicy) or derives the repository from + # agentImage.repository by replacing the last path segment with "golang-adk". + # Note: tag is not configurable here — Go runtime images are digest-pinned at + # controller build time. + goAgentImage: + registry: "" + repository: "" # empty => derived from agentImage.repository + pullPolicy: "" # -- @deprecated Removed in 0.10.0. The A2A SDK now handles SSE buffering and timeouts # internally. These values have no effect and will be removed in a future release. streaming: null From 369300a6f032632dfdad0a38515d6f1f76f68304 Mon Sep 17 00:00:00 2001 From: QuentinBisson Date: Sun, 12 Jul 2026 01:12:03 +0200 Subject: [PATCH 2/3] refactor: give the Go (ADK) runtime image standalone image defaults Encode concrete defaults into DefaultGoImageConfig (registry, repository, pullPolicy) like the other image configs, and drop the repository derivation from the Python image. The Helm chart always emits GO_IMAGE_* and defaults the registry to the global registry, so mirroring through .Values.registry covers the Go runtime image too. The controller warns at startup when the Go and agent image registries differ. Signed-off-by: QuentinBisson --- .../translator/agent/adk_api_translator.go | 14 +- .../translator/agent/deployments.go | 37 +--- .../translator/agent/runtime_test.go | 209 +----------------- go/core/pkg/app/app.go | 15 +- .../templates/controller-configmap.yaml | 12 +- helm/kagent/values.yaml | 9 +- 6 files changed, 40 insertions(+), 256 deletions(-) diff --git a/go/core/internal/controller/translator/agent/adk_api_translator.go b/go/core/internal/controller/translator/agent/adk_api_translator.go index 3001d321b9..0f4e46c4cd 100644 --- a/go/core/internal/controller/translator/agent/adk_api_translator.go +++ b/go/core/internal/controller/translator/agent/adk_api_translator.go @@ -122,12 +122,14 @@ var PythonADKImageDigest string var GoADKImageDigest string var GoADKFullImageDigest string -// DefaultGoImageConfig overrides the image used for the Go (ADK) runtime agent. -// Any field left empty falls back to DefaultImageConfig for registry/pullPolicy, or to -// the derived repository (last segment of DefaultImageConfig.Repository replaced with -// "golang-adk") when Repository is empty. Tag is unused — Go runtime images are -// digest-pinned at controller link time via GoADKImageDigest / GoADKFullImageDigest. -var DefaultGoImageConfig = ImageConfig{} +// DefaultGoImageConfig is the image config for the Go (ADK) runtime agent. +// Tag is unused: Go runtime images are digest-pinned at controller link time +// via GoADKImageDigest / GoADKFullImageDigest. +var DefaultGoImageConfig = ImageConfig{ + Registry: "cr.kagent.dev", + PullPolicy: string(corev1.PullIfNotPresent), + Repository: "kagent-dev/kagent/golang-adk", +} // DefaultSkillsInitImageConfig is the image config for the skills-init container // that clones skill repositories from Git and pulls OCI skill images. diff --git a/go/core/internal/controller/translator/agent/deployments.go b/go/core/internal/controller/translator/agent/deployments.go index 9b23d600d7..fc9f56e77e 100644 --- a/go/core/internal/controller/translator/agent/deployments.go +++ b/go/core/internal/controller/translator/agent/deployments.go @@ -4,7 +4,6 @@ import ( "fmt" "maps" "slices" - "strings" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -80,27 +79,14 @@ func getDefaultLabels(agentName string, incoming map[string]string) map[string]s return defaultLabels } -// getRuntimeImageRepository returns the image repository for a given runtime. -// For the Go runtime: returns DefaultGoImageConfig.Repository when set; otherwise derives -// it from DefaultImageConfig.Repository by replacing the last path segment with "golang-adk". -// For the Python runtime (and default): returns DefaultImageConfig.Repository. +// getRuntimeImageRepository returns the image repository for a given runtime: +// DefaultGoImageConfig.Repository for the Go runtime, DefaultImageConfig.Repository +// otherwise. func getRuntimeImageRepository(runtime v1alpha2.DeclarativeRuntime) string { - switch runtime { - case v1alpha2.DeclarativeRuntime_Go: - if DefaultGoImageConfig.Repository != "" { - return DefaultGoImageConfig.Repository - } - pythonRepo := DefaultImageConfig.Repository - lastSlash := strings.LastIndex(pythonRepo, "/") - if lastSlash == -1 { - return "golang-adk" - } - return pythonRepo[:lastSlash] + "/golang-adk" - case v1alpha2.DeclarativeRuntime_Python: - return DefaultImageConfig.Repository - default: - return DefaultImageConfig.Repository + if runtime == v1alpha2.DeclarativeRuntime_Go { + return DefaultGoImageConfig.Repository } + return DefaultImageConfig.Repository } // validateExtraContainers checks that none of the extra containers use the @@ -171,17 +157,12 @@ func resolveInlineDeployment(agent v1alpha2.AgentObject, mdd *modelDeploymentDat // Determine runtime (defaults to python if not set; substrate SandboxAgents use Go). runtime := v1alpha2.EffectiveDeclarativeRuntimeForAgent(agent) - // Resolve base registry and pull policy; Go runtime uses DefaultGoImageConfig fields - // when set, falling back to DefaultImageConfig for any unset field. + // Resolve base registry and pull policy; the Go runtime uses DefaultGoImageConfig. baseRegistry := DefaultImageConfig.Registry basePullPolicy := DefaultImageConfig.PullPolicy if runtime == v1alpha2.DeclarativeRuntime_Go { - if DefaultGoImageConfig.Registry != "" { - baseRegistry = DefaultGoImageConfig.Registry - } - if DefaultGoImageConfig.PullPolicy != "" { - basePullPolicy = DefaultGoImageConfig.PullPolicy - } + baseRegistry = DefaultGoImageConfig.Registry + basePullPolicy = DefaultGoImageConfig.PullPolicy } // Per-agent spec overrides take precedence over all defaults. diff --git a/go/core/internal/controller/translator/agent/runtime_test.go b/go/core/internal/controller/translator/agent/runtime_test.go index cad05d27ef..7c648df0f9 100644 --- a/go/core/internal/controller/translator/agent/runtime_test.go +++ b/go/core/internal/controller/translator/agent/runtime_test.go @@ -336,159 +336,6 @@ func TestRuntime_DefaultToPython(t *testing.T) { assert.Equal(t, int32(15), container.ReadinessProbe.PeriodSeconds, "Should default to Python's 15s period") } -func TestRuntime_CustomRepositoryPath(t *testing.T) { - withGoRuntimeDigests(t) - ctx := context.Background() - - // Save original DefaultImageConfig.Repository and restore after test - originalRepo := translator.DefaultImageConfig.Repository - defer func() { - translator.DefaultImageConfig.Repository = originalRepo - }() - - // Set a custom repository path (simulating --image-repository flag) - translator.DefaultImageConfig.Repository = "my-registry.com/custom/app" - - // Create agent with Go runtime - agent := &v1alpha2.Agent{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-custom-repo-agent", - Namespace: "test", - }, - Spec: v1alpha2.AgentSpec{ - Type: v1alpha2.AgentType_Declarative, - Declarative: &v1alpha2.DeclarativeAgentSpec{ - Runtime: v1alpha2.DeclarativeRuntime_Go, - SystemMessage: "Test Go agent with custom repo", - ModelConfig: "test-model", - }, - }, - } - - // Create model config - modelConfig := &v1alpha2.ModelConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-model", - Namespace: "test", - }, - Spec: v1alpha2.ModelConfigSpec{ - Provider: "OpenAI", - Model: "gpt-4o", - }, - } - - // Set up fake client - scheme := schemev1.Scheme - err := v1alpha2.AddToScheme(scheme) - require.NoError(t, err) - - kubeClient := fake.NewClientBuilder(). - WithScheme(scheme). - WithObjects(agent, modelConfig). - Build() - - // Create translator - defaultModel := types.NamespacedName{ - Namespace: "test", - Name: "test-model", - } - translatorInstance := translator.NewAdkApiTranslator(kubeClient, defaultModel, nil, "", nil) - - // Translate agent - result, err := translator.TranslateAgent(ctx, translatorInstance, agent) - require.NoError(t, err) - require.NotNil(t, result) - - // Extract deployment from manifest - var deployment *appsv1.Deployment - for _, obj := range result.Manifest { - if dep, ok := obj.(*appsv1.Deployment); ok { - deployment = dep - break - } - } - require.NotNil(t, deployment, "Deployment should be in manifest") - - // Verify container image uses custom repository base with golang-adk - require.Len(t, deployment.Spec.Template.Spec.Containers, 1) - container := deployment.Spec.Template.Spec.Containers[0] - assert.Contains(t, container.Image, "my-registry.com/custom/golang-adk", "Image should use custom repository with golang-adk") - assert.Contains(t, container.Image, "@sha256:test-go-base", "Go runtime should use digest-pinned golang-adk image") -} - -func TestRuntime_CustomRepositoryPath_WithSkillsUsesFullTag(t *testing.T) { - withGoRuntimeDigests(t) - ctx := context.Background() - - originalRepo := translator.DefaultImageConfig.Repository - defer func() { - translator.DefaultImageConfig.Repository = originalRepo - }() - translator.DefaultImageConfig.Repository = "my-registry.com/custom/app" - - agent := &v1alpha2.Agent{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-custom-repo-skills-agent", - Namespace: "test", - }, - Spec: v1alpha2.AgentSpec{ - Type: v1alpha2.AgentType_Declarative, - Declarative: &v1alpha2.DeclarativeAgentSpec{ - Runtime: v1alpha2.DeclarativeRuntime_Go, - SystemMessage: "Test Go agent with custom repo and skills", - ModelConfig: "test-model", - }, - Skills: &v1alpha2.SkillForAgent{ - Refs: []string{"example.com/skill:latest"}, - }, - }, - } - - modelConfig := &v1alpha2.ModelConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-model", - Namespace: "test", - }, - Spec: v1alpha2.ModelConfigSpec{ - Provider: "OpenAI", - Model: "gpt-4o", - }, - } - - scheme := schemev1.Scheme - err := v1alpha2.AddToScheme(scheme) - require.NoError(t, err) - - kubeClient := fake.NewClientBuilder(). - WithScheme(scheme). - WithObjects(agent, modelConfig). - Build() - - defaultModel := types.NamespacedName{ - Namespace: "test", - Name: "test-model", - } - translatorInstance := translator.NewAdkApiTranslator(kubeClient, defaultModel, nil, "", nil) - - result, err := translator.TranslateAgent(ctx, translatorInstance, agent) - require.NoError(t, err) - require.NotNil(t, result) - - var deployment *appsv1.Deployment - for _, obj := range result.Manifest { - if dep, ok := obj.(*appsv1.Deployment); ok { - deployment = dep - break - } - } - require.NotNil(t, deployment, "Deployment should be in manifest") - - require.Len(t, deployment.Spec.Template.Spec.Containers, 1) - container := deployment.Spec.Template.Spec.Containers[0] - assert.Contains(t, container.Image, "my-registry.com/custom/golang-adk", "Image should use custom repository with golang-adk") - assert.Contains(t, container.Image, "@sha256:test-go-full", "Go runtime with skills should use digest-pinned golang-adk-full image") -} - // withGoImageConfig sets DefaultGoImageConfig for the duration of a test and restores it // via t.Cleanup. func withGoImageConfig(t *testing.T, cfg translator.ImageConfig) { @@ -499,11 +346,10 @@ func withGoImageConfig(t *testing.T, cfg translator.ImageConfig) { } // TestRuntime_GoImageConfig_FlatRepository tests that DefaultGoImageConfig.Repository is -// used verbatim, enabling flat-name registry layouts where the last-segment derivation -// would produce the wrong name (e.g. "kagent-golang-adk" instead of ".../golang-adk"). +// used verbatim, enabling flat-name registry layouts (e.g. "kagent-golang-adk"). func TestRuntime_GoImageConfig_FlatRepository(t *testing.T) { withGoRuntimeDigests(t) - withGoImageConfig(t, translator.ImageConfig{Repository: "kagent-golang-adk"}) + withGoImageConfig(t, translator.ImageConfig{Registry: "cr.kagent.dev", Repository: "kagent-golang-adk"}) ctx := context.Background() agent := &v1alpha2.Agent{ @@ -550,7 +396,7 @@ func TestRuntime_GoImageConfig_FlatRepository(t *testing.T) { func TestRuntime_GoImageConfig_ExplicitRegistry(t *testing.T) { withGoRuntimeDigests(t) withPythonRuntimeDigest(t) - withGoImageConfig(t, translator.ImageConfig{Registry: "my.registry.io"}) + withGoImageConfig(t, translator.ImageConfig{Registry: "my.registry.io", Repository: "kagent-dev/kagent/golang-adk"}) ctx := context.Background() goAgent := &v1alpha2.Agent{ @@ -614,7 +460,7 @@ func TestRuntime_GoImageConfig_ExplicitRegistry(t *testing.T) { // explicitly and the agent uses skills (which requires the full image). func TestRuntime_GoImageConfig_FlatRepository_WithSkillsUsesFullDigest(t *testing.T) { withGoRuntimeDigests(t) - withGoImageConfig(t, translator.ImageConfig{Repository: "kagent-golang-adk"}) + withGoImageConfig(t, translator.ImageConfig{Registry: "cr.kagent.dev", Repository: "kagent-golang-adk"}) ctx := context.Background() agent := &v1alpha2.Agent{ @@ -656,57 +502,12 @@ func TestRuntime_GoImageConfig_FlatRepository_WithSkillsUsesFullDigest(t *testin assert.Contains(t, img, "@sha256:test-go-full", "Skills agent should use the full digest") } -// TestRuntime_GoImageConfig_EmptyFallsBackToDerivation ensures backward compatibility: -// when DefaultGoImageConfig is all-empty the Go runtime image is derived exactly as before. -func TestRuntime_GoImageConfig_EmptyFallsBackToDerivation(t *testing.T) { - withGoRuntimeDigests(t) - withGoImageConfig(t, translator.ImageConfig{}) // all empty - - ctx := context.Background() - agent := &v1alpha2.Agent{ - ObjectMeta: metav1.ObjectMeta{Name: "go-fallback-agent", Namespace: "test"}, - Spec: v1alpha2.AgentSpec{ - Type: v1alpha2.AgentType_Declarative, - Declarative: &v1alpha2.DeclarativeAgentSpec{ - Runtime: v1alpha2.DeclarativeRuntime_Go, - SystemMessage: "test", - ModelConfig: "test-model", - }, - }, - } - modelConfig := &v1alpha2.ModelConfig{ - ObjectMeta: metav1.ObjectMeta{Name: "test-model", Namespace: "test"}, - Spec: v1alpha2.ModelConfigSpec{Provider: "OpenAI", Model: "gpt-4o"}, - } - - scheme := schemev1.Scheme - require.NoError(t, v1alpha2.AddToScheme(scheme)) - kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(agent, modelConfig).Build() - translatorInstance := translator.NewAdkApiTranslator(kubeClient, types.NamespacedName{Namespace: "test", Name: "test-model"}, nil, "", nil) - - result, err := translator.TranslateAgent(ctx, translatorInstance, agent) - require.NoError(t, err) - - var deployment *appsv1.Deployment - for _, obj := range result.Manifest { - if dep, ok := obj.(*appsv1.Deployment); ok { - deployment = dep - break - } - } - require.NotNil(t, deployment) - - img := deployment.Spec.Template.Spec.Containers[0].Image - assert.Contains(t, img, "golang-adk", "Fallback should still produce the derived golang-adk repository") - assert.Contains(t, img, "@sha256:test-go-base") -} - // TestRuntime_GoImageConfig_ExplicitPullPolicy tests that DefaultGoImageConfig.PullPolicy // is applied to the Go runtime and does not affect the Python runtime. func TestRuntime_GoImageConfig_ExplicitPullPolicy(t *testing.T) { withGoRuntimeDigests(t) withPythonRuntimeDigest(t) - withGoImageConfig(t, translator.ImageConfig{PullPolicy: "Always"}) + withGoImageConfig(t, translator.ImageConfig{Registry: "cr.kagent.dev", Repository: "kagent-dev/kagent/golang-adk", PullPolicy: "Always"}) ctx := context.Background() goAgent := &v1alpha2.Agent{ diff --git a/go/core/pkg/app/app.go b/go/core/pkg/app/app.go index 7cb0871106..ecb054985f 100644 --- a/go/core/pkg/app/app.go +++ b/go/core/pkg/app/app.go @@ -218,9 +218,9 @@ func (cfg *Config) SetFlags(commandLine *flag.FlagSet) { commandLine.StringVar(&agent_translator.DefaultSkillsInitImageConfig.Tag, "skills-init-image-tag", agent_translator.DefaultSkillsInitImageConfig.Tag, "The tag to use for the skills init image.") commandLine.StringVar(&agent_translator.DefaultSkillsInitImageConfig.PullPolicy, "skills-init-image-pull-policy", agent_translator.DefaultSkillsInitImageConfig.PullPolicy, "The pull policy to use for the skills init image.") commandLine.StringVar(&agent_translator.DefaultSkillsInitImageConfig.Repository, "skills-init-image-repository", agent_translator.DefaultSkillsInitImageConfig.Repository, "The repository to use for the skills init image.") - commandLine.StringVar(&agent_translator.DefaultGoImageConfig.Registry, "go-image-registry", agent_translator.DefaultGoImageConfig.Registry, "The registry to use for the Go (ADK) runtime agent image. When empty, falls back to --image-registry.") - commandLine.StringVar(&agent_translator.DefaultGoImageConfig.Repository, "go-image-repository", agent_translator.DefaultGoImageConfig.Repository, "The repository to use for the Go (ADK) runtime agent image. When empty, derived from --image-repository by replacing the last path segment with \"golang-adk\".") - commandLine.StringVar(&agent_translator.DefaultGoImageConfig.PullPolicy, "go-image-pull-policy", agent_translator.DefaultGoImageConfig.PullPolicy, "The pull policy to use for the Go (ADK) runtime agent image. When empty, falls back to --image-pull-policy.") + commandLine.StringVar(&agent_translator.DefaultGoImageConfig.Registry, "go-image-registry", agent_translator.DefaultGoImageConfig.Registry, "The registry to use for the Go (ADK) runtime agent image.") + commandLine.StringVar(&agent_translator.DefaultGoImageConfig.Repository, "go-image-repository", agent_translator.DefaultGoImageConfig.Repository, "The repository to use for the Go (ADK) runtime agent image.") + commandLine.StringVar(&agent_translator.DefaultGoImageConfig.PullPolicy, "go-image-pull-policy", agent_translator.DefaultGoImageConfig.PullPolicy, "The pull policy to use for the Go (ADK) runtime agent image.") commandLine.StringVar(&cfg.Openshell.GatewayURL, "openshell-gateway-url", "", "gRPC target for the OpenShell sandbox gateway (e.g. dns:///openshell.openshell.svc:443). When empty, the Sandbox controller is disabled.") commandLine.StringVar(&cfg.Openshell.Token, "openshell-token", "", "Static bearer token for the OpenShell gateway. Prefer --openshell-token-file for secrets.") @@ -379,6 +379,15 @@ func Start(getExtensionConfig GetExtensionConfig, migrationRunner MigrationRunne setupLog.Info("Starting KAgent Controller", "version", Version, "git_commit", GitCommit, "build_date", BuildDate, "config", cfg) + // The Go (ADK) runtime image is configured independently of the agent image. + // Warn operators who mirror only the agent image so a Go-runtime pod does not + // silently pull from the public registry. + if agent_translator.DefaultGoImageConfig.Registry != agent_translator.DefaultImageConfig.Registry { + setupLog.Info("Go (ADK) runtime image registry differs from the agent image registry; set --go-image-registry (GO_IMAGE_REGISTRY) to mirror it", + "goImageRegistry", agent_translator.DefaultGoImageConfig.Registry, + "agentImageRegistry", agent_translator.DefaultImageConfig.Registry) + } + // if the enable-http2 flag is false (the default), http/2 should be disabled // due to its vulnerabilities. More specifically, disabling http/2 will // prevent from being vulnerable to the HTTP/2 Stream Cancellation and diff --git a/helm/kagent/templates/controller-configmap.yaml b/helm/kagent/templates/controller-configmap.yaml index 2e20686e6f..e6b64faa51 100644 --- a/helm/kagent/templates/controller-configmap.yaml +++ b/helm/kagent/templates/controller-configmap.yaml @@ -20,15 +20,9 @@ data: SKILLS_INIT_IMAGE_REGISTRY: {{ .Values.controller.skillsInitImage.registry | default .Values.registry | quote }} SKILLS_INIT_IMAGE_REPOSITORY: {{ .Values.controller.skillsInitImage.repository | quote }} SKILLS_INIT_IMAGE_TAG: {{ coalesce .Values.controller.skillsInitImage.tag .Values.tag .Chart.Version | quote }} - {{- with .Values.controller.goAgentImage.registry }} - GO_IMAGE_REGISTRY: {{ . | quote }} - {{- end }} - {{- with .Values.controller.goAgentImage.repository }} - GO_IMAGE_REPOSITORY: {{ . | quote }} - {{- end }} - {{- with .Values.controller.goAgentImage.pullPolicy }} - GO_IMAGE_PULL_POLICY: {{ . | quote }} - {{- end }} + GO_IMAGE_PULL_POLICY: {{ .Values.controller.goAgentImage.pullPolicy | default .Values.imagePullPolicy | quote }} + GO_IMAGE_REGISTRY: {{ .Values.controller.goAgentImage.registry | default .Values.registry | quote }} + GO_IMAGE_REPOSITORY: {{ .Values.controller.goAgentImage.repository | quote }} LEADER_ELECT: {{ include "kagent.leaderElectionEnabled" . | quote }} # OpenTelemetry Configuration OTEL_TRACING_ENABLED: {{ .Values.otel.tracing.enabled | quote }} diff --git a/helm/kagent/values.yaml b/helm/kagent/values.yaml index df121afe5c..aa5b796179 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -188,14 +188,11 @@ controller: tag: "" # Will default to global, then Chart version pullPolicy: "" # -- The image used for the Go (ADK) runtime agent. - # Each field is optional: when empty, the controller falls back to the corresponding - # agentImage value (registry/pullPolicy) or derives the repository from - # agentImage.repository by replacing the last path segment with "golang-adk". - # Note: tag is not configurable here — Go runtime images are digest-pinned at - # controller build time. + # Registry and pullPolicy default to the global registry/imagePullPolicy when empty. + # Tag is not configurable here (Go runtime images are digest-pinned at controller build time). goAgentImage: registry: "" - repository: "" # empty => derived from agentImage.repository + repository: kagent-dev/kagent/golang-adk pullPolicy: "" # -- @deprecated Removed in 0.10.0. The A2A SDK now handles SSE buffering and timeouts # internally. These values have no effect and will be removed in a future release. From bb40cf4b260b0d94aff596ea5d157c3ea0e7cb19 Mon Sep 17 00:00:00 2001 From: QuentinBisson Date: Fri, 17 Jul 2026 09:59:11 +0200 Subject: [PATCH 3/3] chore: retrigger ci Signed-off-by: QuentinBisson