Files
helm-charts/freesailarr/templates/freesailarr.sfs.yaml
T
jklein a37588c2fe freesailarr: schema, null-safe fallbacks, seccompProfile, NOTES.txt
Retrofits the repo-wide groundwork that #59 deliberately left out to keep the
reviewed scope. Chart 2.0.0+up1.0.0 - a major, because seccompProfile changes
runtime behaviour; appVersion stays the freely chosen marker 1.0.0.

values.schema.json (#68). The largest schema in the repo: nine app blocks with
probes, resources, security contexts, env and image. additionalProperties is
false on every chart-owned level; the objects that go verbatim into the pod
spec stay open. It rejects a mistyped app name, a mistyped key at any depth and
ingress.domain - the key #59 removed, which until now would have been accepted
in silence.

Null safety (#99), both halves, because they cover different cases. The
defaultedDict helper catches a key WITH a chart default, where Helm's
coalescing deletes the null before the schema ever sees it; the widened
["object","null"] types catch a key WITHOUT one, where the null survives into
validation. Measured on the old chart: radarr.securityContext: null rendered
"securityContext: null" - the whole hardening gone - and
securityContext.capabilities: null dropped the capability block entirely, so
the container kept every default capability instead of dropping ALL. Both
render green.

The fallback table reproduces THIS chart's defaults including its deviations:
the five linuxserver.io containers keep runAsNonRoot unset and a writable root
filesystem so their s6 init can drop to PUID/PGID, flaresolverr keeps its
numerically pinned uid, gluetun keeps NET_ADMIN, and recyclarr keeps its exec
probes. Restoring the "clean" standard there would put the linuxserver
containers back on the image's uid 911 - the failure calibre-web-automated hit.
The sync between table and values.yaml is not left to discipline: rendering
with every block of the silent class explicitly set to null is byte-identical
to rendering the defaults, which is how a drift shows up.

seccompProfile RuntimeDefault (#84) in the pod context, so it covers the init
container too. Trivy's KSV-0104 drops from ten findings to none.

NOTES.txt (#43) covering what actually bites: the pod is not Ready until the
tunnel is up, the two fields the 1Password item needs, the warning when
firewallOutboundSubnets is unset, the immutable volumeClaimTemplates, and the
measured fact that imports do not hardlink across the media categories.

Also, from point 5 of the plan: checksum/config does not apply - the chart has
no ConfigMap - but checksum/secret-ref does, and was missing. Helm never sees
the secret's contents, yet it does see which op:// item the OnePasswordItem
points at, so repointing secrets.gluetunSecretRef now restarts the pod instead
of leaving the old credentials running.

README: the #43 status line was stale (it still claimed only homeassistant
ships NOTES.txt), and the mandatory-icon rule now records freesailarr as a
documented exception - #59 dropped the icon and did not amend the rule.
2026-08-24 16:06:20 +02:00

530 lines
27 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 .Values.qbittorrent.networkInterface }}
initContainers:
# qBittorrent has no environment variable for the session's network
# interface - it only reads it from the persisted config file, which
# the app itself creates on first 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. Seeding the two keys before qbittorrent
# starts closes that leak and makes a fresh install work out of the
# box. The edit is idempotent and preserves all other settings:
# existing keys are replaced, missing ones are inserted into the
# [BitTorrent] section (qBittorrent 5.x reads them there), and a
# missing file or section is created.
- name: qbittorrent-config-interface
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"
# 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"
env:
- name: QBT_INTERFACE
value: {{ .Values.qbittorrent.networkInterface | quote }}
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: media-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 }}
{{- 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 }}
{{- 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: media-movies
{{- with .Values.media.movies.subPath }}
subPath: {{ . | quote }}
{{- end }}
- mountPath: /downloads
name: media-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 }}
{{- 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: media-tv
{{- with .Values.media.tv.subPath }}
subPath: {{ . | quote }}
{{- end }}
- mountPath: /downloads
name: media-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: media-movies
{{- with .Values.media.movies.subPath }}
subPath: {{ . | quote }}
{{- end }}
- mountPath: /tv
name: media-tv
{{- with .Values.media.tv.subPath }}
subPath: {{ . | quote }}
{{- end }}
{{- 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: {}
{{- if or .Values.qbittorrent.enabled .Values.radarr.enabled .Values.sonarr.enabled }}
- name: media-downloads
persistentVolumeClaim:
claimName: {{ required "media.downloads.existingClaim is required when qbittorrent, radarr or sonarr is enabled" .Values.media.downloads.existingClaim }}
{{- end }}
{{- if or .Values.radarr.enabled .Values.bazarr.enabled }}
- name: media-movies
persistentVolumeClaim:
claimName: {{ required "media.movies.existingClaim is required when radarr or bazarr is enabled" .Values.media.movies.existingClaim }}
{{- end }}
{{- if or .Values.sonarr.enabled .Values.bazarr.enabled }}
- name: media-tv
persistentVolumeClaim:
claimName: {{ required "media.tv.existingClaim is required when sonarr or bazarr is enabled" .Values.media.tv.existingClaim }}
{{- 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 }}
# 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" }}
{{- $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 }}