From b16d3eaaddac1e7f1367312681c13cbc60721b73 Mon Sep 17 00:00:00 2001 From: RealAnna Date: Mon, 20 Feb 2023 07:52:20 +0100 Subject: [PATCH] feat: introduced liveness and readiness probes Signed-off-by: RealAnna --- cmd/helmify/flags.go | 1 + examples/operator/templates/_helpers.tpl | 14 +++ examples/operator/templates/deployment.yaml | 35 +++++--- examples/operator/values.yaml | 12 +++ pkg/config/config.go | 2 + pkg/helm/init.go | 14 +++ pkg/processor/deployment/deployment.go | 17 +++- pkg/processor/probes/probes.go | 97 +++++++++++++++++++++ pkg/processor/probes/probes_test.go | 87 ++++++++++++++++++ 9 files changed, 261 insertions(+), 18 deletions(-) create mode 100644 pkg/processor/probes/probes.go create mode 100644 pkg/processor/probes/probes_test.go diff --git a/cmd/helmify/flags.go b/cmd/helmify/flags.go index b33579d8..af939ccc 100644 --- a/cmd/helmify/flags.go +++ b/cmd/helmify/flags.go @@ -37,6 +37,7 @@ func ReadFlags() config.Config { flag.BoolVar(&result.VeryVerbose, "vv", false, "Enable very verbose output. Same as verbose but with DEBUG. Example: helmify -vv") flag.BoolVar(&crd, "crd-dir", false, "Enable crd install into 'crds' directory.\nWarning: CRDs placed in 'crds' directory will not be templated by Helm.\nSee https://helm.sh/docs/chart_best_practices/custom_resource_definitions/#some-caveats-and-explanations\nExample: helmify -crd-dir") flag.BoolVar(&result.ImagePullSecrets, "image-pull-secrets", false, "Allows the user to use existing secrets as imagePullSecrets in values.yaml") + flag.BoolVar(&result.Probes, "probes", false, "Allows the user to customize liveness and readiness probes") flag.Parse() if h || help { diff --git a/examples/operator/templates/_helpers.tpl b/examples/operator/templates/_helpers.tpl index c57b7442..3f492aee 100644 --- a/examples/operator/templates/_helpers.tpl +++ b/examples/operator/templates/_helpers.tpl @@ -60,3 +60,17 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* +Renders a value that contains template. +Usage: +{{ include "tplvalues.render" ( dict "value" .Values.path.to.the.Value "context" $) }} +*/}} +{{- define "tplvalues.render" -}} + {{- if typeIs "string" .value }} + {{- tpl .value .context }} + {{- else }} + {{- tpl (.value | toYaml) .context }} + {{- end }} +{{- end -}} + diff --git a/examples/operator/templates/deployment.yaml b/examples/operator/templates/deployment.yaml index 314890d3..70168fa7 100644 --- a/examples/operator/templates/deployment.yaml +++ b/examples/operator/templates/deployment.yaml @@ -60,21 +60,8 @@ spec: value: {{ .Values.kubernetesClusterDomain }} image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag | default .Chart.AppVersion }} - livenessProbe: - httpGet: - path: /healthz - port: 8081 - initialDelaySeconds: 15 - periodSeconds: 20 name: manager - readinessProbe: - httpGet: - path: /readyz - port: 8081 - initialDelaySeconds: 5 - periodSeconds: 10 - resources: {{- toYaml .Values.controllerManager.manager.resources | nindent 10 - }} + resources: {{- toYaml .Values.controllerManager.manager.resources | nindent 10 }} securityContext: allowPrivilegeEscalation: false volumeMounts: @@ -83,6 +70,26 @@ spec: subPath: controller_manager_config.yaml - mountPath: /my.ca name: secret-volume + {{- if .Values.controllerManager.manager.livenessProbe }} + livenessProbe: {{- include "tplvalues.render" (dict "value" .Values.controllerManager.manager.livenessProbe "context" $) | nindent 10 }} + {{- else }} + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + {{- end }} + {{- if .Values.controllerManager.manager.readinessProbe }} + readinessProbe: {{- include "tplvalues.render" (dict "value" .Values.controllerManager.manager.readinessProbe "context" $) | nindent 10 }} + {{- else }} + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + {{- end }} imagePullSecrets: - name: {{ include "operator.fullname" . }}-secret-registry-credentials securityContext: diff --git a/examples/operator/values.yaml b/examples/operator/values.yaml index 98e6621e..1e48cb97 100644 --- a/examples/operator/values.yaml +++ b/examples/operator/values.yaml @@ -10,6 +10,18 @@ controllerManager: image: repository: controller tag: latest + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 resources: limits: cpu: 100m diff --git a/pkg/config/config.go b/pkg/config/config.go index b7d3c682..9c755b25 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -23,6 +23,8 @@ type Config struct { Crd bool // ImagePullSecrets flag ImagePullSecrets bool + // Probes flag if true the probes will be parametrised + Probes bool } func (c *Config) Validate() error { diff --git a/pkg/helm/init.go b/pkg/helm/init.go index a8d77dd5..43b22df8 100644 --- a/pkg/helm/init.go +++ b/pkg/helm/init.go @@ -99,6 +99,20 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* +Renders a value that contains template. +Usage: +{{ include "tplvalues.render" ( dict "value" .Values.path.to.the.Value "context" $) }} +*/}} +{{- define "tplvalues.render" -}} + {{- if typeIs "string" .value }} + {{- tpl .value .context }} + {{- else }} + {{- tpl (.value | toYaml) .context }} + {{- end }} +{{- end -}} + ` const defaultChartfile = `apiVersion: v2 diff --git a/pkg/processor/deployment/deployment.go b/pkg/processor/deployment/deployment.go index a4358bb6..35dcaf05 100644 --- a/pkg/processor/deployment/deployment.go +++ b/pkg/processor/deployment/deployment.go @@ -7,10 +7,10 @@ import ( "text/template" "github.com/arttor/helmify/pkg/cluster" + "github.com/arttor/helmify/pkg/helmify" "github.com/arttor/helmify/pkg/processor" "github.com/arttor/helmify/pkg/processor/imagePullSecrets" - - "github.com/arttor/helmify/pkg/helmify" + "github.com/arttor/helmify/pkg/processor/probes" yamlformat "github.com/arttor/helmify/pkg/yaml" "github.com/iancoleman/strcase" "github.com/pkg/errors" @@ -47,7 +47,7 @@ const selectorTempl = `%[1]s {{- include "%[2]s.selectorLabels" . | nindent 6 }} %[3]s` -const envValue = "{{ .Values.%[1]s.%[2]s.%[3]s }}" +const envValue = "{{ .Values.%[1]s.%[2]s.%[3]s.%[4]s }}" // New creates processor for k8s Deployment resource. func New() helmify.Processor { @@ -162,12 +162,21 @@ func (d deployment) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstr imagePullSecrets.ProcessSpecMap(specMap, &values) } + if appMeta.Config().Probes { + err = probes.ProcessSpecMap(nameCamel, specMap, &values) + if err != nil { + return true, nil, err + } + } + spec, err := yamlformat.Marshal(specMap, 6) if err != nil { return true, nil, err } - spec = strings.ReplaceAll(spec, "'", "") + spec = strings.ReplaceAll(spec, "'", "") + spec = strings.ReplaceAll(spec, "|\n ", "") + spec = strings.ReplaceAll(spec, "|-\n ", "") return true, &result{ values: values, data: struct { diff --git a/pkg/processor/probes/probes.go b/pkg/processor/probes/probes.go new file mode 100644 index 00000000..78346ecd --- /dev/null +++ b/pkg/processor/probes/probes.go @@ -0,0 +1,97 @@ +package probes + +import ( + "fmt" + + "github.com/arttor/helmify/pkg/helmify" + yamlformat "github.com/arttor/helmify/pkg/yaml" + "github.com/iancoleman/strcase" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/yaml" +) + +const livenessProbe = "livenessProbe" +const readinessProbe = "readinessProbe" + +const livenessProbeTemplate = "{{- if .Values.%[1]s.%[2]s.livenessProbe }}\n" + + "livenessProbe: {{- include \"tplvalues.render\" (dict \"value\" .Values.%[1]s.%[2]s.livenessProbe \"context\" $) | nindent 10 }}\n" + + " {{- else }}\n" + + "livenessProbe:\n%[3]s" + + "\n{{- end }}" + +const readinessProbeTemplate = "\n{{- if .Values.%[1]s.%[2]s.readinessProbe }}\n" + + "readinessProbe: {{- include \"tplvalues.render\" (dict \"value\" .Values.%[1]s.%[2]s.readinessProbe \"context\" $) | nindent 10 }}\n" + + " {{- else }}\n" + + "readinessProbe:\n%[3]s" + + "\n{{- end }}" + +// ProcessSpecMap adds 'probes' to the Containers in specMap, if they are defined +func ProcessSpecMap(name string, specMap map[string]interface{}, values *helmify.Values) error { + + cs, _, err := unstructured.NestedSlice(specMap, "containers") + if err != nil { + return err + } + + strContainers := make([]interface{}, len(cs)) + for i, c := range cs { + castedContainer := c.(map[string]interface{}) + strContainers[i], err = setProbesTemplates(name, castedContainer, values) + if err != nil { + return err + } + } + + return unstructured.SetNestedSlice(specMap, strContainers, "containers") + +} + +func setProbesTemplates(name string, castedContainer map[string]interface{}, values *helmify.Values) (string, error) { + + var ready, live string + var err error + if _, defined := castedContainer[livenessProbe]; defined { + live, err = setProbe(name, castedContainer, values, livenessProbe) + if err != nil { + return "", err + } + delete(castedContainer, livenessProbe) + } + if _, defined := castedContainer[readinessProbe]; defined { + ready, err = setProbe(name, castedContainer, values, readinessProbe) + if err != nil { + return "", err + } + delete(castedContainer, readinessProbe) + } + return setMap(name, castedContainer, live, ready) + +} + +func setMap(name string, castedContainer map[string]interface{}, live string, ready string) (string, error) { + containerName := strcase.ToLowerCamel(castedContainer["name"].(string)) + content, err := yaml.Marshal(castedContainer) + + if err != nil { + return "", err + } + strContainer := string(content) + if live != "" { + strContainer = strContainer + fmt.Sprintf(livenessProbeTemplate, name, containerName, live) + } + if ready != "" { + strContainer = strContainer + fmt.Sprintf(readinessProbeTemplate, name, containerName, ready) + } + return strContainer, nil +} + +func setProbe(name string, castedContainer map[string]interface{}, values *helmify.Values, probe string) (string, error) { + containerName := strcase.ToLowerCamel(castedContainer["name"].(string)) + live, err := yamlformat.Marshal(castedContainer[probe], 1) + if err != nil { + return "", err + } + + return live, unstructured.SetNestedField(*values, castedContainer[probe], name, containerName, probe) + +} diff --git a/pkg/processor/probes/probes_test.go b/pkg/processor/probes/probes_test.go new file mode 100644 index 00000000..d16e8ca0 --- /dev/null +++ b/pkg/processor/probes/probes_test.go @@ -0,0 +1,87 @@ +package probes + +import ( + "testing" + + "github.com/arttor/helmify/pkg/helmify" + "github.com/stretchr/testify/require" + "sigs.k8s.io/yaml" +) + +func Test_setProbesTemplates(t *testing.T) { + + tests := []struct { + name string + deploymentName string + container map[string]interface{} + wantMap string + wantValue string + wantErr bool + }{ + { + name: "no probe no data generated", + deploymentName: "test", + container: map[string]interface{}{ + "name": "mycontainer", + }, + wantMap: "", + wantErr: false, + }, + { + name: "readinessProbe probe", + deploymentName: "test", + container: map[string]interface{}{ + "name": "mycontainer", + readinessProbe: map[string]interface{}{ + "timeoutSeconds": "1", + "periodSeconds": "20", + }, + }, + wantMap: "\n{{- if .Values.test.mycontainer.readinessProbe }}\n" + + "readinessProbe: {{- include \"tplvalues.render\" (dict \"value\" .Values.test.mycontainer.readinessProbe \"context\" $) | nindent 10 }}\n {{- else }}\n" + + "readinessProbe:\n" + + " periodSeconds: \"20\"\n" + + " timeoutSeconds: \"1\"\n" + + "{{- end }}", + wantValue: "readinessProbe:\n periodSeconds: \"20\"\n timeoutSeconds: \"1\"\n", + wantErr: false, + }, + { + name: "add livenessProbe probe", + deploymentName: "test", + container: map[string]interface{}{ + "name": "mycontainer", + livenessProbe: map[string]interface{}{ + "timeoutSeconds": "14", + "periodSeconds": "2", + }, + }, + wantMap: "{{- if .Values.test.mycontainer.livenessProbe }}\n" + + "livenessProbe: {{- include \"tplvalues.render\" (dict \"value\" .Values.test.mycontainer.livenessProbe \"context\" $) | nindent 10 }}\n" + + " {{- else }}\nlivenessProbe:\n" + + " periodSeconds: \"2\"\n" + + " timeoutSeconds: \"14\"\n" + + "{{- end }}", + wantValue: "livenessProbe:\n periodSeconds: \"2\"\n timeoutSeconds: \"14\"\n", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := make(helmify.Values) + res, err := setProbesTemplates(tt.deploymentName, tt.container, &v) + require.True(t, (err != nil) == tt.wantErr) + + require.Contains(t, res, tt.wantMap) + if tt.wantValue != "" { + val := (v)["test"].(map[string]interface{})["mycontainer"] + t.Log("VAL", val) + b, err := yaml.Marshal(val) + require.Nil(t, err) + require.Contains(t, string(b), tt.wantValue) + } else { + require.Empty(t, v) + } + }) + } +}