Merge pull request 'freesailarr: MediathekArr as the tenth container, off by default (3.1.0+up1.0.0)' (#25) from feat/freesailarr-mediathekarr into main
This commit was merged in pull request #25.
This commit is contained in:
commit
afa1efb598
11 files changed
+717
-49
No files matched your search
@@ -14,7 +14,7 @@ Collection of some self-made helm-charts. Each top-level directory is a standalo
|
|||||||
| `bentopdf` | BentoPDF | Stateless Deployment + `config.json` ConfigMap, `subPath`-mounted into the web root and always rendered (empty renders as `{}`) |
|
| `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 |
|
| `calibre-web-automated` | Calibre-Web Automated | StatefulSet, extra consume-PVC |
|
||||||
| `cyberchef` | CyberChef | Stateless Deployment |
|
| `cyberchef` | CyberChef | Stateless Deployment |
|
||||||
| `freesailarr` | gluetun VPN media stack (qbittorrent, prowlarr, flaresolverr, radarr, recyclarr, sonarr, seerr, bazarr) | 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; 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) |
|
| `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) |
|
| `gotenberg` | Gotenberg | Deployment + Service only (no ingress) |
|
||||||
| `homeassistant` | Home Assistant | StatefulSet; the only chart carrying Helm tests (`templates/tests/`) |
|
| `homeassistant` | Home Assistant | StatefulSet; the only chart carrying Helm tests (`templates/tests/`) |
|
||||||
| `matrix-synapse` | Matrix Synapse | StatefulSet plus an nginx delegation Deployment/Service |
|
| `matrix-synapse` | Matrix Synapse | StatefulSet plus an nginx delegation Deployment/Service |
|
||||||
@@ -149,15 +149,15 @@ What Renovate keeps current:
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Chart app versions (`appVersion:` in `Chart.yaml`) | One `customManagers` entry per chart, hard-coding the image repository |
|
| Chart app versions (`appVersion:` in `Chart.yaml`) | One `customManagers` entry per chart, hard-coding the image repository |
|
||||||
| Sidecar/init images in `values.yaml` (nginx, guacd, busybox, alpine) | Built-in `helm-values` manager |
|
| Sidecar/init images in `values.yaml` (nginx, guacd, busybox, alpine) | Built-in `helm-values` manager |
|
||||||
| **All** images of `freesailarr`, including its main one | Built-in `helm-values` manager — the chart pins every tag in `values.yaml` and has no app-version block |
|
| **All** images of `freesailarr`, including its main one | Built-in `helm-values` manager — the chart pins every tag in `values.yaml` and has no app-version block. One exception: `mediathekarr` is pinned by **digest** (its tags carry no version) and has its own `customManagers` block |
|
||||||
| GitHub Actions | Built-in `github-actions` manager |
|
| GitHub Actions | Built-in `github-actions` manager |
|
||||||
| Tool versions pinned as workflow env vars (`TRIVY_VERSION`) | `# renovate: datasource=… depName=…` comment above the line |
|
| Tool versions pinned as workflow env vars (`TRIVY_VERSION`) | `# renovate: datasource=… depName=…` comment above the line |
|
||||||
|
|
||||||
Each chart renders its main image as `<repo>:{{ .Chart.AppVersion }}` — repository in the template, version in `Chart.yaml`. Renovate cannot join two files, so the mapping lives in `renovate.json5`, one block per chart. It is deliberately *not* a `# renovate:` comment inside each `Chart.yaml`: that would touch all chart directories and force a version bump plus a publish per chart for a pure comment change. Charts whose tags carry a flavour or date suffix (`apache-tika`, `minecraft-server`, `rss-bridge`, `searxng`, `teamspeak-server`) override `versioning` in their block; the reasoning sits next to each one.
|
Each chart renders its main image as `<repo>:{{ .Chart.AppVersion }}` — repository in the template, version in `Chart.yaml`. Renovate cannot join two files, so the mapping lives in `renovate.json5`, one block per chart. It is deliberately *not* a `# renovate:` comment inside each `Chart.yaml`: that would touch all chart directories and force a version bump plus a publish per chart for a pure comment change. Charts whose tags carry a flavour or date suffix (`apache-tika`, `minecraft-server`, `rss-bridge`, `searxng`, `teamspeak-server`) override `versioning` in their block; the reasoning sits next to each one.
|
||||||
|
|
||||||
One rule those overrides share, learned from `searxng` and worth knowing before writing the next one: **a capture group is a claim about how the value is compared.** Renovate's regex versioning parses `major`, `minor`, `patch`, `build` and `revision` with `Number.parseInt` (`modules/versioning/regex/index.js`, `_parse`), so a non-numeric commit hash captured as `build` becomes whatever digits lead it — `9fea41204` is `9`, `777ba8fa4` is `777`, and a hash starting with a letter is `NaN`. Measured on renovate 41.173.1 and 44.48.0, that makes Renovate offer an image pushed *earlier the same day* as an update to a newer one, unattended under the patch/minor automerge rule. Parts of a tag that carry no ordering belong **outside** every group: the anchored pattern still requires them to be there, they simply take no part in the comparison. `prerelease` is not the way out either — it sorts correctly but marks every tag `isStable === false`, which hands the whole chart to `ignoreUnstable`. `freesailarr` has no block on purpose — see the images exception above; a block would either duplicate the `helm-values` updates or rewrite an `appVersion` that selects no image.
|
One rule those overrides share, learned from `searxng` and worth knowing before writing the next one: **a capture group is a claim about how the value is compared.** Renovate's regex versioning parses `major`, `minor`, `patch`, `build` and `revision` with `Number.parseInt` (`modules/versioning/regex/index.js`, `_parse`), so a non-numeric commit hash captured as `build` becomes whatever digits lead it — `9fea41204` is `9`, `777ba8fa4` is `777`, and a hash starting with a letter is `NaN`. Measured on renovate 41.173.1 and 44.48.0, that makes Renovate offer an image pushed *earlier the same day* as an update to a newer one, unattended under the patch/minor automerge rule. Parts of a tag that carry no ordering belong **outside** every group: the anchored pattern still requires them to be there, they simply take no part in the comparison. `prerelease` is not the way out either — it sorts correctly but marks every tag `isStable === false`, which hands the whole chart to `ignoreUnstable`. `freesailarr` has no **app-version** block on purpose — see the images exception above; a block would either duplicate the `helm-values` updates or rewrite an `appVersion` that selects no image. It does carry one `customManagers` block, for `mediathekarr`: that image is pinned by digest because its tags carry no version (`latest` is a 2024 build, `beta` moves), and the built-in `helm-values` manager reads only `repository`/`tag` and skips it (`skipReason: invalid-value`), so the two cannot collide. Digest rules need the same guard as versioning rules and for a sharper reason: while the pin is current, a matching rule and a broken one produce the identical empty result. Verified with two dry-run fixtures — the real digest yields no update, a deliberately stale one yields exactly one `digest` update back to the pinned digest.
|
||||||
|
|
||||||
A consequence of pinning every tag in `values.yaml`: `ci/renovate-bump-chart.sh` sees an unchanged `appVersion` and bumps the chart **patch**. That holds for all nine images alike — none of them is special — and it is the intended outcome, since `appVersion` here is the chart's own marker rather than a tracked upstream version.
|
A consequence of pinning every tag in `values.yaml`: `ci/renovate-bump-chart.sh` sees an unchanged `appVersion` and bumps the chart **patch**. That holds for all ten images alike, the digest-pinned one included — none of them is special — and it is the intended outcome, since `appVersion` here is the chart's own marker rather than a tracked upstream version.
|
||||||
|
|
||||||
Two dependencies are constrained beyond their versioning. **nginx** (the Synapse delegation sidecar) is limited to **even minors** via `allowedVersions`. nginx numbers its release branches by minor — even is stable, odd is mainline — and the chart belongs on stable. Patch updates inside the stable branch still flow through normally. The constraint tolerates a variant suffix (`1.30.4-alpine`) on purpose: docker versioning only ever offers candidates carrying the current tag's suffix, so a suffix-blind constraint would silently freeze nginx the day the sidecar moves to a slimmer base image — a switch to `1.30.4-alpine` that #84 has commissioned a check on — with no error and no warning.
|
Two dependencies are constrained beyond their versioning. **nginx** (the Synapse delegation sidecar) is limited to **even minors** via `allowedVersions`. nginx numbers its release branches by minor — even is stable, odd is mainline — and the chart belongs on stable. Patch updates inside the stable branch still flow through normally. The constraint tolerates a variant suffix (`1.30.4-alpine`) on purpose: docker versioning only ever offers candidates carrying the current tag's suffix, so a suffix-blind constraint would silently freeze nginx the day the sidecar moves to a slimmer base image — a switch to `1.30.4-alpine` that #84 has commissioned a check on — with no error and no warning.
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ These guidelines are **binding for every current and future chart** in this repo
|
|||||||
### 3. Images
|
### 3. Images
|
||||||
|
|
||||||
- The main app image tag comes from `{{ .Chart.AppVersion }}` — never hardcoded.
|
- The main app image tag comes from `{{ .Chart.AppVersion }}` — never hardcoded.
|
||||||
- **Documented exception — freesailarr**: the chart bundles nine independently versioned upstreams, so none of them is "the" app version. **No** tag comes from `appVersion`; all nine images are pinned as `<app>.image.repository` / `<app>.image.tag` in `values.yaml`, which is also what puts every one of them under the built-in `helm-values` manager. Its `appVersion` is the chart's own version marker, freely chosen and tied to no upstream — raised by hand when a new edition of the chart is marked as a whole. Consequently the chart has **no** block in `renovate.json5` and is **not** in the `change-app-version.yml` choice list.
|
- **Documented exception — freesailarr**: the chart bundles ten independently versioned upstreams (nine of them enabled by default), so none of them is "the" app version. **No** tag comes from `appVersion`; every image is pinned as `<app>.image.repository` / `<app>.image.tag` in `values.yaml`, which is also what puts them under the built-in `helm-values` manager. Its `appVersion` is the chart's own version marker, freely chosen and tied to no upstream — raised by hand when a new edition of the chart is marked as a whole. Consequently the chart has **no app-version** block in `renovate.json5` and is **not** in the `change-app-version.yml` choice list. `mediathekarr` adds a second deviation on top: its tags carry no version at all, so it is pinned by `image.digest` as well and is the one image in this chart with a `customManagers` block — a `repository`/`tag` pair alone would leave it without any update path.
|
||||||
- **Documented exception — minecraft-server**: the only chart whose main image tag can be overridden, through `image.tagOverride` (#158, empty by default — the tag still comes from `appVersion`). `appVersion` is the tag and therefore fixes the JVM, and since chart 5.0.0 only the **newest** Java line is maintained out of this one directory; the older lines stay published as frozen chart versions (java8 and java17 at 4.0.0, java21 at 4.1.0) and never receive another change. Without the override, running an older JVM would mean installing a frozen chart and giving up every later fix — so the line is a decision at rollout time rather than a property of the chart. The override is named `tagOverride`, not `tag`, because it decouples the running image from the chart version, and it is deliberately outside Renovate's reach (`renovate.json5` tracks the `appVersion` in `Chart.yaml` and nothing else). The chart checks the pairing itself: the Java line of the **effective** tag against `minecraft.version`, failing the render with both values named — a mismatch used to install silently and only break the container afterwards.
|
- **Documented exception — minecraft-server**: the only chart whose main image tag can be overridden, through `image.tagOverride` (#158, empty by default — the tag still comes from `appVersion`). `appVersion` is the tag and therefore fixes the JVM, and since chart 5.0.0 only the **newest** Java line is maintained out of this one directory; the older lines stay published as frozen chart versions (java8 and java17 at 4.0.0, java21 at 4.1.0) and never receive another change. Without the override, running an older JVM would mean installing a frozen chart and giving up every later fix — so the line is a decision at rollout time rather than a property of the chart. The override is named `tagOverride`, not `tag`, because it decouples the running image from the chart version, and it is deliberately outside Renovate's reach (`renovate.json5` tracks the `appVersion` in `Chart.yaml` and nothing else). The chart checks the pairing itself: the Java line of the **effective** tag against `minecraft.version`, failing the render with both values named — a mismatch used to install silently and only break the container afterwards.
|
||||||
- Auxiliary images (sidecars, init containers, secondary workloads — e.g. the delegation nginx in matrix-synapse, guacd in patchmon, busybox init/consume images in paperless-ngx) are pinned via values instead of being hardcoded in templates, and their pinned versions are reviewed alongside every app-version update round.
|
- Auxiliary images (sidecars, init containers, secondary workloads — e.g. the delegation nginx in matrix-synapse, guacd in patchmon, busybox init/consume images in paperless-ngx) are pinned via values instead of being hardcoded in templates, and their pinned versions are reviewed alongside every app-version update round.
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ These guidelines are **binding for every current and future chart** in this repo
|
|||||||
- The TLS secret is named `tls-<release>-<chart>-ingress`.
|
- The TLS secret is named `tls-<release>-<chart>-ingress`.
|
||||||
- **Documented exception — homeassistant**: keeps its upstream-style flexible ingress values (host/tls lists, no required values); only its `ingress.className` default follows the standard (`nginx`, clearable to omit the field).
|
- **Documented exception — homeassistant**: keeps its upstream-style flexible ingress values (host/tls lists, no required values); only its `ingress.className` default follows the standard (`nginx`, clearable to omit the field).
|
||||||
- **A shared domain with sub-paths is the second admissible ingress shape.** Where a chart exposes several apps, they may either each take a host of their own or share one host and be told apart by path (`domain.tld/sonarr`, `domain.tld/radarr`, …) — one DNS name and one certificate for the whole bundle. The rules for the shared shape: the domain is a single value (`ingress.host`), each app's path is its own value defaulting to `/<app>`, the paths render in one Ingress object `<release>-<chart>-ingress` with the standard TLS secret `tls-<release>-<chart>-ingress`, and an app that carries a host of its own always wins and keeps its own object — so both shapes mix without extra values. An app that needs a per-path proxy annotation (`rewrite-target` is per **object**, not per path) gets its own object on the same host, repeating the same TLS secret name and **omitting** the cert-manager annotation, so that exactly one certificate is requested. Apps that cannot serve a base path must be rendered at `/` or refused with a `fail` — never rendered somewhere they provably cannot work.
|
- **A shared domain with sub-paths is the second admissible ingress shape.** Where a chart exposes several apps, they may either each take a host of their own or share one host and be told apart by path (`domain.tld/sonarr`, `domain.tld/radarr`, …) — one DNS name and one certificate for the whole bundle. The rules for the shared shape: the domain is a single value (`ingress.host`), each app's path is its own value defaulting to `/<app>`, the paths render in one Ingress object `<release>-<chart>-ingress` with the standard TLS secret `tls-<release>-<chart>-ingress`, and an app that carries a host of its own always wins and keeps its own object — so both shapes mix without extra values. An app that needs a per-path proxy annotation (`rewrite-target` is per **object**, not per path) gets its own object on the same host, repeating the same TLS secret name and **omitting** the cert-manager annotation, so that exactly one certificate is requested. Apps that cannot serve a base path must be rendered at `/` or refused with a `fail` — never rendered somewhere they provably cannot work.
|
||||||
- **Documented exception — freesailarr**: the chart bundles several independently exposed apps in one pod, so it has **no** `ingress.domain`. It offers both shapes. By default each app carries its own `<app>.ingress.enabled` plus a required `<app>.ingress.host` (required only while that app's ingress is enabled and it is not on the shared domain) and renders its own Ingress object `<release>-<chart>-<app>-ingress` with its own TLS secret `tls-<release>-<chart>-<app>-ingress`. Setting the global `ingress.host` switches every app without a host of its own to the shared-domain shape under `<app>.ingress.path`; qBittorrent (no base path, needs a prefix-stripping rewrite) gets the extra object described above, and seerr is pinned to `/`. `clusterIssuer`, `className` and `annotations` stay global defaults under `ingress.` and are overridable per app.
|
- **Documented exception — freesailarr**: the chart bundles several independently exposed apps in one pod, so it has **no** `ingress.domain`. It offers both shapes. By default each app carries its own `<app>.ingress.enabled` plus a required `<app>.ingress.host` (required only while that app's ingress is enabled and it is not on the shared domain) and renders its own Ingress object `<release>-<chart>-<app>-ingress` with its own TLS secret `tls-<release>-<chart>-<app>-ingress`. Setting the global `ingress.host` switches every app without a host of its own to the shared-domain shape under `<app>.ingress.path`; qBittorrent (no base path, needs a prefix-stripping rewrite) gets the extra object described above, and seerr as well as mediathekarr are pinned to `/` — mediathekarr because its UI links assets with root-absolute paths, which a rewrite cannot fix; with both of them on one shared host the duplicate-path guard fires and one of them needs a host of its own (its ingress is therefore off by default). `clusterIssuer`, `className` and `annotations` stay global defaults under `ingress.` and are overridable per app.
|
||||||
|
|
||||||
### 5. Security hardening
|
### 5. Security hardening
|
||||||
|
|
||||||
@@ -351,7 +351,7 @@ Reference implementation: `template-chart/values.schema.json` and `template-char
|
|||||||
|
|
||||||
### 13. Everything else
|
### 13. Everything else
|
||||||
|
|
||||||
- `icon:` is mandatory in every `Chart.yaml`. **Documented exception — freesailarr, the only chart without one**: the rule everywhere else is "the icon is this chart's one upstream's logo", and a chart bundling nine independently versioned projects has no such upstream — picking one of the nine would imply a "main app" the stack deliberately does not have. That is the whole criterion, and it applies to no other chart in the repo: a single-upstream chart without an icon is a missing icon, not an exception. The omission costs an INFO line in `helm lint --strict` (`icon is recommended`), which still exits 0.
|
- `icon:` is mandatory in every `Chart.yaml`. **Documented exception — freesailarr, the only chart without one**: the rule everywhere else is "the icon is this chart's one upstream's logo", and a chart bundling ten independently versioned projects has no such upstream — picking one of the ten would imply a "main app" the stack deliberately does not have. That is the whole criterion, and it applies to no other chart in the repo: a single-upstream chart without an icon is a missing icon, not an exception. The omission costs an INFO line in `helm lint --strict` (`icon is recommended`), which still exits 0.
|
||||||
- `helm lint --strict` must pass without findings.
|
- `helm lint --strict` must pass without findings.
|
||||||
- **Every chart ships a `templates/NOTES.txt`** (#43), `template-chart` included — so this is a requirement for a new chart, not a nice-to-have it can defer. No list of names here on purpose: a count goes stale the moment the next chart lands, which is exactly how the previous wording came to claim a number it never matched. Reference implementation: `template-chart/templates/NOTES.txt`, a placeholder-carrying template rather than finished prose, so a new chart adapts it instead of starting from an empty file. The bar it sets: print what an operator would otherwise have to look up in the templates (the rendered access URL, **which fields the 1Password item must carry**, the options actually active in this release, and the chart's known traps), conditional on the values really set. A `NOTES.txt` that only repeats the values is skimmed once and never read again. **No real domain anywhere** — the URL renders from `ingress.domain`, and where an example is unavoidable it is `example.com`. A `NOTES.txt` renders no Kubernetes object, so writing or reworking one is purely additive: a minor bump.
|
- **Every chart ships a `templates/NOTES.txt`** (#43), `template-chart` included — so this is a requirement for a new chart, not a nice-to-have it can defer. No list of names here on purpose: a count goes stale the moment the next chart lands, which is exactly how the previous wording came to claim a number it never matched. Reference implementation: `template-chart/templates/NOTES.txt`, a placeholder-carrying template rather than finished prose, so a new chart adapts it instead of starting from an empty file. The bar it sets: print what an operator would otherwise have to look up in the templates (the rendered access URL, **which fields the 1Password item must carry**, the options actually active in this release, and the chart's known traps), conditional on the values really set. A `NOTES.txt` that only repeats the values is skimmed once and never read again. **No real domain anywhere** — the URL renders from `ingress.domain`, and where an example is unavoidable it is `example.com`. A `NOTES.txt` renders no Kubernetes object, so writing or reworking one is purely additive: a minor bump.
|
||||||
- CI: every installable chart has fixtures under `ci/<chart>/` (`test-values.yaml`, optionally `fixtures.yaml`/`fixtures.env.yaml`/`settings.env`) or a justified entry in `ci/excluded-charts.txt` — the install-test fails if both are missing.
|
- CI: every installable chart has fixtures under `ci/<chart>/` (`test-values.yaml`, optionally `fixtures.yaml`/`fixtures.env.yaml`/`settings.env`) or a justified entry in `ci/excluded-charts.txt` — the install-test fails if both are missing.
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
# Nine containers in one pod, several GB of images to pull, and the app
|
# Ten containers in one pod, several GB of images to pull, and the app
|
||||||
# containers only start once gluetun has completed its Wireguard handshake -
|
# containers only start once gluetun has completed its Wireguard handshake -
|
||||||
# each of them then runs its own startup probe (150s budget, 300s for seerr's
|
# each of them then runs its own startup probe (150s budget, 300s for seerr's
|
||||||
# first-boot migrations). The default of 300s is not enough for that.
|
# first-boot migrations). mediathekarr adds ~850 MB of image and waits for
|
||||||
INSTALL_TIMEOUT=900
|
# gluetun's health server BEFORE its process starts (its own start gate), so its
|
||||||
|
# 300s startup budget only begins once the tunnel is up. The default of 300s is
|
||||||
|
# not enough for any of that.
|
||||||
|
INSTALL_TIMEOUT=1200
|
||||||
|
|
||||||
# The gluetun credentials for ci/freesailarr/fixtures.env.yaml. Missing or empty
|
# The gluetun credentials for ci/freesailarr/fixtures.env.yaml. Missing or empty
|
||||||
# values fail the install-test loudly instead of skipping it - see
|
# values fail the install-test loudly instead of skipping it - see
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# freesailarr: the whole stack in one pod - gluetun plus eight app containers
|
# freesailarr: the whole stack in one pod - gluetun plus nine app containers
|
||||||
# that share its network namespace and sit behind its kill switch. Nothing here
|
# that share its network namespace and sit behind its kill switch. Nothing here
|
||||||
# is a shortcut around that: the install-test brings up a real Wireguard tunnel
|
# is a shortcut around that: the install-test brings up a real Wireguard tunnel
|
||||||
# with a CI-only Mullvad key (see ci/freesailarr/fixtures.env.yaml), because a
|
# with a CI-only Mullvad key (see ci/freesailarr/fixtures.env.yaml), because a
|
||||||
@@ -37,3 +37,11 @@ ingress:
|
|||||||
# in the CI cluster, and the install-test checks readiness, not routing.
|
# in the CI cluster, and the install-test checks readiness, not routing.
|
||||||
host: freesailarr.example.com
|
host: freesailarr.example.com
|
||||||
clusterIssuer: ci
|
clusterIssuer: ci
|
||||||
|
|
||||||
|
mediathekarr:
|
||||||
|
# On in CI although it is off by default, so the install-test actually covers
|
||||||
|
# the tenth container: its digest-pinned image (single-arch amd64, ~850 MB),
|
||||||
|
# its start gate against gluetun's health server, and its probes on the two
|
||||||
|
# ports. Its ingress stays off - MediathekArr can only be served at the root
|
||||||
|
# of a host, and the root of the shared host above belongs to seerr.
|
||||||
|
enabled: true
|
||||||
+12
-10
@@ -1,23 +1,25 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
name: freesailarr
|
name: freesailarr
|
||||||
description: A Helm chart to deploy a Wireguard-tunneled media automation stack on Kubernetes - a VPN gateway with indexer, downloader, library managers, subtitle fetcher and request front end in one pod, all reaching the internet only through the tunnel
|
description: A Helm chart to deploy a Wireguard-tunneled media automation stack on Kubernetes - a VPN gateway with indexer, downloader, library managers, subtitle fetcher, request front end and an optional public-broadcaster indexer in one pod, all reaching the internet only through the tunnel
|
||||||
# No icon on purpose: the stack has no single upstream, and every other chart's
|
# No icon on purpose: the stack has no single upstream, and every other chart's
|
||||||
# icon is its one project's logo. Picking any of the nine here would suggest
|
# icon is its one project's logo. Picking any of the ten here would suggest
|
||||||
# the same false "main app" the versioning deliberately avoids. The field is
|
# the same false "main app" the versioning deliberately avoids. The field is
|
||||||
# optional, so it is omitted rather than filled with an arbitrary choice.
|
# optional, so it is omitted rather than filled with an arbitrary choice.
|
||||||
|
|
||||||
type: application
|
type: application
|
||||||
|
|
||||||
version: 3.0.5+up1.0.0
|
version: 3.1.0+up1.0.0
|
||||||
# The chart's own version marker, freely chosen and tied to no upstream. The
|
# The chart's own version marker, freely chosen and tied to no upstream. The
|
||||||
# stack bundles nine independently versioned projects, so there is no single
|
# stack bundles ten independently versioned projects (nine of them on by
|
||||||
# "app version" to track here.
|
# default), so there is no single "app version" to track here.
|
||||||
#
|
#
|
||||||
# It is NOT the source of any image tag: all nine images are pinned in
|
# It is NOT the source of any image tag: all ten images are pinned in
|
||||||
# values.yaml, where Renovate's helm-values manager proposes them. Nothing
|
# values.yaml, where Renovate's helm-values manager proposes them - except
|
||||||
# rewrites this field automatically - the chart has no app-version block in
|
# mediathekarr, which is pinned by digest and has its own customManagers block
|
||||||
# renovate.json5 and is not in the change-app-version.yml choice list. Raise it
|
# in renovate.json5, because its tags carry no version. Nothing rewrites this
|
||||||
# by hand when you want to mark a new edition of the chart as a whole.
|
# field automatically - the chart has no app-version block in renovate.json5
|
||||||
|
# and is not in the change-app-version.yml choice list. Raise it by hand when
|
||||||
|
# you want to mark a new edition of the chart as a whole.
|
||||||
appVersion: "1.0.0"
|
appVersion: "1.0.0"
|
||||||
|
|
||||||
maintainers:
|
maintainers:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{{- $sharedNames := list -}}
|
{{- $sharedNames := list -}}
|
||||||
{{- range $shared }}{{ $sharedNames = append $sharedNames .name }}{{ end -}}
|
{{- range $shared }}{{ $sharedNames = append $sharedNames .name }}{{ end -}}
|
||||||
freesailarr — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
freesailarr — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
||||||
Nine containers in ONE pod, sharing gluetun's network namespace.
|
{{ if .Values.mediathekarr.enabled }}Ten{{ else }}Nine{{ end }} containers in ONE pod, sharing gluetun's network namespace.
|
||||||
|
|
||||||
NOTHING IS READY UNTIL THE TUNNEL IS UP
|
NOTHING IS READY UNTIL THE TUNNEL IS UP
|
||||||
gluetun's readiness probe is its health server, which answers 200 only while
|
gluetun's readiness probe is its health server, which answers 200 only while
|
||||||
@@ -134,6 +134,101 @@ QBITTORRENT HAS NO SAVE PATH SET (qbittorrent.savePath is empty)
|
|||||||
WebUI before starting any download.
|
WebUI before starting any download.
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- if .Values.mediathekarr.enabled }}
|
||||||
|
{{- $dir := .Values.mediathekarr.downloads.directory }}
|
||||||
|
{{- $gate := .Values.mediathekarr.startGate }}
|
||||||
|
{{- $mediaStartup := fromYaml (include "freesailarr.probeFor" (dict "root" $ "app" "mediathekarr" "kind" "startupProbe" "given" .Values.mediathekarr.startupProbe)) }}
|
||||||
|
|
||||||
|
MEDIATHEKARR: TWO PORTS, AND THE SABNZBD API IS NOT AT /api
|
||||||
|
Every container shares one network namespace, so both consumers reach it over
|
||||||
|
127.0.0.1 — no service name, no DNS, no ingress:
|
||||||
|
|
||||||
|
Prowlarr -> Newznab indexer, http://127.0.0.1:5008
|
||||||
|
Sonarr/Radarr -> SABnzbd host 127.0.0.1, port 5007, API path /download/api
|
||||||
|
|
||||||
|
A request to /api on 5007 answers with a Newznab validation error and sends
|
||||||
|
you looking at the wrong half of the app — the upstream documentation only
|
||||||
|
mentions port 5007 at all.
|
||||||
|
|
||||||
|
Sonarr and Radarr must import from
|
||||||
|
|
||||||
|
/downloads/{{ $dir }}/complete (one sub-directory per category below it)
|
||||||
|
|
||||||
|
which is exactly the path the chart hands MediathekArr and which it reports
|
||||||
|
back as the SABnzbd complete_dir. Any deviation — a remote path mapping, a
|
||||||
|
different mount — imports NOTHING, without an error.
|
||||||
|
|
||||||
|
The UI (setup wizard and download queue) has no ingress{{ if and .Values.mediathekarr.ingress.enabled .Values.mediathekarr.ingress.host }}, other than the one
|
||||||
|
you gave it on {{ .Values.mediathekarr.ingress.host }}{{ end }}:
|
||||||
|
|
||||||
|
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ $name }}-mediathekarr-service 5007:5007
|
||||||
|
{{- if $gate.enabled }}
|
||||||
|
|
||||||
|
IT DELIBERATELY STARTS LATE, AND THAT IS THE POINT
|
||||||
|
The container waits (up to {{ $gate.timeoutSeconds }}s) for gluetun to report healthy before
|
||||||
|
it starts MediathekArr{{ if gt (int $gate.settleSeconds) 0 }}, plus {{ $gate.settleSeconds }}s of settling time{{ end }}. Started before the tunnel,
|
||||||
|
MediathekArr fails its one ruleset fetch, never retries it, and then answers
|
||||||
|
EVERY search with 0 results — container up, Ready, nothing in the log. That
|
||||||
|
is why a green pod proves nothing here and why the gate exits instead of
|
||||||
|
starting anyway on timeout: a crash-looping container is visible, a silently
|
||||||
|
empty indexer is not.
|
||||||
|
|
||||||
|
kubectl -n {{ .Release.Namespace }} logs {{ $name }}-sfs-0 -c mediathekarr | head -3
|
||||||
|
|
||||||
|
The startup probe has to outlast the gate ({{ mul (int $mediaStartup.periodSeconds) (int $mediaStartup.failureThreshold) }}s budget against a {{ $gate.timeoutSeconds }}s gate),
|
||||||
|
otherwise the kubelet kills the container while it is still waiting.
|
||||||
|
{{- else }}
|
||||||
|
|
||||||
|
THE START GATE IS OFF (mediathekarr.startGate.enabled: false)
|
||||||
|
MediathekArr may then start before the Wireguard tunnel is up. Its ruleset
|
||||||
|
fetch fails once, is never retried, and the indexer answers every search
|
||||||
|
with 0 results afterwards — measured over eleven minutes, with the container
|
||||||
|
up, Ready and no error anywhere. Nothing reports this state. Check a search
|
||||||
|
by hand after every restart, or switch the gate back on.
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
A FAILED MEDIATHEKARR DOWNLOAD NEVER SAYS WHY
|
||||||
|
MediathekArr does not check the HTTP status code. On a geo-blocked fetch it
|
||||||
|
writes the ~476-byte HTML error page out as .mp4 and logs "Download
|
||||||
|
completed"; only mkvmerge then refuses the file and the status flips to
|
||||||
|
Failed — with fail_message: null. Measured A/B with one identical NZB:
|
||||||
|
direct 94,235,525 bytes and Completed, through a foreign exit 0 bytes and
|
||||||
|
Failed, fail_message null in BOTH cases.
|
||||||
|
|
||||||
|
So a geo block is indistinguishable from a broken server, an expired URL or a
|
||||||
|
full disk, in Sonarr and in the container log alike. And the protection is an
|
||||||
|
accident: it rests entirely on mkvmerge rejecting an HTML page. Anything a
|
||||||
|
geo endpoint returns that mkvmerge accepts is reported as Completed and walks
|
||||||
|
into your library.
|
||||||
|
|
||||||
|
WHETHER A SHOW WORKS THROUGH YOUR EXIT: READ THE MARKER, NOT A LIST
|
||||||
|
The tunnel currently exits {{ with .Values.gluetun.vpn.serverCities }}in {{ . }}{{ else }}{{ with .Values.gluetun.vpn.serverCountries }}in {{ . }}{{ else }}wherever gluetun picks{{ end }}{{ end }}. ARD and ZDF block parts of their
|
||||||
|
catalogue outside Germany, and every container hangs in gluetun's namespace —
|
||||||
|
there is no per-container exit.
|
||||||
|
|
||||||
|
A table of series goes stale; the marker in the media URL does not. It
|
||||||
|
predicted the outcome in 584 of 584 measured cases:
|
||||||
|
|
||||||
|
/de/, /geo/de/, /deutschlandweit/, geoavailability_D -> blocked outside DE
|
||||||
|
/weltweit/ -> open
|
||||||
|
/none/ (ZDF) -> open
|
||||||
|
/dach/, deChAt (WDR variant) -> open
|
||||||
|
|
||||||
|
The rule of thumb "own production = available worldwide" is MEASURABLY WRONG
|
||||||
|
(an ARD own production was blocked, a licensed series was open). Only the
|
||||||
|
marker counts.
|
||||||
|
|
||||||
|
The escape route is one value — gluetun.vpn.serverCountries/serverCities set
|
||||||
|
to a German exit — and its price is that it moves the WHOLE stack, all ten
|
||||||
|
containers, qBittorrent included, into that jurisdiction and onto those
|
||||||
|
servers. That is a deployment decision, not a chart setting to flip lightly.
|
||||||
|
|
||||||
|
OPEN, EXPLICITLY UNMEASURED: HLS ENTRIES
|
||||||
|
Part of the catalogue is served as an HLS playlist instead of a progressive
|
||||||
|
MP4. The image carries an ffmpeg-based M3U8 path, but whether such an entry
|
||||||
|
completes end to end has not been measured. If it does not, those entries fail
|
||||||
|
regardless of any geo block — and, per the paragraph above, without a reason.
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
EVERY SERVICE POINTS AT THE SAME POD IP
|
EVERY SERVICE POINTS AT THE SAME POD IP
|
||||||
One ClusterIP service per app{{ if .Values.recyclarr.enabled }} (recyclarr has none — no port in daemon mode){{ end }}, but all
|
One ClusterIP service per app{{ if .Values.recyclarr.enabled }} (recyclarr has none — no port in daemon mode){{ end }}, but all
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ implementation lives in template-chart.
|
|||||||
{{/*
|
{{/*
|
||||||
Render additive env entries (the repo-wide <app>.env convention: tpl-rendered,
|
Render additive env entries (the repo-wide <app>.env convention: tpl-rendered,
|
||||||
supporting the value, secretKeyRef and configMapKeyRef forms). Shared as a
|
supporting the value, secretKeyRef and configMapKeyRef forms). Shared as a
|
||||||
helper here because this chart has nine containers, each with its own env block.
|
helper here because this chart has ten containers, each with its own env block.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
{{- include "freesailarr.env" (dict "root" $ "env" .Values.<app>.env) | nindent 12 }}
|
{{- include "freesailarr.env" (dict "root" $ "env" .Values.<app>.env) | nindent 12 }}
|
||||||
@@ -176,6 +176,14 @@ recyclarr is deliberately absent: in daemon mode it serves no port, so it gets
|
|||||||
no container port, no service and no ingress. flaresolverr is present - it
|
no container port, no service and no ingress. flaresolverr is present - it
|
||||||
listens on 8191 and therefore gets a service - but it has no ingress block in
|
listens on 8191 and therefore gets a service - but it has no ingress block in
|
||||||
values and is never exposed from outside the cluster.
|
values and is never exposed from outside the cluster.
|
||||||
|
|
||||||
|
"port" is the app's PRIMARY port: the one its service is named after and the one
|
||||||
|
an ingress points at. mediathekarr is the only app with a second one, because it
|
||||||
|
runs two processes in one container - the SABnzbd role on 5007 (the UI, hence
|
||||||
|
the primary) and the Newznab role Prowlarr consumes on 5008. Optional
|
||||||
|
"extraPorts" entries ({name, port}) are additional ports of the SAME service and
|
||||||
|
are declared to gluetun's firewall alongside the primary; they never take part
|
||||||
|
in the ingress, which addresses a service port by name.
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "freesailarr.portApps" -}}
|
{{- define "freesailarr.portApps" -}}
|
||||||
{{- $apps := list -}}
|
{{- $apps := list -}}
|
||||||
@@ -186,7 +194,8 @@ values and is never exposed from outside the cluster.
|
|||||||
(dict "name" "radarr" "port" 7878)
|
(dict "name" "radarr" "port" 7878)
|
||||||
(dict "name" "sonarr" "port" 8989)
|
(dict "name" "sonarr" "port" 8989)
|
||||||
(dict "name" "seerr" "port" 5055)
|
(dict "name" "seerr" "port" 5055)
|
||||||
(dict "name" "bazarr" "port" 6767) -}}
|
(dict "name" "bazarr" "port" 6767)
|
||||||
|
(dict "name" "mediathekarr" "port" 5007 "extraPorts" (list (dict "name" "ma-newznab" "port" 5008))) -}}
|
||||||
{{- if (index $.Values $app.name).enabled -}}
|
{{- if (index $.Values $app.name).enabled -}}
|
||||||
{{- $apps = append $apps $app -}}
|
{{- $apps = append $apps $app -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
@@ -214,8 +223,9 @@ ingress block with enabled: true. Used by the ingress template.
|
|||||||
{{/*
|
{{/*
|
||||||
The sub-path an app is served under in the shared-domain mode, e.g. "/sonarr".
|
The sub-path an app is served under in the shared-domain mode, e.g. "/sonarr".
|
||||||
Null-safe by construction: an absent or erased <app>.ingress.path falls back to
|
Null-safe by construction: an absent or erased <app>.ingress.path falls back to
|
||||||
"/<app>", and to "/" for seerr, which has no base-path support at all and can
|
"/<app>", and to "/" for the two apps that can only live at the root - seerr,
|
||||||
therefore only live at the root.
|
which has no base-path support at all, and mediathekarr, whose UI links its
|
||||||
|
assets with root-absolute paths (see the guard in sharedIngressApps).
|
||||||
|
|
||||||
Input: dict "root" $ "app" <app name>.
|
Input: dict "root" $ "app" <app name>.
|
||||||
*/}}
|
*/}}
|
||||||
@@ -226,7 +236,7 @@ Input: dict "root" $ "app" <app name>.
|
|||||||
{{- $path = $config.path -}}
|
{{- $path = $config.path -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- if or (kindIs "invalid" $path) (eq (toString $path) "") -}}
|
{{- if or (kindIs "invalid" $path) (eq (toString $path) "") -}}
|
||||||
{{- if eq .app "seerr" -}}
|
{{- if has .app (list "seerr" "mediathekarr") -}}
|
||||||
{{- $path = "/" -}}
|
{{- $path = "/" -}}
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
{{- $path = printf "/%s" .app -}}
|
{{- $path = printf "/%s" .app -}}
|
||||||
@@ -250,9 +260,12 @@ Two guards live here rather than in the ingress template, so that every
|
|||||||
consumer - the ingress objects, the UrlBase env vars and NOTES.txt - sees the
|
consumer - the ingress objects, the UrlBase env vars and NOTES.txt - sees the
|
||||||
same validated list:
|
same validated list:
|
||||||
|
|
||||||
1. seerr under any path other than "/". seerr has no base-path support
|
1. seerr or mediathekarr under any path other than "/". seerr has no base-path
|
||||||
(upstream request open since May 2022), so a chart that rendered it under
|
support (upstream request open since May 2022) and MediathekArr's UI links
|
||||||
a sub-path would produce something that provably cannot work.
|
its assets with root-absolute paths, so a chart that rendered either under
|
||||||
|
a sub-path would produce something that provably cannot work. Both at "/"
|
||||||
|
on one host is caught by guard 2 below - one of them then needs a host of
|
||||||
|
its own.
|
||||||
2. Two apps claiming the same path on the shared host. Only paths on the
|
2. Two apps claiming the same path on the shared host. Only paths on the
|
||||||
shared host can collide; apps with their own host are on a different host
|
shared host can collide; apps with their own host are on a different host
|
||||||
and are therefore not part of this check.
|
and are therefore not part of this check.
|
||||||
@@ -269,11 +282,14 @@ same validated list:
|
|||||||
{{- $config := (index $root.Values $app.name).ingress -}}
|
{{- $config := (index $root.Values $app.name).ingress -}}
|
||||||
{{- if not ($config.host | default "") -}}
|
{{- if not ($config.host | default "") -}}
|
||||||
{{- $path := include "freesailarr.appPath" (dict "root" $root "app" $app.name) -}}
|
{{- $path := include "freesailarr.appPath" (dict "root" $root "app" $app.name) -}}
|
||||||
{{- if and (eq $app.name "seerr") (ne $path "/") -}}
|
{{- $rootOnly := dict
|
||||||
{{- fail (printf "seerr.ingress.path is %q, but seerr has no base-path support and can only be served at \"/\" (https://github.com/Fallenbagel/jellyseerr/issues/97). Either leave seerr.ingress.path at \"/\" or give seerr its own seerr.ingress.host." $path) -}}
|
"seerr" "seerr has no base-path support (https://github.com/Fallenbagel/jellyseerr/issues/97)"
|
||||||
|
"mediathekarr" "MediathekArr links its UI assets with root-absolute paths (/download/css/..., measured), so a sub-path stays broken even behind a prefix-stripping rewrite - the browser then asks for those assets at the host root" -}}
|
||||||
|
{{- if and (hasKey $rootOnly $app.name) (ne $path "/") -}}
|
||||||
|
{{- fail (printf "%s.ingress.path is %q, but %s and it can therefore only be served at \"/\". Either leave %s.ingress.path at \"/\" or give it its own %s.ingress.host." $app.name $path (index $rootOnly $app.name) $app.name $app.name) -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- if hasKey $seen $path -}}
|
{{- if hasKey $seen $path -}}
|
||||||
{{- fail (printf "%s.ingress.path and %s.ingress.path are both %q on the shared host %q - every app under ingress.host needs its own path" (index $seen $path) $app.name $path $host) -}}
|
{{- fail (printf "%s.ingress.path and %s.ingress.path are both %q on the shared host %q - every app under ingress.host needs its own path. Where neither can move (seerr and mediathekarr can only be served at \"/\"), give one of them its own <app>.ingress.host." (index $seen $path) $app.name $path $host) -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- $_ := set $seen $path $app.name -}}
|
{{- $_ := set $seen $path $app.name -}}
|
||||||
{{- $apps = append $apps (dict "name" $app.name "port" $app.port "path" $path) -}}
|
{{- $apps = append $apps (dict "name" $app.name "port" $app.port "path" $path) -}}
|
||||||
@@ -356,6 +372,9 @@ alive. It is not what opens the ports today.
|
|||||||
{{- $ports := list -}}
|
{{- $ports := list -}}
|
||||||
{{- range $app := include "freesailarr.portApps" . | fromJsonArray -}}
|
{{- range $app := include "freesailarr.portApps" . | fromJsonArray -}}
|
||||||
{{- $ports = append $ports (int $app.port) -}}
|
{{- $ports = append $ports (int $app.port) -}}
|
||||||
|
{{- range $extra := $app.extraPorts | default list -}}
|
||||||
|
{{- $ports = append $ports (int $extra.port) -}}
|
||||||
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- $ports = append $ports 9999 -}}
|
{{- $ports = append $ports 9999 -}}
|
||||||
{{- join "," $ports -}}
|
{{- join "," $ports -}}
|
||||||
@@ -366,7 +385,7 @@ Whether a media category is mounted at all, i.e. whether at least one app that
|
|||||||
mounts it is enabled. "true" when it is, the empty string when it is not (an
|
mounts it is enabled. "true" when it is, the empty string when it is not (an
|
||||||
empty string is falsy in a template "if", so the result can be used directly).
|
empty string is falsy in a template "if", so the result can be used directly).
|
||||||
|
|
||||||
downloads: qbittorrent, radarr, sonarr
|
downloads: qbittorrent, radarr, sonarr, mediathekarr
|
||||||
movies: radarr, bazarr
|
movies: radarr, bazarr
|
||||||
tv: sonarr, bazarr
|
tv: sonarr, bazarr
|
||||||
|
|
||||||
@@ -375,7 +394,7 @@ Input: dict "root" $ "category" <"downloads"|"movies"|"tv">.
|
|||||||
{{- define "freesailarr.mediaCategoryEnabled" -}}
|
{{- define "freesailarr.mediaCategoryEnabled" -}}
|
||||||
{{- $v := .root.Values -}}
|
{{- $v := .root.Values -}}
|
||||||
{{- if eq .category "downloads" -}}
|
{{- if eq .category "downloads" -}}
|
||||||
{{- if or $v.qbittorrent.enabled $v.radarr.enabled $v.sonarr.enabled -}}true{{- end -}}
|
{{- if or $v.qbittorrent.enabled $v.radarr.enabled $v.sonarr.enabled $v.mediathekarr.enabled -}}true{{- end -}}
|
||||||
{{- else if eq .category "movies" -}}
|
{{- else if eq .category "movies" -}}
|
||||||
{{- if or $v.radarr.enabled $v.bazarr.enabled -}}true{{- end -}}
|
{{- if or $v.radarr.enabled $v.bazarr.enabled -}}true{{- end -}}
|
||||||
{{- else if eq .category "tv" -}}
|
{{- else if eq .category "tv" -}}
|
||||||
@@ -885,6 +904,47 @@ bazarr:
|
|||||||
- FOWNER
|
- FOWNER
|
||||||
- SETGID
|
- SETGID
|
||||||
- SETUID
|
- SETUID
|
||||||
|
mediathekarr:
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- /bin/bash
|
||||||
|
- -c
|
||||||
|
- >-
|
||||||
|
wget -q -T 5 -O /dev/null "http://127.0.0.1:5008/api?t=caps" &&
|
||||||
|
wget -q -T 5 -O /dev/null "http://127.0.0.1:5007/download/api?mode=version&output=json&apikey=x"
|
||||||
|
periodSeconds: 30
|
||||||
|
timeoutSeconds: 15
|
||||||
|
failureThreshold: 3
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /download/
|
||||||
|
port: 5007
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /download/
|
||||||
|
port: 5007
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 60
|
||||||
|
resources:
|
||||||
|
limitFactor: 3
|
||||||
|
requests:
|
||||||
|
cpu: 0.1
|
||||||
|
memory: 256Mi
|
||||||
|
ephemeralStorage: 100Mi
|
||||||
|
securityContext:
|
||||||
|
runAsUser: {{ .Values.puid }}
|
||||||
|
runAsGroup: {{ .Values.pgid }}
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
runAsNonRoot: true
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
configInit:
|
configInit:
|
||||||
resources:
|
resources:
|
||||||
limitFactor: 3
|
limitFactor: 3
|
||||||
@@ -905,7 +965,7 @@ configInit:
|
|||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
The effective pod security context. Separate from the per-container table
|
The effective pod security context. Separate from the per-container table
|
||||||
because it is the one block shared by all nine containers.
|
because it is the one block shared by all containers of the pod.
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "freesailarr.podSecurityContext" -}}
|
{{- define "freesailarr.podSecurityContext" -}}
|
||||||
{{- $given := (fromYaml (include "freesailarr.subBlock" (dict "block" .Values.securityContext "key" "pod" "path" "securityContext"))).value -}}
|
{{- $given := (fromYaml (include "freesailarr.subBlock" (dict "block" .Values.securityContext "key" "pod" "path" "securityContext"))).value -}}
|
||||||
|
|||||||
@@ -21,5 +21,16 @@ spec:
|
|||||||
port: {{ int $app.port }}
|
port: {{ int $app.port }}
|
||||||
targetPort: {{ $app.name }}
|
targetPort: {{ $app.name }}
|
||||||
name: {{ $app.name }}
|
name: {{ $app.name }}
|
||||||
|
{{- /* An app with more than one port keeps them in ONE service rather than
|
||||||
|
getting a second object: they belong to the same container and the
|
||||||
|
same pod IP, and only the primary port is ever addressed by an
|
||||||
|
ingress. mediathekarr is the only such app today - 5007 (SABnzbd
|
||||||
|
role, primary) and 5008 (Newznab role). */}}
|
||||||
|
{{- range $extra := $app.extraPorts | default list }}
|
||||||
|
- protocol: TCP
|
||||||
|
port: {{ int $extra.port }}
|
||||||
|
targetPort: {{ $extra.name }}
|
||||||
|
name: {{ $extra.name }}
|
||||||
|
{{- end }}
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
{{- end }}
|
{{- end }}
|
||||||
@@ -521,6 +521,109 @@ spec:
|
|||||||
subPath: {{ . | quote }}
|
subPath: {{ . | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- if .Values.mediathekarr.enabled }}
|
||||||
|
# MediathekArr, the tenth container and the only one off by default: a fake
|
||||||
|
# Usenet indexer for Prowlarr (Newznab, port 5008) and a fake SABnzbd
|
||||||
|
# for Sonarr/Radarr (port 5007), fetching from the ARD and ZDF media
|
||||||
|
# libraries. Both consumers sit in this pod and reach it over
|
||||||
|
# 127.0.0.1, so it needs neither a service nor DNS to do its job.
|
||||||
|
- name: mediathekarr
|
||||||
|
# Tag AND digest: the tag stays readable, the digest is what is
|
||||||
|
# actually pulled. "beta" moves, and "latest" is a 2024 build - see
|
||||||
|
# the values comment.
|
||||||
|
image: "{{ .Values.mediathekarr.image.repository }}:{{ .Values.mediathekarr.image.tag }}@{{ required "mediathekarr.image.digest is required - the tag alone is a moving reference" .Values.mediathekarr.image.digest }}"
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
{{- if .Values.mediathekarr.startGate.enabled }}
|
||||||
|
# THE START GATE. Kubernetes cannot order two regular containers, and
|
||||||
|
# an initContainer waiting for gluetun would deadlock (gluetun only
|
||||||
|
# starts once the init containers are done). So the wait happens here,
|
||||||
|
# in front of the image entrypoint: poll gluetun's health server -
|
||||||
|
# 200 only while the tunnel actually works - and exec the entrypoint
|
||||||
|
# afterwards.
|
||||||
|
#
|
||||||
|
# Why this is not optional decoration: MediathekArr fetches its
|
||||||
|
# ruleset once at start and never retries. Started before the tunnel,
|
||||||
|
# it answered every Newznab search with 0 results for over eleven
|
||||||
|
# minutes - container up, Ready, nothing in the log. The failure case
|
||||||
|
# looks exactly like the healthy one, which is why the gate exits
|
||||||
|
# instead of starting anyway when the timeout passes: a crash-looping
|
||||||
|
# container is visible, a silently empty indexer is not.
|
||||||
|
#
|
||||||
|
# wget comes with the (Debian) image, bash is its entrypoint's own
|
||||||
|
# interpreter - no extra tooling and no shell built into the chart.
|
||||||
|
command:
|
||||||
|
- /bin/bash
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -eu
|
||||||
|
deadline=$(( SECONDS + {{ int .Values.mediathekarr.startGate.timeoutSeconds }} ))
|
||||||
|
echo "mediathekarr: waiting for gluetun to report healthy (127.0.0.1:9999)"
|
||||||
|
until wget -q -T 5 -O /dev/null http://127.0.0.1:9999/; do
|
||||||
|
if [ "$SECONDS" -ge "$deadline" ]; then
|
||||||
|
echo "mediathekarr: gluetun did not report healthy within {{ int .Values.mediathekarr.startGate.timeoutSeconds }}s - exiting instead of starting without a tunnel, because a MediathekArr started too early answers every search with 0 results and reports nothing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep {{ int .Values.mediathekarr.startGate.intervalSeconds }}
|
||||||
|
done
|
||||||
|
echo "mediathekarr: gluetun healthy after ${SECONDS}s"
|
||||||
|
{{- if gt (int .Values.mediathekarr.startGate.settleSeconds) 0 }}
|
||||||
|
sleep {{ int .Values.mediathekarr.startGate.settleSeconds }}
|
||||||
|
{{- end }}
|
||||||
|
exec /app/docker_start.sh
|
||||||
|
{{- end }}
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
value: {{ .Values.timezone | quote }}
|
||||||
|
{{- /* Both download paths are set by the chart, not left to the
|
||||||
|
image: complete/ has to be visible to Sonarr/Radarr under
|
||||||
|
the SAME absolute path (they are handed this value as the
|
||||||
|
SABnzbd complete_dir, and a deviation imports nothing
|
||||||
|
without an error), and incomplete/ has to live on the media
|
||||||
|
claim rather than in an emptyDir, which would count against
|
||||||
|
the container's ephemeral-storage limit and let one large
|
||||||
|
download evict the whole pod. Setting them also keeps the
|
||||||
|
image's built-in /data/mediathek default out of play, which
|
||||||
|
the read-only root filesystem does not provide. */}}
|
||||||
|
- name: DOWNLOAD_INCOMPLETE_PATH
|
||||||
|
value: {{ printf "/downloads/%s/incomplete" .Values.mediathekarr.downloads.directory | quote }}
|
||||||
|
- name: DOWNLOAD_COMPLETE_PATH
|
||||||
|
value: {{ printf "/downloads/%s/complete" .Values.mediathekarr.downloads.directory | quote }}
|
||||||
|
{{- include "freesailarr.env" (dict "root" $ "env" .Values.mediathekarr.env) | nindent 12 }}
|
||||||
|
ports:
|
||||||
|
- containerPort: 5007
|
||||||
|
protocol: TCP
|
||||||
|
name: mediathekarr
|
||||||
|
- containerPort: 5008
|
||||||
|
protocol: TCP
|
||||||
|
name: ma-newznab
|
||||||
|
livenessProbe:
|
||||||
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "mediathekarr" "kind" "livenessProbe" "given" .Values.mediathekarr.livenessProbe) | nindent 12 }}
|
||||||
|
readinessProbe:
|
||||||
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "mediathekarr" "kind" "readinessProbe" "given" .Values.mediathekarr.readinessProbe) | nindent 12 }}
|
||||||
|
startupProbe:
|
||||||
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "mediathekarr" "kind" "startupProbe" "given" .Values.mediathekarr.startupProbe) | nindent 12 }}
|
||||||
|
resources:
|
||||||
|
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "mediathekarr" "given" .Values.mediathekarr.resources) | nindent 12 }}
|
||||||
|
securityContext:
|
||||||
|
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "mediathekarr" "given" .Values.mediathekarr.securityContext) | nindent 12 }}
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /app/config
|
||||||
|
name: mediathekarr-config
|
||||||
|
{{- /* The same claim, the same mount path as sonarr/radarr - that
|
||||||
|
is what makes the complete_dir path identical on both
|
||||||
|
sides. */}}
|
||||||
|
- mountPath: /downloads
|
||||||
|
name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "downloads") }}
|
||||||
|
{{- with .Values.media.downloads.subPath }}
|
||||||
|
subPath: {{ . | quote }}
|
||||||
|
{{- end }}
|
||||||
|
# Not a measured requirement: the container ran through with a
|
||||||
|
# read-only root and without a writable /tmp. Kept as cheap
|
||||||
|
# insurance for the .NET runtime, which is the kind of consumer that
|
||||||
|
# reaches for /tmp on a code path nobody exercised yet.
|
||||||
|
- mountPath: /tmp
|
||||||
|
name: mediathekarr-tmp
|
||||||
|
{{- end }}
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
volumes:
|
volumes:
|
||||||
# The tun device gluetun needs to create the Wireguard tunnel.
|
# The tun device gluetun needs to create the Wireguard tunnel.
|
||||||
@@ -542,11 +645,11 @@ spec:
|
|||||||
setup with three separate claims still renders byte-identically to
|
setup with three separate claims still renders byte-identically to
|
||||||
2.0.1 and the diff itself proves the change is inert there.
|
2.0.1 and the diff itself proves the change is inert there.
|
||||||
*/}}
|
*/}}
|
||||||
{{- if or .Values.qbittorrent.enabled .Values.radarr.enabled .Values.sonarr.enabled }}
|
{{- if or .Values.qbittorrent.enabled .Values.radarr.enabled .Values.sonarr.enabled .Values.mediathekarr.enabled }}
|
||||||
{{- if include "freesailarr.mediaVolumeOwned" (dict "root" $ "category" "downloads") }}
|
{{- if include "freesailarr.mediaVolumeOwned" (dict "root" $ "category" "downloads") }}
|
||||||
- name: media-downloads
|
- name: media-downloads
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: {{ required "media.downloads.existingClaim is required when qbittorrent, radarr or sonarr is enabled" .Values.media.downloads.existingClaim }}
|
claimName: {{ required "media.downloads.existingClaim is required when qbittorrent, radarr, sonarr or mediathekarr is enabled" .Values.media.downloads.existingClaim }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if or .Values.radarr.enabled .Values.bazarr.enabled }}
|
{{- if or .Values.radarr.enabled .Values.bazarr.enabled }}
|
||||||
@@ -579,12 +682,16 @@ spec:
|
|||||||
- name: seerr-tmp
|
- name: seerr-tmp
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- if .Values.mediathekarr.enabled }}
|
||||||
|
- name: mediathekarr-tmp
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
# Config volumes for gluetun and the enabled apps. NOTE: volumeClaimTemplates
|
# Config volumes for gluetun and the enabled apps. NOTE: volumeClaimTemplates
|
||||||
# are immutable - toggling an app's enabled flag later requires recreating
|
# are immutable - toggling an app's enabled flag later requires recreating
|
||||||
# the StatefulSet once (kubectl delete statefulset <name> --cascade=orphan
|
# the StatefulSet once (kubectl delete statefulset <name> --cascade=orphan
|
||||||
# before upgrading; existing PVCs and their data survive).
|
# before upgrading; existing PVCs and their data survive).
|
||||||
volumeClaimTemplates:
|
volumeClaimTemplates:
|
||||||
{{- range $app := list "gluetun" "qbittorrent" "prowlarr" "radarr" "recyclarr" "sonarr" "seerr" "bazarr" }}
|
{{- range $app := list "gluetun" "qbittorrent" "prowlarr" "radarr" "recyclarr" "sonarr" "seerr" "bazarr" "mediathekarr" }}
|
||||||
{{- $config := index $.Values $app }}
|
{{- $config := index $.Values $app }}
|
||||||
{{- if or (eq $app "gluetun") $config.enabled }}
|
{{- if or (eq $app "gluetun") $config.enabled }}
|
||||||
- metadata:
|
- metadata:
|
||||||
|
|||||||
@@ -535,6 +535,76 @@
|
|||||||
"$ref": "#/definitions/containerSecurityContext"
|
"$ref": "#/definitions/containerSecurityContext"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"mediathekarr": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"$ref": "#/definitions/pinnedImage"
|
||||||
|
},
|
||||||
|
"ingress": {
|
||||||
|
"$ref": "#/definitions/appIngress"
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"$ref": "#/definitions/appStorage"
|
||||||
|
},
|
||||||
|
"startGate": {
|
||||||
|
"description": "Delays the container until gluetun reports healthy. NOT null-safe on purpose and not merged through the fallback helper: this block decides whether the gate is rendered at all, and an erased block would silently produce a container that starts before the tunnel - the failure this chart writes three paragraphs about, and one that looks green.",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"intervalSeconds": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"timeoutSeconds": {
|
||||||
|
"description": "Total wait before the container exits instead of starting without a tunnel. Keep the startup probe's budget (periodSeconds x failureThreshold) above it, or the kubelet kills the container while the gate is still waiting.",
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"settleSeconds": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"directory": {
|
||||||
|
"description": "Sub-directory on the shared downloads claim holding incomplete/ and complete/. The chart derives DOWNLOAD_INCOMPLETE_PATH and DOWNLOAD_COMPLETE_PATH from it; complete/ is what Sonarr/Radarr are told to import from, under the identical absolute path.",
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"$ref": "#/definitions/env"
|
||||||
|
},
|
||||||
|
"livenessProbe": {
|
||||||
|
"$ref": "#/definitions/probe"
|
||||||
|
},
|
||||||
|
"readinessProbe": {
|
||||||
|
"$ref": "#/definitions/probe"
|
||||||
|
},
|
||||||
|
"startupProbe": {
|
||||||
|
"$ref": "#/definitions/probe"
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"$ref": "#/definitions/resources"
|
||||||
|
},
|
||||||
|
"securityContext": {
|
||||||
|
"$ref": "#/definitions/containerSecurityContext"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
@@ -746,6 +816,27 @@
|
|||||||
"object",
|
"object",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"pinnedImage": {
|
||||||
|
"description": "Like image, plus the digest mediathekarr is pinned by: its tags are unusable (\"latest\" is a 2024 build, \"beta\" moves), so the tag stays as the readable label and the digest is what is actually pulled. Deliberately a separate definition instead of adding an optional digest to image: on every other image in this chart a digest key would be a value the templates never read - exactly the silent no-op the schema exists to reject (#68).",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"repository": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"number"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"digest": {
|
||||||
|
"description": "Image digest, sha256:<64 hex>. Required - the template refuses to render without it.",
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^sha256:[0-9a-f]{64}$"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+261
-4
@@ -4,10 +4,11 @@
|
|||||||
# the Wireguard tunnel.
|
# the Wireguard tunnel.
|
||||||
#
|
#
|
||||||
# Image pinning (documented deviation from the Chart.AppVersion standard):
|
# Image pinning (documented deviation from the Chart.AppVersion standard):
|
||||||
# NO image tag comes from appVersion. All nine images, gluetun included, are
|
# NO image tag comes from appVersion. All ten images, gluetun included, are
|
||||||
# pinned here via <app>.image.repository / <app>.image.tag, because the stack
|
# pinned here via <app>.image.repository / <app>.image.tag, because the stack
|
||||||
# combines nine independently versioned upstream projects and none of them is
|
# combines ten independently versioned upstream projects and none of them is
|
||||||
# "the" one app version.
|
# "the" one app version. mediathekarr additionally carries an image.digest -
|
||||||
|
# see the reasoning in its own block at the end of this file.
|
||||||
#
|
#
|
||||||
# That is also what puts every image under Renovate: the built-in helm-values
|
# That is also what puts every image under Renovate: the built-in helm-values
|
||||||
# manager reads image.repository/image.tag pairs out of this file and proposes
|
# manager reads image.repository/image.tag pairs out of this file and proposes
|
||||||
@@ -84,7 +85,7 @@ storage:
|
|||||||
# Media libraries as pre-existing PVCs (shared with the separately deployed
|
# Media libraries as pre-existing PVCs (shared with the separately deployed
|
||||||
# Jellyfin) - the chart mounts but never creates them. Each claim is required
|
# Jellyfin) - the chart mounts but never creates them. Each claim is required
|
||||||
# as soon as an app that mounts it is enabled:
|
# as soon as an app that mounts it is enabled:
|
||||||
# downloads: qbittorrent, radarr, sonarr
|
# downloads: qbittorrent, radarr, sonarr, mediathekarr
|
||||||
# movies: radarr, bazarr
|
# movies: radarr, bazarr
|
||||||
# tv: sonarr, bazarr
|
# tv: sonarr, bazarr
|
||||||
#
|
#
|
||||||
@@ -874,3 +875,259 @@ bazarr:
|
|||||||
- FOWNER
|
- FOWNER
|
||||||
- SETGID
|
- SETGID
|
||||||
- SETUID
|
- SETUID
|
||||||
|
|
||||||
|
# MediathekArr: the tenth container, and the only one that is OFF by default. It fakes a Usenet indexer towards Prowlarr and a SABnzbd downloader
|
||||||
|
# towards Sonarr/Radarr, and fetches the content over plain HTTP from the ARD
|
||||||
|
# and ZDF media libraries. Both consumers already sit in this pod and share
|
||||||
|
# gluetun's network namespace, so they reach it over 127.0.0.1 - no service, no
|
||||||
|
# DNS, no ingress detour.
|
||||||
|
#
|
||||||
|
# Off by default because it is additive and nobody's existing release should
|
||||||
|
# grow a container on an upgrade: enabling it creates a new
|
||||||
|
# volumeClaimTemplate, which a StatefulSet cannot take without being recreated
|
||||||
|
# (see the NOTES and the volumeClaimTemplates comment in the StatefulSet).
|
||||||
|
#
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# THE GEO CAVEAT, AND HOW TO CHECK IT YOURSELF
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# The tunnel leaves through Zurich by this chart's default, and ARD and ZDF
|
||||||
|
# block parts of their catalogue outside Germany. Every container hangs in
|
||||||
|
# gluetun's namespace; there is no bypass for a single container.
|
||||||
|
#
|
||||||
|
# Measured, 480 URLs across 12 channels: 104 of 473 loadable URLs were blocked
|
||||||
|
# through Zurich (22.0%), none of them on the German side. The spread is very
|
||||||
|
# uneven - ONE 97.5%, ZDFneo 47.5%, ZDFinfo 37.5%, Das Erste 20.5%, ZDF 10.3%,
|
||||||
|
# BR 0% - so a single number tells you little about the show you actually want.
|
||||||
|
#
|
||||||
|
# THE PROCEDURE, NOT A TABLE OF SERIES. A list of titles is stale in half a
|
||||||
|
# year; the marker in the CDN path is not. Over two measurement rounds it
|
||||||
|
# predicted the outcome perfectly - 584 of 584 cases in the full survey, not a
|
||||||
|
# single misclassification. Look at the media URL of an entry and read the
|
||||||
|
# path:
|
||||||
|
#
|
||||||
|
# /de/, /geo/de/, /deutschlandweit/, geoavailability_D -> blocked outside DE
|
||||||
|
# /weltweit/ -> open
|
||||||
|
# /none/ (ZDF) -> open
|
||||||
|
# /dach/, deChAt (WDR variant) -> open, Switzerland
|
||||||
|
# is part of DACH
|
||||||
|
#
|
||||||
|
# The obvious rule of thumb "own production = available worldwide" is MEASURABLY
|
||||||
|
# WRONG and is named here so nobody re-invents it: an ARD own production was
|
||||||
|
# blocked, a licensed series was open. Only the marker counts.
|
||||||
|
#
|
||||||
|
# THE ESCAPE ROUTE AND ITS PRICE. The exit is a deployment decision, not a chart
|
||||||
|
# property: set gluetun.vpn.serverCountries/serverCities to a German exit and
|
||||||
|
# the blocks disappear. The price is that this moves the WHOLE stack, all ten
|
||||||
|
# containers - qBittorrent included - into that jurisdiction and onto those
|
||||||
|
# servers. There is no per-container exit, and this chart deliberately does not
|
||||||
|
# pretend otherwise.
|
||||||
|
#
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# THE fail_message: null TRAP - READ THIS BEFORE DEBUGGING A FAILED DOWNLOAD
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# MediathekArr never checks the HTTP status code. Measured A/B with an identical
|
||||||
|
# NZB: direct (DE) -> status Completed, 94,235,525 bytes, fail_message null;
|
||||||
|
# through Zurich -> status Failed, 0 bytes, fail_message ALSO null.
|
||||||
|
#
|
||||||
|
# On a geo-blocked fetch it writes the ~476-byte HTML error page out as .mp4 and
|
||||||
|
# logs "Download completed". Only mkvmerge then refuses the file (exit code 2)
|
||||||
|
# and flips the status to Failed. Two consequences:
|
||||||
|
#
|
||||||
|
# 1. Sonarr is told "failed" without a reason. A geo block looks exactly like
|
||||||
|
# a broken server, an expired URL or a full disk - and the container log
|
||||||
|
# does not mention the 403 either.
|
||||||
|
# 2. That protection is an accident, not error handling. It rests entirely on
|
||||||
|
# mkvmerge rejecting an HTML page. Any geo response mkvmerge would accept
|
||||||
|
# is reported as Completed, and then a broken file walks into the library.
|
||||||
|
# It did not happen in either measurement round (Akamai consistently
|
||||||
|
# returns HTML), but that is not something to rely on.
|
||||||
|
#
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# OPEN QUESTION, EXPLICITLY UNMEASURED: HLS
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Part of the catalogue is served as an HLS playlist (application/x-mpegURL)
|
||||||
|
# instead of a progressive MP4. The image carries an ffmpeg-based M3U8 path
|
||||||
|
# (log lines "Starting M3U8 download" / "M3U8 download completed", and ffmpeg is
|
||||||
|
# present in the image), so the code is there - but whether such an entry
|
||||||
|
# actually completes end to end has NOT been measured. If it does not, those
|
||||||
|
# entries fail regardless of any geo block. Stated as an open question on
|
||||||
|
# purpose: no assumption in either direction.
|
||||||
|
mediathekarr:
|
||||||
|
enabled: false
|
||||||
|
# Pinned BY DIGEST, unlike every other image in this chart, because the tags
|
||||||
|
# are unusable: "latest" is from 2024-11-12 (= v0.2.5) and the upstream README
|
||||||
|
# points at "beta", which is a moving tag. tag + digest together keep the
|
||||||
|
# human-readable line and the immutable reference.
|
||||||
|
#
|
||||||
|
# The image is SINGLE-ARCH, linux/amd64 only - there is no arm64 variant. The
|
||||||
|
# target cluster is amd64 throughout, so this is a note, not a problem; on a
|
||||||
|
# mixed cluster the pod would need a nodeSelector this chart does not offer.
|
||||||
|
#
|
||||||
|
# Renovate: the built-in helm-values manager cannot follow this pair (the tag
|
||||||
|
# carries no version), so renovate.json5 has a customManagers block that
|
||||||
|
# matches exactly these three lines and proposes digest updates for the beta
|
||||||
|
# tag. Renaming or reordering the keys silently switches that off - the rule
|
||||||
|
# would still be valid, it just would not match any more (#158).
|
||||||
|
image:
|
||||||
|
repository: docker.io/pcjones/mediathekarr
|
||||||
|
tag: beta
|
||||||
|
digest: sha256:3ed7e1487810e1519301bd56a0c420e260e428295b029a47806c0397e4a52a33
|
||||||
|
# TWO ports, not one, and the upstream documentation only mentions the first:
|
||||||
|
# 5007 MediathekArrDownloader - the SABnzbd role and the web UI
|
||||||
|
# 5008 MediathekArrServer - the Newznab role Prowlarr talks to
|
||||||
|
# The SABnzbd API lives under /download/api, NOT under /api: a request to
|
||||||
|
# /api on 5007 answers with a Newznab validation error and sends you looking
|
||||||
|
# in the wrong place. Both ports are declared to gluetun's firewall
|
||||||
|
# automatically (see the firewallInputPorts helper).
|
||||||
|
#
|
||||||
|
# No ingress by default: MediathekArr's UI links its assets with root-absolute
|
||||||
|
# paths (/download/css/..., measured), so it can only be served at the ROOT of
|
||||||
|
# a host - a sub-path is broken even behind a prefix-stripping rewrite,
|
||||||
|
# because the browser then requests the assets at the host root. On the shared
|
||||||
|
# domain the root belongs to seerr, so enabling this ingress there needs
|
||||||
|
# either seerr switched off or a host of its own here. Both consumers reach
|
||||||
|
# MediathekArr over localhost and need no ingress at all; the UI is only
|
||||||
|
# needed to run the setup wizard, for which a port-forward is enough.
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
host: null
|
||||||
|
# Read only in the shared-domain mode. "/" is the only value that works;
|
||||||
|
# anything else fails the render rather than producing a broken UI.
|
||||||
|
path: /
|
||||||
|
# clusterIssuer: null
|
||||||
|
# className: null
|
||||||
|
# annotations: {}
|
||||||
|
storage:
|
||||||
|
# Size of the MediathekArr config volume (/app/config, a small JSON).
|
||||||
|
size: 1Gi
|
||||||
|
# THE START GATE - the sharpest point of this container, and the one whose
|
||||||
|
# failure mode looks green.
|
||||||
|
#
|
||||||
|
# If MediathekArr starts before the tunnel is up, its ruleset fetch fails once
|
||||||
|
# and is never retried: the instance then answered every Newznab search with 0
|
||||||
|
# results for over eleven minutes, while the container was up, Ready and
|
||||||
|
# logged nothing wrong. A green pod proves nothing here. With the start
|
||||||
|
# delayed until gluetun reports healthy, the ruleset loads and the search
|
||||||
|
# matches an instance running with direct egress (the search itself is not
|
||||||
|
# geo-blocked - 56 hits through either exit).
|
||||||
|
#
|
||||||
|
# Kubernetes cannot express "start after that container": gluetun is a regular
|
||||||
|
# container, and an initContainer waiting for it would deadlock. So the gate
|
||||||
|
# lives in this container's command - it polls gluetun's health server on
|
||||||
|
# 127.0.0.1:9999 (the same endpoint gluetun's own probes use, 200 only while
|
||||||
|
# the tunnel works) and only then execs the image entrypoint.
|
||||||
|
#
|
||||||
|
# On timeout the container EXITS instead of starting anyway. That is
|
||||||
|
# deliberate: a crash-looping container is visible, a silently empty indexer
|
||||||
|
# is not.
|
||||||
|
#
|
||||||
|
# Keep startupProbe budget > timeoutSeconds + app start, or the kubelet kills
|
||||||
|
# the container while the gate is still waiting. Defaults: 180s gate,
|
||||||
|
# 5s * 60 = 300s startup budget.
|
||||||
|
startGate:
|
||||||
|
enabled: true
|
||||||
|
# Poll interval and total budget for gluetun becoming healthy. gluetun's own
|
||||||
|
# startup budget is 150s, so 180s covers a normal handshake with room.
|
||||||
|
intervalSeconds: 3
|
||||||
|
timeoutSeconds: 180
|
||||||
|
# Extra pause after gluetun reports healthy, before the app starts. NOT a
|
||||||
|
# measured requirement - gluetun's 200 is the actual condition - but a cheap
|
||||||
|
# buffer for DNS and routing to settle. 0 disables it.
|
||||||
|
settleSeconds: 10
|
||||||
|
# WHERE THE DOWNLOADS GO. Both directories live on the SHARED downloads claim
|
||||||
|
# (media.downloads), mounted at /downloads exactly like sonarr, radarr and
|
||||||
|
# qbittorrent mount it, under this sub-directory:
|
||||||
|
#
|
||||||
|
# /downloads/<directory>/incomplete work in progress
|
||||||
|
# /downloads/<directory>/complete finished, plus one sub-dir per category
|
||||||
|
#
|
||||||
|
# Two reasons, both of them the kind that fails silently:
|
||||||
|
#
|
||||||
|
# 1. complete/ MUST be visible to Sonarr/Radarr under the SAME absolute
|
||||||
|
# path. The chart hands that path to MediathekArr
|
||||||
|
# (DOWNLOAD_COMPLETE_PATH), which reports it to Sonarr as the SABnzbd
|
||||||
|
# complete_dir. Every container here mounts the claim at /downloads, so
|
||||||
|
# the paths are identical by construction - a deviation imports nothing,
|
||||||
|
# without an error.
|
||||||
|
# 2. incomplete/ must NOT be an emptyDir. An emptyDir counts against the
|
||||||
|
# container's ephemeral-storage limit, and one large download makes the
|
||||||
|
# kubelet evict the WHOLE pod - all ten containers. Same trap as the one
|
||||||
|
# documented in ytdlp-web-player.
|
||||||
|
#
|
||||||
|
# Do not set DOWNLOAD_COMPLETE_PATH or DOWNLOAD_INCOMPLETE_PATH in the env
|
||||||
|
# block below: the chart always sets both, and a second entry of the same name
|
||||||
|
# is not a defined state (see README, "Environment variables").
|
||||||
|
downloads:
|
||||||
|
directory: mediathekarr
|
||||||
|
# Extra environment variables, appended to the ones the chart always sets.
|
||||||
|
# Useful ones the image reads: CATEGORIES, MAX_PARALLEL_DOWNLOADS,
|
||||||
|
# MEDIATHEKARR_API_BASE_URL.
|
||||||
|
env: []
|
||||||
|
# Probes on measured 200 endpoints - MediathekArr has NO health endpoint at
|
||||||
|
# all (/health, /healthz, /api/health, /download/health, /status and /ping all
|
||||||
|
# answer 404).
|
||||||
|
#
|
||||||
|
# Liveness is an exec on purpose: one container runs TWO processes, and the
|
||||||
|
# kubelet allows one probe of each kind. The Newznab half can wedge while the
|
||||||
|
# downloader half still answers, and that is exactly the failure that makes
|
||||||
|
# searches come back empty. The command checks both ports and restarts the
|
||||||
|
# container when either stops answering. (The entrypoint already exits when
|
||||||
|
# one of the two processes CRASHES - the exec probe covers the case where it
|
||||||
|
# is alive but not serving.)
|
||||||
|
#
|
||||||
|
# Readiness and startup use the downloader port: that is the one the Service
|
||||||
|
# and any ingress point at, and /download/ answers 200 without a query string.
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- /bin/bash
|
||||||
|
- -c
|
||||||
|
- >-
|
||||||
|
wget -q -T 5 -O /dev/null "http://127.0.0.1:5008/api?t=caps" &&
|
||||||
|
wget -q -T 5 -O /dev/null "http://127.0.0.1:5007/download/api?mode=version&output=json&apikey=x"
|
||||||
|
periodSeconds: 30
|
||||||
|
timeoutSeconds: 15
|
||||||
|
failureThreshold: 3
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /download/
|
||||||
|
port: 5007
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
# The startup probe gates liveness/readiness AND has to outlast the start gate
|
||||||
|
# (see startGate above): 5s * 60 = 300s against a 180s gate.
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /download/
|
||||||
|
port: 5007
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 60
|
||||||
|
resources:
|
||||||
|
limitFactor: 3
|
||||||
|
# ephemeral-storage stays small on purpose: downloads land on the media
|
||||||
|
# claim, not on the container filesystem (see downloads above).
|
||||||
|
requests:
|
||||||
|
cpu: 0.1
|
||||||
|
memory: 256Mi
|
||||||
|
ephemeralStorage: 100Mi
|
||||||
|
# Measured, including a real download and the mkvmerge remux: the container
|
||||||
|
# runs fully hardened - non-root, uid/gid from puid/pgid,
|
||||||
|
# readOnlyRootFilesystem: true - as long as its writable paths are volumes.
|
||||||
|
# Those are /app/config (the config claim) and the two download directories on
|
||||||
|
# the media claim; the image's built-in default /data/mediathek is never
|
||||||
|
# touched, because the chart overrides both paths by environment (measured:
|
||||||
|
# read-only root, no /data/mediathek mount, no error).
|
||||||
|
# PUID/PGID are NOT needed (the image's gosu path is only taken when they are
|
||||||
|
# set), and the
|
||||||
|
# commented-out non-root block in its Dockerfile is outdated for the beta.
|
||||||
|
# runAsUser/runAsGroup come from the global puid/pgid values in the template -
|
||||||
|
# do not set them here.
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
runAsNonRoot: true
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
+40
-6
@@ -212,14 +212,48 @@
|
|||||||
//
|
//
|
||||||
// When adding a chart, add a block here — see README "Creating a new chart".
|
// When adding a chart, add a block here — see README "Creating a new chart".
|
||||||
//
|
//
|
||||||
// Exception, deliberately no block: `freesailarr`. It bundles nine
|
// Exception, deliberately no APP-VERSION block: `freesailarr`. It bundles
|
||||||
// independently versioned upstreams, so none of them is "the" app version
|
// ten independently versioned upstreams, so none of them is "the" app
|
||||||
// and no image tag comes from appVersion — all nine are pinned as
|
// version and no image tag comes from appVersion — they are pinned as
|
||||||
// image.repository/image.tag in its values.yaml, where the built-in
|
// image.repository/image.tag in its values.yaml, where the built-in
|
||||||
// helm-values manager already finds them and proposes one update per
|
// helm-values manager already finds them and proposes one update per
|
||||||
// image. Adding a block here would either duplicate those updates or
|
// image. Adding an appVersion block here would either duplicate those
|
||||||
// rewrite an appVersion that selects nothing. Its appVersion is a
|
// updates or rewrite an appVersion that selects nothing. Its appVersion is
|
||||||
// hand-maintained marker; see freesailarr/Chart.yaml.
|
// a hand-maintained marker; see freesailarr/Chart.yaml. The one image the
|
||||||
|
// built-in manager cannot follow — mediathekarr, pinned by digest — has its
|
||||||
|
// own block directly below.
|
||||||
|
|
||||||
|
{
|
||||||
|
description: [
|
||||||
|
"freesailarr / mediathekarr: the only image in this repo pinned by DIGEST, because its",
|
||||||
|
"tags carry no version at all — 'latest' is a 2024 build (v0.2.5) and the upstream README",
|
||||||
|
"points at the moving 'beta'. The built-in helm-values manager reads repository/tag and",
|
||||||
|
"would have nothing to compare, so this manager captures all three keys and lets Renovate",
|
||||||
|
"propose digest updates for the beta tag (currentDigest + currentValue = a digest-only",
|
||||||
|
"lookup; automerged under the 'digest' update type in the rule above).",
|
||||||
|
"",
|
||||||
|
"The pattern is anchored on the three keys in their values.yaml order: repository, tag,",
|
||||||
|
"digest. Reordering or renaming them does not fail anything — it silently stops matching,",
|
||||||
|
"and the image then never gets an update again (#158: a valid rule is not a matching rule).",
|
||||||
|
"",
|
||||||
|
"Verified by dry-run (renovate 44.69.11, --platform=local --dry-run=lookup, real registry),",
|
||||||
|
"two fixtures, because 'no update' is what BOTH a working and a broken rule look like while",
|
||||||
|
"the pin is current:",
|
||||||
|
" currentDigest = the pinned one -> extracted with currentValue 'beta', updates [] (the",
|
||||||
|
" beta tag really does still point at it).",
|
||||||
|
" currentDigest = sha256:0000... -> updates [{updateType: digest, newValue: beta,",
|
||||||
|
" newDigest: sha256:3ed7e14...}] on branch renovate/docker.io-pcjones-mediathekarr-beta.",
|
||||||
|
"The same run also shows the built-in helm-values manager finding this image and skipping it",
|
||||||
|
"(skipReason invalid-value, 'no currentDigest or pinDigests' — it does not read the digest",
|
||||||
|
"key), so the two managers cannot produce competing PRs.",
|
||||||
|
],
|
||||||
|
customType: "regex",
|
||||||
|
managerFilePatterns: ["freesailarr/values.yaml"],
|
||||||
|
matchStrings: [
|
||||||
|
"repository: (?<depName>docker\\.io/pcjones/mediathekarr)\\s+tag: (?<currentValue>[^\\s]+)\\s+digest: (?<currentDigest>sha256:[0-9a-f]{64})",
|
||||||
|
],
|
||||||
|
datasourceTemplate: "docker",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
description: "apache-tika: four-part version plus flavour suffix (3.3.1.0-full); the compatibility group keeps the chart on -full.",
|
description: "apache-tika: four-part version plus flavour suffix (3.3.1.0-full); the compatibility group keeps the chart on -full.",
|
||||||
|
|||||||
Reference in new issue
Block a user