diff --git a/internal/controller/image_overrides.go b/internal/controller/image_overrides.go new file mode 100644 index 000000000..aebf9b8ff --- /dev/null +++ b/internal/controller/image_overrides.go @@ -0,0 +1,128 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/kubernetes/scheme" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + + configclient "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" +) + +const ( + daemonSetKind = "DaemonSet" +) + +func imageOverrides(component string, overrides configclient.Client) func(objs []unstructured.Unstructured) ([]unstructured.Unstructured, error) { + imageOverridesWrapper := func(objs []unstructured.Unstructured) ([]unstructured.Unstructured, error) { + if overrides == nil { + return objs, nil + } + + return fixImages(objs, func(image string) (string, error) { + return overrides.ImageMeta().AlterImage(component, image) + }) + } + + return imageOverridesWrapper +} + +// fixImages alters images using the give alter func +// NB. The implemented approach is specific for the provider components YAML & for the cert-manager manifest; it is not +// intended to cover all the possible objects used to deploy containers existing in Kubernetes. +func fixImages(objs []unstructured.Unstructured, alterImageFunc func(image string) (string, error)) ([]unstructured.Unstructured, error) { + for i := range objs { + if err := fixDeploymentImages(&objs[i], alterImageFunc); err != nil { + return nil, err + } + + if err := fixDaemonSetImages(&objs[i], alterImageFunc); err != nil { + return nil, err + } + } + + return objs, nil +} + +func fixDeploymentImages(o *unstructured.Unstructured, alterImageFunc func(image string) (string, error)) error { + if o.GetKind() != deploymentKind { + return nil + } + + // Convert Unstructured into a typed object + d := &appsv1.Deployment{} + if err := scheme.Scheme.Convert(o, d, nil); err != nil { + return err + } + + if err := fixPodSpecImages(&d.Spec.Template.Spec, alterImageFunc); err != nil { + return fmt.Errorf("%w: failed to fix containers in deployment %s", err, d.Name) + } + + // Convert typed object back to Unstructured + return scheme.Scheme.Convert(d, o, nil) +} + +func fixDaemonSetImages(o *unstructured.Unstructured, alterImageFunc func(image string) (string, error)) error { + if o.GetKind() != daemonSetKind { + return nil + } + + // Convert Unstructured into a typed object + d := &appsv1.DaemonSet{} + if err := scheme.Scheme.Convert(o, d, nil); err != nil { + return err + } + + if err := fixPodSpecImages(&d.Spec.Template.Spec, alterImageFunc); err != nil { + return fmt.Errorf("%w: failed to fix containers in deamonSet %s", err, d.Name) + } + // Convert typed object back to Unstructured + return scheme.Scheme.Convert(d, o, nil) +} + +func fixPodSpecImages(podSpec *corev1.PodSpec, alterImageFunc func(image string) (string, error)) error { + if err := fixContainersImage(podSpec.Containers, alterImageFunc); err != nil { + return fmt.Errorf("%w: failed to fix containers", err) + } + + if err := fixContainersImage(podSpec.InitContainers, alterImageFunc); err != nil { + return fmt.Errorf("%w: failed to fix init containers", err) + } + + return nil +} + +func fixContainersImage(containers []corev1.Container, alterImageFunc func(image string) (string, error)) error { + for j := range containers { + container := &containers[j] + + image, err := alterImageFunc(container.Image) + if err != nil { + return fmt.Errorf("%w: failed to fix image for container %s", err, container.Name) + } + + container.Image = image + } + + return nil +} diff --git a/internal/controller/image_overrides_test.go b/internal/controller/image_overrides_test.go new file mode 100644 index 000000000..2c92c6aed --- /dev/null +++ b/internal/controller/image_overrides_test.go @@ -0,0 +1,171 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "fmt" + "testing" + + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/kubernetes/scheme" +) + +// inspectImages identifies the container images required to install the objects defined in the objs. +// NB. The implemented approach is specific for the provider components YAML & for the cert-manager manifest; it is not +// intended to cover all the possible objects used to deploy containers existing in Kubernetes. +func inspectImages(objs []unstructured.Unstructured) ([]string, error) { + images := []string{} + + for i := range objs { + o := objs[i] + + var podSpec corev1.PodSpec + + switch o.GetKind() { + case deploymentKind: + d := &appsv1.Deployment{} + if err := scheme.Scheme.Convert(&o, d, nil); err != nil { + return nil, err + } + + podSpec = d.Spec.Template.Spec + case daemonSetKind: + d := &appsv1.DaemonSet{} + if err := scheme.Scheme.Convert(&o, d, nil); err != nil { + return nil, err + } + + podSpec = d.Spec.Template.Spec + default: + continue + } + + for _, c := range podSpec.Containers { + images = append(images, c.Image) + } + + for _, c := range podSpec.InitContainers { + images = append(images, c.Image) + } + } + + return images, nil +} + +func TestFixImages(t *testing.T) { + type args struct { + objs []unstructured.Unstructured + alterImageFunc func(image string) (string, error) + } + + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "fix deployment containers images", + args: args{ + objs: []unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": deploymentKind, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []map[string]interface{}{ + { + "image": "container-image", + }, + }, + "initContainers": []map[string]interface{}{ + { + "image": "init-container-image", + }, + }, + }, + }, + }, + }, + }, + }, + alterImageFunc: func(image string) (string, error) { + return fmt.Sprintf("foo-%s", image), nil + }, + }, + want: []string{"foo-container-image", "foo-init-container-image"}, + wantErr: false, + }, + { + name: "fix daemonSet containers images", + args: args{ + objs: []unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": daemonSetKind, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []map[string]interface{}{ + { + "image": "container-image", + }, + }, + "initContainers": []map[string]interface{}{ + { + "image": "init-container-image", + }, + }, + }, + }, + }, + }, + }, + }, + alterImageFunc: func(image string) (string, error) { + return fmt.Sprintf("foo-%s", image), nil + }, + }, + want: []string{"foo-container-image", "foo-init-container-image"}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + got, err := fixImages(tt.args.objs, tt.args.alterImageFunc) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + + g.Expect(err).ToNot(HaveOccurred()) + + gotImages, err := inspectImages(got) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(gotImages).To(Equal(tt.want)) + }) + } +} diff --git a/internal/controller/manifests_downloader_test.go b/internal/controller/manifests_downloader_test.go index d9a8f45c3..762475a6c 100644 --- a/internal/controller/manifests_downloader_test.go +++ b/internal/controller/manifests_downloader_test.go @@ -82,11 +82,6 @@ func TestProviderDownloadWithOverrides(t *testing.T) { overridesClient, err := configclient.New(ctx, "", configclient.InjectReader(reader)) g.Expect(err).ToNot(HaveOccurred()) - overridesClient.Variables().Set("images", ` -all: - repository: "myorg.io/local-repo" -`) - p := &phaseReconciler{ ctrlClient: fakeclient, provider: &operatorv1.CoreProvider{ @@ -111,6 +106,6 @@ all: _, err = p.fetch(ctx) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(p.components.Images()).To(HaveExactElements([]string{"myorg.io/local-repo/cluster-api-controller:v1.4.3"})) + g.Expect(p.components.Images()).To(HaveExactElements([]string{"registry.k8s.io/cluster-api/cluster-api-controller:v1.4.3"})) g.Expect(p.components.Version()).To(Equal("v1.4.3")) } diff --git a/internal/controller/phases.go b/internal/controller/phases.go index fb8cf0a96..b20e61ac3 100644 --- a/internal/controller/phases.go +++ b/internal/controller/phases.go @@ -148,12 +148,6 @@ func (p *phaseReconciler) initializePhaseReconciler(ctx context.Context) (reconc return reconcile.Result{}, err } - if p.overridesClient != nil { - if imageOverrides, err := p.overridesClient.Variables().Get("images"); err == nil { - reader.Set("images", imageOverrides) - } - } - // Load provider's secret and config url. p.configClient, err = configclient.New(ctx, "", configclient.InjectReader(reader)) if err != nil { @@ -468,6 +462,11 @@ func (p *phaseReconciler) fetch(ctx context.Context) (reconcile.Result, error) { return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition) } + // Apply image overrides to the provider manifests. + if err := repository.AlterComponents(p.components, imageOverrides(p.components.ManifestLabel(), p.overridesClient)); err != nil { + return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition) + } + conditions.Set(p.provider, conditions.TrueCondition(operatorv1.ProviderInstalledCondition)) return reconcile.Result{}, nil