Merge pull request #132 from jk-cluster/feat/rss-bridge-chart
rss-bridge: new chart
This commit is contained in:
15 files changed
+1063
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
|
||||
@@ -108,3 +108,8 @@ misconfigurations:
|
||||
- id: AVD-KSV-0119
|
||||
paths: ["**/homeassistant.yaml"]
|
||||
statement: "NET_RAW is the capability in question and is added on purpose for device discovery; see AVD-KSV-0022."
|
||||
|
||||
# --- rss-bridge: port 80 is fixed by the image ----------------------------
|
||||
- id: AVD-KSV-0117
|
||||
paths: ["**/rss-bridge.yaml"]
|
||||
statement: "nginx inside the image listens on 80, and the only lever the entrypoint offers (HTTP_PORT) is a sed against /etc/nginx/sites-enabled/default, which a read-only root filesystem rejects - so the alternative to port 80 is giving up readOnlyRootFilesystem. The chart does not merely rely on the kubelet's ip_unprivileged_port_start=0 default, it pins the sysctl in the pod securityContext, so the bind does not depend on node configuration. Measured in k3d with only that value changed: at 1024 nginx fails with 'bind() to 0.0.0.0:80 failed (13: Permission denied)', and adding NET_BIND_SERVICE does NOT fix it (for a non-root process the capability lands in the permitted but not the effective set) - which is why this chart adds no capability, unlike samba. Justified in rss-bridge/values.yaml above securityContext.pod.sysctls."
|
||||
@@ -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,318 @@
|
||||
{{/*
|
||||
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 ip_unprivileged_port_start sysctl is what lets a non-root nginx bind
|
||||
port 80, and this chart has no choice about that port - see the comment on
|
||||
it in values.yaml. Dropping it from a rebuilt default does not fail at
|
||||
render time; it fails on the first node whose default differs.
|
||||
- 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")
|
||||
"sysctls" (list (dict "name" "net.ipv4.ip_unprivileged_port_start" "value" "0")) -}}
|
||||
{{- 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,140 @@
|
||||
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
|
||||
# Registry written out in full on purpose: an unqualified Docker Hub
|
||||
# reference puts "rssbridge" in the registry position, which says
|
||||
# nothing about where the image comes from and is reported as an
|
||||
# untrusted registry (AVD-KSV-0125). See ci/trivy/data/ksv0125.yaml.
|
||||
image: "docker.io/rssbridge/rss-bridge:{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
{{- range $value := .Values.rssBridge.env }}
|
||||
{{- if $value.value }}
|
||||
- name: {{ $value.name | quote }}
|
||||
# quote is required, not cosmetic: a value like "5" or "true"
|
||||
# renders unquoted as a YAML number/bool, and the API server then
|
||||
# rejects the whole manifest with "cannot unmarshal number into
|
||||
# Go struct field EnvVar.value of type string". RSS-Bridge takes
|
||||
# numeric settings this way (RSSBRIDGE_http_timeout), so this is
|
||||
# reachable with the very first env entry someone adds.
|
||||
value: {{ tpl $value.value $ | quote }}
|
||||
{{- else if $value.valueFrom }}
|
||||
{{- if $value.valueFrom.secretKeyRef }}
|
||||
- name: {{ $value.name | quote }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ tpl $value.valueFrom.secretKeyRef.name $ | quote }}
|
||||
key: {{ tpl $value.valueFrom.secretKeyRef.key $ | quote }}
|
||||
{{- else if $value.valueFrom.configMapKeyRef }}
|
||||
- name: {{ $value.name | quote }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ tpl $value.valueFrom.configMapKeyRef.name $ | quote }}
|
||||
key: {{ tpl $value.valueFrom.configMapKeyRef.key $ | quote }}
|
||||
{{- 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,223 @@
|
||||
# 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
|
||||
# Why this chart listens on the privileged port 80, and why that is safe.
|
||||
#
|
||||
# nginx is configured for port 80 inside the image. The only lever the
|
||||
# entrypoint offers is HTTP_PORT, which runs a sed against
|
||||
# /etc/nginx/sites-enabled/default - impossible with a read-only root
|
||||
# filesystem. So the choice is port 80 or readOnlyRootFilesystem, and
|
||||
# port 80 is the cheaper of the two. (The Service selects the port by
|
||||
# NAME, so the number is invisible to it either way.)
|
||||
#
|
||||
# Kubernetes sets net.ipv4.ip_unprivileged_port_start=0 in the pod
|
||||
# network namespace, so binding 80 needs no capability at all. But that
|
||||
# is a runtime default, not a guarantee, and relying on it silently is
|
||||
# exactly the trap the template-chart warns about - so the chart pins the
|
||||
# value instead of inheriting it.
|
||||
#
|
||||
# Measured in k3d (k3s v1.35.5), same pod spec, only this value changed:
|
||||
# sysctl 1024, capabilities.drop [ALL] -> nginx: bind() to
|
||||
# 0.0.0.0:80 failed (13: Permission denied)
|
||||
# sysctl 1024, plus NET_BIND_SERVICE added -> identical failure.
|
||||
# Adding the capability does NOT help: for a non-root process it
|
||||
# lands in the permitted set but not the effective/ambient one, so
|
||||
# it buys nothing here. That is why this chart does not add it.
|
||||
# sysctl 0 (this default) -> binds, HTTP 200.
|
||||
#
|
||||
# Note how that failure looks without probes: the entrypoint starts nginx
|
||||
# in the background and then execs php-fpm, so a failed bind leaves a
|
||||
# container that reports Running with no web server at all. The probes on
|
||||
# /?action=health turn that silent failure into a pod that never becomes
|
||||
# Ready.
|
||||
sysctls:
|
||||
- name: net.ipv4.ip_unprivileged_port_start
|
||||
value: "0"
|
||||
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