Skip to content

Commit

Permalink
Add support for cloudprofile field and validate namespacedcloudprofiles
Browse files Browse the repository at this point in the history
  • Loading branch information
LucaBernstein committed Aug 13, 2024
1 parent 766d672 commit 02e8cef
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 23 deletions.
3 changes: 2 additions & 1 deletion docs/usage/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ metadata:
name: johndoe-openstack
namespace: garden-dev
spec:
cloudProfileName: openstack
cloudProfile:
name: openstack
region: europe-1
secretBindingName: core-openstack
provider:
Expand Down
51 changes: 51 additions & 0 deletions pkg/admission/validator/namespacedcloudprofile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
//
// SPDX-License-Identifier: Apache-2.0

package validator

import (
"context"
"fmt"

extensionswebhook "github.com/gardener/gardener/extensions/pkg/webhook"
"github.com/gardener/gardener/pkg/apis/core"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/validation/field"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"

openstackvalidation "github.com/gardener/gardener-extension-provider-openstack/pkg/apis/openstack/validation"
)

// NewNamespacedCloudProfileValidator returns a new instance of a cloud profile validator.
func NewNamespacedCloudProfileValidator(mgr manager.Manager) extensionswebhook.Validator {
return &namespacedCloudProfile{
decoder: serializer.NewCodecFactory(mgr.GetScheme(), serializer.EnableStrict).UniversalDecoder(),
}
}

type namespacedCloudProfile struct {
decoder runtime.Decoder
}

// Validate validates the given namespaced cloud profile objects.
func (cp *namespacedCloudProfile) Validate(_ context.Context, new, _ client.Object) error {
cloudProfile, ok := new.(*core.NamespacedCloudProfile)
if !ok {
return fmt.Errorf("wrong object type %T", new)
}

providerConfigPath := field.NewPath("status", "cloudProfileSpec").Child("providerConfig")
if cloudProfile.Status.CloudProfileSpec.ProviderConfig == nil {
return field.Required(providerConfigPath, "providerConfig must be calculated for OpenStack namespaced cloud profiles")
}

cpConfig, err := decodeCloudProfileConfig(cp.decoder, cloudProfile.Status.CloudProfileSpec.ProviderConfig)
if err != nil {
return err
}

return openstackvalidation.ValidateCloudProfileConfig(cpConfig, providerConfigPath).ToAggregate()
}
44 changes: 30 additions & 14 deletions pkg/admission/validator/shoot.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import (
"github.com/gardener/gardener/pkg/apis/core"
gardencorehelper "github.com/gardener/gardener/pkg/apis/core/helper"
gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
"github.com/gardener/gardener/pkg/utils/gardener"
kutil "github.com/gardener/gardener/pkg/utils/kubernetes"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/validation/field"
Expand All @@ -33,6 +35,7 @@ func NewShootValidator(mgr manager.Manager) extensionswebhook.Validator {
apiReader: mgr.GetAPIReader(),
decoder: serializer.NewCodecFactory(mgr.GetScheme(), serializer.EnableStrict).UniversalDecoder(),
lenientDecoder: serializer.NewCodecFactory(mgr.GetScheme()).UniversalDecoder(),
scheme: mgr.GetScheme(),
}
}

Expand All @@ -41,6 +44,7 @@ type shoot struct {
apiReader client.Reader
decoder runtime.Decoder
lenientDecoder runtime.Decoder
scheme *runtime.Scheme
}

type validationContext struct {
Expand Down Expand Up @@ -85,19 +89,37 @@ func (s *shoot) Validate(ctx context.Context, new, old client.Object) error {
}
}

shootV1Beta1 := &gardencorev1beta1.Shoot{
TypeMeta: metav1.TypeMeta{
APIVersion: gardencorev1beta1.SchemeGroupVersion.String(),
Kind: "Shoot",
},
}
err := s.scheme.Convert(shoot, shootV1Beta1, ctx)
if err != nil {
return err
}
cloudProfile, err := gardener.GetCloudProfile(ctx, s.client, shootV1Beta1)
if err != nil {
return err
}
if cloudProfile == nil {
return fmt.Errorf("cloudprofile could not be found")
}

if old != nil {
oldShoot, ok := old.(*core.Shoot)
if !ok {
return fmt.Errorf("wrong object type %T for old object", old)
}
return s.validateShootUpdate(ctx, oldShoot, shoot, credentials)
return s.validateShootUpdate(oldShoot, shoot, credentials, cloudProfile)
}

return s.validateShootCreation(ctx, shoot, credentials)
return s.validateShootCreation(shoot, credentials, cloudProfile)
}

func (s *shoot) validateShootCreation(ctx context.Context, shoot *core.Shoot, credentials *openstack.Credentials) error {
valContext, err := newValidationContext(ctx, s.decoder, s.client, shoot)
func (s *shoot) validateShootCreation(shoot *core.Shoot, credentials *openstack.Credentials, cloudProfile *gardencorev1beta1.CloudProfile) error {
valContext, err := newValidationContext(s.decoder, shoot, cloudProfile)
if err != nil {
return err
}
Expand All @@ -112,13 +134,13 @@ func (s *shoot) validateShootCreation(ctx context.Context, shoot *core.Shoot, cr
return allErrs.ToAggregate()
}

func (s *shoot) validateShootUpdate(ctx context.Context, oldShoot, shoot *core.Shoot, credentials *openstack.Credentials) error {
oldValContext, err := newValidationContext(ctx, s.lenientDecoder, s.client, oldShoot)
func (s *shoot) validateShootUpdate(oldShoot, shoot *core.Shoot, credentials *openstack.Credentials, cloudProfile *gardencorev1beta1.CloudProfile) error {
oldValContext, err := newValidationContext(s.lenientDecoder, oldShoot, cloudProfile)
if err != nil {
return err
}

valContext, err := newValidationContext(ctx, s.decoder, s.client, shoot)
valContext, err := newValidationContext(s.decoder, shoot, cloudProfile)
if err != nil {
return err
}
Expand Down Expand Up @@ -167,7 +189,7 @@ func (s *shoot) validateShoot(context *validationContext) field.ErrorList {
return allErrs
}

func newValidationContext(ctx context.Context, decoder runtime.Decoder, c client.Client, shoot *core.Shoot) (*validationContext, error) {
func newValidationContext(decoder runtime.Decoder, shoot *core.Shoot, cloudProfile *gardencorev1beta1.CloudProfile) (*validationContext, error) {
if shoot.Spec.Provider.InfrastructureConfig == nil {
return nil, field.Required(infraConfigPath, "infrastructureConfig must be set for OpenStack shoots")
}
Expand All @@ -184,12 +206,6 @@ func newValidationContext(ctx context.Context, decoder runtime.Decoder, c client
return nil, fmt.Errorf("error decoding controlPlaneConfig: %v", err)
}

cloudProfile := &gardencorev1beta1.CloudProfile{}

if err := c.Get(ctx, client.ObjectKey{Name: *shoot.Spec.CloudProfileName}, cloudProfile); err != nil {
return nil, err
}

if cloudProfile.Spec.ProviderConfig == nil {
return nil, fmt.Errorf("providerConfig is not given for cloud profile %q", cloudProfile.Name)
}
Expand Down
169 changes: 164 additions & 5 deletions pkg/admission/validator/shoot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package validator_test

import (
"context"
"encoding/json"

extensionswebhook "github.com/gardener/gardener/extensions/pkg/webhook"
"github.com/gardener/gardener/pkg/apis/core"
Expand All @@ -18,8 +19,12 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/gardener/gardener-extension-provider-openstack/pkg/admission/validator"
apisopenstack "github.com/gardener/gardener-extension-provider-openstack/pkg/apis/openstack"
apisopenstackv1alpha "github.com/gardener/gardener-extension-provider-openstack/pkg/apis/openstack/v1alpha1"
openstackv1alpha1 "github.com/gardener/gardener-extension-provider-openstack/pkg/apis/openstack/v1alpha1"
)

var _ = Describe("Shoot validator", func() {
Expand All @@ -36,35 +41,84 @@ var _ = Describe("Shoot validator", func() {
shoot *core.Shoot

ctx = context.TODO()

regionName string
imageName string
imageVersion string
architecture *string
)

BeforeEach(func() {
ctrl = gomock.NewController(GinkgoT())

scheme := runtime.NewScheme()
Expect(apisopenstack.AddToScheme(scheme)).To(Succeed())
Expect(apisopenstackv1alpha.AddToScheme(scheme)).To(Succeed())
Expect(gardencorev1beta1.AddToScheme(scheme)).To(Succeed())

c = mockclient.NewMockClient(ctrl)
apiReader = mockclient.NewMockReader(ctrl)

mgr = mockmanager.NewMockManager(ctrl)
mgr.EXPECT().GetScheme().Return(scheme).Times(2)
mgr.EXPECT().GetScheme().Return(scheme).Times(3)
mgr.EXPECT().GetClient().Return(c)
mgr.EXPECT().GetAPIReader().Return(apiReader)
shootValidator = validator.NewShootValidator(mgr)

regionName = "eu-de-1"
imageName = "Foo"
imageVersion = "1.0.0"
architecture = ptr.To("analog")

shoot = &core.Shoot{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: namespace,
},
Spec: core.ShootSpec{
CloudProfileName: "cloudProfile",
CloudProfileName: ptr.To("cloudProfile"),
Provider: core.Provider{
Type: "openstack",
Workers: []core.Worker{},
Type: "openstack",
Workers: []core.Worker{
{
Name: "worker-1",
Volume: &core.Volume{
VolumeSize: "50Gi",
Type: ptr.To("volumeType"),
},
Zones: []string{"zone1"},
Machine: core.Machine{
Image: &core.ShootMachineImage{
Name: imageName,
Version: imageVersion,
},
Architecture: architecture,
},
},
},
InfrastructureConfig: &runtime.RawExtension{
Raw: encode(&openstackv1alpha1.InfrastructureConfig{
TypeMeta: metav1.TypeMeta{
APIVersion: openstackv1alpha1.SchemeGroupVersion.String(),
Kind: "InfrastructureConfig",
},
Networks: openstackv1alpha1.Networks{
Workers: "10.250.0.0/19",
},
FloatingPoolName: "pool-1",
}),
},
ControlPlaneConfig: &runtime.RawExtension{
Raw: encode(&openstackv1alpha1.ControlPlaneConfig{
TypeMeta: metav1.TypeMeta{
APIVersion: openstackv1alpha1.SchemeGroupVersion.String(),
Kind: "ControlPlaneConfig",
},
LoadBalancerProvider: "haproxy",
}),
},
},
Region: "us-west",
Region: "eu-de-1",
Networking: &core.Networking{
Nodes: ptr.To("10.250.0.0/16"),
},
Expand All @@ -82,5 +136,110 @@ var _ = Describe("Shoot validator", func() {
Expect(err).NotTo(HaveOccurred())
})
})

Context("Shoot creation", func() {
var (
cloudProfileKey client.ObjectKey
namespacedCloudProfileKey client.ObjectKey

cloudProfile *gardencorev1beta1.CloudProfile
namespacedCloudProfile *gardencorev1beta1.NamespacedCloudProfile
)

BeforeEach(func() {
cloudProfileKey = client.ObjectKey{Name: "openstack"}
namespacedCloudProfileKey = client.ObjectKey{Name: "openstack-nscpfl", Namespace: namespace}

cloudProfile = &gardencorev1beta1.CloudProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "openstack",
},
Spec: gardencorev1beta1.CloudProfileSpec{
Regions: []gardencorev1beta1.Region{
{
Name: regionName,
Zones: []gardencorev1beta1.AvailabilityZone{
{
Name: "zone1",
},
{
Name: "zone2",
},
},
},
},
ProviderConfig: &runtime.RawExtension{
Raw: encode(&apisopenstackv1alpha.CloudProfileConfig{
TypeMeta: metav1.TypeMeta{
APIVersion: apisopenstackv1alpha.SchemeGroupVersion.String(),
Kind: "CloudProfileConfig",
},
MachineImages: []apisopenstackv1alpha.MachineImages{
{
Name: imageName,
Versions: []apisopenstackv1alpha.MachineImageVersion{
{
Version: imageVersion,
Regions: []apisopenstackv1alpha.RegionIDMapping{
{
Name: regionName,
ID: "Bar",
Architecture: architecture,
},
},
},
},
},
},
}),
},
},
}

namespacedCloudProfile = &gardencorev1beta1.NamespacedCloudProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "openstack-nscpfl",
},
Spec: gardencorev1beta1.NamespacedCloudProfileSpec{
Parent: gardencorev1beta1.CloudProfileReference{
Kind: "CloudProfile",
Name: "openstack",
},
},
Status: gardencorev1beta1.NamespacedCloudProfileStatus{
CloudProfileSpec: cloudProfile.Spec,
},
}
})

It("should work for CloudProfile reference instead of cloudProfileName in Shoot", func() {
shoot.Spec.CloudProfileName = nil
shoot.Spec.CloudProfile = &core.CloudProfileReference{
Kind: "CloudProfile",
Name: "openstack",
}
c.EXPECT().Get(ctx, cloudProfileKey, &gardencorev1beta1.CloudProfile{}).SetArg(2, *cloudProfile)

err := shootValidator.Validate(ctx, shoot, nil)
Expect(err).NotTo(HaveOccurred())
})

It("should work for NamespacedCloudProfile referenced from Shoot", func() {
shoot.Spec.CloudProfileName = nil
shoot.Spec.CloudProfile = &core.CloudProfileReference{
Kind: "NamespacedCloudProfile",
Name: "openstack-nscpfl",
}
c.EXPECT().Get(ctx, namespacedCloudProfileKey, &gardencorev1beta1.NamespacedCloudProfile{}).SetArg(2, *namespacedCloudProfile)

err := shootValidator.Validate(ctx, shoot, nil)
Expect(err).NotTo(HaveOccurred())
})
})
})
})

func encode(obj runtime.Object) []byte {
data, _ := json.Marshal(obj)
return data
}
Loading

0 comments on commit 02e8cef

Please sign in to comment.