From 525a59da14d92e90145ca1f772db587023effd94 Mon Sep 17 00:00:00 2001 From: Shashank Ram Date: Mon, 16 Oct 2023 14:31:44 -0700 Subject: [PATCH] WIP: feature gate envs Signed-off-by: Shashank Ram --- changelog/v0.34.8/feature-crd.yaml | 5 + 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, 361 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..c03716095 --- /dev/null +++ b/changelog/v0.34.8/feature-crd.yaml @@ -0,0 +1,5 @@ +changelog: + - type: NEW_FEATURE + description: | + Render feature flag environment variables for containers. + skipCI: false diff --git a/codegen/cmd_test.go b/codegen/cmd_test.go index 4e611bfcd..512a533f2 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 []FeatureGateEnvVar, 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, + FeatureGateEnvVars: 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 FeatureGateEnvVars is specified", + map[string]string{"Foo": "true"}, + nil, + nil, + nil), + Entry("when Env is not specified and FeatureGateEnvVars is specified", + map[string]string{"Foo": "true"}, + nil, + []FeatureGateEnvVar{ + { + Name: "FEATURE_ENABLE_FOO", + Value: "{{ $.Values.featureGates.Foo | quote }}", + }, + }, + nil), + Entry("when Env and FeatureGateEnvVars are specified, true value", + map[string]string{"Foo": "true"}, + []v1.EnvVar{ + { + Name: "FOO", + Value: "bar", + }, + }, + []FeatureGateEnvVar{ + { + 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 FeatureGateEnvVars are specified, false value", + map[string]string{"Foo": "false"}, + []v1.EnvVar{ + { + Name: "FOO", + Value: "bar", + }, + }, + []FeatureGateEnvVar{ + { + 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..905845878 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 + + // FeatureGateEnvVars are used to enable/disable features based on Helm values. + // At least 1 environment variable must be set via Env to use this. + FeatureGateEnvVars []FeatureGateEnvVar +} + +// FeatureGateEnvVar is used to enable/disable features via environment variables +type FeatureGateEnvVar struct { + // Name of the environment variable for the feature. + // E.g. FEATURE_ENABLE_EXTERNAL_WORKLOADS + Name string + + // Helm value to enable/disable the feature. + // E.g. $.Values.featureGates.ExternalWorkloads + Value string } type ContainerPort struct { diff --git a/codegen/templates/chart/operator-deployment.yamltmpl b/codegen/templates/chart/operator-deployment.yamltmpl index f7239de19..54ca791b7 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.FeatureGateEnvVars ]] + - 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: {} +