Adds a second, optional ingress shape and fixes the download target of qBittorrent. The existing per-app-host shape stays the default and renders unchanged. Shared domain (#167) Setting ingress.host moves every app without a host of its own into one Ingress object with one path each (<app>.ingress.path, default /<app>), one TLS secret and therefore one DNS name and one certificate for the whole stack. A per-app host always wins and keeps its own object, so mixed operation needs no extra values. The apps differ in what they can do under a sub-path, and the chart follows that rather than pretending they are alike: - radarr, sonarr, prowlarr get <APP>__SERVER__URLBASE from the chart, but only when their own env block does not already carry the variable. - bazarr has no environment variable for its base path and stays one documented manual step (Settings -> General -> Base URL). - qbittorrent has no base path at all and renders a second object on the same host with a prefix-stripping rewrite. It repeats the same TLS secret name and deliberately omits the cert-manager annotation, so both objects state their own TLS while only one certificate is requested. - seerr has no base path either and cannot be rewritten, so it is pinned to "/" and a fail refuses any other path. A second fail refuses two apps claiming the same path. qBittorrent save path (#166) Without Session\DefaultSavePath qBittorrent writes to its built-in default /config/Downloads - the 1Gi config volume. It fills within minutes, every torrent goes to "errored" and qBittorrent can no longer write its own metadata to BT_backup, so a restart in that state costs the torrent list. The init container (renamed qbittorrent-config-interface -> qbittorrent-config-seed, since it no longer only sets the interface) now seeds qbittorrent.savePath, default /downloads. Unlike the interface binding it is inserted ONLY when the key is absent and never replaced: a path changed in the WebUI is a legitimate operator decision and must survive a restart. This is the reason for the major bump: an installation that never set the path writes somewhere else after the upgrade. Documentation NOTES.txt and the media block in values.yaml now spell out why a shared claim with three subPaths does not produce hardlinks: the kernel compares the vfsmount in do_linkat, not the superblock, so two bind mounts of one filesystem are EXDEV - while stat reports the same device number on both sides and suggests the opposite. The layout that would work (one claim, one mount, categories as sub-directories) is named as a possibility; the chart cannot express it today. README.md gains the shared domain as the second admissible ingress shape of the style guideline.
930 lines
29 KiB
Smarty
930 lines
29 KiB
Smarty
{{/*
|
|
Render the body of a container "resources:" block from a values dict of the form
|
|
|
|
resources:
|
|
limitFactor: 3 # optional, default 3
|
|
requests: { cpu, memory, ephemeral-storage / ephemeralStorage, ... }
|
|
limits: { ... } # optional, every entry optional
|
|
|
|
Rules (variant B, see issue #19):
|
|
- requests are rendered as given (camelCase "ephemeralStorage" is normalized
|
|
to the Kubernetes key "ephemeral-storage").
|
|
- an explicitly set limit always wins.
|
|
- a missing memory / ephemeral-storage limit is computed as
|
|
limitFactor x the corresponding request.
|
|
- a missing cpu limit is NOT set at all (the kubelet then treats CPU as
|
|
unlimited; CPU is compressible, so this cannot kill the workload).
|
|
|
|
Usage:
|
|
resources:
|
|
{{- include "freesailarr.resources" .Values.<app>.resources | nindent 12 }}
|
|
|
|
NOTE: charts in this repo deliberately have no dependencies, so this helper 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.resources" -}}
|
|
{{- $resources := . | default dict -}}
|
|
{{- $factor := 3.0 -}}
|
|
{{- if not (kindIs "invalid" $resources.limitFactor) -}}
|
|
{{- $factor = float64 $resources.limitFactor -}}
|
|
{{- end -}}
|
|
{{- $requests := dict -}}
|
|
{{- range $key, $value := ($resources.requests | default dict) -}}
|
|
{{- if not (kindIs "invalid" $value) -}}
|
|
{{- $_ := set $requests (include "freesailarr.resourceKey" $key) $value -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- $limits := dict -}}
|
|
{{- range $key, $value := ($resources.limits | default dict) -}}
|
|
{{- if not (kindIs "invalid" $value) -}}
|
|
{{- $_ := set $limits (include "freesailarr.resourceKey" $key) $value -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- range $key, $request := $requests -}}
|
|
{{- if and (ne $key "cpu") (not (hasKey $limits $key)) -}}
|
|
{{- $_ := set $limits $key (include "freesailarr.multiplyQuantity" (dict "quantity" $request "factor" $factor)) -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- $out := dict -}}
|
|
{{- with $requests }}{{- $_ := set $out "requests" . }}{{- end -}}
|
|
{{- with $limits }}{{- $_ := set $out "limits" . }}{{- end -}}
|
|
{{- toYaml $out -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Normalize a resource name from values to its Kubernetes key.
|
|
*/}}
|
|
{{- define "freesailarr.resourceKey" -}}
|
|
{{- if eq . "ephemeralStorage" -}}ephemeral-storage{{- else -}}{{ . }}{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Multiply a Kubernetes quantity by a factor, keeping its suffix.
|
|
Input: dict "quantity" <string|number> "factor" <float64>.
|
|
Handles plain numbers ("2", "0.5", 1073741824), decimal ("1.5Gi"), binary
|
|
("512Mi", "1Gi", "10Ki") and milli/SI suffixes ("100m", "500M"). Integer
|
|
results are printed without a decimal point.
|
|
*/}}
|
|
{{- define "freesailarr.multiplyQuantity" -}}
|
|
{{- $quantity := printf "%v" .quantity -}}
|
|
{{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}}
|
|
{{- if not $number -}}
|
|
{{- fail (printf "cannot parse resource quantity %q" $quantity) -}}
|
|
{{- end -}}
|
|
{{- $suffix := trimPrefix $number $quantity -}}
|
|
{{- if not (regexMatch "^(m|k|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$" $suffix) -}}
|
|
{{- fail (printf "unsupported resource quantity suffix %q in %q" $suffix $quantity) -}}
|
|
{{- end -}}
|
|
{{- $product := mulf (float64 $number) .factor -}}
|
|
{{- if eq $product (floor $product) -}}
|
|
{{- printf "%d%s" (int64 $product) $suffix -}}
|
|
{{- else -}}
|
|
{{- printf "%v%s" $product $suffix -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Render the replica count for a Deployment/StatefulSet.
|
|
|
|
Input: dict
|
|
"scaleDown" the chart-global .Values.scaleDown flag (may be nil)
|
|
"replicas" the workload's replicas value (may be nil)
|
|
|
|
Rules (issue #25):
|
|
- scaleDown: true takes precedence and always yields 0.
|
|
- otherwise an explicitly set replicas value is used as given, including an
|
|
explicit 0 — Helm's "default" would swallow 0, hence the explicit
|
|
kindIs "invalid" nil check.
|
|
- a missing replicas value defaults to 1.
|
|
|
|
Usage:
|
|
replicas: {{ include "freesailarr.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.replicas) }}
|
|
|
|
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.replicas" -}}
|
|
{{- if .scaleDown -}}
|
|
0
|
|
{{- else if not (kindIs "invalid" .replicas) -}}
|
|
{{- .replicas -}}
|
|
{{- else -}}
|
|
1
|
|
{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Render additive env entries (the repo-wide <app>.env convention: tpl-rendered,
|
|
supporting the value, secretKeyRef and configMapKeyRef forms). Shared as a
|
|
helper here because this chart has nine containers, each with its own env block.
|
|
|
|
Usage:
|
|
{{- include "freesailarr.env" (dict "root" $ "env" .Values.<app>.env) | nindent 12 }}
|
|
*/}}
|
|
{{- define "freesailarr.env" -}}
|
|
{{- $root := .root -}}
|
|
{{- range $value := .env }}
|
|
{{- if $value.value }}
|
|
- name: {{ $value.name }}
|
|
value: {{ tpl $value.value $root | quote }}
|
|
{{- else if $value.valueFrom }}
|
|
{{- if $value.valueFrom.secretKeyRef }}
|
|
- name: {{ $value.name }}
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: {{ tpl $value.valueFrom.secretKeyRef.name $root | quote }}
|
|
key: {{ tpl $value.valueFrom.secretKeyRef.key $root | quote }}
|
|
{{- else if $value.valueFrom.configMapKeyRef }}
|
|
- name: {{ $value.name }}
|
|
valueFrom:
|
|
configMapKeyRef:
|
|
name: {{ tpl $value.valueFrom.configMapKeyRef.name $root | quote }}
|
|
key: {{ tpl $value.valueFrom.configMapKeyRef.key $root | quote }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
The enabled app containers that listen on a port, as a JSON array of
|
|
{name, port} dicts (container port name == app name == service name suffix).
|
|
Consumed by the per-app services, the per-app ingresses and the firewall-ports
|
|
helper below:
|
|
|
|
{{- $apps := include "freesailarr.portApps" . | fromJsonArray }}
|
|
|
|
recyclarr is deliberately absent: in daemon mode it serves no port, so it gets
|
|
no container port, no service and no ingress. flaresolverr is present - it
|
|
listens on 8191 and therefore gets a service - but it has no ingress block in
|
|
values and is never exposed from outside the cluster.
|
|
*/}}
|
|
{{- define "freesailarr.portApps" -}}
|
|
{{- $apps := list -}}
|
|
{{- range $app := list
|
|
(dict "name" "qbittorrent" "port" 8080)
|
|
(dict "name" "prowlarr" "port" 9696)
|
|
(dict "name" "flaresolverr" "port" 8191)
|
|
(dict "name" "radarr" "port" 7878)
|
|
(dict "name" "sonarr" "port" 8989)
|
|
(dict "name" "seerr" "port" 5055)
|
|
(dict "name" "bazarr" "port" 6767) -}}
|
|
{{- if (index $.Values $app.name).enabled -}}
|
|
{{- $apps = append $apps $app -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- toJson $apps -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
The apps that render an Ingress: the enabled port apps whose values carry an
|
|
ingress block with enabled: true. Used by the ingress template.
|
|
|
|
{{- $apps := include "freesailarr.ingressApps" . | fromJsonArray }}
|
|
*/}}
|
|
{{- define "freesailarr.ingressApps" -}}
|
|
{{- $apps := list -}}
|
|
{{- range $app := include "freesailarr.portApps" . | fromJsonArray -}}
|
|
{{- $config := index $.Values $app.name -}}
|
|
{{- if and $config.ingress $config.ingress.enabled -}}
|
|
{{- $apps = append $apps $app -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- toJson $apps -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
The sub-path an app is served under in the shared-domain mode, e.g. "/sonarr".
|
|
Null-safe by construction: an absent or erased <app>.ingress.path falls back to
|
|
"/<app>", and to "/" for seerr, which has no base-path support at all and can
|
|
therefore only live at the root.
|
|
|
|
Input: dict "root" $ "app" <app name>.
|
|
*/}}
|
|
{{- define "freesailarr.appPath" -}}
|
|
{{- $config := (index .root.Values .app).ingress -}}
|
|
{{- $path := "" -}}
|
|
{{- if kindIs "map" $config -}}
|
|
{{- $path = $config.path -}}
|
|
{{- end -}}
|
|
{{- if or (kindIs "invalid" $path) (eq (toString $path) "") -}}
|
|
{{- if eq .app "seerr" -}}
|
|
{{- $path = "/" -}}
|
|
{{- else -}}
|
|
{{- $path = printf "/%s" .app -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- $path -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
The apps served under the shared domain (ingress.host), as a JSON array of
|
|
{name, port, path} dicts, sorted by path length descending so the rendered list
|
|
is deterministic.
|
|
|
|
The shared-domain mode is off unless ingress.host is set; then it covers the
|
|
apps whose ingress renders at all (freesailarr.ingressApps, i.e. the enabled
|
|
port apps with ingress.enabled) and which do not carry a host of their own. A
|
|
per-app host always wins and keeps rendering its own object exactly as before,
|
|
which is what makes mixed operation fall out for free.
|
|
|
|
Two guards live here rather than in the ingress template, so that every
|
|
consumer - the ingress objects, the UrlBase env vars and NOTES.txt - sees the
|
|
same validated list:
|
|
|
|
1. seerr under any path other than "/". seerr has no base-path support
|
|
(upstream request open since May 2022), so a chart that rendered it under
|
|
a sub-path would produce something that provably cannot work.
|
|
2. Two apps claiming the same path on the shared host. Only paths on the
|
|
shared host can collide; apps with their own host are on a different host
|
|
and are therefore not part of this check.
|
|
|
|
{{- $shared := include "freesailarr.sharedIngressApps" . | fromJsonArray }}
|
|
*/}}
|
|
{{- define "freesailarr.sharedIngressApps" -}}
|
|
{{- $root := . -}}
|
|
{{- $apps := list -}}
|
|
{{- $host := $root.Values.ingress.host | default "" -}}
|
|
{{- if and $root.Values.ingress.enabled $host -}}
|
|
{{- $seen := dict -}}
|
|
{{- range $app := include "freesailarr.ingressApps" $root | fromJsonArray -}}
|
|
{{- $config := (index $root.Values $app.name).ingress -}}
|
|
{{- if not ($config.host | default "") -}}
|
|
{{- $path := include "freesailarr.appPath" (dict "root" $root "app" $app.name) -}}
|
|
{{- if and (eq $app.name "seerr") (ne $path "/") -}}
|
|
{{- fail (printf "seerr.ingress.path is %q, but seerr has no base-path support and can only be served at \"/\" (https://github.com/Fallenbagel/jellyseerr/issues/97). Either leave seerr.ingress.path at \"/\" or give seerr its own seerr.ingress.host." $path) -}}
|
|
{{- end -}}
|
|
{{- if hasKey $seen $path -}}
|
|
{{- fail (printf "%s.ingress.path and %s.ingress.path are both %q on the shared host %q - every app under ingress.host needs its own path" (index $seen $path) $app.name $path $host) -}}
|
|
{{- end -}}
|
|
{{- $_ := set $seen $path $app.name -}}
|
|
{{- $apps = append $apps (dict "name" $app.name "port" $app.port "path" $path) -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- /* Sort by path length descending, ties broken alphabetically. The keys are
|
|
unique because the guard above rejects duplicate paths. */ -}}
|
|
{{- $keyed := dict -}}
|
|
{{- $keys := list -}}
|
|
{{- range $app := $apps -}}
|
|
{{- $key := printf "%03d|%s" (sub 999 (len $app.path)) $app.path -}}
|
|
{{- $_ := set $keyed $key $app -}}
|
|
{{- $keys = append $keys $key -}}
|
|
{{- end -}}
|
|
{{- $sorted := list -}}
|
|
{{- range $key := sortAlpha $keys -}}
|
|
{{- $sorted = append $sorted (index $keyed $key) -}}
|
|
{{- end -}}
|
|
{{- toJson $sorted -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
The value for a servarr app's <APP>__SERVER__URLBASE variable, or the empty
|
|
string when the chart must not set it. Radarr, Sonarr and Prowlarr read their
|
|
base path from this environment variable (schema
|
|
<APP>__CONFIGNAMESPACE__CONFIGITEM, https://wiki.servarr.com/sonarr/environment-variables),
|
|
so the chart can configure them itself as soon as they render under a sub-path.
|
|
|
|
Empty - i.e. the chart sets nothing - in three cases:
|
|
|
|
- the app does not render under the shared host, or renders at "/";
|
|
- the app's own env block already carries the variable. The env blocks are
|
|
documented as additive; emitting a second entry with the same name would
|
|
break that promise, and two identically named variables in one container
|
|
are not a defined state to build on.
|
|
|
|
Input: dict "root" $ "app" <"radarr"|"sonarr"|"prowlarr">.
|
|
*/}}
|
|
{{- define "freesailarr.urlBase" -}}
|
|
{{- $root := .root -}}
|
|
{{- $app := .app -}}
|
|
{{- $var := printf "%s__SERVER__URLBASE" (upper $app) -}}
|
|
{{- $path := "" -}}
|
|
{{- range $entry := include "freesailarr.sharedIngressApps" $root | fromJsonArray -}}
|
|
{{- if eq $entry.name $app -}}
|
|
{{- $path = $entry.path -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- if eq $path "/" -}}
|
|
{{- $path = "" -}}
|
|
{{- end -}}
|
|
{{- if $path -}}
|
|
{{- range $entry := (index $root.Values $app).env -}}
|
|
{{- if eq (toString $entry.name) $var -}}
|
|
{{- $path = "" -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- $path -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Comma-separated FIREWALL_INPUT_PORTS for gluetun: the ports of the enabled app
|
|
containers (ingress traffic and the kubelet probes both arrive on the pod IP)
|
|
plus 9999 for gluetun's own health server.
|
|
|
|
Measured caveat: inside a pod network these rules never actually match. gluetun
|
|
detects the pod's own subnet and inserts
|
|
"-A INPUT -d <pod-subnet> -i eth0 -j ACCEPT" ahead of the per-port rules, and
|
|
every packet arriving at the pod IP matches it by destination - the packet
|
|
counters on the --dport rules stayed at 0 while the subnet rule absorbed the
|
|
traffic. The declaration is kept as an explicit statement of which ports are
|
|
meant to be reachable, and because that subnet detection is gluetun's, not a
|
|
guarantee of the chart's: a CNI that hands out a /32 or a future gluetun that
|
|
drops the rule would make these the only thing keeping probes and ingress
|
|
alive. It is not what opens the ports today.
|
|
*/}}
|
|
{{- define "freesailarr.firewallInputPorts" -}}
|
|
{{- $ports := list -}}
|
|
{{- range $app := include "freesailarr.portApps" . | fromJsonArray -}}
|
|
{{- $ports = append $ports (int $app.port) -}}
|
|
{{- end -}}
|
|
{{- $ports = append $ports 9999 -}}
|
|
{{- join "," $ports -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Whether a media category is mounted at all, i.e. whether at least one app that
|
|
mounts it is enabled. "true" when it is, the empty string when it is not (an
|
|
empty string is falsy in a template "if", so the result can be used directly).
|
|
|
|
downloads: qbittorrent, radarr, sonarr
|
|
movies: radarr, bazarr
|
|
tv: sonarr, bazarr
|
|
|
|
Input: dict "root" $ "category" <"downloads"|"movies"|"tv">.
|
|
*/}}
|
|
{{- define "freesailarr.mediaCategoryEnabled" -}}
|
|
{{- $v := .root.Values -}}
|
|
{{- if eq .category "downloads" -}}
|
|
{{- if or $v.qbittorrent.enabled $v.radarr.enabled $v.sonarr.enabled -}}true{{- end -}}
|
|
{{- else if eq .category "movies" -}}
|
|
{{- if or $v.radarr.enabled $v.bazarr.enabled -}}true{{- end -}}
|
|
{{- else if eq .category "tv" -}}
|
|
{{- if or $v.sonarr.enabled $v.bazarr.enabled -}}true{{- end -}}
|
|
{{- else -}}
|
|
{{- fail (printf "unknown media category %q" .category) -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
The pod volume name a media category mounts (issue #164).
|
|
|
|
Several categories may point at the same existingClaim - one library volume with
|
|
the categories as sub-directories is the intended layout. A pod may reference
|
|
such a claim only ONCE: two volume entries with the same claimName leave the pod
|
|
in Init:0/1 forever, without a single kubelet event (measured on RKE2/Longhorn,
|
|
with RWO as well as RWX). The claim therefore gets exactly one volume entry and
|
|
the categories differ only in the subPath of their mounts.
|
|
|
|
This helper names the owner of that entry: the FIRST ENABLED category, in the
|
|
fixed order
|
|
|
|
downloads, movies, tv
|
|
|
|
that carries the same claim. Two properties of that rule are load-bearing:
|
|
|
|
- The order is fixed and defined here, not derived from the values. A rule
|
|
like "the alphabetically smallest" or "whichever comes first in the values"
|
|
would move the volume name when an unrelated flag changes, and a moved
|
|
volume name is a pod restart.
|
|
- Only ENABLED categories can own the entry, because the three entries are
|
|
rendered under different conditions (see mediaCategoryEnabled above). A
|
|
disabled owner renders no entry, so its name would be a dangling reference
|
|
and the pod would be rejected by the API server. Example: with qbittorrent,
|
|
radarr and sonarr off and bazarr on, "downloads" is not mounted at all -
|
|
"movies" owns a claim shared by all three categories.
|
|
|
|
A category with no claim owns itself, so the required() message for the missing
|
|
claim still fires at the category the user actually configured.
|
|
|
|
Input: dict "root" $ "category" <"downloads"|"movies"|"tv">.
|
|
Usage (mount) : name: {{ include "freesailarr.mediaVolumeName" (dict "root" $ "category" "movies") }}
|
|
Usage (volume): render the entry only for a category that owns itself.
|
|
*/}}
|
|
{{- define "freesailarr.mediaVolumeName" -}}
|
|
{{- $root := .root -}}
|
|
{{- $claim := (index $root.Values.media .category).existingClaim | default "" -}}
|
|
{{- $owner := "" -}}
|
|
{{- if $claim -}}
|
|
{{- range $candidate := list "downloads" "movies" "tv" -}}
|
|
{{- if not $owner -}}
|
|
{{- if include "freesailarr.mediaCategoryEnabled" (dict "root" $root "category" $candidate) -}}
|
|
{{- if eq ((index $root.Values.media $candidate).existingClaim | default "") $claim -}}
|
|
{{- $owner = $candidate -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- if not $owner -}}
|
|
{{- $owner = .category -}}
|
|
{{- end -}}
|
|
media-{{ $owner }}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Whether a media category owns its volume entry, i.e. whether the volumes block
|
|
renders an entry for it. "true" or the empty string, see mediaVolumeName above.
|
|
|
|
Input: dict "root" $ "category" <"downloads"|"movies"|"tv">.
|
|
*/}}
|
|
{{- define "freesailarr.mediaVolumeOwned" -}}
|
|
{{- if eq (include "freesailarr.mediaVolumeName" .) (printf "media-%s" .category) -}}true{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Recursive merge of an override onto defaults where null never deletes and set
|
|
values always win, including false and 0 (issue #99).
|
|
|
|
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 -}}
|