diff --git a/README.md b/README.md index 2e7fc12..70ee0fd 100644 --- a/README.md +++ b/README.md @@ -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//` (`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). diff --git a/freesailarr/Chart.yaml b/freesailarr/Chart.yaml index 814a1cf..e7b5f84 100644 --- a/freesailarr/Chart.yaml +++ b/freesailarr/Chart.yaml @@ -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. diff --git a/freesailarr/templates/NOTES.txt b/freesailarr/templates/NOTES.txt new file mode 100644 index 0000000..e8e6976 --- /dev/null +++ b/freesailarr/templates/NOTES.txt @@ -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 .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..existingClaim is mounted, never created, and is meant to be + shared with your Jellyfin. One claim can serve all three categories via + media..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 }} diff --git a/freesailarr/templates/_helpers.tpl b/freesailarr/templates/_helpers.tpl index 64e7a41..47e236b 100644 --- a/freesailarr/templates/_helpers.tpl +++ b/freesailarr/templates/_helpers.tpl @@ -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" "given" "path" . + +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" "key" "path" . +Needed because ".Values..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" "given" + and, for probes, "kind" . + +"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 -}} diff --git a/freesailarr/templates/freesailarr.sfs.yaml b/freesailarr/templates/freesailarr.sfs.yaml index 4a45414..d896f3c 100644 --- a/freesailarr/templates/freesailarr.sfs.yaml +++ b/freesailarr/templates/freesailarr.sfs.yaml @@ -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 diff --git a/freesailarr/values.schema.json b/freesailarr/values.schema.json new file mode 100644 index 0000000..1a73c9d --- /dev/null +++ b/freesailarr/values.schema.json @@ -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" + ] + } + } +} diff --git a/freesailarr/values.yaml b/freesailarr/values.yaml index 9b22037..b432f1e 100644 --- a/freesailarr/values.yaml +++ b/freesailarr/values.yaml @@ -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 .securityContext, which wins over the pod value. pod: fsGroup: 1000 fsGroupChangePolicy: OnRootMismatch + seccompProfile: + type: RuntimeDefault storage: # StorageClass for all config volumeClaimTemplates (null = cluster default).