Skip to content

Commit 3688af0

Browse files
committed
feat: Nutanix VM image preflight check
1 parent 306a852 commit 3688af0

File tree

4 files changed

+237
-0
lines changed

4 files changed

+237
-0
lines changed

cmd/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/options"
4343
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/cluster"
4444
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight"
45+
preflightnutanix "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight/nutanix"
4546
)
4647

4748
func main() {
@@ -224,6 +225,7 @@ func main() {
224225
Handler: preflight.New(mgr.GetClient(), admission.NewDecoder(mgr.GetScheme()),
225226
[]preflight.Checker{
226227
// Add your preflight checkers here.
228+
&preflightnutanix.Checker{},
227229
}...,
228230
),
229231
})
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package nutanix
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
corev1 "k8s.io/api/core/v1"
8+
"k8s.io/apimachinery/pkg/types"
9+
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
10+
11+
prism "github.com/nutanix-cloud-native/prism-go-client"
12+
prismcredentials "github.com/nutanix-cloud-native/prism-go-client/environment/credentials"
13+
prismv4 "github.com/nutanix-cloud-native/prism-go-client/v4"
14+
15+
carenvariables "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/variables"
16+
)
17+
18+
func newV4Client(
19+
ctx context.Context,
20+
client ctrlclient.Client,
21+
clusterNamespace string,
22+
clusterConfig *carenvariables.ClusterConfigSpec,
23+
) (
24+
*prismv4.Client,
25+
error,
26+
) {
27+
if clusterConfig.Nutanix.PrismCentralEndpoint.Credentials.SecretRef.Name == "" {
28+
return nil, fmt.Errorf("Prism Central credentials reference SecretRef.Name has no value")
29+
}
30+
31+
credentialsSecret := &corev1.Secret{}
32+
if err := client.Get(
33+
ctx,
34+
types.NamespacedName{
35+
Namespace: clusterNamespace,
36+
Name: clusterConfig.Nutanix.PrismCentralEndpoint.Credentials.SecretRef.Name,
37+
},
38+
credentialsSecret,
39+
); err != nil {
40+
return nil, fmt.Errorf("failed to get Prism Central credentials Secret: %w", err)
41+
}
42+
43+
// Get username and password
44+
credentials, err := prismcredentials.ParseCredentials(credentialsSecret.Data["credentials"])
45+
if err != nil {
46+
return nil, fmt.Errorf("failed to parse Prism Central credentials from Secret: %w", err)
47+
}
48+
49+
host, port, err := clusterConfig.Nutanix.PrismCentralEndpoint.ParseURL()
50+
if err != nil {
51+
return nil, fmt.Errorf("failed to parse Prism Central endpoint: %w", err)
52+
}
53+
54+
return prismv4.NewV4Client(prism.Credentials{
55+
Endpoint: fmt.Sprintf("%s:%d", host, port),
56+
Username: credentials.Username,
57+
Password: credentials.Password,
58+
Insecure: clusterConfig.Nutanix.PrismCentralEndpoint.Insecure,
59+
// TODO AdditionalTrustBundle
60+
})
61+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package nutanix
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
vmmv4 "github.com/nutanix/ntnx-api-golang-clients/vmm-go-client/v4/models/vmm/v4/content"
8+
9+
prismv4 "github.com/nutanix-cloud-native/prism-go-client/v4"
10+
11+
capxv1 "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/external/github.com/nutanix-cloud-native/cluster-api-provider-nutanix/api/v1beta1"
12+
carenv1 "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
13+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight"
14+
)
15+
16+
func (n *Checker) VMImageCheck(details carenv1.NutanixMachineDetails, field string) preflight.Check {
17+
return func(ctx context.Context) preflight.CheckResult {
18+
result := preflight.CheckResult{
19+
Allowed: true,
20+
Field: field,
21+
}
22+
23+
if details.ImageLookup != nil {
24+
result.Allowed = false
25+
result.Message = "ImageLookup is not yet supported"
26+
return result
27+
}
28+
29+
if details.Image != nil {
30+
images, err := getVMImages(n.nutanixClient, details.Image)
31+
if err != nil {
32+
result.Allowed = false
33+
result.Error = true
34+
result.Message = fmt.Sprintf("failed to count matching VM Images: %s", err)
35+
return result
36+
}
37+
38+
if len(images) != 1 {
39+
result.Allowed = false
40+
result.Message = fmt.Sprintf("expected to find 1 VM Image, found %d", len(images))
41+
return result
42+
}
43+
}
44+
45+
return result
46+
}
47+
}
48+
49+
func getVMImages(
50+
client *prismv4.Client,
51+
id *capxv1.NutanixResourceIdentifier,
52+
) ([]vmmv4.Image, error) {
53+
switch {
54+
case id.IsUUID():
55+
resp, err := client.ImagesApiInstance.GetImageById(id.UUID)
56+
if err != nil {
57+
return nil, err
58+
}
59+
image, ok := resp.GetData().(vmmv4.Image)
60+
if !ok {
61+
return nil, fmt.Errorf("failed to get data returned by GetImageById")
62+
}
63+
return []vmmv4.Image{image}, nil
64+
case id.IsName():
65+
filter_ := fmt.Sprintf("name eq '%s'", *id.Name)
66+
resp, err := client.ImagesApiInstance.ListImages(nil, nil, &filter_, nil, nil)
67+
if err != nil {
68+
return nil, err
69+
}
70+
if resp == nil || resp.GetData() == nil {
71+
// No images were returned.
72+
return []vmmv4.Image{}, nil
73+
}
74+
images, ok := resp.GetData().([]vmmv4.Image)
75+
if !ok {
76+
return nil, fmt.Errorf("failed to get data returned by ListImages")
77+
}
78+
return images, nil
79+
default:
80+
return nil, fmt.Errorf("image identifier is missing both name and uuid")
81+
}
82+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
11+
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
12+
13+
prismv4 "github.com/nutanix-cloud-native/prism-go-client/v4"
14+
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+
type Checker struct {
21+
client ctrlclient.Client
22+
nutanixClient *prismv4.Client
23+
cluster *clusterv1.Cluster
24+
clusterConfig *variables.ClusterConfigSpec
25+
}
26+
27+
func (n *Checker) Provider() string {
28+
return "nutanix"
29+
}
30+
31+
func (n *Checker) Checks(
32+
ctx context.Context,
33+
client ctrlclient.Client,
34+
cluster *clusterv1.Cluster,
35+
) ([]preflight.Check, error) {
36+
n.client = client
37+
38+
clusterConfig, err := variables.UnmarshalClusterConfigVariable(cluster.Spec.Topology.Variables)
39+
if err != nil {
40+
return nil, fmt.Errorf("failed to unmarshal topology variable %q: %w", carenv1.ClusterConfigVariableName, err)
41+
}
42+
43+
if clusterConfig.Nutanix == nil {
44+
return nil, fmt.Errorf("missing Nutanix configuration in cluster topology")
45+
}
46+
47+
// Initialize Nutanix client from the credentials referenced by the cluster configuration.
48+
n.nutanixClient, err = newV4Client(ctx, client, cluster.Namespace, clusterConfig)
49+
if err != nil {
50+
return nil, fmt.Errorf("failed to create Nutanix client: %w", err)
51+
}
52+
53+
checks := []preflight.Check{}
54+
if clusterConfig.ControlPlane != nil && clusterConfig.ControlPlane.Nutanix != nil {
55+
checks = append(
56+
checks,
57+
n.VMImageCheck(clusterConfig.ControlPlane.Nutanix.MachineDetails, "controlPlane.nutanix.machineDetails"),
58+
)
59+
}
60+
61+
if cluster.Spec.Topology.Workers != nil {
62+
for i, md := range cluster.Spec.Topology.Workers.MachineDeployments {
63+
if md.Variables == nil {
64+
continue
65+
}
66+
67+
workerConfig, err := variables.UnmarshalWorkerConfigVariable(md.Variables.Overrides)
68+
if err != nil {
69+
return nil, fmt.Errorf(
70+
"failed to unmarshal topology variable %q %d: %w",
71+
carenv1.WorkerConfigVariableName,
72+
i,
73+
err,
74+
)
75+
}
76+
77+
if workerConfig.Nutanix == nil {
78+
continue
79+
}
80+
81+
n.VMImageCheck(
82+
workerConfig.Nutanix.MachineDetails,
83+
fmt.Sprintf(
84+
"workers.machineDeployments[.name=%s].variables.overrides[.name=workerConfig].value.nutanix.machineDetails",
85+
md.Name,
86+
),
87+
)
88+
}
89+
}
90+
91+
return checks, nil
92+
}

0 commit comments

Comments
 (0)