Files
helm-charts/rss-bridge/values.yaml
T
jklein 45b2d2ea4e
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 17s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 15s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 8s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 24s
Install-test / Install-test rss-bridge (pull_request) Successful in 1m10s
Validate Helm Charts / Install-test (pull_request) Successful in 1m10s
feat(rss-bridge): Gateway-API as an alternative to the Ingress (last chart)
rss-bridge was the twenty-second and last chart of the Gateway-API sweep:
22 charts carried an Ingress template, 21 an HTTPRoute template, and the
difference was exactly this one. It had been held back as frozen by #51 -
a runner defect, not a property of the chart - and that freeze is lifted.

One HTTPRoute carrying ALL THREE hostnames, because nothing differs per
host: every name serves the same Service at "/", measured on the live
Ingress. spec.hostnames is a list, so one object is the whole answer and a
second would repeat one rule. (Unlike homeassistant, whose paths differ per
host, or matrix-synapse, whose two routes want different backends.)

httpRoute.hostnames is its own MANDATORY value, which is a third shape for
this repository and follows from the values rather than from taste. Most
charts fall back to ingress.domain; freesailarr and homeassistant reject a
hostnames key outright, because a chart-global list could not say which app
or host entry it belonged to. This chart has no ingress.domain either, but
its names are unambiguous - and the ingress.hosts list it would otherwise
reuse carries per-host tlsSecretName values that address nothing once TLS
sits on the Gateway's listener. Reusing it would drag a structure whose
other half is meaningless into the new mode.

rss-bridge.tlsGroups is therefore INERT in Gateway mode, and that is named
as a simplification rather than quietly translated: the helper exists only
to group hosts by TLS secret name, and there is nothing left to group. The
required() on ingress.hosts needed no change to become conditional - it
already sits inside the Ingress template behind ingress.enabled - so what
is new is that values.yaml and the schema now say so.

The ingress.enabled-derived-value screen that caught sure, freesailarr and
ytdlp-web-player was run here and came back CLEAN: outside the Ingress
template the only readers of an ingress value are NOTES.txt, which knows
its own mode, and tlsGroups, called only from behind the switch. The app
takes no base URL in its environment, so no "published" helper is needed.

First chart of the sweep whose Gateway branch has NO loss to explain, and
it was measured rather than assumed (read-only kubectl against the live
object): the running Ingress carries the cert-manager annotation plus Helm
and Rancher/Fleet bookkeeping, and not one nginx.ingress.kubernetes.io
annotation. The group-3 screen came back empty, so NOTES.txt says so
positively instead of copying a loss paragraph from another chart.

Also corrects the last occurrence in the repository of the outdated
sentence "charts in this repo deliberately have no dependencies"
(rss-bridge/templates/_helpers.tpl) - dependencies are admissible but
expensive since #214, and ytdlp-web-player, stalwart and paperless-ngx use
subcharts. The reason the helper is duplicated is unchanged, only its
content: sharing it would mean a library chart, a second artefact to
publish, version and pull for about one change a year.

Minor, not major: additive, defaults unchanged, and relaxing a required is
not a tightening. Proven with an EMPTY helm template diff against
origin/main for the defaults (229 lines, byte-identical); the Gateway mode
with its overlay renders the HTTPRoute in place of the Ingress and nothing
else changes. appVersion untouched - 2026-09-30 is the newest date tag the
registry has, everything newer is sha-* and latest.

ci/run-httproute-match-test.sh goes from 56 assertions over 21 charts to
58 over 22. The overlay at ci/httproute/rss-bridge.yaml has to SUPPLY
hostnames, the second in the repository that does (homeassistant's was the
first, for a different reason), because this chart has no fallback.

NOT PROVEN, and stated in the PR as well: that traffic flows. No Gateway
controller runs in CI and no HTTPRoute reaches "Accepted: True" there. For
this chart there is additionally NO successful install-test at all - it
fails on the Gitea runner in either mode (#51, reproduced on two images
while the same chart and image run healthy in the cluster). The chart is
deliberately NOT added to ci/excluded-charts.txt: an exclusion would
silence the signal, and the day the runner is fixed the test goes green by
itself. So the only red contexts expected here are
"Install-test / Install-test rss-bridge" and the collective
"Validate Helm Charts / Install-test".

Fixes #114
Fixes #40
2026-10-13 09:58:11 +00:00

353 lines
17 KiB
YAML

# 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 21 MiB resident at startup and
# roughly 64 MiB after serving feeds, so the 20Mi the original manifest
# requested was a third of what the workload occupies around the clock.
# (Re-measured on the 2025-08-05 image; the 2025-06-03 image came out
# marginally higher at 24 MiB / 71 MiB under the identical procedure.)
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 a warm container serving five concurrent feed fetches was
# measured peaking at 119 MiB - the old limit was already below the real
# peak. php-fpm is configured for up to five workers with a 128M PHP
# memory_limit each, so the theoretical ceiling is far higher still.
# 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().
#
# MUTUALLY EXCLUSIVE WITH httpRoute.enabled (Gitea #114) - the render aborts
# when both are true, naming both value paths. Note the default here is TRUE,
# so switching to Gateway mode means setting this to false explicitly.
#
# Re-checked for #114, because this is the value that trips charts up in the
# Gateway mode: anything COMPUTED from ingress.enabled flips when the mode
# changes, and three charts of the sweep (sure, freesailarr,
# ytdlp-web-player) had exactly such a value - one of them a silent
# authentication bypass. This chart has none. The grep was run over the whole
# chart directory: outside the Ingress template the only readers of an
# ingress value are NOTES.txt (which knows its own mode) and the tlsGroups
# helper (called only from behind this switch). RSS-Bridge takes no base URL
# and no hostname in its environment, so there is nothing here that could
# come out wrong while the pod stayed Ready - no "published" helper needed.
enabled: true
# NOT READ IN GATEWAY MODE: the chart renders no Certificate there, so there
# is nothing for cert-manager to do on behalf of this release. Required only
# while ingress.enabled - the single read lives inside the Ingress template,
# behind that switch.
clusterIssuer: null
# NOT READ IN GATEWAY MODE: the class is a property of the Gateway there, and
# the Gateway belongs to the platform (cluster-config).
className: nginx
# Extra annotations for the Ingress; merged with the cert-manager
# cluster-issuer annotation the chart always sets.
#
# NOT READ IN GATEWAY MODE: the Gateway API has no annotation channel for
# controller behaviour, so anything put here is gone in that mode.
#
# Unlike most charts of the sweep, this one loses NOTHING by that today, and
# it was measured rather than assumed (2026-10-13, read-only kubectl against
# the live object): the running Ingress carries the cert-manager
# cluster-issuer annotation plus Helm and Rancher/Fleet bookkeeping, and not
# one nginx.ingress.kubernetes.io annotation. Group 3 of the sweep found an
# operator-set annotation filling a chart's gap on four of its six charts -
# that screen came back empty here. An absence of evidence about THIS
# installation is not a guarantee about yours: whatever you put here still
# disappears in Gateway mode.
annotations: {}
# The hosts this Ingress serves. Required - the chart ships no default,
# because hostnames are installation data and have no business in a chart.
#
# NOT READ IN GATEWAY MODE, and this is the one value of the block where that
# needed a decision rather than a note - see httpRoute.hostnames below. The
# required() on this list lives inside the Ingress template, behind
# ingress.enabled, so it is already conditional on the mode and demands
# nothing in Gateway mode.
#
# 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).
#
# tlsSecretName IS MEANINGLESS IN GATEWAY MODE - the certificate sits on the
# Gateway's listener - which is why httpRoute.hostnames below is a plain list
# of strings rather than a reuse of this one.
hosts: null
# THE GATEWAY-API ALTERNATIVE TO THE INGRESS ABOVE (Gitea #114), and this chart
# is the LAST of that sweep - 22 charts carried an Ingress template, 21 an
# HTTPRoute template, and the difference was this one. It was held back because
# its install-test fails on the Gitea runner (#51), not for anything about its
# shape, and that test is still red: the failure is a known, bounded
# environment defect and no finding about this mode.
#
# Off by default, so an existing release is unaffected. Mutually exclusive with
# ingress.enabled - the render aborts when both are true, naming both value
# paths. Note that ingress.enabled DEFAULTS TO TRUE here, so this mode needs
# ingress.enabled: false set explicitly.
#
# The chart renders ONLY an HTTPRoute with parentRefs - no Gateway, no
# gatewayClassName, no Certificate and no implementation-specific policy
# object. Class and certificate belong to the Gateway and therefore to the
# platform (cluster-config), which is what keeps this chart independent of
# whether the cluster runs nginx-gateway-fabric, Cilium, Envoy Gateway or
# Istio.
#
# ONE OBJECT FOR EVERY HOSTNAME, because nothing differs per host: all three
# hostnames of the live installation serve the same Service at "/", measured on
# the running Ingress. spec.hostnames is a list and its rules apply to every
# hostname of the object, so one object is enough - unlike homeassistant (paths
# differ per host) or matrix-synapse (different paths and backends).
#
# WHAT BECOMES INERT, AND IT IS A SIMPLIFICATION RATHER THAN A LOSS:
# rss-bridge.tlsGroups, the helper that groups hosts by TLS secret name so the
# three grown certificates keep their names. With TLS on the listener there is
# nothing left for it to group, and this mode never calls it.
#
# The Gateway-API CRDs must be installed in the cluster: the chart does not and
# cannot detect them (.Capabilities.APIVersions is unreliable at template
# time), so with httpRoute.enabled: true and no CRDs the INSTALL fails.
httpRoute:
enabled: false
# The Gateway(s) this route attaches to - a platform object, usually in
# another namespace, which must allow this namespace in its listener's
# allowedRoutes. One entry per parent, the fields are the Gateway API's
# ParentReference (name, and optionally namespace, sectionName, port, group,
# kind), passed through unchanged:
#
# parentRefs:
# - name: public
# namespace: gateway
# sectionName: https
#
# REQUIRED while httpRoute.enabled is true - the render aborts on an empty
# list, because a route without a parent is created, is valid and routes
# nothing, with no event to say so.
parentRefs: []
# The public names of this release in Gateway mode. REQUIRED while
# httpRoute.enabled is true - the render aborts on an empty list.
#
# Its own value, and both halves of that are deliberate:
#
# Why not a fallback like the other charts: most of them have an
# ingress.domain - one public name - and an empty hostnames list falls back
# to it so there is a single source of truth. This chart has no
# ingress.domain; it has a LIST of hosts, so there is no single name to fall
# back to.
#
# Why not a reuse of ingress.hosts: those entries carry an optional
# tlsSecretName, which exists only to keep three grown certificates alive on
# their existing secret names, and which addresses nothing once TLS sits on
# the Gateway's listener. Reusing the list would drag a structure whose
# second field is meaningless into this mode and invite somebody to set it
# there. (freesailarr and homeassistant REJECT a hostnames key outright, for
# the opposite reason: a chart-global list could not say which app or which
# host entry it belonged to. Here there is one Service on one path, so a
# chart-global list says exactly one thing - which is why this chart gets the
# key those two refuse.)
#
# An empty list does NOT render nothing: an HTTPRoute without hostnames
# matches every request that reaches its listener, so on a shared Gateway
# this release would answer for hostnames belonging to other releases. Hence
# the abort.
#
# hostnames:
# - rss-bridge.example.com
# - feeds.example.com
hostnames: []