Skip to content

Skip copy for given container registry domain #865

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ A mutating webhook for Kubernetes, pointing the images to a new location.`,
webhook.ImageSwapPolicy(imageSwapPolicy),
webhook.ImageCopyPolicy(imageCopyPolicy),
webhook.ImageCopyDeadline(imageCopyDeadline),
webhook.ImageCopySkipRegistries(cfg.ImageCopySkipRegistries),
)
if err != nil {
log.Err(err).Msg("error creating webhook")
Expand Down
9 changes: 5 additions & 4 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ type Config struct {

ListenAddress string

DryRun bool `yaml:"dryRun"`
ImageSwapPolicy string `yaml:"imageSwapPolicy" validate:"oneof=always exists"`
ImageCopyPolicy string `yaml:"imageCopyPolicy" validate:"oneof=delayed immediate force none"`
ImageCopyDeadline time.Duration `yaml:"imageCopyDeadline"`
DryRun bool `yaml:"dryRun"`
ImageSwapPolicy string `yaml:"imageSwapPolicy" validate:"oneof=always exists"`
ImageCopyPolicy string `yaml:"imageCopyPolicy" validate:"oneof=delayed immediate force none"`
ImageCopyDeadline time.Duration `yaml:"imageCopyDeadline"`
ImageCopySkipRegistries []string `yaml:"skipRegistries"`

Source Source `yaml:"source"`
Target Registry `yaml:"target"`
Expand Down
83 changes: 54 additions & 29 deletions pkg/webhook/image_swapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"time"

"github.com/alitto/pond"
Expand Down Expand Up @@ -61,6 +62,13 @@ func ImageCopyDeadline(deadline time.Duration) Option {
}
}

// ImageCopySkipRegistries allows to pass the skipRegistries option
func ImageCopySkipRegistries(registries []string) Option {
return func(swapper *ImageSwapper) {
swapper.imageCopySkipRegistries = registries
}
}

// Copier allows to pass the copier option
func Copier(pool *pond.WorkerPool) Option {
return func(swapper *ImageSwapper) {
Expand All @@ -83,10 +91,12 @@ type ImageSwapper struct {

imageSwapPolicy types.ImageSwapPolicy
imageCopyPolicy types.ImageCopyPolicy

imageCopySkipRegistries []string
}

// NewImageSwapper returns a new ImageSwapper initialized.
func NewImageSwapper(registryClient registry.Client, imagePullSecretProvider secrets.ImagePullSecretsProvider, filters []config.JMESPathFilter, imageSwapPolicy types.ImageSwapPolicy, imageCopyPolicy types.ImageCopyPolicy, imageCopyDeadline time.Duration) kwhmutating.Mutator {
func NewImageSwapper(registryClient registry.Client, imagePullSecretProvider secrets.ImagePullSecretsProvider, filters []config.JMESPathFilter, imageSwapPolicy types.ImageSwapPolicy, imageCopyPolicy types.ImageCopyPolicy, imageCopyDeadline time.Duration, skipRegistries []string) kwhmutating.Mutator {
return &ImageSwapper{
registryClient: registryClient,
imagePullSecretProvider: imagePullSecretProvider,
Expand All @@ -95,6 +105,7 @@ func NewImageSwapper(registryClient registry.Client, imagePullSecretProvider sec
imageSwapPolicy: imageSwapPolicy,
imageCopyPolicy: imageCopyPolicy,
imageCopyDeadline: imageCopyDeadline,
imageCopySkipRegistries: skipRegistries,
}
}

Expand All @@ -106,6 +117,7 @@ func NewImageSwapperWithOpts(registryClient registry.Client, opts ...Option) kwh
filters: []config.JMESPathFilter{},
imageSwapPolicy: types.ImageSwapPolicyExists,
imageCopyPolicy: types.ImageCopyPolicyDelayed,
imageCopySkipRegistries: []string{},
}

for _, opt := range opts {
Expand All @@ -132,8 +144,8 @@ func NewImageSwapperWebhookWithOpts(registryClient registry.Client, opts ...Opti
return kwhmutating.NewWebhook(mcfg)
}

func NewImageSwapperWebhook(registryClient registry.Client, imagePullSecretProvider secrets.ImagePullSecretsProvider, filters []config.JMESPathFilter, imageSwapPolicy types.ImageSwapPolicy, imageCopyPolicy types.ImageCopyPolicy, imageCopyDeadline time.Duration) (webhook.Webhook, error) {
imageSwapper := NewImageSwapper(registryClient, imagePullSecretProvider, filters, imageSwapPolicy, imageCopyPolicy, imageCopyDeadline)
func NewImageSwapperWebhook(registryClient registry.Client, imagePullSecretProvider secrets.ImagePullSecretsProvider, filters []config.JMESPathFilter, imageSwapPolicy types.ImageSwapPolicy, imageCopyPolicy types.ImageCopyPolicy, imageCopyDeadline time.Duration, skipRegistries []string) (webhook.Webhook, error) {
imageSwapper := NewImageSwapper(registryClient, imagePullSecretProvider, filters, imageSwapPolicy, imageCopyPolicy, imageCopyDeadline, skipRegistries)
mt := kwhmutating.MutatorFunc(imageSwapper.Mutate)
mcfg := kwhmutating.WebhookConfig{
ID: "k8s-image-swapper",
Expand Down Expand Up @@ -211,34 +223,47 @@ func (p *ImageSwapper) Mutate(ctx context.Context, ar *kwhmodel.AdmissionReview,
targetRef := p.targetRef(srcRef)
targetImage := targetRef.DockerReference().String()

imageCopierLogger := logger.With().
Str("source-image", srcRef.DockerReference().String()).
Str("target-image", targetImage).
Logger()

imageCopierContext := imageCopierLogger.WithContext(lctx)
// create an object responsible for the image copy
imageCopier := ImageCopier{
sourcePod: pod,
sourceImageRef: srcRef,
targetImageRef: targetRef,
imagePullPolicy: container.ImagePullPolicy,
imageSwapper: p,
context: imageCopierContext,
// check if the registry domain is in the skip list and skip the image if it is - used when the private registry already has configured pull through cache for the source registry
domain := reference.Domain(srcRef.DockerReference())
makeCopy := true
for _, skipRegistry := range p.imageCopySkipRegistries {
if strings.EqualFold(domain, skipRegistry) {
log.Ctx(lctx).Debug().Str("registry", domain).Msg("skip due to source registry being in the skip list")
makeCopy = false
break
}
}

// imageCopyPolicy
switch p.imageCopyPolicy {
case types.ImageCopyPolicyDelayed:
p.copier.Submit(imageCopier.start)
case types.ImageCopyPolicyImmediate:
p.copier.SubmitAndWait(imageCopier.withDeadline().start)
case types.ImageCopyPolicyForce:
imageCopier.withDeadline().start()
case types.ImageCopyPolicyNone:
// do not copy image
default:
panic("unknown imageCopyPolicy")
if makeCopy {
imageCopierLogger := logger.With().
Str("source-image", srcRef.DockerReference().String()).
Str("target-image", targetImage).
Logger()

imageCopierContext := imageCopierLogger.WithContext(lctx)
// create an object responsible for the image copy
imageCopier := ImageCopier{
sourcePod: pod,
sourceImageRef: srcRef,
targetImageRef: targetRef,
imagePullPolicy: container.ImagePullPolicy,
imageSwapper: p,
context: imageCopierContext,
}

// imageCopyPolicy
switch p.imageCopyPolicy {
case types.ImageCopyPolicyDelayed:
p.copier.Submit(imageCopier.start)
case types.ImageCopyPolicyImmediate:
p.copier.SubmitAndWait(imageCopier.withDeadline().start)
case types.ImageCopyPolicyForce:
imageCopier.withDeadline().start()
case types.ImageCopyPolicyNone:
// do not copy image
default:
panic("unknown imageCopyPolicy")
}
}

// imageSwapPolicy
Expand Down
71 changes: 71 additions & 0 deletions pkg/webhook/image_swapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ func TestImageSwapper_Mutate(t *testing.T) {
copier.StopAndWait()

ecrClient.AssertExpectations(t)
assert.Equal(t, uint64(4), copier.SubmittedTasks())
}

// TestImageSwapper_MutateWithImagePullSecrets tests mutating with imagePullSecret support
Expand Down Expand Up @@ -391,6 +392,7 @@ func TestImageSwapper_MutateWithImagePullSecrets(t *testing.T) {
copier.StopAndWait()

ecrClient.AssertExpectations(t)
assert.Equal(t, uint64(1), copier.SubmittedTasks())
}

func TestImageSwapper_GAR_Mutate(t *testing.T) {
Expand Down Expand Up @@ -422,4 +424,73 @@ func TestImageSwapper_GAR_Mutate(t *testing.T) {
assert.JSONEq(t, expected, string(resp.(*model.MutatingAdmissionResponse).JSONPatchPatch))
assert.Nil(t, resp.(*model.MutatingAdmissionResponse).Warnings)
assert.NoError(t, err, "Webhook executed without errors")
assert.Equal(t, uint64(4), copier.SubmittedTasks())
}

func TestImageSwapper_skipRegistryGcrIo_Mutate(t *testing.T) {
registryClient, _ := registry.NewMockGARClient(nil, "us-central1-docker.pkg.dev/gcp-project-123/main")

admissionReview, _ := readAdmissionReviewFromFile("admissionreview-simple.json")
admissionReviewModel := model.NewAdmissionReviewV1(admissionReview)

copier := pond.New(1, 1)
wh, err := NewImageSwapperWebhookWithOpts(
registryClient,
Copier(copier),
ImageSwapPolicy(types.ImageSwapPolicyAlways),
ImageCopySkipRegistries([]string{"k8s.gcr.io"}),
)

assert.NoError(t, err, "NewImageSwapperWebhookWithOpts executed without errors")

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

// container with name "skip-test-gar" should be skipped, hence there is no "replace" operation for it
expected := `[
{"op":"replace","path":"/spec/initContainers/0/image","value":"us-central1-docker.pkg.dev/gcp-project-123/main/docker.io/library/init-container:latest"},
{"op":"replace","path":"/spec/containers/0/image","value":"us-central1-docker.pkg.dev/gcp-project-123/main/docker.io/library/nginx:latest"},
{"op":"replace","path":"/spec/containers/1/image","value":"us-central1-docker.pkg.dev/gcp-project-123/main/k8s.gcr.io/ingress-nginx/controller@sha256:9bba603b99bf25f6d117cf1235b6598c16033ad027b143c90fa5b3cc583c5713"},
{"op":"replace","path":"/spec/containers/2/image","value":"us-central1-docker.pkg.dev/gcp-project-123/main/123456789.dkr.ecr.ap-southeast-2.amazonaws.com/k8s.gcr.io/ingress-nginx/controller@sha256:9bba603b99bf25f6d117cf1235b6598c16033ad027b143c90fa5b3cc583c5713"}
]`

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

// check the amount of submitted tasks for the copier
assert.Equal(t, uint64(3), copier.SubmittedTasks())
}

func TestImageSwapper_skipRegistryDockerIo_Mutate(t *testing.T) {
registryClient, _ := registry.NewMockGARClient(nil, "us-central1-docker.pkg.dev/gcp-project-123/main")

admissionReview, _ := readAdmissionReviewFromFile("admissionreview-simple.json")
admissionReviewModel := model.NewAdmissionReviewV1(admissionReview)

copier := pond.New(1, 1)
wh, err := NewImageSwapperWebhookWithOpts(
registryClient,
Copier(copier),
ImageSwapPolicy(types.ImageSwapPolicyAlways),
ImageCopySkipRegistries([]string{"docker.io"}),
)

assert.NoError(t, err, "NewImageSwapperWebhookWithOpts executed without errors")

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

// container with name "skip-test-gar" should be skipped, hence there is no "replace" operation for it
expected := `[
{"op":"replace","path":"/spec/initContainers/0/image","value":"us-central1-docker.pkg.dev/gcp-project-123/main/docker.io/library/init-container:latest"},
{"op":"replace","path":"/spec/containers/0/image","value":"us-central1-docker.pkg.dev/gcp-project-123/main/docker.io/library/nginx:latest"},
{"op":"replace","path":"/spec/containers/1/image","value":"us-central1-docker.pkg.dev/gcp-project-123/main/k8s.gcr.io/ingress-nginx/controller@sha256:9bba603b99bf25f6d117cf1235b6598c16033ad027b143c90fa5b3cc583c5713"},
{"op":"replace","path":"/spec/containers/2/image","value":"us-central1-docker.pkg.dev/gcp-project-123/main/123456789.dkr.ecr.ap-southeast-2.amazonaws.com/k8s.gcr.io/ingress-nginx/controller@sha256:9bba603b99bf25f6d117cf1235b6598c16033ad027b143c90fa5b3cc583c5713"}
]`

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

// check the amount of submitted tasks for the copier
assert.Equal(t, uint64(2), copier.SubmittedTasks())
}
Loading