{{/* Render the body of a container "resources:" block from a values dict of the form resources: limitFactor: 3 # optional, default 3 requests: { cpu, memory, ephemeral-storage / ephemeralStorage, ... } limits: { ... } # optional, every entry optional Rules (variant B, see issue #19): - requests are rendered as given (camelCase "ephemeralStorage" is normalized to the Kubernetes key "ephemeral-storage"). - an explicitly set limit always wins. - a missing memory / ephemeral-storage limit is computed as limitFactor x the corresponding request. - a missing cpu limit is NOT set at all (the kubelet then treats CPU as unlimited; CPU is compressible, so this cannot kill the workload). NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with the chart's name as define prefix, because sharing it would mean a library chart. template-chart/templates/_helpers.tpl is the reference implementation. */}} {{- define "convertx.resources" -}} {{- $resources := . | default dict -}} {{- $factor := 3.0 -}} {{- if not (kindIs "invalid" $resources.limitFactor) -}} {{- $factor = float64 $resources.limitFactor -}} {{- end -}} {{- $requests := dict -}} {{- range $key, $value := ($resources.requests | default dict) -}} {{- if not (kindIs "invalid" $value) -}} {{- $_ := set $requests (include "convertx.resourceKey" $key) $value -}} {{- end -}} {{- end -}} {{- $limits := dict -}} {{- range $key, $value := ($resources.limits | default dict) -}} {{- if not (kindIs "invalid" $value) -}} {{- $_ := set $limits (include "convertx.resourceKey" $key) $value -}} {{- end -}} {{- end -}} {{- range $key, $request := $requests -}} {{- if and (ne $key "cpu") (not (hasKey $limits $key)) -}} {{- $_ := set $limits $key (include "convertx.multiplyQuantity" (dict "quantity" $request "factor" $factor)) -}} {{- end -}} {{- end -}} {{- $out := dict -}} {{- with $requests }}{{- $_ := set $out "requests" . }}{{- end -}} {{- with $limits }}{{- $_ := set $out "limits" . }}{{- end -}} {{- toYaml $out -}} {{- end -}} {{/* Normalize a resource name from values to its Kubernetes key. */}} {{- define "convertx.resourceKey" -}} {{- if eq . "ephemeralStorage" -}}ephemeral-storage{{- else -}}{{ . }}{{- end -}} {{- end -}} {{/* Multiply a Kubernetes quantity by a factor, keeping its suffix. Input: dict "quantity" "factor" . Handles plain numbers ("2", "0.5", 1073741824), decimal ("1.5Gi"), binary ("512Mi", "1Gi", "10Ki") and milli/SI suffixes ("100m", "500M"). Integer results are printed without a decimal point. */}} {{- define "convertx.multiplyQuantity" -}} {{- $quantity := include "convertx.formatNumber" .quantity -}} {{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}} {{- if not $number -}} {{- fail (printf "cannot parse resource quantity %q" $quantity) -}} {{- end -}} {{- $suffix := trimPrefix $number $quantity -}} {{- if not (regexMatch "^(m|k|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$" $suffix) -}} {{- fail (printf "unsupported resource quantity suffix %q in %q" $suffix $quantity) -}} {{- end -}} {{- $product := mulf (float64 $number) .factor -}} {{- if eq $product (floor $product) -}} {{- printf "%d%s" (int64 $product) $suffix -}} {{- else -}} {{- printf "%s%s" (include "convertx.formatNumber" $product) $suffix -}} {{- end -}} {{- end -}} {{/* Render a number as a plain decimal string, never in exponential notation. Why this exists (issue #157): the null-safe fallback helpers pass values through fromYaml, which parses every number as float64. Go's "%v" switches float64 to exponential notation from about 1e6 upwards, so a bare byte count like 1048576 became "1.048576e+06" - not a valid Kubernetes quantity, and the render aborted. Strings ("512Mi") are passed through untouched. */}} {{- define "convertx.formatNumber" -}} {{- if kindIs "float64" . -}} {{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}} {{- else -}} {{- printf "%v" . -}} {{- end -}} {{- end -}} {{/* Render the replica count for the StatefulSet. Rules (issue #25): - scaleDown: true takes precedence and always yields 0. - otherwise an explicitly set replicas value is used as given, including an explicit 0 - Helm's "default" would swallow 0, hence the explicit kindIs "invalid" nil check. - a missing replicas value defaults to 1. */}} {{- define "convertx.replicas" -}} {{- if .scaleDown -}} 0 {{- else if not (kindIs "invalid" .replicas) -}} {{- .replicas -}} {{- else -}} 1 {{- end -}} {{- end -}} {{/* Merge a user-supplied values sub-tree onto a dict of chart defaults so that a null / empty override falls back to the defaults instead of erasing them. Input: dict "defaults" "given" "path" . Output: YAML of the merged dict (consume it with fromYaml). Why this exists (issues #61 and #99): Helm merges values maps recursively, but an override key written without children - e.g. a leftover securityContext: after deleting its last remaining sub-key - is YAML null. Where the chart defines a default, Helm treats that null as a deletion of the default; the template then renders "securityContext: null": no readOnlyRootFilesystem, no dropped capabilities, no allowPrivilegeEscalation: false. The workload starts and reports healthy while being unhardened, so nothing points at the mistake. This helper defines the opposite semantics: an empty block means "chart defaults apply". */}} {{- define "convertx.defaultedDict" -}} {{- $defaults := .defaults | default dict -}} {{- $given := .given -}} {{- $path := .path | default "value" -}} {{- if kindIs "invalid" $given -}} {{- $given = dict -}} {{- end -}} {{- if not (kindIs "map" $given) -}} {{- fail (printf "%s must be a mapping or null, got %s (%v)" $path (kindOf $given) $given) -}} {{- end -}} {{- $out := deepCopy $defaults -}} {{- range $key, $value := $given -}} {{- if not (kindIs "invalid" $value) -}} {{- $default := index $defaults $key -}} {{- if and (kindIs "map" $value) (kindIs "map" $default) -}} {{- $_ := set $out $key (fromYaml (include "convertx.defaultedDict" (dict "defaults" $default "given" $value "path" (printf "%s.%s" $path $key)))) -}} {{- else if kindIs "map" $default -}} {{- fail (printf "%s.%s must be a mapping or null, got %s (%v)" $path $key (kindOf $value) $value) -}} {{- else -}} {{- $_ := set $out $key $value -}} {{- end -}} {{- end -}} {{- end -}} {{- toYaml $out -}} {{- end -}} {{/* Pick one sub-block out of a values block that may itself be null. Input: dict "block" "key" "path" . Returns YAML of dict "value" , after rejecting a non-mapping block with a message that names the values path. Needed because ".Values.convertx.securityContext" can itself be erased, and indexing into nil inside a template is not an error but silently yields nothing. */}} {{- define "convertx.subBlock" -}} {{- $block := .block -}} {{- if kindIs "invalid" $block -}} {{- $block = dict -}} {{- end -}} {{- if not (kindIs "map" $block) -}} {{- fail (printf "%s must be a mapping or null, got %s (%v)" .path (kindOf $block) $block) -}} {{- end -}} {{- toYaml (dict "value" (index $block .key)) -}} {{- end -}} {{/* The effective security contexts, probes and resources (issue #99). These defaults MUST stay in sync with the corresponding blocks in values.yaml, which stays the documented, user-facing place for them; this copy is only the fallback for the case where an override has erased the block. TWO OF THEM REPRODUCE A DELIBERATE DEVIATION AND MUST NOT BE "TIDIED": - runAsUser / runAsGroup are 1000, not the repo's arbitrary 20000. The image creates its convertx user at uid 1000 and owns /home/convertx with it, and that uid owns the data on the PersistentVolume. Restoring the repo default here would move the runtime uid off the volume's data. - the probes point at /healthcheck, not at "/". "/" answers 302 in every default configuration of this chart, and the redirect target changes once the first account exists. The port name "http" is part of the context and belongs here too: a default that loses it leaves the workload with a probe that cannot succeed. */}} {{- define "convertx.podSecurityContext" -}} {{- $given := (fromYaml (include "convertx.subBlock" (dict "block" . "key" "pod" "path" "convertx.securityContext"))).value -}} {{- $defaults := dict "runAsNonRoot" true "runAsUser" 1000 "runAsGroup" 1000 "fsGroup" 1000 "fsGroupChangePolicy" "Always" "seccompProfile" (dict "type" "RuntimeDefault") -}} {{- include "convertx.defaultedDict" (dict "defaults" $defaults "given" $given "path" "convertx.securityContext.pod") -}} {{- end -}} {{- define "convertx.containerSecurityContext" -}} {{- $given := (fromYaml (include "convertx.subBlock" (dict "block" . "key" "container" "path" "convertx.securityContext"))).value -}} {{- $defaults := dict "allowPrivilegeEscalation" false "readOnlyRootFilesystem" true "runAsNonRoot" true "capabilities" (dict "drop" (list "ALL") "add" (list)) -}} {{- include "convertx.defaultedDict" (dict "defaults" $defaults "given" $given "path" "convertx.securityContext.container") -}} {{- end -}} {{- define "convertx.livenessProbe" -}} {{- $defaults := dict "httpGet" (dict "path" "/healthcheck" "port" "http") "periodSeconds" 10 "timeoutSeconds" 5 "failureThreshold" 3 -}} {{- include "convertx.defaultedDict" (dict "defaults" $defaults "given" . "path" "convertx.livenessProbe") -}} {{- end -}} {{- define "convertx.readinessProbe" -}} {{- $defaults := dict "httpGet" (dict "path" "/healthcheck" "port" "http") "periodSeconds" 10 "timeoutSeconds" 5 "failureThreshold" 3 -}} {{- include "convertx.defaultedDict" (dict "defaults" $defaults "given" . "path" "convertx.readinessProbe") -}} {{- end -}} {{- define "convertx.startupProbe" -}} {{- $defaults := dict "httpGet" (dict "path" "/healthcheck" "port" "http") "periodSeconds" 10 "timeoutSeconds" 5 "failureThreshold" 30 -}} {{- include "convertx.defaultedDict" (dict "defaults" $defaults "given" . "path" "convertx.startupProbe") -}} {{- end -}} {{/* The effective resources block, fed into the resources helper above so that an erased "resources:" still yields the chart's requests and computed limits instead of a container without any resource accounting. The numbers reproduce values.yaml, which carries the reasoning: they are sized for converter subprocesses (LibreOffice, Calibre, ImageMagick, ffmpeg), not for an idle web server, and the ephemeral-storage figure has to cover the two emptyDirs this chart mounts. */}} {{- define "convertx.effectiveResources" -}} {{- $defaults := dict "limitFactor" 3 "requests" (dict "cpu" 0.25 "memory" "1Gi" "ephemeralStorage" "1Gi") -}} {{- $merged := fromYaml (include "convertx.defaultedDict" (dict "defaults" $defaults "given" . "path" "convertx.resources")) -}} {{- include "convertx.resources" $merged -}} {{- end -}} {{/* Render a values boolean as the literal string the application accepts. Input: dict "value" "path" "default" Why this is not just "{{ .Values.x }}" (README, "Values that must not lie", point 6): ConvertX parses each of these variables as process.env.X?.toLowerCase() === "true" so ANYTHING that is not the literal "true" means false - "yes", "1", "on", "TRUE " with a trailing space. Two of the three variables this helper renders open the instance up (ACCOUNT_REGISTRATION, ALLOW_UNAUTHENTICATED), and a value written to switch one of them ON that silently lands on OFF is the kind of failure nobody goes looking for, because the symptom is the safe state. So: a bool renders as "true"/"false", the strings "true"/"false" render normalized to lower case, nil takes the given default, and everything else ABORTS THE RENDER naming the values path. Go's "not" is deliberately unused here - it treats every non-empty string as true, so "not \"false\"" is false and the string form would flip the wrong way. */}} {{- define "convertx.boolString" -}} {{- $v := .value -}} {{- if kindIs "invalid" $v -}} {{- $v = .default -}} {{- end -}} {{- if kindIs "bool" $v -}} {{- ternary "true" "false" $v -}} {{- else -}} {{- $s := lower (printf "%v" $v) -}} {{- if or (eq $s "true") (eq $s "false") -}} {{- $s -}} {{- else -}} {{- fail (printf "%s must be true or false, got %q - ConvertX compares this variable against the literal string \"true\", so any other spelling would silently mean false" .path $v) -}} {{- end -}} {{- end -}} {{- end -}} {{/* Render AUTO_DELETE_EVERY_N_HOURS. An explicit 0 is the documented way to switch the cleanup off, so this cannot go through Helm's "default": that swallows a 0 and would restore 24-hour deletion on exactly the install that asked for none. Same nil-check shape as the replicas helper, and for the same reason. */}} {{- define "convertx.autoDeleteEveryNHours" -}} {{- if kindIs "invalid" . -}} 24 {{- else -}} {{- include "convertx.formatNumber" . -}} {{- end -}} {{- end -}}