Skip to content

Commit f0233f5

Browse files
committed
feat: allow template selection based on tags
* `sourceNode + templateID` and `templateSelector` are mutually exclusive * automatically detects both `sourceNode` + `templateID` * errors out if anything but one (1) VM template with desired flags was found
1 parent 51a9d0a commit f0233f5

16 files changed

+610
-9
lines changed

api/v1alpha1/proxmoxmachine_types.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,16 @@ type VirtualMachineCloneSpec struct {
162162
// will be cloned onto the same node as SourceNode.
163163
//
164164
// +kubebuilder:validation:MinLength=1
165+
// +optional
165166
SourceNode string `json:"sourceNode"`
166167

167168
// TemplateID the vm_template vmid used for cloning a new VM.
168169
// +optional
169170
TemplateID *int32 `json:"templateID,omitempty"`
170171

172+
// +optional
173+
TemplateSelector *TemplateSelector `json:"templateSelector,omitempty"`
174+
171175
// Description for the new VM.
172176
// +optional
173177
Description *string `json:"description,omitempty"`
@@ -202,6 +206,14 @@ type VirtualMachineCloneSpec struct {
202206
Target *string `json:"target,omitempty"`
203207
}
204208

209+
// TemplateSelector defines tags for looking up images.
210+
type TemplateSelector struct {
211+
// Specifies all tags to look for, when looking up the VM template.
212+
//
213+
// +kubebuilder:validation:MinItems=1
214+
MatchTags []string `json:"matchTags"`
215+
}
216+
205217
// NetworkSpec defines the virtual machine's network configuration.
206218
type NetworkSpec struct {
207219
// Default is the default network device,
@@ -526,9 +538,20 @@ func (r *ProxmoxMachine) GetTemplateID() int32 {
526538
return -1
527539
}
528540

541+
// GetTemplateSelectorTags get the tags, the desired vm template should have.
542+
func (r *ProxmoxMachine) GetTemplateSelectorTags() []string {
543+
if r.Spec.TemplateSelector != nil && r.Spec.TemplateSelector.MatchTags != nil {
544+
return r.Spec.TemplateSelector.MatchTags
545+
}
546+
return nil
547+
}
548+
529549
// GetNode get the Proxmox node used to provision this machine.
530550
func (r *ProxmoxMachine) GetNode() string {
531-
return r.Spec.SourceNode
551+
if r.Spec.SourceNode != "" {
552+
return r.Spec.SourceNode
553+
}
554+
return ""
532555
}
533556

534557
// FormatSize returns the format required for the Proxmox API.

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 25 additions & 0 deletions
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_proxmoxclusters.yaml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -548,13 +548,25 @@ spec:
548548
a new VM.
549549
format: int32
550550
type: integer
551+
templateSelector:
552+
description: TemplateSelector defines tags for looking up
553+
images.
554+
properties:
555+
matchTags:
556+
description: Specifies all tags to look for, when looking
557+
up the VM template.
558+
items:
559+
type: string
560+
minItems: 1
561+
type: array
562+
required:
563+
- matchTags
564+
type: object
551565
virtualMachineID:
552566
description: VirtualMachineID is the Proxmox identifier
553567
for the ProxmoxMachine VM.
554568
format: int64
555569
type: integer
556-
required:
557-
- sourceNode
558570
type: object
559571
type: object
560572
x-kubernetes-validations:

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -588,13 +588,25 @@ spec:
588588
for cloning a new VM.
589589
format: int32
590590
type: integer
591+
templateSelector:
592+
description: TemplateSelector defines tags for looking
593+
up images.
594+
properties:
595+
matchTags:
596+
description: Specifies all tags to look for,
597+
when looking up the VM template.
598+
items:
599+
type: string
600+
minItems: 1
601+
type: array
602+
required:
603+
- matchTags
604+
type: object
591605
virtualMachineID:
592606
description: VirtualMachineID is the Proxmox identifier
593607
for the ProxmoxMachine VM.
594608
format: int64
595609
type: integer
596-
required:
597-
- sourceNode
598610
type: object
599611
type: object
600612
x-kubernetes-validations:

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,13 +513,24 @@ spec:
513513
VM.
514514
format: int32
515515
type: integer
516+
templateSelector:
517+
description: TemplateSelector defines tags for looking up images.
518+
properties:
519+
matchTags:
520+
description: Specifies all tags to look for, when looking up the
521+
VM template.
522+
items:
523+
type: string
524+
minItems: 1
525+
type: array
526+
required:
527+
- matchTags
528+
type: object
516529
virtualMachineID:
517530
description: VirtualMachineID is the Proxmox identifier for the ProxmoxMachine
518531
VM.
519532
format: int64
520533
type: integer
521-
required:
522-
- sourceNode
523534
type: object
524535
x-kubernetes-validations:
525536
- message: Must set full=true when specifying format

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -546,13 +546,25 @@ spec:
546546
a new VM.
547547
format: int32
548548
type: integer
549+
templateSelector:
550+
description: TemplateSelector defines tags for looking up
551+
images.
552+
properties:
553+
matchTags:
554+
description: Specifies all tags to look for, when looking
555+
up the VM template.
556+
items:
557+
type: string
558+
minItems: 1
559+
type: array
560+
required:
561+
- matchTags
562+
type: object
549563
virtualMachineID:
550564
description: VirtualMachineID is the Proxmox identifier for
551565
the ProxmoxMachine VM.
552566
format: int64
553567
type: integer
554-
required:
555-
- sourceNode
556568
type: object
557569
required:
558570
- spec

docs/advanced-setups.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@ This behaviour can be configured in the `ProxmoxCluster` CR through the field `.
176176

177177
For example, setting it to `0` (zero), entirely disables scheduling based on memory. Alternatively, if you set it to any value greater than `0`, the scheduler will treat your host as it would have `${value}%` of memory. In real numbers that would mean, if you have a host with 64GB of memory and set the number to `300`, the scheduler would allow you to provision guests with a total of 192GB memory and therefore overprovision the host. (Use with caution! It's strongly suggested to have memory ballooning configured everywhere.). Or, if you were to set it to `95` for example, it would treat your host as it would only have 60,8GB of memory, and leave the remaining 3,2GB for the host.
178178

179+
## Template lookup based on Proxmox tags
180+
181+
Our provider is able to look up templates based on their attached tags, for `ProxmoxMachine` resources, that make use of an tag selector.
182+
183+
For example, you can set the `TEMPLATE_TAGS="tag1,tag2"` environment variable. Your custom image will then be used when using the [auto-image](https://github.com/ionos-cloud/cluster-api-provider-ionoscloud/blob/main/templates/cluster-template-auto-image.yaml) template.
184+
185+
179186
## Proxmox RBAC with least privileges
180187

181188
For the Proxmox API user/token you create for CAPMOX, these are the minimum required permissions.

envfile.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export PROXMOX_TOKEN=""
33
export PROXMOX_SECRET=""
44
export PROXMOX_SOURCENODE="pve"
55
export TEMPLATE_VMID=100
6+
export TEMPLATE_TAGS="tag1,tag2"
67
export VM_SSH_KEYS="ssh-ed25519 ..., ssh-ed25519 ..."
78
export KUBERNETES_VERSION="1.25.1"
89
export CONTROL_PLANE_ENDPOINT_IP=10.10.10.4

internal/service/vmservice/vm.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,17 @@ func createVM(ctx context.Context, scope *scope.MachineScope) (proxmox.VMCloneRe
371371
}
372372

373373
templateID := scope.ProxmoxMachine.GetTemplateID()
374+
if templateID == -1 {
375+
var err error
376+
377+
templateSelectorTags := scope.ProxmoxMachine.GetTemplateSelectorTags()
378+
options.Node, templateID, err = scope.InfraCluster.ProxmoxClient.FindVMTemplateByTags(ctx, templateSelectorTags)
379+
380+
if err != nil {
381+
scope.SetFailureMessage(err)
382+
return proxmox.VMCloneResponse{}, err
383+
}
384+
}
374385
res, err := scope.InfraCluster.ProxmoxClient.CloneVM(ctx, int(templateID), options)
375386
if err != nil {
376387
return res, err

internal/webhook/proxmoxmachine_webhook.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ func (p *ProxmoxMachine) ValidateCreate(_ context.Context, obj runtime.Object) (
5959
return warnings, err
6060
}
6161

62+
err = validateTemplate(machine)
63+
if err != nil {
64+
warnings = append(warnings, fmt.Sprintf("cannot create proxmox machine %s", machine.GetName()))
65+
return warnings, err
66+
}
67+
6268
return warnings, nil
6369
}
6470

@@ -75,6 +81,12 @@ func (p *ProxmoxMachine) ValidateUpdate(_ context.Context, _, newObj runtime.Obj
7581
return warnings, err
7682
}
7783

84+
err = validateTemplate(newMachine)
85+
if err != nil {
86+
warnings = append(warnings, fmt.Sprintf("cannot create proxmox machine %s", newMachine.GetName()))
87+
return warnings, err
88+
}
89+
7890
return warnings, nil
7991
}
8092

@@ -83,6 +95,32 @@ func (p *ProxmoxMachine) ValidateDelete(_ context.Context, _ runtime.Object) (wa
8395
return nil, nil
8496
}
8597

98+
func validateTemplate(machine *infrav1.ProxmoxMachine) error {
99+
gk, name := machine.GroupVersionKind().GroupKind(), machine.GetName()
100+
101+
if (machine.Spec.TemplateID != nil || machine.Spec.SourceNode != "") && (machine.Spec.TemplateSelector != nil) {
102+
return apierrors.NewInvalid(
103+
gk,
104+
name,
105+
field.ErrorList{
106+
field.Invalid(
107+
field.NewPath("spec"), machine.Spec, "spec.sourceNode AND spec.templateID can not be used in combination with spec.templateSelector"),
108+
})
109+
}
110+
111+
if (machine.Spec.TemplateID == nil || machine.Spec.SourceNode == "") && (machine.Spec.TemplateSelector == nil) {
112+
return apierrors.NewInvalid(
113+
gk,
114+
name,
115+
field.ErrorList{
116+
field.Invalid(
117+
field.NewPath("spec"), machine.Spec, "must define either spec.sourceNode AND spec.templateID, or spec.templateSelector"),
118+
})
119+
}
120+
121+
return nil
122+
}
123+
86124
func validateNetworks(machine *infrav1.ProxmoxMachine) error {
87125
if machine.Spec.Network == nil {
88126
return nil

internal/webhook/proxmoxmachine_webhook_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func validProxmoxMachine(name string) infrav1.ProxmoxMachine {
111111
Spec: infrav1.ProxmoxMachineSpec{
112112
VirtualMachineCloneSpec: infrav1.VirtualMachineCloneSpec{
113113
SourceNode: "pve",
114+
TemplateID: ptr.To(int32(1337)),
114115
},
115116
NumSockets: 1,
116117
NumCores: 1,

pkg/proxmox/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type Client interface {
3030
ConfigureVM(ctx context.Context, vm *proxmox.VirtualMachine, options ...VirtualMachineOption) (*proxmox.Task, error)
3131

3232
FindVMResource(ctx context.Context, vmID uint64) (*proxmox.ClusterResource, error)
33+
FindVMTemplateByTags(ctx context.Context, templateTags []string) (string, int32, error)
3334

3435
GetVM(ctx context.Context, nodeName string, vmID int64) (*proxmox.VirtualMachine, error)
3536

pkg/proxmox/goproxmox/api_client.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"context"
2222
"fmt"
2323
"net/url"
24+
"slices"
2425
"strings"
2526

2627
"github.com/go-logr/logr"
@@ -141,6 +142,51 @@ func (c *APIClient) FindVMResource(ctx context.Context, vmID uint64) (*proxmox.C
141142
return nil, fmt.Errorf("unable to find VM with ID %d on any of the nodes", vmID)
142143
}
143144

145+
// FindVMTemplateByTags tries to find a VMID by its tags across the whole cluster.
146+
func (c *APIClient) FindVMTemplateByTags(ctx context.Context, templateTags []string) (string, int32, error) {
147+
vmTemplates := make([]*proxmox.ClusterResource, 0)
148+
149+
sortedTags := make([]string, len(templateTags))
150+
for i, tag := range templateTags {
151+
// Proxmox VM tags are always lowercase
152+
sortedTags[i] = strings.ToLower(tag)
153+
}
154+
slices.Sort(sortedTags)
155+
uniqueTags := slices.Compact(sortedTags)
156+
157+
cluster, err := c.Cluster(ctx)
158+
if err != nil {
159+
return "", -1, fmt.Errorf("cannot get cluster status: %w", err)
160+
}
161+
162+
vmResources, err := cluster.Resources(ctx, "vm")
163+
if err != nil {
164+
return "", -1, fmt.Errorf("could not list vm resources: %w", err)
165+
}
166+
167+
for _, vm := range vmResources {
168+
if vm.Template == 0 {
169+
continue
170+
}
171+
if len(vm.Tags) == 0 {
172+
continue
173+
}
174+
175+
vmTags := strings.Split(vm.Tags, ";")
176+
slices.Sort(vmTags)
177+
178+
if slices.Equal(vmTags, uniqueTags) {
179+
vmTemplates = append(vmTemplates, vm)
180+
}
181+
}
182+
183+
if n := len(vmTemplates); n != 1 {
184+
return "", -1, fmt.Errorf("found %d VM templates with tags %q", n, templateTags)
185+
}
186+
187+
return vmTemplates[0].Node, int32(vmTemplates[0].VMID), nil
188+
}
189+
144190
// DeleteVM deletes a VM based on the nodeName and vmID.
145191
func (c *APIClient) DeleteVM(ctx context.Context, nodeName string, vmID int64) (*proxmox.Task, error) {
146192
// A vmID can not be lower than 100.

0 commit comments

Comments
 (0)