Skip to content

Commit 828d58b

Browse files
feat(preflight): Add VM Image kubernetes version check
This check ensures images that have the standard NKP naming scheme have the image with the same kubernetes version as the cluster version.
1 parent cc32a9c commit 828d58b

File tree

2 files changed

+511
-0
lines changed

2 files changed

+511
-0
lines changed
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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+
"regexp"
10+
"strings"
11+
12+
vmmv4 "github.com/nutanix/ntnx-api-golang-clients/vmm-go-client/v4/models/vmm/v4/content"
13+
14+
carenv1 "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
15+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight"
16+
)
17+
18+
// Examples: nkp-ubuntu-22.04-vgpu-1.32.3-20250604180644, nkp-rocky-9.5-release-cis-1.32.3-20250430150550.
19+
var kubernetesVersionRegex = regexp.MustCompile(`-(\d+\.\d+\.\d+)-\d{14}$`)
20+
21+
type imageKubernetesVersionCheck struct {
22+
machineDetails *carenv1.NutanixMachineDetails
23+
field string
24+
nclient client
25+
clusterK8sVersion string
26+
}
27+
28+
func (c *imageKubernetesVersionCheck) Name() string {
29+
return "NutanixVMImageKubernetesVersion"
30+
}
31+
32+
func (c *imageKubernetesVersionCheck) Run(ctx context.Context) preflight.CheckResult {
33+
result := preflight.CheckResult{
34+
Allowed: false,
35+
}
36+
37+
if c.machineDetails.ImageLookup != nil {
38+
result.Allowed = true
39+
result.Warnings = append(
40+
result.Warnings,
41+
fmt.Sprintf("%s uses imageLookup, which is not yet supported by checks", c.field),
42+
)
43+
return result
44+
}
45+
46+
if c.machineDetails.Image != nil {
47+
images, err := getVMImages(c.nclient, c.machineDetails.Image)
48+
if err != nil {
49+
result.Allowed = false
50+
result.Error = true
51+
result.Causes = append(result.Causes, preflight.Cause{
52+
Message: fmt.Sprintf("failed to get VM Image: %s", err),
53+
Field: c.field,
54+
})
55+
return result
56+
}
57+
58+
if len(images) != 1 {
59+
result.Allowed = false
60+
result.Causes = append(result.Causes, preflight.Cause{
61+
Message: fmt.Sprintf("expected to find 1 VM Image, found %d", len(images)),
62+
Field: c.field,
63+
})
64+
return result
65+
}
66+
67+
// Check Kubernetes version if cluster version is provided
68+
if c.clusterK8sVersion != "" {
69+
if err := c.checkKubernetesVersion(&images[0]); err != nil {
70+
result.Allowed = false
71+
result.Error = true
72+
result.Causes = append(result.Causes, preflight.Cause{
73+
Message: err.Error(),
74+
Field: c.field,
75+
})
76+
return result
77+
}
78+
}
79+
80+
result.Allowed = true
81+
return result
82+
}
83+
84+
// Neither ImageLookup nor Image is specified.
85+
return result
86+
}
87+
88+
func (c *imageKubernetesVersionCheck) checkKubernetesVersion(image *vmmv4.Image) error {
89+
imageName := ""
90+
if image.Name != nil {
91+
imageName = *image.Name
92+
}
93+
94+
if imageName == "" {
95+
return fmt.Errorf("VM image name is empty")
96+
}
97+
98+
imageK8sVersion, err := extractKubernetesVersionFromImageName(imageName)
99+
if err != nil {
100+
return fmt.Errorf("failed to extract Kubernetes version from image name '%s': %s. "+
101+
"This check assumes NKP image naming convention. "+
102+
"You can opt out of this check if using custom image naming", imageName, err)
103+
}
104+
105+
if imageK8sVersion != c.clusterK8sVersion {
106+
return fmt.Errorf(
107+
"kubernetes version mismatch: cluster version '%s' does not match image version '%s' (from image name '%s')",
108+
c.clusterK8sVersion,
109+
imageK8sVersion,
110+
imageName,
111+
)
112+
}
113+
114+
return nil
115+
}
116+
117+
// Examples: nkp-ubuntu-22.04-vgpu-1.32.3-20250604180644 -> 1.32.3.
118+
func extractKubernetesVersionFromImageName(imageName string) (string, error) {
119+
matches := kubernetesVersionRegex.FindStringSubmatch(imageName)
120+
if len(matches) < 2 {
121+
return "", fmt.Errorf(
122+
"image name does not match expected NKP naming convention (expected pattern: *-<k8s-version>-<timestamp>)",
123+
)
124+
}
125+
return matches[1], nil
126+
}
127+
128+
func newVMImageKubernetesVersionChecks(
129+
cd *checkDependencies,
130+
) []preflight.Check {
131+
checks := make([]preflight.Check, 0)
132+
133+
if cd.nclient == nil {
134+
return checks
135+
}
136+
137+
// Get cluster Kubernetes version for version matching
138+
clusterK8sVersion := ""
139+
if cd.cluster != nil && cd.cluster.Spec.Topology != nil && cd.cluster.Spec.Topology.Version != "" {
140+
clusterK8sVersion = strings.TrimPrefix(cd.cluster.Spec.Topology.Version, "v")
141+
}
142+
143+
if cd.nutanixClusterConfigSpec != nil && cd.nutanixClusterConfigSpec.ControlPlane != nil &&
144+
cd.nutanixClusterConfigSpec.ControlPlane.Nutanix != nil {
145+
checks = append(checks,
146+
&imageKubernetesVersionCheck{
147+
machineDetails: &cd.nutanixClusterConfigSpec.ControlPlane.Nutanix.MachineDetails,
148+
field: "cluster.spec.topology.variables[.name=clusterConfig]" +
149+
".value.nutanix.controlPlane.machineDetails",
150+
nclient: cd.nclient,
151+
clusterK8sVersion: clusterK8sVersion,
152+
},
153+
)
154+
}
155+
156+
for mdName, nutanixWorkerNodeConfigSpec := range cd.nutanixWorkerNodeConfigSpecByMachineDeploymentName {
157+
if nutanixWorkerNodeConfigSpec.Nutanix != nil {
158+
checks = append(checks,
159+
&imageKubernetesVersionCheck{
160+
machineDetails: &nutanixWorkerNodeConfigSpec.Nutanix.MachineDetails,
161+
field: fmt.Sprintf("cluster.spec.topology.workers.machineDeployments[.name=%s]"+
162+
".variables[.name=workerConfig].value.nutanix.machineDetails", mdName),
163+
nclient: cd.nclient,
164+
clusterK8sVersion: clusterK8sVersion,
165+
},
166+
)
167+
}
168+
}
169+
170+
return checks
171+
}

0 commit comments

Comments
 (0)