mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-09 14:02:56 +09:00
feat(tenant): inherit _namespace.gateway from parent
Mirror the existing _namespace.ingress inheritance for Gateway API attachment. A child tenant under a Gateway-owning ancestor publishes its routes through that ancestor's Gateway by default, without burning a dedicated LB IP / Certificate per child. Resolution rule (tenant.gatewayEffective): * gateway=true explicit → own Gateway (LB IP + cert burned) * gateway=false explicit → parked (no inherit, no own) * gateway unset → inherit $parentNamespace.gateway Removes the auto-default for derived-apex tenants — the previous shape gave every derived-apex child its own Gateway / IP / cert, which scales linearly with tenant depth and produces ~N×K LB IPs on a tree of N top-level × K derived-apex tenants. Inheritance collapses that to one LB IP per Gateway-owning ancestor. Custom-apex tenants (host explicitly set to a non-derived value) now fall through to inheritance instead of auto-firing a separate Gateway. The previous auto-fire conflated 'custom apex' with 'want public exposure' and would have silently failed TLS termination anyway (the ancestor's cert does not cover an unrelated apex). The new rule: explicit gateway=true is the only way to ask for a separate Gateway. The namespace.cozystack.io/gateway label and _namespace.gateway in the rendered cozystack-values Secret now both carry the owner-tenant name — could be self (when this tenant owns one) or any ancestor up the chain. Apps in the tenant render HTTPRoutes whose parentRefs.namespace points at that owner. helm-unittest pinned for the HelmRelease render side. The cross-namespace attach end of the flow (ReferenceGrants, listener / SAN expansion on the owner Gateway) lands separately in the TenantGateway controller. Signed-off-by: Aleksei Sviridkin <f@lex.la>
This commit is contained in:
3 files changed
+112
-157
No files matched your search
@@ -15,46 +15,47 @@
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
tenant.gatewayEffective resolves whether a tenant should have its own
|
||||
per-tenant Gateway. Cozystack targets low-skill operators: the default
|
||||
must "just work" without forcing them to learn that `gateway` is a
|
||||
separate flag from `host`.
|
||||
tenant.gatewayEffective resolves whether THIS tenant should own a
|
||||
dedicated Gateway resource. Returns the literal string "true" or
|
||||
"false" (callers must compare with `eq ... "true"`, bool coercion
|
||||
is not safe).
|
||||
|
||||
When the helper returns "false" the tenant does NOT skip Gateway
|
||||
routing — it attaches its published Routes to the Gateway of the
|
||||
nearest ancestor that owns one, via _namespace.gateway propagation
|
||||
in namespace.yaml. This mirrors the existing _namespace.ingress
|
||||
inheritance: a child tenant publishes through its parent's
|
||||
publishing layer unless it explicitly asks for its own.
|
||||
|
||||
Rules:
|
||||
1. If tenant.spec.gateway is set explicitly (true|false) → use it.
|
||||
Operator's explicit choice always wins, regardless of platform
|
||||
state — the helmrelease for `gateway` will fail upstream if the
|
||||
platform doesn't have gateway.enabled, but that's a user-visible
|
||||
error which is the right outcome for explicit opt-in.
|
||||
2. Otherwise (gateway unset) → consult both:
|
||||
a. `_cluster.gateway-enabled` — the platform-level flag. If the
|
||||
platform has not opted in to Gateway API, no auto-default
|
||||
fires. Auto-on a Gateway when the cluster doesn't ship the
|
||||
gateway-application chart only produces broken HelmReleases.
|
||||
b. `tenant.spec.host` — when the platform IS Gateway-enabled,
|
||||
a tenant with derived apex (`host` empty, computed as
|
||||
`<name>.<parent apex>`) gets auto-on; a tenant with a
|
||||
custom non-derived apex requires explicit opt-in, since
|
||||
a custom apex is a deliberate operator choice and they may
|
||||
not have intended public exposure.
|
||||
1. tenant.spec.gateway set explicitly (true|false) → use it.
|
||||
Explicit choice always wins. A tenant that asks for its own
|
||||
Gateway gets its own Service / LB IP / Certificate.
|
||||
2. Otherwise → "false". The tenant inherits the parent's Gateway
|
||||
through _namespace.gateway. Derived-apex children (`host`
|
||||
empty) flow naturally: their predefined hostnames
|
||||
`*.<name>.<parent-apex>` are routed by the ancestor's
|
||||
controller which extends listener / SAN coverage as children
|
||||
attach. If no ancestor owns a Gateway, _namespace.gateway
|
||||
stays empty and apps fall back to Ingress (legacy path).
|
||||
|
||||
Escape hatch: an operator who wants a derived-apex tenant without a
|
||||
Gateway sets `gateway: false` explicitly on the tenant CR.
|
||||
Custom-apex tenants (operator sets tenant.spec.host to something
|
||||
not derived from the parent apex, e.g. `customer1.io`) must opt in
|
||||
explicitly via `gateway: true` if they want public exposure. The
|
||||
ancestor's TLS cert does not cover their apex and Let's Encrypt
|
||||
wildcards are single-level, so silent inheritance would either
|
||||
leak through the wrong cert or fail to terminate TLS. Forcing the
|
||||
explicit flag keeps "I want my apex routable" a deliberate choice.
|
||||
|
||||
Implementation note: values.yaml leaves `gateway` absent (no key) so
|
||||
Helm reads it as missing → `kindIs "invalid"`. cozyvalues-gen's
|
||||
Implementation note: values.yaml leaves `gateway` absent (no key)
|
||||
so Helm reads it as missing → `kindIs "invalid"`. cozyvalues-gen's
|
||||
`[gateway]` (optional) marker syntax does not allow nullable schema
|
||||
typing, so the "key absent" form is what distinguishes "unset" from
|
||||
explicit `false`.
|
||||
|
||||
The helper renders the literal string "true" or "false" — callers
|
||||
must compare with `eq ... "true"` rather than rely on bool coercion.
|
||||
typing, so the "key absent" form is what distinguishes "unset"
|
||||
from explicit `false`.
|
||||
*/}}
|
||||
{{- define "tenant.gatewayEffective" -}}
|
||||
{{- if kindIs "invalid" .Values.gateway -}}
|
||||
{{- $platformOn := eq ((index .Values "_cluster" "gateway-enabled") | default "false") "true" -}}
|
||||
{{- $derivedApex := eq (.Values.host | default "") "" -}}
|
||||
{{- if and $platformOn $derivedApex -}}true{{- else -}}false{{- end -}}
|
||||
false
|
||||
{{- else -}}
|
||||
{{- if .Values.gateway -}}true{{- else -}}false{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -30,19 +30,25 @@
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Gateway reference is NOT inherited from parent. A child tenant
|
||||
namespace is not on the parent Gateway's allowedRoutes whitelist,
|
||||
and the parent Gateway's wildcard cert only covers <apex> +
|
||||
*.<apex> (one level) while a child apex would be two levels deep —
|
||||
inheritance would break both admission and TLS. Each tenant that
|
||||
wants Gateway-attached apps either sets tenant.spec.gateway=true
|
||||
explicitly or hits the auto-default for derived-apex tenants
|
||||
(tenant.gatewayEffective). The same helper is consulted by
|
||||
gateway.yaml (HelmRelease render) so the per-tenant Gateway, the
|
||||
namespace label, and _namespace.gateway propagation to apps stay
|
||||
in lockstep.
|
||||
Gateway reference inherits from the parent, same shape as ingress
|
||||
above: $gateway is the name of the nearest ancestor (inclusive)
|
||||
that owns a Gateway. Apps in this tenant render HTTPRoutes whose
|
||||
parentRefs.namespace points at that owner namespace, and the
|
||||
owner's Gateway lists this namespace in allowedRoutes via the
|
||||
matching namespace.cozystack.io/gateway label.
|
||||
|
||||
Override fires only when the tenant itself owns a Gateway —
|
||||
tenant.gatewayEffective returns "true" for explicit
|
||||
tenant.spec.gateway=true and for tenants with a custom non-derived
|
||||
host (their apex isn't covered by any ancestor's cert / Issuer).
|
||||
In those cases the controller renders a separate Gateway in this
|
||||
tenant's namespace, with its own LB Service / IP / Certificate.
|
||||
|
||||
Cross-namespace attach is gated by ReferenceGrants rendered by the
|
||||
TenantGateway controller in the owner namespace, one per child
|
||||
that inherits.
|
||||
*/}}
|
||||
{{- $gateway := "" }}
|
||||
{{- $gateway := $parentNamespace.gateway | default "" }}
|
||||
{{- if eq (include "tenant.gatewayEffective" .) "true" }}
|
||||
{{- $gateway = $tenantName }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
suite: tenant gateway default — auto-on for derived apex, opt-in for custom
|
||||
suite: tenant gateway default — inherit from parent, opt-in for own Gateway
|
||||
templates:
|
||||
- templates/gateway.yaml
|
||||
|
||||
# Cozystack targets low-skill operators. A tenant with a derived apex
|
||||
# (`<name>.<parent>`, computed by the chart when tenant.spec.host is
|
||||
# empty) implicitly expects routability under that apex — forcing
|
||||
# operators to also set `tenant.spec.gateway: true` is a UX bug.
|
||||
#
|
||||
# The auto-default fires only when the platform itself is gateway-
|
||||
# enabled (`_cluster.gateway-enabled=true`). Auto-on a Gateway when
|
||||
# the cluster doesn't ship the gateway-application chart only produces
|
||||
# broken HelmReleases — explicit opt-in still works in that case so
|
||||
# operators can preview the wiring on a non-Gateway cluster.
|
||||
# Cozystack targets low-skill operators. A child tenant under a Gateway-
|
||||
# enabled parent must publish through the parent's Gateway by default —
|
||||
# forcing every child to set `tenant.spec.gateway: true` would break
|
||||
# parity with how `_namespace.ingress` already inherits, and would burn
|
||||
# one LB IP per child tenant for no isolation benefit.
|
||||
#
|
||||
# Rules under test (see _helpers.tpl::tenant.gatewayEffective):
|
||||
# - platform off + any host + gateway unset → OFF (auto-default
|
||||
# waits for the
|
||||
# platform flag)
|
||||
# - platform on + host="" + gateway unset → ON (auto-default)
|
||||
# - platform on + host=custom + gateway unset → OFF (custom apex
|
||||
# needs explicit
|
||||
# opt-in)
|
||||
# - gateway=true explicit → ON (regardless)
|
||||
# - gateway=false explicit → OFF (escape hatch)
|
||||
# - gateway unset → OFF own (inherit from parent)
|
||||
# - gateway=true explicit → ON own (LB IP / cert burned)
|
||||
# - gateway=false explicit → OFF own (parked, no inherit)
|
||||
#
|
||||
# The inheritance propagation itself happens in namespace.yaml:
|
||||
# $gateway := $parentNamespace.gateway | default ""
|
||||
# overridden to $tenantName when tenant.gatewayEffective is "true".
|
||||
# That logic is exercised by the cozystack-basics secret test for the
|
||||
# root tenant (cozystack-values-secret_test.yaml) plus by the
|
||||
# controller-level reconciler tests for the cross-namespace attach
|
||||
# flow — helm-unittest cannot mock the `lookup` call namespace.yaml
|
||||
# uses for ownerReferences, so the chart-level pin here covers only
|
||||
# the HelmRelease render side.
|
||||
|
||||
release:
|
||||
name: tenant-alice
|
||||
@@ -35,9 +34,35 @@ set:
|
||||
gateway-enabled: "true"
|
||||
|
||||
tests:
|
||||
- it: derived apex + gateway unset → renders HelmRelease (auto-on)
|
||||
- it: gateway unset → inherit, no own HelmRelease
|
||||
# The default for a derived-apex child. _namespace.gateway will
|
||||
# propagate the parent's owner-name through namespace.yaml; the
|
||||
# tenant chart does NOT render its own gateway HelmRelease.
|
||||
set:
|
||||
host: ""
|
||||
asserts:
|
||||
- template: templates/gateway.yaml
|
||||
hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: gateway unset on custom-apex tenant → inherit, no own HelmRelease
|
||||
# Custom apex without explicit `gateway: true` falls through to
|
||||
# inheritance — same rule. The HTTPRoutes for this tenant's apps
|
||||
# will attach to the parent Gateway but fail TLS (parent cert
|
||||
# doesn't cover customer1.io). That's the operator's responsibility
|
||||
# to notice; we don't auto-Gateway because they might just want
|
||||
# the namespace for isolation with no public exposure.
|
||||
set:
|
||||
host: customer1.io
|
||||
asserts:
|
||||
- template: templates/gateway.yaml
|
||||
hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: gateway=true explicit on derived apex → own HelmRelease
|
||||
set:
|
||||
gateway: true
|
||||
host: ""
|
||||
asserts:
|
||||
- template: templates/gateway.yaml
|
||||
equal:
|
||||
@@ -48,26 +73,10 @@ tests:
|
||||
path: metadata.name
|
||||
value: gateway
|
||||
|
||||
- it: custom apex + gateway unset → no HelmRelease (explicit opt-in)
|
||||
set:
|
||||
host: customer1.io
|
||||
asserts:
|
||||
- template: templates/gateway.yaml
|
||||
hasDocuments:
|
||||
count: 0
|
||||
|
||||
|
||||
- it: gateway=true explicit on derived apex → renders (passthrough)
|
||||
set:
|
||||
gateway: true
|
||||
host: ""
|
||||
asserts:
|
||||
- template: templates/gateway.yaml
|
||||
equal:
|
||||
path: kind
|
||||
value: HelmRelease
|
||||
|
||||
- it: gateway=true explicit on custom apex → renders (overrides default)
|
||||
- it: gateway=true explicit on custom apex → own HelmRelease
|
||||
# The intended path for an operator who wants a custom apex
|
||||
# publicly reachable: set host + gateway=true. The tenant gets
|
||||
# its own Gateway / LB IP / Certificate / Issuer.
|
||||
set:
|
||||
gateway: true
|
||||
host: customer1.io
|
||||
@@ -77,10 +86,12 @@ tests:
|
||||
path: kind
|
||||
value: HelmRelease
|
||||
|
||||
- it: gateway=false explicit on derived apex → no HelmRelease (escape hatch)
|
||||
# The reason this matters: an operator who wants a derived-apex
|
||||
# tenant for namespace-isolation purposes only (no public exposure)
|
||||
# must be able to opt out without inventing a custom apex.
|
||||
- it: gateway=false explicit → no own HelmRelease (parked)
|
||||
# The escape hatch. Operator opts out of any Gateway publishing,
|
||||
# even inheritance: namespace.yaml writes _namespace.gateway as
|
||||
# empty string (the cozystack-basics-side label pins this in
|
||||
# cozystack-values-secret_test.yaml), and the parent Gateway's
|
||||
# allowedRoutes selector therefore does not match this namespace.
|
||||
set:
|
||||
gateway: false
|
||||
host: ""
|
||||
@@ -89,74 +100,11 @@ tests:
|
||||
hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: gateway=false explicit on custom apex → no HelmRelease (passthrough)
|
||||
set:
|
||||
gateway: false
|
||||
host: customer1.io
|
||||
asserts:
|
||||
- template: templates/gateway.yaml
|
||||
hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: tenant-root with gateway unset and host set → no auto-default
|
||||
# tenant-root always has tenant.spec.host set explicitly (it's the
|
||||
# platform apex). This must NOT auto-Gateway tenant-root just
|
||||
# because the chart receives the platform apex via `host`.
|
||||
# Operators who want gateway on tenant-root set it explicitly.
|
||||
release:
|
||||
name: tenant-root
|
||||
namespace: tenant-root
|
||||
set:
|
||||
host: example.org
|
||||
asserts:
|
||||
- template: templates/gateway.yaml
|
||||
hasDocuments:
|
||||
count: 0
|
||||
|
||||
# The platform-off cases below pin a critical regression-guard: a
|
||||
# tenant on a cluster where `gateway.enabled=false` at the platform
|
||||
# level must NOT silently auto-Gateway. The gateway-application
|
||||
# chart isn't installed on such clusters, so an auto-on would
|
||||
# produce a broken HelmRelease. The e2e install bats lives in this
|
||||
# mode (gateway not enabled in the bundle), and a regression here
|
||||
# broke `Create tenant with isolated mode enabled` until the
|
||||
# platform-flag gate landed.
|
||||
|
||||
- it: platform off + derived apex + gateway unset → no HelmRelease
|
||||
set:
|
||||
host: ""
|
||||
_cluster:
|
||||
expose-ingress: tenant-root
|
||||
expose-external-ips: "192.0.2.10"
|
||||
gateway-enabled: "false"
|
||||
asserts:
|
||||
- template: templates/gateway.yaml
|
||||
hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: platform unset + derived apex + gateway unset → no HelmRelease
|
||||
# gateway-enabled empty string — what cozystack-values renders
|
||||
# when an operator hasn't flipped gateway.enabled in the platform
|
||||
# chart. The helper treats anything other than the literal
|
||||
# "true" as off. This is what an upgraded existing cluster looks
|
||||
# like before the operator opts in.
|
||||
set:
|
||||
host: ""
|
||||
_cluster:
|
||||
expose-ingress: tenant-root
|
||||
expose-external-ips: "192.0.2.10"
|
||||
gateway-enabled: ""
|
||||
asserts:
|
||||
- template: templates/gateway.yaml
|
||||
hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: platform off + gateway=true explicit → renders (operator override wins)
|
||||
# Explicit operator opt-in always wins, even when the platform
|
||||
# hasn't flipped gateway.enabled. The gateway HelmRelease will
|
||||
# fail at runtime (no chart), but that's a user-visible error
|
||||
# which is the right outcome — the operator deliberately asked
|
||||
# for it.
|
||||
- it: gateway=true on platform-off cluster → own HelmRelease (operator override)
|
||||
# The HelmRelease will fail at runtime (no gateway-application
|
||||
# chart on the cluster) — that's a user-visible error, which is
|
||||
# the right outcome for explicit opt-in against a not-yet-
|
||||
# provisioned platform.
|
||||
set:
|
||||
gateway: true
|
||||
host: ""
|
||||
|
||||
Reference in new issue
Block a user