Skip to content

Commit 9f1addd

Browse files
authored
feat: adds image templating (#498)
* feat: adds image templating * fix: undo refactors and simplify code * fix: adds changes based on review * test: adds cases describing new how image lookup and image work * fix: minor linting issues * fix: do not use FIQL * fix: updates defaults and go doc * test: adds unit tests to get lookup by format * fix: changes image description
1 parent 0f22b59 commit 9f1addd

18 files changed

+1317
-25
lines changed

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ cluster-e2e-templates-v1beta1: ## Generate cluster templates for v1beta1
278278
kustomize build $(NUTANIX_E2E_TEMPLATES)/v1beta1/cluster-template-clusterclass --load-restrictor LoadRestrictionsNone > $(NUTANIX_E2E_TEMPLATES)/v1beta1/cluster-template-clusterclass.yaml
279279
kustomize build $(NUTANIX_E2E_TEMPLATES)/v1beta1/cluster-template-clusterclass --load-restrictor LoadRestrictionsNone > $(NUTANIX_E2E_TEMPLATES)/v1beta1/clusterclass-nutanix-quick-start.yaml
280280
kustomize build $(NUTANIX_E2E_TEMPLATES)/v1beta1/cluster-template-topology --load-restrictor LoadRestrictionsNone > $(NUTANIX_E2E_TEMPLATES)/v1beta1/cluster-template-topology.yaml
281+
kustomize build $(NUTANIX_E2E_TEMPLATES)/v1beta1/cluster-template-image-lookup --load-restrictor LoadRestrictionsNone > $(NUTANIX_E2E_TEMPLATES)/v1beta1/cluster-template-image-lookup.yaml
281282

282283
cluster-e2e-templates-no-kubeproxy: ##Generate cluster templates without kubeproxy
283284
# v1beta1
@@ -303,6 +304,7 @@ cluster-templates: ## Generate cluster templates for all flavors
303304
kustomize build $(TEMPLATES_DIR)/csi3 > $(TEMPLATES_DIR)/cluster-template-csi3.yaml
304305
kustomize build $(TEMPLATES_DIR)/clusterclass > $(TEMPLATES_DIR)/cluster-template-clusterclass.yaml
305306
kustomize build $(TEMPLATES_DIR)/topology > $(TEMPLATES_DIR)/cluster-template-topology.yaml
307+
kustomize build $(TEMPLATES_DIR)/image-lookup/ > $(TEMPLATES_DIR)/cluster-template-image-lookup.yaml
306308

307309
##@ Testing
308310

api/v1beta1/nutanixmachine_types.go

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,30 @@ const (
4747
NutanixMachineBootstrapRefKindImage = "Image"
4848
)
4949

50+
// NutanixImageLookup defines how to fetch images for the cluster
51+
// using the fields combined.
52+
type NutanixImageLookup struct {
53+
// Format is the naming format to look up the image for this
54+
// machine It will be ignored if an explicit image is set. Supports
55+
// substitutions for {{.BaseOS}} and {{.K8sVersion}} with the base OS and
56+
// kubernetes version, respectively. The BaseOS will be the value in
57+
// BaseOS and the K8sVersion is the value in the Machine .spec.version, with the v prefix removed.
58+
// This is effectively the defined by the packages produced by kubernetes/release without v as a
59+
// prefix: 1.13.0, 1.12.5-mybuild.1, or 1.17.3. For example, the default
60+
// image format of {{.BaseOS}}-?{{.K8sVersion}}-* and BaseOS as "rhel-8.10" will end up
61+
// searching for images that match the pattern rhel-8.10-1.30.5-* for a
62+
// Machine that is targeting kubernetes v1.30.5. See
63+
// also: https://golang.org/pkg/text/template/
64+
// +kubebuilder:default:="capx-{{.BaseOS}}-{{.K8sVersion}}-*"
65+
Format *string `json:"format,omitempty"`
66+
// BaseOS is the name of the base operating system to use for
67+
// image lookup.
68+
// +kubebuilder:validation:MinLength:=1
69+
BaseOS string `json:"baseOS"`
70+
}
71+
5072
// NutanixMachineSpec defines the desired state of NutanixMachine
73+
// +kubebuilder:validation:XValidation:rule="has(self.image) != has(self.imageLookup)",message="Either 'image' or 'imageLookup' must be set, but not both"
5174
type NutanixMachineSpec struct {
5275
// SPEC FIELDS - desired state of NutanixMachine
5376
// Important: Run "make" to regenerate code after modifying this file
@@ -67,11 +90,16 @@ type NutanixMachineSpec struct {
6790
// The minimum memorySize is 2Gi bytes
6891
// +kubebuilder:validation:Required
6992
MemorySize resource.Quantity `json:"memorySize"`
70-
// image is to identify the rhcos image uploaded to the Prism Central (PC)
93+
// image is to identify the nutanix machine image uploaded to the Prism Central (PC)
7194
// The image identifier (uuid or name) can be obtained from the Prism Central console
7295
// or using the prism_central API.
73-
// +kubebuilder:validation:Required
74-
Image NutanixResourceIdentifier `json:"image"`
96+
// +kubebuilder:validation:Optional
97+
// +optional
98+
Image *NutanixResourceIdentifier `json:"image,omitempty"`
99+
// imageLookup is a container that holds how to look up rhcos images for the cluster.
100+
// +kubebuilder:validation:Optional
101+
// +optional
102+
ImageLookup *NutanixImageLookup `json:"imageLookup,omitempty"`
75103
// cluster is to identify the cluster (the Prism Element under management
76104
// of the Prism Central), in which the Machine's VM will be created.
77105
// The cluster identifier (uuid or name) can be obtained from the Prism Central console
@@ -93,17 +121,14 @@ type NutanixMachineSpec struct {
93121
// +kubebuilder:validation:Optional
94122
// +kubebuilder:validation:Enum:=legacy;uefi
95123
BootType NutanixBootType `json:"bootType,omitempty"`
96-
97124
// systemDiskSize is size (in Quantity format) of the system disk of the VM
98125
// The minimum systemDiskSize is 20Gi bytes
99126
// +kubebuilder:validation:Required
100127
SystemDiskSize resource.Quantity `json:"systemDiskSize"`
101-
102128
// BootstrapRef is a reference to a bootstrap provider-specific resource
103129
// that holds configuration details.
104130
// +optional
105131
BootstrapRef *corev1.ObjectReference `json:"bootstrapRef,omitempty"`
106-
107132
// List of GPU devices that need to be added to the machines.
108133
// +kubebuilder:validation:Optional
109134
GPUs []NutanixGPU `json:"gpus,omitempty"`
@@ -144,14 +169,13 @@ type NutanixMachineStatus struct {
144169
FailureMessage *string `json:"failureMessage,omitempty"`
145170
}
146171

147-
//+kubebuilder:object:root=true
148-
//+kubebuilder:resource:path=nutanixmachines,shortName=nma,scope=Namespaced,categories=cluster-api
149-
//+kubebuilder:subresource:status
150-
//+kubebuilder:storageversion
151-
//+kubebuilder:printcolumn:name="Address",type="string",JSONPath=".status.addresses[0].address",description="The VM address"
172+
// +kubebuilder:object:root=true
173+
// +kubebuilder:resource:path=nutanixmachines,shortName=nma,scope=Namespaced,categories=cluster-api
174+
// +kubebuilder:subresource:status
175+
// +kubebuilder:storageversion
176+
// +kubebuilder:printcolumn:name="Address",type="string",JSONPath=".status.addresses[0].address",description="The VM address"
152177
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.ready",description="NutanixMachine ready status"
153178
// +kubebuilder:printcolumn:name="ProviderID",type="string",JSONPath=".spec.providerID",description="NutanixMachine instance ID"
154-
155179
// NutanixMachine is the Schema for the nutanixmachines API
156180
type NutanixMachine struct {
157181
metav1.TypeMeta `json:",inline"`

api/v1beta1/zz_generated.deepcopy.go

Lines changed: 30 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/infrastructure.cluster.x-k8s.io_nutanixmachines.yaml

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ spec:
168168
type: array
169169
image:
170170
description: |-
171-
image is to identify the rhcos image uploaded to the Prism Central (PC)
171+
image is to identify the nutanix machine image uploaded to the Prism Central (PC)
172172
The image identifier (uuid or name) can be obtained from the Prism Central console
173173
or using the prism_central API.
174174
properties:
@@ -187,6 +187,34 @@ spec:
187187
required:
188188
- type
189189
type: object
190+
imageLookup:
191+
description: imageLookup is a container that holds how to look up
192+
rhcos images for the cluster.
193+
properties:
194+
baseOS:
195+
description: |-
196+
BaseOS is the name of the base operating system to use for
197+
image lookup.
198+
minLength: 1
199+
type: string
200+
format:
201+
default: capx-{{.BaseOS}}-{{.K8sVersion}}-*
202+
description: |-
203+
Format is the naming format to look up the image for this
204+
machine It will be ignored if an explicit image is set. Supports
205+
substitutions for {{.BaseOS}} and {{.K8sVersion}} with the base OS and
206+
kubernetes version, respectively. The BaseOS will be the value in
207+
BaseOS and the K8sVersion is the value in the Machine .spec.version, with the v prefix removed.
208+
This is effectively the defined by the packages produced by kubernetes/release without v as a
209+
prefix: 1.13.0, 1.12.5-mybuild.1, or 1.17.3. For example, the default
210+
image format of {{.BaseOS}}-?{{.K8sVersion}}-* and BaseOS as "rhel-8.10" will end up
211+
searching for images that match the pattern rhel-8.10-1.30.5-* for a
212+
Machine that is targeting kubernetes v1.30.5. See
213+
also: https://golang.org/pkg/text/template/
214+
type: string
215+
required:
216+
- baseOS
217+
type: object
190218
memorySize:
191219
anyOf:
192220
- type: integer
@@ -264,12 +292,14 @@ spec:
264292
minimum: 1
265293
type: integer
266294
required:
267-
- image
268295
- memorySize
269296
- systemDiskSize
270297
- vcpuSockets
271298
- vcpusPerSocket
272299
type: object
300+
x-kubernetes-validations:
301+
- message: Either 'image' or 'imageLookup' must be set, but not both
302+
rule: has(self.image) != has(self.imageLookup)
273303
status:
274304
description: NutanixMachineStatus defines the observed state of NutanixMachine
275305
properties:

config/crd/bases/infrastructure.cluster.x-k8s.io_nutanixmachinetemplates.yaml

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ spec:
191191
type: array
192192
image:
193193
description: |-
194-
image is to identify the rhcos image uploaded to the Prism Central (PC)
194+
image is to identify the nutanix machine image uploaded to the Prism Central (PC)
195195
The image identifier (uuid or name) can be obtained from the Prism Central console
196196
or using the prism_central API.
197197
properties:
@@ -211,6 +211,34 @@ spec:
211211
required:
212212
- type
213213
type: object
214+
imageLookup:
215+
description: imageLookup is a container that holds how to
216+
look up rhcos images for the cluster.
217+
properties:
218+
baseOS:
219+
description: |-
220+
BaseOS is the name of the base operating system to use for
221+
image lookup.
222+
minLength: 1
223+
type: string
224+
format:
225+
default: capx-{{.BaseOS}}-{{.K8sVersion}}-*
226+
description: |-
227+
Format is the naming format to look up the image for this
228+
machine It will be ignored if an explicit image is set. Supports
229+
substitutions for {{.BaseOS}} and {{.K8sVersion}} with the base OS and
230+
kubernetes version, respectively. The BaseOS will be the value in
231+
BaseOS and the K8sVersion is the value in the Machine .spec.version, with the v prefix removed.
232+
This is effectively the defined by the packages produced by kubernetes/release without v as a
233+
prefix: 1.13.0, 1.12.5-mybuild.1, or 1.17.3. For example, the default
234+
image format of {{.BaseOS}}-?{{.K8sVersion}}-* and BaseOS as "rhel-8.10" will end up
235+
searching for images that match the pattern rhel-8.10-1.30.5-* for a
236+
Machine that is targeting kubernetes v1.30.5. See
237+
also: https://golang.org/pkg/text/template/
238+
type: string
239+
required:
240+
- baseOS
241+
type: object
214242
memorySize:
215243
anyOf:
216244
- type: integer
@@ -293,12 +321,15 @@ spec:
293321
minimum: 1
294322
type: integer
295323
required:
296-
- image
297324
- memorySize
298325
- systemDiskSize
299326
- vcpuSockets
300327
- vcpusPerSocket
301328
type: object
329+
x-kubernetes-validations:
330+
- message: Either 'image' or 'imageLookup' must be set, but not
331+
both
332+
rule: has(self.image) != has(self.imageLookup)
302333
required:
303334
- spec
304335
type: object

controllers/helpers.go

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@ limitations under the License.
1717
package controllers
1818

1919
import (
20+
"bytes"
2021
"context"
2122
"errors"
2223
"fmt"
2324
"reflect"
25+
"regexp"
26+
"sort"
2427
"strconv"
2528
"strings"
29+
"text/template"
2630
"time"
2731

2832
"github.com/google/uuid"
@@ -303,7 +307,7 @@ func GetSubnetUUID(ctx context.Context, client *prismclientv3.Client, peUUID str
303307

304308
// GetImage returns an image. If no UUID is provided, returns the unique image with the name.
305309
// Returns an error if no image has the UUID, if no image has the name, or more than one image has the name.
306-
func GetImage(ctx context.Context, client *prismclientv3.Client, id infrav1.NutanixResourceIdentifier) (*prismclientv3.ImageIntentResponse, error) {
310+
func GetImage(ctx context.Context, client *prismclientv3.Client, id *infrav1.NutanixResourceIdentifier) (*prismclientv3.ImageIntentResponse, error) {
307311
switch {
308312
case id.IsUUID():
309313
resp, err := client.V3.GetImage(ctx, *id.UUID)
@@ -338,6 +342,70 @@ func GetImage(ctx context.Context, client *prismclientv3.Client, id infrav1.Nuta
338342
}
339343
}
340344

345+
type ImageLookup struct {
346+
BaseOS string
347+
K8sVersion string
348+
}
349+
350+
func GetImageByLookup(
351+
ctx context.Context,
352+
client *prismclientv3.Client,
353+
imageTemplate,
354+
imageLookupBaseOS,
355+
k8sVersion *string,
356+
) (*prismclientv3.ImageIntentResponse, error) {
357+
if strings.Contains(*k8sVersion, "v") {
358+
k8sVersion = ptr.To(strings.Replace(*k8sVersion, "v", "", 1))
359+
}
360+
params := ImageLookup{*imageLookupBaseOS, *k8sVersion}
361+
t, err := template.New("k8sTemplate").Parse(*imageTemplate)
362+
if err != nil {
363+
return nil, fmt.Errorf("failed to parse template given %s %v", *imageTemplate, err)
364+
}
365+
var templateBytes bytes.Buffer
366+
err = t.Execute(&templateBytes, params)
367+
if err != nil {
368+
return nil, fmt.Errorf(
369+
"failed to substitute string %s with params %v error: %w",
370+
*imageTemplate,
371+
params,
372+
err,
373+
)
374+
}
375+
responseImages, err := client.V3.ListAllImage(ctx, "")
376+
if err != nil {
377+
return nil, err
378+
}
379+
re := regexp.MustCompile(templateBytes.String())
380+
foundImages := make([]*prismclientv3.ImageIntentResponse, 0)
381+
for _, s := range responseImages.Entities {
382+
imageSpec := s.Spec
383+
if re.Match([]byte(*imageSpec.Name)) {
384+
foundImages = append(foundImages, s)
385+
}
386+
}
387+
sorted := sortImagesByLatestCreationTime(foundImages)
388+
if len(sorted) == 0 {
389+
return nil, fmt.Errorf("failed to find image with filter %s", templateBytes.String())
390+
}
391+
return sorted[0], nil
392+
}
393+
394+
// returns the images with the latest creation time first.
395+
func sortImagesByLatestCreationTime(
396+
images []*prismclientv3.ImageIntentResponse,
397+
) []*prismclientv3.ImageIntentResponse {
398+
sort.Slice(images, func(i, j int) bool {
399+
if images[i].Metadata.CreationTime == nil || images[j].Metadata.CreationTime == nil {
400+
return images[i].Metadata.CreationTime != nil
401+
}
402+
timeI := *images[i].Metadata.CreationTime
403+
timeJ := *images[j].Metadata.CreationTime
404+
return timeI.After(timeJ)
405+
})
406+
return images
407+
}
408+
341409
func ImageMarkedForDeletion(image *prismclientv3.ImageIntentResponse) bool {
342410
state := *image.Status.State
343411
return state == ImageStateDeletePending || state == ImageStateDeleteInProgress

0 commit comments

Comments
 (0)