Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
37 changes: 34 additions & 3 deletions api/v1alpha1/proxmoxmachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,16 +242,38 @@ type VirtualMachineCloneSpec struct {
Target *string `json:"target,omitempty"`
}

// TemplateSelector defines MatchTags for looking up VM templates.
// TemplateResolutionPolicy defines how MatchTags are evaluated against template tags.
type TemplateResolutionPolicy string

const (
// TemplateResolutionPolicyExact requires an exact 1:1 match between MatchTags and the template's tags.
TemplateResolutionPolicyExact TemplateResolutionPolicy = "exact"
// TemplateResolutionPolicySubset requires the template's tags to contain all MatchTags, but allows additional tags.
TemplateResolutionPolicySubset TemplateResolutionPolicy = "subset"
)

// TemplateSelector defines criteria for looking up VM templates.
type TemplateSelector struct {
// Specifies all tags to look for, when looking up the VM template.
// Passed tags must be an exact 1:1 match with the tags on the template you want to use.
// If multiple VM templates with the same set of tags are found, provisioning will fail.
// When ResolutionPolicy is "exact" (the default), the template's tags must be an exact 1:1 match
// with MatchTags. If multiple VM templates with the same set of tags are found, provisioning will fail.
// When ResolutionPolicy is "subset", the template's tags must contain all of the MatchTags, but may
// have additional tags.
//
// +listType=set
// +kubebuilder:validation:items:Pattern=`^(?i)[a-z0-9_][a-z0-9_\-\+\.]*$`
// +kubebuilder:validation:MinItems=1
MatchTags []string `json:"matchTags"`

// ResolutionPolicy controls how MatchTags are evaluated against template tags.
// When not set, or set to "exact", the behaviour is identical to the previous implementation
// and requires an exact 1:1 tag match. When set to "subset", the template's tags must contain
// all MatchTags, but may include additional tags.
//
// +kubebuilder:validation:Enum=exact;subset
// +kubebuilder:default=exact
// +optional
ResolutionPolicy TemplateResolutionPolicy `json:"resolutionPolicy,omitempty"`
}

// NetworkSpec defines the virtual machine's network configuration.
Expand Down Expand Up @@ -626,6 +648,15 @@ func (r *ProxmoxMachine) GetTemplateSelectorTags() []string {
return nil
}

// GetTemplateResolutionPolicy returns the resolution policy for selecting VM templates.
// If no TemplateSelector or ResolutionPolicy is set, TemplateResolutionPolicyExact is returned.
func (r *ProxmoxMachine) GetTemplateResolutionPolicy() TemplateResolutionPolicy {
if r.Spec.TemplateSelector != nil && r.Spec.TemplateSelector.ResolutionPolicy != "" {
return r.Spec.TemplateSelector.ResolutionPolicy
}
return TemplateResolutionPolicyExact
}

// GetNode get the Proxmox node used to provision this machine.
func (r *ProxmoxMachine) GetNode() string {
return r.Spec.SourceNode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -648,14 +648,27 @@ spec:
matchTags:
description: |-
Specifies all tags to look for, when looking up the VM template.
Passed tags must be an exact 1:1 match with the tags on the template you want to use.
If multiple VM templates with the same set of tags are found, provisioning will fail.
When ResolutionPolicy is "exact" (the default), the template's tags must be an exact 1:1 match
with MatchTags. If multiple VM templates with the same set of tags are found, provisioning will fail.
When ResolutionPolicy is "subset", the template's tags must contain all of the MatchTags, but may
have additional tags.
items:
pattern: ^(?i)[a-z0-9_][a-z0-9_\-\+\.]*$
type: string
minItems: 1
type: array
x-kubernetes-list-type: set
resolutionPolicy:
default: exact
description: |-
ResolutionPolicy controls how MatchTags are evaluated against template tags.
When not set, or set to "exact", the behaviour is identical to the previous implementation
and requires an exact 1:1 tag match. When set to "subset", the template's tags must contain
all MatchTags, but may include additional tags.
enum:
- exact
- subset
type: string
required:
- matchTags
type: object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -688,14 +688,27 @@ spec:
matchTags:
description: |-
Specifies all tags to look for, when looking up the VM template.
Passed tags must be an exact 1:1 match with the tags on the template you want to use.
If multiple VM templates with the same set of tags are found, provisioning will fail.
When ResolutionPolicy is "exact" (the default), the template's tags must be an exact 1:1 match
with MatchTags. If multiple VM templates with the same set of tags are found, provisioning will fail.
When ResolutionPolicy is "subset", the template's tags must contain all of the MatchTags, but may
have additional tags.
items:
pattern: ^(?i)[a-z0-9_][a-z0-9_\-\+\.]*$
type: string
minItems: 1
type: array
x-kubernetes-list-type: set
resolutionPolicy:
default: exact
description: |-
ResolutionPolicy controls how MatchTags are evaluated against template tags.
When not set, or set to "exact", the behaviour is identical to the previous implementation
and requires an exact 1:1 tag match. When set to "subset", the template's tags must contain
all MatchTags, but may include additional tags.
enum:
- exact
- subset
type: string
required:
- matchTags
type: object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -607,14 +607,27 @@ spec:
matchTags:
description: |-
Specifies all tags to look for, when looking up the VM template.
Passed tags must be an exact 1:1 match with the tags on the template you want to use.
If multiple VM templates with the same set of tags are found, provisioning will fail.
When ResolutionPolicy is "exact" (the default), the template's tags must be an exact 1:1 match
with MatchTags. If multiple VM templates with the same set of tags are found, provisioning will fail.
When ResolutionPolicy is "subset", the template's tags must contain all of the MatchTags, but may
have additional tags.
items:
pattern: ^(?i)[a-z0-9_][a-z0-9_\-\+\.]*$
type: string
minItems: 1
type: array
x-kubernetes-list-type: set
resolutionPolicy:
default: exact
description: |-
ResolutionPolicy controls how MatchTags are evaluated against template tags.
When not set, or set to "exact", the behaviour is identical to the previous implementation
and requires an exact 1:1 tag match. When set to "subset", the template's tags must contain
all MatchTags, but may include additional tags.
enum:
- exact
- subset
type: string
required:
- matchTags
type: object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -648,14 +648,27 @@ spec:
matchTags:
description: |-
Specifies all tags to look for, when looking up the VM template.
Passed tags must be an exact 1:1 match with the tags on the template you want to use.
If multiple VM templates with the same set of tags are found, provisioning will fail.
When ResolutionPolicy is "exact" (the default), the template's tags must be an exact 1:1 match
with MatchTags. If multiple VM templates with the same set of tags are found, provisioning will fail.
When ResolutionPolicy is "subset", the template's tags must contain all of the MatchTags, but may
have additional tags.
items:
pattern: ^(?i)[a-z0-9_][a-z0-9_\-\+\.]*$
type: string
minItems: 1
type: array
x-kubernetes-list-type: set
resolutionPolicy:
default: exact
description: |-
ResolutionPolicy controls how MatchTags are evaluated against template tags.
When not set, or set to "exact", the behaviour is identical to the previous implementation
and requires an exact 1:1 tag match. When set to "subset", the template's tags must contain
all MatchTags, but may include additional tags.
enum:
- exact
- subset
type: string
required:
- matchTags
type: object
Expand Down
24 changes: 22 additions & 2 deletions docs/advanced-setups.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,32 @@ For example, setting it to `0` (zero), entirely disables scheduling based on mem

## Template lookup based on Proxmox tags

Our provider is able to look up templates based on their attached tags, for `ProxmoxMachine` resources, that make use of an tag selector.
Our provider is able to look up templates based on their attached tags, for `ProxmoxMachine` resources, that make use of a tag selector.

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.

Please note: Passed tags must be an exact 1:1 match with the tags on the template you want to use. The matched result must be unique. If multiple templates are found, provisioning will fail.
Template selection is controlled by the `TemplateSelector` on the `ProxmoxMachine`:

- `matchTags`: the list of tags that should be used when searching for a VM template.
- `resolutionPolicy`: controls how `matchTags` are evaluated against the tags on a template. It supports two values:
- `exact` (default): the template's tags must be an exact 1:1 match with `matchTags` (after normalisation). This preserves the behaviour from earlier releases.
- `subset`: the template's tags must contain all of the `matchTags`, but may include additional tags.

The lookup must always result in a unique template. If no template or more than one template matches the configured tags under the chosen `resolutionPolicy`, provisioning will fail.

### Using TemplateSelector with ClusterClass

When using the provided ClusterClass (for example `cluster-class-cilium.yaml`), you can drive tag-based template selection via the `templateSelector` topology variable on the `Cluster`:

- If `templateSelector` is **not** set, the ClusterClass uses the explicit `sourceNode` / `templateID` fields on the `ProxmoxMachineTemplate` (the behaviour from earlier releases).
- If `templateSelector` **is** set, the ClusterClass injects it into all `ProxmoxMachineTemplate` resources and removes `sourceNode` / `templateID`. In this mode, templates are resolved by tags.

A `Cluster` using subset matching for tag-based template selection would set, for example:

- `spec.topology.variables.templateSelector.matchTags` to the list of tags that identify the desired templates (e.g. `capmox`, `template`, and a Kubernetes version tag), and
- `spec.topology.variables.templateSelector.resolutionPolicy` to `subset`.

With this configuration, each `ProxmoxMachineTemplate` created by the ClusterClass will use tag-based template lookup with `subset` semantics, and provisioning will only succeed if exactly one template matches the configured tags for each machine role.
## Proxmox RBAC with least privileges

For the Proxmox API user/token you create for CAPMOX, these are the minimum required permissions.
Expand Down
3 changes: 2 additions & 1 deletion internal/service/vmservice/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,8 @@ func createVM(ctx context.Context, scope *scope.MachineScope) (proxmox.VMCloneRe
if templateID == -1 {
var err error
templateSelectorTags := scope.ProxmoxMachine.GetTemplateSelectorTags()
options.Node, templateID, err = scope.InfraCluster.ProxmoxClient.FindVMTemplateByTags(ctx, templateSelectorTags)
templateResolutionPolicy := string(scope.ProxmoxMachine.GetTemplateResolutionPolicy())
options.Node, templateID, err = scope.InfraCluster.ProxmoxClient.FindVMTemplateByTags(ctx, templateSelectorTags, templateResolutionPolicy)

if err != nil {
if errors.Is(err, goproxmox.ErrTemplateNotFound) {
Expand Down
8 changes: 6 additions & 2 deletions internal/service/vmservice/vm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ func TestEnsureVirtualMachine_CreateVM_FullOptions_TemplateSelector(t *testing.T
Target: "node2",
}

proxmoxClient.EXPECT().FindVMTemplateByTags(context.Background(), vmTemplateTags).Return("node1", 123, nil).Once()
// ResolutionPolicy is not set on the TemplateSelector in this test, so the default
// policy is "exact". The vmservice must therefore pass "exact" to FindVMTemplateByTags.
proxmoxClient.EXPECT().FindVMTemplateByTags(context.Background(), vmTemplateTags, "exact").Return("node1", 123, nil).Once()

response := proxmox.VMCloneResponse{NewID: 123, Task: newTask()}
proxmoxClient.EXPECT().CloneVM(context.Background(), 123, expectedOptions).Return(response, nil).Once()
Expand Down Expand Up @@ -209,7 +211,9 @@ func TestEnsureVirtualMachine_CreateVM_FullOptions_TemplateSelector_VMTemplateNo
machineScope.ProxmoxMachine.Spec.Storage = ptr.To("storage")
machineScope.ProxmoxMachine.Spec.Target = ptr.To("node2")

proxmoxClient.EXPECT().FindVMTemplateByTags(context.Background(), vmTemplateTags).Return("", -1, goproxmox.ErrTemplateNotFound).Once()
// As above, no ResolutionPolicy is set on the TemplateSelector, so "exact" is the default
// and must be passed to FindVMTemplateByTags. In this scenario the template lookup fails.
proxmoxClient.EXPECT().FindVMTemplateByTags(context.Background(), vmTemplateTags, "exact").Return("", -1, goproxmox.ErrTemplateNotFound).Once()

_, err := createVM(ctx, machineScope)

Expand Down
2 changes: 1 addition & 1 deletion pkg/proxmox/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type Client interface {
ConfigureVM(ctx context.Context, vm *proxmox.VirtualMachine, options ...VirtualMachineOption) (*proxmox.Task, error)

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

CheckID(ctx context.Context, vmID int64) (bool, error)

Expand Down
34 changes: 32 additions & 2 deletions pkg/proxmox/goproxmox/api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func (c *APIClient) FindVMResource(ctx context.Context, vmID uint64) (*proxmox.C
}

// FindVMTemplateByTags tries to find a VMID by its tags across the whole cluster.
func (c *APIClient) FindVMTemplateByTags(ctx context.Context, templateTags []string) (string, int32, error) {
func (c *APIClient) FindVMTemplateByTags(ctx context.Context, templateTags []string, resolutionPolicy string) (string, int32, error) {
vmTemplates := make([]*proxmox.ClusterResource, 0)

sortedTags := make([]string, len(templateTags))
Expand Down Expand Up @@ -173,9 +173,39 @@ func (c *APIClient) FindVMTemplateByTags(ctx context.Context, templateTags []str
}

vmTags := strings.Split(vm.Tags, ";")
for i := range vmTags {
vmTags[i] = strings.ToLower(strings.TrimSpace(vmTags[i]))
}
slices.Sort(vmTags)
vmTags = slices.Compact(vmTags)

// Check whether the template’s tags (tagsPresent) contain all the requested tags (tagsWanted)
isSuperset := func(tagsPresent, tagsWanted []string) bool {
presentIdx, wantedIdx := 0, 0
for presentIdx < len(tagsPresent) && wantedIdx < len(tagsWanted) {
switch {
case tagsPresent[presentIdx] == tagsWanted[wantedIdx]:
presentIdx++
wantedIdx++
case tagsPresent[presentIdx] < tagsWanted[wantedIdx]:
presentIdx++
default:
return false
}
}
return wantedIdx == len(tagsWanted)
}

matches := false
if resolutionPolicy == "subset" {
matches = isSuperset(vmTags, uniqueTags)
} else {
if len(vmTags) == len(uniqueTags) && slices.Equal(vmTags, uniqueTags) {
matches = true
}
}

if slices.Equal(vmTags, uniqueTags) {
if matches {
vmTemplates = append(vmTemplates, vm)
}
}
Expand Down
Loading