Skip to content

Commit 8194df5

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 8194df5

File tree

5 files changed

+241
-18
lines changed

5 files changed

+241
-18
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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func (n *Checker) Init(
2424
prismCentralEndpointSpec,
2525
controlPlaneNutanixNodeSpec,
2626
nutanixNodeSpecByMachineDeploymentName,
27+
addonsSpec,
2728
errCauses := specsFromCluster(cluster)
2829
if len(errCauses) > 0 {
2930
return initErrorCheck(errCauses...)
@@ -49,8 +50,13 @@ func (n *Checker) Init(
4950
controlPlaneNutanixNodeSpec,
5051
"cluster.spec.topology[.name=clusterConfig].value.controlPlane.nutanix",
5152
),
53+
newStorageContainerCheck(nv4client,
54+
controlPlaneNutanixNodeSpec,
55+
"cluster.spec.topology[.name=clusterConfig].value.controlPlane.nutanix",
56+
addonsSpec),
5257
)
5358
}
59+
5460
for _, md := range cluster.Spec.Topology.Workers.MachineDeployments {
5561
if nutanixNodeSpecByMachineDeploymentName[md.Name] == nil {
5662
continue
@@ -64,8 +70,15 @@ func (n *Checker) Init(
6470
md.Name,
6571
),
6672
),
73+
newStorageContainerCheck(
74+
nv4client,
75+
controlPlaneNutanixNodeSpec,
76+
"cluster.spec.topology.workers.machineDeployments[.name=%s].variables[.name=workerConfig].value.nutanix",
77+
addonsSpec,
78+
),
6779
)
6880
}
81+
6982
return checks
7083
}
7184

pkg/webhook/preflight/nutanix/specs.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,19 @@ func specsFromCluster(
1919
*carenv1.NutanixPrismCentralEndpointSpec,
2020
*carenv1.NutanixNodeSpec,
2121
map[string]*carenv1.NutanixNodeSpec,
22+
*variables.Addons,
2223
[]preflight.Cause,
2324
) {
24-
var prismCentralEndpointSpec *carenv1.NutanixPrismCentralEndpointSpec
25-
var controlPlaneNutanixNodeSpec *carenv1.NutanixNodeSpec
25+
var (
26+
prismCentralEndpointSpec *carenv1.NutanixPrismCentralEndpointSpec
27+
controlPlaneNutanixNodeSpec *carenv1.NutanixNodeSpec
28+
addonsSpec *variables.Addons
29+
)
2630
nutanixNodeSpecByMachineDeploymentName := make(map[string]*carenv1.NutanixNodeSpec)
2731

2832
clusterConfig, err := variables.UnmarshalClusterConfigVariable(cluster.Spec.Topology.Variables)
2933
if err != nil {
30-
return nil, nil, nil, []preflight.Cause{
34+
return nil, nil, nil, nil, []preflight.Cause{
3135
{
3236
Message: err.Error(),
3337
Field: "cluster.spec.topology.variables[.name=clusterConfig]",
@@ -43,6 +47,10 @@ func specsFromCluster(
4347
controlPlaneNutanixNodeSpec = clusterConfig.ControlPlane.Nutanix
4448
}
4549

50+
if clusterConfig.Addons != nil {
51+
addonsSpec = clusterConfig.Addons
52+
}
53+
4654
causes := []preflight.Cause{}
4755
if cluster.Spec.Topology.Workers != nil {
4856
for _, md := range cluster.Spec.Topology.Workers.MachineDeployments {
@@ -62,5 +70,11 @@ func specsFromCluster(
6270
}
6371
}
6472
}
65-
return prismCentralEndpointSpec, controlPlaneNutanixNodeSpec, nutanixNodeSpecByMachineDeploymentName, causes
73+
74+
// TODO: we should consider returning a struct instead
75+
return prismCentralEndpointSpec,
76+
controlPlaneNutanixNodeSpec,
77+
nutanixNodeSpecByMachineDeploymentName,
78+
addonsSpec,
79+
causes
6680
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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/api/variables"
17+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight"
18+
)
19+
20+
func newStorageContainerCheck(
21+
client *prismv4.Client,
22+
nutanixNodeSpec *carenv1.NutanixNodeSpec,
23+
nutanixNodeSpecField string,
24+
addonsSpec *variables.Addons,
25+
) preflight.Check {
26+
if nutanixNodeSpec == nil {
27+
return func(ctx context.Context) preflight.CheckResult {
28+
return preflight.CheckResult{
29+
Name: "NutanixStorageContainer",
30+
Allowed: false,
31+
Error: true,
32+
Causes: []preflight.Cause{
33+
{
34+
Message: "NutanixNodeSpec is missing",
35+
Field: nutanixNodeSpecField,
36+
},
37+
},
38+
}
39+
}
40+
}
41+
42+
return func(ctx context.Context) preflight.CheckResult {
43+
return storageContainerCheck(
44+
client,
45+
nutanixNodeSpec,
46+
nutanixNodeSpecField,
47+
ptr.To(addonsSpec.CSI.Providers["nutanix"]),
48+
)
49+
}
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 storageContainerCheck(
56+
nutanixClient *prismv4.Client,
57+
nodeSpec *carenv1.NutanixNodeSpec,
58+
field string,
59+
csiSpec *carenv1.CSIProvider,
60+
) preflight.CheckResult {
61+
const (
62+
csiParameterKeyStorageContainer = "storageContainer"
63+
)
64+
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(nutanixClient, 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+
func getStorageContainer(
118+
client *prismv4.Client,
119+
nodeSpec *carenv1.NutanixNodeSpec,
120+
storageContainerName string,
121+
) (*clustermgmtv4.StorageContainer, error) {
122+
cluster, err := getCluster(client, &nodeSpec.MachineDetails.Cluster)
123+
if err != nil {
124+
return nil, fmt.Errorf("failed to get cluster: %w", err)
125+
}
126+
127+
fltr := fmt.Sprintf("name eq %q and clusterExtId eq %q", storageContainerName, *cluster.ExtId)
128+
resp, err := client.StorageContainerAPI.ListStorageContainers(nil, nil, &fltr, nil, nil)
129+
if err != nil {
130+
return nil, fmt.Errorf("failed to list storage containers: %w", err)
131+
}
132+
133+
containers, ok := resp.GetData().([]clustermgmtv4.StorageContainer)
134+
if !ok {
135+
return nil, fmt.Errorf("failed to get data returned by ListStorageContainers(filter=%q)", fltr)
136+
}
137+
138+
if len(containers) == 0 {
139+
return nil, fmt.Errorf(
140+
"no storage container named %q found on cluster named %q",
141+
storageContainerName,
142+
*cluster.Name,
143+
)
144+
}
145+
146+
if len(containers) > 1 {
147+
return nil, fmt.Errorf(
148+
"multiple storage containers found with name %q on cluster %q",
149+
storageContainerName,
150+
*cluster.Name,
151+
)
152+
}
153+
154+
return ptr.To(containers[0]), nil
155+
}
156+
157+
func getCluster(
158+
client *prismv4.Client,
159+
clusterIdentifier *v1beta1.NutanixResourceIdentifier,
160+
) (*clustermgmtv4.Cluster, error) {
161+
switch clusterIdentifier.Type {
162+
case v1beta1.NutanixIdentifierUUID:
163+
resp, err := client.ClustersApiInstance.GetClusterById(clusterIdentifier.UUID)
164+
if err != nil {
165+
return nil, err
166+
}
167+
168+
cluster, ok := resp.GetData().(clustermgmtv4.Cluster)
169+
if !ok {
170+
return nil, fmt.Errorf("failed to get data returned by GetClusterById")
171+
}
172+
173+
return &cluster, nil
174+
case v1beta1.NutanixIdentifierName:
175+
filter := fmt.Sprintf("name eq '%s'", *clusterIdentifier.Name)
176+
resp, err := client.ClustersApiInstance.ListClusters(nil, nil, &filter, nil, nil, nil)
177+
if err != nil {
178+
return nil, err
179+
}
180+
181+
if resp == nil || resp.GetData() == nil {
182+
return nil, fmt.Errorf("no clusters were returned")
183+
}
184+
185+
clusters, ok := resp.GetData().([]clustermgmtv4.Cluster)
186+
if !ok {
187+
return nil, fmt.Errorf("failed to get data returned by ListClusters")
188+
}
189+
190+
if len(clusters) == 0 {
191+
return nil, fmt.Errorf("no clusters found with name %q", *clusterIdentifier.Name)
192+
}
193+
194+
if len(clusters) > 1 {
195+
return nil, fmt.Errorf("multiple clusters found with name %q", *clusterIdentifier.Name)
196+
}
197+
198+
return &clusters[0], nil
199+
default:
200+
return nil, fmt.Errorf("cluster identifier is missing both name and uuid")
201+
}
202+
}

0 commit comments

Comments
 (0)