Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 58 additions & 36 deletions pkg/webhook/preflight/nutanix/imagekubernetesversioncheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
),
},
}
}

Expand All @@ -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",
},
},
}
Expand All @@ -54,51 +62,65 @@ 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, then retry.", //nolint:lll // The message is long.
*c.machineDetails.Image,
),
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. The name does not have the cluster Kubernetes version %q. Change the VM Image name, or use a different VM Image.", //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(
Expand Down
17 changes: 7 additions & 10 deletions pkg/webhook/preflight/nutanix/imagekubernetesversioncheck_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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\". The name does not have the cluster Kubernetes version \"1.32.3\". Change the VM Image name, or use a different VM Image.", ///nolint:lll // The message is long.
Field: "machineDetails.image",
},
},
Expand Down Expand Up @@ -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{}
Expand All @@ -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\". The name does not have the cluster Kubernetes version \"1.32.3\". Change the VM Image name, or use a different VM Image.", ///nolint:lll // The message is long.
Field: "machineDetails.image",
},
},
Expand Down Expand Up @@ -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",
},
},
Expand Down Expand Up @@ -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, then retry.", //nolint:lll // The message is long.
Field: "machineDetails.image",
},
},
Expand Down Expand Up @@ -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",
},
},
Expand Down