Validate Helm Charts / Detect changed charts (pull_request) Successful in 5s
Validate Helm Charts / Resource quantities (pull_request) Successful in 17s
Validate Helm Charts / Selector match (pull_request) Successful in 16s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Build stalwart (pull_request) Successful in 8s
Validate Helm Charts / Trivy config stalwart (pull_request) Successful in 22s
Install-test / Install-test stalwart (pull_request) Successful in 1m44s
Validate Helm Charts / Install-test (pull_request) Successful in 1m44s
The policy route for mta-sts.<domain> belonged to another repository
(jk-cluster-config) while pointing at this chart's own Service. It moves
here as mtaSts.hosts, a list, so several domains - subdomains included -
are maintainable in one place and in whichever routing mode is active.
THE LIST IS THE SWITCH. No mtaSts.enabled: "enabled with an empty list"
would be a state with no meaning that the chart would then have to catch.
The schema rejects the key outright, so the rule is enforced rather than
documented. Default [] renders nothing in either mode - the defaults and
the unchanged CI fixture both produce a byte-identical helm template
against origin/main.
Entry form {host, tlsSecretName, clusterIssuer?}. The two TLS fields are
read ONLY in Ingress mode; in Gateway mode TLS sits on the listener, so
requiring them there would demand a value that does nothing.
THE PATH IS HARDWIRED. RFC 8461 fixes /.well-known/mta-sts.txt, rendered
with pathType Exact (type: Exact in the route). A values key for a
protocol-mandated path can only ever be set wrong, and wrong is invisible
from this side. Exact also matters in itself: a prefix on these hostnames
would publish the mail server's whole management surface under them.
ONE INGRESS PER CLUSTER ISSUER, NOT ONE OBJECT - and the plan's own three
requirements forced that. cert-manager reads the two fields at different
scopes: cluster-issuer is an ANNOTATION (object-scoped, and absent means
nothing is issued for that object at all), while secretName is
tls-entry-scoped and yields one Certificate per entry. So an object cannot
both request a certificate and leave another host's secret alone, which is
what an empty per-entry issuer has to mean. Measured in jk-cluster:
ingress-shim names the Certificate AFTER THE SECRET and owns it from the
Ingress, so a shared secret must sit in exactly one object (hence grouping
the tls list by secret, reusing rss-bridge.tlsGroups' shape) and a secret
may not span two issuer groups (hence the abort). Objects are named by
issuer, not by index: an index is only free where the object holds no
certificate, which is the precondition homeassistant's route naming states
for itself.
Gateway mode renders ONE HTTPRoute with every hostname - nothing differs
per host there - attached to the same Gateway(s) as the WebUI route.
WHAT THE CHART CANNOT PROMISE, AND THEREFORE NAMES: it registers a route,
it does not create a domain. The domains live in Stalwart's data store, so
an entry whose domain is missing there answers 404 at a correctly routed
endpoint with every object green - and MTA-STS failures are silent on the
SENDER's side, which delivers in the clear and logs nothing here. Same for
the _mta-sts.<domain> TXT record and the policy's content. The subdomain
trap is documented too: mta-sts.sub.<domain> is one label below
*.<domain>, so a parent wildcard certificate does not cover it.
Not folded into certificate.extraDnsNames deliberately - that would hang
the MAIL certificate on this list, letting a bad policy entry take STARTTLS
and IMAPS with it.
Minor, additive, empty default: 1.2.0+upv0.16.25 -> 1.3.0+upv0.16.25.
CI: ci/httproute/stalwart.yaml enters all three shapes of the entry form
(with issuer, without, and a subdomain), taking the HTTPRoute render test
from 56 assertions over 21 charts to 58 over 21 - stalwart from 2 to 4.
The Ingress half cannot be covered there, since an overlay's whole job is
to switch the mode off, so ci/stalwart/test-values.yaml carries two
entries and the install-test has a real API server accept them.
Fixes #123
140 lines
8.0 KiB
YAML
140 lines
8.0 KiB
YAML
apiVersion: v2
|
|
name: stalwart
|
|
description: A Helm chart to deploy a cluster-capable Stalwart mail server (SMTP, IMAP, JMAP, ManageSieve) on Kubernetes
|
|
icon: https://avatars.githubusercontent.com/u/76623657
|
|
|
|
type: application
|
|
|
|
# appVersion is the exact image tag, and the "v" is part of it - so it is part
|
|
# of appVersion and of the "+up" suffix. Precedent: calibre-web-automated
|
|
# (+upv4.0.6), matrix-synapse (+upv1.159.0), ytdlp-web-player (+upv1.2.0).
|
|
#
|
|
# The glibc image, not "-alpine". Both flavours are published for every release
|
|
# from the same source and carry the same uid/gid 2000 and the same two volumes;
|
|
# the choice is a deployment decision, and keeping it out of the tag is what
|
|
# lets renovate.json5 anchor the version with a plain three-part regex instead
|
|
# of maintaining a compatibility suffix. The moving tags "latest", "v0.16" and
|
|
# their -alpine twins are excluded by that same anchor: measured on 2026-10-02
|
|
# all three of latest / v0.16 / v0.16.24 resolved to one digest pushed seconds
|
|
# apart, so either floating name would change the running image with no commit
|
|
# in this repository and nothing to report it.
|
|
# MINOR, not major, and the evidence is a diff rather than a judgement. That
|
|
# held for the per-group spread constraint and PodDisruptionBudget of Gitea #96
|
|
# (both gated on the COMPUTED replica count being >= 2, and the chart ships
|
|
# full: 1 / mx: 0) and it holds again for the MTA-STS routes of Gitea #123: the
|
|
# list is the switch and it ships EMPTY, so no object is added and no value
|
|
# becomes required in either routing mode. `helm template` against the previous
|
|
# version renders byte-for-byte identically for the defaults AND for
|
|
# ci/stalwart/test-values.yaml as it stands on origin/main - measured, both
|
|
# empty diffs. New optional values with unchanged defaults is exactly the minor
|
|
# case (README, "Versioning").
|
|
version: 1.3.0+upv0.16.25
|
|
appVersion: "v0.16.25"
|
|
|
|
maintainers:
|
|
- name: jklein
|
|
|
|
# The second chart in this repo with a dependency (after ytdlp-web-player).
|
|
# README, "Chart dependencies", holds the bar it has to clear and the price;
|
|
# both are restated here because the price is paid by whoever reads this file
|
|
# next, not by whoever added the line.
|
|
#
|
|
# WHY ONE HERE. Stalwart's in-memory store is not a cache that can be dropped:
|
|
# from v0.16 it also carries the cluster coordinator ("the Default variant
|
|
# reuses the Redis connection already configured on the in-memory store"), so a
|
|
# multi-node install does not work without it. Redis/Valkey is admissible as
|
|
# the in-memory store ONLY - never as the data or blob store. Offering it inside
|
|
# the chart means the wiring (host, port, egress rule) is in one place instead
|
|
# of split across two releases that have to agree.
|
|
#
|
|
# WHY IT IS OFF BY DEFAULT: an installation that already runs a Valkey points
|
|
# externalServices.valkey.host at it and this dependency renders nothing. The
|
|
# subchart is the path for an installation that has none.
|
|
#
|
|
# THE SUBCHART IS OUR OWN valkey CHART, resolved from the published index -
|
|
# NOT upstream's. So guideline 14.2 ("we consume their chart as it comes") does
|
|
# not apply here and there is nothing of someone else's to leave unpatched: the
|
|
# subchart follows this repo's conventions because it IS this repo's chart. What
|
|
# does apply is everything about the subchart being invisible downstream, plus
|
|
# one hazard that a self-referencing dependency adds and an external one does
|
|
# not - see the PR-ordering warning below.
|
|
dependencies:
|
|
- name: valkey
|
|
# KEEP THIS VERSION MOVING - it is the only path a Valkey update has.
|
|
#
|
|
# A subchart is invisible to everything downstream that watches Helm release
|
|
# versions: what this file declares is baked into our packaged .tgz, and
|
|
# Fleet only ever sees the version of "stalwart". So a new valkey chart - a
|
|
# security fix included - reaches a cluster ONLY when this line is raised
|
|
# and a new stalwart version is published. Nothing reports the gap.
|
|
#
|
|
# Renovate's BUILT-IN helmv3 manager reads this block and opens the PR (no
|
|
# customManagers entry exists or would help - repository and version sit
|
|
# together in one file, which is the case that manager is for), and
|
|
# ci/renovate-bump-chart.sh bumps this chart's patch version, which is what
|
|
# actually ships it. That chain is the whole safety net; switched off, this
|
|
# line needs a human on a calendar instead.
|
|
#
|
|
# MEASURED, not assumed, because the registry is self-hosted and the version
|
|
# carries build metadata - two things that could each have broken the chain
|
|
# silently. Renovate 44.132.2, dry run on 2026-10-02 with this dependency
|
|
# pinned at a lower version: manager helmv3 matched the file, took
|
|
# registryUrls from the line below, and offered
|
|
# newVersion: "1.0.0+up9.1.2.alpine", versioning: "helm"
|
|
# with the release timestamp read from our index. So both the self-hosted
|
|
# index and the "+up..." suffix are handled.
|
|
#
|
|
# Independent of appVersion: raising it is a patch or minor bump of this
|
|
# chart and is never tied to the Stalwart image.
|
|
version: "1.0.0+up9.1.2.alpine"
|
|
# Our own published index, and the READ path only. It is anonymously
|
|
# readable - measured 2026-10-02: GET .../index.yaml returns 200 without
|
|
# credentials - so ci/helm-dependencies.sh needs no secret and was not
|
|
# touched. Verified with an empty HELM_REPOSITORY_CONFIG, i.e. the state a
|
|
# fresh runner is in: helm reports "Getting updates for unmanaged Helm
|
|
# repositories" and resolves the exact build-metadata version from the URL
|
|
# alone, with no "helm repo add" anywhere. Both "helm dependency update" and
|
|
# "helm dependency build" were tried.
|
|
#
|
|
# The PUBLISH target and its credentials still live in 1Password and appear
|
|
# nowhere in this repository - see README, "CI/CD".
|
|
repository: "https://git.jk-dev.de/api/packages/jlab-cluster/helm"
|
|
# Off by default, so a plain install renders none of its objects.
|
|
#
|
|
# NOT "valkey.enabled", AND THAT IS NOT A STYLE CHOICE. Everything under the
|
|
# "valkey:" key is passed to the subchart as its values, and our valkey
|
|
# chart ships a values.schema.json with additionalProperties: false and no
|
|
# "enabled" property - so "valkey.enabled" is rejected by the SUBCHART:
|
|
# Error: values don't meet the specifications of the schema(s) in the
|
|
# following chart(s): valkey: - at '': additional properties 'enabled'
|
|
# not allowed
|
|
# Measured. Worse, Helm skips a disabled subchart's schema validation, so
|
|
# the error appears only for whoever first sets it to true - the default
|
|
# path renders clean.
|
|
#
|
|
# The condition is therefore a key of OUR OWN, outside the subchart's value
|
|
# subtree, which is also the honest shape: "enabled" is not a valkey value.
|
|
# It only ever looked like one because upstream's schema was permissive.
|
|
# The alternative - adding "enabled" to valkey/values.schema.json - would
|
|
# have to be its own PR and be published first (see the warning below), for
|
|
# a key that chart does not read.
|
|
condition: valkeySubchart.enabled
|
|
#
|
|
# !!! NEVER CHANGE valkey/ AND THIS CHART IN THE SAME PULL REQUEST !!!
|
|
#
|
|
# The hazard a self-referencing dependency adds. This dependency resolves out of
|
|
# the PUBLISHED index, not out of the working tree, and valkey/ is only
|
|
# published by a push to main that touches it. So in one PR carrying both:
|
|
#
|
|
# - every helm command here - lint, package, render, AND the install-test -
|
|
# pulls the OLD valkey .tgz, because the new one does not exist yet;
|
|
# - the PR therefore goes green against a version of the subchart that is not
|
|
# the one in the diff, and the diff to valkey/ is reviewed while nothing
|
|
# actually exercised it;
|
|
# - after the merge both charts publish, and the first install of the new
|
|
# stalwart is the first time the new valkey is used anywhere.
|
|
#
|
|
# Two consecutive pull requests instead: change valkey/, merge, let it publish,
|
|
# then raise the version here. The ordering is not a nicety - the green check on
|
|
# the combined PR is a statement about the wrong artifact.
|