Skip to content

Commit 9d87597

Browse files
feat(preflight): Add a check for storage containers
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 f0cb9f8 commit 9d87597

File tree

3 files changed

+211
-14
lines changed

3 files changed

+211
-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.20250602134145-662333927e6d
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
@@ -102,7 +102,7 @@ require (
102102
github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect
103103
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
104104
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
105-
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
105+
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
106106
github.com/hashicorp/hcl v1.0.0 // indirect
107107
github.com/huandu/xstrings v1.5.0 // indirect
108108
github.com/imdario/mergo v0.3.13 // indirect
@@ -120,7 +120,6 @@ require (
120120
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
121121
github.com/modern-go/reflect2 v1.0.2 // indirect
122122
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
123-
github.com/nutanix/ntnx-api-golang-clients/storage-go-client/v4 v4.0.2-alpha.3 // indirect
124123
github.com/nutanix/ntnx-api-golang-clients/volumes-go-client/v4 v4.0.1-beta.1 // indirect
125124
github.com/oklog/ulid v1.3.1 // indirect
126125
github.com/opencontainers/go-digest v1.0.0 // indirect

go.sum

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,12 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92Bcuy
162162
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
163163
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
164164
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
165-
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
166165
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
167166
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
168-
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
169-
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
170-
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
171-
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
172-
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
167+
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
168+
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
169+
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
170+
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
173171
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
174172
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
175173
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
@@ -227,16 +225,14 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
227225
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
228226
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
229227
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
230-
github.com/nutanix-cloud-native/prism-go-client v0.5.1 h1:ykiXPCILzEMORHz7XvI8KXNomChsdLIpOAlT/YqBCmo=
231-
github.com/nutanix-cloud-native/prism-go-client v0.5.1/go.mod h1:QhLX+sEep0cStzHVYU6mPgIlnA8U3DySskagrbDprRk=
228+
github.com/nutanix-cloud-native/prism-go-client v0.5.2-0.20250602134145-662333927e6d h1:ZjrHbyZLeaWMhtBNCe8uIfvAHs0ebqx8WxJRW59hnMg=
229+
github.com/nutanix-cloud-native/prism-go-client v0.5.2-0.20250602134145-662333927e6d/go.mod h1:N/O9fz5fimjb30RxlPbKbGs/Z2lqMgDqrb6CrsZvQrA=
232230
github.com/nutanix/ntnx-api-golang-clients/clustermgmt-go-client/v4 v4.0.1-beta.2 h1:s1u5/GEw3mTZakepJoTD1OvPVU1YuioRxmKZin+W99s=
233231
github.com/nutanix/ntnx-api-golang-clients/clustermgmt-go-client/v4 v4.0.1-beta.2/go.mod h1:sd4Fnk6MVfEDVY+8WyRoQTmLhi2SgZ3riySWErVHf8E=
234232
github.com/nutanix/ntnx-api-golang-clients/networking-go-client/v4 v4.0.2-beta.1 h1:PvZQwYhhJtxmzLpnzEhHTpp2fV6woc6W65PHGsHzVfs=
235233
github.com/nutanix/ntnx-api-golang-clients/networking-go-client/v4 v4.0.2-beta.1/go.mod h1:+eZgV1+xL/r84qmuFSVt5R8OFRO70rEz92jOnVgJNco=
236234
github.com/nutanix/ntnx-api-golang-clients/prism-go-client/v4 v4.0.1-beta.1 h1:hvy3QCc2SgVidYxTq0rRPOazJOt1PP8A86kW7j6sywU=
237235
github.com/nutanix/ntnx-api-golang-clients/prism-go-client/v4 v4.0.1-beta.1/go.mod h1:Yhk+xD4mN90OKEHnk5ARf97CX5p4+MEC/B/YIVoZeZ0=
238-
github.com/nutanix/ntnx-api-golang-clients/storage-go-client/v4 v4.0.2-alpha.3 h1:K3I9YtqKcKKxSL4+tcxnFeLOoaptiVlpsOJ9Xzq3shM=
239-
github.com/nutanix/ntnx-api-golang-clients/storage-go-client/v4 v4.0.2-alpha.3/go.mod h1:kz3gO87xtWnPOCP2kN7yw5LvCDVRnvg8BOWL7CarqXA=
240236
github.com/nutanix/ntnx-api-golang-clients/vmm-go-client/v4 v4.0.1-beta.1 h1:XuTRvYu1kiNjdXOYVwyjhKlFWyo9nMit6GsOYV8+5Cg=
241237
github.com/nutanix/ntnx-api-golang-clients/vmm-go-client/v4 v4.0.1-beta.1/go.mod h1:CaWm4GFpAjQQDc6YXl/dUDrHpuW54h8j6Cj7EslE4Qk=
242238
github.com/nutanix/ntnx-api-golang-clients/volumes-go-client/v4 v4.0.1-beta.1 h1:VJSaQDnnYeNEk1mkQqEbt573OdM62+5s/B0e9kszdas=
@@ -302,7 +298,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
302298
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
303299
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
304300
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
305-
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
306301
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
307302
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
308303
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Copyright 2025 Nutanix. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package nutanix
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
prismv4 "github.com/nutanix-cloud-native/prism-go-client/v4"
11+
clustermgmtv4 "github.com/nutanix/ntnx-api-golang-clients/clustermgmt-go-client/v4/models/clustermgmt/v4/config"
12+
"k8s.io/utils/ptr"
13+
14+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/external/github.com/nutanix-cloud-native/cluster-api-provider-nutanix/api/v1beta1"
15+
carenv1 "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
16+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight"
17+
)
18+
19+
func (n *nutanixChecker) initStorageContainerChecks() []preflight.Check {
20+
checks := []preflight.Check{}
21+
22+
if n.nutanixClusterConfigSpec != nil && n.nutanixClusterConfigSpec.ControlPlane != nil &&
23+
n.nutanixClusterConfigSpec.ControlPlane.Nutanix != nil {
24+
checks = append(checks,
25+
n.storageContainerCheck(
26+
n.nutanixClusterConfigSpec.ControlPlane.Nutanix,
27+
"cluster.spec.topology[.name=clusterConfig].value.controlPlane.nutanix",
28+
&n.nutanixClusterConfigSpec.Addons.CSI.Providers.NutanixCSI,
29+
),
30+
)
31+
}
32+
33+
for mdName, nutanixWorkerNodeConfigSpec := range n.nutanixWorkerNodeConfigSpecByMachineDeploymentName {
34+
if nutanixWorkerNodeConfigSpec.Nutanix != nil {
35+
checks = append(checks,
36+
n.storageContainerCheck(
37+
nutanixWorkerNodeConfigSpec.Nutanix,
38+
fmt.Sprintf(
39+
"cluster.spec.topology.workers.machineDeployments[.name=%s]"+
40+
".variables[.name=workerConfig].value.nutanix",
41+
mdName,
42+
),
43+
&n.nutanixClusterConfigSpec.Addons.CSI.Providers.NutanixCSI,
44+
),
45+
)
46+
}
47+
}
48+
49+
return checks
50+
}
51+
52+
// storageContainerCheck checks if the storage container specified in the CSIProvider's StorageClassConfigs exists.
53+
// It admits the NodeSpec instead of the MachineDetails because the failure domains will be specified in the NodeSpec
54+
// and the MachineDetails.Cluster will be nil in that case.
55+
func (n *nutanixChecker) storageContainerCheck(
56+
nodeSpec *carenv1.NutanixNodeSpec,
57+
field string,
58+
csiSpec *carenv1.CSIProvider,
59+
) preflight.Check {
60+
const (
61+
csiParameterKeyStorageContainer = "storageContainer"
62+
)
63+
64+
return func(ctx context.Context) preflight.CheckResult {
65+
result := &preflight.CheckResult{}
66+
if csiSpec == nil {
67+
result.Allowed = false
68+
result.Error = true
69+
result.Causes = append(result.Causes, preflight.Cause{
70+
Message: fmt.Sprintf("no storage container found for cluster %q", *nodeSpec.MachineDetails.Cluster.Name),
71+
Field: field,
72+
})
73+
74+
return *result
75+
}
76+
77+
if csiSpec.StorageClassConfigs == nil {
78+
result.Allowed = false
79+
result.Causes = append(result.Causes, preflight.Cause{
80+
Message: fmt.Sprintf(
81+
"no storage class configs found for cluster %q",
82+
*nodeSpec.MachineDetails.Cluster.Name,
83+
),
84+
Field: field,
85+
})
86+
87+
return *result
88+
}
89+
90+
for _, storageClassConfig := range csiSpec.StorageClassConfigs {
91+
if storageClassConfig.Parameters == nil {
92+
continue
93+
}
94+
95+
storageContainer, ok := storageClassConfig.Parameters[csiParameterKeyStorageContainer]
96+
if !ok {
97+
continue
98+
}
99+
100+
// TODO: check if cluster name is set, if not use uuid.
101+
// If neither is set, use the cluster name from the NodeSpec failure domain.
102+
if _, err := getStorageContainer(n.v4client, nodeSpec, storageContainer); err != nil {
103+
result.Allowed = false
104+
result.Error = true
105+
result.Causes = append(result.Causes, preflight.Cause{
106+
Message: fmt.Sprintf("failed to check if storage container named %q exists: %s", storageContainer, err),
107+
Field: field,
108+
})
109+
110+
return *result
111+
}
112+
}
113+
114+
return *result
115+
}
116+
}
117+
118+
func getStorageContainer(
119+
client *prismv4.Client,
120+
nodeSpec *carenv1.NutanixNodeSpec,
121+
storageContainerName string,
122+
) (*clustermgmtv4.StorageContainer, error) {
123+
cluster, err := getCluster(client, &nodeSpec.MachineDetails.Cluster)
124+
if err != nil {
125+
return nil, fmt.Errorf("failed to get cluster: %w", err)
126+
}
127+
128+
fltr := fmt.Sprintf("name eq %q and clusterExtId eq %q", storageContainerName, *cluster.ExtId)
129+
resp, err := client.StorageContainerAPI.ListStorageContainers(nil, nil, &fltr, nil, nil)
130+
if err != nil {
131+
return nil, fmt.Errorf("failed to list storage containers: %w", err)
132+
}
133+
134+
containers, ok := resp.GetData().([]clustermgmtv4.StorageContainer)
135+
if !ok {
136+
return nil, fmt.Errorf("failed to get data returned by ListStorageContainers(filter=%q)", fltr)
137+
}
138+
139+
if len(containers) == 0 {
140+
return nil, fmt.Errorf(
141+
"no storage container named %q found on cluster named %q",
142+
storageContainerName,
143+
*cluster.Name,
144+
)
145+
}
146+
147+
if len(containers) > 1 {
148+
return nil, fmt.Errorf(
149+
"multiple storage containers found with name %q on cluster %q",
150+
storageContainerName,
151+
*cluster.Name,
152+
)
153+
}
154+
155+
return ptr.To(containers[0]), nil
156+
}
157+
158+
func getCluster(
159+
client *prismv4.Client,
160+
clusterIdentifier *v1beta1.NutanixResourceIdentifier,
161+
) (*clustermgmtv4.Cluster, error) {
162+
switch clusterIdentifier.Type {
163+
case v1beta1.NutanixIdentifierUUID:
164+
resp, err := client.ClustersApiInstance.GetClusterById(clusterIdentifier.UUID)
165+
if err != nil {
166+
return nil, err
167+
}
168+
169+
cluster, ok := resp.GetData().(clustermgmtv4.Cluster)
170+
if !ok {
171+
return nil, fmt.Errorf("failed to get data returned by GetClusterById")
172+
}
173+
174+
return &cluster, nil
175+
case v1beta1.NutanixIdentifierName:
176+
filter := fmt.Sprintf("name eq '%s'", *clusterIdentifier.Name)
177+
resp, err := client.ClustersApiInstance.ListClusters(nil, nil, &filter, nil, nil, nil)
178+
if err != nil {
179+
return nil, err
180+
}
181+
182+
if resp == nil || resp.GetData() == nil {
183+
return nil, fmt.Errorf("no clusters were returned")
184+
}
185+
186+
clusters, ok := resp.GetData().([]clustermgmtv4.Cluster)
187+
if !ok {
188+
return nil, fmt.Errorf("failed to get data returned by ListClusters")
189+
}
190+
191+
if len(clusters) == 0 {
192+
return nil, fmt.Errorf("no clusters found with name %q", *clusterIdentifier.Name)
193+
}
194+
195+
if len(clusters) > 1 {
196+
return nil, fmt.Errorf("multiple clusters found with name %q", *clusterIdentifier.Name)
197+
}
198+
199+
return &clusters[0], nil
200+
default:
201+
return nil, fmt.Errorf("cluster identifier is missing both name and uuid")
202+
}
203+
}

0 commit comments

Comments
 (0)