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
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user