Merge pull request #155 from jk-cluster/feat/freesailarr-retrofit
freesailarr: Schema, Null-Absicherung, seccompProfile und NOTES.txt
This commit is contained in:
7 files changed
+1404
-57
No files matched your search
@@ -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).
|
||||
@@ -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.
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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 -}}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
Reference in new issue
Block a user