media.downloads, media.movies and media.tv rendered three fixed volume entries, one per category. Two categories on the same existingClaim therefore produced two pod volumes with an identical claimName, and such a pod never starts: it stays in Init:0/1 without a single kubelet event (measured on RKE2/Longhorn, RWO and RWX alike). The volume list is now deduplicated by claim name. A new mediaVolumeName helper names the entry after the first ENABLED category in the fixed order downloads, movies, tv that carries the claim; the other categories mount that same entry and differ only in their subPath. The restriction to enabled categories matters because the three entries render under different conditions - a canonical owner whose entry is not rendered would be a dangling reference. With three distinct claims the chart renders byte-identically to 2.0.1, hence a patch bump. Also corrected in the values documentation, after measuring instead of assuming: the kubelet creates missing subPath directories itself, but they inherit the claim root's mode, and fsGroup cannot be relied on to fix that - Longhorn's fsGroupPolicy (ReadWriteOnceWithFSType) skips fsGroup on RWX claims entirely. What has to hold is that the claim root is writable for PUID/PGID. No chown or init container was added.
109 lines
5.4 KiB
Plaintext
109 lines
5.4 KiB
Plaintext
{{- $name := printf "%s-%s" .Release.Name .Chart.Name -}}
|
|
{{- $replicas := include "freesailarr.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.replicas) -}}
|
|
{{- /* The effective (null-safe) startup probe, so the budget below matches the
|
|
container even when the block has been erased (#99). */ -}}
|
|
{{- $startup := fromYaml (include "freesailarr.probeFor" (dict "root" $ "app" "gluetun" "kind" "startupProbe" "given" .Values.gluetun.startupProbe)) -}}
|
|
{{- $budget := mul (int $startup.periodSeconds) (int $startup.failureThreshold) -}}
|
|
{{- $ingressApps := include "freesailarr.ingressApps" . | fromJsonArray -}}
|
|
{{- $portApps := include "freesailarr.portApps" . | fromJsonArray -}}
|
|
freesailarr — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
|
Nine containers in ONE pod, sharing gluetun's network namespace.
|
|
|
|
NOTHING IS READY UNTIL THE TUNNEL IS UP
|
|
gluetun's readiness probe is its health server, which answers 200 only while
|
|
the Wireguard tunnel is actually working. So "pod not Ready" almost always
|
|
means "no tunnel", not "app broken". The startup budget is {{ $budget }}s before
|
|
Kubernetes starts killing the container:
|
|
|
|
kubectl -n {{ .Release.Namespace }} logs {{ $name }}-sfs-0 -c gluetun -f
|
|
|
|
While gluetun is down its firewall keeps blocking every app container's
|
|
egress. That is the kill switch doing its job — the apps stay up and simply
|
|
reach nothing.
|
|
|
|
THE 1PASSWORD ITEM AND THE TWO FIELDS IT MUST CONTAIN
|
|
secrets.gluetunSecretRef -> {{ .Values.secrets.gluetunSecretRef | default "(not set)" }}
|
|
wireguard-private-key
|
|
wireguard-addresses
|
|
|
|
A missing field does not fail the install: gluetun starts with the variable
|
|
empty and never completes a handshake.
|
|
|
|
kubectl -n {{ .Release.Namespace }} get onepassworditem {{ $name }}-gluetun-secret
|
|
{{- if not .Values.gluetun.vpn.firewallOutboundSubnets }}
|
|
|
|
SET gluetun.vpn.firewallOutboundSubnets — IT IS EMPTY RIGHT NOW
|
|
Without it the gluetun firewall blocks cluster-internal egress as well:
|
|
lookups against cluster DNS and seerr reaching an in-cluster Jellyfin both
|
|
fail, in a way that looks like an application bug. Set it to the cluster pod
|
|
and service CIDRs, e.g. "10.42.0.0/16,10.43.0.0/16" on k3s.
|
|
{{- end }}
|
|
|
|
ACCESS
|
|
{{- if and .Values.ingress.enabled $ingressApps }}
|
|
{{- range $ingressApps }}
|
|
{{- $cfg := (index $.Values .name).ingress }}
|
|
{{ .name }}: https://{{ $cfg.host }}
|
|
{{- end }}
|
|
|
|
Each app 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 }}
|
|
No Ingress is rendered{{ if not .Values.ingress.enabled }} (ingress.enabled is false){{ end }}. Reach an app through its own service:
|
|
{{- range $portApps }}
|
|
|
|
kubectl -n {{ $.Release.Namespace }} port-forward svc/{{ $name }}-{{ .name }}-service {{ .port }}:{{ .port }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- if .Values.qbittorrent.enabled }}
|
|
|
|
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.
|
|
{{- end }}
|
|
|
|
EVERY SERVICE POINTS AT THE SAME POD IP
|
|
One ClusterIP service per app{{ if .Values.recyclarr.enabled }} (recyclarr has none — no port in daemon mode){{ end }}, but all
|
|
containers share gluetun's network namespace, so the services differ only in
|
|
their port. They give you stable per-app DNS names, NOT network separation
|
|
between the apps.
|
|
|
|
TOGGLING AN APP OFF AND ON LATER NEEDS ONE MANUAL STEP
|
|
The config volumes come from volumeClaimTemplates, and those are immutable.
|
|
Changing any <app>.enabled after the first install makes the upgrade fail
|
|
until the StatefulSet is recreated — the data survives:
|
|
|
|
kubectl -n {{ .Release.Namespace }} delete statefulset {{ $name }}-sfs --cascade=orphan
|
|
|
|
THE MEDIA CLAIMS ARE YOURS, THE CONFIG VOLUMES ARE THE CHART'S
|
|
media.<category>.existingClaim is mounted, never created, and is meant to be
|
|
shared with your Jellyfin. One claim can serve all three categories via
|
|
media.<category>.subPath.
|
|
|
|
Measured, so you do not plan around something that does not happen: Radarr
|
|
and Sonarr do NOT hardlink between the categories, not even on one claim.
|
|
Kubernetes makes every volumeMount its own bind mount and link() across two
|
|
mount points fails with EXDEV regardless of subPath, so an import copies.
|
|
|
|
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
|
|
do it for you: fsGroupChangePolicy is OnRootMismatch so a large library is
|
|
not re-chowned on every start, and on an RWX claim a driver like Longhorn
|
|
(fsGroupPolicy ReadWriteOnceWithFSType) ignores fsGroup altogether.
|
|
{{- if eq $replicas "0" }}
|
|
|
|
SCALED TO ZERO
|
|
{{ if .Values.scaleDown }}scaleDown: true{{ else }}replicas: 0{{ end }} — the StatefulSet runs with replicas: 0. PVCs, secrets,
|
|
services{{ if .Values.ingress.enabled }} and ingresses{{ end }} stay in place; set it back and upgrade to bring the
|
|
stack up again.
|
|
{{- else if ne $replicas "1" }}
|
|
|
|
MORE THAN ONE REPLICA IS NOT MEANINGFUL
|
|
replicas is {{ $replicas }}. Each replica would run its own isolated stack on its own
|
|
config volumes, all of them fighting over the same media claims and the same
|
|
VPN credentials.
|
|
{{- end }}
|