Skip to content

Commit e81009d

Browse files
Add CloudProfile validation (#769)
1 parent d7a5dfb commit e81009d

5 files changed

Lines changed: 166 additions & 38 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// SPDX-FileCopyrightText: SAP SE or an SAP affiliate company and Gardener contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package validator
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
extensionswebhook "github.com/gardener/gardener/extensions/pkg/webhook"
11+
"github.com/gardener/gardener/pkg/apis/core"
12+
"k8s.io/apimachinery/pkg/runtime"
13+
"k8s.io/apimachinery/pkg/runtime/serializer"
14+
"k8s.io/apimachinery/pkg/util/validation/field"
15+
"sigs.k8s.io/controller-runtime/pkg/client"
16+
"sigs.k8s.io/controller-runtime/pkg/manager"
17+
18+
ironcorevalidation "github.com/ironcore-dev/gardener-extension-provider-ironcore/pkg/apis/ironcore/validation"
19+
)
20+
21+
// NewCloudProfileValidator returns a new instance of a cloud profile validator.
22+
func NewCloudProfileValidator(mgr manager.Manager) extensionswebhook.Validator {
23+
return &cloudProfile{
24+
decoder: serializer.NewCodecFactory(mgr.GetScheme(), serializer.EnableStrict).UniversalDecoder(),
25+
}
26+
}
27+
28+
type cloudProfile struct {
29+
decoder runtime.Decoder
30+
}
31+
32+
// Validate validates the given CloudProfile objects.
33+
func (cp *cloudProfile) Validate(_ context.Context, newObj, _ client.Object) error {
34+
cloudProfile, ok := newObj.(*core.CloudProfile)
35+
if !ok {
36+
return fmt.Errorf("wrong object type %T", newObj)
37+
}
38+
39+
providerConfigPath := field.NewPath("spec").Child("providerConfig")
40+
if cloudProfile.Spec.ProviderConfig == nil {
41+
return field.Required(providerConfigPath, "providerConfig must be set for Ironcore cloud profiles")
42+
}
43+
44+
cpConfig, err := decodeCloudProfileConfig(cp.decoder, cloudProfile.Spec.ProviderConfig)
45+
if err != nil {
46+
return err
47+
}
48+
49+
return ironcorevalidation.ValidateCloudProfileConfig(cpConfig, cloudProfile.Spec.MachineImages, providerConfigPath).ToAggregate()
50+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// SPDX-FileCopyrightText: SAP SE or an SAP affiliate company and Gardener contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package validator
5+
6+
import (
7+
"github.com/gardener/gardener/extensions/pkg/util"
8+
"k8s.io/apimachinery/pkg/runtime"
9+
10+
"github.com/ironcore-dev/gardener-extension-provider-ironcore/pkg/apis/ironcore"
11+
)
12+
13+
func decodeCloudProfileConfig(decoder runtime.Decoder, config *runtime.RawExtension) (*ironcore.CloudProfileConfig, error) {
14+
cloudProfileConfig := &ironcore.CloudProfileConfig{}
15+
if err := util.Decode(decoder, config.Raw, cloudProfileConfig); err != nil {
16+
return nil, err
17+
}
18+
return cloudProfileConfig, nil
19+
}

pkg/admission/validator/webhook.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func New(mgr manager.Manager) (*extensionswebhook.Webhook, error) {
3434
Path: "/webhooks/validate",
3535
Validators: map[extensionswebhook.Validator][]extensionswebhook.Type{
3636
NewShootValidator(mgr): {{Obj: &core.Shoot{}}},
37+
NewCloudProfileValidator(mgr): {{Obj: &core.CloudProfile{}}},
3738
NewSecretBindingValidator(mgr): {{Obj: &core.SecretBinding{}}},
3839
},
3940
Target: extensionswebhook.TargetSeed,
@@ -54,5 +55,9 @@ func NewSecretsWebhook(mgr manager.Manager) (*extensionswebhook.Webhook, error)
5455
Validators: map[extensionswebhook.Validator][]extensionswebhook.Type{
5556
NewSecretValidator(): {{Obj: &corev1.Secret{}}},
5657
},
58+
Target: extensionswebhook.TargetSeed,
59+
ObjectSelector: &metav1.LabelSelector{
60+
MatchLabels: map[string]string{constants.LabelExtensionProviderTypePrefix + ironcore.Type: "true"},
61+
},
5762
})
5863
}

pkg/apis/ironcore/validation/cloudprofile.go

Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ package validation
66
import (
77
"fmt"
88

9+
"github.com/gardener/gardener/extensions/pkg/util"
910
gardenercore "github.com/gardener/gardener/pkg/apis/core"
10-
gardenercorehelper "github.com/gardener/gardener/pkg/apis/core/helper"
1111
v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants"
12+
"github.com/gardener/gardener/pkg/utils"
1213
apivalidation "k8s.io/apimachinery/pkg/api/validation"
1314
"k8s.io/apimachinery/pkg/util/validation/field"
15+
"k8s.io/utils/ptr"
1416
"k8s.io/utils/strings/slices"
1517

1618
apisironcore "github.com/ironcore-dev/gardener-extension-provider-ironcore/pkg/apis/ironcore"
@@ -21,19 +23,12 @@ func ValidateCloudProfileConfig(cpConfig *apisironcore.CloudProfileConfig, machi
2123
allErrs := field.ErrorList{}
2224
machineImagesPath := fldPath.Child("machineImages")
2325

24-
for _, image := range machineImages {
25-
var processed bool
26-
for i, imageConfig := range cpConfig.MachineImages {
27-
if image.Name == imageConfig.Name {
28-
allErrs = append(allErrs, validateVersions(imageConfig.Versions, gardenercorehelper.ToExpirableVersions(image.Versions), machineImagesPath.Index(i).Child("versions"))...)
29-
processed = true
30-
break
31-
}
32-
}
33-
if !processed && len(image.Versions) > 0 {
34-
allErrs = append(allErrs, field.Required(machineImagesPath, fmt.Sprintf("must provide an image mapping for image %q", image.Name)))
35-
}
26+
// validate all provider images fields
27+
for i, machineImage := range cpConfig.MachineImages {
28+
idxPath := machineImagesPath.Index(i)
29+
allErrs = append(allErrs, ValidateProviderMachineImage(idxPath, machineImage)...)
3630
}
31+
allErrs = append(allErrs, validateProviderImagesMapping(cpConfig.MachineImages, machineImages, field.NewPath("spec").Child("machineImages"))...)
3732

3833
if cpConfig.StorageClasses.Default != nil {
3934
for _, msg := range apivalidation.NameIsDNSLabel(cpConfig.StorageClasses.Default.Name, false) {
@@ -50,27 +45,81 @@ func ValidateCloudProfileConfig(cpConfig *apisironcore.CloudProfileConfig, machi
5045
return allErrs
5146
}
5247

53-
func validateVersions(versionsConfig []apisironcore.MachineImageVersion, versions []gardenercore.ExpirableVersion, fldPath *field.Path) field.ErrorList {
48+
// ValidateProviderMachineImage validates a CloudProfileConfig MachineImages entry.
49+
func ValidateProviderMachineImage(validationPath *field.Path, machineImage apisironcore.MachineImages) field.ErrorList {
5450
allErrs := field.ErrorList{}
5551

56-
for _, version := range versions {
57-
var processed bool
58-
for j, versionConfig := range versionsConfig {
59-
jdxPath := fldPath.Index(j)
60-
if version.Version == versionConfig.Version {
61-
if len(versionConfig.Image) == 0 {
62-
allErrs = append(allErrs, field.Required(jdxPath.Child("image"), "must provide an image"))
63-
}
64-
if !slices.Contains(v1beta1constants.ValidArchitectures, *versionConfig.Architecture) {
65-
allErrs = append(allErrs, field.NotSupported(jdxPath.Child("architecture"), *versionConfig.Architecture, v1beta1constants.ValidArchitectures))
52+
if len(machineImage.Name) == 0 {
53+
allErrs = append(allErrs, field.Required(validationPath.Child("name"), "must provide a name"))
54+
}
55+
56+
if len(machineImage.Versions) == 0 {
57+
allErrs = append(allErrs, field.Required(validationPath.Child("versions"), fmt.Sprintf("must provide at least one version for machine image %q", machineImage.Name)))
58+
}
59+
for j, version := range machineImage.Versions {
60+
jdxPath := validationPath.Child("versions").Index(j)
61+
if len(version.Version) == 0 {
62+
allErrs = append(allErrs, field.Required(jdxPath.Child("version"), "must provide a version"))
63+
}
64+
if len(version.Image) == 0 {
65+
allErrs = append(allErrs, field.Required(jdxPath.Child("image"), "must provide an image"))
66+
}
67+
versionArch := ptr.Deref(version.Architecture, v1beta1constants.ArchitectureAMD64)
68+
if !slices.Contains(v1beta1constants.ValidArchitectures, versionArch) {
69+
allErrs = append(allErrs, field.NotSupported(jdxPath.Child("architecture"), versionArch, v1beta1constants.ValidArchitectures))
70+
}
71+
}
72+
73+
return allErrs
74+
}
75+
76+
// NewProviderImagesContext creates a new ImagesContext for provider images.
77+
func NewProviderImagesContext(providerImages []apisironcore.MachineImages) *util.ImagesContext[apisironcore.MachineImages, apisironcore.MachineImageVersion] {
78+
return util.NewImagesContext(
79+
utils.CreateMapFromSlice(providerImages, func(mi apisironcore.MachineImages) string { return mi.Name }),
80+
func(mi apisironcore.MachineImages) map[string]apisironcore.MachineImageVersion {
81+
return utils.CreateMapFromSlice(mi.Versions, func(v apisironcore.MachineImageVersion) string { return providerMachineImageKey(v) })
82+
},
83+
)
84+
}
85+
86+
func providerMachineImageKey(v apisironcore.MachineImageVersion) string {
87+
return VersionArchitectureKey(v.Version, ptr.Deref(v.Architecture, v1beta1constants.ArchitectureAMD64))
88+
}
89+
90+
// VersionArchitectureKey returns a key for a version and architecture.
91+
func VersionArchitectureKey(version, architecture string) string {
92+
return version + "-" + architecture
93+
}
94+
95+
// verify that for each cp image a provider image exists
96+
func validateProviderImagesMapping(cpConfigImages []apisironcore.MachineImages, machineImages []gardenercore.MachineImage, fldPath *field.Path) field.ErrorList {
97+
allErrs := field.ErrorList{}
98+
99+
providerImages := NewProviderImagesContext(cpConfigImages)
100+
101+
// for each image in the CloudProfile, check if it exists in the CloudProfileConfig
102+
for idxImage, machineImage := range machineImages {
103+
if len(machineImage.Versions) == 0 {
104+
continue
105+
}
106+
machineImagePath := fldPath.Index(idxImage)
107+
if _, existsInParent := providerImages.GetImage(machineImage.Name); !existsInParent {
108+
allErrs = append(allErrs, field.Required(machineImagePath, fmt.Sprintf("must provide a provider image mapping for image %q", machineImage.Name)))
109+
continue
110+
}
111+
112+
// validate that for each version and architecture of an image in the cloud profile a
113+
// corresponding provider specific image in the cloud profile config exists
114+
for versionIdx, version := range machineImage.Versions {
115+
imageVersionPath := machineImagePath.Child("versions").Index(versionIdx)
116+
for _, expectedArchitecture := range version.Architectures {
117+
if _, exists := providerImages.GetImageVersion(machineImage.Name, VersionArchitectureKey(version.Version, expectedArchitecture)); !exists {
118+
allErrs = append(allErrs, field.Required(imageVersionPath,
119+
fmt.Sprintf("must provide an image mapping for version %q and architecture: %s", version.Version, expectedArchitecture)))
66120
}
67-
processed = true
68-
break
69121
}
70122
}
71-
if !processed {
72-
allErrs = append(allErrs, field.Required(fldPath, fmt.Sprintf("must provide an image mapping for version %q", version.Version)))
73-
}
74123
}
75124

76125
return allErrs

pkg/apis/ironcore/validation/cloudprofile_test.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -110,30 +110,35 @@ var _ = Describe("CloudProfileConfig validation", func() {
110110
errorList := ValidateCloudProfileConfig(cloudProfileConfig, machineImages, nilPath)
111111
Expect(errorList).To(ConsistOf(
112112
PointTo(MatchFields(IgnoreExtras, Fields{
113-
"Type": Equal(field.ErrorTypeRequired),
114-
"Field": Equal("machineImages"),
113+
"Type": Equal(field.ErrorTypeRequired),
114+
"Field": Equal("spec.machineImages[1]"),
115+
"Detail": Equal("must provide a provider image mapping for image \"suse\""),
115116
})),
116117
))
117118
})
118119

119120
It("should forbid unsupported machine image version configuration", func() {
120121
cloudProfileConfig.MachineImages[0].Versions[0].Image = ""
121122
cloudProfileConfig.MachineImages[0].Versions[0].Architecture = ptr.To[string]("foo")
122-
machineImages[0].Versions = append(machineImages[0].Versions, core.MachineImageVersion{ExpirableVersion: core.ExpirableVersion{Version: "2.0.0"}})
123+
machineImages[0].Versions = append(machineImages[0].Versions, core.MachineImageVersion{ExpirableVersion: core.ExpirableVersion{Version: "2.0.0"}, Architectures: []string{"amd64"}})
124+
123125
errorList := ValidateCloudProfileConfig(cloudProfileConfig, machineImages, nilPath)
124126

125127
Expect(errorList).To(ConsistOf(
126128
PointTo(MatchFields(IgnoreExtras, Fields{
127-
"Type": Equal(field.ErrorTypeRequired),
128-
"Field": Equal("machineImages[0].versions"),
129+
"Type": Equal(field.ErrorTypeRequired),
130+
"Field": Equal("machineImages[0].versions[0].image"),
131+
"Detail": Equal("must provide an image"),
129132
})),
130133
PointTo(MatchFields(IgnoreExtras, Fields{
131-
"Type": Equal(field.ErrorTypeRequired),
132-
"Field": Equal("machineImages[0].versions[0].image"),
134+
"Type": Equal(field.ErrorTypeNotSupported),
135+
"Field": Equal("machineImages[0].versions[0].architecture"),
136+
"BadValue": Equal("foo"),
133137
})),
134138
PointTo(MatchFields(IgnoreExtras, Fields{
135-
"Type": Equal(field.ErrorTypeNotSupported),
136-
"Field": Equal("machineImages[0].versions[0].architecture"),
139+
"Type": Equal(field.ErrorTypeRequired),
140+
"Field": Equal("spec.machineImages[0].versions[1]"),
141+
"Detail": Equal("must provide an image mapping for version \"2.0.0\" and architecture: amd64"),
137142
})),
138143
))
139144
})

0 commit comments

Comments
 (0)