Repository: kubernetes-sigs/cluster-api-bootstrap-provider-kubeadm (go)

Issue:
Pre/PostKubeadmCommands are not properly escaped
/kind bug

**What steps did you take and what happened:**

I applied the following `KubeadmConfig` configuration:
```yaml
---
apiVersion: bootstrap.cluster.x-k8s.io/v1alpha2
kind: KubeadmConfig
metadata:
  name: controlplane-0
spec:
  joinConfiguration:
    controlPlane: {}
    nodeRegistration:
      name: 'test1-controlplane-0'
  postKubeadmCommands:
    - "sed 's/{{ .PodSubnet }}/192.168.0.0\\/16/' /kind/manifests/default-cni.yaml > /tmp/default-cni.yaml"
    - "kubectl --kubeconfig /etc/kubernetes/admin.conf apply -f /tmp/default-cni.yaml"
```

Which resulted in the following `runcmd` being rendered and stored in the `.Status.BootstrapData`:

```yaml
runcmd:
  - 'kubeadm join --config /tmp/kubeadm-controlplane-join-config.yaml'
  - 'sed 's/{{ .PodSubnet }}/192.168.0.0\/16/' /kind/manifests/default-cni.yaml > /tmp/default-cni.yaml'
  - 'kubectl --kubeconfig /etc/kubernetes/admin.conf apply -f /tmp/default-cni.yaml'
```

When this BootstrapData was executed by CAPD (that's the only provider I've tried with this so far), it resulted in the following error:

```
I0830 03:31:04.600521       1 machine.go:142] DockerMachine-controller "level"=0 "msg"="Running machine bootstrap scripts" "cluster"="test1" "docker-cluster"="test1" "docker-machine"={"Namespace":"test1","Name":"controlplane-0"} "machine"="controlplane-0"
E0830 03:31:04.605028       1 machine.go:145] DockerMachine-controller "msg"="" "error"="cloud-config is not valid yaml: error converting YAML to JSON: yaml: line 74: did not find expected '-' indicator" "cluster"="test1" "docker-cluster"="test1" "docker-machine"={"Namespace":"test1","Name":"controlplane-0"} "machine"="controlplane-0"
E0830 03:31:04.605541       1 controller.go:218] controller-runtime/controller "msg"="Reconciler error" "error"="failed to exec DockerMachine bootstrap: failed to join a control plane node with kubeadm: cloud-config is not valid yaml: error converting YAML to JSON: yaml: line 74: did not find expected '-' indicator"  "controller"="dockermachine" "request"={"Namespace":"test1","Name":"controlplane-0"}
```

This in turn made the CAPD unable to provision the node.

**What did you expect to happen:**

I expected the `runcmd:` section to be appropriately escaped, so that the quotes used in the `KubeadmConfig` spec didn't matter.

**Anything else you would like to add:**

Switching from:
```yaml
...
  postKubeadmCommands:
    - "sed 's/{{ .PodSubnet }}/192.168.0.0\\/16/' /kind/manifests/default-cni.yaml > /tmp/default-cni.yaml"
    - "kubectl --kubeconfig /etc/kubernetes/admin.conf apply -f /tmp/default-cni.yaml"
```
to:
```yaml
  postKubeadmCommands:
    - 'sed "s/{{ .PodSubnet }}/192.168.0.0\\/16/" /kind/manifests/default-cni.yaml > /tmp/default-cni.yaml'
    - 'kubectl --kubeconfig /etc/kubernetes/admin.conf apply -f /tmp/default-cni.yaml'
```

Worked just fine though.

**Environment:**

- Cluster API version:
```
gcr.io/k8s-staging-cluster-api/cluster-api-controller    latest    f387a1543134    6 hours ago  
  42.4MB
```
- Cluster API Bootstrap Provider Kubeadm version:
```
gcr.io/k8s-staging-capi-kubeadm/cluster-api-kubeadm-controller    latest    001af5a9b57b    6 hours ago    43.3MB
```
- Cluster API Infrastructure Provider version:
```
jbergius/capd-manager    2f4a918    da5158aae882    23 hours ago    157MB
```
- Kubernetes version: (use `kubectl version`): `1.15.3`
- OS (e.g. from `/etc/os-release`): `Ubuntu 19.04`


Pull request description:
 🐛 Fixes an issue where KubeadmConfig Pre/PostKubeadmCommands were not being properly escaped 
<!-- please add a icon to the title of this PR (see https://sigs.k8s.io/cluster-api/VERSIONING.md), and delete this line and similar ones -->
<!-- the icon will be either ⚠️ (:warning:, major or breaking changes), ✨ (:sparkles:, minor or feature additions), 🐛 (:bug:, patch and bugfixes), 📖 (:book:, documentation or proposals), or 🏃 (:running:, other) -->

**What this PR does / why we need it**:
Removes the commands single quote and formats it with `%q`

**Which issue(s) this PR fixes** :
Fixes #192 


Patch:
diff --git a/cloudinit/commands.go b/cloudinit/commands.go
index 1481cae..61a79fd 100644
--- a/cloudinit/commands.go
+++ b/cloudinit/commands.go
@@ -19,7 +19,7 @@ package cloudinit
 const (
 	commandsTemplate = `{{- define "commands" -}}
 {{ range . }}
-  - '{{.}}'
+  - {{printf "%q" .}}
 {{- end -}}
 {{- end -}}
 `


Test patch:
diff --git a/cloudinit/cloudinit_test.go b/cloudinit/cloudinit_test.go
index 7f85007..895c5cf 100644
--- a/cloudinit/cloudinit_test.go
+++ b/cloudinit/cloudinit_test.go
@@ -77,3 +77,41 @@ func TestNewInitControlPlaneAdditionalFileEncodings(t *testing.T) {
 		}
 	}
 }
+
+func TestNewInitControlPlaneCommands(t *testing.T) {
+	cpinput := &ControlPlaneInput{
+		BaseUserData: BaseUserData{
+			Header:              "test",
+			PreKubeadmCommands:  []string{`"echo $(date) ': hello world!'"`},
+			PostKubeadmCommands: []string{"echo $(date) ': hello world!'"},
+			AdditionalFiles:     nil,
+			WriteFiles:          nil,
+			Users:               nil,
+			NTP:                 nil,
+		},
+		Certificates:         cluster.Certificates{},
+		ClusterConfiguration: "my-cluster-config",
+		InitConfiguration:    "my-init-config",
+	}
+
+	for _, certificate := range cpinput.Certificates {
+		certificate.KeyPair = &certs.KeyPair{
+			Cert: []byte("some certificate"),
+			Key:  []byte("some key"),
+		}
+	}
+
+	out, err := NewInitControlPlane(cpinput)
+	if err != nil {
+		t.Fatal(err)
+	}
+	expectedCommands := []string{
+		`"\"echo $(date) ': hello world!'\""`,
+		`"echo $(date) ': hello world!'"`,
+	}
+	for _, f := range expectedCommands {
+		if !bytes.Contains(out, []byte(f)) {
+			t.Errorf("%s\ndid not contain\n%s", out, f)
+		}
+	}
+}