Repository: kubermatic/machine-controller
Pull request #1163 (2022-01-13 14:52:58)

set the machine_controller_workers metric to an actual value
**What this PR does / why we need it**:
We define a metric but have not set its value since refactoring the MC to use controller-runtime in #592. This PR restores setting the metric value.

**Optional Release Note**:
```release-note
Fix machine_controller_workers metric always being 0.
```


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

This pull-request has been approved by: *<a href="https://github.com/kubermatic/machine-controller/pull/1163#" title="Author self-approved">xrstf</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/master/OWNERS)~~ [xrstf]

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":[]} -->

Patch:
diff --git a/.prow.yaml b/.prow.yaml
index 98975b3a..45050280 100644
--- a/.prow.yaml
+++ b/.prow.yaml
@@ -475,7 +475,6 @@ presubmits:
 
   - name: pull-machine-controller-e2e-linode
     always_run: false
-    run_if_changed: pkg\/cloudprovider\/provider\/linode\/.*
     optional: true
     decorate: true
     error_on_eviction: true
diff --git a/pkg/cloudprovider/provider/aws/provider.go b/pkg/cloudprovider/provider/aws/provider.go
index ceda33da..1381be5f 100644
--- a/pkg/cloudprovider/provider/aws/provider.go
+++ b/pkg/cloudprovider/provider/aws/provider.go
@@ -436,7 +436,7 @@ func (p *provider) getConfig(s v1alpha1.ProviderSpec) (*Config, *providerconfigt
 		c.DiskIops = rawConfig.DiskIops
 	}
 
-	c.EBSVolumeEncrypted, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.EBSVolumeEncrypted)
+	c.EBSVolumeEncrypted, _, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.EBSVolumeEncrypted)
 	if err != nil {
 		return nil, nil, nil, fmt.Errorf("failed to get ebsVolumeEncrypted value: %v", err)
 	}
@@ -450,7 +450,7 @@ func (p *provider) getConfig(s v1alpha1.ProviderSpec) (*Config, *providerconfigt
 		}
 		c.SpotMaxPrice = pointer.StringPtr(maxPrice)
 
-		persistentRequest, err := p.configVarResolver.GetConfigVarBoolValue(rawConfig.SpotInstanceConfig.PersistentRequest)
+		persistentRequest, _, err := p.configVarResolver.GetConfigVarBoolValue(rawConfig.SpotInstanceConfig.PersistentRequest)
 		if err != nil {
 			return nil, nil, nil, err
 		}
diff --git a/pkg/cloudprovider/provider/azure/provider.go b/pkg/cloudprovider/provider/azure/provider.go
index 558d42f9..a4c707b1 100644
--- a/pkg/cloudprovider/provider/azure/provider.go
+++ b/pkg/cloudprovider/provider/azure/provider.go
@@ -271,7 +271,7 @@ func (p *provider) getConfig(s v1alpha1.ProviderSpec) (*config, *providerconfigt
 		return nil, nil, fmt.Errorf("failed to get the value of \"routeTableName\" field, error = %v", err)
 	}
 
-	c.AssignPublicIP, err = p.configVarResolver.GetConfigVarBoolValue(rawCfg.AssignPublicIP)
+	c.AssignPublicIP, _, err = p.configVarResolver.GetConfigVarBoolValue(rawCfg.AssignPublicIP)
 	if err != nil {
 		return nil, nil, fmt.Errorf("failed to get the value of \"assignPublicIP\" field, error = %v", err)
 	}
diff --git a/pkg/cloudprovider/provider/digitalocean/provider.go b/pkg/cloudprovider/provider/digitalocean/provider.go
index 61cf5fef..81e99d99 100644
--- a/pkg/cloudprovider/provider/digitalocean/provider.go
+++ b/pkg/cloudprovider/provider/digitalocean/provider.go
@@ -133,19 +133,19 @@ func (p *provider) getConfig(s v1alpha1.ProviderSpec) (*Config, *providerconfigt
 	if err != nil {
 		return nil, nil, err
 	}
-	c.Backups, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.Backups)
+	c.Backups, _, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.Backups)
 	if err != nil {
 		return nil, nil, err
 	}
-	c.IPv6, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.IPv6)
+	c.IPv6, _, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.IPv6)
 	if err != nil {
 		return nil, nil, err
 	}
-	c.PrivateNetworking, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.PrivateNetworking)
+	c.PrivateNetworking, _, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.PrivateNetworking)
 	if err != nil {
 		return nil, nil, err
 	}
-	c.Monitoring, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.Monitoring)
+	c.Monitoring, _, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.Monitoring)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/pkg/cloudprovider/provider/gce/config.go b/pkg/cloudprovider/provider/gce/config.go
index 1c70e400..092e2ff1 100644
--- a/pkg/cloudprovider/provider/gce/config.go
+++ b/pkg/cloudprovider/provider/gce/config.go
@@ -161,7 +161,7 @@ func newConfig(resolver *providerconfig.ConfigVarResolver, spec v1alpha1.Provide
 		return nil, fmt.Errorf("cannot retrieve subnetwork: %v", err)
 	}
 
-	cfg.preemptible, err = resolver.GetConfigVarBoolValue(cpSpec.Preemptible)
+	cfg.preemptible, _, err = resolver.GetConfigVarBoolValue(cpSpec.Preemptible)
 	if err != nil {
 		return nil, fmt.Errorf("cannot retrieve preemptible: %v", err)
 	}
@@ -170,18 +170,18 @@ func newConfig(resolver *providerconfig.ConfigVarResolver, spec v1alpha1.Provide
 	cfg.assignPublicIPAddress = true
 
 	if cpSpec.AssignPublicIPAddress != nil {
-		cfg.assignPublicIPAddress, err = resolver.GetConfigVarBoolValue(*cpSpec.AssignPublicIPAddress)
+		cfg.assignPublicIPAddress, _, err = resolver.GetConfigVarBoolValue(*cpSpec.AssignPublicIPAddress)
 		if err != nil {
 			return nil, fmt.Errorf("failed to retrieve assignPublicIPAddress: %v", err)
 		}
 	}
 
-	cfg.multizone, err = resolver.GetConfigVarBoolValue(cpSpec.MultiZone)
+	cfg.multizone, _, err = resolver.GetConfigVarBoolValue(cpSpec.MultiZone)
 	if err != nil {
 		return nil, fmt.Errorf("failed to retrieve multizone: %v", err)
 	}
 
-	cfg.regional, err = resolver.GetConfigVarBoolValue(cpSpec.Regional)
+	cfg.regional, _, err = resolver.GetConfigVarBoolValue(cpSpec.Regional)
 	if err != nil {
 		return nil, fmt.Errorf("failed to retrieve regional: %v", err)
 	}
diff --git a/pkg/cloudprovider/provider/linode/provider.go b/pkg/cloudprovider/provider/linode/provider.go
index 1692757d..dbcd01c0 100644
--- a/pkg/cloudprovider/provider/linode/provider.go
+++ b/pkg/cloudprovider/provider/linode/provider.go
@@ -140,11 +140,11 @@ func (p *provider) getConfig(s v1alpha1.ProviderSpec) (*Config, *providerconfigt
 	if err != nil {
 		return nil, nil, err
 	}
-	c.Backups, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.Backups)
+	c.Backups, _, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.Backups)
 	if err != nil {
 		return nil, nil, err
 	}
-	c.PrivateNetworking, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.PrivateNetworking)
+	c.PrivateNetworking, _, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.PrivateNetworking)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/pkg/cloudprovider/provider/openstack/provider.go b/pkg/cloudprovider/provider/openstack/provider.go
index 8f60f6d6..972e57b3 100644
--- a/pkg/cloudprovider/provider/openstack/provider.go
+++ b/pkg/cloudprovider/provider/openstack/provider.go
@@ -255,7 +255,7 @@ func (p *provider) getConfig(s v1alpha1.ProviderSpec) (*Config, *providerconfigt
 	if err != nil {
 		return nil, nil, nil, err
 	}
-	c.TrustDevicePath, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.TrustDevicePath)
+	c.TrustDevicePath, _, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.TrustDevicePath)
 	if err != nil {
 		return nil, nil, nil, err
 	}
diff --git a/pkg/cloudprovider/provider/scaleway/provider.go b/pkg/cloudprovider/provider/scaleway/provider.go
index 02d428ad..1f173829 100644
--- a/pkg/cloudprovider/provider/scaleway/provider.go
+++ b/pkg/cloudprovider/provider/scaleway/provider.go
@@ -127,7 +127,7 @@ func (p *provider) getConfig(s v1alpha1.ProviderSpec) (*Config, *providerconfigt
 	if err != nil {
 		return nil, nil, err
 	}
-	c.IPv6, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.IPv6)
+	c.IPv6, _, err = p.configVarResolver.GetConfigVarBoolValue(rawConfig.IPv6)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/pkg/controller/machine/machine_controller.go b/pkg/controller/machine/machine_controller.go
index 5a150663..0f246995 100644
--- a/pkg/controller/machine/machine_controller.go
+++ b/pkg/controller/machine/machine_controller.go
@@ -216,6 +216,8 @@ func Add(
 		return err
 	}
 
+	metrics.Workers.Set(float64(numWorkers))
+
 	return c.Watch(
 		&source.Kind{Type: &corev1.Node{}},
 		handler.EnqueueRequestsFromMapFunc(func(node client.Object) (result []reconcile.Request) {
diff --git a/pkg/providerconfig/types.go b/pkg/providerconfig/types.go
index a1380b85..a984e21c 100644
--- a/pkg/providerconfig/types.go
+++ b/pkg/providerconfig/types.go
@@ -116,37 +116,60 @@ func (cvr *ConfigVarResolver) GetConfigVarStringValueOrEnv(configVar providercon
 	return envVal, nil
 }
 
-func (cvr *ConfigVarResolver) GetConfigVarBoolValue(configVar providerconfigtypes.ConfigVarBool) (bool, error) {
-	cvs := providerconfigtypes.ConfigVarString{Value: strconv.FormatBool(configVar.Value), SecretKeyRef: configVar.SecretKeyRef}
-	stringVal, err := cvr.GetConfigVarStringValue(cvs)
-	if err != nil {
-		return false, err
+// GetConfigVarBoolValue returns a boolean from a ConfigVarBool. If there is no valid source for the boolean,
+// the second bool returned will be false (to be able to differentiate between "false" and "unset")
+func (cvr *ConfigVarResolver) GetConfigVarBoolValue(configVar providerconfigtypes.ConfigVarBool) (bool, bool, error) {
+	// We need all three of these to fetch and use a secret
+	if configVar.SecretKeyRef.Name != "" && configVar.SecretKeyRef.Namespace != "" && configVar.SecretKeyRef.Key != "" {
+		secret := &corev1.Secret{}
+		name := types.NamespacedName{Namespace: configVar.SecretKeyRef.Namespace, Name: configVar.SecretKeyRef.Name}
+		if err := cvr.client.Get(cvr.ctx, name, secret); err != nil {
+			return false, false, fmt.Errorf("error retrieving secret '%s' from namespace '%s': '%v'", configVar.SecretKeyRef.Name, configVar.SecretKeyRef.Namespace, err)
+		}
+		if val, ok := secret.Data[configVar.SecretKeyRef.Key]; ok {
+			boolVal, err := strconv.ParseBool(string(val))
+			return boolVal, (err == nil), err
+		}
+		return false, false, fmt.Errorf("secret '%s' in namespace '%s' has no key '%s'", configVar.SecretKeyRef.Name, configVar.SecretKeyRef.Namespace, configVar.SecretKeyRef.Key)
 	}
-	boolVal, err := strconv.ParseBool(stringVal)
-	if err != nil {
-		return false, err
+
+	// We need all three of these to fetch and use a configmap
+	if configVar.ConfigMapKeyRef.Name != "" && configVar.ConfigMapKeyRef.Namespace != "" && configVar.ConfigMapKeyRef.Key != "" {
+		configMap := &corev1.ConfigMap{}
+		name := types.NamespacedName{Namespace: configVar.ConfigMapKeyRef.Namespace, Name: configVar.ConfigMapKeyRef.Name}
+		if err := cvr.client.Get(cvr.ctx, name, configMap); err != nil {
+			return false, false, fmt.Errorf("error retrieving configmap '%s' from namespace '%s': '%v'", configVar.ConfigMapKeyRef.Name, configVar.ConfigMapKeyRef.Namespace, err)
+		}
+		if val, ok := configMap.Data[configVar.ConfigMapKeyRef.Key]; ok {
+			boolVal, err := strconv.ParseBool(val)
+			return boolVal, (err == nil), err
+		}
+		return false, false, fmt.Errorf("configmap '%s' in namespace '%s' has no key '%s'", configVar.ConfigMapKeyRef.Name, configVar.ConfigMapKeyRef.Namespace, configVar.ConfigMapKeyRef.Key)
+	}
+
+	if configVar.Value == nil {
+		return false, false, nil
 	}
-	return boolVal, nil
+
+	return configVar.Value != nil && *configVar.Value, true, nil
 }
 
 func (cvr *ConfigVarResolver) GetConfigVarBoolValueOrEnv(configVar providerconfigtypes.ConfigVarBool, envVarName string) (bool, error) {
-	cvs := providerconfigtypes.ConfigVarString{Value: strconv.FormatBool(configVar.Value), SecretKeyRef: configVar.SecretKeyRef}
-	stringVal, err := cvr.GetConfigVarStringValue(cvs)
-	if err != nil {
-		return false, err
+	boolVal, valid, err := cvr.GetConfigVarBoolValue(configVar)
+	if valid && err == nil {
+		return boolVal, nil
 	}
-	if stringVal == "" {
-		envVal, envValFound := os.LookupEnv(envVarName)
-		if !envValFound {
-			return false, fmt.Errorf("all mechanisms(value, secret, configMap) of getting the value failed, including reading from environment variable = %s which was not set", envVarName)
+
+	envVal, envValFound := os.LookupEnv(envVarName)
+	if envValFound {
+		envValBool, err := strconv.ParseBool(envVal)
+		if err != nil {
+			return false, err
 		}
-		stringVal = envVal
+		return envValBool, nil
 	}
-	boolVal, err := strconv.ParseBool(stringVal)
-	if err != nil {
-		return false, err
-	}
-	return boolVal, nil
+
+	return false, nil
 }
 
 func NewConfigVarResolver(ctx context.Context, client ctrlruntimeclient.Client) *ConfigVarResolver {
diff --git a/pkg/providerconfig/types/types.go b/pkg/providerconfig/types/types.go
index 340e0a72..6bf7063c 100644
--- a/pkg/providerconfig/types/types.go
+++ b/pkg/providerconfig/types/types.go
@@ -21,7 +21,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"strconv"
 
 	clusterv1alpha1 "github.com/kubermatic/machine-controller/pkg/apis/cluster/v1alpha1"
 
@@ -221,7 +220,7 @@ func (configVarString *ConfigVarString) UnmarshalJSON(b []byte) error {
 }
 
 type ConfigVarBool struct {
-	Value           bool                       `json:"value,omitempty"`
+	Value           *bool                      `json:"value,omitempty"`
 	SecretKeyRef    GlobalSecretKeySelector    `json:"secretKeyRef,omitempty"`
 	ConfigMapKeyRef GlobalConfigMapKeySelector `json:"configMapKeyRef,omitempty"`
 }
@@ -247,7 +246,11 @@ func (configVarBool ConfigVarBool) MarshalJSON() ([]byte, error) {
 	}
 
 	if secretKeyRefEmpty && configMapKeyRefEmpty {
-		return []byte(fmt.Sprintf("%v", configVarBool.Value)), nil
+		jsonVal, err := json.Marshal(configVarBool.Value)
+		if err != nil {
+			return []byte{}, err
+		}
+		return jsonVal, nil
 	}
 
 	buffer := bytes.NewBufferString("{")
@@ -271,20 +274,31 @@ func (configVarBool ConfigVarBool) MarshalJSON() ([]byte, error) {
 		buffer.WriteString(fmt.Sprintf(`%s"configMapKeyRef":%s`, leadingComma, jsonVal))
 	}
 
-	buffer.WriteString(fmt.Sprintf(`,"value":%v}`, configVarBool.Value))
+	if configVarBool.Value != nil {
+		jsonVal, err := json.Marshal(configVarBool.Value)
+		if err != nil {
+			return []byte{}, err
+		}
+
+		buffer.WriteString(fmt.Sprintf(`,"value":%v`, string(jsonVal)))
+	}
+
+	buffer.WriteString("}")
 
 	return buffer.Bytes(), nil
 }
 
 func (configVarBool *ConfigVarBool) UnmarshalJSON(b []byte) error {
 	if !bytes.HasPrefix(b, []byte("{")) {
-		value, err := strconv.ParseBool(string(b))
-		if err != nil {
-			return fmt.Errorf("Error converting string to bool: '%v'", err)
+		var val *bool
+		if err := json.Unmarshal(b, &val); err != nil {
+			return fmt.Errorf("Error parsing value: '%v'", err)
 		}
-		configVarBool.Value = value
+		configVarBool.Value = val
+
 		return nil
 	}
+
 	var cvbDummy configVarBoolWithoutUnmarshaller
 	err := json.Unmarshal(b, &cvbDummy)
 	if err != nil {
diff --git a/pkg/userdata/centos/provider.go b/pkg/userdata/centos/provider.go
index 69673693..57973aa6 100644
--- a/pkg/userdata/centos/provider.go
+++ b/pkg/userdata/centos/provider.go
@@ -220,8 +220,15 @@ write_files:
       {{- if eq .CloudProviderName "vsphere" }}
       open-vm-tools \
       {{- end }}
+      {{- if eq .CloudProviderName "nutanix" }}
+      iscsi-initiator-utils \
+      {{- end }}
       ipvsadm
-
+      
+    {{- /* iscsid service is required on Nutanix machines for CSI driver to attach volumes. */}}
+    {{- if eq .CloudProviderName "nutanix" }}
+    systemctl enable --now iscsid
+    {{ end }}
 {{ .ContainerRuntimeScript | indent 4 }}
 
 {{ safeDownloadBinariesScript .KubeletVersion | indent 4 }}
diff --git a/pkg/userdata/rhel/provider.go b/pkg/userdata/rhel/provider.go
index 4f53467a..411ebd32 100644
--- a/pkg/userdata/rhel/provider.go
+++ b/pkg/userdata/rhel/provider.go
@@ -225,7 +225,15 @@ write_files:
       {{- if eq .CloudProviderName "vsphere" }}
       open-vm-tools \
       {{- end }}
+      {{- if eq .CloudProviderName "nutanix" }}
+      iscsi-initiator-utils \
+      {{- end }}
       ipvsadm
+    
+    {{- /* iscsid service is required on Nutanix machines for CSI driver to attach volumes. */}}
+    {{- if eq .CloudProviderName "nutanix" }}
+    systemctl enable --now iscsid
+    {{ end }}
 {{ .ContainerRuntimeScript | indent 4 }}
 {{ safeDownloadBinariesScript .KubeletVersion | indent 4 }}
     # set kubelet nodeip environment variable
diff --git a/pkg/userdata/ubuntu/provider.go b/pkg/userdata/ubuntu/provider.go
index 5d26595a..e9bcff1e 100644
--- a/pkg/userdata/ubuntu/provider.go
+++ b/pkg/userdata/ubuntu/provider.go
@@ -215,7 +215,15 @@ write_files:
       {{- if eq .CloudProviderName "vsphere" }}
       open-vm-tools \
       {{- end }}
+      {{- if eq .CloudProviderName "nutanix" }}
+      open-iscsi \
+      {{- end }}
       ipvsadm
+    
+    {{- /* iscsid service is required on Nutanix machines for CSI driver to attach volumes. */}}
+    {{- if eq .CloudProviderName "nutanix" }}
+    systemctl enable --now iscsid
+    {{ end }}
 
     # Update grub to include kernel command options to enable swap accounting.
     # Exclude alibaba cloud until this is fixed https://github.com/kubermatic/machine-controller/issues/682


Test patch:
diff --git a/pkg/providerconfig/types/types_test.go b/pkg/providerconfig/types/types_test.go
index dad0b655..1ee10796 100644
--- a/pkg/providerconfig/types/types_test.go
+++ b/pkg/providerconfig/types/types_test.go
@@ -22,6 +22,7 @@ import (
 	"testing"
 
 	"k8s.io/api/core/v1"
+	"k8s.io/utils/pointer"
 )
 
 func TestConfigVarStringUnmarshalling(t *testing.T) {
@@ -45,21 +46,50 @@ func TestConfigVarStringUnmarshalling(t *testing.T) {
 }
 
 func TestConfigVarBoolUnmarshalling(t *testing.T) {
-	jsonBool := []byte("true")
-	jsonMapBool := []byte(`{"value":true}`)
-
-	expectedResult := ConfigVarBool{Value: true}
-
-	var jsonBoolTarget ConfigVarBool
-	var jsonMapBoolTarget ConfigVarBool
-
-	err := json.Unmarshal(jsonBool, &jsonBoolTarget)
-	if err != nil || !reflect.DeepEqual(expectedResult, jsonBoolTarget) {
-		t.Fatalf("Decoding raw bool into configVarBool failed! Error: '%v'", err)
+	testCases := []struct {
+		jsonString string
+		expected   ConfigVarBool
+	}{
+		{
+			jsonString: "true",
+			expected:   ConfigVarBool{Value: pointer.Bool(true)},
+		},
+		{
+			jsonString: `{"value":true}`,
+			expected:   ConfigVarBool{Value: pointer.Bool(true)},
+		},
+		{
+			jsonString: "null",
+			expected:   ConfigVarBool{},
+		},
+		{
+			jsonString: `{"value":null}`,
+			expected:   ConfigVarBool{},
+		},
+		{
+			jsonString: `{"secretKeyRef":{"namespace":"ns","name":"name","key":"key"}}`,
+			expected:   ConfigVarBool{Value: nil, SecretKeyRef: GlobalSecretKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"}},
+		},
+		{
+			jsonString: `{"value": null, "secretKeyRef":{"namespace":"ns","name":"name","key":"key"}}`,
+			expected:   ConfigVarBool{Value: nil, SecretKeyRef: GlobalSecretKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"}},
+		},
+		{
+			jsonString: `{"value":false, "secretKeyRef":{"namespace":"ns","name":"name","key":"key"}}`,
+			expected:   ConfigVarBool{Value: pointer.Bool(false), SecretKeyRef: GlobalSecretKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"}},
+		},
+		{
+			jsonString: `{"value":true, "secretKeyRef":{"namespace":"ns","name":"name","key":"key"}}`,
+			expected:   ConfigVarBool{Value: pointer.Bool(true), SecretKeyRef: GlobalSecretKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"}},
+		},
 	}
-	err = json.Unmarshal(jsonMapBool, &jsonMapBoolTarget)
-	if err != nil || !reflect.DeepEqual(expectedResult, jsonMapBoolTarget) {
-		t.Fatalf("Decoding map bool into configVarBool failed! Error: '%v'", err)
+
+	for _, testCase := range testCases {
+		var cvb ConfigVarBool
+		err := json.Unmarshal([]byte(testCase.jsonString), &cvb)
+		if err != nil || !reflect.DeepEqual(testCase.expected, cvb) {
+			t.Fatalf("Decoding '%s' into configVarBool failed! Error: '%v'", testCase.jsonString, err)
+		}
 	}
 }
 
@@ -97,11 +127,23 @@ func TestConfigVarBoolMarshalling(t *testing.T) {
 		expected string
 	}{
 		{
-			cvb:      ConfigVarBool{Value: true},
+			cvb:      ConfigVarBool{},
+			expected: `null`,
+		},
+		{
+			cvb:      ConfigVarBool{Value: pointer.Bool(true)},
 			expected: `true`,
 		},
 		{
 			cvb:      ConfigVarBool{SecretKeyRef: GlobalSecretKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"}},
+			expected: `{"secretKeyRef":{"namespace":"ns","name":"name","key":"key"}}`,
+		},
+		{
+			cvb:      ConfigVarBool{SecretKeyRef: GlobalSecretKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"}, Value: pointer.Bool(true)},
+			expected: `{"secretKeyRef":{"namespace":"ns","name":"name","key":"key"},"value":true}`,
+		},
+		{
+			cvb:      ConfigVarBool{SecretKeyRef: GlobalSecretKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"}, Value: pointer.Bool(false)},
 			expected: `{"secretKeyRef":{"namespace":"ns","name":"name","key":"key"},"value":false}`,
 		},
 	}
@@ -155,17 +197,19 @@ func TestConfigVarStringMarshallingAndUnmarshalling(t *testing.T) {
 func TestConfigVarBoolMarshallingAndUnmarshalling(t *testing.T) {
 
 	testCases := []ConfigVarBool{
-		{Value: true},
+		{},
+		{Value: pointer.Bool(false)},
+		{Value: pointer.Bool(true)},
 		{SecretKeyRef: GlobalSecretKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"}},
-		{Value: true, SecretKeyRef: GlobalSecretKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"}},
+		{Value: pointer.Bool(true), SecretKeyRef: GlobalSecretKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"}},
 		{ConfigMapKeyRef: GlobalConfigMapKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"}},
-		{Value: true, ConfigMapKeyRef: GlobalConfigMapKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"}},
+		{Value: pointer.Bool(true), ConfigMapKeyRef: GlobalConfigMapKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"}},
 		{
 			ConfigMapKeyRef: GlobalConfigMapKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"},
 			SecretKeyRef:    GlobalSecretKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"},
 		},
 		{
-			Value:           true,
+			Value:           pointer.Bool(true),
 			ConfigMapKeyRef: GlobalConfigMapKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"},
 			SecretKeyRef:    GlobalSecretKeySelector{ObjectReference: v1.ObjectReference{Namespace: "ns", Name: "name"}, Key: "key"},
 		},
diff --git a/pkg/userdata/centos/provider_test.go b/pkg/userdata/centos/provider_test.go
index 2b4cc44c..86f30751 100644
--- a/pkg/userdata/centos/provider_test.go
+++ b/pkg/userdata/centos/provider_test.go
@@ -193,6 +193,16 @@ func TestUserDataGeneration(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "kubelet-v1.21-nutanix",
+			spec: clusterv1alpha1.MachineSpec{
+				ObjectMeta: metav1.ObjectMeta{Name: "node1"},
+				Versions: clusterv1alpha1.MachineVersionInfo{
+					Kubelet: "1.21.5",
+				},
+			},
+			cloudProviderName: stringPtr("nutanix"),
+		},
 	}
 
 	defaultCloudProvider := &fakeCloudConfigProvider{
diff --git a/pkg/userdata/centos/testdata/kubelet-containerd-v1.20-aws.yaml b/pkg/userdata/centos/testdata/kubelet-containerd-v1.20-aws.yaml
index dc062671..4a2bc488 100644
--- a/pkg/userdata/centos/testdata/kubelet-containerd-v1.20-aws.yaml
+++ b/pkg/userdata/centos/testdata/kubelet-containerd-v1.20-aws.yaml
@@ -80,7 +80,6 @@ write_files:
       curl \
       ipvsadm
 
-
     yum install -y yum-utils
     yum-config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
     yum-config-manager --save --setopt=docker-ce-stable.module_hotfixes=true
diff --git a/pkg/userdata/centos/testdata/kubelet-v1.20-aws.yaml b/pkg/userdata/centos/testdata/kubelet-v1.20-aws.yaml
index 609bdc49..71417c80 100644
--- a/pkg/userdata/centos/testdata/kubelet-v1.20-aws.yaml
+++ b/pkg/userdata/centos/testdata/kubelet-v1.20-aws.yaml
@@ -80,7 +80,6 @@ write_files:
       curl \
       ipvsadm
 
-
     yum install -y yum-utils
     yum-config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
     yum-config-manager --save --setopt=docker-ce-stable.module_hotfixes=true
diff --git a/pkg/userdata/centos/testdata/kubelet-v1.21-aws-external.yaml b/pkg/userdata/centos/testdata/kubelet-v1.21-aws-external.yaml
index 4e417e4f..eb9c0e54 100644
--- a/pkg/userdata/centos/testdata/kubelet-v1.21-aws-external.yaml
+++ b/pkg/userdata/centos/testdata/kubelet-v1.21-aws-external.yaml
@@ -80,7 +80,6 @@ write_files:
       curl \
       ipvsadm
 
-
     yum install -y yum-utils
     yum-config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
     yum-config-manager --save --setopt=docker-ce-stable.module_hotfixes=true
diff --git a/pkg/userdata/centos/testdata/kubelet-v1.21-aws.yaml b/pkg/userdata/centos/testdata/kubelet-v1.21-aws.yaml
index f2eaa17c..e95d9e99 100644
--- a/pkg/userdata/centos/testdata/kubelet-v1.21-aws.yaml
+++ b/pkg/userdata/centos/testdata/kubelet-v1.21-aws.yaml
@@ -80,7 +80,6 @@ write_files:
       curl \
       ipvsadm
 
-
     yum install -y yum-utils
     yum-config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
     yum-config-manager --save --setopt=docker-ce-stable.module_hotfixes=true
diff --git a/pkg/userdata/centos/testdata/kubelet-v1.21-nutanix.yaml b/pkg/userdata/centos/testdata/kubelet-v1.21-nutanix.yaml
new file mode 100644
index 00000000..bee7b653
--- /dev/null
+++ b/pkg/userdata/centos/testdata/kubelet-v1.21-nutanix.yaml
@@ -0,0 +1,429 @@
+#cloud-config
+
+hostname: node1
+
+
+ssh_pwauth: false
+
+write_files:
+
+- path: "/etc/systemd/journald.conf.d/max_disk_use.conf"
+  content: |
+    [Journal]
+    SystemMaxUse=5G
+
+
+- path: "/opt/load-kernel-modules.sh"
+  permissions: "0755"
+  content: |
+    #!/usr/bin/env bash
+    set -euo pipefail
+
+    modprobe ip_vs
+    modprobe ip_vs_rr
+    modprobe ip_vs_wrr
+    modprobe ip_vs_sh
+
+    if modinfo nf_conntrack_ipv4 &> /dev/null; then
+      modprobe nf_conntrack_ipv4
+    else
+      modprobe nf_conntrack
+    fi
+
+
+- path: "/etc/sysctl.d/k8s.conf"
+  content: |
+    net.bridge.bridge-nf-call-ip6tables = 1
+    net.bridge.bridge-nf-call-iptables = 1
+    kernel.panic_on_oops = 1
+    kernel.panic = 10
+    net.ipv4.ip_forward = 1
+    vm.overcommit_memory = 1
+    fs.inotify.max_user_watches = 1048576
+
+
+- path: /etc/selinux/config
+  content: |
+    # This file controls the state of SELinux on the system.
+    # SELINUX= can take one of these three values:
+    #     enforcing - SELinux security policy is enforced.
+    #     permissive - SELinux prints warnings instead of enforcing.
+    #     disabled - No SELinux policy is loaded.
+    SELINUX=permissive
+    # SELINUXTYPE= can take one of three two values:
+    #     targeted - Targeted processes are protected,
+    #     minimum - Modification of targeted policy. Only selected processes are protected.
+    #     mls - Multi Level Security protection.
+    SELINUXTYPE=targeted
+
+- path: "/opt/bin/setup"
+  permissions: "0755"
+  content: |
+    #!/bin/bash
+    set -xeuo pipefail
+
+    setenforce 0 || true
+    systemctl restart systemd-modules-load.service
+    sysctl --system
+    sed -i.orig '/.*swap.*/d' /etc/fstab
+    swapoff -a
+
+    hostnamectl set-hostname node1
+
+
+    yum install -y \
+      device-mapper-persistent-data \
+      lvm2 \
+      ebtables \
+      ethtool \
+      nfs-utils \
+      bash-completion \
+      sudo \
+      socat \
+      wget \
+      curl \
+      iscsi-initiator-utils \
+      ipvsadm
+    systemctl enable --now iscsid
+
+
+    yum install -y yum-utils
+    yum-config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
+    yum-config-manager --save --setopt=docker-ce-stable.module_hotfixes=true
+
+    mkdir -p /etc/systemd/system/containerd.service.d /etc/systemd/system/docker.service.d
+
+    cat <<EOF | tee /etc/systemd/system/containerd.service.d/environment.conf /etc/systemd/system/docker.service.d/environment.conf
+    [Service]
+    Restart=always
+    EnvironmentFile=-/etc/environment
+    EOF
+
+    yum install -y \
+        docker-ce-cli-19.03* \
+        containerd.io-1.4* \
+        docker-ce-19.03* \
+        yum-plugin-versionlock
+    yum versionlock add docker-ce* containerd.io
+
+    systemctl daemon-reload
+    systemctl enable --now docker
+
+
+    opt_bin=/opt/bin
+    usr_local_bin=/usr/local/bin
+    cni_bin_dir=/opt/cni/bin
+    mkdir -p /etc/cni/net.d /etc/kubernetes/dynamic-config-dir /etc/kubernetes/manifests "$opt_bin" "$cni_bin_dir"
+    arch=${HOST_ARCH-}
+    if [ -z "$arch" ]
+    then
+    case $(uname -m) in
+    x86_64)
+        arch="amd64"
+        ;;
+    aarch64)
+        arch="arm64"
+        ;;
+    *)
+        echo "unsupported CPU architecture, exiting"
+        exit 1
+        ;;
+    esac
+    fi
+    CNI_VERSION="${CNI_VERSION:-v0.8.7}"
+    cni_base_url="https://github.com/containernetworking/plugins/releases/download/$CNI_VERSION"
+    cni_filename="cni-plugins-linux-$arch-$CNI_VERSION.tgz"
+    curl -Lfo "$cni_bin_dir/$cni_filename" "$cni_base_url/$cni_filename"
+    cni_sum=$(curl -Lf "$cni_base_url/$cni_filename.sha256")
+    cd "$cni_bin_dir"
+    sha256sum -c <<<"$cni_sum"
+    tar xvf "$cni_filename"
+    rm -f "$cni_filename"
+    cd -
+    CRI_TOOLS_RELEASE="${CRI_TOOLS_RELEASE:-v1.22.0}"
+    cri_tools_base_url="https://github.com/kubernetes-sigs/cri-tools/releases/download/${CRI_TOOLS_RELEASE}"
+    cri_tools_filename="crictl-${CRI_TOOLS_RELEASE}-linux-${arch}.tar.gz"
+    curl -Lfo "$opt_bin/$cri_tools_filename" "$cri_tools_base_url/$cri_tools_filename"
+    cri_tools_sum=$(curl -Lf "$cri_tools_base_url/$cri_tools_filename.sha256" | sed 's/\*\///')
+    cd "$opt_bin"
+    sha256sum -c <<<"$cri_tools_sum"
+    tar xvf "$cri_tools_filename"
+    rm -f "$cri_tools_filename"
+    ln -sf "$opt_bin/crictl" "$usr_local_bin"/crictl || echo "symbolic link is skipped"
+    cd -
+    KUBE_VERSION="${KUBE_VERSION:-v1.21.5}"
+    kube_dir="$opt_bin/kubernetes-$KUBE_VERSION"
+    kube_base_url="https://storage.googleapis.com/kubernetes-release/release/$KUBE_VERSION/bin/linux/$arch"
+    kube_sum_file="$kube_dir/sha256"
+    mkdir -p "$kube_dir"
+    : >"$kube_sum_file"
+
+    for bin in kubelet kubeadm kubectl; do
+        curl -Lfo "$kube_dir/$bin" "$kube_base_url/$bin"
+        chmod +x "$kube_dir/$bin"
+        sum=$(curl -Lf "$kube_base_url/$bin.sha256")
+        echo "$sum  $kube_dir/$bin" >>"$kube_sum_file"
+    done
+    sha256sum -c "$kube_sum_file"
+
+    for bin in kubelet kubeadm kubectl; do
+        ln -sf "$kube_dir/$bin" "$opt_bin"/$bin
+    done
+
+    if [[ ! -x /opt/bin/health-monitor.sh ]]; then
+        curl -Lfo /opt/bin/health-monitor.sh https://raw.githubusercontent.com/kubermatic/machine-controller/7967a0af2b75f29ad2ab227eeaa26ea7b0f2fbde/pkg/userdata/scripts/health-monitor.sh
+        chmod +x /opt/bin/health-monitor.sh
+    fi
+
+    # set kubelet nodeip environment variable
+    mkdir -p /etc/systemd/system/kubelet.service.d/
+    /opt/bin/setup_net_env.sh
+
+    systemctl enable --now kubelet
+    systemctl enable --now --no-block kubelet-healthcheck.service
+
+- path: "/opt/bin/supervise.sh"
+  permissions: "0755"
+  content: |
+    #!/bin/bash
+    set -xeuo pipefail
+    while ! "$@"; do
+      sleep 1
+    done
+
+- path: "/etc/systemd/system/kubelet.service"
+  content: |
+    [Unit]
+    After=docker.service
+    Requires=docker.service
+
+    Description=kubelet: The Kubernetes Node Agent
+    Documentation=https://kubernetes.io/docs/home/
+
+    [Service]
+    Restart=always
+    StartLimitInterval=0
+    RestartSec=10
+    CPUAccounting=true
+    MemoryAccounting=true
+
+    Environment="PATH=/opt/bin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin/"
+    EnvironmentFile=-/etc/environment
+
+    ExecStartPre=/bin/bash /opt/load-kernel-modules.sh
+    ExecStartPre=/bin/bash /opt/bin/setup_net_env.sh
+    ExecStart=/opt/bin/kubelet $KUBELET_EXTRA_ARGS \
+      --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf \
+      --kubeconfig=/var/lib/kubelet/kubeconfig \
+      --config=/etc/kubernetes/kubelet.conf \
+      --network-plugin=cni \
+      --cert-dir=/etc/kubernetes/pki \
+      --cloud-provider=nutanix \
+      --cloud-config=/etc/kubernetes/cloud-config \
+      --hostname-override=node1 \
+      --exit-on-lock-contention \
+      --lock-file=/tmp/kubelet.lock \
+      --container-runtime=docker \
+      --container-runtime-endpoint=unix:///var/run/dockershim.sock \
+      --dynamic-config-dir=/etc/kubernetes/dynamic-config-dir \
+      --feature-gates=DynamicKubeletConfig=true \
+      --node-ip ${KUBELET_NODE_IP}
+
+    [Install]
+    WantedBy=multi-user.target
+
+- path: "/etc/kubernetes/cloud-config"
+  permissions: "0600"
+  content: |
+    {config:true}
+
+- path: "/opt/bin/setup_net_env.sh"
+  permissions: "0755"
+  content: |
+    #!/usr/bin/env bash
+    echodate() {
+      echo "[$(date -Is)]" "$@"
+    }
+
+    # get the default interface IP address
+    DEFAULT_IFC_IP=$(ip -o  route get 1 | grep -oP "src \K\S+")
+
+    # get the full hostname
+    FULL_HOSTNAME=$(hostname -f)
+
+    if [ -z "${DEFAULT_IFC_IP}" ]
+    then
+    	echodate "Failed to get IP address for the default route interface"
+    	exit 1
+    fi
+
+    # write the nodeip_env file
+    # we need the line below because flatcar has the same string "coreos" in that file
+    if grep -q coreos /etc/os-release
+    then
+      echo -e "KUBELET_NODE_IP=${DEFAULT_IFC_IP}\nKUBELET_HOSTNAME=${FULL_HOSTNAME}" > /etc/kubernetes/nodeip.conf
+    elif [ ! -d /etc/systemd/system/kubelet.service.d ]
+    then
+    	echodate "Can't find kubelet service extras directory"
+    	exit 1
+    else
+      echo -e "[Service]\nEnvironment=\"KUBELET_NODE_IP=${DEFAULT_IFC_IP}\"\nEnvironment=\"KUBELET_HOSTNAME=${FULL_HOSTNAME}\"" > /etc/systemd/system/kubelet.service.d/nodeip.conf
+    fi
+
+
+- path: "/etc/kubernetes/bootstrap-kubelet.conf"
+  permissions: "0600"
+  content: |
+    apiVersion: v1
+    clusters:
+    - cluster:
+        certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVXakNDQTBLZ0F3SUJBZ0lKQUxmUmxXc0k4WVFITUEwR0NTcUdTSWIzRFFFQkJRVUFNSHN4Q3pBSkJnTlYKQkFZVEFsVlRNUXN3Q1FZRFZRUUlFd0pEUVRFV01CUUdBMVVFQnhNTlUyRnVJRVp5WVc1amFYTmpiekVVTUJJRwpBMVVFQ2hNTFFuSmhaR1pwZEhwcGJtTXhFakFRQmdOVkJBTVRDV3h2WTJGc2FHOXpkREVkTUJzR0NTcUdTSWIzCkRRRUpBUllPWW5KaFpFQmtZVzVuWVM1amIyMHdIaGNOTVRRd056RTFNakEwTmpBMVdoY05NVGN3TlRBME1qQTAKTmpBMVdqQjdNUXN3Q1FZRFZRUUdFd0pWVXpFTE1Ba0dBMVVFQ0JNQ1EwRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhGREFTQmdOVkJBb1RDMEp5WVdSbWFYUjZhVzVqTVJJd0VBWURWUVFERXdsc2IyTmhiR2h2CmMzUXhIVEFiQmdrcWhraUc5dzBCQ1FFV0RtSnlZV1JBWkdGdVoyRXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEIKQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDVmQWpwNGZUY2VrV1VUZnpzcDBreWloMU9ZYnNHTDBLWDFlUmJTUwpSOE9kMCs5UTYySHlueStHRndNVGI0QS9LVThtc3NvSHZjY2VTQUFid2ZieEZLLytzNTFUb2JxVW5PUlpyT29UClpqa1V5Z2J5WERTSzk5WUJiY1IxUGlwOHZ3TVRtNFhLdUx0Q2lnZUJCZGpqQVFkZ1VPMjhMRU5HbHNNbm1lWWsKSmZPRFZHblZtcjVMdGI5QU5BOElLeVRmc25ISjRpT0NTL1BsUGJVajJxN1lub1ZMcG9zVUJNbGdVYi9DeWtYMwptT29MYjR5SkpReUEvaVNUNlp4aUlFajM2RDR5V1o1bGc3WUpsK1VpaUJRSEdDblBkR3lpcHFWMDZleDBoZVlXCmNhaVc4TFdaU1VROTNqUStXVkNIOGhUN0RRTzFkbXN2VW1YbHEvSmVBbHdRL1FJREFRQUJvNEhnTUlIZE1CMEcKQTFVZERnUVdCQlJjQVJPdGhTNFA0VTd2VGZqQnlDNTY5UjdFNkRDQnJRWURWUjBqQklHbE1JR2lnQlJjQVJPdApoUzRQNFU3dlRmakJ5QzU2OVI3RTZLRi9wSDB3ZXpFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ1RBa05CCk1SWXdGQVlEVlFRSEV3MVRZVzRnUm5KaGJtTnBjMk52TVJRd0VnWURWUVFLRXd0Q2NtRmtabWwwZW1sdVl6RVMKTUJBR0ExVUVBeE1KYkc5allXeG9iM04wTVIwd0d3WUpLb1pJaHZjTkFRa0JGZzVpY21Ga1FHUmhibWRoTG1OdgpiWUlKQUxmUmxXc0k4WVFITUF3R0ExVWRFd1FGTUFNQkFmOHdEUVlKS29aSWh2Y05BUUVGQlFBRGdnRUJBRzZoClU5ZjlzTkgwLzZvQmJHR3kyRVZVMFVnSVRVUUlyRldvOXJGa3JXNWsvWGtEalFtKzNsempUMGlHUjRJeEUvQW8KZVU2c1FodWE3d3JXZUZFbjQ3R0w5OGxuQ3NKZEQ3b1pOaEZtUTk1VGIvTG5EVWpzNVlqOWJyUDBOV3pYZllVNApVSzJabklOSlJjSnBCOGlSQ2FDeEU4RGRjVUYwWHFJRXE2cEEyNzJzbm9MbWlYTE12Tmwza1lFZG0ramU2dm9ECjU4U05WRVVzenR6UXlYbUpFaENwd1ZJMEE2UUNqelhqK3F2cG13M1paSGk4SndYZWk4WlpCTFRTRkJraThaN24Kc0g5QkJIMzgvU3pVbUFONFFIU1B5MWdqcW0wME9BRThOYVlEa2gvYnpFNGQ3bUxHR01XcC9XRTNLUFN1ODJIRgprUGU2WG9TYmlMbS9reGszMlQwPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
+        server: https://server:443
+      name: ""
+    contexts: null
+    current-context: ""
+    kind: Config
+    preferences: {}
+    users:
+    - name: ""
+      user:
+        token: my-token
+
+
+- path: "/etc/kubernetes/kubelet.conf"
+  content: |
+    apiVersion: kubelet.config.k8s.io/v1beta1
+    authentication:
+      anonymous:
+        enabled: false
+      webhook:
+        cacheTTL: 0s
+        enabled: true
+      x509:
+        clientCAFile: /etc/kubernetes/pki/ca.crt
+    authorization:
+      mode: Webhook
+      webhook:
+        cacheAuthorizedTTL: 0s
+        cacheUnauthorizedTTL: 0s
+    cgroupDriver: systemd
+    clusterDomain: cluster.local
+    cpuManagerReconcilePeriod: 0s
+    evictionHard:
+      imagefs.available: 15%
+      memory.available: 100Mi
+      nodefs.available: 10%
+      nodefs.inodesFree: 5%
+    evictionPressureTransitionPeriod: 0s
+    featureGates:
+      RotateKubeletServerCertificate: true
+    fileCheckFrequency: 0s
+    httpCheckFrequency: 0s
+    imageMinimumGCAge: 0s
+    kind: KubeletConfiguration
+    kubeReserved:
+      cpu: 200m
+      ephemeral-storage: 1Gi
+      memory: 200Mi
+    logging: {}
+    memorySwap: {}
+    nodeStatusReportFrequency: 0s
+    nodeStatusUpdateFrequency: 0s
+    protectKernelDefaults: true
+    rotateCertificates: true
+    runtimeRequestTimeout: 0s
+    serverTLSBootstrap: true
+    shutdownGracePeriod: 0s
+    shutdownGracePeriodCriticalPods: 0s
+    staticPodPath: /etc/kubernetes/manifests
+    streamingConnectionIdleTimeout: 0s
+    syncFrequency: 0s
+    systemReserved:
+      cpu: 200m
+      ephemeral-storage: 1Gi
+      memory: 200Mi
+    tlsCipherSuites:
+    - TLS_AES_128_GCM_SHA256
+    - TLS_AES_256_GCM_SHA384
+    - TLS_CHACHA20_POLY1305_SHA256
+    - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
+    - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
+    - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
+    - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+    - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
+    - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
+    volumePluginDir: /var/lib/kubelet/volumeplugins
+    volumeStatsAggPeriod: 0s
+
+
+- path: "/etc/kubernetes/pki/ca.crt"
+  content: |
+    -----BEGIN CERTIFICATE-----
+    MIIEWjCCA0KgAwIBAgIJALfRlWsI8YQHMA0GCSqGSIb3DQEBBQUAMHsxCzAJBgNV
+    BAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEUMBIG
+    A1UEChMLQnJhZGZpdHppbmMxEjAQBgNVBAMTCWxvY2FsaG9zdDEdMBsGCSqGSIb3
+    DQEJARYOYnJhZEBkYW5nYS5jb20wHhcNMTQwNzE1MjA0NjA1WhcNMTcwNTA0MjA0
+    NjA1WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBG
+    cmFuY2lzY28xFDASBgNVBAoTC0JyYWRmaXR6aW5jMRIwEAYDVQQDEwlsb2NhbGhv
+    c3QxHTAbBgkqhkiG9w0BCQEWDmJyYWRAZGFuZ2EuY29tMIIBIjANBgkqhkiG9w0B
+    AQEFAAOCAQ8AMIIBCgKCAQEAt5fAjp4fTcekWUTfzsp0kyih1OYbsGL0KX1eRbSS
+    R8Od0+9Q62Hyny+GFwMTb4A/KU8mssoHvcceSAAbwfbxFK/+s51TobqUnORZrOoT
+    ZjkUygbyXDSK99YBbcR1Pip8vwMTm4XKuLtCigeBBdjjAQdgUO28LENGlsMnmeYk
+    JfODVGnVmr5Ltb9ANA8IKyTfsnHJ4iOCS/PlPbUj2q7YnoVLposUBMlgUb/CykX3
+    mOoLb4yJJQyA/iST6ZxiIEj36D4yWZ5lg7YJl+UiiBQHGCnPdGyipqV06ex0heYW
+    caiW8LWZSUQ93jQ+WVCH8hT7DQO1dmsvUmXlq/JeAlwQ/QIDAQABo4HgMIHdMB0G
+    A1UdDgQWBBRcAROthS4P4U7vTfjByC569R7E6DCBrQYDVR0jBIGlMIGigBRcAROt
+    hS4P4U7vTfjByC569R7E6KF/pH0wezELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNB
+    MRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRQwEgYDVQQKEwtCcmFkZml0emluYzES
+    MBAGA1UEAxMJbG9jYWxob3N0MR0wGwYJKoZIhvcNAQkBFg5icmFkQGRhbmdhLmNv
+    bYIJALfRlWsI8YQHMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAG6h
+    U9f9sNH0/6oBbGGy2EVU0UgITUQIrFWo9rFkrW5k/XkDjQm+3lzjT0iGR4IxE/Ao
+    eU6sQhua7wrWeFEn47GL98lnCsJdD7oZNhFmQ95Tb/LnDUjs5Yj9brP0NWzXfYU4
+    UK2ZnINJRcJpB8iRCaCxE8DdcUF0XqIEq6pA272snoLmiXLMvNl3kYEdm+je6voD
+    58SNVEUsztzQyXmJEhCpwVI0A6QCjzXj+qvpmw3ZZHi8JwXei8ZZBLTSFBki8Z7n
+    sH9BBH38/SzUmAN4QHSPy1gjqm00OAE8NaYDkh/bzE4d7mLGGMWp/WE3KPSu82HF
+    kPe6XoSbiLm/kxk32T0=
+    -----END CERTIFICATE-----
+
+- path: "/etc/systemd/system/setup.service"
+  permissions: "0644"
+  content: |
+    [Install]
+    WantedBy=multi-user.target
+
+    [Unit]
+    Requires=network-online.target
+    After=network-online.target
+
+    [Service]
+    Type=oneshot
+    RemainAfterExit=true
+    EnvironmentFile=-/etc/environment
+    ExecStart=/opt/bin/supervise.sh /opt/bin/setup
+
+- path: "/etc/profile.d/opt-bin-path.sh"
+  permissions: "0644"
+  content: |
+    export PATH="/opt/bin:$PATH"
+
+- path: /etc/docker/daemon.json
+  permissions: "0644"
+  content: |
+    {"exec-opts":["native.cgroupdriver=systemd"],"storage-driver":"overlay2","log-driver":"json-file","log-opts":{"max-file":"5","max-size":"10m"}}
+
+- path: /etc/systemd/system/kubelet-healthcheck.service
+  permissions: "0644"
+  content: |
+    [Unit]
+    Requires=kubelet.service
+    After=kubelet.service
+
+    [Service]
+    ExecStart=/opt/bin/health-monitor.sh kubelet
+
+    [Install]
+    WantedBy=multi-user.target
+
+
+runcmd:
+- systemctl start setup.service
diff --git a/pkg/userdata/centos/testdata/kubelet-v1.21-vsphere-mirrors.yaml b/pkg/userdata/centos/testdata/kubelet-v1.21-vsphere-mirrors.yaml
index 096bd745..238c7ebf 100644
--- a/pkg/userdata/centos/testdata/kubelet-v1.21-vsphere-mirrors.yaml
+++ b/pkg/userdata/centos/testdata/kubelet-v1.21-vsphere-mirrors.yaml
@@ -93,7 +93,6 @@ write_files:
       open-vm-tools \
       ipvsadm
 
-
     yum install -y yum-utils
     yum-config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
     yum-config-manager --save --setopt=docker-ce-stable.module_hotfixes=true
diff --git a/pkg/userdata/centos/testdata/kubelet-v1.21-vsphere-proxy.yaml b/pkg/userdata/centos/testdata/kubelet-v1.21-vsphere-proxy.yaml
index def43e8c..04783408 100644
--- a/pkg/userdata/centos/testdata/kubelet-v1.21-vsphere-proxy.yaml
+++ b/pkg/userdata/centos/testdata/kubelet-v1.21-vsphere-proxy.yaml
@@ -93,7 +93,6 @@ write_files:
       open-vm-tools \
       ipvsadm
 
-
     yum install -y yum-utils
     yum-config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
     yum-config-manager --save --setopt=docker-ce-stable.module_hotfixes=true
diff --git a/pkg/userdata/centos/testdata/kubelet-v1.21-vsphere.yaml b/pkg/userdata/centos/testdata/kubelet-v1.21-vsphere.yaml
index 21425294..d7fd521f 100644
--- a/pkg/userdata/centos/testdata/kubelet-v1.21-vsphere.yaml
+++ b/pkg/userdata/centos/testdata/kubelet-v1.21-vsphere.yaml
@@ -85,7 +85,6 @@ write_files:
       open-vm-tools \
       ipvsadm
 
-
     yum install -y yum-utils
     yum-config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
     yum-config-manager --save --setopt=docker-ce-stable.module_hotfixes=true
diff --git a/pkg/userdata/centos/testdata/kubelet-v1.22-aws.yaml b/pkg/userdata/centos/testdata/kubelet-v1.22-aws.yaml
index 19f34f44..ba9391fc 100644
--- a/pkg/userdata/centos/testdata/kubelet-v1.22-aws.yaml
+++ b/pkg/userdata/centos/testdata/kubelet-v1.22-aws.yaml
@@ -80,7 +80,6 @@ write_files:
       curl \
       ipvsadm
 
-
     yum install -y yum-utils
     yum-config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
     yum-config-manager --save --setopt=docker-ce-stable.module_hotfixes=true
diff --git a/pkg/userdata/centos/testdata/kubelet-v1.23-aws.yaml b/pkg/userdata/centos/testdata/kubelet-v1.23-aws.yaml
index b5015b26..642ff989 100644
--- a/pkg/userdata/centos/testdata/kubelet-v1.23-aws.yaml
+++ b/pkg/userdata/centos/testdata/kubelet-v1.23-aws.yaml
@@ -80,7 +80,6 @@ write_files:
       curl \
       ipvsadm
 
-
     yum install -y yum-utils
     yum-config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
     yum-config-manager --save --setopt=docker-ce-stable.module_hotfixes=true
diff --git a/pkg/userdata/rhel/provider_test.go b/pkg/userdata/rhel/provider_test.go
index c381c13e..08d37b42 100644
--- a/pkg/userdata/rhel/provider_test.go
+++ b/pkg/userdata/rhel/provider_test.go
@@ -193,6 +193,16 @@ func TestUserDataGeneration(t *testing.T) {
 			registryMirrors:   map[string][]string{"docker.io": {"https://registry.docker-cn.com"}},
 			pauseImage:        "192.168.100.100:5000/kubernetes/pause:v3.1",
 		},
+		{
+			name: "kubelet-v1.22-nutanix",
+			spec: clusterv1alpha1.MachineSpec{
+				ObjectMeta: metav1.ObjectMeta{Name: "node1"},
+				Versions: clusterv1alpha1.MachineVersionInfo{
+					Kubelet: "1.22.2",
+				},
+			},
+			cloudProviderName: stringPtr("nutanix"),
+		},
 	}
 
 	defaultCloudProvider := &fakeCloudConfigProvider{
diff --git a/pkg/userdata/rhel/testdata/kubelet-v1.22-nutanix.yaml b/pkg/userdata/rhel/testdata/kubelet-v1.22-nutanix.yaml
new file mode 100644
index 00000000..a9576567
--- /dev/null
+++ b/pkg/userdata/rhel/testdata/kubelet-v1.22-nutanix.yaml
@@ -0,0 +1,465 @@
+#cloud-config
+bootcmd:
+- modprobe ip_tables
+
+hostname: node1
+fqdn: node1
+
+
+ssh_pwauth: false
+
+write_files:
+
+- path: "/etc/systemd/journald.conf.d/max_disk_use.conf"
+  content: |
+    [Journal]
+    SystemMaxUse=5G
+
+
+- path: "/opt/load-kernel-modules.sh"
+  permissions: "0755"
+  content: |
+    #!/usr/bin/env bash
+    set -euo pipefail
+
+    modprobe ip_vs
+    modprobe ip_vs_rr
+    modprobe ip_vs_wrr
+    modprobe ip_vs_sh
+
+    if modinfo nf_conntrack_ipv4 &> /dev/null; then
+      modprobe nf_conntrack_ipv4
+    else
+      modprobe nf_conntrack
+    fi
+
+
+- path: "/etc/sysctl.d/k8s.conf"
+  content: |
+    net.bridge.bridge-nf-call-ip6tables = 1
+    net.bridge.bridge-nf-call-iptables = 1
+    kernel.panic_on_oops = 1
+    kernel.panic = 10
+    net.ipv4.ip_forward = 1
+    vm.overcommit_memory = 1
+    fs.inotify.max_user_watches = 1048576
+
+
+- path: /etc/selinux/config
+  content: |
+    # This file controls the state of SELinux on the system.
+    # SELINUX= can take one of these three values:
+    #     enforcing - SELinux security policy is enforced.
+    #     permissive - SELinux prints warnings instead of enforcing.
+    #     disabled - No SELinux policy is loaded.
+    SELINUX=permissive
+    # SELINUXTYPE= can take one of three two values:
+    #     targeted - Targeted processes are protected,
+    #     minimum - Modification of targeted policy. Only selected processes are protected.
+    #     mls - Multi Level Security protection.
+    SELINUXTYPE=targeted
+
+- path: "/opt/bin/setup"
+  permissions: "0755"
+  content: |
+    #!/bin/bash
+    set -xeuo pipefail
+
+    setenforce 0 || true
+    systemctl restart systemd-modules-load.service
+    sysctl --system
+    sed -i.orig '/.*swap.*/d' /etc/fstab
+    swapoff -a
+
+    hostnamectl set-hostname node1
+
+
+    yum install -y \
+      device-mapper-persistent-data \
+      lvm2 \
+      ebtables \
+      ethtool \
+      nfs-utils \
+      bash-completion \
+      sudo \
+      socat \
+      wget \
+      curl \
+      iscsi-initiator-utils \
+      ipvsadm
+    systemctl enable --now iscsid
+
+
+    yum install -y yum-utils
+    yum-config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
+    yum-config-manager --save --setopt=docker-ce-stable.module_hotfixes=true
+
+    mkdir -p /etc/systemd/system/containerd.service.d /etc/systemd/system/docker.service.d
+
+    cat <<EOF | tee /etc/systemd/system/containerd.service.d/environment.conf /etc/systemd/system/docker.service.d/environment.conf
+    [Service]
+    Restart=always
+    EnvironmentFile=-/etc/environment
+    EOF
+
+    yum install -y \
+        docker-ce-cli-19.03* \
+        containerd.io-1.4* \
+        docker-ce-19.03* \
+        yum-plugin-versionlock
+    yum versionlock add docker-ce* containerd.io
+
+    systemctl daemon-reload
+    systemctl enable --now docker
+
+    opt_bin=/opt/bin
+    usr_local_bin=/usr/local/bin
+    cni_bin_dir=/opt/cni/bin
+    mkdir -p /etc/cni/net.d /etc/kubernetes/dynamic-config-dir /etc/kubernetes/manifests "$opt_bin" "$cni_bin_dir"
+    arch=${HOST_ARCH-}
+    if [ -z "$arch" ]
+    then
+    case $(uname -m) in
+    x86_64)
+        arch="amd64"
+        ;;
+    aarch64)
+        arch="arm64"
+        ;;
+    *)
+        echo "unsupported CPU architecture, exiting"
+        exit 1
+        ;;
+    esac
+    fi
+    CNI_VERSION="${CNI_VERSION:-v0.8.7}"
+    cni_base_url="https://github.com/containernetworking/plugins/releases/download/$CNI_VERSION"
+    cni_filename="cni-plugins-linux-$arch-$CNI_VERSION.tgz"
+    curl -Lfo "$cni_bin_dir/$cni_filename" "$cni_base_url/$cni_filename"
+    cni_sum=$(curl -Lf "$cni_base_url/$cni_filename.sha256")
+    cd "$cni_bin_dir"
+    sha256sum -c <<<"$cni_sum"
+    tar xvf "$cni_filename"
+    rm -f "$cni_filename"
+    cd -
+    CRI_TOOLS_RELEASE="${CRI_TOOLS_RELEASE:-v1.22.0}"
+    cri_tools_base_url="https://github.com/kubernetes-sigs/cri-tools/releases/download/${CRI_TOOLS_RELEASE}"
+    cri_tools_filename="crictl-${CRI_TOOLS_RELEASE}-linux-${arch}.tar.gz"
+    curl -Lfo "$opt_bin/$cri_tools_filename" "$cri_tools_base_url/$cri_tools_filename"
+    cri_tools_sum=$(curl -Lf "$cri_tools_base_url/$cri_tools_filename.sha256" | sed 's/\*\///')
+    cd "$opt_bin"
+    sha256sum -c <<<"$cri_tools_sum"
+    tar xvf "$cri_tools_filename"
+    rm -f "$cri_tools_filename"
+    ln -sf "$opt_bin/crictl" "$usr_local_bin"/crictl || echo "symbolic link is skipped"
+    cd -
+    KUBE_VERSION="${KUBE_VERSION:-v1.22.2}"
+    kube_dir="$opt_bin/kubernetes-$KUBE_VERSION"
+    kube_base_url="https://storage.googleapis.com/kubernetes-release/release/$KUBE_VERSION/bin/linux/$arch"
+    kube_sum_file="$kube_dir/sha256"
+    mkdir -p "$kube_dir"
+    : >"$kube_sum_file"
+
+    for bin in kubelet kubeadm kubectl; do
+        curl -Lfo "$kube_dir/$bin" "$kube_base_url/$bin"
+        chmod +x "$kube_dir/$bin"
+        sum=$(curl -Lf "$kube_base_url/$bin.sha256")
+        echo "$sum  $kube_dir/$bin" >>"$kube_sum_file"
+    done
+    sha256sum -c "$kube_sum_file"
+
+    for bin in kubelet kubeadm kubectl; do
+        ln -sf "$kube_dir/$bin" "$opt_bin"/$bin
+    done
+
+    if [[ ! -x /opt/bin/health-monitor.sh ]]; then
+        curl -Lfo /opt/bin/health-monitor.sh https://raw.githubusercontent.com/kubermatic/machine-controller/7967a0af2b75f29ad2ab227eeaa26ea7b0f2fbde/pkg/userdata/scripts/health-monitor.sh
+        chmod +x /opt/bin/health-monitor.sh
+    fi
+
+    # set kubelet nodeip environment variable
+    mkdir -p /etc/systemd/system/kubelet.service.d/
+    /opt/bin/setup_net_env.sh
+
+    systemctl enable --now kubelet
+    systemctl enable --now --no-block kubelet-healthcheck.service
+
+- path: "/opt/bin/supervise.sh"
+  permissions: "0755"
+  content: |
+    #!/bin/bash
+    set -xeuo pipefail
+    while ! "$@"; do
+      sleep 1
+    done
+
+- path: "/etc/systemd/system/kubelet.service"
+  content: |
+    [Unit]
+    After=docker.service
+    Requires=docker.service
+
+    Description=kubelet: The Kubernetes Node Agent
+    Documentation=https://kubernetes.io/docs/home/
+
+    [Service]
+    Restart=always
+    StartLimitInterval=0
+    RestartSec=10
+    CPUAccounting=true
+    MemoryAccounting=true
+
+    Environment="PATH=/opt/bin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin/"
+    EnvironmentFile=-/etc/environment
+
+    ExecStartPre=/bin/bash /opt/load-kernel-modules.sh
+    ExecStartPre=/bin/bash /opt/bin/setup_net_env.sh
+    ExecStart=/opt/bin/kubelet $KUBELET_EXTRA_ARGS \
+      --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf \
+      --kubeconfig=/var/lib/kubelet/kubeconfig \
+      --config=/etc/kubernetes/kubelet.conf \
+      --network-plugin=cni \
+      --cert-dir=/etc/kubernetes/pki \
+      --cloud-provider=nutanix \
+      --cloud-config=/etc/kubernetes/cloud-config \
+      --hostname-override=node1 \
+      --exit-on-lock-contention \
+      --lock-file=/tmp/kubelet.lock \
+      --container-runtime=docker \
+      --container-runtime-endpoint=unix:///var/run/dockershim.sock \
+      --dynamic-config-dir=/etc/kubernetes/dynamic-config-dir \
+      --feature-gates=DynamicKubeletConfig=true \
+      --node-ip ${KUBELET_NODE_IP}
+
+    [Install]
+    WantedBy=multi-user.target
+
+- path: "/etc/kubernetes/cloud-config"
+  permissions: "0600"
+  content: |
+    {config:true}
+
+- path: "/opt/bin/setup_net_env.sh"
+  permissions: "0755"
+  content: |
+    #!/usr/bin/env bash
+    echodate() {
+      echo "[$(date -Is)]" "$@"
+    }
+
+    # get the default interface IP address
+    DEFAULT_IFC_IP=$(ip -o  route get 1 | grep -oP "src \K\S+")
+
+    # get the full hostname
+    FULL_HOSTNAME=$(hostname -f)
+
+    if [ -z "${DEFAULT_IFC_IP}" ]
+    then
+    	echodate "Failed to get IP address for the default route interface"
+    	exit 1
+    fi
+
+    # write the nodeip_env file
+    # we need the line below because flatcar has the same string "coreos" in that file
+    if grep -q coreos /etc/os-release
+    then
+      echo -e "KUBELET_NODE_IP=${DEFAULT_IFC_IP}\nKUBELET_HOSTNAME=${FULL_HOSTNAME}" > /etc/kubernetes/nodeip.conf
+    elif [ ! -d /etc/systemd/system/kubelet.service.d ]
+    then
+    	echodate "Can't find kubelet service extras directory"
+    	exit 1
+    else
+      echo -e "[Service]\nEnvironment=\"KUBELET_NODE_IP=${DEFAULT_IFC_IP}\"\nEnvironment=\"KUBELET_HOSTNAME=${FULL_HOSTNAME}\"" > /etc/systemd/system/kubelet.service.d/nodeip.conf
+    fi
+
+
+- path: "/etc/kubernetes/bootstrap-kubelet.conf"
+  permissions: "0600"
+  content: |
+    apiVersion: v1
+    clusters:
+    - cluster:
+        certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVXakNDQTBLZ0F3SUJBZ0lKQUxmUmxXc0k4WVFITUEwR0NTcUdTSWIzRFFFQkJRVUFNSHN4Q3pBSkJnTlYKQkFZVEFsVlRNUXN3Q1FZRFZRUUlFd0pEUVRFV01CUUdBMVVFQnhNTlUyRnVJRVp5WVc1amFYTmpiekVVTUJJRwpBMVVFQ2hNTFFuSmhaR1pwZEhwcGJtTXhFakFRQmdOVkJBTVRDV3h2WTJGc2FHOXpkREVkTUJzR0NTcUdTSWIzCkRRRUpBUllPWW5KaFpFQmtZVzVuWVM1amIyMHdIaGNOTVRRd056RTFNakEwTmpBMVdoY05NVGN3TlRBME1qQTAKTmpBMVdqQjdNUXN3Q1FZRFZRUUdFd0pWVXpFTE1Ba0dBMVVFQ0JNQ1EwRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhGREFTQmdOVkJBb1RDMEp5WVdSbWFYUjZhVzVqTVJJd0VBWURWUVFERXdsc2IyTmhiR2h2CmMzUXhIVEFiQmdrcWhraUc5dzBCQ1FFV0RtSnlZV1JBWkdGdVoyRXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEIKQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDVmQWpwNGZUY2VrV1VUZnpzcDBreWloMU9ZYnNHTDBLWDFlUmJTUwpSOE9kMCs5UTYySHlueStHRndNVGI0QS9LVThtc3NvSHZjY2VTQUFid2ZieEZLLytzNTFUb2JxVW5PUlpyT29UClpqa1V5Z2J5WERTSzk5WUJiY1IxUGlwOHZ3TVRtNFhLdUx0Q2lnZUJCZGpqQVFkZ1VPMjhMRU5HbHNNbm1lWWsKSmZPRFZHblZtcjVMdGI5QU5BOElLeVRmc25ISjRpT0NTL1BsUGJVajJxN1lub1ZMcG9zVUJNbGdVYi9DeWtYMwptT29MYjR5SkpReUEvaVNUNlp4aUlFajM2RDR5V1o1bGc3WUpsK1VpaUJRSEdDblBkR3lpcHFWMDZleDBoZVlXCmNhaVc4TFdaU1VROTNqUStXVkNIOGhUN0RRTzFkbXN2VW1YbHEvSmVBbHdRL1FJREFRQUJvNEhnTUlIZE1CMEcKQTFVZERnUVdCQlJjQVJPdGhTNFA0VTd2VGZqQnlDNTY5UjdFNkRDQnJRWURWUjBqQklHbE1JR2lnQlJjQVJPdApoUzRQNFU3dlRmakJ5QzU2OVI3RTZLRi9wSDB3ZXpFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ1RBa05CCk1SWXdGQVlEVlFRSEV3MVRZVzRnUm5KaGJtTnBjMk52TVJRd0VnWURWUVFLRXd0Q2NtRmtabWwwZW1sdVl6RVMKTUJBR0ExVUVBeE1KYkc5allXeG9iM04wTVIwd0d3WUpLb1pJaHZjTkFRa0JGZzVpY21Ga1FHUmhibWRoTG1OdgpiWUlKQUxmUmxXc0k4WVFITUF3R0ExVWRFd1FGTUFNQkFmOHdEUVlKS29aSWh2Y05BUUVGQlFBRGdnRUJBRzZoClU5ZjlzTkgwLzZvQmJHR3kyRVZVMFVnSVRVUUlyRldvOXJGa3JXNWsvWGtEalFtKzNsempUMGlHUjRJeEUvQW8KZVU2c1FodWE3d3JXZUZFbjQ3R0w5OGxuQ3NKZEQ3b1pOaEZtUTk1VGIvTG5EVWpzNVlqOWJyUDBOV3pYZllVNApVSzJabklOSlJjSnBCOGlSQ2FDeEU4RGRjVUYwWHFJRXE2cEEyNzJzbm9MbWlYTE12Tmwza1lFZG0ramU2dm9ECjU4U05WRVVzenR6UXlYbUpFaENwd1ZJMEE2UUNqelhqK3F2cG13M1paSGk4SndYZWk4WlpCTFRTRkJraThaN24Kc0g5QkJIMzgvU3pVbUFONFFIU1B5MWdqcW0wME9BRThOYVlEa2gvYnpFNGQ3bUxHR01XcC9XRTNLUFN1ODJIRgprUGU2WG9TYmlMbS9reGszMlQwPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
+        server: https://server:443
+      name: ""
+    contexts: null
+    current-context: ""
+    kind: Config
+    preferences: {}
+    users:
+    - name: ""
+      user:
+        token: my-token
+
+
+- path: "/etc/kubernetes/kubelet.conf"
+  content: |
+    apiVersion: kubelet.config.k8s.io/v1beta1
+    authentication:
+      anonymous:
+        enabled: false
+      webhook:
+        cacheTTL: 0s
+        enabled: true
+      x509:
+        clientCAFile: /etc/kubernetes/pki/ca.crt
+    authorization:
+      mode: Webhook
+      webhook:
+        cacheAuthorizedTTL: 0s
+        cacheUnauthorizedTTL: 0s
+    cgroupDriver: systemd
+    clusterDomain: cluster.local
+    cpuManagerReconcilePeriod: 0s
+    evictionHard:
+      imagefs.available: 15%
+      memory.available: 100Mi
+      nodefs.available: 10%
+      nodefs.inodesFree: 5%
+    evictionPressureTransitionPeriod: 0s
+    featureGates:
+      RotateKubeletServerCertificate: true
+    fileCheckFrequency: 0s
+    httpCheckFrequency: 0s
+    imageMinimumGCAge: 0s
+    kind: KubeletConfiguration
+    kubeReserved:
+      cpu: 200m
+      ephemeral-storage: 1Gi
+      memory: 200Mi
+    logging: {}
+    memorySwap: {}
+    nodeStatusReportFrequency: 0s
+    nodeStatusUpdateFrequency: 0s
+    protectKernelDefaults: true
+    rotateCertificates: true
+    runtimeRequestTimeout: 0s
+    serverTLSBootstrap: true
+    shutdownGracePeriod: 0s
+    shutdownGracePeriodCriticalPods: 0s
+    staticPodPath: /etc/kubernetes/manifests
+    streamingConnectionIdleTimeout: 0s
+    syncFrequency: 0s
+    systemReserved:
+      cpu: 200m
+      ephemeral-storage: 1Gi
+      memory: 200Mi
+    tlsCipherSuites:
+    - TLS_AES_128_GCM_SHA256
+    - TLS_AES_256_GCM_SHA384
+    - TLS_CHACHA20_POLY1305_SHA256
+    - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
+    - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
+    - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
+    - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+    - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
+    - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
+    volumePluginDir: /var/lib/kubelet/volumeplugins
+    volumeStatsAggPeriod: 0s
+
+
+- path: "/etc/kubernetes/pki/ca.crt"
+  content: |
+    -----BEGIN CERTIFICATE-----
+    MIIEWjCCA0KgAwIBAgIJALfRlWsI8YQHMA0GCSqGSIb3DQEBBQUAMHsxCzAJBgNV
+    BAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEUMBIG
+    A1UEChMLQnJhZGZpdHppbmMxEjAQBgNVBAMTCWxvY2FsaG9zdDEdMBsGCSqGSIb3
+    DQEJARYOYnJhZEBkYW5nYS5jb20wHhcNMTQwNzE1MjA0NjA1WhcNMTcwNTA0MjA0
+    NjA1WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBG
+    cmFuY2lzY28xFDASBgNVBAoTC0JyYWRmaXR6aW5jMRIwEAYDVQQDEwlsb2NhbGhv
+    c3QxHTAbBgkqhkiG9w0BCQEWDmJyYWRAZGFuZ2EuY29tMIIBIjANBgkqhkiG9w0B
+    AQEFAAOCAQ8AMIIBCgKCAQEAt5fAjp4fTcekWUTfzsp0kyih1OYbsGL0KX1eRbSS
+    R8Od0+9Q62Hyny+GFwMTb4A/KU8mssoHvcceSAAbwfbxFK/+s51TobqUnORZrOoT
+    ZjkUygbyXDSK99YBbcR1Pip8vwMTm4XKuLtCigeBBdjjAQdgUO28LENGlsMnmeYk
+    JfODVGnVmr5Ltb9ANA8IKyTfsnHJ4iOCS/PlPbUj2q7YnoVLposUBMlgUb/CykX3
+    mOoLb4yJJQyA/iST6ZxiIEj36D4yWZ5lg7YJl+UiiBQHGCnPdGyipqV06ex0heYW
+    caiW8LWZSUQ93jQ+WVCH8hT7DQO1dmsvUmXlq/JeAlwQ/QIDAQABo4HgMIHdMB0G
+    A1UdDgQWBBRcAROthS4P4U7vTfjByC569R7E6DCBrQYDVR0jBIGlMIGigBRcAROt
+    hS4P4U7vTfjByC569R7E6KF/pH0wezELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNB
+    MRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRQwEgYDVQQKEwtCcmFkZml0emluYzES
+    MBAGA1UEAxMJbG9jYWxob3N0MR0wGwYJKoZIhvcNAQkBFg5icmFkQGRhbmdhLmNv
+    bYIJALfRlWsI8YQHMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAG6h
+    U9f9sNH0/6oBbGGy2EVU0UgITUQIrFWo9rFkrW5k/XkDjQm+3lzjT0iGR4IxE/Ao
+    eU6sQhua7wrWeFEn47GL98lnCsJdD7oZNhFmQ95Tb/LnDUjs5Yj9brP0NWzXfYU4
+    UK2ZnINJRcJpB8iRCaCxE8DdcUF0XqIEq6pA272snoLmiXLMvNl3kYEdm+je6voD
+    58SNVEUsztzQyXmJEhCpwVI0A6QCjzXj+qvpmw3ZZHi8JwXei8ZZBLTSFBki8Z7n
+    sH9BBH38/SzUmAN4QHSPy1gjqm00OAE8NaYDkh/bzE4d7mLGGMWp/WE3KPSu82HF
+    kPe6XoSbiLm/kxk32T0=
+    -----END CERTIFICATE-----
+
+- path: "/etc/systemd/system/setup.service"
+  permissions: "0644"
+  content: |
+    [Install]
+    WantedBy=multi-user.target
+
+    [Unit]
+    Requires=network-online.target
+    After=network-online.target
+
+    [Service]
+    Type=oneshot
+    RemainAfterExit=true
+    EnvironmentFile=-/etc/environment
+    ExecStart=/opt/bin/supervise.sh /opt/bin/setup
+
+- path: "/etc/profile.d/opt-bin-path.sh"
+  permissions: "0644"
+  content: |
+    export PATH="/opt/bin:$PATH"
+
+- path: /etc/docker/daemon.json
+  permissions: "0644"
+  content: |
+    {"exec-opts":["native.cgroupdriver=systemd"],"storage-driver":"overlay2","log-driver":"json-file","log-opts":{"max-file":"5","max-size":"10m"}}
+
+- path: /etc/systemd/system/kubelet-healthcheck.service
+  permissions: "0644"
+  content: |
+    [Unit]
+    Requires=kubelet.service
+    After=kubelet.service
+
+    [Service]
+    ExecStart=/opt/bin/health-monitor.sh kubelet
+
+    [Install]
+    WantedBy=multi-user.target
+
+
+- path: "/opt/bin/disable-nm-cloud-setup"
+  permissions: "0755"
+  content: |
+    #!/bin/bash
+    set -xeuo pipefail
+    if systemctl status 'nm-cloud-setup.timer' 2> /dev/null | grep -Fq "Active:"; then
+            systemctl stop nm-cloud-setup.timer
+            systemctl disable nm-cloud-setup.service
+            systemctl disable nm-cloud-setup.timer
+            reboot
+    fi
+
+- path: "/etc/systemd/system/disable-nm-cloud-setup.service"
+  permissions: "0644"
+  content: |
+    [Install]
+    WantedBy=multi-user.target
+
+    [Unit]
+    Requires=network-online.target
+    After=network-online.target
+
+    [Service]
+    Type=oneshot
+    RemainAfterExit=true
+    EnvironmentFile=-/etc/environment
+    ExecStart=/opt/bin/supervise.sh /opt/bin/disable-nm-cloud-setup
+
+rh_subscription:
+    username: ""
+    password: ""
+    auto-attach: false
+
+runcmd:
+- systemctl start setup.service
+- systemctl start disable-nm-cloud-setup.service
diff --git a/pkg/userdata/ubuntu/provider_test.go b/pkg/userdata/ubuntu/provider_test.go
index c1229bcd..0962a064 100644
--- a/pkg/userdata/ubuntu/provider_test.go
+++ b/pkg/userdata/ubuntu/provider_test.go
@@ -444,6 +444,32 @@ func TestUserDataGeneration(t *testing.T) {
 				DistUpgradeOnBoot: true,
 			},
 		},
+		{
+			name: "nutanix",
+			providerSpec: &providerconfigtypes.Config{
+				CloudProvider:        "nutanix",
+				SSHPublicKeys:        []string{"ssh-rsa AAABBB"},
+				OverwriteCloudConfig: stringPtr("custom\ncloud\nconfig"),
+			},
+			spec: clusterv1alpha1.MachineSpec{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "node1",
+				},
+				Versions: clusterv1alpha1.MachineVersionInfo{
+					Kubelet: "1.21.5",
+				},
+			},
+			ccProvider: &fakeCloudConfigProvider{
+				name:   "nutanix",
+				config: "{nutanix-config:true}",
+				err:    nil,
+			},
+			DNSIPs:           []net.IP{net.ParseIP("10.10.10.10")},
+			kubernetesCACert: "CACert",
+			osConfig: &Config{
+				DistUpgradeOnBoot: false,
+			},
+		},
 	}...)
 
 	for _, test := range tests {
diff --git a/pkg/userdata/ubuntu/testdata/nutanix.yaml b/pkg/userdata/ubuntu/testdata/nutanix.yaml
new file mode 100644
index 00000000..23450179
--- /dev/null
+++ b/pkg/userdata/ubuntu/testdata/nutanix.yaml
@@ -0,0 +1,440 @@
+#cloud-config
+
+hostname: node1
+
+
+ssh_pwauth: false
+ssh_authorized_keys:
+- "ssh-rsa AAABBB"
+
+write_files:
+
+- path: "/etc/systemd/journald.conf.d/max_disk_use.conf"
+  content: |
+    [Journal]
+    SystemMaxUse=5G
+
+
+- path: "/opt/load-kernel-modules.sh"
+  permissions: "0755"
+  content: |
+    #!/usr/bin/env bash
+    set -euo pipefail
+
+    modprobe ip_vs
+    modprobe ip_vs_rr
+    modprobe ip_vs_wrr
+    modprobe ip_vs_sh
+
+    if modinfo nf_conntrack_ipv4 &> /dev/null; then
+      modprobe nf_conntrack_ipv4
+    else
+      modprobe nf_conntrack
+    fi
+
+
+- path: "/etc/sysctl.d/k8s.conf"
+  content: |
+    net.bridge.bridge-nf-call-ip6tables = 1
+    net.bridge.bridge-nf-call-iptables = 1
+    kernel.panic_on_oops = 1
+    kernel.panic = 10
+    net.ipv4.ip_forward = 1
+    vm.overcommit_memory = 1
+    fs.inotify.max_user_watches = 1048576
+
+
+- path: "/etc/default/grub.d/60-swap-accounting.cfg"
+  content: |
+    # Added by kubermatic machine-controller
+    # Enable cgroups memory and swap accounting
+    GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"
+
+- path: "/opt/bin/setup"
+  permissions: "0755"
+  content: |
+    #!/bin/bash
+    set -xeuo pipefail
+    if systemctl is-active ufw; then systemctl stop ufw; fi
+    systemctl mask ufw
+    systemctl restart systemd-modules-load.service
+    sysctl --system
+    sed -i.orig '/.*swap.*/d' /etc/fstab
+    swapoff -a
+
+    apt-get update
+
+    DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install -y \
+      curl \
+      ca-certificates \
+      ceph-common \
+      cifs-utils \
+      conntrack \
+      e2fsprogs \
+      ebtables \
+      ethtool \
+      glusterfs-client \
+      iptables \
+      jq \
+      kmod \
+      openssh-client \
+      nfs-common \
+      socat \
+      util-linux \
+      open-iscsi \
+      ipvsadm
+    systemctl enable --now iscsid
+
+
+    # Update grub to include kernel command options to enable swap accounting.
+    # Exclude alibaba cloud until this is fixed https://github.com/kubermatic/machine-controller/issues/682
+
+
+    apt-get update
+    apt-get install -y apt-transport-https ca-certificates curl software-properties-common lsb-release
+    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
+    add-apt-repository "deb https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
+
+    mkdir -p /etc/systemd/system/containerd.service.d /etc/systemd/system/docker.service.d
+
+    cat <<EOF | tee /etc/systemd/system/containerd.service.d/environment.conf /etc/systemd/system/docker.service.d/environment.conf
+    [Service]
+    Restart=always
+    EnvironmentFile=-/etc/environment
+    EOF
+
+    apt-get install --allow-downgrades -y \
+        containerd.io=1.4* \
+        docker-ce-cli=5:19.03* \
+        docker-ce=5:19.03*
+    apt-mark hold docker-ce* containerd.io
+
+    systemctl daemon-reload
+    systemctl enable --now docker
+
+
+    opt_bin=/opt/bin
+    usr_local_bin=/usr/local/bin
+    cni_bin_dir=/opt/cni/bin
+    mkdir -p /etc/cni/net.d /etc/kubernetes/dynamic-config-dir /etc/kubernetes/manifests "$opt_bin" "$cni_bin_dir"
+    arch=${HOST_ARCH-}
+    if [ -z "$arch" ]
+    then
+    case $(uname -m) in
+    x86_64)
+        arch="amd64"
+        ;;
+    aarch64)
+        arch="arm64"
+        ;;
+    *)
+        echo "unsupported CPU architecture, exiting"
+        exit 1
+        ;;
+    esac
+    fi
+    CNI_VERSION="${CNI_VERSION:-v0.8.7}"
+    cni_base_url="https://github.com/containernetworking/plugins/releases/download/$CNI_VERSION"
+    cni_filename="cni-plugins-linux-$arch-$CNI_VERSION.tgz"
+    curl -Lfo "$cni_bin_dir/$cni_filename" "$cni_base_url/$cni_filename"
+    cni_sum=$(curl -Lf "$cni_base_url/$cni_filename.sha256")
+    cd "$cni_bin_dir"
+    sha256sum -c <<<"$cni_sum"
+    tar xvf "$cni_filename"
+    rm -f "$cni_filename"
+    cd -
+    CRI_TOOLS_RELEASE="${CRI_TOOLS_RELEASE:-v1.22.0}"
+    cri_tools_base_url="https://github.com/kubernetes-sigs/cri-tools/releases/download/${CRI_TOOLS_RELEASE}"
+    cri_tools_filename="crictl-${CRI_TOOLS_RELEASE}-linux-${arch}.tar.gz"
+    curl -Lfo "$opt_bin/$cri_tools_filename" "$cri_tools_base_url/$cri_tools_filename"
+    cri_tools_sum=$(curl -Lf "$cri_tools_base_url/$cri_tools_filename.sha256" | sed 's/\*\///')
+    cd "$opt_bin"
+    sha256sum -c <<<"$cri_tools_sum"
+    tar xvf "$cri_tools_filename"
+    rm -f "$cri_tools_filename"
+    ln -sf "$opt_bin/crictl" "$usr_local_bin"/crictl || echo "symbolic link is skipped"
+    cd -
+    KUBE_VERSION="${KUBE_VERSION:-v1.21.5}"
+    kube_dir="$opt_bin/kubernetes-$KUBE_VERSION"
+    kube_base_url="https://storage.googleapis.com/kubernetes-release/release/$KUBE_VERSION/bin/linux/$arch"
+    kube_sum_file="$kube_dir/sha256"
+    mkdir -p "$kube_dir"
+    : >"$kube_sum_file"
+
+    for bin in kubelet kubeadm kubectl; do
+        curl -Lfo "$kube_dir/$bin" "$kube_base_url/$bin"
+        chmod +x "$kube_dir/$bin"
+        sum=$(curl -Lf "$kube_base_url/$bin.sha256")
+        echo "$sum  $kube_dir/$bin" >>"$kube_sum_file"
+    done
+    sha256sum -c "$kube_sum_file"
+
+    for bin in kubelet kubeadm kubectl; do
+        ln -sf "$kube_dir/$bin" "$opt_bin"/$bin
+    done
+
+    if [[ ! -x /opt/bin/health-monitor.sh ]]; then
+        curl -Lfo /opt/bin/health-monitor.sh https://raw.githubusercontent.com/kubermatic/machine-controller/7967a0af2b75f29ad2ab227eeaa26ea7b0f2fbde/pkg/userdata/scripts/health-monitor.sh
+        chmod +x /opt/bin/health-monitor.sh
+    fi
+
+    # set kubelet nodeip environment variable
+    /opt/bin/setup_net_env.sh
+
+    systemctl enable --now kubelet
+    systemctl enable --now --no-block kubelet-healthcheck.service
+
+- path: "/opt/bin/supervise.sh"
+  permissions: "0755"
+  content: |
+    #!/bin/bash
+    set -xeuo pipefail
+    while ! "$@"; do
+      sleep 1
+    done
+
+- path: "/etc/systemd/system/kubelet.service"
+  content: |
+    [Unit]
+    After=docker.service
+    Requires=docker.service
+
+    Description=kubelet: The Kubernetes Node Agent
+    Documentation=https://kubernetes.io/docs/home/
+
+    [Service]
+    Restart=always
+    StartLimitInterval=0
+    RestartSec=10
+    CPUAccounting=true
+    MemoryAccounting=true
+
+    Environment="PATH=/opt/bin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin/"
+    EnvironmentFile=-/etc/environment
+
+    ExecStartPre=/bin/bash /opt/load-kernel-modules.sh
+    ExecStartPre=/bin/bash /opt/bin/setup_net_env.sh
+    ExecStart=/opt/bin/kubelet $KUBELET_EXTRA_ARGS \
+      --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf \
+      --kubeconfig=/var/lib/kubelet/kubeconfig \
+      --config=/etc/kubernetes/kubelet.conf \
+      --network-plugin=cni \
+      --cert-dir=/etc/kubernetes/pki \
+      --cloud-provider=nutanix \
+      --cloud-config=/etc/kubernetes/cloud-config \
+      --hostname-override=node1 \
+      --exit-on-lock-contention \
+      --lock-file=/tmp/kubelet.lock \
+      --container-runtime=docker \
+      --container-runtime-endpoint=unix:///var/run/dockershim.sock \
+      --dynamic-config-dir=/etc/kubernetes/dynamic-config-dir \
+      --feature-gates=DynamicKubeletConfig=true \
+      --node-ip ${KUBELET_NODE_IP}
+
+    [Install]
+    WantedBy=multi-user.target
+
+- path: "/etc/systemd/system/kubelet.service.d/extras.conf"
+  content: |
+    [Service]
+    Environment="KUBELET_EXTRA_ARGS=--resolv-conf=/run/systemd/resolve/resolv.conf"
+
+- path: "/etc/kubernetes/cloud-config"
+  permissions: "0600"
+  content: |
+    custom
+    cloud
+    config
+
+- path: "/opt/bin/setup_net_env.sh"
+  permissions: "0755"
+  content: |
+    #!/usr/bin/env bash
+    echodate() {
+      echo "[$(date -Is)]" "$@"
+    }
+
+    # get the default interface IP address
+    DEFAULT_IFC_IP=$(ip -o  route get 1 | grep -oP "src \K\S+")
+
+    # get the full hostname
+    FULL_HOSTNAME=$(hostname -f)
+
+    if [ -z "${DEFAULT_IFC_IP}" ]
+    then
+    	echodate "Failed to get IP address for the default route interface"
+    	exit 1
+    fi
+
+    # write the nodeip_env file
+    # we need the line below because flatcar has the same string "coreos" in that file
+    if grep -q coreos /etc/os-release
+    then
+      echo -e "KUBELET_NODE_IP=${DEFAULT_IFC_IP}\nKUBELET_HOSTNAME=${FULL_HOSTNAME}" > /etc/kubernetes/nodeip.conf
+    elif [ ! -d /etc/systemd/system/kubelet.service.d ]
+    then
+    	echodate "Can't find kubelet service extras directory"
+    	exit 1
+    else
+      echo -e "[Service]\nEnvironment=\"KUBELET_NODE_IP=${DEFAULT_IFC_IP}\"\nEnvironment=\"KUBELET_HOSTNAME=${FULL_HOSTNAME}\"" > /etc/systemd/system/kubelet.service.d/nodeip.conf
+    fi
+
+
+- path: "/etc/kubernetes/bootstrap-kubelet.conf"
+  permissions: "0600"
+  content: |
+    apiVersion: v1
+    clusters:
+    - cluster:
+        certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVXakNDQTBLZ0F3SUJBZ0lKQUxmUmxXc0k4WVFITUEwR0NTcUdTSWIzRFFFQkJRVUFNSHN4Q3pBSkJnTlYKQkFZVEFsVlRNUXN3Q1FZRFZRUUlFd0pEUVRFV01CUUdBMVVFQnhNTlUyRnVJRVp5WVc1amFYTmpiekVVTUJJRwpBMVVFQ2hNTFFuSmhaR1pwZEhwcGJtTXhFakFRQmdOVkJBTVRDV3h2WTJGc2FHOXpkREVkTUJzR0NTcUdTSWIzCkRRRUpBUllPWW5KaFpFQmtZVzVuWVM1amIyMHdIaGNOTVRRd056RTFNakEwTmpBMVdoY05NVGN3TlRBME1qQTAKTmpBMVdqQjdNUXN3Q1FZRFZRUUdFd0pWVXpFTE1Ba0dBMVVFQ0JNQ1EwRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhGREFTQmdOVkJBb1RDMEp5WVdSbWFYUjZhVzVqTVJJd0VBWURWUVFERXdsc2IyTmhiR2h2CmMzUXhIVEFiQmdrcWhraUc5dzBCQ1FFV0RtSnlZV1JBWkdGdVoyRXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEIKQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDVmQWpwNGZUY2VrV1VUZnpzcDBreWloMU9ZYnNHTDBLWDFlUmJTUwpSOE9kMCs5UTYySHlueStHRndNVGI0QS9LVThtc3NvSHZjY2VTQUFid2ZieEZLLytzNTFUb2JxVW5PUlpyT29UClpqa1V5Z2J5WERTSzk5WUJiY1IxUGlwOHZ3TVRtNFhLdUx0Q2lnZUJCZGpqQVFkZ1VPMjhMRU5HbHNNbm1lWWsKSmZPRFZHblZtcjVMdGI5QU5BOElLeVRmc25ISjRpT0NTL1BsUGJVajJxN1lub1ZMcG9zVUJNbGdVYi9DeWtYMwptT29MYjR5SkpReUEvaVNUNlp4aUlFajM2RDR5V1o1bGc3WUpsK1VpaUJRSEdDblBkR3lpcHFWMDZleDBoZVlXCmNhaVc4TFdaU1VROTNqUStXVkNIOGhUN0RRTzFkbXN2VW1YbHEvSmVBbHdRL1FJREFRQUJvNEhnTUlIZE1CMEcKQTFVZERnUVdCQlJjQVJPdGhTNFA0VTd2VGZqQnlDNTY5UjdFNkRDQnJRWURWUjBqQklHbE1JR2lnQlJjQVJPdApoUzRQNFU3dlRmakJ5QzU2OVI3RTZLRi9wSDB3ZXpFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ1RBa05CCk1SWXdGQVlEVlFRSEV3MVRZVzRnUm5KaGJtTnBjMk52TVJRd0VnWURWUVFLRXd0Q2NtRmtabWwwZW1sdVl6RVMKTUJBR0ExVUVBeE1KYkc5allXeG9iM04wTVIwd0d3WUpLb1pJaHZjTkFRa0JGZzVpY21Ga1FHUmhibWRoTG1OdgpiWUlKQUxmUmxXc0k4WVFITUF3R0ExVWRFd1FGTUFNQkFmOHdEUVlKS29aSWh2Y05BUUVGQlFBRGdnRUJBRzZoClU5ZjlzTkgwLzZvQmJHR3kyRVZVMFVnSVRVUUlyRldvOXJGa3JXNWsvWGtEalFtKzNsempUMGlHUjRJeEUvQW8KZVU2c1FodWE3d3JXZUZFbjQ3R0w5OGxuQ3NKZEQ3b1pOaEZtUTk1VGIvTG5EVWpzNVlqOWJyUDBOV3pYZllVNApVSzJabklOSlJjSnBCOGlSQ2FDeEU4RGRjVUYwWHFJRXE2cEEyNzJzbm9MbWlYTE12Tmwza1lFZG0ramU2dm9ECjU4U05WRVVzenR6UXlYbUpFaENwd1ZJMEE2UUNqelhqK3F2cG13M1paSGk4SndYZWk4WlpCTFRTRkJraThaN24Kc0g5QkJIMzgvU3pVbUFONFFIU1B5MWdqcW0wME9BRThOYVlEa2gvYnpFNGQ3bUxHR01XcC9XRTNLUFN1ODJIRgprUGU2WG9TYmlMbS9reGszMlQwPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
+        server: https://server:443
+      name: ""
+    contexts: null
+    current-context: ""
+    kind: Config
+    preferences: {}
+    users:
+    - name: ""
+      user:
+        token: my-token
+
+
+- path: "/etc/kubernetes/pki/ca.crt"
+  content: |
+    -----BEGIN CERTIFICATE-----
+    MIIEWjCCA0KgAwIBAgIJALfRlWsI8YQHMA0GCSqGSIb3DQEBBQUAMHsxCzAJBgNV
+    BAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEUMBIG
+    A1UEChMLQnJhZGZpdHppbmMxEjAQBgNVBAMTCWxvY2FsaG9zdDEdMBsGCSqGSIb3
+    DQEJARYOYnJhZEBkYW5nYS5jb20wHhcNMTQwNzE1MjA0NjA1WhcNMTcwNTA0MjA0
+    NjA1WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBG
+    cmFuY2lzY28xFDASBgNVBAoTC0JyYWRmaXR6aW5jMRIwEAYDVQQDEwlsb2NhbGhv
+    c3QxHTAbBgkqhkiG9w0BCQEWDmJyYWRAZGFuZ2EuY29tMIIBIjANBgkqhkiG9w0B
+    AQEFAAOCAQ8AMIIBCgKCAQEAt5fAjp4fTcekWUTfzsp0kyih1OYbsGL0KX1eRbSS
+    R8Od0+9Q62Hyny+GFwMTb4A/KU8mssoHvcceSAAbwfbxFK/+s51TobqUnORZrOoT
+    ZjkUygbyXDSK99YBbcR1Pip8vwMTm4XKuLtCigeBBdjjAQdgUO28LENGlsMnmeYk
+    JfODVGnVmr5Ltb9ANA8IKyTfsnHJ4iOCS/PlPbUj2q7YnoVLposUBMlgUb/CykX3
+    mOoLb4yJJQyA/iST6ZxiIEj36D4yWZ5lg7YJl+UiiBQHGCnPdGyipqV06ex0heYW
+    caiW8LWZSUQ93jQ+WVCH8hT7DQO1dmsvUmXlq/JeAlwQ/QIDAQABo4HgMIHdMB0G
+    A1UdDgQWBBRcAROthS4P4U7vTfjByC569R7E6DCBrQYDVR0jBIGlMIGigBRcAROt
+    hS4P4U7vTfjByC569R7E6KF/pH0wezELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNB
+    MRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRQwEgYDVQQKEwtCcmFkZml0emluYzES
+    MBAGA1UEAxMJbG9jYWxob3N0MR0wGwYJKoZIhvcNAQkBFg5icmFkQGRhbmdhLmNv
+    bYIJALfRlWsI8YQHMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAG6h
+    U9f9sNH0/6oBbGGy2EVU0UgITUQIrFWo9rFkrW5k/XkDjQm+3lzjT0iGR4IxE/Ao
+    eU6sQhua7wrWeFEn47GL98lnCsJdD7oZNhFmQ95Tb/LnDUjs5Yj9brP0NWzXfYU4
+    UK2ZnINJRcJpB8iRCaCxE8DdcUF0XqIEq6pA272snoLmiXLMvNl3kYEdm+je6voD
+    58SNVEUsztzQyXmJEhCpwVI0A6QCjzXj+qvpmw3ZZHi8JwXei8ZZBLTSFBki8Z7n
+    sH9BBH38/SzUmAN4QHSPy1gjqm00OAE8NaYDkh/bzE4d7mLGGMWp/WE3KPSu82HF
+    kPe6XoSbiLm/kxk32T0=
+    -----END CERTIFICATE-----
+
+- path: "/etc/systemd/system/setup.service"
+  permissions: "0644"
+  content: |
+    [Install]
+    WantedBy=multi-user.target
+
+    [Unit]
+    Requires=network-online.target
+    After=network-online.target
+
+    [Service]
+    Type=oneshot
+    RemainAfterExit=true
+    EnvironmentFile=-/etc/environment
+    ExecStart=/opt/bin/supervise.sh /opt/bin/setup
+
+- path: "/etc/profile.d/opt-bin-path.sh"
+  permissions: "0644"
+  content: |
+    export PATH="/opt/bin:$PATH"
+
+- path: /etc/docker/daemon.json
+  permissions: "0644"
+  content: |
+    {"exec-opts":["native.cgroupdriver=systemd"],"storage-driver":"overlay2","log-driver":"json-file","log-opts":{"max-file":"5","max-size":"10m"}}
+
+- path: "/etc/kubernetes/kubelet.conf"
+  content: |
+    apiVersion: kubelet.config.k8s.io/v1beta1
+    authentication:
+      anonymous:
+        enabled: false
+      webhook:
+        cacheTTL: 0s
+        enabled: true
+      x509:
+        clientCAFile: /etc/kubernetes/pki/ca.crt
+    authorization:
+      mode: Webhook
+      webhook:
+        cacheAuthorizedTTL: 0s
+        cacheUnauthorizedTTL: 0s
+    cgroupDriver: systemd
+    clusterDNS:
+    - 10.10.10.10
+    clusterDomain: cluster.local
+    cpuManagerReconcilePeriod: 0s
+    evictionHard:
+      imagefs.available: 15%
+      memory.available: 100Mi
+      nodefs.available: 10%
+      nodefs.inodesFree: 5%
+    evictionPressureTransitionPeriod: 0s
+    featureGates:
+      RotateKubeletServerCertificate: true
+    fileCheckFrequency: 0s
+    httpCheckFrequency: 0s
+    imageMinimumGCAge: 0s
+    kind: KubeletConfiguration
+    kubeReserved:
+      cpu: 200m
+      ephemeral-storage: 1Gi
+      memory: 200Mi
+    logging: {}
+    memorySwap: {}
+    nodeStatusReportFrequency: 0s
+    nodeStatusUpdateFrequency: 0s
+    protectKernelDefaults: true
+    rotateCertificates: true
+    runtimeRequestTimeout: 0s
+    serverTLSBootstrap: true
+    shutdownGracePeriod: 0s
+    shutdownGracePeriodCriticalPods: 0s
+    staticPodPath: /etc/kubernetes/manifests
+    streamingConnectionIdleTimeout: 0s
+    syncFrequency: 0s
+    systemReserved:
+      cpu: 200m
+      ephemeral-storage: 1Gi
+      memory: 200Mi
+    tlsCipherSuites:
+    - TLS_AES_128_GCM_SHA256
+    - TLS_AES_256_GCM_SHA384
+    - TLS_CHACHA20_POLY1305_SHA256
+    - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
+    - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
+    - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
+    - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+    - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
+    - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
+    volumePluginDir: /var/lib/kubelet/volumeplugins
+    volumeStatsAggPeriod: 0s
+
+
+- path: /etc/systemd/system/kubelet-healthcheck.service
+  permissions: "0644"
+  content: |
+    [Unit]
+    Requires=kubelet.service
+    After=kubelet.service
+
+    [Service]
+    ExecStart=/opt/bin/health-monitor.sh kubelet
+
+    [Install]
+    WantedBy=multi-user.target
+
+
+runcmd:
+- systemctl start setup.service