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
Original file line number Diff line number Diff line change
Expand Up @@ -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{}

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.

Why don't we just encode the defaults into this variable like we do for the other image configs?

@QuentinBisson QuentinBisson Jun 15, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hey @iplay88keys I wanted to preserve the issue's framing.

The empty default was there to avoid a breaking change: the controller currently derives the Go image from agentImage.repository, so anyone mirroring images only sets that one value and the Go image follows. If we hardcode the default and always emit GO_IMAGE_REPOSITORY, their next helm upgrade silently repoints the Go image back to cr.kagent.dev and pods fail to pull.

I do agree that this is not the better change though and I will replace this PR with the way it is done for the other images.


// DefaultSkillsInitImageConfig is the image config for the skills-init container
// that clones skill repositories from Git and pulls OCI skill images.
var DefaultSkillsInitImageConfig = ImageConfig{
Expand Down
37 changes: 23 additions & 14 deletions go/core/internal/controller/translator/agent/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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
}
}
Comment on lines +174 to +185

// Per-agent spec overrides take precedence over all defaults.
registry := baseRegistry
if spec.ImageRegistry != "" {
registry = spec.ImageRegistry
}
Expand All @@ -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)
}
Expand Down
277 changes: 277 additions & 0 deletions go/core/internal/controller/translator/agent/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Comment on lines +542 to +545
}

// 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")
}
3 changes: 3 additions & 0 deletions go/core/pkg/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
9 changes: 9 additions & 0 deletions helm/kagent/templates/controller-configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
Loading