Adds a second, optional ingress shape and fixes the download target of qBittorrent. The existing per-app-host shape stays the default and renders unchanged. Shared domain (#167) Setting ingress.host moves every app without a host of its own into one Ingress object with one path each (<app>.ingress.path, default /<app>), one TLS secret and therefore one DNS name and one certificate for the whole stack. A per-app host always wins and keeps its own object, so mixed operation needs no extra values. The apps differ in what they can do under a sub-path, and the chart follows that rather than pretending they are alike: - radarr, sonarr, prowlarr get <APP>__SERVER__URLBASE from the chart, but only when their own env block does not already carry the variable. - bazarr has no environment variable for its base path and stays one documented manual step (Settings -> General -> Base URL). - qbittorrent has no base path at all and renders a second object on the same host with a prefix-stripping rewrite. It repeats the same TLS secret name and deliberately omits the cert-manager annotation, so both objects state their own TLS while only one certificate is requested. - seerr has no base path either and cannot be rewritten, so it is pinned to "/" and a fail refuses any other path. A second fail refuses two apps claiming the same path. qBittorrent save path (#166) Without Session\DefaultSavePath qBittorrent writes to its built-in default /config/Downloads - the 1Gi config volume. It fills within minutes, every torrent goes to "errored" and qBittorrent can no longer write its own metadata to BT_backup, so a restart in that state costs the torrent list. The init container (renamed qbittorrent-config-interface -> qbittorrent-config-seed, since it no longer only sets the interface) now seeds qbittorrent.savePath, default /downloads. Unlike the interface binding it is inserted ONLY when the key is absent and never replaced: a path changed in the WebUI is a legitimate operator decision and must survive a restart. This is the reason for the major bump: an installation that never set the path writes somewhere else after the upgrade. Documentation NOTES.txt and the media block in values.yaml now spell out why a shared claim with three subPaths does not produce hardlinks: the kernel compares the vfsmount in do_linkat, not the superblock, so two bind mounts of one filesystem are EXDEV - while stat reports the same device number on both sides and suggests the opposite. The layout that would work (one claim, one mount, categories as sub-directories) is named as a possibility; the chart cannot express it today. README.md gains the shared domain as the second admissible ingress shape of the style guideline.
603 lines
32 KiB
YAML
603 lines
32 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 }}
|
|
{{- 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 }}
|
|
{{- 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 }}
|
|
{{- 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 }}
|
|
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 }}
|
|
{{- 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 }}
|
|
{{- 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 }}
|
|
# 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 }}
|