-
Notifications
You must be signed in to change notification settings - Fork 7
feat: Nutanix VM image preflight check #1130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
dlipovetsky
wants to merge
6
commits into
dlipovetsky/preflight-checks-framework
Choose a base branch
from
dlipovetsky/preflight-nutanix-vmimage
base: dlipovetsky/preflight-checks-framework
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+365
−1
Draft
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
317ab8c
feat: Nutanix VM image preflight check
dlipovetsky fc06a6c
Implement single VM image check for control plane and workers
dlipovetsky bd13c66
Efficient, threadsafe data initialization
dlipovetsky aece891
Factor helpers out of checker
dlipovetsky 171bbf0
Return preflight.Cause instead of metav1.StatusCause
dlipovetsky 46a03bd
Initialize Nutanix client in Init
dlipovetsky File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
// Copyright 2025 Nutanix. All rights reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package nutanix | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" | ||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" | ||
|
||
prismv4 "github.com/nutanix-cloud-native/prism-go-client/v4" | ||
|
||
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight" | ||
preflightutil "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight/util" | ||
) | ||
|
||
type Checker struct { | ||
client ctrlclient.Client | ||
cluster *clusterv1.Cluster | ||
|
||
nutanixClient *prismv4.Client | ||
variablesGetter *preflightutil.VariablesGetter | ||
} | ||
|
||
func (n *Checker) Init( | ||
ctx context.Context, | ||
client ctrlclient.Client, | ||
cluster *clusterv1.Cluster, | ||
) []preflight.Check { | ||
n.client = client | ||
n.cluster = cluster | ||
n.variablesGetter = preflightutil.NewVariablesGetter(cluster) | ||
|
||
// Initialize the Nutanix client. If it fails, return a check that indicates the error. | ||
clusterConfig, err := n.variablesGetter.ClusterConfig() | ||
if err != nil { | ||
return []preflight.Check{ | ||
func(ctx context.Context) preflight.CheckResult { | ||
return preflight.CheckResult{ | ||
Name: "NutanixClientInitialization", | ||
Allowed: false, | ||
Error: true, | ||
Causes: []preflight.Cause{ | ||
{ | ||
Message: fmt.Sprintf("failed to read clusterConfig variable: %s", err), | ||
Field: "cluster.spec.topology.variables", | ||
}, | ||
}, | ||
} | ||
}, | ||
} | ||
} | ||
|
||
n.nutanixClient, err = v4client(ctx, client, cluster, clusterConfig.Nutanix) | ||
// TODO Verify the credentials by making a users API call. | ||
if err != nil { | ||
return []preflight.Check{ | ||
func(ctx context.Context) preflight.CheckResult { | ||
return preflight.CheckResult{ | ||
Name: "NutanixClientInitialization", | ||
Allowed: false, | ||
Error: true, | ||
Causes: []preflight.Cause{ | ||
{ | ||
Message: fmt.Sprintf("failed to initialize Nutanix client: %s", err), | ||
Field: "cluster.spec.topology.variables[.name=clusterConfig].nutanix", | ||
}, | ||
}, | ||
} | ||
}, | ||
} | ||
} | ||
|
||
return []preflight.Check{ | ||
n.VMImages, | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package nutanix | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
corev1 "k8s.io/api/core/v1" | ||
"k8s.io/apimachinery/pkg/types" | ||
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" | ||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" | ||
|
||
prism "github.com/nutanix-cloud-native/prism-go-client" | ||
prismcredentials "github.com/nutanix-cloud-native/prism-go-client/environment/credentials" | ||
prismv4 "github.com/nutanix-cloud-native/prism-go-client/v4" | ||
|
||
carenv1 "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1" | ||
) | ||
|
||
func v4client(ctx context.Context, | ||
client ctrlclient.Client, | ||
cluster *clusterv1.Cluster, | ||
nutanixSpec *carenv1.NutanixSpec, | ||
) ( | ||
*prismv4.Client, | ||
error, | ||
) { | ||
if nutanixSpec == nil { | ||
return nil, fmt.Errorf("nutanixSpec is nil") | ||
} | ||
|
||
if nutanixSpec.PrismCentralEndpoint.Credentials.SecretRef.Name == "" { | ||
return nil, fmt.Errorf("prism Central credentials reference SecretRef.Name has no value") | ||
} | ||
|
||
credentialsSecret := &corev1.Secret{} | ||
if err := client.Get( | ||
ctx, | ||
types.NamespacedName{ | ||
Namespace: cluster.Namespace, | ||
Name: nutanixSpec.PrismCentralEndpoint.Credentials.SecretRef.Name, | ||
}, | ||
credentialsSecret, | ||
); err != nil { | ||
return nil, fmt.Errorf("failed to get Prism Central credentials Secret: %w", err) | ||
} | ||
|
||
// Get username and password | ||
credentials, err := prismcredentials.ParseCredentials(credentialsSecret.Data["credentials"]) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to parse Prism Central credentials from Secret: %w", err) | ||
} | ||
|
||
host, port, err := nutanixSpec.PrismCentralEndpoint.ParseURL() | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to parse Prism Central endpoint: %w", err) | ||
} | ||
|
||
nutanixClient, err := prismv4.NewV4Client(prism.Credentials{ | ||
Endpoint: fmt.Sprintf("%s:%d", host, port), | ||
Username: credentials.Username, | ||
Password: credentials.Password, | ||
Insecure: nutanixSpec.PrismCentralEndpoint.Insecure, | ||
}) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to create Prism V4 client: %w", err) | ||
} | ||
|
||
return nutanixClient, nil | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
package nutanix | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
vmmv4 "github.com/nutanix/ntnx-api-golang-clients/vmm-go-client/v4/models/vmm/v4/content" | ||
|
||
prismv4 "github.com/nutanix-cloud-native/prism-go-client/v4" | ||
|
||
capxv1 "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/external/github.com/nutanix-cloud-native/cluster-api-provider-nutanix/api/v1beta1" | ||
carenv1 "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1" | ||
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight" | ||
) | ||
|
||
func (n *Checker) VMImages(ctx context.Context) preflight.CheckResult { | ||
result := preflight.CheckResult{ | ||
Name: "VMImages", | ||
Allowed: true, | ||
} | ||
|
||
// Check control plane VM image. | ||
clusterConfig, err := n.variablesGetter.ClusterConfig() | ||
if err != nil { | ||
result.Error = true | ||
result.Allowed = false | ||
result.Causes = append(result.Causes, preflight.Cause{ | ||
Message: fmt.Sprintf("failed to read clusterConfig variable: %s", err), | ||
Field: "cluster.spec.topology.variables", | ||
}) | ||
} | ||
if clusterConfig != nil && clusterConfig.ControlPlane != nil && clusterConfig.ControlPlane.Nutanix != nil { | ||
n.vmImageCheckForMachineDetails( | ||
ctx, | ||
&clusterConfig.ControlPlane.Nutanix.MachineDetails, | ||
"cluster.spec.topology.variables[.name=clusterConfig].controlPlane.nutanix.machineDetails", | ||
&result, | ||
) | ||
} | ||
|
||
// Check worker VM images. | ||
if n.cluster.Spec.Topology.Workers != nil { | ||
for _, md := range n.cluster.Spec.Topology.Workers.MachineDeployments { | ||
workerConfig, err := n.variablesGetter.WorkerConfigForMachineDeployment(md) | ||
if err != nil { | ||
result.Error = true | ||
result.Causes = append(result.Causes, preflight.Cause{ | ||
Message: fmt.Sprintf("failed to read workerConfig variable: %s", err), | ||
Field: fmt.Sprintf( | ||
"cluster.spec.topology.workers.machineDeployments[.name=%s].variables.overrides", | ||
md.Name, | ||
), | ||
}) | ||
} | ||
if workerConfig != nil && workerConfig.Nutanix != nil { | ||
n.vmImageCheckForMachineDetails( | ||
ctx, | ||
&workerConfig.Nutanix.MachineDetails, | ||
fmt.Sprintf( | ||
"workers.machineDeployments[.name=%s].variables.overrides[.name=workerConfig].value.nutanix.machineDetails", | ||
md.Name, | ||
), | ||
&result, | ||
) | ||
} | ||
} | ||
} | ||
|
||
return result | ||
} | ||
|
||
func (n *Checker) vmImageCheckForMachineDetails( | ||
ctx context.Context, | ||
details *carenv1.NutanixMachineDetails, | ||
field string, | ||
result *preflight.CheckResult, | ||
) { | ||
if details.ImageLookup != nil { | ||
result.Allowed = false | ||
result.Error = true | ||
result.Causes = append(result.Causes, preflight.Cause{ | ||
Message: "ImageLookup is not yet supported", | ||
Field: field, | ||
}) | ||
return | ||
} | ||
|
||
if details.Image != nil { | ||
images, err := getVMImages(n.nutanixClient, details.Image) | ||
if err != nil { | ||
result.Allowed = false | ||
result.Error = true | ||
result.Causes = append(result.Causes, preflight.Cause{ | ||
Message: fmt.Sprintf("failed to count matching VM Images: %s", err), | ||
Field: field, | ||
}) | ||
return | ||
} | ||
|
||
if len(images) != 1 { | ||
result.Allowed = false | ||
result.Causes = append(result.Causes, preflight.Cause{ | ||
Message: fmt.Sprintf("expected to find 1 VM Image, found %d", len(images)), | ||
Field: field, | ||
}) | ||
} | ||
} | ||
} | ||
|
||
func getVMImages( | ||
client *prismv4.Client, | ||
id *capxv1.NutanixResourceIdentifier, | ||
) ([]vmmv4.Image, error) { | ||
switch { | ||
case id.IsUUID(): | ||
resp, err := client.ImagesApiInstance.GetImageById(id.UUID) | ||
if err != nil { | ||
return nil, err | ||
} | ||
image, ok := resp.GetData().(vmmv4.Image) | ||
if !ok { | ||
return nil, fmt.Errorf("failed to get data returned by GetImageById") | ||
} | ||
return []vmmv4.Image{image}, nil | ||
case id.IsName(): | ||
filter_ := fmt.Sprintf("name eq '%s'", *id.Name) | ||
resp, err := client.ImagesApiInstance.ListImages(nil, nil, &filter_, nil, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if resp == nil || resp.GetData() == nil { | ||
// No images were returned. | ||
return []vmmv4.Image{}, nil | ||
} | ||
images, ok := resp.GetData().([]vmmv4.Image) | ||
if !ok { | ||
return nil, fmt.Errorf("failed to get data returned by ListImages") | ||
} | ||
return images, nil | ||
default: | ||
return nil, fmt.Errorf("image identifier is missing both name and uuid") | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package nutanix | ||
|
||
import ( | ||
"fmt" | ||
"sync" | ||
|
||
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" | ||
|
||
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/variables" | ||
) | ||
|
||
// VariablesGetter provides methods to retrieve variables from a Cluster object. | ||
// These methods are thread-safe and cache the results for efficiency. | ||
type VariablesGetter struct { | ||
cluster *clusterv1.Cluster | ||
workerConfigGetterByMachineDeploymentName map[string]func() (*variables.WorkerNodeConfigSpec, error) | ||
workerConfigGetterByMachineDeploymentNameMutex sync.Mutex | ||
} | ||
|
||
func NewVariablesGetter(cluster *clusterv1.Cluster) *VariablesGetter { | ||
return &VariablesGetter{ | ||
cluster: cluster, | ||
workerConfigGetterByMachineDeploymentName: make( | ||
map[string]func() (*variables.WorkerNodeConfigSpec, error), | ||
), | ||
workerConfigGetterByMachineDeploymentNameMutex: sync.Mutex{}, | ||
} | ||
} | ||
|
||
// ClusterConfig retrieves the cluster configuration variables from the Cluster object. | ||
// This method is thread-safe, and caches the result. | ||
func (g *VariablesGetter) ClusterConfig() (*variables.ClusterConfigSpec, error) { | ||
return sync.OnceValues(func() (*variables.ClusterConfigSpec, error) { | ||
if g.cluster.Spec.Topology.Variables == nil { | ||
return nil, nil | ||
} | ||
|
||
clusterConfig, err := variables.UnmarshalClusterConfigVariable(g.cluster.Spec.Topology.Variables) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to unmarshal .variables[.name=clusterConfig]: %w", err) | ||
} | ||
|
||
return clusterConfig, nil | ||
})() | ||
} | ||
|
||
// WorkerConfigForMachineDeployment retrieves the worker configuration variables for the given MachineDeployment. | ||
// This method is thread-safe, and caches the result. | ||
func (g *VariablesGetter) WorkerConfigForMachineDeployment( | ||
md clusterv1.MachineDeploymentTopology, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚫 [golangci] reported by reviewdog 🐶 |
||
) (*variables.WorkerNodeConfigSpec, error) { | ||
g.workerConfigGetterByMachineDeploymentNameMutex.Lock() | ||
defer g.workerConfigGetterByMachineDeploymentNameMutex.Unlock() | ||
|
||
fn, ok := g.workerConfigGetterByMachineDeploymentName[md.Name] | ||
if !ok { | ||
fn = sync.OnceValues(func() (*variables.WorkerNodeConfigSpec, error) { | ||
if md.Variables == nil { | ||
return nil, nil | ||
} | ||
|
||
workerConfig, err := variables.UnmarshalWorkerConfigVariable(md.Variables.Overrides) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to unmarshal .variables.overrides[.name=workerConfig]: %w", err) | ||
} | ||
|
||
return workerConfig, nil | ||
}) | ||
} | ||
return fn() | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 [golangci] reported by reviewdog 🐶
(*Checker).vmImageCheckForMachineDetails - ctx is unused (unparam)