diff --git a/internal/apis/compute/validation/machine.go b/internal/apis/compute/validation/machine.go index e4db74662..fbf5aeb64 100644 --- a/internal/apis/compute/validation/machine.go +++ b/internal/apis/compute/validation/machine.go @@ -9,6 +9,8 @@ import ( "github.com/ironcore-dev/ironcore/internal/admission/plugin/machinevolumedevices/device" ironcorevalidation "github.com/ironcore-dev/ironcore/internal/api/validation" "github.com/ironcore-dev/ironcore/internal/apis/compute" + "github.com/ironcore-dev/ironcore/internal/apis/networking" + networkvalidation "github.com/ironcore-dev/ironcore/internal/apis/networking/validation" "github.com/ironcore-dev/ironcore/internal/apis/storage" storagevalidation "github.com/ironcore-dev/ironcore/internal/apis/storage/validation" corev1 "k8s.io/api/core/v1" @@ -98,6 +100,75 @@ func validateMachineSpec(machineSpec *compute.MachineSpec, fldPath *field.Path) allErrs = append(allErrs, metav1validation.ValidateLabels(machineSpec.MachinePoolSelector, fldPath.Child("machinePoolSelector"))...) + seenNwiNames := sets.NewString() + for i, nwi := range machineSpec.NetworkInterfaces { + if seenNwiNames.Has(nwi.Name) { + allErrs = append(allErrs, field.Duplicate(fldPath.Child("networkInterface").Index(i).Child("name"), nwi.Name)) + } else { + seenNwiNames.Insert(nwi.Name) + } + allErrs = append(allErrs, validateNetworkInterface(&nwi, fldPath.Child("networkInterface").Index(i))...) + } + + return allErrs +} + +func validateNetworkInterface(networkInterface *compute.NetworkInterface, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + + for _, msg := range apivalidation.NameIsDNSLabel(networkInterface.Name, false) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), networkInterface.Name, msg)) + } + + allErrs = append(allErrs, validateNetworkInterfaceSource(&networkInterface.NetworkInterfaceSource, fldPath)...) + + return allErrs +} + +func validateNetworkInterfaceSource(source *compute.NetworkInterfaceSource, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + var numDefs int + if source.NetworkInterfaceRef != nil { + numDefs++ + for _, msg := range apivalidation.NameIsDNSLabel(source.NetworkInterfaceRef.Name, false) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("networkInterfaceRef").Child("name"), source.NetworkInterfaceRef.Name, msg)) + } + } + if source.Ephemeral != nil { + if numDefs > 0 { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("ephemeral"), "must only specify one networkInterface source")) + } else { + numDefs++ + allErrs = append(allErrs, validateEphemeralNetworkInterface(source.Ephemeral, fldPath.Child("ephemeral"))...) + } + } + if numDefs == 0 { + allErrs = append(allErrs, field.Invalid(fldPath, source, "must specify at least one networkInterface source")) + } + return allErrs +} + +func validateEphemeralNetworkInterface(source *compute.EphemeralNetworkInterfaceSource, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + + if source.NetworkInterfaceTemplate == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("NetworkInterfaceTemplate"), "must specify networkInterface template ")) + } else { + allErrs = append(allErrs, validateNetworkInterfaceTemplateSpecForMachine(source.NetworkInterfaceTemplate, fldPath.Child("networkInterfaceTemplate"))...) + } + + return allErrs +} + +func validateNetworkInterfaceTemplateSpecForMachine(template *networking.NetworkInterfaceTemplateSpec, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + + if template == nil { + allErrs = append(allErrs, field.Required(fldPath, "")) + } else { + allErrs = append(allErrs, networkvalidation.ValidateNetworkInterfaceSpec(&template.Spec, &template.ObjectMeta, fldPath)...) + } + return allErrs } diff --git a/internal/apis/compute/validation/machine_test.go b/internal/apis/compute/validation/machine_test.go index 3a888ab7d..a658f0ecf 100644 --- a/internal/apis/compute/validation/machine_test.go +++ b/internal/apis/compute/validation/machine_test.go @@ -6,6 +6,8 @@ package validation import ( commonv1alpha1 "github.com/ironcore-dev/ironcore/api/common/v1alpha1" "github.com/ironcore-dev/ironcore/internal/apis/compute" + "github.com/ironcore-dev/ironcore/internal/apis/ipam" + "github.com/ironcore-dev/ironcore/internal/apis/networking" . "github.com/ironcore-dev/ironcore/internal/testutils/validation" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -188,6 +190,250 @@ var _ = Describe("Machine", func() { ), ) + DescribeTable("ValidateMachineNetworkInterface", + func(machine *compute.Machine, match types.GomegaMatcher) { + errList := ValidateMachine(machine) + Expect(errList).To(match) + }, + Entry("invalid networkInterface name", + &compute.Machine{ + Spec: compute.MachineSpec{ + NetworkInterfaces: []compute.NetworkInterface{{Name: "bar*"}}, + }, + }, + ContainElement(InvalidField("spec.networkInterface[0].name")), + ), + Entry("duplicate networkInterface name", + &compute.Machine{ + Spec: compute.MachineSpec{ + NetworkInterfaces: []compute.NetworkInterface{ + {Name: "foo"}, + {Name: "foo"}, + }, + }, + }, + ContainElement(DuplicateField("spec.networkInterface[1].name")), + ), + Entry("invalid networkInterfaceRef name", + &compute.Machine{ + Spec: compute.MachineSpec{ + NetworkInterfaces: []compute.NetworkInterface{ + { + Name: "bar", + NetworkInterfaceSource: compute.NetworkInterfaceSource{ + NetworkInterfaceRef: &corev1.LocalObjectReference{Name: "foo*"}, + }, + }, + }, + }, + }, + ContainElement(InvalidField("spec.networkInterface[0].networkInterfaceRef.name")), + ), + Entry("invalid networkInterface prefix length for IPv4 IPSource", + &compute.Machine{ + Spec: compute.MachineSpec{ + NetworkInterfaces: []compute.NetworkInterface{ + { + Name: "foo", + NetworkInterfaceSource: compute.NetworkInterfaceSource{ + Ephemeral: &compute.EphemeralNetworkInterfaceSource{ + NetworkInterfaceTemplate: &networking.NetworkInterfaceTemplateSpec{ + Spec: networking.NetworkInterfaceSpec{ + NetworkRef: corev1.LocalObjectReference{Name: "bar"}, + IPs: []networking.IPSource{{ + Ephemeral: &networking.EphemeralPrefixSource{ + PrefixTemplate: &ipam.PrefixTemplateSpec{ + Spec: ipam.PrefixSpec{ + IPFamily: corev1.IPv4Protocol, + Prefix: commonv1alpha1.MustParseNewIPPrefix("10.0.0.0/24"), + }, + }}, + }}, + }, + }, + }, + }, + }, + }, + }, + }, + ContainElement(ForbiddenField("spec.networkInterface[0].ephemeral.networkInterfaceTemplate.ips[0].ephemeral.prefixTemplate.spec.prefix")), + ), + Entry("invalid ephemral networkInterface prefix length for IPv6 IPSource", + &compute.Machine{ + Spec: compute.MachineSpec{ + NetworkInterfaces: []compute.NetworkInterface{ + { + Name: "foo", + NetworkInterfaceSource: compute.NetworkInterfaceSource{ + Ephemeral: &compute.EphemeralNetworkInterfaceSource{ + NetworkInterfaceTemplate: &networking.NetworkInterfaceTemplateSpec{ + Spec: networking.NetworkInterfaceSpec{ + NetworkRef: corev1.LocalObjectReference{Name: "bar"}, + IPs: []networking.IPSource{{ + Ephemeral: &networking.EphemeralPrefixSource{ + PrefixTemplate: &ipam.PrefixTemplateSpec{ + Spec: ipam.PrefixSpec{ + IPFamily: corev1.IPv6Protocol, + Prefix: commonv1alpha1.MustParseNewIPPrefix("2001:db8::/64"), + }, + }}, + }}, + }, + }, + }, + }, + }, + }, + }, + }, + ContainElement(ForbiddenField("spec.networkInterface[0].ephemeral.networkInterfaceTemplate.ips[0].ephemeral.prefixTemplate.spec.prefix")), + ), + Entry("invalid networkInterface prefix length derived from parent prefix for IPv4 IPSource", + &compute.Machine{ + Spec: compute.MachineSpec{ + NetworkInterfaces: []compute.NetworkInterface{ + { + Name: "foo", + NetworkInterfaceSource: compute.NetworkInterfaceSource{ + Ephemeral: &compute.EphemeralNetworkInterfaceSource{ + NetworkInterfaceTemplate: &networking.NetworkInterfaceTemplateSpec{ + Spec: networking.NetworkInterfaceSpec{ + NetworkRef: corev1.LocalObjectReference{Name: "bar"}, + IPFamilies: []corev1.IPFamily{"IPv4"}, + IPs: []networking.IPSource{{ + Ephemeral: &networking.EphemeralPrefixSource{ + PrefixTemplate: &ipam.PrefixTemplateSpec{ + Spec: ipam.PrefixSpec{ + IPFamily: corev1.IPv4Protocol, + ParentSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"foo": "bar"}, + }, + PrefixLength: 40, + }, + }}, + }}, + }, + }, + }, + }, + }, + }, + }, + }, + ContainElement(ForbiddenField("spec.networkInterface[0].ephemeral.networkInterfaceTemplate.ips[0].ephemeral.prefixTemplate.spec.prefixLength")), + ), + Entry("invalid ephemral networkInterface prefix length for IPv6 IPSource", + &compute.Machine{ + Spec: compute.MachineSpec{ + NetworkInterfaces: []compute.NetworkInterface{ + { + Name: "foo", + NetworkInterfaceSource: compute.NetworkInterfaceSource{ + Ephemeral: &compute.EphemeralNetworkInterfaceSource{ + NetworkInterfaceTemplate: &networking.NetworkInterfaceTemplateSpec{ + Spec: networking.NetworkInterfaceSpec{ + NetworkRef: corev1.LocalObjectReference{Name: "bar"}, + IPFamilies: []corev1.IPFamily{"IPv6"}, + IPs: []networking.IPSource{{ + Ephemeral: &networking.EphemeralPrefixSource{ + PrefixTemplate: &ipam.PrefixTemplateSpec{ + Spec: ipam.PrefixSpec{ + IPFamily: corev1.IPv6Protocol, + ParentSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"foo": "bar"}, + }, + PrefixLength: 132, + }, + }}, + }}, + }, + }, + }, + }, + }, + }, + }, + }, + ContainElement(ForbiddenField("spec.networkInterface[0].ephemeral.networkInterfaceTemplate.ips[0].ephemeral.prefixTemplate.spec.prefixLength")), + ), + ) + + DescribeTable("ValidateMachineNetworkInterface prefix length derived from parent prefix", + func(machine *compute.Machine, match types.GomegaMatcher) { + errList := ValidateMachine(machine) + Expect(errList).To(match) + }, + Entry("valid networkInterface prefix length derived from parent prefix for IPv4 IPSource", + &compute.Machine{ + Spec: compute.MachineSpec{ + NetworkInterfaces: []compute.NetworkInterface{ + { + Name: "foo", + NetworkInterfaceSource: compute.NetworkInterfaceSource{ + Ephemeral: &compute.EphemeralNetworkInterfaceSource{ + NetworkInterfaceTemplate: &networking.NetworkInterfaceTemplateSpec{ + Spec: networking.NetworkInterfaceSpec{ + NetworkRef: corev1.LocalObjectReference{Name: "bar"}, + IPFamilies: []corev1.IPFamily{"IPv4"}, + IPs: []networking.IPSource{{ + Ephemeral: &networking.EphemeralPrefixSource{ + PrefixTemplate: &ipam.PrefixTemplateSpec{ + Spec: ipam.PrefixSpec{ + IPFamily: corev1.IPv4Protocol, + ParentRef: &corev1.LocalObjectReference{ + Name: "root", + }, + PrefixLength: 32, + }, + }}, + }}, + }, + }, + }, + }, + }, + }, + }, + }, + Not(ContainElement(ForbiddenField("spec.networkInterface[0].ephemeral.networkInterfaceTemplate.ips[0].ephemeral.prefixTemplate.spec.prefixLength"))), + ), + Entry("valid ephemral networkInterface prefix length derived from parent prefix for IPv6 IPSource", + &compute.Machine{ + Spec: compute.MachineSpec{ + NetworkInterfaces: []compute.NetworkInterface{ + { + Name: "foo", + NetworkInterfaceSource: compute.NetworkInterfaceSource{ + Ephemeral: &compute.EphemeralNetworkInterfaceSource{ + NetworkInterfaceTemplate: &networking.NetworkInterfaceTemplateSpec{ + Spec: networking.NetworkInterfaceSpec{ + NetworkRef: corev1.LocalObjectReference{Name: "bar"}, + IPFamilies: []corev1.IPFamily{"IPv6"}, + IPs: []networking.IPSource{{ + Ephemeral: &networking.EphemeralPrefixSource{ + PrefixTemplate: &ipam.PrefixTemplateSpec{ + Spec: ipam.PrefixSpec{ + IPFamily: corev1.IPv6Protocol, + ParentRef: &corev1.LocalObjectReference{ + Name: "root", + }, + PrefixLength: 128, + }, + }}, + }}, + }, + }, + }, + }, + }, + }, + }, + }, + Not(ContainElement(ForbiddenField("spec.networkInterface[0].ephemeral.networkInterfaceTemplate.ips[0].ephemeral.prefixTemplate.spec.prefixLength"))), + ), + ) + DescribeTable("ValidateMachineUpdate", func(newMachine, oldMachine *compute.Machine, match types.GomegaMatcher) { errList := ValidateMachineUpdate(newMachine, oldMachine) diff --git a/internal/apis/networking/validation/networkinterface.go b/internal/apis/networking/validation/networkinterface.go index db198b244..c0827072f 100644 --- a/internal/apis/networking/validation/networkinterface.go +++ b/internal/apis/networking/validation/networkinterface.go @@ -21,7 +21,7 @@ func ValidateNetworkInterface(networkInterface *networking.NetworkInterface) fie var allErrs field.ErrorList allErrs = append(allErrs, apivalidation.ValidateObjectMetaAccessor(networkInterface, true, apivalidation.NameIsDNSLabel, field.NewPath("metadata"))...) - allErrs = append(allErrs, validateNetworkInterfaceSpec(&networkInterface.Spec, &networkInterface.ObjectMeta, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateNetworkInterfaceSpec(&networkInterface.Spec, &networkInterface.ObjectMeta, field.NewPath("spec"))...) return allErrs } @@ -37,7 +37,7 @@ func ValidateNetworkInterfaceUpdate(newNetworkInterface, oldNetworkInterface *ne return allErrs } -func validateNetworkInterfaceSpec(spec *networking.NetworkInterfaceSpec, nicMeta *metav1.ObjectMeta, fldPath *field.Path) field.ErrorList { +func ValidateNetworkInterfaceSpec(spec *networking.NetworkInterfaceSpec, nicMeta *metav1.ObjectMeta, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if spec.NetworkRef == (corev1.LocalObjectReference{}) {