Skip to content

Commit ee06a8d

Browse files
feat(preflight): Storage container checks for Nutanix
Add pre-flight checks to ensure the storage container mentioned in CSI Provider storage class config parameters is present in all relevant AOS clusters i.e. control plane and workers.
1 parent 01bb8ce commit ee06a8d

File tree

4 files changed

+218
-14
lines changed

4 files changed

+218
-14
lines changed

go.mod

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ require (
2020
github.com/google/uuid v1.6.0
2121
github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api v0.0.0-00010101000000-000000000000
2222
github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common v0.7.0
23-
github.com/nutanix-cloud-native/prism-go-client v0.5.1
23+
github.com/nutanix-cloud-native/prism-go-client v0.5.2-0.20250527140144-d12ef75a05ef
2424
github.com/nutanix/ntnx-api-golang-clients/clustermgmt-go-client/v4 v4.0.1-beta.2
2525
github.com/nutanix/ntnx-api-golang-clients/networking-go-client/v4 v4.0.2-beta.1
2626
github.com/nutanix/ntnx-api-golang-clients/prism-go-client/v4 v4.0.1-beta.1
@@ -93,7 +93,7 @@ require (
9393
github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect
9494
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
9595
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
96-
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
96+
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
9797
github.com/hashicorp/hcl v1.0.0 // indirect
9898
github.com/huandu/xstrings v1.5.0 // indirect
9999
github.com/imdario/mergo v0.3.13 // indirect
@@ -111,7 +111,6 @@ require (
111111
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
112112
github.com/modern-go/reflect2 v1.0.2 // indirect
113113
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
114-
github.com/nutanix/ntnx-api-golang-clients/storage-go-client/v4 v4.0.2-alpha.3 // indirect
115114
github.com/nutanix/ntnx-api-golang-clients/volumes-go-client/v4 v4.0.1-beta.1 // indirect
116115
github.com/opencontainers/go-digest v1.0.0 // indirect
117116
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect

go.sum

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,12 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92Bcuy
131131
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
132132
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
133133
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
134-
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
135134
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
136135
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
137-
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
138-
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
139-
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
140-
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
141-
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
136+
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
137+
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
138+
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
139+
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
142140
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
143141
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
144142
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
@@ -188,16 +186,14 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
188186
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
189187
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
190188
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
191-
github.com/nutanix-cloud-native/prism-go-client v0.5.1 h1:ykiXPCILzEMORHz7XvI8KXNomChsdLIpOAlT/YqBCmo=
192-
github.com/nutanix-cloud-native/prism-go-client v0.5.1/go.mod h1:QhLX+sEep0cStzHVYU6mPgIlnA8U3DySskagrbDprRk=
189+
github.com/nutanix-cloud-native/prism-go-client v0.5.2-0.20250527140144-d12ef75a05ef h1:fwURB6RMZ8Tzk9fVjYutS/TG5Tm2gYCsLWbgQSkyjnQ=
190+
github.com/nutanix-cloud-native/prism-go-client v0.5.2-0.20250527140144-d12ef75a05ef/go.mod h1:N/O9fz5fimjb30RxlPbKbGs/Z2lqMgDqrb6CrsZvQrA=
193191
github.com/nutanix/ntnx-api-golang-clients/clustermgmt-go-client/v4 v4.0.1-beta.2 h1:s1u5/GEw3mTZakepJoTD1OvPVU1YuioRxmKZin+W99s=
194192
github.com/nutanix/ntnx-api-golang-clients/clustermgmt-go-client/v4 v4.0.1-beta.2/go.mod h1:sd4Fnk6MVfEDVY+8WyRoQTmLhi2SgZ3riySWErVHf8E=
195193
github.com/nutanix/ntnx-api-golang-clients/networking-go-client/v4 v4.0.2-beta.1 h1:PvZQwYhhJtxmzLpnzEhHTpp2fV6woc6W65PHGsHzVfs=
196194
github.com/nutanix/ntnx-api-golang-clients/networking-go-client/v4 v4.0.2-beta.1/go.mod h1:+eZgV1+xL/r84qmuFSVt5R8OFRO70rEz92jOnVgJNco=
197195
github.com/nutanix/ntnx-api-golang-clients/prism-go-client/v4 v4.0.1-beta.1 h1:hvy3QCc2SgVidYxTq0rRPOazJOt1PP8A86kW7j6sywU=
198196
github.com/nutanix/ntnx-api-golang-clients/prism-go-client/v4 v4.0.1-beta.1/go.mod h1:Yhk+xD4mN90OKEHnk5ARf97CX5p4+MEC/B/YIVoZeZ0=
199-
github.com/nutanix/ntnx-api-golang-clients/storage-go-client/v4 v4.0.2-alpha.3 h1:K3I9YtqKcKKxSL4+tcxnFeLOoaptiVlpsOJ9Xzq3shM=
200-
github.com/nutanix/ntnx-api-golang-clients/storage-go-client/v4 v4.0.2-alpha.3/go.mod h1:kz3gO87xtWnPOCP2kN7yw5LvCDVRnvg8BOWL7CarqXA=
201197
github.com/nutanix/ntnx-api-golang-clients/vmm-go-client/v4 v4.0.1-beta.1 h1:XuTRvYu1kiNjdXOYVwyjhKlFWyo9nMit6GsOYV8+5Cg=
202198
github.com/nutanix/ntnx-api-golang-clients/vmm-go-client/v4 v4.0.1-beta.1/go.mod h1:CaWm4GFpAjQQDc6YXl/dUDrHpuW54h8j6Cj7EslE4Qk=
203199
github.com/nutanix/ntnx-api-golang-clients/volumes-go-client/v4 v4.0.1-beta.1 h1:VJSaQDnnYeNEk1mkQqEbt573OdM62+5s/B0e9kszdas=
@@ -261,7 +257,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
261257
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
262258
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
263259
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
264-
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
265260
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
266261
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
267262
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

pkg/webhook/preflight/nutanix/checker.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"fmt"
99

10+
prismv4 "github.com/nutanix-cloud-native/prism-go-client/v4"
1011
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
1112
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
1213

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package nutanix
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/external/github.com/nutanix-cloud-native/cluster-api-provider-nutanix/api/v1beta1"
7+
carenv1 "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
8+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight"
9+
prismv4 "github.com/nutanix-cloud-native/prism-go-client/v4"
10+
clustermgmtv4 "github.com/nutanix/ntnx-api-golang-clients/clustermgmt-go-client/v4/models/clustermgmt/v4/config"
11+
"k8s.io/utils/ptr"
12+
)
13+
14+
// StorageContainers checks if the storage container specified in the CSIProvider's StorageClassConfigs exists
15+
// in the Nutanix cluster specified in the NutanixNodeSpec.
16+
func (n *Checker) StorageContainers(ctx context.Context) preflight.CheckResult {
17+
result := preflight.CheckResult{
18+
Name: "StorageContainers",
19+
Allowed: true,
20+
}
21+
22+
// Check control plane VM image.
23+
clusterConfig, err := n.variablesGetter.ClusterConfig()
24+
if err != nil {
25+
result.Error = true
26+
result.Allowed = false
27+
result.Causes = append(result.Causes, preflight.Cause{
28+
Message: fmt.Sprintf("failed to read clusterConfig variable: %s", err),
29+
Field: "cluster.spec.topology.variables",
30+
})
31+
}
32+
33+
// If the clusterConfig is nil or does not have Addons or CSI, we do not have to check for storage containers.
34+
if clusterConfig == nil || clusterConfig.Addons == nil || clusterConfig.Addons.CSI == nil {
35+
return result
36+
}
37+
38+
csiProvider := ptr.To(clusterConfig.Addons.CSI.Providers["nutanix"])
39+
// If the CSIProvider is nil, we cannot check for storage containers.
40+
if csiProvider == nil {
41+
return result
42+
}
43+
44+
if clusterConfig.ControlPlane != nil && clusterConfig.ControlPlane.Nutanix != nil {
45+
n.storageContainerCheck(
46+
ctx,
47+
clusterConfig.ControlPlane.Nutanix,
48+
csiProvider,
49+
"cluster.spec.topology.variables[.name=clusterConfig].controlPlane.nutanix",
50+
&result,
51+
)
52+
}
53+
54+
// Check worker VM images.
55+
if n.cluster.Spec.Topology.Workers != nil {
56+
for _, md := range n.cluster.Spec.Topology.Workers.MachineDeployments {
57+
workerConfig, err := n.variablesGetter.WorkerConfigForMachineDeployment(md)
58+
if err != nil {
59+
result.Error = true
60+
result.Causes = append(result.Causes, preflight.Cause{
61+
Message: fmt.Sprintf("failed to read workerConfig variable: %s", err),
62+
Field: fmt.Sprintf(
63+
"cluster.spec.topology.workers.machineDeployments[.name=%s].variables.overrides",
64+
md.Name,
65+
),
66+
})
67+
}
68+
if workerConfig != nil && workerConfig.Nutanix != nil {
69+
n.storageContainerCheck(
70+
ctx,
71+
workerConfig.Nutanix,
72+
csiProvider,
73+
fmt.Sprintf(
74+
"workers.machineDeployments[.name=%s].variables.overrides[.name=workerConfig].value.nutanix",
75+
md.Name,
76+
),
77+
&result,
78+
)
79+
}
80+
}
81+
}
82+
83+
return result
84+
}
85+
86+
// storageContainerCheck checks if the storage container specified in the CSIProvider's StorageClassConfigs exists.
87+
// It admits the NodeSpec instead of the MachineDetails because the failure domains will be specified in the NodeSpec
88+
// and the MachineDetails.Cluster will be nil in that case.
89+
func (n *Checker) storageContainerCheck(ctx context.Context, nodeSpec *carenv1.NutanixNodeSpec, csiSpec *carenv1.CSIProvider, field string, result *preflight.CheckResult) {
90+
const (
91+
csiParameterKeyStorageContainer = "storageContainer"
92+
)
93+
94+
if csiSpec == nil {
95+
result.Allowed = false
96+
result.Error = true
97+
result.Causes = append(result.Causes, preflight.Cause{
98+
Message: fmt.Sprintf("no storage container found for cluster %q", nodeSpec.MachineDetails.Cluster.Name),
99+
Field: field,
100+
})
101+
102+
return
103+
}
104+
105+
if csiSpec.StorageClassConfigs == nil {
106+
result.Allowed = false
107+
result.Causes = append(result.Causes, preflight.Cause{
108+
Message: fmt.Sprintf("no storage class configs found for cluster %q", nodeSpec.MachineDetails.Cluster.Name),
109+
Field: field,
110+
})
111+
112+
return
113+
}
114+
115+
for _, storageClassConfig := range csiSpec.StorageClassConfigs {
116+
if storageClassConfig.Parameters == nil {
117+
continue
118+
}
119+
120+
storageContainer, ok := storageClassConfig.Parameters[csiParameterKeyStorageContainer]
121+
if !ok {
122+
continue
123+
}
124+
125+
// TODO: check if cluster name is set, if not use uuid. If neither is set, use the cluster name from the NodeSpec failure domain.
126+
if _, err := getStorageContainer(n.nutanixClient, nodeSpec, storageContainer); err != nil {
127+
result.Allowed = false
128+
result.Error = true
129+
result.Causes = append(result.Causes, preflight.Cause{
130+
Message: fmt.Sprintf("failed to check if storage container named %q exists: %s", storageContainer, err),
131+
Field: field,
132+
})
133+
134+
return
135+
}
136+
}
137+
}
138+
139+
func getStorageContainer(client *prismv4.Client, nodeSpec *carenv1.NutanixNodeSpec, storageContainerName string) (*clustermgmtv4.StorageContainer, error) {
140+
cluster, err := getCluster(client, &nodeSpec.MachineDetails.Cluster)
141+
if err != nil {
142+
return nil, fmt.Errorf("failed to get cluster: %w", err)
143+
}
144+
145+
fltr := fmt.Sprintf("name eq '%s' and clusterExtId eq '%s'", storageContainerName, cluster.ExtId)
146+
resp, err := client.StorageContainerAPI.ListStorageContainers(nil, nil, &fltr, nil, nil)
147+
if err != nil {
148+
return nil, fmt.Errorf("failed to list storage containers: %w", err)
149+
}
150+
151+
containers, ok := resp.GetData().([]clustermgmtv4.StorageContainer)
152+
if !ok {
153+
return nil, fmt.Errorf("failed to get data returned by ListStorageContainers(filter=%q)", fltr)
154+
}
155+
156+
if len(containers) == 0 {
157+
return nil, fmt.Errorf("no storage container named %q found on cluster named %q", storageContainerName, cluster.Name)
158+
}
159+
160+
if len(containers) > 1 {
161+
return nil, fmt.Errorf("multiple storage containers found with name %q on cluster %q", storageContainerName, cluster.Name)
162+
}
163+
164+
return ptr.To(containers[0]), nil
165+
}
166+
167+
func getCluster(client *prismv4.Client, clusterIdentifier *v1beta1.NutanixResourceIdentifier) (*clustermgmtv4.Cluster, error) {
168+
switch clusterIdentifier.Type {
169+
case v1beta1.NutanixIdentifierUUID:
170+
resp, err := client.ClustersApiInstance.GetClusterById(clusterIdentifier.UUID)
171+
if err != nil {
172+
return nil, err
173+
}
174+
175+
cluster, ok := resp.GetData().(clustermgmtv4.Cluster)
176+
if !ok {
177+
return nil, fmt.Errorf("failed to get data returned by GetClusterById")
178+
}
179+
180+
return &cluster, nil
181+
case v1beta1.NutanixIdentifierName:
182+
filter := fmt.Sprintf("name eq '%s'", *clusterIdentifier.Name)
183+
resp, err := client.ClustersApiInstance.ListClusters(nil, nil, &filter, nil, nil, nil)
184+
if err != nil {
185+
return nil, err
186+
}
187+
188+
if resp == nil || resp.GetData() == nil {
189+
return nil, fmt.Errorf("no clusters were returned")
190+
}
191+
192+
clusters, ok := resp.GetData().([]clustermgmtv4.Cluster)
193+
if !ok {
194+
return nil, fmt.Errorf("failed to get data returned by ListClusters")
195+
}
196+
197+
if len(clusters) == 0 {
198+
return nil, fmt.Errorf("no clusters found with name %q", *clusterIdentifier.Name)
199+
}
200+
201+
if len(clusters) > 1 {
202+
return nil, fmt.Errorf("multiple clusters found with name %q", *clusterIdentifier.Name)
203+
}
204+
205+
return &clusters[0], nil
206+
default:
207+
return nil, fmt.Errorf("cluster identifier is missing both name and uuid")
208+
}
209+
}

0 commit comments

Comments
 (0)