Skip to content

Commit 18a34e9

Browse files
authored
Merge pull request #616 from Danil-Grigorev/image-overrides-support
🐛 Allow to use image overrides from mounted custerctl-config.yaml file
2 parents 05d29b5 + 1a099d6 commit 18a34e9

File tree

4 files changed

+305
-12
lines changed

4 files changed

+305
-12
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"fmt"
21+
22+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
23+
"k8s.io/client-go/kubernetes/scheme"
24+
25+
appsv1 "k8s.io/api/apps/v1"
26+
corev1 "k8s.io/api/core/v1"
27+
28+
configclient "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
29+
)
30+
31+
const (
32+
daemonSetKind = "DaemonSet"
33+
)
34+
35+
func imageOverrides(component string, overrides configclient.Client) func(objs []unstructured.Unstructured) ([]unstructured.Unstructured, error) {
36+
imageOverridesWrapper := func(objs []unstructured.Unstructured) ([]unstructured.Unstructured, error) {
37+
if overrides == nil {
38+
return objs, nil
39+
}
40+
41+
return fixImages(objs, func(image string) (string, error) {
42+
return overrides.ImageMeta().AlterImage(component, image)
43+
})
44+
}
45+
46+
return imageOverridesWrapper
47+
}
48+
49+
// fixImages alters images using the give alter func
50+
// NB. The implemented approach is specific for the provider components YAML & for the cert-manager manifest; it is not
51+
// intended to cover all the possible objects used to deploy containers existing in Kubernetes.
52+
func fixImages(objs []unstructured.Unstructured, alterImageFunc func(image string) (string, error)) ([]unstructured.Unstructured, error) {
53+
for i := range objs {
54+
if err := fixDeploymentImages(&objs[i], alterImageFunc); err != nil {
55+
return nil, err
56+
}
57+
58+
if err := fixDaemonSetImages(&objs[i], alterImageFunc); err != nil {
59+
return nil, err
60+
}
61+
}
62+
63+
return objs, nil
64+
}
65+
66+
func fixDeploymentImages(o *unstructured.Unstructured, alterImageFunc func(image string) (string, error)) error {
67+
if o.GetKind() != deploymentKind {
68+
return nil
69+
}
70+
71+
// Convert Unstructured into a typed object
72+
d := &appsv1.Deployment{}
73+
if err := scheme.Scheme.Convert(o, d, nil); err != nil {
74+
return err
75+
}
76+
77+
if err := fixPodSpecImages(&d.Spec.Template.Spec, alterImageFunc); err != nil {
78+
return fmt.Errorf("%w: failed to fix containers in deployment %s", err, d.Name)
79+
}
80+
81+
// Convert typed object back to Unstructured
82+
return scheme.Scheme.Convert(d, o, nil)
83+
}
84+
85+
func fixDaemonSetImages(o *unstructured.Unstructured, alterImageFunc func(image string) (string, error)) error {
86+
if o.GetKind() != daemonSetKind {
87+
return nil
88+
}
89+
90+
// Convert Unstructured into a typed object
91+
d := &appsv1.DaemonSet{}
92+
if err := scheme.Scheme.Convert(o, d, nil); err != nil {
93+
return err
94+
}
95+
96+
if err := fixPodSpecImages(&d.Spec.Template.Spec, alterImageFunc); err != nil {
97+
return fmt.Errorf("%w: failed to fix containers in deamonSet %s", err, d.Name)
98+
}
99+
// Convert typed object back to Unstructured
100+
return scheme.Scheme.Convert(d, o, nil)
101+
}
102+
103+
func fixPodSpecImages(podSpec *corev1.PodSpec, alterImageFunc func(image string) (string, error)) error {
104+
if err := fixContainersImage(podSpec.Containers, alterImageFunc); err != nil {
105+
return fmt.Errorf("%w: failed to fix containers", err)
106+
}
107+
108+
if err := fixContainersImage(podSpec.InitContainers, alterImageFunc); err != nil {
109+
return fmt.Errorf("%w: failed to fix init containers", err)
110+
}
111+
112+
return nil
113+
}
114+
115+
func fixContainersImage(containers []corev1.Container, alterImageFunc func(image string) (string, error)) error {
116+
for j := range containers {
117+
container := &containers[j]
118+
119+
image, err := alterImageFunc(container.Image)
120+
if err != nil {
121+
return fmt.Errorf("%w: failed to fix image for container %s", err, container.Name)
122+
}
123+
124+
container.Image = image
125+
}
126+
127+
return nil
128+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"fmt"
21+
"testing"
22+
23+
. "github.com/onsi/gomega"
24+
appsv1 "k8s.io/api/apps/v1"
25+
corev1 "k8s.io/api/core/v1"
26+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
27+
"k8s.io/client-go/kubernetes/scheme"
28+
)
29+
30+
// inspectImages identifies the container images required to install the objects defined in the objs.
31+
// NB. The implemented approach is specific for the provider components YAML & for the cert-manager manifest; it is not
32+
// intended to cover all the possible objects used to deploy containers existing in Kubernetes.
33+
func inspectImages(objs []unstructured.Unstructured) ([]string, error) {
34+
images := []string{}
35+
36+
for i := range objs {
37+
o := objs[i]
38+
39+
var podSpec corev1.PodSpec
40+
41+
switch o.GetKind() {
42+
case deploymentKind:
43+
d := &appsv1.Deployment{}
44+
if err := scheme.Scheme.Convert(&o, d, nil); err != nil {
45+
return nil, err
46+
}
47+
48+
podSpec = d.Spec.Template.Spec
49+
case daemonSetKind:
50+
d := &appsv1.DaemonSet{}
51+
if err := scheme.Scheme.Convert(&o, d, nil); err != nil {
52+
return nil, err
53+
}
54+
55+
podSpec = d.Spec.Template.Spec
56+
default:
57+
continue
58+
}
59+
60+
for _, c := range podSpec.Containers {
61+
images = append(images, c.Image)
62+
}
63+
64+
for _, c := range podSpec.InitContainers {
65+
images = append(images, c.Image)
66+
}
67+
}
68+
69+
return images, nil
70+
}
71+
72+
func TestFixImages(t *testing.T) {
73+
type args struct {
74+
objs []unstructured.Unstructured
75+
alterImageFunc func(image string) (string, error)
76+
}
77+
78+
tests := []struct {
79+
name string
80+
args args
81+
want []string
82+
wantErr bool
83+
}{
84+
{
85+
name: "fix deployment containers images",
86+
args: args{
87+
objs: []unstructured.Unstructured{
88+
{
89+
Object: map[string]interface{}{
90+
"apiVersion": "apps/v1",
91+
"kind": deploymentKind,
92+
"spec": map[string]interface{}{
93+
"template": map[string]interface{}{
94+
"spec": map[string]interface{}{
95+
"containers": []map[string]interface{}{
96+
{
97+
"image": "container-image",
98+
},
99+
},
100+
"initContainers": []map[string]interface{}{
101+
{
102+
"image": "init-container-image",
103+
},
104+
},
105+
},
106+
},
107+
},
108+
},
109+
},
110+
},
111+
alterImageFunc: func(image string) (string, error) {
112+
return fmt.Sprintf("foo-%s", image), nil
113+
},
114+
},
115+
want: []string{"foo-container-image", "foo-init-container-image"},
116+
wantErr: false,
117+
},
118+
{
119+
name: "fix daemonSet containers images",
120+
args: args{
121+
objs: []unstructured.Unstructured{
122+
{
123+
Object: map[string]interface{}{
124+
"apiVersion": "apps/v1",
125+
"kind": daemonSetKind,
126+
"spec": map[string]interface{}{
127+
"template": map[string]interface{}{
128+
"spec": map[string]interface{}{
129+
"containers": []map[string]interface{}{
130+
{
131+
"image": "container-image",
132+
},
133+
},
134+
"initContainers": []map[string]interface{}{
135+
{
136+
"image": "init-container-image",
137+
},
138+
},
139+
},
140+
},
141+
},
142+
},
143+
},
144+
},
145+
alterImageFunc: func(image string) (string, error) {
146+
return fmt.Sprintf("foo-%s", image), nil
147+
},
148+
},
149+
want: []string{"foo-container-image", "foo-init-container-image"},
150+
wantErr: false,
151+
},
152+
}
153+
154+
for _, tt := range tests {
155+
t.Run(tt.name, func(t *testing.T) {
156+
g := NewWithT(t)
157+
158+
got, err := fixImages(tt.args.objs, tt.args.alterImageFunc)
159+
if tt.wantErr {
160+
g.Expect(err).To(HaveOccurred())
161+
return
162+
}
163+
164+
g.Expect(err).ToNot(HaveOccurred())
165+
166+
gotImages, err := inspectImages(got)
167+
g.Expect(err).ToNot(HaveOccurred())
168+
g.Expect(gotImages).To(Equal(tt.want))
169+
})
170+
}
171+
}

internal/controller/manifests_downloader_test.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,6 @@ func TestProviderDownloadWithOverrides(t *testing.T) {
8282
overridesClient, err := configclient.New(ctx, "", configclient.InjectReader(reader))
8383
g.Expect(err).ToNot(HaveOccurred())
8484

85-
overridesClient.Variables().Set("images", `
86-
all:
87-
repository: "myorg.io/local-repo"
88-
`)
89-
9085
p := &phaseReconciler{
9186
ctrlClient: fakeclient,
9287
provider: &operatorv1.CoreProvider{
@@ -111,6 +106,6 @@ all:
111106
_, err = p.fetch(ctx)
112107
g.Expect(err).ToNot(HaveOccurred())
113108

114-
g.Expect(p.components.Images()).To(HaveExactElements([]string{"myorg.io/local-repo/cluster-api-controller:v1.4.3"}))
109+
g.Expect(p.components.Images()).To(HaveExactElements([]string{"registry.k8s.io/cluster-api/cluster-api-controller:v1.4.3"}))
115110
g.Expect(p.components.Version()).To(Equal("v1.4.3"))
116111
}

internal/controller/phases.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,6 @@ func (p *phaseReconciler) initializePhaseReconciler(ctx context.Context) (reconc
148148
return reconcile.Result{}, err
149149
}
150150

151-
if p.overridesClient != nil {
152-
if imageOverrides, err := p.overridesClient.Variables().Get("images"); err == nil {
153-
reader.Set("images", imageOverrides)
154-
}
155-
}
156-
157151
// Load provider's secret and config url.
158152
p.configClient, err = configclient.New(ctx, "", configclient.InjectReader(reader))
159153
if err != nil {
@@ -468,6 +462,11 @@ func (p *phaseReconciler) fetch(ctx context.Context) (reconcile.Result, error) {
468462
return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition)
469463
}
470464

465+
// Apply image overrides to the provider manifests.
466+
if err := repository.AlterComponents(p.components, imageOverrides(p.components.ManifestLabel(), p.overridesClient)); err != nil {
467+
return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition)
468+
}
469+
471470
conditions.Set(p.provider, conditions.TrueCondition(operatorv1.ProviderInstalledCondition))
472471

473472
return reconcile.Result{}, nil

0 commit comments

Comments
 (0)