Skip to content

Commit 96e26c5

Browse files
fix(webhook): Update cluster webhook to validate failure domains (#1233)
Use XOR instead of OR for cluster and subnet presence check against failure domains on control plane and worker overrides. **How has this been tested?** Tested in conjunction with #1236 in #1226 * Create a cluster with control plane and machine deployments with failure domains with cluster and subnet variables also set in controlPlane machineDetails and machineDeployment variables overrides. ```yaml apiVersion: cluster.x-k8s.io/v1beta1 kind: Cluster metadata: labels: cluster.x-k8s.io/cluster-name: nkp-sid-caren cluster.x-k8s.io/provider: nutanix name: nkp-sid-caren spec: ... topology: class: nutanix-quick-start controlPlane: metadata: {} replicas: 3 variables: - name: clusterConfig value: ... controlPlane: nutanix: failureDomains: - fd-1 - fd-2 - fd-3 machineDetails: bootType: uefi cluster: type: name name: ncn-dev-sandbox subnets: - type: name name: vlan173 imageLookup: baseOS: rocky-9.6 format: nkp-{{.BaseOS}}-release-{{.K8sVersion}}-* memorySize: 4Gi systemDiskSize: 40Gi vcpuSockets: 2 vcpusPerSocket: 1 ... - name: workerConfig value: nutanix: machineDetails: bootType: uefi cluster: name: ncn-dev-sandbox type: name imageLookup: baseOS: rocky-9.6 format: nkp-{{.BaseOS}}-release-{{.K8sVersion}}-* memorySize: 4Gi subnets: - name: vlan173 type: name systemDiskSize: 40Gi vcpuSockets: 2 vcpusPerSocket: 1 version: 1.33.1 workers: machineDeployments: - class: default-worker failureDomain: fd-1 metadata: annotations: cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size: "1" cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "1" name: md-0 variables: overrides: - name: workerConfig value: nutanix: machineDetails: bootType: uefi cluster: type: name name: ncn-dev-sandbox subnets: - type: name name: vlan173 imageLookup: baseOS: rocky-9.6 format: nkp-{{.BaseOS}}-release-{{.K8sVersion}}-* memorySize: 8Gi systemDiskSize: 80Gi vcpuSockets: 8 vcpusPerSocket: 1 ``` and expect failures from the webhook ```sh $ k apply -f nkp-sid-caren-neg.yaml ... Error from server (Forbidden): error when creating "nkp-sid-caren-neg.yaml": admission webhook "cluster-validator.caren.nutanix.com" denied the request: [spec.topology.variables.clusterConfig.value.controlPlane.nutanix.machineDetails.cluster: Forbidden: "cluster" must not be set when failureDomains are configured., spec.topology.variables.clusterConfig.value.controlPlane.nutanix.machineDetails.subnets: Forbidden: "subnets" must not be set when failureDomains are configured., spec.topology.workers.machineDeployments.variables.overrides.workerConfig.value.nutanix.machineDetails.cluster: Forbidden: "cluster" must not be set when failureDomain is configured., spec.topology.workers.machineDeployments.variables.overrides.workerConfig.value.nutanix.machineDetails.subnets: Forbidden: "subnets" must not be set when failureDomain is configured.] ``` * Create a cluster without failure domains with a machine deployment with cluster and subnet not set ```yaml apiVersion: cluster.x-k8s.io/v1beta1 kind: Cluster metadata: labels: cluster.x-k8s.io/cluster-name: nkp-sid-caren-no-fd cluster.x-k8s.io/provider: nutanix name: nkp-sid-caren-no-fd spec: ... topology: class: nutanix-quick-start controlPlane: metadata: {} replicas: 3 variables: - name: clusterConfig value: ... controlPlane: nutanix: machineDetails: bootType: uefi imageLookup: baseOS: rocky-9.6 format: nkp-{{.BaseOS}}-release-{{.K8sVersion}}-* memorySize: 4Gi systemDiskSize: 40Gi vcpuSockets: 2 vcpusPerSocket: 1 ... - name: workerConfig value: nutanix: machineDetails: bootType: uefi cluster: name: ncn-dev-sandbox type: name imageLookup: baseOS: rocky-9.6 format: nkp-{{.BaseOS}}-release-{{.K8sVersion}}-* memorySize: 4Gi subnets: - name: vlan173 type: name systemDiskSize: 40Gi vcpuSockets: 2 vcpusPerSocket: 1 version: 1.33.1 workers: machineDeployments: - class: default-worker metadata: annotations: cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size: "1" cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "1" name: md-0 variables: overrides: - name: workerConfig value: nutanix: machineDetails: bootType: uefi imageLookup: baseOS: rocky-9.6 format: nkp-{{.BaseOS}}-release-{{.K8sVersion}}-* memorySize: 8Gi cluster: type: name name: ncn-dev-sandbox subnets: - type: name name: vlan173 systemDiskSize: 80Gi vcpuSockets: 8 vcpusPerSocket: 1 - class: default-worker metadata: annotations: cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size: "1" cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "1" name: md-1 variables: overrides: - name: workerConfig value: nutanix: machineDetails: bootType: uefi imageLookup: baseOS: rocky-9.6 format: nkp-{{.BaseOS}}-release-{{.K8sVersion}}-* memorySize: 8Gi systemDiskSize: 80Gi vcpuSockets: 8 vcpusPerSocket: 1 ``` and expect failures from webhook ```sh $ k apply -f no-fd-neg.yaml ... Error from server (Forbidden): error when creating "no-fd-neg.yaml": admission webhook "cluster-validator.caren.nutanix.com" denied the request: [spec.topology.variables.clusterConfig.value.controlPlane.nutanix.machineDetails.cluster: Required value: "cluster" must be set when failureDomains are not configured., spec.topology.variables.clusterConfig.value.controlPlane.nutanix.machineDetails.subnets: Required value: "subnets" must be set when failureDomains are not configured., spec.topology.workers.machineDeployments.variables.overrides.workerConfig.value.nutanix.machineDetails.cluster: Required value: "cluster" must be set when failureDomain is not configured., spec.topology.workers.machineDeployments.variables.overrides.workerConfig.value.nutanix.machineDetails.subnets: Required value: "subnets" must be set when failureDomain is not configured.] ```
1 parent 17abac1 commit 96e26c5

File tree

13 files changed

+183
-105
lines changed

13 files changed

+183
-105
lines changed

api/external/github.com/nutanix-cloud-native/cluster-api-provider-nutanix/api/v1beta1/nutanixmachine_types.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,12 @@ type NutanixMachineSpec struct {
132132
// The cluster identifier (uuid or name) can be obtained from the Prism Central console
133133
// or using the prism_central API.
134134
// +kubebuilder:validation:Optional
135-
Cluster NutanixResourceIdentifier `json:"cluster"`
135+
Cluster NutanixResourceIdentifier `json:"cluster,omitzero"`
136136
// subnet is to identify the cluster's network subnet to use for the Machine's VM
137137
// The cluster identifier (uuid or name) can be obtained from the Prism Central console
138138
// or using the prism_central API.
139139
// +kubebuilder:validation:Optional
140-
Subnets []NutanixResourceIdentifier `json:"subnet"`
140+
Subnets []NutanixResourceIdentifier `json:"subnet,omitempty"`
141141
// List of categories that need to be added to the machines. Categories must already exist in Prism Central
142142
// +kubebuilder:validation:Optional
143143
AdditionalCategories []NutanixCategoryIdentifier `json:"additionalCategories,omitempty"`

api/v1alpha1/nutanix_node_types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,12 @@ type NutanixMachineDetails struct {
8484
// +kubebuilder:validation:Optional
8585
GPUs []capxv1.NutanixGPU `json:"gpus,omitempty"`
8686
}
87+
88+
// HasClusterAndSubnets checks if cluster and subnets are configured in the machine details.
89+
// It returns (hasCluster, hasSubnets).
90+
func (md *NutanixMachineDetails) HasClusterAndSubnets() (hasCluster, hasSubnets bool) {
91+
hasCluster = md.Cluster != nil &&
92+
(md.Cluster.IsName() || md.Cluster.IsUUID())
93+
hasSubnets = len(md.Subnets) > 0
94+
return hasCluster, hasSubnets
95+
}

charts/cluster-api-runtime-extensions-nutanix/defaultclusterclasses/nutanix-cluster-class.yaml

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -304,16 +304,10 @@ spec:
304304
template:
305305
spec:
306306
bootType: legacy
307-
cluster:
308-
name: ""
309-
type: name
310307
image:
311308
name: ""
312309
type: name
313310
memorySize: 4Gi
314-
subnet:
315-
- name: ""
316-
type: name
317311
systemDiskSize: 40Gi
318312
vcpuSockets: 2
319313
vcpusPerSocket: 1
@@ -328,16 +322,10 @@ spec:
328322
template:
329323
spec:
330324
bootType: legacy
331-
cluster:
332-
name: ""
333-
type: name
334325
image:
335326
name: ""
336327
type: name
337328
memorySize: 4Gi
338-
subnet:
339-
- name: ""
340-
type: name
341329
systemDiskSize: 40Gi
342330
vcpuSockets: 2
343331
vcpusPerSocket: 1

common/pkg/capi/clustertopology/handlers/mutation/meta.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
1515
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
1616
"sigs.k8s.io/cluster-api/exp/runtime/topologymutation"
17+
ctrl "sigs.k8s.io/controller-runtime"
1718
"sigs.k8s.io/controller-runtime/pkg/client"
1819

1920
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/handlers"
@@ -99,12 +100,27 @@ func (mgp metaGeneratePatches) GeneratePatches(
99100
vars map[string]apiextensionsv1.JSON,
100101
holderRef runtimehooksv1.HolderReference,
101102
) error {
102-
for _, h := range mgp.mutators {
103+
log := ctrl.LoggerFrom(ctx).WithValues(
104+
"holderRef", holderRef,
105+
"objectKind", obj.GetObjectKind().GroupVersionKind().String(),
106+
"handlerName", mgp.name,
107+
)
108+
109+
log.V(3).Info("Starting mutation pipeline", "handlerCount", len(mgp.mutators))
110+
111+
for i, h := range mgp.mutators {
112+
handlerName := fmt.Sprintf("%T", h)
113+
log.V(5).Info("Running mutator", "index", i, "handler", handlerName)
114+
103115
if err := h.Mutate(ctx, obj.(*unstructured.Unstructured), vars, holderRef, clusterKey, clusterGetter); err != nil {
116+
log.Error(err, "Mutator failed", "index", i, "handler", handlerName)
104117
return err
105118
}
119+
120+
log.V(5).Info("Mutator completed successfully", "index", i, "handler", handlerName)
106121
}
107122

123+
log.V(3).Info("Mutation pipeline completed successfully", "handlerCount", len(mgp.mutators))
108124
return nil
109125
},
110126
)

hack/examples/overlays/clusterclasses/nutanix/kustomization.yaml.tmpl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,7 @@ patches:
4949
kind: KubeadmControlPlaneTemplate
5050
path: ../../../patches/cis-admissionconfiguration.yaml
5151
# END CIS patches
52+
53+
- target:
54+
kind: NutanixMachineTemplate
55+
path: ../../../patches/nutanix/remove-cluster-and-subnet-from-machinetemplates.yaml
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- op: remove
2+
path: /spec/template/spec/cluster
3+
- op: remove
4+
path: /spec/template/spec/subnet

hack/third-party/capx/go.mod

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
module github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/external/capx
55

6-
go 1.23.0
6+
go 1.24.0
77

8-
toolchain go1.24.2
8+
toolchain go1.24.3
99

10-
require github.com/nutanix-cloud-native/cluster-api-provider-nutanix v1.7.0-beta.2
10+
require github.com/nutanix-cloud-native/cluster-api-provider-nutanix v1.7.0-beta.4
1111

1212
require (
1313
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
@@ -28,7 +28,6 @@ require (
2828
github.com/nutanix-cloud-native/prism-go-client v0.5.0 // indirect
2929
github.com/pkg/errors v0.9.1 // indirect
3030
github.com/x448/float16 v0.8.4 // indirect
31-
go.uber.org/automaxprocs v1.6.0 // indirect
3231
golang.org/x/net v0.38.0 // indirect
3332
golang.org/x/text v0.23.0 // indirect
3433
google.golang.org/protobuf v1.36.5 // indirect

hack/third-party/capx/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/
7676
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
7777
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
7878
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
79-
github.com/nutanix-cloud-native/cluster-api-provider-nutanix v1.7.0-beta.2 h1:RsqSaYm5Vg6887taqJ8mRtqa1zckiwtqZVf3/xJRcro=
80-
github.com/nutanix-cloud-native/cluster-api-provider-nutanix v1.7.0-beta.2/go.mod h1:8F3sXZInz+dShppYLmMzQwUAzFsZTyaxaB3bf5rIk/A=
79+
github.com/nutanix-cloud-native/cluster-api-provider-nutanix v1.7.0-beta.4 h1:SNfZlG/AJ/RUpEij0Lu1XbrIoOxk+tZvzQktFdPkGYc=
80+
github.com/nutanix-cloud-native/cluster-api-provider-nutanix v1.7.0-beta.4/go.mod h1:6AJwae8W/nGmITlnuTnvMxCxxztctEAUJulxC9z/jgU=
8181
github.com/nutanix-cloud-native/prism-go-client v0.5.0 h1:aSNuKDOK7+q676MQyetYXcySY41IjSvN2UmrDIU3+6s=
8282
github.com/nutanix-cloud-native/prism-go-client v0.5.0/go.mod h1:QhLX+sEep0cStzHVYU6mPgIlnA8U3DySskagrbDprRk=
8383
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func (h *nutanixControlPlaneFailureDomains) Mutate(
7171
)
7272
if err != nil {
7373
if variables.IsNotFoundError(err) {
74-
log.V(5).Info("ControlPlane nutanix failureDomains variable not defined", "error", err.Error())
74+
log.V(5).Info("ControlPlane nutanix failureDomains variable not defined in cluster config")
7575
return nil
7676
}
7777
log.V(5).Error(err, "failed to get controlPlane nutanix failureDomains variable")

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,6 @@ func (h *nutanixMachineDetailsPatchHandler) Mutate(
9999
spec.BootType = nutanixMachineDetailsVar.BootType
100100
if nutanixMachineDetailsVar.Cluster != nil {
101101
spec.Cluster = *nutanixMachineDetailsVar.Cluster
102-
} else {
103-
// Have to set the required Type field.
104-
spec.Cluster = capxv1.NutanixResourceIdentifier{
105-
Type: capxv1.NutanixIdentifierName,
106-
}
107102
}
108103

109104
switch {
@@ -122,7 +117,10 @@ func (h *nutanixMachineDetailsPatchHandler) Mutate(
122117
spec.MemorySize = nutanixMachineDetailsVar.MemorySize
123118
spec.SystemDiskSize = nutanixMachineDetailsVar.SystemDiskSize
124119

125-
spec.Subnets = slices.Clone(nutanixMachineDetailsVar.Subnets)
120+
if len(nutanixMachineDetailsVar.Subnets) > 0 {
121+
spec.Subnets = slices.Clone(nutanixMachineDetailsVar.Subnets)
122+
}
123+
126124
spec.AdditionalCategories = slices.Clone(nutanixMachineDetailsVar.AdditionalCategories)
127125
spec.GPUs = slices.Clone(nutanixMachineDetailsVar.GPUs)
128126
spec.Project = nutanixMachineDetailsVar.Project.DeepCopy()

0 commit comments

Comments
 (0)