Seven guides in docs/ were written next to the code and left in the
source tree, where no operator looks. The website carries them now, so
a second copy here only lets the two drift again: the Backup Classes
page had already fallen behind its original.
docs/release.md, docs/deprecating-applications.md and the two SeaweedFS
runbooks in docs/operations stay. A migration, a bats suite, a workflow
or AGENTS.md reads each of them by path, which is the test for keeping
a document in this repository.
Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Lets an operator bound the writes lost when an isolated Redis master is
failed over: the master refuses writes while fewer replicas than
configured are connected and recent. Both values are unset by default, so
the rendered Redis config is unchanged.
Redis treats 0 in either directive as disabling the check, so the
chart refuses minReplicasMaxLag 0 next to a replica requirement
instead of silently turning the protection off.
Negative values are refused by the schema (minimum 0), since Redis
rejects them at CONFIG SET and the operator would stop healing the
Sentinels of that instance. A requirement of replicas or more is
refused at render: a master with replicas pods has replicas-1
replicas, so it would refuse every write for good.
Assisted-by: LLM
Signed-off-by: Yasar Rahman <yasar.rehman@gmail.com>
The Sentinels were left on redis:6.2.6-alpine when the operator moved to
the freshworks-oss fork, so that its new compiled-in default could not
move them on upgrade. 6.2 is end of life, and every instance now runs a
Sentinel one or two major versions behind the Redis it monitors.
The rollback risk behind that pin, a newer engine rewriting dump.rdb in
a format the older one cannot load, does not apply to Sentinel. It
stores no RDB or AOF, only sentinel.conf on an emptyDir that the
config-copy init container rewrites from the operator's ConfigMap on
every start, so moving back to an older image starts from a clean
config. Taking the image from the same version map as the data pods also
means one image per node instead of two.
The e2e comment that relied on busybox's timeout exit code in the
Sentinel container is updated, since that container now carries
coreutils.
Signed-off-by: Yasar Rahman <yasar.rehman@gmail.com>
Assisted-by: LLM
Every Sentinel pod gets the Redis pods' resources, so the three
Sentinels of an instance reserve three times a Redis replica's memory
while holding only their configuration and the failover state. On a
t1.nano instance that is 3 x 128Mi while the VPA recommender's uncapped
target for a Sentinel container is 16Mi, and the waste grows with every
larger preset, because the dataset that drives the Redis sizing never
reaches a Sentinel.
sentinel.resources and sentinel.resourcesPreset follow the convention of
the top-level pair, and sentinel.resourcesPreset falls back to
resourcesPreset. Explicit Redis resources are not carried over once the
sentinel block sets either key, since they were sized for the dataset.
An empty sentinel.resources map counts as unset, as an empty top-level
resources does.
An empty sentinel block keeps today's sizing, so existing instances
render unchanged and an upgrade does not roll their Sentinels. The
Sentinel WorkloadMonitor reports the preset the Sentinels actually use.
Signed-off-by: Yasar Rahman <yasar.rehman@gmail.com>
Assisted-by: LLM
The tenant Gateway's TLS-passthrough listeners are designed to route
databases by SNI under a per-engine subdomain (*.redis.<apex>,
*.postgres.<apex>), and a passthrough listener forwards the handshake
untouched, so the engine's own server certificate must carry
<release>.<engine>.<apex> or a verifying client fails the hostname
check.
The name is issued next to the existing flat <release>.<apex>, under
the same TLS-and-external condition, so that a later move between the
two hostname schemes reissues no certificate and leaves clients on the
same trust anchor. Nothing renders a route yet; the certificate is
prepared ahead of it. Adding the name changes the SAN list, so existing
external TLS releases get their server certificate reissued once on
upgrade, from the same CA.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
A Redis release with external: true and TLS off publishes a plaintext
endpoint outside the cluster, and nothing stopped a tenant from asking
for one. The chart never infers TLS from external, because enabling it
moves Redis to a TLS-only listener, so external: true without an
explicit tls.enabled: true is exactly that case. A
ValidatingAdmissionPolicy on the Redis kind now refuses it with a
message naming the release and a remedy that works.
TLS is fixed when a Redis instance is created: the operator's
RedisFailover schema refuses a change, but the Redis API accepts it.
A spec's tls.enabled is therefore a request, not the instance's state,
and cannot prove on UPDATE that an instance has TLS; trusting it would
let a tenant flip it, expose the instance, and remove it again. So
external access can be enabled only at creation, where tls.enabled
does decide the instance, and any UPDATE turning it on is refused.
The RedisFailover schema does not exist before the first reconcile, so
a release admitted as external with TLS could have TLS dropped before
the instance is created. tls.enabled is therefore frozen while
external access stays on. Withdrawing external access in the same edit
lifts the freeze, which leaves a tenant a way to unblock an instance
whose TLS was flipped earlier, and the callers trusted by the tenant
host policy are exempt so an administrator can repair one in place.
Other edits to an instance that is already external pass, so releases
exposed in plaintext before the policy keep working.
Postgres is left out on purpose. CNPG pins ssl=on, and its default
pg_hba accepts plaintext from any client that asks, whatever the
chart's tls.enabled says; that field only decides whether the external
hostname goes into the certificate. A policy keyed on it would close
nothing.
The decisions are tested by compiling the shipped template with the
apiserver's own CEL compiler, since a structural chart test cannot
show what the policy admits.
BREAKING CHANGE: creating a Redis with external: true and without
tls.enabled: true, which applied before, is now refused, including a
GitOps re-apply of such a manifest into a fresh cluster. Turning
external on for an existing Redis is refused as well, so exposing an
instance means creating a new one with TLS, and tls.enabled can no
longer be changed on an instance with external access.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The null-namespace case only checked that the in-cluster name is
present, so a chart that emitted a bare "my-redis." SAN, or fell back
to the suite-level host, would still pass. It now carries the same
absence check as the empty-host case, plus the suite host itself.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The redis, valkey and harbor Redis failovers pin
oliver006/redis_exporter:v1.55.0-alpine, which upstream publishes for
amd64 only. On an arm64 node the exporter sidecar exits with an exec
format error, so the Redis pod never becomes fully ready.
The unsuffixed v1.55.0 tag is a multi-arch index (amd64, arm, arm64)
of the same exporter release, with the same entrypoint, user and
port, and none of these charts run a shell in that container. Pin it
by digest, so the reference is reproducible.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Two sentences of the boilerplate shared by the app READMEs were wrong.
"When left empty, the preset is applied" described an all-or-nothing
rule, but cozy-lib.resources.defaultingSanitize merges per key: every
resource that resources leaves unset comes from the preset. And the
legacy flat names were called aliases of their "1:1 instance-type
equivalents", which reads as the c1 series; nano through small match t1,
and medium matches c1.small rather than c1.medium, the trap
docs/operations/resource-presets.md already warns about.
Signed-off-by: Yan Bondarenko <202671653+yankawai@users.noreply.github.com>
The operator side of the carried patch computes the leaf's SAN list in
redisCertificateSANs. The comment in certmanager.yaml still named it
redisCertificateDNSNames, a name the patched source does not contain.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The no / optional / yes constraint on tls.authClients lives in
values.schema.json, and nothing the chart renders shows whether it is
enforced: a bogus value either fails validation up front or reaches the
template as a string. Assert the failure, so the enum cannot be dropped
from the schema without a red test.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
freshworks-oss/redis-operator v3.3.5 has no TLS support. The operator
work that packages/apps/redis renders against was ported upstream as
freshworks-oss/redis-operator#85 and is not in a release yet, so carry it
here over the v3.3.5 tarball until one lands.
Three pieces, and TLS is silently off unless all three are present:
- images/redis-operator/patches/tls.diff is the source half. The
Dockerfile already applies patches/*.diff, and the shell expands that
to labels.diff then tls.diff; they touch disjoint files, so the order
does not matter, but it is alphabetically stable either way.
- patches/crd-tls.diff teaches the vendored CRD about spec.tls. Without
it the apiserver prunes spec.tls from every RedisFailover the redis
chart renders and the patched binary waits for a field it never sees.
- patches/rbac-tls.diff grants Create, Get and Update on Secrets, so the
operator can publish the CA-only Secret named by
spec.tls.caCertSecretName. Those are the only Secret calls in the
patched source: upstream v3.3.5 only ever reads one, and the patch
adds Create and Update on a Secret it addresses by name, so nothing
lists, watches, patches or deletes.
tls.diff is cut from that PR's commit 2ad2ed8d1 rebased onto v3.3.5, and
its header pins that SHA so the next refresh can be diffed against it.
Beyond the TLS listener it carries the renewal roll: on every reconcile the operator stamps a hash of the TLS
Secret's tls.crt and ca.crt into the pod template of the Redis
StatefulSet and the Sentinel Deployment, so a renewed certificate rolls
Redis through the operator's own roller and Sentinel through its
Deployment. The effective spec.tls.authClients value is folded into
the same hash, so changing it rolls the pods the same way instead of
landing at the next unrelated restart, and the operator presents its
client certificate whenever the Secret carries one, independent of that
field, so neither direction of the change locks it out of the instance
it reconciles. It also mounts the TLS volume 0440 when the pod declares an
fsGroup, rejects a caCertSecretName that names the TLS Secret itself,
and waits for the TLS material before it writes the ConfigMaps and the
workloads, so a first TLS install starts its pods once.
tls.enabled is fixed at creation. Redis replication and Sentinel cannot
straddle plaintext and TLS: a replica restarted onto the other protocol
cannot sync from the master, and Sentinels restarted onto it fail over
to such a replica while the master still takes writes. The rule that
refuses the change is a CEL transition rule on the RedisFailover spec;
it lives in the CRD, so it ships here through patches/crd-tls.diff
together with the schema block it references. The cut carries source
only, and the Dockerfile strips the tarball's manifests/, charts/,
example/ and .github/ before applying the patches, so the image builds
from source alone and the two upstream tests that read the CRD YAML skip
instead of failing against v3.3.5's stale copy; the CRD half is
patches/crd-tls.diff.
cert-manager Certificates get no grant. The patched operator manages a
Certificate only under spec.tls.certManager, and packages/apps/redis
pins the other mode: it always supplies spec.tls.certificateSecret, so
that code path is unreachable as the platform ships it. A comment at
the rule site records the decision and names the verbs the rule would
need, so a re-vendor review reads the absence as deliberate.
The chart patches go through the mechanism the packages that customise a
vendored chart already use — reloader, grafana-operator, ingress-nginx,
seaweedfs and kamaji all re-apply a patches/*.diff at the end of update:
— because `rm -rf charts` discards a hand-edit and the next `make update`
would take TLS back out without failing.
tls.diff also carries one change that is not TLS and takes effect with
TLS off: MakeSlaveOfWithPort connects to the replica on its own port
rather than on the master's. It changes a dial, not a pod template, so
nothing rolls when this image ships; the probes dial localhost only
under TLS, and the plaintext pod templates stay byte-identical to
v3.3.5. The patch header names it, so a reader of patches/ does not
take the file for TLS-only.
The grant is narrower than the ported operator's own chart asks for:
that one also grants patch on Secrets, and create, delete, get, list,
patch, update and watch on Certificates. None of that is reachable from
the source as shipped here.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The tenant read path was two grants on Secret names: the operator's
CA-only <release>-ca-cert in the dashboard Role, and the same name in the
ApplicationDefinition's include list. Replace both with the projection
the previous commit declares.
A name grant conveys whatever occupies the name. The operator holds
create and update on Secrets cluster-wide, so what sits at
<release>-ca-cert is the operator's choice rather than the platform's,
and RBAC has no key-level filter to catch it if that ever stops being
just ca.crt. <release>.tenant-ca is written only by the CA-extraction
controller, which copies one named key and refuses PEM private key
material outright. That is the difference between a name that happens to
hold a certificate today and provenance the platform vouches for, and it
is the reason postgres, mongodb, nats and qdrant all reach the tenant
this way — mongodb's operator publishes a CA Secret of its own too, and
that one is excluded rather than granted.
Also drop spec.caCert from the ApplicationDefinition. The
ApplicationDefinition CRD does not define that field and Flux applies
these server-side, so the apiserver rejects the whole object with "field
not declared in schema" and the redis-rd release never goes ready. It was
carried deliberately as a merge-order gate for a controller that reads
it; that controller landed as the TenantProjection reconciler, which
reads a sentinel in the app chart instead, so the gate has nothing left
to wait for.
hack/check-redis-rd-secrets.bats pins the whole rendered Secret surface
rather than probing for known-bad names, alongside the existing guards
for nats, qdrant and mongodb: an include with neither resourceNames nor
matchLabels restricts nothing, and a second matchLabels entry selecting
a label cert-manager also stamps on the key-bearing Secrets would grant
the CA private key while every name in the file stayed correct.
The connection example passes the generated password as well. Password
generation is on by default, so the command as printed connected and then
answered NOAUTH on the first real command.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The tenant needs the release's CA certificate to verify the server, and
the platform's contract for handing one over is the key-free
<release>.tenant-ca projection: the CA-extraction controller lifts a
single named key out of a source Secret, refuses to publish anything
that parses as PEM private key material, and stamps
internal.cozystack.io/tenant-ca on what it writes. postgres, mongodb,
nats and qdrant all reach the tenant this way; redis is the fifth.
Source the chart's own cert-manager CA Secret, <release>-ca-tls, rather
than the CA-only Secret the operator publishes at
spec.tls.caCertSecretName. Both carry the same certificate, but
<release>-ca-tls exists as soon as cert-manager issues the CA, so the
tenant read path does not also depend on the operator having reconciled
the RedisFailover and written its copy. That the source holds the CA
private key beside ca.crt is the intended shape rather than a
compromise — projecting one key out of a key-bearing Secret is what the
controller is for, and it refuses private key material outright.
Gated on tls.enabled, because the source Secret only exists when TLS is
on. The test pins that the gate is tls.enabled and not external access:
an externally exposed plaintext release must render no sentinel, or the
controller waits forever on a Secret nothing creates.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The TLS section described the per-release cert-manager chain without
saying that cert-manager has to be present to issue it. It is present on
every system variant: the platform installs it in the variant-independent
body of the system bundle, so isp-hosted carries it as well. What
isp-hosted lacks is reloader, and the renewal section already covers
that gap.
The cert-manager sentence separates the two ways a cluster can lack it:
without the controller but with its CRDs the resources render and nothing
issues them, and without the CRDs the release fails on an unknown kind.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The generated README renders the tls.authClients default as `{}` because the
block carries no keys. Materialising the default to fix that column corrupts
the schema and the CRD, so note the trap at the line someone would edit.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
When external access is on the chart creates <release>-external-lb
itself, but the leaf certificate covered only the four Services the
operator creates. That name resolves in-cluster like any other Service,
so a client dialing it under TLS failed hostname verification against a
Service this chart owns. Add its four DNS forms next to the operator's,
gated on the same flag that creates it.
Also record at the image pin how the digest moves: it is refreshed by release preparation, not by this change, so an edit to the Dockerfile under images/ ships only once the next repin lands.
The README list of in-cluster names the certificate covers names the
external LoadBalancer Service as well.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
TLS was enabled automatically whenever external was true. In this
operator TLS is not additive — the rendered config sets port 0 and moves
the listener to tls-port — so that inference turned every already-running
externally-reachable Redis into a TLS-only server on its first reconcile
after the upgrade and cut every plaintext client with it. A platform
upgrade must not sever working connections.
Resolve tls.enabled to false when unset, whatever external says. Enabling
TLS becomes an explicit act at creation time, and existing releases keep
the behaviour they have today.
Creation time, not any time: a running instance cannot change mode. A
replica restarted onto the TLS listener cannot replicate from a master
still on plaintext, and Sentinels already on TLS would fail over to a
stale replica, so the operator's RedisFailover schema rejects a change of
tls.enabled and the HelmRelease upgrade fails while the instance keeps
serving what it served before. The README says so and points at creating
a new instance and migrating.
Pin the decision directly: external: true on its own now has to leave the
cert-manager chain unrendered, the operator without spec.tls, and the
tenant without TLS secrets. Every case that previously reached TLS through
external says enabled: true instead, and the null and empty-map forms
resolve to off alongside the unset one.
With nothing inferred there is no upgrade-time behaviour change left, so
the breaking-change marker and footer are gone from this branch.
Say how to get out of it as well. The cozystack Redis API accepts the
change, so the rejection lands on the HelmRelease, and every later edit
to that instance queues behind the failure until tls.enabled goes back
to what it was.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Requiring _namespace.host turned a nil dereference into a hard render
failure for a shape that legitimately has no host. packages/apps/tenant
computes an empty host whenever neither the tenant nor its parent sets
one, and Helm's required rejects the empty string as readily as nil, so
an external Redis running in such a tenant would have stopped templating
altogether on upgrade — no object left to inspect and no error pointing
at the cause.
Emit the external SAN when a host exists and skip it when none does. A
tenant without a host has no external name to certify, so there is
nothing being silently dropped, and the in-cluster names still cover
every client that verifies inside the cluster. Reading through a
defaulted dict keeps an absent _namespace from dereferencing nil.
Replace the test that pinned the failure with two that pin rendering,
for an empty host and for an absent namespace map.
Add the coupling the CA-only secret name needs. Its name has to match
across the operator spec that publishes it and the tenant grant that
exposes it; renaming one alone leaves the tenant without a trust anchor
and produces no error anywhere.
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The certificate covers <release>.<tenant-host> and the TLS section told
users to connect there, but the external Service is a plain LoadBalancer
with no external-dns annotation — unlike the opensearch external Service,
which annotates its hostname explicitly. Nothing points that name at the
LoadBalancer address, so the one documented external path does not work
as written, and the IP fallback fails hostname verification because the
certificate's only IP SANs are loopback.
Describe what is actually true and name the manual step needed to reach
the endpoint from outside. Publishing the name from the chart is the
better fix, but that is a platform-integration decision about which
hostname redis should claim, not a documentation change.
Also disambiguate the CA source comment. Source and destination are
different Secrets — the declared source is read, the projection is
written — and the previous wording put both names close enough together
to read as a contradiction.
The hostname-verification note names which clients it applies to:
redis-cli, the one the README shows, verifies the chain and not the
name, so it connects to a bare IP and hides the mismatch.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Redis and Sentinel read tls-cert-file and tls-key-file once at startup,
so a leaf renewed by cert-manager 30 days before its one-year expiry is
served only after the pods restart. The operator does that restart: on
every reconcile it reads the TLS Secret and stamps a hash of tls.crt and
ca.crt into the pod template of the Redis StatefulSet and the Sentinel
Deployment, so a renewal rolls Redis through the operator's own roller
(the StatefulSet is OnDelete; the roller replaces one pod per reconcile,
replicas before the master) and Sentinel through its Deployment.
The TLS section presented the setup as turnkey and said nothing about
renewal. Say what happens, and name the fallback: when the roll cannot
run, because the operator is down or its reconcile of this RedisFailover
keeps failing, the deadline is the old certificate's expiry, since past
it the operator's own TLS connections to Sentinel and Redis fail along
with the clients.
The CA has a clock of its own: five years, reissued on the same key
before the end, so a ca.crt copied once verifies new leaves only until
the original CA certificate expires. Say that too, and point at
re-reading the projection instead of baking the copy in.
A stakater reloader annotation is not an alternative here and is not
used: the operator regenerates the pod template on every reconcile, so
nothing written into it from outside survives to the next one.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The trust anchor was selected only by the internal.cozystack.io/tenant-ca
label, which nothing stamps until the CA-extraction controller merges. A
label-only entry selects nothing until then, so a TLS-enabled Redis gave
the tenant no way to obtain the certificate needed to verify the server —
the dashboard Role grants the Secret by name, but it never became a
tenant secret and so never appeared on the product surface.
Name the operator's CA-only Secret directly alongside the auth secret.
It holds ca.crt and no private key, so this widens tenant visibility by
exactly the object the tenant needs and nothing else. The label selector
stays for the converged path; once the controller publishes its
projection the anchor is reachable under both names.
Cover the two assertions the feature depended on and did not have:
caCertSecretName, which has to agree with both the dashboard Role and the
declared CA source, and the loopback IP SANs, added by their own fix
commit and until now asserted nowhere.
Correct the tri-state helper's comment. It claimed the kindIs "invalid"
guard covers an explicitly null tls.enabled; the schema types that field
as boolean and rejects null before any template runs. The guard covers
the reachable shape, a tls map with no enabled key, now tested.
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Assisted-By: Claude <noreply@anthropic.com>
TLS replaces plaintext in this operator rather than running beside it —
with TLS on, Redis and Sentinel are configured with port 0 and serve only
on the TLS port. Since tls.enabled falls back to external, an already
deployed instance with external: true switches to TLS-only on its first
reconcile after the upgrade and every plaintext client breaks. That was
documented as a feature and nowhere as a change of behaviour for existing
instances. State it as a breaking change and name tls.enabled: false as
the opt-out.
Also spell out which hostname the certificate actually covers. The SAN
list carries the four service names and the external tenant hostname, and
its only IP entries are the loopback addresses used by the in-pod probes
and the metrics sidecar, so connecting to the external LoadBalancer by IP
fails for clients that verify the hostname.
The TLS section marker sat above the version parameter, which put version
in the TLS table in the generated README instead of Common parameters.
Move the marker below it. Regenerating also reorders version ahead of tls
in keysOrder, matching how postgres orders the same two fields.
Pin the PackageSource CRD replacement policy the way the mongodb-operator
one is pinned. The RedisFailover CRD gained spec.tls with the fork, and
Helm never upgrades CRDs shipped in crds/, so losing CreateReplace would
leave the live CRD behind and the apiserver would strip spec.tls from
every RedisFailover the chart renders — TLS silently off, no error.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The external SAN dereferenced .Values._namespace.host directly, so a
render without that injected value aborted on a nil pointer with no
indication of what was missing. TLS now defaults on whenever external is
true, which puts that lookup on the default path for every externally
reachable Redis.
Wrap it in required with an explicit message, matching how the nats chart
guards the same lookup in its own certificate template. Failing is the
right behaviour rather than dropping the SAN: a certificate issued
without the external hostname is accepted by nothing outside the cluster
and the resulting handshake error points nowhere near the cause.
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Two defects in the generated ApplicationDefinition and the certificate
template.
The regenerated openAPISchema had lost the x-cozystack-options extension
on storageClass that values.schema.json still carries, which is what
drives the storage class picker in the dashboard form. The schema was
generated with cozyvalues-gen 1.5.0 while CI pins 1.6.0, and the older
release drops the extension. Regenerated with the pinned version, which
restores it.
The cluster-domain lookup indexed .Values._cluster directly, so a render
where the platform injects no _cluster map failed with "index of untyped
nil" before the documented cozy.local fallback could apply. Default the
parent map so the fallback is actually reachable, and extend the test to
cover an absent map as well as an empty domain — the previous test only
exercised a domain set to the empty string, which never reached the
failing path.
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The leaf certificate built its .svc.<domain> SANs from the platform's
cluster-domain value with cluster.local as the fallback, while every
other chart in the tree falls back to cozy.local, which is the domain
cozystack actually configures. The platform always injects the value so
the fallback does not fire today, but if it ever did the certificate
would carry names nothing resolves under and every TLS dial by FQDN
would fail hostname verification.
Pin the fallback with a test; the existing cluster-domain coverage only
exercised values that were explicitly set.
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The dashboard Role handed the tenant the release's leaf Secret whenever
tls.authClients was yes, on the reasoning that a client needing to
authenticate can reuse the server identity as its own. That Secret holds
tls.key, so any subject at the tenant's use level could read the private
key of the certificate Redis and Sentinel present and impersonate those
endpoints to every client that trusts the release CA.
Withhold it. The tenant keeps read access to the CA-only Secret, which is
what verifying the server actually requires and which carries no private
key at all.
This leaves authClients=optional and =yes without a client certificate a
tenant can obtain, since the CA private key needed to sign one is also
withheld by design. Document that limitation in README.md rather than
paper over it: in-cluster components are unaffected because the operator
mounts the leaf for them, but an external client has no supported path to
a certificate this CA accepts, so those two values are only usable when
the certificate is supplied out of band.
Also correct the neighbouring comment, which described the per-release CA
as trusted by the whole cluster. It is self-signed and scoped to one
release, and nothing outside that release trusts it.
Add the test target the package was missing. The TLS suite added with
this feature sat in tests/ with no way to invoke it, so CI had never run
any of it; the assertions covering this Role were passing only in
principle. All 24 now run, including the two that pin the leaf Secret as
withheld at both authClients values.
The authClients sentence also says to quote the value: an unquoted yes or
no in a manifest is read as a boolean and the string field rejects it.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
When the chart supplies the cert via spec.tls.certificateSecret, the operator stops generating SANs, so redis_exporter dialing rediss://127.0.0.1 fails CA verification because a "localhost" DNS SAN does not match an IP literal. Add 127.0.0.1 and ::1 ipAddresses to the leaf Certificate, mirroring the operator's own self-generated cert.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
The redis-operator now publishes an Opaque Secret holding only ca.crt
(no private key) via spec.tls.caCertSecretName. Point that field at
<release>-ca-cert and grant the dashboard Role get/list/watch on it so
tenants can verify the server without touching any private key.
Drop the dashboard grant on <release>-ca-tls: that cert-manager secret
holds the CA private key and must never be exposed to tenants. The leaf
secret <release>-tls is now granted only when authClients=yes, where
clients reuse it as their client identity.
Refresh the vendored RedisFailover CRD with the new caCertSecretName
field and update the helm-unittest RBAC cases for the new layout.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
TLS_ECDHE_ECDSA cipher suites require the server certificate to assert the
digital signature key usage so the server can sign ephemeral key-exchange
parameters. Without it, strict TLS clients reject the handshake even though
the cert is otherwise valid. Added the usage to the leaf Certificate and
extended the helm-unittest suite to cover it.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Lift the documented `no` / `optional` / `yes` values for tls.authClients
from prose into an `## @enum {string} AuthClients` typedef in
packages/apps/redis/values.yaml. cozyvalues-gen then propagates the
constraint into:
- values.schema.json — adds `"enum": ["no","optional","yes"]` so
invalid values fail chart-value validation up front.
- api/apps/v1alpha1/redis/types.go — `AuthClients` becomes its own
Go type with a `+kubebuilder:validation:Enum=...` annotation,
aligning the cozystack Redis CR with the operator-side enum on
the RedisFailover CRD.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Expose tls.enabled in the Redis chart values, mirroring the pattern
already in use by mariadb, nats, opensearch, qdrant and rabbitmq. The
flag is tri-state: when unset, TLS follows the value of `external`; an
explicit boolean overrides the default.
When TLS is enabled the chart renders the standard cozystack 4-resource
cert-manager chain in the release namespace:
- Issuer (self-signed bootstrap)
- Certificate (CA, ECDSA, isCA, rotationPolicy: Never, 5y)
- Issuer (CA-backed)
- Certificate (leaf, secret <release>-tls, ECDSA, 1y, rotationPolicy: Always)
The leaf certificate covers the SAN list the redis-operator computes for
its own Certificate (operator/redisfailover/service/generator.go:
redisCertificateDNSNames): short, namespace, .svc and .svc.<clusterDomain>
forms for rfr- / rfrm- / rfrs- / rfs- services plus a wildcard for
per-pod headless DNS names of the redis statefulset. Localhost is
included for in-pod readiness probes; when `external` is true the
tenant LB hostname is added too.
The chart hands the resulting Secret to the operator via
spec.tls.certificateSecret.secretName so the operator never creates a
duplicate Certificate; it just mounts the Secret into both Redis and
Sentinel pods and wires the redis tls-* directives. tls.authClients
maps to the Redis tls-auth-clients directive and defaults to "no".
The dashboard-resources Role is extended with the new TLS secret names
only when TLS is on, so the cozystack dashboard can read them to
display connection details.
Helm unit tests in tests/tls_test.yaml cover the tri-state resolution
(four enable/external combinations), SAN list coverage for all four
service names and the wildcard, custom cluster-domain handling, the
external SAN injection path, authClients propagation, and dashboard
RBAC expansion.
The comment above the CA usages named the wrong bits: cert-manager maps
signing to the digital signature bit, and crl sign and ocsp signing are
separate usages this chart does not request.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
The freshworks-oss operator's compiled-in default image moved from
redis:6.2.6-alpine to 7.2.4; Harbor pinned no image at all and apps/redis left
its sentinels unpinned, so an upgrade would move them silently. 7.2.4 rewrites
dump.rdb into a format 6.2.6 refuses to load, so a platform rollback would leave
Harbor's Redis crash-looping on its own data. Pin redis+sentinel (Harbor) and
the sentinel (apps/redis) at the version they run today; bump deliberately and
separately.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
The freshworks-oss operator filters RedisFailovers by the
redis-failover.freshworks.com/operator-group label matching its
OPERATOR_GROUP_ID; without the label it ignores the CR and no Redis is
provisioned. Stamp the group (cozystack) so the migrated operator adopts
the app's clusters.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
Add the storageclass dynamic-dropdown x-cozystack-options annotation to the
storageClass field of the data apps (openbao, clickhouse, mariadb, redis,
harbor, opensearch, rabbitmq, mongodb, postgres, nats, kafka, qdrant,
foundationdb, http-cache) at the values.yaml source, so the extension is
emitted by cozyvalues-gen instead of hand-edited into values.schema.json.
kafka carries two (Kafka and ZooKeeper structs); foundationdb is nested
under the Storage struct.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
Mark fields that reference cluster resources with the x-cozystack-options
vendor keyword so the dashboard renders them as dynamic dropdowns:
GPU, instanceType, instanceProfile, network (vm-instance), GPU/
instanceType (kubernetes nodeGroups), image (vm-disk), storagePool
(bucket), storageClass (all apps) and disks[].name (vm-instance). Drop
the stale static instanceProfile enum.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
Annotate storageClass as immutable across every stateful app whose
chart exposes one — clickhouse, foundationdb, harbor, http-cache,
kafka (Kafka + ZooKeeper), kubernetes (top-level only), mariadb,
mongodb, nats, openbao, opensearch, postgres, qdrant, rabbitmq,
redis, vm-disk. Without coverage across the catalogue the UI would
render storageClass read-only on some apps and editable on others —
a worse mental model than uniform behaviour.
The annotation lands at the schema layer (CEL XValidation rule
'self == oldSelf' on values.schema.json / types.go / embedded
openAPISchema in cozyrds/<app>.yaml). The cozystack aggregated
apiserver does not yet evaluate CEL on application-backed resources;
that is tracked at cozystack/cozystack#2657. The UI
(cozystack/cozystack-ui#6) consumes the marker today.
The PVC-pinning argument is identical for every chart: Kubernetes
fixes a PVC's storageClassName at creation time, so editing this
field on an existing resource never migrated data. The schema
annotation makes that contract explicit.
kubernetes.nodeGroups[name].storageClass is intentionally NOT
annotated: the field is optional and undefaulted, so a strict
'self == oldSelf' rule would block any future attempt to set it on
an existing node group. A richer ratcheting form is needed there —
see docs/storage-immutability.md.
A refactor in pkg/registry/apps/application/rest.go extracts the
inline schema-build path into buildSpecSchema so it can be
unit-tested. Three regression tests in rest_spec_schema_test.go pin
the contract that x-kubernetes-validations survives the
v1->internal->structural conversion (loaded from the actual MariaDB
ApplicationDefinition fixture on disk, not a hand-edited mock).
Each per-app README gains a one-line blockquote pointing to a new
docs/storage-immutability.md that explains the contract and which
consumers enforce it today.
Requires cozystack/cozyvalues-gen with @immutable support — bump the
pre-commit pin in .github/workflows/pre-commit.yml once the release
exists.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Address review feedback from coderabbitai across:
- packages/apps/kubernetes/README.md
- packages/apps/mariadb/README.md
- packages/apps/nats/README.md
- packages/apps/qdrant/README.md
- packages/apps/redis/README.md
- packages/apps/tcp-balancer/README.md
- packages/apps/vpn/README.md
Each of these READMEs left an inline 7-row preset table listing only
the legacy flat names after the parameters block above already
defaulted to an instance-type value (e.g. `t1.nano`). cozyvalues-gen
only regenerates the `## Parameters` block, so the hand-written
section drifted.
Replace the inline tables with the same short paragraph already used
by postgres and kafka READMEs: name the five series, note that the
seven legacy names remain accepted as deprecated aliases, and link to
the canonical reference at docs/operations/resource-presets.md so the
full size matrix and legacy mapping live in exactly one place.
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Rewrite resourcesPreset values in every apps/* and extras/* chart from
the legacy flat names to the new instance-type form, using the 1:1
mapping:
nano -> t1.nano micro -> t1.micro
small -> t1.small medium -> c1.small
large -> c1.medium xlarge -> c1.large
2xlarge -> c1.xlarge
Each affected values.yaml carries the canonical 47-value enum (40
instance-type names plus 7 legacy aliases) so the schema accepts both
forms during the deprecation window. cozyvalues-gen regenerates the
matching api/apps/v1alpha1/*/types.go kubebuilder enum and defaults,
the chart values.schema.json, the README parameter table, and the
ApplicationDefinition openAPISchema in packages/system/*-rd/cozyrds.
Three hardcoded legacy literals in chart templates migrated:
- packages/apps/harbor/templates/harbor.yaml:121 (portal)
- packages/apps/opensearch/templates/opensearch.yaml:43 (dashboards)
- packages/system/keycloak/templates/sts.yaml:115 (heap fallback)
packages/apps/foundationdb/Makefile gains the missing hack/update-crd.sh
call. Without it, every regen left foundationdb-rd cozyrds stale and
new FoundationDB app CRs through cozystack-api were rejected at
validation for any non-legacy resourcesPreset value.
Every default-rendered chart produces identical CPU and memory to the
value applied before the rename, courtesy of the legacy alias table in
cozy-lib and the 1:1 default mapping.
Co-authored-by: Matthieu ROBIN <matthieu@hidora.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Move common-envs.mk and package.mk from scripts/ to hack/ directory.
Update all Makefile includes to use new paths. Remove unused
issue-flux-certificates.sh script.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>