From 4959d24b7ee903bd373a834ebca6603b721e1f7b Mon Sep 17 00:00:00 2001
From: Shashank Ram <21697719+shashankram@users.noreply.github.com>
Date: Wed, 18 Oct 2023 03:05:53 +0530
Subject: [PATCH] codegen: allow templated env vars (#510)

Enables setting environment variables on containers
that can leverage templated Helm values.

A new field is needed for this because the existing
model.Env exposes the env vars in values.yaml which
isn't suitable when the env vars must use a templated
value based on other Helm values.

Signed-off-by: Shashank Ram <shashank.ram@solo.io>
---
 changelog/v0.34.8/feature-crd.yaml            |   7 +
 codegen/cmd_test.go                           | 123 +++++++++++++++++
 codegen/model/chart.go                        |  15 ++
 .../chart/operator-deployment.yamltmpl        |   4 +
 codegen/test/chart-featureenv/Chart.yaml      |   8 ++
 .../chart-featureenv/templates/_helpers.tpl   |  54 ++++++++
 .../templates/deployment.yaml                 | 129 ++++++++++++++++++
 .../test/chart-featureenv/templates/rbac.yaml |   2 +
 codegen/test/chart-featureenv/values.yaml     |  21 +++
 9 files changed, 363 insertions(+)
 create mode 100644 changelog/v0.34.8/feature-crd.yaml
 create mode 100644 codegen/test/chart-featureenv/Chart.yaml
 create mode 100644 codegen/test/chart-featureenv/templates/_helpers.tpl
 create mode 100644 codegen/test/chart-featureenv/templates/deployment.yaml
 create mode 100644 codegen/test/chart-featureenv/templates/rbac.yaml
 create mode 100644 codegen/test/chart-featureenv/values.yaml

diff --git a/changelog/v0.34.8/feature-crd.yaml b/changelog/v0.34.8/feature-crd.yaml
new file mode 100644
index 000000000..e826115c1
--- /dev/null
+++ b/changelog/v0.34.8/feature-crd.yaml
@@ -0,0 +1,7 @@
+changelog:
+  - type: NEW_FEATURE
+    issueLink: https://github.com/solo-io/gloo-mesh-enterprise/issues/12370
+    resolvesIssue: false
+    description: |
+      Render feature flag environment variables for containers.
+    skipCI: false
diff --git a/codegen/cmd_test.go b/codegen/cmd_test.go
index 4e611bfcd..498befb6d 100644
--- a/codegen/cmd_test.go
+++ b/codegen/cmd_test.go
@@ -1835,6 +1835,129 @@ roleRef:
 			[]v1.EnvVar{{Name: "FOO", ValueFrom: &v1.EnvVarSource{SecretKeyRef: &v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: "bar"}, Key: "baz"}}}}),
 	)
 
+	DescribeTable("rendering feature gate env vars",
+		func(featureGatesVals map[string]string, envs []v1.EnvVar, featureEnvs []TemplateEnvVar, expectedEnvVars []v1.EnvVar) {
+			cmd := &Command{
+				Chart: &Chart{
+					Operators: []Operator{
+						{
+							Name: "painter",
+							Deployment: Deployment{
+								Container: Container{
+									Image: Image{
+										Tag:        "v0.0.0",
+										Repository: "painter",
+										Registry:   "quay.io/solo-io",
+										PullPolicy: "IfNotPresent",
+									},
+									Env:             envs,
+									TemplateEnvVars: featureEnvs,
+								},
+							},
+						},
+					},
+
+					Values: nil,
+					Data: Data{
+						ApiVersion:  "v1",
+						Description: "",
+						Name:        "Painting Operator",
+						Version:     "v0.0.1",
+						Home:        "https://docs.solo.io/skv2/latest",
+						Sources: []string{
+							"https://github.com/solo-io/skv2",
+						},
+					},
+				},
+
+				ManifestRoot: "codegen/test/chart-featureenv",
+			}
+
+			err := cmd.Execute()
+			Expect(err).NotTo(HaveOccurred())
+
+			painterValues := map[string]interface{}{}
+			// featureGates := map[string]interface{}{"Foo": true}
+			helmValues := map[string]interface{}{"painter": painterValues, "featureGates": featureGatesVals}
+
+			renderedManifests := helmTemplate("codegen/test/chart-featureenv", helmValues)
+
+			var renderedDeployment *appsv1.Deployment
+			decoder := kubeyaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(renderedManifests), 4096)
+			for {
+				obj := &unstructured.Unstructured{}
+				err := decoder.Decode(obj)
+				if err != nil {
+					break
+				}
+				if obj.GetName() != "painter" || obj.GetKind() != "Deployment" {
+					continue
+				}
+
+				bytes, err := obj.MarshalJSON()
+				Expect(err).NotTo(HaveOccurred())
+				renderedDeployment = &appsv1.Deployment{}
+				err = json.Unmarshal(bytes, renderedDeployment)
+				Expect(err).NotTo(HaveOccurred())
+			}
+			Expect(renderedDeployment).NotTo(BeNil())
+			renderedEnvVars := renderedDeployment.Spec.Template.Spec.Containers[0].Env
+			Expect(renderedEnvVars).To(ConsistOf(expectedEnvVars))
+		},
+
+		Entry("when neither Env nor TemplateEnvVar is specified",
+			map[string]string{"Foo": "true"},
+			nil,
+			nil,
+			nil),
+		Entry("when Env is not specified and TemplateEnvVar is specified",
+			map[string]string{"Foo": "true"},
+			nil,
+			[]TemplateEnvVar{
+				{
+					Name:  "FEATURE_ENABLE_FOO",
+					Value: "{{ $.Values.featureGates.Foo | quote }}",
+				},
+			},
+			nil),
+		Entry("when Env and TemplateEnvVar are specified, true value",
+			map[string]string{"Foo": "true"},
+			[]v1.EnvVar{
+				{
+					Name:  "FOO",
+					Value: "bar",
+				},
+			},
+			[]TemplateEnvVar{
+				{
+					Name:  "FEATURE_ENABLE_FOO",
+					Value: "{{ $.Values.featureGates.Foo | quote }}",
+				},
+			},
+			[]v1.EnvVar{
+				{Name: "FOO", Value: "bar"},
+				{Name: "FEATURE_ENABLE_FOO", Value: "true"},
+			}),
+		Entry("when Env and TemplateEnvVar are specified, false value",
+			map[string]string{"Foo": "false"},
+			[]v1.EnvVar{
+				{
+					Name:  "FOO",
+					Value: "bar",
+				},
+			},
+			[]TemplateEnvVar{
+				{
+					Name:  "FEATURE_ENABLE_FOO",
+					Value: "{{ $.Values.featureGates.Foo | quote }}",
+				},
+			},
+			[]v1.EnvVar{
+				{Name: "FOO", Value: "bar"},
+				{Name: "FEATURE_ENABLE_FOO", Value: "false"},
+			}),
+	)
+
 	DescribeTable("rendering service ports",
 		func(portName string) {
 			cmd := &Command{
diff --git a/codegen/model/chart.go b/codegen/model/chart.go
index 71ef4e76b..1d60721be 100644
--- a/codegen/model/chart.go
+++ b/codegen/model/chart.go
@@ -133,6 +133,21 @@ type Container struct {
 	Resources       *corev1.ResourceRequirements
 	SecurityContext *corev1.SecurityContext
 	ContainerPorts  []ContainerPort
+
+	// TemplateEnvVars renders environment variables that can use templated Helm values.
+	// At least 1 environment variable must be set via Env to use this.
+	TemplateEnvVars []TemplateEnvVar
+}
+
+// TemplateEnvVar corresponds to an environment variable that can use templated Helm values
+type TemplateEnvVar struct {
+	// Name of the environment variable
+	// E.g. FOO_BAR
+	Name string
+
+	// Helm value
+	// E.g. {{ .Values.foo.bar }}
+	Value string
 }
 
 type ContainerPort struct {
diff --git a/codegen/templates/chart/operator-deployment.yamltmpl b/codegen/templates/chart/operator-deployment.yamltmpl
index f7239de19..d0337db5a 100644
--- a/codegen/templates/chart/operator-deployment.yamltmpl
+++ b/codegen/templates/chart/operator-deployment.yamltmpl
@@ -111,6 +111,10 @@ spec:
 {{- if [[ $containerVar ]].env }}
         env:
 {{ toYaml [[ $containerVar ]].env | indent 10 }}
+[[- range $f := $container.TemplateEnvVars ]]
+          - name: [[ $f.Name ]]
+            value: [[ $f.Value ]]
+[[- end ]]
 {{- else if [[ $containerVar ]].extraEnvs }}
         env:
 {{- end }}
diff --git a/codegen/test/chart-featureenv/Chart.yaml b/codegen/test/chart-featureenv/Chart.yaml
new file mode 100644
index 000000000..01037b07a
--- /dev/null
+++ b/codegen/test/chart-featureenv/Chart.yaml
@@ -0,0 +1,8 @@
+# Code generated by skv2. DO NOT EDIT.
+
+apiVersion: v1
+home: https://docs.solo.io/skv2/latest
+name: Painting Operator
+sources:
+- https://github.com/solo-io/skv2
+version: v0.0.1
diff --git a/codegen/test/chart-featureenv/templates/_helpers.tpl b/codegen/test/chart-featureenv/templates/_helpers.tpl
new file mode 100644
index 000000000..0c155a127
--- /dev/null
+++ b/codegen/test/chart-featureenv/templates/_helpers.tpl
@@ -0,0 +1,54 @@
+# Code generated by skv2. DO NOT EDIT.
+
+
+
+{{/* Below are library functions provided by skv2 */}}
+
+{{- /*
+
+"skv2.utils.merge" takes an array of three values:
+- the top context
+- the yaml block that will be merged in (override)
+- the name of the base template (source)
+
+note: the source must be a named template (helm partial). This is necessary for the merging logic.
+
+The behaviour is as follows, to align with already existing helm behaviour:
+- If no source is found (template is empty), the merged output will be empty
+- If no overrides are specified, the source is rendered as is
+- If overrides are specified and source is not empty, overrides will be merged in to the source.
+
+Overrides can replace / add to deeply nested dictionaries, but will completely replace lists.
+Examples:
+
+┌─────────────────────┬───────────────────────┬────────────────────────┐
+│ Source (template)   │       Overrides       │        Result          │
+├─────────────────────┼───────────────────────┼────────────────────────┤
+│ metadata:           │ metadata:             │ metadata:              │
+│   labels:           │   labels:             │   labels:              │
+│     app: gloo       │    app: gloo1         │     app: gloo1         │
+│     cluster: useast │    author: infra-team │     author: infra-team │
+│                     │                       │     cluster: useast    │
+├─────────────────────┼───────────────────────┼────────────────────────┤
+│ lists:              │ lists:                │ lists:                 │
+│   groceries:        │  groceries:           │   groceries:           │
+│   - apple           │   - grapes            │   - grapes             │
+│   - banana          │                       │                        │
+└─────────────────────┴───────────────────────┴────────────────────────┘
+
+skv2.utils.merge is a fork of a helm library chart function (https://github.com/helm/charts/blob/master/incubator/common/templates/_util.tpl).
+This includes some optimizations to speed up chart rendering time, and merges in a value (overrides) with a named template, unlike the upstream
+version, which merges two named templates.
+
+*/ -}}
+{{- define "skv2.utils.merge" -}}
+{{- $top := first . -}}
+{{- $overrides := (index . 1) -}}
+{{- $tpl := fromYaml (include (index . 2) $top) -}}
+{{- if or (empty $overrides) (empty $tpl) -}}
+{{ include (index . 2) $top }} {{/* render source as is */}}
+{{- else -}}
+{{- $merged := merge $overrides $tpl -}}
+{{- toYaml $merged -}} {{/* render source with overrides as YAML */}}
+{{- end -}}
+{{- end -}}
\ No newline at end of file
diff --git a/codegen/test/chart-featureenv/templates/deployment.yaml b/codegen/test/chart-featureenv/templates/deployment.yaml
new file mode 100644
index 000000000..1d52de490
--- /dev/null
+++ b/codegen/test/chart-featureenv/templates/deployment.yaml
@@ -0,0 +1,129 @@
+# Code generated by skv2. DO NOT EDIT.
+
+
+
+{{- $painter := $.Values.painter }}
+---
+
+{{- define "painter.deploymentSpec" }}
+# Deployment manifest for painter
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app: painter
+  annotations:
+    app.kubernetes.io/name: painter
+  name: painter
+  namespace: {{ default .Release.Namespace $.Values.painter.namespace }}
+spec:
+  selector:
+    matchLabels:
+      app: painter
+  template:
+    metadata:
+      labels:
+        app: painter
+      annotations:
+        app.kubernetes.io/name: painter
+    spec:
+      serviceAccountName: painter
+      containers:
+{{- $painter := $.Values.painter }}
+{{- $painterImage := $painter.image }}
+      - name: painter
+        image: {{ $painterImage.registry }}/{{ $painterImage.repository }}:{{ $painterImage.tag }}
+        imagePullPolicy: {{ $painterImage.pullPolicy }}
+{{- if $painter.env }}
+        env:
+{{ toYaml $painter.env | indent 10 }}
+          - name: FEATURE_ENABLE_FOO
+            value: {{ $.Values.featureGates.Foo | quote }}
+{{- else if $painter.extraEnvs }}
+        env:
+{{- end }}
+{{- range $name, $item := $painter.extraEnvs }}
+          - name: {{ $name }}
+{{- $item | toYaml | nindent 12 }}
+{{- end }}
+        resources:
+{{- if $painter.resources }}
+{{ toYaml $painter.resources | indent 10}}
+{{- else}}
+          requests:
+            cpu: 500m
+            memory: 256Mi
+{{- end }}
+        {{- /*
+          Render securityContext configs if it is set.
+          If securityContext is not set, render the default securityContext.
+          If securityContext is set to 'false', render an empty map.
+        */}}
+        securityContext:
+{{- if or ($painter.securityContext) (eq "map[]" (printf "%v" $painter.securityContext)) }}
+{{ toYaml $painter.securityContext | indent 10}}
+{{/* Because securityContext is nil by default we can only perform following conversion if it is a boolean. Skip conditional otherwise. */}}
+{{- else if eq (ternary $painter.securityContext true (eq "bool" (printf "%T" $painter.securityContext))) false }}
+          {}
+{{- else}}
+          runAsNonRoot: true
+          {{- if not $painter.floatingUserId }}
+          runAsUser: {{ printf "%.0f" (float64 $painter.runAsUser) }}
+          {{- end }}
+          readOnlyRootFilesystem: true
+          allowPrivilegeEscalation: false
+          capabilities:
+            drop:
+            - ALL
+{{- end }}
+      {{- if $painterImage.pullSecret }}
+      imagePullSecrets:
+        - name: {{ $painterImage.pullSecret }}
+      {{- end}}
+{{- end }} {{/* define "painter.deploymentSpec" */}}
+
+{{/* Render painter deployment template with overrides from values*/}}
+{{ if $painter.enabled }}
+{{- $painterDeploymentOverrides := dict }}
+{{- if $painter.deploymentOverrides }}
+{{- $painterDeploymentOverrides = $painter.deploymentOverrides  }}
+{{- end }}
+---
+{{ include "skv2.utils.merge" (list . $painterDeploymentOverrides "painter.deploymentSpec") }}
+{{- end }}
+---
+{{ if $painter.enabled }}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  labels:
+    app: painter
+  {{- if $painter.serviceAccount}}
+  {{- if $painter.serviceAccount.extraAnnotations }}
+  annotations:
+    {{- range $key, $value := $painter.serviceAccount.extraAnnotations }}
+    {{ $key }}: {{ $value }}
+    {{- end }}
+  {{- end }}
+  {{- end}}
+  name: painter
+  namespace: {{ default .Release.Namespace $.Values.painter.namespace }}
+{{- end }}
+
+
+{{- define "painter.serviceSpec"}}
+
+{{- end }} {{/* define "painter.serviceSpec" */}}
+{{ if $painter.enabled }}
+{{/* Render painter service template with overrides from values*/}}
+{{- $painterServiceOverrides := dict }}
+{{- if $painter.serviceOverrides }}
+{{- $painterServiceOverrides = $painter.serviceOverrides  }}
+{{- end }}
+
+---
+
+{{ include "skv2.utils.merge" (list . $painterServiceOverrides "painter.serviceSpec") }}
+{{- end }}
+
diff --git a/codegen/test/chart-featureenv/templates/rbac.yaml b/codegen/test/chart-featureenv/templates/rbac.yaml
new file mode 100644
index 000000000..feb93b669
--- /dev/null
+++ b/codegen/test/chart-featureenv/templates/rbac.yaml
@@ -0,0 +1,2 @@
+# Code generated by skv2. DO NOT EDIT.
+
diff --git a/codegen/test/chart-featureenv/values.yaml b/codegen/test/chart-featureenv/values.yaml
new file mode 100644
index 000000000..600495e13
--- /dev/null
+++ b/codegen/test/chart-featureenv/values.yaml
@@ -0,0 +1,21 @@
+# Code generated by skv2. DO NOT EDIT.
+
+painter:
+  deploymentOverrides: null
+  enabled: true
+  env:
+    - name: FOO
+      value: bar
+  extraEnvs: {}
+  floatingUserId: false
+  image:
+    pullPolicy: IfNotPresent
+    registry: quay.io/solo-io
+    repository: painter
+    tag: v0.0.0
+  ports: {}
+  runAsUser: 10101
+  serviceOverrides: null
+  serviceType: ""
+  sidecars: {}
+