From cb2e6b286cb26256ee74a0aeaece34cdf7232ffe Mon Sep 17 00:00:00 2001 From: Rodrigo Fior Kuntzer Date: Mon, 1 Sep 2025 10:28:21 +0200 Subject: [PATCH] fix: allow docker image to be mutated by admission webhooks, change the registry and have the image name becoming a suffix of the final image used in the pod Signed-off-by: Rodrigo Fior Kuntzer --- go.mod | 4 +- go.sum | 8 +- pkg/utils/utils.go | 45 +++++++-- pkg/utils/utils_test.go | 209 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 253 insertions(+), 13 deletions(-) create mode 100644 pkg/utils/utils_test.go diff --git a/go.mod b/go.mod index 10e07e73..c5d36641 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/deckarep/golang-set/v2 v2.8.0 github.com/evanphx/json-patch v4.12.0+incompatible github.com/go-logr/logr v1.4.3 + github.com/google/go-containerregistry v0.20.3 github.com/onsi/ginkgo/v2 v2.22.2 github.com/onsi/gomega v1.36.2 github.com/prometheus/client_golang v1.21.1 @@ -70,6 +71,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect @@ -106,7 +108,7 @@ require ( golang.org/x/term v0.32.0 // indirect golang.org/x/text v0.26.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.33.0 // indirect + golang.org/x/tools v0.34.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect diff --git a/go.sum b/go.sum index af5dbb7a..e3585979 100644 --- a/go.sum +++ b/go.sum @@ -75,6 +75,8 @@ github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYu github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= +github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -119,6 +121,8 @@ github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -246,8 +250,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index b1074a5e..4685bec0 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -3,7 +3,6 @@ package utils import ( "encoding/hex" "fmt" - "regexp" "strconv" "strings" @@ -14,10 +13,12 @@ import ( asdbv1 "github.com/aerospike/aerospike-kubernetes-operator/v4/api/v1" "github.com/aerospike/aerospike-kubernetes-operator/v4/api/v1beta1" + registryname "github.com/google/go-containerregistry/pkg/name" ) const ( - DockerHubImagePrefix = "docker.io/" + DockerHubImagePrefix = "docker.io/" + DockerHubParsedRegistry = "index.docker.io" // ReasonImagePullBackOff when pod status is Pending as container image pull failed. ReasonImagePullBackOff = "ImagePullBackOff" @@ -63,24 +64,48 @@ func IsImageEqual(image1, image2 string) bool { desiredRegistry, desiredName, desiredVersion := ParseDockerImageTag(desiredImageWithVersion) actualRegistry, actualName, actualVersion := ParseDockerImageTag(actualImageWithVersion) - // registry name, image name and version should match. - return desiredRegistry == actualRegistry && desiredName == actualName && (desiredVersion == actualVersion || - (desiredVersion == ":latest" && actualVersion == "") || - (actualVersion == ":latest" && desiredVersion == "")) + // image version should match first + if desiredVersion == actualVersion || + (desiredVersion == "latest" && actualVersion == "") || + (actualVersion == "latest" && desiredVersion == "") { + // if either desired or actual registry is docker hub, but the registries don't match, + // then we allow the names to match if one is a suffix of the other. + // This is to allow for pull through cache registries that prepend their path to the + // image name. + // e.g. aerospike/aerospike-server-enterprise:8.1 should match + // 000000000000.dkr.ecr.some-region.amazonaws.com/docker-hub/aerospike/aerospike-server-enterprise:8.1 + if (desiredRegistry == DockerHubParsedRegistry || actualRegistry == DockerHubParsedRegistry) && + desiredRegistry != actualRegistry { + return strings.HasSuffix(desiredName, actualName) || strings.HasSuffix(actualName, desiredName) + } + + return desiredRegistry == actualRegistry && desiredName == actualName + } + + return false } // ParseDockerImageTag parses input tag into registry, name and version. func ParseDockerImageTag(tag string) ( registry string, name string, version string, ) { - if tag == "" { + // remove @sha256: digest if exists + digest := "" + if idx := strings.Index(tag, "@sha256:"); idx != -1 { + digest = tag[idx:] + tag = tag[:idx] + } + + ref, err := registryname.ParseReference(tag) + if err != nil { return "", "", "" } - r := regexp.MustCompile(`(?P[^/]+/)?(?P[^:]+)(?P:.+)?`) - matches := r.FindStringSubmatch(tag) + registry = ref.Context().RegistryStr() + name = ref.Context().RepositoryStr() + version = ref.Identifier() + digest // version can be tag or digest - return matches[1], matches[2], strings.TrimPrefix(matches[3], ":") + return registry, name, version } // IsPVCTerminating returns true if pvc's DeletionTimestamp has been set diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go new file mode 100644 index 00000000..3d48c4f7 --- /dev/null +++ b/pkg/utils/utils_test.go @@ -0,0 +1,209 @@ +package utils + +import "testing" + +func TestIsImageEqual(t *testing.T) { + type args struct { + image1 string + image2 string + } + + tests := []struct { + name string + args args + want bool + }{ + { + name: "same image with version", + args: args{ + image1: "aerospike/aerospike-server-enterprise:8.1", + image2: "aerospike/aerospike-server-enterprise:8.1", + }, + want: true, + }, + { + name: "same image without latest version", + args: args{ + image1: "aerospike/aerospike-server-enterprise:latest", + image2: "aerospike/aerospike-server-enterprise", + }, + want: true, + }, + { + name: "same image without latest version", + args: args{ + image1: "aerospike/aerospike-server-enterprise", + image2: "aerospike/aerospike-server-enterprise:latest", + }, + want: true, + }, + { + name: "same image without docker.io prefix", + args: args{ + image1: "docker.io/aerospike/aerospike-server-enterprise:8.1", + image2: "aerospike/aerospike-server-enterprise:8.1", + }, + want: true, + }, + { + name: "different image with version", + args: args{ + image1: "aerospike/aerospike-server-enterprise:8.1", + image2: "aerospike/aerospike-server-enterprise:8.0", + }, + want: false, + }, + { + name: "different image name", + args: args{ + image1: "aerospike/aerospike-server-enterprise:8.1", + image2: "aerospike/aerospike-server:8.1", + }, + want: false, + }, + { + name: "same image with pull through cache registry", + args: args{ + image1: "aerospike/aerospike-server-enterprise:8.1", + image2: "000000000000.dkr.ecr.some-region.amazonaws.com/docker-hub/aerospike/aerospike-server-enterprise:8.1", + }, + want: true, + }, + { + name: "same image with pull through cache registry", + args: args{ + image1: "000000000000.dkr.ecr.some-region.amazonaws.com/docker-hub/aerospike/aerospike-server-enterprise:8.1", + image2: "aerospike/aerospike-server-enterprise:8.1", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsImageEqual(tt.args.image1, tt.args.image2); got != tt.want { + t.Errorf("IsImageEqual() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParseDockerImageTag(t *testing.T) { + type args struct { + tag string + } + + tests := []struct { + name string + args args + wantRegistry string + wantName string + wantVersion string + }{ + { + name: "image without registry", + args: args{ + tag: "aerospike/aerospike-server-enterprise:8.1", + }, + wantRegistry: "index.docker.io", + wantName: "aerospike/aerospike-server-enterprise", + wantVersion: "8.1", + }, + { + name: "image with registry", + args: args{ + tag: "000000000000.dkr.ecr.some-region.amazonaws.com/docker-hub/aerospike/aerospike-server-enterprise:8.1", + }, + wantRegistry: "000000000000.dkr.ecr.some-region.amazonaws.com", + wantName: "docker-hub/aerospike/aerospike-server-enterprise", + wantVersion: "8.1", + }, + { + name: "image without version", + args: args{ + tag: "aerospike/aerospike-server-enterprise", + }, + wantRegistry: "index.docker.io", + wantName: "aerospike/aerospike-server-enterprise", + wantVersion: "latest", + }, + { + name: "empty image", + args: args{ + tag: "", + }, + wantRegistry: "", + wantName: "", + wantVersion: "", + }, + { + name: "version with digest", + args: args{ + tag: "aerospike/aerospike-server-enterprise:8.1@sha256:abcdef", + }, + wantRegistry: "index.docker.io", + wantName: "aerospike/aerospike-server-enterprise", + wantVersion: "8.1@sha256:abcdef", + }, + { + name: "digest without version", + args: args{ + tag: "aerospike/aerospike-server-enterprise@sha256:abcdef", + }, + wantRegistry: "index.docker.io", + wantName: "aerospike/aerospike-server-enterprise", + wantVersion: "latest@sha256:abcdef", + }, + { + name: "registry with port", + args: args{ + tag: "my-registry.com:5000/aerospike/aerospike-server-enterprise:8.1", + }, + wantRegistry: "my-registry.com:5000", + wantName: "aerospike/aerospike-server-enterprise", + wantVersion: "8.1", + }, + { + name: "registry with ip", + args: args{ + tag: "127.0.0.1:5000/aerospike/aerospike-server-enterprise:8.1", + }, + wantRegistry: "127.0.0.1:5000", + wantName: "aerospike/aerospike-server-enterprise", + wantVersion: "8.1", + }, + { + name: "localhost registry", + args: args{ + tag: "localhost:5000/aerospike/aerospike-server-enterprise:8.1", + }, + wantRegistry: "localhost:5000", + wantName: "aerospike/aerospike-server-enterprise", + wantVersion: "8.1", + }, + { + name: "library image", + args: args{ + tag: "aerospike-server-enterprise:8.1", + }, + wantRegistry: "index.docker.io", + wantName: "library/aerospike-server-enterprise", + wantVersion: "8.1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotRegistry, gotName, gotVersion := ParseDockerImageTag(tt.args.tag) + if gotRegistry != tt.wantRegistry { + t.Errorf("ParseDockerImageTag() gotRegistry = %v, want %v", gotRegistry, tt.wantRegistry) + } + + if gotName != tt.wantName { + t.Errorf("ParseDockerImageTag() gotName = %v, want %v", gotName, tt.wantName) + } + + if gotVersion != tt.wantVersion { + t.Errorf("ParseDockerImageTag() gotVersion = %v, want %v", gotVersion, tt.wantVersion) + } + }) + } +}