From 5998d13d2fabaa6352086a8df40a0c9284b3ebc9 Mon Sep 17 00:00:00 2001 From: RealAnna <89971034+RealAnna@users.noreply.github.com> Date: Thu, 16 Feb 2023 12:53:59 +0100 Subject: [PATCH] feat: introduce readiness and liveness probe feature Signed-off-by: RealAnna feat: added fix Signed-off-by: RealAnna feat: added fix Signed-off-by: RealAnna feat: added fix Signed-off-by: RealAnna feat: added fix Signed-off-by: RealAnna feat: introduce readiness and liveness probe feature (#3) * feat: introduce readiness and liveness probe feature feat: introduce readiness and liveness probe feature Signed-off-by: realanna * Update pkg/processor/probes/probes.go Co-authored-by: Florian Bacher Signed-off-by: realanna --------- Signed-off-by: realanna Co-authored-by: Florian Bacher --- cmd/helmify/flags.go | 1 + examples/operator/templates/_helpers.tpl | 14 +++ examples/operator/templates/deployment.yaml | 39 ++++---- examples/operator/values.yaml | 12 +++ pkg/config/config.go | 2 + pkg/helm/init.go | 14 +++ pkg/helmify/model.go | 4 +- pkg/metadata/metadata.go | 10 ++- pkg/processor/deployment/deployment.go | 53 ++++++++--- pkg/processor/probes/probes.go | 99 +++++++++++++++++++++ 10 files changed, 217 insertions(+), 31 deletions(-) create mode 100644 pkg/processor/probes/probes.go diff --git a/cmd/helmify/flags.go b/cmd/helmify/flags.go index b33579d8..52108af9 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", true, "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..0f1c443f 100644 --- a/examples/operator/templates/deployment.yaml +++ b/examples/operator/templates/deployment.yaml @@ -53,28 +53,15 @@ spec: key: VAR1 name: {{ include "operator.fullname" . }}-secret-vars - name: VAR2 - value: {{ .Values.controllerManager.manager.var2 }} + value: {{ .Values.controllerManager.manager.env.var2 }} - name: VAR3_MY_ENV - value: {{ .Values.controllerManager.manager.var3MyEnv }} + value: {{ .Values.controllerManager.manager.env.var3MyEnv }} - name: KUBERNETES_CLUSTER_DOMAIN 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 12 }} + {{- 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 12 }} + {{- 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/helmify/model.go b/pkg/helmify/model.go index fd009ffe..1dcc4994 100644 --- a/pkg/helmify/model.go +++ b/pkg/helmify/model.go @@ -1,9 +1,10 @@ package helmify import ( - "github.com/arttor/helmify/pkg/config" "io" + "github.com/arttor/helmify/pkg/config" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -43,6 +44,7 @@ type AppMetadata interface { TemplatedName(objName string) string // TemplatedString converts a string to templated string with chart name. TemplatedString(str string) string + TemplatedValue(container string, str string) string // TrimName trims common prefix from object name if exists. // We trim common prefix because helm already using release for this purpose. TrimName(objName string) string diff --git a/pkg/metadata/metadata.go b/pkg/metadata/metadata.go index 22370433..1e98c345 100644 --- a/pkg/metadata/metadata.go +++ b/pkg/metadata/metadata.go @@ -2,15 +2,18 @@ package metadata import ( "fmt" - "github.com/arttor/helmify/pkg/config" "strings" + "github.com/arttor/helmify/pkg/config" + "github.com/arttor/helmify/pkg/helmify" "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" ) +const valuesTempl = `{{- include "tplvalues.render" (dict "value" .Values.%s.%s.%s "context" $)}}` + const nameTeml = `{{ include "%s.fullname" . }}-%s` var nsGVK = schema.GroupVersionKind{ @@ -96,6 +99,11 @@ func (a *Service) TemplatedString(str string) string { return fmt.Sprintf(nameTeml, a.conf.ChartName, name) } +func (a *Service) TemplatedValue(container string, str string) string { + name := a.TrimName(str) + return fmt.Sprintf(valuesTempl, a.conf.ChartName, container, name) +} + func extractAppNamespace(obj *unstructured.Unstructured) string { if obj.GroupVersionKind() == nsGVK { return obj.GetName() diff --git a/pkg/processor/deployment/deployment.go b/pkg/processor/deployment/deployment.go index a4358bb6..5941d52a 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 { @@ -130,8 +130,11 @@ func (d deployment) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstr depl.Spec.Template.Spec.Volumes[i].PersistentVolumeClaim.ClaimName = tempPVCName } + // remove from spec things that will be processed separately + cleanSpec := cleanSpec(*depl.Spec.Template.Spec.DeepCopy()) + // replace container resources with template to values. - specMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&depl.Spec.Template.Spec) + specMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&cleanSpec) if err != nil { return true, nil, err } @@ -139,19 +142,20 @@ func (d deployment) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstr if err != nil { return true, nil, err } + for i := range containers { containerName := strcase.ToLowerCamel((containers[i].(map[string]interface{})["name"]).(string)) res, exists, err := unstructured.NestedMap(values, nameCamel, containerName, "resources") if err != nil { return true, nil, err } - if !exists || len(res) == 0 { - continue - } - err = unstructured.SetNestedField(containers[i].(map[string]interface{}), fmt.Sprintf(`{{- toYaml .Values.%s.%s.resources | nindent 10 }}`, nameCamel, containerName), "resources") - if err != nil { - return true, nil, err + if exists && len(res) > 0 { + err = unstructured.SetNestedField(containers[i].(map[string]interface{}), fmt.Sprintf(`{{- toYaml .Values.%s.%s.resources | nindent 10 }}`, nameCamel, containerName), "resources") + if err != nil { + return true, nil, err + } } + } err = unstructured.SetNestedSlice(specMap, containers, "containers") if err != nil { @@ -162,12 +166,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, depl.Spec.Template.Spec) + 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 { @@ -188,6 +201,15 @@ func (d deployment) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstr }, nil } +func cleanSpec(spec corev1.PodSpec) corev1.PodSpec { + + for i := 0; i < len(spec.Containers); i++ { + spec.Containers[i].LivenessProbe = nil + spec.Containers[i].ReadinessProbe = nil + } + return spec +} + func processReplicas(name string, deployment *appsv1.Deployment, values *helmify.Values) (string, error) { if deployment.Spec.Replicas == nil { return "", nil @@ -208,6 +230,7 @@ func processPodSpec(name string, appMeta helmify.AppMetadata, pod *corev1.PodSpe values := helmify.Values{} for i, c := range pod.Containers { processed, err := processPodContainer(name, appMeta, c, &values) + if err != nil { return nil, err } @@ -260,13 +283,16 @@ func processPodContainer(name string, appMeta helmify.AppMetadata, c corev1.Cont if e.ConfigMapRef != nil { e.ConfigMapRef.Name = appMeta.TemplatedName(e.ConfigMapRef.Name) } + } + c.Env = append(c.Env, corev1.EnvVar{ Name: cluster.DomainEnv, Value: fmt.Sprintf("{{ .Values.%s }}", cluster.DomainKey), }) for k, v := range c.Resources.Requests { err = unstructured.SetNestedField(*values, v.ToUnstructured(), name, containerName, "resources", "requests", k.String()) + if err != nil { return c, errors.Wrap(err, "unable to set container resources value") } @@ -277,7 +303,8 @@ func processPodContainer(name string, appMeta helmify.AppMetadata, c corev1.Cont return c, errors.Wrap(err, "unable to set container resources value") } } - return c, nil + + return c, err } func processEnv(name string, appMeta helmify.AppMetadata, c corev1.Container, values *helmify.Values) (corev1.Container, error) { @@ -293,7 +320,7 @@ func processEnv(name string, appMeta helmify.AppMetadata, c corev1.Container, va if err != nil { return c, errors.Wrap(err, "unable to set deployment value field") } - c.Env[i].Value = fmt.Sprintf(envValue, name, containerName, strcase.ToLowerCamel(strings.ToLower(c.Env[i].Name))) + c.Env[i].Value = fmt.Sprintf(envValue, name, containerName, "env", strcase.ToLowerCamel(strings.ToLower(c.Env[i].Name))) } } return c, nil diff --git a/pkg/processor/probes/probes.go b/pkg/processor/probes/probes.go new file mode 100644 index 00000000..ab8ba4a9 --- /dev/null +++ b/pkg/processor/probes/probes.go @@ -0,0 +1,99 @@ +package probes + +import ( + "fmt" + + "github.com/arttor/helmify/pkg/helmify" + yamlformat "github.com/arttor/helmify/pkg/yaml" + "github.com/iancoleman/strcase" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +const livenessProbe = "\n{{- 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 readinessProbe = "\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, pspec corev1.PodSpec) error { + + cs, _, err := unstructured.NestedSlice(specMap, "containers") + if err != nil { + return err + } + + strContainers, err := templateContainers(name, cs, pspec, values) + if err != nil { + return err + } + return unstructured.SetNestedSlice(specMap, strContainers, "containers") + +} + +func templateContainers(name string, cs []interface{}, pspec corev1.PodSpec, values *helmify.Values) ([]interface{}, error) { + strContainers := make([]interface{}, len(pspec.Containers)) + for i := range cs { + containerName := strcase.ToLowerCamel(pspec.Containers[i].Name) + + content, err := yamlformat.Marshal(cs[i], 0) + if err != nil { + return nil, err + } + strContainers[i] = content + err = setProbesTemplates(name, &(pspec.Containers[i]), &strContainers[i], containerName) + if err != nil { + return nil, err + } + err = setProbeField(name, &(pspec.Containers[i]), values) + if err != nil { + return nil, err + } + } + return strContainers, nil +} + +func setProbesTemplates(name string, container *corev1.Container, strContainers *interface{}, containerName string) error { + + if container.LivenessProbe != nil { + live, err := yamlformat.Marshal(container.LivenessProbe, 1) + if err != nil { + return err + } + *strContainers = (*strContainers).(string) + fmt.Sprintf(livenessProbe, name, containerName, live) + } + if container.ReadinessProbe != nil { + ready, err := yamlformat.Marshal(container.ReadinessProbe, 1) + if err != nil { + return err + } + *strContainers = (*strContainers).(string) + fmt.Sprintf(readinessProbe, name, containerName, ready) + } + return nil + +} + +func setProbeField(name string, c *corev1.Container, values *helmify.Values) error { + + containerName := strcase.ToLowerCamel(c.Name) + if c.LivenessProbe != nil { + ready, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(c.LivenessProbe) + err := unstructured.SetNestedField(*values, ready, name, containerName, "livenessProbe") + if err != nil { + return err + } + } + if c.ReadinessProbe != nil { + ready, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(c.ReadinessProbe) + return unstructured.SetNestedField(*values, ready, name, containerName, "readinessProbe") + } + return nil +}