Black-box E2E testing framework for HyperFleet cluster lifecycle management. Tests hit the HyperFleet API, create ephemeral clusters, verify adapter execution and K8s resource creation, then clean up. Built with Go 1.26, Ginkgo v2, Gomega, and a hand-written generic HTTP client.
Test suites: e2e/cluster/, e2e/nodepool/, e2e/adapter/.
Run make check before declaring work done. It runs everything in order:
| Target | What it does |
|---|---|
make check |
generate → fmt-check → vet → lint → test (all-in-one) |
make build |
generate → compile binary to bin/hyperfleet-e2e |
make fmt |
Format code and imports (golangci-lint fmt) |
make test |
Unit tests only (./pkg/...) |
make lint |
golangci-lint (pinned in tools/go.mod, config: .golangci.yml) |
make generate |
No-op (reserved for future code generation) |
Pre-flight order: make check then make build.
| Topic | Location |
|---|---|
| Getting started | docs/getting-started.md |
| Architecture | docs/architecture.md |
| Test placement strategy | architecture repo — which layer a test belongs in (unit / integration / E2E) |
| Test writing guide | docs/development.md |
| Debugging | docs/debugging.md (prow job mapping, CI troubleshooting, log locations) |
| Local kind setup | docs/local-kind-setup.md |
| Runbook | docs/runbook.md (prow job schedules, label-to-CI mapping, test execution) |
| Contributing | CONTRIBUTING.md |
| Test case templates | test-design/templates/ |
| Test case documents | test-design/testcases/ |
| User journey maps | test-design/user-journeys/ |
| Config defaults | pkg/config/defaults.go |
| Config struct & validation | pkg/config/config.go |
| Pollers | pkg/helper/pollers.go |
| Custom matchers | pkg/helper/matchers.go |
| Helper core (New, TestDataPath, CleanupTestCluster) | pkg/helper/helper.go + pkg/helper/suite.go |
| Synchronous validators (HasResourceCondition, AdapterNameToConditionType) | pkg/helper/validation.go |
| Payload template vars | pkg/client/payload.go (templateVars struct) |
| Labels | pkg/labels/labels.go |
| Condition type constants | pkg/client/constants.go |
| Config file | configs/config.yaml |
| Identity config & transport | pkg/config/config.go (IdentityConfig), pkg/helper/suite.go (WithBearerToken wiring) |
- IMPORTANT: Test files use
.goextension, NOT_test.go. E2E tests are compiled into the binary, not run viago test. The single exception ise2e/e2e_suite_test.go— the ginkgo CLI entry point for parallel execution (make e2e-ginkgo); never add spec code there. - Location:
e2e/{suite}/descriptive-name.go(package matches directory name) - Test name format:
[Suite: component][category] Description(e.g.,[Suite: cluster][baseline] Cluster Resource Type Lifecycle). Known categories:baseline,update,delete,concurrent,negative,perf. - Test suites auto-register via blank import in
e2e/e2e.go
Every test MUST have exactly one severity label from pkg/labels:
labels.Tier0— critical path, blocks releaselabels.Tier1— important featureslabels.Tier2— edge cases, can defer
Optional: labels.Negative, labels.Performance, labels.Upgrade, labels.Disruptive, labels.Slow
IMPORTANT: Use pollers with custom matchers. Do NOT create WaitFor* wrapper functions that hide Eventually inside helpers.
// Wait for resource condition
Eventually(h.PollCluster(ctx, clusterID), h.Cfg.Timeouts.Cluster.Reconciled, h.Cfg.Polling.Interval).
Should(helper.HaveResourceCondition(client.ConditionTypeReconciled, client.ResourceConditionStatusTrue))
// Wait for all adapters at generation
Eventually(h.PollClusterAdapterStatuses(ctx, clusterID), h.Cfg.Timeouts.Adapter.Processing, h.Cfg.Polling.Interval).
Should(helper.HaveAllAdaptersAtGeneration(h.Cfg.Adapters.Cluster, expectedGen))
// Wait for hard-delete (404)
Eventually(h.PollClusterHTTPStatus(ctx, clusterID), timeout, h.Cfg.Polling.Interval).
Should(Equal(http.StatusNotFound))
// Wait for namespace cleanup
Eventually(h.PollNamespacesByPrefix(ctx, clusterID), timeout, h.Cfg.Polling.Interval).
Should(BeEmpty())Available pollers: PollCluster, PollNodePool, PollClusterAdapterStatuses, PollNodePoolAdapterStatuses, PollClusterHTTPStatus, PollNodePoolHTTPStatus, PollNamespacesByPrefix.
Available matchers: HaveResourceCondition, HaveAllAdaptersWithCondition, HaveAllAdaptersAtGeneration, HaveAuditIdentity.
For one-off complex assertions, use Eventually(func(g Gomega) { ... }).Should(Succeed()) with g.Expect() (not bare Expect()).
Every test MUST clean up resources with ginkgo.DeferCleanup inline right after resource creation.
Resolve payload paths via h.TestDataPath() — never hardcode testdata/ as a prefix (breaks when TESTDATA_DIR is overridden, e.g., in CI):
h.Client.CreateClusterFromPayload(ctx, h.TestDataPath("payloads/clusters/cluster-request.json"))Payloads in testdata/payloads/ support Go templates. Available variables (defined in pkg/client/payload.go):
.Random— 8-char random hex.UUID— full UUID v4.Timestamp— Unix seconds.TimestampMs— Unix milliseconds
Use ginkgo.By() for major steps. IMPORTANT: Never use ginkgo.By() inside Eventually closures.
Always use config values: h.Cfg.Timeouts.Cluster.Reconciled, h.Cfg.Timeouts.NodePool.Reconciled, h.Cfg.Timeouts.Adapter.Processing, h.Cfg.Polling.Interval. Never hardcode durations.
When server.jwt.enabled=true, the API authenticates requests via JWT. The E2E framework acquires a token from K8s using the TokenRequest API — no static tokens or secrets to manage.
# configs/config.yaml
identity:
expectedIdentity: "system:serviceaccount:hyperfleet:hyperfleet-e2e-sa"
tokenRequest:
serviceAccountName: "hyperfleet-e2e-sa"
namespace: "hyperfleet"Or via env vars:
HYPERFLEET_IDENTITY_TOKENREQUEST_SERVICEACCOUNTNAME=hyperfleet-e2e-sa \
HYPERFLEET_IDENTITY_TOKENREQUEST_NAMESPACE=hyperfleet \
HYPERFLEET_IDENTITY_EXPECTEDIDENTITY=system:serviceaccount:hyperfleet:hyperfleet-e2e-sa \
./bin/hyperfleet-e2e testThe acquired token is injected as Authorization: Bearer on every API request via client.WithBearerToken.
The expectedIdentity field is the audit identity the API resolves from the JWT claim. It's used by test assertions to verify created_by / deleted_by fields:
if expected := h.ExpectedIdentity(); expected != "" {
Expect(cluster).To(helper.HaveAuditIdentity(expected))
}When expectedIdentity is empty, audit assertions are skipped.
- Use
_test.gosuffix for E2E test files (sole exception:e2e/e2e_suite_test.go, the ginkgo CLI entry point) - Hardcode timeout durations — use
h.Cfg.Timeouts.* - Skip cleanup (
DeferCleanup) - Use
ginkgo.By()insideEventuallyclosures - Import
e2e/*packages frompkg/code
Validate()inpkg/config/config.goreturnserror, does not panic — only checks thatAPI.URLis non-emptyhelper.New()callslog.Fatalfif config is nil — tests must callSetSuiteConfigbefore running- Config priority: CLI flags > env vars (
HYPERFLEET_*prefix) >configs/config.yaml> built-in defaults (seepkg/config/defaults.go) - Config file path priority:
--configflag >HYPERFLEET_CONFIGenv >./configs/config.yamlauto-detect - Adapter names come from
h.Cfg.Adapters.Clusterandh.Cfg.Adapters.NodePoolat runtime — never hardcode adapter names. Values inconfigs/config.yaml(e.g.,cl-namespace) override compiled defaults inpkg/config/defaults.go(e.g.,clusters-namespace) e2e-ciMakefile target setsTESTDATA_DIRto absolute path and writes JUnit XML tooutput/- JWT auth is required when the API has
server.jwt.enabled=true(production default). Setidentity.tokenRequest.serviceAccountNameand.namespace— the framework acquires a token via the K8s TokenRequest API at startup.