rss-bridge: new chart
Takes over the four loose manifests in cluster-config/rss-bridge/ as a proper chart, on the current template-chart standard: strict values schema, null-safe helpers, computed limits, replicas/scaleDown, checksum/config, NOTES.txt. Two things deviate from the repo standard on purpose: - The Ingress models host AND TLS secret name per entry instead of the single conventional tls-<release>-<chart>-ingress. The installation this replaces has grown three separate certificates, one per host. Renaming them would make cert-manager issue three new ones and leave each host unreachable until it did - a self-inflicted outage on a deployment that has been up for over a year. Entries sharing a secret name are grouped into one tls entry, and omitting the name falls back to the convention, so consolidating later is a values change. - Requests and limits are decoupled. The old manifest requested 2Gi of ephemeral storage, identical to its limit, reserving two gigabytes for a workload measured at 2.2 MB after fetching all five whitelisted bridges. The request drops to 64Mi; the limit stays at 2Gi. Hardening was verified rather than assumed. The image ships no USER and runs as root by default, but runs fully hardened once its writable paths are provided: uid/gid 33, readOnlyRootFilesystem, all capabilities dropped, seccompProfile RuntimeDefault. The writable paths were found by running the image with a writable rootfs, exercising every bridge and reading its diff layer - /app/cache, /run, /run/php, /var/lib/nginx. /tmp did not show up that way, because PHP's opcache creates its lock file there and unlinks it immediately; it only surfaced when the hardened container died at startup. Stopping at the first writable path would have shipped a chart that cannot start. Probes use the app's own /?action=health, which renders fixed JSON without touching the cache or any remote site, so neither a scraped site going down nor an unwritable cache can restart the pod. Fixes #128
This commit is contained in:
14 files changed
+1011
No files matched your search
@@ -34,6 +34,7 @@ on:
|
||||
- voucher-vault
|
||||
- minecraft-router
|
||||
- minecraft-server
|
||||
- rss-bridge
|
||||
workflow_call:
|
||||
inputs:
|
||||
app-version:
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
name: Publish RSS-Bridge Helm Chart
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- rss-bridge/**
|
||||
jobs:
|
||||
publish-rss-bridge:
|
||||
name: Publish RSS-Bridge Helm Chart
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'rss-bridge'
|
||||
secrets: inherit
|
||||
@@ -18,6 +18,7 @@ Collection of some self-made helm-charts. Each top-level directory is a standalo
|
||||
| `paperless-ngx` | Paperless-ngx | Largest chart: consume CronJob, SMB-connector PV/PVC, pre/post-consumption scripts ConfigMap |
|
||||
| `patchmon` | PatchMon | Two Deployments (frontend + backend), OIDC secret |
|
||||
| `rallly` | Rallly | Deployment, DB/OIDC/SMTP secrets |
|
||||
| `rss-bridge` | RSS-Bridge | Deployment + whitelist ConfigMap; `ingress.hosts` is a required list of host + optional `tlsSecretName`, so several hosts can keep their own existing certificates (documented deviation from the one-TLS-secret rule, see its `values.yaml`) |
|
||||
| `samba` | Samba | StatefulSet, users secret + config ConfigMap |
|
||||
| `satisfactory` | Satisfactory game server | StatefulSet, no ingress |
|
||||
| `teamspeak-server` | TeamSpeak server | StatefulSet, no ingress |
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# rss-bridge: stateless, no secrets and no infrastructure to stand up, so only
|
||||
# the required ingress values are set. The Ingress just has to apply - there is
|
||||
# no ingress-nginx/cert-manager in the CI cluster.
|
||||
#
|
||||
# Two hosts share a TLS secret name and a third has its own, so the install
|
||||
# test renders the secret-grouping helper as well. That helper is what keeps
|
||||
# the three existing certificates of the takeover installation alive, and it is
|
||||
# not exercised by the chart defaults - an install test that never rendered it
|
||||
# would not notice if it broke.
|
||||
ingress:
|
||||
clusterIssuer: ci
|
||||
hosts:
|
||||
- host: rss-bridge.ci.local
|
||||
tlsSecretName: tls-ci-shared-ingress
|
||||
- host: rssbridge.ci.local
|
||||
tlsSecretName: tls-ci-shared-ingress
|
||||
- host: rss-bridge.ci.example
|
||||
tlsSecretName: tls-ci-second-ingress
|
||||
|
||||
# A concrete list rather than the default "*", so the ConfigMap render and the
|
||||
# subPath mount are covered by something the app has to parse.
|
||||
whitelist:
|
||||
- Youtube
|
||||
- Heise
|
||||
@@ -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,12 @@
|
||||
apiVersion: v2
|
||||
name: rss-bridge
|
||||
description: A Helm chart to deploy RSS-Bridge on Kubernetes
|
||||
icon: https://raw.githubusercontent.com/RSS-Bridge/rss-bridge/master/static/logo_300px.png
|
||||
|
||||
type: application
|
||||
|
||||
version: 1.0.0+up2025.06.03
|
||||
appVersion: "2025-06-03"
|
||||
|
||||
maintainers:
|
||||
- name: jklein
|
||||
@@ -0,0 +1,42 @@
|
||||
RSS-Bridge {{ .Chart.AppVersion }} was installed as release "{{ .Release.Name }}".
|
||||
|
||||
{{ if .Values.ingress.enabled -}}
|
||||
Reachable at:
|
||||
{{ range $entry := .Values.ingress.hosts }} https://{{ $entry.host }}
|
||||
{{ end }}
|
||||
Each host serves the same instance. Certificates are issued by the
|
||||
cert-manager cluster issuer "{{ .Values.ingress.clusterIssuer }}" into these secrets:
|
||||
{{ range $secret := (fromYaml (include "rss-bridge.tlsGroups" (dict "hosts" .Values.ingress.hosts "root" $))).order }} {{ $secret }}
|
||||
{{ end }}
|
||||
{{- else -}}
|
||||
The Ingress is disabled (ingress.enabled=false). Reach the instance with:
|
||||
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80
|
||||
|
||||
and open http://localhost:8080
|
||||
{{- end }}
|
||||
|
||||
Enabled bridges ({{ len .Values.whitelist }} entr{{ if eq (len .Values.whitelist) 1 }}y{{ else }}ies{{ end }} in whitelist.txt):
|
||||
{{ range $entry := .Values.whitelist }} {{ $entry }}
|
||||
{{ end }}
|
||||
{{- if has "*" .Values.whitelist }}
|
||||
"*" enables every bridge the image ships. Narrow it down by listing bridge
|
||||
names in the "whitelist" value.
|
||||
{{- end }}
|
||||
|
||||
Useful to know:
|
||||
|
||||
* Health endpoint: /?action=health - a fixed JSON document that touches
|
||||
neither the cache nor any remote site. All three probes use it.
|
||||
* Changing the "whitelist" value rolls the pod: the deployment carries a
|
||||
checksum over the ConfigMap, because a subPath mount would otherwise never
|
||||
reach the running container.
|
||||
* The feed cache lives in an emptyDir at /app/cache and is therefore
|
||||
deliberately lost on every restart. RSS-Bridge refills it on demand; it
|
||||
holds only fetched feeds, no configuration and no user data.
|
||||
* The container runs read-only and non-root as uid 33 (www-data).
|
||||
{{- if .Values.scaleDown }}
|
||||
|
||||
WARNING: scaleDown is true, so this release is running with 0 replicas.
|
||||
Set scaleDown=false to bring it back up.
|
||||
{{- end }}
|
||||
@@ -0,0 +1,313 @@
|
||||
{{/*
|
||||
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).
|
||||
|
||||
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. template-chart holds the reference implementation.
|
||||
*/}}
|
||||
{{- define "rss-bridge.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 "rss-bridge.resourceKey" $key) $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $limits := dict -}}
|
||||
{{- range $key, $value := ($resources.limits | default dict) -}}
|
||||
{{- if not (kindIs "invalid" $value) -}}
|
||||
{{- $_ := set $limits (include "rss-bridge.resourceKey" $key) $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- range $key, $request := $requests -}}
|
||||
{{- if and (ne $key "cpu") (not (hasKey $limits $key)) -}}
|
||||
{{- $_ := set $limits $key (include "rss-bridge.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 "rss-bridge.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 "rss-bridge.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.
|
||||
*/}}
|
||||
{{- define "rss-bridge.replicas" -}}
|
||||
{{- if .scaleDown -}}
|
||||
0
|
||||
{{- else if not (kindIs "invalid" .replicas) -}}
|
||||
{{- .replicas -}}
|
||||
{{- else -}}
|
||||
1
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Merge a user-supplied values sub-tree onto a dict of chart defaults so that a
|
||||
null / empty override falls back to the defaults instead of erasing them.
|
||||
|
||||
Input: dict "defaults" <dict> "given" <dict|nil> "path" <string, optional
|
||||
values path used in error messages>. Output: YAML of the merged dict (consume
|
||||
it with fromYaml).
|
||||
|
||||
Why this exists (issues #61 and #99): Helm merges values maps recursively, but
|
||||
an override key written without children - e.g. a leftover
|
||||
|
||||
securityContext:
|
||||
|
||||
after deleting its last remaining sub-key - is YAML null. Where the chart
|
||||
defines a default, Helm treats that null as a deletion of the default; the
|
||||
template then renders "securityContext: null": no readOnlyRootFilesystem, no
|
||||
dropped capabilities, no allowPrivilegeEscalation: false. The workload starts
|
||||
and reports healthy while being unhardened, so nothing points at the mistake.
|
||||
The same shape erases a probe (it disappears) or the resources block (no
|
||||
requests, no computed limits). This helper defines the opposite semantics: an
|
||||
empty block means "chart defaults apply".
|
||||
|
||||
Rules:
|
||||
- a nil "given" is treated as an empty dict (all defaults apply).
|
||||
- a key whose override value is null keeps the default (nulls never delete).
|
||||
- a key present in both as a map is merged recursively, so a partially
|
||||
filled block only overrides the keys it actually sets.
|
||||
- a scalar override for a key the defaults define as a map is rejected with
|
||||
a message naming the values path, instead of failing later with a template
|
||||
field error.
|
||||
- any other set value wins as given, including an explicit false or 0 -
|
||||
Helm's "default" and sprig's "mergeOverwrite" would both swallow those
|
||||
zero values, hence the explicit kindIs "invalid" nil check.
|
||||
- keys only present in the override are passed through unchanged.
|
||||
*/}}
|
||||
{{- define "rss-bridge.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 "rss-bridge.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>.
|
||||
Returns YAML of dict "value" <the sub-block, may be nil>, after rejecting a
|
||||
non-mapping block with a message that names the values path. Needed because
|
||||
".Values.rssBridge.securityContext" can itself be erased, and indexing into nil
|
||||
inside a template is not an error but silently yields nothing.
|
||||
*/}}
|
||||
{{- define "rss-bridge.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 effective security contexts, probes and resources (issue #99).
|
||||
|
||||
The defaults below MUST stay in sync with the corresponding blocks in
|
||||
values.yaml, which stays the documented, user-facing place for them; this copy
|
||||
is only the fallback for the case where an override has erased the block.
|
||||
|
||||
Two chart-specific facts a rebuilt default must not lose:
|
||||
|
||||
- uid/gid 33 (www-data) is not a stylistic choice. The image ships /app and
|
||||
everything in it owned by www-data, and both nginx and php-fpm are
|
||||
configured for that user. A "cleaner" high uid would leave the app unable
|
||||
to read its own bridge sources.
|
||||
- the probes address the port BY NAME (rss-bridge-port) and use the app's
|
||||
own health action. A default that drops the query string would probe the
|
||||
1.5 MB front page instead of a 49-byte JSON response, and a default that
|
||||
drops the port name would not resolve at all.
|
||||
*/}}
|
||||
{{- define "rss-bridge.podSecurityContext" -}}
|
||||
{{- $given := (fromYaml (include "rss-bridge.subBlock" (dict "block" . "key" "pod" "path" "rssBridge.securityContext"))).value -}}
|
||||
{{- $defaults := dict
|
||||
"runAsNonRoot" true
|
||||
"runAsUser" 33
|
||||
"runAsGroup" 33
|
||||
"fsGroup" 33
|
||||
"fsGroupChangePolicy" "Always"
|
||||
"seccompProfile" (dict "type" "RuntimeDefault") -}}
|
||||
{{- include "rss-bridge.defaultedDict" (dict "defaults" $defaults "given" $given "path" "rssBridge.securityContext.pod") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "rss-bridge.containerSecurityContext" -}}
|
||||
{{- $given := (fromYaml (include "rss-bridge.subBlock" (dict "block" . "key" "container" "path" "rssBridge.securityContext"))).value -}}
|
||||
{{- $defaults := dict
|
||||
"allowPrivilegeEscalation" false
|
||||
"readOnlyRootFilesystem" true
|
||||
"runAsNonRoot" true
|
||||
"capabilities" (dict "drop" (list "ALL")) -}}
|
||||
{{- include "rss-bridge.defaultedDict" (dict "defaults" $defaults "given" $given "path" "rssBridge.securityContext.container") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The probe target, shared by all three probes.
|
||||
|
||||
"/?action=health" is RSS-Bridge's own health action: it renders a fixed JSON
|
||||
document and touches neither the file cache nor any remote site, so no outage
|
||||
of a scraped website and no unwritable cache directory can restart the pod
|
||||
(guideline 3). The front page "/" would be a bad choice for the opposite
|
||||
reason - it enumerates every bridge and answers with ~1.5 MB.
|
||||
*/}}
|
||||
{{- define "rss-bridge.probeHttpGet" -}}
|
||||
{{- dict "path" "/?action=health" "port" "rss-bridge-port" | toYaml -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "rss-bridge.livenessProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (fromYaml (include "rss-bridge.probeHttpGet" .))
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 3 -}}
|
||||
{{- include "rss-bridge.defaultedDict" (dict "defaults" $defaults "given" . "path" "rssBridge.livenessProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "rss-bridge.readinessProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (fromYaml (include "rss-bridge.probeHttpGet" .))
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 3 -}}
|
||||
{{- include "rss-bridge.defaultedDict" (dict "defaults" $defaults "given" . "path" "rssBridge.readinessProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "rss-bridge.startupProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (fromYaml (include "rss-bridge.probeHttpGet" .))
|
||||
"periodSeconds" 5
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 30 -}}
|
||||
{{- include "rss-bridge.defaultedDict" (dict "defaults" $defaults "given" . "path" "rssBridge.startupProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The effective resources block, fed into the resources helper above so that an
|
||||
erased "resources:" still yields the chart's requests and computed limits
|
||||
instead of a container without any resource accounting.
|
||||
|
||||
The explicit limits are part of the default on purpose - see the measurements
|
||||
in values.yaml. Reproducing only the requests here would let limitFactor
|
||||
compute a 384Mi memory limit, which is below the measured concurrent peak.
|
||||
*/}}
|
||||
{{- define "rss-bridge.effectiveResources" -}}
|
||||
{{- $defaults := dict
|
||||
"limitFactor" 3
|
||||
"requests" (dict "cpu" 0.1 "memory" "128Mi" "ephemeral-storage" "64Mi")
|
||||
"limits" (dict "memory" "512Mi" "ephemeral-storage" "2Gi") -}}
|
||||
{{- $merged := fromYaml (include "rss-bridge.defaultedDict" (dict "defaults" $defaults "given" . "path" "rssBridge.resources")) -}}
|
||||
{{- include "rss-bridge.resources" $merged -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Group the configured ingress hosts by their TLS secret name.
|
||||
|
||||
Returns YAML of dict "order" <list of secret names, in first-appearance order>
|
||||
and "groups" <secret name -> list of hosts>. Hosts that share a secret name end
|
||||
up in one tls entry (one certificate with several SANs); hosts with distinct
|
||||
names keep one certificate each, which is what the existing installation needs
|
||||
(see the TLS comment in values.yaml).
|
||||
|
||||
First-appearance order rather than sorted order, so the rendered tls list can
|
||||
be compared line by line against the manifest the chart replaces.
|
||||
*/}}
|
||||
{{- define "rss-bridge.tlsGroups" -}}
|
||||
{{- $root := .root -}}
|
||||
{{- $default := printf "tls-%s-%s-ingress" $root.Release.Name $root.Chart.Name -}}
|
||||
{{- $groups := dict -}}
|
||||
{{- $order := list -}}
|
||||
{{- range $entry := .hosts -}}
|
||||
{{- $host := required "Every ingress.hosts entry needs a host (ingress.hosts[].host)" $entry.host -}}
|
||||
{{- $secret := $entry.tlsSecretName | default $default -}}
|
||||
{{- if not (hasKey $groups $secret) -}}
|
||||
{{- $_ := set $groups $secret (list) -}}
|
||||
{{- $order = append $order $secret -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $groups $secret (append (index $groups $secret) $host) -}}
|
||||
{{- end -}}
|
||||
{{- toYaml (dict "order" $order "groups" $groups) -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: "{{ .Release.Name }}-{{ .Chart.Name }}-configmap"
|
||||
data:
|
||||
# One bridge name per line. The application trims every line and drops empty
|
||||
# ones, so the block scalar style is not load-bearing.
|
||||
whitelist.txt: |-
|
||||
{{- range $entry := required "A bridge whitelist must be provided (whitelist)" .Values.whitelist }}
|
||||
{{ $entry }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,130 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
spec:
|
||||
replicas: {{ include "rss-bridge.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.rssBridge.replicas) }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
template:
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
annotations:
|
||||
# Hash of the rendered ConfigMap: a changed whitelist changes the pod
|
||||
# template and thus rolls the pods. Without it a config change stays
|
||||
# invisible to the running container - whitelist.txt is a subPath
|
||||
# mount, which the kubelet never updates, and RSS-Bridge reads the file
|
||||
# once at request bootstrap from the path it was started with.
|
||||
checksum/config: {{ include (print $.Template.BasePath "/rss-bridge.configmap.yaml") . | sha256sum }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
{{- include "rss-bridge.podSecurityContext" .Values.rssBridge.securityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
image: "rssbridge/rss-bridge:{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
{{- range $value := .Values.rssBridge.env }}
|
||||
{{- if $value.value }}
|
||||
- name: {{ $value.name }}
|
||||
value: {{ tpl $value.value $ }}
|
||||
{{- else if $value.valueFrom }}
|
||||
{{- if $value.valueFrom.secretKeyRef }}
|
||||
- name: {{ $value.name }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ tpl $value.valueFrom.secretKeyRef.name $ }}
|
||||
key: {{ tpl $value.valueFrom.secretKeyRef.key $ }}
|
||||
{{- else if $value.valueFrom.configMapKeyRef }}
|
||||
- name: {{ $value.name }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ tpl $value.valueFrom.configMapKeyRef.name $ }}
|
||||
key: {{ tpl $value.valueFrom.configMapKeyRef.key $ }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
ports:
|
||||
# The port NAME is load-bearing and must not be renamed: the
|
||||
# Service selects it by name (targetPort: rss-bridge-port), and so
|
||||
# do the probes.
|
||||
- containerPort: 80
|
||||
protocol: TCP
|
||||
name: rss-bridge-port
|
||||
# Probes, resources and both security contexts go through the
|
||||
# null-safe helpers (#99): an erased block means "chart defaults
|
||||
# apply" instead of a vanished probe or an unhardened container.
|
||||
# Never wrap these in an "if" - a guard hides the erased case
|
||||
# completely instead of rendering a visible null.
|
||||
livenessProbe:
|
||||
{{- include "rss-bridge.livenessProbe" .Values.rssBridge.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- include "rss-bridge.readinessProbe" .Values.rssBridge.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- include "rss-bridge.startupProbe" .Values.rssBridge.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "rss-bridge.effectiveResources" .Values.rssBridge.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- include "rss-bridge.containerSecurityContext" .Values.rssBridge.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: whitelist
|
||||
mountPath: /app/whitelist.txt
|
||||
subPath: whitelist.txt
|
||||
readOnly: true
|
||||
# The four writable paths below are what keeps
|
||||
# readOnlyRootFilesystem: true possible. They are image facts
|
||||
# rather than user configuration, so they are not values.
|
||||
#
|
||||
# They were found empirically, not guessed: the image was run with
|
||||
# a writable root filesystem, exercised across every whitelisted
|
||||
# bridge, and its diff layer read afterwards. Three of the four
|
||||
# showed up that way. /tmp did NOT - PHP's opcache creates its lock
|
||||
# file there and unlinks it immediately, so it never appears in a
|
||||
# filesystem diff and only surfaced when the hardened container
|
||||
# died at startup with "Unable to create lock file: Read-only file
|
||||
# system". Stopping at the first writable path found would have
|
||||
# shipped a chart that cannot start.
|
||||
- name: cache
|
||||
# RSS-Bridge's file cache. Not optional: the app checks the
|
||||
# directory with is_writable() and throws on every feed request
|
||||
# if it is not - while /?action=health still answers 200, so the
|
||||
# probes would report a healthy pod that serves no feeds.
|
||||
mountPath: /app/cache
|
||||
- name: tmp
|
||||
# PHP opcache lock file (created and immediately unlinked) and
|
||||
# PHP's default upload temp dir.
|
||||
mountPath: /tmp
|
||||
- name: run
|
||||
# nginx.pid
|
||||
mountPath: /run
|
||||
- name: run-php
|
||||
# php-fpm's pid file and its fastcgi socket. Nested inside /run
|
||||
# on purpose: the emptyDir above does not contain the php
|
||||
# subdirectory the pool config expects, and php-fpm does not
|
||||
# create it.
|
||||
mountPath: /run/php
|
||||
- name: nginx-lib
|
||||
# nginx creates its body/fastcgi/proxy/scgi/uwsgi temp
|
||||
# directories here at startup.
|
||||
mountPath: /var/lib/nginx
|
||||
volumes:
|
||||
- name: whitelist
|
||||
configMap:
|
||||
name: "{{ .Release.Name }}-{{ .Chart.Name }}-configmap"
|
||||
- name: cache
|
||||
emptyDir: {}
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
- name: run
|
||||
emptyDir: {}
|
||||
- name: run-php
|
||||
emptyDir: {}
|
||||
- name: nginx-lib
|
||||
emptyDir: {}
|
||||
restartPolicy: Always
|
||||
@@ -0,0 +1,42 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- $hosts := required "At least one ingress host must be provided (ingress.hosts)" .Values.ingress.hosts }}
|
||||
{{- /* helm lint reports a failed required() but keeps rendering with an empty
|
||||
string, which is not iterable. Anything but a list is coerced away here
|
||||
so lint reports the missing value instead of a template stack trace; a
|
||||
real render never reaches this, because required() aborts on nil and the
|
||||
schema rejects every non-array. */ -}}
|
||||
{{- if not (kindIs "slice" $hosts) }}{{- $hosts = list }}{{- end }}
|
||||
{{- $tls := fromYaml (include "rss-bridge.tlsGroups" (dict "hosts" $hosts "root" $)) }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided" .Values.ingress.clusterIssuer }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
rules:
|
||||
{{- range $entry := $hosts }}
|
||||
- host: {{ $entry.host }}
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: {{ $.Release.Name }}-{{ $.Chart.Name }}-service
|
||||
port:
|
||||
number: 80
|
||||
path: /
|
||||
pathType: Prefix
|
||||
{{- end }}
|
||||
tls:
|
||||
{{- range $secret := $tls.order }}
|
||||
- hosts:
|
||||
{{- range $host := index $tls.groups $secret }}
|
||||
- {{ $host }}
|
||||
{{- end }}
|
||||
secretName: {{ $secret }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
spec:
|
||||
selector:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
ports:
|
||||
# Port 80 and the named targetPort are kept exactly as the manifest this
|
||||
# chart replaces had them, so the takeover does not change how traffic
|
||||
# reaches the container.
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: rss-bridge-port
|
||||
type: ClusterIP
|
||||
@@ -0,0 +1,191 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||
"title": "rss-bridge values",
|
||||
"description": "Strict values schema (issue #68). additionalProperties is false on every chart-owned object level, so a key the chart does not read is rejected at render time instead of being silently ignored. Objects that go verbatim into the Kubernetes pod spec (probes, security contexts, global, free-form annotations) stay open, because the chart does not interpret their contents. The hardening, probe and resources blocks additionally accept null - the chart's null-safe helpers turn an erased block into 'chart defaults apply' (issue #99), and the widened types are what lets that null reach the helper instead of aborting validation.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["whitelist"],
|
||||
"properties": {
|
||||
"global": {
|
||||
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
|
||||
"type": "object"
|
||||
},
|
||||
"scaleDown": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"whitelist": {
|
||||
"description": "Bridge names rendered into whitelist.txt, one per line. The single entry \"*\" enables every bridge. An empty list would disable every bridge, which is a silent way to serve nothing, so at least one entry is required.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"rssBridge": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"replicas": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"resources": {
|
||||
"$ref": "#/definitions/resources"
|
||||
},
|
||||
"livenessProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"readinessProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"startupProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"securityContext": {
|
||||
"$ref": "#/definitions/securityContext"
|
||||
},
|
||||
"env": {
|
||||
"$ref": "#/definitions/env"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ingress": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Gates the whole Ingress template (issue #93). Nothing outside the Ingress template reads an ingress value in this chart - the container takes no host in its environment - so switching it off frees every value below (guideline 1, checked rather than assumed).",
|
||||
"type": "boolean"
|
||||
},
|
||||
"clusterIssuer": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"className": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"annotations": {
|
||||
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open.",
|
||||
"type": "object"
|
||||
},
|
||||
"hosts": {
|
||||
"description": "Hosts this Ingress serves. Each entry carries a host and an optional tlsSecretName; entries sharing a secret name are grouped into one tls entry. Null is accepted by the schema so the template's required() produces the readable error instead of a schema violation.",
|
||||
"type": ["array", "null"],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["host"],
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"tlsSecretName": {
|
||||
"description": "Overrides the conventional tls-<release>-<chart>-ingress name for this host. Set only where an existing certificate must keep its secret name - see the deviation note in values.yaml.",
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"resources": {
|
||||
"description": "May be null / empty: the chart then falls back to its default requests AND its explicit limits (issue #99). Reproducing only the requests would let limitFactor compute a memory limit below the measured peak.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"limitFactor": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"requests": {
|
||||
"$ref": "#/definitions/resourceList"
|
||||
},
|
||||
"limits": {
|
||||
"$ref": "#/definitions/resourceList"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourceList": {
|
||||
"description": "Both spellings of the ephemeral-storage key are accepted because the resources helper normalizes camelCase to the Kubernetes key.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"cpu": {
|
||||
"type": ["string", "number", "null"]
|
||||
},
|
||||
"memory": {
|
||||
"type": ["string", "number", "null"]
|
||||
},
|
||||
"ephemeral-storage": {
|
||||
"type": ["string", "number", "null"]
|
||||
},
|
||||
"ephemeralStorage": {
|
||||
"type": ["string", "number", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"type": ["array", "null"],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueFrom": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"secretKeyRef": {
|
||||
"$ref": "#/definitions/keyRef"
|
||||
},
|
||||
"configMapKeyRef": {
|
||||
"$ref": "#/definitions/keyRef"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"keyRef": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "key"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityContext": {
|
||||
"description": "May be null / empty at any level: the chart then falls back to its hardening defaults (issue #99) instead of rendering an unhardened workload. The fallback reproduces THIS chart's defaults, including uid/gid 33 - the image owns /app as www-data, so a 'cleaner' high uid would leave the app unable to read its own bridge sources (guideline 4).",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"pod": {
|
||||
"description": "Merged onto the chart's pod hardening defaults and rendered as the pod's securityContext; kept open because the chart does not interpret the individual fields.",
|
||||
"type": ["object", "null"]
|
||||
},
|
||||
"container": {
|
||||
"description": "Merged onto the chart's container hardening defaults and rendered as the container's securityContext; kept open because the chart does not interpret the individual fields.",
|
||||
"type": ["object", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"probe": {
|
||||
"description": "Merged onto the chart's probe defaults and rendered as a container probe; kept open because the chart does not interpret the individual fields. May be null / empty, which means the defaults apply (issue #99) rather than the probe disappearing. The chart's default carries the probe's context - the named port rss-bridge-port and the /?action=health query string - not just its timings (guideline 5).",
|
||||
"type": ["object", "null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
# 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
|
||||
|
||||
# The bridges RSS-Bridge offers, rendered into the ConfigMap as whitelist.txt
|
||||
# and mounted at /app/whitelist.txt.
|
||||
#
|
||||
# A list rather than a blob of file content: the file format is one bridge name
|
||||
# per line, and the application trims and filters every line before use
|
||||
# (lib/Configuration.php), so nothing is lost by modelling it as YAML - while a
|
||||
# hardcoded file body in the template could not be changed without editing the
|
||||
# chart.
|
||||
#
|
||||
# The single entry "*" is RSS-Bridge's own wildcard and enables every bridge.
|
||||
# It is the default here because it is exactly what the image does when no
|
||||
# whitelist.txt exists at all, so a default install behaves like stock
|
||||
# RSS-Bridge instead of silently serving nothing. Installations that want a
|
||||
# narrower set list the bridge names explicitly:
|
||||
#
|
||||
# whitelist:
|
||||
# - Youtube
|
||||
# - Heise
|
||||
#
|
||||
# Changing this value rolls the pod - the deployment carries a checksum over
|
||||
# the rendered ConfigMap (see rss-bridge.deployment.yaml). Without that the
|
||||
# change would not reach the running container at all: the whitelist is a
|
||||
# subPath mount, which the kubelet never refreshes.
|
||||
whitelist:
|
||||
- "*"
|
||||
|
||||
rssBridge:
|
||||
# Replica count for this workload; 0 is allowed and scales it down.
|
||||
# The chart-global scaleDown flag takes precedence.
|
||||
replicas: 1
|
||||
# Verified against the image, not assumed (issue #128).
|
||||
#
|
||||
# The image ships no USER and runs everything as root by default. It does,
|
||||
# however, run fully hardened once its writable paths are provided: nginx and
|
||||
# php-fpm both start, the front page renders and real bridges fetch feeds.
|
||||
#
|
||||
# uid/gid 33 is www-data, and it is not interchangeable: /app and every file
|
||||
# in it is owned by www-data in the image, nginx.conf declares "user
|
||||
# www-data" and the php-fpm pool declares "user = www-data". Both emit a
|
||||
# harmless notice that those directives are ignored when the master process
|
||||
# is not root, and then serve normally as uid 33.
|
||||
securityContext:
|
||||
pod:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 33
|
||||
runAsGroup: 33
|
||||
fsGroup: 33
|
||||
fsGroupChangePolicy: Always
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
container:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
# All three probes hit RSS-Bridge's own health action, which renders a fixed
|
||||
# 49-byte JSON document without touching the file cache or any remote site.
|
||||
# That keeps liveness free of external dependencies (guideline 3): a scraped
|
||||
# website going down, or a cache directory that is not writable, must not
|
||||
# restart the pod.
|
||||
#
|
||||
# Deliberately not "/": the front page enumerates every enabled bridge and
|
||||
# answers with roughly 1.5 MB, which is a wasteful thing to fetch every ten
|
||||
# seconds forever.
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /?action=health
|
||||
port: rss-bridge-port
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /?action=health
|
||||
port: rss-bridge-port
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
# The startup probe gates liveness/readiness, so those need no
|
||||
# initialDelaySeconds. Startup budget = 5s * 30 = 150s, which is generous:
|
||||
# the container is ready in about two seconds (nginx plus php-fpm, no
|
||||
# migrations, no warm-up).
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /?action=health
|
||||
port: rss-bridge-port
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 30
|
||||
# Extra environment variables. Values are rendered through tpl; the value,
|
||||
# secretKeyRef and configMapKeyRef forms are supported.
|
||||
#
|
||||
# RSS-Bridge maps RSSBRIDGE_<section>_<key> onto its config, so for example
|
||||
# "RSSBRIDGE_system_timezone: Europe/Berlin" works here.
|
||||
env: []
|
||||
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".
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 0.1
|
||||
# Measured with the shipped image: about 59 MiB resident while idle and
|
||||
# roughly 89 MiB after serving feeds, so the previous 20Mi request was
|
||||
# a third of what the workload actually occupies around the clock.
|
||||
memory: 128Mi
|
||||
# The previous manifest requested 2Gi of ephemeral storage - identical to
|
||||
# its limit - and thereby reserved two gigabytes of node scheduling
|
||||
# capacity for a workload that does not come close to using it.
|
||||
#
|
||||
# Everything RSS-Bridge writes was enumerated by running the image with a
|
||||
# writable root filesystem, exercising every whitelisted bridge and then
|
||||
# reading the container's diff layer. The only path that grows is the
|
||||
# feed cache under /app/cache: 17 entries and 2.2 MB after fetching all
|
||||
# five bridges, largest single entry 196 KB, and the application purges
|
||||
# it on its own (FileCache enable_purge). /run and /var/lib/nginx add
|
||||
# under 100 KB together.
|
||||
#
|
||||
# 64Mi is therefore roughly thirty times the measured footprint while
|
||||
# freeing 97% of the old reservation. The limit below stays large, so
|
||||
# the decoupling costs no safety margin (same shape as gotenberg, #106).
|
||||
ephemeralStorage: 64Mi
|
||||
limits:
|
||||
# Set explicitly rather than left to limitFactor x request.
|
||||
#
|
||||
# memory: the manifest this chart replaces capped the container at
|
||||
# 100Mi, but five concurrent feed fetches were measured peaking at
|
||||
# 119 MiB - the old limit was already below the real peak, and php-fpm
|
||||
# is configured for up to five workers with a 128M PHP memory_limit
|
||||
# each. 512Mi covers the measured peak with room to spare and still
|
||||
# bounds a runaway bridge.
|
||||
memory: 512Mi
|
||||
# Unchanged from the previous manifest on purpose: a large cap costs
|
||||
# nothing (it reserves no capacity) and guarantees this change cannot
|
||||
# make eviction more likely than it is today.
|
||||
ephemeralStorage: 2Gi
|
||||
# No CPU limit by default (repo standard): CPU is compressible, so a limit
|
||||
# only throttles. The previous manifest capped CPU at 0.5.
|
||||
|
||||
ingress:
|
||||
# Gates the whole Ingress template. Checked in both directions per guideline
|
||||
# 1: nothing outside this template reads an ingress value - the container
|
||||
# takes no domain in its environment and nginx serves as default_server
|
||||
# without a server_name - so switching the Ingress off frees every value
|
||||
# below, and none of them needs a separate required().
|
||||
enabled: true
|
||||
clusterIssuer: null
|
||||
className: nginx
|
||||
# Extra annotations for the Ingress; merged with the cert-manager
|
||||
# cluster-issuer annotation the chart always sets.
|
||||
annotations: {}
|
||||
# The hosts this Ingress serves. Required - the chart ships no default,
|
||||
# because hostnames are installation data and have no business in a chart.
|
||||
#
|
||||
# Each entry is a host plus an OPTIONAL tlsSecretName:
|
||||
#
|
||||
# hosts:
|
||||
# - host: rss-bridge.example.com
|
||||
# - host: feeds.example.com
|
||||
# tlsSecretName: tls-legacy-feeds-ingress
|
||||
#
|
||||
# Entries sharing a tlsSecretName are grouped into one tls entry, so they
|
||||
# share a single certificate with several SANs. Entries with distinct names
|
||||
# get one certificate each. Omitting tlsSecretName falls back to the repo
|
||||
# convention, tls-<release>-<chart>-ingress.
|
||||
#
|
||||
# DOCUMENTED DEVIATION - why the secret name is configurable at all:
|
||||
#
|
||||
# The repo convention (guideline 4) is exactly one TLS secret per Ingress,
|
||||
# named tls-<release>-<chart>-ingress. The installation this chart takes over
|
||||
# has grown three separate secrets, one per host, from three years of
|
||||
# kubectl apply. Renaming them would not be a cosmetic cleanup: cert-manager
|
||||
# keys certificates by secret name, so three new names mean three fresh
|
||||
# issuances, and the service is unreachable on each host until its
|
||||
# certificate is issued. That is a self-inflicted outage plus three
|
||||
# avoidable Let's Encrypt issuances, for a deployment that has been running
|
||||
# untouched for over a year.
|
||||
#
|
||||
# Making host and secret name per-entry values keeps the existing names in
|
||||
# place today and turns the eventual consolidation onto one certificate - or
|
||||
# the retirement of the legacy hostnames - into a values change instead of a
|
||||
# template change (the explicit ask from cluster-config#33).
|
||||
hosts: null
|
||||
Reference in new issue
Block a user