Skip to content

Commit 10da176

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 10da176

File tree

4 files changed

+220
-14
lines changed

4 files changed

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

0 commit comments

Comments
 (0)