diff --git a/pkg/webhook/preflight/nutanix/imagekubernetesversioncheck.go b/pkg/webhook/preflight/nutanix/imagekubernetesversioncheck.go index 912b8332c..b45120f51 100644 --- a/pkg/webhook/preflight/nutanix/imagekubernetesversioncheck.go +++ b/pkg/webhook/preflight/nutanix/imagekubernetesversioncheck.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/blang/semver/v4" - vmmv4 "github.com/nutanix/ntnx-api-golang-clients/vmm-go-client/v4/models/vmm/v4/content" 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" @@ -29,8 +28,13 @@ func (c *imageKubernetesVersionCheck) Name() string { func (c *imageKubernetesVersionCheck) Run(ctx context.Context) preflight.CheckResult { if c.machineDetails.ImageLookup != nil { return preflight.CheckResult{ - Allowed: true, - Warnings: []string{fmt.Sprintf("%s uses imageLookup, which is not yet supported by checks", c.field)}, + Allowed: true, + Warnings: []string{ + fmt.Sprintf( + "%s uses imageLookup, which is not yet supported by checks", + c.field, + ), + }, } } @@ -42,8 +46,12 @@ func (c *imageKubernetesVersionCheck) Run(ctx context.Context) preflight.CheckRe InternalError: true, Causes: []preflight.Cause{ { - Message: fmt.Sprintf("Failed to get VM Image: %s", err), - Field: c.field + ".image", + Message: fmt.Sprintf( + "Failed to get VM Image %q: %s. This is usually a temporary error. Please retry.", + c.machineDetails.Image, + err, + ), + Field: c.field + ".image", }, }, } @@ -54,51 +62,66 @@ func (c *imageKubernetesVersionCheck) Run(ctx context.Context) preflight.CheckRe Allowed: true, } } + image := images[0] - if err := c.checkKubernetesVersion(&images[0]); err != nil { + if image.Name == nil || *image.Name == "" { return preflight.CheckResult{ Allowed: false, InternalError: false, Causes: []preflight.Cause{ { - Message: "Kubernetes version check failed: " + err.Error(), - Field: c.field + ".image", + Message: fmt.Sprintf( + "The VM Image identified by %q has no name. Give the VM Image a name, or use a different VM Image. Make sure the VM Image contains the Kubernetes version supported by the VM Image. Choose a VM Image that supports the cluster Kubernetes version: %q", //nolint:lll // The message is long. + *c.machineDetails.Image, + c.clusterK8sVersion, + ), + Field: c.field + ".image", }, }, } } - } - - return preflight.CheckResult{Allowed: true} -} - -func (c *imageKubernetesVersionCheck) checkKubernetesVersion(image *vmmv4.Image) error { - imageName := "" - if image.Name != nil { - imageName = *image.Name - } - - if imageName == "" { - return fmt.Errorf("VM image name is empty") - } - - parsedVersion, err := semver.Parse(c.clusterK8sVersion) - if err != nil { - return fmt.Errorf("failed to parse kubernetes version %q: %v", c.clusterK8sVersion, err) - } - // For example, "1.33.1+fips.0" becomes "1.33.1". - k8sVersion := parsedVersion.FinalizeVersion() + // Uses the same function that is used by the Cluster API topology validation webhook. + parsedClusterK8sVersion, err := semver.ParseTolerant(c.clusterK8sVersion) + if err != nil { + return preflight.CheckResult{ + Allowed: false, + // The Cluster API topology validation webhook should prevent this from happening, + // so if it does, treat it as an internal error. + InternalError: true, + Causes: []preflight.Cause{ + { + Message: fmt.Sprintf( + "The Cluster Kubernetes version %q is not a valid semantic version. This error should not happen under normal circumstances. Please report.", //nolint:lll // The message is long. + c.clusterK8sVersion, + ), + Field: c.field + ".image", + }, + }, + } + } - if !strings.Contains(imageName, k8sVersion) { - return fmt.Errorf( - "kubernetes version %q is not part of image name %q", - c.clusterK8sVersion, - imageName, - ) + finalizedClusterK8sVersion := parsedClusterK8sVersion.FinalizeVersion() + if !strings.Contains(*image.Name, finalizedClusterK8sVersion) { + return preflight.CheckResult{ + Allowed: false, + InternalError: false, + Causes: []preflight.Cause{ + { + Message: fmt.Sprintf( + "The VM Image identified by %q has the name %q. Make sure the VM Image name contains the Kubernetes version supported by the VM Image. Choose a VM Image that supports the cluster Kubernetes version: %q.", //nolint:lll // The message is long. + *c.machineDetails.Image, + *image.Name, + finalizedClusterK8sVersion, + ), + Field: c.field + ".image", + }, + }, + } + } } - return nil + return preflight.CheckResult{Allowed: true} } func newVMImageKubernetesVersionChecks( diff --git a/pkg/webhook/preflight/nutanix/imagekubernetesversioncheck_test.go b/pkg/webhook/preflight/nutanix/imagekubernetesversioncheck_test.go index 6edf303ac..ca37fbb54 100644 --- a/pkg/webhook/preflight/nutanix/imagekubernetesversioncheck_test.go +++ b/pkg/webhook/preflight/nutanix/imagekubernetesversioncheck_test.go @@ -79,8 +79,7 @@ func TestVMImageCheckWithKubernetesVersion(t *testing.T) { InternalError: false, Causes: []preflight.Cause{ { - ///nolint:lll // The message is long. - Message: "Kubernetes version check failed: kubernetes version \"1.32.3\" is not part of image name \"kubedistro-ubuntu-22.04-vgpu-1.31.5-20250604180644\"", + Message: "The VM Image identified by \"test-uuid\" has the name \"kubedistro-ubuntu-22.04-vgpu-1.31.5-20250604180644\". Make sure the VM Image name contains the Kubernetes version supported by the VM Image. Choose a VM Image that supports the cluster Kubernetes version: \"1.32.3\".", ///nolint:lll // The message is long. Field: "machineDetails.image", }, }, @@ -112,7 +111,7 @@ func TestVMImageCheckWithKubernetesVersion(t *testing.T) { }, }, { - name: "custom image name - extraction fails", + name: "custom image name has no Kubernetes version", nclient: &mocknclient{ getImageByIdFunc: func(uuid *string) (*vmmv4.GetImageApiResponse, error) { resp := &vmmv4.GetImageApiResponse{} @@ -137,8 +136,7 @@ func TestVMImageCheckWithKubernetesVersion(t *testing.T) { InternalError: false, Causes: []preflight.Cause{ { - ///nolint:lll // The message is long. - Message: "Kubernetes version check failed: kubernetes version \"1.32.3\" is not part of image name \"my-custom-image-name\"", + Message: "The VM Image identified by \"test-uuid\" has the name \"my-custom-image-name\". Make sure the VM Image name contains the Kubernetes version supported by the VM Image. Choose a VM Image that supports the cluster Kubernetes version: \"1.32.3\".", ///nolint:lll // The message is long. Field: "machineDetails.image", }, }, @@ -167,11 +165,10 @@ func TestVMImageCheckWithKubernetesVersion(t *testing.T) { clusterK8sVersion: "invalid.version", want: preflight.CheckResult{ Allowed: false, - InternalError: false, + InternalError: true, Causes: []preflight.Cause{ { - //nolint:lll // The message is long. - Message: "Kubernetes version check failed: failed to parse kubernetes version \"invalid.version\": No Major.Minor.Patch elements found", + Message: "The Cluster Kubernetes version \"invalid.version\" is not a valid semantic version. This error should not happen under normal circumstances. Please report.", //nolint:lll // The message is long. Field: "machineDetails.image", }, }, @@ -203,7 +200,7 @@ func TestVMImageCheckWithKubernetesVersion(t *testing.T) { InternalError: false, Causes: []preflight.Cause{ { - Message: "Kubernetes version check failed: VM image name is empty", + Message: "The VM Image identified by \"test-uuid\" has no name. Give the VM Image a name, or use a different VM Image. Make sure the VM Image contains the Kubernetes version supported by the VM Image. Choose a VM Image that supports the cluster Kubernetes version: \"1.32.3\"", //nolint:lll // The message is long. Field: "machineDetails.image", }, }, @@ -260,7 +257,7 @@ func TestVMImageCheckWithKubernetesVersion(t *testing.T) { InternalError: true, Causes: []preflight.Cause{ { - Message: "Failed to get VM Image: some error", + Message: "Failed to get VM Image \"test-uuid\": some error. This is usually a temporary error. Please retry.", //nolint:lll // The message is long. Field: "machineDetails.image", }, },