Skip to content

test(e2e): validate operator deployability with an in-cluster federation e2e - #179

Open
scotwells wants to merge 23 commits into
mainfrom
feat/e2e-in-cluster
Open

test(e2e): validate operator deployability with an in-cluster federation e2e#179
scotwells wants to merge 23 commits into
mainfrom
feat/e2e-in-cluster

Conversation

@scotwells

@scotwells scotwells commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Why

Our e2e until now exercised the operators as an in-process test binary, which never touched the way they actually ship: as deployed Instances authenticating to Karmada through real RBAC, wired by the real kustomize overlays. This PR makes e2e validate deployability — RBAC, manifests, image, service-account identity, leader election — not just control logic. That difference is not academic: on its first real runs this harness caught three genuine gaps invisible to in-process tests (two production RBAC grants and a missing cell hub credential — see Verification status). That class of gap is exactly what this exists to catch.

What's in the PR

  • Harness (Taskfile.yaml, hack/e2e/): stands up three Kind clusters — a management/control-plane cluster hosting Karmada plus the dfw and ord POP cells — installs Karmada, registers the cells over the Docker-bridge network, and installs the compute/NSO/quota CRDs. Idempotent and re-runnable.
  • In-cluster deploy layer (test/e2e/deploy/{management,cell}): references the real production overlays (config/overlays/{management-plane,cell}) and layers only the deviations a local/CI Kind environment forces, each annotated inline with why (discovery milo→single, webhook + cert-manager CSI not present, cell quota credential absent, dev image). Federation identity is a Karmada-native ServiceAccount token bound to the real compute-manager hub ClusterRole, so the operators authenticate as a non-admin identity and any missing grant surfaces as a forbidden error. The cell operators also get a hub credential mirroring how infra wires real cells (see Verification status), so the Instance write-back path is exercised.
  • Chainsaw suites (9): federation to both cells (dfw + ord), propagation-policy lifecycle, workload-deployment federation, referenced-data mounts + delete-cascade, instance projection, instance write-back, deletion cascade.
  • Stability tuning for constrained hosts (Kind kubeadm patches + Karmada install): etcd unsafe-no-fsync and leader-election disabled on the single-instance infrastructure control-plane components (base Kind + Karmada), relaxed Karmada API server probes, retried Karmada-NodePort applies, serial suite execution, and widened Chainsaw timeouts. These are throwaway-cluster/CI tuning only, never production, and the compute operators keep leader election on so it stays exercised.
  • CI workflow (.github/workflows/test-e2e.yml): runs the whole harness on GitHub Actions in phased provision/deploy/test steps, and on failure collects per-cluster operator logs, Karmada component logs, events, and a full Kind log export as an artifact.

Verification status

All 9 Chainsaw suites green on CI (run 29066748470): provision → in-cluster deploy of the real overlays to all three clusters → the full 9-suite federation run all pass. Management + both cell operators come up Ready with zero RBAC denials; the federation flow is validated end to end (WorkloadDeployment → Karmada hub companion + city-dfw/city-ord PropagationPolicy → propagation to the POP cell → Instance created → Instance written back to the hub → projected).

Three real gaps this harness caught and fixed along the way — the point of #149, all invisible to an in-process test:

  • RBAC: networks — the management WorkloadReconciler watches networking.datumapis.com/networks, but the compute ClusterRole granted only locations/networkcontexts/subnets, so the Network watch was denied and the manager could not reconcile WorkloadDeployments cleanly. Added networks get/list/watch.
  • RBAC: namespaces watch — the role granted namespaces get,list but not watch, so the federator's namespace→UID informer could never establish its watch; its cache refreshed only via backed-off re-LISTs, so namespaces created after startup stayed invisible for minutes and their WorkloadDeployments never federated. Added watch. (Post-fix diagnostics sweep: zero compute-manager forbidden across all four planes.)
  • Cell hub credential — the e2e cells ran with FEDERATION_KUBECONFIG empty, which silently no-ops the cell InstanceReconciler's write-back, so cell-created Instances never reached the hub or the management InstanceProjector (Karmada never propagates Instances, so status aggregation can't surface them — write-back is the sole path). Verified against datum-cloud/infra: every cell that runs compute DOES get a hub credential via apps/compute-system/edge/manager-patch.yaml; the overlay's "empty is production-exact" comment was wrong and is corrected. The harness now mints a cell Karmada credential mirroring infra, exercising the full cell→hub→projector path.

Follow-up (not blocking)

Closes #149.

scotwells and others added 23 commits July 9, 2026 16:49
Restore the multi-cluster e2e building blocks from the archived local-run
branch so the federation layer can be exercised end to end again:

- hack/e2e/kind-control-plane.yaml exposes the Karmada API server on a host
  NodePort for developer kubectl and Chainsaw access.
- hack/e2e/make-internal-kubeconfig.sh and patch-cluster-secret.sh rewrite
  member-cluster kubeconfigs (and the Cluster apiEndpoint) to the Docker-bridge
  IP so the in-Docker Karmada controller can reach the POP cell API servers.
- test/e2e/chainsaw-config.yaml declares the downstream/pop-dfw/pop-ord clusters
  the suites reference.

These are environment scaffolding only. Unlike the retired harness, the
operators are deployed in-cluster via the real kustomize overlays rather than
run on the host; that path lands in the following commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the retired host-run operator harness with an in-cluster deploy path so
e2e exercises the same kustomize overlays, RBAC, and pod wiring that run in
production. The Taskfile now builds a dev image, side-loads it into every Kind
cluster, wires the management operator's access to the Karmada hub, and applies
the real overlays to the control-plane and both POP cells, waiting on rollout.

Deploy layer (test/e2e/deploy/{management,cell}) references the production
overlays verbatim and layers only the deviations a local Kind environment
forces, each annotated inline so drift from production stays visible:

- management: discovery.mode milo -> single (no Milo control plane in Kind);
  admission webhook disabled and its cert-manager CSI serving-cert volume
  removed (no cert-manager/CSI driver in Kind; the suites create
  WorkloadDeployments, never Workloads, so the Workload webhook is never
  exercised); dev image with IfNotPresent pull.
- cell: discovery.quotaKubeconfigPath dropped so the operator takes its
  documented quota opt-out branch instead of crash-looping on the absent edge
  credential; dev image with IfNotPresent pull. FEDERATION_KUBECONFIG stays
  empty exactly as in production, so cell controllers run against the local
  cluster where Karmada propagates WorkloadDeployments.

Federation access for the management operator mints a Karmada-native
ServiceAccount token bound to the real compute-manager hub ClusterRole
(config/base/downstream-rbac), so the operator authenticates as a non-admin
identity and any missing grant surfaces as a forbidden error rather than being
masked by cluster-admin.

The Karmada bring-up is hardened for busy hosts: a 10m Helm wait that resumes a
timed-out install, a wait for the cluster.karmada.io aggregated API before
joining members, retries around karmadactl join while the member token Secret
populates, and leader election disabled on the single-replica Karmada
controllers to avoid a lease-renewal crashloop under etcd write-latency spikes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make e2e:karmada:install robust to a Karmada control plane whose deployments
were scaled to zero (e.g. quiesced between runs on a busy host, or left partial
by an earlier failed install). Before waiting for the deployments to become
Available, scale any that sit at zero replicas back to one, so an idempotent
re-run resumes the environment instead of blocking forever on a deployment that
has no desired replicas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restore the eight chainsaw e2e suites that exercise the compute
federation and referenced-data delivery paths, bringing them back onto
the in-cluster harness branch:

- full-federation, workload-deployment-federation, instance-projection,
  instance-writeback, propagation-policy-lifecycle, deletion-cascade
  (from split/federation-e2e)
- referenced-data-mounts, referenced-data-delete-cascade
  (from archive/e2e-local-deferred)

Restored verbatim; adaptation to the in-cluster deploy path follows in a
separate commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Delete seven loose assert-*.yaml files that no chainsaw-test.yaml
  referenced; two still asserted the pre-merge PropagationPolicy name
  workload-deployments-dfw (the federator now emits city-<cityCode>).
  The suites assert these resources inline, so nothing is lost.

- Enable featureFlags.enableReferencedDataGate on the cell deploy layer.
  Its only consumer is the cell WorkloadDeploymentReconciler, which stamps
  the ReferencedData scheduling gate; without it the referenced-data suites
  observe an Instance that is never gated. The management layer sets the same
  flag for parity (inert there — that overlay runs management controllers only).

- Document the upstream-cluster-name label value (cluster-single). Both
  operators run discovery.mode=single with the default cluster name single;
  the management federator stamps EncodeClusterName(single)=cluster-single on
  the hub namespace and the write-back/projection copy it, so the existing
  assertions hold. Corrected the instance-projection and instance-writeback
  descriptions, which named a stale provider symbol and an incorrect
  cluster-<cell> example.

- Refresh referenced-data prose (suite descriptions + README) that referred
  to the retired host-run operator-start tasks; the feature flag now ships
  through the deploy layer under task e2e:up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both POP cells now run operators, but every restored suite exercises only
the dfw cell. Add full-federation-ord: an ord-placed WorkloadDeployment
must produce PropagationPolicy city-ord, propagate to pop-ord, and create
its Instance there, then write back and project like the dfw path. This is
the coverage that catches city-code routing regressions and a
mis-registered second cell — the dfw suites would stay green through both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Server-side apply already validates on the API server, so the client-side
OpenAPI schema download is redundant — and on a resource-constrained API server
that download times out ("failed to download openapi"), failing the apply. Pass
--validate=false (and --server-side where it was missing) on the CRD, hub-RBAC,
federation-component, and operator applies so bring-up survives a slow API
server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bake the control-plane stability fixes into cluster creation so bring-up no
longer needs manual runtime patching on a resource-constrained host, where a
base Kind cluster co-located with the full Karmada control plane otherwise
crash-loops on etcd write-latency spikes.

- kind configs (control-plane + new kind-pop for the cells) disable etcd fsync
  and turn off leader election on the single-instance kube-scheduler and
  kube-controller-manager via kubeadmConfigPatches. kind renders its kubeadm
  config as v1beta3, so extraArgs use the map form.
- the Karmada install disables fsync on the Karmada control plane's own etcd
  StatefulSet, alongside the leader-election disable already applied to its
  controllers.

Data durability does not matter for these throwaway e2e clusters, and fsync
contention between the co-located etcds was the root cause of the latency
spikes. Measured effect: hub writes drop from multi-second timeouts to ~170ms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The management WorkloadReconciler watches networking.datumapis.com Networks, but
the compute ClusterRole only granted locations, networkcontexts, and subnets —
so the Network informer was denied ("networks ... is forbidden") and the manager
never reconciled WorkloadDeployments cleanly. Add networks to the read-only
networking rule. Surfaced by the in-cluster federation e2e: with this grant the
manager runs with zero RBAC denials and federates WorkloadDeployments to the
hub.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… policy)

Two corrections to the e2e management overlay found by deploying it for real:

- The webhook-server-tls volume is now backed by an emptyDir rather than
  deleting the volume + mount. A strategic-merge "$patch: delete" on a single
  volumeMount dropped the container's entire volumeMounts list, which unmounted
  the federation kubeconfig — the manager then crash-looped on a missing
  downstream-kubeconfig.yaml. Overriding only the volume source keeps every
  mount intact; the webhook server is disabled, so an empty cert dir is fine.
- Drop the ResourceMetricsPolicy: its CRD (resourcemetrics.miloapis.com) is
  owned by a separate operator absent from Kind, so the apply failed with "no
  matches for kind ResourceMetricsPolicy". It is orthogonal to the federation
  path under test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…der load

The chainsaw suites put sustained load on the co-hosted Karmada control plane.
Under that load a transient etcd/DNS blip made the API server's default health
probes fail and the kubelet SIGKILLed it (exit 137), cascading into a crashloop
that failed every suite. Relax the karmada-apiserver liveness/readiness probes
so it rides out blips instead of being killed, and widen the chainsaw timeouts
(assert 120s, delete 120s) so Karmada propagation and finalizer-driven cascade
deletes have room on the constrained node. Both are constrained-host/CI tuning,
not production changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the placeholder E2E workflow (a single throwaway Kind cluster
running a commented-out make target) with one that stands up the full
harness the Taskfile builds and runs the Chainsaw suites against it:
three Kind clusters (management/control-plane hosting Karmada, plus the
dfw and ord POP cells), the real production kustomize overlays, and the
real hub RBAC. This validates the operators as deployed pods
authenticating to Karmada as a non-admin identity rather than as an
in-process test binary.

The job splits provisioning, deploy, and test into separate steps so a
failure lands on the phase that broke, and always collects per-cluster
operator logs, Karmada component logs, events, and a full kind log export
as an artifact when any step fails.

Runs on the free ubuntu-latest runner: the harness is engineered for a
constrained host (single-replica Karmada, leader election disabled,
generous component waits), so a larger paid runner is not needed unless
real runs show resource pressure. Triggers only on pull_request and
pushes to main rather than every branch push, given the job's cost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Running all suites concurrently floods the single federation manager: many
WorkloadDeployments federate at once and individual suites' downstream
assertions time out with "resource not found" even though the federation path
itself works (the first suite to reach the manager passes). Pin --parallel 1 so
the federation path stays uncontended; a faster local run can still override
with `task e2e:test -- --parallel N`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first CI run failed workload-deployment-federation on the city-dfw
PropagationPolicy assertion with "lengths of slices don't match": the
federator emits three resourceSelectors (WorkloadDeployment plus the
always-on ConfigMap and Secret referenced-data selectors that co-propagate
companions), but the suite asserted only the WorkloadDeployment. Chainsaw
matches list length exactly, so assert all three. Apply the same correction
to full-federation-ord's city-ord assertion, which carried the same stale
single-selector expectation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ired labels

Both suites synthesize objects that the real cell path would stamp, and the
first CI run showed the operators correctly rejecting the incomplete fixtures:

- instance-projection: the projector logged "missing the
  workload-deployment-name label; cannot resolve its WorkloadDeployment" and
  never created the projection. The injected hub Instance now carries
  workload-deployment-name, and upstream-namespace points at the project
  namespace (where the WD lives and the projection lands), not the hub
  ns-<uid>. Also restores the owner-reference assertion the projector sets.

- instance-writeback: writeBackToUpstream derives identity from the hub
  namespace labels and requires the full linking-label set on the Instance
  (instance_controller.go). Seed both so the write-back fires instead of
  erroring on missing identity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
referenced.CompanionName returns the source object's own name when it is a
valid DNS subdomain within the length budget (the kind argument is ignored),
so the companions materialize as app-config and app-secret, not
configmap.app-config and secret.app-secret. The first CI run confirmed this:
the referenced-data-delete-cascade suite (source names gc-test-config /
gc-test-secret) found its un-prefixed companions, while referenced-data-mounts
timed out looking for the prefixed names. Correct the assertions and the
naming-convention note in the README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Right after the Karmada install, applies made over the API server's host
NodePort can hit a transient connection drop ("EOF" / API discovery failure)
while the control plane settles, which failed provisioning (karmada:configure)
on an otherwise-healthy environment. Retry the federation-component apply and
the hub-RBAC apply a few times so a momentary blip no longer breaks the run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…#144

Scenario 2 asserted a level-triggered hub-side companion GC reclaiming a
stranded companion, but that reconciler is not in this branch — it ships in
PR #144 (refdata-hub-gc), which was still unmerged when the branch was cut, so
the scenario could never pass. Keep the happy-path cascade (Scenario 1), which
exercises the delete path that IS implemented, and restore the backstop
scenario once #144 lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The serial diagnostic run showed two assertions that need more than 30s on a
constrained CI runner (the object does appear, just later):

- full-federation assert-instance-on-pop-dfw waits on the whole project WD →
  federate → Karmada propagate → cell reconcile → Instance chain.
- instance-projection wait-for-downstream-namespace waits on federation of a
  freshly-created namespace.

Raise both to 120s. This is a timing allowance, not a masked stall.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
full-federation-ord, instance-writeback, and referenced-data-delete-cascade
asserted federated/companion resources with 30-60s per-step timeouts, too tight
for the federate->propagate chain on a constrained CI runner (the same class of
delay already given 120s in full-federation and instance-projection). Bump the
failing steps to 120s. Noted as provably-timing: the resources appear, just
after the old deadline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The federator resolves WorkloadDeployment source namespaces through an
informer, which needs watch in addition to get/list. With watch
forbidden, the cache only refreshes on periodic re-lists whose backoff
grows as the denied watch keeps retrying, so namespaces created after
manager startup stay invisible for minutes and their WorkloadDeployments
never federate.

Second production RBAC gap surfaced by running the operators in-cluster
under their real ServiceAccount (after the networks grant).
The federated Instance settles Ready=False, not Unknown: it is blocked before
scheduling completes (a scheduling gate, or no matching Location in the e2e
environment), which is a definite not-ready state rather than an indeterminate
one. The old Unknown expectation predates the scheduling-gate semantics. Assert
status only — the exact blocking reason is environment-dependent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The e2e cells ran with FEDERATION_KUBECONFIG empty, which disabled the cell
InstanceReconciler's write-back to the hub — so cell-created Instances never
reached the Karmada hub or the management InstanceProjector, failing
instance-writeback and the write-back/projection steps of full-federation and
full-federation-ord.

This was a harness bug, not production behavior. Verified against datum-cloud/
infra: every cell that runs compute gets a hub credential via
apps/compute-system/edge/manager-patch.yaml (FEDERATION_KUBECONFIG + a mounted
client-cert kubeconfig to the Karmada hub). The cell overlay ships the value
empty on purpose because infra, not the overlay, supplies it per cell.

Mirror that here: federation:setup mints the same compute-manager Karmada SA
token and publishes it on both cells as compute-cell-federation-kubeconfig,
reaching the kind hub over the control-plane node's docker-bridge IP + NodePort
(cell pods live in other kind clusters, so the in-cluster Service the management
pod uses is unreachable). federation_patch.yaml mounts it the way infra does.
The corrected overlay comment now states what production actually does. Only the
plain write-back client is built for cells, so this does not gate readiness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build an in-cluster e2e environment (deploy operators to cells)

1 participant