feat(convertx): new chart for ConvertX (1.0.0+upv0.19.0)
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 7s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Successful in 3s
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Successful in 10s
Install-test / Install-test ${{ fromJSON(inputs.charts) }} (pull_request) Successful in 2m35s
Validate Helm Charts / Install-test (pull_request) Successful in 2m35s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 7s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Successful in 3s
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Successful in 10s
Install-test / Install-test ${{ fromJSON(inputs.charts) }} (pull_request) Successful in 2m35s
Validate Helm Charts / Install-test (pull_request) Successful in 2m35s
ConvertX is a self-hosted file converter with a web UI. StatefulSet with one replica, a PVC at /app/data holding the SQLite database, the uploads and the converted output, a ClusterIP Service and the standard Ingress. No external database, cache or object storage. The chart runs non-root as uid/gid 1000 and bypasses the image entrypoint. The image carries no USER, so it starts as root, and /entrypoint.sh exists only to drop to uid 1000 with gosu. Overriding command with the image's own CMD lets Kubernetes do that work instead - runAsUser/runAsGroup 1000 and fsGroup in place of the entrypoint's recursive chown - so the repo's hardening baseline stays intact rather than being relaxed for this image. What that costs is named at the command: line in the StatefulSet: if upstream ever moves real work into the entrypoint, this chart skips it silently, with no error and no failed probe. The entrypoint of v0.19.0 does nothing but uid/gid plumbing (and with PUID unset its first branch is already a plain exec "$@"), and ConvertX migrates its SQLite schema in-process at import, so the bypass is safe today. It is to be re-read at every app-version update. The one line the bypass does lose is "export HOME=/home/convertx", so the chart sets HOME itself instead of relying on the runtime's passwd lookup, and mounts an emptyDir there: LibreOffice, Calibre, Inkscape and fontconfig build their profiles under HOME on the first conversion, and it sits on the read-only root filesystem. /tmp is the second emptyDir, for the toolchain's scratch files. Both count against the container's ephemeral-storage limit, which is why that request is sized for them. uid 1000 rather than the repo's arbitrary 20000, deliberately: the image owns /home/convertx with it, and the uid owns data on a PersistentVolume, so the number is not free to tidy later. The fallback defaults in _helpers.tpl reproduce the deviation rather than the repo standard. All three probes use /healthcheck, a static 200 that the application exempts from authentication. "/" is never a 200 with this chart's defaults: it answers 302 to /setup while no account exists and to /login afterwards, so a probe against it would pass on a redirect whose meaning changes on day two. The startup budget is 300s because ConvertX shells out to ~20 converter binaries at import just to log their versions. Values: - secrets.jwtSecretRef, required, field "jwt-secret". Without JWT_SECRET the application mints a random one per process, so every restart invalidates every session - silently. - accountRegistration, allowUnauthenticated and httpAllowed, all false. Upstream warns explicitly against the first two on an internet-facing instance; the warning is carried into values.yaml and NOTES.txt. They go through a helper that renders the literal "true"/"false" the application's strict comparison needs and aborts the render on anything else - "yes" would otherwise be filed under "off" without a word. - autoDeleteEveryNHours, default 24, 0 disables. It governs retention on the PVC. The 0 is honoured through an explicit nil check; Helm's "default" would swallow it and restore 24-hour deletion on the install that asked for none. - ingress.maxUploadFileSize, default 1024m. ConvertX enforces no size limit of its own (maxRequestBodySize is MAX_SAFE_INTEGER), so unlike outline there is exactly one ceiling here and this is it. Switching the Ingress off frees BOTH domain and clusterIssuer, checked in both directions: ConvertX has no base-URL, public-URL or redirect-URI variable, so nothing outside the Ingress reads the domain. WEBROOT is deliberately not a value. It shifts every route including /healthcheck, which would break all three probes while the application stayed healthy, and this chart's Ingress still maps "/". PUID/PGID/UMASK are not values either - they configure the bypassed entrypoint and nothing else. Wiring: publish-convertx.yml, the change-app-version.yml choice list, ci/convertx/ (test-values, the JWT Secret fixture the stub OnePasswordItem CRD cannot materialize, and INSTALL_TIMEOUT=900 for a ~1.4 GiB image), a renovate.json5 block anchored on the v prefix so the moving latest, main and pr-<n> tags cannot be selected, and the README chart row. Verified locally: helm lint --strict clean, render against the CI fixtures, the null-safe fallbacks for every erased block, the resource-quantity render test, and NOTES.txt through the define/ConfigMap trick since helm template does not render it. Runtime behaviour is unverified here - this workspace has no Docker daemon and no k3d, so the PR install-test is the only runtime proof.
This commit is contained in:
17 files changed
+1435
No files matched your search
@@ -0,0 +1,224 @@
|
||||
{{- $replicas := include "convertx.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.convertx.replicas) -}}
|
||||
{{- $startup := fromYaml (include "convertx.startupProbe" .Values.convertx.startupProbe) -}}
|
||||
{{- $autoDelete := include "convertx.autoDeleteEveryNHours" .Values.convertx.autoDeleteEveryNHours -}}
|
||||
{{- $registration := include "convertx.boolString" (dict "value" .Values.convertx.accountRegistration "default" false "path" "convertx.accountRegistration") -}}
|
||||
{{- $unauthenticated := include "convertx.boolString" (dict "value" .Values.convertx.allowUnauthenticated "default" false "path" "convertx.allowUnauthenticated") -}}
|
||||
{{- $httpAllowed := include "convertx.boolString" (dict "value" .Values.convertx.httpAllowed "default" false "path" "convertx.httpAllowed") -}}
|
||||
ConvertX {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
||||
|
||||
{{- if .Values.ingress.enabled }}
|
||||
|
||||
ACCESS
|
||||
https://{{ .Values.ingress.domain }}
|
||||
|
||||
The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}"
|
||||
cluster issuer. Until it has been issued the browser shows a warning; follow it with
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} get certificate tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
{{- else }}
|
||||
|
||||
ACCESS — NO INGRESS
|
||||
ingress.enabled is false, so this release has no Ingress and no certificate,
|
||||
and neither ingress.clusterIssuer nor ingress.domain is needed. ConvertX has
|
||||
no base-URL setting of its own — it builds no absolute links — so nothing
|
||||
outside the Ingress reads the domain in this chart.
|
||||
|
||||
Reach the release inside the cluster with
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80
|
||||
|
||||
and open http://localhost:8080. Note that the session cookie is marked
|
||||
"secure"{{ if eq $httpAllowed "true" }} — except that convertx.httpAllowed is true in this release, which
|
||||
clears that flag, so logging in over plain http works{{ else }}, so logging in over plain http will NOT work: the browser
|
||||
refuses to store the cookie. convertx.httpAllowed: true clears that flag{{ end }}.
|
||||
{{- end }}
|
||||
{{ if eq $replicas "0" }}
|
||||
SCALED TO ZERO
|
||||
{{ if .Values.scaleDown }}scaleDown: true{{ else }}convertx.replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so the endpoint
|
||||
above does not answer until you set it back and upgrade again. PVC, Secret{{ if .Values.ingress.enabled }},
|
||||
Service and Ingress stay in place, so neither the conversions nor the
|
||||
certificate are touched.{{ else }} and
|
||||
Service stay in place, so the conversions on the PVC are untouched.{{ end }}
|
||||
{{ end }}
|
||||
WHOEVER OPENS THE PAGE FIRST BECOMES THE ADMINISTRATOR
|
||||
A fresh ConvertX has no users, and with no users it serves a SETUP FORM to
|
||||
every visitor — no password, no invitation, nothing to bypass. The first
|
||||
account created that way owns the instance, and convertx.accountRegistration
|
||||
does not gate it: that value decides whether accounts TWO and onwards can be
|
||||
created, not the first one.
|
||||
|
||||
So on a fresh install, register yourself before the DNS name is reachable by
|
||||
anyone else. There is no second chance and no "reset admin" command; the
|
||||
repair is deleting the users row from the SQLite database on the PVC.
|
||||
{{- if eq $unauthenticated "true" }}
|
||||
|
||||
This release has convertx.allowUnauthenticated: true, so the point above is
|
||||
moot in the worst way — see the warning further down.
|
||||
{{- end }}
|
||||
|
||||
THE 1PASSWORD ITEM MUST CARRY THE FIELD THE CHART READS
|
||||
secrets.jwtSecretRef points at
|
||||
{{ .Values.secrets.jwtSecretRef }}
|
||||
The 1Password operator materializes that item as the Kubernetes Secret
|
||||
{{ .Release.Name }}-{{ .Chart.Name }}-secret, with one Secret key per FIELD of the item. So the field
|
||||
name is not free-form: this chart reads exactly one field, and it must be
|
||||
spelled
|
||||
|
||||
jwt-secret a long random string — openssl rand -hex 32
|
||||
|
||||
An item without it does not fail the install. The release goes through and
|
||||
the pod then hangs in CreateContainerConfigError naming the key it could not
|
||||
find. Watch the materialization with
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} get onepassworditem {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
||||
kubectl -n {{ .Release.Namespace }} get secret {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
||||
|
||||
WHY THIS VALUE IS REQUIRED RATHER THAN OPTIONAL: ConvertX signs its session
|
||||
cookies with JWT_SECRET, and when the variable is unset it quietly generates
|
||||
a random one PER PROCESS. Every restart — a node drain, an image update, an
|
||||
OOM kill — would then invalidate every session, with no log line and no
|
||||
error. Rotating the field has the same effect on purpose; after a rotation:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} rollout restart statefulset/{{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
|
||||
Editing the value INSIDE the item does not roll the pod by itself (Helm
|
||||
never sees a secret's content). Pointing secrets.jwtSecretRef at a DIFFERENT
|
||||
item does — the pod template carries a checksum over the rendered
|
||||
OnePasswordItem.
|
||||
|
||||
TWO SWITCHES THAT OPEN THE INSTANCE UP
|
||||
convertx.accountRegistration {{ $registration }}
|
||||
convertx.allowUnauthenticated {{ $unauthenticated }}
|
||||
|
||||
Upstream warns explicitly against enabling either on an instance reachable
|
||||
from the internet, and this chart puts a public DNS name in front by
|
||||
default. accountRegistration lets anyone with the URL create an account;
|
||||
allowUnauthenticated removes the login entirely, so anyone who reaches the
|
||||
Service can upload files and read every conversion on the volume.
|
||||
{{- if or (eq $registration "true") (eq $unauthenticated "true") }}
|
||||
|
||||
*** {{ if and (eq $registration "true") (eq $unauthenticated "true") }}BOTH OF THEM ARE ON{{ else }}ONE OF THEM IS ON{{ end }} IN THIS RELEASE. ***
|
||||
{{- if eq $unauthenticated "true" }}
|
||||
convertx.allowUnauthenticated: true — there is no login. Anyone who can
|
||||
reach {{ if .Values.ingress.enabled }}https://{{ .Values.ingress.domain }}{{ else }}the Service{{ end }} can use it and read other people's files.
|
||||
{{- end }}
|
||||
{{- if eq $registration "true" }}
|
||||
convertx.accountRegistration: true — anyone with the URL can sign up.
|
||||
{{- end }}
|
||||
If this instance is reachable from the internet, that is almost certainly
|
||||
not what you wanted.
|
||||
{{- end }}
|
||||
|
||||
RETENTION IS A VALUE, AND IT IS THE ONLY THING THAT EMPTIES THE VOLUME
|
||||
{{- if eq $autoDelete "0" }}
|
||||
convertx.autoDeleteEveryNHours is 0 — THE CLEANUP IS OFF. Uploads and
|
||||
converted output accumulate on the PVC for as long as the release lives and
|
||||
nothing reclaims them. Watch the volume, or set a non-zero number of hours.
|
||||
{{- else }}
|
||||
convertx.autoDeleteEveryNHours is {{ $autoDelete }} — ConvertX deletes the uploads and the
|
||||
output of any job older than {{ $autoDelete }} hours. That is a hard delete, not an
|
||||
archive: a conversion nobody downloaded in time is gone. Set it to 0 to
|
||||
switch the cleanup off, and then size storage.amount for unbounded growth.
|
||||
{{- end }}
|
||||
|
||||
STORAGE
|
||||
{{ .Release.Name }}-{{ .Chart.Name }}-pvc, {{ .Values.storage.amount }}, {{ if .Values.storage.storageClass }}storage class "{{ .Values.storage.storageClass }}"{{ else }}on the cluster's default storage class
|
||||
(storage.storageClass is unset){{ end }}, mounted at /app/data.
|
||||
|
||||
Everything this release persists is on that one volume: the SQLite database
|
||||
(users, jobs, file names), every uploaded file and every converted result.
|
||||
There is no external database and no object storage, so one snapshot of the
|
||||
PVC is a complete backup — and losing it loses the accounts along with the
|
||||
files.
|
||||
|
||||
Because the PVC is rendered by the chart, helm uninstall deletes it and the
|
||||
data with it, unless the storage class reclaim policy says otherwise.
|
||||
{{- if gt (int $replicas) 1 }}
|
||||
|
||||
convertx.replicas is {{ $replicas }}, and that does not work. The PVC is ReadWriteOnce, so
|
||||
the extra pods normally stay Pending on "Multi-Attach error for volume"; on
|
||||
a genuinely RWX storage class they would start and then corrupt the SQLite
|
||||
database two processes are now writing. Set it back to 1.
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.enabled }}
|
||||
|
||||
THE INGRESS IS THE ONLY UPLOAD CEILING
|
||||
ingress.maxUploadFileSize {{ .Values.ingress.maxUploadFileSize }}
|
||||
|
||||
ConvertX enforces no size limit of its own — none, at any setting — so this
|
||||
annotation is the whole story. A file above it never reaches the
|
||||
application: the user gets the ingress controller's 413 page and nothing
|
||||
appears in the pod log, which makes it look like a broken upload button
|
||||
rather than a limit.
|
||||
|
||||
Raising it has a second cost: the upload lands on the PVC above, so a
|
||||
ceiling far beyond storage.amount just moves the failure from a 413 to a
|
||||
full volume.
|
||||
{{- else }}
|
||||
|
||||
UPLOAD CEILING — NONE FROM THIS CHART
|
||||
ingress.enabled is false, so the chart sets no body-size limit anywhere, and
|
||||
ConvertX enforces none of its own. Whatever proxy you put in front brings
|
||||
its ceiling with it — ingress-nginx defaults to 1m, small enough to look
|
||||
like a broken upload button.
|
||||
{{- end }}
|
||||
|
||||
WHAT A GREEN POD PROVES
|
||||
All three probes are HTTP GETs against /healthcheck, which ConvertX answers
|
||||
with a static 200 and no authentication. It touches neither the database nor
|
||||
the volume, so a green pod proves the process is serving — not that a
|
||||
conversion works and not that the volume is writable.
|
||||
|
||||
It is deliberately NOT "/": with the defaults of this chart "/" answers 302,
|
||||
to /setup while no account exists and to /login afterwards. A probe would
|
||||
pass on that redirect, and pass on a different one from day two onwards.
|
||||
|
||||
The startup probe grants {{ $startup.failureThreshold }} × {{ $startup.periodSeconds }}s = {{ mul $startup.failureThreshold $startup.periodSeconds }}s. That budget is generous because
|
||||
ConvertX shells out to about twenty converter binaries at startup just to
|
||||
log their versions.
|
||||
|
||||
THE FIRST PULL IS SLOW, AND THAT IS NORMAL
|
||||
The image is roughly 1.4 GiB compressed — LibreOffice, Calibre, ImageMagick,
|
||||
ffmpeg, Inkscape and a TeX Live installation are all inside it. On a node
|
||||
that has not seen it before, expect several minutes in ContainerCreating
|
||||
before the startup probe budget above even begins. A pod that looks stuck
|
||||
there is usually still pulling:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} describe pod {{ .Release.Name }}-{{ .Chart.Name }}-sfs-0
|
||||
|
||||
THIS CHART BYPASSES THE IMAGE ENTRYPOINT
|
||||
The image runs as root and drops privileges itself; this chart starts the
|
||||
application directly (command: bun run dist/src/index.js) as uid 1000, with
|
||||
fsGroup doing what the entrypoint's chown would have done. PUID, PGID and
|
||||
UMASK therefore configure NOTHING here — setting them through convertx.env
|
||||
has no effect at all, and the uid belongs in convertx.securityContext.pod.
|
||||
|
||||
The cost to know about: if upstream moves real work into that entrypoint,
|
||||
this chart skips it silently. Re-read the upstream entrypoint.sh at every
|
||||
app-version update.
|
||||
{{- if .Values.convertx.env }}
|
||||
|
||||
EXTRA ENVIRONMENT VARIABLES ARE ACTIVE
|
||||
convertx.env adds {{ len .Values.convertx.env }} variable(s), rendered after the ones the chart sets
|
||||
itself. Two warnings that go with that block:
|
||||
|
||||
- WEBROOT moves every route of the application, /healthcheck included, so
|
||||
it breaks all three probes while the app is perfectly healthy — and this
|
||||
chart's Ingress still maps "/". Do not set it here.
|
||||
- An entry repeating a name the chart already sets is additive, not a
|
||||
replacement. At install yours wins; on an EXISTING release Kubernetes
|
||||
merges env by name and the chart's own value survives instead, while helm
|
||||
upgrade still reports success. Read it back off the StatefulSet.
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
|
||||
EXTRA INGRESS ANNOTATIONS ARE ACTIVE
|
||||
{{- range $key, $value := . }}
|
||||
{{ $key }}: {{ $value }}
|
||||
{{- end }}
|
||||
These are merged with the cert-manager and proxy-body-size annotations the
|
||||
chart always sets. Annotations are interpreted by the ingress controller, so
|
||||
a misspelled key is silently ignored — it does not fail the render.
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,327 @@
|
||||
{{/*
|
||||
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" <string|number> "factor" <float64>.
|
||||
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" <dict> "given" <dict|nil> "path" <string, optional
|
||||
values path used in error messages>. 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" <dict|nil> "key" <string> "path" <string>.
|
||||
Returns YAML of dict "value" <the sub-block, may be nil>, 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" <bool|string|nil> "path" <values path, for the error>
|
||||
"default" <bool, used when the value is nil>
|
||||
|
||||
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 -}}
|
||||
@@ -0,0 +1,34 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided" .Values.ingress.clusterIssuer }}
|
||||
# ConvertX enforces no upload limit of its own (it starts its HTTP server
|
||||
# with maxRequestBodySize: Number.MAX_SAFE_INTEGER), so these two
|
||||
# annotations are the ONLY ceiling on an upload. Both spellings are set
|
||||
# because the two nginx ingress controllers read different keys.
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: {{ .Values.ingress.maxUploadFileSize }}
|
||||
nginx.org/client-max-body-size: {{ .Values.ingress.maxUploadFileSize }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
rules:
|
||||
- host: {{ required "A Domain/Host must be provided" .Values.ingress.domain }}
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
port:
|
||||
name: http
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ required "A Domain/Host must be provided" .Values.ingress.domain }}
|
||||
secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
{{- end }}
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.storage.amount }}
|
||||
{{- if .Values.storage.storageClass }}
|
||||
storageClassName: {{ .Values.storage.storageClass }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
||||
spec:
|
||||
itemPath: {{ required "A Secret-Reference for the JWT signing key must be provided (secrets.jwtSecretRef)" .Values.secrets.jwtSecretRef }}
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
spec:
|
||||
selector:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: http
|
||||
name: http
|
||||
type: ClusterIP
|
||||
@@ -0,0 +1,201 @@
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
spec:
|
||||
serviceName: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
replicas: {{ include "convertx.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.convertx.replicas) }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
template:
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
annotations:
|
||||
# Hash of the rendered OnePasswordItem, so that repointing
|
||||
# secrets.jwtSecretRef at a DIFFERENT vault item really rolls the pod
|
||||
# instead of leaving the old signing key in the running environment.
|
||||
# A change of the item's CONTENT inside the vault is not covered -
|
||||
# Helm never sees it; see the comment on secrets in values.yaml.
|
||||
# The name differs from checksum/config on purpose: that one promises
|
||||
# "the config changed", this one only "the config source changed".
|
||||
#
|
||||
# operator.1password.io/auto-restart is deliberately NOT set: the
|
||||
# 1Password operator only restarts Deployments, so on a StatefulSet it
|
||||
# is a silent no-op that advertises a guarantee it does not give.
|
||||
checksum/secret-ref: {{ include (print $.Template.BasePath "/convertx.secret.yaml") . | sha256sum }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
{{- include "convertx.podSecurityContext" .Values.convertx.securityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
image: "ghcr.io/c4illin/convertx:{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
||||
# THE IMAGE ENTRYPOINT IS BYPASSED ON PURPOSE. READ THIS BEFORE THE
|
||||
# NEXT APP-VERSION BUMP - IT IS THE ONE BET THIS CHART MAKES.
|
||||
#
|
||||
# ghcr.io/c4illin/convertx declares
|
||||
# ENTRYPOINT ["/entrypoint.sh"]
|
||||
# CMD ["bun", "run", "dist/src/index.js"]
|
||||
# and carries no USER, so the image runs as root by default and
|
||||
# /entrypoint.sh is what drops it to uid 1000 with gosu. Setting
|
||||
# "command" here replaces the ENTRYPOINT (and discards the CMD), so
|
||||
# the application is started directly and Kubernetes does the
|
||||
# privilege work instead: runAsUser/runAsGroup 1000 in the pod
|
||||
# security context, and fsGroup 1000 in place of the entrypoint's
|
||||
# recursive chown of /app/data. The hardening baseline of this repo
|
||||
# stays intact - no root, no capabilities, read-only root fs.
|
||||
#
|
||||
# What the entrypoint of v0.19.0 actually does, read rather than
|
||||
# assumed: nothing but uid/gid plumbing. No migration, no config
|
||||
# generation, no secret generation. ConvertX creates and migrates
|
||||
# its SQLite schema in-process at import time, so starting it
|
||||
# directly is migration-safe. Its very first branch is
|
||||
# if [ -z "$PUID" ] || [ "$PUID" -eq 0 ]; then ... exec "$@"
|
||||
# i.e. with PUID unset it is already a pure pass-through - which is
|
||||
# why bypassing it costs nothing TODAY and is exactly why the risk
|
||||
# below is about tomorrow. The one line we do lose is
|
||||
# "export HOME=/home/convertx", and this chart sets HOME itself
|
||||
# below rather than relying on the runtime's passwd lookup.
|
||||
#
|
||||
# THE RISK: if upstream ever moves real work into the entrypoint - a
|
||||
# data migration is the obvious candidate - this chart skips it
|
||||
# silently. There is no error, no log line and no failed probe; the
|
||||
# application simply starts against an unmigrated volume. So
|
||||
# RE-READ entrypoint.sh AT EVERY APP-VERSION UPDATE, not only on
|
||||
# major jumps, and if it has grown anything beyond uid/gid handling,
|
||||
# stop and reconsider this override.
|
||||
command:
|
||||
- bun
|
||||
- run
|
||||
- dist/src/index.js
|
||||
|
||||
# Not decoration. The application resolves its data paths relative
|
||||
# to the working directory - uploadsDir is "./data/uploads/" and
|
||||
# outputDir is "./data/output/", hardcoded and configurable by no
|
||||
# environment variable. The image sets WORKDIR /app, so this only
|
||||
# restates it; it is here because a different working directory
|
||||
# would put every upload somewhere other than the mounted PVC, and
|
||||
# the application would report no error at all.
|
||||
workingDir: /app
|
||||
|
||||
env:
|
||||
# The entrypoint would have exported this; the image declares no
|
||||
# ENV HOME, so without this line HOME comes from the container
|
||||
# runtime's /etc/passwd lookup for uid 1000. That happens to
|
||||
# resolve to /home/convertx in this image - and "happens to" is
|
||||
# not a basis for the directory LibreOffice, Calibre, Inkscape and
|
||||
# fontconfig write their profiles into on the first conversion.
|
||||
# Set explicitly, and mounted writable below.
|
||||
- name: HOME
|
||||
value: "/home/convertx"
|
||||
# The port the application binds. containerPort, the Service's
|
||||
# targetPort and the three probes all name the same 3000; moving
|
||||
# it means moving all four, which is why it is pinned here rather
|
||||
# than exposed as a value.
|
||||
- name: PORT
|
||||
value: "3000"
|
||||
# Signs the session cookies. Without it ConvertX falls back to
|
||||
# randomUUID() per process, so every restart logs everyone out -
|
||||
# see secrets.jwtSecretRef in values.yaml.
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
||||
key: jwt-secret
|
||||
# The three booleans go through convertx.boolString, which renders
|
||||
# the literal "true"/"false" the application's strict comparison
|
||||
# needs and aborts the render on anything else.
|
||||
- name: ACCOUNT_REGISTRATION
|
||||
value: "{{ include "convertx.boolString" (dict "value" .Values.convertx.accountRegistration "default" false "path" "convertx.accountRegistration") }}"
|
||||
- name: ALLOW_UNAUTHENTICATED
|
||||
value: "{{ include "convertx.boolString" (dict "value" .Values.convertx.allowUnauthenticated "default" false "path" "convertx.allowUnauthenticated") }}"
|
||||
- name: HTTP_ALLOWED
|
||||
value: "{{ include "convertx.boolString" (dict "value" .Values.convertx.httpAllowed "default" false "path" "convertx.httpAllowed") }}"
|
||||
- name: AUTO_DELETE_EVERY_N_HOURS
|
||||
value: "{{ include "convertx.autoDeleteEveryNHours" .Values.convertx.autoDeleteEveryNHours }}"
|
||||
{{- range $value := .Values.convertx.env }}
|
||||
{{- if $value.value }}
|
||||
- name: {{ $value.name | quote }}
|
||||
# quote is required, not cosmetic: a value like "8080" or "true"
|
||||
# renders unquoted as a YAML number/bool, and the API server then
|
||||
# rejects the whole manifest.
|
||||
value: {{ tpl $value.value $ | quote }}
|
||||
{{- else if $value.valueFrom }}
|
||||
{{- if $value.valueFrom.secretKeyRef }}
|
||||
- name: {{ $value.name }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ tpl $value.valueFrom.secretKeyRef.name $ | quote }}
|
||||
key: {{ tpl $value.valueFrom.secretKeyRef.key $ | quote }}
|
||||
{{- else if $value.valueFrom.configMapKeyRef }}
|
||||
- name: {{ $value.name }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ tpl $value.valueFrom.configMapKeyRef.name $ | quote }}
|
||||
key: {{ tpl $value.valueFrom.configMapKeyRef.key $ | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
protocol: TCP
|
||||
name: http
|
||||
# Probes, resources and both security contexts go through the
|
||||
# null-safe helpers (#99): an erased block means "chart defaults
|
||||
# apply" instead of a vanished probe or an unhardened container.
|
||||
# Never wrap these in an "if" - a guard hides the erased case
|
||||
# completely instead of rendering a visible null.
|
||||
livenessProbe:
|
||||
{{- include "convertx.livenessProbe" .Values.convertx.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- include "convertx.readinessProbe" .Values.convertx.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- include "convertx.startupProbe" .Values.convertx.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "convertx.effectiveResources" .Values.convertx.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- include "convertx.containerSecurityContext" .Values.convertx.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
# SQLite database, uploads and converted output - everything the
|
||||
# application persists lives under this one path.
|
||||
- mountPath: /app/data
|
||||
name: data
|
||||
# Two writable emptyDirs so the root filesystem can stay
|
||||
# read-only. Neither is a precaution: ConvertX itself writes only
|
||||
# under /app/data, but it runs its conversions as subprocesses
|
||||
# (soffice, ebook-convert, magick, gs, xelatex, ffmpeg...) that
|
||||
# inherit its environment, and those need scratch space.
|
||||
#
|
||||
# /home/convertx is HOME. LibreOffice creates its user profile
|
||||
# there on first use - it is invoked without
|
||||
# -env:UserInstallation, so there is no way to point it elsewhere
|
||||
# - and Calibre, Inkscape, ImageMagick and fontconfig do the same
|
||||
# with their own config and cache directories. The directory
|
||||
# exists in the image (useradd -m) and is owned by 1000:1000, but
|
||||
# it sits on the read-only root filesystem, so it has to be
|
||||
# overmounted to be usable.
|
||||
- mountPath: /home/convertx
|
||||
name: home
|
||||
# /tmp is the scratch directory of the toolchain: Ghostscript,
|
||||
# TeX Live and Calibre all use it during a conversion.
|
||||
#
|
||||
# Both of these count against the container's ephemeral-storage
|
||||
# limit - see the resources comment in values.yaml.
|
||||
- mountPath: /tmp
|
||||
name: tmp
|
||||
restartPolicy: Always
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Release.Name }}-{{ .Chart.Name }}-pvc
|
||||
- name: home
|
||||
emptyDir: {}
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
Reference in new issue
Block a user