Skip to content

Commit d10ab4c

Browse files
authored
fix: machineDetails fields "cluster" and "subnets" should be optional (#1217)
https://jira.nutanix.com/browse/NCN-108428: When failureDomain is configured, machineDetails "cluster" and "subnets" should be optional. Added webhook validation to Cluster to make sure either cluster/subnets fields are configured, or failureDomain is configured, for both controlPlane and worker nodes. Added unit tests. Did local integration test. Below is the sample errors in my local test. ``` $ kubectl create -f cl-fd2.yml --dry-run=server Error from server (Forbidden): error when creating "cl-fd2.yml": admission webhook "cluster-validator.caren.nutanix.com" denied the request: [spec.topology.variables.clusterConfig.value.controlPlane.nutanix.machineDetails.cluster: Required value: "cluster" must set when failureDomains is not configured., spec.topology.variables.clusterConfig.value.controlPlane.nutanix.machineDetails.subnets: Required value: "subnets" must set when failureDomains is not configured., spec.topology.workers.machineDeployments.variables.workerConfig.nutanix.machineDetails.cluster: Required value: "cluster" must set when failureDomain is not configured., spec.topology.workers.machineDeployments.variables.workerConfig.nutanix.machineDetails.subnets: Required value: "subnets" must set when failureDomain is not configured.] ```
1 parent d69c324 commit d10ab4c

File tree

14 files changed

+466
-43
lines changed

14 files changed

+466
-43
lines changed

api/v1alpha1/crds/caren.nutanix.com_nutanixclusterconfigs.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -511,9 +511,7 @@ spec:
511511
format: int32
512512
type: integer
513513
required:
514-
- cluster
515514
- memorySize
516-
- subnets
517515
- systemDiskSize
518516
- vcpuSockets
519517
- vcpusPerSocket

api/v1alpha1/crds/caren.nutanix.com_nutanixworkernodeconfigs.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,7 @@ spec:
234234
format: int32
235235
type: integer
236236
required:
237-
- cluster
238237
- memorySize
239-
- subnets
240238
- systemDiskSize
241239
- vcpuSockets
242240
- vcpusPerSocket

api/v1alpha1/nutanix_node_types.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,15 @@ type NutanixMachineDetails struct {
5050

5151
// cluster identifies the Prism Element in which the machine will be created.
5252
// The identifier (uuid or name) can be obtained from the console or API.
53-
// +kubebuilder:validation:Required
54-
Cluster capxv1.NutanixResourceIdentifier `json:"cluster"`
53+
// +kubebuilder:validation:Optional
54+
// +optional
55+
Cluster *capxv1.NutanixResourceIdentifier `json:"cluster,omitempty"`
5556

5657
// subnet identifies the network subnet to use for the machine.
5758
// The identifier (uuid or name) can be obtained from the console or API.
58-
// +kubebuilder:validation:Required
59-
Subnets []capxv1.NutanixResourceIdentifier `json:"subnets"`
59+
// +kubebuilder:validation:Optional
60+
// +optional
61+
Subnets []capxv1.NutanixResourceIdentifier `json:"subnets,omitempty"`
6062

6163
// List of categories that need to be added to the machines. Categories must already
6264
// exist in Prism Central. One category key can have more than one value.

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/handlers/generic/mutation/kubeproxymode/variables_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,18 @@ func minimalNutanixClusterConfigSpec() v1alpha1.NutanixClusterConfigSpec {
162162
Type: capxv1.NutanixIdentifierName,
163163
Name: ptr.To("fake-image"),
164164
},
165-
Cluster: capxv1.NutanixResourceIdentifier{
165+
Cluster: &capxv1.NutanixResourceIdentifier{
166166
Type: capxv1.NutanixIdentifierName,
167167
Name: ptr.To("fake-pe-cluster"),
168168
},
169169
MemorySize: resource.MustParse("8Gi"),
170170
SystemDiskSize: resource.MustParse("40Gi"),
171-
Subnets: []capxv1.NutanixResourceIdentifier{},
171+
Subnets: []capxv1.NutanixResourceIdentifier{
172+
{
173+
Type: capxv1.NutanixIdentifierName,
174+
Name: ptr.To("fake-subnet"),
175+
},
176+
},
172177
},
173178
},
174179
},

pkg/handlers/nutanix/mutation/controlplanefailuredomains/variables_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,18 @@ func minimumClusterConfigSpec() v1alpha1.NutanixClusterConfigSpec {
5050
Type: capxv1.NutanixIdentifierName,
5151
Name: ptr.To("fake-image"),
5252
},
53-
Cluster: capxv1.NutanixResourceIdentifier{
53+
Cluster: &capxv1.NutanixResourceIdentifier{
5454
Type: capxv1.NutanixIdentifierName,
5555
Name: ptr.To("fake-pe-cluster"),
5656
},
5757
MemorySize: resource.MustParse("8Gi"),
5858
SystemDiskSize: resource.MustParse("40Gi"),
59-
Subnets: []capxv1.NutanixResourceIdentifier{},
59+
Subnets: []capxv1.NutanixResourceIdentifier{
60+
{
61+
Type: capxv1.NutanixIdentifierName,
62+
Name: ptr.To("fake-subnet"),
63+
},
64+
},
6065
},
6166
},
6267
},

pkg/handlers/nutanix/mutation/machinedetails/inject.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,14 @@ func (h *nutanixMachineDetailsPatchHandler) Mutate(
9797
spec := obj.Spec.Template.Spec
9898

9999
spec.BootType = nutanixMachineDetailsVar.BootType
100-
spec.Cluster = nutanixMachineDetailsVar.Cluster
100+
if nutanixMachineDetailsVar.Cluster != nil {
101+
spec.Cluster = *nutanixMachineDetailsVar.Cluster
102+
} else {
103+
// Have to set the required Type field.
104+
spec.Cluster = capxv1.NutanixResourceIdentifier{
105+
Type: capxv1.NutanixIdentifierName,
106+
}
107+
}
101108

102109
switch {
103110
case nutanixMachineDetailsVar.Image != nil:
@@ -118,7 +125,6 @@ func (h *nutanixMachineDetailsPatchHandler) Mutate(
118125
spec.Subnets = slices.Clone(nutanixMachineDetailsVar.Subnets)
119126
spec.AdditionalCategories = slices.Clone(nutanixMachineDetailsVar.AdditionalCategories)
120127
spec.GPUs = slices.Clone(nutanixMachineDetailsVar.GPUs)
121-
122128
spec.Project = nutanixMachineDetailsVar.Project.DeepCopy()
123129

124130
obj.Spec.Template.Spec = spec

pkg/handlers/nutanix/mutation/machinedetails/inject_control_plane_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ var (
2727
Type: capxv1.NutanixIdentifierName,
2828
Name: ptr.To("fake-image"),
2929
},
30-
Cluster: capxv1.NutanixResourceIdentifier{
30+
Cluster: &capxv1.NutanixResourceIdentifier{
3131
Type: capxv1.NutanixIdentifierName,
3232
Name: ptr.To("fake-pe-cluster"),
3333
},
@@ -72,7 +72,7 @@ var (
7272
ImageLookup: &capxv1.NutanixImageLookup{
7373
BaseOS: "rockylinux-9",
7474
},
75-
Cluster: capxv1.NutanixResourceIdentifier{
75+
Cluster: &capxv1.NutanixResourceIdentifier{
7676
Type: capxv1.NutanixIdentifierName,
7777
Name: ptr.To("fake-pe-cluster"),
7878
},

pkg/handlers/nutanix/mutation/machinedetails/variables_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,18 @@ func minimumClusterConfigSpec() v1alpha1.NutanixClusterConfigSpec {
109109
Type: capxv1.NutanixIdentifierName,
110110
Name: ptr.To("fake-image"),
111111
},
112-
Cluster: capxv1.NutanixResourceIdentifier{
112+
Cluster: &capxv1.NutanixResourceIdentifier{
113113
Type: capxv1.NutanixIdentifierName,
114114
Name: ptr.To("fake-pe-cluster"),
115115
},
116116
MemorySize: resource.MustParse("8Gi"),
117117
SystemDiskSize: resource.MustParse("40Gi"),
118-
Subnets: []capxv1.NutanixResourceIdentifier{},
118+
Subnets: []capxv1.NutanixResourceIdentifier{
119+
{
120+
Type: capxv1.NutanixIdentifierName,
121+
Name: ptr.To("fake-subnet"),
122+
},
123+
},
119124
},
120125
},
121126
},

pkg/webhook/cluster/nutanix_validator.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net/netip"
1212

1313
v1 "k8s.io/api/admission/v1"
14+
"k8s.io/apimachinery/pkg/util/validation/field"
1415
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
1516
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
1617
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
@@ -89,9 +90,146 @@ func (a *nutanixValidator) validate(
8990
}
9091
}
9192

93+
if err := validateTopologyFailureDomainConfig(clusterConfig, cluster); err != nil {
94+
return admission.Denied(err.Error())
95+
}
96+
9297
return admission.Allowed("")
9398
}
9499

100+
// validateTopologyFailureDomainConfig validates the failure domain related configuration in cluster topology.
101+
func validateTopologyFailureDomainConfig(
102+
clusterConfig *variables.ClusterConfigSpec,
103+
cluster *clusterv1.Cluster,
104+
) error {
105+
fldErrs := field.ErrorList{}
106+
107+
// Validate control plane failure domain configuration
108+
if controlPlaneErrs := validateControlPlaneFailureDomainConfig(clusterConfig); controlPlaneErrs != nil {
109+
fldErrs = append(fldErrs, controlPlaneErrs...)
110+
}
111+
112+
// Validate worker failure domain configuration
113+
if workerErrs := validateWorkerFailureDomainConfig(cluster); workerErrs != nil {
114+
fldErrs = append(fldErrs, workerErrs...)
115+
}
116+
117+
return fldErrs.ToAggregate()
118+
}
119+
120+
// validateControlPlaneFailureDomainConfig validates that either failureDomains are set,
121+
// or cluster and subnets are set with machineDetails, for control plane.
122+
func validateControlPlaneFailureDomainConfig(clusterConfig *variables.ClusterConfigSpec) field.ErrorList {
123+
fldErrs := field.ErrorList{}
124+
fldPath := field.NewPath(
125+
"spec",
126+
"topology",
127+
"variables",
128+
"clusterConfig",
129+
"value",
130+
"controlPlane",
131+
"nutanix",
132+
"machineDetails",
133+
)
134+
135+
if clusterConfig.ControlPlane != nil &&
136+
clusterConfig.ControlPlane.Nutanix != nil &&
137+
len(clusterConfig.ControlPlane.Nutanix.FailureDomains) == 0 {
138+
machineDetails := clusterConfig.ControlPlane.Nutanix.MachineDetails
139+
if machineDetails.Cluster == nil || (!machineDetails.Cluster.IsName() && !machineDetails.Cluster.IsUUID()) {
140+
fldErrs = append(fldErrs, field.Required(
141+
fldPath.Child("cluster"),
142+
"\"cluster\" must be set when failureDomains are not configured.",
143+
))
144+
}
145+
146+
if len(machineDetails.Subnets) == 0 {
147+
fldErrs = append(fldErrs, field.Required(
148+
fldPath.Child("subnets"),
149+
"\"subnets\" must be set when failureDomains are not configured.",
150+
))
151+
}
152+
}
153+
154+
return fldErrs
155+
}
156+
157+
// validateWorkerFailureDomainConfig validates either failureDomains is set,
158+
// or cluster and subnets are set with machineDetails, for workers.
159+
func validateWorkerFailureDomainConfig(cluster *clusterv1.Cluster) field.ErrorList {
160+
fldErrs := field.ErrorList{}
161+
workerConfigVarPath := field.NewPath("spec", "topology", "variables", "workerConfig")
162+
workerConfigMDVarOverridePath := field.NewPath(
163+
"spec",
164+
"topology",
165+
"workers",
166+
"machineDeployments",
167+
"variables",
168+
"overrides",
169+
"workerConfig",
170+
)
171+
172+
// Get the machineDetails from cluster variable "workerConfig" if it is configured
173+
defaultWorkerConfig, err := variables.UnmarshalWorkerConfigVariable(cluster.Spec.Topology.Variables)
174+
if err != nil {
175+
fldErrs = append(fldErrs, field.InternalError(workerConfigVarPath,
176+
fmt.Errorf("failed to unmarshal cluster topology variable %q: %w", v1alpha1.WorkerConfigVariableName, err)))
177+
}
178+
179+
if cluster.Spec.Topology.Workers != nil {
180+
for i := range cluster.Spec.Topology.Workers.MachineDeployments {
181+
md := cluster.Spec.Topology.Workers.MachineDeployments[i]
182+
if md.FailureDomain != nil && *md.FailureDomain != "" {
183+
// failureDomain is configured so we can skip checking the cluster and subnet in machineDetails
184+
continue
185+
}
186+
187+
// Get the machineDetails from the overrides variable "workerConfig" if it is configured,
188+
// otherwise use the defaultWorkerConfig if it is configured.
189+
var workerConfig *variables.WorkerNodeConfigSpec
190+
if md.Variables != nil && len(md.Variables.Overrides) > 0 {
191+
workerConfig, err = variables.UnmarshalWorkerConfigVariable(md.Variables.Overrides)
192+
if err != nil {
193+
fldErrs = append(fldErrs, field.InternalError(
194+
workerConfigMDVarOverridePath,
195+
fmt.Errorf(
196+
"failed to unmarshal worker overrides variable %q: %w",
197+
v1alpha1.WorkerConfigVariableName,
198+
err,
199+
),
200+
))
201+
}
202+
}
203+
204+
wcfgPath := workerConfigMDVarOverridePath
205+
if workerConfig == nil {
206+
workerConfig = defaultWorkerConfig
207+
wcfgPath = workerConfigVarPath
208+
}
209+
if workerConfig == nil || workerConfig.Nutanix == nil {
210+
continue
211+
}
212+
213+
machineDetails := workerConfig.Nutanix.MachineDetails
214+
if machineDetails.Cluster == nil ||
215+
(!machineDetails.Cluster.IsName() && !machineDetails.Cluster.IsUUID()) {
216+
fldErrs = append(fldErrs, field.Required(
217+
wcfgPath.Child("value", "nutanix", "machineDetails", "cluster"),
218+
"\"cluster\" must be set when failureDomain is not configured.",
219+
))
220+
}
221+
if len(machineDetails.Subnets) == 0 {
222+
fldErrs = append(fldErrs, field.Required(
223+
wcfgPath.Child("value", "nutanix", "machineDetails", "subnets"),
224+
"\"subnets\" must be set when failureDomain is not configured.",
225+
))
226+
}
227+
}
228+
}
229+
230+
return fldErrs
231+
}
232+
95233
// validatePrismCentralIPNotInLoadBalancerIPRange checks if the Prism Central IP is not
96234
// in the MetalLB Load Balancer IP range and error out if it is.
97235
func validatePrismCentralIPNotInLoadBalancerIPRange(

0 commit comments

Comments
 (0)