Repository: kubermatic/machine-controller
Pull request #1541 (2023-01-27 12:00:40)

Anexia Provider: add support for named VM templates
**What this PR does / why we need it**:
This PR adds support for named templates. Template IDs may break over time when a new build is released. With named templates the latest build can be selected without specifying the Template ID.



**What type of PR is this?**
/kind feature
/kind documentation
/kind api-change
<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind design

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
/kind chore
-->

**Does this PR introduce a user-facing change? Then add your Release Note here**:
<!--
Write your release note:
1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required".
2. If no release note is required, just write "NONE".
-->
```release-note
Anexia Provider: Optionally configure "named templates" instead of (less stable) "template identifiers".
```

**Documentation**:
<!--
Please do one of the following options:
- Add a link to the existing documentation
- Add a link to the kubermatic/docs pull request
- If no documentation change is applicable then add:
  - TBD (documentation will be added later)
  - NONE (no documentation needed for this PR)
-->
```documentation
https://github.com/kubermatic/machine-controller/blob/main/docs/anexia.md
```


kubermatic-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubermatic/machine-controller/pull/1541#" title="Author self-approved">marioreggiori</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign moadqassem for approval by writing `/assign @moadqassem` in a comment. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubermatic%2Fmachine-controller).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubermatic/machine-controller/blob/main/OWNERS)**
- **[pkg/cloudprovider/provider/OWNERS](https://github.com/kubermatic/machine-controller/blob/main/pkg/cloudprovider/provider/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["moadqassem"]} -->
kubermatic-bot: Thanks for your pull request. Before we can look at it, you'll need to add a 'DCO signoff' to your commits.

:memo: **Please follow instructions in the [contributing guide](https://github.com/kubermatic/machine-controller/blob/master/CONTRIBUTING.md) to update your commits with the DCO**

Full details of the Developer Certificate of Origin can be found at [developercertificate.org](https://developercertificate.org/).

**The list of commits missing DCO signoff**:

- [777b053](https://github.com/kubermatic/machine-controller/commits/777b053ffe3c91d4b1efde63b6caf08f531367b2) Extract resolve template logic into separate helper function

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>

kubermatic-bot: Hi @marioreggiori. Thanks for your PR.

I'm waiting for a [kubermatic](https://github.com/orgs/kubermatic/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://github.com/orgs/kubermatic/people) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubermatic%2Fmachine-controller).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>

ahmedwaleedmalik: /ok-to-test
/assign @ahmedwaleedmalik 
ahmedwaleedmalik: /test pull-machine-controller-e2e-anexia
ahmedwaleedmalik: > pull-machine-controller-e2e-anexia 

@marioreggiori seems like `flatcar on v1.26.0` is not working maybe. 
kubermatic-bot: @marioreggiori: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-machine-controller-e2e-anexia | 06385780b7a47f88f659c360e1904cb5855a138b | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1541/pull-machine-controller-e2e-anexia/1619981409152143360) | true | `/test pull-machine-controller-e2e-anexia`

[Full PR test history](https://public-prow.loodse.com/pr-history?org=kubermatic&repo=machine-controller&pr=1541)

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->

Patch:
diff --git a/OWNERS_ALIASES b/OWNERS_ALIASES
index d833f9f6..befce274 100644
--- a/OWNERS_ALIASES
+++ b/OWNERS_ALIASES
@@ -19,9 +19,3 @@ aliases:
     - hdurand0710
     - mfranczy
     - sankalp-r
-
-  # Temporary SIG to oversee changes in userdata and cloudprovider sub-directories
-  # This SIG is responsible for ensuring that OSM and machine-controller are in sync
-  sig-osm:
-    - ahmedwaleedmalik
-    - moadqassem
diff --git a/docs/anexia.md b/docs/anexia.md
index 5a224f53..cf826f8f 100644
--- a/docs/anexia.md
+++ b/docs/anexia.md
@@ -10,10 +10,16 @@ An example machine deployment can be found here: [examples/anexia-machinedeploym
 
 ## Templates
 
+You can configure the template to use by its name (using the attribute `template`) or its identifier (using the attribute `templateID`).
+
+When specifying the template by its name, the template build to use can optionally be set (attribute `templateBuild`). Omitting `templateBuild` will yield the latest available build (at time the time of creating the `Machine`) for the specified named template.
+
+Template identifiers (attribute `templateID`) always link to a given `template`-`templateBuild` combination, so using the identifier in configuration has the same drawback as specifying an exact build to use.
+
+Templates are rotated pretty often to include security patches and other updates. Outdated versions of templates are not retained and get removed after some time. Because of this, we do not recommend using the `templateID` attribute or pinning to a fixed build unless really required.
+
 To retrieve all available templates against a given location:
 
 ```
 https://engine.anexia-it.com/api/vsphere/v1/provisioning/templates.json/<location identifier>/templates?page=1&limit=50&api_key=<API Key>
 ```
-
-Templates are rotated pretty often, to include updates and latest security patches. Outdated versions of templates are not retained as a result and they get removed after some time.
diff --git a/examples/anexia-machinedeployment.yaml b/examples/anexia-machinedeployment.yaml
index 743bddca..efb1564b 100644
--- a/examples/anexia-machinedeployment.yaml
+++ b/examples/anexia-machinedeployment.yaml
@@ -31,8 +31,10 @@ spec:
                 name: machine-controller-anexia
                 key: token
             vlanID: "<< ANEXIA_VLAN_ID >>"
-            # Currently only the flatcar template is supported: 12c28aa7-604d-47e9-83fb-5f1d1f1837b3
-            templateID: "<< ANEXIA_TEMPLATE_ID >>"
+            # Currently only the "Flatcar Linux Stable" template is supported.
+            # Use templateBuild to specify a build. If empty => latest
+            # Alternatively use templateID for a specific template.
+            template: "<< ANEXIA_TEMPLATE_NAME >>"
             locationID: "<< ANEXIA_LOCATION_ID >>"
             cpus: 2
             memory: 2048
diff --git a/go.mod b/go.mod
index cdfef642..3a754384 100644
--- a/go.mod
+++ b/go.mod
@@ -135,6 +135,8 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/onsi/ginkgo/v2 v2.6.0 // indirect
+	github.com/onsi/gomega v1.24.1 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/openshift/api v0.0.0-20211217221424-8779abfbd571 // indirect
 	github.com/openshift/custom-resource-status v1.1.2 // indirect
diff --git a/go.sum b/go.sum
index cd5b4ee4..2eb99649 100644
--- a/go.sum
+++ b/go.sum
@@ -529,7 +529,6 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
 github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
-github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
 github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
 github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
@@ -538,6 +537,7 @@ github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7
 github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0=
 github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo=
 github.com/onsi/ginkgo/v2 v2.6.0 h1:9t9b9vRUbFq3C4qKFCGkVuq/fIHji802N1nrtkh1mNc=
+github.com/onsi/ginkgo/v2 v2.6.0/go.mod h1:63DOGlLAH8+REH8jUGdL3YpCpu7JODesutUjdENfUAc=
 github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
@@ -549,6 +549,7 @@ github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8lu
 github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM=
 github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
 github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E=
+github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/openshift/api v0.0.0-20211217221424-8779abfbd571 h1:+ShYlGoPriGahTTFTjQ0RtNXW0srxDodk2STdc238Rk=
@@ -615,6 +616,7 @@ github.com/rollbar/rollbar-go/errors v0.0.0-20210929193720-32947096267e/go.mod h
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
+github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM=
 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.10 h1:wsfMs0iv+MJiViM37qh5VEKISi3/ZUq2nNKNdqmumAs=
 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.10/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
 github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
diff --git a/pkg/cloudprovider/provider/OWNERS b/pkg/cloudprovider/provider/OWNERS
deleted file mode 100644
index 31bc1a72..00000000
--- a/pkg/cloudprovider/provider/OWNERS
+++ /dev/null
@@ -1,13 +0,0 @@
-# See the OWNERS docs: https://git.k8s.io/community/contributors/guide/owners.md
-
-approvers:
-  - sig-osm
-
-reviewers:
-  - sig-osm
-
-labels:
-  - sig/osm
-
-options:
-  no_parent_owners: true
\ No newline at end of file
diff --git a/pkg/cloudprovider/provider/anexia/provider.go b/pkg/cloudprovider/provider/anexia/provider.go
index 1d7a8180..b036d914 100644
--- a/pkg/cloudprovider/provider/anexia/provider.go
+++ b/pkg/cloudprovider/provider/anexia/provider.go
@@ -28,6 +28,8 @@ import (
 	"time"
 
 	"go.anx.io/go-anxcloud/pkg/api"
+	corev1 "go.anx.io/go-anxcloud/pkg/apis/core/v1"
+	vspherev1 "go.anx.io/go-anxcloud/pkg/apis/vsphere/v1"
 	"go.anx.io/go-anxcloud/pkg/client"
 	anxclient "go.anx.io/go-anxcloud/pkg/client"
 	anxaddr "go.anx.io/go-anxcloud/pkg/ipam/address"
@@ -94,7 +96,7 @@ func (p *provider) Create(ctx context.Context, machine *clusterv1alpha1.Machine,
 	// ensure conditions are present on machine
 	ensureConditions(&status)
 
-	config, _, err := p.getConfig(machine.Spec.ProviderSpec)
+	config, _, err := p.getConfig(ctx, machine.Spec.ProviderSpec)
 	if err != nil {
 		return nil, fmt.Errorf("unable to get provider config: %w", err)
 	}
@@ -107,7 +109,7 @@ func (p *provider) Create(ctx context.Context, machine *clusterv1alpha1.Machine,
 		Machine:      machine,
 	})
 
-	client, err := getClient(config.Token)
+	_, client, err := getClient(config.Token)
 	if err != nil {
 		return nil, err
 	}
@@ -270,7 +272,26 @@ func ensureConditions(status *anxtypes.ProviderStatus) {
 	}
 }
 
-func (p *provider) resolveConfig(config anxtypes.RawConfig) (*resolvedConfig, error) {
+func resolveTemplateID(ctx context.Context, a api.API, config anxtypes.RawConfig, configVarResolver *providerconfig.ConfigVarResolver, locationID string) (string, error) {
+	templateName, err := configVarResolver.GetConfigVarStringValue(config.Template)
+	if err != nil {
+		return "", fmt.Errorf("failed to get 'template': %w", err)
+	}
+
+	templateBuild, err := configVarResolver.GetConfigVarStringValue(config.TemplateBuild)
+	if err != nil {
+		return "", fmt.Errorf("failed to get 'templateBuild': %w", err)
+	}
+
+	template, err := vspherev1.FindNamedTemplate(ctx, a, templateName, templateBuild, corev1.Location{Identifier: locationID})
+	if err != nil {
+		return "", fmt.Errorf("failed to retrieve named template: %w", err)
+	}
+
+	return template.Identifier, nil
+}
+
+func (p *provider) resolveConfig(ctx context.Context, config anxtypes.RawConfig) (*resolvedConfig, error) {
 	var err error
 	ret := resolvedConfig{
 		RawConfig: config,
@@ -291,6 +312,21 @@ func (p *provider) resolveConfig(config anxtypes.RawConfig) (*resolvedConfig, er
 		return nil, fmt.Errorf("failed to get 'templateID': %w", err)
 	}
 
+	// when "templateID" is not set, we expect "template" to be
+	if ret.TemplateID == "" {
+		a, _, err := getClient(ret.Token)
+		if err != nil {
+			return nil, fmt.Errorf("failed initializing API clients: %w", err)
+		}
+
+		templateID, err := resolveTemplateID(ctx, a, config, p.configVarResolver, ret.LocationID)
+		if err != nil {
+			return nil, fmt.Errorf("failed retrieving template id from named template: %w", err)
+		}
+
+		ret.TemplateID = templateID
+	}
+
 	ret.VlanID, err = p.configVarResolver.GetConfigVarStringValue(config.VlanID)
 	if err != nil {
 		return nil, fmt.Errorf("failed to get 'vlanID': %w", err)
@@ -325,7 +361,7 @@ func (p *provider) resolveConfig(config anxtypes.RawConfig) (*resolvedConfig, er
 	return &ret, nil
 }
 
-func (p *provider) getConfig(provSpec clusterv1alpha1.ProviderSpec) (*resolvedConfig, *providerconfigtypes.Config, error) {
+func (p *provider) getConfig(ctx context.Context, provSpec clusterv1alpha1.ProviderSpec) (*resolvedConfig, *providerconfigtypes.Config, error) {
 	if provSpec.Value == nil {
 		return nil, nil, fmt.Errorf("machine.spec.providerSpec.value is nil")
 	}
@@ -343,7 +379,7 @@ func (p *provider) getConfig(provSpec clusterv1alpha1.ProviderSpec) (*resolvedCo
 		return nil, nil, fmt.Errorf("error parsing provider config: %w", err)
 	}
 
-	resolvedConfig, err := p.resolveConfig(*rawConfig)
+	resolvedConfig, err := p.resolveConfig(ctx, *rawConfig)
 	if err != nil {
 		return nil, nil, fmt.Errorf("error resolving config: %w", err)
 	}
@@ -362,14 +398,14 @@ func (p *provider) AddDefaults(spec clusterv1alpha1.MachineSpec) (clusterv1alpha
 }
 
 // Validate returns success or failure based according to its ProviderSpec.
-func (p *provider) Validate(_ context.Context, machinespec clusterv1alpha1.MachineSpec) error {
-	config, _, err := p.getConfig(machinespec.ProviderSpec)
+func (p *provider) Validate(ctx context.Context, machinespec clusterv1alpha1.MachineSpec) error {
+	config, _, err := p.getConfig(ctx, machinespec.ProviderSpec)
 	if err != nil {
 		return fmt.Errorf("failed to parse config: %w", err)
 	}
 
 	if config.Token == "" {
-		return errors.New("token is missing")
+		return errors.New("token not set")
 	}
 
 	if config.CPUs == 0 {
@@ -399,7 +435,7 @@ func (p *provider) Validate(_ context.Context, machinespec clusterv1alpha1.Machi
 	}
 
 	if config.TemplateID == "" {
-		return errors.New("template id is missing")
+		return errors.New("no valid template configured")
 	}
 
 	if config.VlanID == "" {
@@ -410,12 +446,12 @@ func (p *provider) Validate(_ context.Context, machinespec clusterv1alpha1.Machi
 }
 
 func (p *provider) Get(ctx context.Context, machine *clusterv1alpha1.Machine, pd *cloudprovidertypes.ProviderData) (instance.Instance, error) {
-	config, _, err := p.getConfig(machine.Spec.ProviderSpec)
+	config, _, err := p.getConfig(ctx, machine.Spec.ProviderSpec)
 	if err != nil {
 		return nil, newError(common.InvalidConfigurationMachineError, "failed to retrieve config: %v", err)
 	}
 
-	cli, err := getClient(config.Token)
+	_, cli, err := getClient(config.Token)
 	if err != nil {
 		return nil, newError(common.InvalidConfigurationMachineError, "failed to create Anexia client: %v", err)
 	}
@@ -480,12 +516,12 @@ func (p *provider) Cleanup(ctx context.Context, machine *clusterv1alpha1.Machine
 	}()
 
 	ensureConditions(&status)
-	config, _, err := p.getConfig(machine.Spec.ProviderSpec)
+	config, _, err := p.getConfig(ctx, machine.Spec.ProviderSpec)
 	if err != nil {
 		return false, newError(common.InvalidConfigurationMachineError, "failed to parse MachineSpec: %v", err)
 	}
 
-	cli, err := getClient(config.Token)
+	_, cli, err := getClient(config.Token)
 	if err != nil {
 		return false, newError(common.InvalidConfigurationMachineError, "failed to create Anexia client: %v", err)
 	}
@@ -544,10 +580,21 @@ func (p *provider) SetMetricsForMachines(_ clusterv1alpha1.MachineList) error {
 	return nil
 }
 
-func getClient(token string) (anxclient.Client, error) {
+func getClient(token string) (api.API, anxclient.Client, error) {
 	tokenOpt := anxclient.TokenFromString(token)
 	client := anxclient.HTTPClient(&http.Client{Timeout: 120 * time.Second})
-	return anxclient.New(tokenOpt, client)
+
+	a, err := api.NewAPI(api.WithClientOptions(client, tokenOpt))
+	if err != nil {
+		return nil, nil, fmt.Errorf("error creating generic API client: %w", err)
+	}
+
+	legacyClient, err := anxclient.New(tokenOpt, client)
+	if err != nil {
+		return nil, nil, fmt.Errorf("error creating legacy client: %w", err)
+	}
+
+	return a, legacyClient, nil
 }
 
 func getProviderStatus(machine *clusterv1alpha1.Machine) anxtypes.ProviderStatus {
diff --git a/pkg/cloudprovider/provider/anexia/types/types.go b/pkg/cloudprovider/provider/anexia/types/types.go
index 25547e9d..e6d8e9f2 100644
--- a/pkg/cloudprovider/provider/anexia/types/types.go
+++ b/pkg/cloudprovider/provider/anexia/types/types.go
@@ -57,7 +57,10 @@ type RawConfig struct {
 	Token      providerconfigtypes.ConfigVarString `json:"token,omitempty"`
 	VlanID     providerconfigtypes.ConfigVarString `json:"vlanID"`
 	LocationID providerconfigtypes.ConfigVarString `json:"locationID"`
-	TemplateID providerconfigtypes.ConfigVarString `json:"templateID"`
+
+	TemplateID    providerconfigtypes.ConfigVarString `json:"templateID"`
+	Template      providerconfigtypes.ConfigVarString `json:"template"`
+	TemplateBuild providerconfigtypes.ConfigVarString `json:"templateBuild"`
 
 	CPUs   int `json:"cpus"`
 	Memory int `json:"memory"`
diff --git a/pkg/userdata/OWNERS b/pkg/userdata/OWNERS
deleted file mode 100644
index 31bc1a72..00000000
--- a/pkg/userdata/OWNERS
+++ /dev/null
@@ -1,13 +0,0 @@
-# See the OWNERS docs: https://git.k8s.io/community/contributors/guide/owners.md
-
-approvers:
-  - sig-osm
-
-reviewers:
-  - sig-osm
-
-labels:
-  - sig/osm
-
-options:
-  no_parent_owners: true
\ No newline at end of file


Test patch:
diff --git a/pkg/cloudprovider/provider/anexia/provider_test.go b/pkg/cloudprovider/provider/anexia/provider_test.go
index 5daa367c..4bda59f2 100644
--- a/pkg/cloudprovider/provider/anexia/provider_test.go
+++ b/pkg/cloudprovider/provider/anexia/provider_test.go
@@ -23,12 +23,15 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"net/url"
+	"strings"
 	"testing"
 	"time"
 
 	"github.com/gophercloud/gophercloud/testhelper"
 	"go.anx.io/go-anxcloud/pkg/api"
+	"go.anx.io/go-anxcloud/pkg/api/mock"
 	corev1 "go.anx.io/go-anxcloud/pkg/apis/core/v1"
+	vspherev1 "go.anx.io/go-anxcloud/pkg/apis/vsphere/v1"
 	"go.anx.io/go-anxcloud/pkg/client"
 	anxclient "go.anx.io/go-anxcloud/pkg/client"
 	"go.anx.io/go-anxcloud/pkg/core"
@@ -47,11 +50,20 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
-const TestIdentifier = "TestIdent"
+const (
+	TestIdentifier   = "TestIdent"
+	testTemplateName = "test-template"
+)
 
 func TestAnexiaProvider(t *testing.T) {
 	testhelper.SetupHTTP()
 	client, server := anxclient.NewTestClient(nil, testhelper.Mux)
+
+	a := mock.NewMockAPI()
+	a.FakeExisting(&vspherev1.Template{Identifier: "TEMPLATE-ID-OLD-BUILD", Name: testTemplateName, Build: "b01"})
+	a.FakeExisting(&vspherev1.Template{Identifier: "TEMPLATE-ID", Name: testTemplateName, Build: "b02"})
+	a.FakeExisting(&vspherev1.Template{Identifier: "WRONG-TEMPLATE-NAME", Name: "Wrong Template Name", Build: "b02"})
+
 	t.Cleanup(func() {
 		testhelper.TeardownHTTP()
 		server.Close()
@@ -157,6 +169,62 @@ func TestAnexiaProvider(t *testing.T) {
 		testhelper.AssertNoErr(t, err)
 	})
 
+	t.Run("Test resolve template", func(t *testing.T) {
+		t.Parallel()
+
+		type testCase struct {
+			config             anxtypes.RawConfig
+			expectedError      string
+			expectedTemplateID string
+		}
+
+		testCases := []testCase{
+			// fail
+			{
+				// Template name does not exist
+				config:        hookableConfig(func(c *anxtypes.RawConfig) { c.Template.Value = "non-existing-template-name" }),
+				expectedError: "failed to retrieve named template",
+			},
+			{
+				// Template build does not exist
+				config: hookableConfig(func(c *anxtypes.RawConfig) {
+					c.Template.Value = testTemplateName
+					c.TemplateBuild.Value = "b42"
+				}),
+				expectedError: "failed to retrieve named template",
+			},
+			// pass
+			{
+				// With named template
+				config:             hookableConfig(func(c *anxtypes.RawConfig) { c.Template.Value = testTemplateName; c.TemplateID.Value = "" }),
+				expectedTemplateID: "TEMPLATE-ID",
+			},
+			{
+				// With named template and not latest build
+				config: hookableConfig(func(c *anxtypes.RawConfig) {
+					c.Template.Value = testTemplateName
+					c.TemplateBuild.Value = "b01"
+				}),
+				expectedTemplateID: "TEMPLATE-ID-OLD-BUILD",
+			},
+		}
+
+		provider := New(nil).(*provider)
+		for _, testCase := range testCases {
+			templateID, err := resolveTemplateID(context.TODO(), a, testCase.config, provider.configVarResolver, "foo")
+			if testCase.expectedError != "" {
+				if err != nil {
+					testhelper.AssertErr(t, err)
+					testhelper.AssertEquals(t, true, strings.Contains(err.Error(), testCase.expectedError))
+					continue
+				}
+			} else {
+				testhelper.AssertNoErr(t, err)
+				testhelper.AssertEquals(t, testCase.expectedTemplateID, templateID)
+			}
+		}
+	})
+
 	t.Run("Test is VM Provisioning", func(t *testing.T) {
 		t.Parallel()
 		providerStatus := anxtypes.ProviderStatus{
@@ -209,38 +277,38 @@ func TestAnexiaProvider(t *testing.T) {
 	})
 }
 
-func TestValidate(t *testing.T) {
-	t.Parallel()
+// this generates a full config and allows hooking into it to e.g. remove a value.
+func hookableConfig(hook func(*anxtypes.RawConfig)) anxtypes.RawConfig {
+	config := anxtypes.RawConfig{
+		CPUs: 1,
 
-	// this generates a full config and allows hooking into it to e.g. remove a value
-	hookableConfig := func(hook func(*anxtypes.RawConfig)) anxtypes.RawConfig {
-		config := anxtypes.RawConfig{
-			CPUs: 1,
+		Memory: 2,
 
-			Memory: 2,
+		Disks: []anxtypes.RawDisk{
+			{Size: 5, PerformanceType: newConfigVarString("ENT6")},
+		},
 
-			Disks: []anxtypes.RawDisk{
-				{Size: 5, PerformanceType: newConfigVarString("ENT6")},
-			},
+		Token:      newConfigVarString("test-token"),
+		VlanID:     newConfigVarString("test-vlan"),
+		LocationID: newConfigVarString("test-location"),
+		TemplateID: newConfigVarString("test-template-id"),
+	}
 
-			Token:      newConfigVarString("test-token"),
-			VlanID:     newConfigVarString("test-vlan"),
-			LocationID: newConfigVarString("test-location"),
-			TemplateID: newConfigVarString("test-template"),
-		}
+	if hook != nil {
+		hook(&config)
+	}
 
-		if hook != nil {
-			hook(&config)
-		}
+	return config
+}
 
-		return config
-	}
+func TestValidate(t *testing.T) {
+	t.Parallel()
 
 	var configCases []ConfigTestCase
 	configCases = append(configCases,
 		ConfigTestCase{
 			Config: hookableConfig(func(c *anxtypes.RawConfig) { c.Token.Value = "" }),
-			Error:  errors.New("token is missing"),
+			Error:  errors.New("token not set"),
 		},
 		ConfigTestCase{
 			Config: hookableConfig(func(c *anxtypes.RawConfig) { c.CPUs = 0 }),
@@ -270,10 +338,6 @@ func TestValidate(t *testing.T) {
 			Config: hookableConfig(func(c *anxtypes.RawConfig) { c.LocationID.Value = "" }),
 			Error:  errors.New("location id is missing"),
 		},
-		ConfigTestCase{
-			Config: hookableConfig(func(c *anxtypes.RawConfig) { c.TemplateID.Value = "" }),
-			Error:  errors.New("template id is missing"),
-		},
 		ConfigTestCase{
 			Config: hookableConfig(func(c *anxtypes.RawConfig) { c.VlanID.Value = "" }),
 			Error:  errors.New("vlan id is missing"),