Skip to content

Commit

Permalink
Add NamespacedCloudProfile validation and mutation (#1016)
Browse files Browse the repository at this point in the history
  • Loading branch information
LucaBernstein authored Nov 20, 2024
1 parent dd6be7b commit b18bd72
Show file tree
Hide file tree
Showing 7 changed files with 774 additions and 51 deletions.
103 changes: 103 additions & 0 deletions pkg/admission/mutator/namespacedcloudprofile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
//
// SPDX-License-Identifier: Apache-2.0

package mutator

import (
"context"
"encoding/json"
"fmt"
"maps"
"slices"

extensionswebhook "github.com/gardener/gardener/extensions/pkg/webhook"
gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
"github.com/gardener/gardener/pkg/utils"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"

"github.com/gardener/gardener-extension-provider-azure/pkg/apis/azure/v1alpha1"
)

// NewNamespacedCloudProfileMutator returns a new instance of a NamespacedCloudProfile mutator.
func NewNamespacedCloudProfileMutator(mgr manager.Manager) extensionswebhook.Mutator {
return &namespacedCloudProfile{
client: mgr.GetClient(),
decoder: serializer.NewCodecFactory(mgr.GetScheme(), serializer.EnableStrict).UniversalDecoder(),
}
}

type namespacedCloudProfile struct {
client client.Client
decoder runtime.Decoder
}

// Mutate mutates the given NamespacedCloudProfile object.
func (p *namespacedCloudProfile) Mutate(_ context.Context, newObj, _ client.Object) error {
profile, ok := newObj.(*gardencorev1beta1.NamespacedCloudProfile)
if !ok {
return fmt.Errorf("wrong object type %T", newObj)
}

// Ignore NamespacedCloudProfiles being deleted and wait for core mutator to patch the status.
if profile.DeletionTimestamp != nil || profile.Generation != profile.Status.ObservedGeneration ||
profile.Spec.ProviderConfig == nil || profile.Status.CloudProfileSpec.ProviderConfig == nil {
return nil
}

specConfig := &v1alpha1.CloudProfileConfig{}
if _, _, err := p.decoder.Decode(profile.Spec.ProviderConfig.Raw, nil, specConfig); err != nil {
return fmt.Errorf("could not decode providerConfig of namespacedCloudProfile spec for '%s': %w", profile.Name, err)
}
statusConfig := &v1alpha1.CloudProfileConfig{}
if _, _, err := p.decoder.Decode(profile.Status.CloudProfileSpec.ProviderConfig.Raw, nil, statusConfig); err != nil {
return fmt.Errorf("could not decode providerConfig of namespacedCloudProfile status for '%s': %w", profile.Name, err)
}

statusConfig.MachineImages = mergeMachineImages(specConfig.MachineImages, statusConfig.MachineImages)
statusConfig.MachineTypes = mergeMachineTypes(specConfig.MachineTypes, statusConfig.MachineTypes)

modifiedStatusConfig, err := json.Marshal(statusConfig)
if err != nil {
return err
}
profile.Status.CloudProfileSpec.ProviderConfig.Raw = modifiedStatusConfig

return nil
}

func mergeMachineImages(specMachineImages, statusMachineImages []v1alpha1.MachineImages) []v1alpha1.MachineImages {
specImages := utils.CreateMapFromSlice(specMachineImages, func(mi v1alpha1.MachineImages) string { return mi.Name })
statusImages := utils.CreateMapFromSlice(statusMachineImages, func(mi v1alpha1.MachineImages) string { return mi.Name })
for _, specMachineImage := range specImages {
if _, exists := statusImages[specMachineImage.Name]; !exists {
statusImages[specMachineImage.Name] = specMachineImage
} else {
statusImageVersions := utils.CreateMapFromSlice(statusImages[specMachineImage.Name].Versions, func(v v1alpha1.MachineImageVersion) string { return v.Version })
specImageVersions := utils.CreateMapFromSlice(specImages[specMachineImage.Name].Versions, func(v v1alpha1.MachineImageVersion) string { return v.Version })
for _, version := range specImageVersions {
statusImageVersions[version.Version] = version
}

statusImages[specMachineImage.Name] = v1alpha1.MachineImages{
Name: specMachineImage.Name,
Versions: slices.Collect(maps.Values(statusImageVersions)),
}
}
}
return slices.Collect(maps.Values(statusImages))
}

func mergeMachineTypes(specMachineTypes, statusMachineTypes []v1alpha1.MachineType) []v1alpha1.MachineType {
specImages := utils.CreateMapFromSlice(specMachineTypes, func(mi v1alpha1.MachineType) string { return mi.Name })
statusImages := utils.CreateMapFromSlice(statusMachineTypes, func(mi v1alpha1.MachineType) string { return mi.Name })
for _, specMachineImage := range specImages {
if _, exists := statusImages[specMachineImage.Name]; !exists {
statusImages[specMachineImage.Name] = specMachineImage
}
}
return slices.Collect(maps.Values(statusImages))
}
144 changes: 144 additions & 0 deletions pkg/admission/mutator/namespacedcloudprofile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
//
// SPDX-License-Identifier: Apache-2.0

package mutator_test

import (
"context"

"github.com/gardener/gardener/extensions/pkg/util"
extensionswebhook "github.com/gardener/gardener/extensions/pkg/webhook"
"github.com/gardener/gardener/pkg/apis/core/v1beta1"
"github.com/gardener/gardener/pkg/utils/test"
. "github.com/gardener/gardener/pkg/utils/test/matchers"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gstruct"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/manager"

"github.com/gardener/gardener-extension-provider-azure/pkg/admission/mutator"
api "github.com/gardener/gardener-extension-provider-azure/pkg/apis/azure"
"github.com/gardener/gardener-extension-provider-azure/pkg/apis/azure/install"
)

var _ = Describe("NamespacedCloudProfile Mutator", func() {
var (
fakeClient client.Client
fakeManager manager.Manager
namespace string
ctx = context.Background()
decoder runtime.Decoder

namespacedCloudProfileMutator extensionswebhook.Mutator
namespacedCloudProfile *v1beta1.NamespacedCloudProfile
)

BeforeEach(func() {
scheme := runtime.NewScheme()
utilruntime.Must(install.AddToScheme(scheme))
utilruntime.Must(v1beta1.AddToScheme(scheme))
fakeClient = fakeclient.NewClientBuilder().WithScheme(scheme).Build()
fakeManager = &test.FakeManager{
Client: fakeClient,
Scheme: scheme,
}
namespace = "garden-dev"
decoder = serializer.NewCodecFactory(fakeManager.GetScheme(), serializer.EnableStrict).UniversalDecoder()

namespacedCloudProfileMutator = mutator.NewNamespacedCloudProfileMutator(fakeManager)
namespacedCloudProfile = &v1beta1.NamespacedCloudProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "profile-1",
Namespace: namespace,
},
}
})

Describe("#Mutate", func() {
It("should succeed for NamespacedCloudProfile without provider config", func() {
Expect(namespacedCloudProfileMutator.Mutate(ctx, namespacedCloudProfile, nil)).To(Succeed())
})

It("should skip if NamespacedCloudProfile is in deletion phase", func() {
namespacedCloudProfile.DeletionTimestamp = ptr.To(metav1.Now())
expectedProfile := namespacedCloudProfile.DeepCopy()

Expect(namespacedCloudProfileMutator.Mutate(ctx, namespacedCloudProfile, nil)).To(Succeed())

Expect(namespacedCloudProfile).To(DeepEqual(expectedProfile))
})

Describe("merge the provider configurations from a NamespacedCloudProfile and the parent CloudProfile", func() {
It("should correctly merge extended machineImages", func() {
namespacedCloudProfile.Status.CloudProfileSpec.ProviderConfig = &runtime.RawExtension{Raw: []byte(`{
"apiVersion":"azure.provider.extensions.gardener.cloud/v1alpha1",
"kind":"CloudProfileConfig",
"machineImages":[
{"name":"image-1","versions":[{"version":"1.0","id":"local/image:1.0"}]}
]}`)}
namespacedCloudProfile.Spec.ProviderConfig = &runtime.RawExtension{Raw: []byte(`{
"apiVersion":"azure.provider.extensions.gardener.cloud/v1alpha1",
"kind":"CloudProfileConfig",
"machineImages":[
{"name":"image-1","versions":[{"version":"1.1","id":"local/image:1.1"}]},
{"name":"image-2","versions":[{"version":"2.0","id":"local/image:2.0"}]}
]}`)}

Expect(namespacedCloudProfileMutator.Mutate(ctx, namespacedCloudProfile, nil)).To(Succeed())

mergedConfig, err := decodeCloudProfileConfig(decoder, namespacedCloudProfile.Status.CloudProfileSpec.ProviderConfig)
Expect(err).ToNot(HaveOccurred())
Expect(mergedConfig.MachineImages).To(ConsistOf(
MatchFields(IgnoreExtras, Fields{
"Name": Equal("image-1"),
"Versions": ContainElements(
api.MachineImageVersion{Version: "1.0", ID: ptr.To("local/image:1.0"), Architecture: ptr.To("amd64")},
api.MachineImageVersion{Version: "1.1", ID: ptr.To("local/image:1.1"), Architecture: ptr.To("amd64")},
),
}),
MatchFields(IgnoreExtras, Fields{
"Name": Equal("image-2"),
"Versions": ContainElements(api.MachineImageVersion{Version: "2.0", ID: ptr.To("local/image:2.0"), Architecture: ptr.To("amd64")}),
}),
))
})

It("should correctly merge added machineTypes", func() {
namespacedCloudProfile.Status.CloudProfileSpec.ProviderConfig = &runtime.RawExtension{Raw: []byte(`{
"apiVersion":"azure.provider.extensions.gardener.cloud/v1alpha1",
"kind":"CloudProfileConfig",
"machineTypes":[{"name":"type-1"}]}`)}
namespacedCloudProfile.Spec.ProviderConfig = &runtime.RawExtension{Raw: []byte(`{
"apiVersion":"azure.provider.extensions.gardener.cloud/v1alpha1",
"kind":"CloudProfileConfig",
"machineTypes":[{"name":"type-2","acceleratedNetworking":true}]}`)}

Expect(namespacedCloudProfileMutator.Mutate(ctx, namespacedCloudProfile, nil)).To(Succeed())

mergedConfig, err := decodeCloudProfileConfig(decoder, namespacedCloudProfile.Status.CloudProfileSpec.ProviderConfig)
Expect(err).ToNot(HaveOccurred())
var boolNil *bool
Expect(mergedConfig.MachineTypes).To(ConsistOf(
MatchFields(IgnoreExtras, Fields{"Name": Equal("type-1"), "AcceleratedNetworking": BeEquivalentTo(boolNil)}),
MatchFields(IgnoreExtras, Fields{"Name": Equal("type-2"), "AcceleratedNetworking": BeEquivalentTo(ptr.To(true))}),
))
})
})
})
})

func decodeCloudProfileConfig(decoder runtime.Decoder, config *runtime.RawExtension) (*api.CloudProfileConfig, error) {
cloudProfileConfig := &api.CloudProfileConfig{}
if err := util.Decode(decoder, config.Raw, cloudProfileConfig); err != nil {
return nil, err
}
return cloudProfileConfig, nil
}
5 changes: 3 additions & 2 deletions pkg/admission/mutator/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const (

var logger = log.Log.WithName("azure-mutator-webhook")

// New creates a new webhook that mutates Shoot resources.
// New creates a new webhook that mutates Shoot and NamespacedCloudProfile resources.
func New(mgr manager.Manager) (*extensionswebhook.Webhook, error) {
logger.Info("Setting up webhook", "name", Name)

Expand All @@ -30,7 +30,8 @@ func New(mgr manager.Manager) (*extensionswebhook.Webhook, error) {
Name: Name,
Path: "/webhooks/mutate",
Mutators: map[extensionswebhook.Mutator][]extensionswebhook.Type{
NewShootMutator(mgr): {{Obj: &gardencorev1beta1.Shoot{}}},
NewShootMutator(mgr): {{Obj: &gardencorev1beta1.Shoot{}}},
NewNamespacedCloudProfileMutator(mgr): {{Obj: &gardencorev1beta1.NamespacedCloudProfile{}}},
},
Target: extensionswebhook.TargetSeed,
ObjectSelector: &metav1.LabelSelector{
Expand Down
Loading

0 comments on commit b18bd72

Please sign in to comment.