Merge pull request #59 from jk-cluster/feat/freesailarr-chart
Add freesailarr chart: gluetun-tunneled media automation stack
This commit is contained in:
14 files changed
+1661
-1
No files matched your search
@@ -35,6 +35,11 @@ on:
|
||||
- minecraft-router
|
||||
- minecraft-server
|
||||
- rss-bridge
|
||||
# freesailarr is deliberately absent: none of its nine images takes
|
||||
# its tag from appVersion (they are all pinned in its values.yaml),
|
||||
# so this workflow would rewrite appVersion and republish the chart
|
||||
# without changing a single deployed image. Update its images through
|
||||
# a Renovate PR, and its appVersion by hand.
|
||||
workflow_call:
|
||||
inputs:
|
||||
app-version:
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
name: Publish Freesailarr Helm Chart
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- freesailarr/**
|
||||
jobs:
|
||||
publish-freesailarr:
|
||||
name: Publish Freesailarr Helm Chart
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'freesailarr'
|
||||
secrets: inherit
|
||||
@@ -9,6 +9,7 @@ Collection of some self-made helm-charts. Each top-level directory is a standalo
|
||||
| `apache-tika` | Apache Tika | Deployment + Service only (no ingress) |
|
||||
| `calibre-web-automated` | Calibre-Web Automated | StatefulSet, extra consume-PVC |
|
||||
| `cyberchef` | CyberChef | Stateless Deployment |
|
||||
| `freesailarr` | gluetun VPN media stack (qbittorrent, prowlarr, flaresolverr, radarr, recyclarr, sonarr, seerr, bazarr) | Single-pod StatefulSet: all apps share gluetun's network namespace; config volumes via volumeClaimTemplates, media PVCs as existingClaims (optional per-category `subPath`, so one claim can serve all three categories). One ClusterIP Service and one Ingress **per app** — no global `ingress.domain`, each app has its own required host (documented ingress exception below) |
|
||||
| `gotenberg` | Gotenberg | Deployment + Service only (no ingress) |
|
||||
| `homeassistant` | Home Assistant | StatefulSet, has `NOTES.txt` and Helm tests |
|
||||
| `matrix-synapse` | Matrix Synapse | StatefulSet plus an nginx delegation Deployment/Service |
|
||||
@@ -65,10 +66,13 @@ What Renovate keeps current:
|
||||
| --- | --- |
|
||||
| Chart app versions (`appVersion:` in `Chart.yaml`) | One `customManagers` entry per chart, hard-coding the image repository |
|
||||
| Sidecar/init images in `values.yaml` (nginx, guacd, busybox, alpine) | Built-in `helm-values` manager |
|
||||
| **All** images of `freesailarr`, including its main one | Built-in `helm-values` manager — the chart pins every tag in `values.yaml` and has no app-version block |
|
||||
| GitHub Actions | Built-in `github-actions` manager |
|
||||
| Tool versions pinned as workflow env vars (`TRIVY_VERSION`) | `# renovate: datasource=… depName=…` comment above the line |
|
||||
|
||||
Each chart renders its main image as `<repo>:{{ .Chart.AppVersion }}` — repository in the template, version in `Chart.yaml`. Renovate cannot join two files, so the mapping lives in `renovate.json5`, one block per chart. It is deliberately *not* a `# renovate:` comment inside each `Chart.yaml`: that would touch all chart directories and force a version bump plus a publish per chart for a pure comment change. Charts whose tags carry a flavour or date suffix (`apache-tika`, `minecraft-server`, `rss-bridge`, `teamspeak-server`) override `versioning` in their block; the reasoning sits next to each one.
|
||||
Each chart renders its main image as `<repo>:{{ .Chart.AppVersion }}` — repository in the template, version in `Chart.yaml`. Renovate cannot join two files, so the mapping lives in `renovate.json5`, one block per chart. It is deliberately *not* a `# renovate:` comment inside each `Chart.yaml`: that would touch all chart directories and force a version bump plus a publish per chart for a pure comment change. Charts whose tags carry a flavour or date suffix (`apache-tika`, `minecraft-server`, `rss-bridge`, `teamspeak-server`) override `versioning` in their block; the reasoning sits next to each one. `freesailarr` has no block on purpose — see the images exception above; a block would either duplicate the `helm-values` updates or rewrite an `appVersion` that selects no image.
|
||||
|
||||
A consequence of pinning every tag in `values.yaml`: `ci/renovate-bump-chart.sh` sees an unchanged `appVersion` and bumps the chart **patch**. That holds for all nine images alike — none of them is special — and it is the intended outcome, since `appVersion` here is the chart's own marker rather than a tracked upstream version.
|
||||
|
||||
One dependency is constrained beyond its versioning: **nginx** (the Synapse delegation sidecar) is limited to **even minors** via `allowedVersions`. nginx numbers its release branches by minor — even is stable, odd is mainline — and the chart belongs on stable. Patch updates inside the stable branch still flow through normally. The constraint tolerates a variant suffix (`1.30.4-alpine`) on purpose: docker versioning only ever offers candidates carrying the current tag's suffix, so a suffix-blind constraint would silently freeze nginx the day the sidecar moves to a slimmer base image — a switch to `1.30.4-alpine` that #84 has commissioned a check on — with no error and no warning.
|
||||
|
||||
@@ -142,6 +146,7 @@ These guidelines are **binding for every current and future chart** in this repo
|
||||
### 3. Images
|
||||
|
||||
- The main app image tag comes from `{{ .Chart.AppVersion }}` — never hardcoded.
|
||||
- **Documented exception — freesailarr**: the chart bundles nine independently versioned upstreams, so none of them is "the" app version. **No** tag comes from `appVersion`; all nine images are pinned as `<app>.image.repository` / `<app>.image.tag` in `values.yaml`, which is also what puts every one of them under the built-in `helm-values` manager. Its `appVersion` is the chart's own version marker, freely chosen and tied to no upstream — raised by hand when a new edition of the chart is marked as a whole. Consequently the chart has **no** block in `renovate.json5` and is **not** in the `change-app-version.yml` choice list.
|
||||
- Auxiliary images (sidecars, init containers, secondary workloads — e.g. the delegation nginx in matrix-synapse, guacd in patchmon, busybox init/consume images in paperless-ngx) are pinned via values instead of being hardcoded in templates, and their pinned versions are reviewed alongside every app-version update round.
|
||||
|
||||
### 4. Ingress
|
||||
@@ -151,6 +156,7 @@ These guidelines are **binding for every current and future chart** in this repo
|
||||
- `ingress.annotations` takes optional extra annotations that are merged with the cert-manager `cluster-issuer` annotation the chart always sets.
|
||||
- The TLS secret is named `tls-<release>-<chart>-ingress`.
|
||||
- **Documented exception — homeassistant**: keeps its upstream-style flexible ingress values (host/tls lists, no required values); only its `ingress.className` default follows the standard (`nginx`, clearable to omit the field).
|
||||
- **Documented exception — freesailarr**: the chart bundles several independently exposed apps in one pod, so it has **no** `ingress.domain`. Each app carries its own `<app>.ingress.enabled` plus a required `<app>.ingress.host` (required only while that app's ingress is enabled) and renders its own Ingress object `<release>-<chart>-<app>-ingress` with its own TLS secret `tls-<release>-<chart>-<app>-ingress`. `clusterIssuer`, `className` and `annotations` stay global defaults under `ingress.` and are overridable per app.
|
||||
|
||||
### 5. Security hardening
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Charts excluded from the PR install-test, one per line: <chart> <reason>.
|
||||
# ci/run-install-test.sh skips these with the given reason.
|
||||
freesailarr gluetun never becomes healthy without a real Wireguard key and VPN connectivity, and every app container sits behind its firewall - the pod cannot become Ready on a CI runner
|
||||
satisfactory the game-server image is multiple GB and Steam downloads 10GB+ game files on first start - impractical on a CI runner
|
||||
template-chart scaffold with placeholder image "ghcr.io/ORG/IMAGE:xxx", never published
|
||||
@@ -0,0 +1,39 @@
|
||||
# Render values for the misconfiguration scan only (ci/run-trivy-config.sh).
|
||||
#
|
||||
# freesailarr has no ci/freesailarr/test-values.yaml because it is excluded from
|
||||
# the install-test (gluetun never becomes healthy without a real Wireguard key),
|
||||
# but it is still scanned - the chart runs nine containers with several
|
||||
# documented hardening deviations, which is exactly what the scan should see.
|
||||
#
|
||||
# Only the values the chart declares `required` - nothing that changes the
|
||||
# security posture (no securityContext, image, capability or resource overrides).
|
||||
# The ingress hosts are per app since the chart has no global ingress.domain.
|
||||
secrets:
|
||||
gluetunSecretRef: op://Vault/scan/placeholder
|
||||
ingress:
|
||||
clusterIssuer: letsencrypt
|
||||
media:
|
||||
downloads:
|
||||
existingClaim: scan-downloads
|
||||
movies:
|
||||
existingClaim: scan-movies
|
||||
tv:
|
||||
existingClaim: scan-tv
|
||||
qbittorrent:
|
||||
ingress:
|
||||
host: qbittorrent.scan.example.com
|
||||
prowlarr:
|
||||
ingress:
|
||||
host: prowlarr.scan.example.com
|
||||
radarr:
|
||||
ingress:
|
||||
host: radarr.scan.example.com
|
||||
sonarr:
|
||||
ingress:
|
||||
host: sonarr.scan.example.com
|
||||
seerr:
|
||||
ingress:
|
||||
host: seerr.scan.example.com
|
||||
bazarr:
|
||||
ingress:
|
||||
host: bazarr.scan.example.com
|
||||
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: v2
|
||||
name: freesailarr
|
||||
description: A Helm chart to deploy a Wireguard-tunneled media automation stack on Kubernetes - a VPN gateway with indexer, downloader, library managers, subtitle fetcher and request front end in one pod, all reaching the internet only through the tunnel
|
||||
# No icon on purpose: the stack has no single upstream, and every other chart's
|
||||
# icon is its one project's logo. Picking any of the nine here would suggest
|
||||
# the same false "main app" the versioning deliberately avoids. The field is
|
||||
# optional, so it is omitted rather than filled with an arbitrary choice.
|
||||
|
||||
type: application
|
||||
|
||||
version: 1.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.
|
||||
#
|
||||
# It is NOT the source of any image tag: all nine images are pinned in
|
||||
# values.yaml, where Renovate's helm-values manager proposes them. Nothing
|
||||
# rewrites this field automatically - the chart has no app-version block in
|
||||
# renovate.json5 and is not in the change-app-version.yml choice list. Raise it
|
||||
# by hand when you want to mark a new edition of the chart as a whole.
|
||||
appVersion: "1.0.0"
|
||||
|
||||
maintainers:
|
||||
- name: jklein
|
||||
@@ -0,0 +1,220 @@
|
||||
{{/*
|
||||
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 -}}
|
||||
|
||||
{{/*
|
||||
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 -}}
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-gluetun-secret
|
||||
spec:
|
||||
itemPath: {{ required "You must provide a gluetunSecret Reference" .Values.secrets.gluetunSecretRef }}
|
||||
@@ -0,0 +1,51 @@
|
||||
{{/*
|
||||
One Ingress object per app whose ingress is enabled, each with its own host and
|
||||
its own TLS secret. The hosts are independent values - they do not have to
|
||||
share a domain - and each ingress points at its own app's service.
|
||||
|
||||
<app>.ingress.host is required, but only for an app whose ingress is actually
|
||||
enabled: the required call sits inside the loop over the enabled apps, so
|
||||
turning an app's ingress off must never make the chart demand a host for it.
|
||||
*/}}
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $app := include "freesailarr.ingressApps" $ | fromJsonArray }}
|
||||
{{- $config := (index $.Values $app.name).ingress }}
|
||||
{{- /* clusterIssuer, className and annotations default to the global ingress
|
||||
block and are overridable per app; an explicitly set per-app value wins
|
||||
even when it is empty, hence the nil check instead of "default". */}}
|
||||
{{- $clusterIssuer := $.Values.ingress.clusterIssuer }}
|
||||
{{- if not (kindIs "invalid" $config.clusterIssuer) }}{{- $clusterIssuer = $config.clusterIssuer }}{{- end }}
|
||||
{{- $className := $.Values.ingress.className }}
|
||||
{{- if not (kindIs "invalid" $config.className) }}{{- $className = $config.className }}{{- end }}
|
||||
{{- $annotations := $.Values.ingress.annotations }}
|
||||
{{- if not (kindIs "invalid" $config.annotations) }}{{- $annotations = $config.annotations }}{{- end }}
|
||||
{{- $host := required (printf "%s.ingress.host must be provided while %s.ingress.enabled is true" $app.name $app.name) $config.host }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $.Release.Name }}-{{ $.Chart.Name }}-{{ $app.name }}-ingress
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided" $clusterIssuer }}
|
||||
{{- with $annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ $className }}
|
||||
rules:
|
||||
- host: {{ $host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: {{ $.Release.Name }}-{{ $.Chart.Name }}-{{ $app.name }}-service
|
||||
port:
|
||||
name: {{ $app.name }}
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ $host | quote }}
|
||||
secretName: tls-{{ $.Release.Name }}-{{ $.Chart.Name }}-{{ $app.name }}-ingress
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,25 @@
|
||||
{{/*
|
||||
One ClusterIP service per enabled app container that listens on a port
|
||||
(recyclarr has none in daemon mode, so it gets no service).
|
||||
|
||||
All containers share gluetun's network namespace, so every one of these
|
||||
services resolves to the same pod IP and they differ only in their port. Their
|
||||
purpose is a stable, per-app cluster-internal DNS name - each ingress points at
|
||||
its own app's service - not network separation between the apps.
|
||||
*/}}
|
||||
{{- range $app := include "freesailarr.portApps" $ | fromJsonArray }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ $.Release.Name }}-{{ $.Chart.Name }}-{{ $app.name }}-service
|
||||
spec:
|
||||
selector:
|
||||
app: {{ $.Release.Name }}-{{ $.Chart.Name }}-sfs
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: {{ int $app.port }}
|
||||
targetPort: {{ $app.name }}
|
||||
name: {{ $app.name }}
|
||||
type: ClusterIP
|
||||
{{- end }}
|
||||
@@ -0,0 +1,524 @@
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
spec:
|
||||
replicas: {{ include "freesailarr.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.replicas) }}
|
||||
# Governing service name. Each app has its own service (see
|
||||
# freesailarr.service.yaml); nothing consumes the per-pod DNS name a
|
||||
# governing service would provide, so the chart deliberately creates no
|
||||
# object under this name - the field only has to be set.
|
||||
serviceName: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
template:
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext.pod | nindent 8 }}
|
||||
{{- if and .Values.qbittorrent.enabled .Values.qbittorrent.networkInterface }}
|
||||
initContainers:
|
||||
# qBittorrent has no environment variable for the session's network
|
||||
# interface - it only reads it from the persisted config file, which
|
||||
# the app itself creates on first start. Without the binding
|
||||
# libtorrent listens and announces on every interface of the shared
|
||||
# pod network namespace (eth0/pod IP and loopback included); gluetun's
|
||||
# kill switch drops those packets, trackers stay unreachable and
|
||||
# torrents stall at 0 bytes. Seeding the two keys before qbittorrent
|
||||
# starts closes that leak and makes a fresh install work out of the
|
||||
# box. The edit is idempotent and preserves all other settings:
|
||||
# existing keys are replaced, missing ones are inserted into the
|
||||
# [BitTorrent] section (qBittorrent 5.x reads them there), and a
|
||||
# missing file or section is created.
|
||||
- name: qbittorrent-config-interface
|
||||
image: "{{ .Values.qbittorrent.configInit.image.repository }}:{{ .Values.qbittorrent.configInit.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
set -eu
|
||||
conf=/config/qBittorrent/qBittorrent.conf
|
||||
mkdir -p /config/qBittorrent
|
||||
[ -f "$conf" ] || printf '[BitTorrent]\n' > "$conf"
|
||||
# A file without a trailing newline would glue an appended
|
||||
# section header onto the last key.
|
||||
if [ -s "$conf" ] && [ -n "$(tail -c 1 "$conf")" ]; then
|
||||
printf '\n' >> "$conf"
|
||||
fi
|
||||
grep -q '^\[BitTorrent\]$' "$conf" || printf '[BitTorrent]\n' >> "$conf"
|
||||
# Drop any existing binding keys, then re-insert them directly
|
||||
# after the [BitTorrent] header.
|
||||
awk -v ifc="$QBT_INTERFACE" '
|
||||
/^Session\\Interface=/ { next }
|
||||
/^Session\\InterfaceName=/ { next }
|
||||
{ print }
|
||||
/^\[BitTorrent\]$/ {
|
||||
print "Session\\Interface=" ifc
|
||||
print "Session\\InterfaceName=" ifc
|
||||
}
|
||||
' "$conf" > "${conf}.tmp"
|
||||
mv "${conf}.tmp" "$conf"
|
||||
echo "qBittorrent session bound to interface $QBT_INTERFACE"
|
||||
env:
|
||||
- name: QBT_INTERFACE
|
||||
value: {{ .Values.qbittorrent.networkInterface | quote }}
|
||||
resources:
|
||||
{{- include "freesailarr.resources" .Values.qbittorrent.configInit.resources | nindent 12 }}
|
||||
securityContext:
|
||||
runAsUser: {{ .Values.puid }}
|
||||
runAsGroup: {{ .Values.pgid }}
|
||||
{{- toYaml .Values.qbittorrent.configInit.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- mountPath: /config
|
||||
name: qbittorrent-config
|
||||
{{- end }}
|
||||
containers:
|
||||
# gluetun is the VPN gateway: all app containers share the pod's
|
||||
# network namespace, talk to each other over localhost and reach the
|
||||
# internet only through the Wireguard tunnel (gluetun's firewall acts
|
||||
# as kill switch).
|
||||
- name: gluetun
|
||||
image: "{{ .Values.gluetun.image.repository }}:{{ .Values.gluetun.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
# An uncleanly exited gluetun (crash, OOM killer, node issue) leaves
|
||||
# its ip rule, routing table and tun device behind in the shared pod
|
||||
# network namespace, and the restarted container then fails forever
|
||||
# ("adding ip rule 101 ... file exists" / silent tunnel breakage)
|
||||
# while the kill switch blocks all egress. The upstream-recommended
|
||||
# postStart hook is racy (it runs concurrently with the entrypoint
|
||||
# and can delete the rule the new instance just created), so the
|
||||
# stale state is cleaned synchronously before gluetun starts.
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- 'while ip rule del table 51820 2>/dev/null; do :; done; while ip -6 rule del table 51820 2>/dev/null; do :; done; ip link del tun0 2>/dev/null; exec /gluetun-entrypoint'
|
||||
env:
|
||||
- name: TZ
|
||||
value: {{ .Values.timezone | quote }}
|
||||
- name: VPN_SERVICE_PROVIDER
|
||||
value: {{ .Values.gluetun.vpn.serviceProvider | quote }}
|
||||
- name: VPN_TYPE
|
||||
value: {{ .Values.gluetun.vpn.type | quote }}
|
||||
- name: WIREGUARD_PRIVATE_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-gluetun-secret
|
||||
key: wireguard-private-key
|
||||
- name: WIREGUARD_ADDRESSES
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-gluetun-secret
|
||||
key: wireguard-addresses
|
||||
{{- with .Values.gluetun.vpn.serverCities }}
|
||||
- name: SERVER_CITIES
|
||||
value: {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- with .Values.gluetun.vpn.serverCountries }}
|
||||
- name: SERVER_COUNTRIES
|
||||
value: {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- with .Values.gluetun.vpn.dnsUpstreamResolvers }}
|
||||
- name: DNS_UPSTREAM_RESOLVERS
|
||||
value: {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- with .Values.gluetun.vpn.firewallOutboundSubnets }}
|
||||
- name: FIREWALL_OUTBOUND_SUBNETS
|
||||
value: {{ . | quote }}
|
||||
{{- end }}
|
||||
# Declare the UI/probe ports of the enabled apps and the health
|
||||
# server as reachable. Note that inside a pod network gluetun's own
|
||||
# pod-subnet rule already accepts this traffic and these per-port
|
||||
# rules never match (measured - see the helper's comment); they are
|
||||
# the explicit statement of intent and the fallback if that subnet
|
||||
# detection ever stops covering the pod IP.
|
||||
- name: FIREWALL_INPUT_PORTS
|
||||
value: {{ include "freesailarr.firewallInputPorts" . | quote }}
|
||||
# Bind the health server to all interfaces so the kubelet HTTP
|
||||
# probes (which connect to the pod IP, not localhost) reach it.
|
||||
- name: HEALTH_SERVER_ADDRESS
|
||||
value: ":9999"
|
||||
{{- include "freesailarr.env" (dict "root" $ "env" .Values.gluetun.env) | nindent 12 }}
|
||||
ports:
|
||||
- containerPort: 9999
|
||||
protocol: TCP
|
||||
name: health
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.gluetun.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.gluetun.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- toYaml .Values.gluetun.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "freesailarr.resources" .Values.gluetun.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.gluetun.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- mountPath: /gluetun
|
||||
name: gluetun-config
|
||||
- mountPath: /dev/net/tun
|
||||
name: dev-net-tun
|
||||
# gluetun creates /tmp/gluetun (its public-IP cache) mode 0644,
|
||||
# i.e. without the execute bit it needs to enter its own
|
||||
# directory. It runs as root but with all capabilities except
|
||||
# NET_ADMIN dropped, so DAC_OVERRIDE no longer papers over that
|
||||
# and every write fails with "clearing public IP data: open
|
||||
# /tmp/gluetun/ip: permission denied". An emptyDir provides the
|
||||
# path with usable permissions.
|
||||
# Remaining, cosmetic: gluetun also chowns the file, which needs
|
||||
# CAP_CHOWN. With all capabilities but NET_ADMIN dropped that call
|
||||
# is denied and logs "clearing public IP data: chown
|
||||
# /tmp/gluetun/ip: operation not permitted" on every VPN restart.
|
||||
# The file itself is written and the public IP is persisted
|
||||
# (verified at runtime), so this is a log line, not a failure -
|
||||
# granting CHOWN just to silence it would widen the container's
|
||||
# capabilities for nothing.
|
||||
- mountPath: /tmp/gluetun
|
||||
name: gluetun-tmp
|
||||
{{- if .Values.qbittorrent.enabled }}
|
||||
- name: qbittorrent
|
||||
image: "{{ .Values.qbittorrent.image.repository }}:{{ .Values.qbittorrent.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: PUID
|
||||
value: {{ .Values.puid | quote }}
|
||||
- name: PGID
|
||||
value: {{ .Values.pgid | quote }}
|
||||
- name: TZ
|
||||
value: {{ .Values.timezone | quote }}
|
||||
- name: WEBUI_PORT
|
||||
value: "8080"
|
||||
{{- include "freesailarr.env" (dict "root" $ "env" .Values.qbittorrent.env) | nindent 12 }}
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
name: qbittorrent
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.qbittorrent.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.qbittorrent.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- toYaml .Values.qbittorrent.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "freesailarr.resources" .Values.qbittorrent.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.qbittorrent.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- mountPath: /config
|
||||
name: qbittorrent-config
|
||||
- mountPath: /downloads
|
||||
name: media-downloads
|
||||
{{- with .Values.media.downloads.subPath }}
|
||||
subPath: {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.prowlarr.enabled }}
|
||||
- name: prowlarr
|
||||
image: "{{ .Values.prowlarr.image.repository }}:{{ .Values.prowlarr.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: PUID
|
||||
value: {{ .Values.puid | quote }}
|
||||
- name: PGID
|
||||
value: {{ .Values.pgid | quote }}
|
||||
- name: TZ
|
||||
value: {{ .Values.timezone | quote }}
|
||||
{{- include "freesailarr.env" (dict "root" $ "env" .Values.prowlarr.env) | nindent 12 }}
|
||||
ports:
|
||||
- containerPort: 9696
|
||||
protocol: TCP
|
||||
name: prowlarr
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.prowlarr.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.prowlarr.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- toYaml .Values.prowlarr.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "freesailarr.resources" .Values.prowlarr.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.prowlarr.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- mountPath: /config
|
||||
name: prowlarr-config
|
||||
{{- end }}
|
||||
{{- if .Values.flaresolverr.enabled }}
|
||||
- name: flaresolverr
|
||||
image: "{{ .Values.flaresolverr.image.repository }}:{{ .Values.flaresolverr.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: TZ
|
||||
value: {{ .Values.timezone | quote }}
|
||||
{{- include "freesailarr.env" (dict "root" $ "env" .Values.flaresolverr.env) | nindent 12 }}
|
||||
ports:
|
||||
- containerPort: 8191
|
||||
protocol: TCP
|
||||
name: flaresolverr
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.flaresolverr.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.flaresolverr.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- toYaml .Values.flaresolverr.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "freesailarr.resources" .Values.flaresolverr.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- toYaml .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
|
||||
# chromium needs more shared memory than the container default.
|
||||
- mountPath: /tmp
|
||||
name: flaresolverr-tmp
|
||||
- mountPath: /dev/shm
|
||||
name: flaresolverr-shm
|
||||
{{- end }}
|
||||
{{- if .Values.radarr.enabled }}
|
||||
- name: radarr
|
||||
image: "{{ .Values.radarr.image.repository }}:{{ .Values.radarr.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: PUID
|
||||
value: {{ .Values.puid | quote }}
|
||||
- name: PGID
|
||||
value: {{ .Values.pgid | quote }}
|
||||
- name: TZ
|
||||
value: {{ .Values.timezone | quote }}
|
||||
{{- include "freesailarr.env" (dict "root" $ "env" .Values.radarr.env) | nindent 12 }}
|
||||
ports:
|
||||
- containerPort: 7878
|
||||
protocol: TCP
|
||||
name: radarr
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.radarr.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.radarr.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- toYaml .Values.radarr.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "freesailarr.resources" .Values.radarr.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.radarr.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- mountPath: /config
|
||||
name: radarr-config
|
||||
- mountPath: /movies
|
||||
name: media-movies
|
||||
{{- with .Values.media.movies.subPath }}
|
||||
subPath: {{ . | quote }}
|
||||
{{- end }}
|
||||
- mountPath: /downloads
|
||||
name: media-downloads
|
||||
{{- with .Values.media.downloads.subPath }}
|
||||
subPath: {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.recyclarr.enabled }}
|
||||
- name: recyclarr
|
||||
image: "{{ .Values.recyclarr.image.repository }}:{{ .Values.recyclarr.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: TZ
|
||||
value: {{ .Values.timezone | quote }}
|
||||
- name: RECYCLARR_CREATE_CONFIG
|
||||
value: "true"
|
||||
{{- include "freesailarr.env" (dict "root" $ "env" .Values.recyclarr.env) | nindent 12 }}
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.recyclarr.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.recyclarr.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- toYaml .Values.recyclarr.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "freesailarr.resources" .Values.recyclarr.resources | nindent 12 }}
|
||||
securityContext:
|
||||
runAsUser: {{ .Values.puid }}
|
||||
runAsGroup: {{ .Values.pgid }}
|
||||
{{- toYaml .Values.recyclarr.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- mountPath: /config
|
||||
name: recyclarr-config
|
||||
# Writable scratch space for the .NET runtime so the root
|
||||
# filesystem stays read-only.
|
||||
- mountPath: /tmp
|
||||
name: recyclarr-tmp
|
||||
{{- end }}
|
||||
{{- if .Values.sonarr.enabled }}
|
||||
- name: sonarr
|
||||
image: "{{ .Values.sonarr.image.repository }}:{{ .Values.sonarr.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: PUID
|
||||
value: {{ .Values.puid | quote }}
|
||||
- name: PGID
|
||||
value: {{ .Values.pgid | quote }}
|
||||
- name: TZ
|
||||
value: {{ .Values.timezone | quote }}
|
||||
{{- include "freesailarr.env" (dict "root" $ "env" .Values.sonarr.env) | nindent 12 }}
|
||||
ports:
|
||||
- containerPort: 8989
|
||||
protocol: TCP
|
||||
name: sonarr
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.sonarr.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.sonarr.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- toYaml .Values.sonarr.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "freesailarr.resources" .Values.sonarr.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.sonarr.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- mountPath: /config
|
||||
name: sonarr-config
|
||||
- mountPath: /tv
|
||||
name: media-tv
|
||||
{{- with .Values.media.tv.subPath }}
|
||||
subPath: {{ . | quote }}
|
||||
{{- end }}
|
||||
- mountPath: /downloads
|
||||
name: media-downloads
|
||||
{{- with .Values.media.downloads.subPath }}
|
||||
subPath: {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.seerr.enabled }}
|
||||
- name: seerr
|
||||
image: "{{ .Values.seerr.image.repository }}:{{ .Values.seerr.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: TZ
|
||||
value: {{ .Values.timezone | quote }}
|
||||
{{- include "freesailarr.env" (dict "root" $ "env" .Values.seerr.env) | nindent 12 }}
|
||||
ports:
|
||||
- containerPort: 5055
|
||||
protocol: TCP
|
||||
name: seerr
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.seerr.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.seerr.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- toYaml .Values.seerr.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "freesailarr.resources" .Values.seerr.resources | nindent 12 }}
|
||||
securityContext:
|
||||
runAsUser: {{ .Values.puid }}
|
||||
runAsGroup: {{ .Values.pgid }}
|
||||
{{- toYaml .Values.seerr.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- mountPath: /app/config
|
||||
name: seerr-config
|
||||
- mountPath: /tmp
|
||||
name: seerr-tmp
|
||||
{{- end }}
|
||||
{{- if .Values.bazarr.enabled }}
|
||||
- name: bazarr
|
||||
image: "{{ .Values.bazarr.image.repository }}:{{ .Values.bazarr.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: PUID
|
||||
value: {{ .Values.puid | quote }}
|
||||
- name: PGID
|
||||
value: {{ .Values.pgid | quote }}
|
||||
- name: TZ
|
||||
value: {{ .Values.timezone | quote }}
|
||||
{{- include "freesailarr.env" (dict "root" $ "env" .Values.bazarr.env) | nindent 12 }}
|
||||
ports:
|
||||
- containerPort: 6767
|
||||
protocol: TCP
|
||||
name: bazarr
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.bazarr.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.bazarr.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- toYaml .Values.bazarr.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "freesailarr.resources" .Values.bazarr.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.bazarr.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- mountPath: /config
|
||||
name: bazarr-config
|
||||
- mountPath: /movies
|
||||
name: media-movies
|
||||
{{- with .Values.media.movies.subPath }}
|
||||
subPath: {{ . | quote }}
|
||||
{{- end }}
|
||||
- mountPath: /tv
|
||||
name: media-tv
|
||||
{{- with .Values.media.tv.subPath }}
|
||||
subPath: {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
restartPolicy: Always
|
||||
volumes:
|
||||
# The tun device gluetun needs to create the Wireguard tunnel.
|
||||
- name: dev-net-tun
|
||||
hostPath:
|
||||
path: /dev/net/tun
|
||||
type: CharDevice
|
||||
# Writable /tmp/gluetun for gluetun's public-IP cache (see the mount).
|
||||
- name: gluetun-tmp
|
||||
emptyDir: {}
|
||||
{{- if or .Values.qbittorrent.enabled .Values.radarr.enabled .Values.sonarr.enabled }}
|
||||
- name: media-downloads
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ required "media.downloads.existingClaim is required when qbittorrent, radarr or sonarr is enabled" .Values.media.downloads.existingClaim }}
|
||||
{{- end }}
|
||||
{{- if or .Values.radarr.enabled .Values.bazarr.enabled }}
|
||||
- name: media-movies
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ required "media.movies.existingClaim is required when radarr or bazarr is enabled" .Values.media.movies.existingClaim }}
|
||||
{{- end }}
|
||||
{{- if or .Values.sonarr.enabled .Values.bazarr.enabled }}
|
||||
- name: media-tv
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ required "media.tv.existingClaim is required when sonarr or bazarr is enabled" .Values.media.tv.existingClaim }}
|
||||
{{- end }}
|
||||
{{- if .Values.flaresolverr.enabled }}
|
||||
- name: flaresolverr-tmp
|
||||
emptyDir: {}
|
||||
- name: flaresolverr-shm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 512Mi
|
||||
{{- end }}
|
||||
{{- if .Values.recyclarr.enabled }}
|
||||
- name: recyclarr-tmp
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- if .Values.seerr.enabled }}
|
||||
- name: seerr-tmp
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
# Config volumes for gluetun and the enabled apps. NOTE: volumeClaimTemplates
|
||||
# are immutable - toggling an app's enabled flag later requires recreating
|
||||
# the StatefulSet once (kubectl delete statefulset <name> --cascade=orphan
|
||||
# before upgrading; existing PVCs and their data survive).
|
||||
volumeClaimTemplates:
|
||||
{{- range $app := list "gluetun" "qbittorrent" "prowlarr" "radarr" "recyclarr" "sonarr" "seerr" "bazarr" }}
|
||||
{{- $config := index $.Values $app }}
|
||||
{{- if or (eq $app "gluetun") $config.enabled }}
|
||||
- metadata:
|
||||
name: {{ $app }}-config
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ $config.storage.size }}
|
||||
{{- with $.Values.storage.storageClass }}
|
||||
storageClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,712 @@
|
||||
# freesailarr runs gluetun plus the whole media automation stack in a single
|
||||
# StatefulSet pod: every app container shares gluetun's network namespace, the
|
||||
# containers talk to each other over localhost and all traffic leaves through
|
||||
# the Wireguard tunnel.
|
||||
#
|
||||
# Image pinning (documented deviation from the Chart.AppVersion standard):
|
||||
# NO image tag comes from appVersion. All nine images, gluetun included, are
|
||||
# pinned here via <app>.image.repository / <app>.image.tag, because the stack
|
||||
# combines nine independently versioned upstream projects and none of them is
|
||||
# "the" one app version.
|
||||
#
|
||||
# That is also what puts every image under Renovate: the built-in helm-values
|
||||
# manager reads image.repository/image.tag pairs out of this file and proposes
|
||||
# one update per image, and ci/renovate-bump-chart.sh bumps the chart version
|
||||
# along with it. A chart whose main image came from appVersion would instead
|
||||
# need a hard-coded block in renovate.json5 - freesailarr deliberately has none.
|
||||
#
|
||||
# Chart.yaml's appVersion is therefore the chart's own version marker, freely
|
||||
# chosen and tied to no upstream - not the source of any tag here. Nothing
|
||||
# rewrites it automatically; raise it by hand when you want to mark a new
|
||||
# edition of the chart as a whole.
|
||||
|
||||
# Scale the whole chart down without uninstalling it: scaleDown: true renders
|
||||
# every Deployment/StatefulSet with replicas: 0 (and suspends CronJobs) and
|
||||
# takes precedence over any per-workload replicas value. PVCs, Secrets and
|
||||
# Services stay in place; scaleDown: false brings the workloads back up.
|
||||
scaleDown: false
|
||||
|
||||
# Replica count for the whole stack (one pod contains all containers); 0 is
|
||||
# allowed and scales it down. The chart-global scaleDown flag takes precedence.
|
||||
# More than 1 replica is not meaningful: every replica would run its own
|
||||
# isolated stack on its own config volumes.
|
||||
replicas: 1
|
||||
|
||||
secrets:
|
||||
# op:// item path for the gluetun VPN credentials. The 1Password item must
|
||||
# contain the fields "wireguard-private-key" (the Wireguard private key) and
|
||||
# "wireguard-addresses" (the Wireguard interface address, e.g. 10.x.y.z/32).
|
||||
gluetunSecretRef: null
|
||||
|
||||
# User/group the linuxserver.io containers drop to after init (their PUID/PGID
|
||||
# mechanism) and the runAsUser/runAsGroup for the seerr and recyclarr
|
||||
# containers. Media and config volumes must be writable for this user.
|
||||
puid: 1000
|
||||
pgid: 1000
|
||||
# Timezone for all containers.
|
||||
timezone: Europe/Zurich
|
||||
|
||||
securityContext:
|
||||
# Pod-level security context. No pod-level runAsNonRoot/runAsUser: the pod
|
||||
# mixes root containers (gluetun, linuxserver.io images) with non-root ones
|
||||
# (seerr, recyclarr, flaresolverr); users are set per container instead.
|
||||
# fsGroup (keep in sync with pgid) makes freshly provisioned config volumes
|
||||
# group-writable for the non-root containers; fsGroupChangePolicy
|
||||
# OnRootMismatch avoids recursively re-chowning the (large, shared) media
|
||||
# claims on every pod start.
|
||||
pod:
|
||||
fsGroup: 1000
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
|
||||
storage:
|
||||
# StorageClass for all config volumeClaimTemplates (null = cluster default).
|
||||
storageClass: null
|
||||
|
||||
# Media libraries as pre-existing PVCs (shared with the separately deployed
|
||||
# Jellyfin) - the chart mounts but never creates them. Each claim is required
|
||||
# as soon as an app that mounts it is enabled:
|
||||
# downloads: qbittorrent, radarr, sonarr
|
||||
# movies: radarr, bazarr
|
||||
# tv: sonarr, bazarr
|
||||
#
|
||||
# subPath mounts a sub-directory of the claim instead of its root (null = the
|
||||
# claim root, i.e. one claim per category). The same existingClaim may back all
|
||||
# three categories with different subPaths - one library volume, one
|
||||
# sub-directory per category, e.g. downloads / movies / tv-shows.
|
||||
#
|
||||
# What this buys and what it does NOT buy - measured in a k3d cluster against
|
||||
# this chart, because the usual argument for a shared volume is hardlinking:
|
||||
# - It buys the layout: one claim to provision, back up and share with
|
||||
# Jellyfin, with the categories as sibling directories instead of three
|
||||
# separate volumes.
|
||||
# - It does NOT buy hardlinked imports. Radarr and Sonarr import by moving,
|
||||
# and a move within one filesystem can be a hardlink or a rename instead of
|
||||
# a copy - but Kubernetes turns every volumeMount into its own bind mount,
|
||||
# and link()/rename() across two mount points fails with EXDEV even when
|
||||
# both sides are the same claim on the same device. Measured: /downloads and
|
||||
# /movies in the radarr container both report device 65025 and resolve to
|
||||
# the same PV directory, yet `ln /downloads/f /movies/f` fails with
|
||||
# "Cross-device link", while the identical link succeeds when the claim root
|
||||
# is mounted once and both categories are sub-directories under it.
|
||||
# This is not a property of subPath: two plain mounts of the same claim
|
||||
# without any subPath fail exactly the same way, so the chart has never
|
||||
# supported cross-category hardlinks.
|
||||
# - Consequence today: an import still copies, so download and library each
|
||||
# hold a full copy until the torrent is removed.
|
||||
# Getting hardlinks would mean giving the apps a single mount that contains all
|
||||
# categories (the "one /data mount" layout the *arr guides recommend) instead of
|
||||
# one mount per category - a separate change to the chart's mount layout, not a
|
||||
# values setting.
|
||||
media:
|
||||
downloads:
|
||||
existingClaim: null
|
||||
subPath: null
|
||||
movies:
|
||||
existingClaim: null
|
||||
subPath: null
|
||||
tv:
|
||||
existingClaim: null
|
||||
subPath: null
|
||||
|
||||
# Ingress defaults. There is deliberately no global domain: every app carries
|
||||
# its own <app>.ingress.host, so the hosts are independent of each other and
|
||||
# may live in completely different domains. clusterIssuer, className and
|
||||
# annotations are the defaults for all apps and can be overridden per app in
|
||||
# the <app>.ingress block (an explicitly set per-app value always wins).
|
||||
ingress:
|
||||
# Master switch: false suppresses every app ingress regardless of the
|
||||
# per-app toggles.
|
||||
enabled: true
|
||||
clusterIssuer: null
|
||||
className: nginx
|
||||
# Extra annotations for the Ingress objects; merged with the cert-manager
|
||||
# cluster-issuer annotation the chart always sets.
|
||||
annotations: {}
|
||||
|
||||
gluetun:
|
||||
# Pinned here like every other image in this chart, so Renovate's helm-values
|
||||
# manager proposes gluetun updates too. Docker Hub tags carry a v prefix.
|
||||
image:
|
||||
repository: docker.io/qmcgaw/gluetun
|
||||
tag: v3.41.1
|
||||
vpn:
|
||||
serviceProvider: mullvad
|
||||
type: wireguard
|
||||
serverCities: Zurich
|
||||
serverCountries: Switzerland
|
||||
dnsUpstreamResolvers: quad9
|
||||
# Comma-separated CIDRs the gluetun firewall allows outbound traffic to
|
||||
# outside the VPN tunnel. Set this to the cluster pod and service CIDRs
|
||||
# (e.g. "10.42.0.0/16,10.43.0.0/16" on k3s) - without it the firewall
|
||||
# blocks cluster-internal egress such as DNS lookups against cluster DNS
|
||||
# and seerr talking to the in-cluster Jellyfin.
|
||||
firewallOutboundSubnets: null
|
||||
storage:
|
||||
# Size of the gluetun config volume (server list cache under /gluetun).
|
||||
size: 1Gi
|
||||
# Extra environment variables, appended to the ones the chart always sets.
|
||||
# Values are rendered through tpl; value, secretKeyRef and configMapKeyRef
|
||||
# forms are supported. (Same for every <app>.env block below.)
|
||||
env: []
|
||||
# gluetun's built-in HTTP health server; the chart binds it to :9999 (env
|
||||
# HEALTH_SERVER_ADDRESS) so the kubelet probes, which connect to the pod IP,
|
||||
# can reach it. It returns 200 only while the VPN tunnel is up and working.
|
||||
# Liveness is more tolerant (60s) than readiness (30s): during a short
|
||||
# tunnel re-negotiation gluetun recovers on its own and a kubelet restart
|
||||
# would only prolong the outage; the kill switch keeps all egress blocked
|
||||
# the whole time, so nothing runs unprotected.
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 9999
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 9999
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
# The startup probe gates liveness/readiness. Budget for image start plus
|
||||
# Wireguard handshake: 5s * 30 = 150s.
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 9999
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 30
|
||||
resources:
|
||||
# Limits are optional: memory and ephemeral-storage limits default to
|
||||
# limitFactor x the request, an explicitly set limit always wins, and a
|
||||
# CPU limit is only set when configured explicitly under "limits".
|
||||
# (Same for every <app>.resources block below.)
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 0.1
|
||||
memory: 128Mi
|
||||
ephemeralStorage: 50Mi
|
||||
# Deviations from the hardening baseline, forced by gluetun's role as VPN
|
||||
# gateway:
|
||||
# - runs as root (no runAsNonRoot): required to create the tun device and
|
||||
# program the firewall.
|
||||
# - NET_ADMIN capability: creates the Wireguard tunnel and the iptables
|
||||
# rules (firewall, kill switch).
|
||||
# - readOnlyRootFilesystem: false: gluetun rewrites /etc/resolv.conf and
|
||||
# other files on its root filesystem at runtime.
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- NET_ADMIN
|
||||
|
||||
qbittorrent:
|
||||
enabled: true
|
||||
image:
|
||||
repository: docker.io/linuxserver/qbittorrent
|
||||
tag: 5.2.1
|
||||
# Every app gets its own Ingress object (named <release>-<chart>-<app>-ingress)
|
||||
# with its own TLS secret tls-<release>-<chart>-<app>-ingress, so the apps can
|
||||
# be exposed independently and under unrelated hosts.
|
||||
# host is required while enabled is true and has no default - an app whose
|
||||
# ingress is switched off never demands one. clusterIssuer, className and
|
||||
# annotations may be set here to override the global ingress block.
|
||||
# (Same block for prowlarr, radarr, sonarr, seerr and bazarr below.)
|
||||
ingress:
|
||||
enabled: true
|
||||
host: null
|
||||
# clusterIssuer: null
|
||||
# className: null
|
||||
# annotations: {}
|
||||
storage:
|
||||
size: 1Gi
|
||||
# Network interface qBittorrent binds its BitTorrent session to - gluetun's
|
||||
# Wireguard device. Without this binding libtorrent listens and announces on
|
||||
# every interface in the shared pod network namespace, including eth0 (the
|
||||
# pod IP) and loopback. Those announces bypass the tunnel, so gluetun's kill
|
||||
# switch (OUTPUT DROP except tun0 and the allowed cluster subnets) drops
|
||||
# them: trackers report "skipping tracker announce (unreachable)", no peers
|
||||
# are found and torrents stall at 0 bytes in stalledDL. Binding to tun0 is
|
||||
# therefore both the functional fix and a leak fix - it stops qBittorrent
|
||||
# from listening on the pod IP past the tunnel. The value is written into
|
||||
# /config/qBittorrent/qBittorrent.conf by the config-interface init
|
||||
# container below; setting it to null or "" removes that init container and
|
||||
# leaves the binding to the user (not recommended).
|
||||
networkInterface: tun0
|
||||
# The init container that seeds qbittorrent.networkInterface into the
|
||||
# persisted qBittorrent config before the app starts. Runs as puid/pgid so
|
||||
# the config file keeps the ownership the app expects.
|
||||
configInit:
|
||||
# Auxiliary busybox image for the init container, pinned here like every
|
||||
# other image in this chart and proposed by Renovate the same way.
|
||||
image:
|
||||
repository: docker.io/busybox
|
||||
tag: "1.38.0"
|
||||
resources:
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 16Mi
|
||||
ephemeralStorage: 1Mi
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
env: []
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
# The startup probe gates liveness/readiness. Startup budget = 5s * 30 = 150s.
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 30
|
||||
resources:
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 0.2
|
||||
memory: 512Mi
|
||||
ephemeralStorage: 100Mi
|
||||
# Deviations from the hardening baseline, forced by the linuxserver.io
|
||||
# images (s6-overlay init that starts as root and drops to PUID/PGID):
|
||||
# - no runAsNonRoot: s6-overlay must start as root to apply PUID/PGID and
|
||||
# chown /config.
|
||||
# - readOnlyRootFilesystem: false: the s6 init applies PUID/PGID by
|
||||
# rewriting /etc/passwd and /etc/group and stages runtime state on the
|
||||
# root filesystem. With a read-only root filesystem the containers still
|
||||
# start, but silently keep the image's built-in uid/gid (911) instead of
|
||||
# PUID/PGID (verified at runtime) - unacceptable for the shared media
|
||||
# volumes.
|
||||
# - capabilities: ALL dropped except what the s6-overlay init needs:
|
||||
# CHOWN/FOWNER/DAC_OVERRIDE (taking ownership of /config and /run) and
|
||||
# SETUID/SETGID (dropping from root to the PUID/PGID user).
|
||||
# (Same block for all linuxserver.io containers: qbittorrent, prowlarr,
|
||||
# radarr, sonarr, bazarr.)
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- CHOWN
|
||||
- DAC_OVERRIDE
|
||||
- FOWNER
|
||||
- SETGID
|
||||
- SETUID
|
||||
|
||||
prowlarr:
|
||||
enabled: true
|
||||
image:
|
||||
repository: docker.io/linuxserver/prowlarr
|
||||
tag: 2.3.5
|
||||
# See the qbittorrent ingress comment.
|
||||
ingress:
|
||||
enabled: true
|
||||
host: null
|
||||
# clusterIssuer: null
|
||||
# className: null
|
||||
# annotations: {}
|
||||
storage:
|
||||
size: 1Gi
|
||||
env: []
|
||||
# The *arr apps expose /ping as an unauthenticated health endpoint.
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 9696
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 9696
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
# The startup probe gates liveness/readiness. Startup budget = 5s * 30 = 150s.
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 9696
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 30
|
||||
resources:
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 0.1
|
||||
memory: 256Mi
|
||||
ephemeralStorage: 100Mi
|
||||
# linuxserver.io image - see the qbittorrent securityContext comment.
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- CHOWN
|
||||
- DAC_OVERRIDE
|
||||
- FOWNER
|
||||
- SETGID
|
||||
- SETUID
|
||||
|
||||
flaresolverr:
|
||||
enabled: true
|
||||
image:
|
||||
repository: ghcr.io/flaresolverr/flaresolverr
|
||||
tag: v3.5.0
|
||||
# No ingress and no config volume: flaresolverr is stateless and only
|
||||
# consumed by prowlarr over localhost.
|
||||
env: []
|
||||
# /health is flaresolverr's dedicated lightweight health endpoint.
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8191
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8191
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
# The startup probe gates liveness/readiness. Startup budget = 5s * 30 = 150s.
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8191
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 30
|
||||
resources:
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 0.2
|
||||
memory: 512Mi
|
||||
ephemeralStorage: 200Mi
|
||||
# Deviations from the hardening baseline, forced by the flaresolverr image:
|
||||
# - readOnlyRootFilesystem: false: on startup flaresolverr copies and
|
||||
# patches the chromedriver binary to the hardcoded path /app/chromedriver
|
||||
# and writes chromedriver caches under /app (its home directory).
|
||||
# - runAsUser/runAsGroup pin the image's "flaresolverr" user numerically
|
||||
# (uid/gid 1000; the image declares it by name only, which the kubelet's
|
||||
# runAsNonRoot check cannot verify).
|
||||
# Chromium's scratch paths (/tmp, /dev/shm) are emptyDirs.
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
radarr:
|
||||
enabled: true
|
||||
image:
|
||||
repository: docker.io/linuxserver/radarr
|
||||
tag: 6.1.1
|
||||
# See the qbittorrent ingress comment.
|
||||
ingress:
|
||||
enabled: true
|
||||
host: null
|
||||
# clusterIssuer: null
|
||||
# className: null
|
||||
# annotations: {}
|
||||
storage:
|
||||
size: 1Gi
|
||||
env: []
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 7878
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 7878
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
# The startup probe gates liveness/readiness. Startup budget = 5s * 30 = 150s.
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 7878
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 30
|
||||
resources:
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 0.1
|
||||
memory: 256Mi
|
||||
ephemeralStorage: 100Mi
|
||||
# linuxserver.io image - see the qbittorrent securityContext comment.
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- CHOWN
|
||||
- DAC_OVERRIDE
|
||||
- FOWNER
|
||||
- SETGID
|
||||
- SETUID
|
||||
|
||||
recyclarr:
|
||||
enabled: true
|
||||
image:
|
||||
repository: ghcr.io/recyclarr/recyclarr
|
||||
tag: 8.6.0
|
||||
# No ingress: recyclarr has no web UI, it syncs TRaSH guides to radarr and
|
||||
# sonarr on a schedule (daemon mode).
|
||||
storage:
|
||||
size: 1Gi
|
||||
env: []
|
||||
# recyclarr serves no HTTP port in daemon mode, so all probes exec the CLI.
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- recyclarr
|
||||
- --version
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 10
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- recyclarr
|
||||
- --version
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 10
|
||||
failureThreshold: 3
|
||||
# The startup probe gates liveness/readiness. Startup budget = 5s * 30 = 150s.
|
||||
startupProbe:
|
||||
exec:
|
||||
command:
|
||||
- recyclarr
|
||||
- --version
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 10
|
||||
failureThreshold: 30
|
||||
resources:
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 0.05
|
||||
memory: 128Mi
|
||||
ephemeralStorage: 50Mi
|
||||
# The image supports running as an arbitrary non-root user; runAsUser and
|
||||
# runAsGroup are set from the global puid/pgid values in the template - do
|
||||
# not set them here.
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
sonarr:
|
||||
enabled: true
|
||||
image:
|
||||
repository: docker.io/linuxserver/sonarr
|
||||
tag: 4.0.17
|
||||
# See the qbittorrent ingress comment.
|
||||
ingress:
|
||||
enabled: true
|
||||
host: null
|
||||
# clusterIssuer: null
|
||||
# className: null
|
||||
# annotations: {}
|
||||
storage:
|
||||
size: 1Gi
|
||||
env: []
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 8989
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 8989
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
# The startup probe gates liveness/readiness. Startup budget = 5s * 30 = 150s.
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 8989
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 30
|
||||
resources:
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 0.1
|
||||
memory: 256Mi
|
||||
ephemeralStorage: 100Mi
|
||||
# linuxserver.io image - see the qbittorrent securityContext comment.
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- CHOWN
|
||||
- DAC_OVERRIDE
|
||||
- FOWNER
|
||||
- SETGID
|
||||
- SETUID
|
||||
|
||||
seerr:
|
||||
enabled: true
|
||||
image:
|
||||
repository: ghcr.io/seerr-team/seerr
|
||||
tag: v3.3.0
|
||||
# See the qbittorrent ingress comment.
|
||||
ingress:
|
||||
enabled: true
|
||||
host: null
|
||||
# clusterIssuer: null
|
||||
# className: null
|
||||
# annotations: {}
|
||||
storage:
|
||||
size: 1Gi
|
||||
env: []
|
||||
# /api/v1/settings/public answers unauthenticated once the app is up.
|
||||
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
|
||||
# The startup probe gates liveness/readiness. First boot runs database
|
||||
# migrations: budget 10s * 30 = 300s.
|
||||
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
|
||||
# The image runs fine as an arbitrary non-root user; runAsUser and
|
||||
# runAsGroup are set from the global puid/pgid values in the template - do
|
||||
# not set them here.
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
bazarr:
|
||||
enabled: true
|
||||
image:
|
||||
repository: docker.io/linuxserver/bazarr
|
||||
tag: 1.5.6
|
||||
# See the qbittorrent ingress comment.
|
||||
ingress:
|
||||
enabled: true
|
||||
host: null
|
||||
# clusterIssuer: null
|
||||
# className: null
|
||||
# annotations: {}
|
||||
storage:
|
||||
size: 1Gi
|
||||
env: []
|
||||
# bazarr has no unauthenticated health endpoint; / serves the UI with 200.
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 6767
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 6767
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
# The startup probe gates liveness/readiness. Startup budget = 5s * 30 = 150s.
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 6767
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 30
|
||||
resources:
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 0.1
|
||||
memory: 256Mi
|
||||
ephemeralStorage: 100Mi
|
||||
# linuxserver.io image - see the qbittorrent securityContext comment.
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- CHOWN
|
||||
- DAC_OVERRIDE
|
||||
- FOWNER
|
||||
- SETGID
|
||||
- SETUID
|
||||
@@ -148,6 +148,15 @@
|
||||
// mapping here leaves the charts untouched.
|
||||
//
|
||||
// When adding a chart, add a block here — see README "Creating a new chart".
|
||||
//
|
||||
// Exception, deliberately no block: `freesailarr`. It bundles nine
|
||||
// independently versioned upstreams, so none of them is "the" app version
|
||||
// and no image tag comes from appVersion — all nine are pinned as
|
||||
// image.repository/image.tag in its values.yaml, where the built-in
|
||||
// helm-values manager already finds them and proposes one update per
|
||||
// image. Adding a block here would either duplicate those updates or
|
||||
// rewrite an appVersion that selects nothing. Its appVersion is a
|
||||
// hand-maintained marker; see freesailarr/Chart.yaml.
|
||||
|
||||
{
|
||||
description: "apache-tika: four-part version plus flavour suffix (3.3.1.0-full); the compatibility group keeps the chart on -full.",
|
||||
|
||||
Reference in new issue
Block a user