Repository: rancher-sandbox/cluster-api-provider-rke2 (go)

Issue:
convert AdditionalUserData.Config  field in CRD from string to dict
**Describe the solution you'd like:**
We would like to have in CRD _RKE2ConfigTemplate_ and _RKE2ControlPlane_ the field AdditionalUserData.Config as dict instead of string.

**Why do you want this feature:**
We are using Helm as template engine. We use multiple values file to override specific part of the chart.
We would like to have a one to one matching between types of CRD used in the chart and type in the value of the chart.

If the field, AdditionalUserData.Config,  was a dict, we could benefit from native helm features to merge override values files.

Pull request:
Add arbitrary data field
<!--
Label the PR with the kind of change this for:

kind/feature
kind/bug
kind/documentation
kind/regression
kind/*
-->

**What this PR does / why we need it**:

<!-- Enter a description of the change and why this change is needed -->

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #167 

**Special notes for your reviewer**:

**Checklist**:
<!-- Put an "X" character inside the brackets of each completed task. Some may be optional depending on the PR in which case these can be deleted -->

- [ ] squashed commits into logical changes
- [ ] includes documentation
- [x] adds unit tests
- [ ] adds or updates e2e tests


Patch:
diff --git a/bootstrap/api/v1alpha1/rke2config_types.go b/bootstrap/api/v1alpha1/rke2config_types.go
index 4fe3d15..07f4b7e 100644
--- a/bootstrap/api/v1alpha1/rke2config_types.go
+++ b/bootstrap/api/v1alpha1/rke2config_types.go
@@ -164,13 +164,21 @@ type RKE2AgentConfig struct {
 	AdditionalUserData AdditionalUserData `json:"additionalUserData,omitempty"`
 }
 
-// AdditionalUserData is a field that allows users to specify additional cloud-init configuration .
+// AdditionalUserData is a field that allows users to specify additional
+// cloud-init configuration .
+// +kubebuilder:validation:XValidation:rule="!has(self.data) || !has(self.config)", message="Only config or data could be populated at once"
 type AdditionalUserData struct {
 	// In case of using ignition, the data format is documented here: https://kinvolk.io/docs/flatcar-container-linux/latest/provisioning/cl-config/
 	// NOTE: All fields of the UserData that are managed by the RKE2Config controller will be ignored, this include "write_files", "runcmd", "ntp".
 	// +optional
+	// Deprecated: Data is reserved for the arbitrary cloud-init data
 	Config string `json:"config,omitempty"`
 
+	// Data allows to pass arbitrary set of key/value pairs consistent with
+	// https://cloudinit.readthedocs.io/en/latest/reference/modules.html
+	// to extend existing cloud-init configuration
+	Data map[string]string `json:"data,omitempty"`
+
 	// Strict controls if Config should be strictly parsed. If so, warnings are treated as errors.
 	// +optional
 	Strict bool `json:"strict,omitempty"`
diff --git a/bootstrap/api/v1alpha1/zz_generated.deepcopy.go b/bootstrap/api/v1alpha1/zz_generated.deepcopy.go
index dc0c5fe..f21525a 100644
--- a/bootstrap/api/v1alpha1/zz_generated.deepcopy.go
+++ b/bootstrap/api/v1alpha1/zz_generated.deepcopy.go
@@ -30,6 +30,13 @@ import (
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *AdditionalUserData) DeepCopyInto(out *AdditionalUserData) {
 	*out = *in
+	if in.Data != nil {
+		in, out := &in.Data, &out.Data
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalUserData.
@@ -209,7 +216,7 @@ func (in *RKE2AgentConfig) DeepCopyInto(out *RKE2AgentConfig) {
 		*out = new(ComponentConfig)
 		(*in).DeepCopyInto(*out)
 	}
-	out.AdditionalUserData = in.AdditionalUserData
+	in.AdditionalUserData.DeepCopyInto(&out.AdditionalUserData)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RKE2AgentConfig.
diff --git a/bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_rke2configs.yaml b/bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_rke2configs.yaml
index 3683673..8524aae 100644
--- a/bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_rke2configs.yaml
+++ b/bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_rke2configs.yaml
@@ -48,13 +48,24 @@ spec:
                           documented here: https://kinvolk.io/docs/flatcar-container-linux/latest/provisioning/cl-config/
                           NOTE: All fields of the UserData that are managed by the
                           RKE2Config controller will be ignored, this include "write_files",
-                          "runcmd", "ntp".'
+                          "runcmd", "ntp". Deprecated: Data is reserved for the arbitrary
+                          cloud-init data'
                         type: string
+                      data:
+                        additionalProperties:
+                          type: string
+                        description: Data allows to pass arbitrary set of key/value
+                          pairs consistent with https://cloudinit.readthedocs.io/en/latest/reference/modules.html
+                          to extend existing cloud-init configuration
+                        type: object
                       strict:
                         description: Strict controls if Config should be strictly
                           parsed. If so, warnings are treated as errors.
                         type: boolean
                     type: object
+                    x-kubernetes-validations:
+                    - message: Only config or data could be populated at once
+                      rule: '!has(self.data) || !has(self.config)'
                   airGapped:
                     description: AirGapped is a boolean value to define if the bootstrapping
                       should be air-gapped, basically supposing that online container
diff --git a/bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_rke2configtemplates.yaml b/bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_rke2configtemplates.yaml
index 5079b57..64bcf2a 100644
--- a/bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_rke2configtemplates.yaml
+++ b/bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_rke2configtemplates.yaml
@@ -61,13 +61,24 @@ spec:
                                   format is documented here: https://kinvolk.io/docs/flatcar-container-linux/latest/provisioning/cl-config/
                                   NOTE: All fields of the UserData that are managed
                                   by the RKE2Config controller will be ignored, this
-                                  include "write_files", "runcmd", "ntp".'
+                                  include "write_files", "runcmd", "ntp". Deprecated:
+                                  Data is reserved for the arbitrary cloud-init data'
                                 type: string
+                              data:
+                                additionalProperties:
+                                  type: string
+                                description: Data allows to pass arbitrary set of
+                                  key/value pairs consistent with https://cloudinit.readthedocs.io/en/latest/reference/modules.html
+                                  to extend existing cloud-init configuration
+                                type: object
                               strict:
                                 description: Strict controls if Config should be strictly
                                   parsed. If so, warnings are treated as errors.
                                 type: boolean
                             type: object
+                            x-kubernetes-validations:
+                            - message: Only config or data could be populated at once
+                              rule: '!has(self.data) || !has(self.config)'
                           airGapped:
                             description: AirGapped is a boolean value to define if
                               the bootstrapping should be air-gapped, basically supposing
diff --git a/bootstrap/internal/cloudinit/cloudinit.go b/bootstrap/internal/cloudinit/cloudinit.go
index 33eacdc..0cbdbb9 100644
--- a/bootstrap/internal/cloudinit/cloudinit.go
+++ b/bootstrap/internal/cloudinit/cloudinit.go
@@ -86,23 +86,29 @@ ntp:
     {{- end -}}	
 {{- end -}}
 {{- end -}}
+`
+	arbitraryTemplate = `{{- define "arbitrary" -}}{{- range $key, $value := . }}
+{{ $key -}}: {{ $value -}}
+{{- end -}}
+{{- end -}}
 `
 )
 
 // BaseUserData is shared across all the various types of files written to disk.
 type BaseUserData struct {
-	Header              string
-	PreRKE2Commands     []string
-	DeployRKE2Commands  []string
-	PostRKE2Commands    []string
-	WriteFiles          []bootstrapv1.File
-	ConfigFile          bootstrapv1.File
-	RKE2Version         string
-	SentinelFileCommand string
-	AirGapped           bool
-	NTPServers          []string
-	CISEnabled          bool
-	AdditionalCloudInit string
+	Header                  string
+	PreRKE2Commands         []string
+	DeployRKE2Commands      []string
+	PostRKE2Commands        []string
+	WriteFiles              []bootstrapv1.File
+	ConfigFile              bootstrapv1.File
+	RKE2Version             string
+	SentinelFileCommand     string
+	AirGapped               bool
+	NTPServers              []string
+	CISEnabled              bool
+	AdditionalCloudInit     string
+	AdditionalArbitraryData map[string]string
 }
 
 func generate(kind string, tpl string, data interface{}) ([]byte, error) {
@@ -119,6 +125,10 @@ func generate(kind string, tpl string, data interface{}) ([]byte, error) {
 		return nil, errors.Wrap(err, "failed to parse ntp template")
 	}
 
+	if _, err := tm.Parse(arbitraryTemplate); err != nil {
+		return nil, errors.Wrap(err, "failed to parse arbitrary template")
+	}
+
 	t, err := tm.Parse(tpl)
 	if err != nil {
 		return nil, errors.Wrapf(err, "failed to parse %s template", kind)
@@ -159,3 +169,41 @@ func cleanupAdditionalCloudInit(cloudInitData string) (string, error) {
 
 	return res, nil
 }
+
+func cleanupArbitraryData(arbitraryData map[string]string) error {
+	// Remove ignored fields from the map
+	for _, field := range ignoredCloudInitFields {
+		delete(arbitraryData, field)
+	}
+
+	if len(arbitraryData) == 0 {
+		return nil
+	}
+
+	m := make(map[string]interface{})
+
+	kind := "arbitrary_prepare"
+	tm := template.New(kind).Funcs(defaultTemplateFuncMap)
+
+	if _, err := tm.Parse(arbitraryTemplate); err != nil {
+		return errors.Wrap(err, "failed to parse arbitrary keys template")
+	}
+
+	t, err := tm.Parse(`{{template "arbitrary" .AdditionalArbitraryData}}`)
+	if err != nil {
+		return errors.Wrap(err, "failed to parse arbitrary template")
+	}
+
+	var out bytes.Buffer
+	if err := t.Execute(&out, BaseUserData{
+		AdditionalArbitraryData: arbitraryData,
+	}); err != nil {
+		return errors.Wrapf(err, "failed to generate %s template", kind)
+	}
+
+	if err := yaml.Unmarshal(out.Bytes(), m); err != nil {
+		return fmt.Errorf("failed to unmarshal arbitrary cloud-init data: %w, please check if you put valid yaml data: %s", err, out.String())
+	}
+
+	return nil
+}
diff --git a/bootstrap/internal/cloudinit/controlplane_init.go b/bootstrap/internal/cloudinit/controlplane_init.go
index 6fa086f..ce419d7 100644
--- a/bootstrap/internal/cloudinit/controlplane_init.go
+++ b/bootstrap/internal/cloudinit/controlplane_init.go
@@ -27,6 +27,7 @@ const (
 	controlPlaneCloudInit = `{{.Header}}
 {{template "files" .WriteFiles}}
 {{template "ntp" .NTPServers}}
+{{template "arbitrary" .AdditionalArbitraryData}}
 runcmd:
 {{- template "commands" .PreRKE2Commands }}
   - {{ if .AirGapped }}INSTALL_RKE2_ARTIFACT_PATH=/opt/rke2-artifacts sh /opt/install.sh{{ else }}'curl -sfL https://get.rke2.io | INSTALL_RKE2_VERSION=%[1]s sh -s - server'{{ end }} 
@@ -63,6 +64,10 @@ func NewInitControlPlane(input *ControlPlaneInput) ([]byte, error) {
 		return nil, err
 	}
 
+	if err := cleanupArbitraryData(input.AdditionalArbitraryData); err != nil {
+		return nil, err
+	}
+
 	controlPlaneCloudJoinWithVersion := fmt.Sprintf(controlPlaneCloudInit, input.RKE2Version)
 	userData, err := generate("InitControlplane", controlPlaneCloudJoinWithVersion, input)
 
diff --git a/bootstrap/internal/cloudinit/controlplane_join.go b/bootstrap/internal/cloudinit/controlplane_join.go
index c7c2c15..10b190b 100644
--- a/bootstrap/internal/cloudinit/controlplane_join.go
+++ b/bootstrap/internal/cloudinit/controlplane_join.go
@@ -35,6 +35,10 @@ func NewJoinControlPlane(input *ControlPlaneInput) ([]byte, error) {
 		return nil, err
 	}
 
+	if err := cleanupArbitraryData(input.AdditionalArbitraryData); err != nil {
+		return nil, err
+	}
+
 	controlPlaneCloudJoinWithVersion := fmt.Sprintf(controlPlaneCloudInit, input.RKE2Version)
 	userData, err := generate("JoinControlplane", controlPlaneCloudJoinWithVersion, input)
 
diff --git a/bootstrap/internal/cloudinit/worker_join.go b/bootstrap/internal/cloudinit/worker_join.go
index 98ea370..080e2b2 100644
--- a/bootstrap/internal/cloudinit/worker_join.go
+++ b/bootstrap/internal/cloudinit/worker_join.go
@@ -25,6 +25,7 @@ const (
 	workerCloudInit = `{{.Header}}
 {{template "files" .WriteFiles}}
 {{template "ntp" .NTPServers}}
+{{template "arbitrary" .AdditionalArbitraryData}}
 runcmd:
 {{- template "commands" .PreRKE2Commands }}
   - '{{ if .AirGapped }}INSTALL_RKE2_ARTIFACT_PATH=/opt/rke2-artifacts INSTALL_RKE2_TYPE="agent" sh /opt/install.sh{{ else }}curl -sfL https://get.rke2.io | INSTALL_RKE2_VERSION=%[1]s INSTALL_RKE2_TYPE="agent" sh -s -{{end}}'
@@ -54,6 +55,10 @@ func NewJoinWorker(input *BaseUserData) ([]byte, error) {
 		return nil, err
 	}
 
+	if err := cleanupArbitraryData(input.AdditionalArbitraryData); err != nil {
+		return nil, err
+	}
+
 	workerCloudJoinWithVersion := fmt.Sprintf(workerCloudInit, input.RKE2Version)
 	userData, err := generate("JoinWorker", workerCloudJoinWithVersion, input)
 
diff --git a/bootstrap/internal/controllers/rke2config_controller.go b/bootstrap/internal/controllers/rke2config_controller.go
index 097b1af..5fc82e6 100644
--- a/bootstrap/internal/controllers/rke2config_controller.go
+++ b/bootstrap/internal/controllers/rke2config_controller.go
@@ -402,15 +402,16 @@ func (r *RKE2ConfigReconciler) handleClusterNotInitialized(ctx context.Context,
 
 	cpinput := &cloudinit.ControlPlaneInput{
 		BaseUserData: cloudinit.BaseUserData{
-			AirGapped:           scope.Config.Spec.AgentConfig.AirGapped,
-			CISEnabled:          scope.Config.Spec.AgentConfig.CISProfile != "",
-			PreRKE2Commands:     scope.Config.Spec.PreRKE2Commands,
-			PostRKE2Commands:    scope.Config.Spec.PostRKE2Commands,
-			ConfigFile:          initConfigFile,
-			RKE2Version:         scope.Config.Spec.AgentConfig.Version,
-			WriteFiles:          files,
-			NTPServers:          ntpServers,
-			AdditionalCloudInit: scope.Config.Spec.AgentConfig.AdditionalUserData.Config,
+			AirGapped:               scope.Config.Spec.AgentConfig.AirGapped,
+			CISEnabled:              scope.Config.Spec.AgentConfig.CISProfile != "",
+			PreRKE2Commands:         scope.Config.Spec.PreRKE2Commands,
+			PostRKE2Commands:        scope.Config.Spec.PostRKE2Commands,
+			ConfigFile:              initConfigFile,
+			RKE2Version:             scope.Config.Spec.AgentConfig.Version,
+			WriteFiles:              files,
+			NTPServers:              ntpServers,
+			AdditionalCloudInit:     scope.Config.Spec.AgentConfig.AdditionalUserData.Config,
+			AdditionalArbitraryData: scope.Config.Spec.AgentConfig.AdditionalUserData.Data,
 		},
 		Certificates: certificates,
 	}
@@ -694,15 +695,16 @@ func (r *RKE2ConfigReconciler) joinWorker(ctx context.Context, scope *Scope) (re
 	}
 
 	wkInput := &cloudinit.BaseUserData{
-		PreRKE2Commands:     scope.Config.Spec.PreRKE2Commands,
-		AirGapped:           scope.Config.Spec.AgentConfig.AirGapped,
-		CISEnabled:          scope.Config.Spec.AgentConfig.CISProfile != "",
-		PostRKE2Commands:    scope.Config.Spec.PostRKE2Commands,
-		ConfigFile:          wkJoinConfigFile,
-		RKE2Version:         scope.Config.Spec.AgentConfig.Version,
-		WriteFiles:          files,
-		NTPServers:          ntpServers,
-		AdditionalCloudInit: scope.Config.Spec.AgentConfig.AdditionalUserData.Config,
+		PreRKE2Commands:         scope.Config.Spec.PreRKE2Commands,
+		AirGapped:               scope.Config.Spec.AgentConfig.AirGapped,
+		CISEnabled:              scope.Config.Spec.AgentConfig.CISProfile != "",
+		PostRKE2Commands:        scope.Config.Spec.PostRKE2Commands,
+		ConfigFile:              wkJoinConfigFile,
+		RKE2Version:             scope.Config.Spec.AgentConfig.Version,
+		WriteFiles:              files,
+		NTPServers:              ntpServers,
+		AdditionalCloudInit:     scope.Config.Spec.AgentConfig.AdditionalUserData.Config,
+		AdditionalArbitraryData: scope.Config.Spec.AgentConfig.AdditionalUserData.Data,
 	}
 
 	var userData []byte
diff --git a/bootstrap/internal/ignition/butane/butane.go b/bootstrap/internal/ignition/butane/butane.go
index 376323c..ec59fda 100644
--- a/bootstrap/internal/ignition/butane/butane.go
+++ b/bootstrap/internal/ignition/butane/butane.go
@@ -54,7 +54,6 @@ systemd:
         Description=rke2-install
         Wants=network-online.target
         After=network-online.target network.target
-        ConditionPathExists=!/run/cluster-api/bootstrap-success.complete
         [Service]
         User=root
         # To not restart the unit when it exits, as it is expected.
diff --git a/controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanes.yaml b/controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanes.yaml
index cf5945c..926d721 100644
--- a/controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanes.yaml
+++ b/controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanes.yaml
@@ -48,13 +48,24 @@ spec:
                           documented here: https://kinvolk.io/docs/flatcar-container-linux/latest/provisioning/cl-config/
                           NOTE: All fields of the UserData that are managed by the
                           RKE2Config controller will be ignored, this include "write_files",
-                          "runcmd", "ntp".'
+                          "runcmd", "ntp". Deprecated: Data is reserved for the arbitrary
+                          cloud-init data'
                         type: string
+                      data:
+                        additionalProperties:
+                          type: string
+                        description: Data allows to pass arbitrary set of key/value
+                          pairs consistent with https://cloudinit.readthedocs.io/en/latest/reference/modules.html
+                          to extend existing cloud-init configuration
+                        type: object
                       strict:
                         description: Strict controls if Config should be strictly
                           parsed. If so, warnings are treated as errors.
                         type: boolean
                     type: object
+                    x-kubernetes-validations:
+                    - message: Only config or data could be populated at once
+                      rule: '!has(self.data) || !has(self.config)'
                   airGapped:
                     description: AirGapped is a boolean value to define if the bootstrapping
                       should be air-gapped, basically supposing that online container
diff --git a/go.mod b/go.mod
index 40e748e..9a9130f 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/coreos/ignition/v2 v2.16.2
 	github.com/go-logr/logr v1.3.0
 	github.com/onsi/ginkgo/v2 v2.13.0
-	github.com/onsi/gomega v1.30.0
+	github.com/onsi/gomega v1.29.0
 	github.com/pkg/errors v0.9.1
 	github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace
 	gopkg.in/yaml.v3 v3.0.1
diff --git a/go.sum b/go.sum
index bac023a..d731822 100644
--- a/go.sum
+++ b/go.sum
@@ -376,8 +376,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
 github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
-github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
-github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
+github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
+github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
diff --git a/pkg/rke2/workload_cluster.go b/pkg/rke2/workload_cluster.go
index cb5aecc..5e93977 100644
--- a/pkg/rke2/workload_cluster.go
+++ b/pkg/rke2/workload_cluster.go
@@ -142,10 +142,6 @@ func (w *Workload) PatchNodes(ctx context.Context, cp *ControlPlane) error {
 
 	for i := range w.Nodes {
 		node := w.Nodes[i]
-		if _, found := cp.Machines[node.Name]; !found {
-			continue
-		}
-
 		if helper, ok := w.nodePatchHelpers[node.Name]; ok {
 			if err := helper.Patch(ctx, node); err != nil {
 				conditions.MarkUnknown(


Test patch:
diff --git a/bootstrap/internal/cloudinit/cloudinit_test.go b/bootstrap/internal/cloudinit/cloudinit_test.go
index 1c865af..74b1223 100644
--- a/bootstrap/internal/cloudinit/cloudinit_test.go
+++ b/bootstrap/internal/cloudinit/cloudinit_test.go
@@ -43,6 +43,7 @@ write_files:
     content: |
       
 
+
 runcmd:
   - 'INSTALL_RKE2_ARTIFACT_PATH=/opt/rke2-artifacts INSTALL_RKE2_TYPE="agent" sh /opt/install.sh'
   - 'systemctl enable rke2-agent.service'
@@ -76,6 +77,7 @@ write_files:
     content: |
       
 
+
 runcmd:
   - 'curl -sfL https://get.rke2.io | INSTALL_RKE2_VERSION=v1.25.6+rke2r1 INSTALL_RKE2_TYPE="agent" sh -s -'
   - 'systemctl enable rke2-agent.service'
@@ -111,6 +113,7 @@ ntp:
   enabled: true
   servers:
   - "test.ntp.org"
+
 runcmd:
   - 'curl -sfL https://get.rke2.io | INSTALL_RKE2_VERSION= INSTALL_RKE2_TYPE="agent" sh -s -'
   - 'systemctl enable rke2-agent.service'
@@ -145,6 +148,7 @@ write_files:
     content: |
       
 
+
 runcmd:
   - 'curl -sfL https://get.rke2.io | INSTALL_RKE2_VERSION=v1.25.6+rke2r1 INSTALL_RKE2_TYPE="agent" sh -s -'
   - '/opt/rke2-cis-script.sh'
@@ -166,6 +170,7 @@ write_files:
 -   path: 
     content: |
 
+
 runcmd:
   - 'curl -sfL https://get.rke2.io | INSTALL_RKE2_VERSION= INSTALL_RKE2_TYPE=\"agent\" sh -s -'
   - 'systemctl enable rke2-agent.service'
@@ -186,8 +191,11 @@ users:
 
 var _ = Describe("CloudInit with custom entries", func() {
 	var input *BaseUserData
+	var cloudInitData string
+	var arbitraryData map[string]string
+
 	BeforeEach(func() {
-		cloudInitData := `## template: jinja
+		cloudInitData = `## template: jinja
 #cloud-config
 device_aliases: {'ephemeral0': '/dev/vdb'}
 disk_setup:
@@ -206,18 +214,67 @@ write_files:
     content: |
       192.168.0.1 test
 
+
 runcmd:
   - 'print hello world' 
 `
+		arbitraryData = map[string]string{
+			"disk_setup": `
+  ephemeral0:
+    table_type: mbr
+    layout: False
+    overwrite: False`,
+			"device_aliases": "{'ephemeral0': '/dev/vdb'}",
+			"runcmd": `
+  - 'print hello world'`,
+		}
+	})
+
+	It("Should apply the arbitrary data and cleanup the input values", func() {
+		input = &BaseUserData{
+			AirGapped:               false,
+			CISEnabled:              true,
+			RKE2Version:             "v1.25.6+rke2r1",
+			AdditionalArbitraryData: arbitraryData,
+		}
+		workerCloudInitData, err := NewJoinWorker(input)
+		Expect(err).ToNot(HaveOccurred())
+		workerCloudInitString := string(workerCloudInitData)
+		_, err = GinkgoWriter.Write(workerCloudInitData)
+		Expect(err).NotTo(HaveOccurred())
+
+		Expect(workerCloudInitString).To(Equal(`## template: jinja
+#cloud-config
+
+write_files:
+-   path: 
+    content: |
+      
+
+
+device_aliases: {'ephemeral0': '/dev/vdb'}
+disk_setup: 
+  ephemeral0:
+    table_type: mbr
+    layout: False
+    overwrite: False
+runcmd:
+  - 'curl -sfL https://get.rke2.io | INSTALL_RKE2_VERSION=v1.25.6+rke2r1 INSTALL_RKE2_TYPE="agent" sh -s -'
+  - '/opt/rke2-cis-script.sh'
+  - 'systemctl enable rke2-agent.service'
+  - 'systemctl start rke2-agent.service'
+  - 'mkdir /run/cluster-api' 
+  - 'echo success > /run/cluster-api/bootstrap-success.complete'
+`))
+	})
+
+	It("Should remove the runcmd, write_files and ntp lines", func() {
 		input = &BaseUserData{
 			AirGapped:           false,
 			CISEnabled:          true,
 			RKE2Version:         "v1.25.6+rke2r1",
 			AdditionalCloudInit: cloudInitData,
 		}
-	})
-
-	It("Should remove the runcmd, write_files and ntp lines", func() {
 		workerCloudInitData, err := NewJoinWorker(input)
 		Expect(err).ToNot(HaveOccurred())
 		workerCloudInitString := string(workerCloudInitData)
@@ -232,6 +289,7 @@ write_files:
     content: |
       
 
+
 runcmd:
   - 'curl -sfL https://get.rke2.io | INSTALL_RKE2_VERSION=v1.25.6+rke2r1 INSTALL_RKE2_TYPE="agent" sh -s -'
   - '/opt/rke2-cis-script.sh'
diff --git a/bootstrap/internal/ignition/butane/butane_test.go b/bootstrap/internal/ignition/butane/butane_test.go
index 8935205..e39cab2 100644
--- a/bootstrap/internal/ignition/butane/butane_test.go
+++ b/bootstrap/internal/ignition/butane/butane_test.go
@@ -119,7 +119,7 @@ var _ = Describe("Render", func() {
 
 		Expect(ign.Systemd.Units).To(HaveLen(3))
 		Expect(ign.Systemd.Units[0].Name).To(Equal("rke2-install.service"))
-		Expect(ign.Systemd.Units[0].Contents).To(Equal(pointer.String("[Unit]\nDescription=rke2-install\nWants=network-online.target\nAfter=network-online.target network.target\nConditionPathExists=!/run/cluster-api/bootstrap-success.complete\n[Service]\nUser=root\n# To not restart the unit when it exits, as it is expected.\nType=oneshot\nExecStart=/etc/rke2-install.sh\n[Install]\nWantedBy=multi-user.target\n")))
+		Expect(ign.Systemd.Units[0].Contents).To(Equal(pointer.String("[Unit]\nDescription=rke2-install\nWants=network-online.target\nAfter=network-online.target network.target\n[Service]\nUser=root\n# To not restart the unit when it exits, as it is expected.\nType=oneshot\nExecStart=/etc/rke2-install.sh\n[Install]\nWantedBy=multi-user.target\n")))
 		Expect(ign.Systemd.Units[0].Enabled).To(Equal(pointer.Bool(true)))
 
 		Expect(ign.Systemd.Units[1].Name).To(Equal("ntpd.service"))
diff --git a/controlplane/internal/controllers/rke2controlplane_controller_test.go b/controlplane/internal/controllers/rke2controlplane_controller_test.go
deleted file mode 100644
index 6094409..0000000
--- a/controlplane/internal/controllers/rke2controlplane_controller_test.go
+++ /dev/null
@@ -1,170 +0,0 @@
-package controllers
-
-import (
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
-	bootstrapv1 "github.com/rancher-sandbox/cluster-api-provider-rke2/bootstrap/api/v1alpha1"
-	controlplanev1 "github.com/rancher-sandbox/cluster-api-provider-rke2/controlplane/api/v1alpha1"
-
-	// "github.com/rancher-sandbox/cluster-api-provider-rke2/pkg/kubeconfig"
-	"github.com/rancher-sandbox/cluster-api-provider-rke2/pkg/rke2"
-	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
-	"sigs.k8s.io/cluster-api/util/collections"
-	"sigs.k8s.io/cluster-api/util/conditions"
-	"sigs.k8s.io/cluster-api/util/kubeconfig"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-)
-
-var _ = Describe("Reconclie control plane conditions", func() {
-	var (
-		err          error
-		cp           *rke2.ControlPlane
-		rcp          *controlplanev1.RKE2ControlPlane
-		ns           *corev1.Namespace
-		nodeName     = "node1"
-		node         *corev1.Node
-		orphanedNode *corev1.Node
-		machine      *clusterv1.Machine
-		config       *bootstrapv1.RKE2Config
-	)
-
-	BeforeEach(func() {
-		ns, err = testEnv.CreateNamespace(ctx, "ns")
-		Expect(err).ToNot(HaveOccurred())
-
-		annotations := map[string]string{
-			"test": "true",
-		}
-		node = &corev1.Node{
-			ObjectMeta: metav1.ObjectMeta{
-				Name: nodeName,
-				Labels: map[string]string{
-					"node-role.kubernetes.io/master": "true",
-				},
-				Annotations: map[string]string{
-					clusterv1.MachineAnnotation: nodeName,
-				},
-			},
-			Status: corev1.NodeStatus{
-				Conditions: []corev1.NodeCondition{{
-					Type:   corev1.NodeReady,
-					Status: corev1.ConditionTrue,
-				}},
-			},
-		}
-		Expect(testEnv.Create(ctx, node.DeepCopy())).To(Succeed())
-		Expect(testEnv.Status().Update(ctx, node.DeepCopy())).To(Succeed())
-		Expect(testEnv.Get(ctx, client.ObjectKeyFromObject(node), node)).To(Succeed())
-
-		orphanedNode = &corev1.Node{ObjectMeta: metav1.ObjectMeta{
-			Name: "missing-machine",
-			Labels: map[string]string{
-				"node-role.kubernetes.io/master": "true",
-			},
-		}}
-		Expect(testEnv.Create(ctx, orphanedNode)).To(Succeed())
-
-		config = &bootstrapv1.RKE2Config{ObjectMeta: metav1.ObjectMeta{
-			Name:      "config",
-			Namespace: ns.Name,
-		}, Spec: bootstrapv1.RKE2ConfigSpec{
-			AgentConfig: bootstrapv1.RKE2AgentConfig{
-				NodeAnnotations: annotations,
-			},
-		}}
-		Expect(testEnv.Create(ctx, config)).To(Succeed())
-
-		machine = &clusterv1.Machine{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      nodeName,
-				Namespace: ns.Name,
-			},
-			Spec: clusterv1.MachineSpec{
-				ClusterName: "cluster",
-				Bootstrap: clusterv1.Bootstrap{
-					ConfigRef: &corev1.ObjectReference{
-						Kind:       "RKE2Config",
-						APIVersion: bootstrapv1.GroupVersion.String(),
-						Name:       config.Name,
-						Namespace:  config.Namespace,
-					},
-				},
-				InfrastructureRef: corev1.ObjectReference{
-					Kind:       "Pod",
-					APIVersion: "v1",
-					Name:       "stub",
-					Namespace:  ns.Name,
-				},
-			},
-			Status: clusterv1.MachineStatus{
-				NodeRef: &corev1.ObjectReference{
-					Kind:      "Node",
-					Name:      nodeName,
-					UID:       node.GetUID(),
-					Namespace: "",
-				},
-				Conditions: clusterv1.Conditions{
-					clusterv1.Condition{
-						Type:   clusterv1.ReadyCondition,
-						Status: corev1.ConditionTrue,
-					},
-				},
-			},
-		}
-		ml := clusterv1.MachineList{Items: []clusterv1.Machine{*machine.DeepCopy()}}
-		Expect(testEnv.Create(ctx, machine.DeepCopy())).To(Succeed())
-
-		cluster := &clusterv1.Cluster{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      "test",
-				Namespace: ns.Name,
-			},
-		}
-		Expect(testEnv.Client.Create(ctx, cluster)).To(Succeed())
-
-		rcp = &controlplanev1.RKE2ControlPlane{
-			Status: controlplanev1.RKE2ControlPlaneStatus{
-				Initialized: true,
-			},
-		}
-
-		cp, err = rke2.NewControlPlane(ctx, testEnv.GetClient(), cluster, rcp, collections.FromMachineList(&ml))
-		Expect(err).ToNot(HaveOccurred())
-
-		ref := metav1.OwnerReference{
-			APIVersion: clusterv1.GroupVersion.String(),
-			Kind:       clusterv1.ClusterKind,
-			UID:        cp.Cluster.GetUID(),
-			Name:       cp.Cluster.GetName(),
-		}
-		Expect(testEnv.Client.Create(ctx, kubeconfig.GenerateSecretWithOwner(
-			client.ObjectKeyFromObject(cp.Cluster),
-			kubeconfig.FromEnvTestConfig(testEnv.Config, cp.Cluster),
-			ref))).To(Succeed())
-	})
-
-	AfterEach(func() {
-		Expect(testEnv.DeleteAllOf(ctx, node)).To(Succeed())
-		testEnv.Cleanup(ctx, node, ns)
-	})
-
-	It("should reconcile cp and machine conditions successfully", func() {
-		r := &RKE2ControlPlaneReconciler{
-			Client:                    testEnv.GetClient(),
-			Scheme:                    testEnv.GetScheme(),
-			managementCluster:         &rke2.Management{Client: testEnv.GetClient()},
-			managementClusterUncached: &rke2.Management{Client: testEnv.GetClient()},
-		}
-		_, err := r.reconcileControlPlaneConditions(ctx, cp)
-		Expect(err).ToNot(HaveOccurred())
-		Expect(testEnv.Get(ctx, client.ObjectKeyFromObject(machine), machine)).To(Succeed())
-		Expect(conditions.IsTrue(machine, controlplanev1.NodeMetadataUpToDate)).To(BeTrue())
-		Expect(testEnv.Get(ctx, client.ObjectKeyFromObject(node), node)).To(Succeed())
-		Expect(node.GetAnnotations()).To(HaveKeyWithValue("test", "true"))
-		Expect(conditions.IsFalse(rcp, controlplanev1.ControlPlaneComponentsHealthyCondition)).To(BeTrue())
-		Expect(conditions.GetMessage(rcp, controlplanev1.ControlPlaneComponentsHealthyCondition)).To(Equal(
-			"Control plane node missing-machine does not have a corresponding machine"))
-	})
-})
diff --git a/controlplane/internal/controllers/suite_test.go b/controlplane/internal/controllers/suite_test.go
index d2dcf3c..13aa0b1 100644
--- a/controlplane/internal/controllers/suite_test.go
+++ b/controlplane/internal/controllers/suite_test.go
@@ -17,61 +17,64 @@ limitations under the License.
 package controllers
 
 import (
-	"fmt"
-	"path"
+	"path/filepath"
 	"testing"
 
-	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
-	"k8s.io/client-go/kubernetes/scheme"
-	ctrl "sigs.k8s.io/controller-runtime"
-
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 
-	// +kubebuilder:scaffold:imports
-	bootstrapv1 "github.com/rancher-sandbox/cluster-api-provider-rke2/bootstrap/api/v1alpha1"
+	"k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/client-go/rest"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/envtest"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+
 	controlplanev1 "github.com/rancher-sandbox/cluster-api-provider-rke2/controlplane/api/v1alpha1"
-	"github.com/rancher-sandbox/cluster-api-provider-rke2/test/helpers"
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 )
 
+// These tests use Ginkgo (BDD-style Go testing framework). Refer to
+// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
+
 var (
-	testEnv *helpers.TestEnvironment
-	ctx     = ctrl.SetupSignalHandler()
+	cfg       *rest.Config
+	k8sClient client.Client
+	testEnv   *envtest.Environment
 )
 
 func TestAPIs(t *testing.T) {
 	RegisterFailHandler(Fail)
-	setup()
-	defer teardown()
+
 	RunSpecs(t, "Controller Suite")
 }
 
-func setup() {
-	utilruntime.Must(clusterv1.AddToScheme(scheme.Scheme))
-	utilruntime.Must(bootstrapv1.AddToScheme(scheme.Scheme))
-	utilruntime.Must(controlplanev1.AddToScheme(scheme.Scheme))
+var _ = BeforeSuite(func() {
+	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
 
-	testEnvConfig := helpers.NewTestEnvironmentConfiguration([]string{
-		path.Join("bootstrap", "config", "crd", "bases"),
-		path.Join("controlplane", "config", "crd", "bases"),
-	},
-	)
-	var err error
-	testEnv, err = testEnvConfig.Build()
-	if err != nil {
-		panic(err)
+	By("bootstrapping test environment")
+	testEnv = &envtest.Environment{
+		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "config", "crd", "bases")},
+		ErrorIfCRDPathMissing: true,
 	}
-	go func() {
-		fmt.Println("Starting the manager")
-		if err := testEnv.StartManager(ctx); err != nil {
-			panic(fmt.Sprintf("Failed to start the envtest manager: %v", err))
-		}
-	}()
-}
 
-func teardown() {
-	if err := testEnv.Stop(); err != nil {
-		panic(fmt.Sprintf("Failed to stop envtest: %v", err))
-	}
-}
+	var err error
+	// cfg is defined in this file globally.
+	cfg, err = testEnv.Start()
+	Expect(err).NotTo(HaveOccurred())
+	Expect(cfg).NotTo(BeNil())
+
+	err = controlplanev1.AddToScheme(scheme.Scheme)
+	Expect(err).NotTo(HaveOccurred())
+
+	//+kubebuilder:scaffold:scheme
+
+	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
+	Expect(err).NotTo(HaveOccurred())
+	Expect(k8sClient).NotTo(BeNil())
+})
+
+var _ = AfterSuite(func() {
+	By("tearing down the test environment")
+	err := testEnv.Stop()
+	Expect(err).NotTo(HaveOccurred())
+})
diff --git a/pkg/rke2/workload_cluster_test.go b/pkg/rke2/workload_cluster_test.go
index d0c19db..330d0f3 100644
--- a/pkg/rke2/workload_cluster_test.go
+++ b/pkg/rke2/workload_cluster_test.go
@@ -158,3 +158,35 @@ var _ = Describe("Node metadata propagation", func() {
 		}))
 	})
 })
+
+var _ = Describe("Cloud-init fields validation", func() {
+	var (
+		err error
+		ns  *corev1.Namespace
+	)
+
+	BeforeEach(func() {
+		ns, err = testEnv.CreateNamespace(ctx, "ns")
+		Expect(err).ToNot(HaveOccurred())
+	})
+
+	AfterEach(func() {
+		testEnv.Cleanup(ctx, ns)
+	})
+
+	It("should prevent populating config and data fields", func() {
+		Expect(testEnv.Create(ctx, &bootstrapv1.RKE2Config{ObjectMeta: metav1.ObjectMeta{
+			Name:      "config",
+			Namespace: ns.Name,
+		}, Spec: bootstrapv1.RKE2ConfigSpec{
+			AgentConfig: bootstrapv1.RKE2AgentConfig{
+				AdditionalUserData: bootstrapv1.AdditionalUserData{
+					Config: "some",
+					Data: map[string]string{
+						"no": "way",
+					},
+				},
+			},
+		}})).ToNot(Succeed())
+	})
+})