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

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:
jklein committed 2026-09-30 15:26:33 +00:00
1 parent 61e28a1459
commit 5be10251f4
17 files changed
+1435

No files matched your search

+1
View File
@@ -17,6 +17,7 @@ Every chart has a wiki page of its own — `https://git.jk-dev.de/jlab-cluster/h
| `apache-tika` | Apache Tika | Deployment + Service only (no ingress) |
| `bentopdf` | BentoPDF | Stateless Deployment + `config.json` ConfigMap, `subPath`-mounted into the web root and always rendered (empty renders as `{}`) |
| `calibre-web-automated` | Calibre-Web Automated | StatefulSet, extra consume-PVC |
| `convertx` | ConvertX | StatefulSet + PVC at `/app/data` (SQLite, uploads and converted output in one volume), no external database. **The only chart that overrides the image's `command` to bypass its entrypoint**: the image carries no `USER` and its entrypoint exists solely to drop to uid 1000 with `gosu`, so the chart starts the app directly and lets `runAsUser`/`fsGroup` do that work instead — the hardening baseline stays intact, at the price that any logic upstream later moves into that entrypoint is skipped silently (re-read it at every app-version update; the reasoning sits at the `command:` line). Pins uid 1000 rather than the repo's 20000, because the image owns `/home/convertx` with it and the uid owns PV data. `readOnlyRootFilesystem: true` with two emptyDirs, `/tmp` and `/home/convertx` — the latter is `HOME`, which the chart also sets explicitly, because LibreOffice, Calibre and fontconfig build their profiles there on the first conversion. Probes use `/healthcheck` (static 200, unauthenticated); `/` is never a 200, it 302s to `/setup` or `/login`. `JWT_SECRET` is a required `OnePasswordItem` — unset, the app mints a fresh one per process and every restart logs everyone out. The ingress `proxy-body-size` annotation is the **only** upload ceiling, since the app enforces none. ~1.4 GiB image, hence a raised `INSTALL_TIMEOUT` |
| `cyberchef` | CyberChef | Stateless Deployment |
| `freesailarr` | gluetun VPN media stack (qbittorrent, prowlarr, flaresolverr, radarr, recyclarr, sonarr, seerr, bazarr, and mediathekarr off by default) | Single-pod StatefulSet: all apps share gluetun's network namespace; config volumes via volumeClaimTemplates, media PVCs as existingClaims (optional per-category `subPath`, so one claim can serve all three categories). One ClusterIP Service per app (mediathekarr's carries both of its ports); no global `ingress.domain`. Ingress in two shapes that mix: one host per app (default, each app's host required), or one shared `ingress.host` with the apps as sub-paths (documented ingress exception below) |
| `gotenberg` | Gotenberg | Deployment + Service only (no ingress) |