Files
helm-charts/rss-bridge/values.schema.json
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

224 lines
12 KiB
JSON

{
"$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). Mutually exclusive with httpRoute.enabled (Gitea #114) - the render aborts when both are true - and the default here is TRUE, so Gateway mode needs this set to false explicitly. Re-checked for #114, the sweep in which three charts turned out to compute a value from this switch that silently flipped in Gateway mode: this chart computes none. Outside the Ingress template the only readers of an ingress value are NOTES.txt, which knows its own mode, and the tlsGroups helper, which is called only from behind this switch.",
"type": "boolean"
},
"clusterIssuer": {
"description": "Not read in Gateway mode: the chart renders no Certificate there. Required only while ingress.enabled, because its single read lives inside the Ingress template behind that switch.",
"type": ["string", "null"]
},
"className": {
"description": "Not read in Gateway mode: the class is a property of the Gateway there, and the Gateway belongs to the platform.",
"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. Not read in Gateway mode: the Gateway API has no annotation channel for controller behaviour. Measured on the live object (2026-10-13): the running Ingress carries no nginx.ingress.kubernetes.io annotation at all, so unlike most charts of the sweep this one loses nothing by that today - an absence of evidence about this installation, not a guarantee about another.",
"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. Not read in Gateway mode, where the public names come from httpRoute.hostnames instead - the required() on this list sits inside the Ingress template behind ingress.enabled and is therefore already conditional on the mode.",
"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. MEANINGLESS IN GATEWAY MODE, where the certificate sits on the Gateway's listener - which is why httpRoute.hostnames is a plain list of strings rather than a reuse of this list.",
"type": ["string", "null"]
}
}
}
}
}
},
"httpRoute": {
"description": "Gateway-API alternative to the Ingress (Gitea #114), and this chart is the last of that sweep. 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 it set to false explicitly. The chart renders only an HTTPRoute with parentRefs; class and certificate sit on the Gateway and belong to the platform. ONE object carries every hostname, because nothing differs per host - all hostnames serve the same Service at '/'. The helper rss-bridge.tlsGroups is inert in this mode: with TLS on the listener there is nothing left to group by secret name. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"parentRefs": {
"description": "The Gateway(s) the route attaches to. Required while httpRoute.enabled is true, enforced by required()-equivalent in the template (an empty list aborts the render) - a JSON Schema \"required\" cannot do it, because the key ships as an empty list and is therefore present. Entries are the Gateway API's ParentReference and are passed through unchanged.",
"type": "array",
"items": {
"$ref": "#/definitions/parentRef"
}
},
"hostnames": {
"description": "The public names of this release in Gateway mode. Required while httpRoute.enabled is true - the template aborts on an empty list, which a JSON Schema \"required\" cannot express because the key ships as an empty list. Its own value rather than a fallback or a reuse: this chart has no ingress.domain to fall back to, and ingress.hosts entries carry tlsSecretName values that address nothing once TLS sits on the Gateway's listener. Unlike freesailarr and homeassistant - which reject a hostnames key outright because a chart-global list could not say which app or host entry it belonged to - this chart serves one Service on one path, so a chart-global list is unambiguous. An empty list would not render nothing: a route without hostnames matches every request reaching its listener.",
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
}
}
}
},
"definitions": {
"parentRef": {
"description": "A Gateway API ParentReference, passed verbatim into HTTPRoute.spec.parentRefs. Deliberately left OPEN (no additionalProperties: false), for the same reason probes and security contexts are: Kubernetes owns this schema, the Gateway API adds fields to it across releases, and a closed list here would reject a field only upstream knows about. What the chart can assert is that \"name\" is there - it is required by the API and a reference without it attaches to nothing, which is the silent failure this chart aborts the render over.",
"type": "object",
"required": ["name"]
},
"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"]
}
}
}