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 a356cfea7..3f083c6ba 100644 --- a/go/core/internal/controller/translator/agent/adk_api_translator.go +++ b/go/core/internal/controller/translator/agent/adk_api_translator.go @@ -108,6 +108,52 @@ func normalizeImageDigest(digest string) string { return "sha256:" + strings.TrimPrefix(digest, "sha256:") } +// splitImageTag splits a controller image-tag value into a name and an optional +// digest. IMAGE_TAG is the tag component of registry/repository:IMAGE_TAG, and +// operators sometimes embed a digest as "tag@sha256:hex". A digest-only value +// ("sha256:hex" or "@sha256:hex") has an empty name. +func splitImageTag(tag string) (name, digest string, err error) { + tag = strings.TrimSpace(tag) + if tag == "" { + return "", "", fmt.Errorf("image tag is empty") + } + name, rawDigest, hasDigest := strings.Cut(tag, "@") + name = strings.TrimSpace(name) + if !hasDigest { + if strings.HasPrefix(name, "sha256:") { + return "", normalizeImageDigest(name), nil + } + return name, "", nil + } + rawDigest = strings.TrimSpace(rawDigest) + if rawDigest == "" { + return "", "", fmt.Errorf("image tag %q has an empty digest", tag) + } + return name, normalizeImageDigest(rawDigest), nil +} + +func fullVariantTag(name string) (string, error) { + name = strings.TrimSpace(name) + if name == "" { + return "", fmt.Errorf("image tag has no name") + } + if strings.HasSuffix(name, "-full") { + return name, nil + } + return name + "-full", nil +} + +func formatImageRef(registry, repository, tag, digest string) string { + base := fmt.Sprintf("%s/%s", registry, repository) + if tag != "" && digest != "" { + return fmt.Sprintf("%s:%s@%s", base, tag, digest) + } + if digest != "" { + return fmt.Sprintf("%s@%s", base, digest) + } + return fmt.Sprintf("%s:%s", base, tag) +} + var DefaultImageConfig = ImageConfig{ Registry: "ghcr.io", Tag: version.Get().Version, @@ -117,16 +163,40 @@ var DefaultImageConfig = ImageConfig{ } // PythonADKImageDigest, PythonADKFullImageDigest, GoADKImageDigest, and GoADKFullImageDigest -// default to the pushed runtime image manifest digests baked in at controller link time, and -// can be overridden at runtime via the --app[-full]-image-digest / --golang-adk[-full]-image-digest -// flags (for mirrored registries that re-assign digests). They are only consulted for sandbox -// agents — Substrate requires digest-pinned refs — while regular agents reference images by tag. -// The "full" variants bundle the sandbox runtime (code execution / bash tools); the slim -// variants do not. +// default to the pushed runtime image manifest digests baked in at controller link time +// (scripts/controller-digest-ldflags.sh). Released controller images always have the +// full-variant values populated. +// +// PythonADKFullImageDigestOverride and GoADKFullImageDigestOverride are empty unless the +// operator sets --app-full-image-digest / --golang-adk-full-image-digest (or the matching +// APP_FULL_IMAGE_DIGEST / GOLANG_ADK_FULL_IMAGE_DIGEST env, including Helm fullDigest). +// That is the only signal that a declarative skills agent should digest-pin the full +// variant: the baked digest is the upstream image, which may not match a mirror or the +// digest the operator embedded in IMAGE_TAG. +// +// Sandbox agents always pin (Substrate rejects tag refs): override if set, else baked. +// Regular agents reference images by tag so mirrored registries that rewrite digests +// still resolve. The "full" variants bundle the sandbox runtime (code execution / bash +// tools); the slim variants do not. var PythonADKImageDigest string var PythonADKFullImageDigest string var GoADKImageDigest string var GoADKFullImageDigest string +var PythonADKFullImageDigestOverride string +var GoADKFullImageDigestOverride string + +// fullRuntimeDigest returns the digest to use for a full-variant image. +// An explicit runtime override always wins. Sandbox (pinDigest) falls back to the +// link-time digest. Declarative agents do not: a baked digest is not an operator pin. +func fullRuntimeDigest(baked, override string, pinDigest bool) string { + if d := strings.TrimSpace(override); d != "" { + return d + } + if pinDigest { + return baked + } + return "" +} // DefaultGoImageConfig is the image config for the Go (ADK) runtime agent. // Regular agents reference it by tag; sandbox agents pin by digest via diff --git a/go/core/internal/controller/translator/agent/deployments.go b/go/core/internal/controller/translator/agent/deployments.go index 39355b786..30e35b1f5 100644 --- a/go/core/internal/controller/translator/agent/deployments.go +++ b/go/core/internal/controller/translator/agent/deployments.go @@ -4,6 +4,7 @@ import ( "fmt" "maps" "slices" + "strings" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -124,7 +125,7 @@ func resolvePythonRuntimeImage(registry string, full, pinDigest bool) (string, e digest := PythonADKImageDigest imageLabel := "app" if full { - digest = PythonADKFullImageDigest + digest = fullRuntimeDigest(PythonADKFullImageDigest, PythonADKFullImageDigestOverride, pinDigest) imageLabel = "app-full" } return resolveRuntimeImage(registry, repo, DefaultImageConfig.Tag, digest, imageLabel, full, pinDigest) @@ -135,7 +136,7 @@ func resolveGoRuntimeImage(registry string, full, pinDigest bool) (string, error digest := GoADKImageDigest imageLabel := "golang-adk" if full { - digest = GoADKFullImageDigest + digest = fullRuntimeDigest(GoADKFullImageDigest, GoADKFullImageDigestOverride, pinDigest) imageLabel = "golang-adk-full" } return resolveRuntimeImage(registry, repo, DefaultGoImageConfig.Tag, digest, imageLabel, full, pinDigest) @@ -149,23 +150,54 @@ func resolveGoRuntimeImage(registry string, full, pinDigest bool) (string, error // the repository and are published under "-full" (see APP_FULL_IMAGE_TAG / // GOLANG_ADK_FULL_IMAGE_TAG in the Makefile). // +// IMAGE_TAG is parsed as a tag, a tag@digest, or a digest-only value. The +// "-full" suffix is applied only to the tag name. A digest embedded in IMAGE_TAG +// is never reused on the full variant (the slim and full images have different +// manifests). If IMAGE_TAG includes a digest, the full image is referenced by +// tag only unless the operator set an explicit runtime full digest (Helm +// fullDigest / APP_FULL_IMAGE_DIGEST / --app-full-image-digest). The link-time +// baked digest is not that signal: released builds always populate it. +// // Sandbox agents require pinDigest: Substrate ActorTemplate validation rejects // image refs without a digest, so those use the link-time (or flag-overridden) // runtime image digests. func resolveRuntimeImage(registry, repository, tag, digest, imageLabel string, full, pinDigest bool) (string, error) { - if !pinDigest { - if full { - tag += "-full" + name, embeddedDigest, err := splitImageTag(tag) + if err != nil { + return "", fmt.Errorf("invalid %s image tag %q: %w", imageLabel, tag, err) + } + + if pinDigest { + if d := normalizeImageDigest(digest); d != "" { + return formatImageRef(registry, repository, "", d), nil } - return fmt.Sprintf("%s/%s:%s", registry, repository, tag), nil + return "", fmt.Errorf( + "%s image digest is not set; rebuild the controller after pushing agent runtime images, or override it via --%s-image-digest", + imageLabel, imageLabel, + ) + } + + if !full { + return formatImageRef(registry, repository, name, embeddedDigest), nil } - if d := normalizeImageDigest(digest); d != "" { - return fmt.Sprintf("%s/%s@%s", registry, repository, d), nil + + fullTag, err := fullVariantTag(name) + if err != nil { + if d := normalizeImageDigest(digest); d != "" { + return formatImageRef(registry, repository, "", d), nil + } + return "", fmt.Errorf( + "cannot derive %s image from digest-only tag %q: set a tag (for example 0.10.0-rc3) so the controller can use the published %s-full tag, or pin the full image via --%s-image-digest", + imageLabel, tag, strings.TrimSuffix(imageLabel, "-full"), imageLabel, + ) + } + + // Never reuse the slim digest on the full image. + var fullDigest string + if embeddedDigest != "" { + fullDigest = normalizeImageDigest(digest) } - return "", fmt.Errorf( - "%s image digest is not set; rebuild the controller after pushing agent runtime images, or override it via --%s-image-digest", - imageLabel, imageLabel, - ) + return formatImageRef(registry, repository, fullTag, fullDigest), nil } func resolveInlineDeployment(agent v1alpha2.AgentObject, mdd *modelDeploymentData) (*resolvedDeployment, error) { diff --git a/go/core/internal/controller/translator/agent/imageconfig_test.go b/go/core/internal/controller/translator/agent/imageconfig_test.go index 5b89607d6..aeaaec5a8 100644 --- a/go/core/internal/controller/translator/agent/imageconfig_test.go +++ b/go/core/internal/controller/translator/agent/imageconfig_test.go @@ -1,6 +1,7 @@ package agent import ( + "strings" "testing" "github.com/stretchr/testify/require" @@ -8,6 +9,11 @@ import ( "github.com/kagent-dev/kagent/go/api/v1alpha2" ) +const ( + testSlimDigest = "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + testFullDigest = "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" +) + func TestImageConfigImage(t *testing.T) { cfg := ImageConfig{ Registry: "ghcr.io", @@ -186,3 +192,232 @@ func TestResolveInlineDeploymentImagePinning(t *testing.T) { require.NoError(t, err) require.Contains(t, sdep.Image, "@sha256:pin-test", "sandbox agents require digest-pinned images (Substrate rejects tag refs)") } + +func TestSplitImageTag(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + tag string + wantName string + wantDigest string + wantErr string + }{ + {name: "bare tag", tag: "0.10.0-rc3", wantName: "0.10.0-rc3"}, + {name: "tag at digest", tag: "0.10.0-rc3@" + testSlimDigest, wantName: "0.10.0-rc3", wantDigest: testSlimDigest}, + {name: "digest only", tag: testSlimDigest, wantDigest: testSlimDigest}, + {name: "at digest only", tag: "@" + testSlimDigest, wantDigest: testSlimDigest}, + {name: "empty", tag: " ", wantErr: "image tag is empty"}, + {name: "empty digest", tag: "0.10.0-rc3@", wantErr: "empty digest"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + gotName, gotDigest, err := splitImageTag(tt.tag) + if tt.wantErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.wantErr) + return + } + require.NoError(t, err) + require.Equal(t, tt.wantName, gotName) + require.Equal(t, tt.wantDigest, gotDigest) + }) + } +} + +func TestFullRuntimeDigest(t *testing.T) { + t.Parallel() + require.Equal(t, testFullDigest, fullRuntimeDigest("sha256:baked", testFullDigest, false)) + require.Equal(t, testFullDigest, fullRuntimeDigest("sha256:baked", testFullDigest, true)) + require.Equal(t, "", fullRuntimeDigest("sha256:baked", "", false), "baked digest is not a declarative pin") + require.Equal(t, "sha256:baked", fullRuntimeDigest("sha256:baked", "", true)) + require.Equal(t, "", fullRuntimeDigest("", "", false)) +} + +func TestResolveRuntimeImageDigestPinnedTagDoesNotAppendFullToDigest(t *testing.T) { + originalTag := DefaultImageConfig.Tag + originalFull := PythonADKFullImageDigest + originalOverride := PythonADKFullImageDigestOverride + t.Cleanup(func() { + DefaultImageConfig.Tag = originalTag + PythonADKFullImageDigest = originalFull + PythonADKFullImageDigestOverride = originalOverride + }) + // Released controller builds always bake a full digest. That must not pin + // the declarative full image, or IMAGE_TAG=tag@digest becomes tag-full@baked. + DefaultImageConfig.Tag = "0.10.0-rc3@" + testSlimDigest + PythonADKFullImageDigest = testFullDigest + PythonADKFullImageDigestOverride = "" + + got, err := resolvePythonRuntimeImage("ghcr.io", true, false) + require.NoError(t, err) + require.Equal(t, "ghcr.io/kagent-dev/kagent/app:0.10.0-rc3-full", got) + require.NotContains(t, got, "deadbeef") + require.NotContains(t, got, testFullDigest) + require.False(t, strings.Contains(got, testSlimDigest+"-full"), "must not append -full after the slim digest") +} + +func TestResolveRuntimeImageDigestPinnedTagUsesExplicitFullDigest(t *testing.T) { + originalTag := DefaultImageConfig.Tag + originalFull := PythonADKFullImageDigest + originalOverride := PythonADKFullImageDigestOverride + t.Cleanup(func() { + DefaultImageConfig.Tag = originalTag + PythonADKFullImageDigest = originalFull + PythonADKFullImageDigestOverride = originalOverride + }) + DefaultImageConfig.Tag = "0.10.0-rc3@" + testSlimDigest + PythonADKFullImageDigest = "sha256:bakedbakedbakedbakedbakedbakedbakedbakedbakedbakedbakedbakedbake" + PythonADKFullImageDigestOverride = testFullDigest + + got, err := resolvePythonRuntimeImage("ghcr.io", true, false) + require.NoError(t, err) + require.Equal(t, "ghcr.io/kagent-dev/kagent/app:0.10.0-rc3-full@"+testFullDigest, got) + require.NotContains(t, got, "deadbeef") + require.NotContains(t, got, "baked") +} + +func TestResolveRuntimeImageDigestPinnedTagDoesNotReuseSlimDigestForGo(t *testing.T) { + originalTag := DefaultGoImageConfig.Tag + originalFull := GoADKFullImageDigest + originalOverride := GoADKFullImageDigestOverride + t.Cleanup(func() { + DefaultGoImageConfig.Tag = originalTag + GoADKFullImageDigest = originalFull + GoADKFullImageDigestOverride = originalOverride + }) + DefaultGoImageConfig.Tag = "0.10.0-rc3@" + testSlimDigest + GoADKFullImageDigest = "sha256:bakedbakedbakedbakedbakedbakedbakedbakedbakedbakedbakedbakedbake" + GoADKFullImageDigestOverride = testFullDigest + + got, err := resolveGoRuntimeImage("ghcr.io", true, false) + require.NoError(t, err) + require.Equal(t, "ghcr.io/kagent-dev/kagent/golang-adk:0.10.0-rc3-full@"+testFullDigest, got) + require.NotContains(t, got, "deadbeef") + require.NotContains(t, got, "baked") +} + +func TestResolveRuntimeImageBakedFullDigestStillPinsSandbox(t *testing.T) { + originalFull := PythonADKFullImageDigest + originalOverride := PythonADKFullImageDigestOverride + t.Cleanup(func() { + PythonADKFullImageDigest = originalFull + PythonADKFullImageDigestOverride = originalOverride + }) + PythonADKFullImageDigest = testFullDigest + PythonADKFullImageDigestOverride = "" + + got, err := resolvePythonRuntimeImage("ghcr.io", true, true) + require.NoError(t, err) + require.Equal(t, "ghcr.io/kagent-dev/kagent/app@"+testFullDigest, got) +} + +func TestResolveRuntimeImageExplicitFullDigestOverridesSandboxBaked(t *testing.T) { + originalFull := PythonADKFullImageDigest + originalOverride := PythonADKFullImageDigestOverride + t.Cleanup(func() { + PythonADKFullImageDigest = originalFull + PythonADKFullImageDigestOverride = originalOverride + }) + PythonADKFullImageDigest = "sha256:bakedbakedbakedbakedbakedbakedbakedbakedbakedbakedbakedbakedbake" + PythonADKFullImageDigestOverride = testFullDigest + + got, err := resolvePythonRuntimeImage("ghcr.io", true, true) + require.NoError(t, err) + require.Equal(t, "ghcr.io/kagent-dev/kagent/app@"+testFullDigest, got) +} + +func TestResolveRuntimeImagePreservesEmbeddedDigestForSlimImage(t *testing.T) { + originalTag := DefaultImageConfig.Tag + t.Cleanup(func() { DefaultImageConfig.Tag = originalTag }) + DefaultImageConfig.Tag = "0.10.0-rc3@" + testSlimDigest + + got, err := resolvePythonRuntimeImage("ghcr.io", false, false) + require.NoError(t, err) + require.Equal(t, "ghcr.io/kagent-dev/kagent/app:0.10.0-rc3@"+testSlimDigest, got) +} + +func TestResolveRuntimeImageDoesNotDoubleFullSuffix(t *testing.T) { + originalTag := DefaultImageConfig.Tag + t.Cleanup(func() { DefaultImageConfig.Tag = originalTag }) + DefaultImageConfig.Tag = "0.10.0-rc3-full" + + got, err := resolvePythonRuntimeImage("ghcr.io", true, false) + require.NoError(t, err) + require.Equal(t, "ghcr.io/kagent-dev/kagent/app:0.10.0-rc3-full", got) +} + +func TestResolveRuntimeImageDigestOnlyTagFailsClosedWithoutExplicitFullDigest(t *testing.T) { + originalTag := DefaultImageConfig.Tag + originalFull := PythonADKFullImageDigest + originalOverride := PythonADKFullImageDigestOverride + t.Cleanup(func() { + DefaultImageConfig.Tag = originalTag + PythonADKFullImageDigest = originalFull + PythonADKFullImageDigestOverride = originalOverride + }) + DefaultImageConfig.Tag = testSlimDigest + PythonADKFullImageDigest = testFullDigest + PythonADKFullImageDigestOverride = "" + + _, err := resolvePythonRuntimeImage("ghcr.io", true, false) + require.Error(t, err) + require.Contains(t, err.Error(), "digest-only") + require.Contains(t, err.Error(), "app-full-image-digest") +} + +func TestResolveRuntimeImageDigestOnlyTagUsesExplicitFullDigest(t *testing.T) { + originalTag := DefaultImageConfig.Tag + originalFull := PythonADKFullImageDigest + originalOverride := PythonADKFullImageDigestOverride + t.Cleanup(func() { + DefaultImageConfig.Tag = originalTag + PythonADKFullImageDigest = originalFull + PythonADKFullImageDigestOverride = originalOverride + }) + DefaultImageConfig.Tag = testSlimDigest + PythonADKFullImageDigest = "sha256:bakedbakedbakedbakedbakedbakedbakedbakedbakedbakedbakedbakedbake" + PythonADKFullImageDigestOverride = testFullDigest + + got, err := resolvePythonRuntimeImage("ghcr.io", true, false) + require.NoError(t, err) + require.Equal(t, "ghcr.io/kagent-dev/kagent/app@"+testFullDigest, got) +} + +func TestResolveRuntimeImageRejectsEmptyDigest(t *testing.T) { + originalTag := DefaultImageConfig.Tag + t.Cleanup(func() { DefaultImageConfig.Tag = originalTag }) + DefaultImageConfig.Tag = "0.10.0-rc3@" + + _, err := resolvePythonRuntimeImage("ghcr.io", true, false) + require.Error(t, err) + require.Contains(t, err.Error(), "empty digest") +} + +func TestResolveInlineDeploymentSkillsDropsEmbeddedSlimDigest(t *testing.T) { + originalTag := DefaultImageConfig.Tag + originalFull := PythonADKFullImageDigest + originalOverride := PythonADKFullImageDigestOverride + t.Cleanup(func() { + DefaultImageConfig.Tag = originalTag + PythonADKFullImageDigest = originalFull + PythonADKFullImageDigestOverride = originalOverride + }) + DefaultImageConfig.Tag = "0.10.0-rc3@" + testSlimDigest + PythonADKFullImageDigest = testFullDigest + PythonADKFullImageDigestOverride = "" + + agent := &v1alpha2.Agent{ + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{SystemMessage: "test", ModelConfig: "test-model"}, + Skills: &v1alpha2.SkillForAgent{Refs: []string{"example.com/skill:latest"}}, + }, + } + dep, err := resolveInlineDeployment(agent, &modelDeploymentData{}) + require.NoError(t, err) + require.Equal(t, "ghcr.io/kagent-dev/kagent/app:0.10.0-rc3-full", dep.Image) + require.NotContains(t, dep.Image, "deadbeef") + require.NotContains(t, dep.Image, testFullDigest) +} diff --git a/go/core/pkg/app/app.go b/go/core/pkg/app/app.go index f093ce35a..2ab30303d 100644 --- a/go/core/pkg/app/app.go +++ b/go/core/pkg/app/app.go @@ -215,9 +215,9 @@ func (cfg *Config) SetFlags(commandLine *flag.FlagSet) { 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.") commandLine.StringVar(&agent_translator.PythonADKImageDigest, "app-image-digest", agent_translator.PythonADKImageDigest, "Manifest digest (sha256:...) for the Python agent runtime image used by sandbox agents. Defaults to the digest baked in at build time; override when a mirrored registry re-assigns digests.") - commandLine.StringVar(&agent_translator.PythonADKFullImageDigest, "app-full-image-digest", agent_translator.PythonADKFullImageDigest, "Manifest digest (sha256:...) for the full Python agent runtime image used by sandbox agents. Defaults to the digest baked in at build time; override when a mirrored registry re-assigns digests.") + commandLine.StringVar(&agent_translator.PythonADKFullImageDigestOverride, "app-full-image-digest", "", "Explicit manifest digest (sha256:...) for the full Python agent runtime image (skills/SRT). Sandbox agents fall back to the link-time digest when this is unset. Declarative skills agents pin the full variant only when this is set (IMAGE_TAG embedding a digest is not enough, because released builds always bake an upstream digest).") commandLine.StringVar(&agent_translator.GoADKImageDigest, "golang-adk-image-digest", agent_translator.GoADKImageDigest, "Manifest digest (sha256:...) for the Go agent runtime image used by sandbox agents. Defaults to the digest baked in at build time; override when a mirrored registry re-assigns digests.") - commandLine.StringVar(&agent_translator.GoADKFullImageDigest, "golang-adk-full-image-digest", agent_translator.GoADKFullImageDigest, "Manifest digest (sha256:...) for the full Go agent runtime image used by sandbox agents. Defaults to the digest baked in at build time; override when a mirrored registry re-assigns digests.") + commandLine.StringVar(&agent_translator.GoADKFullImageDigestOverride, "golang-adk-full-image-digest", "", "Explicit manifest digest (sha256:...) for the full Go agent runtime image (skills/SRT). Same override rules as --app-full-image-digest.") commandLine.StringVar(&agent_translator.DefaultSkillsInitImageConfig.Registry, "skills-init-image-registry", agent_translator.DefaultSkillsInitImageConfig.Registry, "The registry to use for the skills init image.") 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.") diff --git a/helm/kagent/templates/controller-configmap.yaml b/helm/kagent/templates/controller-configmap.yaml index 2f21b0119..0a04a14dd 100644 --- a/helm/kagent/templates/controller-configmap.yaml +++ b/helm/kagent/templates/controller-configmap.yaml @@ -19,6 +19,9 @@ data: 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 }} + {{- if and .Values.controller.agentImage.fullDigest (not (eq .Values.controller.agentImage.fullDigest "")) }} + APP_FULL_IMAGE_DIGEST: {{ .Values.controller.agentImage.fullDigest | quote }} + {{- end }} SKILLS_INIT_IMAGE_PULL_POLICY: {{ .Values.controller.skillsInitImage.pullPolicy | default .Values.imagePullPolicy | quote }} SKILLS_INIT_IMAGE_REGISTRY: {{ .Values.controller.skillsInitImage.registry | default .Values.registry | quote }} SKILLS_INIT_IMAGE_REPOSITORY: {{ .Values.controller.skillsInitImage.repository | quote }} @@ -27,6 +30,9 @@ data: GO_IMAGE_REGISTRY: {{ .Values.controller.goAgentImage.registry | default .Values.registry | quote }} GO_IMAGE_REPOSITORY: {{ .Values.controller.goAgentImage.repository | quote }} GO_IMAGE_TAG: {{ coalesce .Values.controller.goAgentImage.tag .Values.tag .Chart.Version | quote }} + {{- if and .Values.controller.goAgentImage.fullDigest (not (eq .Values.controller.goAgentImage.fullDigest "")) }} + GOLANG_ADK_FULL_IMAGE_DIGEST: {{ .Values.controller.goAgentImage.fullDigest | quote }} + {{- end }} LEADER_ELECT: {{ include "kagent.leaderElectionEnabled" . | quote }} # OpenTelemetry Configuration OTEL_TRACING_ENABLED: {{ .Values.otel.tracing.enabled | quote }} diff --git a/helm/kagent/tests/controller-deployment_test.yaml b/helm/kagent/tests/controller-deployment_test.yaml index 66b1faa65..620053f86 100644 --- a/helm/kagent/tests/controller-deployment_test.yaml +++ b/helm/kagent/tests/controller-deployment_test.yaml @@ -235,6 +235,31 @@ tests: - equal: path: data.IMAGE_PULL_SECRET value: "pull-secret" + - it: should set APP_FULL_IMAGE_DIGEST when agentImage.fullDigest is set + template: controller-configmap.yaml + set: + controller: + agentImage: + fullDigest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + asserts: + - equal: + path: data.APP_FULL_IMAGE_DIGEST + value: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + - it: should not set APP_FULL_IMAGE_DIGEST when agentImage.fullDigest is unset + template: controller-configmap.yaml + asserts: + - notExists: + path: data.APP_FULL_IMAGE_DIGEST + - it: should set GOLANG_ADK_FULL_IMAGE_DIGEST when goAgentImage.fullDigest is set + template: controller-configmap.yaml + set: + controller: + goAgentImage: + fullDigest: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + asserts: + - equal: + path: data.GOLANG_ADK_FULL_IMAGE_DIGEST + value: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - it: should not use controller.agentImage.pullSecret when not set template: controller-configmap.yaml set: diff --git a/helm/kagent/values.yaml b/helm/kagent/values.yaml index 123053d7a..f9eb4e455 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -218,6 +218,13 @@ controller: registry: "" repository: kagent-dev/kagent/app tag: "" # Will default to global, then Chart version + # -- Explicit manifest digest (sha256:...) of the full (skills/SRT) Python + # agent image. Forwarded as APP_FULL_IMAGE_DIGEST. Required to digest-pin + # declarative skills agents when agentImage.tag embeds a digest + # (tag@sha256:...); the controller's link-time digest is not used for that + # path. Sandbox agents use this when set, otherwise the link-time digest. + # @default -- "" (declarative: tag-only full image; sandbox: link-time digest) + fullDigest: "" pullPolicy: "" # -- Image pull secret name set on agent pods created by the controller pullSecret: "" @@ -232,6 +239,11 @@ controller: registry: "" repository: kagent-dev/kagent/golang-adk tag: "" # Will default to global, then Chart version + # -- Explicit manifest digest (sha256:...) of the full (skills/SRT) Go + # agent image. Forwarded as GOLANG_ADK_FULL_IMAGE_DIGEST. Same rules as + # agentImage.fullDigest. + # @default -- "" (declarative: tag-only full image; sandbox: link-time digest) + fullDigest: "" 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.