freesailarr: shared domain with sub-paths, qBittorrent save path (3.0.0+up1.0.0)
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.
This commit is contained in:
8 files changed
+559
-46
No files matched your search
@@ -6,6 +6,10 @@
|
||||
{{- $budget := mul (int $startup.periodSeconds) (int $startup.failureThreshold) -}}
|
||||
{{- $ingressApps := include "freesailarr.ingressApps" . | fromJsonArray -}}
|
||||
{{- $portApps := include "freesailarr.portApps" . | fromJsonArray -}}
|
||||
{{- $sharedHost := .Values.ingress.host | default "" -}}
|
||||
{{- $shared := include "freesailarr.sharedIngressApps" . | fromJsonArray -}}
|
||||
{{- $sharedNames := list -}}
|
||||
{{- range $shared }}{{ $sharedNames = append $sharedNames .name }}{{ end -}}
|
||||
freesailarr — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
||||
Nine containers in ONE pod, sharing gluetun's network namespace.
|
||||
|
||||
@@ -41,13 +45,53 @@ SET gluetun.vpn.firewallOutboundSubnets — IT IS EMPTY RIGHT NOW
|
||||
|
||||
ACCESS
|
||||
{{- if and .Values.ingress.enabled $ingressApps }}
|
||||
{{- range $shared }}
|
||||
{{ .name }}: https://{{ $sharedHost }}{{ if ne .path "/" }}{{ .path }}{{ end }}
|
||||
{{- end }}
|
||||
{{- range $ingressApps }}
|
||||
{{- $cfg := (index $.Values .name).ingress }}
|
||||
{{- if not (has .name $sharedNames) }}
|
||||
{{ .name }}: https://{{ $cfg.host }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if $shared }}
|
||||
|
||||
Each app has its OWN Ingress and its own TLS secret; while cert-manager is
|
||||
still issuing, the browser warns:
|
||||
These apps share one host, one Ingress and one certificate.
|
||||
{{- if has "bazarr" $sharedNames }}
|
||||
|
||||
BAZARR NEEDS ONE MANUAL STEP UNDER A SUB-PATH
|
||||
Bazarr reads its base path only from its own configuration - it has no
|
||||
environment variable for it, so the chart cannot set it. Open bazarr,
|
||||
go to Settings -> General -> Base URL, enter
|
||||
|
||||
{{ include "freesailarr.appPath" (dict "root" $ "app" "bazarr") }}
|
||||
|
||||
and restart the pod. Until then bazarr answers only at the root and the
|
||||
sub-path shows a broken UI. radarr, sonarr and prowlarr need nothing -
|
||||
the chart sets their UrlBase itself.
|
||||
{{- end }}
|
||||
{{- if has "qbittorrent" $sharedNames }}
|
||||
|
||||
QBITTORRENT SITS BEHIND A PREFIX-STRIPPING REWRITE
|
||||
It has no base path of its own, so it gets a second Ingress object on the
|
||||
same host whose rewrite cuts the prefix off. Two things there look exactly
|
||||
like a login failure and send you looking in the wrong place: its session
|
||||
cookie is marked "Secure" (over plain HTTP the browser never sends it back
|
||||
and every click bounces to the login form), and it bans the client IP for a
|
||||
while after a few failed attempts. If it does not work at all, set
|
||||
qbittorrent.ingress.host and it is back on a host of its own - no chart
|
||||
change needed.
|
||||
{{- end }}
|
||||
{{- if not (has "seerr" $sharedNames) }}
|
||||
|
||||
NOTHING SERVES THE ROOT OF {{ $sharedHost }}
|
||||
seerr is the app that sits at "/" and it is not on the shared host, so
|
||||
https://{{ $sharedHost }}/ answers 404. The sub-paths are unaffected.
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
Every app listed with a host of its own has its OWN Ingress and its own TLS
|
||||
secret. While cert-manager is still issuing, the browser warns:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} get certificate
|
||||
{{- else }}
|
||||
@@ -62,6 +106,33 @@ ACCESS
|
||||
qBittorrent's WebUI rejects a Host header whose port differs from its own
|
||||
({{ "8080" }}) with a plain 401. A port-forward on a different local port therefore
|
||||
looks like a login failure — forward 8080:8080, or go through the ingress.
|
||||
{{- if .Values.qbittorrent.savePath }}
|
||||
|
||||
QBITTORRENT DOWNLOADS GO TO {{ .Values.qbittorrent.savePath }}
|
||||
The chart seeds Session\DefaultSavePath into qBittorrent's config, because
|
||||
its built-in default is /config/Downloads — the config volume{{ with .Values.qbittorrent.storage.size }} ({{ . }}){{ end }}, which
|
||||
fills within minutes and puts EVERY torrent into "errored" while a large,
|
||||
empty media volume sits next to it. The failure looks like a network or VPN
|
||||
problem; only "df" shows what it is.
|
||||
|
||||
The value is seeded ONLY when the key is absent, never replaced. So if you
|
||||
changed the path in the WebUI, it stays yours and this value does nothing —
|
||||
check which one is actually in effect:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} exec {{ $name }}-sfs-0 -c qbittorrent -- \
|
||||
grep SavePath /config/qBittorrent/qBittorrent.conf
|
||||
|
||||
If that shows a path under /config, downloads are landing on the config
|
||||
volume: fix it under Options -> Downloads -> Default Save Path (the running
|
||||
app owns the file; editing it under the app is overwritten on exit).
|
||||
{{- else }}
|
||||
|
||||
QBITTORRENT HAS NO SAVE PATH SET (qbittorrent.savePath is empty)
|
||||
It therefore falls back to /config/Downloads — the small config volume. That
|
||||
fills within minutes and puts every torrent into "errored". Either set
|
||||
qbittorrent.savePath or set Options -> Downloads -> Default Save Path in the
|
||||
WebUI before starting any download.
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
EVERY SERVICE POINTS AT THE SAME POD IP
|
||||
@@ -87,6 +158,29 @@ THE MEDIA CLAIMS ARE YOURS, THE CONFIG VOLUMES ARE THE CHART'S
|
||||
Kubernetes makes every volumeMount its own bind mount and link() across two
|
||||
mount points fails with EXDEV regardless of subPath, so an import copies.
|
||||
|
||||
HARDLINKS NEED ONE MOUNT, NOT ONE CLAIM
|
||||
This is the point where the obvious conclusion is wrong, so it is spelled
|
||||
out: putting downloads on the same claim as movies and tv, as a third
|
||||
subPath, does NOT produce hardlinks. It buys the layout, nothing else.
|
||||
|
||||
The kernel compares the VFSMOUNT in do_linkat, not the superblock, and two
|
||||
bind mounts of the same filesystem are two vfsmounts — so link() across
|
||||
them is EXDEV even though it is one volume. Measured on Longhorn/NFS: both
|
||||
mount points report st_dev 0:56, the mount table shows two mount IDs on
|
||||
that same device, a cross-mount link() fails, a link() inside one mount
|
||||
succeeds with link count 2.
|
||||
|
||||
Note the trap in that measurement: "stat" reports the SAME device number
|
||||
on both sides. Every intuition says a hardlink must work there. It does
|
||||
not, and the device number is exactly what makes this look like a bug in
|
||||
the app instead of a property of the mount namespace.
|
||||
|
||||
The layout that WOULD hardlink is one claim with exactly ONE mount and the
|
||||
categories as sub-directories inside it — the "single /data mount" the *arr
|
||||
guides recommend. This chart cannot express that today: it mounts each
|
||||
category separately. Naming the possibility, not giving instructions —
|
||||
there are no values for it.
|
||||
|
||||
The containers write as {{ .Values.puid }}:{{ .Values.pgid }}. The claim's root directory has to be
|
||||
writable for them, and a subPath directory the kubelet creates inherits that
|
||||
root's mode — so fix the ownership before the first start. The pod will not
|
||||
|
||||
@@ -194,6 +194,131 @@ ingress block with enabled: true. Used by the ingress template.
|
||||
{{- toJson $apps -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The sub-path an app is served under in the shared-domain mode, e.g. "/sonarr".
|
||||
Null-safe by construction: an absent or erased <app>.ingress.path falls back to
|
||||
"/<app>", and to "/" for seerr, which has no base-path support at all and can
|
||||
therefore only live at the root.
|
||||
|
||||
Input: dict "root" $ "app" <app name>.
|
||||
*/}}
|
||||
{{- define "freesailarr.appPath" -}}
|
||||
{{- $config := (index .root.Values .app).ingress -}}
|
||||
{{- $path := "" -}}
|
||||
{{- if kindIs "map" $config -}}
|
||||
{{- $path = $config.path -}}
|
||||
{{- end -}}
|
||||
{{- if or (kindIs "invalid" $path) (eq (toString $path) "") -}}
|
||||
{{- if eq .app "seerr" -}}
|
||||
{{- $path = "/" -}}
|
||||
{{- else -}}
|
||||
{{- $path = printf "/%s" .app -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $path -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The apps served under the shared domain (ingress.host), as a JSON array of
|
||||
{name, port, path} dicts, sorted by path length descending so the rendered list
|
||||
is deterministic.
|
||||
|
||||
The shared-domain mode is off unless ingress.host is set; then it covers the
|
||||
apps whose ingress renders at all (freesailarr.ingressApps, i.e. the enabled
|
||||
port apps with ingress.enabled) and which do not carry a host of their own. A
|
||||
per-app host always wins and keeps rendering its own object exactly as before,
|
||||
which is what makes mixed operation fall out for free.
|
||||
|
||||
Two guards live here rather than in the ingress template, so that every
|
||||
consumer - the ingress objects, the UrlBase env vars and NOTES.txt - sees the
|
||||
same validated list:
|
||||
|
||||
1. seerr under any path other than "/". seerr has no base-path support
|
||||
(upstream request open since May 2022), so a chart that rendered it under
|
||||
a sub-path would produce something that provably cannot work.
|
||||
2. Two apps claiming the same path on the shared host. Only paths on the
|
||||
shared host can collide; apps with their own host are on a different host
|
||||
and are therefore not part of this check.
|
||||
|
||||
{{- $shared := include "freesailarr.sharedIngressApps" . | fromJsonArray }}
|
||||
*/}}
|
||||
{{- define "freesailarr.sharedIngressApps" -}}
|
||||
{{- $root := . -}}
|
||||
{{- $apps := list -}}
|
||||
{{- $host := $root.Values.ingress.host | default "" -}}
|
||||
{{- if and $root.Values.ingress.enabled $host -}}
|
||||
{{- $seen := dict -}}
|
||||
{{- range $app := include "freesailarr.ingressApps" $root | fromJsonArray -}}
|
||||
{{- $config := (index $root.Values $app.name).ingress -}}
|
||||
{{- if not ($config.host | default "") -}}
|
||||
{{- $path := include "freesailarr.appPath" (dict "root" $root "app" $app.name) -}}
|
||||
{{- if and (eq $app.name "seerr") (ne $path "/") -}}
|
||||
{{- fail (printf "seerr.ingress.path is %q, but seerr has no base-path support and can only be served at \"/\" (https://github.com/Fallenbagel/jellyseerr/issues/97). Either leave seerr.ingress.path at \"/\" or give seerr its own seerr.ingress.host." $path) -}}
|
||||
{{- end -}}
|
||||
{{- if hasKey $seen $path -}}
|
||||
{{- fail (printf "%s.ingress.path and %s.ingress.path are both %q on the shared host %q - every app under ingress.host needs its own path" (index $seen $path) $app.name $path $host) -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $seen $path $app.name -}}
|
||||
{{- $apps = append $apps (dict "name" $app.name "port" $app.port "path" $path) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- /* Sort by path length descending, ties broken alphabetically. The keys are
|
||||
unique because the guard above rejects duplicate paths. */ -}}
|
||||
{{- $keyed := dict -}}
|
||||
{{- $keys := list -}}
|
||||
{{- range $app := $apps -}}
|
||||
{{- $key := printf "%03d|%s" (sub 999 (len $app.path)) $app.path -}}
|
||||
{{- $_ := set $keyed $key $app -}}
|
||||
{{- $keys = append $keys $key -}}
|
||||
{{- end -}}
|
||||
{{- $sorted := list -}}
|
||||
{{- range $key := sortAlpha $keys -}}
|
||||
{{- $sorted = append $sorted (index $keyed $key) -}}
|
||||
{{- end -}}
|
||||
{{- toJson $sorted -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The value for a servarr app's <APP>__SERVER__URLBASE variable, or the empty
|
||||
string when the chart must not set it. Radarr, Sonarr and Prowlarr read their
|
||||
base path from this environment variable (schema
|
||||
<APP>__CONFIGNAMESPACE__CONFIGITEM, https://wiki.servarr.com/sonarr/environment-variables),
|
||||
so the chart can configure them itself as soon as they render under a sub-path.
|
||||
|
||||
Empty - i.e. the chart sets nothing - in three cases:
|
||||
|
||||
- the app does not render under the shared host, or renders at "/";
|
||||
- the app's own env block already carries the variable. The env blocks are
|
||||
documented as additive; emitting a second entry with the same name would
|
||||
break that promise, and two identically named variables in one container
|
||||
are not a defined state to build on.
|
||||
|
||||
Input: dict "root" $ "app" <"radarr"|"sonarr"|"prowlarr">.
|
||||
*/}}
|
||||
{{- define "freesailarr.urlBase" -}}
|
||||
{{- $root := .root -}}
|
||||
{{- $app := .app -}}
|
||||
{{- $var := printf "%s__SERVER__URLBASE" (upper $app) -}}
|
||||
{{- $path := "" -}}
|
||||
{{- range $entry := include "freesailarr.sharedIngressApps" $root | fromJsonArray -}}
|
||||
{{- if eq $entry.name $app -}}
|
||||
{{- $path = $entry.path -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if eq $path "/" -}}
|
||||
{{- $path = "" -}}
|
||||
{{- end -}}
|
||||
{{- if $path -}}
|
||||
{{- range $entry := (index $root.Values $app).env -}}
|
||||
{{- if eq (toString $entry.name) $var -}}
|
||||
{{- $path = "" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $path -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Comma-separated FIREWALL_INPUT_PORTS for gluetun: the ports of the enabled app
|
||||
containers (ingress traffic and the kubelet probes both arrive on the pod IP)
|
||||
|
||||
@@ -1,13 +1,38 @@
|
||||
{{/*
|
||||
One Ingress object per app whose ingress is enabled, each with its own host and
|
||||
its own TLS secret. The hosts are independent values - they do not have to
|
||||
share a domain - and each ingress points at its own app's service.
|
||||
Two ingress shapes, and they mix freely.
|
||||
|
||||
<app>.ingress.host is required, but only for an app whose ingress is actually
|
||||
enabled: the required call sits inside the loop over the enabled apps, so
|
||||
turning an app's ingress off must never make the chart demand a host for it.
|
||||
1. One host per app (the default): every app whose ingress is enabled gets its
|
||||
own Ingress object with its own host and its own TLS secret. The hosts are
|
||||
independent values - they do not have to share a domain - and each ingress
|
||||
points at its own app's service.
|
||||
|
||||
<app>.ingress.host is required, but only for an app whose ingress is
|
||||
actually enabled and which is not served under the shared domain: the
|
||||
required call sits inside the loop over the enabled apps, so turning an
|
||||
app's ingress off must never make the chart demand a host for it.
|
||||
|
||||
2. One shared domain with sub-paths (ingress.host): the apps that carry no host
|
||||
of their own move into ONE object with one path each, one TLS secret, and
|
||||
therefore one DNS name and one certificate for the whole stack. An app that
|
||||
does keep its own host stays in shape 1.
|
||||
|
||||
qBittorrent renders a second object on the same host in shape 2 - see the
|
||||
comment on that block.
|
||||
*/}}
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- $sharedHost := .Values.ingress.host | default "" }}
|
||||
{{- $shared := include "freesailarr.sharedIngressApps" $ | fromJsonArray }}
|
||||
{{- /* qBittorrent needs a rewrite annotation the others must not get, so it is
|
||||
split out of the shared object and rendered separately below. */}}
|
||||
{{- $sharedMain := list }}
|
||||
{{- $sharedQbt := dict }}
|
||||
{{- range $app := $shared }}
|
||||
{{- if eq $app.name "qbittorrent" }}
|
||||
{{- $sharedQbt = $app }}
|
||||
{{- else }}
|
||||
{{- $sharedMain = append $sharedMain $app }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $app := include "freesailarr.ingressApps" $ | fromJsonArray }}
|
||||
{{- $config := (index $.Values $app.name).ingress }}
|
||||
{{- /* clusterIssuer, className and annotations default to the global ingress
|
||||
@@ -19,7 +44,9 @@ turning an app's ingress off must never make the chart demand a host for it.
|
||||
{{- if not (kindIs "invalid" $config.className) }}{{- $className = $config.className }}{{- end }}
|
||||
{{- $annotations := $.Values.ingress.annotations }}
|
||||
{{- if not (kindIs "invalid" $config.annotations) }}{{- $annotations = $config.annotations }}{{- end }}
|
||||
{{- $host := required (printf "%s.ingress.host must be provided while %s.ingress.enabled is true" $app.name $app.name) $config.host }}
|
||||
{{- /* Skip the apps that live on the shared domain; they are rendered below. */}}
|
||||
{{- if or (not $sharedHost) ($config.host | default "") }}
|
||||
{{- $host := required (printf "%s.ingress.host must be provided while %s.ingress.enabled is true (or set the shared ingress.host to serve the app under a sub-path instead)" $app.name $app.name) $config.host }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
@@ -49,3 +76,95 @@ spec:
|
||||
secretName: tls-{{ $.Release.Name }}-{{ $.Chart.Name }}-{{ $app.name }}-ingress
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if $sharedMain }}
|
||||
{{/*
|
||||
The shared-domain object. Rendered only when at least one non-qBittorrent app
|
||||
actually contributes a path, so switching every app ingress off while leaving
|
||||
ingress.host set produces NO object rather than an empty one without rules.
|
||||
|
||||
The paths come sorted by length descending. nginx matches the longest prefix
|
||||
regardless of order, so this is about a deterministic, reviewable rendering,
|
||||
not about matching behaviour.
|
||||
*/}}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided" .Values.ingress.clusterIssuer }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
rules:
|
||||
- host: {{ $sharedHost | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range $app := $sharedMain }}
|
||||
- backend:
|
||||
service:
|
||||
name: {{ $.Release.Name }}-{{ $.Chart.Name }}-{{ $app.name }}-service
|
||||
port:
|
||||
name: {{ $app.name }}
|
||||
path: {{ $app.path | quote }}
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ $sharedHost | quote }}
|
||||
secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
{{- end }}
|
||||
{{- if $sharedQbt }}
|
||||
{{/*
|
||||
qBittorrent on the shared domain, as its own object on the same host.
|
||||
|
||||
qBittorrent has no base-path setting: it serves its WebUI at the root and the
|
||||
proxy has to strip the prefix
|
||||
(https://github.com/qbittorrent/wiki/blob/master/NGINX-Reverse-Proxy-for-Web-UI.md).
|
||||
nginx applies rewrite-target per INGRESS OBJECT, not per path, and the other
|
||||
apps must not be rewritten - their UrlBase contains the prefix, so they expect
|
||||
the full path. Hence the split.
|
||||
|
||||
The tls block repeats the SAME secret name as the main object on purpose: this
|
||||
way neither object depends on an assumption about whether nginx applies the
|
||||
other one's TLS configuration to its paths - both state it themselves. The
|
||||
cert-manager annotation is deliberately absent, so only one Certificate is
|
||||
requested for that one secret. The one case where it IS set here is when no
|
||||
main object renders at all (qBittorrent is the only app on the shared host) -
|
||||
otherwise nothing would ever request the certificate.
|
||||
*/}}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-qbittorrent-ingress
|
||||
annotations:
|
||||
# Strip the path prefix: capture group 2 is everything after "<path>/".
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /$2
|
||||
{{- if not $sharedMain }}
|
||||
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided" .Values.ingress.clusterIssuer }}
|
||||
{{- end }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
rules:
|
||||
- host: {{ $sharedHost | quote }}
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-qbittorrent-service
|
||||
port:
|
||||
name: qbittorrent
|
||||
path: {{ printf "%s(/|$)(.*)" $sharedQbt.path | quote }}
|
||||
pathType: ImplementationSpecific
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ $sharedHost | quote }}
|
||||
secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -34,21 +34,31 @@ spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
{{- include "freesailarr.podSecurityContext" . | nindent 8 }}
|
||||
{{- if and .Values.qbittorrent.enabled .Values.qbittorrent.networkInterface }}
|
||||
{{- if and .Values.qbittorrent.enabled (or .Values.qbittorrent.networkInterface .Values.qbittorrent.savePath) }}
|
||||
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
|
||||
# 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:
|
||||
@@ -65,6 +75,7 @@ spec:
|
||||
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" '
|
||||
@@ -78,9 +89,30 @@ spec:
|
||||
' "$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:
|
||||
@@ -239,6 +271,14 @@ spec:
|
||||
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
|
||||
@@ -300,6 +340,14 @@ spec:
|
||||
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
|
||||
@@ -368,6 +416,14 @@ spec:
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user