From 9656037af4ac0f923cb21b9fed83977d8ac19737 Mon Sep 17 00:00:00 2001 From: Alexandr Demicev Date: Thu, 20 Nov 2025 10:49:44 +0100 Subject: [PATCH] Allow overriding image name in clusterctl config Signed-off-by: Alexandr Demicev --- .../client/cluster/cert_manager_test.go | 4 +- .../client/config/imagemeta_client.go | 9 ++ .../client/config/imagemeta_client_test.go | 98 ++++++++++++++----- cmd/clusterctl/internal/test/fake_reader.go | 4 +- docs/book/src/clusterctl/configuration.md | 27 +++++ 5 files changed, 115 insertions(+), 27 deletions(-) diff --git a/cmd/clusterctl/client/cluster/cert_manager_test.go b/cmd/clusterctl/client/cluster/cert_manager_test.go index cab681d02acb..2da4c2257211 100644 --- a/cmd/clusterctl/client/cluster/cert_manager_test.go +++ b/cmd/clusterctl/client/cluster/cert_manager_test.go @@ -61,7 +61,7 @@ var certManagerNamespaceYaml = []byte("apiVersion: v1\n" + func Test_getManifestObjs(t *testing.T) { g := NewWithT(t) - defaultConfigClient, err := config.New(context.Background(), "", config.InjectReader(test.NewFakeReader().WithImageMeta(config.CertManagerImageComponent, "bar-repository.io", ""))) + defaultConfigClient, err := config.New(context.Background(), "", config.InjectReader(test.NewFakeReader().WithImageMeta(config.CertManagerImageComponent, "bar-repository.io", "", ""))) g.Expect(err).ToNot(HaveOccurred()) type fields struct { @@ -109,7 +109,7 @@ func Test_getManifestObjs(t *testing.T) { name: "successfully gets the cert-manager components for a custom release", fields: fields{ configClient: func() config.Client { - configClient, err := config.New(context.Background(), "", config.InjectReader(test.NewFakeReader().WithImageMeta(config.CertManagerImageComponent, "bar-repository.io", "").WithCertManager("", "v1.0.0", ""))) + configClient, err := config.New(context.Background(), "", config.InjectReader(test.NewFakeReader().WithImageMeta(config.CertManagerImageComponent, "bar-repository.io", "", "").WithCertManager("", "v1.0.0", ""))) g.Expect(err).ToNot(HaveOccurred()) return configClient }(), diff --git a/cmd/clusterctl/client/config/imagemeta_client.go b/cmd/clusterctl/client/config/imagemeta_client.go index 23654f6f5b1d..421e78522585 100644 --- a/cmd/clusterctl/client/config/imagemeta_client.go +++ b/cmd/clusterctl/client/config/imagemeta_client.go @@ -125,6 +125,9 @@ type imageMeta struct { // repository sets the container registry to pull images from. Repository string `json:"repository,omitempty"` + // Name allows to specify a different name for the image. + Name string `json:"name,omitempty"` + // Tag allows to specify a tag for the images. Tag string `json:"tag,omitempty"` } @@ -135,6 +138,9 @@ func (i *imageMeta) Union(other *imageMeta) { if other.Repository != "" { i.Repository = other.Repository } + if other.Name != "" { + i.Name = other.Name + } if other.Tag != "" { i.Tag = other.Tag } @@ -146,6 +152,9 @@ func (i *imageMeta) ApplyToImage(image container.Image) string { if i.Repository != "" { image.Repository = strings.TrimSuffix(i.Repository, "/") } + if i.Name != "" { + image.Name = i.Name + } if i.Tag != "" { image.Tag = i.Tag } diff --git a/cmd/clusterctl/client/config/imagemeta_client_test.go b/cmd/clusterctl/client/config/imagemeta_client_test.go index 3b383a4b0fcd..256ec3d1c66e 100644 --- a/cmd/clusterctl/client/config/imagemeta_client_test.go +++ b/cmd/clusterctl/client/config/imagemeta_client_test.go @@ -55,7 +55,7 @@ func Test_imageMetaClient_AlterImage(t *testing.T) { { name: "image config for cert-manager/cert-manager-cainjector: image for the cert-manager/cert-manager-cainjector should be changed", fields: fields{ - reader: test.NewFakeReader().WithImageMeta(fmt.Sprintf("%s/cert-manager-cainjector", CertManagerImageComponent), "foo-repository.io", "foo-tag"), + reader: test.NewFakeReader().WithImageMeta(fmt.Sprintf("%s/cert-manager-cainjector", CertManagerImageComponent), "foo-repository.io", "", "foo-tag"), }, args: args{ component: CertManagerImageComponent, @@ -67,7 +67,7 @@ func Test_imageMetaClient_AlterImage(t *testing.T) { { name: "image config for cert-manager/cert-manager-cainjector: image for the cert-manager/cert-manager-webhook should not be changed", fields: fields{ - reader: test.NewFakeReader().WithImageMeta(fmt.Sprintf("%s/cert-manager-cainjector", CertManagerImageComponent), "foo-repository.io", "foo-tag"), + reader: test.NewFakeReader().WithImageMeta(fmt.Sprintf("%s/cert-manager-cainjector", CertManagerImageComponent), "foo-repository.io", "", "foo-tag"), }, args: args{ component: CertManagerImageComponent, @@ -79,7 +79,7 @@ func Test_imageMetaClient_AlterImage(t *testing.T) { { name: "image config for cert-manager: images for the cert-manager should be changed", fields: fields{ - reader: test.NewFakeReader().WithImageMeta(CertManagerImageComponent, "foo-repository.io", "foo-tag"), + reader: test.NewFakeReader().WithImageMeta(CertManagerImageComponent, "foo-repository.io", "", "foo-tag"), }, args: args{ component: CertManagerImageComponent, @@ -92,8 +92,8 @@ func Test_imageMetaClient_AlterImage(t *testing.T) { name: "image config for cert-manager/cert-manager-cainjector and for cert-manager: images for the cert-manager/cert-manager-cainjector should be changed according to the most specific", fields: fields{ reader: test.NewFakeReader(). - WithImageMeta(fmt.Sprintf("%s/cert-manager-cainjector", CertManagerImageComponent), "foo-repository.io", "foo-tag"). - WithImageMeta(CertManagerImageComponent, "bar-repository.io", "bar-tag"), + WithImageMeta(fmt.Sprintf("%s/cert-manager-cainjector", CertManagerImageComponent), "foo-repository.io", "", "foo-tag"). + WithImageMeta(CertManagerImageComponent, "bar-repository.io", "", "bar-tag"), }, args: args{ component: CertManagerImageComponent, @@ -106,8 +106,8 @@ func Test_imageMetaClient_AlterImage(t *testing.T) { name: "image config for cert-manager/cert-manager-cainjector and for cert-manager: images for the cert-manager/cert-manager-cainjector should be changed according to the most specific (mixed case)", fields: fields{ reader: test.NewFakeReader(). - WithImageMeta(fmt.Sprintf("%s/cert-manager-cainjector", CertManagerImageComponent), "foo-repository.io", ""). - WithImageMeta(CertManagerImageComponent, "", "bar-tag"), + WithImageMeta(fmt.Sprintf("%s/cert-manager-cainjector", CertManagerImageComponent), "foo-repository.io", "", ""). + WithImageMeta(CertManagerImageComponent, "", "", "bar-tag"), }, args: args{ component: CertManagerImageComponent, @@ -120,8 +120,8 @@ func Test_imageMetaClient_AlterImage(t *testing.T) { name: "image config for cert-manager/cert-manager-cainjector and for cert-manager: images for the cert-manager/cert-manager-webhook should be changed according to the most generic", fields: fields{ reader: test.NewFakeReader(). - WithImageMeta(fmt.Sprintf("%s/cert-manager-cainjector", CertManagerImageComponent), "foo-repository.io", "foo-tag"). - WithImageMeta(CertManagerImageComponent, "bar-repository.io", "bar-tag"), + WithImageMeta(fmt.Sprintf("%s/cert-manager-cainjector", CertManagerImageComponent), "foo-repository.io", "", "foo-tag"). + WithImageMeta(CertManagerImageComponent, "bar-repository.io", "", "bar-tag"), }, args: args{ component: CertManagerImageComponent, @@ -133,7 +133,7 @@ func Test_imageMetaClient_AlterImage(t *testing.T) { { name: "image config for all: images for the cert-manager should be changed", fields: fields{ - reader: test.NewFakeReader().WithImageMeta(allImageConfig, "foo-repository.io", "foo-tag"), + reader: test.NewFakeReader().WithImageMeta(allImageConfig, "foo-repository.io", "", "foo-tag"), }, args: args{ component: CertManagerImageComponent, @@ -146,8 +146,8 @@ func Test_imageMetaClient_AlterImage(t *testing.T) { name: "image config for all and for cert-manager: images for the cert-manager should be changed according to the most specific", fields: fields{ reader: test.NewFakeReader(). - WithImageMeta(allImageConfig, "foo-repository.io", "foo-tag"). - WithImageMeta(CertManagerImageComponent, "bar-repository.io", "bar-tag"), + WithImageMeta(allImageConfig, "foo-repository.io", "", "foo-tag"). + WithImageMeta(CertManagerImageComponent, "bar-repository.io", "", "bar-tag"), }, args: args{ component: CertManagerImageComponent, @@ -160,8 +160,8 @@ func Test_imageMetaClient_AlterImage(t *testing.T) { name: "image config for all and for cert-manager: images for the cert-manager should be changed according to the most specific (mixed case)", fields: fields{ reader: test.NewFakeReader(). - WithImageMeta(allImageConfig, "foo-repository.io", ""). - WithImageMeta(CertManagerImageComponent, "", "bar-tag"), + WithImageMeta(allImageConfig, "foo-repository.io", "", ""). + WithImageMeta(CertManagerImageComponent, "", "", "bar-tag"), }, args: args{ component: CertManagerImageComponent, @@ -174,9 +174,9 @@ func Test_imageMetaClient_AlterImage(t *testing.T) { name: "image config for cert-manager/cert-manager-cainjector, for cert-manager and for all: images for the cert-manager/cert-manager-cainjector should be changed according to the most specific", fields: fields{ reader: test.NewFakeReader(). - WithImageMeta(fmt.Sprintf("%s/cert-manager-cainjector", CertManagerImageComponent), "foo-repository.io", "foo-tag"). - WithImageMeta(CertManagerImageComponent, "bar-repository.io", "bar-tag"). - WithImageMeta(allImageConfig, "baz-repository.io", "baz-tag"), + WithImageMeta(fmt.Sprintf("%s/cert-manager-cainjector", CertManagerImageComponent), "foo-repository.io", "", "foo-tag"). + WithImageMeta(CertManagerImageComponent, "bar-repository.io", "", "bar-tag"). + WithImageMeta(allImageConfig, "baz-repository.io", "", "baz-tag"), }, args: args{ component: CertManagerImageComponent, @@ -189,9 +189,9 @@ func Test_imageMetaClient_AlterImage(t *testing.T) { name: "image config for cert-manager/cert-manager-cainjector, for cert-manager and for all: images for the cert-manager/cert-manager-cainjector should be changed according to the most specific (mixed case)", fields: fields{ reader: test.NewFakeReader(). - WithImageMeta(fmt.Sprintf("%s/cert-manager-cainjector", CertManagerImageComponent), "foo-repository.io", ""). - WithImageMeta(CertManagerImageComponent, "", "bar-tag"). - WithImageMeta(allImageConfig, "baz-repository.io", "baz-tag"), + WithImageMeta(fmt.Sprintf("%s/cert-manager-cainjector", CertManagerImageComponent), "foo-repository.io", "", ""). + WithImageMeta(CertManagerImageComponent, "", "", "bar-tag"). + WithImageMeta(allImageConfig, "baz-repository.io", "", "baz-tag"), }, args: args{ component: CertManagerImageComponent, @@ -204,9 +204,9 @@ func Test_imageMetaClient_AlterImage(t *testing.T) { name: "image config for cert-manager/cert-manager-cainjector, for cert-manager and for all: images for the cert-manager/cert-manager-webhook should be changed according to the most generic", fields: fields{ reader: test.NewFakeReader(). - WithImageMeta(fmt.Sprintf("%s/cert-manager-cainjector", CertManagerImageComponent), "foo-repository.io", "foo-tag"). - WithImageMeta(CertManagerImageComponent, "bar-repository.io", ""). - WithImageMeta(allImageConfig, "baz-repository.io", "baz-tag"), + WithImageMeta(fmt.Sprintf("%s/cert-manager-cainjector", CertManagerImageComponent), "foo-repository.io", "", "foo-tag"). + WithImageMeta(CertManagerImageComponent, "bar-repository.io", "", ""). + WithImageMeta(allImageConfig, "baz-repository.io", "", "baz-tag"), }, args: args{ component: CertManagerImageComponent, @@ -230,7 +230,7 @@ func Test_imageMetaClient_AlterImage(t *testing.T) { { name: "fails if wrong image name", fields: fields{ - reader: test.NewFakeReader().WithImageMeta(allImageConfig, "foo-Repository.io", ""), + reader: test.NewFakeReader().WithImageMeta(allImageConfig, "foo-Repository.io", "", ""), }, args: args{ component: "any", @@ -239,6 +239,56 @@ func Test_imageMetaClient_AlterImage(t *testing.T) { want: "", wantErr: true, }, + { + name: "image config for cluster-api/cluster-api-controller with name override only: only image name should be changed", + fields: fields{ + reader: test.NewFakeReader().WithImageMeta("cluster-api/cluster-api-controller", "", "custom-cluster-api-controller", ""), + }, + args: args{ + component: "cluster-api", + image: "registry.k8s.io/cluster-api/cluster-api-controller:v1.8.0", + }, + want: "registry.k8s.io/cluster-api/custom-cluster-api-controller:v1.8.0", + wantErr: false, + }, + { + name: "image config for cluster-api/cluster-api-controller with repository and name override: repository and name should be changed, tag preserved", + fields: fields{ + reader: test.NewFakeReader().WithImageMeta("cluster-api/cluster-api-controller", "myorg.io/mirror", "custom-cluster-api-controller", ""), + }, + args: args{ + component: "cluster-api", + image: "registry.k8s.io/cluster-api/cluster-api-controller:v1.10.6", + }, + want: "myorg.io/mirror/custom-cluster-api-controller:v1.10.6", + wantErr: false, + }, + { + name: "image config for cluster-api/cluster-api-controller with specific name override and generic repository from all: both should be applied", + fields: fields{ + reader: test.NewFakeReader(). + WithImageMeta(allImageConfig, "myorg.io/mirror", "", ""). + WithImageMeta("cluster-api/cluster-api-controller", "", "custom-cluster-api-controller", ""), + }, + args: args{ + component: "cluster-api", + image: "registry.k8s.io/cluster-api/cluster-api-controller:v1.10.6", + }, + want: "myorg.io/mirror/custom-cluster-api-controller:v1.10.6", + wantErr: false, + }, + { + name: "image config for cluster-api/cluster-api-controller with repository, name and tag override: all fields should be changed", + fields: fields{ + reader: test.NewFakeReader().WithImageMeta("cluster-api/cluster-api-controller", "myorg.io/mirror", "custom-cluster-api-controller", "v1.5.0"), + }, + args: args{ + component: "cluster-api", + image: "registry.k8s.io/cluster-api/cluster-api-controller:v1.10.6", + }, + want: "myorg.io/mirror/custom-cluster-api-controller:v1.5.0", + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/cmd/clusterctl/internal/test/fake_reader.go b/cmd/clusterctl/internal/test/fake_reader.go index d34fa6821fc2..2ea3ce953fdb 100644 --- a/cmd/clusterctl/internal/test/fake_reader.go +++ b/cmd/clusterctl/internal/test/fake_reader.go @@ -54,6 +54,7 @@ type configCertManager struct { // avoid circular dependencies between pkg/client/config and pkg/internal/test. type imageMeta struct { Repository string `json:"repository,omitempty"` + Name string `json:"name,omitempty"` Tag string `json:"tag,omitempty"` } @@ -119,9 +120,10 @@ func (f *FakeReader) WithCertManager(url, version, timeout string) *FakeReader { return f } -func (f *FakeReader) WithImageMeta(component, repository, tag string) *FakeReader { +func (f *FakeReader) WithImageMeta(component, repository, name, tag string) *FakeReader { f.imageMetas[component] = imageMeta{ Repository: repository, + Name: name, Tag: tag, } diff --git a/docs/book/src/clusterctl/configuration.md b/docs/book/src/clusterctl/configuration.md index 719c4b9c1fbd..3a3d5a4ff7c8 100644 --- a/docs/book/src/clusterctl/configuration.md +++ b/docs/book/src/clusterctl/configuration.md @@ -261,6 +261,33 @@ images: tag: v1.5.3 ``` +Additionally, you can override the image name itself. This is useful when images +have been mirrored with different names: + +```yaml +images: + all: + repository: myorg.io/local-repo + cluster-api: + name: mirrored-cluster-api-controller +``` + +This would transform `registry.k8s.io/cluster-api/cluster-api-controller:v1.10.6` into +`myorg.io/local-repo/mirrored-cluster-api-controller:v1.10.6`. + +Or you can specify all three fields to completely override the image: + +```yaml +images: + cluster-api: + repository: myorg.io/local-repo + name: mirrored-cluster-api-controller + tag: v1.10.6 +``` + +This would transform `registry.k8s.io/cluster-api/cluster-api-controller:v1.8.0` into +`myorg.io/local-repo/mirrored-cluster-api-controller:v1.10.6`, replacing both the image location and version. + ## Debugging/Logging To have more verbose logs you can use the `-v` flag when running the `clusterctl` and set the level of the logging verbose with a positive integer number, ie. `-v 3`.