Merge pull request #155 from jk-cluster/feat/freesailarr-retrofit

freesailarr: Schema, Null-Absicherung, seccompProfile und NOTES.txt
This commit is contained in:
jklein authored and GitHub committed 2026-08-24 14:33:54 +00:00
commit 14d22fcfe7
7 files changed
+1404 -57

No files matched your search

+2 -2
View File
@@ -247,8 +247,8 @@ Reference implementation: `template-chart/values.schema.json` and `template-char
### 13. Everything else
- `icon:` is mandatory in every `Chart.yaml`.
- `icon:` is mandatory in every `Chart.yaml`. **Documented exception — freesailarr, the only chart without one**: the rule everywhere else is "the icon is this chart's one upstream's logo", and a chart bundling nine independently versioned projects has no such upstream — picking one of the nine would imply a "main app" the stack deliberately does not have. That is the whole criterion, and it applies to no other chart in the repo: a single-upstream chart without an icon is a missing icon, not an exception. The omission costs an INFO line in `helm lint --strict` (`icon is recommended`), which still exits 0.
- `helm lint --strict` must pass without findings.
- `NOTES.txt` for every chart is planned (#43); currently only homeassistant ships one.
- `NOTES.txt` for every chart is planned (#43); seven charts ship one so far (`apache-tika`, `cyberchef`, `freesailarr`, `gotenberg`, `homeassistant`, `patchmon`, `rss-bridge`).
- CI: every installable chart has fixtures under `ci/<chart>/` (`test-values.yaml`, optionally `fixtures.yaml`/`settings.env`) or a justified entry in `ci/excluded-charts.txt` — the install-test fails if both are missing.
- Charts have no dependencies (`charts/` directories are empty).
+1 -1
View File
@@ -8,7 +8,7 @@ description: A Helm chart to deploy a Wireguard-tunneled media automation stack
type: application
version: 1.0.0+up1.0.0
version: 2.0.0+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.
+106
View File
@@ -0,0 +1,106 @@
{{- $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 }}. 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).
{{- 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 }}
+494
View File
@@ -218,3 +218,497 @@ alive. It is not what opens the ports today.
{{- $ports = append $ports 9999 -}}
{{- join "," $ports -}}
{{- end -}}
{{/*
Recursive merge of an override onto defaults where null never deletes and set
values always win, including false and 0 (issue #99).
Input: dict "defaults" <dict> "given" <dict|nil> "path" <values path, for errors>.
NOTE: like the resources helper above, this is duplicated into every chart's
templates/_helpers.tpl with the chart's name as define prefix. The reference
implementation lives in template-chart.
*/}}
{{- define "freesailarr.defaultedDict" -}}
{{- $defaults := .defaults | default dict -}}
{{- $given := .given -}}
{{- $path := .path | default "value" -}}
{{- if kindIs "invalid" $given -}}
{{- $given = dict -}}
{{- end -}}
{{- if not (kindIs "map" $given) -}}
{{- fail (printf "%s must be a mapping or null, got %s (%v)" $path (kindOf $given) $given) -}}
{{- end -}}
{{- $out := deepCopy $defaults -}}
{{- range $key, $value := $given -}}
{{- if not (kindIs "invalid" $value) -}}
{{- $default := index $defaults $key -}}
{{- if and (kindIs "map" $value) (kindIs "map" $default) -}}
{{- $_ := set $out $key (fromYaml (include "freesailarr.defaultedDict" (dict "defaults" $default "given" $value "path" (printf "%s.%s" $path $key)))) -}}
{{- else if kindIs "map" $default -}}
{{- fail (printf "%s.%s must be a mapping or null, got %s (%v)" $path $key (kindOf $value) $value) -}}
{{- else -}}
{{- $_ := set $out $key $value -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- toYaml $out -}}
{{- end -}}
{{/*
Pick one sub-block out of a values block that may itself be null.
Input: dict "block" <dict|nil> "key" <string> "path" <string>.
Needed because ".Values.<app>.securityContext" can itself be erased, and
indexing into nil inside a template is not an error but silently yields
nothing.
*/}}
{{- define "freesailarr.subBlock" -}}
{{- $block := .block -}}
{{- if kindIs "invalid" $block -}}
{{- $block = dict -}}
{{- end -}}
{{- if not (kindIs "map" $block) -}}
{{- fail (printf "%s must be a mapping or null, got %s (%v)" .path (kindOf $block) $block) -}}
{{- end -}}
{{- toYaml (dict "value" (index $block .key)) -}}
{{- end -}}
{{/*
The chart's own defaults for every block of the "silent class" from issue #99 -
security contexts, probes and resources. An override that erases one of these
blocks (`key:` with no children, i.e. null) would otherwise render a container
without hardening, without a probe or without resource accounting: it comes up,
reports green and is quietly weaker. The accessors below merge the user's block
onto these defaults with freesailarr.defaultedDict, where null never deletes.
These MUST stay in sync with values.yaml, which remains the documented,
user-facing place for them; this copy is only the fallback. The sync is not left
to discipline: rendering the chart with every one of these blocks explicitly set
to null must produce byte-identical output to rendering it with the defaults,
which is how the table was verified and how a drift shows up.
Two rules this table exists to honour (README, "Chart style guidelines"):
- It reproduces THIS chart's defaults, deviations included, not the repo
standard. The five linuxserver.io containers deliberately run without
runAsNonRoot and with a writable root filesystem, because their s6-overlay
init starts as root and drops to PUID/PGID; "restoring" the clean standard
here would leave them on the image's built-in uid 911 and make the existing
media volumes inaccessible - the exact failure calibre-web-automated hit.
flaresolverr's numerically pinned uid/gid 1000 and gluetun's NET_ADMIN are
reproduced for the same reason.
- The probe defaults carry their CONTEXT, not just timings: every httpGet
keeps its path and port, and recyclarr - which serves no HTTP port in
daemon mode - keeps its exec command. A probe default without them is worse
than no default.
*/}}
{{- define "freesailarr.fallbacks" -}}
gluetun:
livenessProbe:
httpGet:
path: /
port: 9999
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
readinessProbe:
httpGet:
path: /
port: 9999
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
startupProbe:
httpGet:
path: /
port: 9999
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.1
memory: 128Mi
ephemeralStorage: 50Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
add:
- NET_ADMIN
qbittorrent:
livenessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
startupProbe:
httpGet:
path: /
port: 8080
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.2
memory: 512Mi
ephemeralStorage: 100Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
add:
- CHOWN
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
prowlarr:
livenessProbe:
httpGet:
path: /ping
port: 9696
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ping
port: 9696
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
startupProbe:
httpGet:
path: /ping
port: 9696
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.1
memory: 256Mi
ephemeralStorage: 100Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
add:
- CHOWN
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
flaresolverr:
livenessProbe:
httpGet:
path: /health
port: 8191
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 8191
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
startupProbe:
httpGet:
path: /health
port: 8191
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.2
memory: 512Mi
ephemeralStorage: 200Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
capabilities:
drop:
- ALL
radarr:
livenessProbe:
httpGet:
path: /ping
port: 7878
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ping
port: 7878
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
startupProbe:
httpGet:
path: /ping
port: 7878
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.1
memory: 256Mi
ephemeralStorage: 100Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
add:
- CHOWN
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
recyclarr:
livenessProbe:
exec:
command:
- recyclarr
- --version
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
exec:
command:
- recyclarr
- --version
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
startupProbe:
exec:
command:
- recyclarr
- --version
periodSeconds: 5
timeoutSeconds: 10
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.05
memory: 128Mi
ephemeralStorage: 50Mi
securityContext:
runAsUser: {{ .Values.puid }}
runAsGroup: {{ .Values.pgid }}
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
sonarr:
livenessProbe:
httpGet:
path: /ping
port: 8989
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ping
port: 8989
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
startupProbe:
httpGet:
path: /ping
port: 8989
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.1
memory: 256Mi
ephemeralStorage: 100Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
add:
- CHOWN
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
seerr:
livenessProbe:
httpGet:
path: /api/v1/settings/public
port: 5055
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/v1/settings/public
port: 5055
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
startupProbe:
httpGet:
path: /api/v1/settings/public
port: 5055
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.1
memory: 256Mi
ephemeralStorage: 100Mi
securityContext:
runAsUser: {{ .Values.puid }}
runAsGroup: {{ .Values.pgid }}
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
bazarr:
livenessProbe:
httpGet:
path: /
port: 6767
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 6767
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
startupProbe:
httpGet:
path: /
port: 6767
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 30
resources:
limitFactor: 3
requests:
cpu: 0.1
memory: 256Mi
ephemeralStorage: 100Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
add:
- CHOWN
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
configInit:
resources:
limitFactor: 3
requests:
cpu: 10m
memory: 16Mi
ephemeralStorage: 1Mi
securityContext:
runAsUser: {{ .Values.puid }}
runAsGroup: {{ .Values.pgid }}
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
{{- end -}}
{{/*
The effective pod security context. Separate from the per-container table
because it is the one block shared by all nine containers.
*/}}
{{- define "freesailarr.podSecurityContext" -}}
{{- $given := (fromYaml (include "freesailarr.subBlock" (dict "block" .Values.securityContext "key" "pod" "path" "securityContext"))).value -}}
{{- $defaults := dict
"fsGroup" 1000
"fsGroupChangePolicy" "OnRootMismatch"
"seccompProfile" (dict "type" "RuntimeDefault") -}}
{{- include "freesailarr.defaultedDict" (dict "defaults" $defaults "given" $given "path" "securityContext.pod") -}}
{{- end -}}
{{/*
The effective probe / security context / resources for one container.
Input: dict "root" $ "app" <key in the fallback table> "given" <the values block>
and, for probes, "kind" <livenessProbe|readinessProbe|startupProbe>.
"given" is passed in rather than derived from "app" because the qbittorrent init
container's blocks live under qbittorrent.configInit, not under a top-level key
of the same name.
*/}}
{{- define "freesailarr.probeFor" -}}
{{- $fb := fromYaml (include "freesailarr.fallbacks" .root) -}}
{{- $defaults := index $fb .app .kind -}}
{{- include "freesailarr.defaultedDict" (dict "defaults" $defaults "given" .given "path" (printf "%s.%s" .app .kind)) -}}
{{- end -}}
{{- define "freesailarr.securityContextFor" -}}
{{- $fb := fromYaml (include "freesailarr.fallbacks" .root) -}}
{{- $defaults := index $fb .app "securityContext" -}}
{{- include "freesailarr.defaultedDict" (dict "defaults" $defaults "given" .given "path" (printf "%s.securityContext" .app)) -}}
{{- end -}}
{{- define "freesailarr.resourcesFor" -}}
{{- $fb := fromYaml (include "freesailarr.fallbacks" .root) -}}
{{- $defaults := index $fb .app "resources" -}}
{{- $merged := fromYaml (include "freesailarr.defaultedDict" (dict "defaults" $defaults "given" .given "path" (printf "%s.resources" .app))) -}}
{{- include "freesailarr.resources" $merged -}}
{{- end -}}
+59 -54
View File
@@ -17,12 +17,23 @@ spec:
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:
{{- toYaml .Values.securityContext.pod | nindent 8 }}
{{- include "freesailarr.podSecurityContext" . | nindent 8 }}
{{- if and .Values.qbittorrent.enabled .Values.qbittorrent.networkInterface }}
initContainers:
# qBittorrent has no environment variable for the session's network
@@ -71,11 +82,9 @@ spec:
- name: QBT_INTERFACE
value: {{ .Values.qbittorrent.networkInterface | quote }}
resources:
{{- include "freesailarr.resources" .Values.qbittorrent.configInit.resources | nindent 12 }}
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "configInit" "given" .Values.qbittorrent.configInit.resources) | nindent 12 }}
securityContext:
runAsUser: {{ .Values.puid }}
runAsGroup: {{ .Values.pgid }}
{{- toYaml .Values.qbittorrent.configInit.securityContext | nindent 12 }}
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "configInit" "given" .Values.qbittorrent.configInit.securityContext) | nindent 12 }}
volumeMounts:
- mountPath: /config
name: qbittorrent-config
@@ -151,15 +160,15 @@ spec:
protocol: TCP
name: health
livenessProbe:
{{- toYaml .Values.gluetun.livenessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "gluetun" "kind" "livenessProbe" "given" .Values.gluetun.livenessProbe) | nindent 12 }}
readinessProbe:
{{- toYaml .Values.gluetun.readinessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "gluetun" "kind" "readinessProbe" "given" .Values.gluetun.readinessProbe) | nindent 12 }}
startupProbe:
{{- toYaml .Values.gluetun.startupProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "gluetun" "kind" "startupProbe" "given" .Values.gluetun.startupProbe) | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.gluetun.resources | nindent 12 }}
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "gluetun" "given" .Values.gluetun.resources) | nindent 12 }}
securityContext:
{{- toYaml .Values.gluetun.securityContext | nindent 12 }}
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "gluetun" "given" .Values.gluetun.securityContext) | nindent 12 }}
volumeMounts:
- mountPath: /gluetun
name: gluetun-config
@@ -201,15 +210,15 @@ spec:
protocol: TCP
name: qbittorrent
livenessProbe:
{{- toYaml .Values.qbittorrent.livenessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "qbittorrent" "kind" "livenessProbe" "given" .Values.qbittorrent.livenessProbe) | nindent 12 }}
readinessProbe:
{{- toYaml .Values.qbittorrent.readinessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "qbittorrent" "kind" "readinessProbe" "given" .Values.qbittorrent.readinessProbe) | nindent 12 }}
startupProbe:
{{- toYaml .Values.qbittorrent.startupProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "qbittorrent" "kind" "startupProbe" "given" .Values.qbittorrent.startupProbe) | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.qbittorrent.resources | nindent 12 }}
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "qbittorrent" "given" .Values.qbittorrent.resources) | nindent 12 }}
securityContext:
{{- toYaml .Values.qbittorrent.securityContext | nindent 12 }}
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "qbittorrent" "given" .Values.qbittorrent.securityContext) | nindent 12 }}
volumeMounts:
- mountPath: /config
name: qbittorrent-config
@@ -236,15 +245,15 @@ spec:
protocol: TCP
name: prowlarr
livenessProbe:
{{- toYaml .Values.prowlarr.livenessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "prowlarr" "kind" "livenessProbe" "given" .Values.prowlarr.livenessProbe) | nindent 12 }}
readinessProbe:
{{- toYaml .Values.prowlarr.readinessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "prowlarr" "kind" "readinessProbe" "given" .Values.prowlarr.readinessProbe) | nindent 12 }}
startupProbe:
{{- toYaml .Values.prowlarr.startupProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "prowlarr" "kind" "startupProbe" "given" .Values.prowlarr.startupProbe) | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.prowlarr.resources | nindent 12 }}
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "prowlarr" "given" .Values.prowlarr.resources) | nindent 12 }}
securityContext:
{{- toYaml .Values.prowlarr.securityContext | nindent 12 }}
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "prowlarr" "given" .Values.prowlarr.securityContext) | nindent 12 }}
volumeMounts:
- mountPath: /config
name: prowlarr-config
@@ -262,15 +271,15 @@ spec:
protocol: TCP
name: flaresolverr
livenessProbe:
{{- toYaml .Values.flaresolverr.livenessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "flaresolverr" "kind" "livenessProbe" "given" .Values.flaresolverr.livenessProbe) | nindent 12 }}
readinessProbe:
{{- toYaml .Values.flaresolverr.readinessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "flaresolverr" "kind" "readinessProbe" "given" .Values.flaresolverr.readinessProbe) | nindent 12 }}
startupProbe:
{{- toYaml .Values.flaresolverr.startupProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "flaresolverr" "kind" "startupProbe" "given" .Values.flaresolverr.startupProbe) | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.flaresolverr.resources | nindent 12 }}
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "flaresolverr" "given" .Values.flaresolverr.resources) | nindent 12 }}
securityContext:
{{- toYaml .Values.flaresolverr.securityContext | nindent 12 }}
{{- 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
@@ -297,15 +306,15 @@ spec:
protocol: TCP
name: radarr
livenessProbe:
{{- toYaml .Values.radarr.livenessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "radarr" "kind" "livenessProbe" "given" .Values.radarr.livenessProbe) | nindent 12 }}
readinessProbe:
{{- toYaml .Values.radarr.readinessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "radarr" "kind" "readinessProbe" "given" .Values.radarr.readinessProbe) | nindent 12 }}
startupProbe:
{{- toYaml .Values.radarr.startupProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "radarr" "kind" "startupProbe" "given" .Values.radarr.startupProbe) | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.radarr.resources | nindent 12 }}
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "radarr" "given" .Values.radarr.resources) | nindent 12 }}
securityContext:
{{- toYaml .Values.radarr.securityContext | nindent 12 }}
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "radarr" "given" .Values.radarr.securityContext) | nindent 12 }}
volumeMounts:
- mountPath: /config
name: radarr-config
@@ -331,17 +340,15 @@ spec:
value: "true"
{{- include "freesailarr.env" (dict "root" $ "env" .Values.recyclarr.env) | nindent 12 }}
livenessProbe:
{{- toYaml .Values.recyclarr.livenessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "recyclarr" "kind" "livenessProbe" "given" .Values.recyclarr.livenessProbe) | nindent 12 }}
readinessProbe:
{{- toYaml .Values.recyclarr.readinessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "recyclarr" "kind" "readinessProbe" "given" .Values.recyclarr.readinessProbe) | nindent 12 }}
startupProbe:
{{- toYaml .Values.recyclarr.startupProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "recyclarr" "kind" "startupProbe" "given" .Values.recyclarr.startupProbe) | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.recyclarr.resources | nindent 12 }}
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "recyclarr" "given" .Values.recyclarr.resources) | nindent 12 }}
securityContext:
runAsUser: {{ .Values.puid }}
runAsGroup: {{ .Values.pgid }}
{{- toYaml .Values.recyclarr.securityContext | nindent 12 }}
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "recyclarr" "given" .Values.recyclarr.securityContext) | nindent 12 }}
volumeMounts:
- mountPath: /config
name: recyclarr-config
@@ -367,15 +374,15 @@ spec:
protocol: TCP
name: sonarr
livenessProbe:
{{- toYaml .Values.sonarr.livenessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "sonarr" "kind" "livenessProbe" "given" .Values.sonarr.livenessProbe) | nindent 12 }}
readinessProbe:
{{- toYaml .Values.sonarr.readinessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "sonarr" "kind" "readinessProbe" "given" .Values.sonarr.readinessProbe) | nindent 12 }}
startupProbe:
{{- toYaml .Values.sonarr.startupProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "sonarr" "kind" "startupProbe" "given" .Values.sonarr.startupProbe) | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.sonarr.resources | nindent 12 }}
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "sonarr" "given" .Values.sonarr.resources) | nindent 12 }}
securityContext:
{{- toYaml .Values.sonarr.securityContext | nindent 12 }}
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "sonarr" "given" .Values.sonarr.securityContext) | nindent 12 }}
volumeMounts:
- mountPath: /config
name: sonarr-config
@@ -403,17 +410,15 @@ spec:
protocol: TCP
name: seerr
livenessProbe:
{{- toYaml .Values.seerr.livenessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "seerr" "kind" "livenessProbe" "given" .Values.seerr.livenessProbe) | nindent 12 }}
readinessProbe:
{{- toYaml .Values.seerr.readinessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "seerr" "kind" "readinessProbe" "given" .Values.seerr.readinessProbe) | nindent 12 }}
startupProbe:
{{- toYaml .Values.seerr.startupProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "seerr" "kind" "startupProbe" "given" .Values.seerr.startupProbe) | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.seerr.resources | nindent 12 }}
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "seerr" "given" .Values.seerr.resources) | nindent 12 }}
securityContext:
runAsUser: {{ .Values.puid }}
runAsGroup: {{ .Values.pgid }}
{{- toYaml .Values.seerr.securityContext | nindent 12 }}
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "seerr" "given" .Values.seerr.securityContext) | nindent 12 }}
volumeMounts:
- mountPath: /app/config
name: seerr-config
@@ -437,15 +442,15 @@ spec:
protocol: TCP
name: bazarr
livenessProbe:
{{- toYaml .Values.bazarr.livenessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "bazarr" "kind" "livenessProbe" "given" .Values.bazarr.livenessProbe) | nindent 12 }}
readinessProbe:
{{- toYaml .Values.bazarr.readinessProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "bazarr" "kind" "readinessProbe" "given" .Values.bazarr.readinessProbe) | nindent 12 }}
startupProbe:
{{- toYaml .Values.bazarr.startupProbe | nindent 12 }}
{{- include "freesailarr.probeFor" (dict "root" $ "app" "bazarr" "kind" "startupProbe" "given" .Values.bazarr.startupProbe) | nindent 12 }}
resources:
{{- include "freesailarr.resources" .Values.bazarr.resources | nindent 12 }}
{{- include "freesailarr.resourcesFor" (dict "root" $ "app" "bazarr" "given" .Values.bazarr.resources) | nindent 12 }}
securityContext:
{{- toYaml .Values.bazarr.securityContext | nindent 12 }}
{{- include "freesailarr.securityContextFor" (dict "root" $ "app" "bazarr" "given" .Values.bazarr.securityContext) | nindent 12 }}
volumeMounts:
- mountPath: /config
name: bazarr-config
+730
View File
@@ -0,0 +1,730 @@
{
"$schema": "https://json-schema.org/draft-07/schema#",
"title": "freesailarr values",
"description": "Values schema (issue #68). additionalProperties is false on every chart-owned object level, so a key the chart does not read is rejected at render time instead of being silently ignored. The objects that go verbatim into the pod spec - probes, security contexts, free-form annotations, global - stay open, because the chart does not interpret their contents. The hardening, probe and resources blocks additionally accept null: an override may erase them, and the chart then falls back to its own defaults (issue #99) rather than rendering a container without hardening, without a probe or without resource accounting.",
"type": "object",
"additionalProperties": false,
"properties": {
"global": {
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
"type": "object"
},
"scaleDown": {
"type": "boolean"
},
"replicas": {
"type": "integer",
"minimum": 0
},
"secrets": {
"type": "object",
"additionalProperties": false,
"properties": {
"gluetunSecretRef": {
"description": "op:// item path for the gluetun VPN credentials. The item must carry the fields wireguard-private-key and wireguard-addresses; the template reads exactly those names.",
"type": [
"string",
"null"
]
}
}
},
"puid": {
"description": "User the linuxserver.io containers drop to after their s6-overlay init, and runAsUser for seerr and recyclarr.",
"type": "integer"
},
"pgid": {
"type": "integer"
},
"timezone": {
"type": [
"string",
"null"
]
},
"securityContext": {
"$ref": "#/definitions/podSecurityContextBlock"
},
"storage": {
"description": "StorageClass for the config volumeClaimTemplates. Deliberately NOT null-safe: an erased block yields an invalid claim template, which fails loudly, and issue #99 covers only the silent class.",
"type": "object",
"additionalProperties": false,
"properties": {
"storageClass": {
"type": [
"string",
"null"
]
}
}
},
"media": {
"type": "object",
"additionalProperties": false,
"properties": {
"downloads": {
"type": "object",
"additionalProperties": false,
"properties": {
"existingClaim": {
"type": [
"string",
"null"
]
},
"subPath": {
"description": "Sub-directory of the claim to mount instead of its root. null means the claim root.",
"type": [
"string",
"null"
]
}
}
},
"movies": {
"type": "object",
"additionalProperties": false,
"properties": {
"existingClaim": {
"type": [
"string",
"null"
]
},
"subPath": {
"description": "Sub-directory of the claim to mount instead of its root. null means the claim root.",
"type": [
"string",
"null"
]
}
}
},
"tv": {
"type": "object",
"additionalProperties": false,
"properties": {
"existingClaim": {
"type": [
"string",
"null"
]
},
"subPath": {
"description": "Sub-directory of the claim to mount instead of its root. null means the claim root.",
"type": [
"string",
"null"
]
}
}
}
}
},
"ingress": {
"description": "Defaults for the per-app ingresses. There is deliberately no global domain - each app carries its own host.",
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"description": "Master switch; false suppresses every app ingress regardless of the per-app toggles.",
"type": "boolean"
},
"clusterIssuer": {
"type": [
"string",
"null"
]
},
"className": {
"type": [
"string",
"null"
]
},
"annotations": {
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open.",
"type": "object"
}
}
},
"gluetun": {
"type": "object",
"additionalProperties": false,
"properties": {
"image": {
"$ref": "#/definitions/image"
},
"vpn": {
"type": "object",
"additionalProperties": false,
"properties": {
"serviceProvider": {
"type": [
"string",
"null"
]
},
"type": {
"type": [
"string",
"null"
]
},
"serverCities": {
"type": [
"string",
"null"
]
},
"serverCountries": {
"type": [
"string",
"null"
]
},
"dnsUpstreamResolvers": {
"type": [
"string",
"null"
]
},
"firewallOutboundSubnets": {
"description": "Comma-separated CIDRs the gluetun firewall allows outbound outside the tunnel. Set to the cluster pod and service CIDRs.",
"type": [
"string",
"null"
]
}
}
},
"storage": {
"$ref": "#/definitions/appStorage"
},
"env": {
"$ref": "#/definitions/env"
},
"livenessProbe": {
"$ref": "#/definitions/probe"
},
"readinessProbe": {
"$ref": "#/definitions/probe"
},
"startupProbe": {
"$ref": "#/definitions/probe"
},
"resources": {
"$ref": "#/definitions/resources"
},
"securityContext": {
"$ref": "#/definitions/containerSecurityContext"
}
}
},
"qbittorrent": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"image": {
"$ref": "#/definitions/image"
},
"ingress": {
"$ref": "#/definitions/appIngress"
},
"storage": {
"$ref": "#/definitions/appStorage"
},
"networkInterface": {
"description": "Interface the BitTorrent session binds to (gluetun's tun device). null or \"\" removes the init container that seeds it and leaves the binding unset - which lets libtorrent announce past the tunnel.",
"type": [
"string",
"null"
]
},
"configInit": {
"type": "object",
"additionalProperties": false,
"properties": {
"image": {
"$ref": "#/definitions/image"
},
"resources": {
"$ref": "#/definitions/resources"
},
"securityContext": {
"$ref": "#/definitions/containerSecurityContext"
}
}
},
"env": {
"$ref": "#/definitions/env"
},
"livenessProbe": {
"$ref": "#/definitions/probe"
},
"readinessProbe": {
"$ref": "#/definitions/probe"
},
"startupProbe": {
"$ref": "#/definitions/probe"
},
"resources": {
"$ref": "#/definitions/resources"
},
"securityContext": {
"$ref": "#/definitions/containerSecurityContext"
}
}
},
"prowlarr": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"image": {
"$ref": "#/definitions/image"
},
"ingress": {
"$ref": "#/definitions/appIngress"
},
"storage": {
"$ref": "#/definitions/appStorage"
},
"env": {
"$ref": "#/definitions/env"
},
"livenessProbe": {
"$ref": "#/definitions/probe"
},
"readinessProbe": {
"$ref": "#/definitions/probe"
},
"startupProbe": {
"$ref": "#/definitions/probe"
},
"resources": {
"$ref": "#/definitions/resources"
},
"securityContext": {
"$ref": "#/definitions/containerSecurityContext"
}
}
},
"flaresolverr": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"image": {
"$ref": "#/definitions/image"
},
"env": {
"$ref": "#/definitions/env"
},
"livenessProbe": {
"$ref": "#/definitions/probe"
},
"readinessProbe": {
"$ref": "#/definitions/probe"
},
"startupProbe": {
"$ref": "#/definitions/probe"
},
"resources": {
"$ref": "#/definitions/resources"
},
"securityContext": {
"$ref": "#/definitions/containerSecurityContext"
}
}
},
"radarr": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"image": {
"$ref": "#/definitions/image"
},
"ingress": {
"$ref": "#/definitions/appIngress"
},
"storage": {
"$ref": "#/definitions/appStorage"
},
"env": {
"$ref": "#/definitions/env"
},
"livenessProbe": {
"$ref": "#/definitions/probe"
},
"readinessProbe": {
"$ref": "#/definitions/probe"
},
"startupProbe": {
"$ref": "#/definitions/probe"
},
"resources": {
"$ref": "#/definitions/resources"
},
"securityContext": {
"$ref": "#/definitions/containerSecurityContext"
}
}
},
"recyclarr": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"image": {
"$ref": "#/definitions/image"
},
"storage": {
"$ref": "#/definitions/appStorage"
},
"env": {
"$ref": "#/definitions/env"
},
"livenessProbe": {
"$ref": "#/definitions/probe"
},
"readinessProbe": {
"$ref": "#/definitions/probe"
},
"startupProbe": {
"$ref": "#/definitions/probe"
},
"resources": {
"$ref": "#/definitions/resources"
},
"securityContext": {
"$ref": "#/definitions/containerSecurityContext"
}
}
},
"sonarr": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"image": {
"$ref": "#/definitions/image"
},
"ingress": {
"$ref": "#/definitions/appIngress"
},
"storage": {
"$ref": "#/definitions/appStorage"
},
"env": {
"$ref": "#/definitions/env"
},
"livenessProbe": {
"$ref": "#/definitions/probe"
},
"readinessProbe": {
"$ref": "#/definitions/probe"
},
"startupProbe": {
"$ref": "#/definitions/probe"
},
"resources": {
"$ref": "#/definitions/resources"
},
"securityContext": {
"$ref": "#/definitions/containerSecurityContext"
}
}
},
"seerr": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"image": {
"$ref": "#/definitions/image"
},
"ingress": {
"$ref": "#/definitions/appIngress"
},
"storage": {
"$ref": "#/definitions/appStorage"
},
"env": {
"$ref": "#/definitions/env"
},
"livenessProbe": {
"$ref": "#/definitions/probe"
},
"readinessProbe": {
"$ref": "#/definitions/probe"
},
"startupProbe": {
"$ref": "#/definitions/probe"
},
"resources": {
"$ref": "#/definitions/resources"
},
"securityContext": {
"$ref": "#/definitions/containerSecurityContext"
}
}
},
"bazarr": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"image": {
"$ref": "#/definitions/image"
},
"ingress": {
"$ref": "#/definitions/appIngress"
},
"storage": {
"$ref": "#/definitions/appStorage"
},
"env": {
"$ref": "#/definitions/env"
},
"livenessProbe": {
"$ref": "#/definitions/probe"
},
"readinessProbe": {
"$ref": "#/definitions/probe"
},
"startupProbe": {
"$ref": "#/definitions/probe"
},
"resources": {
"$ref": "#/definitions/resources"
},
"securityContext": {
"$ref": "#/definitions/containerSecurityContext"
}
}
}
},
"definitions": {
"image": {
"description": "Every image in this chart is pinned here, gluetun included; no tag comes from appVersion. This is also what puts them all under Renovate's helm-values manager, which reads exactly these two keys.",
"type": "object",
"additionalProperties": false,
"properties": {
"repository": {
"type": "string"
},
"tag": {
"type": [
"string",
"number"
]
}
}
},
"appIngress": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"host": {
"description": "Required while this app's ingress is enabled, and never demanded when it is off.",
"type": [
"string",
"null"
]
},
"clusterIssuer": {
"type": [
"string",
"null"
]
},
"className": {
"type": [
"string",
"null"
]
},
"annotations": {
"description": "Overrides the global ingress annotations for this app; free-form, so kept open.",
"type": [
"object",
"null"
]
}
}
},
"appStorage": {
"description": "Size of this app's config volumeClaimTemplate. Not null-safe on purpose: an erased size yields an invalid claim template and fails loudly.",
"type": "object",
"additionalProperties": false,
"properties": {
"size": {
"type": [
"string",
"null"
]
}
}
},
"resources": {
"description": "May be null / empty: the chart then falls back to this container's own default requests and computed limits (issue #99).",
"type": [
"object",
"null"
],
"additionalProperties": false,
"properties": {
"limitFactor": {
"type": "number",
"exclusiveMinimum": 0
},
"requests": {
"$ref": "#/definitions/resourceList"
},
"limits": {
"$ref": "#/definitions/resourceList"
}
}
},
"resourceList": {
"description": "Both spellings of the ephemeral-storage key are accepted because the resources helper normalizes camelCase to the Kubernetes key.",
"type": [
"object",
"null"
],
"additionalProperties": false,
"properties": {
"cpu": {
"type": [
"string",
"number",
"null"
]
},
"memory": {
"type": [
"string",
"number",
"null"
]
},
"ephemeral-storage": {
"type": [
"string",
"number",
"null"
]
},
"ephemeralStorage": {
"type": [
"string",
"number",
"null"
]
}
}
},
"env": {
"type": [
"array",
"null"
],
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
},
"valueFrom": {
"type": "object",
"additionalProperties": false,
"properties": {
"secretKeyRef": {
"$ref": "#/definitions/keyRef"
},
"configMapKeyRef": {
"$ref": "#/definitions/keyRef"
}
}
}
}
}
},
"keyRef": {
"type": "object",
"additionalProperties": false,
"required": [
"name",
"key"
],
"properties": {
"name": {
"type": "string"
},
"key": {
"type": "string"
}
}
},
"podSecurityContextBlock": {
"description": "May be null / empty at any level: the chart then falls back to its pod hardening defaults (issue #99), seccompProfile included.",
"type": [
"object",
"null"
],
"additionalProperties": false,
"properties": {
"pod": {
"description": "Merged onto the chart's pod hardening defaults; kept open because the chart does not interpret the individual fields.",
"type": [
"object",
"null"
]
}
}
},
"containerSecurityContext": {
"description": "Merged onto THIS container's hardening defaults; kept open because the chart does not interpret the individual fields. May be null / empty, in which case those defaults apply (issue #99). The fallbacks reproduce this chart's deliberate deviations - the linuxserver.io containers start as root and keep a writable root filesystem so their s6 init can drop to PUID/PGID, and restoring the 'clean' standard there would leave them on the image's uid 911.",
"type": [
"object",
"null"
]
},
"probe": {
"description": "Merged onto this container's probe defaults; kept open because the chart does not interpret the individual fields. May be null / empty, which means the defaults apply (issue #99) rather than the probe disappearing. The defaults carry their context - path and port, and recyclarr's exec command, since it serves no HTTP port.",
"type": [
"object",
"null"
]
}
}
}
+12
View File
@@ -54,9 +54,21 @@ securityContext:
# group-writable for the non-root containers; fsGroupChangePolicy
# OnRootMismatch avoids recursively re-chowning the (large, shared) media
# claims on every pod start.
# 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
# value being repeated. It is worth naming what it has to survive here:
# gluetun runs as root with NET_ADMIN, creates a tun device and programs
# iptables, and the s6-overlay init of the five linuxserver.io images starts
# as root and drops to PUID/PGID via CHOWN/SETUID/SETGID. Both were verified
# against a real tunnel under this filter - see the chart's PR.
# A container that genuinely needs a different filter can still override it
# under <app>.securityContext, which wins over the pod value.
pod:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
storage:
# StorageClass for all config volumeClaimTemplates (null = cluster default).