diff --git a/api/v1alpha1/proxmoxmachine_types.go b/api/v1alpha1/proxmoxmachine_types.go index 535395fe..6583cc09 100644 --- a/api/v1alpha1/proxmoxmachine_types.go +++ b/api/v1alpha1/proxmoxmachine_types.go @@ -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. @@ -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 diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclusters.yaml index 35973a96..d8037b46 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclusters.yaml @@ -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 diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclustertemplates.yaml index 7efef5ec..27da5c7e 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxclustertemplates.yaml @@ -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 diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachines.yaml index 58861b3e..a19ef7e7 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachines.yaml @@ -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 diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachinetemplates.yaml index 12e1503e..000cedeb 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachinetemplates.yaml @@ -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 diff --git a/docs/advanced-setups.md b/docs/advanced-setups.md index d2d478ae..09095354 100644 --- a/docs/advanced-setups.md +++ b/docs/advanced-setups.md @@ -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. diff --git a/internal/service/vmservice/vm.go b/internal/service/vmservice/vm.go index 5b2ef743..3fc7c8b6 100644 --- a/internal/service/vmservice/vm.go +++ b/internal/service/vmservice/vm.go @@ -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) { diff --git a/internal/service/vmservice/vm_test.go b/internal/service/vmservice/vm_test.go index cbec7958..7490a472 100644 --- a/internal/service/vmservice/vm_test.go +++ b/internal/service/vmservice/vm_test.go @@ -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() @@ -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) diff --git a/pkg/proxmox/client.go b/pkg/proxmox/client.go index 246bd6b6..64f20570 100644 --- a/pkg/proxmox/client.go +++ b/pkg/proxmox/client.go @@ -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) diff --git a/pkg/proxmox/goproxmox/api_client.go b/pkg/proxmox/goproxmox/api_client.go index fa38f960..aefcb5ea 100644 --- a/pkg/proxmox/goproxmox/api_client.go +++ b/pkg/proxmox/goproxmox/api_client.go @@ -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)) @@ -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) } } diff --git a/pkg/proxmox/goproxmox/api_client_test.go b/pkg/proxmox/goproxmox/api_client_test.go index 2d7553b7..de814ee5 100644 --- a/pkg/proxmox/goproxmox/api_client_test.go +++ b/pkg/proxmox/goproxmox/api_client_test.go @@ -382,89 +382,99 @@ func TestProxmoxAPIClient_FindVMTemplateByTags(t *testing.T) { &proxmox.ClusterResource{VMID: 302, Name: "ubuntu-22.04-k8s-v1.29.2", Node: "capmox02", Tags: "capmox;template;v1.29.2", Template: uint64(1)}, } tests := []struct { - name string - http []int - vmTags []string - fails bool - err string - vmTemplateNode string - vmTemplateID int32 + name string + http []int + vmTags []string + resolutionPolicy string + fails bool + err string + vmTemplateNode string + vmTemplateID int32 }{ { - name: "clusterstatus broken", - http: []int{500, 200}, - fails: true, - err: "cannot get cluster status: 500", + name: "clusterstatus broken", + http: []int{500, 200}, + resolutionPolicy: "exact", + fails: true, + err: "cannot get cluster status: 500", }, { - name: "resourcelisting broken", - http: []int{200, 500}, - fails: true, - err: "could not list vm resources: 500", + name: "resourcelisting broken", + http: []int{200, 500}, + resolutionPolicy: "exact", + fails: true, + err: "could not list vm resources: 500", }, { - name: "find-template", - http: []int{200, 200}, - vmTags: []string{"template", "capmox", "v1.28.3"}, - fails: false, - err: "", - vmTemplateNode: "capmox01", - vmTemplateID: 201, + name: "find-template", + http: []int{200, 200}, + vmTags: []string{"template", "capmox", "v1.28.3"}, + resolutionPolicy: "exact", + fails: false, + err: "", + vmTemplateNode: "capmox01", + vmTemplateID: 201, }, { - name: "find-template-nil", - http: []int{200, 200}, - vmTags: nil, - fails: true, - err: "VM template not found: found 0 VM templates with tags \"\"", - vmTemplateNode: "capmox01", - vmTemplateID: 201, + name: "find-template-nil", + http: []int{200, 200}, + vmTags: nil, + resolutionPolicy: "subset", + fails: true, + err: "VM template not found: found 4 VM templates with tags \"\"", + vmTemplateNode: "capmox01", + vmTemplateID: 201, }, { // Proxmox VM tags are always lowercase - name: "find-template-uppercase", - http: []int{200, 200}, - vmTags: []string{"TEMPLATE", "CAPMOX", "v1.28.3"}, - fails: false, - err: "", - vmTemplateNode: "capmox01", - vmTemplateID: 201, + name: "find-template-uppercase", + http: []int{200, 200}, + vmTags: []string{"TEMPLATE", "CAPMOX", "v1.28.3"}, + resolutionPolicy: "exact", + fails: false, + err: "", + vmTemplateNode: "capmox01", + vmTemplateID: 201, }, { - name: "find-template-unordered", - http: []int{200, 200}, - vmTags: []string{"template", "capmox", "v1.30.2"}, - fails: false, - err: "", - vmTemplateNode: "capmox02", - vmTemplateID: 202, + name: "find-template-unordered", + http: []int{200, 200}, + vmTags: []string{"template", "capmox", "v1.30.2"}, + resolutionPolicy: "exact", + fails: false, + err: "", + vmTemplateNode: "capmox02", + vmTemplateID: 202, }, { - name: "find-template-duplicate-tag", - http: []int{200, 200}, - vmTags: []string{"template", "capmox", "capmox", "v1.30.2"}, - fails: false, - err: "", - vmTemplateNode: "capmox02", - vmTemplateID: 202, + name: "find-template-duplicate-tag", + http: []int{200, 200}, + vmTags: []string{"template", "capmox", "capmox", "v1.30.2"}, + resolutionPolicy: "exact", + fails: false, + err: "", + vmTemplateNode: "capmox02", + vmTemplateID: 202, }, { - name: "find-multiple-templates", - http: []int{200, 200}, - vmTags: []string{"template", "capmox"}, - fails: true, - err: "VM template not found: found 0 VM templates with tags \"template;capmox\"", - vmTemplateID: 69, - vmTemplateNode: "nice", + name: "find-multiple-templates-any-version", + http: []int{200, 200}, + vmTags: []string{"template", "capmox"}, + resolutionPolicy: "subset", + fails: true, + err: "VM template not found: found 4 VM templates with tags \"template;capmox\"", + vmTemplateID: 69, + vmTemplateNode: "nice", }, { - name: "find-multiple-templates", - http: []int{200, 200}, - vmTags: []string{"template", "capmox", "v1.29.2"}, - fails: true, - err: "VM template not found: found 2 VM templates with tags \"template;capmox;v1.29.2\"", - vmTemplateID: 69, - vmTemplateNode: "nice", + name: "find-multiple-templates-v1.29.2", + http: []int{200, 200}, + vmTags: []string{"template", "capmox", "v1.29.2"}, + resolutionPolicy: "exact", + fails: true, + err: "VM template not found: found 2 VM templates with tags \"template;capmox;v1.29.2\"", + vmTemplateID: 69, + vmTemplateNode: "nice", }, } @@ -477,7 +487,7 @@ func TestProxmoxAPIClient_FindVMTemplateByTags(t *testing.T) { httpmock.RegisterResponder(http.MethodGet, `=~/cluster/resources`, newJSONResponder(test.http[1], proxmoxClusterResources)) - vmTemplateNode, vmTemplateID, err := client.FindVMTemplateByTags(context.Background(), test.vmTags) + vmTemplateNode, vmTemplateID, err := client.FindVMTemplateByTags(context.Background(), test.vmTags, test.resolutionPolicy) if test.fails { require.Error(t, err) diff --git a/pkg/proxmox/proxmoxtest/mock_client.go b/pkg/proxmox/proxmoxtest/mock_client.go index af6e6f3a..decfe76a 100644 --- a/pkg/proxmox/proxmoxtest/mock_client.go +++ b/pkg/proxmox/proxmoxtest/mock_client.go @@ -389,9 +389,9 @@ func (_c *MockClient_FindVMResource_Call) RunAndReturn(run func(context.Context, return _c } -// FindVMTemplateByTags provides a mock function with given fields: ctx, templateTags -func (_m *MockClient) FindVMTemplateByTags(ctx context.Context, templateTags []string) (string, int32, error) { - ret := _m.Called(ctx, templateTags) +// FindVMTemplateByTags provides a mock function with given fields: ctx, templateTags, resolutionPolicy +func (_m *MockClient) FindVMTemplateByTags(ctx context.Context, templateTags []string, resolutionPolicy string) (string, int32, error) { + ret := _m.Called(ctx, templateTags, resolutionPolicy) if len(ret) == 0 { panic("no return value specified for FindVMTemplateByTags") @@ -400,23 +400,23 @@ func (_m *MockClient) FindVMTemplateByTags(ctx context.Context, templateTags []s var r0 string var r1 int32 var r2 error - if rf, ok := ret.Get(0).(func(context.Context, []string) (string, int32, error)); ok { - return rf(ctx, templateTags) + if rf, ok := ret.Get(0).(func(context.Context, []string, string) (string, int32, error)); ok { + return rf(ctx, templateTags, resolutionPolicy) } - if rf, ok := ret.Get(0).(func(context.Context, []string) string); ok { - r0 = rf(ctx, templateTags) + if rf, ok := ret.Get(0).(func(context.Context, []string, string) string); ok { + r0 = rf(ctx, templateTags, resolutionPolicy) } else { r0 = ret.Get(0).(string) } - if rf, ok := ret.Get(1).(func(context.Context, []string) int32); ok { - r1 = rf(ctx, templateTags) + if rf, ok := ret.Get(1).(func(context.Context, []string, string) int32); ok { + r1 = rf(ctx, templateTags, resolutionPolicy) } else { r1 = ret.Get(1).(int32) } - if rf, ok := ret.Get(2).(func(context.Context, []string) error); ok { - r2 = rf(ctx, templateTags) + if rf, ok := ret.Get(2).(func(context.Context, []string, string) error); ok { + r2 = rf(ctx, templateTags, resolutionPolicy) } else { r2 = ret.Error(2) } @@ -432,13 +432,14 @@ type MockClient_FindVMTemplateByTags_Call struct { // FindVMTemplateByTags is a helper method to define mock.On call // - ctx context.Context // - templateTags []string -func (_e *MockClient_Expecter) FindVMTemplateByTags(ctx interface{}, templateTags interface{}) *MockClient_FindVMTemplateByTags_Call { - return &MockClient_FindVMTemplateByTags_Call{Call: _e.mock.On("FindVMTemplateByTags", ctx, templateTags)} +// - resolutionPolicy string +func (_e *MockClient_Expecter) FindVMTemplateByTags(ctx interface{}, templateTags interface{}, resolutionPolicy interface{}) *MockClient_FindVMTemplateByTags_Call { + return &MockClient_FindVMTemplateByTags_Call{Call: _e.mock.On("FindVMTemplateByTags", ctx, templateTags, resolutionPolicy)} } -func (_c *MockClient_FindVMTemplateByTags_Call) Run(run func(ctx context.Context, templateTags []string)) *MockClient_FindVMTemplateByTags_Call { +func (_c *MockClient_FindVMTemplateByTags_Call) Run(run func(ctx context.Context, templateTags []string, resolutionPolicy string)) *MockClient_FindVMTemplateByTags_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].([]string)) + run(args[0].(context.Context), args[1].([]string), args[2].(string)) }) return _c } @@ -448,7 +449,7 @@ func (_c *MockClient_FindVMTemplateByTags_Call) Return(_a0 string, _a1 int32, _a return _c } -func (_c *MockClient_FindVMTemplateByTags_Call) RunAndReturn(run func(context.Context, []string) (string, int32, error)) *MockClient_FindVMTemplateByTags_Call { +func (_c *MockClient_FindVMTemplateByTags_Call) RunAndReturn(run func(context.Context, []string, string) (string, int32, error)) *MockClient_FindVMTemplateByTags_Call { _c.Call.Return(run) return _c } diff --git a/templates/cluster-class-calico.yaml b/templates/cluster-class-calico.yaml index 83e80a70..19626978 100644 --- a/templates/cluster-class-calico.yaml +++ b/templates/cluster-class-calico.yaml @@ -377,6 +377,23 @@ spec: example: 100 workerNode: *machineSpec loadBalancer: *machineSpec + - name: templateSelector + required: false + schema: + openAPIV3Schema: + description: When set, we use tag-based template selection (and drop sourceNode/templateID). + type: object + properties: + matchTags: + type: array + items: + type: string + minItems: 1 + resolutionPolicy: + type: string + enum: ["exact","subset"] + required: + - matchTags patches: - name: ProxmoxClusterTemplateGeneral description: "Configure Cluster" @@ -512,20 +529,6 @@ spec: path: /etc/kubernetes/admin.conf type: FileOrCreate name: kubeconfig - - selector: - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 - kind: ProxmoxMachineTemplate - matchResources: - controlPlane: true - jsonPatches: - - op: replace - path: /spec/template/spec/sourceNode - valueFrom: - variable: cloneSpec.machineSpec.controlPlane.sourceNode - - op: replace - path: /spec/template/spec/templateID - valueFrom: - variable: cloneSpec.machineSpec.controlPlane.templateID - name: kube-proxy-setup description: "kube-proxy configuration" enabledIf: "{{ if eq .kubeProxy.mode \"ipvs\" }}true{{ end }}" @@ -573,6 +576,26 @@ spec: template: | - name: root sshAuthorizedKeys: {{ .cloneSpec.sshAuthorizedKeys }} + - name: TemplateSelectorMode-ControlPlane + enabledIf: '{{ if .templateSelector }}true{{ end }}' + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ProxmoxMachineTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: /spec/template/spec/templateSelector + valueFrom: + variable: templateSelector + - op: remove + path: /spec/template/spec/sourceNode + - op: remove + path: /spec/template/spec/templateID + - name: TemplateSelectorMode-Worker + enabledIf: '{{ if .templateSelector }}true{{ end }}' + definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 kind: ProxmoxMachineTemplate @@ -582,11 +605,68 @@ spec: names: - proxmox-worker jsonPatches: - - op: replace + - op: add + path: /spec/template/spec/templateSelector + valueFrom: + variable: templateSelector + - op: remove + path: /spec/template/spec/sourceNode + - op: remove + path: /spec/template/spec/templateID + - name: TemplateSelectorMode-LoadBalancer + enabledIf: '{{ if .templateSelector }}true{{ end }}' + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ProxmoxMachineTemplate + matchResources: + machineDeploymentClass: + names: + - proxmox-loadbalancer + jsonPatches: + - op: add + path: /spec/template/spec/templateSelector + valueFrom: + variable: templateSelector + - op: remove + path: /spec/template/spec/sourceNode + - op: remove + path: /spec/template/spec/templateID + + - name: ExplicitMode-ControlPlane + enabledIf: '{{ if not .templateSelector }}true{{ end }}' + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ProxmoxMachineTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: /spec/template/spec/sourceNode + valueFrom: + variable: cloneSpec.machineSpec.controlPlane.sourceNode + - op: add + path: /spec/template/spec/templateID + valueFrom: + variable: cloneSpec.machineSpec.controlPlane.templateID + - name: ExplicitMode-Worker + enabledIf: '{{ if not .templateSelector }}true{{ end }}' + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ProxmoxMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - proxmox-worker + jsonPatches: + - op: add path: /spec/template/spec/sourceNode valueFrom: variable: cloneSpec.machineSpec.workerNode.sourceNode - - op: replace + - op: add path: /spec/template/spec/templateID valueFrom: variable: cloneSpec.machineSpec.workerNode.templateID @@ -607,6 +687,29 @@ spec: template: | - name: root sshAuthorizedKeys: {{ .cloneSpec.sshAuthorizedKeys }} + - name: ExplicitMode-LoadBalancer + enabledIf: '{{ if not .templateSelector }}true{{ end }}' + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ProxmoxMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - proxmox-loadbalancer + jsonPatches: + - op: add + path: /spec/template/spec/sourceNode + valueFrom: + variable: cloneSpec.machineSpec.loadBalancer.sourceNode + - op: add + path: /spec/template/spec/templateID + valueFrom: + variable: cloneSpec.machineSpec.loadBalancer.templateID + - name: ControlPlaneNodeSockets + enabledIf: '{{ if not .templateSelector }}true{{ end }}' + definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 kind: ProxmoxMachineTemplate diff --git a/templates/cluster-class-cilium.yaml b/templates/cluster-class-cilium.yaml index 7b696ae7..0e961841 100644 --- a/templates/cluster-class-cilium.yaml +++ b/templates/cluster-class-cilium.yaml @@ -377,6 +377,23 @@ spec: example: 100 workerNode: *machineSpec loadBalancer: *machineSpec + - name: templateSelector + required: false + schema: + openAPIV3Schema: + description: When set, we use tag-based template selection (and drop sourceNode/templateID). + type: object + properties: + matchTags: + type: array + items: + type: string + minItems: 1 + resolutionPolicy: + type: string + enum: ["exact","subset"] + required: + - matchTags patches: - name: ProxmoxClusterTemplateGeneral description: "Configure Cluster" @@ -512,20 +529,6 @@ spec: path: /etc/kubernetes/admin.conf type: FileOrCreate name: kubeconfig - - selector: - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 - kind: ProxmoxMachineTemplate - matchResources: - controlPlane: true - jsonPatches: - - op: replace - path: /spec/template/spec/sourceNode - valueFrom: - variable: cloneSpec.machineSpec.controlPlane.sourceNode - - op: replace - path: /spec/template/spec/templateID - valueFrom: - variable: cloneSpec.machineSpec.controlPlane.templateID - name: kube-proxy-setup description: "kube-proxy configuration" enabledIf: "{{ if eq .kubeProxy.mode \"ipvs\" }}true{{ end }}" @@ -573,6 +576,26 @@ spec: template: | - name: root sshAuthorizedKeys: {{ .cloneSpec.sshAuthorizedKeys }} + - name: TemplateSelectorMode-ControlPlane + enabledIf: '{{ if .templateSelector }}true{{ end }}' + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ProxmoxMachineTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: /spec/template/spec/templateSelector + valueFrom: + variable: templateSelector + - op: remove + path: /spec/template/spec/sourceNode + - op: remove + path: /spec/template/spec/templateID + - name: TemplateSelectorMode-Worker + enabledIf: '{{ if .templateSelector }}true{{ end }}' + definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 kind: ProxmoxMachineTemplate @@ -582,11 +605,68 @@ spec: names: - proxmox-worker jsonPatches: - - op: replace + - op: add + path: /spec/template/spec/templateSelector + valueFrom: + variable: templateSelector + - op: remove + path: /spec/template/spec/sourceNode + - op: remove + path: /spec/template/spec/templateID + - name: TemplateSelectorMode-LoadBalancer + enabledIf: '{{ if .templateSelector }}true{{ end }}' + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ProxmoxMachineTemplate + matchResources: + machineDeploymentClass: + names: + - proxmox-loadbalancer + jsonPatches: + - op: add + path: /spec/template/spec/templateSelector + valueFrom: + variable: templateSelector + - op: remove + path: /spec/template/spec/sourceNode + - op: remove + path: /spec/template/spec/templateID + + - name: ExplicitMode-ControlPlane + enabledIf: '{{ if not .templateSelector }}true{{ end }}' + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ProxmoxMachineTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: /spec/template/spec/sourceNode + valueFrom: + variable: cloneSpec.machineSpec.controlPlane.sourceNode + - op: add + path: /spec/template/spec/templateID + valueFrom: + variable: cloneSpec.machineSpec.controlPlane.templateID + - name: ExplicitMode-Worker + enabledIf: '{{ if not .templateSelector }}true{{ end }}' + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ProxmoxMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - proxmox-worker + jsonPatches: + - op: add path: /spec/template/spec/sourceNode valueFrom: variable: cloneSpec.machineSpec.workerNode.sourceNode - - op: replace + - op: add path: /spec/template/spec/templateID valueFrom: variable: cloneSpec.machineSpec.workerNode.templateID @@ -599,7 +679,7 @@ spec: matchResources: machineDeploymentClass: names: - - proxmox-loadbalancer + - proxmox-loadbalancer jsonPatches: - op: add path: /spec/template/spec/users @@ -607,6 +687,29 @@ spec: template: | - name: root sshAuthorizedKeys: {{ .cloneSpec.sshAuthorizedKeys }} + - name: ExplicitMode-LoadBalancer + enabledIf: '{{ if not .templateSelector }}true{{ end }}' + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ProxmoxMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - proxmox-loadbalancer + jsonPatches: + - op: add + path: /spec/template/spec/sourceNode + valueFrom: + variable: cloneSpec.machineSpec.loadBalancer.sourceNode + - op: add + path: /spec/template/spec/templateID + valueFrom: + variable: cloneSpec.machineSpec.loadBalancer.templateID + - name: ControlPlaneNodeSockets + enabledIf: '{{ if not .templateSelector }}true{{ end }}' + definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 kind: ProxmoxMachineTemplate diff --git a/templates/cluster-class.yaml b/templates/cluster-class.yaml index cecdd33d..ae278201 100644 --- a/templates/cluster-class.yaml +++ b/templates/cluster-class.yaml @@ -347,6 +347,23 @@ spec: example: 100 workerNode: *machineSpec loadBalancer: *machineSpec + - name: templateSelector + required: false + schema: + openAPIV3Schema: + description: When set, we use tag-based template selection (and drop sourceNode/templateID). + type: object + properties: + matchTags: + type: array + items: + type: string + minItems: 1 + resolutionPolicy: + type: string + enum: ["exact","subset"] + required: + - matchTags patches: - name: ProxmoxClusterTemplateGeneral description: "Configure Cluster" @@ -482,20 +499,6 @@ spec: path: /etc/kubernetes/admin.conf type: FileOrCreate name: kubeconfig - - selector: - apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 - kind: ProxmoxMachineTemplate - matchResources: - controlPlane: true - jsonPatches: - - op: replace - path: /spec/template/spec/sourceNode - valueFrom: - variable: cloneSpec.machineSpec.controlPlane.sourceNode - - op: replace - path: /spec/template/spec/templateID - valueFrom: - variable: cloneSpec.machineSpec.controlPlane.templateID - name: kube-proxy-setup description: "kube-proxy configuration" enabledIf: "{{ if eq .kubeProxy.mode \"ipvs\" }}true{{ end }}" @@ -543,6 +546,26 @@ spec: template: | - name: root sshAuthorizedKeys: {{ .cloneSpec.sshAuthorizedKeys }} + - name: TemplateSelectorMode-ControlPlane + enabledIf: '{{ if .templateSelector }}true{{ end }}' + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ProxmoxMachineTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: /spec/template/spec/templateSelector + valueFrom: + variable: templateSelector + - op: remove + path: /spec/template/spec/sourceNode + - op: remove + path: /spec/template/spec/templateID + - name: TemplateSelectorMode-Worker + enabledIf: '{{ if .templateSelector }}true{{ end }}' + definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 kind: ProxmoxMachineTemplate @@ -552,11 +575,68 @@ spec: names: - proxmox-worker jsonPatches: - - op: replace + - op: add + path: /spec/template/spec/templateSelector + valueFrom: + variable: templateSelector + - op: remove + path: /spec/template/spec/sourceNode + - op: remove + path: /spec/template/spec/templateID + - name: TemplateSelectorMode-LoadBalancer + enabledIf: '{{ if .templateSelector }}true{{ end }}' + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ProxmoxMachineTemplate + matchResources: + machineDeploymentClass: + names: + - proxmox-loadbalancer + jsonPatches: + - op: add + path: /spec/template/spec/templateSelector + valueFrom: + variable: templateSelector + - op: remove + path: /spec/template/spec/sourceNode + - op: remove + path: /spec/template/spec/templateID + + - name: ExplicitMode-ControlPlane + enabledIf: '{{ if not .templateSelector }}true{{ end }}' + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ProxmoxMachineTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: /spec/template/spec/sourceNode + valueFrom: + variable: cloneSpec.machineSpec.controlPlane.sourceNode + - op: add + path: /spec/template/spec/templateID + valueFrom: + variable: cloneSpec.machineSpec.controlPlane.templateID + - name: ExplicitMode-Worker + enabledIf: '{{ if not .templateSelector }}true{{ end }}' + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ProxmoxMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - proxmox-worker + jsonPatches: + - op: add path: /spec/template/spec/sourceNode valueFrom: variable: cloneSpec.machineSpec.workerNode.sourceNode - - op: replace + - op: add path: /spec/template/spec/templateID valueFrom: variable: cloneSpec.machineSpec.workerNode.templateID @@ -577,6 +657,29 @@ spec: template: | - name: root sshAuthorizedKeys: {{ .cloneSpec.sshAuthorizedKeys }} + - name: ExplicitMode-LoadBalancer + enabledIf: '{{ if not .templateSelector }}true{{ end }}' + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ProxmoxMachineTemplate + matchResources: + controlPlane: false + machineDeploymentClass: + names: + - proxmox-loadbalancer + jsonPatches: + - op: add + path: /spec/template/spec/sourceNode + valueFrom: + variable: cloneSpec.machineSpec.loadBalancer.sourceNode + - op: add + path: /spec/template/spec/templateID + valueFrom: + variable: cloneSpec.machineSpec.loadBalancer.templateID + - name: ControlPlaneNodeSockets + enabledIf: '{{ if not .templateSelector }}true{{ end }}' + definitions: - selector: apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 kind: ProxmoxMachineTemplate