Merge pull request #165 from jk-cluster/fix/freesailarr-shared-media-claim
freesailarr: one volume entry per media claim (2.0.2+up1.0.0)
This commit is contained in:
5 files changed
+168
-15
No files matched your search
@@ -8,7 +8,7 @@ description: A Helm chart to deploy a Wireguard-tunneled media automation stack
|
||||
|
||||
type: application
|
||||
|
||||
version: 2.0.1+up1.0.0
|
||||
version: 2.0.2+up1.0.0
|
||||
# The chart's own version marker, freely chosen and tied to no upstream. The
|
||||
# stack bundles nine independently versioned projects, so there is no single
|
||||
# "app version" to track here.
|
||||
|
||||
@@ -87,10 +87,12 @@ 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.
|
||||
|
||||
The containers write as {{ .Values.puid }}:{{ .Values.pgid }}. If the claims already hold data
|
||||
owned by someone else, fix the ownership before the first start — the pod
|
||||
will not do it for you (fsGroupChangePolicy is OnRootMismatch, deliberately,
|
||||
so a large library is not recursively re-chowned on every start).
|
||||
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
|
||||
|
||||
@@ -219,6 +219,96 @@ alive. It is not what opens the ports today.
|
||||
{{- join "," $ports -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Whether a media category is mounted at all, i.e. whether at least one app that
|
||||
mounts it is enabled. "true" when it is, the empty string when it is not (an
|
||||
empty string is falsy in a template "if", so the result can be used directly).
|
||||
|
||||
downloads: qbittorrent, radarr, sonarr
|
||||
movies: radarr, bazarr
|
||||
tv: sonarr, bazarr
|
||||
|
||||
Input: dict "root" $ "category" <"downloads"|"movies"|"tv">.
|
||||
*/}}
|
||||
{{- define "freesailarr.mediaCategoryEnabled" -}}
|
||||
{{- $v := .root.Values -}}
|
||||
{{- if eq .category "downloads" -}}
|
||||
{{- if or $v.qbittorrent.enabled $v.radarr.enabled $v.sonarr.enabled -}}true{{- end -}}
|
||||
{{- else if eq .category "movies" -}}
|
||||
{{- if or $v.radarr.enabled $v.bazarr.enabled -}}true{{- end -}}
|
||||
{{- else if eq .category "tv" -}}
|
||||
{{- if or $v.sonarr.enabled $v.bazarr.enabled -}}true{{- end -}}
|
||||
{{- else -}}
|
||||
{{- fail (printf "unknown media category %q" .category) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The pod volume name a media category mounts (issue #164).
|
||||
|
||||
Several categories may point at the same existingClaim - one library volume with
|
||||
the categories as sub-directories is the intended layout. A pod may reference
|
||||
such a claim only ONCE: two volume entries with the same claimName leave the pod
|
||||
in Init:0/1 forever, without a single kubelet event (measured on RKE2/Longhorn,
|
||||
with RWO as well as RWX). The claim therefore gets exactly one volume entry and
|
||||
the categories differ only in the subPath of their mounts.
|
||||
|
||||
This helper names the owner of that entry: the FIRST ENABLED category, in the
|
||||
fixed order
|
||||
|
||||
downloads, movies, tv
|
||||
|
||||
that carries the same claim. Two properties of that rule are load-bearing:
|
||||
|
||||
- The order is fixed and defined here, not derived from the values. A rule
|
||||
like "the alphabetically smallest" or "whichever comes first in the values"
|
||||
would move the volume name when an unrelated flag changes, and a moved
|
||||
volume name is a pod restart.
|
||||
- Only ENABLED categories can own the entry, because the three entries are
|
||||
rendered under different conditions (see mediaCategoryEnabled above). A
|
||||
disabled owner renders no entry, so its name would be a dangling reference
|
||||
and the pod would be rejected by the API server. Example: with qbittorrent,
|
||||
radarr and sonarr off and bazarr on, "downloads" is not mounted at all -
|
||||
"movies" owns a claim shared by all three categories.
|
||||
|
||||
A category with no claim owns itself, so the required() message for the missing
|
||||
claim still fires at the category the user actually configured.
|
||||
|
||||
Input: dict "root" $ "category" <"downloads"|"movies"|"tv">.
|
||||
Usage (mount) : name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "movies") }}
|
||||
Usage (volume): render the entry only for a category that owns itself.
|
||||
*/}}
|
||||
{{- define "freesailarr.mediaVolumeName" -}}
|
||||
{{- $root := .root -}}
|
||||
{{- $claim := (index $root.Values.media .category).existingClaim | default "" -}}
|
||||
{{- $owner := "" -}}
|
||||
{{- if $claim -}}
|
||||
{{- range $candidate := list "downloads" "movies" "tv" -}}
|
||||
{{- if not $owner -}}
|
||||
{{- if include "freesailarr.mediaCategoryEnabled" (dict "root" $root "category" $candidate) -}}
|
||||
{{- if eq ((index $root.Values.media $candidate).existingClaim | default "") $claim -}}
|
||||
{{- $owner = $candidate -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not $owner -}}
|
||||
{{- $owner = .category -}}
|
||||
{{- end -}}
|
||||
media-{{ $owner }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Whether a media category owns its volume entry, i.e. whether the volumes block
|
||||
renders an entry for it. "true" or the empty string, see mediaVolumeName above.
|
||||
|
||||
Input: dict "root" $ "category" <"downloads"|"movies"|"tv">.
|
||||
*/}}
|
||||
{{- define "freesailarr.mediaVolumeOwned" -}}
|
||||
{{- if eq (include "freesailarr.mediaVolumeName" .) (printf "media-%s" .category) -}}true{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Recursive merge of an override onto defaults where null never deletes and set
|
||||
values always win, including false and 0 (issue #99).
|
||||
|
||||
@@ -223,7 +223,7 @@ spec:
|
||||
- mountPath: /config
|
||||
name: qbittorrent-config
|
||||
- mountPath: /downloads
|
||||
name: media-downloads
|
||||
name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "downloads") }}
|
||||
{{- with .Values.media.downloads.subPath }}
|
||||
subPath: {{ . | quote }}
|
||||
{{- end }}
|
||||
@@ -319,12 +319,12 @@ spec:
|
||||
- mountPath: /config
|
||||
name: radarr-config
|
||||
- mountPath: /movies
|
||||
name: media-movies
|
||||
name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "movies") }}
|
||||
{{- with .Values.media.movies.subPath }}
|
||||
subPath: {{ . | quote }}
|
||||
{{- end }}
|
||||
- mountPath: /downloads
|
||||
name: media-downloads
|
||||
name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "downloads") }}
|
||||
{{- with .Values.media.downloads.subPath }}
|
||||
subPath: {{ . | quote }}
|
||||
{{- end }}
|
||||
@@ -387,12 +387,12 @@ spec:
|
||||
- mountPath: /config
|
||||
name: sonarr-config
|
||||
- mountPath: /tv
|
||||
name: media-tv
|
||||
name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "tv") }}
|
||||
{{- with .Values.media.tv.subPath }}
|
||||
subPath: {{ . | quote }}
|
||||
{{- end }}
|
||||
- mountPath: /downloads
|
||||
name: media-downloads
|
||||
name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "downloads") }}
|
||||
{{- with .Values.media.downloads.subPath }}
|
||||
subPath: {{ . | quote }}
|
||||
{{- end }}
|
||||
@@ -455,12 +455,12 @@ spec:
|
||||
- mountPath: /config
|
||||
name: bazarr-config
|
||||
- mountPath: /movies
|
||||
name: media-movies
|
||||
name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "movies") }}
|
||||
{{- with .Values.media.movies.subPath }}
|
||||
subPath: {{ . | quote }}
|
||||
{{- end }}
|
||||
- mountPath: /tv
|
||||
name: media-tv
|
||||
name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "tv") }}
|
||||
{{- with .Values.media.tv.subPath }}
|
||||
subPath: {{ . | quote }}
|
||||
{{- end }}
|
||||
@@ -475,21 +475,38 @@ spec:
|
||||
# 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: {}
|
||||
|
||||
+47
-3
@@ -54,6 +54,13 @@ securityContext:
|
||||
# group-writable for the non-root containers; fsGroupChangePolicy
|
||||
# OnRootMismatch avoids recursively re-chowning the (large, shared) media
|
||||
# claims on every pod start.
|
||||
# How far that reaches is the CSI driver's decision, not this chart's, and the
|
||||
# media claims are the case where it stops short: a driver whose fsGroupPolicy
|
||||
# is ReadWriteOnceWithFSType - Longhorn's setting - applies fsGroup to RWO
|
||||
# volumes only, so on an RWX media claim fsGroup is ignored outright and
|
||||
# OnRootMismatch never even gets a say. The config claims are RWO and are
|
||||
# covered normally. Consequence for the media claims: their root directory has
|
||||
# to be writable for PUID/PGID by itself, see the media block below (#164).
|
||||
# seccompProfile RuntimeDefault applies the container runtime's default
|
||||
# syscall filter to every container in the pod, init containers included, and
|
||||
# is set at pod level so a container added later inherits it without the
|
||||
@@ -82,9 +89,46 @@ storage:
|
||||
# tv: sonarr, bazarr
|
||||
#
|
||||
# subPath mounts a sub-directory of the claim instead of its root (null = the
|
||||
# claim root, i.e. one claim per category). The same existingClaim may back all
|
||||
# three categories with different subPaths - one library volume, one
|
||||
# sub-directory per category, e.g. downloads / movies / tv-shows.
|
||||
# claim root, i.e. one claim per category). The same existingClaim may back two
|
||||
# or all three categories, which is what makes the one-library-volume layout
|
||||
# possible - one claim, one sub-directory per category:
|
||||
#
|
||||
# media:
|
||||
# downloads: { existingClaim: media-library, subPath: downloads }
|
||||
# movies: { existingClaim: media-library, subPath: movies }
|
||||
# tv: { existingClaim: media-library, subPath: tv-shows }
|
||||
#
|
||||
# Categories sharing a claim get exactly ONE pod volume entry and differ only in
|
||||
# the subPath of their mounts (#164). That is not cosmetic: a pod that lists the
|
||||
# same claim twice never starts - it stays in Init:0/1 forever without a single
|
||||
# kubelet event (measured on RKE2/Longhorn, with RWO as well as RWX). The entry
|
||||
# is named after the first ENABLED category in the fixed order downloads,
|
||||
# movies, tv that carries the claim, so the name does not wander when an
|
||||
# unrelated app flag changes (see the mediaVolumeName helper).
|
||||
#
|
||||
# The subPath directories do NOT have to exist beforehand - the kubelet creates
|
||||
# a missing one. What it does not do is give it usable permissions: the created
|
||||
# directory belongs to root and inherits the mode of the CLAIM ROOT. So the one
|
||||
# thing that has to hold is:
|
||||
#
|
||||
# the root directory of the claim must be writable for PUID/PGID.
|
||||
#
|
||||
# Do not count on fsGroup for that. Whether the kubelet applies fsGroup to a
|
||||
# volume at all is the CSI driver's decision (its fsGroupPolicy), not the pod's:
|
||||
# Longhorn ships ReadWriteOnceWithFSType, so fsGroup applies to its RWO volumes
|
||||
# and is silently ignored on an RWX claim - measured by the reporter of #164 on
|
||||
# the target cluster (kubectl get csidriver driver.longhorn.io -o
|
||||
# jsonpath='{.spec.fsGroupPolicy}').
|
||||
#
|
||||
# In practice this works out on Longhorn because Longhorn hands out its volume
|
||||
# root as mode 777, and a subPath directory created under it inherits those 777
|
||||
# - root-owned, but writable for everyone. That is a property of the driver, not
|
||||
# of Kubernetes. Measured in k3d against a claim whose root is 0755 and with no
|
||||
# fsGroup in effect: the kubelet creates /movies as root:root 0755 and the
|
||||
# container's `touch` fails with EPERM. On such a driver, prepare the claim root
|
||||
# once (chown 1000:1000 or chmod g+w with the right group) before the first
|
||||
# start - the chart deliberately does not chown anything: a multi-TB library
|
||||
# must not be walked on every pod start.
|
||||
#
|
||||
# What this buys and what it does NOT buy - measured in a k3d cluster against
|
||||
# this chart, because the usual argument for a shared volume is hardlinking:
|
||||
|
||||
Reference in new issue
Block a user