Skip to content

Commit 2689d6c

Browse files
committed
feat: add imagePullSecrets support for container-based skills
Add authentication support for pulling skill images from private registries (Artifactory, ACR, ECR, etc.) by introducing a new imagePullSecrets field under spec.skills. When imagePullSecrets is set, a docker-auth-init init container is prepended that merges all kubernetes.io/dockerconfigjson secrets into a single config.json using jq. The skills-init container then reads that config via the DOCKER_CONFIG env var, which krane picks up automatically when pulling skill images. Closes #1222 Signed-off-by: ppeau <patrice.peau@gmail.com>
1 parent fb611b0 commit 2689d6c

11 files changed

Lines changed: 486 additions & 7 deletions

File tree

docker/skills-init/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ FROM alpine:3.23
1717
ARG PYTHON_UID=1001
1818
ARG PYTHON_GID=1001
1919

20-
RUN apk upgrade --no-cache && apk add --no-cache git
20+
RUN apk upgrade --no-cache && apk add --no-cache git jq
2121
COPY --from=krane-builder /build/krane /usr/local/bin/krane
2222

2323
# Run as the same UID/GID as the main agent container (python user) so that

go/api/config/crd/bases/kagent.dev_agents.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13319,6 +13319,31 @@ spec:
1331913319
maxItems: 20
1332013320
minItems: 1
1332113321
type: array
13322+
imagePullSecrets:
13323+
description: |-
13324+
ImagePullSecrets is a list of references to secrets in the same namespace to use for
13325+
pulling skill images from private registries. Each referenced secret must be of type
13326+
kubernetes.io/dockerconfigjson. The credentials from all secrets are merged and made
13327+
available to the skills-init container at /.kagent/.docker/config.json; krane will
13328+
use them automatically when pulling images.
13329+
items:
13330+
description: |-
13331+
LocalObjectReference contains enough information to let you locate the
13332+
referenced object inside the same namespace.
13333+
properties:
13334+
name:
13335+
default: ""
13336+
description: |-
13337+
Name of the referent.
13338+
This field is effectively required, but due to backwards compatibility is
13339+
allowed to be empty. Instances of this type with an empty value here are
13340+
almost certainly wrong.
13341+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
13342+
type: string
13343+
type: object
13344+
x-kubernetes-map-type: atomic
13345+
maxItems: 20
13346+
type: array
1332213347
initContainer:
1332313348
description: Configuration for the skills-init init container.
1332413349
properties:

go/api/config/crd/bases/kagent.dev_sandboxagents.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10969,6 +10969,31 @@ spec:
1096910969
maxItems: 20
1097010970
minItems: 1
1097110971
type: array
10972+
imagePullSecrets:
10973+
description: |-
10974+
ImagePullSecrets is a list of references to secrets in the same namespace to use for
10975+
pulling skill images from private registries. Each referenced secret must be of type
10976+
kubernetes.io/dockerconfigjson. The credentials from all secrets are merged and made
10977+
available to the skills-init container at /.kagent/.docker/config.json; krane will
10978+
use them automatically when pulling images.
10979+
items:
10980+
description: |-
10981+
LocalObjectReference contains enough information to let you locate the
10982+
referenced object inside the same namespace.
10983+
properties:
10984+
name:
10985+
default: ""
10986+
description: |-
10987+
Name of the referent.
10988+
This field is effectively required, but due to backwards compatibility is
10989+
allowed to be empty. Instances of this type with an empty value here are
10990+
almost certainly wrong.
10991+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
10992+
type: string
10993+
type: object
10994+
x-kubernetes-map-type: atomic
10995+
maxItems: 20
10996+
type: array
1097210997
initContainer:
1097310998
description: Configuration for the skills-init init container.
1097410999
properties:

go/api/v1alpha2/agent_types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ type SkillForAgent struct {
9797
// +optional
9898
Refs []string `json:"refs,omitempty"`
9999

100+
// ImagePullSecrets is a list of references to secrets in the same namespace to use for
101+
// pulling skill images from private registries. Each referenced secret must be of type
102+
// kubernetes.io/dockerconfigjson. The credentials from all secrets are merged and made
103+
// available to the skills-init container at /.kagent/.docker/config.json; krane will
104+
// use them automatically when pulling images.
105+
// +optional
106+
// +kubebuilder:validation:MaxItems=20
107+
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
108+
100109
// Reference to a Secret containing git credentials.
101110
// Applied to all gitRefs entries.
102111
// The secret should contain a `token` key for HTTPS auth,

go/api/v1alpha2/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go/core/internal/controller/translator/agent/adk_api_translator.go

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,6 +1324,9 @@ func prepareSkillsInitData(
13241324
// buildSkillsInitContainer creates the unified init container and associated volumes
13251325
// for fetching skills from both Git repositories and OCI registries.
13261326
// If authSecretRef is non-nil a single Secret volume is created and mounted at /git-auth.
1327+
// If imagePullSecrets is non-empty, a docker-auth-init container is prepended that merges
1328+
// all kubernetes.io/dockerconfigjson secrets into /.kagent/.docker/config.json; krane
1329+
// reads the directory via the DOCKER_CONFIG env var and uses those credentials automatically.
13271330
func buildSkillsInitContainer(
13281331
gitRefs []v1alpha2.GitRepo,
13291332
authSecretRef *corev1.LocalObjectReference,
@@ -1332,14 +1335,15 @@ func buildSkillsInitContainer(
13321335
securityContext *corev1.SecurityContext,
13331336
env []corev1.EnvVar,
13341337
resources corev1.ResourceRequirements,
1335-
) (container corev1.Container, volumes []corev1.Volume, err error) {
1338+
imagePullSecrets []corev1.LocalObjectReference,
1339+
) (containers []corev1.Container, volumes []corev1.Volume, err error) {
13361340
data, err := prepareSkillsInitData(gitRefs, authSecretRef, ociRefs, insecureOCI)
13371341
if err != nil {
1338-
return corev1.Container{}, nil, err
1342+
return nil, nil, err
13391343
}
13401344
script, err := buildSkillsScript(data)
13411345
if err != nil {
1342-
return corev1.Container{}, nil, err
1346+
return nil, nil, err
13431347
}
13441348
initSecCtx := securityContext
13451349
if initSecCtx != nil {
@@ -1367,7 +1371,57 @@ func buildSkillsInitContainer(
13671371
})
13681372
}
13691373

1370-
container = corev1.Container{
1374+
// If imagePullSecrets are specified, build a docker-auth-init container that merges all
1375+
// kubernetes.io/dockerconfigjson secrets into a single config.json using jq, then mount
1376+
// the result into the skills-init container so krane can authenticate to private registries.
1377+
if len(imagePullSecrets) > 0 {
1378+
// Shared EmptyDir volume for the merged Docker config.
1379+
volumes = append(volumes, corev1.Volume{
1380+
Name: "kagent-docker-config",
1381+
VolumeSource: corev1.VolumeSource{
1382+
EmptyDir: &corev1.EmptyDirVolumeSource{},
1383+
},
1384+
})
1385+
1386+
// Mount each imagePullSecret as a read-only directory under /docker-secrets/<name>.
1387+
authInitVolumeMounts := []corev1.VolumeMount{
1388+
{Name: "kagent-docker-config", MountPath: "/docker-config-out"},
1389+
}
1390+
for _, secret := range imagePullSecrets {
1391+
volName := "pull-secret-" + secret.Name
1392+
volumes = append(volumes, corev1.Volume{
1393+
Name: volName,
1394+
VolumeSource: corev1.VolumeSource{
1395+
Secret: &corev1.SecretVolumeSource{
1396+
SecretName: secret.Name,
1397+
},
1398+
},
1399+
})
1400+
authInitVolumeMounts = append(authInitVolumeMounts, corev1.VolumeMount{
1401+
Name: volName,
1402+
MountPath: "/docker-secrets/" + secret.Name,
1403+
ReadOnly: true,
1404+
})
1405+
}
1406+
1407+
mergeScript := buildDockerAuthMergeScript(imagePullSecrets)
1408+
dockerAuthInitContainer := corev1.Container{
1409+
Name: "docker-auth-init",
1410+
Image: DefaultSkillsInitImageConfig.Image(),
1411+
Command: []string{"/bin/sh", "-c", mergeScript},
1412+
VolumeMounts: authInitVolumeMounts,
1413+
}
1414+
containers = append(containers, dockerAuthInitContainer)
1415+
1416+
// Mount the merged config into skills-init so krane picks it up via DOCKER_CONFIG.
1417+
volumeMounts = append(volumeMounts, corev1.VolumeMount{
1418+
Name: "kagent-docker-config",
1419+
MountPath: "/.kagent/.docker",
1420+
ReadOnly: true,
1421+
})
1422+
}
1423+
1424+
skillsInitContainer := corev1.Container{
13711425
Name: "skills-init",
13721426
Image: DefaultSkillsInitImageConfig.Image(),
13731427
Command: []string{"/bin/sh", "-c", script},
@@ -1377,7 +1431,36 @@ func buildSkillsInitContainer(
13771431
Resources: resources,
13781432
}
13791433

1380-
return container, volumes, nil
1434+
// If a merged Docker config is available, point krane to it via DOCKER_CONFIG.
1435+
if len(imagePullSecrets) > 0 {
1436+
skillsInitContainer.Env = append(skillsInitContainer.Env, corev1.EnvVar{
1437+
Name: "DOCKER_CONFIG",
1438+
Value: "/.kagent/.docker",
1439+
})
1440+
}
1441+
1442+
containers = append(containers, skillsInitContainer)
1443+
return containers, volumes, nil
1444+
}
1445+
1446+
// buildDockerAuthMergeScript generates a shell script that merges the .auths sections from
1447+
// all kubernetes.io/dockerconfigjson secrets (mounted under /docker-secrets/<name>/) into a
1448+
// single Docker config.json at /docker-config-out/config.json using jq.
1449+
func buildDockerAuthMergeScript(imagePullSecrets []corev1.LocalObjectReference) string {
1450+
var sb strings.Builder
1451+
sb.WriteString(`set -e
1452+
mkdir -p /docker-config-out
1453+
merged='{"auths":{}}'
1454+
`)
1455+
for _, secret := range imagePullSecrets {
1456+
sb.WriteString(`if [ -f /docker-secrets/` + secret.Name + `/.dockerconfigjson ]; then
1457+
merged="$(printf '%s\n%s\n' "$merged" "$(cat /docker-secrets/` + secret.Name + `/.dockerconfigjson)" | jq -s '.[0].auths * .[1].auths | {"auths": .}')"
1458+
fi
1459+
`)
1460+
}
1461+
sb.WriteString(`printf '%s' "$merged" > /docker-config-out/config.json
1462+
`)
1463+
return sb.String()
13811464
}
13821465

13831466
func (a *adkApiTranslator) runPlugins(ctx context.Context, agent v1alpha2.AgentObject, outputs *AgentOutputs) error {

0 commit comments

Comments
 (0)