Collapses the eight open Renovate PRs for this stack into one change, and answers what two of them turned out to carry. The eight tags: qbittorrent 5.2.3 -> 5.2.4, bazarr 1.6.0 -> 1.6.2, sonarr 4.0.19 -> 4.0.20, flaresolverr v3.5.0 -> v3.5.2, recyclarr 8.7.1 -> 8.7.3, prowlarr 2.5.2 -> 2.6.5, radarr 6.3.0 -> 6.4.4, seerr v3.4.1 -> v3.5.0. gluetun, the busybox init image and the digest-pinned mediathekarr have no update pending and are untouched. Major rather than patch, because prowlarr 2.6.3 and radarr 6.4.3 are not plain bumps: they removed the hard-coded trusted proxy networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7, fe80::/10, read in Startup.cs at the previously pinned tags) and replaced them with a TrustedNetworks setting whose own default is string.Empty - ForwardedHeadersConfigurator returns before registering a single KnownNetwork while it is empty. Behind an in-cluster ingress controller that means the forwarded headers are ignored: the client address the app reports is the controller's pod IP, the scheme reads as http behind TLS termination, and under "Disabled for Local Addresses" a proxied request stops counting as local. So a <app>.trustedNetworks value is added for prowlarr, radarr AND sonarr, rendered as <APP>__SERVER__TRUSTEDNETWORKS and only when non-empty - an empty variable cannot be told apart from an unset one by the app, but looks handled to a reader of the manifest. The env form was verified per app rather than inferred: all three bind ServerOptions to <App>:Server next to a plain AddEnvironmentVariables(). A helper suppresses the chart's entry when the app's own env block already carries the variable, for the reason the UrlBase helper states - two identically named variables in one container are not a defined state. sonarr is included because it is already in the empty state: 4.0.19, the version this chart shipped before, has no hard-coded networks either. It arrived in what the convention classes as a plain app-version bump, so nothing reviewed or documented it. Repairing two and knowingly leaving the third would be the same silent pattern. The default is empty, and this therefore does NOT restore the behaviour from before the upgrade. That is deliberate and follows upstream's reason - trusting X-Forwarded-For from any private network is a spoofing vector - and it is made visible instead of reinstated: NOTES.txt warns in exactly the combination that costs something, the app published through an Ingress or an HTTPRoute and the value empty, and stays quiet otherwise. AllowedHosts, added by the same upstream releases, is deliberately not exposed. Its default is string.Empty as well and the app then accepts any host, so there is nothing to repair; the trap is documented instead, because if it is ever set the app appends only localhost, 127.0.0.1, [::1] and its own hostname - neither the pod IP nor the Service DNS name - and the kubelet probes the pod by IP, which an incomplete list answers with HTTP 400. At default values the rendered manifest changes in exactly eight lines, the image tags; the new value renders nothing until it is set.
730 lines
39 KiB
YAML
730 lines
39 KiB
YAML
apiVersion: apps/v1
|
|
kind: StatefulSet
|
|
metadata:
|
|
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
|
labels:
|
|
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
|
spec:
|
|
replicas: {{ include "freesailarr.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.replicas) }}
|
|
# Governing service name. Each app has its own service (see
|
|
# freesailarr.service.yaml); nothing consumes the per-pod DNS name a
|
|
# governing service would provide, so the chart deliberately creates no
|
|
# object under this name - the field only has to be set.
|
|
serviceName: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
|
selector:
|
|
matchLabels:
|
|
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
|
template:
|
|
metadata:
|
|
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
|
annotations:
|
|
# There is no ConfigMap in this chart, so checksum/config does not
|
|
# apply. What does apply is the op:// path: Helm never sees the secret's
|
|
# contents - the 1Password operator fills the Secret in the cluster -
|
|
# but it does see which item the OnePasswordItem points at. Repointing
|
|
# secrets.gluetunSecretRef at a different vault item therefore takes
|
|
# effect instead of silently leaving the old credentials in the running
|
|
# pod. The name differs from checksum/config on purpose: that one
|
|
# promises "the config changed", this one only "the config source
|
|
# changed" (#82).
|
|
checksum/secret-ref: {{ include (print $.Template.BasePath "/freesailarr-gluetun.secret.yaml") . | sha256sum }}
|
|
labels:
|
|
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
|
spec:
|
|
automountServiceAccountToken: false
|
|
securityContext:
|
|
{{- include "freesailarr.podSecurityContext" . | nindent 8 }}
|
|
{{- if and .Values.qbittorrent.enabled (or .Values.qbittorrent.networkInterface .Values.qbittorrent.savePath) }}
|
|
initContainers:
|
|
# qBittorrent reads neither the session's network interface nor its
|
|
# default save path from the environment - both live only in the
|
|
# persisted config file, which the app itself creates on first start.
|
|
# This container seeds them before qbittorrent starts, so a fresh
|
|
# install works out of the box. Both edits preserve all other settings:
|
|
# keys are inserted into the [BitTorrent] section (qBittorrent 5.x
|
|
# reads them there), and a missing file or section is created.
|
|
#
|
|
# The two keys are handled DIFFERENTLY on purpose:
|
|
#
|
|
# - Session\Interface is ENFORCED on every start. Without the binding
|
|
# libtorrent listens and announces on every interface of the shared
|
|
# pod network namespace (eth0/pod IP and loopback included);
|
|
# gluetun's kill switch drops those packets, trackers stay
|
|
# unreachable and torrents stall at 0 bytes. A deviating value is a
|
|
# leak, so an existing key is replaced.
|
|
# - Session\DefaultSavePath is only INSERTED WHEN ABSENT, never
|
|
# replaced. Without it qBittorrent falls back to /config/Downloads,
|
|
# i.e. the small config volume, which fills within minutes and puts
|
|
# every torrent into "errored" (#166). But where downloads go is a
|
|
# legitimate operator decision: someone who changes the path in the
|
|
# WebUI must not have it overwritten on every pod restart.
|
|
- name: qbittorrent-config-seed
|
|
image: "{{ .Values.qbittorrent.configInit.image.repository }}:{{ .Values.qbittorrent.configInit.image.tag }}"
|
|
imagePullPolicy: IfNotPresent
|
|
command:
|
|
- sh
|
|
- -c
|
|
- |
|
|
set -eu
|
|
conf=/config/qBittorrent/qBittorrent.conf
|
|
mkdir -p /config/qBittorrent
|
|
[ -f "$conf" ] || printf '[BitTorrent]\n' > "$conf"
|
|
# A file without a trailing newline would glue an appended
|
|
# section header onto the last key.
|
|
if [ -s "$conf" ] && [ -n "$(tail -c 1 "$conf")" ]; then
|
|
printf '\n' >> "$conf"
|
|
fi
|
|
grep -q '^\[BitTorrent\]$' "$conf" || printf '[BitTorrent]\n' >> "$conf"
|
|
{{- if .Values.qbittorrent.networkInterface }}
|
|
# Drop any existing binding keys, then re-insert them directly
|
|
# after the [BitTorrent] header.
|
|
awk -v ifc="$QBT_INTERFACE" '
|
|
/^Session\\Interface=/ { next }
|
|
/^Session\\InterfaceName=/ { next }
|
|
{ print }
|
|
/^\[BitTorrent\]$/ {
|
|
print "Session\\Interface=" ifc
|
|
print "Session\\InterfaceName=" ifc
|
|
}
|
|
' "$conf" > "${conf}.tmp"
|
|
mv "${conf}.tmp" "$conf"
|
|
echo "qBittorrent session bound to interface $QBT_INTERFACE"
|
|
{{- end }}
|
|
{{- if .Values.qbittorrent.savePath }}
|
|
# Insert only when the key is absent - an existing value is the
|
|
# operator's and stays untouched.
|
|
if grep -q '^Session\\DefaultSavePath=' "$conf"; then
|
|
echo "qBittorrent save path already set, left untouched: $(grep -m1 '^Session\\DefaultSavePath=' "$conf")"
|
|
else
|
|
awk -v sp="$QBT_SAVE_PATH" '
|
|
{ print }
|
|
/^\[BitTorrent\]$/ { print "Session\\DefaultSavePath=" sp }
|
|
' "$conf" > "${conf}.tmp"
|
|
mv "${conf}.tmp" "$conf"
|
|
echo "qBittorrent default save path seeded: $QBT_SAVE_PATH"
|
|
fi
|
|
{{- end }}
|
|
env:
|
|
{{- if .Values.qbittorrent.networkInterface }}
|
|
- name: QBT_INTERFACE
|
|
value: {{ .Values.qbittorrent.networkInterface | quote }}
|
|
{{- end }}
|
|
{{- if .Values.qbittorrent.savePath }}
|
|
- name: QBT_SAVE_PATH
|
|
value: {{ .Values.qbittorrent.savePath | quote }}
|
|
{{- end }}
|
|
resources:
|
|
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "configInit" "given" .Values.qbittorrent.configInit.resources) | nindent 12 }}
|
|
securityContext:
|
|
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "configInit" "given" .Values.qbittorrent.configInit.securityContext) | nindent 12 }}
|
|
volumeMounts:
|
|
- mountPath: /config
|
|
name: qbittorrent-config
|
|
{{- end }}
|
|
containers:
|
|
# gluetun is the VPN gateway: all app containers share the pod's
|
|
# network namespace, talk to each other over localhost and reach the
|
|
# internet only through the Wireguard tunnel (gluetun's firewall acts
|
|
# as kill switch).
|
|
- name: gluetun
|
|
image: "{{ .Values.gluetun.image.repository }}:{{ .Values.gluetun.image.tag }}"
|
|
imagePullPolicy: IfNotPresent
|
|
# An uncleanly exited gluetun (crash, OOM killer, node issue) leaves
|
|
# its ip rule, routing table and tun device behind in the shared pod
|
|
# network namespace, and the restarted container then fails forever
|
|
# ("adding ip rule 101 ... file exists" / silent tunnel breakage)
|
|
# while the kill switch blocks all egress. The upstream-recommended
|
|
# postStart hook is racy (it runs concurrently with the entrypoint
|
|
# and can delete the rule the new instance just created), so the
|
|
# stale state is cleaned synchronously before gluetun starts.
|
|
command:
|
|
- /bin/sh
|
|
- -c
|
|
- 'while ip rule del table 51820 2>/dev/null; do :; done; while ip -6 rule del table 51820 2>/dev/null; do :; done; ip link del tun0 2>/dev/null; exec /gluetun-entrypoint'
|
|
env:
|
|
- name: TZ
|
|
value: {{ .Values.timezone | quote }}
|
|
- name: VPN_SERVICE_PROVIDER
|
|
value: {{ .Values.gluetun.vpn.serviceProvider | quote }}
|
|
- name: VPN_TYPE
|
|
value: {{ .Values.gluetun.vpn.type | quote }}
|
|
- name: WIREGUARD_PRIVATE_KEY
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: {{ .Release.Name }}-{{ .Chart.Name }}-gluetun-secret
|
|
key: wireguard-private-key
|
|
- name: WIREGUARD_ADDRESSES
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: {{ .Release.Name }}-{{ .Chart.Name }}-gluetun-secret
|
|
key: wireguard-addresses
|
|
{{- with .Values.gluetun.vpn.serverCities }}
|
|
- name: SERVER_CITIES
|
|
value: {{ . | quote }}
|
|
{{- end }}
|
|
{{- with .Values.gluetun.vpn.serverCountries }}
|
|
- name: SERVER_COUNTRIES
|
|
value: {{ . | quote }}
|
|
{{- end }}
|
|
{{- with .Values.gluetun.vpn.dnsUpstreamResolvers }}
|
|
- name: DNS_UPSTREAM_RESOLVERS
|
|
value: {{ . | quote }}
|
|
{{- end }}
|
|
{{- with .Values.gluetun.vpn.firewallOutboundSubnets }}
|
|
- name: FIREWALL_OUTBOUND_SUBNETS
|
|
value: {{ . | quote }}
|
|
{{- end }}
|
|
# Declare the UI/probe ports of the enabled apps and the health
|
|
# server as reachable. Note that inside a pod network gluetun's own
|
|
# pod-subnet rule already accepts this traffic and these per-port
|
|
# rules never match (measured - see the helper's comment); they are
|
|
# the explicit statement of intent and the fallback if that subnet
|
|
# detection ever stops covering the pod IP.
|
|
- name: FIREWALL_INPUT_PORTS
|
|
value: {{ include "freesailarr.firewallInputPorts" . | quote }}
|
|
# Bind the health server to all interfaces so the kubelet HTTP
|
|
# probes (which connect to the pod IP, not localhost) reach it.
|
|
- name: HEALTH_SERVER_ADDRESS
|
|
value: ":9999"
|
|
{{- include "freesailarr.env" (dict "root" $ "env" .Values.gluetun.env) | nindent 12 }}
|
|
ports:
|
|
- containerPort: 9999
|
|
protocol: TCP
|
|
name: health
|
|
livenessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "gluetun" "kind" "livenessProbe" "given" .Values.gluetun.livenessProbe) | nindent 12 }}
|
|
readinessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "gluetun" "kind" "readinessProbe" "given" .Values.gluetun.readinessProbe) | nindent 12 }}
|
|
startupProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "gluetun" "kind" "startupProbe" "given" .Values.gluetun.startupProbe) | nindent 12 }}
|
|
resources:
|
|
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "gluetun" "given" .Values.gluetun.resources) | nindent 12 }}
|
|
securityContext:
|
|
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "gluetun" "given" .Values.gluetun.securityContext) | nindent 12 }}
|
|
volumeMounts:
|
|
- mountPath: /gluetun
|
|
name: gluetun-config
|
|
- mountPath: /dev/net/tun
|
|
name: dev-net-tun
|
|
# gluetun creates /tmp/gluetun (its public-IP cache) mode 0644,
|
|
# i.e. without the execute bit it needs to enter its own
|
|
# directory. It runs as root but with all capabilities except
|
|
# NET_ADMIN dropped, so DAC_OVERRIDE no longer papers over that
|
|
# and every write fails with "clearing public IP data: open
|
|
# /tmp/gluetun/ip: permission denied". An emptyDir provides the
|
|
# path with usable permissions.
|
|
# Remaining, cosmetic: gluetun also chowns the file, which needs
|
|
# CAP_CHOWN. With all capabilities but NET_ADMIN dropped that call
|
|
# is denied and logs "clearing public IP data: chown
|
|
# /tmp/gluetun/ip: operation not permitted" on every VPN restart.
|
|
# The file itself is written and the public IP is persisted
|
|
# (verified at runtime), so this is a log line, not a failure -
|
|
# granting CHOWN just to silence it would widen the container's
|
|
# capabilities for nothing.
|
|
- mountPath: /tmp/gluetun
|
|
name: gluetun-tmp
|
|
{{- if .Values.qbittorrent.enabled }}
|
|
- name: qbittorrent
|
|
image: "{{ .Values.qbittorrent.image.repository }}:{{ .Values.qbittorrent.image.tag }}"
|
|
imagePullPolicy: IfNotPresent
|
|
env:
|
|
- name: PUID
|
|
value: {{ .Values.puid | quote }}
|
|
- name: PGID
|
|
value: {{ .Values.pgid | quote }}
|
|
- name: TZ
|
|
value: {{ .Values.timezone | quote }}
|
|
- name: WEBUI_PORT
|
|
value: "8080"
|
|
{{- include "freesailarr.env" (dict "root" $ "env" .Values.qbittorrent.env) | nindent 12 }}
|
|
ports:
|
|
- containerPort: 8080
|
|
protocol: TCP
|
|
name: qbittorrent
|
|
livenessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "qbittorrent" "kind" "livenessProbe" "given" .Values.qbittorrent.livenessProbe) | nindent 12 }}
|
|
readinessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "qbittorrent" "kind" "readinessProbe" "given" .Values.qbittorrent.readinessProbe) | nindent 12 }}
|
|
startupProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "qbittorrent" "kind" "startupProbe" "given" .Values.qbittorrent.startupProbe) | nindent 12 }}
|
|
resources:
|
|
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "qbittorrent" "given" .Values.qbittorrent.resources) | nindent 12 }}
|
|
securityContext:
|
|
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "qbittorrent" "given" .Values.qbittorrent.securityContext) | nindent 12 }}
|
|
volumeMounts:
|
|
- mountPath: /config
|
|
name: qbittorrent-config
|
|
- mountPath: /downloads
|
|
name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "downloads") }}
|
|
{{- with .Values.media.downloads.subPath }}
|
|
subPath: {{ . | quote }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- if .Values.prowlarr.enabled }}
|
|
- name: prowlarr
|
|
image: "{{ .Values.prowlarr.image.repository }}:{{ .Values.prowlarr.image.tag }}"
|
|
imagePullPolicy: IfNotPresent
|
|
env:
|
|
- name: PUID
|
|
value: {{ .Values.puid | quote }}
|
|
- name: PGID
|
|
value: {{ .Values.pgid | quote }}
|
|
- name: TZ
|
|
value: {{ .Values.timezone | quote }}
|
|
{{- /* Base path for the shared-domain mode: the chart sets
|
|
UrlBase itself as soon as prowlarr renders under a
|
|
sub-path, unless the env block above already carries
|
|
the variable - see the freesailarr.urlBase helper. */}}
|
|
{{- with (include "freesailarr.urlBase" (dict "root" $ "app" "prowlarr")) }}
|
|
- name: PROWLARR__SERVER__URLBASE
|
|
value: {{ . | quote }}
|
|
{{- end }}
|
|
{{- /* Proxy networks whose forwarded headers prowlarr honours.
|
|
Emitted only when set - empty is the default and means no
|
|
proxy is trusted. See the freesailarr.trustedNetworks
|
|
helper and the value's comment in values.yaml. */}}
|
|
{{- with (include "freesailarr.trustedNetworks" (dict "root" $ "app" "prowlarr")) }}
|
|
- name: PROWLARR__SERVER__TRUSTEDNETWORKS
|
|
value: {{ . | quote }}
|
|
{{- end }}
|
|
{{- include "freesailarr.env" (dict "root" $ "env" .Values.prowlarr.env) | nindent 12 }}
|
|
ports:
|
|
- containerPort: 9696
|
|
protocol: TCP
|
|
name: prowlarr
|
|
livenessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "prowlarr" "kind" "livenessProbe" "given" .Values.prowlarr.livenessProbe) | nindent 12 }}
|
|
readinessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "prowlarr" "kind" "readinessProbe" "given" .Values.prowlarr.readinessProbe) | nindent 12 }}
|
|
startupProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "prowlarr" "kind" "startupProbe" "given" .Values.prowlarr.startupProbe) | nindent 12 }}
|
|
resources:
|
|
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "prowlarr" "given" .Values.prowlarr.resources) | nindent 12 }}
|
|
securityContext:
|
|
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "prowlarr" "given" .Values.prowlarr.securityContext) | nindent 12 }}
|
|
volumeMounts:
|
|
- mountPath: /config
|
|
name: prowlarr-config
|
|
{{- end }}
|
|
{{- if .Values.flaresolverr.enabled }}
|
|
- name: flaresolverr
|
|
image: "{{ .Values.flaresolverr.image.repository }}:{{ .Values.flaresolverr.image.tag }}"
|
|
imagePullPolicy: IfNotPresent
|
|
env:
|
|
- name: TZ
|
|
value: {{ .Values.timezone | quote }}
|
|
{{- include "freesailarr.env" (dict "root" $ "env" .Values.flaresolverr.env) | nindent 12 }}
|
|
ports:
|
|
- containerPort: 8191
|
|
protocol: TCP
|
|
name: flaresolverr
|
|
livenessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "flaresolverr" "kind" "livenessProbe" "given" .Values.flaresolverr.livenessProbe) | nindent 12 }}
|
|
readinessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "flaresolverr" "kind" "readinessProbe" "given" .Values.flaresolverr.readinessProbe) | nindent 12 }}
|
|
startupProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "flaresolverr" "kind" "startupProbe" "given" .Values.flaresolverr.startupProbe) | nindent 12 }}
|
|
resources:
|
|
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "flaresolverr" "given" .Values.flaresolverr.resources) | nindent 12 }}
|
|
securityContext:
|
|
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "flaresolverr" "given" .Values.flaresolverr.securityContext) | nindent 12 }}
|
|
volumeMounts:
|
|
# Writable paths for headless chromium so the root filesystem
|
|
# stays read-only; /dev/shm is a memory-backed emptyDir because
|
|
# chromium needs more shared memory than the container default.
|
|
- mountPath: /tmp
|
|
name: flaresolverr-tmp
|
|
- mountPath: /dev/shm
|
|
name: flaresolverr-shm
|
|
{{- end }}
|
|
{{- if .Values.radarr.enabled }}
|
|
- name: radarr
|
|
image: "{{ .Values.radarr.image.repository }}:{{ .Values.radarr.image.tag }}"
|
|
imagePullPolicy: IfNotPresent
|
|
env:
|
|
- name: PUID
|
|
value: {{ .Values.puid | quote }}
|
|
- name: PGID
|
|
value: {{ .Values.pgid | quote }}
|
|
- name: TZ
|
|
value: {{ .Values.timezone | quote }}
|
|
{{- /* Base path for the shared-domain mode: the chart sets
|
|
UrlBase itself as soon as radarr renders under a
|
|
sub-path, unless the env block above already carries
|
|
the variable - see the freesailarr.urlBase helper. */}}
|
|
{{- with (include "freesailarr.urlBase" (dict "root" $ "app" "radarr")) }}
|
|
- name: RADARR__SERVER__URLBASE
|
|
value: {{ . | quote }}
|
|
{{- end }}
|
|
{{- /* See the prowlarr TRUSTEDNETWORKS comment above. */}}
|
|
{{- with (include "freesailarr.trustedNetworks" (dict "root" $ "app" "radarr")) }}
|
|
- name: RADARR__SERVER__TRUSTEDNETWORKS
|
|
value: {{ . | quote }}
|
|
{{- end }}
|
|
{{- include "freesailarr.env" (dict "root" $ "env" .Values.radarr.env) | nindent 12 }}
|
|
ports:
|
|
- containerPort: 7878
|
|
protocol: TCP
|
|
name: radarr
|
|
livenessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "radarr" "kind" "livenessProbe" "given" .Values.radarr.livenessProbe) | nindent 12 }}
|
|
readinessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "radarr" "kind" "readinessProbe" "given" .Values.radarr.readinessProbe) | nindent 12 }}
|
|
startupProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "radarr" "kind" "startupProbe" "given" .Values.radarr.startupProbe) | nindent 12 }}
|
|
resources:
|
|
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "radarr" "given" .Values.radarr.resources) | nindent 12 }}
|
|
securityContext:
|
|
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "radarr" "given" .Values.radarr.securityContext) | nindent 12 }}
|
|
volumeMounts:
|
|
- mountPath: /config
|
|
name: radarr-config
|
|
- mountPath: /movies
|
|
name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "movies") }}
|
|
{{- with .Values.media.movies.subPath }}
|
|
subPath: {{ . | quote }}
|
|
{{- end }}
|
|
- mountPath: /downloads
|
|
name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "downloads") }}
|
|
{{- with .Values.media.downloads.subPath }}
|
|
subPath: {{ . | quote }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- if .Values.recyclarr.enabled }}
|
|
- name: recyclarr
|
|
image: "{{ .Values.recyclarr.image.repository }}:{{ .Values.recyclarr.image.tag }}"
|
|
imagePullPolicy: IfNotPresent
|
|
env:
|
|
- name: TZ
|
|
value: {{ .Values.timezone | quote }}
|
|
- name: RECYCLARR_CREATE_CONFIG
|
|
value: "true"
|
|
{{- include "freesailarr.env" (dict "root" $ "env" .Values.recyclarr.env) | nindent 12 }}
|
|
livenessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "recyclarr" "kind" "livenessProbe" "given" .Values.recyclarr.livenessProbe) | nindent 12 }}
|
|
readinessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "recyclarr" "kind" "readinessProbe" "given" .Values.recyclarr.readinessProbe) | nindent 12 }}
|
|
startupProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "recyclarr" "kind" "startupProbe" "given" .Values.recyclarr.startupProbe) | nindent 12 }}
|
|
resources:
|
|
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "recyclarr" "given" .Values.recyclarr.resources) | nindent 12 }}
|
|
securityContext:
|
|
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "recyclarr" "given" .Values.recyclarr.securityContext) | nindent 12 }}
|
|
volumeMounts:
|
|
- mountPath: /config
|
|
name: recyclarr-config
|
|
# Writable scratch space for the .NET runtime so the root
|
|
# filesystem stays read-only.
|
|
- mountPath: /tmp
|
|
name: recyclarr-tmp
|
|
{{- end }}
|
|
{{- if .Values.sonarr.enabled }}
|
|
- name: sonarr
|
|
image: "{{ .Values.sonarr.image.repository }}:{{ .Values.sonarr.image.tag }}"
|
|
imagePullPolicy: IfNotPresent
|
|
env:
|
|
- name: PUID
|
|
value: {{ .Values.puid | quote }}
|
|
- name: PGID
|
|
value: {{ .Values.pgid | quote }}
|
|
- name: TZ
|
|
value: {{ .Values.timezone | quote }}
|
|
{{- /* Base path for the shared-domain mode: the chart sets
|
|
UrlBase itself as soon as sonarr renders under a
|
|
sub-path, unless the env block above already carries
|
|
the variable - see the freesailarr.urlBase helper. */}}
|
|
{{- with (include "freesailarr.urlBase" (dict "root" $ "app" "sonarr")) }}
|
|
- name: SONARR__SERVER__URLBASE
|
|
value: {{ . | quote }}
|
|
{{- end }}
|
|
{{- /* See the prowlarr TRUSTEDNETWORKS comment above. Sonarr did
|
|
not lose the trust with this chart version - 4.0.19 was
|
|
already in the empty state. */}}
|
|
{{- with (include "freesailarr.trustedNetworks" (dict "root" $ "app" "sonarr")) }}
|
|
- name: SONARR__SERVER__TRUSTEDNETWORKS
|
|
value: {{ . | quote }}
|
|
{{- end }}
|
|
{{- include "freesailarr.env" (dict "root" $ "env" .Values.sonarr.env) | nindent 12 }}
|
|
ports:
|
|
- containerPort: 8989
|
|
protocol: TCP
|
|
name: sonarr
|
|
livenessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "sonarr" "kind" "livenessProbe" "given" .Values.sonarr.livenessProbe) | nindent 12 }}
|
|
readinessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "sonarr" "kind" "readinessProbe" "given" .Values.sonarr.readinessProbe) | nindent 12 }}
|
|
startupProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "sonarr" "kind" "startupProbe" "given" .Values.sonarr.startupProbe) | nindent 12 }}
|
|
resources:
|
|
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "sonarr" "given" .Values.sonarr.resources) | nindent 12 }}
|
|
securityContext:
|
|
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "sonarr" "given" .Values.sonarr.securityContext) | nindent 12 }}
|
|
volumeMounts:
|
|
- mountPath: /config
|
|
name: sonarr-config
|
|
- mountPath: /tv
|
|
name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "tv") }}
|
|
{{- with .Values.media.tv.subPath }}
|
|
subPath: {{ . | quote }}
|
|
{{- end }}
|
|
- mountPath: /downloads
|
|
name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "downloads") }}
|
|
{{- with .Values.media.downloads.subPath }}
|
|
subPath: {{ . | quote }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- if .Values.seerr.enabled }}
|
|
- name: seerr
|
|
image: "{{ .Values.seerr.image.repository }}:{{ .Values.seerr.image.tag }}"
|
|
imagePullPolicy: IfNotPresent
|
|
env:
|
|
- name: TZ
|
|
value: {{ .Values.timezone | quote }}
|
|
{{- include "freesailarr.env" (dict "root" $ "env" .Values.seerr.env) | nindent 12 }}
|
|
ports:
|
|
- containerPort: 5055
|
|
protocol: TCP
|
|
name: seerr
|
|
livenessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "seerr" "kind" "livenessProbe" "given" .Values.seerr.livenessProbe) | nindent 12 }}
|
|
readinessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "seerr" "kind" "readinessProbe" "given" .Values.seerr.readinessProbe) | nindent 12 }}
|
|
startupProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "seerr" "kind" "startupProbe" "given" .Values.seerr.startupProbe) | nindent 12 }}
|
|
resources:
|
|
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "seerr" "given" .Values.seerr.resources) | nindent 12 }}
|
|
securityContext:
|
|
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "seerr" "given" .Values.seerr.securityContext) | nindent 12 }}
|
|
volumeMounts:
|
|
- mountPath: /app/config
|
|
name: seerr-config
|
|
- mountPath: /tmp
|
|
name: seerr-tmp
|
|
{{- end }}
|
|
{{- if .Values.bazarr.enabled }}
|
|
- name: bazarr
|
|
image: "{{ .Values.bazarr.image.repository }}:{{ .Values.bazarr.image.tag }}"
|
|
imagePullPolicy: IfNotPresent
|
|
env:
|
|
- name: PUID
|
|
value: {{ .Values.puid | quote }}
|
|
- name: PGID
|
|
value: {{ .Values.pgid | quote }}
|
|
- name: TZ
|
|
value: {{ .Values.timezone | quote }}
|
|
{{- include "freesailarr.env" (dict "root" $ "env" .Values.bazarr.env) | nindent 12 }}
|
|
ports:
|
|
- containerPort: 6767
|
|
protocol: TCP
|
|
name: bazarr
|
|
livenessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "bazarr" "kind" "livenessProbe" "given" .Values.bazarr.livenessProbe) | nindent 12 }}
|
|
readinessProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "bazarr" "kind" "readinessProbe" "given" .Values.bazarr.readinessProbe) | nindent 12 }}
|
|
startupProbe:
|
|
{{- include "freesailarr.probeFor" (dict "root" $ "app" "bazarr" "kind" "startupProbe" "given" .Values.bazarr.startupProbe) | nindent 12 }}
|
|
resources:
|
|
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "bazarr" "given" .Values.bazarr.resources) | nindent 12 }}
|
|
securityContext:
|
|
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "bazarr" "given" .Values.bazarr.securityContext) | nindent 12 }}
|
|
volumeMounts:
|
|
- mountPath: /config
|
|
name: bazarr-config
|
|
- mountPath: /movies
|
|
name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "movies") }}
|
|
{{- with .Values.media.movies.subPath }}
|
|
subPath: {{ . | quote }}
|
|
{{- end }}
|
|
- mountPath: /tv
|
|
name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "tv") }}
|
|
{{- with .Values.media.tv.subPath }}
|
|
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.
|
|
- name: dev-net-tun
|
|
hostPath:
|
|
path: /dev/net/tun
|
|
type: CharDevice
|
|
# Writable /tmp/gluetun for gluetun's public-IP cache (see the mount).
|
|
- name: gluetun-tmp
|
|
emptyDir: {}
|
|
{{- /*
|
|
One volume entry per distinct media claim, not per category: several
|
|
categories may point at the same existingClaim, and a pod that
|
|
references one claim twice never starts (#164). The entry belongs to the
|
|
first enabled category that carries the claim; the others mount the same
|
|
entry with their own subPath - see the mediaVolumeName helper for the
|
|
ordering rule. Deliberately a template comment and not a "#" one like
|
|
its neighbours: it must not appear in the rendered manifest, so that a
|
|
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 .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, sonarr or mediathekarr is enabled" .Values.media.downloads.existingClaim }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- if or .Values.radarr.enabled .Values.bazarr.enabled }}
|
|
{{- if include "freesailarr.mediaVolumeOwned" (dict "root" $ "category" "movies") }}
|
|
- name: media-movies
|
|
persistentVolumeClaim:
|
|
claimName: {{ required "media.movies.existingClaim is required when radarr or bazarr is enabled" .Values.media.movies.existingClaim }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- if or .Values.sonarr.enabled .Values.bazarr.enabled }}
|
|
{{- if include "freesailarr.mediaVolumeOwned" (dict "root" $ "category" "tv") }}
|
|
- name: media-tv
|
|
persistentVolumeClaim:
|
|
claimName: {{ required "media.tv.existingClaim is required when sonarr or bazarr is enabled" .Values.media.tv.existingClaim }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- if .Values.flaresolverr.enabled }}
|
|
- name: flaresolverr-tmp
|
|
emptyDir: {}
|
|
- name: flaresolverr-shm
|
|
emptyDir:
|
|
medium: Memory
|
|
sizeLimit: 512Mi
|
|
{{- end }}
|
|
{{- if .Values.recyclarr.enabled }}
|
|
- name: recyclarr-tmp
|
|
emptyDir: {}
|
|
{{- end }}
|
|
{{- if .Values.seerr.enabled }}
|
|
- 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" "mediathekarr" }}
|
|
{{- $config := index $.Values $app }}
|
|
{{- if or (eq $app "gluetun") $config.enabled }}
|
|
- metadata:
|
|
name: {{ $app }}-config
|
|
spec:
|
|
accessModes:
|
|
- ReadWriteOnce
|
|
resources:
|
|
requests:
|
|
storage: {{ $config.storage.size }}
|
|
{{- with $.Values.storage.storageClass }}
|
|
storageClassName: {{ . }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end }}
|