Repository: kubermatic/machine-controller
Pull request #1646 (2023-05-24 12:31:27)

Fix empty network config handling
**What this PR does / why we need it**:
Provisioning a VM without defining a `Network` in the `providerConfig` object results in a nil pointer exception. This bug got introduced by https://github.com/kubermatic/machine-controller/pull/1637

**What type of PR is this?**
<!--
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
-->
/kind bug

**Special notes for your reviewer**:

We'd love to get a backport of these changes in v1.54

**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: Fix nil pointer exception when providing no network configuration
```

**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
NONE
```


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

This pull-request has been approved by: *<a href="https://github.com/kubermatic/machine-controller/pull/1646#" title="Author self-approved">eiabea</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign moadqassem for approval. 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)**

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: Hi @eiabea. 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>

LittleFox94: This is a bug introduced in #1637, right? If so, should be mentioned in the description
kubermatic-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubermatic/machine-controller/pull/1646#pullrequestreview-1445533788" title="Approved">ahmedwaleedmalik</a>*, *<a href="https://github.com/kubermatic/machine-controller/pull/1646#" title="Author self-approved">eiabea</a>*

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

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

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

- ~~[OWNERS](https://github.com/kubermatic/machine-controller/blob/main/OWNERS)~~ [ahmedwaleedmalik]

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":[]} -->
kubermatic-bot: LGTM label has been added.  <details>Git tree hash: d948fcb50461cb022eefd14be9cf258b6482a83f</details>
ahmedwaleedmalik: /cherry-pick release/v1.56
kubermatic-bot: @ahmedwaleedmalik: once the present PR merges, I will cherry-pick it on top of release/v1.56 in a new PR and assign it to you.

<details>

In response to [this](https://github.com/kubermatic/machine-controller/pull/1646#issuecomment-1563981971):

>/cherry-pick release/v1.56


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: /cherry-pick release/v1.54
kubermatic-bot: @ahmedwaleedmalik: once the present PR merges, I will cherry-pick it on top of release/v1.54 in a new PR and assign it to you.

<details>

In response to [this](https://github.com/kubermatic/machine-controller/pull/1646#issuecomment-1563982231):

>/cherry-pick release/v1.54


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>
kubermatic-bot: @eiabea: 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-test | 1a0739f210d20923bd03f660c098ce2fa5ec8dad | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1646/pull-machine-controller-test/1662007337902149632) | true | `/test pull-machine-controller-test`

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

<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 -->
ahmedwaleedmalik: /approve cancel
kubermatic-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubermatic/machine-controller/pull/1646#" title="Author self-approved">eiabea</a>*
**Once this PR has been reviewed and has the lgtm label**, please ask for approval from ahmedwaleedmalik. 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)**

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":["ahmedwaleedmalik"]} -->
ahmedwaleedmalik: @eiabea please fix the unit tests
eiabea: /retest
kubermatic-bot: @eiabea: Cannot trigger testing until a trusted user reviews the PR and leaves an `/ok-to-test` message.

<details>

In response to [this](https://github.com/kubermatic/machine-controller/pull/1646#issuecomment-1564146793):

>/retest


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>

Patch:
diff --git a/README.md b/README.md
index 913b2a16..b1da95a6 100644
--- a/README.md
+++ b/README.md
@@ -7,35 +7,36 @@
 - [Kubermatic machine-controller](#kubermatic-machine-controller)
   - [Table of Contents](#table-of-contents)
   - [Features](#features)
-    - [What works](#what-works)
-    - [Supported Kubernetes versions](#supported-kubernetes-versions)
-  - [What does not work](#what-does-not-work)
+    - [What Works](#what-works)
+    - [Supported Kubernetes Versions](#supported-kubernetes-versions)
+    - [Community Providers](#community-providers)
+  - [What doesn't Work](#what-doesnt-work)
   - [Quickstart](#quickstart)
-    - [Deploy the machine-controller](#deploy-the-machine-controller)
-    - [Creating a machineDeployment](#creating-a-machinedeployment)
-  - [Advanced usage](#advanced-usage)
-    - [Specifying the apiserver endpoint](#specifying-the-apiserver-endpoint)
-    - [CA-data](#ca-data)
-    - [Apiserver endpoint](#apiserver-endpoint)
+    - [Deploy machine-controller](#deploy-the-machine-controller)
+    - [Creating a MachineDeployment](#creating-a-machinedeployment)
+  - [Advanced Usage](#advanced-usage)
+    - [Specifying the Apiserver Endpoint](#specifying-the-apiserver-endpoint)
+    - [CA Data](#ca-data)
+    - [Apiserver Endpoint](#apiserver-endpoint)
       - [Example cluster-info ConfigMap](#example-cluster-info-configmap)
   - [Development](#development)
     - [Testing](#testing)
-      - [Unittests](#unittests)
-      - [End-to-End locally](#end-to-end-locally)
+      - [Unit Tests](#unit-tests)
+      - [End-to-End Locally](#end-to-end-locally)
   - [Troubleshooting](#troubleshooting)
   - [Contributing](#contributing)
-    - [Before you start](#before-you-start)
-    - [Pull requests](#pull-requests)
+    - [Before You Start](#before-you-start)
+    - [Pull Requests](#pull-requests)
   - [Changelog](#changelog)
 
 ## Features
 
-### What works
+### What Works
 
-- Creation of worker nodes on AWS, Digitalocean, Openstack, Azure, Google Cloud Platform, Nutanix, VMWare Cloud Director, VMWare Vsphere, Linode, Hetzner cloud and Kubevirt (experimental)
-- Using Ubuntu, Flatcar or CentOS 7 distributions ([not all distributions work on all providers](/docs/operating-system.md))
+- Creation of worker nodes on AWS, Digitalocean, Openstack, Azure, Google Cloud Platform, Nutanix, VMWare Cloud Director, VMWare vSphere, Hetzner Cloud and Kubevirt
+- Using Ubuntu, Flatcar, CentOS 7 or Rocky Linux 8 distributions ([not all distributions work on all providers](/docs/operating-system.md))
 
-### Supported Kubernetes versions
+### Supported Kubernetes Versions
 
 machine-controller tries to follow the Kubernetes version
 [support policy](https://kubernetes.io/docs/setup/release/version-skew-policy/) as close as possible.
@@ -47,13 +48,25 @@ Currently supported K8S versions are:
 - 1.25
 - 1.24
 
-## What does not work
+### Community Providers
+
+Some cloud providers implemented in machine-controller have been graciously contributed by community members. Those cloud providers are not part of the automated end-to-end
+tests run by the machine-controller developers and thus, their status cannot be guaranteed. The machine-controller developers assume that they are functional, but can only
+offer limited support for new features or bugfixes in those providers.
+
+The current list of community providers is:
+
+- Linode
+- Vultr
+- OpenNebula
+
+## What Doesn't Work
 
 - Master creation (Not planned at the moment)
 
 ## Quickstart
 
-### Deploy the machine-controller
+### Deploy machine-controller
 
 - Install [cert-manager](https://cert-manager.io/) for generating certificates used by webhooks since they serve using HTTPS
 
@@ -64,16 +77,16 @@ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/
 - Run `kubectl apply -f examples/operating-system-manager.yaml` to deploy the operating-system-manager which is responsible for managing user data for worker machines.
 - Run `kubectl apply -f examples/machine-controller.yaml` to deploy the machine-controller.
 
-### Creating a machineDeployment
+### Creating a `MachineDeployment`
 
 ```bash
 # edit examples/$cloudprovider-machinedeployment.yaml & create the machineDeployment
 kubectl create -f examples/$cloudprovider-machinedeployment.yaml
 ```
 
-## Advanced usage
+## Advanced Usage
 
-### Specifying the apiserver endpoint
+### Specifying the Apiserver Endpoint
 
 By default the controller looks for a `cluster-info` ConfigMap within the `kube-public` Namespace.
 If one is found which contains a minimal kubeconfig (kubeadm cluster have them by default), this kubeconfig will be used for the node bootstrapping.
@@ -84,11 +97,11 @@ The kubeconfig only needs to contain two things:
 
 If no ConfigMap can be found:
 
-### CA-data
+### CA Data
 
-The CA will be loaded from the passed kubeconfig when running outside the cluster or from `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt` when running inside the cluster.
+The Certificate Authority (CA) will be loaded from the passed kubeconfig when running outside the cluster or from `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt` when running inside the cluster.
 
-### Apiserver endpoint
+### Apiserver Endpoint
 
 The first endpoint from the kubernetes endpoints will be taken. `kubectl get endpoints kubernetes -o yaml`
 
@@ -119,11 +132,11 @@ data:
 
 ### Testing
 
-#### Unittests
+#### Unit Tests
 
 Simply run `make test-unit`
 
-#### End-to-End locally
+#### End-to-End Locally
 
 **_[WIP]_**
 
@@ -135,13 +148,13 @@ If you encounter issues [file an issue][1] or talk to us on the [#kubermatic cha
 
 Thanks for taking the time to join our community and start contributing!
 
-### Before you start
+### Before You Start
 
 - Please familiarize yourself with the [Code of Conduct][4] before contributing.
 - See [CONTRIBUTING.md][5] for instructions on the developer certificate of origin that we require.
 - Read how [we're using ZenHub][6] for project and roadmap planning
 
-### Pull requests
+### Pull Requests
 
 - We welcome pull requests. Feel free to dig through the [issues][1] and jump in.
 
diff --git a/docs/cloud-provider.md b/docs/cloud-provider.md
index 2d882241..74edb7a4 100644
--- a/docs/cloud-provider.md
+++ b/docs/cloud-provider.md
@@ -148,6 +148,8 @@ tags:
 
 ## OpenNebula
 
+**Note:** This is a [community provider](../README.md#community-providers).
+
 ### machine.spec.providerConfig.cloudProviderSpec
 
 ```yaml
@@ -233,6 +235,8 @@ labels:
 
 ## Linode
 
+**Note:** This is a [community provider](../README.md#community-providers).
+
 ### machine.spec.providerConfig.cloudProviderSpec
 ```yaml
 # your linode token
@@ -361,6 +365,8 @@ Refer to the [VSphere](./vsphere.md#provider-configuration) specific documentati
 
 ## Vultr
 
+**Note:** This is a [community provider](../README.md#community-providers).
+
 ### machine.spec.providerConfig.cloudProviderSpec
 ```yaml
 apiKey: "<< VULTR_API_KEY >>"
diff --git a/pkg/cloudprovider/provider.go b/pkg/cloudprovider/provider.go
index 1c20a2a3..28161700 100644
--- a/pkg/cloudprovider/provider.go
+++ b/pkg/cloudprovider/provider.go
@@ -66,9 +66,6 @@ var (
 		providerconfigtypes.CloudProviderHetzner: func(cvr *providerconfig.ConfigVarResolver) cloudprovidertypes.Provider {
 			return hetzner.New(cvr)
 		},
-		providerconfigtypes.CloudProviderLinode: func(cvr *providerconfig.ConfigVarResolver) cloudprovidertypes.Provider {
-			return linode.New(cvr)
-		},
 		providerconfigtypes.CloudProviderVsphere: func(cvr *providerconfig.ConfigVarResolver) cloudprovidertypes.Provider {
 			return vsphere.New(cvr)
 		},
@@ -78,9 +75,6 @@ var (
 		providerconfigtypes.CloudProviderEquinixMetal: func(cvr *providerconfig.ConfigVarResolver) cloudprovidertypes.Provider {
 			return equinixmetal.New(cvr)
 		},
-		providerconfigtypes.CloudProviderVultr: func(cvr *providerconfig.ConfigVarResolver) cloudprovidertypes.Provider {
-			return vultr.New(cvr)
-		},
 		// NB: This is explicitly left to allow old Packet machines to be deleted.
 		// We can handle those machines in the same way as Equinix Metal machines
 		// because there are no API changes.
@@ -113,6 +107,17 @@ var (
 		providerconfigtypes.CloudProviderVMwareCloudDirector: func(cvr *providerconfig.ConfigVarResolver) cloudprovidertypes.Provider {
 			return vcd.New(cvr)
 		},
+	}
+
+	// communityProviders holds a map of cloud providers that have been implemented by community members and
+	// contributed to machine-controller. They are not end-to-end tested by the machine-controller development team.
+	communityProviders = map[providerconfigtypes.CloudProvider]func(cvr *providerconfig.ConfigVarResolver) cloudprovidertypes.Provider{
+		providerconfigtypes.CloudProviderLinode: func(cvr *providerconfig.ConfigVarResolver) cloudprovidertypes.Provider {
+			return linode.New(cvr)
+		},
+		providerconfigtypes.CloudProviderVultr: func(cvr *providerconfig.ConfigVarResolver) cloudprovidertypes.Provider {
+			return vultr.New(cvr)
+		},
 		providerconfigtypes.CloudProviderOpenNebula: func(cvr *providerconfig.ConfigVarResolver) cloudprovidertypes.Provider {
 			return opennebula.New(cvr)
 		},
@@ -124,5 +129,8 @@ func ForProvider(p providerconfigtypes.CloudProvider, cvr *providerconfig.Config
 	if p, found := providers[p]; found {
 		return NewValidationCacheWrappingCloudProvider(p(cvr)), nil
 	}
+	if p, found := communityProviders[p]; found {
+		return NewValidationCacheWrappingCloudProvider(p(cvr)), nil
+	}
 	return nil, ErrProviderNotFound
 }
diff --git a/pkg/cloudprovider/provider/anexia/provider.go b/pkg/cloudprovider/provider/anexia/provider.go
index 8b2ca206..842954be 100644
--- a/pkg/cloudprovider/provider/anexia/provider.go
+++ b/pkg/cloudprovider/provider/anexia/provider.go
@@ -166,16 +166,19 @@ func provisionVM(ctx context.Context, log *zap.SugaredLogger, client anxclient.C
 
 		vm.Script = base64.StdEncoding.EncodeToString([]byte(reconcileContext.UserData))
 
-		for index, dnsServer := range reconcileContext.ProviderConfig.Network.DNS.Servers {
-			switch index {
-			case 0:
-				vm.DNS1 = dnsServer
-			case 1:
-				vm.DNS2 = dnsServer
-			case 2:
-				vm.DNS3 = dnsServer
-			case 3:
-				vm.DNS4 = dnsServer
+		providerCfg := reconcileContext.ProviderConfig
+		if providerCfg.Network != nil {
+			for index, dnsServer := range providerCfg.Network.DNS.Servers {
+				switch index {
+				case 0:
+					vm.DNS1 = dnsServer
+				case 1:
+					vm.DNS2 = dnsServer
+				case 2:
+					vm.DNS3 = dnsServer
+				case 3:
+					vm.DNS4 = dnsServer
+				}
 			}
 		}
 


Test patch:
diff --git a/pkg/cloudprovider/provider/anexia/helper_test.go b/pkg/cloudprovider/provider/anexia/helper_test.go
index 38c3a37e..c6256fa2 100644
--- a/pkg/cloudprovider/provider/anexia/helper_test.go
+++ b/pkg/cloudprovider/provider/anexia/helper_test.go
@@ -20,15 +20,27 @@ import (
 	"encoding/json"
 	"testing"
 
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
 	"github.com/gophercloud/gophercloud/testhelper"
 
 	"github.com/kubermatic/machine-controller/pkg/apis/cluster/v1alpha1"
+	clusterv1alpha1 "github.com/kubermatic/machine-controller/pkg/apis/cluster/v1alpha1"
 	anxtypes "github.com/kubermatic/machine-controller/pkg/cloudprovider/provider/anexia/types"
+	cloudprovidertypes "github.com/kubermatic/machine-controller/pkg/cloudprovider/types"
 	"github.com/kubermatic/machine-controller/pkg/providerconfig/types"
+	providerconfigtypes "github.com/kubermatic/machine-controller/pkg/providerconfig/types"
 
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
+type jsonObject = map[string]interface{}
+
+type ProvisionVMTestCase struct {
+	ReconcileContext reconcileContext
+	AssertJSONBody   func(jsonBody jsonObject)
+}
+
 type ConfigTestCase struct {
 	Config anxtypes.RawConfig
 	Error  error
@@ -67,3 +79,78 @@ func newConfigVarString(str string) types.ConfigVarString {
 		Value: str,
 	}
 }
+
+// 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,
+
+		Memory: 2,
+
+		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"),
+	}
+
+	if hook != nil {
+		hook(&config)
+	}
+
+	return config
+}
+
+// this generates a full reconcileContext with some default values and allows hooking into it to e.g. remove/overwrite a value.
+func hookableReconcileContext(locationID string, templateID string, hook func(*reconcileContext)) reconcileContext {
+	context := reconcileContext{
+		Machine: &v1alpha1.Machine{
+			ObjectMeta: metav1.ObjectMeta{Name: "TestMachine"},
+		},
+		Status:   &anxtypes.ProviderStatus{},
+		UserData: "",
+		Config: resolvedConfig{
+			VlanID:     "VLAN-ID",
+			LocationID: locationID,
+			TemplateID: templateID,
+			Disks: []resolvedDisk{
+				{
+					RawDisk: anxtypes.RawDisk{
+						Size: 5,
+					},
+				},
+			},
+			RawConfig: anxtypes.RawConfig{
+				CPUs:   5,
+				Memory: 5,
+			},
+		},
+		ProviderData: &cloudprovidertypes.ProviderData{
+			Update: func(m *clusterv1alpha1.Machine, mods ...cloudprovidertypes.MachineModifier) error {
+				return nil
+			},
+		},
+		ProviderConfig: &providerconfigtypes.Config{
+			Network: &providerconfigtypes.NetworkConfig{
+				DNS: providerconfigtypes.DNSConfig{
+					Servers: []string{
+						"1.1.1.1",
+						"",
+						"192.168.0.1",
+						"192.168.0.2",
+						"192.168.0.3",
+					},
+				},
+			},
+		},
+	}
+
+	if hook != nil {
+		hook(&context)
+	}
+
+	return context
+}
diff --git a/pkg/cloudprovider/provider/anexia/provider_test.go b/pkg/cloudprovider/provider/anexia/provider_test.go
index b0faddd7..ab2b40ba 100644
--- a/pkg/cloudprovider/provider/anexia/provider_test.go
+++ b/pkg/cloudprovider/provider/anexia/provider_test.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -41,7 +42,6 @@ import (
 	"go.uber.org/zap"
 
 	"github.com/kubermatic/machine-controller/pkg/apis/cluster/v1alpha1"
-	clusterv1alpha1 "github.com/kubermatic/machine-controller/pkg/apis/cluster/v1alpha1"
 	cloudprovidererrors "github.com/kubermatic/machine-controller/pkg/cloudprovider/errors"
 	anxtypes "github.com/kubermatic/machine-controller/pkg/cloudprovider/provider/anexia/types"
 	cloudprovidertypes "github.com/kubermatic/machine-controller/pkg/cloudprovider/types"
@@ -66,6 +66,7 @@ func TestAnexiaProvider(t *testing.T) {
 	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"})
+	a.FakeExisting(&vspherev1.Template{Identifier: "TEMPLATE-ID-NO-NETWORK-CONFIG", Name: "no-network-config", Build: "b03"})
 
 	t.Cleanup(func() {
 		testhelper.TeardownHTTP()
@@ -74,6 +75,61 @@ func TestAnexiaProvider(t *testing.T) {
 
 	t.Run("Test provision VM", func(t *testing.T) {
 		t.Parallel()
+
+		testCases := []ProvisionVMTestCase{
+			{
+				// Provision a generic VM with some custom dns entries
+				ReconcileContext: hookableReconcileContext("LOCATION-ID", "TEMPLATE-ID", func(rc *reconcileContext) {
+					rc.ProviderConfig = &providerconfigtypes.Config{
+						Network: &providerconfigtypes.NetworkConfig{
+							DNS: providerconfigtypes.DNSConfig{
+								Servers: []string{
+									"1.1.1.1",
+									"",
+									"192.168.0.1",
+									"192.168.0.2",
+									"192.168.0.3",
+								},
+							},
+						},
+					}
+				}),
+				AssertJSONBody: func(jsonBody jsonObject) {
+					testhelper.AssertEquals(t, jsonBody["cpu_performance_type"], "performance")
+					testhelper.AssertEquals(t, jsonBody["hostname"], "TestMachine")
+					testhelper.AssertEquals(t, jsonBody["memory_mb"], json.Number("5"))
+
+					testhelper.AssertEquals(t, jsonBody["dns1"], "1.1.1.1")
+					_, exists := jsonBody["dns2"]
+					testhelper.AssertEquals(t, exists, false)
+					testhelper.AssertEquals(t, jsonBody["dns3"], "192.168.0.1")
+					testhelper.AssertEquals(t, jsonBody["dns4"], "192.168.0.2")
+
+					networkArray := jsonBody["network"].([]interface{})
+					networkObject := networkArray[0].(jsonObject)
+					testhelper.AssertEquals(t, networkObject["vlan"], "VLAN-ID")
+					testhelper.AssertEquals(t, networkObject["nic_type"], "vmxnet3")
+					testhelper.AssertEquals(t, networkObject["ips"].([]interface{})[0], "8.8.8.8")
+				},
+			},
+			{
+				// Provision a VM without any ProviderConfig
+				ReconcileContext: hookableReconcileContext("LOCATION-ID", "TEMPLATE-ID-NO-NETWORK-CONFIG", func(rc *reconcileContext) {
+					rc.ProviderConfig = &providerconfigtypes.Config{}
+				}),
+				AssertJSONBody: func(jsonBody jsonObject) {
+					_, exists := jsonBody["dns1"]
+					testhelper.AssertEquals(t, exists, false)
+					_, exists = jsonBody["dns2"]
+					testhelper.AssertEquals(t, exists, false)
+					_, exists = jsonBody["dns3"]
+					testhelper.AssertEquals(t, exists, false)
+					_, exists = jsonBody["dns4"]
+					testhelper.AssertEquals(t, exists, false)
+				},
+			},
+		}
+
 		testhelper.Mux.HandleFunc("/api/ipam/v1/address/reserve/ip/count.json", func(writer http.ResponseWriter, request *http.Request) {
 			err := json.NewEncoder(writer).Encode(address.ReserveRandomSummary{
 				Data: []address.ReservedIP{
@@ -86,108 +142,46 @@ func TestAnexiaProvider(t *testing.T) {
 			testhelper.AssertNoErr(t, err)
 		})
 
-		testhelper.Mux.HandleFunc("/api/vsphere/v1/provisioning/vm.json/LOCATION-ID/templates/TEMPLATE-ID", func(writer http.ResponseWriter, request *http.Request) {
-			testhelper.TestMethod(t, request, http.MethodPost)
-			type jsonObject = map[string]interface{}
-			expectedJSON := map[string]interface{}{
-				"cpu_performance_type": "performance",
-				"hostname":             "TestMachine",
-				"memory_mb":            json.Number("5"),
-				"network": []jsonObject{
-					{
-						"vlan":     "VLAN-ID",
-						"nic_type": "vmxnet3",
-						"ips":      []interface{}{"8.8.8.8"},
-					},
-				},
-			}
-			var jsonBody jsonObject
-			decoder := json.NewDecoder(request.Body)
-			decoder.UseNumber()
-			testhelper.AssertNoErr(t, decoder.Decode(&jsonBody))
-			testhelper.AssertEquals(t, expectedJSON["cpu_performance_type"], jsonBody["cpu_performance_type"])
-			testhelper.AssertEquals(t, expectedJSON["hostname"], jsonBody["hostname"])
-			testhelper.AssertEquals(t, expectedJSON["memory_mb"], jsonBody["memory_mb"])
-
-			testhelper.AssertEquals(t, jsonBody["dns1"], "1.1.1.1")
-			testhelper.AssertEquals(t, jsonBody["dns2"], nil)
-			testhelper.AssertEquals(t, jsonBody["dns3"], "192.168.0.1")
-			testhelper.AssertEquals(t, jsonBody["dns4"], "192.168.0.2")
-			testhelper.AssertEquals(t, expectedJSON["count"], jsonBody["count"])
-
-			expectedNetwork := expectedJSON["network"].([]jsonObject)[0]
-			bodyNetwork := jsonBody["network"].([]interface{})[0].(jsonObject)
-			testhelper.AssertEquals(t, expectedNetwork["vlan"], bodyNetwork["vlan"])
-			testhelper.AssertEquals(t, expectedNetwork["nic_type"], bodyNetwork["nic_type"])
-			testhelper.AssertEquals(t, expectedNetwork["ips"].([]interface{})[0], bodyNetwork["ips"].([]interface{})[0])
-
-			err := json.NewEncoder(writer).Encode(vm.ProvisioningResponse{
-				Progress:   100,
-				Errors:     nil,
-				Identifier: "TEST-IDENTIFIER",
-				Queued:     false,
+		for _, testCase := range testCases {
+			templateID := testCase.ReconcileContext.Config.TemplateID
+			locationID := testCase.ReconcileContext.Config.LocationID
+
+			testhelper.Mux.HandleFunc(fmt.Sprintf("/api/vsphere/v1/provisioning/vm.json/%s/templates/%s", locationID, templateID), func(writer http.ResponseWriter, request *http.Request) {
+				testhelper.TestMethod(t, request, http.MethodPost)
+				var jsonBody jsonObject
+				decoder := json.NewDecoder(request.Body)
+				decoder.UseNumber()
+				testhelper.AssertNoErr(t, decoder.Decode(&jsonBody))
+
+				testCase.AssertJSONBody(jsonBody)
+
+				err := json.NewEncoder(writer).Encode(vm.ProvisioningResponse{
+					Progress:   100,
+					Errors:     nil,
+					Identifier: templateID,
+					Queued:     false,
+				})
+				testhelper.AssertNoErr(t, err)
 			})
-			testhelper.AssertNoErr(t, err)
-		})
 
-		testhelper.Mux.HandleFunc("/api/vsphere/v1/provisioning/progress.json/TEST-IDENTIFIER", func(writer http.ResponseWriter, request *http.Request) {
-			testhelper.TestMethod(t, request, http.MethodGet)
+			testhelper.Mux.HandleFunc(fmt.Sprintf("/api/vsphere/v1/provisioning/progress.json/%s", templateID), func(writer http.ResponseWriter, request *http.Request) {
+				testhelper.TestMethod(t, request, http.MethodGet)
 
-			err := json.NewEncoder(writer).Encode(progress.Progress{
-				TaskIdentifier: "TEST-IDENTIFIER",
-				Queued:         false,
-				Progress:       100,
-				VMIdentifier:   "VM-IDENTIFIER",
-				Errors:         nil,
+				err := json.NewEncoder(writer).Encode(progress.Progress{
+					TaskIdentifier: templateID,
+					Queued:         false,
+					Progress:       100,
+					VMIdentifier:   "VM-IDENTIFIER",
+					Errors:         nil,
+				})
+				testhelper.AssertNoErr(t, err)
 			})
-			testhelper.AssertNoErr(t, err)
-		})
 
-		providerStatus := anxtypes.ProviderStatus{}
-		ctx := createReconcileContext(context.Background(), reconcileContext{
-			Machine: &v1alpha1.Machine{
-				ObjectMeta: metav1.ObjectMeta{Name: "TestMachine"},
-			},
-			Status:   &providerStatus,
-			UserData: "",
-			Config: resolvedConfig{
-				VlanID:     "VLAN-ID",
-				LocationID: "LOCATION-ID",
-				TemplateID: "TEMPLATE-ID",
-				Disks: []resolvedDisk{
-					{
-						RawDisk: anxtypes.RawDisk{
-							Size: 5,
-						},
-					},
-				},
-				RawConfig: anxtypes.RawConfig{
-					CPUs:   5,
-					Memory: 5,
-				},
-			},
-			ProviderData: &cloudprovidertypes.ProviderData{
-				Update: func(m *clusterv1alpha1.Machine, mods ...cloudprovidertypes.MachineModifier) error {
-					return nil
-				},
-			},
-			ProviderConfig: &providerconfigtypes.Config{
-				Network: &providerconfigtypes.NetworkConfig{
-					DNS: providerconfigtypes.DNSConfig{
-						Servers: []string{
-							"1.1.1.1",
-							"",
-							"192.168.0.1",
-							"192.168.0.2",
-							"192.168.0.3",
-						},
-					},
-				},
-			},
-		})
+			ctx := createReconcileContext(context.Background(), testCase.ReconcileContext)
 
-		err := provisionVM(ctx, log, client)
-		testhelper.AssertNoErr(t, err)
+			err := provisionVM(ctx, log, client)
+			testhelper.AssertNoErr(t, err)
+		}
 	})
 
 	t.Run("Test resolve template", func(t *testing.T) {
@@ -298,30 +292,6 @@ func TestAnexiaProvider(t *testing.T) {
 	})
 }
 
-// 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,
-
-		Memory: 2,
-
-		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"),
-	}
-
-	if hook != nil {
-		hook(&config)
-	}
-
-	return config
-}
-
 func TestValidate(t *testing.T) {
 	t.Parallel()