Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 256 additions & 0 deletions go/api/config/crd/bases/kagent.dev_agents.yaml

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions go/api/v1alpha2/agent_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,21 @@ type SkillForAgent struct {
// +kubebuilder:validation:MinItems=1
// +optional
GitRefs []GitRepo `json:"gitRefs,omitempty"`

// Configuration for the skills-init init container.
// +optional
InitContainer *SkillsInitContainer `json:"initContainer,omitempty"`
}

// SkillsInitContainer configures the skills-init init container.
type SkillsInitContainer struct {
// Resource requirements for the skills-init init container.
// +optional
Resources *corev1.ResourceRequirements `json:"resources,omitempty"`

// Security context for the skills-init init container.
// +optional
SecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"`
}

// GitRepo specifies a single Git repository to fetch skills from.
Expand Down
30 changes: 30 additions & 0 deletions go/api/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,18 @@ func (a *adkApiTranslator) buildManifest(
sharedEnv = append(sharedEnv, skillsEnv)

insecure := agent.Spec.Skills != nil && agent.Spec.Skills.InsecureSkipVerify
container, skillsVolumes, err := buildSkillsInitContainer(gitRefs, gitAuthSecretRef, skills, insecure, dep.SecurityContext)
initEnv := append(dep.Env, sharedEnv...)
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

initEnv := append(dep.Env, sharedEnv...) propagates all user-specified deployment env vars (including potential SecretKeyRefs) into the skills-init init container. Since init containers often run with different permissions/attack surface, consider limiting init container env to only what it needs (e.g., shared/proxy-related env) or adding an explicit skills.initContainer.env field rather than inheriting dep.Env wholesale.

Suggested change
initEnv := append(dep.Env, sharedEnv...)
initEnv := make([]corev1.EnvVar, 0, len(sharedEnv))
initEnv = append(initEnv, sharedEnv...)

Copilot uses AI. Check for mistakes.

var initResources *corev1.ResourceRequirements
initSecCtx := dep.SecurityContext.DeepCopy()
if agent.Spec.Skills.InitContainer != nil {
initResources = agent.Spec.Skills.InitContainer.Resources.DeepCopy()
if agent.Spec.Skills.InitContainer.SecurityContext != nil {
initSecCtx = agent.Spec.Skills.InitContainer.SecurityContext.DeepCopy()
}
}

container, skillsVolumes, err := buildSkillsInitContainer(gitRefs, gitAuthSecretRef, skills, insecure, initSecCtx, initEnv, getDefaultResources(initResources))
if err != nil {
return nil, fmt.Errorf("failed to build skills init container: %w", err)
}
Expand Down Expand Up @@ -1744,6 +1755,8 @@ func buildSkillsInitContainer(
ociRefs []string,
insecureOCI bool,
securityContext *corev1.SecurityContext,
env []corev1.EnvVar,
resources corev1.ResourceRequirements,
) (container corev1.Container, volumes []corev1.Volume, err error) {
data, err := prepareSkillsInitData(gitRefs, authSecretRef, ociRefs, insecureOCI)
if err != nil {
Expand All @@ -1754,11 +1767,6 @@ func buildSkillsInitContainer(
return corev1.Container{}, nil, err
}

initSecCtx := securityContext
if initSecCtx != nil {
initSecCtx = initSecCtx.DeepCopy()
}

volumeMounts := []corev1.VolumeMount{
{Name: "kagent-skills", MountPath: "/skills"},
}
Expand All @@ -1785,7 +1793,9 @@ func buildSkillsInitContainer(
Image: DefaultSkillsInitImageConfig.Image(),
Command: []string{"/bin/sh", "-c", script},
VolumeMounts: volumeMounts,
SecurityContext: initSecCtx,
SecurityContext: securityContext,
Env: env,
Resources: resources,
}

return container, volumes, nil
Expand Down
232 changes: 232 additions & 0 deletions go/core/internal/controller/translator/agent/git_skills_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
translator "github.com/kagent-dev/kagent/go/core/internal/controller/translator/agent"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
schemev1 "k8s.io/client-go/kubernetes/scheme"
Expand Down Expand Up @@ -505,3 +506,234 @@ func Test_AdkApiTranslator_SkillsConfigurableImage(t *testing.T) {
require.NotNil(t, skillsInitContainer)
assert.Equal(t, "custom-registry/skills-init:latest", skillsInitContainer.Image)
}

func Test_AdkApiTranslator_SkillsInitContainer(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,
}

boolFalse := false

tests := []struct {
name string
agent *v1alpha2.Agent
wantResources corev1.ResourceRequirements
wantSecurityContext *corev1.SecurityContext
wantEnvContains []string // env var names expected on the init container
wantDefaultResources bool // expect the default resource values
}{
{
name: "no initContainer - gets default resources and no securityContext",
agent: &v1alpha2.Agent{
ObjectMeta: metav1.ObjectMeta{Name: "agent-defaults", 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"},
},
},
},
wantDefaultResources: true,
},
{
name: "custom resources on initContainer",
agent: &v1alpha2.Agent{
ObjectMeta: metav1.ObjectMeta{Name: "agent-custom-resources", 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"},
InitContainer: &v1alpha2.SkillsInitContainer{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
corev1.ResourceMemory: resource.MustParse("64Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("200m"),
corev1.ResourceMemory: resource.MustParse("256Mi"),
},
},
},
},
},
},
wantResources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
corev1.ResourceMemory: resource.MustParse("64Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("200m"),
corev1.ResourceMemory: resource.MustParse("256Mi"),
},
},
},
{
name: "custom securityContext on initContainer",
agent: &v1alpha2.Agent{
ObjectMeta: metav1.ObjectMeta{Name: "agent-custom-secctx", 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"},
InitContainer: &v1alpha2.SkillsInitContainer{
SecurityContext: &corev1.SecurityContext{
AllowPrivilegeEscalation: &boolFalse,
},
},
},
},
},
wantDefaultResources: true,
wantSecurityContext: &corev1.SecurityContext{
AllowPrivilegeEscalation: &boolFalse,
},
},
{
name: "both resources and securityContext on initContainer",
agent: &v1alpha2.Agent{
ObjectMeta: metav1.ObjectMeta{Name: "agent-both-overrides", 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"},
InitContainer: &v1alpha2.SkillsInitContainer{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
},
},
SecurityContext: &corev1.SecurityContext{
AllowPrivilegeEscalation: &boolFalse,
},
},
},
},
},
wantResources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
},
},
wantSecurityContext: &corev1.SecurityContext{
AllowPrivilegeEscalation: &boolFalse,
},
},
{
name: "init container receives dep env vars",
agent: &v1alpha2.Agent{
ObjectMeta: metav1.ObjectMeta{Name: "agent-env", Namespace: namespace},
Spec: v1alpha2.AgentSpec{
Type: v1alpha2.AgentType_Declarative,
Declarative: &v1alpha2.DeclarativeAgentSpec{
SystemMessage: "test",
ModelConfig: modelName,
Deployment: &v1alpha2.DeclarativeDeploymentSpec{
SharedDeploymentSpec: v1alpha2.SharedDeploymentSpec{
Env: []corev1.EnvVar{
{Name: "CUSTOM_VAR", Value: "custom-value"},
},
},
},
},
Skills: &v1alpha2.SkillForAgent{
Refs: []string{"ghcr.io/org/skill:v1"},
},
},
},
wantDefaultResources: true,
wantEnvContains: []string{"CUSTOM_VAR", "KAGENT_SKILLS_FOLDER"},
},
}

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)

var deployment *appsv1.Deployment
for _, obj := range outputs.Manifest {
if d, ok := obj.(*appsv1.Deployment); ok {
deployment = d
}
}
require.NotNil(t, deployment)

var initContainer *corev1.Container
for i := range deployment.Spec.Template.Spec.InitContainers {
if deployment.Spec.Template.Spec.InitContainers[i].Name == "skills-init" {
initContainer = &deployment.Spec.Template.Spec.InitContainers[i]
}
}
require.NotNil(t, initContainer, "skills-init container should exist")

// Check resources
if tt.wantDefaultResources {
assert.Equal(t, resource.MustParse("100m"), initContainer.Resources.Requests[corev1.ResourceCPU])
assert.Equal(t, resource.MustParse("384Mi"), initContainer.Resources.Requests[corev1.ResourceMemory])
assert.Equal(t, resource.MustParse("2000m"), initContainer.Resources.Limits[corev1.ResourceCPU])
assert.Equal(t, resource.MustParse("1Gi"), initContainer.Resources.Limits[corev1.ResourceMemory])
} else {
assert.Equal(t, tt.wantResources, initContainer.Resources)
}

// Check securityContext
if tt.wantSecurityContext != nil {
require.NotNil(t, initContainer.SecurityContext)
assert.Equal(t, tt.wantSecurityContext, initContainer.SecurityContext)
}

// Check env vars
if len(tt.wantEnvContains) > 0 {
envNames := make(map[string]bool)
for _, e := range initContainer.Env {
envNames[e.Name] = true
}
for _, name := range tt.wantEnvContains {
assert.True(t, envNames[name], "init container should have env var %s", name)
}
}
})
}
}
Loading
Loading