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