Skip to content

Commit

Permalink
feat: Support for pod.spec.initContainers (#118)
Browse files Browse the repository at this point in the history
* feat: Support for pod.spec.initContainers

Adding support for containers in `pod.spec.initContainers`, swapping the image
in the same way as `pod.spec.containers`.

fixes #73 #96
  • Loading branch information
estahn committed Oct 2, 2021
1 parent 1ddcef7 commit 725ff2c
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 81 deletions.
9 changes: 8 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ jobs:

- name: Scan image
uses: anchore/scan-action@v3
id: scan
with:
image: "ghcr.io/estahn/k8s-image-swapper:latest"
fail-build: true
fail-build: false
acs-report-enable: true

- name: Upload Anchore scan SARIF report
uses: github/codeql-action/upload-sarif@v1
with:
sarif_file: ${{ steps.scan.outputs.sarif }}
162 changes: 83 additions & 79 deletions pkg/webhook/image_swapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,97 +155,101 @@ func (p *ImageSwapper) Mutate(ctx context.Context, ar *kwhmodel.AdmissionReview,
lctx := logger.
WithContext(ctx)

for i, container := range pod.Spec.Containers {
srcRef, err := alltransports.ParseImageName("docker://" + container.Image)
if err != nil {
log.Ctx(lctx).Warn().Msgf("invalid source name %s: %v", container.Image, err)
continue
}

// skip if the source and target registry domain are equal (e.g. same ECR registries)
if domain := reference.Domain(srcRef.DockerReference()); domain == p.registryClient.Endpoint() {
continue
}

filterCtx := NewFilterContext(*ar, pod, container)
if filterMatch(filterCtx, p.filters) {
log.Ctx(lctx).Debug().Msg("skip due to filter condition")
continue
}

targetImage := p.targetName(srcRef)

copyFn := func() {
// Avoid unnecessary copying by ending early. For images such as :latest we adhere to the
// image pull policy.
if p.registryClient.ImageExists(targetImage) && container.ImagePullPolicy != corev1.PullAlways {
return
}

// Create repository
createRepoName := reference.TrimNamed(srcRef.DockerReference()).String()
log.Ctx(lctx).Debug().Str("repository", createRepoName).Msg("create repository")
if err := p.registryClient.CreateRepository(createRepoName); err != nil {
log.Err(err)
}

// Retrieve secrets and auth credentials
imagePullSecrets, err := p.imagePullSecretProvider.GetImagePullSecrets(pod)
containerSets := []*[]corev1.Container{&pod.Spec.Containers, &pod.Spec.InitContainers}
for _, containerSet := range containerSets {
containers := *containerSet
for i, container := range containers {
srcRef, err := alltransports.ParseImageName("docker://" + container.Image)
if err != nil {
log.Err(err)
log.Ctx(lctx).Warn().Msgf("invalid source name %s: %v", container.Image, err)
continue
}

authFile, err := imagePullSecrets.AuthFile()
if authFile != nil {
defer func() {
if err := os.RemoveAll(authFile.Name()); err != nil {
log.Err(err)
}
}()
// skip if the source and target registry domain are equal (e.g. same ECR registries)
if domain := reference.Domain(srcRef.DockerReference()); domain == p.registryClient.Endpoint() {
continue
}

if err != nil {
log.Err(err)
filterCtx := NewFilterContext(*ar, pod, container)
if filterMatch(filterCtx, p.filters) {
log.Ctx(lctx).Debug().Msg("skip due to filter condition")
continue
}

// Copy image
// TODO: refactor to use structure instead of passing file name / string
// or transform registryClient creds into auth compatible form, e.g.
// {"auths":{"aws_account_id.dkr.ecr.region.amazonaws.com":{"username":"AWS","password":"..." }}}
log.Ctx(lctx).Trace().Str("source", srcRef.DockerReference().String()).Str("target", targetImage).Msg("copy image")
if err := copyImage(srcRef.DockerReference().String(), authFile.Name(), targetImage, p.registryClient.Credentials()); err != nil {
log.Ctx(lctx).Err(err).Str("source", srcRef.DockerReference().String()).Str("target", targetImage).Msg("copying image to target registry failed")
targetImage := p.targetName(srcRef)

copyFn := func() {
// Avoid unnecessary copying by ending early. For images such as :latest we adhere to the
// image pull policy.
if p.registryClient.ImageExists(targetImage) && container.ImagePullPolicy != corev1.PullAlways {
return
}

// Create repository
createRepoName := reference.TrimNamed(srcRef.DockerReference()).String()
log.Ctx(lctx).Debug().Str("repository", createRepoName).Msg("create repository")
if err := p.registryClient.CreateRepository(createRepoName); err != nil {
log.Err(err)
}

// Retrieve secrets and auth credentials
imagePullSecrets, err := p.imagePullSecretProvider.GetImagePullSecrets(pod)
if err != nil {
log.Err(err)
}

authFile, err := imagePullSecrets.AuthFile()
if authFile != nil {
defer func() {
if err := os.RemoveAll(authFile.Name()); err != nil {
log.Err(err)
}
}()
}

if err != nil {
log.Err(err)
}

// Copy image
// TODO: refactor to use structure instead of passing file name / string
// or transform registryClient creds into auth compatible form, e.g.
// {"auths":{"aws_account_id.dkr.ecr.region.amazonaws.com":{"username":"AWS","password":"..." }}}
log.Ctx(lctx).Trace().Str("source", srcRef.DockerReference().String()).Str("target", targetImage).Msg("copy image")
if err := copyImage(srcRef.DockerReference().String(), authFile.Name(), targetImage, p.registryClient.Credentials()); err != nil {
log.Ctx(lctx).Err(err).Str("source", srcRef.DockerReference().String()).Str("target", targetImage).Msg("copying image to target registry failed")
}
}
}

// imageCopyPolicy
switch p.imageCopyPolicy {
case types.ImageCopyPolicyDelayed:
p.copier.Submit(copyFn)
case types.ImageCopyPolicyImmediate:
// TODO: Implement deadline
p.copier.SubmitAndWait(copyFn)
case types.ImageCopyPolicyForce:
// TODO: Implement deadline
copyFn()
default:
panic("unknown imageCopyPolicy")
}
// imageCopyPolicy
switch p.imageCopyPolicy {
case types.ImageCopyPolicyDelayed:
p.copier.Submit(copyFn)
case types.ImageCopyPolicyImmediate:
// TODO: Implement deadline
p.copier.SubmitAndWait(copyFn)
case types.ImageCopyPolicyForce:
// TODO: Implement deadline
copyFn()
default:
panic("unknown imageCopyPolicy")
}

// imageSwapPolicy
switch p.imageSwapPolicy {
case types.ImageSwapPolicyAlways:
log.Ctx(lctx).Debug().Str("image", targetImage).Msg("set new container image")
pod.Spec.Containers[i].Image = targetImage
case types.ImageSwapPolicyExists:
if p.registryClient.ImageExists(targetImage) {
// imageSwapPolicy
switch p.imageSwapPolicy {
case types.ImageSwapPolicyAlways:
log.Ctx(lctx).Debug().Str("image", targetImage).Msg("set new container image")
pod.Spec.Containers[i].Image = targetImage
} else {
log.Ctx(lctx).Debug().Str("image", targetImage).Msg("container image not found in target registry, not swapping")
containers[i].Image = targetImage
case types.ImageSwapPolicyExists:
if p.registryClient.ImageExists(targetImage) {
log.Ctx(lctx).Debug().Str("image", targetImage).Msg("set new container image")
containers[i].Image = targetImage
} else {
log.Ctx(lctx).Debug().Str("image", targetImage).Msg("container image not found in target registry, not swapping")
}
default:
panic("unknown imageSwapPolicy")
}
default:
panic("unknown imageSwapPolicy")
}
}

Expand Down
20 changes: 19 additions & 1 deletion pkg/webhook/image_swapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,19 @@ func TestImageSwapper_Mutate(t *testing.T) {
defer func() { execCommand = exec.Command }()

ecrClient := new(mockECRClient)
ecrClient.On(
"CreateRepository",
&ecr.CreateRepositoryInput{
ImageScanningConfiguration: &ecr.ImageScanningConfiguration{
ScanOnPush: aws.Bool(true),
},
ImageTagMutability: aws.String("MUTABLE"),
RepositoryName: aws.String("docker.io/library/init-container"),
Tags: []*ecr.Tag{{
Key: aws.String("CreatedBy"),
Value: aws.String("k8s-image-swapper"),
}},
}).Return(mock.Anything)
ecrClient.On(
"CreateRepository",
&ecr.CreateRepositoryInput{
Expand Down Expand Up @@ -268,7 +281,12 @@ func TestImageSwapper_Mutate(t *testing.T) {

resp, err := wh.Review(context.TODO(), admissionReviewModel)

assert.JSONEq(t, "[{\"op\":\"replace\",\"path\":\"/spec/containers/0/image\",\"value\":\"123456789.dkr.ecr.ap-southeast-2.amazonaws.com/docker.io/library/nginx:latest\"}]", string(resp.(*model.MutatingAdmissionResponse).JSONPatchPatch))
expected := `[
{"op":"replace","path":"/spec/initContainers/0/image","value":"123456789.dkr.ecr.ap-southeast-2.amazonaws.com/docker.io/library/init-container:latest"},
{"op":"replace","path":"/spec/containers/0/image","value":"123456789.dkr.ecr.ap-southeast-2.amazonaws.com/docker.io/library/nginx:latest"}
]`

assert.JSONEq(t, expected, string(resp.(*model.MutatingAdmissionResponse).JSONPatchPatch))
assert.Nil(t, resp.(*model.MutatingAdmissionResponse).Warnings)
assert.NoError(t, err, "Webhook executed without errors")

Expand Down
17 changes: 17 additions & 0 deletions test/requests/admissionreview-simple.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@
],
"dnsPolicy": "ClusterFirst",
"enableServiceLinks": true,
"initContainers": [
{
"image": "init-container",
"imagePullPolicy": "Always",
"name": "init-container28",
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"volumeMounts": [
{
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "default-token-fxbar",
"readOnly": true
}
]
}
],
"priority": 0,
"restartPolicy": "Never",
"schedulerName": "default-scheduler",
Expand Down

0 comments on commit 725ff2c

Please sign in to comment.