HyperFleet E2E is a Ginkgo-based black-box testing framework for validating HyperFleet cluster lifecycle management.
- Ephemeral resource management - Tests create and cleanup temporary resources
- Configuration-driven execution - Behavior controlled through config files, env vars, and CLI flags
- Structured logging - All operations tracked with component, cluster ID, and error context
- Generic HTTP client - Type-safe API client using hand-written types matching the generic resource API contract
pkg/
├── client/ - HyperFleet API client (generic HTTP)
│ └── kubernetes/ - Kubernetes client (client-go)
├── config/ - Configuration loading and validation
├── descriptor/ - Entity metadata, catalog and path construction
├── e2e/ - Test execution engine (Ginkgo)
├── helper/ - Test helpers (pollers, matchers, resource management)
├── labels/ - Test label definitions
└── util/ - Shared utility functions
HyperFleet E2E creates ephemeral resources per test for complete isolation.
Resource Lifecycle: Per-test creation and cleanup
- Each test creates its own cluster via API
- Full isolation between tests
- Automatic cleanup after completion
- Supports parallel execution
Workflow:
Test starts
→ Create new Helper instance (helper.New())
→ CreateTestResource() creates cluster via API
→ Poll for cluster Reconciled condition (pollers + matchers)
→ Execute test assertions
→ CleanupTestResource() deletes via the API; adapters remove downstream resources
Test ends
Example Configuration:
api:
url: https://api.hyperfleet.example.com
timeouts:
cluster:
reconciled: 5m # Example override; the default is defaultEntityTimeouts in pkg/config/descriptor.goPurpose: Configuration loading, validation, and management
Key Features:
- Multi-source configuration loading (file, environment variables, CLI flags)
- Detailed validation with helpful error messages
- Configuration priority enforcement
Configuration Priority Chain:
CLI Flags (highest priority)
↓
Environment Variables (HYPERFLEET_* prefix)
↓
Config File (configs/config.yaml)
↓
Built-in Defaults (lowest priority)
Key Types:
Config- Top-level configuration structAPIConfig,TimeoutsConfig,PollingConfig,LogConfig- Nested configuration sections
Key Functions:
Load()- Load and validate configurationValidate()- Validate configuration requirementsDisplay()- Log merged configuration using structured logging
Purpose: Generic HTTP client for the HyperFleet API with test-friendly methods
Key Features:
- Generic HTTP response handler
- Structured error handling
- Convenience methods for common operations (cluster, nodepool, channel, version, wifconfig)
- Auth injection via
http.RoundTripper
Key Types:
HyperFleetClient- Main client structResource/ResourceList- Generic resource types matching the API contractAdapterStatus/AdapterStatusList- Adapter status types
Key Methods:
Clusters:
CreateCluster(ctx, req)/CreateClusterFromPayload(ctx, path)- Create clusterGetCluster(ctx, clusterID)- Fetch cluster detailsListClusters(ctx)- List all clustersDeleteCluster(ctx, clusterID)- Soft-delete clusterPatchCluster(ctx, clusterID, req)/PatchClusterFromPayload(ctx, clusterID, path)- Update clusterGetClusterStatuses(ctx, clusterID)- Fetch adapter statuses
NodePools:
CreateNodePool(ctx, clusterID, req)/CreateNodePoolFromPayload(ctx, clusterID, path)- Create nodepoolGetNodePool(ctx, clusterID, npID)- Fetch nodepool detailsListNodePools(ctx, clusterID)- List nodepools for a clusterDeleteNodePool(ctx, clusterID, npID)- Soft-delete nodepoolPatchNodePool(ctx, clusterID, npID, req)/PatchNodePoolFromPayload(ctx, clusterID, npID, path)- Update nodepoolGetNodePoolStatuses(ctx, clusterID, npID)- Fetch adapter statuses
Purpose: Test helper utilities — resource management, pollers, matchers, K8s verification
Key Features:
- Per-test helper instance creation (
New()) - Resource lifecycle management (create, cleanup)
- Pollers for async assertions with
Eventually - Custom Gomega matchers for resource and adapter conditions
- Kubernetes resource verification (namespaces, deployments, jobs, configmaps)
- Adapter deployment/uninstall via Helm
Key Types:
Helper- Main struct withCfg,Entities,Client,K8sClient,AdapterDeploymentList
Key Methods:
Resource Management (resource.go):
CreateTestResource(ctx, entity, parentID, payloadPath)- Create a temporary resource and return its IDCleanupTestResource(ctx, entity, parentID, resourceID)- Delete through the API using the descriptor's cleanup policyDeferCleanup(entity, parentID, resourceID)- Register warning-only cleanup in the current Ginkgo node
Pollers (pollers.go) — thin functions returning current state for use with Eventually:
Poll(ctx, entity, parentID, resourceID)- Returns(*Resource, error)PollAdapterStatuses(ctx, entity, parentID, resourceID)- Returns(*AdapterStatusList, error)PollHTTPStatus(ctx, entity, parentID, resourceID)- Returns the HTTP status codePollNamespacesByPrefix(ctx, prefix)- Returns([]string, error)
Use an empty parentID for root resources. Child descriptors derive their nested
path from their parent's plural. Resolve a known kind with
h.Entities.MustGet("Cluster"); use Lookup for runtime input that can be unknown.
The catalog is built after configuration has been resolved. pkg/descriptor
owns the type, catalog and path construction, with no config or helper imports.
The single table is EntityDescriptors in pkg/config/descriptor.go. Each row
combines routing, cleanup and the default runtime settings: required adapters,
Timeouts and perf Thresholds. There is no second kind switch, config field,
default constant or env alias to update when adding a row. Nested paths are
derived from parent plural, parent ID and plural.
config.Load() overlays the kind-keyed config on the row defaults, builds and
validates the catalog once, and stores it in Config.Entities; helper.New()
uses the same catalog. Config keys are the kind in lower case, env vars the kind
in upper case:
adapters.<kind>orHYPERFLEET_ADAPTERS_<KIND>(comma- or space-separated) replaces the row's adapter list. Blank names are dropped, and the list must name at least one adapter; remove the key to keep the default.API_ADAPTERS_CLUSTERandAPI_ADAPTERS_NODEPOOLare aliases for the cluster and nodepool keys until the per-kind suites go.timeouts.<kind>.reconciled/.deleted, orHYPERFLEET_TIMEOUTS_<KIND>_RECONCILED/_DELETED, replaces one field; a missing or zero value keeps the row default. Values decode like every other duration in the config: a value needs a unit (90s,5m), and a bare YAML number other than zero fails. The keysTimeoutsConfigowns, such astimeouts.adapter.processing, are shared, not per kind.- Thresholds are code in the row and have no config key.
Load() fails before the suite starts on a YAML key that names no table kind
(for example the typo timeouts.nodepools), an unknown field under
timeouts.<kind>, a scalar or list where a map belongs (such as
timeouts.cluster: 5m), a timeout that is not a duration (such as "300" or 300, with
no unit), and an empty adapter list. A bare number fails for the shared durations too, such as
polling.interval. Env vars
for an unknown kind are not checked. descriptor.NewCatalog then checks each
row (EntityDescriptor.Validate), so Load() also fails on a negative timeout
or a row with adapters and no reconciled timeout. A row that waits for
hard-delete needs a positive deleted timeout, but config cannot remove one, so
only a table row can fail that check. Startup logs
adapters_<kind>, timeout_<kind>_reconciled and timeout_<kind>_deleted for
each row with adapters or timeouts in the "Loaded configuration" entry.
Fixture rows register routing and cleanup metadata. Parent and CascadeChild wait for hard-delete, so they default to the Cluster timeouts; their adapters and the fixture thresholds are unset until the fixture adapters are deployed and baselined. They are not runnable E2E fixtures yet; registration does not contact an API route or establish performance baselines.
Cleanup policy preserves existing behavior: Cluster and NodePool wait for 404, Channel explicitly deletes eligible Versions before itself, and WifConfig still tolerates conflicts during teardown. These are cleanup choices, not API deletion semantics. The API and adapters own downstream resource deletion. Hard-delete polling retains the existing deadline-between-requests loop and sleep interval. Requests use the caller's context; the cleanup timeout does not impose a new request deadline. Resource IDs are concatenated as before and passed to the client/API without new helper validation.
Custom Matchers (matchers.go) — reusable Gomega matchers:
HaveResourceCondition(condType, status)- Matches*Resourcewith given conditionHaveAllAdaptersWithCondition(adapters, condType, status)- All required adapters have conditionHaveAllAdaptersAtGeneration(adapters, gen)- All adapters at generation with Applied/Available/Health=True
Condition Validation (validation.go):
HasResourceCondition(conditions, condType, status)- Synchronous condition checkHasAdapterCondition(conditions, condType, status)- Synchronous adapter condition checkAllConditionsTrue(conditions, condTypes)- All specified conditions are TrueAdapterNameToConditionType(adapterName)- Convert adapter name to condition type string
Kubernetes Verification (k8s.go):
VerifyNamespaceActive(ctx, name, labels, annotations)- Namespace exists and ActiveVerifyDeploymentAvailable(ctx, ns, labels, annotations)- Deployment is AvailableVerifyJobComplete(ctx, ns, labels, annotations)- Job has completedVerifyConfigMap(ctx, ns, labels, annotations)- ConfigMap exists with expected metadata
Adapter Operations (adapter.go):
DeployAdapter(ctx, opts)- Deploy adapter via Helm upgrade --install;opts.Entitysupplies its descriptorUninstallAdapter(ctx, releaseName, namespace)- Uninstall adapter via Helm
Purpose: Test execution engine, Ginkgo configuration, and process-wide structured logging
Key Features:
- Ginkgo suite configuration
- JUnit report generation
- Test filtering (labels, focus, skip)
- Suite timeout management
- Direct use of Go's
log/slogthroughout the framework - Shared
hyperfleet-loggerhandler, which addscomponent,version, andhostnameto every record - Ginkgo log handler that adds
test_casefor log records emitted during a spec - Configurable output format (text, JSON), level (debug, info, warn, error), and output (stdout, stderr)
Key Functions:
RunTests(ctx)- Main entry point for test execution- Configures Ginkgo reporters, timeouts, and filters
- Handles suite-level setup and teardown
initLogging(cfg)- Configures the defaultsloglogger with the shared handler and Ginkgo context
Configuration values are applied in this priority order:
┌──────────────────────────────────────────────────────────────────┐
│ 1. CLI Flags (highest priority) │
│ Example: --api-url, --log-level │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 2. Environment Variables │
│ Example: HYPERFLEET_API_URL, HYPERFLEET_LOG_LEVEL │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 3. Config File │
│ Example: configs/config.yaml │
└──────────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 4. Built-in Defaults (lowest priority) │
│ Example: timeout: 30m, poll: 10s │
└──────────────────────────────────────────────────────────────────┘
Example: If you set HYPERFLEET_API_URL=https://env.example.com and also pass --api-url=https://flag.example.com, the CLI flag value (https://flag.example.com) wins.
CLI Invoked (hyperfleet-e2e test)
↓
┌─────────────────────────────────────┐
│ Load Configuration │
│ • Read config file │
│ • Apply environment variables │
│ • Apply CLI flags │
│ • Validate configuration │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Initialize Logger │
│ • Setup structured logging │
│ • Display merged configuration │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Configure Ginkgo │
│ • Apply label filters │
│ • Setup JUnit reporter │
│ • Configure timeouts │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Run Test Suites │
│ • Discover all e2e/*/*.go │
│ • Execute matched tests │
│ • Collect results │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Generate Reports │
│ • JUnit XML (if configured) │
│ • Console output │
│ • Exit code (0 = pass, 1 = fail) │
└─────────────────────────────────────┘
The framework uses hand-written Go types (Resource, AdapterStatus, ResourceCondition, etc.) that mirror the generic resource API contract. With the migration to the generic resource layer, the API uses a single Resource type with map[string]any spec for all entities (clusters, nodepools, channels, versions, wifconfigs).
- Types: Defined in
pkg/client/resource.go-Resource,ResourceCondition,AdapterStatus, etc. - Generic CRUD:
CreateResource,GetResource,ListResources,PatchResource,DeleteResource - Entity convenience methods:
GetCluster,CreateNodePool, etc. delegate to generic CRUD with entity-specific paths - Auth:
WithAuthorizationTokenwrapshttp.RoundTripperand injects a mode-specific scheme on API-host requests;WithBearerTokenremains a compatibility wrapper.
Example:
// Create client with the scheme selected by AUTH_MODE.
cl, err := client.NewHyperFleetClient(apiURL, nil, client.WithAuthorizationToken(token, cfg.MachineAuthorizationScheme()))
if err != nil {
log.Fatalf("create client: %v", err)
}
cluster, err := cl.GetCluster(ctx, clusterID)