Skip to content

Commit 442c703

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 442c703

File tree

4 files changed

+219
-14
lines changed

4 files changed

+219
-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=

pkg/webhook/preflight/nutanix/checker.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ func (n *nutanixChecker) Init(
5353
}
5454

5555
checks = append(checks, n.initVMImageChecks()...)
56+
checks = append(checks, n.initStorageContainerChecks()...)
5657

5758
// Add more checks here as needed.
5859

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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(
71+
"no storage container found for cluster %q",
72+
*nodeSpec.MachineDetails.Cluster.Name,
73+
),
74+
Field: field,
75+
})
76+
77+
return *result
78+
}
79+
80+
if csiSpec.StorageClassConfigs == nil {
81+
result.Allowed = false
82+
result.Causes = append(result.Causes, preflight.Cause{
83+
Message: fmt.Sprintf(
84+
"no storage class configs found for cluster %q",
85+
*nodeSpec.MachineDetails.Cluster.Name,
86+
),
87+
Field: field,
88+
})
89+
90+
return *result
91+
}
92+
93+
for _, storageClassConfig := range csiSpec.StorageClassConfigs {
94+
if storageClassConfig.Parameters == nil {
95+
continue
96+
}
97+
98+
storageContainer, ok := storageClassConfig.Parameters[csiParameterKeyStorageContainer]
99+
if !ok {
100+
continue
101+
}
102+
103+
// TODO: check if cluster name is set, if not use uuid.
104+
// If neither is set, use the cluster name from the NodeSpec failure domain.
105+
if _, err := getStorageContainer(n.v4client, nodeSpec, storageContainer); err != nil {
106+
result.Allowed = false
107+
result.Error = true
108+
result.Causes = append(result.Causes, preflight.Cause{
109+
Message: fmt.Sprintf(
110+
"failed to check if storage container named %q exists: %s",
111+
storageContainer,
112+
err,
113+
),
114+
Field: field,
115+
})
116+
117+
return *result
118+
}
119+
}
120+
121+
return *result
122+
}
123+
}
124+
125+
func getStorageContainer(
126+
client *prismv4.Client,
127+
nodeSpec *carenv1.NutanixNodeSpec,
128+
storageContainerName string,
129+
) (*clustermgmtv4.StorageContainer, error) {
130+
cluster, err := getCluster(client, &nodeSpec.MachineDetails.Cluster)
131+
if err != nil {
132+
return nil, fmt.Errorf("failed to get cluster: %w", err)
133+
}
134+
135+
fltr := fmt.Sprintf("name eq %q and clusterExtId eq %q", storageContainerName, *cluster.ExtId)
136+
resp, err := client.StorageContainerAPI.ListStorageContainers(nil, nil, &fltr, nil, nil)
137+
if err != nil {
138+
return nil, fmt.Errorf("failed to list storage containers: %w", err)
139+
}
140+
141+
containers, ok := resp.GetData().([]clustermgmtv4.StorageContainer)
142+
if !ok {
143+
return nil, fmt.Errorf("failed to get data returned by ListStorageContainers(filter=%q)", fltr)
144+
}
145+
146+
if len(containers) == 0 {
147+
return nil, fmt.Errorf(
148+
"no storage container named %q found on cluster named %q",
149+
storageContainerName,
150+
*cluster.Name,
151+
)
152+
}
153+
154+
if len(containers) > 1 {
155+
return nil, fmt.Errorf(
156+
"multiple storage containers found with name %q on cluster %q",
157+
storageContainerName,
158+
*cluster.Name,
159+
)
160+
}
161+
162+
return ptr.To(containers[0]), nil
163+
}
164+
165+
func getCluster(
166+
client *prismv4.Client,
167+
clusterIdentifier *v1beta1.NutanixResourceIdentifier,
168+
) (*clustermgmtv4.Cluster, error) {
169+
switch clusterIdentifier.Type {
170+
case v1beta1.NutanixIdentifierUUID:
171+
resp, err := client.ClustersApiInstance.GetClusterById(clusterIdentifier.UUID)
172+
if err != nil {
173+
return nil, err
174+
}
175+
176+
cluster, ok := resp.GetData().(clustermgmtv4.Cluster)
177+
if !ok {
178+
return nil, fmt.Errorf("failed to get data returned by GetClusterById")
179+
}
180+
181+
return &cluster, nil
182+
case v1beta1.NutanixIdentifierName:
183+
filter := fmt.Sprintf("name eq '%s'", *clusterIdentifier.Name)
184+
resp, err := client.ClustersApiInstance.ListClusters(nil, nil, &filter, nil, nil, nil)
185+
if err != nil {
186+
return nil, err
187+
}
188+
189+
if resp == nil || resp.GetData() == nil {
190+
return nil, fmt.Errorf("no clusters were returned")
191+
}
192+
193+
clusters, ok := resp.GetData().([]clustermgmtv4.Cluster)
194+
if !ok {
195+
return nil, fmt.Errorf("failed to get data returned by ListClusters")
196+
}
197+
198+
if len(clusters) == 0 {
199+
return nil, fmt.Errorf("no clusters found with name %q", *clusterIdentifier.Name)
200+
}
201+
202+
if len(clusters) > 1 {
203+
return nil, fmt.Errorf("multiple clusters found with name %q", *clusterIdentifier.Name)
204+
}
205+
206+
return &clusters[0], nil
207+
default:
208+
return nil, fmt.Errorf("cluster identifier is missing both name and uuid")
209+
}
210+
}

0 commit comments

Comments
 (0)