Skip to content

Commit 1ffbc7b

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 1ffbc7b

File tree

5 files changed

+242
-18
lines changed

5 files changed

+242
-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: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Copyright 2024 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/api/variables"
18+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight"
19+
)
20+
21+
func newStorageContainerCheck(
22+
client *prismv4.Client,
23+
nutanixNodeSpec *carenv1.NutanixNodeSpec,
24+
nutanixNodeSpecField string,
25+
addonsSpec *variables.Addons,
26+
) preflight.Check {
27+
if nutanixNodeSpec == nil {
28+
return func(ctx context.Context) preflight.CheckResult {
29+
return preflight.CheckResult{
30+
Name: "NutanixStorageContainer",
31+
Allowed: false,
32+
Error: true,
33+
Causes: []preflight.Cause{
34+
{
35+
Message: "NutanixNodeSpec is missing",
36+
Field: nutanixNodeSpecField,
37+
},
38+
},
39+
}
40+
}
41+
}
42+
43+
return func(ctx context.Context) preflight.CheckResult {
44+
return storageContainerCheck(
45+
client,
46+
nutanixNodeSpec,
47+
nutanixNodeSpecField,
48+
ptr.To(addonsSpec.CSI.Providers["nutanix"]),
49+
)
50+
}
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 storageContainerCheck(
57+
nutanixClient *prismv4.Client,
58+
nodeSpec *carenv1.NutanixNodeSpec,
59+
field string,
60+
csiSpec *carenv1.CSIProvider,
61+
) preflight.CheckResult {
62+
const (
63+
csiParameterKeyStorageContainer = "storageContainer"
64+
)
65+
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("no storage container found for cluster %q", *nodeSpec.MachineDetails.Cluster.Name),
72+
Field: field,
73+
})
74+
75+
return *result
76+
}
77+
78+
if csiSpec.StorageClassConfigs == nil {
79+
result.Allowed = false
80+
result.Causes = append(result.Causes, preflight.Cause{
81+
Message: fmt.Sprintf(
82+
"no storage class configs found for cluster %q",
83+
*nodeSpec.MachineDetails.Cluster.Name,
84+
),
85+
Field: field,
86+
})
87+
88+
return *result
89+
}
90+
91+
for _, storageClassConfig := range csiSpec.StorageClassConfigs {
92+
if storageClassConfig.Parameters == nil {
93+
continue
94+
}
95+
96+
storageContainer, ok := storageClassConfig.Parameters[csiParameterKeyStorageContainer]
97+
if !ok {
98+
continue
99+
}
100+
101+
// TODO: check if cluster name is set, if not use uuid.
102+
// If neither is set, use the cluster name from the NodeSpec failure domain.
103+
if _, err := getStorageContainer(nutanixClient, nodeSpec, storageContainer); err != nil {
104+
result.Allowed = false
105+
result.Error = true
106+
result.Causes = append(result.Causes, preflight.Cause{
107+
Message: fmt.Sprintf("failed to check if storage container named %q exists: %s", storageContainer, err),
108+
Field: field,
109+
})
110+
111+
return *result
112+
}
113+
}
114+
115+
return *result
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)