Repository: kubermatic/machine-controller
Pull request #1406 (2022-08-15 10:53:37)

Makefile: fix test-unit-docker
**What this PR does / why we need it**:

GOFLAGS need to be passed with quotes to ensure they are not split over
multiple arguments to `make`.

<details>
<summary>Failure without the added quotes</summary>

```
$ make test-unit-docker
# GNU Make 4.3
# Built for x86_64-pc-linux-gnu
# Copyright (C) 1988-2020 Free Software Foundation, Inc.
# License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
# This is free software: you are free to change and redistribute it.
# There is NO WARRANTY, to the extent permitted by law.
make: invalid option -- 'a'
Usage: make [options] [target] ...
Options:
  -b, -m                      Ignored for compatibility.
  -B, --always-make           Unconditionally make all targets.
  -C DIRECTORY, --directory=DIRECTORY
                              Change to DIRECTORY before doing anything.
  -d                          Print lots of debugging information.
  --debug[=FLAGS]             Print various types of debugging information.
  -e, --environment-overrides
                              Environment variables override makefiles.
  -E STRING, --eval=STRING    Evaluate STRING as a makefile statement.
  -f FILE, --file=FILE, --makefile=FILE
                              Read FILE as a makefile.
  -h, --help                  Print this message and exit.
  -i, --ignore-errors         Ignore errors from recipes.
  -I DIRECTORY, --include-dir=DIRECTORY
                              Search DIRECTORY for included makefiles.
  -j [N], --jobs[=N]          Allow N jobs at once; infinite jobs with no arg.
  -k, --keep-going            Keep going when some targets can't be made.
  -l [N], --load-average[=N], --max-load[=N]
                              Don't start multiple jobs unless load is below N.
  -L, --check-symlink-times   Use the latest mtime between symlinks and target.
  -n, --just-print, --dry-run, --recon
                              Don't actually run any recipe; just print them.
  -o FILE, --old-file=FILE, --assume-old=FILE
                              Consider FILE to be very old and don't remake it.
  -O[TYPE], --output-sync[=TYPE]
                              Synchronize output of parallel jobs by TYPE.
  -p, --print-data-base       Print make's internal database.
  -q, --question              Run no recipe; exit status says if up to date.
  -r, --no-builtin-rules      Disable the built-in implicit rules.
  -R, --no-builtin-variables  Disable the built-in variable settings.
  -s, --silent, --quiet       Don't echo recipes.
  --no-silent                 Echo recipes (disable --silent mode).
  -S, --no-keep-going, --stop
                              Turns off -k.
  -t, --touch                 Touch targets instead of remaking them.
  --trace                     Print tracing information.
  -v, --version               Print the version number of make and exit.
  -w, --print-directory       Print the current directory.
  --no-print-directory        Turn off -w, even if it was turned on implicitly.
  -W FILE, --what-if=FILE, --new-file=FILE, --assume-new=FILE
                              Consider FILE to be infinitely new.
  --warn-undefined-variables  Warn when an undefined variable is referenced.

This program built for x86_64-pc-linux-gnu
Report bugs to <bug-make@gnu.org>

# Make data base, printed on Mon Aug 15 10:49:03 2022

# Variables

# environment
GOPATH = /go
# default
.VARIABLES :=
# environment
HOSTNAME = d4e782dacd29
# environment
GOCACHE = /cache
# default
.LOADED :=
# environment
GOLANG_VERSION = 1.18.3
# default
.SHELLFLAGS := -c
# default
MAKECMDGOALS := test-unit
# environment
PATH = /go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# environment
GNUMAKEFLAGS :=
# environment
HOME = /root
# default
.RECIPEPREFIX :=
# command line
GOFLAGS = -mod=readonly
# default
.FEATURES := target-specific order-only second-expansion else-if shortest-stem undefine oneshell nocomment grouped-target extra-prereqs archives jobserver output-sync check-symlink load
# variable set hash-table stats:
# Load=14/1024=1%, Rehash=0, Collisions=1/17=6%

# Pattern-specific Variable Values

# No pattern-specific variable values.

# Directories


# No files, no impossibilities in 0 directories.

# Implicit Rules

# No implicit rules.

# Files

# Not a target:
test-unit:
#  Command line target.
#  Implicit rule search has not been done.
#  Modification time never checked.
#  File has not been updated.

# files hash-table stats:
# Load=1/1024=0%, Rehash=0, Collisions=0/1=0%
# VPATH Search Paths

# No 'vpath' search paths.

# No general ('VPATH' variable) search path.

# strcache buffers: 1 (0) / strings = 1 / storage = 10 B / avg = 10 B
# current buf: size = 8162 B / used = 10 B / count = 1 / avg = 10 B

# strcache performance: lookups = 2 / hit rate = 50%
# hash-table stats:
# Load=1/8192=0%, Rehash=0, Collisions=0/2=0%
# Finished Make data base on Mon Aug 15 10:49:03 2022

make: *** [Makefile:88: test-unit-docker] Fehler 2
```
</details>

**What type of PR is this?**
/kind bug
/kind failing-test


**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
NONE
```

**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/1406#" title="Author self-approved">LittleFox94</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **hdurand0710** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @hdurand0710` in a comment when ready.

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/master/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":["hdurand0710"]} -->
kubermatic-bot: Hi @LittleFox94. 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>

embik: /ok-to-test
kubermatic-bot: @LittleFox94: 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-kubevirt | f2bcee9525ecb98bcc4797e8a0dce6604f470019 | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1406/pull-machine-controller-e2e-kubevirt/1559131480863543296) | true | `/test pull-machine-controller-e2e-kubevirt`

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

<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 -->
kubermatic-bot: @LittleFox94: The following tests **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-kubevirt | f2bcee9525ecb98bcc4797e8a0dce6604f470019 | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1406/pull-machine-controller-e2e-kubevirt/1559131480863543296) | true | `/test pull-machine-controller-e2e-kubevirt`
pull-machine-controller-e2e-ubuntu-upgrade | f2bcee9525ecb98bcc4797e8a0dce6604f470019 | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1406/pull-machine-controller-e2e-ubuntu-upgrade/1559131475163484160) | true | `/test pull-machine-controller-e2e-ubuntu-upgrade`

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

<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 -->
kubermatic-bot: @LittleFox94: The following tests **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-kubevirt | f2bcee9525ecb98bcc4797e8a0dce6604f470019 | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1406/pull-machine-controller-e2e-kubevirt/1559131480863543296) | true | `/test pull-machine-controller-e2e-kubevirt`
pull-machine-controller-e2e-ubuntu-upgrade | f2bcee9525ecb98bcc4797e8a0dce6604f470019 | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1406/pull-machine-controller-e2e-ubuntu-upgrade/1559131475163484160) | true | `/test pull-machine-controller-e2e-ubuntu-upgrade`
pull-machine-controller-e2e-openstack | f2bcee9525ecb98bcc4797e8a0dce6604f470019 | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1406/pull-machine-controller-e2e-openstack/1559131481702404096) | true | `/test pull-machine-controller-e2e-openstack`

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

<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 -->
kubermatic-bot: @LittleFox94: The following tests **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-kubevirt | f2bcee9525ecb98bcc4797e8a0dce6604f470019 | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1406/pull-machine-controller-e2e-kubevirt/1559131480863543296) | true | `/test pull-machine-controller-e2e-kubevirt`
pull-machine-controller-e2e-ubuntu-upgrade | f2bcee9525ecb98bcc4797e8a0dce6604f470019 | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1406/pull-machine-controller-e2e-ubuntu-upgrade/1559131475163484160) | true | `/test pull-machine-controller-e2e-ubuntu-upgrade`
pull-machine-controller-e2e-openstack | f2bcee9525ecb98bcc4797e8a0dce6604f470019 | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1406/pull-machine-controller-e2e-openstack/1559131481702404096) | true | `/test pull-machine-controller-e2e-openstack`
pull-machine-controller-e2e-openstack-project-auth | f2bcee9525ecb98bcc4797e8a0dce6604f470019 | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1406/pull-machine-controller-e2e-openstack-project-auth/1559131482541264896) | true | `/test pull-machine-controller-e2e-openstack-project-auth`

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

<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 -->
kubermatic-bot: @LittleFox94: The following tests **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-kubevirt | f2bcee9525ecb98bcc4797e8a0dce6604f470019 | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1406/pull-machine-controller-e2e-kubevirt/1559131480863543296) | true | `/test pull-machine-controller-e2e-kubevirt`
pull-machine-controller-e2e-ubuntu-upgrade | f2bcee9525ecb98bcc4797e8a0dce6604f470019 | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1406/pull-machine-controller-e2e-ubuntu-upgrade/1559131475163484160) | true | `/test pull-machine-controller-e2e-ubuntu-upgrade`
pull-machine-controller-e2e-openstack | f2bcee9525ecb98bcc4797e8a0dce6604f470019 | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1406/pull-machine-controller-e2e-openstack/1559131481702404096) | true | `/test pull-machine-controller-e2e-openstack`
pull-machine-controller-e2e-openstack-project-auth | f2bcee9525ecb98bcc4797e8a0dce6604f470019 | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1406/pull-machine-controller-e2e-openstack-project-auth/1559131482541264896) | true | `/test pull-machine-controller-e2e-openstack-project-auth`
pull-machine-controller-e2e-vsphere | f2bcee9525ecb98bcc4797e8a0dce6604f470019 | [link](https://public-prow.loodse.com/view/gs/prow-dev-public-data/pr-logs/pull/kubermatic_machine-controller/1406/pull-machine-controller-e2e-vsphere/1559131483380125696) | true | `/test pull-machine-controller-e2e-vsphere`

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

<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/.prow/provider-anexia.yaml b/.prow/provider-anexia.yaml
index 96a7ab09..807b1adf 100644
--- a/.prow/provider-anexia.yaml
+++ b/.prow/provider-anexia.yaml
@@ -32,6 +32,10 @@ presubmits:
             - "./hack/ci/run-e2e-tests.sh"
           args:
             - "TestAnexiaProvisioningE2E"
+          env:
+            # OperatingSystemManager does not yet support Anexia
+            - name: OPERATING_SYSTEM_MANAGER
+              value: "false"
           securityContext:
             privileged: true
           resources:
diff --git a/.prow/provider-vsphere.yaml b/.prow/provider-vsphere.yaml
index 104d807c..ece98554 100644
--- a/.prow/provider-vsphere.yaml
+++ b/.prow/provider-vsphere.yaml
@@ -20,7 +20,7 @@ presubmits:
     labels:
       preset-hetzner: "true"
       preset-e2e-ssh: "true"
-      preset-vsphere-legacy: "true"
+      preset-vsphere: "true"
       preset-rhel: "true"
       preset-goproxy: "true"
       preset-kind-volume-mounts: "true"
diff --git a/Makefile b/Makefile
index bfb27d53..feba5331 100644
--- a/Makefile
+++ b/Makefile
@@ -91,7 +91,7 @@ test-unit-docker:
 		-e GOCACHE=/cache \
 		-w /go/src/github.com/kubermatic/machine-controller \
 		golang:$(GO_VERSION) \
-			make test-unit GOFLAGS=$(GOFLAGS)
+			make test-unit "GOFLAGS=$(GOFLAGS)"
 
 .PHONY: test-unit
 test-unit:
diff --git a/docs/cloud-provider.md b/docs/cloud-provider.md
index c18284f4..75a06564 100644
--- a/docs/cloud-provider.md
+++ b/docs/cloud-provider.md
@@ -152,7 +152,8 @@ tags:
 ### machine.spec.providerConfig.cloudProviderSpec
 
 ```yaml
-serviceAccount: "<< GOOGLE_SERVICE_ACCOUNT >>"
+# The service account needs to be base64-encoded.
+serviceAccount: "<< GOOGLE_SERVICE_ACCOUNT_BASE64 >>"
 # See https://cloud.google.com/compute/docs/regions-zones/
 zone: "europe-west3-a"
 # See https://cloud.google.com/compute/docs/machine-types
@@ -302,8 +303,8 @@ tags:
 
 ### machine.spec.providerConfig.cloudProviderSpec
 ```yaml
-# kubeconfig to access KubeVirt cluster
-kubeconfig: '<< KUBECONFIG >>'
+# base64-encoded kubeconfig to access KubeVirt cluster
+kubeconfig: '<< KUBECONFIG_BASE64 >>'
 # KubeVirt namespace
 namespace: kube-system
 # kubernetes storage class
diff --git a/examples/anexia-machinedeployment.yaml b/examples/anexia-machinedeployment.yaml
index 156cfd21..f5210a44 100644
--- a/examples/anexia-machinedeployment.yaml
+++ b/examples/anexia-machinedeployment.yaml
@@ -36,7 +36,15 @@ spec:
             locationID: "<< ANEXIA_LOCATION_ID >>"
             cpus: 2
             memory: 2048
-            diskSize: 60
+
+            # only a single disk is currently supported, but support for multiple disks is planned already
+            disks:
+            - size: 60
+              performanceType: ENT6
+
+            # You may have this old disk config attribute in your config - please migrate to the disks attribute.
+            # For now it is still recognized though.
+            #diskSize: 60
           # Flatcar is the only supported operating system
           operatingSystem: "flatcar"
           operatingSystemSpec:
diff --git a/examples/gce-machinedeployment.yaml b/examples/gce-machinedeployment.yaml
index 37c8eecb..c7ab56ff 100644
--- a/examples/gce-machinedeployment.yaml
+++ b/examples/gce-machinedeployment.yaml
@@ -6,8 +6,13 @@ metadata:
   name: machine-controller-gce
   namespace: kube-system
 type: Opaque
-stringData:
-  serviceAccount: "<< GOOGLE_SERVICE_ACCOUNT >>"
+data:
+  # The base64 encoding here is only to satisfy Kubernetes'
+  # Secret storage and to prevent multiline string replacement
+  # issues if we used stringData here (because the GCP SA is
+  # a multiline JSON string).
+  serviceAccount: "<< GOOGLE_SERVICE_ACCOUNT_BASE64 >>"
+
 ---
 apiVersion: "cluster.k8s.io/v1alpha1"
 kind: MachineDeployment
diff --git a/examples/kubevirt-machinedeployment.yaml b/examples/kubevirt-machinedeployment.yaml
index a40b9570..c7d959a2 100644
--- a/examples/kubevirt-machinedeployment.yaml
+++ b/examples/kubevirt-machinedeployment.yaml
@@ -28,9 +28,9 @@ spec:
           cloudProviderSpec:
             auth:
               kubeconfig:
-                # Can also be set via the env var 'KUBEVIRT_KUBECONFIG' on the machine-controller
-                # If specified directly, this value should be a base64 encoded kubeconfig in either yaml or json format.
-                value: "<< KUBECONFIG >>"
+                # Can also be set via the env var 'KUBEVIRT_KUBECONFIG' on the machine-controller.
+                # If instead specified directly, this value should be a base64 encoded kubeconfig.
+                value: "<< KUBECONFIG_BASE64 >>"
             virtualMachine:
               template:
                 cpus: "1"
diff --git a/examples/operating-system-manager.yaml b/examples/operating-system-manager.yaml
index 15087182..9e513819 100644
--- a/examples/operating-system-manager.yaml
+++ b/examples/operating-system-manager.yaml
@@ -1076,7 +1076,7 @@ spec:
     spec:
       serviceAccountName: operating-system-manager-webhook
       containers:
-        - image: quay.io/kubermatic/operating-system-manager:v0.6.0
+        - image: quay.io/kubermatic/operating-system-manager:v1.0.0
           imagePullPolicy: IfNotPresent
           name: webhook
           command:
@@ -1377,7 +1377,7 @@ spec:
     spec:
       serviceAccountName: operating-system-manager
       containers:
-        - image: quay.io/kubermatic/operating-system-manager:v0.6.0
+        - image: quay.io/kubermatic/operating-system-manager:v1.0.0
           imagePullPolicy: IfNotPresent
           name: operating-system-manager
           command:
diff --git a/go.mod b/go.mod
index 6ece1c6a..bc2ae6ae 100644
--- a/go.mod
+++ b/go.mod
@@ -44,7 +44,7 @@ require (
 	gopkg.in/gcfg.v1 v1.2.3
 	gopkg.in/yaml.v3 v3.0.1
 	// Please ensure that you update the image tags in `examples/operating-system-manager.yaml` as well.
-	k8c.io/operating-system-manager v0.6.0
+	k8c.io/operating-system-manager v1.0.0
 	k8s.io/api v0.24.2
 	k8s.io/apiextensions-apiserver v0.24.2
 	k8s.io/apimachinery v0.24.2
diff --git a/go.sum b/go.sum
index b138f343..a3615c58 100644
--- a/go.sum
+++ b/go.sum
@@ -1521,8 +1521,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-k8c.io/operating-system-manager v0.6.0 h1:c+WJOV+BlW9NgSi7/QCNKCTXVwcW89s3PlWQDqQBRhA=
-k8c.io/operating-system-manager v0.6.0/go.mod h1:8Q1xpjJomTG9X6lfx/y3+yGHCackHtqxuYEk0TIPMfA=
+k8c.io/operating-system-manager v1.0.0 h1:E1dCaLHypgaaLNgm50jcT3uwk3vok3xWYOnFcspXJ38=
+k8c.io/operating-system-manager v1.0.0/go.mod h1:8Q1xpjJomTG9X6lfx/y3+yGHCackHtqxuYEk0TIPMfA=
 k8s.io/api v0.23.3/go.mod h1:w258XdGyvCmnBj/vGzQMj6kzdufJZVUwEM1U2fRJwSQ=
 k8s.io/api v0.24.2 h1:g518dPU/L7VRLxWfcadQn2OnsiGWVOadTLpdnqgY2OI=
 k8s.io/api v0.24.2/go.mod h1:AHqbSkTm6YrQ0ObxjO3Pmp/ubFF/KuM7jU+3khoBsOg=
diff --git a/pkg/cloudprovider/provider/anexia/instance.go b/pkg/cloudprovider/provider/anexia/instance.go
index cc9a9e60..fa53467a 100644
--- a/pkg/cloudprovider/provider/anexia/instance.go
+++ b/pkg/cloudprovider/provider/anexia/instance.go
@@ -17,6 +17,8 @@ limitations under the License.
 package anexia
 
 import (
+	"net"
+
 	"go.anx.io/go-anxcloud/pkg/vsphere/info"
 
 	"github.com/kubermatic/machine-controller/pkg/cloudprovider/instance"
@@ -26,7 +28,8 @@ import (
 )
 
 type anexiaInstance struct {
-	info *info.Info
+	info              *info.Info
+	reservedAddresses []string
 }
 
 func (ai *anexiaInstance) Name() string {
@@ -52,19 +55,30 @@ func (ai *anexiaInstance) ProviderID() string {
 func (ai *anexiaInstance) Addresses() map[string]v1.NodeAddressType {
 	addresses := map[string]v1.NodeAddressType{}
 
-	if ai.info == nil {
-		return addresses
+	if ai.reservedAddresses != nil {
+		for _, reservedIP := range ai.reservedAddresses {
+			addresses[reservedIP] = v1.NodeExternalIP
+		}
 	}
 
-	for _, network := range ai.info.Network {
-		for _, ip := range network.IPv4 {
-			addresses[ip] = v1.NodeExternalIP
+	if ai.info != nil {
+		for _, network := range ai.info.Network {
+			for _, ip := range network.IPv4 {
+				addresses[ip] = v1.NodeExternalIP
+			}
+			for _, ip := range network.IPv6 {
+				addresses[ip] = v1.NodeExternalIP
+			}
 		}
-		for _, ip := range network.IPv6 {
+	}
+
+	for ip := range addresses {
+		parsed := net.ParseIP(ip)
+		if parsed.IsPrivate() {
+			addresses[ip] = v1.NodeInternalIP
+		} else {
 			addresses[ip] = v1.NodeExternalIP
 		}
-
-		// TODO mark RFC1918 and RFC4193 addresses as internal
 	}
 
 	return addresses
diff --git a/pkg/cloudprovider/provider/anexia/provider.go b/pkg/cloudprovider/provider/anexia/provider.go
index 7d451e84..84172372 100644
--- a/pkg/cloudprovider/provider/anexia/provider.go
+++ b/pkg/cloudprovider/provider/anexia/provider.go
@@ -37,7 +37,6 @@ import (
 	cloudprovidererrors "github.com/kubermatic/machine-controller/pkg/cloudprovider/errors"
 	"github.com/kubermatic/machine-controller/pkg/cloudprovider/instance"
 	anxtypes "github.com/kubermatic/machine-controller/pkg/cloudprovider/provider/anexia/types"
-	"github.com/kubermatic/machine-controller/pkg/cloudprovider/provider/anexia/utils"
 	cloudprovidertypes "github.com/kubermatic/machine-controller/pkg/cloudprovider/types"
 	"github.com/kubermatic/machine-controller/pkg/providerconfig"
 	providerconfigtypes "github.com/kubermatic/machine-controller/pkg/providerconfig/types"
@@ -54,10 +53,37 @@ const (
 	ProvisionedType = "Provisioned"
 )
 
+var (
+	// ErrConfigDiskSizeAndDisks is returned when the config has both DiskSize and Disks set, which is unsupported.
+	ErrConfigDiskSizeAndDisks = errors.New("both the deprecated DiskSize and new Disks attribute are set")
+
+	// ErrMultipleDisksNotYetImplemented is returned when multiple disks are configured.
+	ErrMultipleDisksNotYetImplemented = errors.New("multiple disks configured, but this feature is not yet implemented")
+)
+
 type provider struct {
 	configVarResolver *providerconfig.ConfigVarResolver
 }
 
+// resolvedDisk contains the resolved values from types.RawDisk.
+type resolvedDisk struct {
+	anxtypes.RawDisk
+
+	PerformanceType string
+}
+
+// resolvedConfig contains the resolved values from types.RawConfig.
+type resolvedConfig struct {
+	anxtypes.RawConfig
+
+	Token      string
+	VlanID     string
+	LocationID string
+	TemplateID string
+
+	Disks []resolvedDisk
+}
+
 func (p *provider) Create(ctx context.Context, machine *clusterv1alpha1.Machine, data *cloudprovidertypes.ProviderData, userdata string) (instance instance.Instance, retErr error) {
 	status := getProviderStatus(machine)
 	klog.V(3).Infof(fmt.Sprintf("'%s' has status %#v", machine.Name, status))
@@ -70,10 +96,10 @@ func (p *provider) Create(ctx context.Context, machine *clusterv1alpha1.Machine,
 		return nil, fmt.Errorf("unable to get provider config: %w", err)
 	}
 
-	ctx = utils.CreateReconcileContext(ctx, utils.ReconcileContext{
+	ctx = createReconcileContext(ctx, reconcileContext{
 		Status:       &status,
 		UserData:     userdata,
-		Config:       config,
+		Config:       *config,
 		ProviderData: data,
 		Machine:      machine,
 	})
@@ -108,7 +134,7 @@ func (p *provider) Create(ctx context.Context, machine *clusterv1alpha1.Machine,
 }
 
 func waitForVM(ctx context.Context, client anxclient.Client) error {
-	reconcileContext := utils.GetReconcileContext(ctx)
+	reconcileContext := getReconcileContext(ctx)
 	api := vsphere.NewAPI(client)
 	var identifier string
 	err := wait.PollImmediate(5*time.Second, 1*time.Minute, func() (bool, error) {
@@ -135,7 +161,7 @@ func waitForVM(ctx context.Context, client anxclient.Client) error {
 }
 
 func provisionVM(ctx context.Context, client anxclient.Client) error {
-	reconcileContext := utils.GetReconcileContext(ctx)
+	reconcileContext := getReconcileContext(ctx)
 	vmAPI := vsphere.NewAPI(client)
 
 	ctx, cancel := context.WithTimeout(ctx, anxtypes.CreateRequestTimeout)
@@ -164,12 +190,15 @@ func provisionVM(ctx context.Context, client anxclient.Client) error {
 			reconcileContext.Machine.Name,
 			config.CPUs,
 			config.Memory,
-			config.DiskSize,
+			config.Disks[0].Size,
 			networkInterfaces,
 		)
 
+		vm.DiskType = config.Disks[0].PerformanceType
+
 		vm.Script = base64.StdEncoding.EncodeToString([]byte(reconcileContext.UserData))
 
+		// We generate a fresh SSH key but will never actually use it - we just want a valid public key to disable password authentication for our fresh VM.
 		sshKey, err := ssh.NewKey()
 		if err != nil {
 			return newError(common.CreateMachineError, "failed to generate ssh key: %v", err)
@@ -220,14 +249,15 @@ func provisionVM(ctx context.Context, client anxclient.Client) error {
 }
 
 func getIPAddress(ctx context.Context, client anxclient.Client) (string, error) {
-	reconcileContext := utils.GetReconcileContext(ctx)
+	reconcileContext := getReconcileContext(ctx)
 	status := reconcileContext.Status
 
 	// only use IP if it is still unbound
 	if status.ReservedIP != "" && status.IPState == anxtypes.IPStateUnbound {
-		klog.Info("reusing already provisioned ip", "IP", status.ReservedIP)
+		klog.Infof("reusing already provisioned ip %q", status.ReservedIP)
 		return status.ReservedIP, nil
 	}
+
 	klog.Info(fmt.Sprintf("Creating a new IP for machine %q", reconcileContext.Machine.Name))
 	addrAPI := anxaddr.NewAPI(client)
 	config := reconcileContext.Config
@@ -251,7 +281,7 @@ func getIPAddress(ctx context.Context, client anxclient.Client) (string, error)
 }
 
 func isAlreadyProvisioning(ctx context.Context) bool {
-	status := utils.GetReconcileContext(ctx).Status
+	status := getReconcileContext(ctx).Status
 	condition := meta.FindStatusCondition(status.Conditions, ProvisionedType)
 	lastChange := condition.LastTransitionTime.Time
 	const reasonInProvisioning = "InProvisioning"
@@ -278,50 +308,85 @@ func ensureConditions(status *anxtypes.ProviderStatus) {
 	}
 }
 
-func (p *provider) getConfig(provSpec clusterv1alpha1.ProviderSpec) (*anxtypes.Config, *providerconfigtypes.Config, error) {
-	if provSpec.Value == nil {
-		return nil, nil, fmt.Errorf("machine.spec.providerSpec.value is nil")
+func (p *provider) resolveConfig(config anxtypes.RawConfig) (*resolvedConfig, error) {
+	var err error
+	ret := resolvedConfig{
+		RawConfig: config,
 	}
-	pconfig, err := providerconfigtypes.GetConfig(provSpec)
+
+	ret.Token, err = p.configVarResolver.GetConfigVarStringValueOrEnv(config.Token, anxtypes.AnxTokenEnv)
 	if err != nil {
-		return nil, nil, err
+		return nil, fmt.Errorf("failed to get 'token': %w", err)
 	}
 
-	if pconfig.OperatingSystemSpec.Raw == nil {
-		return nil, nil, errors.New("operatingSystemSpec in the MachineDeployment cannot be empty")
+	ret.LocationID, err = p.configVarResolver.GetConfigVarStringValue(config.LocationID)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get 'locationID': %w", err)
 	}
 
-	rawConfig, err := anxtypes.GetConfig(*pconfig)
+	ret.TemplateID, err = p.configVarResolver.GetConfigVarStringValue(config.TemplateID)
 	if err != nil {
-		return nil, nil, err
+		return nil, fmt.Errorf("failed to get 'templateID': %w", err)
 	}
 
-	c := anxtypes.Config{}
-	c.Token, err = p.configVarResolver.GetConfigVarStringValueOrEnv(rawConfig.Token, anxtypes.AnxTokenEnv)
+	ret.VlanID, err = p.configVarResolver.GetConfigVarStringValue(config.VlanID)
 	if err != nil {
-		return nil, nil, fmt.Errorf("failed to get 'token': %w", err)
+		return nil, fmt.Errorf("failed to get 'vlanID': %w", err)
+	}
+
+	if config.DiskSize != 0 {
+		if len(config.Disks) != 0 {
+			return nil, ErrConfigDiskSizeAndDisks
+		}
+
+		klog.Warningf("Configuration uses the deprecated DiskSize attribute, please migrate to the Disks array instead.")
+
+		config.Disks = []anxtypes.RawDisk{
+			{
+				Size: config.DiskSize,
+			},
+		}
+		config.DiskSize = 0
+	}
+
+	ret.Disks = make([]resolvedDisk, len(config.Disks))
+
+	for idx, disk := range config.Disks {
+		ret.Disks[idx].RawDisk = disk
+
+		ret.Disks[idx].PerformanceType, err = p.configVarResolver.GetConfigVarStringValue(disk.PerformanceType)
+		if err != nil {
+			return nil, fmt.Errorf("failed to get 'performanceType' of disk %v: %w", idx, err)
+		}
 	}
 
-	c.CPUs = rawConfig.CPUs
-	c.Memory = rawConfig.Memory
-	c.DiskSize = rawConfig.DiskSize
+	return &ret, nil
+}
 
-	c.LocationID, err = p.configVarResolver.GetConfigVarStringValue(rawConfig.LocationID)
+func (p *provider) getConfig(provSpec clusterv1alpha1.ProviderSpec) (*resolvedConfig, *providerconfigtypes.Config, error) {
+	if provSpec.Value == nil {
+		return nil, nil, fmt.Errorf("machine.spec.providerSpec.value is nil")
+	}
+	pconfig, err := providerconfigtypes.GetConfig(provSpec)
 	if err != nil {
-		return nil, nil, fmt.Errorf("failed to get 'locationID': %w", err)
+		return nil, nil, err
 	}
 
-	c.TemplateID, err = p.configVarResolver.GetConfigVarStringValue(rawConfig.TemplateID)
+	if pconfig.OperatingSystemSpec.Raw == nil {
+		return nil, nil, errors.New("operatingSystemSpec in the MachineDeployment cannot be empty")
+	}
+
+	rawConfig, err := anxtypes.GetConfig(*pconfig)
 	if err != nil {
-		return nil, nil, fmt.Errorf("failed to get 'templateID': %w", err)
+		return nil, nil, fmt.Errorf("error parsing provider config: %w", err)
 	}
 
-	c.VlanID, err = p.configVarResolver.GetConfigVarStringValue(rawConfig.VlanID)
+	resolvedConfig, err := p.resolveConfig(*rawConfig)
 	if err != nil {
-		return nil, nil, fmt.Errorf("failed to get 'vlanID': %w", err)
+		return nil, nil, fmt.Errorf("error resolving config: %w", err)
 	}
 
-	return &c, pconfig, nil
+	return resolvedConfig, pconfig, nil
 }
 
 // New returns an Anexia provider.
@@ -349,8 +414,18 @@ func (p *provider) Validate(_ context.Context, machinespec clusterv1alpha1.Machi
 		return errors.New("cpu count is missing")
 	}
 
-	if config.DiskSize == 0 {
-		return errors.New("disk size is missing")
+	if len(config.Disks) == 0 {
+		return errors.New("no disks configured")
+	}
+
+	if len(config.Disks) > 1 {
+		return ErrMultipleDisksNotYetImplemented
+	}
+
+	for _, disk := range config.Disks {
+		if disk.Size == 0 {
+			return errors.New("disk size is missing")
+		}
 	}
 
 	if config.Memory == 0 {
@@ -375,7 +450,7 @@ func (p *provider) Validate(_ context.Context, machinespec clusterv1alpha1.Machi
 func (p *provider) Get(ctx context.Context, machine *clusterv1alpha1.Machine, _ *cloudprovidertypes.ProviderData) (instance.Instance, error) {
 	config, _, err := p.getConfig(machine.Spec.ProviderSpec)
 	if err != nil {
-		return nil, newError(common.InvalidConfigurationMachineError, "failed to parse MachineSpec: %v", err)
+		return nil, newError(common.InvalidConfigurationMachineError, "failed to retrieve config: %v", err)
 	}
 
 	cli, err := getClient(config.Token)
@@ -388,10 +463,17 @@ func (p *provider) Get(ctx context.Context, machine *clusterv1alpha1.Machine, _
 	if err != nil {
 		return nil, newError(common.InvalidConfigurationMachineError, "failed to get machine status: %v", err)
 	}
+
 	if status.InstanceID == "" {
 		return nil, cloudprovidererrors.ErrInstanceNotFound
 	}
 
+	instance := anexiaInstance{}
+
+	if status.IPState == anxtypes.IPStateBound && status.ReservedIP != "" {
+		instance.reservedAddresses = []string{status.ReservedIP}
+	}
+
 	timeoutCtx, cancel := context.WithTimeout(ctx, anxtypes.GetRequestTimeout)
 	defer cancel()
 
@@ -399,10 +481,9 @@ func (p *provider) Get(ctx context.Context, machine *clusterv1alpha1.Machine, _
 	if err != nil {
 		return nil, fmt.Errorf("failed get machine info: %w", err)
 	}
+	instance.info = &info
 
-	return &anexiaInstance{
-		info: &info,
-	}, nil
+	return &instance, nil
 }
 
 func (p *provider) GetCloudConfig(_ clusterv1alpha1.MachineSpec) (string, string, error) {
diff --git a/pkg/cloudprovider/provider/anexia/utils/utils.go b/pkg/cloudprovider/provider/anexia/reconcile_context.go
similarity index 70%
rename from pkg/cloudprovider/provider/anexia/utils/utils.go
rename to pkg/cloudprovider/provider/anexia/reconcile_context.go
index 51a2a549..dea3577c 100644
--- a/pkg/cloudprovider/provider/anexia/utils/utils.go
+++ b/pkg/cloudprovider/provider/anexia/reconcile_context.go
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package utils
+package anexia
 
 import (
 	"context"
@@ -26,24 +26,25 @@ import (
 
 type contextKey byte
 
-const MachineReconcileContextKey contextKey = 0
+const machineReconcileContextKey contextKey = 0
 
-type ReconcileContext struct {
+type reconcileContext struct {
 	Machine      *v1alpha1.Machine
 	Status       *anxtypes.ProviderStatus
 	UserData     string
-	Config       *anxtypes.Config
+	Config       resolvedConfig
 	ProviderData *cloudprovidertypes.ProviderData
 }
 
-func CreateReconcileContext(ctx context.Context, cc ReconcileContext) context.Context {
-	return context.WithValue(ctx, MachineReconcileContextKey, cc)
+func createReconcileContext(ctx context.Context, cc reconcileContext) context.Context {
+	return context.WithValue(ctx, machineReconcileContextKey, cc)
 }
 
-func GetReconcileContext(ctx context.Context) ReconcileContext {
-	rawContext := ctx.Value(MachineReconcileContextKey)
-	if recContext, ok := rawContext.(ReconcileContext); ok {
+func getReconcileContext(ctx context.Context) reconcileContext {
+	rawContext := ctx.Value(machineReconcileContextKey)
+	if recContext, ok := rawContext.(reconcileContext); ok {
 		return recContext
 	}
-	return ReconcileContext{}
+
+	return reconcileContext{}
 }
diff --git a/pkg/cloudprovider/provider/anexia/types/types.go b/pkg/cloudprovider/provider/anexia/types/types.go
index dd0faca8..25547e9d 100644
--- a/pkg/cloudprovider/provider/anexia/types/types.go
+++ b/pkg/cloudprovider/provider/anexia/types/types.go
@@ -46,14 +46,26 @@ var StatusUpdateFailed = cloudprovidererrors.TerminalError{
 	Message: "Unable to update the machine status",
 }
 
+// RawDisk specifies a single disk, with some values maybe being fetched from secrets.
+type RawDisk struct {
+	Size            int                                 `json:"size"`
+	PerformanceType providerconfigtypes.ConfigVarString `json:"performanceType"`
+}
+
+// RawConfig contains all the configuration values for VMs to create, with some values maybe being fetched from secrets.
 type RawConfig struct {
 	Token      providerconfigtypes.ConfigVarString `json:"token,omitempty"`
 	VlanID     providerconfigtypes.ConfigVarString `json:"vlanID"`
 	LocationID providerconfigtypes.ConfigVarString `json:"locationID"`
 	TemplateID providerconfigtypes.ConfigVarString `json:"templateID"`
-	CPUs       int                                 `json:"cpus"`
-	Memory     int                                 `json:"memory"`
-	DiskSize   int                                 `json:"diskSize"`
+
+	CPUs   int `json:"cpus"`
+	Memory int `json:"memory"`
+
+	// Deprecated, use Disks instead.
+	DiskSize int `json:"diskSize"`
+
+	Disks []RawDisk `json:"disks"`
 }
 
 type ProviderStatus struct {
@@ -65,16 +77,6 @@ type ProviderStatus struct {
 	Conditions       []v1.Condition `json:"conditions,omitempty"`
 }
 
-type Config struct {
-	Token      string
-	VlanID     string
-	LocationID string
-	TemplateID string
-	CPUs       int
-	Memory     int
-	DiskSize   int
-}
-
 func GetConfig(pconfig providerconfigtypes.Config) (*RawConfig, error) {
 	rawConfig := &RawConfig{}
 
diff --git a/pkg/cloudprovider/provider/gce/config.go b/pkg/cloudprovider/provider/gce/config.go
index cacea48e..934adca0 100644
--- a/pkg/cloudprovider/provider/gce/config.go
+++ b/pkg/cloudprovider/provider/gce/config.go
@@ -226,17 +226,23 @@ func newConfig(resolver *providerconfig.ConfigVarResolver, spec v1alpha1.Provide
 // postprocessServiceAccount processes the service account and creates a JWT configuration
 // out of it.
 func (cfg *config) postprocessServiceAccount() error {
-	sa, err := base64.StdEncoding.DecodeString(cfg.serviceAccount)
-	if err != nil {
-		return fmt.Errorf("failed to decode base64 service account: %w", err)
+	sa := cfg.serviceAccount
+
+	// safely decode the service account, in case we did not read the value
+	// from a "known-safe" location (like the MachineDeployment), but from
+	// an environment variable.
+	decoded, err := base64.StdEncoding.DecodeString(cfg.serviceAccount)
+	if err == nil {
+		sa = string(decoded)
 	}
+
 	sam := map[string]string{}
-	err = json.Unmarshal(sa, &sam)
+	err = json.Unmarshal([]byte(sa), &sam)
 	if err != nil {
 		return fmt.Errorf("failed unmarshalling service account: %w", err)
 	}
 	cfg.projectID = sam["project_id"]
-	cfg.jwtConfig, err = google.JWTConfigFromJSON(sa, compute.ComputeScope)
+	cfg.jwtConfig, err = google.JWTConfigFromJSON([]byte(sa), compute.ComputeScope)
 	if err != nil {
 		return fmt.Errorf("failed preparing JWT: %w", err)
 	}
diff --git a/pkg/cloudprovider/provider/gce/types/types.go b/pkg/cloudprovider/provider/gce/types/types.go
index c1b059d8..0ac0f333 100644
--- a/pkg/cloudprovider/provider/gce/types/types.go
+++ b/pkg/cloudprovider/provider/gce/types/types.go
@@ -30,6 +30,7 @@ import (
 // CloudProviderSpec contains the specification of the cloud provider taken
 // from the provider configuration.
 type CloudProviderSpec struct {
+	// ServiceAccount must be base64-encoded.
 	ServiceAccount               providerconfigtypes.ConfigVarString  `json:"serviceAccount,omitempty"`
 	Zone                         providerconfigtypes.ConfigVarString  `json:"zone"`
 	MachineType                  providerconfigtypes.ConfigVarString  `json:"machineType"`


Test patch:
diff --git a/pkg/cloudprovider/provider/anexia/instance_test.go b/pkg/cloudprovider/provider/anexia/instance_test.go
new file mode 100644
index 00000000..0d7641a5
--- /dev/null
+++ b/pkg/cloudprovider/provider/anexia/instance_test.go
@@ -0,0 +1,127 @@
+/*
+Copyright 2022 The Machine Controller Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package anexia
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/testhelper"
+
+	"go.anx.io/go-anxcloud/pkg/vsphere/info"
+	v1 "k8s.io/api/core/v1"
+)
+
+func TestAnexiaInstance(t *testing.T) {
+	addressCheck := func(t *testing.T, testcase string, instance *anexiaInstance, expected map[string]v1.NodeAddressType) {
+		t.Run(testcase, func(t *testing.T) {
+			addresses := instance.Addresses()
+
+			testhelper.AssertDeepEquals(t, expected, addresses)
+		})
+	}
+
+	t.Run("empty instance", func(t *testing.T) {
+		instance := anexiaInstance{}
+		addressCheck(t, "no addresses", &instance, map[string]v1.NodeAddressType{})
+	})
+
+	t.Run("instance with only reservedAddresses set", func(t *testing.T) {
+		instance := anexiaInstance{
+			reservedAddresses: []string{"10.0.0.2", "fda0:23::2", "8.8.8.8", "2001:db8::2"},
+		}
+
+		addressCheck(t, "expected addresses", &instance, map[string]v1.NodeAddressType{
+			"10.0.0.2":    v1.NodeInternalIP,
+			"fda0:23::2":  v1.NodeInternalIP,
+			"8.8.8.8":     v1.NodeExternalIP,
+			"2001:db8::2": v1.NodeExternalIP,
+		})
+	})
+
+	t.Run("instance with only info set", func(t *testing.T) {
+		instance := anexiaInstance{
+			info: &info.Info{
+				Network: []info.Network{
+					{
+						IPv4: []string{"10.0.0.2"},
+						IPv6: []string{"fda0:23::2"},
+					},
+					{
+						IPv4: []string{"8.8.8.8"},
+						IPv6: []string{"2001:db8::2"},
+					},
+				},
+			},
+		}
+
+		addressCheck(t, "expected addresses", &instance, map[string]v1.NodeAddressType{
+			"10.0.0.2":    v1.NodeInternalIP,
+			"fda0:23::2":  v1.NodeInternalIP,
+			"8.8.8.8":     v1.NodeExternalIP,
+			"2001:db8::2": v1.NodeExternalIP,
+		})
+	})
+
+	t.Run("instance with both reservedAddresses and info set, full overlapping set", func(t *testing.T) {
+		instance := anexiaInstance{
+			reservedAddresses: []string{"10.0.0.2", "fda0:23::2", "8.8.8.8", "2001:db8::2"},
+			info: &info.Info{
+				Network: []info.Network{
+					{
+						IPv4: []string{"10.0.0.2"},
+						IPv6: []string{"fda0:23::2"},
+					},
+					{
+						IPv4: []string{"8.8.8.8"},
+						IPv6: []string{"2001:db8::2"},
+					},
+				},
+			},
+		}
+
+		addressCheck(t, "expected addresses", &instance, map[string]v1.NodeAddressType{
+			"10.0.0.2":    v1.NodeInternalIP,
+			"fda0:23::2":  v1.NodeInternalIP,
+			"8.8.8.8":     v1.NodeExternalIP,
+			"2001:db8::2": v1.NodeExternalIP,
+		})
+	})
+
+	t.Run("instance with both reservedAddresses and info set, some overlap, each adding some", func(t *testing.T) {
+		instance := anexiaInstance{
+			reservedAddresses: []string{"10.0.0.2", "8.8.8.8", "2001:db8::2"},
+			info: &info.Info{
+				Network: []info.Network{
+					{
+						IPv4: []string{"10.0.0.2"},
+						IPv6: []string{"fda0:23::2"},
+					},
+					{
+						IPv6: []string{"2001:db8::2"},
+					},
+				},
+			},
+		}
+
+		addressCheck(t, "expected addresses", &instance, map[string]v1.NodeAddressType{
+			"10.0.0.2":    v1.NodeInternalIP,
+			"fda0:23::2":  v1.NodeInternalIP,
+			"8.8.8.8":     v1.NodeExternalIP,
+			"2001:db8::2": v1.NodeExternalIP,
+		})
+	})
+}
diff --git a/pkg/cloudprovider/provider/anexia/provider_test.go b/pkg/cloudprovider/provider/anexia/provider_test.go
index 8592f576..b26d29f3 100644
--- a/pkg/cloudprovider/provider/anexia/provider_test.go
+++ b/pkg/cloudprovider/provider/anexia/provider_test.go
@@ -33,7 +33,6 @@ import (
 	"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"
-	"github.com/kubermatic/machine-controller/pkg/cloudprovider/provider/anexia/utils"
 	cloudprovidertypes "github.com/kubermatic/machine-controller/pkg/cloudprovider/types"
 
 	"k8s.io/apimachinery/pkg/api/meta"
@@ -58,13 +57,13 @@ func TestAnexiaProvider(t *testing.T) {
 		testhelper.Mux.HandleFunc("/api/vsphere/v1/search/by_name.json", createSearchHandler(t, waitUntilVMIsFound))
 
 		providerStatus := anxtypes.ProviderStatus{}
-		ctx := utils.CreateReconcileContext(context.Background(), utils.ReconcileContext{
+		ctx := createReconcileContext(context.Background(), reconcileContext{
 			Machine: &v1alpha1.Machine{
 				ObjectMeta: metav1.ObjectMeta{Name: "TestMachine"},
 			},
 			Status:   &providerStatus,
 			UserData: "",
-			Config:   &anxtypes.Config{},
+			Config:   resolvedConfig{},
 
 			ProviderData: &cloudprovidertypes.ProviderData{
 				Update: func(m *clusterv1alpha1.Machine, mod ...cloudprovidertypes.MachineModifier) error {
@@ -150,19 +149,27 @@ func TestAnexiaProvider(t *testing.T) {
 		})
 
 		providerStatus := anxtypes.ProviderStatus{}
-		ctx := utils.CreateReconcileContext(context.Background(), utils.ReconcileContext{
+		ctx := createReconcileContext(context.Background(), reconcileContext{
 			Machine: &v1alpha1.Machine{
 				ObjectMeta: metav1.ObjectMeta{Name: "TestMachine"},
 			},
 			Status:   &providerStatus,
 			UserData: "",
-			Config: &anxtypes.Config{
+			Config: resolvedConfig{
 				VlanID:     "VLAN-ID",
 				LocationID: "LOCATION-ID",
 				TemplateID: "TEMPLATE-ID",
-				CPUs:       5,
-				Memory:     5,
-				DiskSize:   5,
+				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 {
@@ -186,10 +193,10 @@ func TestAnexiaProvider(t *testing.T) {
 				},
 			},
 		}
-		ctx := utils.CreateReconcileContext(context.Background(), utils.ReconcileContext{
+		ctx := createReconcileContext(context.Background(), reconcileContext{
 			Status:       &providerStatus,
 			UserData:     "",
-			Config:       nil,
+			Config:       resolvedConfig{},
 			ProviderData: nil,
 		})
 
@@ -214,7 +221,7 @@ func TestAnexiaProvider(t *testing.T) {
 			ReservedIP: "",
 			IPState:    "",
 		}
-		ctx := utils.CreateReconcileContext(context.Background(), utils.ReconcileContext{Status: providerStatus})
+		ctx := createReconcileContext(context.Background(), reconcileContext{Status: providerStatus})
 
 		t.Run("with unbound reserved IP", func(t *testing.T) {
 			expectedIP := "8.8.8.8"
@@ -230,42 +237,79 @@ 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
+	hookableConfig := func(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"),
+		}
+
+		if hook != nil {
+			hook(&config)
+		}
+
+		return config
+	}
+
 	var configCases []ConfigTestCase
 	configCases = append(configCases,
 		ConfigTestCase{
-			Config: anxtypes.RawConfig{},
+			Config: hookableConfig(func(c *anxtypes.RawConfig) { c.Token.Value = "" }),
 			Error:  errors.New("token is missing"),
 		},
 		ConfigTestCase{
-			Config: anxtypes.RawConfig{Token: newConfigVarString("TEST-TOKEN")},
+			Config: hookableConfig(func(c *anxtypes.RawConfig) { c.CPUs = 0 }),
 			Error:  errors.New("cpu count is missing"),
 		},
 		ConfigTestCase{
-			Config: anxtypes.RawConfig{Token: newConfigVarString("TEST-TOKEN"), CPUs: 1},
+			Config: hookableConfig(func(c *anxtypes.RawConfig) { c.Disks = []anxtypes.RawDisk{} }),
+			Error:  errors.New("no disks configured"),
+		},
+		ConfigTestCase{
+			Config: hookableConfig(func(c *anxtypes.RawConfig) { c.DiskSize = 10 }),
+			Error:  ErrConfigDiskSizeAndDisks,
+		},
+		ConfigTestCase{
+			Config: hookableConfig(func(c *anxtypes.RawConfig) { c.Disks = append(c.Disks, anxtypes.RawDisk{Size: 10}) }),
+			Error:  ErrMultipleDisksNotYetImplemented,
+		},
+		ConfigTestCase{
+			Config: hookableConfig(func(c *anxtypes.RawConfig) { c.Disks[0].Size = 0 }),
 			Error:  errors.New("disk size is missing"),
 		},
 		ConfigTestCase{
-			Config: anxtypes.RawConfig{Token: newConfigVarString("TEST-TOKEN"), CPUs: 1, DiskSize: 5},
+			Config: hookableConfig(func(c *anxtypes.RawConfig) { c.Memory = 0 }),
 			Error:  errors.New("memory size is missing"),
 		},
 		ConfigTestCase{
-			Config: anxtypes.RawConfig{Token: newConfigVarString("TEST-TOKEN"), CPUs: 1, DiskSize: 5, Memory: 5},
+			Config: hookableConfig(func(c *anxtypes.RawConfig) { c.LocationID.Value = "" }),
 			Error:  errors.New("location id is missing"),
 		},
 		ConfigTestCase{
-			Config: anxtypes.RawConfig{Token: newConfigVarString("TEST-TOKEN"), CPUs: 1, DiskSize: 5, Memory: 5,
-				LocationID: newConfigVarString("TLID")},
-			Error: errors.New("template id is missing"),
+			Config: hookableConfig(func(c *anxtypes.RawConfig) { c.TemplateID.Value = "" }),
+			Error:  errors.New("template id is missing"),
 		},
 		ConfigTestCase{
-			Config: anxtypes.RawConfig{Token: newConfigVarString("TEST-TOKEN"), CPUs: 1, DiskSize: 5, Memory: 5,
-				LocationID: newConfigVarString("LID"), TemplateID: newConfigVarString("TID")},
-			Error: errors.New("vlan id is missing"),
+			Config: hookableConfig(func(c *anxtypes.RawConfig) { c.VlanID.Value = "" }),
+			Error:  errors.New("vlan id is missing"),
 		},
 		ConfigTestCase{
-			Config: anxtypes.RawConfig{Token: newConfigVarString("TEST-TOKEN"), CPUs: 1, DiskSize: 5, Memory: 5,
-				LocationID: newConfigVarString("LID"), TemplateID: newConfigVarString("TID"), VlanID: newConfigVarString("VLAN")},
-			Error: nil,
+			Config: hookableConfig(func(c *anxtypes.RawConfig) { c.DiskSize = 10; c.Disks = []anxtypes.RawDisk{} }),
+			Error:  nil,
+		},
+		ConfigTestCase{
+			Config: hookableConfig(nil),
+			Error:  nil,
 		},
 	)
 
@@ -273,7 +317,9 @@ func TestValidate(t *testing.T) {
 	for _, testCase := range getSpecsForValidationTest(t, configCases) {
 		err := provider.Validate(context.Background(), testCase.Spec)
 		if testCase.ExpectedError != nil {
-			testhelper.AssertEquals(t, testCase.ExpectedError.Error(), err.Error())
+			if !errors.Is(err, testCase.ExpectedError) {
+				testhelper.AssertEquals(t, testCase.ExpectedError.Error(), err.Error())
+			}
 		} else {
 			testhelper.AssertEquals(t, testCase.ExpectedError, err)
 		}
diff --git a/test/e2e/provisioning/all_e2e_test.go b/test/e2e/provisioning/all_e2e_test.go
index c19c979c..25244065 100644
--- a/test/e2e/provisioning/all_e2e_test.go
+++ b/test/e2e/provisioning/all_e2e_test.go
@@ -20,6 +20,7 @@ package provisioning
 
 import (
 	"context"
+	"encoding/base64"
 	"flag"
 	"fmt"
 	"os"
@@ -293,12 +294,23 @@ func TestKubevirtProvisioningE2E(t *testing.T) {
 	selector := OsSelector("ubuntu", "centos", "flatcar", "rockylinux")
 
 	params := []string{
-		fmt.Sprintf("<< KUBECONFIG >>=%s", kubevirtKubeconfig),
+		fmt.Sprintf("<< KUBECONFIG_BASE64 >>=%s", safeBase64Encoding(kubevirtKubeconfig)),
 	}
 
 	runScenarios(t, selector, params, kubevirtManifest, fmt.Sprintf("kubevirt-%s", *testRunIdentifier))
 }
 
+// safeBase64Encoding takes a value and encodes it with base64
+// if it is not already encoded.
+func safeBase64Encoding(value string) string {
+	// If there was no error, the original value was already encoded.
+	if _, err := base64.StdEncoding.DecodeString(value); err == nil {
+		return value
+	}
+
+	return base64.StdEncoding.EncodeToString([]byte(value))
+}
+
 func TestOpenstackProvisioningE2E(t *testing.T) {
 	t.Parallel()
 
@@ -617,7 +629,7 @@ func TestAzureProvisioningE2E(t *testing.T) {
 		t.Fatal("unable to run the test suite, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, AZURE_CLIENT_ID and AZURE_CLIENT_SECRET environment variables cannot be empty")
 	}
 
-	selector := Not(OsSelector("sles", "amzn2", "rhel", "rockylinux", "flatcar"))
+	selector := Not(OsSelector("sles", "amzn2"))
 	// act
 	params := []string{
 		fmt.Sprintf("<< AZURE_TENANT_ID >>=%s", azureTenantID),
@@ -708,8 +720,9 @@ func TestGCEProvisioningE2E(t *testing.T) {
 	// Act. GCE does not support CentOS.
 	selector := OsSelector("ubuntu")
 	params := []string{
-		fmt.Sprintf("<< GOOGLE_SERVICE_ACCOUNT >>=%s", googleServiceAccount),
+		fmt.Sprintf("<< GOOGLE_SERVICE_ACCOUNT_BASE64 >>=%s", safeBase64Encoding(googleServiceAccount)),
 	}
+
 	runScenarios(t, selector, params, GCEManifest, fmt.Sprintf("gce-%s", *testRunIdentifier))
 }
 
@@ -858,7 +871,7 @@ func getVSphereTestParams(t *testing.T) []string {
 func TestVsphereProvisioningE2E(t *testing.T) {
 	t.Parallel()
 
-	selector := Not(OsSelector("sles", "amzn2"))
+	selector := Not(OsSelector("sles", "amzn2", "rockylinux"))
 	params := getVSphereTestParams(t)
 
 	runScenarios(t, selector, params, VSPhereManifest, fmt.Sprintf("vs-%s", *testRunIdentifier))
diff --git a/test/e2e/provisioning/helper.go b/test/e2e/provisioning/helper.go
index cefad450..39812773 100644
--- a/test/e2e/provisioning/helper.go
+++ b/test/e2e/provisioning/helper.go
@@ -57,11 +57,11 @@ var (
 	}
 
 	vSphereOSImageTemplates = map[string]string{
-		string(providerconfigtypes.OperatingSystemCentOS):     "machine-controller-e2e-centos",
-		string(providerconfigtypes.OperatingSystemFlatcar):    "machine-controller-e2e-flatcar",
-		string(providerconfigtypes.OperatingSystemRHEL):       "machine-controller-e2e-rhel",
-		string(providerconfigtypes.OperatingSystemRockyLinux): "machine-controller-e2e-rockylinux",
-		string(providerconfigtypes.OperatingSystemUbuntu):     "machine-controller-e2e-ubuntu",
+		string(providerconfigtypes.OperatingSystemCentOS):     "kkp-centos-7",
+		string(providerconfigtypes.OperatingSystemFlatcar):    "kkp-flatcar-3033.2.2",
+		string(providerconfigtypes.OperatingSystemRHEL):       "kkp-rhel-8.6",
+		string(providerconfigtypes.OperatingSystemRockyLinux): "kkp-rockylinux-8",
+		string(providerconfigtypes.OperatingSystemUbuntu):     "kkp-ubuntu-20.04",
 	}
 )
 
diff --git a/test/e2e/provisioning/testdata/machinedeployment-gce.yaml b/test/e2e/provisioning/testdata/machinedeployment-gce.yaml
index a2d9eb4d..5fb0a6c8 100644
--- a/test/e2e/provisioning/testdata/machinedeployment-gce.yaml
+++ b/test/e2e/provisioning/testdata/machinedeployment-gce.yaml
@@ -24,8 +24,9 @@ spec:
             - "<< YOUR_PUBLIC_KEY >>"
           cloudProvider: "gce"
           cloudProviderSpec:
-            # If empty, can be set via GOOGLE_SERVICE_ACCOUNT env var
-            serviceAccount: "<< GOOGLE_SERVICE_ACCOUNT >>"
+            # If empty, can be set via GOOGLE_SERVICE_ACCOUNT env var. The environment variable
+            # should be plaintext. The value in the cloudProviderSpec however must be base64-encoded.
+            serviceAccount: "<< GOOGLE_SERVICE_ACCOUNT_BASE64 >>"
             # See https://cloud.google.com/compute/docs/regions-zones/
             zone: "europe-west3-a"
             # See https://cloud.google.com/compute/docs/machine-types
diff --git a/test/e2e/provisioning/testdata/machinedeployment-kubevirt.yaml b/test/e2e/provisioning/testdata/machinedeployment-kubevirt.yaml
index 29a42da2..5c66b562 100644
--- a/test/e2e/provisioning/testdata/machinedeployment-kubevirt.yaml
+++ b/test/e2e/provisioning/testdata/machinedeployment-kubevirt.yaml
@@ -28,7 +28,7 @@ spec:
           cloudProviderSpec:
             auth:
               kubeconfig:
-                value: '<< KUBECONFIG >>'
+                value: '<< KUBECONFIG_BASE64 >>'
             virtualMachine:
               template:
                 cpus: "1"
diff --git a/test/e2e/provisioning/testdata/machinedeployment-vsphere-datastore-cluster.yaml b/test/e2e/provisioning/testdata/machinedeployment-vsphere-datastore-cluster.yaml
index 3242f90e..743faff4 100644
--- a/test/e2e/provisioning/testdata/machinedeployment-vsphere-datastore-cluster.yaml
+++ b/test/e2e/provisioning/testdata/machinedeployment-vsphere-datastore-cluster.yaml
@@ -27,8 +27,8 @@ spec:
             templateVMName: '<< OS_Image_Template >>'
             username: '<< VSPHERE_USERNAME >>'
             vsphereURL: '<< VSPHERE_ADDRESS >>'
-            datacenter: 'dc-1'
-            folder: '/dc-1/vm/e2e-tests'
+            datacenter: 'Hamburg'
+            folder: '/Hamburg/vm/Kubermatic-dev'
             password: << VSPHERE_PASSWORD >>
             # example: 'https://your-vcenter:8443'. '/sdk' gets appended automatically
             datastoreCluster: 'dsc-1'
diff --git a/test/e2e/provisioning/testdata/machinedeployment-vsphere-resource-pool.yaml b/test/e2e/provisioning/testdata/machinedeployment-vsphere-resource-pool.yaml
index a5402136..13d8af42 100644
--- a/test/e2e/provisioning/testdata/machinedeployment-vsphere-resource-pool.yaml
+++ b/test/e2e/provisioning/testdata/machinedeployment-vsphere-resource-pool.yaml
@@ -27,8 +27,8 @@ spec:
             templateVMName: '<< OS_Image_Template >>'
             username: '<< VSPHERE_USERNAME >>'
             vsphereURL: '<< VSPHERE_ADDRESS >>'
-            datacenter: 'dc-1'
-            folder: '/dc-1/vm/e2e-tests'
+            datacenter: 'Hamburg'
+            folder: '/Hamburg/vm/Kubermatic-dev'
             password: << VSPHERE_PASSWORD >>
             datastoreCluster: 'dsc-1'
             resourcePool: 'e2e-resource-pool'
diff --git a/test/e2e/provisioning/testdata/machinedeployment-vsphere-static-ip.yaml b/test/e2e/provisioning/testdata/machinedeployment-vsphere-static-ip.yaml
index eac0bdcf..7ebc2155 100644
--- a/test/e2e/provisioning/testdata/machinedeployment-vsphere-static-ip.yaml
+++ b/test/e2e/provisioning/testdata/machinedeployment-vsphere-static-ip.yaml
@@ -27,11 +27,11 @@ spec:
             templateVMName: '<< OS_Image_Template >>'
             username: '<< VSPHERE_USERNAME >>'
             vsphereURL: '<< VSPHERE_ADDRESS >>'
-            datacenter: 'dc-1'
-            folder: '/dc-1/vm/e2e-tests'
+            datacenter: 'Hamburg'
+            folder: '/Hamburg/vm/Kubermatic-dev'
             password: << VSPHERE_PASSWORD >>
             # example: 'https://your-vcenter:8443'. '/sdk' gets appended automatically
-            datastore: HS-FreeNAS
+            datastore: alpha1
             allowInsecure: true
             cpus: 2
             MemoryMB: 2048
diff --git a/test/e2e/provisioning/testdata/machinedeployment-vsphere.yaml b/test/e2e/provisioning/testdata/machinedeployment-vsphere.yaml
index 921d0066..e14f7f4a 100644
--- a/test/e2e/provisioning/testdata/machinedeployment-vsphere.yaml
+++ b/test/e2e/provisioning/testdata/machinedeployment-vsphere.yaml
@@ -27,11 +27,11 @@ spec:
             templateVMName: '<< OS_Image_Template >>'
             username: '<< VSPHERE_USERNAME >>'
             vsphereURL: '<< VSPHERE_ADDRESS >>'
-            datacenter: 'dc-1'
-            folder: '/dc-1/vm/e2e-tests'
+            datacenter: 'Hamburg'
+            folder: '/Hamburg/vm/Kubermatic-dev'
             password: << VSPHERE_PASSWORD >>
             # example: 'https://your-vcenter:8443'. '/sdk' gets appended automatically
-            datastore: HS-FreeNAS
+            datastore: alpha1
             cpus: 2
             MemoryMB: 4096
             diskSizeGB: << DISK_SIZE >>