freesailarr: MediathekArr as the tenth container, off by default (3.1.0+up1.0.0) #25

Merged
jklein merged 2 commits from feat/freesailarr-mediathekarr into main 2026-09-17 17:19:01 +02:00
Member

Uebernommen von GitHub-PR #246, das dort fertig und gruen war, aber nie gemergt wurde. Neu aufgesetzt auf dieser Instanz, damit freesailarr 3.1.0 genau einmal veroeffentlicht wird — von der Seite, die dafuer jetzt zustaendig ist. Der GitHub-PR wird geschlossen.

#123-Verweise im uebernommenen Text unten meinen die GitHub-Nummerierung, nicht die Nummern dieses Repos — dieselbe Einschraenkung wie bei den migrierten Issues. Betroffen sind unter anderem #245 (hier #20) und #158.

Was beim Uebernehmen zu tun war

Der Branch ist 37 Commits aelter als main und stammt von vor dem Umzug (#253 auf GitHub). Er wurde per Merge aktualisiert, nicht per Rebase: so bleibt der signierte Commit der urspruenglichen Arbeit unveraendert, und nur der Merge-Commit kommt neu dazu (ebenfalls signiert).

Zwei Dateien ueberschnitten sich, beide inhaltlich. Aufgeloest wurde zusammenfuehrend, nicht auswaehlend — beide Aussagen sind wahr: der Chart hat jetzt zehn Images, und das Repo liegt auf Gitea.

  • README.md — der einzige echte Konflikt. Dieser Branch schreibt die freesailarr-Zeile von neun auf zehn Upstreams um; auf main war daneben ein minecraft-server-Punkt entstanden, den dieser Branch nie hatte. Beide sind drin.
  • renovate.json5 — hat sauber automatisch gemergt, was nicht dasselbe ist wie richtig, also nachgelesen: die freesailarr-Ausnahme sagt jetzt „ten" und verweist auf den neuen mediathekarr-Block, waehrend die beiden Ergaenzungen direkt daneben unveraendert stehen (der Hinweis, dass der Post-Upgrade-Hook auf der gemeinsam genutzten Gitea-Instanz freigegeben ist, und die forge-neutrale Fassung der Automerge-Begruendung). Keine Formulierung, die der Umzug neutral gemacht hat, bringt „GitHub" zurueck.

Gegengeprueft nach dem Merge: renovate-config-validator sauber, helm lint --strict freesailarr ohne Befund, ci/run-quantity-render-test.sh 22 Charts / 0 failed. Der Netto-Unterschied zu main ist genau der Aenderungssatz aus #246 — dieselben 11 Dateien.


MediathekArr joins the freesailarr pod as the tenth container: a fake Usenet indexer for Prowlarr (Newznab, 5008) and a fake SABnzbd for Sonarr/Radarr (5007), fetching from the ARD and ZDF media libraries. Both consumers already share gluetun's network namespace and reach it over 127.0.0.1 — no service, no DNS, no ingress detour.

Minor bump, 3.0.5+up1.0.0 → 3.1.0+up1.0.0: mediathekarr.enabled defaults to false, the volumeClaimTemplate only appears with the flag, and the render with the flag off is byte-identical to 3.0.5 (verified against the previous chart for both the default values and the CI values).

The four points this could have failed on

1. The start gate on gluetun. MediathekArr fetches its ruleset once at start and never retries. Started before the tunnel it answers every Newznab search with 0 results — container up, Ready, nothing in the log. Kubernetes cannot order two regular containers, and an initContainer waiting for gluetun would deadlock, so the wait sits in front of the image entrypoint: poll gluetun's health server on 127.0.0.1:9999 (the same endpoint gluetun's own probes use, 200 only while the tunnel works), then exec /app/docker_start.sh. On timeout the container exits instead of starting blind — a crash loop is visible, a silently empty indexer is not. The startup-probe budget (300s) deliberately outlasts the gate (180s), otherwise the kubelet would kill the container while it is still waiting.

2. incomplete/ is not an emptyDir. Both incomplete/ and complete/ live on the shared downloads claim. An emptyDir counts against the container's ephemeral-storage limit, and one large download would make the kubelet evict the whole pod with all ten containers — the same trap documented in ytdlp-web-player.

3. The complete path Sonarr sees. The claim is mounted at /downloads, exactly as sonarr/radarr mount it, and the chart sets the path itself from one knob (mediathekarr.downloads.directory), so the SABnzbd complete_dir MediathekArr reports is the same absolute path on both sides. Verified against the running image: "complete_dir": "/downloads/mediathekarr/complete" and category dir /downloads/mediathekarr/complete/tv.

Note on DOWNLOAD_FOLDER_PATH_MAPPING — the issue has been corrected, please do not "add it back". That variable does not exist in the pinned beta image; it comes from upstream's docker-compose.yml for the 2024 latest (v0.2.5). The beta reads DOWNLOAD_COMPLETE_PATH / DOWNLOAD_INCOMPLETE_PATH (confirmed both in the binary's string table and at runtime: "Overriding complete path from environment variable"). The chart implements the requirement — the path Sonarr sees must match — with the variables this image actually reads.

4. Two ports, and the SAB API is not at /api. 5007 (SABnzbd role + UI, primary) and 5008 (Newznab role). Both are declared to gluetun's firewall and both live on one Service (a generic optional extraPorts in the portApps helper). Measured: 5008 /api?t=caps → 200, 5007 /download/ → 200, 5007 /download/api?mode=version… → 200, while 5007 /api answers with a misleading Newznab validation error.

What the install-test still has to show

The gate mechanism is proven below, in the real image with the rendered command and a shared network namespace. What is not proven and is exactly what this PR's install-test has to demonstrate:

  • that gluetun's real readiness timing satisfies the gate (the local proof used a stub with gluetun's health semantics — no Wireguard credentials and an amd64-only image on an arm64 machine), and
  • that the ten-container pod actually reaches Ready with the gate in front of the tenth.

mediathekarr is therefore enabled in ci/freesailarr/test-values.yaml, and INSTALL_TIMEOUT goes to 1200s (≈850 MB more image, plus a startup budget that only begins once the tunnel is up). Not re-verified either, because the issue reports it as measured: that an early start yields 0 results for eleven minutes.

Everything else

  • Image pinned by digest (beta@sha256:3ed7e14…): latest is a 2024 build and beta moves. Single-arch linux/amd64, noted in the chart. The built-in helm-values manager cannot follow that pair, so this chart gets its first customManagers block — verified by dry-run with two fixtures, because while the pin is current a matching rule and a broken one produce the identical empty result (#158). The same run shows helm-values skipping the image, so the two managers cannot open competing PRs.
  • Hardening as measured: non-root, uid/gid from puid/pgid, readOnlyRootFilesystem: true, all capabilities dropped, no PUID/PGID. /tmp is an emptyDir marked as insurance, not a measured requirement.
  • Probes on measured 200 endpoints — the app has no health endpoint at all. Readiness and startup on /download/ (5007, the port the Service and any ingress use); liveness is an exec covering both ports, because one container runs two processes and a wedged Newznab half is precisely what makes searches come back empty.
  • No ingress by default. MediathekArr's UI links its assets root-absolute (/download/css/…), so it can only be served at the root of a host — a sub-path stays broken even behind a prefix-stripping rewrite. On the shared domain the root belongs to seerr; a sub-path now fails the render (like seerr) instead of producing a broken UI, and a collision at / names the way out (give one of them its own host).
  • Documentation in values.yaml and NOTES.txt: the geo marker procedure instead of a table of series (584/584 correct), the escape route "German exit for the whole stack" and its price (it moves all ten containers, qBittorrent included, into that jurisdiction), the explicit note that "own production = free worldwide" is measurably wrong, and the fail_message: null trap including the fact that mkvmerge rejecting an HTML page is the only, accidental protection.
  • HLS stays an open question. The beta image does carry an ffmpeg-based M3U8 path (Starting M3U8 download / M3U8 download completed, ffmpeg present) — so the code exists, the outcome is unmeasured. Written that way, not as "HLS is probably supported".

Verified locally

  • Gate, phase by phase (real image, rendered command verbatim, shared netns, read-only root, uid 1000): while the health endpoint returned 503 → only the waiting line, no dotnet process, 5007/5008 refuse connections. Health flips → gluetun healthy after 58s → 10s settle → app starts, Ruleset update completed successfully, both ports 200. Timeout variant (15s) → did not report healthy within 15s, exit code 1, app never started.
  • Newznab search after the gate: t=search&q=tatort → 100 <item>s, t=tvsearch reports total="5294". The rendered liveness exec command exits 0 inside the container.
  • Inertness with the flag off: diff clean against the 3.0.5 render, twice.
  • Renovate dry-run (44.69.11, --platform=local --dry-run=lookup, real registry): real digest → updates: [] (the beta tag still points at it); a deliberately stale digest → exactly one digest update back to sha256:3ed7e14….
  • helm lint --strict clean, helm package ok, ci/run-quantity-render-test.sh 22 charts / 0 failed, and the full CI render accepted by a real API server (kubectl apply --dry-run=server in a throwaway k3d cluster).

Fixes #20

> **Uebernommen von GitHub-PR #246**, das dort fertig und gruen war, aber nie gemergt wurde. Neu aufgesetzt auf dieser Instanz, damit freesailarr 3.1.0 **genau einmal** veroeffentlicht wird — von der Seite, die dafuer jetzt zustaendig ist. Der GitHub-PR wird geschlossen. > > **`#123`-Verweise im uebernommenen Text unten meinen die GitHub-Nummerierung**, nicht die Nummern dieses Repos — dieselbe Einschraenkung wie bei den migrierten Issues. Betroffen sind unter anderem #245 (hier **#20**) und #158. ## Was beim Uebernehmen zu tun war Der Branch ist 37 Commits aelter als `main` und stammt von **vor** dem Umzug (#253 auf GitHub). Er wurde per **Merge** aktualisiert, nicht per Rebase: so bleibt der signierte Commit der urspruenglichen Arbeit unveraendert, und nur der Merge-Commit kommt neu dazu (ebenfalls signiert). Zwei Dateien ueberschnitten sich, beide inhaltlich. Aufgeloest wurde **zusammenfuehrend, nicht auswaehlend** — beide Aussagen sind wahr: der Chart hat jetzt zehn Images, *und* das Repo liegt auf Gitea. - **`README.md`** — der einzige echte Konflikt. Dieser Branch schreibt die freesailarr-Zeile von neun auf zehn Upstreams um; auf `main` war daneben ein minecraft-server-Punkt entstanden, den dieser Branch nie hatte. **Beide** sind drin. - **`renovate.json5`** — hat sauber automatisch gemergt, was nicht dasselbe ist wie richtig, also nachgelesen: die freesailarr-Ausnahme sagt jetzt „ten" und verweist auf den neuen mediathekarr-Block, waehrend die beiden Ergaenzungen direkt daneben unveraendert stehen (der Hinweis, dass der Post-Upgrade-Hook auf der gemeinsam genutzten Gitea-Instanz freigegeben ist, und die forge-neutrale Fassung der Automerge-Begruendung). **Keine Formulierung, die der Umzug neutral gemacht hat, bringt „GitHub" zurueck.** Gegengeprueft nach dem Merge: `renovate-config-validator` sauber, `helm lint --strict freesailarr` ohne Befund, `ci/run-quantity-render-test.sh` 22 Charts / 0 failed. Der Netto-Unterschied zu `main` ist genau der Aenderungssatz aus #246 — dieselben 11 Dateien. --- MediathekArr joins the freesailarr pod as the tenth container: a fake Usenet indexer for Prowlarr (Newznab, 5008) and a fake SABnzbd for Sonarr/Radarr (5007), fetching from the ARD and ZDF media libraries. Both consumers already share gluetun's network namespace and reach it over `127.0.0.1` — no service, no DNS, no ingress detour. **Minor bump, `3.0.5+up1.0.0` → `3.1.0+up1.0.0`**: `mediathekarr.enabled` defaults to `false`, the `volumeClaimTemplate` only appears with the flag, and the render with the flag off is **byte-identical** to 3.0.5 (verified against the previous chart for both the default values and the CI values). ## The four points this could have failed on **1. The start gate on gluetun.** MediathekArr fetches its ruleset once at start and never retries. Started before the tunnel it answers *every* Newznab search with 0 results — container up, Ready, nothing in the log. Kubernetes cannot order two regular containers, and an initContainer waiting for gluetun would deadlock, so the wait sits in front of the image entrypoint: poll gluetun's health server on `127.0.0.1:9999` (the same endpoint gluetun's own probes use, 200 only while the tunnel works), then `exec /app/docker_start.sh`. On timeout the container **exits** instead of starting blind — a crash loop is visible, a silently empty indexer is not. The startup-probe budget (300s) deliberately outlasts the gate (180s), otherwise the kubelet would kill the container while it is still waiting. **2. `incomplete/` is not an emptyDir.** Both `incomplete/` and `complete/` live on the shared downloads claim. An emptyDir counts against the container's ephemeral-storage limit, and one large download would make the kubelet evict the **whole pod** with all ten containers — the same trap documented in `ytdlp-web-player`. **3. The complete path Sonarr sees.** The claim is mounted at `/downloads`, exactly as sonarr/radarr mount it, and the chart sets the path itself from one knob (`mediathekarr.downloads.directory`), so the SABnzbd `complete_dir` MediathekArr reports is the same absolute path on both sides. Verified against the running image: `"complete_dir": "/downloads/mediathekarr/complete"` and category dir `/downloads/mediathekarr/complete/tv`. > **Note on `DOWNLOAD_FOLDER_PATH_MAPPING` — the issue has been corrected, please do not "add it back".** That variable does **not exist** in the pinned `beta` image; it comes from upstream's `docker-compose.yml` for the 2024 `latest` (v0.2.5). The `beta` reads `DOWNLOAD_COMPLETE_PATH` / `DOWNLOAD_INCOMPLETE_PATH` (confirmed both in the binary's string table and at runtime: *"Overriding complete path from environment variable"*). The chart implements the requirement — the path Sonarr sees must match — with the variables this image actually reads. **4. Two ports, and the SAB API is not at `/api`.** 5007 (SABnzbd role + UI, primary) and 5008 (Newznab role). Both are declared to gluetun's firewall and both live on **one** Service (a generic optional `extraPorts` in the `portApps` helper). Measured: `5008 /api?t=caps` → 200, `5007 /download/` → 200, `5007 /download/api?mode=version…` → 200, while `5007 /api` answers with a misleading Newznab validation error. ## What the install-test still has to show The gate **mechanism** is proven below, in the real image with the rendered command and a shared network namespace. What is **not** proven and is exactly what this PR's install-test has to demonstrate: - that gluetun's **real readiness timing** satisfies the gate (the local proof used a stub with gluetun's health semantics — no Wireguard credentials and an amd64-only image on an arm64 machine), and - that the **ten-container pod actually reaches Ready** with the gate in front of the tenth. `mediathekarr` is therefore enabled in `ci/freesailarr/test-values.yaml`, and `INSTALL_TIMEOUT` goes to 1200s (≈850 MB more image, plus a startup budget that only begins once the tunnel is up). Not re-verified either, because the issue reports it as measured: that an early start yields 0 results for eleven minutes. ## Everything else - **Image pinned by digest** (`beta@sha256:3ed7e14…`): `latest` is a 2024 build and `beta` moves. Single-arch `linux/amd64`, noted in the chart. The built-in `helm-values` manager cannot follow that pair, so this chart gets its **first `customManagers` block** — verified by dry-run with two fixtures, because while the pin is current a matching rule and a broken one produce the identical empty result (#158). The same run shows `helm-values` skipping the image, so the two managers cannot open competing PRs. - **Hardening as measured**: non-root, uid/gid from `puid`/`pgid`, `readOnlyRootFilesystem: true`, all capabilities dropped, no PUID/PGID. `/tmp` is an emptyDir marked as *insurance, not a measured requirement*. - **Probes** on measured 200 endpoints — the app has no health endpoint at all. Readiness and startup on `/download/` (5007, the port the Service and any ingress use); liveness is an `exec` covering **both** ports, because one container runs two processes and a wedged Newznab half is precisely what makes searches come back empty. - **No ingress by default.** MediathekArr's UI links its assets root-absolute (`/download/css/…`), so it can only be served at the **root of a host** — a sub-path stays broken even behind a prefix-stripping rewrite. On the shared domain the root belongs to seerr; a sub-path now fails the render (like seerr) instead of producing a broken UI, and a collision at `/` names the way out (give one of them its own host). - **Documentation** in `values.yaml` and `NOTES.txt`: the geo **marker procedure** instead of a table of series (584/584 correct), the escape route "German exit for the whole stack" **and its price** (it moves all ten containers, qBittorrent included, into that jurisdiction), the explicit note that "own production = free worldwide" is measurably wrong, and the **`fail_message: null` trap** including the fact that mkvmerge rejecting an HTML page is the only, accidental protection. - **HLS stays an open question.** The beta image does carry an ffmpeg-based M3U8 path (`Starting M3U8 download` / `M3U8 download completed`, ffmpeg present) — so the code exists, the outcome is unmeasured. Written that way, not as "HLS is probably supported". ## Verified locally - **Gate, phase by phase** (real image, rendered command verbatim, shared netns, read-only root, uid 1000): while the health endpoint returned 503 → only the waiting line, **no dotnet process, 5007/5008 refuse connections**. Health flips → `gluetun healthy after 58s` → 10s settle → app starts, `Ruleset update completed successfully`, both ports 200. Timeout variant (15s) → `did not report healthy within 15s`, **exit code 1**, app never started. - **Newznab search after the gate**: `t=search&q=tatort` → 100 `<item>`s, `t=tvsearch` reports `total="5294"`. The rendered liveness `exec` command exits 0 inside the container. - **Inertness with the flag off**: `diff` clean against the 3.0.5 render, twice. - **Renovate dry-run** (44.69.11, `--platform=local --dry-run=lookup`, real registry): real digest → `updates: []` (the beta tag still points at it); a deliberately stale digest → exactly one `digest` update back to `sha256:3ed7e14…`. - `helm lint --strict` clean, `helm package` ok, `ci/run-quantity-render-test.sh` 22 charts / 0 failed, and the full CI render **accepted by a real API server** (`kubectl apply --dry-run=server` in a throwaway k3d cluster). Fixes #20
jlab-bot added 2 commits 2026-09-17 11:41:31 +02:00
MediathekArr fakes a Usenet indexer towards Prowlarr and a SABnzbd downloader
towards Sonarr/Radarr and fetches from the ARD and ZDF media libraries. Both
consumers already share gluetun's network namespace, so they reach it over
127.0.0.1 - no service, no DNS, no ingress detour. Purely additive: the flag
defaults to false and a default render is byte-identical to 3.0.5 (verified
against the previous chart for both the default values and the CI values).

The four things that fail silently without them:

- A START GATE on gluetun. MediathekArr fetches its ruleset once at start and
  never retries; started before the tunnel it answers every Newznab search with
  0 results while the container is up, Ready and quiet. Kubernetes cannot order
  two regular containers and an initContainer would deadlock, so the wait sits
  in front of the image entrypoint: poll gluetun's health server on
  127.0.0.1:9999 and exec the entrypoint afterwards; on timeout the container
  exits rather than starting blind, because a crash loop is visible and an empty
  indexer is not. The startup probe budget (300s) deliberately outlasts the gate
  (180s).
- incomplete/ AND complete/ ON THE SHARED DOWNLOADS CLAIM, not in an emptyDir.
  An emptyDir counts against the container's ephemeral-storage limit, and one
  large download would have the kubelet evict the whole pod with all ten
  containers - the trap ytdlp-web-player documents.
- THE COMPLETE PATH SONARR SEES. The claim is mounted at /downloads exactly as
  sonarr/radarr mount it, and the chart sets DOWNLOAD_COMPLETE_PATH itself, so
  the SABnzbd complete_dir it reports is the same absolute path on both sides.
- TWO PORTS: 5007 (SABnzbd role and UI, primary) and 5008 (Newznab role for
  Prowlarr), both declared to gluetun's firewall and both on one Service. The
  SABnzbd API is at /download/api - /api on 5007 answers with a misleading
  Newznab validation error.

Further details:

- Image pinned by digest (tag + digest): "latest" is a 2024 build, "beta"
  moves. Single-arch linux/amd64. The built-in helm-values manager cannot
  follow that pair, so renovate.json5 gets the chart's first customManagers
  block; verified by dry-run with two fixtures, since a matching and a broken
  rule look identical while the pin is current.
- Hardened as measured: non-root, uid/gid from puid/pgid,
  readOnlyRootFilesystem: true, all capabilities dropped, no PUID/PGID.
- Probes on measured 200 endpoints (there is no health endpoint): readiness and
  startup on /download/, liveness as an exec covering BOTH ports, because one
  container runs two processes and a wedged Newznab half is exactly what makes
  searches come back empty.
- No ingress by default: the UI links its assets with root-absolute paths, so it
  can only be served at the root of a host; on the shared domain that root
  belongs to seerr. A sub-path fails the render instead of producing a broken
  UI.
- values.yaml and NOTES.txt document the geo MARKER PROCEDURE (not a table of
  series), the escape route of a German exit and its price for the whole stack,
  the fail_message: null trap, and the HLS question as an open question.
- CI: mediathekarr enabled in ci/freesailarr/test-values.yaml, install timeout
  raised to 1200s for the extra image and the gate.

Fixes #245
Merge main into the MediathekArr branch
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 4s
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Successful in 11s
Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request) Successful in 3m45s
ae621746a1
The branch predates the Gitea move (#253) by 37 commits and both sides
touched the same two files. Merged rather than rebased, so the signed
commit of the original work stays intact, and resolved by combining
rather than choosing - both statements are true at once: the chart has
ten images now, and the repo lives on Gitea.

README.md, the one real conflict: this branch rewrote the freesailarr
image bullet from nine upstreams to ten, while main had grown a
minecraft-server bullet next to it that this branch never had. Kept
both - our "ten" wording and main's new bullet.

renovate.json5 merged cleanly, but "cleanly" is not "correctly", so the
result was read rather than trusted: the freesailarr exception now says
ten and gained the pointer to the mediathekarr digest block, while the
two things main added right beside it survived unchanged - the note that
the post-upgrade hook is allow-listed on the shared Gitea instance, and
the forge-neutral wording of the automerge rule. Nothing from before the
move brought "GitHub" back into a sentence that had been made neutral.
jklein approved these changes 2026-09-17 17:18:41 +02:00
jklein merged commit afa1efb598 into main 2026-09-17 17:19:01 +02:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jlab-cluster/helm-charts#25