freesailarr: MediathekArr as the tenth container, off by default (3.1.0+up1.0.0)
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
This commit is contained in:
11 files changed
+717
-49
No files matched your search
@@ -10,7 +10,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 `{}`) |
|
||||
| `calibre-web-automated` | Calibre-Web Automated | StatefulSet, extra consume-PVC |
|
||||
| `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) |
|
||||
| `homeassistant` | Home Assistant | StatefulSet; the only chart carrying Helm tests (`templates/tests/`) |
|
||||
| `matrix-synapse` | Matrix Synapse | StatefulSet plus an nginx delegation Deployment/Service |
|
||||
@@ -96,15 +96,15 @@ What Renovate keeps current:
|
||||
| --- | --- |
|
||||
| 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 |
|
||||
| **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 |
|
||||
| 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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -181,7 +181,7 @@ These guidelines are **binding for every current and future chart** in this repo
|
||||
### 3. Images
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
### 4. Ingress
|
||||
@@ -192,7 +192,7 @@ These guidelines are **binding for every current and future chart** in this repo
|
||||
- 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).
|
||||
- **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
|
||||
|
||||
@@ -286,7 +286,7 @@ Reference implementation: `template-chart/values.schema.json` and `template-char
|
||||
|
||||
### 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.
|
||||
- **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.
|
||||
|
||||
@@ -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 -
|
||||
# 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.
|
||||
INSTALL_TIMEOUT=900
|
||||
# first-boot migrations). mediathekarr adds ~850 MB of image and waits for
|
||||
# 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
|
||||
# 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
|
||||
# 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
|
||||
@@ -37,3 +37,11 @@ ingress:
|
||||
# in the CI cluster, and the install-test checks readiness, not routing.
|
||||
host: freesailarr.example.com
|
||||
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
|
||||
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
|
||||
# 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
|
||||
# optional, so it is omitted rather than filled with an arbitrary choice.
|
||||
|
||||
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
|
||||
# stack bundles nine independently versioned projects, so there is no single
|
||||
# "app version" to track here.
|
||||
# stack bundles ten independently versioned projects (nine of them on by
|
||||
# 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
|
||||
# values.yaml, where Renovate's helm-values manager proposes them. Nothing
|
||||
# rewrites this 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.
|
||||
# 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 - except
|
||||
# mediathekarr, which is pinned by digest and has its own customManagers block
|
||||
# in renovate.json5, because its tags carry no version. Nothing rewrites this
|
||||
# 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"
|
||||
|
||||
maintainers:
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{{- $sharedNames := list -}}
|
||||
{{- range $shared }}{{ $sharedNames = append $sharedNames .name }}{{ end -}}
|
||||
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
|
||||
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.
|
||||
{{- 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
|
||||
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,
|
||||
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:
|
||||
{{- 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
|
||||
listens on 8191 and therefore gets a service - but it has no ingress block in
|
||||
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" -}}
|
||||
{{- $apps := list -}}
|
||||
@@ -186,7 +194,8 @@ values and is never exposed from outside the cluster.
|
||||
(dict "name" "radarr" "port" 7878)
|
||||
(dict "name" "sonarr" "port" 8989)
|
||||
(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 -}}
|
||||
{{- $apps = append $apps $app -}}
|
||||
{{- 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".
|
||||
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
|
||||
therefore only live at the root.
|
||||
"/<app>", and to "/" for the two apps that can only live at the root - seerr,
|
||||
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>.
|
||||
*/}}
|
||||
@@ -226,7 +236,7 @@ Input: dict "root" $ "app" <app name>.
|
||||
{{- $path = $config.path -}}
|
||||
{{- end -}}
|
||||
{{- if or (kindIs "invalid" $path) (eq (toString $path) "") -}}
|
||||
{{- if eq .app "seerr" -}}
|
||||
{{- if has .app (list "seerr" "mediathekarr") -}}
|
||||
{{- $path = "/" -}}
|
||||
{{- else -}}
|
||||
{{- $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
|
||||
same validated list:
|
||||
|
||||
1. seerr under any path other than "/". seerr has no base-path support
|
||||
(upstream request open since May 2022), so a chart that rendered it under
|
||||
a sub-path would produce something that provably cannot work.
|
||||
1. seerr or mediathekarr under any path other than "/". seerr has no base-path
|
||||
support (upstream request open since May 2022) and MediathekArr's UI links
|
||||
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
|
||||
shared host can collide; apps with their own host are on a different host
|
||||
and are therefore not part of this check.
|
||||
@@ -269,11 +282,14 @@ same validated list:
|
||||
{{- $config := (index $root.Values $app.name).ingress -}}
|
||||
{{- if not ($config.host | default "") -}}
|
||||
{{- $path := include "freesailarr.appPath" (dict "root" $root "app" $app.name) -}}
|
||||
{{- if and (eq $app.name "seerr") (ne $path "/") -}}
|
||||
{{- 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) -}}
|
||||
{{- $rootOnly := dict
|
||||
"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 -}}
|
||||
{{- 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 -}}
|
||||
{{- $_ := set $seen $path $app.name -}}
|
||||
{{- $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 -}}
|
||||
{{- range $app := include "freesailarr.portApps" . | fromJsonArray -}}
|
||||
{{- $ports = append $ports (int $app.port) -}}
|
||||
{{- range $extra := $app.extraPorts | default list -}}
|
||||
{{- $ports = append $ports (int $extra.port) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $ports = append $ports 9999 -}}
|
||||
{{- 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
|
||||
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
|
||||
tv: sonarr, bazarr
|
||||
|
||||
@@ -375,7 +394,7 @@ Input: dict "root" $ "category" <"downloads"|"movies"|"tv">.
|
||||
{{- define "freesailarr.mediaCategoryEnabled" -}}
|
||||
{{- $v := .root.Values -}}
|
||||
{{- 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" -}}
|
||||
{{- if or $v.radarr.enabled $v.bazarr.enabled -}}true{{- end -}}
|
||||
{{- else if eq .category "tv" -}}
|
||||
@@ -885,6 +904,47 @@ bazarr:
|
||||
- FOWNER
|
||||
- SETGID
|
||||
- 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:
|
||||
resources:
|
||||
limitFactor: 3
|
||||
@@ -905,7 +965,7 @@ configInit:
|
||||
|
||||
{{/*
|
||||
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" -}}
|
||||
{{- $given := (fromYaml (include "freesailarr.subBlock" (dict "block" .Values.securityContext "key" "pod" "path" "securityContext"))).value -}}
|
||||
|
||||
@@ -21,5 +21,16 @@ spec:
|
||||
port: {{ int $app.port }}
|
||||
targetPort: {{ $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
|
||||
{{- end }}
|
||||
@@ -521,6 +521,109 @@ spec:
|
||||
subPath: {{ . | quote }}
|
||||
{{- 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
|
||||
volumes:
|
||||
# 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
|
||||
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") }}
|
||||
- name: media-downloads
|
||||
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 }}
|
||||
{{- if or .Values.radarr.enabled .Values.bazarr.enabled }}
|
||||
@@ -579,12 +682,16 @@ spec:
|
||||
- name: seerr-tmp
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- if .Values.mediathekarr.enabled }}
|
||||
- name: mediathekarr-tmp
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
# Config volumes for gluetun and the enabled apps. NOTE: volumeClaimTemplates
|
||||
# are immutable - toggling an app's enabled flag later requires recreating
|
||||
# the StatefulSet once (kubectl delete statefulset <name> --cascade=orphan
|
||||
# before upgrading; existing PVCs and their data survive).
|
||||
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 }}
|
||||
{{- if or (eq $app "gluetun") $config.enabled }}
|
||||
- metadata:
|
||||
|
||||
@@ -535,6 +535,76 @@
|
||||
"$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": {
|
||||
@@ -746,6 +816,27 @@
|
||||
"object",
|
||||
"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.
|
||||
#
|
||||
# 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
|
||||
# combines nine independently versioned upstream projects and none of them is
|
||||
# "the" one app version.
|
||||
# combines ten independently versioned upstream projects and none of them is
|
||||
# "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
|
||||
# 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
|
||||
# Jellyfin) - the chart mounts but never creates them. Each claim is required
|
||||
# as soon as an app that mounts it is enabled:
|
||||
# downloads: qbittorrent, radarr, sonarr
|
||||
# downloads: qbittorrent, radarr, sonarr, mediathekarr
|
||||
# movies: radarr, bazarr
|
||||
# tv: sonarr, bazarr
|
||||
#
|
||||
@@ -874,3 +875,259 @@ bazarr:
|
||||
- FOWNER
|
||||
- SETGID
|
||||
- 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
@@ -201,14 +201,48 @@
|
||||
//
|
||||
// When adding a chart, add a block here — see README "Creating a new chart".
|
||||
//
|
||||
// Exception, deliberately no block: `freesailarr`. It bundles nine
|
||||
// independently versioned upstreams, so none of them is "the" app version
|
||||
// and no image tag comes from appVersion — all nine are pinned as
|
||||
// Exception, deliberately no APP-VERSION block: `freesailarr`. It bundles
|
||||
// ten independently versioned upstreams, so none of them is "the" app
|
||||
// version and no image tag comes from appVersion — they are pinned as
|
||||
// image.repository/image.tag in its values.yaml, where the built-in
|
||||
// helm-values manager already finds them and proposes one update per
|
||||
// image. Adding a block here would either duplicate those updates or
|
||||
// rewrite an appVersion that selects nothing. Its appVersion is a
|
||||
// hand-maintained marker; see freesailarr/Chart.yaml.
|
||||
// image. Adding an appVersion block here would either duplicate those
|
||||
// updates or rewrite an appVersion that selects nothing. Its appVersion is
|
||||
// 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.",
|
||||
|
||||
Reference in new issue
Block a user