Skip to content

Commit 317ab8c

Browse files
committed
feat: Nutanix VM image preflight check
1 parent b377301 commit 317ab8c

File tree

4 files changed

+233
-0
lines changed

4 files changed

+233
-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: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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+
}
24+
25+
func (n *Checker) Init(
26+
ctx context.Context,
27+
client ctrlclient.Client,
28+
cluster *clusterv1.Cluster,
29+
) ([]preflight.Check, error) {
30+
n.client = client
31+
32+
clusterConfig, err := variables.UnmarshalClusterConfigVariable(cluster.Spec.Topology.Variables)
33+
if err != nil {
34+
return nil, fmt.Errorf("failed to unmarshal topology variable %q: %w", carenv1.ClusterConfigVariableName, err)
35+
}
36+
37+
if clusterConfig.Nutanix == nil {
38+
return nil, fmt.Errorf("missing Nutanix configuration in cluster topology")
39+
}
40+
41+
// Initialize Nutanix client from the credentials referenced by the cluster configuration.
42+
n.nutanixClient, err = newV4Client(ctx, client, cluster.Namespace, clusterConfig)
43+
if err != nil {
44+
return nil, fmt.Errorf("failed to create Nutanix client: %w", err)
45+
}
46+
47+
checks := []preflight.Check{}
48+
if clusterConfig.ControlPlane != nil && clusterConfig.ControlPlane.Nutanix != nil {
49+
checks = append(
50+
checks,
51+
n.VMImageCheck(
52+
&clusterConfig.ControlPlane.Nutanix.MachineDetails,
53+
"controlPlane.nutanix.machineDetails",
54+
),
55+
)
56+
}
57+
58+
if cluster.Spec.Topology.Workers != nil {
59+
for i, md := range cluster.Spec.Topology.Workers.MachineDeployments {
60+
if md.Variables == nil {
61+
continue
62+
}
63+
64+
workerConfig, err := variables.UnmarshalWorkerConfigVariable(md.Variables.Overrides)
65+
if err != nil {
66+
return nil, fmt.Errorf(
67+
"failed to unmarshal topology variable %q %d: %w",
68+
carenv1.WorkerConfigVariableName,
69+
i,
70+
err,
71+
)
72+
}
73+
74+
if workerConfig.Nutanix == nil {
75+
continue
76+
}
77+
78+
n.VMImageCheck(
79+
&workerConfig.Nutanix.MachineDetails,
80+
fmt.Sprintf(
81+
"workers.machineDeployments[.name=%s].variables.overrides[.name=workerConfig].value.nutanix.machineDetails",
82+
md.Name,
83+
),
84+
)
85+
}
86+
}
87+
88+
return checks, nil
89+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
})
60+
}
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+
}

0 commit comments

Comments
 (0)