From 69af23695fd175d85bed1c5c7c794f55da6afafc Mon Sep 17 00:00:00 2001 From: Andreas Fritzler Date: Tue, 12 Apr 2022 16:49:28 +0200 Subject: [PATCH] Implement `compute` validation Implement validation for the following `compute` types: - `Machine` - `MachineClass` - `MachinePool` Adjust tests to fit the new validation. --- apis/compute/validation/helper.go | 41 ++++++ apis/compute/validation/machine.go | 102 +++++++++++++ apis/compute/validation/machine_test.go | 134 ++++++++++++++++++ apis/compute/validation/machineclass.go | 43 ++++++ apis/compute/validation/machineclass_test.go | 62 ++++++++ apis/compute/validation/machinepool.go | 58 ++++++++ apis/compute/validation/machinepool_test.go | 71 ++++++++++ .../validation/validation_suite_test.go | 73 ++++++++++ controllers/compute/machine_scheduler_test.go | 4 + .../compute/machineclass_controller_test.go | 1 + go.mod | 1 + registry/compute/machine/strategy.go | 8 +- registry/compute/machineclass/strategy.go | 8 +- registry/compute/machinepool/strategy.go | 8 +- 14 files changed, 608 insertions(+), 6 deletions(-) create mode 100644 apis/compute/validation/helper.go create mode 100644 apis/compute/validation/machine.go create mode 100644 apis/compute/validation/machine_test.go create mode 100644 apis/compute/validation/machineclass.go create mode 100644 apis/compute/validation/machineclass_test.go create mode 100644 apis/compute/validation/machinepool.go create mode 100644 apis/compute/validation/machinepool_test.go create mode 100644 apis/compute/validation/validation_suite_test.go diff --git a/apis/compute/validation/helper.go b/apis/compute/validation/helper.go new file mode 100644 index 000000000..04d50b3b8 --- /dev/null +++ b/apis/compute/validation/helper.go @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 by the OnMetal authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package validation + +import ( + "reflect" + + "github.com/onmetal/onmetal-api/equality" + apivalidation "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func ValidateImmutable(newVal, oldVal interface{}, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + if !equality.Semantic.DeepEqual(oldVal, newVal) { + allErrs = append(allErrs, field.Invalid(fldPath, newVal, apivalidation.FieldImmutableErrorMsg)) + } + return allErrs +} + +func ValidateSetOnceField(newVal, oldVal interface{}, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + if !reflect.ValueOf(oldVal).IsZero() { + allErrs = append(allErrs, apivalidation.ValidateImmutableField(newVal, oldVal, fldPath)...) + } + return allErrs +} diff --git a/apis/compute/validation/machine.go b/apis/compute/validation/machine.go new file mode 100644 index 000000000..5a6ec2412 --- /dev/null +++ b/apis/compute/validation/machine.go @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2022 by the OnMetal authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package validation + +import ( + "github.com/onmetal/onmetal-api/apis/compute" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/validation" + apivalidation "k8s.io/apimachinery/pkg/api/validation" + metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// ValidateMachine validates a Machine object. +func ValidateMachine(machine *compute.Machine) field.ErrorList { + var allErrs field.ErrorList + + allErrs = append(allErrs, apivalidation.ValidateObjectMetaAccessor(machine, true, apivalidation.NameIsDNSLabel, field.NewPath("metadata"))...) + allErrs = append(allErrs, validateMachineSpec(&machine.Spec, field.NewPath("spec"))...) + + return allErrs +} + +// ValidateMachineUpdate validates a Machine object before an update. +func ValidateMachineUpdate(newMachine, oldMachine *compute.Machine) field.ErrorList { + var allErrs field.ErrorList + + allErrs = append(allErrs, apivalidation.ValidateObjectMetaAccessorUpdate(newMachine, oldMachine, field.NewPath("metadata"))...) + allErrs = append(allErrs, validateMachineSpecUpdate(&newMachine.Spec, &oldMachine.Spec, newMachine.DeletionTimestamp != nil, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateMachine(newMachine)...) + + return allErrs +} + +// validateMachineSpec validates the spec of a Machine object. +func validateMachineSpec(machineSpec *compute.MachineSpec, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + + if machineSpec.MachineClassRef == (corev1.LocalObjectReference{}) { + allErrs = append(allErrs, field.Required(fldPath.Child("machineClassRef"), "must specify a machine class ref")) + } + + for _, msg := range apivalidation.NameIsDNSLabel(machineSpec.MachineClassRef.Name, false) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("machineClassRef").Child("name"), machineSpec.MachineClassRef.Name, msg)) + } + + if machineSpec.MachinePoolRef.Name != "" { + for _, msg := range apivalidation.NameIsDNSLabel(machineSpec.MachinePoolRef.Name, false) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("machinePoolRef").Child("name"), machineSpec.MachinePoolRef.Name, msg)) + } + } + + if machineSpec.IgnitionRef != nil && machineSpec.IgnitionRef.Name != "" { + for _, msg := range apivalidation.NameIsDNSLabel(machineSpec.IgnitionRef.Name, false) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("ignitionRef").Child("name"), machineSpec.IgnitionRef.Name, msg)) + } + } + + for i, vol := range machineSpec.Volumes { + for _, msg := range apivalidation.NameIsDNSLabel(vol.Name, false) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("volume").Index(i).Child("name"), vol.Name, msg)) + } + if vol.VolumeClaimRef != nil && vol.VolumeClaimRef.Name != "" { + for _, msg := range apivalidation.NameIsDNSLabel(vol.VolumeClaimRef.Name, false) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("volume").Index(i).Child("volumeClaimRef").Child("name"), vol.VolumeClaimRef.Name, msg)) + } + } + } + + if machineSpec.Image == "" { + allErrs = append(allErrs, field.Required(fldPath.Child("image"), "must specify an image")) + } + + allErrs = append(allErrs, metav1validation.ValidateLabels(machineSpec.MachinePoolSelector, fldPath.Child("machinePoolSelector"))...) + + return allErrs +} + +// validateMachineSpecUpdate validates the spec of a Machine object before an update. +func validateMachineSpecUpdate(new, old *compute.MachineSpec, deletionTimestampSet bool, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + + allErrs = append(allErrs, validation.ValidateImmutableField(new.Image, old.Image, fldPath.Child("image"))...) + allErrs = append(allErrs, apivalidation.ValidateImmutableField(new.MachineClassRef, old.MachineClassRef, fldPath.Child("machineClassRef"))...) + allErrs = append(allErrs, ValidateSetOnceField(new.MachinePoolRef, old.MachinePoolRef, fldPath.Child("machinePoolRef"))...) + + return allErrs +} diff --git a/apis/compute/validation/machine_test.go b/apis/compute/validation/machine_test.go new file mode 100644 index 000000000..8f2e1cc85 --- /dev/null +++ b/apis/compute/validation/machine_test.go @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2022 by the OnMetal authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package validation + +import ( + commonv1alpha1 "github.com/onmetal/onmetal-api/apis/common/v1alpha1" + "github.com/onmetal/onmetal-api/apis/compute" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("Machine", func() { + DescribeTable("ValidateMachine", + func(machine *compute.Machine, match types.GomegaMatcher) { + errList := ValidateMachine(machine) + Expect(errList).To(match) + }, + Entry("missing name", + &compute.Machine{}, + ContainElement(RequiredField("metadata.name")), + ), + Entry("missing namespace", + &compute.Machine{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, + ContainElement(RequiredField("metadata.namespace")), + ), + Entry("bad name", + &compute.Machine{ObjectMeta: metav1.ObjectMeta{Name: "foo*"}}, + ContainElement(InvalidField("metadata.name")), + ), + Entry("no machine class ref", + &compute.Machine{}, + ContainElement(RequiredField("spec.machineClassRef")), + ), + Entry("invalid machine class ref name", + &compute.Machine{ + Spec: compute.MachineSpec{ + MachineClassRef: corev1.LocalObjectReference{Name: "foo*"}, + }, + }, + ContainElement(InvalidField("spec.machineClassRef.name")), + ), + Entry("invalid volume name", + &compute.Machine{ + Spec: compute.MachineSpec{ + Volumes: []compute.Volume{{Name: "foo*"}}, + }, + }, + ContainElement(InvalidField("spec.volume[0].name")), + ), + Entry("invalid volumeClaimRef name", + &compute.Machine{ + Spec: compute.MachineSpec{ + Volumes: []compute.Volume{ + {Name: "foo", VolumeSource: compute.VolumeSource{ + VolumeClaimRef: &corev1.LocalObjectReference{Name: "foo*"}}, + }, + }, + }, + }, + ContainElement(InvalidField("spec.volume[0].volumeClaimRef.name")), + ), + Entry("invalid ignition ref name", + &compute.Machine{ + Spec: compute.MachineSpec{ + IgnitionRef: &commonv1alpha1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "foo*", + }, + }, + }, + }, + ContainElement(InvalidField("spec.ignitionRef.name")), + ), + ) + + DescribeTable("ValidateMachineUpdate", + func(newMachine, oldMachine *compute.Machine, match types.GomegaMatcher) { + errList := ValidateMachineUpdate(newMachine, oldMachine) + Expect(errList).To(match) + }, + Entry("immutable machineClassRef", + &compute.Machine{ + Spec: compute.MachineSpec{ + MachineClassRef: corev1.LocalObjectReference{Name: "foo"}, + }, + }, + &compute.Machine{ + Spec: compute.MachineSpec{ + MachineClassRef: corev1.LocalObjectReference{Name: "bar"}, + }, + }, + ContainElement(ImmutableField("spec.machineClassRef")), + ), + Entry("immutable machinePoolRef if set", + &compute.Machine{ + Spec: compute.MachineSpec{ + MachinePoolRef: corev1.LocalObjectReference{Name: "foo"}, + }, + }, + &compute.Machine{ + Spec: compute.MachineSpec{ + MachinePoolRef: corev1.LocalObjectReference{Name: "bar"}, + }, + }, + ContainElement(ImmutableField("spec.machinePoolRef")), + ), + Entry("mutable machinePoolRef if not set", + &compute.Machine{ + Spec: compute.MachineSpec{ + MachinePoolRef: corev1.LocalObjectReference{Name: "foo"}, + }, + }, + &compute.Machine{}, + Not(ContainElement(ImmutableField("spec.machinePoolRef"))), + ), + ) +}) diff --git a/apis/compute/validation/machineclass.go b/apis/compute/validation/machineclass.go new file mode 100644 index 000000000..615b25cfa --- /dev/null +++ b/apis/compute/validation/machineclass.go @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 by the OnMetal authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package validation + +import ( + "github.com/onmetal/onmetal-api/apis/compute" + apivalidation "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// ValidateMachineClass validates a MachineClass object. +func ValidateMachineClass(machineClass *compute.MachineClass) field.ErrorList { + var allErrs field.ErrorList + + allErrs = append(allErrs, apivalidation.ValidateObjectMetaAccessor(machineClass, false, apivalidation.NameIsDNSLabel, field.NewPath("metadata"))...) + + return allErrs +} + +// ValidateMachineClassUpdate validates a MachineClass object before an update. +func ValidateMachineClassUpdate(newMachineClass, oldMachineClass *compute.MachineClass) field.ErrorList { + var allErrs field.ErrorList + + allErrs = append(allErrs, apivalidation.ValidateObjectMetaAccessorUpdate(newMachineClass, oldMachineClass, field.NewPath("metadata"))...) + allErrs = append(allErrs, ValidateImmutable(newMachineClass.Capabilities, oldMachineClass.Capabilities, field.NewPath("capabilities"))...) + allErrs = append(allErrs, ValidateMachineClass(newMachineClass)...) + + return allErrs +} diff --git a/apis/compute/validation/machineclass_test.go b/apis/compute/validation/machineclass_test.go new file mode 100644 index 000000000..8a8a777ab --- /dev/null +++ b/apis/compute/validation/machineclass_test.go @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022 by the OnMetal authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package validation + +import ( + "github.com/onmetal/onmetal-api/apis/compute" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("MachineClass", func() { + DescribeTable("ValidateMachineClass", + func(machineClass *compute.MachineClass, match types.GomegaMatcher) { + errList := ValidateMachineClass(machineClass) + Expect(errList).To(match) + }, + Entry("missing name", + &compute.MachineClass{}, + ContainElement(RequiredField("metadata.name")), + ), + Entry("bad name", + &compute.MachineClass{ObjectMeta: metav1.ObjectMeta{Name: "foo*"}}, + ContainElement(InvalidField("metadata.name")), + ), + ) + + DescribeTable("ValidateMachineClassUpdate", + func(newMachineClass, oldMachineClass *compute.MachineClass, match types.GomegaMatcher) { + errList := ValidateMachineClassUpdate(newMachineClass, oldMachineClass) + Expect(errList).To(match) + }, + Entry("immutable capabilities", + &compute.MachineClass{ + Capabilities: map[corev1.ResourceName]resource.Quantity{ + "ram": resource.MustParse("1Gi"), + }, + }, + &compute.MachineClass{ + Capabilities: map[corev1.ResourceName]resource.Quantity{}, + }, + ContainElement(ImmutableField("capabilities")), + ), + ) +}) diff --git a/apis/compute/validation/machinepool.go b/apis/compute/validation/machinepool.go new file mode 100644 index 000000000..4737b9955 --- /dev/null +++ b/apis/compute/validation/machinepool.go @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2022 by the OnMetal authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package validation + +import ( + "github.com/onmetal/onmetal-api/apis/compute" + apivalidation "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func ValidateMachinePool(machinePool *compute.MachinePool) field.ErrorList { + var allErrs field.ErrorList + + allErrs = append(allErrs, apivalidation.ValidateObjectMetaAccessor(machinePool, false, apivalidation.NameIsDNSLabel, field.NewPath("metadata"))...) + allErrs = append(allErrs, validateMachinePoolSpec(&machinePool.Spec, field.NewPath("spec"))...) + + return allErrs +} + +func validateMachinePoolSpec(machinePoolSpec *compute.MachinePoolSpec, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + + return allErrs +} + +func ValidateMachinePoolUpdate(newMachinePool, oldMachinePool *compute.MachinePool) field.ErrorList { + var allErrs field.ErrorList + + allErrs = append(allErrs, apivalidation.ValidateObjectMetaAccessorUpdate(newMachinePool, oldMachinePool, field.NewPath("metadata"))...) + allErrs = append(allErrs, validateMachinePoolSpecUpdate(&newMachinePool.Spec, &oldMachinePool.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateMachinePool(newMachinePool)...) + + return allErrs +} + +func validateMachinePoolSpecUpdate(newSpec, oldSpec *compute.MachinePoolSpec, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + + if oldSpec.ProviderID != "" { + allErrs = append(allErrs, ValidateImmutable(newSpec.ProviderID, oldSpec.ProviderID, fldPath.Child("providerID"))...) + } + + return allErrs +} diff --git a/apis/compute/validation/machinepool_test.go b/apis/compute/validation/machinepool_test.go new file mode 100644 index 000000000..d841734cc --- /dev/null +++ b/apis/compute/validation/machinepool_test.go @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022 by the OnMetal authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package validation + +import ( + "github.com/onmetal/onmetal-api/apis/compute" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("MachinePool", func() { + DescribeTable("ValidateMachinePool", + func(machinePool *compute.MachinePool, match types.GomegaMatcher) { + errList := ValidateMachinePool(machinePool) + Expect(errList).To(match) + }, + Entry("missing name", + &compute.MachinePool{}, + ContainElement(RequiredField("metadata.name")), + ), + Entry("bad name", + &compute.MachinePool{ObjectMeta: metav1.ObjectMeta{Name: "foo*"}}, + ContainElement(InvalidField("metadata.name")), + ), + ) + + DescribeTable("ValidateMachinePoolUpdate", + func(newMachinePool, oldMachinePool *compute.MachinePool, match types.GomegaMatcher) { + errList := ValidateMachinePoolUpdate(newMachinePool, oldMachinePool) + Expect(errList).To(match) + }, + Entry("immutable providerID if set", + &compute.MachinePool{ + Spec: compute.MachinePoolSpec{ + ProviderID: "foo", + }, + }, + &compute.MachinePool{ + Spec: compute.MachinePoolSpec{ + ProviderID: "bar", + }, + }, + ContainElement(ImmutableField("spec.providerID")), + ), + Entry("mutable providerID if not set", + &compute.MachinePool{ + Spec: compute.MachinePoolSpec{ + ProviderID: "foo", + }, + }, + &compute.MachinePool{}, + Not(ContainElement(ImmutableField("spec.machinePoolRef"))), + ), + ) +}) diff --git a/apis/compute/validation/validation_suite_test.go b/apis/compute/validation/validation_suite_test.go new file mode 100644 index 000000000..5b01d8bc4 --- /dev/null +++ b/apis/compute/validation/validation_suite_test.go @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2022 by the OnMetal authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package validation + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" + "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/apimachinery/pkg/util/validation/field" + + "testing" +) + +func TestValidation(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Compute Validation Suite") +} + +func InvalidField(fld string) types.GomegaMatcher { + return FieldError(field.ErrorTypeInvalid, fld) +} + +func RequiredField(fld string) types.GomegaMatcher { + return FieldError(field.ErrorTypeRequired, fld) +} + +func FieldError(errorType field.ErrorType, fld string) types.GomegaMatcher { + return MaskedFieldError(func(error *field.Error) { + error.Detail = "" + error.BadValue = nil + }, &field.Error{ + Type: errorType, + Field: fld, + }) +} + +func MaskedFieldError(maskFn func(error *field.Error), fieldErr *field.Error) types.GomegaMatcher { + return WithTransform(func(fieldErr *field.Error) *field.Error { + res := *fieldErr + maskFn(&res) + return &res + }, Equal(fieldErr)) +} + +func ImmutableField(fld string) types.GomegaMatcher { + return And( + MaskedFieldError(func(error *field.Error) { + error.BadValue = nil + error.Detail = "" + }, &field.Error{ + Type: field.ErrorTypeInvalid, + Field: fld, + }), + WithTransform(func(error *field.Error) string { + return error.Detail + }, HavePrefix(validation.FieldImmutableErrorMsg)), + ) +} diff --git a/controllers/compute/machine_scheduler_test.go b/controllers/compute/machine_scheduler_test.go index 4ed404d99..dc5cc4a10 100644 --- a/controllers/compute/machine_scheduler_test.go +++ b/controllers/compute/machine_scheduler_test.go @@ -50,6 +50,7 @@ var _ = Describe("MachineScheduler", func() { GenerateName: "test-machine-", }, Spec: computev1alpha1.MachineSpec{ + Image: "my-image", MachineClassRef: corev1.LocalObjectReference{ Name: "my-machineclass", }, @@ -74,6 +75,7 @@ var _ = Describe("MachineScheduler", func() { GenerateName: "test-machine-", }, Spec: computev1alpha1.MachineSpec{ + Image: "my-image", MachineClassRef: corev1.LocalObjectReference{ Name: "my-machineclass", }, @@ -149,6 +151,7 @@ var _ = Describe("MachineScheduler", func() { GenerateName: "test-machine-", }, Spec: computev1alpha1.MachineSpec{ + Image: "my-image", MachinePoolSelector: map[string]string{ "foo": "bar", }, @@ -203,6 +206,7 @@ var _ = Describe("MachineScheduler", func() { GenerateName: "test-machine-", }, Spec: computev1alpha1.MachineSpec{ + Image: "my-image", MachineClassRef: corev1.LocalObjectReference{ Name: "my-machineclass", }, diff --git a/controllers/compute/machineclass_controller_test.go b/controllers/compute/machineclass_controller_test.go index d3de41b8a..c59fc0084 100644 --- a/controllers/compute/machineclass_controller_test.go +++ b/controllers/compute/machineclass_controller_test.go @@ -45,6 +45,7 @@ var _ = Describe("machineclass controller", func() { GenerateName: "machine-", }, Spec: computev1alpha1.MachineSpec{ + Image: "my-image", MachineClassRef: corev1.LocalObjectReference{ Name: machineClass.Name, }, diff --git a/go.mod b/go.mod index a06c9b091..9663dfb19 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/onmetal/controller-utils v0.5.2 github.com/onmetal/vgopath v0.0.1 github.com/onsi/ginkgo v1.16.5 + github.com/onsi/ginkgo/v2 v2.1.3 github.com/onsi/gomega v1.19.0 github.com/spf13/cobra v1.2.1 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e diff --git a/registry/compute/machine/strategy.go b/registry/compute/machine/strategy.go index 6d3ddd2a6..c5f8dae57 100644 --- a/registry/compute/machine/strategy.go +++ b/registry/compute/machine/strategy.go @@ -20,6 +20,7 @@ import ( "github.com/onmetal/onmetal-api/api" "github.com/onmetal/onmetal-api/apis/compute" + "github.com/onmetal/onmetal-api/apis/compute/validation" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -68,7 +69,8 @@ func (machineStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Ob } func (machineStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { - return field.ErrorList{} + machine := obj.(*compute.Machine) + return validation.ValidateMachine(machine) } func (machineStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { @@ -115,7 +117,9 @@ func (machineStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runt } func (machineStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { - return nil + newMachine := obj.(*compute.Machine) + oldMachine := old.(*compute.Machine) + return validation.ValidateMachineUpdate(newMachine, oldMachine) } func (machineStatusStrategy) WarningsOnUpdate(cxt context.Context, obj, old runtime.Object) []string { diff --git a/registry/compute/machineclass/strategy.go b/registry/compute/machineclass/strategy.go index 09d0131bc..f366eb108 100644 --- a/registry/compute/machineclass/strategy.go +++ b/registry/compute/machineclass/strategy.go @@ -20,6 +20,7 @@ import ( "github.com/onmetal/onmetal-api/api" "github.com/onmetal/onmetal-api/apis/compute" + "github.com/onmetal/onmetal-api/apis/compute/validation" "github.com/onmetal/onmetal-api/equality" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" @@ -76,7 +77,8 @@ func (machineClassStrategy) PrepareForUpdate(ctx context.Context, obj, old runti } func (machineClassStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { - return field.ErrorList{} + machineClass := obj.(*compute.MachineClass) + return validation.ValidateMachineClass(machineClass) } func (machineClassStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { @@ -95,7 +97,9 @@ func (machineClassStrategy) Canonicalize(obj runtime.Object) { } func (machineClassStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { - return field.ErrorList{} + newMachineClass := obj.(*compute.MachineClass) + oldMachineClass := old.(*compute.MachineClass) + return validation.ValidateMachineClassUpdate(newMachineClass, oldMachineClass) } func (machineClassStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { diff --git a/registry/compute/machinepool/strategy.go b/registry/compute/machinepool/strategy.go index dc5ab3532..5402be36b 100644 --- a/registry/compute/machinepool/strategy.go +++ b/registry/compute/machinepool/strategy.go @@ -20,6 +20,7 @@ import ( "github.com/onmetal/onmetal-api/api" "github.com/onmetal/onmetal-api/apis/compute" + "github.com/onmetal/onmetal-api/apis/compute/validation" "github.com/onmetal/onmetal-api/equality" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" @@ -87,7 +88,8 @@ func (machinePoolStrategy) PrepareForUpdate(ctx context.Context, obj, old runtim } func (machinePoolStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { - return field.ErrorList{} + machinePool := obj.(*compute.MachinePool) + return validation.ValidateMachinePool(machinePool) } func (machinePoolStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { @@ -134,7 +136,9 @@ func (machinePoolStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old } func (machinePoolStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { - return nil + newMachinePool := obj.(*compute.MachinePool) + oldMachinePool := old.(*compute.MachinePool) + return validation.ValidateMachinePoolUpdate(newMachinePool, oldMachinePool) } func (machinePoolStatusStrategy) WarningsOnUpdate(cxt context.Context, obj, old runtime.Object) []string {