Skip to content

Commit 6cf9901

Browse files
committed
feat: implement CloudProfile mutator to add capability flavors from provider config
1 parent 77c3f2d commit 6cf9901

3 files changed

Lines changed: 332 additions & 0 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// SPDX-FileCopyrightText: SAP SE or an SAP affiliate company and Gardener contributors
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package mutator
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"slices"
11+
12+
extensionswebhook "github.com/gardener/gardener/extensions/pkg/webhook"
13+
gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
14+
"k8s.io/apimachinery/pkg/runtime"
15+
"k8s.io/apimachinery/pkg/runtime/serializer"
16+
"sigs.k8s.io/controller-runtime/pkg/client"
17+
"sigs.k8s.io/controller-runtime/pkg/manager"
18+
19+
"github.com/gardener/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1"
20+
)
21+
22+
// NewCloudProfileMutator returns a new instance of a CloudProfile mutator.
23+
func NewCloudProfileMutator(mgr manager.Manager) extensionswebhook.Mutator {
24+
return &cloudProfile{
25+
client: mgr.GetClient(),
26+
decoder: serializer.NewCodecFactory(mgr.GetScheme(), serializer.EnableStrict).UniversalDecoder(),
27+
}
28+
}
29+
30+
type cloudProfile struct {
31+
client client.Client
32+
decoder runtime.Decoder
33+
}
34+
35+
// Mutate mutates the given CloudProfile object.
36+
func (p *cloudProfile) Mutate(_ context.Context, newObj, _ client.Object) error {
37+
profile, ok := newObj.(*gardencorev1beta1.CloudProfile)
38+
if !ok {
39+
return fmt.Errorf("wrong object type %T", newObj)
40+
}
41+
42+
// Skip mutation if CloudProfile is being deleted or when no capabilities used in that profile
43+
if profile.DeletionTimestamp != nil || profile.Spec.ProviderConfig == nil || len(profile.Spec.MachineCapabilities) == 0 {
44+
return nil
45+
}
46+
47+
specConfig := &v1alpha1.CloudProfileConfig{}
48+
if _, _, err := p.decoder.Decode(profile.Spec.ProviderConfig.Raw, nil, specConfig); err != nil {
49+
return fmt.Errorf("could not decode providerConfig of cloudProfile for '%s': %w", profile.Name, err)
50+
}
51+
52+
overwriteMachineImageCapabilityFlavors(profile, specConfig)
53+
return nil
54+
}
55+
56+
// overwriteMachineImageCapabilityFlavors updates the capability flavors of machine images in the CloudProfile
57+
func overwriteMachineImageCapabilityFlavors(profile *gardencorev1beta1.CloudProfile, config *v1alpha1.CloudProfileConfig) {
58+
for _, providerMachineImage := range config.MachineImages {
59+
// Find the corresponding machine image in the CloudProfile
60+
imageIdx := slices.IndexFunc(profile.Spec.MachineImages, func(mi gardencorev1beta1.MachineImage) bool {
61+
return mi.Name == providerMachineImage.Name
62+
})
63+
if imageIdx == -1 {
64+
continue
65+
}
66+
67+
// Iterate over versions in the provider's machine image
68+
for _, providerVersion := range providerMachineImage.Versions {
69+
// Find the corresponding version in the CloudProfile's machine image
70+
versionIdx := slices.IndexFunc(profile.Spec.MachineImages[imageIdx].Versions, func(miv gardencorev1beta1.MachineImageVersion) bool {
71+
return miv.Version == providerVersion.Version
72+
})
73+
if versionIdx == -1 {
74+
continue
75+
}
76+
77+
profile.Spec.MachineImages[imageIdx].Versions[versionIdx].CapabilityFlavors = convertCapabilityFlavors(providerVersion.CapabilityFlavors)
78+
}
79+
}
80+
}
81+
82+
// convertCapabilityFlavors converts provider capability flavors to CloudProfile capability flavors
83+
func convertCapabilityFlavors(providerFlavors []v1alpha1.MachineImageFlavor) []gardencorev1beta1.MachineImageFlavor {
84+
capabilityFlavors := make([]gardencorev1beta1.MachineImageFlavor, 0, len(providerFlavors))
85+
for _, providerFlavor := range providerFlavors {
86+
capabilityFlavors = append(capabilityFlavors, gardencorev1beta1.MachineImageFlavor{
87+
Capabilities: providerFlavor.GetCapabilities(),
88+
})
89+
}
90+
return capabilityFlavors
91+
}
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
// SPDX-FileCopyrightText: SAP SE or an SAP affiliate company and Gardener contributors
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package mutator_test
6+
7+
import (
8+
"context"
9+
"fmt"
10+
11+
extensionswebhook "github.com/gardener/gardener/extensions/pkg/webhook"
12+
"github.com/gardener/gardener/pkg/apis/core/v1beta1"
13+
"github.com/gardener/gardener/pkg/utils/test"
14+
. "github.com/onsi/ginkgo/v2"
15+
. "github.com/onsi/gomega"
16+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
"k8s.io/apimachinery/pkg/runtime"
18+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
19+
"k8s.io/utils/ptr"
20+
"sigs.k8s.io/controller-runtime/pkg/client"
21+
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
22+
"sigs.k8s.io/controller-runtime/pkg/manager"
23+
24+
"github.com/gardener/gardener-extension-provider-aws/pkg/admission/mutator"
25+
"github.com/gardener/gardener-extension-provider-aws/pkg/apis/aws/install"
26+
)
27+
28+
var _ = Describe("CloudProfile Mutator", func() {
29+
var (
30+
fakeClient client.Client
31+
fakeManager manager.Manager
32+
ctx = context.Background()
33+
34+
cloudProfileMutator extensionswebhook.Mutator
35+
cloudProfile *v1beta1.CloudProfile
36+
)
37+
38+
BeforeEach(func() {
39+
scheme := runtime.NewScheme()
40+
utilruntime.Must(install.AddToScheme(scheme))
41+
utilruntime.Must(v1beta1.AddToScheme(scheme))
42+
fakeClient = fakeclient.NewClientBuilder().WithScheme(scheme).Build()
43+
fakeManager = &test.FakeManager{
44+
Client: fakeClient,
45+
Scheme: scheme,
46+
}
47+
48+
cloudProfileMutator = mutator.NewCloudProfileMutator(fakeManager)
49+
50+
imageVersion := "1.0.0"
51+
latestImageVersion := "1.0.1"
52+
imageName := "os-1"
53+
54+
machineImages := []v1beta1.MachineImage{
55+
{
56+
Name: imageName,
57+
Versions: []v1beta1.MachineImageVersion{{
58+
ExpirableVersion: v1beta1.ExpirableVersion{
59+
Version: imageVersion,
60+
},
61+
}, {
62+
ExpirableVersion: v1beta1.ExpirableVersion{
63+
Version: latestImageVersion,
64+
},
65+
},
66+
},
67+
},
68+
}
69+
70+
cloudProfile = &v1beta1.CloudProfile{
71+
ObjectMeta: metav1.ObjectMeta{
72+
Name: "aws",
73+
},
74+
Spec: v1beta1.CloudProfileSpec{
75+
MachineImages: machineImages,
76+
},
77+
}
78+
})
79+
80+
Describe("#Mutate", func() {
81+
Context("CloudProfile without machineCapabilities", func() {
82+
BeforeEach(func() {
83+
cloudProfile.Spec.ProviderConfig = nil
84+
})
85+
86+
It("should succeed and not modify the CloudProfile", func() {
87+
cloudProfile.Spec.ProviderConfig = &runtime.RawExtension{Raw: []byte(`{
88+
"apiVersion":"aws.provider.extensions.gardener.cloud/v1alpha1",
89+
"kind":"CloudProfileConfig",
90+
"machineImages":[
91+
{"name":"image-1","versions":[{"version":"1.1","regions":[{"name":"eu2","ami":"ami-124","architecture":"armhf"}]}]}
92+
]}`)}
93+
expectedProfileSpec := cloudProfile.Spec.DeepCopy()
94+
Expect(cloudProfileMutator.Mutate(ctx, cloudProfile, nil)).To(Succeed())
95+
96+
Expect(cloudProfile.Spec.MachineImages).To(Equal(expectedProfileSpec.MachineImages))
97+
})
98+
})
99+
100+
Context("CloudProfile with machineCapabilities", func() {
101+
BeforeEach(func() {
102+
cloudProfile.Spec.MachineCapabilities = []v1beta1.CapabilityDefinition{{
103+
Name: "architecture",
104+
Values: []string{"amd64", "arm64", "armhf"},
105+
}, {
106+
Name: "gpu",
107+
Values: []string{"true", "false"},
108+
}}
109+
})
110+
It("should succeed for CloudProfile without provider config", func() {
111+
expectedProfile := cloudProfile.DeepCopy()
112+
Expect(cloudProfileMutator.Mutate(ctx, cloudProfile, nil)).To(Succeed())
113+
Expect(cloudProfile).To(Equal(expectedProfile))
114+
115+
})
116+
117+
It("should skip if CloudProfile is in deletion phase", func() {
118+
cloudProfile.DeletionTimestamp = ptr.To(metav1.Now())
119+
expectedProfile := cloudProfile.DeepCopy()
120+
121+
Expect(cloudProfileMutator.Mutate(ctx, cloudProfile, nil)).To(Succeed())
122+
123+
Expect(cloudProfile).To(Equal(expectedProfile))
124+
})
125+
126+
It("should fill capabilityFlavors based on provider config", func() {
127+
image1AmiMappings := `"capabilityFlavors":[
128+
{"capabilities":{"architecture":["arm64"]},"regions":[{"name":"image-region-1","ami":"id-img-reg-1"}]},
129+
{"capabilities":{"architecture":["amd64"]},"regions":[{"name":"image-region-2","ami":"id-img-reg-2"}]}
130+
]`
131+
image1FallbackMappings := `"capabilityFlavors":[
132+
{"capabilities":{"architecture":["amd64"]},"regions":[{"name":"image-region-2","ami":"id-img-reg-2"}]}
133+
]`
134+
135+
cloudProfile.Spec.ProviderConfig = &runtime.RawExtension{Raw: []byte(fmt.Sprintf(`{
136+
"apiVersion":"aws.provider.extensions.gardener.cloud/v1alpha1",
137+
"kind":"CloudProfileConfig",
138+
"machineImages":[
139+
{"name":"os-1","versions":[
140+
{"version":"1.0.0",%s},
141+
{"version":"1.0.1",%s}
142+
]}
143+
]}`, image1AmiMappings, image1FallbackMappings))}
144+
Expect(cloudProfileMutator.Mutate(ctx, cloudProfile, nil)).To(Succeed())
145+
Expect(cloudProfile.Spec.MachineImages).To(Equal([]v1beta1.MachineImage{
146+
{
147+
Name: "os-1",
148+
Versions: []v1beta1.MachineImageVersion{
149+
{
150+
ExpirableVersion: v1beta1.ExpirableVersion{Version: "1.0.0"},
151+
CapabilityFlavors: []v1beta1.MachineImageFlavor{
152+
{Capabilities: v1beta1.Capabilities{"architecture": []string{"arm64"}}},
153+
{Capabilities: v1beta1.Capabilities{"architecture": []string{"amd64"}}},
154+
},
155+
},
156+
{
157+
ExpirableVersion: v1beta1.ExpirableVersion{Version: "1.0.1"},
158+
CapabilityFlavors: []v1beta1.MachineImageFlavor{
159+
{Capabilities: v1beta1.Capabilities{"architecture": []string{"amd64"}}},
160+
},
161+
},
162+
},
163+
},
164+
}))
165+
})
166+
167+
It("should overwrite capabilityFlavors when some versions already have them", func() {
168+
twoFlavors := `"capabilityFlavors":[
169+
{"capabilities":{"architecture":["arm64"]},"regions":[{"name":"image-region-1","ami":"id-img-reg-1"}]},
170+
{"capabilities":{"architecture":["amd64"]},"regions":[{"name":"image-region-2","ami":"id-img-reg-2"}]}
171+
]`
172+
oneFlavors := `"capabilityFlavors":[
173+
{"capabilities":{"architecture":["amd64"]},"regions":[{"name":"image-region-2","ami":"id-img-reg-2"}]}
174+
]`
175+
cloudProfile.Spec.MachineImages = []v1beta1.MachineImage{
176+
{
177+
Name: "os-1",
178+
Versions: []v1beta1.MachineImageVersion{
179+
{
180+
ExpirableVersion: v1beta1.ExpirableVersion{Version: "1.0.0"},
181+
CapabilityFlavors: []v1beta1.MachineImageFlavor{
182+
{Capabilities: v1beta1.Capabilities{"architecture": []string{"not-existing"}}},
183+
{Capabilities: v1beta1.Capabilities{"architecture": []string{"amd64"}}},
184+
},
185+
},
186+
{ExpirableVersion: v1beta1.ExpirableVersion{Version: "1.0.1"}},
187+
},
188+
},
189+
{
190+
Name: "os-2",
191+
Versions: []v1beta1.MachineImageVersion{
192+
{ExpirableVersion: v1beta1.ExpirableVersion{Version: "1.0.0"}},
193+
{ExpirableVersion: v1beta1.ExpirableVersion{Version: "1.0.1"}},
194+
},
195+
},
196+
}
197+
cloudProfile.Spec.ProviderConfig = &runtime.RawExtension{Raw: []byte(fmt.Sprintf(`{
198+
"apiVersion":"aws.provider.extensions.gardener.cloud/v1alpha1",
199+
"kind":"CloudProfileConfig",
200+
"machineImages":[
201+
{"name":"os-1","versions":[
202+
{"version":"1.0.0",%s},
203+
{"version":"1.0.1",%s}
204+
]},
205+
{"name":"os-2","versions":[
206+
{"version":"1.0.0",%s},
207+
{"version":"1.0.1",%s}
208+
]}
209+
]}`, twoFlavors, oneFlavors, oneFlavors, twoFlavors))}
210+
Expect(cloudProfileMutator.Mutate(ctx, cloudProfile, nil)).To(Succeed())
211+
Expect(cloudProfile.Spec.MachineImages).To(HaveLen(2))
212+
Expect(cloudProfile.Spec.MachineImages[0].Name).To(Equal("os-1"))
213+
Expect(cloudProfile.Spec.MachineImages[0].Versions).To(HaveLen(2))
214+
Expect(cloudProfile.Spec.MachineImages[0].Versions[0].Version).To(Equal("1.0.0"))
215+
// the existing capabilityFlavors should be overwritten.
216+
Expect(cloudProfile.Spec.MachineImages[0].Versions[0].CapabilityFlavors).To(ConsistOf([]v1beta1.MachineImageFlavor{
217+
{Capabilities: v1beta1.Capabilities{"architecture": []string{"arm64"}}},
218+
{Capabilities: v1beta1.Capabilities{"architecture": []string{"amd64"}}},
219+
}))
220+
Expect(cloudProfile.Spec.MachineImages[0].Versions[1].Version).To(Equal("1.0.1"))
221+
Expect(cloudProfile.Spec.MachineImages[0].Versions[1].CapabilityFlavors).To(ConsistOf([]v1beta1.MachineImageFlavor{
222+
{Capabilities: v1beta1.Capabilities{"architecture": []string{"amd64"}}},
223+
}))
224+
// The second machine image should be added completely.
225+
Expect(cloudProfile.Spec.MachineImages[1].Versions).To(HaveLen(2))
226+
Expect(cloudProfile.Spec.MachineImages[1].Versions[0].Version).To(Equal("1.0.0"))
227+
Expect(cloudProfile.Spec.MachineImages[1].Versions[0].CapabilityFlavors).To(ConsistOf([]v1beta1.MachineImageFlavor{
228+
{Capabilities: v1beta1.Capabilities{"architecture": []string{"amd64"}}},
229+
}))
230+
Expect(cloudProfile.Spec.MachineImages[1].Versions[1].Version).To(Equal("1.0.1"))
231+
Expect(cloudProfile.Spec.MachineImages[1].Versions[1].CapabilityFlavors).To(ConsistOf([]v1beta1.MachineImageFlavor{
232+
{Capabilities: v1beta1.Capabilities{"architecture": []string{"arm64"}}},
233+
{Capabilities: v1beta1.Capabilities{"architecture": []string{"amd64"}}},
234+
}))
235+
236+
})
237+
})
238+
239+
})
240+
})

pkg/admission/mutator/webhook.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func New(mgr manager.Manager) (*extensionswebhook.Webhook, error) {
3333
Mutators: map[extensionswebhook.Mutator][]extensionswebhook.Type{
3434
NewShootMutator(mgr): {{Obj: &gardencorev1beta1.Shoot{}}},
3535
NewNamespacedCloudProfileMutator(mgr): {{Obj: &gardencorev1beta1.NamespacedCloudProfile{}, Subresource: ptr.To("status")}},
36+
NewCloudProfileMutator(mgr): {{Obj: &gardencorev1beta1.CloudProfile{}}},
3637
},
3738
Target: extensionswebhook.TargetSeed,
3839
ObjectSelector: &metav1.LabelSelector{

0 commit comments

Comments
 (0)