Repository: kubernetes/kops (go)

Issue:
--override syntax should support initializing pointers to empty structs
/kind feature

**1. Describe IN DETAIL the feature/behavior/change you would like to see.**

In the kOps API there are a few struct types without any fields:

* [ClassicNetworkingSpec](https://pkg.go.dev/k8s.io/kops/pkg/apis/kops?utm_source=godoc#ClassicNetworkingSpec)
* [DNSAccessSpec](https://pkg.go.dev/k8s.io/kops/pkg/apis/kops?utm_source=godoc#DNSAccessSpec)
* [ExternalNetworkingSpec](https://pkg.go.dev/k8s.io/kops/pkg/apis/kops?utm_source=godoc#ExternalNetworkingSpec)
* GCENetworkingSpec
* KopeioAuthenticationSpec
* KubenetNetworkingSpec
* KuberouterNetworkingSpec
* RBACAuthorizationSpec

All of these are referenced by pointer in other struct types. Much of the relevant kOps behavior checks for non-nil values of these pointers.

Currently the --override syntax does not support initializing these empty structs:

```diff
diff --git a/pkg/commands/set_cluster_test.go b/pkg/commands/set_cluster_test.go
index bd7dd861f5..e847cecbff 100644
--- a/pkg/commands/set_cluster_test.go
+++ b/pkg/commands/set_cluster_test.go
@@ -62,6 +62,21 @@ func TestSetClusterFields(t *testing.T) {
                                },
                        },
                },
+               {
+                       Fields: []string{
+                               "spec.api.dns=",
+                       },
+                       Input: kops.Cluster{
+                               Spec: kops.ClusterSpec{},
+                       },
+                       Output: kops.Cluster{
+                               Spec: kops.ClusterSpec{
+                                       API: &kops.AccessSpec{
+                                               DNS: &kops.DNSAccessSpec{},
+                                       },
+                               },
+                       },
+               },
                {
                        Fields: []string{"spec.kubelet.authorizationMode=Webhook"},
                        Output: kops.Cluster{
```

```
--- FAIL: TestSetClusterFields (0.00s)
    src/k8s.io/kops/pkg/commands/set_cluster_test.go:284: unexpected error from setClusterFields [spec.api.dns=]: cannot set field "spec.api.dns": unhandled type "kops.DNSAccessSpec"
```

**2. Feel free to provide a design supporting your feature request.**

It would be convenient if the syntax supported initializing these empty structs, perhaps when used with an empty value after the equals sign, as demonstrated in the test case above.

Pull request description:
Support setting empty maps and structs
/kind feature

Fixes #12691 

Patch:
diff --git a/upup/models/cloudup/resources/addons/networking.cilium.io/k8s-1.12-v1.8.yaml.template b/upup/models/cloudup/resources/addons/networking.cilium.io/k8s-1.12-v1.8.yaml.template
index 3ca43914b4..fd9a4e09a3 100644
--- a/upup/models/cloudup/resources/addons/networking.cilium.io/k8s-1.12-v1.8.yaml.template
+++ b/upup/models/cloudup/resources/addons/networking.cilium.io/k8s-1.12-v1.8.yaml.template
@@ -420,7 +420,7 @@ spec:
         # Annotation required for prometheus auto-discovery scraping
         # https://docs.cilium.io/en/v1.9/operations/metrics/#installation
         prometheus.io/scrape: "true"
-        prometheus.io/port: "{{ .Networking.Cilium.AgentPrometheusPort }}"
+        prometheus.io/port: {{ printf "%q" .Networking.Cilium.AgentPrometheusPort }}
         {{ end }}
       labels:
         k8s-app: cilium
diff --git a/upup/models/cloudup/resources/addons/networking.cilium.io/k8s-1.12-v1.9.yaml.template b/upup/models/cloudup/resources/addons/networking.cilium.io/k8s-1.12-v1.9.yaml.template
index 62e9ae433c..6898f272b8 100644
--- a/upup/models/cloudup/resources/addons/networking.cilium.io/k8s-1.12-v1.9.yaml.template
+++ b/upup/models/cloudup/resources/addons/networking.cilium.io/k8s-1.12-v1.9.yaml.template
@@ -552,7 +552,7 @@ spec:
         # Annotation required for prometheus auto-discovery scraping
         # https://docs.cilium.io/en/v1.9/operations/metrics/#installation
         prometheus.io/scrape: "true"
-        prometheus.io/port: "{{ .AgentPrometheusPort }}"
+        prometheus.io/port: {{ printf "%q" .AgentPrometheusPort }}
         {{ end }}
 {{- with .AgentPodAnnotations }}
         {{- . | nindent 8 }}
diff --git a/upup/models/cloudup/resources/addons/networking.cilium.io/k8s-1.16-v1.10.yaml.template b/upup/models/cloudup/resources/addons/networking.cilium.io/k8s-1.16-v1.10.yaml.template
index 171be9993a..91ef583eba 100644
--- a/upup/models/cloudup/resources/addons/networking.cilium.io/k8s-1.16-v1.10.yaml.template
+++ b/upup/models/cloudup/resources/addons/networking.cilium.io/k8s-1.16-v1.10.yaml.template
@@ -591,7 +591,7 @@ spec:
         # Annotation required for prometheus auto-discovery scraping
         # https://docs.cilium.io/en/v1.9/operations/metrics/#installation
         prometheus.io/scrape: "true"
-        prometheus.io/port: "{{ .AgentPrometheusPort }}"
+        prometheus.io/port: {{ printf "%q" .AgentPrometheusPort }}
         {{ end }}
 {{- with .AgentPodAnnotations }}
         {{- . | nindent 8 }}
diff --git a/util/pkg/reflectutils/access.go b/util/pkg/reflectutils/access.go
index 0e6f4f19a8..61a1a8d1dd 100644
--- a/util/pkg/reflectutils/access.go
+++ b/util/pkg/reflectutils/access.go
@@ -113,6 +113,10 @@ func setPrimitive(v reflect.Value, newValue string) error {
 		return nil
 	}
 
+	if newValue == "" && (v.Type().Kind() == reflect.Map || v.Type().Kind() == reflect.Struct) {
+		return nil
+	}
+
 	if v.Type().Kind() == reflect.Ptr {
 		val := reflect.New(v.Type().Elem())
 		if err := setPrimitive(val.Elem(), newValue); err != nil {


Test patch:
diff --git a/pkg/commands/set_cluster_test.go b/pkg/commands/set_cluster_test.go
index bd7dd861f5..e847cecbff 100644
--- a/pkg/commands/set_cluster_test.go
+++ b/pkg/commands/set_cluster_test.go
@@ -62,6 +62,21 @@ func TestSetClusterFields(t *testing.T) {
 				},
 			},
 		},
+		{
+			Fields: []string{
+				"spec.api.dns=",
+			},
+			Input: kops.Cluster{
+				Spec: kops.ClusterSpec{},
+			},
+			Output: kops.Cluster{
+				Spec: kops.ClusterSpec{
+					API: &kops.AccessSpec{
+						DNS: &kops.DNSAccessSpec{},
+					},
+				},
+			},
+		},
 		{
 			Fields: []string{"spec.kubelet.authorizationMode=Webhook"},
 			Output: kops.Cluster{
diff --git a/pkg/commands/unset_cluster_test.go b/pkg/commands/unset_cluster_test.go
index 9bb78c19f7..5f64acd0c3 100644
--- a/pkg/commands/unset_cluster_test.go
+++ b/pkg/commands/unset_cluster_test.go
@@ -62,6 +62,23 @@ func TestUnsetClusterFields(t *testing.T) {
 				},
 			},
 		},
+		{
+			Fields: []string{
+				"spec.api.dns",
+			},
+			Input: kops.Cluster{
+				Spec: kops.ClusterSpec{
+					API: &kops.AccessSpec{
+						DNS: &kops.DNSAccessSpec{},
+					},
+				},
+			},
+			Output: kops.Cluster{
+				Spec: kops.ClusterSpec{
+					API: &kops.AccessSpec{},
+				},
+			},
+		},
 		{
 			Fields: []string{"spec.kubelet.authorizationMode"},
 			Input: kops.Cluster{