From 654c1872cd964bb417c29ff7f0a82c39cefa3f85 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Mon, 23 Feb 2026 17:05:05 +0000 Subject: [PATCH 01/10] feat: add git-based skill fetching with shared auth secret Add support for cloning skills from Git repositories in agent init containers. A single gitAuthSecretRef on SkillForAgent applies to all gitRefs entries, matching the pattern used by InsecureSkipVerify for OCI skill refs. - Add GitRepo type and GitAuthSecretRef field to SkillForAgent CRD - Add git-init.sh.tmpl script template for cloning repos - Add buildGitSkillsInitContainer to create the init container - Support branch, tag, commit SHA refs, subdirectory paths, and custom skill names - Add configurable git init image (--git-init-image flag) - Add unit and golden tests Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Eitan Yarmush --- .gitignore | 2 +- go/api/v1alpha2/agent_types.go | 34 +- go/api/v1alpha2/zz_generated.deepcopy.go | 25 + .../translator/agent/adk_api_translator.go | 179 ++++++- .../translator/agent/git-init.sh.tmpl | 27 + .../translator/agent/git_skills_test.go | 466 ++++++++++++++++++ .../inputs/agent_with_git_skills.yaml | 54 ++ .../outputs/agent_with_allowed_headers.json | 4 + .../testdata/outputs/agent_with_code.json | 4 + .../agent_with_cross_namespace_tools.json | 4 + .../outputs/agent_with_custom_sa.json | 4 + .../outputs/agent_with_git_skills.json | 368 ++++++++++++++ .../outputs/agent_with_http_toolserver.json | 4 + .../outputs/agent_with_mcp_service.json | 4 + .../outputs/agent_with_nested_agent.json | 4 + .../outputs/agent_with_passthrough.json | 4 + .../testdata/outputs/agent_with_proxy.json | 4 + .../agent_with_proxy_external_remotemcp.json | 4 + .../outputs/agent_with_proxy_mcpserver.json | 4 + ...t_with_proxy_mcpserver_custom_timeout.json | 4 + .../outputs/agent_with_proxy_service.json | 4 + .../agent_with_scheduling_attributes.json | 4 + .../outputs/agent_with_security_context.json | 4 + .../testdata/outputs/agent_with_skills.json | 4 + .../outputs/agent_with_streaming.json | 4 + ...nt_with_system_message_from_configmap.json | 4 + ...agent_with_system_message_from_secret.json | 4 + .../testdata/outputs/anthropic_agent.json | 4 + .../agent/testdata/outputs/basic_agent.json | 4 + .../agent/testdata/outputs/bedrock_agent.json | 4 + .../agent/testdata/outputs/ollama_agent.json | 4 + .../testdata/outputs/tls-with-custom-ca.json | 4 + .../outputs/tls-with-disabled-verify.json | 4 + .../outputs/tls-with-system-cas-disabled.json | 4 + go/pkg/app/app.go | 1 + .../templates/controller-configmap.yaml | 1 + helm/kagent/values.yaml | 2 + 37 files changed, 1246 insertions(+), 17 deletions(-) create mode 100644 go/internal/controller/translator/agent/git-init.sh.tmpl create mode 100644 go/internal/controller/translator/agent/git_skills_test.go create mode 100644 go/internal/controller/translator/agent/testdata/inputs/agent_with_git_skills.yaml create mode 100644 go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json diff --git a/.gitignore b/.gitignore index 18e726076..6b928ec5e 100644 --- a/.gitignore +++ b/.gitignore @@ -156,7 +156,7 @@ celerybeat.pid # Environments python/.env .venv -env/ +.env/ venv/ ENV/ env.bak/ diff --git a/go/api/v1alpha2/agent_types.go b/go/api/v1alpha2/agent_types.go index 5dc8e4958..40b31d1ca 100644 --- a/go/api/v1alpha2/agent_types.go +++ b/go/api/v1alpha2/agent_types.go @@ -76,9 +76,41 @@ type SkillForAgent struct { InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` // The list of skill images to fetch. - // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=20 + // +optional Refs []string `json:"refs,omitempty"` + + // Reference to a Secret containing git credentials. + // Applied to all gitRefs entries. + // The secret should contain a `token` key for HTTPS auth, + // or `ssh-privatekey` for SSH auth. + // +optional + GitAuthSecretRef *corev1.LocalObjectReference `json:"gitAuthSecretRef,omitempty"` + + // Git repositories to fetch skills from. + // +kubebuilder:validation:MaxItems=20 + // +optional + GitRefs []GitRepo `json:"gitRefs,omitempty"` +} + +// GitRepo specifies a single Git repository to fetch skills from. +type GitRepo struct { + // URL of the git repository (HTTPS or SSH). + // +kubebuilder:validation:Required + URL string `json:"url"` + + // Git reference: branch name, tag, or commit SHA. + // +optional + // +kubebuilder:default="main" + Ref string `json:"ref,omitempty"` + + // Subdirectory within the repo to use as the skill root. + // +optional + Path string `json:"path,omitempty"` + + // Name for the skill directory under /skills. Defaults to the repo name. + // +optional + Name string `json:"name,omitempty"` } // +kubebuilder:validation:XValidation:rule="!has(self.systemMessage) || !has(self.systemMessageFrom)",message="systemMessage and systemMessageFrom are mutually exclusive" diff --git a/go/api/v1alpha2/zz_generated.deepcopy.go b/go/api/v1alpha2/zz_generated.deepcopy.go index b6cf6156f..e275e3e19 100644 --- a/go/api/v1alpha2/zz_generated.deepcopy.go +++ b/go/api/v1alpha2/zz_generated.deepcopy.go @@ -449,6 +449,21 @@ func (in *GeminiVertexAIConfig) DeepCopy() *GeminiVertexAIConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitRepo) DeepCopyInto(out *GitRepo) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitRepo. +func (in *GitRepo) DeepCopy() *GitRepo { + if in == nil { + return nil + } + out := new(GitRepo) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MCPTool) DeepCopyInto(out *MCPTool) { *out = *in @@ -1095,6 +1110,16 @@ func (in *SkillForAgent) DeepCopyInto(out *SkillForAgent) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.GitAuthSecretRef != nil { + in, out := &in.GitAuthSecretRef, &out.GitAuthSecretRef + *out = new(v1.LocalObjectReference) + **out = **in + } + if in.GitRefs != nil { + in, out := &in.GitRefs, &out.GitRefs + *out = make([]GitRepo, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SkillForAgent. diff --git a/go/internal/controller/translator/agent/adk_api_translator.go b/go/internal/controller/translator/agent/adk_api_translator.go index 670a51da4..e5c920216 100644 --- a/go/internal/controller/translator/agent/adk_api_translator.go +++ b/go/internal/controller/translator/agent/adk_api_translator.go @@ -1,8 +1,10 @@ package agent import ( + "bytes" "context" "crypto/sha256" + _ "embed" "encoding/binary" "encoding/hex" "encoding/json" @@ -11,8 +13,11 @@ import ( "maps" "net/url" "os" + "path" + "regexp" "slices" "strings" + "text/template" "time" "github.com/kagent-dev/kagent/go/api/v1alpha2" @@ -88,6 +93,11 @@ var DefaultImageConfig = ImageConfig{ Repository: "kagent-dev/kagent/app", } +// DefaultGitInitImage is the image used by the git-skills-init container to clone +// skill repositories from Git. Configurable via the --git-init-image flag or the +// GIT_INIT_IMAGE environment variable. +var DefaultGitInitImage = "alpine/git:2.47.2" + // TODO(ilackarms): migrate this whole package to pkg/translator type AgentOutputs = translator.AgentOutputs @@ -371,9 +381,14 @@ func (a *adkApiTranslator) buildManifest( ) var skills []string - if agent.Spec.Skills != nil && len(agent.Spec.Skills.Refs) != 0 { + var gitRefs []v1alpha2.GitRepo + var gitAuthSecretRef *corev1.LocalObjectReference + if agent.Spec.Skills != nil { skills = agent.Spec.Skills.Refs + gitRefs = agent.Spec.Skills.GitRefs + gitAuthSecretRef = agent.Spec.Skills.GitAuthSecretRef } + hasSkills := len(skills) > 0 || len(gitRefs) > 0 // Build Deployment volumes := append(secretVol, dep.Volumes...) @@ -382,12 +397,36 @@ func (a *adkApiTranslator) buildManifest( var initContainers []corev1.Container - if len(skills) > 0 { + // Add shared skills volume and env var when any skills (OCI or git) are present + if hasSkills { skillsEnv := corev1.EnvVar{ Name: env.KagentSkillsFolder.Name(), Value: "/skills", } needSandbox = true + volumes = append(volumes, corev1.Volume{ + Name: "kagent-skills", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }) + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: "kagent-skills", + MountPath: "/skills", + ReadOnly: true, + }) + sharedEnv = append(sharedEnv, skillsEnv) + } + + // Git-based skills init container (runs before OCI skills init) + if len(gitRefs) > 0 { + gitContainer, gitVolumes, _ := buildGitSkillsInitContainer(gitRefs, gitAuthSecretRef, dep.SecurityContext) + initContainers = append(initContainers, gitContainer) + volumes = append(volumes, gitVolumes...) + } + + // OCI-based skills init container + if len(skills) > 0 { insecure := agent.Spec.Skills.InsecureSkipVerify command := []string{"kagent-adk", "pull-skills"} if insecure { @@ -405,21 +444,12 @@ func (a *adkApiTranslator) buildManifest( VolumeMounts: []corev1.VolumeMount{ {Name: "kagent-skills", MountPath: "/skills"}, }, - Env: []corev1.EnvVar{skillsEnv}, + Env: []corev1.EnvVar{{ + Name: env.KagentSkillsFolder.Name(), + Value: "/skills", + }}, SecurityContext: initContainerSecurityContext, }) - volumes = append(volumes, corev1.Volume{ - Name: "kagent-skills", - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - }) - volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: "kagent-skills", - MountPath: "/skills", - ReadOnly: true, - }) - sharedEnv = append(sharedEnv, skillsEnv) } // Token volume @@ -1345,6 +1375,125 @@ func collectOtelEnvFromProcess() []corev1.EnvVar { return envVars } +// isCommitSHA returns true if ref looks like a full 40-character hex commit SHA. +var commitSHARegex = regexp.MustCompile(`^[0-9a-fA-F]{40}$`) + +func isCommitSHA(ref string) bool { + return commitSHARegex.MatchString(ref) +} + +// gitSkillName returns the directory name for a git skill ref. +// If Name is set, it is used; otherwise the last path segment of the repo URL +// (with any .git suffix stripped) is used. +func gitSkillName(ref v1alpha2.GitRepo) string { + if ref.Name != "" { + return ref.Name + } + // Extract repo name from URL + u := ref.URL + u = strings.TrimSuffix(u, ".git") + return path.Base(u) +} + +// gitSkillRefData holds pre-computed fields for each git skill ref, used by the script template. +type gitSkillRefData struct { + v1alpha2.GitRepo + Dest string // e.g. /skills/my-skill + IsCommit bool // true if Ref is a 40-char hex SHA + SubPath string // Path with trailing slash stripped + MountPath string // e.g. /git-auth (empty if no auth) +} + +//go:embed git-init.sh.tmpl +var gitInitScriptTmpl string + +// gitSkillsScriptTemplate is the shell script template for cloning skills from Git. +var gitSkillsScriptTemplate = template.Must(template.New("git-skills").Parse(gitInitScriptTmpl)) + +// buildGitSkillsScript builds the shell script that the git-skills-init container runs +// by rendering the gitSkillsScriptTemplate with pre-computed data for each ref. +func buildGitSkillsScript(refs []gitSkillRefData) string { + var buf bytes.Buffer + if err := gitSkillsScriptTemplate.Execute(&buf, refs); err != nil { + // Template is statically defined; this should never fail at runtime. + panic(fmt.Sprintf("failed to render git skills script: %v", err)) + } + return buf.String() +} + +// prepareGitSkillRefs converts GitRepo CRD values to the template-ready data structs. +// If authSecretRef is non-nil, every entry gets a shared MountPath pointing to the +// single auth volume. +func prepareGitSkillRefs(gitRefs []v1alpha2.GitRepo, authSecretRef *corev1.LocalObjectReference) []gitSkillRefData { + data := make([]gitSkillRefData, len(gitRefs)) + for i, ref := range gitRefs { + gitRef := ref.Ref + if gitRef == "" { + gitRef = "main" + } + ref.Ref = gitRef + + data[i] = gitSkillRefData{ + GitRepo: ref, + Dest: "/skills/" + gitSkillName(ref), + IsCommit: isCommitSHA(gitRef), + SubPath: strings.TrimSuffix(ref.Path, "/"), + } + if authSecretRef != nil { + data[i].MountPath = "/git-auth" + } + } + return data +} + +// buildGitSkillsInitContainer creates the init container and associated volumes/mounts +// for cloning skills from Git repositories. +// If authSecretRef is non-nil a single Secret volume is created and mounted at /git-auth. +func buildGitSkillsInitContainer( + gitRefs []v1alpha2.GitRepo, + authSecretRef *corev1.LocalObjectReference, + securityContext *corev1.SecurityContext, +) (container corev1.Container, volumes []corev1.Volume, secretVolumeMounts []corev1.VolumeMount) { + refData := prepareGitSkillRefs(gitRefs, authSecretRef) + script := buildGitSkillsScript(refData) + + initSecCtx := securityContext + if initSecCtx != nil { + initSecCtx = initSecCtx.DeepCopy() + } + + volumeMounts := []corev1.VolumeMount{ + {Name: "kagent-skills", MountPath: "/skills"}, + } + + // Mount single auth secret if provided + if authSecretRef != nil { + volumes = append(volumes, corev1.Volume{ + Name: "git-auth", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: authSecretRef.Name, + }, + }, + }) + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: "git-auth", + MountPath: "/git-auth", + ReadOnly: true, + }) + } + + container = corev1.Container{ + Name: "git-skills-init", + Image: DefaultGitInitImage, + Command: []string{"/bin/sh", "-c", script}, + VolumeMounts: volumeMounts, + SecurityContext: initSecCtx, + } + + return container, volumes, nil +} + func (a *adkApiTranslator) runPlugins(ctx context.Context, agent *v1alpha2.Agent, outputs *AgentOutputs) error { var errs error for _, plugin := range a.plugins { diff --git a/go/internal/controller/translator/agent/git-init.sh.tmpl b/go/internal/controller/translator/agent/git-init.sh.tmpl new file mode 100644 index 000000000..fbc6afbcf --- /dev/null +++ b/go/internal/controller/translator/agent/git-init.sh.tmpl @@ -0,0 +1,27 @@ +set -e +{{- with index . 0 }} +{{- if .MountPath }} +if [ -f '{{ .MountPath }}/ssh-privatekey' ]; then + mkdir -p ~/.ssh + cp '{{ .MountPath }}/ssh-privatekey' ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan github.com gitlab.com bitbucket.org >> ~/.ssh/known_hosts 2>/dev/null +elif [ -f '{{ .MountPath }}/token' ]; then + git config --global credential.helper '!f() { echo username=x-access-token; echo password=$(cat {{ .MountPath }}/token); }; f' +fi +{{- end }} +{{- end }} +{{- range . }} +{{- if .IsCommit }} +git clone -- '{{ .URL }}' '{{ .Dest }}' +cd '{{ .Dest }}' && git checkout '{{ .Ref }}' +{{- else }} +git clone --depth 1 --branch '{{ .Ref }}' -- '{{ .URL }}' '{{ .Dest }}' +{{- end }} +{{- if .SubPath }} +_tmp="$(mktemp -d)" +cp -a '{{ .Dest }}/{{ .SubPath }}/.' "$_tmp/" +rm -rf '{{ .Dest }}' +mv "$_tmp" '{{ .Dest }}' +{{- end }} +{{- end }} diff --git a/go/internal/controller/translator/agent/git_skills_test.go b/go/internal/controller/translator/agent/git_skills_test.go new file mode 100644 index 000000000..891925d8a --- /dev/null +++ b/go/internal/controller/translator/agent/git_skills_test.go @@ -0,0 +1,466 @@ +package agent_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + translator "github.com/kagent-dev/kagent/go/internal/controller/translator/agent" + 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" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func Test_AdkApiTranslator_GitSkills(t *testing.T) { + scheme := schemev1.Scheme + require.NoError(t, v1alpha2.AddToScheme(scheme)) + + namespace := "default" + modelName := "test-model" + + modelConfig := &v1alpha2.ModelConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: modelName, + Namespace: namespace, + }, + Spec: v1alpha2.ModelConfigSpec{ + Model: "gpt-4", + Provider: v1alpha2.ModelProviderOpenAI, + }, + } + + defaultModel := types.NamespacedName{ + Namespace: namespace, + Name: modelName, + } + + tests := []struct { + name string + agent *v1alpha2.Agent + // assertions + wantGitInit bool + wantOCIInit bool + wantSkillsVolume bool + wantGitImage string + wantContainsBranch string + wantContainsCommit string + wantContainsPath string + wantAuthVolume bool + }{ + { + name: "no skills - no init containers", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "agent-no-skills", Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "test", + ModelConfig: modelName, + }, + }, + }, + wantGitInit: false, + wantOCIInit: false, + wantSkillsVolume: false, + }, + { + name: "only OCI skills - no git init container", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "agent-oci-only", Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "test", + ModelConfig: modelName, + }, + Skills: &v1alpha2.SkillForAgent{ + Refs: []string{"ghcr.io/org/skill:v1"}, + }, + }, + }, + wantGitInit: false, + wantOCIInit: true, + wantSkillsVolume: true, + }, + { + name: "only git skills - git init container, no OCI init", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "agent-git-only", Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "test", + ModelConfig: modelName, + }, + Skills: &v1alpha2.SkillForAgent{ + GitRefs: []v1alpha2.GitRepo{ + { + URL: "https://github.com/org/my-skills", + Ref: "v1.0.0", + }, + }, + }, + }, + }, + wantGitInit: true, + wantOCIInit: false, + wantSkillsVolume: true, + wantGitImage: "alpine/git:2.47.2", + wantContainsBranch: "v1.0.0", + }, + { + name: "both OCI and git skills - both init containers", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "agent-both", Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "test", + ModelConfig: modelName, + }, + Skills: &v1alpha2.SkillForAgent{ + Refs: []string{"ghcr.io/org/skill:v1"}, + GitRefs: []v1alpha2.GitRepo{ + { + URL: "https://github.com/org/my-skills", + Ref: "main", + }, + }, + }, + }, + }, + wantGitInit: true, + wantOCIInit: true, + wantSkillsVolume: true, + }, + { + name: "git skill with commit SHA", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "agent-commit", Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "test", + ModelConfig: modelName, + }, + Skills: &v1alpha2.SkillForAgent{ + GitRefs: []v1alpha2.GitRepo{ + { + URL: "https://github.com/org/my-skills", + Ref: "abc123def456abc123def456abc123def456abc1", + }, + }, + }, + }, + }, + wantGitInit: true, + wantOCIInit: false, + wantSkillsVolume: true, + wantContainsCommit: "abc123def456abc123def456abc123def456abc1", + }, + { + name: "git skill with path subdirectory", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "agent-path", Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "test", + ModelConfig: modelName, + }, + Skills: &v1alpha2.SkillForAgent{ + GitRefs: []v1alpha2.GitRepo{ + { + URL: "https://github.com/org/mono-repo", + Ref: "main", + Path: "skills/k8s", + }, + }, + }, + }, + }, + wantGitInit: true, + wantOCIInit: false, + wantSkillsVolume: true, + wantContainsPath: "skills/k8s", + }, + { + name: "git skills with shared auth secret", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "agent-auth", Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "test", + ModelConfig: modelName, + }, + Skills: &v1alpha2.SkillForAgent{ + GitAuthSecretRef: &corev1.LocalObjectReference{ + Name: "github-token", + }, + GitRefs: []v1alpha2.GitRepo{ + { + URL: "https://github.com/org/private-skill", + Ref: "main", + }, + { + URL: "https://github.com/org/another-private-skill", + Ref: "v1.0.0", + }, + }, + }, + }, + }, + wantGitInit: true, + wantOCIInit: false, + wantSkillsVolume: true, + wantAuthVolume: true, + }, + { + name: "git skill with custom name", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "agent-custom-name", Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "test", + ModelConfig: modelName, + }, + Skills: &v1alpha2.SkillForAgent{ + GitRefs: []v1alpha2.GitRepo{ + { + URL: "https://github.com/org/my-skills.git", + Ref: "main", + Name: "custom-skill", + }, + }, + }, + }, + }, + wantGitInit: true, + wantOCIInit: false, + wantSkillsVolume: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + kubeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(modelConfig, tt.agent). + Build() + + trans := translator.NewAdkApiTranslator(kubeClient, defaultModel, nil, "") + + outputs, err := trans.TranslateAgent(context.Background(), tt.agent) + require.NoError(t, err) + require.NotNil(t, outputs) + + // Find deployment in manifest + var deployment *appsv1.Deployment + for _, obj := range outputs.Manifest { + if d, ok := obj.(*appsv1.Deployment); ok { + deployment = d + } + } + require.NotNil(t, deployment, "Deployment should be created") + + initContainers := deployment.Spec.Template.Spec.InitContainers + + // Check git init container + var gitInitContainer *corev1.Container + var ociInitContainer *corev1.Container + for i := range initContainers { + switch initContainers[i].Name { + case "git-skills-init": + gitInitContainer = &initContainers[i] + case "skills-init": + ociInitContainer = &initContainers[i] + } + } + + if tt.wantGitInit { + require.NotNil(t, gitInitContainer, "git-skills-init container should exist") + + if tt.wantGitImage != "" { + assert.Equal(t, tt.wantGitImage, gitInitContainer.Image) + } + + // Verify the script is passed via /bin/sh -c + require.Len(t, gitInitContainer.Command, 3) + assert.Equal(t, "/bin/sh", gitInitContainer.Command[0]) + assert.Equal(t, "-c", gitInitContainer.Command[1]) + script := gitInitContainer.Command[2] + + if tt.wantContainsBranch != "" { + assert.Contains(t, script, tt.wantContainsBranch) + assert.Contains(t, script, "--branch") + } + + if tt.wantContainsCommit != "" { + assert.Contains(t, script, tt.wantContainsCommit) + assert.Contains(t, script, "git checkout") + } + + if tt.wantContainsPath != "" { + assert.Contains(t, script, tt.wantContainsPath) + assert.Contains(t, script, "mktemp") + } + + // Verify /skills volume mount exists + hasSkillsMount := false + for _, vm := range gitInitContainer.VolumeMounts { + if vm.Name == "kagent-skills" && vm.MountPath == "/skills" { + hasSkillsMount = true + } + } + assert.True(t, hasSkillsMount, "git init container should mount kagent-skills volume") + } else { + assert.Nil(t, gitInitContainer, "git-skills-init container should not exist") + } + + if tt.wantOCIInit { + require.NotNil(t, ociInitContainer, "skills-init container should exist") + } else { + assert.Nil(t, ociInitContainer, "skills-init container should not exist") + } + + // Check skills volume exists + hasSkillsVolume := false + for _, v := range deployment.Spec.Template.Spec.Volumes { + if v.Name == "kagent-skills" { + hasSkillsVolume = true + if tt.wantSkillsVolume { + assert.NotNil(t, v.EmptyDir, "kagent-skills should be an EmptyDir volume") + } + } + } + if tt.wantSkillsVolume { + assert.True(t, hasSkillsVolume, "kagent-skills volume should exist") + } else { + assert.False(t, hasSkillsVolume, "kagent-skills volume should not exist") + } + + // Check auth volume + if tt.wantAuthVolume { + hasAuthVolume := false + for _, v := range deployment.Spec.Template.Spec.Volumes { + if v.Secret != nil && v.Name == "git-auth" { + hasAuthVolume = true + assert.Equal(t, "github-token", v.Secret.SecretName, "auth volume should reference the correct secret") + } + } + assert.True(t, hasAuthVolume, "git-auth volume should exist") + + // Verify git init container has auth volume mount + require.NotNil(t, gitInitContainer) + hasAuthMount := false + for _, vm := range gitInitContainer.VolumeMounts { + if vm.Name == "git-auth" && vm.MountPath == "/git-auth" { + hasAuthMount = true + } + } + assert.True(t, hasAuthMount, "git init container should mount auth secret") + + // Verify script contains credential helper setup for all repos + script := gitInitContainer.Command[2] + assert.Contains(t, script, "credential.helper") + } + + // When both exist, verify git init comes before OCI init + if tt.wantGitInit && tt.wantOCIInit { + gitIdx := -1 + ociIdx := -1 + for i, c := range initContainers { + if c.Name == "git-skills-init" { + gitIdx = i + } + if c.Name == "skills-init" { + ociIdx = i + } + } + assert.Less(t, gitIdx, ociIdx, "git init container should come before OCI init container") + } + }) + } +} + +func Test_AdkApiTranslator_GitSkillsConfigurableImage(t *testing.T) { + scheme := schemev1.Scheme + require.NoError(t, v1alpha2.AddToScheme(scheme)) + + namespace := "default" + modelName := "test-model" + + modelConfig := &v1alpha2.ModelConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: modelName, + Namespace: namespace, + }, + Spec: v1alpha2.ModelConfigSpec{ + Model: "gpt-4", + Provider: v1alpha2.ModelProviderOpenAI, + }, + } + + agent := &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "agent-custom-image", Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "test", + ModelConfig: modelName, + }, + Skills: &v1alpha2.SkillForAgent{ + GitRefs: []v1alpha2.GitRepo{ + { + URL: "https://github.com/org/my-skills", + Ref: "main", + }, + }, + }, + }, + } + + // Override the default git init image + originalImage := translator.DefaultGitInitImage + translator.DefaultGitInitImage = "custom-registry/git:latest" + defer func() { translator.DefaultGitInitImage = originalImage }() + + kubeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(modelConfig, agent). + Build() + + defaultModel := types.NamespacedName{ + Namespace: namespace, + Name: modelName, + } + + trans := translator.NewAdkApiTranslator(kubeClient, defaultModel, nil, "") + outputs, err := trans.TranslateAgent(context.Background(), agent) + require.NoError(t, err) + + var deployment *appsv1.Deployment + for _, obj := range outputs.Manifest { + if d, ok := obj.(*appsv1.Deployment); ok { + deployment = d + } + } + require.NotNil(t, deployment) + + var gitInitContainer *corev1.Container + for i := range deployment.Spec.Template.Spec.InitContainers { + if deployment.Spec.Template.Spec.InitContainers[i].Name == "git-skills-init" { + gitInitContainer = &deployment.Spec.Template.Spec.InitContainers[i] + } + } + require.NotNil(t, gitInitContainer) + assert.Equal(t, "custom-registry/git:latest", gitInitContainer.Image) +} diff --git a/go/internal/controller/translator/agent/testdata/inputs/agent_with_git_skills.yaml b/go/internal/controller/translator/agent/testdata/inputs/agent_with_git_skills.yaml new file mode 100644 index 000000000..8a9683ed3 --- /dev/null +++ b/go/internal/controller/translator/agent/testdata/inputs/agent_with_git_skills.yaml @@ -0,0 +1,54 @@ +operation: translateAgent +targetObject: git-skills-agent +namespace: test +objects: + - apiVersion: v1 + kind: Secret + metadata: + name: openai-secret + namespace: test + data: + api-key: c2stdGVzdC1hcGkta2V5 # base64 encoded "sk-test-api-key" + - apiVersion: kagent.dev/v1alpha2 + kind: ModelConfig + metadata: + name: basic-model + namespace: test + spec: + provider: OpenAI + model: gpt-4o + apiKeySecret: openai-secret + apiKeySecretKey: api-key + openAI: + temperature: "0.7" + maxTokens: 1024 + topP: "0.95" + reasoningEffort: "low" + defaultHeaders: + User-Agent: "kagent/1.0" + - apiVersion: kagent.dev/v1alpha2 + kind: Agent + metadata: + name: git-skills-agent + namespace: test + spec: + skills: + refs: + - ghcr.io/org/oci-skill:v1.0 + gitAuthSecretRef: + name: github-token + gitRefs: + - url: https://github.com/org/my-skills + ref: v2.0.0 + path: skills/k8s + name: k8s-skill + - url: https://github.com/org/another-skill + ref: abc123def456abc123def456abc123def456abc1 + - url: https://github.com/org/private-skill + ref: main + type: Declarative + declarative: + description: An agent with git-based skills + systemMessage: You are a helpful assistant with skills from git. + modelConfig: basic-model + tools: [] diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json index af4b9bb0d..c2fd26af3 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json @@ -172,6 +172,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_code.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_code.json index b85b8568f..f09efe9c3 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_code.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_code.json @@ -164,6 +164,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json index 2f4f3b9d8..851e39161 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json @@ -177,6 +177,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json index 59d844720..8d989b8c1 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json @@ -132,6 +132,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json new file mode 100644 index 000000000..79ff61aed --- /dev/null +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json @@ -0,0 +1,368 @@ +{ + "agentCard": { + "capabilities": { + "pushNotifications": false, + "stateTransitionHistory": true, + "streaming": true + }, + "defaultInputModes": [ + "text" + ], + "defaultOutputModes": [ + "text" + ], + "description": "", + "name": "git_skills_agent", + "skills": null, + "url": "http://git-skills-agent.test:8080", + "version": "" + }, + "config": { + "description": "", + "http_tools": null, + "instruction": "You are a helpful assistant with skills from git.", + "model": { + "base_url": "", + "headers": { + "User-Agent": "kagent/1.0" + }, + "max_tokens": 1024, + "model": "gpt-4o", + "reasoning_effort": "low", + "temperature": 0.7, + "top_p": 0.95, + "type": "openai" + }, + "remote_agents": null, + "sse_tools": null, + "stream": false + }, + "manifest": [ + { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "labels": { + "app": "kagent", + "app.kubernetes.io/managed-by": "kagent", + "app.kubernetes.io/name": "git-skills-agent", + "app.kubernetes.io/part-of": "kagent", + "kagent": "git-skills-agent" + }, + "name": "git-skills-agent", + "namespace": "test", + "ownerReferences": [ + { + "apiVersion": "kagent.dev/v1alpha2", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Agent", + "name": "git-skills-agent", + "uid": "" + } + ] + }, + "stringData": { + "agent-card.json": "{\"name\":\"git_skills_agent\",\"description\":\"\",\"url\":\"http://git-skills-agent.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[]}", + "config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"headers\":{\"User-Agent\":\"kagent/1.0\"},\"base_url\":\"\",\"max_tokens\":1024,\"reasoning_effort\":\"low\",\"temperature\":0.7,\"top_p\":0.95},\"description\":\"\",\"instruction\":\"You are a helpful assistant with skills from git.\",\"http_tools\":null,\"sse_tools\":null,\"remote_agents\":null,\"stream\":false}" + } + }, + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "labels": { + "app": "kagent", + "app.kubernetes.io/managed-by": "kagent", + "app.kubernetes.io/name": "git-skills-agent", + "app.kubernetes.io/part-of": "kagent", + "kagent": "git-skills-agent" + }, + "name": "git-skills-agent", + "namespace": "test", + "ownerReferences": [ + { + "apiVersion": "kagent.dev/v1alpha2", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Agent", + "name": "git-skills-agent", + "uid": "" + } + ] + } + }, + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "labels": { + "app": "kagent", + "app.kubernetes.io/managed-by": "kagent", + "app.kubernetes.io/name": "git-skills-agent", + "app.kubernetes.io/part-of": "kagent", + "kagent": "git-skills-agent" + }, + "name": "git-skills-agent", + "namespace": "test", + "ownerReferences": [ + { + "apiVersion": "kagent.dev/v1alpha2", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Agent", + "name": "git-skills-agent", + "uid": "" + } + ] + }, + "spec": { + "selector": { + "matchLabels": { + "app": "kagent", + "kagent": "git-skills-agent" + } + }, + "strategy": { + "rollingUpdate": { + "maxSurge": 1, + "maxUnavailable": 0 + }, + "type": "RollingUpdate" + }, + "template": { + "metadata": { + "annotations": { + "kagent.dev/config-hash": "11835878454410491036" + }, + "labels": { + "app": "kagent", + "app.kubernetes.io/managed-by": "kagent", + "app.kubernetes.io/name": "git-skills-agent", + "app.kubernetes.io/part-of": "kagent", + "kagent": "git-skills-agent" + } + }, + "spec": { + "containers": [ + { + "args": [ + "--host", + "0.0.0.0", + "--port", + "8080", + "--filepath", + "/config" + ], + "env": [ + { + "name": "OPENAI_API_KEY", + "valueFrom": { + "secretKeyRef": { + "key": "api-key", + "name": "openai-secret" + } + } + }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, + { + "name": "KAGENT_NAMESPACE", + "valueFrom": { + "fieldRef": { + "fieldPath": "metadata.namespace" + } + } + }, + { + "name": "KAGENT_NAME", + "value": "git-skills-agent" + }, + { + "name": "KAGENT_URL", + "value": "http://kagent-controller.kagent:8083" + }, + { + "name": "KAGENT_SKILLS_FOLDER", + "value": "/skills" + } + ], + "image": "cr.kagent.dev/kagent-dev/kagent/app:dev", + "imagePullPolicy": "IfNotPresent", + "name": "kagent", + "ports": [ + { + "containerPort": 8080, + "name": "http" + } + ], + "readinessProbe": { + "httpGet": { + "path": "/health", + "port": "http" + }, + "initialDelaySeconds": 15, + "periodSeconds": 15, + "timeoutSeconds": 15 + }, + "resources": { + "limits": { + "cpu": "2", + "memory": "1Gi" + }, + "requests": { + "cpu": "100m", + "memory": "384Mi" + } + }, + "securityContext": { + "privileged": true + }, + "volumeMounts": [ + { + "mountPath": "/config", + "name": "config" + }, + { + "mountPath": "/skills", + "name": "kagent-skills", + "readOnly": true + }, + { + "mountPath": "/var/run/secrets/tokens", + "name": "kagent-token" + } + ] + } + ], + "initContainers": [ + { + "command": [ + "/bin/sh", + "-c", + "set -e\nif [ -f '/git-auth/ssh-privatekey' ]; then\n mkdir -p ~/.ssh\n cp '/git-auth/ssh-privatekey' ~/.ssh/id_rsa\n chmod 600 ~/.ssh/id_rsa\n ssh-keyscan github.com gitlab.com bitbucket.org \u003e\u003e ~/.ssh/known_hosts 2\u003e/dev/null\nelif [ -f '/git-auth/token' ]; then\n git config --global credential.helper '!f() { echo username=x-access-token; echo password=$(cat /git-auth/token); }; f'\nfi\ngit clone --depth 1 --branch 'v2.0.0' -- 'https://github.com/org/my-skills' '/skills/k8s-skill'\n_tmp=\"$(mktemp -d)\"\ncp -a '/skills/k8s-skill/skills/k8s/.' \"$_tmp/\"\nrm -rf '/skills/k8s-skill'\nmv \"$_tmp\" '/skills/k8s-skill'\ngit clone -- 'https://github.com/org/another-skill' '/skills/another-skill'\ncd '/skills/another-skill' \u0026\u0026 git checkout 'abc123def456abc123def456abc123def456abc1'\ngit clone --depth 1 --branch 'main' -- 'https://github.com/org/private-skill' '/skills/private-skill'\n" + ], + "image": "alpine/git:2.47.2", + "name": "git-skills-init", + "resources": {}, + "volumeMounts": [ + { + "mountPath": "/skills", + "name": "kagent-skills" + }, + { + "mountPath": "/git-auth", + "name": "git-auth", + "readOnly": true + } + ] + }, + { + "args": [ + "ghcr.io/org/oci-skill:v1.0" + ], + "command": [ + "kagent-adk", + "pull-skills" + ], + "env": [ + { + "name": "KAGENT_SKILLS_FOLDER", + "value": "/skills" + } + ], + "image": "cr.kagent.dev/kagent-dev/kagent/app:dev", + "name": "skills-init", + "resources": {}, + "volumeMounts": [ + { + "mountPath": "/skills", + "name": "kagent-skills" + } + ] + } + ], + "serviceAccountName": "git-skills-agent", + "volumes": [ + { + "name": "config", + "secret": { + "secretName": "git-skills-agent" + } + }, + { + "emptyDir": {}, + "name": "kagent-skills" + }, + { + "name": "git-auth", + "secret": { + "secretName": "github-token" + } + }, + { + "name": "kagent-token", + "projected": { + "sources": [ + { + "serviceAccountToken": { + "audience": "kagent", + "expirationSeconds": 3600, + "path": "kagent-token" + } + } + ] + } + } + ] + } + } + }, + "status": {} + }, + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "labels": { + "app": "kagent", + "app.kubernetes.io/managed-by": "kagent", + "app.kubernetes.io/name": "git-skills-agent", + "app.kubernetes.io/part-of": "kagent", + "kagent": "git-skills-agent" + }, + "name": "git-skills-agent", + "namespace": "test", + "ownerReferences": [ + { + "apiVersion": "kagent.dev/v1alpha2", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Agent", + "name": "git-skills-agent", + "uid": "" + } + ] + }, + "spec": { + "ports": [ + { + "name": "http", + "port": 8080, + "targetPort": 8080 + } + ], + "selector": { + "app": "kagent", + "kagent": "git-skills-agent" + }, + "type": "ClusterIP" + }, + "status": { + "loadBalancer": {} + } + } + ] +} \ No newline at end of file diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json index b12c1c08d..80e6d08c1 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json @@ -171,6 +171,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json index 56a95c5f2..f4ff634a9 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json @@ -167,6 +167,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json index 1997b15d7..bfb2b49d2 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json @@ -165,6 +165,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json index 25f06b530..08736491e 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json @@ -150,6 +150,10 @@ "/config" ], "env": [ + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json index 956788548..7f4dc6b48 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json @@ -177,6 +177,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json index b24353a57..1b6fc0bff 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json @@ -167,6 +167,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json index f79d0ba3a..4fbe6d675 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json @@ -170,6 +170,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json index 7ab34c958..7a538d368 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json @@ -170,6 +170,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json index f5aa441b9..5aa429ed0 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json @@ -169,6 +169,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json index 5d40e3304..2aae9c6fb 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json @@ -183,6 +183,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json index f86bd478f..75eb75b67 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json @@ -164,6 +164,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json index 11ea121b9..8dc3a3241 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json @@ -164,6 +164,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json index 79ba274cf..b47dbf836 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json @@ -164,6 +164,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json index 43ab1181a..b030a9a79 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json @@ -157,6 +157,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json index b1790f8c0..59b6075c0 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json @@ -157,6 +157,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json b/go/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json index dfcd39d08..cf1cf0241 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json @@ -157,6 +157,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/basic_agent.json b/go/internal/controller/translator/agent/testdata/outputs/basic_agent.json index 7fc2d68a6..9d4bd4181 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/basic_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/basic_agent.json @@ -164,6 +164,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json b/go/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json index 186caae07..adc09773f 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json @@ -170,6 +170,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/ollama_agent.json b/go/internal/controller/translator/agent/testdata/outputs/ollama_agent.json index 24370b227..8d8405268 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/ollama_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/ollama_agent.json @@ -159,6 +159,10 @@ "name": "OLLAMA_API_BASE", "value": "http://localhost:11434" }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json b/go/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json index d2a8c00eb..d3df897df 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json +++ b/go/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json @@ -162,6 +162,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json b/go/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json index 841374b8c..cbac6f420 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json +++ b/go/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json @@ -161,6 +161,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json b/go/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json index 1a0dbe1b6..9e0122215 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json +++ b/go/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json @@ -162,6 +162,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/pkg/app/app.go b/go/pkg/app/app.go index 5387956a7..b0f27737b 100644 --- a/go/pkg/app/app.go +++ b/go/pkg/app/app.go @@ -170,6 +170,7 @@ func (cfg *Config) SetFlags(commandLine *flag.FlagSet) { 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.") + commandLine.StringVar(&agent_translator.DefaultGitInitImage, "git-init-image", agent_translator.DefaultGitInitImage, "The image to use for the git skills init container.") } // LoadFromEnv loads configuration values from environment variables. diff --git a/helm/kagent/templates/controller-configmap.yaml b/helm/kagent/templates/controller-configmap.yaml index 531ba55ff..bd74b8a4d 100644 --- a/helm/kagent/templates/controller-configmap.yaml +++ b/helm/kagent/templates/controller-configmap.yaml @@ -17,6 +17,7 @@ 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 }} + GIT_INIT_IMAGE: {{ .Values.controller.gitInitImage | 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 c6bd29397..917b512b1 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -72,6 +72,8 @@ controller: repository: kagent-dev/kagent/app tag: "" # Will default to global, then Chart version pullPolicy: "" + # -- The image used by the git-skills-init container to clone skills from Git repositories. + gitInitImage: "alpine/git:2.47.2" streaming: # Streaming buffer size for A2A communication maxBufSize: 1Mi # 1024 * 1024 initialBufSize: 4Ki # 4 * 1024 From 14031476e6ce12bd2a18924219929874c9c479bf Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Mon, 23 Feb 2026 18:07:22 +0000 Subject: [PATCH 02/10] feat: unify skills init containers into a single lightweight image Replace two separate init containers (git-skills-init using alpine/git and skills-init using the full ~1GB kagent runtime image) with a single skills-init container using a new lightweight image (~30MB) containing only git and krane. OCI skill pulling now uses krane directly in a shell script instead of shelling out through Python, eliminating the need to pull the full runtime image just to run a Go binary. - Add docker/skills-init/Dockerfile (alpine + git + krane) - Add unified skills-init.sh.tmpl handling both git and OCI skills - Merge buildGitSkillsInitContainer and OCI init into buildSkillsInitContainer - Add ImageConfig.Image() method and DefaultSkillsInitImageConfig - Add ociSkillName() helper for extracting skill names from OCI refs - Rename git-init-image flag/config to structured skills-init-image-* - Add build-skills-init target to Makefile - Regenerate CRDs and golden test files Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Eitan Yarmush --- Makefile | 13 +- docker/skills-init/Dockerfile | 19 ++ go/config/crd/bases/kagent.dev_agents.yaml | 46 ++++- .../translator/agent/adk_api_translator.go | 184 +++++++++++------- .../translator/agent/git_skills_test.go | 169 ++++++++-------- .../{git-init.sh.tmpl => skills-init.sh.tmpl} | 20 +- .../outputs/agent_with_allowed_headers.json | 4 - .../testdata/outputs/agent_with_code.json | 4 - .../agent_with_cross_namespace_tools.json | 4 - .../outputs/agent_with_custom_sa.json | 4 - .../outputs/agent_with_git_skills.json | 34 +--- .../outputs/agent_with_http_toolserver.json | 4 - .../outputs/agent_with_mcp_service.json | 4 - .../outputs/agent_with_nested_agent.json | 4 - .../outputs/agent_with_passthrough.json | 4 - .../testdata/outputs/agent_with_proxy.json | 4 - .../agent_with_proxy_external_remotemcp.json | 4 - .../outputs/agent_with_proxy_mcpserver.json | 4 - ...t_with_proxy_mcpserver_custom_timeout.json | 4 - .../outputs/agent_with_proxy_service.json | 4 - .../agent_with_scheduling_attributes.json | 4 - .../outputs/agent_with_security_context.json | 4 - .../testdata/outputs/agent_with_skills.json | 20 +- .../outputs/agent_with_streaming.json | 4 - ...nt_with_system_message_from_configmap.json | 4 - ...agent_with_system_message_from_secret.json | 4 - .../testdata/outputs/anthropic_agent.json | 4 - .../agent/testdata/outputs/basic_agent.json | 4 - .../agent/testdata/outputs/bedrock_agent.json | 4 - .../agent/testdata/outputs/ollama_agent.json | 4 - .../testdata/outputs/tls-with-custom-ca.json | 4 - .../outputs/tls-with-disabled-verify.json | 4 - .../outputs/tls-with-system-cas-disabled.json | 4 - go/pkg/app/app.go | 5 +- .../templates/kagent.dev_agents.yaml | 46 ++++- .../templates/controller-configmap.yaml | 5 +- helm/kagent/values.yaml | 8 +- 37 files changed, 347 insertions(+), 322 deletions(-) create mode 100644 docker/skills-init/Dockerfile rename go/internal/controller/translator/agent/{git-init.sh.tmpl => skills-init.sh.tmpl} (54%) diff --git a/Makefile b/Makefile index 2ca008f98..4cf354e18 100644 --- a/Makefile +++ b/Makefile @@ -35,16 +35,19 @@ CONTROLLER_IMAGE_NAME ?= controller UI_IMAGE_NAME ?= ui APP_IMAGE_NAME ?= app KAGENT_ADK_IMAGE_NAME ?= kagent-adk +SKILLS_INIT_IMAGE_NAME ?= skills-init CONTROLLER_IMAGE_TAG ?= $(VERSION) UI_IMAGE_TAG ?= $(VERSION) APP_IMAGE_TAG ?= $(VERSION) KAGENT_ADK_IMAGE_TAG ?= $(VERSION) +SKILLS_INIT_IMAGE_TAG ?= $(VERSION) CONTROLLER_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(CONTROLLER_IMAGE_NAME):$(CONTROLLER_IMAGE_TAG) UI_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(UI_IMAGE_NAME):$(UI_IMAGE_TAG) APP_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(APP_IMAGE_NAME):$(APP_IMAGE_TAG) KAGENT_ADK_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(KAGENT_ADK_IMAGE_NAME):$(KAGENT_ADK_IMAGE_TAG) +SKILLS_INIT_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(SKILLS_INIT_IMAGE_NAME):$(SKILLS_INIT_IMAGE_TAG) #take from go/go.mod AWK ?= $(shell command -v gawk || command -v awk) @@ -211,13 +214,13 @@ prune-docker-images: docker images --filter dangling=true -q | xargs -r docker rmi || : .PHONY: build -build: buildx-create build-controller build-ui build-app +build: buildx-create build-controller build-ui build-app build-skills-init @echo "Build completed successfully." @echo "Controller Image: $(CONTROLLER_IMG)" @echo "UI Image: $(UI_IMG)" @echo "App Image: $(APP_IMG)" @echo "Kagent ADK Image: $(KAGENT_ADK_IMG)" - @echo "Tools Image: $(TOOLS_IMG)" + @echo "Skills Init Image: $(SKILLS_INIT_IMG)" .PHONY: build-monitor build-monitor: buildx-create @@ -245,7 +248,7 @@ lint: make -C python lint .PHONY: push -push: push-controller push-ui push-app push-kagent-adk +push: push-controller push-ui push-app push-kagent-adk push-skills-init .PHONY: controller-manifests controller-manifests: @@ -268,6 +271,10 @@ build-kagent-adk: buildx-create build-app: buildx-create build-kagent-adk $(DOCKER_BUILDER) build $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) --build-arg KAGENT_ADK_VERSION=$(KAGENT_ADK_IMAGE_TAG) --build-arg DOCKER_REGISTRY=$(DOCKER_REGISTRY) -t $(APP_IMG) -f python/Dockerfile.app ./python +.PHONY: build-skills-init +build-skills-init: buildx-create + $(DOCKER_BUILDER) build $(DOCKER_BUILD_ARGS) -t $(SKILLS_INIT_IMG) -f docker/skills-init/Dockerfile docker/skills-init + .PHONY: helm-cleanup helm-cleanup: rm -f ./$(HELM_DIST_FOLDER)/*.tgz diff --git a/docker/skills-init/Dockerfile b/docker/skills-init/Dockerfile new file mode 100644 index 000000000..4da6ad5bd --- /dev/null +++ b/docker/skills-init/Dockerfile @@ -0,0 +1,19 @@ +### Stage 0: build krane +FROM golang:1.25-alpine AS krane-builder + +ENV KRANE_VERSION=v0.20.7 +WORKDIR /build + +RUN apk add --no-cache git && \ + git clone --depth 1 --branch $KRANE_VERSION \ + https://github.com/google/go-containerregistry.git + +WORKDIR /build/go-containerregistry/cmd/krane + +RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /build/krane . + +### Stage 1: runtime +FROM alpine:3.21 + +RUN apk add --no-cache git +COPY --from=krane-builder /build/krane /usr/local/bin/krane diff --git a/go/config/crd/bases/kagent.dev_agents.yaml b/go/config/crd/bases/kagent.dev_agents.yaml index 418225433..a29e879b4 100644 --- a/go/config/crd/bases/kagent.dev_agents.yaml +++ b/go/config/crd/bases/kagent.dev_agents.yaml @@ -10027,6 +10027,51 @@ spec: Skills to load into the agent. They will be pulled from the specified container images. and made available to the agent under the `/skills` folder. properties: + gitAuthSecretRef: + description: |- + Reference to a Secret containing git credentials. + Applied to all gitRefs entries. + The secret should contain a `token` key for HTTPS auth, + or `ssh-privatekey` for SSH auth. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + gitRefs: + description: Git repositories to fetch skills from. + items: + description: GitRepo specifies a single Git repository to fetch + skills from. + properties: + name: + description: Name for the skill directory under /skills. + Defaults to the repo name. + type: string + path: + description: Subdirectory within the repo to use as the + skill root. + type: string + ref: + default: main + description: 'Git reference: branch name, tag, or commit + SHA.' + type: string + url: + description: URL of the git repository (HTTPS or SSH). + type: string + required: + - url + type: object + maxItems: 20 + type: array insecureSkipVerify: description: |- Fetch images insecurely from registries (allowing HTTP and skipping TLS verification). @@ -10037,7 +10082,6 @@ spec: items: type: string maxItems: 20 - minItems: 1 type: array type: object type: diff --git a/go/internal/controller/translator/agent/adk_api_translator.go b/go/internal/controller/translator/agent/adk_api_translator.go index e5c920216..312aebccb 100644 --- a/go/internal/controller/translator/agent/adk_api_translator.go +++ b/go/internal/controller/translator/agent/adk_api_translator.go @@ -85,6 +85,11 @@ type ImageConfig struct { Repository string `json:"repository,omitempty"` } +// Image returns the fully qualified image reference (registry/repository:tag). +func (c ImageConfig) Image() string { + return fmt.Sprintf("%s/%s:%s", c.Registry, c.Repository, c.Tag) +} + var DefaultImageConfig = ImageConfig{ Registry: "cr.kagent.dev", Tag: version.Get().Version, @@ -93,10 +98,14 @@ var DefaultImageConfig = ImageConfig{ Repository: "kagent-dev/kagent/app", } -// DefaultGitInitImage is the image used by the git-skills-init container to clone -// skill repositories from Git. Configurable via the --git-init-image flag or the -// GIT_INIT_IMAGE environment variable. -var DefaultGitInitImage = "alpine/git:2.47.2" +// DefaultSkillsInitImageConfig is the image config for the skills-init container +// that clones skill repositories from Git and pulls OCI skill images. +var DefaultSkillsInitImageConfig = ImageConfig{ + Registry: "cr.kagent.dev", + Tag: version.Get().Version, + PullPolicy: string(corev1.PullIfNotPresent), + Repository: "kagent-dev/kagent/skills-init", +} // TODO(ilackarms): migrate this whole package to pkg/translator type AgentOutputs = translator.AgentOutputs @@ -418,38 +427,15 @@ func (a *adkApiTranslator) buildManifest( sharedEnv = append(sharedEnv, skillsEnv) } - // Git-based skills init container (runs before OCI skills init) - if len(gitRefs) > 0 { - gitContainer, gitVolumes, _ := buildGitSkillsInitContainer(gitRefs, gitAuthSecretRef, dep.SecurityContext) - initContainers = append(initContainers, gitContainer) - volumes = append(volumes, gitVolumes...) - } - - // OCI-based skills init container - if len(skills) > 0 { - insecure := agent.Spec.Skills.InsecureSkipVerify - command := []string{"kagent-adk", "pull-skills"} - if insecure { - command = append(command, "--insecure") - } - initContainerSecurityContext := dep.SecurityContext - if initContainerSecurityContext != nil { - initContainerSecurityContext = initContainerSecurityContext.DeepCopy() + // Unified skills init container (handles both git and OCI skills) + if hasSkills { + insecure := agent.Spec.Skills != nil && agent.Spec.Skills.InsecureSkipVerify + container, skillsVolumes, err := buildSkillsInitContainer(gitRefs, gitAuthSecretRef, skills, insecure, dep.SecurityContext) + if err != nil { + return nil, fmt.Errorf("failed to build skills init container: %w", err) } - initContainers = append(initContainers, corev1.Container{ - Name: "skills-init", - Image: dep.Image, - Command: command, - Args: skills, - VolumeMounts: []corev1.VolumeMount{ - {Name: "kagent-skills", MountPath: "/skills"}, - }, - Env: []corev1.EnvVar{{ - Name: env.KagentSkillsFolder.Name(), - Value: "/skills", - }}, - SecurityContext: initContainerSecurityContext, - }) + initContainers = append(initContainers, container) + volumes = append(volumes, skillsVolumes...) } // Token volume @@ -1395,67 +1381,117 @@ func gitSkillName(ref v1alpha2.GitRepo) string { return path.Base(u) } -// gitSkillRefData holds pre-computed fields for each git skill ref, used by the script template. -type gitSkillRefData struct { - v1alpha2.GitRepo - Dest string // e.g. /skills/my-skill - IsCommit bool // true if Ref is a 40-char hex SHA - SubPath string // Path with trailing slash stripped - MountPath string // e.g. /git-auth (empty if no auth) +// skillsInitData holds the template data for the unified skills-init script. +type skillsInitData struct { + AuthMountPath string // "/git-auth" or "" (for git auth) + GitRefs []gitRefData // git repos to clone + OCIRefs []ociRefData // OCI images to pull + InsecureOCI bool // --insecure flag for krane } -//go:embed git-init.sh.tmpl -var gitInitScriptTmpl string +// gitRefData holds pre-computed fields for each git skill ref, used by the script template. +type gitRefData struct { + URL string + Ref string + Dest string // e.g. /skills/my-skill + IsCommit bool // true if Ref is a 40-char hex SHA + SubPath string // Path with trailing slash stripped +} -// gitSkillsScriptTemplate is the shell script template for cloning skills from Git. -var gitSkillsScriptTemplate = template.Must(template.New("git-skills").Parse(gitInitScriptTmpl)) +// ociRefData holds pre-computed fields for each OCI skill ref, used by the script template. +type ociRefData struct { + Image string // full image ref e.g. ghcr.io/org/skill:v1 + Dest string // /skills/ +} + +//go:embed skills-init.sh.tmpl +var skillsInitScriptTmpl string -// buildGitSkillsScript builds the shell script that the git-skills-init container runs -// by rendering the gitSkillsScriptTemplate with pre-computed data for each ref. -func buildGitSkillsScript(refs []gitSkillRefData) string { +// skillsScriptTemplate is the shell script template for fetching skills from Git and OCI. +var skillsScriptTemplate = template.Must(template.New("skills-init").Parse(skillsInitScriptTmpl)) + +// buildSkillsScript renders the unified skills-init shell script. +func buildSkillsScript(data skillsInitData) (string, error) { var buf bytes.Buffer - if err := gitSkillsScriptTemplate.Execute(&buf, refs); err != nil { - // Template is statically defined; this should never fail at runtime. - panic(fmt.Sprintf("failed to render git skills script: %v", err)) + if err := skillsScriptTemplate.Execute(&buf, data); err != nil { + return "", fmt.Errorf("failed to render skills init script: %w", err) + } + return buf.String(), nil +} + +// ociSkillName extracts a skill directory name from an OCI image reference. +// It takes the last path component of the repo (stripped of tag/digest). +func ociSkillName(imageRef string) string { + ref := imageRef + // Strip digest + if i := strings.LastIndex(ref, "@"); i != -1 { + ref = ref[:i] + } + // Strip tag (colon after the last slash is a tag, not a port) + if i := strings.LastIndex(ref, ":"); i != -1 { + if j := strings.LastIndex(ref, "/"); i > j { + ref = ref[:i] + } } - return buf.String() + return path.Base(ref) } -// prepareGitSkillRefs converts GitRepo CRD values to the template-ready data structs. -// If authSecretRef is non-nil, every entry gets a shared MountPath pointing to the -// single auth volume. -func prepareGitSkillRefs(gitRefs []v1alpha2.GitRepo, authSecretRef *corev1.LocalObjectReference) []gitSkillRefData { - data := make([]gitSkillRefData, len(gitRefs)) - for i, ref := range gitRefs { +// prepareSkillsInitData converts CRD values to the template-ready data struct. +func prepareSkillsInitData( + gitRefs []v1alpha2.GitRepo, + authSecretRef *corev1.LocalObjectReference, + ociRefs []string, + insecureOCI bool, +) skillsInitData { + data := skillsInitData{ + InsecureOCI: insecureOCI, + } + + if authSecretRef != nil { + data.AuthMountPath = "/git-auth" + } + + for _, ref := range gitRefs { gitRef := ref.Ref if gitRef == "" { gitRef = "main" } ref.Ref = gitRef - data[i] = gitSkillRefData{ - GitRepo: ref, - Dest: "/skills/" + gitSkillName(ref), + data.GitRefs = append(data.GitRefs, gitRefData{ + URL: ref.URL, + Ref: gitRef, + Dest: "/skills/" + gitSkillName(ref), IsCommit: isCommitSHA(gitRef), SubPath: strings.TrimSuffix(ref.Path, "/"), - } - if authSecretRef != nil { - data[i].MountPath = "/git-auth" - } + }) } + + for _, imageRef := range ociRefs { + data.OCIRefs = append(data.OCIRefs, ociRefData{ + Image: imageRef, + Dest: "/skills/" + ociSkillName(imageRef), + }) + } + return data } -// buildGitSkillsInitContainer creates the init container and associated volumes/mounts -// for cloning skills from Git repositories. +// buildSkillsInitContainer creates the unified init container and associated volumes +// for fetching skills from both Git repositories and OCI registries. // If authSecretRef is non-nil a single Secret volume is created and mounted at /git-auth. -func buildGitSkillsInitContainer( +func buildSkillsInitContainer( gitRefs []v1alpha2.GitRepo, authSecretRef *corev1.LocalObjectReference, + ociRefs []string, + insecureOCI bool, securityContext *corev1.SecurityContext, -) (container corev1.Container, volumes []corev1.Volume, secretVolumeMounts []corev1.VolumeMount) { - refData := prepareGitSkillRefs(gitRefs, authSecretRef) - script := buildGitSkillsScript(refData) +) (container corev1.Container, volumes []corev1.Volume, err error) { + data := prepareSkillsInitData(gitRefs, authSecretRef, ociRefs, insecureOCI) + script, err := buildSkillsScript(data) + if err != nil { + return corev1.Container{}, nil, err + } initSecCtx := securityContext if initSecCtx != nil { @@ -1484,8 +1520,8 @@ func buildGitSkillsInitContainer( } container = corev1.Container{ - Name: "git-skills-init", - Image: DefaultGitInitImage, + Name: "skills-init", + Image: DefaultSkillsInitImageConfig.Image(), Command: []string{"/bin/sh", "-c", script}, VolumeMounts: volumeMounts, SecurityContext: initSecCtx, diff --git a/go/internal/controller/translator/agent/git_skills_test.go b/go/internal/controller/translator/agent/git_skills_test.go index 891925d8a..2ec6ac468 100644 --- a/go/internal/controller/translator/agent/git_skills_test.go +++ b/go/internal/controller/translator/agent/git_skills_test.go @@ -17,7 +17,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -func Test_AdkApiTranslator_GitSkills(t *testing.T) { +func Test_AdkApiTranslator_Skills(t *testing.T) { scheme := schemev1.Scheme require.NoError(t, v1alpha2.AddToScheme(scheme)) @@ -44,13 +44,12 @@ func Test_AdkApiTranslator_GitSkills(t *testing.T) { name string agent *v1alpha2.Agent // assertions - wantGitInit bool - wantOCIInit bool + wantSkillsInit bool wantSkillsVolume bool - wantGitImage string wantContainsBranch string wantContainsCommit string wantContainsPath string + wantContainsKrane bool wantAuthVolume bool }{ { @@ -65,12 +64,11 @@ func Test_AdkApiTranslator_GitSkills(t *testing.T) { }, }, }, - wantGitInit: false, - wantOCIInit: false, + wantSkillsInit: false, wantSkillsVolume: false, }, { - name: "only OCI skills - no git init container", + name: "only OCI skills - unified init container with krane", agent: &v1alpha2.Agent{ ObjectMeta: metav1.ObjectMeta{Name: "agent-oci-only", Namespace: namespace}, Spec: v1alpha2.AgentSpec{ @@ -84,12 +82,12 @@ func Test_AdkApiTranslator_GitSkills(t *testing.T) { }, }, }, - wantGitInit: false, - wantOCIInit: true, - wantSkillsVolume: true, + wantSkillsInit: true, + wantSkillsVolume: true, + wantContainsKrane: true, }, { - name: "only git skills - git init container, no OCI init", + name: "only git skills - unified init container with git clone", agent: &v1alpha2.Agent{ ObjectMeta: metav1.ObjectMeta{Name: "agent-git-only", Namespace: namespace}, Spec: v1alpha2.AgentSpec{ @@ -108,14 +106,12 @@ func Test_AdkApiTranslator_GitSkills(t *testing.T) { }, }, }, - wantGitInit: true, - wantOCIInit: false, + wantSkillsInit: true, wantSkillsVolume: true, - wantGitImage: "alpine/git:2.47.2", wantContainsBranch: "v1.0.0", }, { - name: "both OCI and git skills - both init containers", + name: "both OCI and git skills - single unified init container", agent: &v1alpha2.Agent{ ObjectMeta: metav1.ObjectMeta{Name: "agent-both", Namespace: namespace}, Spec: v1alpha2.AgentSpec{ @@ -135,9 +131,9 @@ func Test_AdkApiTranslator_GitSkills(t *testing.T) { }, }, }, - wantGitInit: true, - wantOCIInit: true, - wantSkillsVolume: true, + wantSkillsInit: true, + wantSkillsVolume: true, + wantContainsKrane: true, }, { name: "git skill with commit SHA", @@ -159,8 +155,7 @@ func Test_AdkApiTranslator_GitSkills(t *testing.T) { }, }, }, - wantGitInit: true, - wantOCIInit: false, + wantSkillsInit: true, wantSkillsVolume: true, wantContainsCommit: "abc123def456abc123def456abc123def456abc1", }, @@ -185,10 +180,9 @@ func Test_AdkApiTranslator_GitSkills(t *testing.T) { }, }, }, - wantGitInit: true, - wantOCIInit: false, - wantSkillsVolume: true, - wantContainsPath: "skills/k8s", + wantSkillsInit: true, + wantSkillsVolume: true, + wantContainsPath: "skills/k8s", }, { name: "git skills with shared auth secret", @@ -217,8 +211,7 @@ func Test_AdkApiTranslator_GitSkills(t *testing.T) { }, }, }, - wantGitInit: true, - wantOCIInit: false, + wantSkillsInit: true, wantSkillsVolume: true, wantAuthVolume: true, }, @@ -243,10 +236,29 @@ func Test_AdkApiTranslator_GitSkills(t *testing.T) { }, }, }, - wantGitInit: true, - wantOCIInit: false, + wantSkillsInit: true, wantSkillsVolume: true, }, + { + name: "OCI skills with insecure flag", + agent: &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{Name: "agent-insecure", Namespace: namespace}, + Spec: v1alpha2.AgentSpec{ + Type: v1alpha2.AgentType_Declarative, + Declarative: &v1alpha2.DeclarativeAgentSpec{ + SystemMessage: "test", + ModelConfig: modelName, + }, + Skills: &v1alpha2.SkillForAgent{ + InsecureSkipVerify: true, + Refs: []string{"localhost:5000/skill:dev"}, + }, + }, + }, + wantSkillsInit: true, + wantSkillsVolume: true, + wantContainsKrane: true, + }, } for _, tt := range tests { @@ -273,30 +285,24 @@ func Test_AdkApiTranslator_GitSkills(t *testing.T) { initContainers := deployment.Spec.Template.Spec.InitContainers - // Check git init container - var gitInitContainer *corev1.Container - var ociInitContainer *corev1.Container + // Find the unified skills-init container + var skillsInitContainer *corev1.Container for i := range initContainers { - switch initContainers[i].Name { - case "git-skills-init": - gitInitContainer = &initContainers[i] - case "skills-init": - ociInitContainer = &initContainers[i] + if initContainers[i].Name == "skills-init" { + skillsInitContainer = &initContainers[i] } } - if tt.wantGitInit { - require.NotNil(t, gitInitContainer, "git-skills-init container should exist") - - if tt.wantGitImage != "" { - assert.Equal(t, tt.wantGitImage, gitInitContainer.Image) - } + if tt.wantSkillsInit { + require.NotNil(t, skillsInitContainer, "skills-init container should exist") + // There should be exactly one init container + assert.Len(t, initContainers, 1, "should have exactly one init container") // Verify the script is passed via /bin/sh -c - require.Len(t, gitInitContainer.Command, 3) - assert.Equal(t, "/bin/sh", gitInitContainer.Command[0]) - assert.Equal(t, "-c", gitInitContainer.Command[1]) - script := gitInitContainer.Command[2] + require.Len(t, skillsInitContainer.Command, 3) + assert.Equal(t, "/bin/sh", skillsInitContainer.Command[0]) + assert.Equal(t, "-c", skillsInitContainer.Command[1]) + script := skillsInitContainer.Command[2] if tt.wantContainsBranch != "" { assert.Contains(t, script, tt.wantContainsBranch) @@ -313,22 +319,21 @@ func Test_AdkApiTranslator_GitSkills(t *testing.T) { assert.Contains(t, script, "mktemp") } + if tt.wantContainsKrane { + assert.Contains(t, script, "krane export") + } + // Verify /skills volume mount exists hasSkillsMount := false - for _, vm := range gitInitContainer.VolumeMounts { + for _, vm := range skillsInitContainer.VolumeMounts { if vm.Name == "kagent-skills" && vm.MountPath == "/skills" { hasSkillsMount = true } } - assert.True(t, hasSkillsMount, "git init container should mount kagent-skills volume") + assert.True(t, hasSkillsMount, "skills-init container should mount kagent-skills volume") } else { - assert.Nil(t, gitInitContainer, "git-skills-init container should not exist") - } - - if tt.wantOCIInit { - require.NotNil(t, ociInitContainer, "skills-init container should exist") - } else { - assert.Nil(t, ociInitContainer, "skills-init container should not exist") + assert.Nil(t, skillsInitContainer, "skills-init container should not exist") + assert.Empty(t, initContainers, "should have no init containers") } // Check skills volume exists @@ -358,40 +363,32 @@ func Test_AdkApiTranslator_GitSkills(t *testing.T) { } assert.True(t, hasAuthVolume, "git-auth volume should exist") - // Verify git init container has auth volume mount - require.NotNil(t, gitInitContainer) + // Verify skills-init container has auth volume mount + require.NotNil(t, skillsInitContainer) hasAuthMount := false - for _, vm := range gitInitContainer.VolumeMounts { + for _, vm := range skillsInitContainer.VolumeMounts { if vm.Name == "git-auth" && vm.MountPath == "/git-auth" { hasAuthMount = true } } - assert.True(t, hasAuthMount, "git init container should mount auth secret") + assert.True(t, hasAuthMount, "skills-init container should mount auth secret") - // Verify script contains credential helper setup for all repos - script := gitInitContainer.Command[2] + // Verify script contains credential helper setup + script := skillsInitContainer.Command[2] assert.Contains(t, script, "credential.helper") } - // When both exist, verify git init comes before OCI init - if tt.wantGitInit && tt.wantOCIInit { - gitIdx := -1 - ociIdx := -1 - for i, c := range initContainers { - if c.Name == "git-skills-init" { - gitIdx = i - } - if c.Name == "skills-init" { - ociIdx = i - } - } - assert.Less(t, gitIdx, ociIdx, "git init container should come before OCI init container") + // Verify insecure flag for OCI skills + if tt.agent.Spec.Skills != nil && tt.agent.Spec.Skills.InsecureSkipVerify { + require.NotNil(t, skillsInitContainer) + script := skillsInitContainer.Command[2] + assert.Contains(t, script, "--insecure") } }) } } -func Test_AdkApiTranslator_GitSkillsConfigurableImage(t *testing.T) { +func Test_AdkApiTranslator_SkillsConfigurableImage(t *testing.T) { scheme := schemev1.Scheme require.NoError(t, v1alpha2.AddToScheme(scheme)) @@ -428,10 +425,14 @@ func Test_AdkApiTranslator_GitSkillsConfigurableImage(t *testing.T) { }, } - // Override the default git init image - originalImage := translator.DefaultGitInitImage - translator.DefaultGitInitImage = "custom-registry/git:latest" - defer func() { translator.DefaultGitInitImage = originalImage }() + // Override the default skills init image config + originalConfig := translator.DefaultSkillsInitImageConfig + translator.DefaultSkillsInitImageConfig = translator.ImageConfig{ + Registry: "custom-registry", + Repository: "skills-init", + Tag: "latest", + } + defer func() { translator.DefaultSkillsInitImageConfig = originalConfig }() kubeClient := fake.NewClientBuilder(). WithScheme(scheme). @@ -455,12 +456,12 @@ func Test_AdkApiTranslator_GitSkillsConfigurableImage(t *testing.T) { } require.NotNil(t, deployment) - var gitInitContainer *corev1.Container + var skillsInitContainer *corev1.Container for i := range deployment.Spec.Template.Spec.InitContainers { - if deployment.Spec.Template.Spec.InitContainers[i].Name == "git-skills-init" { - gitInitContainer = &deployment.Spec.Template.Spec.InitContainers[i] + if deployment.Spec.Template.Spec.InitContainers[i].Name == "skills-init" { + skillsInitContainer = &deployment.Spec.Template.Spec.InitContainers[i] } } - require.NotNil(t, gitInitContainer) - assert.Equal(t, "custom-registry/git:latest", gitInitContainer.Image) + require.NotNil(t, skillsInitContainer) + assert.Equal(t, "custom-registry/skills-init:latest", skillsInitContainer.Image) } diff --git a/go/internal/controller/translator/agent/git-init.sh.tmpl b/go/internal/controller/translator/agent/skills-init.sh.tmpl similarity index 54% rename from go/internal/controller/translator/agent/git-init.sh.tmpl rename to go/internal/controller/translator/agent/skills-init.sh.tmpl index fbc6afbcf..3155f3ecb 100644 --- a/go/internal/controller/translator/agent/git-init.sh.tmpl +++ b/go/internal/controller/translator/agent/skills-init.sh.tmpl @@ -1,17 +1,15 @@ set -e -{{- with index . 0 }} -{{- if .MountPath }} -if [ -f '{{ .MountPath }}/ssh-privatekey' ]; then +{{- if .AuthMountPath }} +if [ -f '{{ .AuthMountPath }}/ssh-privatekey' ]; then mkdir -p ~/.ssh - cp '{{ .MountPath }}/ssh-privatekey' ~/.ssh/id_rsa + cp '{{ .AuthMountPath }}/ssh-privatekey' ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan github.com gitlab.com bitbucket.org >> ~/.ssh/known_hosts 2>/dev/null -elif [ -f '{{ .MountPath }}/token' ]; then - git config --global credential.helper '!f() { echo username=x-access-token; echo password=$(cat {{ .MountPath }}/token); }; f' +elif [ -f '{{ .AuthMountPath }}/token' ]; then + git config --global credential.helper '!f() { echo username=x-access-token; echo password=$(cat {{ .AuthMountPath }}/token); }; f' fi {{- end }} -{{- end }} -{{- range . }} +{{- range .GitRefs }} {{- if .IsCommit }} git clone -- '{{ .URL }}' '{{ .Dest }}' cd '{{ .Dest }}' && git checkout '{{ .Ref }}' @@ -25,3 +23,9 @@ rm -rf '{{ .Dest }}' mv "$_tmp" '{{ .Dest }}' {{- end }} {{- end }} +{{- range .OCIRefs }} +krane export{{ if $.InsecureOCI }} --insecure{{ end }} '{{ .Image }}' '/tmp/oci-skill.tar' +mkdir -p '{{ .Dest }}' +tar xf '/tmp/oci-skill.tar' -C '{{ .Dest }}' +rm -f '/tmp/oci-skill.tar' +{{- end }} diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json index c2fd26af3..af4b9bb0d 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json @@ -172,10 +172,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_code.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_code.json index f09efe9c3..b85b8568f 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_code.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_code.json @@ -164,10 +164,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json index 851e39161..2f4f3b9d8 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json @@ -177,10 +177,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json index 8d989b8c1..59d844720 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json @@ -132,10 +132,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json index 79ff61aed..272fa5de7 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json @@ -164,10 +164,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { @@ -242,10 +238,10 @@ "command": [ "/bin/sh", "-c", - "set -e\nif [ -f '/git-auth/ssh-privatekey' ]; then\n mkdir -p ~/.ssh\n cp '/git-auth/ssh-privatekey' ~/.ssh/id_rsa\n chmod 600 ~/.ssh/id_rsa\n ssh-keyscan github.com gitlab.com bitbucket.org \u003e\u003e ~/.ssh/known_hosts 2\u003e/dev/null\nelif [ -f '/git-auth/token' ]; then\n git config --global credential.helper '!f() { echo username=x-access-token; echo password=$(cat /git-auth/token); }; f'\nfi\ngit clone --depth 1 --branch 'v2.0.0' -- 'https://github.com/org/my-skills' '/skills/k8s-skill'\n_tmp=\"$(mktemp -d)\"\ncp -a '/skills/k8s-skill/skills/k8s/.' \"$_tmp/\"\nrm -rf '/skills/k8s-skill'\nmv \"$_tmp\" '/skills/k8s-skill'\ngit clone -- 'https://github.com/org/another-skill' '/skills/another-skill'\ncd '/skills/another-skill' \u0026\u0026 git checkout 'abc123def456abc123def456abc123def456abc1'\ngit clone --depth 1 --branch 'main' -- 'https://github.com/org/private-skill' '/skills/private-skill'\n" + "set -e\nif [ -f '/git-auth/ssh-privatekey' ]; then\n mkdir -p ~/.ssh\n cp '/git-auth/ssh-privatekey' ~/.ssh/id_rsa\n chmod 600 ~/.ssh/id_rsa\n ssh-keyscan github.com gitlab.com bitbucket.org \u003e\u003e ~/.ssh/known_hosts 2\u003e/dev/null\nelif [ -f '/git-auth/token' ]; then\n git config --global credential.helper '!f() { echo username=x-access-token; echo password=$(cat /git-auth/token); }; f'\nfi\ngit clone --depth 1 --branch 'v2.0.0' -- 'https://github.com/org/my-skills' '/skills/k8s-skill'\n_tmp=\"$(mktemp -d)\"\ncp -a '/skills/k8s-skill/skills/k8s/.' \"$_tmp/\"\nrm -rf '/skills/k8s-skill'\nmv \"$_tmp\" '/skills/k8s-skill'\ngit clone -- 'https://github.com/org/another-skill' '/skills/another-skill'\ncd '/skills/another-skill' \u0026\u0026 git checkout 'abc123def456abc123def456abc123def456abc1'\ngit clone --depth 1 --branch 'main' -- 'https://github.com/org/private-skill' '/skills/private-skill'\nkrane export 'ghcr.io/org/oci-skill:v1.0' '/tmp/oci-skill.tar'\nmkdir -p '/skills/oci-skill'\ntar xf '/tmp/oci-skill.tar' -C '/skills/oci-skill'\nrm -f '/tmp/oci-skill.tar'\n" ], - "image": "alpine/git:2.47.2", - "name": "git-skills-init", + "image": "cr.kagent.dev/kagent-dev/kagent/skills-init:dev", + "name": "skills-init", "resources": {}, "volumeMounts": [ { @@ -258,30 +254,6 @@ "readOnly": true } ] - }, - { - "args": [ - "ghcr.io/org/oci-skill:v1.0" - ], - "command": [ - "kagent-adk", - "pull-skills" - ], - "env": [ - { - "name": "KAGENT_SKILLS_FOLDER", - "value": "/skills" - } - ], - "image": "cr.kagent.dev/kagent-dev/kagent/app:dev", - "name": "skills-init", - "resources": {}, - "volumeMounts": [ - { - "mountPath": "/skills", - "name": "kagent-skills" - } - ] } ], "serviceAccountName": "git-skills-agent", diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json index 80e6d08c1..b12c1c08d 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json @@ -171,10 +171,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json index f4ff634a9..56a95c5f2 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json @@ -167,10 +167,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json index bfb2b49d2..1997b15d7 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json @@ -165,10 +165,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json index 08736491e..25f06b530 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json @@ -150,10 +150,6 @@ "/config" ], "env": [ - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json index 7f4dc6b48..956788548 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json @@ -177,10 +177,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json index 1b6fc0bff..b24353a57 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json @@ -167,10 +167,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json index 4fbe6d675..f79d0ba3a 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json @@ -170,10 +170,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json index 7a538d368..7ab34c958 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json @@ -170,10 +170,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json index 5aa429ed0..f5aa441b9 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json @@ -169,10 +169,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json index 2aae9c6fb..5d40e3304 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json @@ -183,10 +183,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json index 75eb75b67..f86bd478f 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json @@ -164,10 +164,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json index 8dc3a3241..3d0d35007 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json @@ -164,10 +164,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { @@ -239,20 +235,12 @@ ], "initContainers": [ { - "args": [ - "foo:latest" - ], "command": [ - "kagent-adk", - "pull-skills" + "/bin/sh", + "-c", + "set -e\nkrane export 'foo:latest' '/tmp/oci-skill.tar'\nmkdir -p '/skills/foo'\ntar xf '/tmp/oci-skill.tar' -C '/skills/foo'\nrm -f '/tmp/oci-skill.tar'\n" ], - "env": [ - { - "name": "KAGENT_SKILLS_FOLDER", - "value": "/skills" - } - ], - "image": "cr.kagent.dev/kagent-dev/kagent/app:dev", + "image": "cr.kagent.dev/kagent-dev/kagent/skills-init:dev", "name": "skills-init", "resources": {}, "volumeMounts": [ diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json index b47dbf836..79ba274cf 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json @@ -164,10 +164,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json index b030a9a79..43ab1181a 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json @@ -157,10 +157,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json index 59b6075c0..b1790f8c0 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json @@ -157,10 +157,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json b/go/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json index cf1cf0241..dfcd39d08 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json @@ -157,10 +157,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/basic_agent.json b/go/internal/controller/translator/agent/testdata/outputs/basic_agent.json index 9d4bd4181..7fc2d68a6 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/basic_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/basic_agent.json @@ -164,10 +164,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json b/go/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json index adc09773f..186caae07 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json @@ -170,10 +170,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/ollama_agent.json b/go/internal/controller/translator/agent/testdata/outputs/ollama_agent.json index 8d8405268..24370b227 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/ollama_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/ollama_agent.json @@ -159,10 +159,6 @@ "name": "OLLAMA_API_BASE", "value": "http://localhost:11434" }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json b/go/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json index d3df897df..d2a8c00eb 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json +++ b/go/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json @@ -162,10 +162,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json b/go/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json index cbac6f420..841374b8c 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json +++ b/go/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json @@ -161,10 +161,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json b/go/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json index 9e0122215..1a0dbe1b6 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json +++ b/go/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json @@ -162,10 +162,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/pkg/app/app.go b/go/pkg/app/app.go index b0f27737b..666430e2a 100644 --- a/go/pkg/app/app.go +++ b/go/pkg/app/app.go @@ -170,7 +170,10 @@ func (cfg *Config) SetFlags(commandLine *flag.FlagSet) { 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.") - commandLine.StringVar(&agent_translator.DefaultGitInitImage, "git-init-image", agent_translator.DefaultGitInitImage, "The image to use for the git skills init container.") + 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.") + commandLine.StringVar(&agent_translator.DefaultSkillsInitImageConfig.Repository, "skills-init-image-repository", agent_translator.DefaultSkillsInitImageConfig.Repository, "The repository to use for the skills init image.") } // LoadFromEnv loads configuration values from environment variables. diff --git a/helm/kagent-crds/templates/kagent.dev_agents.yaml b/helm/kagent-crds/templates/kagent.dev_agents.yaml index 418225433..a29e879b4 100644 --- a/helm/kagent-crds/templates/kagent.dev_agents.yaml +++ b/helm/kagent-crds/templates/kagent.dev_agents.yaml @@ -10027,6 +10027,51 @@ spec: Skills to load into the agent. They will be pulled from the specified container images. and made available to the agent under the `/skills` folder. properties: + gitAuthSecretRef: + description: |- + Reference to a Secret containing git credentials. + Applied to all gitRefs entries. + The secret should contain a `token` key for HTTPS auth, + or `ssh-privatekey` for SSH auth. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + gitRefs: + description: Git repositories to fetch skills from. + items: + description: GitRepo specifies a single Git repository to fetch + skills from. + properties: + name: + description: Name for the skill directory under /skills. + Defaults to the repo name. + type: string + path: + description: Subdirectory within the repo to use as the + skill root. + type: string + ref: + default: main + description: 'Git reference: branch name, tag, or commit + SHA.' + type: string + url: + description: URL of the git repository (HTTPS or SSH). + type: string + required: + - url + type: object + maxItems: 20 + type: array insecureSkipVerify: description: |- Fetch images insecurely from registries (allowing HTTP and skipping TLS verification). @@ -10037,7 +10082,6 @@ spec: items: type: string maxItems: 20 - minItems: 1 type: array type: object type: diff --git a/helm/kagent/templates/controller-configmap.yaml b/helm/kagent/templates/controller-configmap.yaml index bd74b8a4d..c92c4b3e5 100644 --- a/helm/kagent/templates/controller-configmap.yaml +++ b/helm/kagent/templates/controller-configmap.yaml @@ -17,7 +17,10 @@ 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 }} - GIT_INIT_IMAGE: {{ .Values.controller.gitInitImage | quote }} + 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 }} + SKILLS_INIT_IMAGE_TAG: {{ coalesce .Values.controller.skillsInitImage.tag .Values.tag .Chart.Version | 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 917b512b1..acf301593 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -72,8 +72,12 @@ controller: repository: kagent-dev/kagent/app tag: "" # Will default to global, then Chart version pullPolicy: "" - # -- The image used by the git-skills-init container to clone skills from Git repositories. - gitInitImage: "alpine/git:2.47.2" + # -- The image used by the skills-init container to clone skills from Git and pull OCI skill images. + skillsInitImage: + registry: "" + repository: kagent-dev/kagent/skills-init + tag: "" # Will default to global, then Chart version + pullPolicy: "" streaming: # Streaming buffer size for A2A communication maxBufSize: 1Mi # 1024 * 1024 initialBufSize: 4Ki # 4 * 1024 From 3f038a30ac1a09fd32640f1afa83124cfea28f00 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Mon, 23 Feb 2026 18:24:37 +0000 Subject: [PATCH 03/10] formatting Signed-off-by: Eitan Yarmush --- go/internal/controller/translator/agent/git_skills_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/go/internal/controller/translator/agent/git_skills_test.go b/go/internal/controller/translator/agent/git_skills_test.go index 2ec6ac468..6bc9a6bb2 100644 --- a/go/internal/controller/translator/agent/git_skills_test.go +++ b/go/internal/controller/translator/agent/git_skills_test.go @@ -180,9 +180,9 @@ func Test_AdkApiTranslator_Skills(t *testing.T) { }, }, }, - wantSkillsInit: true, - wantSkillsVolume: true, - wantContainsPath: "skills/k8s", + wantSkillsInit: true, + wantSkillsVolume: true, + wantContainsPath: "skills/k8s", }, { name: "git skills with shared auth secret", From 476948b1fd4b7579ca1db0d886658ba423241161 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Mon, 23 Feb 2026 20:48:45 +0000 Subject: [PATCH 04/10] fix: harden skills-init against injection and add input validation - Use heredoc variable assignments in shell template instead of inline single-quoted values to prevent shell injection via values containing single quotes - Add echo logging before git clone and krane export operations - Stop suppressing ssh-keyscan stderr (remove 2>/dev/null) - Validate SubPath: reject absolute paths and ".." traversal segments - Detect duplicate skill directory names across git and OCI refs - Use url.Parse in gitSkillName to strip query params and fragments - Add unit tests for ociSkillName, gitSkillName, validateSubPath, and duplicate name detection - Remove unused Makefile push target Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Eitan Yarmush --- Makefile | 3 - .../translator/agent/adk_api_translator.go | 59 ++++- .../translator/agent/skills-init.sh.tmpl | 59 +++-- .../translator/agent/skills_unit_test.go | 212 ++++++++++++++++++ .../outputs/agent_with_git_skills.json | 2 +- .../testdata/outputs/agent_with_skills.json | 2 +- 6 files changed, 311 insertions(+), 26 deletions(-) create mode 100644 go/internal/controller/translator/agent/skills_unit_test.go diff --git a/Makefile b/Makefile index 4cf354e18..771f2b6ad 100644 --- a/Makefile +++ b/Makefile @@ -247,9 +247,6 @@ lint: make -C go lint make -C python lint -.PHONY: push -push: push-controller push-ui push-app push-kagent-adk push-skills-init - .PHONY: controller-manifests controller-manifests: make -C go manifests diff --git a/go/internal/controller/translator/agent/adk_api_translator.go b/go/internal/controller/translator/agent/adk_api_translator.go index 312aebccb..134a7baeb 100644 --- a/go/internal/controller/translator/agent/adk_api_translator.go +++ b/go/internal/controller/translator/agent/adk_api_translator.go @@ -1371,16 +1371,38 @@ func isCommitSHA(ref string) bool { // gitSkillName returns the directory name for a git skill ref. // If Name is set, it is used; otherwise the last path segment of the repo URL // (with any .git suffix stripped) is used. +// Query parameters and fragments are stripped before extracting the base name. func gitSkillName(ref v1alpha2.GitRepo) string { if ref.Name != "" { return ref.Name } - // Extract repo name from URL + // Parse the URL to strip query params and fragments u := ref.URL + if parsed, err := url.Parse(u); err == nil { + u = parsed.Path + // If the path is empty (e.g. just a host), fall back to the raw URL + if u == "" { + u = ref.URL + } + } u = strings.TrimSuffix(u, ".git") return path.Base(u) } +// validateSubPath rejects subPath values that are absolute or contain ".." traversal segments. +func validateSubPath(p string) error { + if p == "" { + return nil + } + if path.IsAbs(p) { + return fmt.Errorf("skill subPath must be relative, got %q", p) + } + if slices.Contains(strings.Split(p, "/"), "..") { + return fmt.Errorf("skill subPath must not contain '..', got %q", p) + } + return nil +} + // skillsInitData holds the template data for the unified skills-init script. type skillsInitData struct { AuthMountPath string // "/git-auth" or "" (for git auth) @@ -1437,12 +1459,13 @@ func ociSkillName(imageRef string) string { } // prepareSkillsInitData converts CRD values to the template-ready data struct. +// It validates subPaths and detects duplicate skill directory names. func prepareSkillsInitData( gitRefs []v1alpha2.GitRepo, authSecretRef *corev1.LocalObjectReference, ociRefs []string, insecureOCI bool, -) skillsInitData { +) (skillsInitData, error) { data := skillsInitData{ InsecureOCI: insecureOCI, } @@ -1451,30 +1474,49 @@ func prepareSkillsInitData( data.AuthMountPath = "/git-auth" } + seen := make(map[string]bool) + for _, ref := range gitRefs { + subPath := strings.TrimSuffix(ref.Path, "/") + if err := validateSubPath(subPath); err != nil { + return skillsInitData{}, err + } + gitRef := ref.Ref if gitRef == "" { gitRef = "main" } ref.Ref = gitRef + name := gitSkillName(ref) + if seen[name] { + return skillsInitData{}, fmt.Errorf("duplicate skill directory name %q", name) + } + seen[name] = true + data.GitRefs = append(data.GitRefs, gitRefData{ URL: ref.URL, Ref: gitRef, - Dest: "/skills/" + gitSkillName(ref), + Dest: "/skills/" + name, IsCommit: isCommitSHA(gitRef), - SubPath: strings.TrimSuffix(ref.Path, "/"), + SubPath: subPath, }) } for _, imageRef := range ociRefs { + name := ociSkillName(imageRef) + if seen[name] { + return skillsInitData{}, fmt.Errorf("duplicate skill directory name %q", name) + } + seen[name] = true + data.OCIRefs = append(data.OCIRefs, ociRefData{ Image: imageRef, - Dest: "/skills/" + ociSkillName(imageRef), + Dest: "/skills/" + name, }) } - return data + return data, nil } // buildSkillsInitContainer creates the unified init container and associated volumes @@ -1487,7 +1529,10 @@ func buildSkillsInitContainer( insecureOCI bool, securityContext *corev1.SecurityContext, ) (container corev1.Container, volumes []corev1.Volume, err error) { - data := prepareSkillsInitData(gitRefs, authSecretRef, ociRefs, insecureOCI) + data, err := prepareSkillsInitData(gitRefs, authSecretRef, ociRefs, insecureOCI) + if err != nil { + return corev1.Container{}, nil, err + } script, err := buildSkillsScript(data) if err != nil { return corev1.Container{}, nil, err diff --git a/go/internal/controller/translator/agent/skills-init.sh.tmpl b/go/internal/controller/translator/agent/skills-init.sh.tmpl index 3155f3ecb..5c4afec0c 100644 --- a/go/internal/controller/translator/agent/skills-init.sh.tmpl +++ b/go/internal/controller/translator/agent/skills-init.sh.tmpl @@ -1,31 +1,62 @@ set -e {{- if .AuthMountPath }} -if [ -f '{{ .AuthMountPath }}/ssh-privatekey' ]; then +_auth_mount="$(cat <<'ENDVAL' +{{ .AuthMountPath }} +ENDVAL +)" +if [ -f "${_auth_mount}/ssh-privatekey" ]; then mkdir -p ~/.ssh - cp '{{ .AuthMountPath }}/ssh-privatekey' ~/.ssh/id_rsa + cp "${_auth_mount}/ssh-privatekey" ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa - ssh-keyscan github.com gitlab.com bitbucket.org >> ~/.ssh/known_hosts 2>/dev/null -elif [ -f '{{ .AuthMountPath }}/token' ]; then - git config --global credential.helper '!f() { echo username=x-access-token; echo password=$(cat {{ .AuthMountPath }}/token); }; f' + ssh-keyscan github.com gitlab.com bitbucket.org >> ~/.ssh/known_hosts +elif [ -f "${_auth_mount}/token" ]; then + git config --global credential.helper "!f() { echo username=x-access-token; echo password=\$(cat ${_auth_mount}/token); }; f" fi {{- end }} {{- range .GitRefs }} +_url="$(cat <<'ENDVAL' +{{ .URL }} +ENDVAL +)" +_ref="$(cat <<'ENDVAL' +{{ .Ref }} +ENDVAL +)" +_dest="$(cat <<'ENDVAL' +{{ .Dest }} +ENDVAL +)" {{- if .IsCommit }} -git clone -- '{{ .URL }}' '{{ .Dest }}' -cd '{{ .Dest }}' && git checkout '{{ .Ref }}' +echo "Cloning ${_url} (commit ${_ref}) into ${_dest}" +git clone -- "$_url" "$_dest" +cd "$_dest" && git checkout "$_ref" {{- else }} -git clone --depth 1 --branch '{{ .Ref }}' -- '{{ .URL }}' '{{ .Dest }}' +echo "Cloning ${_url} (ref ${_ref}) into ${_dest}" +git clone --depth 1 --branch "$_ref" -- "$_url" "$_dest" {{- end }} {{- if .SubPath }} +_subpath="$(cat <<'ENDVAL' +{{ .SubPath }} +ENDVAL +)" _tmp="$(mktemp -d)" -cp -a '{{ .Dest }}/{{ .SubPath }}/.' "$_tmp/" -rm -rf '{{ .Dest }}' -mv "$_tmp" '{{ .Dest }}' +cp -a "${_dest}/${_subpath}/." "$_tmp/" +rm -rf "$_dest" +mv "$_tmp" "$_dest" {{- end }} {{- end }} {{- range .OCIRefs }} -krane export{{ if $.InsecureOCI }} --insecure{{ end }} '{{ .Image }}' '/tmp/oci-skill.tar' -mkdir -p '{{ .Dest }}' -tar xf '/tmp/oci-skill.tar' -C '{{ .Dest }}' +_image="$(cat <<'ENDVAL' +{{ .Image }} +ENDVAL +)" +_dest="$(cat <<'ENDVAL' +{{ .Dest }} +ENDVAL +)" +echo "Exporting OCI image ${_image} into ${_dest}" +krane export{{ if $.InsecureOCI }} --insecure{{ end }} "$_image" '/tmp/oci-skill.tar' +mkdir -p "$_dest" +tar xf '/tmp/oci-skill.tar' -C "$_dest" rm -f '/tmp/oci-skill.tar' {{- end }} diff --git a/go/internal/controller/translator/agent/skills_unit_test.go b/go/internal/controller/translator/agent/skills_unit_test.go new file mode 100644 index 000000000..b4fefd3fa --- /dev/null +++ b/go/internal/controller/translator/agent/skills_unit_test.go @@ -0,0 +1,212 @@ +package agent + +import ( + "testing" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" +) + +func Test_ociSkillName(t *testing.T) { + tests := []struct { + name string + imageRef string + want string + }{ + {name: "simple image:tag", imageRef: "skill:latest", want: "skill"}, + {name: "registry/org/skill:tag", imageRef: "ghcr.io/org/skill:v1", want: "skill"}, + {name: "localhost:5000/skill", imageRef: "localhost:5000/skill", want: "skill"}, + {name: "localhost:5000/skill:tag", imageRef: "localhost:5000/skill:v1", want: "skill"}, + {name: "registry:port/org/skill:tag", imageRef: "registry.example.com:8080/org/skill:v1", want: "skill"}, + {name: "digest ref", imageRef: "ghcr.io/org/skill@sha256:abc123", want: "skill"}, + {name: "tag and digest", imageRef: "ghcr.io/org/skill:v1@sha256:abc123", want: "skill"}, + {name: "deeply nested", imageRef: "registry.io/a/b/c/skill:latest", want: "skill"}, + {name: "no tag no digest", imageRef: "ghcr.io/org/skill", want: "skill"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ociSkillName(tt.imageRef) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_gitSkillName(t *testing.T) { + tests := []struct { + name string + ref v1alpha2.GitRepo + want string + }{ + { + name: "explicit name takes precedence", + ref: v1alpha2.GitRepo{URL: "https://github.com/org/repo.git", Name: "custom"}, + want: "custom", + }, + { + name: "strips .git suffix", + ref: v1alpha2.GitRepo{URL: "https://github.com/org/my-repo.git"}, + want: "my-repo", + }, + { + name: "no .git suffix", + ref: v1alpha2.GitRepo{URL: "https://github.com/org/my-repo"}, + want: "my-repo", + }, + { + name: "strips query params", + ref: v1alpha2.GitRepo{URL: "https://github.com/org/repo.git?token=abc"}, + want: "repo", + }, + { + name: "strips fragment", + ref: v1alpha2.GitRepo{URL: "https://github.com/org/repo.git#readme"}, + want: "repo", + }, + { + name: "strips query and fragment", + ref: v1alpha2.GitRepo{URL: "https://github.com/org/repo?foo=bar#baz"}, + want: "repo", + }, + { + name: "SSH URL", + ref: v1alpha2.GitRepo{URL: "git@github.com:org/repo.git"}, + want: "repo", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := gitSkillName(tt.ref) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_validateSubPath(t *testing.T) { + tests := []struct { + name string + path string + wantErr string + }{ + {name: "empty is valid", path: "", wantErr: ""}, + {name: "simple relative path", path: "skills/k8s", wantErr: ""}, + {name: "single segment", path: "subdir", wantErr: ""}, + {name: "absolute path rejected", path: "/etc/passwd", wantErr: "must be relative"}, + {name: "dotdot at start rejected", path: "../escape", wantErr: "must not contain '..'"}, + {name: "dotdot in middle rejected", path: "a/../b", wantErr: "must not contain '..'"}, + {name: "dotdot at end rejected", path: "a/b/..", wantErr: "must not contain '..'"}, + {name: "bare dotdot rejected", path: "..", wantErr: "must not contain '..'"}, + {name: "dots in name are ok", path: "my.skill/v1.0", wantErr: ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateSubPath(tt.path) + if tt.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_prepareSkillsInitData_duplicateNames(t *testing.T) { + tests := []struct { + name string + gitRefs []v1alpha2.GitRepo + ociRefs []string + wantErr string + }{ + { + name: "no duplicates", + gitRefs: []v1alpha2.GitRepo{ + {URL: "https://github.com/org/skill-a", Ref: "main"}, + {URL: "https://github.com/org/skill-b", Ref: "main"}, + }, + wantErr: "", + }, + { + name: "duplicate git repos", + gitRefs: []v1alpha2.GitRepo{ + {URL: "https://github.com/org/skill-a", Ref: "main"}, + {URL: "https://github.com/other/skill-a", Ref: "main"}, + }, + wantErr: `duplicate skill directory name "skill-a"`, + }, + { + name: "duplicate OCI refs", + ociRefs: []string{ + "ghcr.io/org/skill:v1", + "ghcr.io/other/skill:v2", + }, + wantErr: `duplicate skill directory name "skill"`, + }, + { + name: "git and OCI collision", + gitRefs: []v1alpha2.GitRepo{ + {URL: "https://github.com/org/my-skill", Ref: "main"}, + }, + ociRefs: []string{ + "ghcr.io/org/my-skill:v1", + }, + wantErr: `duplicate skill directory name "my-skill"`, + }, + { + name: "explicit name avoids collision", + gitRefs: []v1alpha2.GitRepo{ + {URL: "https://github.com/org/skill-a", Ref: "main", Name: "unique-a"}, + {URL: "https://github.com/org/skill-a", Ref: "v2", Name: "unique-b"}, + }, + wantErr: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := prepareSkillsInitData(tt.gitRefs, nil, tt.ociRefs, false) + if tt.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_prepareSkillsInitData_pathTraversal(t *testing.T) { + _, err := prepareSkillsInitData( + []v1alpha2.GitRepo{ + {URL: "https://github.com/org/repo", Ref: "main", Path: "../escape"}, + }, + nil, nil, false, + ) + require.Error(t, err) + assert.Contains(t, err.Error(), "must not contain '..'") +} + +func Test_prepareSkillsInitData_absolutePath(t *testing.T) { + _, err := prepareSkillsInitData( + []v1alpha2.GitRepo{ + {URL: "https://github.com/org/repo", Ref: "main", Path: "/etc/passwd"}, + }, + nil, nil, false, + ) + require.Error(t, err) + assert.Contains(t, err.Error(), "must be relative") +} + +func Test_prepareSkillsInitData_authMountPath(t *testing.T) { + data, err := prepareSkillsInitData( + []v1alpha2.GitRepo{{URL: "https://github.com/org/repo", Ref: "main"}}, + &corev1.LocalObjectReference{Name: "my-secret"}, + nil, false, + ) + require.NoError(t, err) + assert.Equal(t, "/git-auth", data.AuthMountPath) +} diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json index 272fa5de7..e931e064d 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json @@ -238,7 +238,7 @@ "command": [ "/bin/sh", "-c", - "set -e\nif [ -f '/git-auth/ssh-privatekey' ]; then\n mkdir -p ~/.ssh\n cp '/git-auth/ssh-privatekey' ~/.ssh/id_rsa\n chmod 600 ~/.ssh/id_rsa\n ssh-keyscan github.com gitlab.com bitbucket.org \u003e\u003e ~/.ssh/known_hosts 2\u003e/dev/null\nelif [ -f '/git-auth/token' ]; then\n git config --global credential.helper '!f() { echo username=x-access-token; echo password=$(cat /git-auth/token); }; f'\nfi\ngit clone --depth 1 --branch 'v2.0.0' -- 'https://github.com/org/my-skills' '/skills/k8s-skill'\n_tmp=\"$(mktemp -d)\"\ncp -a '/skills/k8s-skill/skills/k8s/.' \"$_tmp/\"\nrm -rf '/skills/k8s-skill'\nmv \"$_tmp\" '/skills/k8s-skill'\ngit clone -- 'https://github.com/org/another-skill' '/skills/another-skill'\ncd '/skills/another-skill' \u0026\u0026 git checkout 'abc123def456abc123def456abc123def456abc1'\ngit clone --depth 1 --branch 'main' -- 'https://github.com/org/private-skill' '/skills/private-skill'\nkrane export 'ghcr.io/org/oci-skill:v1.0' '/tmp/oci-skill.tar'\nmkdir -p '/skills/oci-skill'\ntar xf '/tmp/oci-skill.tar' -C '/skills/oci-skill'\nrm -f '/tmp/oci-skill.tar'\n" + "set -e\n_auth_mount=\"$(cat \u003c\u003c'ENDVAL'\n/git-auth\nENDVAL\n)\"\nif [ -f \"${_auth_mount}/ssh-privatekey\" ]; then\n mkdir -p ~/.ssh\n cp \"${_auth_mount}/ssh-privatekey\" ~/.ssh/id_rsa\n chmod 600 ~/.ssh/id_rsa\n ssh-keyscan github.com gitlab.com bitbucket.org \u003e\u003e ~/.ssh/known_hosts\nelif [ -f \"${_auth_mount}/token\" ]; then\n git config --global credential.helper \"!f() { echo username=x-access-token; echo password=\\$(cat ${_auth_mount}/token); }; f\"\nfi\n_url=\"$(cat \u003c\u003c'ENDVAL'\nhttps://github.com/org/my-skills\nENDVAL\n)\"\n_ref=\"$(cat \u003c\u003c'ENDVAL'\nv2.0.0\nENDVAL\n)\"\n_dest=\"$(cat \u003c\u003c'ENDVAL'\n/skills/k8s-skill\nENDVAL\n)\"\necho \"Cloning ${_url} (ref ${_ref}) into ${_dest}\"\ngit clone --depth 1 --branch \"$_ref\" -- \"$_url\" \"$_dest\"\n_subpath=\"$(cat \u003c\u003c'ENDVAL'\nskills/k8s\nENDVAL\n)\"\n_tmp=\"$(mktemp -d)\"\ncp -a \"${_dest}/${_subpath}/.\" \"$_tmp/\"\nrm -rf \"$_dest\"\nmv \"$_tmp\" \"$_dest\"\n_url=\"$(cat \u003c\u003c'ENDVAL'\nhttps://github.com/org/another-skill\nENDVAL\n)\"\n_ref=\"$(cat \u003c\u003c'ENDVAL'\nabc123def456abc123def456abc123def456abc1\nENDVAL\n)\"\n_dest=\"$(cat \u003c\u003c'ENDVAL'\n/skills/another-skill\nENDVAL\n)\"\necho \"Cloning ${_url} (commit ${_ref}) into ${_dest}\"\ngit clone -- \"$_url\" \"$_dest\"\ncd \"$_dest\" \u0026\u0026 git checkout \"$_ref\"\n_url=\"$(cat \u003c\u003c'ENDVAL'\nhttps://github.com/org/private-skill\nENDVAL\n)\"\n_ref=\"$(cat \u003c\u003c'ENDVAL'\nmain\nENDVAL\n)\"\n_dest=\"$(cat \u003c\u003c'ENDVAL'\n/skills/private-skill\nENDVAL\n)\"\necho \"Cloning ${_url} (ref ${_ref}) into ${_dest}\"\ngit clone --depth 1 --branch \"$_ref\" -- \"$_url\" \"$_dest\"\n_image=\"$(cat \u003c\u003c'ENDVAL'\nghcr.io/org/oci-skill:v1.0\nENDVAL\n)\"\n_dest=\"$(cat \u003c\u003c'ENDVAL'\n/skills/oci-skill\nENDVAL\n)\"\necho \"Exporting OCI image ${_image} into ${_dest}\"\nkrane export \"$_image\" '/tmp/oci-skill.tar'\nmkdir -p \"$_dest\"\ntar xf '/tmp/oci-skill.tar' -C \"$_dest\"\nrm -f '/tmp/oci-skill.tar'\n" ], "image": "cr.kagent.dev/kagent-dev/kagent/skills-init:dev", "name": "skills-init", diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json index 3d0d35007..c37de8736 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json @@ -238,7 +238,7 @@ "command": [ "/bin/sh", "-c", - "set -e\nkrane export 'foo:latest' '/tmp/oci-skill.tar'\nmkdir -p '/skills/foo'\ntar xf '/tmp/oci-skill.tar' -C '/skills/foo'\nrm -f '/tmp/oci-skill.tar'\n" + "set -e\n_image=\"$(cat \u003c\u003c'ENDVAL'\nfoo:latest\nENDVAL\n)\"\n_dest=\"$(cat \u003c\u003c'ENDVAL'\n/skills/foo\nENDVAL\n)\"\necho \"Exporting OCI image ${_image} into ${_dest}\"\nkrane export \"$_image\" '/tmp/oci-skill.tar'\nmkdir -p \"$_dest\"\ntar xf '/tmp/oci-skill.tar' -C \"$_dest\"\nrm -f '/tmp/oci-skill.tar'\n" ], "image": "cr.kagent.dev/kagent-dev/kagent/skills-init:dev", "name": "skills-init", From 293a3cb96eade102a6b7f61ea32023361bca1a92 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Mon, 23 Feb 2026 21:07:15 +0000 Subject: [PATCH 05/10] fix: add XValidation requiring at least one of refs or gitRefs in skills Without this, users could create a Skills object with neither OCI refs nor git refs, resulting in unnecessary init containers and volumes. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Eitan Yarmush --- go/api/v1alpha2/agent_types.go | 1 + go/config/crd/bases/kagent.dev_agents.yaml | 3 +++ helm/kagent-crds/templates/kagent.dev_agents.yaml | 3 +++ 3 files changed, 7 insertions(+) diff --git a/go/api/v1alpha2/agent_types.go b/go/api/v1alpha2/agent_types.go index 40b31d1ca..d07fd0b5d 100644 --- a/go/api/v1alpha2/agent_types.go +++ b/go/api/v1alpha2/agent_types.go @@ -69,6 +69,7 @@ type AgentSpec struct { AllowedNamespaces *AllowedNamespaces `json:"allowedNamespaces,omitempty"` } +// +kubebuilder:validation:XValidation:rule="size(self.refs) > 0 || size(self.gitRefs) > 0",message="at least one of refs or gitRefs must be specified" type SkillForAgent struct { // Fetch images insecurely from registries (allowing HTTP and skipping TLS verification). // Meant for development and testing purposes only. diff --git a/go/config/crd/bases/kagent.dev_agents.yaml b/go/config/crd/bases/kagent.dev_agents.yaml index a29e879b4..29c175f3e 100644 --- a/go/config/crd/bases/kagent.dev_agents.yaml +++ b/go/config/crd/bases/kagent.dev_agents.yaml @@ -10084,6 +10084,9 @@ spec: maxItems: 20 type: array type: object + x-kubernetes-validations: + - message: at least one of refs or gitRefs must be specified + rule: size(self.refs) > 0 || size(self.gitRefs) > 0 type: allOf: - enum: diff --git a/helm/kagent-crds/templates/kagent.dev_agents.yaml b/helm/kagent-crds/templates/kagent.dev_agents.yaml index a29e879b4..29c175f3e 100644 --- a/helm/kagent-crds/templates/kagent.dev_agents.yaml +++ b/helm/kagent-crds/templates/kagent.dev_agents.yaml @@ -10084,6 +10084,9 @@ spec: maxItems: 20 type: array type: object + x-kubernetes-validations: + - message: at least one of refs or gitRefs must be specified + rule: size(self.refs) > 0 || size(self.gitRefs) > 0 type: allOf: - enum: From e429b6fe862e727793eea99af288b7bf1df32102 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Tue, 24 Feb 2026 15:50:27 +0000 Subject: [PATCH 06/10] remove unused, fix kvalidation Signed-off-by: Eitan Yarmush --- go/api/v1alpha2/agent_types.go | 2 +- go/config/crd/bases/kagent.dev_agents.yaml | 3 --- go/internal/controller/translator/agent/adk_api_translator.go | 3 --- helm/kagent-crds/templates/kagent.dev_agents.yaml | 3 --- 4 files changed, 1 insertion(+), 10 deletions(-) diff --git a/go/api/v1alpha2/agent_types.go b/go/api/v1alpha2/agent_types.go index d07fd0b5d..a5cbea9b2 100644 --- a/go/api/v1alpha2/agent_types.go +++ b/go/api/v1alpha2/agent_types.go @@ -69,7 +69,7 @@ type AgentSpec struct { AllowedNamespaces *AllowedNamespaces `json:"allowedNamespaces,omitempty"` } -// +kubebuilder:validation:XValidation:rule="size(self.refs) > 0 || size(self.gitRefs) > 0",message="at least one of refs or gitRefs must be specified" +// +kubebuilder:validation:AtLeastOneOf=refs,gitRefs type SkillForAgent struct { // Fetch images insecurely from registries (allowing HTTP and skipping TLS verification). // Meant for development and testing purposes only. diff --git a/go/config/crd/bases/kagent.dev_agents.yaml b/go/config/crd/bases/kagent.dev_agents.yaml index 29c175f3e..a29e879b4 100644 --- a/go/config/crd/bases/kagent.dev_agents.yaml +++ b/go/config/crd/bases/kagent.dev_agents.yaml @@ -10084,9 +10084,6 @@ spec: maxItems: 20 type: array type: object - x-kubernetes-validations: - - message: at least one of refs or gitRefs must be specified - rule: size(self.refs) > 0 || size(self.gitRefs) > 0 type: allOf: - enum: diff --git a/go/internal/controller/translator/agent/adk_api_translator.go b/go/internal/controller/translator/agent/adk_api_translator.go index 134a7baeb..7dea7fae7 100644 --- a/go/internal/controller/translator/agent/adk_api_translator.go +++ b/go/internal/controller/translator/agent/adk_api_translator.go @@ -425,10 +425,7 @@ func (a *adkApiTranslator) buildManifest( ReadOnly: true, }) sharedEnv = append(sharedEnv, skillsEnv) - } - // Unified skills init container (handles both git and OCI skills) - if hasSkills { insecure := agent.Spec.Skills != nil && agent.Spec.Skills.InsecureSkipVerify container, skillsVolumes, err := buildSkillsInitContainer(gitRefs, gitAuthSecretRef, skills, insecure, dep.SecurityContext) if err != nil { diff --git a/helm/kagent-crds/templates/kagent.dev_agents.yaml b/helm/kagent-crds/templates/kagent.dev_agents.yaml index 29c175f3e..a29e879b4 100644 --- a/helm/kagent-crds/templates/kagent.dev_agents.yaml +++ b/helm/kagent-crds/templates/kagent.dev_agents.yaml @@ -10084,9 +10084,6 @@ spec: maxItems: 20 type: array type: object - x-kubernetes-validations: - - message: at least one of refs or gitRefs must be specified - rule: size(self.refs) > 0 || size(self.gitRefs) > 0 type: allOf: - enum: From 00cabf6a2f22ca4f2a41bdd6c4780e36d6e135a8 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Wed, 25 Feb 2026 18:55:56 +0000 Subject: [PATCH 07/10] remove unused skill_fetcher code Signed-off-by: Eitan Yarmush --- docker/skills-init/Dockerfile | 1 - go/api/v1alpha2/agent_types.go | 2 + go/config/crd/bases/kagent.dev_agents.yaml | 2 + helm/agents/k8s/templates/agent.yaml | 2 +- .../templates/kagent.dev_agents.yaml | 2 + .../packages/kagent-adk/src/kagent/adk/cli.py | 15 --- .../src/kagent/adk/skill_fetcher.py | 103 ------------------ 7 files changed, 7 insertions(+), 120 deletions(-) delete mode 100644 python/packages/kagent-adk/src/kagent/adk/skill_fetcher.py diff --git a/docker/skills-init/Dockerfile b/docker/skills-init/Dockerfile index 4da6ad5bd..ca0ff825c 100644 --- a/docker/skills-init/Dockerfile +++ b/docker/skills-init/Dockerfile @@ -12,7 +12,6 @@ WORKDIR /build/go-containerregistry/cmd/krane RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /build/krane . -### Stage 1: runtime FROM alpine:3.21 RUN apk add --no-cache git diff --git a/go/api/v1alpha2/agent_types.go b/go/api/v1alpha2/agent_types.go index a5cbea9b2..79dc6f18b 100644 --- a/go/api/v1alpha2/agent_types.go +++ b/go/api/v1alpha2/agent_types.go @@ -78,6 +78,7 @@ type SkillForAgent struct { // The list of skill images to fetch. // +kubebuilder:validation:MaxItems=20 + // +kubebuilder:validation:MinItems=1 // +optional Refs []string `json:"refs,omitempty"` @@ -90,6 +91,7 @@ type SkillForAgent struct { // Git repositories to fetch skills from. // +kubebuilder:validation:MaxItems=20 + // +kubebuilder:validation:MinItems=1 // +optional GitRefs []GitRepo `json:"gitRefs,omitempty"` } diff --git a/go/config/crd/bases/kagent.dev_agents.yaml b/go/config/crd/bases/kagent.dev_agents.yaml index a29e879b4..f2e060572 100644 --- a/go/config/crd/bases/kagent.dev_agents.yaml +++ b/go/config/crd/bases/kagent.dev_agents.yaml @@ -10071,6 +10071,7 @@ spec: - url type: object maxItems: 20 + minItems: 1 type: array insecureSkipVerify: description: |- @@ -10082,6 +10083,7 @@ spec: items: type: string maxItems: 20 + minItems: 1 type: array type: object type: diff --git a/helm/agents/k8s/templates/agent.yaml b/helm/agents/k8s/templates/agent.yaml index 898839cc3..bc338dbba 100644 --- a/helm/agents/k8s/templates/agent.yaml +++ b/helm/agents/k8s/templates/agent.yaml @@ -182,4 +182,4 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} resources: - {{- toYaml .Values.resources | nindent 8 }} \ No newline at end of file + {{- toYaml .Values.resources | nindent 8 }} diff --git a/helm/kagent-crds/templates/kagent.dev_agents.yaml b/helm/kagent-crds/templates/kagent.dev_agents.yaml index a29e879b4..f2e060572 100644 --- a/helm/kagent-crds/templates/kagent.dev_agents.yaml +++ b/helm/kagent-crds/templates/kagent.dev_agents.yaml @@ -10071,6 +10071,7 @@ spec: - url type: object maxItems: 20 + minItems: 1 type: array insecureSkipVerify: description: |- @@ -10082,6 +10083,7 @@ spec: items: type: string maxItems: 20 + minItems: 1 type: array type: object type: diff --git a/python/packages/kagent-adk/src/kagent/adk/cli.py b/python/packages/kagent-adk/src/kagent/adk/cli.py index ff1b1b21a..2a44a196a 100644 --- a/python/packages/kagent-adk/src/kagent/adk/cli.py +++ b/python/packages/kagent-adk/src/kagent/adk/cli.py @@ -15,7 +15,6 @@ from kagent.core import KAgentConfig, configure_logging, configure_tracing from . import AgentConfig, KAgentApp -from .skill_fetcher import fetch_skill from .tools import add_skills_tool_to_agent logger = logging.getLogger(__name__) @@ -102,20 +101,6 @@ def root_agent_factory() -> BaseAgent: ) -@app.command() -def pull_skills( - skills: Annotated[list[str], typer.Argument()], - insecure: Annotated[ - bool, - typer.Option("--insecure", help="Allow insecure connections to registries"), - ] = False, -): - skill_dir = os.environ.get("KAGENT_SKILLS_FOLDER", ".") - logger.info("Pulling skills") - for skill in skills: - fetch_skill(skill, skill_dir, insecure) - - def add_to_agent(sts_integration: ADKTokenPropagationPlugin, agent: BaseAgent): """ Add the plugin to an ADK LLM agent by updating its MCP toolset diff --git a/python/packages/kagent-adk/src/kagent/adk/skill_fetcher.py b/python/packages/kagent-adk/src/kagent/adk/skill_fetcher.py deleted file mode 100644 index 382882b17..000000000 --- a/python/packages/kagent-adk/src/kagent/adk/skill_fetcher.py +++ /dev/null @@ -1,103 +0,0 @@ -from __future__ import annotations - -import logging -import os -import tarfile -from typing import Tuple - -logger = logging.getLogger(__name__) - - -def _parse_image_ref(image: str) -> Tuple[str, str, str]: - """ - Parse an OCI/Docker image reference into (registry, repository, reference). - - reference is either a tag (default "latest") or a digest (e.g., "sha256:..."). - Rules (compatible with Docker/OCI name parsing): - - If the reference contains a digest ("@"), prefer a tag if also present (repo:tag@digest), - otherwise keep the digest as the reference. - - If there is no tag nor digest, default the reference to "latest". - - If the first path component contains a '.' or ':' or equals 'localhost', it is treated as the registry. - Otherwise the registry defaults to docker hub (docker.io), with the special library namespace for single-component names. - """ - name_part = image - ref = "latest" - - if "@" in image: - # Split digest - name_part, digest = image.split("@", 1) - ref = digest - - # Possibly has a tag: detect a colon after the last slash - slash = name_part.rfind("/") - colon = name_part.rfind(":") - if colon > slash: - ref = name_part[colon + 1 :] - name_part = name_part[:colon] - # else: keep default "latest" - - # Determine registry and repo path - parts = name_part.split("/") - if len(parts) == 1: - # Implicit docker hub library image - registry = "registry-1.docker.io" - repo = f"library/{parts[0]}" - else: - first = parts[0] - if first == "localhost" or "." in first or ":" in first: - # Explicit registry (may include port) - registry = first - repo = "/".join(parts[1:]) - else: - # Docker hub with user/org namespace - registry = "docker.io" - repo = "/".join(parts) - - return registry, repo, ref - - -def fetch_using_krane_to_dir(image: str, destination_folder: str, insecure: bool = False) -> None: - """Fetch a skill using krane and extract it to destination_folder.""" - import subprocess - - tar_path = os.path.join(destination_folder, "skill.tar") - os.makedirs(destination_folder, exist_ok=True) - command = ["krane", "export", image, tar_path] - if insecure: - command.insert(1, "--insecure") - # Use krane to pull the image as a tarball - subprocess.run( - command, - check=True, - ) - - # Extract the tarball - with tarfile.open(tar_path, "r") as tar: - tar.extractall(path=destination_folder, filter=tarfile.data_filter) - - # Remove the tarball - os.remove(tar_path) - - -def fetch_skill(skill_image: str, destination_folder: str, insecure: bool = False) -> None: - """ - Fetch a skill packaged as an OCI/Docker image and write its files to destination_folder. - - To build a compatible skill image from a folder (containing SKILL.md), use a simple Dockerfile: - FROM scratch - COPY . / - - Args: - skill_image: The image reference (e.g., "alpine:latest", "ghcr.io/org/skill:tag", or with a digest). - destination_folder: The folder where the skill files should be written. - """ - registry, repo, ref = _parse_image_ref(skill_image) - - # skill name is the last part of the repo - repo_parts = repo.split("/") - skill_name = repo_parts[-1] - logger.info( - f"about to fetching skill {skill_name} from image {skill_image} (registry: {registry}, repo: {repo}, ref: {ref})" - ) - - fetch_using_krane_to_dir(skill_image, os.path.join(destination_folder, skill_name), insecure) From fa869d80e69ec860167cdc9816b359c3872c216c Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Wed, 25 Feb 2026 19:01:29 +0000 Subject: [PATCH 08/10] remove unused Signed-off-by: Eitan Yarmush --- .../unittests/test_skill_fetcher_parse.py | 56 ------------------- 1 file changed, 56 deletions(-) delete mode 100644 python/packages/kagent-adk/tests/unittests/test_skill_fetcher_parse.py diff --git a/python/packages/kagent-adk/tests/unittests/test_skill_fetcher_parse.py b/python/packages/kagent-adk/tests/unittests/test_skill_fetcher_parse.py deleted file mode 100644 index 70c04781e..000000000 --- a/python/packages/kagent-adk/tests/unittests/test_skill_fetcher_parse.py +++ /dev/null @@ -1,56 +0,0 @@ -import os -import sys -from pathlib import Path - -import pytest - -# Ensure the package's src/ is on sys.path for "src" layout -_PKG_ROOT = Path(__file__).resolve().parents[2] # .../packages/kagent-adk -_SRC = _PKG_ROOT / "src" -if str(_SRC) not in sys.path: - sys.path.insert(0, str(_SRC)) - -from kagent.adk.skill_fetcher import _parse_image_ref # noqa: E402 - - -@pytest.mark.parametrize( - "image,expected", - [ - # Docker Hub implicit library and latest tag - ("alpine", ("registry-1.docker.io", "library/alpine", "latest")), - ("ubuntu", ("registry-1.docker.io", "library/ubuntu", "latest")), - # Explicit tag on Docker Hub implicit library - ("alpine:3.19", ("registry-1.docker.io", "library/alpine", "3.19")), - # User namespace on Docker Hub - ("user/image", ("docker.io", "user/image", "latest")), - ("user/image:tag", ("docker.io", "user/image", "tag")), - # Fully-qualified registry without tag -> default latest - ("ghcr.io/org/skill", ("ghcr.io", "org/skill", "latest")), - ("ghcr.io/org/skill:1.2.3", ("ghcr.io", "org/skill", "1.2.3")), - # Digest reference - ( - "ghcr.io/org/skill@sha256:abcdef", - ("ghcr.io", "org/skill", "sha256:abcdef"), - ), - # Tag + digest present: keep the tag as ref (current behavior) - ( - "ghcr.io/org/skill:1@sha256:abcdef", - ("ghcr.io", "org/skill", "1"), - ), - # Registry with port - ( - "registry.example.com:5000/repo/image:tag", - ("registry.example.com:5000", "repo/image", "tag"), - ), - ( - "registry.example.com:5000/repo/image", - ("registry.example.com:5000", "repo/image", "latest"), - ), - ( - "localhost:5000/image", - ("localhost:5000", "image", "latest"), - ), - ], -) -def test_parse_image_ref(image, expected): - assert _parse_image_ref(image) == expected From c417226a4bb37e60b65bffc12552f5fe71ebf22c Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Wed, 25 Feb 2026 19:12:14 +0000 Subject: [PATCH 09/10] fix: update golden test outputs for OTEL metrics temporality env var Regenerate golden test output files to include the new OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE environment variable that was added on main. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Eitan Yarmush --- .../agent/testdata/outputs/agent_with_allowed_headers.json | 4 ++++ .../translator/agent/testdata/outputs/agent_with_code.json | 4 ++++ .../testdata/outputs/agent_with_cross_namespace_tools.json | 4 ++++ .../agent/testdata/outputs/agent_with_custom_sa.json | 4 ++++ .../agent/testdata/outputs/agent_with_git_skills.json | 6 +++++- .../agent/testdata/outputs/agent_with_http_toolserver.json | 4 ++++ .../agent/testdata/outputs/agent_with_mcp_service.json | 4 ++++ .../agent/testdata/outputs/agent_with_nested_agent.json | 4 ++++ .../agent/testdata/outputs/agent_with_passthrough.json | 4 ++++ .../translator/agent/testdata/outputs/agent_with_proxy.json | 4 ++++ .../outputs/agent_with_proxy_external_remotemcp.json | 4 ++++ .../agent/testdata/outputs/agent_with_proxy_mcpserver.json | 4 ++++ .../outputs/agent_with_proxy_mcpserver_custom_timeout.json | 4 ++++ .../agent/testdata/outputs/agent_with_proxy_service.json | 4 ++++ .../testdata/outputs/agent_with_scheduling_attributes.json | 4 ++++ .../agent/testdata/outputs/agent_with_security_context.json | 4 ++++ .../agent/testdata/outputs/agent_with_skills.json | 4 ++++ .../agent/testdata/outputs/agent_with_streaming.json | 4 ++++ .../outputs/agent_with_system_message_from_configmap.json | 4 ++++ .../outputs/agent_with_system_message_from_secret.json | 4 ++++ .../translator/agent/testdata/outputs/anthropic_agent.json | 4 ++++ .../translator/agent/testdata/outputs/basic_agent.json | 4 ++++ .../translator/agent/testdata/outputs/bedrock_agent.json | 4 ++++ .../translator/agent/testdata/outputs/ollama_agent.json | 4 ++++ .../agent/testdata/outputs/tls-with-custom-ca.json | 4 ++++ .../agent/testdata/outputs/tls-with-disabled-verify.json | 4 ++++ .../testdata/outputs/tls-with-system-cas-disabled.json | 4 ++++ 27 files changed, 109 insertions(+), 1 deletion(-) diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json index 7486ef4aa..5f39c65c4 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json @@ -172,6 +172,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_code.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_code.json index ae1ea46d6..cbe8bba80 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_code.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_code.json @@ -164,6 +164,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json index b8a00a7fa..36b17c410 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json @@ -177,6 +177,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json index 367a8e23e..f2fa37e29 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json @@ -132,6 +132,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json index e931e064d..785a27891 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json @@ -164,6 +164,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { @@ -196,7 +200,7 @@ ], "readinessProbe": { "httpGet": { - "path": "/health", + "path": "/.well-known/agent.json", "port": "http" }, "initialDelaySeconds": 15, diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json index a9babf1fe..8efd6f777 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json @@ -171,6 +171,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json index 941a596cc..ca683bbac 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json @@ -167,6 +167,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json index 063d33e6c..1b7a65c1a 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json @@ -165,6 +165,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json index 496c9f140..672c82c5a 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json @@ -150,6 +150,10 @@ "/config" ], "env": [ + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json index c13b84cc2..dd3a3c30f 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json @@ -177,6 +177,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json index 4368cfbfa..d1dab69fc 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json @@ -167,6 +167,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json index 5b7120289..c7f808ece 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json @@ -170,6 +170,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json index e1bea3e71..f3cb60284 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json @@ -170,6 +170,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json index 79307b8a2..827fde55c 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json @@ -169,6 +169,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json index 4e1970f2f..442138f4d 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json @@ -183,6 +183,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json index 226a8a029..1ca6aae00 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json @@ -164,6 +164,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json index 54583a8bf..e52ecdb3d 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json @@ -164,6 +164,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json index 8e23f7d57..0e3aec5db 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json @@ -164,6 +164,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json index a2a6a023a..0314fdc50 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json @@ -157,6 +157,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json index 528b69fd6..c7c358b5b 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json @@ -157,6 +157,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json b/go/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json index c8b265c43..b2ab2d690 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json @@ -157,6 +157,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/basic_agent.json b/go/internal/controller/translator/agent/testdata/outputs/basic_agent.json index 99f0d212d..2c9945ad8 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/basic_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/basic_agent.json @@ -164,6 +164,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json b/go/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json index 8e83d3b2b..0507fa2d1 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json @@ -170,6 +170,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/ollama_agent.json b/go/internal/controller/translator/agent/testdata/outputs/ollama_agent.json index b515ce338..5d9112b1c 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/ollama_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/ollama_agent.json @@ -159,6 +159,10 @@ "name": "OLLAMA_API_BASE", "value": "http://localhost:11434" }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json b/go/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json index a607c8d7f..8e42dd5a8 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json +++ b/go/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json @@ -162,6 +162,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json b/go/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json index 75fd7bf68..b2a7dae22 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json +++ b/go/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json @@ -161,6 +161,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json b/go/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json index 9c6de674a..3333f6d02 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json +++ b/go/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json @@ -162,6 +162,10 @@ } } }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", + "value": "delta" + }, { "name": "KAGENT_NAMESPACE", "valueFrom": { From 2dc4dd86bb97f476201d22893113bbbb8e0a2e37 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Wed, 25 Feb 2026 19:13:31 +0000 Subject: [PATCH 10/10] fix: fix golden tests leaking host OTEL env vars The collectOtelEnvFromProcess() function reads all OTEL_ environment variables from the current process and injects them into deployments. When OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE was set in the test runner's environment, it leaked into golden outputs causing failures. Fix by clearing all OTEL_ env vars at the start of the golden test. Also update agent_with_git_skills golden output for readiness probe path change (/health -> /.well-known/agent.json). Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Eitan Yarmush --- .../translator/agent/adk_translator_golden_test.go | 10 ++++++++++ .../testdata/outputs/agent_with_allowed_headers.json | 4 ---- .../agent/testdata/outputs/agent_with_code.json | 4 ---- .../outputs/agent_with_cross_namespace_tools.json | 4 ---- .../agent/testdata/outputs/agent_with_custom_sa.json | 4 ---- .../agent/testdata/outputs/agent_with_git_skills.json | 4 ---- .../testdata/outputs/agent_with_http_toolserver.json | 4 ---- .../agent/testdata/outputs/agent_with_mcp_service.json | 4 ---- .../testdata/outputs/agent_with_nested_agent.json | 4 ---- .../agent/testdata/outputs/agent_with_passthrough.json | 4 ---- .../agent/testdata/outputs/agent_with_proxy.json | 4 ---- .../outputs/agent_with_proxy_external_remotemcp.json | 4 ---- .../testdata/outputs/agent_with_proxy_mcpserver.json | 4 ---- .../agent_with_proxy_mcpserver_custom_timeout.json | 4 ---- .../testdata/outputs/agent_with_proxy_service.json | 4 ---- .../outputs/agent_with_scheduling_attributes.json | 4 ---- .../testdata/outputs/agent_with_security_context.json | 4 ---- .../agent/testdata/outputs/agent_with_skills.json | 4 ---- .../agent/testdata/outputs/agent_with_streaming.json | 4 ---- .../agent_with_system_message_from_configmap.json | 4 ---- .../outputs/agent_with_system_message_from_secret.json | 4 ---- .../agent/testdata/outputs/anthropic_agent.json | 4 ---- .../translator/agent/testdata/outputs/basic_agent.json | 4 ---- .../agent/testdata/outputs/bedrock_agent.json | 4 ---- .../agent/testdata/outputs/ollama_agent.json | 4 ---- .../agent/testdata/outputs/tls-with-custom-ca.json | 4 ---- .../testdata/outputs/tls-with-disabled-verify.json | 4 ---- .../testdata/outputs/tls-with-system-cas-disabled.json | 4 ---- 28 files changed, 10 insertions(+), 108 deletions(-) diff --git a/go/internal/controller/translator/agent/adk_translator_golden_test.go b/go/internal/controller/translator/agent/adk_translator_golden_test.go index 562f547f3..e7d05a0c2 100644 --- a/go/internal/controller/translator/agent/adk_translator_golden_test.go +++ b/go/internal/controller/translator/agent/adk_translator_golden_test.go @@ -37,6 +37,16 @@ type TestInput struct { // TestGoldenAdkTranslator runs golden tests for the ADK API translator func TestGoldenAdkTranslator(t *testing.T) { + // Clear all OTEL_ env vars so host environment doesn't leak into + // golden outputs via collectOtelEnvFromProcess(). + for _, env := range os.Environ() { + if strings.HasPrefix(env, "OTEL_") { + key, _, _ := strings.Cut(env, "=") + t.Setenv(key, "") + os.Unsetenv(key) + } + } + // Skip if running in CI without update flag updateGolden := os.Getenv("UPDATE_GOLDEN") == "true" diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json index 5f39c65c4..7486ef4aa 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_allowed_headers.json @@ -172,10 +172,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_code.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_code.json index cbe8bba80..ae1ea46d6 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_code.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_code.json @@ -164,10 +164,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json index 36b17c410..b8a00a7fa 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_cross_namespace_tools.json @@ -177,10 +177,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json index f2fa37e29..367a8e23e 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_custom_sa.json @@ -132,10 +132,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json index 785a27891..dffefcdd6 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_git_skills.json @@ -164,10 +164,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json index 8efd6f777..a9babf1fe 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_http_toolserver.json @@ -171,10 +171,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json index ca683bbac..941a596cc 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_mcp_service.json @@ -167,10 +167,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json index 1b7a65c1a..063d33e6c 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_nested_agent.json @@ -165,10 +165,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json index 672c82c5a..496c9f140 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_passthrough.json @@ -150,10 +150,6 @@ "/config" ], "env": [ - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json index dd3a3c30f..c13b84cc2 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy.json @@ -177,10 +177,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json index d1dab69fc..4368cfbfa 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_external_remotemcp.json @@ -167,10 +167,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json index c7f808ece..5b7120289 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json @@ -170,10 +170,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json index f3cb60284..e1bea3e71 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver_custom_timeout.json @@ -170,10 +170,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json index 827fde55c..79307b8a2 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_service.json @@ -169,10 +169,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json index 442138f4d..4e1970f2f 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_scheduling_attributes.json @@ -183,10 +183,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json index 1ca6aae00..226a8a029 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_security_context.json @@ -164,10 +164,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json index e52ecdb3d..54583a8bf 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_skills.json @@ -164,10 +164,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json index 0e3aec5db..8e23f7d57 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_streaming.json @@ -164,10 +164,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json index 0314fdc50..a2a6a023a 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_configmap.json @@ -157,10 +157,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json b/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json index c7c358b5b..528b69fd6 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json +++ b/go/internal/controller/translator/agent/testdata/outputs/agent_with_system_message_from_secret.json @@ -157,10 +157,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json b/go/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json index b2ab2d690..c8b265c43 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/anthropic_agent.json @@ -157,10 +157,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/basic_agent.json b/go/internal/controller/translator/agent/testdata/outputs/basic_agent.json index 2c9945ad8..99f0d212d 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/basic_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/basic_agent.json @@ -164,10 +164,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json b/go/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json index 0507fa2d1..8e83d3b2b 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/bedrock_agent.json @@ -170,10 +170,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/ollama_agent.json b/go/internal/controller/translator/agent/testdata/outputs/ollama_agent.json index 5d9112b1c..b515ce338 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/ollama_agent.json +++ b/go/internal/controller/translator/agent/testdata/outputs/ollama_agent.json @@ -159,10 +159,6 @@ "name": "OLLAMA_API_BASE", "value": "http://localhost:11434" }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json b/go/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json index 8e42dd5a8..a607c8d7f 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json +++ b/go/internal/controller/translator/agent/testdata/outputs/tls-with-custom-ca.json @@ -162,10 +162,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json b/go/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json index b2a7dae22..75fd7bf68 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json +++ b/go/internal/controller/translator/agent/testdata/outputs/tls-with-disabled-verify.json @@ -161,10 +161,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": { diff --git a/go/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json b/go/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json index 3333f6d02..9c6de674a 100644 --- a/go/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json +++ b/go/internal/controller/translator/agent/testdata/outputs/tls-with-system-cas-disabled.json @@ -162,10 +162,6 @@ } } }, - { - "name": "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", - "value": "delta" - }, { "name": "KAGENT_NAMESPACE", "valueFrom": {