Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NamespacedCloudProfile validation and mutation webhooks #1016

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading