Skip to content

ADR-0203: Namespaced multi-tenancy and operator watch scope

Namespaced-by-default tenancy; KollectScope hard-degrades a target/inventory that violates it.

Theme: 02 · API & tenancy · Status: Current

Context

flowchart LR
  subgraph A["team-a namespace"]
    ScopeA["KollectScope"] -. gates .-> PipelineA["Inventory pipeline"]
  end
  subgraph B["team-b namespace"]
    ScopeB["KollectScope"] -. gates .-> PipelineB["Inventory pipeline"]
  end
  PipelineA --> SinkA["Allowed sink A"]
  PipelineB --> SinkB["Allowed sink B"]
  Denied["Disallowed GVK / namespace / sink"] -. blocked .-> ScopeA

Platform teams need Kollect to run safely alongside many tenant teams on one cluster. Prior art (ADR-0102) compares:

  • external-secrets — one cluster-scoped controller reconciling both ClusterSecretStore and namespaced SecretStore, plus optional per-namespace controller installs via Helm controller.watchNamespaces / scopedNamespace.
  • Argo CDAppProject tenancy boundary with a single cluster-scoped controller.

Kollect already ships namespaced KollectTarget, KollectInventory, and KollectScope (ADR-0201). The open question was whether tenancy enforcement and operator deployment scope should wait until Phase 3.

Decision

Namespaced multi-tenancy is Phase 1 priority (ASAP), not Phase 3.

Support both deployment models. Golden path (default): platform cluster-wide operator with per-tenant namespaced KollectScope. Team-owned operator installs are supported but lowest priority for documentation and chart defaults.

Model When Mechanism
Cluster-scoped manager (golden path) Platform team operates one shared operator Watches all namespaces; KollectScope per tenant namespace governs GVKs, workload namespaces, and sink refs
Per-team manager Delegated / team-owned installs Helm with watchNamespaces: [team-a] and tenantMode: true — namespaced Role RBAC; namespaced Profile/Sink/Target/Inventory in team namespace (ADR-0201)

Operator watch scope

  • Documented default for new installs (golden path): cluster-scoped manager (empty watchNamespaces) with per-tenant KollectScope objects.
  • Platform option: empty watchNamespaces → watches all namespaces (cluster-scoped operator).
  • Team-scoped (supported, lowest doc priority): non-empty watchNamespaces + tenantMode: truecache.Options{DefaultNamespaces} restricts informers and reconcilers to those namespaces only (controller-runtime cache options).

RBAC expectations

  • CRD install requires cluster-level permissions (standard for any operator).
  • Reconciler runtime for team-owned installs must work with namespace-scoped Role RBAC only when tenantMode: true — no cluster-wide list/watch beyond what cluster owners accept for CRD bootstrap.
  • Platform golden-path installs use ClusterRole as today.

Overlapping operator installs

Multiple independent operator deployments may watch the same GVK/namespace data intentionally. Kollect does not enforce namespace ownership or reject overlapping watch scopes via webhook. Operational policy and optional sink dedupe (ADR-0305) are the backstop when duplicate collection is undesirable — not admission guardrails. See the deployment topology matrix for path comparison and the multi-operator dedupe runbook.

Helm values:

# kollect-doc: ignore Helm values, not a kollect CR
watchNamespaces: []          # empty = all namespaces
tenantMode: false            # true → Role instead of ClusterRole for the manager SA

Manager flag: --watch-namespaces=team-a,team-b (comma-separated).

KollectScope (namespaced, static)

  • Scope: namespaced (ADR-0201); one object per tenant namespace.
  • Validation: validating webhook rejects invalid GVK entries, duplicate sinkRefs (superseded — see ADR-0414), and blank allowlist entries at admission (ADR-0202). The single allowlist named here became one allowlist per sink family.
  • Enforcement (Phase 1): both validating webhook and reconciler-time checks — hard degrade (no collection / no export) when a KollectTarget or KollectInventory violates scope. Set Degraded=True with reason ScopeGVKDenied, ScopeNamespaceDenied, or ScopeSinkDenied; emit Warning event. Do not soft-warn and continue reconciling forbidden config.

KollectClusterScope remains reserved for platform-wide policy when namespaced scope is insufficient (ADR-0201). Frozen/minimal — no new behavioral knobs without a concrete platform blocker.

KollectClusterInventory federation

Platform rollups use KollectClusterInventory to compose namespace snapshots/shards — explicit federation, not implicit whole-cluster capture:

  • spec.namespaces (optional): explicit namespace names when set; empty or absent carries documented semantics (not a required field)
  • Optional namespace selectors for additive discovery when enabled
  • Rollup export built from shard references; status should eventually expose per-namespace shard metadata (see ROADMAP.md)

Enforcement example (GVK denied)

KollectScope in team-a allows only apps/v1 Deployment. A team applies a KollectTarget whose profileRef resolves to a Profile targeting Flux HelmRelease:

# kollect-doc: superseded sinkRefs was split per family by ADR-0414 — the shipped
# field is databaseSinkRefs; this block is the decision as taken in ADR-0203.
# team-a/kollectscope.yaml — allowlist
apiVersion: kollect.dev/v1alpha1
kind: KollectScope
metadata:
  name: team-a-scope
  namespace: team-a
spec:
  allowedGVKs:
    - group: apps
      version: v1
      kind: Deployment
  allowedNamespaces: [team-a]
  sinkRefs: [team-a-postgres]   # superseded by ADR-0414 — now databaseSinkRefs
---
# team-a/kollecttarget.yaml — violates allowedGVKs
apiVersion: kollect.dev/v1alpha1
kind: KollectTarget
metadata:
  name: helm-releases
  namespace: team-a
spec:
  profileRef: helm-release-summary   # Profile targets helm.toolkit.fluxcd.io/HelmRelease

Outcome: Target reconciler does not register the informer watch. Status:

# kollect-doc: fragment KollectTarget
status:
  conditions:
    - type: Degraded
      status: "True"
      reason: ScopeGVKDenied
      message: 'profile GVK "helm.toolkit.fluxcd.io/v2/HelmRelease" not in scope allowedGVKs'

Inventory sink denied — same pattern when spec.sinkRefs (superseded by ADR-0414) lists a sink the scope does not allow (ScopeSinkDenied). The shipped fields are the per-family snapshotSinkRefs / databaseSinkRefs / eventSinkRefs. Envtest: internal/controller/kollecttarget_scope_test.go.

Consequences

Positive

  • Teams can adopt Kollect early with explicit tenancy boundaries.
  • Platform can offer either shared or per-team operator installs without forking the binary.
  • Overlapping multi-operator installs are allowed — no false sense of exclusive namespace ownership.
  • Aligns with ESO and Flux Helm patterns operators already understand.

Negative

  • Two deployment models require chart and doc clarity to avoid misconfigured RBAC.
  • Overlapping collection when multiple operators watch the same scope is possible — operators must plan sink dedupe or accept duplicate rows (ADR-0305, topology matrix).
  • Resolved (ADR-0204): KollectProfile moves to namespaced scope so tenantMode installs do not need cluster profile RBAC; KollectClusterProfile reserved for platform-shared schemas.
  • Reconciler enforcement adds controller complexity and must stay consistent with webhook rules.

Open questions

  • RESOLVED: Family sink CRDs ship with namespaced + cluster variants; KollectCluster*Sink kinds are retained long-term — not removed pre-v1 (ADR-0414).