Use cases#
The use cases test manent end to end, as it runs in production: API server, workers, client and SSH targets in separate containers, on Postgres. They go from a single scheduled deploy to multi-region wave rollouts, then containers on VMs and Kubernetes, load balancers, percentage rollouts, custom steps, integrations and application servers, and for each one they also check failures and rollbacks.
make test-usecases # all cases (about 30 minutes)
make test-usecases CASES="uc1 uc4" # a subset
make test-usecases KEEP=1 # leaves the lab running at the end
They require Docker with Compose v2, Go, jq and ssh-keygen. The script compiles the Linux binaries on the host, so the golang image is not needed. UC6 and UC7 use privileged containers (Docker in Docker and k3s) and about 2 GB of extra memory for Docker. UC8 uses the Playwright image (about 2 GB on disk), UC10 HAProxy (about 200 MB), UC14 WildFly (about 800 MB) and UC15 Prometheus (about 270 MB). UC11 downloads the Gateway API CRDs from GitHub.
The lab#
flowchart LR
C[manent client] --> A[API]
A --> P[(Postgres)]
W1[worker-1] --> P
W2[worker-2] --> P
subgraph eu-1
EA[eu1-a · eu-1a]
EB[eu1-b · eu-1b]
EC[eu1-c · eu-1c]
end
subgraph us-1
UA[us1-a · us-1a]
UB[us1-b · us-1b]
end
W1 --> eu-1
W1 --> us-1
W2 --> eu-1
W2 --> us-1
subgraph uc67[UC6 · UC7]
R[(registry)]
VA[vm-a · Docker]
VB[vm-b · Docker]
K[k3s]
end
W1 --> uc67
W2 --> uc67
- Targets: Debian containers with SSH (for the
remote-copyplugin,releaseslayout) and an HTTP server that exposes the live release:http://<target>:8080/<app>/current/healthz. A release withouthealthzsimulates a faulty version. - Environment configuration: targets, parameters and secrets come from the
fileprovider (test/usecases/config/envconfig); descriptors contain only the artifact. - Access: static tokens with different roles (
cideploys,cabapproves,admincan bypass freezes,vieweronly reads), and an OIDC identity provider for tests (test/usecases/oidc) with the same users for the web interface. - Simulated faults: the
post_commandfails or waits if the case creates a file on the target. Workers and targets are stopped or killed withdocker compose. - Region profile: for UC5 the generic workers are replaced by one worker per region (
dbos.region,region_routing), each on its region's network only. The orchestrator reaches no target. - Containers and Kubernetes: for UC6 and UC7 the lab starts an image registry, two VMs with Docker (
vm-a,vm-b) and a single-node k3s cluster. The test application is a small image with/healthz; the faulty version has none, so itsHEALTHCHECKandreadinessProbestay red. These services start only for their case and are removed at the end. - Load balancer: for UC10 a real HAProxy with the Data Plane API v3 (
test/usecases/haproxy) in front of theeu-1targets: theuc10-shopbackend with HTTP health checks is drained per batch, theuc10-bluebackend holds the two slots of the percentage rollout. For UC15 and UC16 theuc15-webanduc16-webbackends have servers named after the slots, and HAProxy exports its metrics per server to a real Prometheus (metricsprofile, scraped every second). UC11 uses the same k3s cluster with the Gateway API CRDs and anHTTPRoutecreated upfront, as infrastructure would. - CI and Terraform: for UC18 an
alpinerunner with the client (toolsprofile) runs the GitHub Action script and the GitLab template lines as the two CIs would; for UC19 thehashicorp/terraformimage (terraformprofile) uses the provider built fromintegrations/terraformwithdev_overrides, without a registry. - Windows: for UC20 an emulated Windows host (
test/usecases/windows,windowsprofile) with a real PowerShell 7, OpenSSH, a WinRM endpoint over HTTPS that runs the commands with PowerShell, and modules with the IIS and service cmdlets the plugin uses, on a JSON configuration from which a web server serves the IIS applications. The plugin scripts run as on Windows Server; IIS and the service manager are imitated. - Windows, second host: for UC21 a second emulated host (
win2) and, onwin1, the cmdlets of scheduled tasks and NLB, anmsiexec.exeand the ARR part ofMicrosoft.Web.Administration(compiled from C# into the image, under$env:windiras on Windows), all on JSON state. The pools' PowerShell runs unchanged. - Azure: for UC22 the
fakesservice also serves, on port 9400, Azure emulated at protocol level (internal/azurefake, also used by the plugin tests): Entra ID token with the lab service principal, Resource Manager for App Service and Container Apps with long-running operations, Kudu, the web apps and container apps themselves (they serve the released version) and the Log Analytics query API. The Container Apps images come from the lab registry. - External services: Slack, Teams, ServiceNow, Datadog, New Relic, Elastic and CloudWatch have no local version (or none light enough), so
test/usecases/fakesemulates their APIs (and an HTTP endpoint for custom steps; for the metrics systems the series to return are set withPOST /control/metrics, and CloudWatch speaks the CBOR protocol of the AWS SDK with a SigV4 signature) and records every request, which the cases read back. UC14 uses a real WildFly with the HTTP management API.
The cases#
| Case | Scenario | What it checks |
|---|---|---|
| UC1 | Scheduled deploy (schedule.at) on one target in an AZ, then on three AZs of a region |
Wait until the scheduled time, cancellation, idempotency, faulty release with automatic rollback, failure without rollback and recovery, manual rollback, wrong checksum, dry-run, a target that is down |
| UC2 | Environment configuration and promotion | Authentication and permissions, conflicts with the environment configuration, promotion test → prod with approval, ban on self-approval, rejection, freeze and override |
| UC3 | Several workers on the same database | One deploy at a time per environment and environments in parallel, a worker killed during a deploy (automatic recovery by the other), graceful shutdown that waits for the running deploy |
| UC4 | Wave rollout in one region | Canary and approval, one AZ at a time with bake and health gate, red gate on the canary, failure in the last AZ with rollback-all, rejection, onFailure: stop with drift in the inventory |
| UC5 | Multi-region wave rollout | Passes and health-gate probes run by the region workers, regions in parallel, rollback-wave, a region worker that is down (the pass waits for it), freeze of a single region |
| UC6 | Containers on VMs with Docker (container plugin) | Image pinned by digest, promotion test → prod in waves, dry-run, unhealthy image with rollback-all, manual rollback, VM down, tag moved in the registry (the promotion refuses the different image) |
| UC7 | Manifests on Kubernetes (kubernetes plugin) | Server-side dry-run, same manifests in test and prod with different environment configuration, blocked rollout and rollback, prune of removed objects, release history, image not pinned to a digest, manual rollback |
| UC8 | Web interface | Headers and configuration, whoami, approvals, cursor and SSE stream of the API; with Playwright: OIDC sign-in, every view checked with axe in both themes, deploy detail with live log, promotion after a dry-run, ban on self-approval, approval by another user followed live, keyboard and Italian |
| UC9 | Deploy chains | A deploy waiting for another (after) without touching its target, start after the end of the dependency, failed dependency (DEPENDENCY_FAILED) and refusal at submit, composite release in topological order with idempotent resubmission, failed release with onFailure: rollback (successful deploys rolled back to the previous version) |
| UC10 | Load balancer and router on HAProxy | Every batch drained from the backend during its deploy and back once healthy, durable waits, capacity check (min_healthy) that postpones the drain while another server is down, no failed request through HAProxy during the rollout; router with two slots of targets: first release with the governance approval moved into the rollout (approvals.when: rollout), 50% step with equal weights and both versions served, faulty release with the weights back to the stable slot |
| UC11 | Percentage rollout on Kubernetes with a Helm chart | Candidate in the free slot (uc11-web-a/-b) with the slot label, approval after the tests at weight 0, testers' rule of the HTTPRoute, steps on the main rule's weights, history pointing to the slot, replaced slot scaled to zero after scaleDownDelay, faulty release that never becomes ready with the traffic left on the stable slot |
| UC12 | Custom steps | Plan with type and spec_hash but no command or URL, dry-run with only its own command and secrets, command over SSH on one target with parameter, secret file and declared output, secret redacted in the deploy log, HTTP request with bearer, Idempotency-Key and JSON body built by manent, polling of the job, exit 75 retried, permanent error with no retries and targets untouched |
| UC13 | Chat and change management | Slack message posted at start and updated with the outcome, approval request with buttons on Slack, change opened in ServiceNow with the deploy as correlation_id, requester's click refused (self-approval), CAB's click accepted (signed Slack request), invalid signature refused, deploy started only after the change approval and change closed with the outcome, change rejected in ServiceNow, Teams webhook refused because not https |
| UC14 | WildFly (jboss plugin) | Dry-run, one deployment per version with the same runtime-name, switch with the server suspended, war refused by WildFly with the version in service untouched, red post-test with automatic rollback, manual rollback without uploading again, parameter conflicting with the environment configuration refused, JVM restart, keep_releases |
| UC15 | Canary analysis on Prometheus | Load from the host through HAProxy, HAProxy metrics per server (= slot) in a real Prometheus, Mann-Whitney comparison of the candidate and stable series at every reading of the 50% step, first release without comparisons, healthy release promoted with score 100, faulty release (one page missing) rejected by the critical metric on the 4xx share with the p-value in the gate and the traffic back to the stable slot |
| UC16 | Canary analysis on Datadog, New Relic, Elastic and CloudWatch | One metric per system, each read with its own API and credentials (Datadog keys, NerdGraph key and NRQL TIMESERIES, ES|QL with API key, GetMetricData signed with SigV4), equivalent series promoted with score 100, two non-critical metrics worse: score 50, enough during the bake (marginal) but red at its end, critical CloudWatch metric worse: rollout stopped before the end of the bake, traffic back to the stable slot |
| UC17 | Real state of the targets | Reconciliation every 10 seconds (reconcile in the lab configuration): targets read equal to the inventory with region and AZ, manual rollback of the current symlink on a target reported as drifted with the version found, out_of_sync and manent inventory --out-of-sync, worker log and manent_targets metric, release removed (missing), target stopped (unknown with the error), back in sync without a new deploy, reading of the previous deploy ignored after a new release |
| UC18 | CI: GitHub Action and GitLab template | Action script with the variables of a runner (INPUT_*, GITHUB_OUTPUT, GITHUB_STEP_SUMMARY): deploy with version and source from the pipeline, outputs and job summary, job re-run with the same id without new runs, dry-run, faulty release with the step failed, state=ERROR and rollback in the summary, validate, env-validate of a valid configuration and of one with an unknown field; GitLab job lines with the inputs substituted and a pipeline variable in the id, dotenv report |
| UC19 | Terraform (manent provider) | terraform apply of the first and the next release, empty plan with the same version, rollback done with the CLI that shows in the plan and is undone by the apply, target changed by hand released again with redeploy_out_of_sync, candidate environment configuration checked in the plan (valid with the targets, invalid with the plan failed), faulty release with the apply failed and automatic rollback, destroy that leaves the release in service |
| UC20 | Windows (windows plugin) | IIS application over WinRM (HTTPS, basic): dry-run, deploy with the application pool stopped and restarted, physicalPath on the release and application served, release without healthz with a red post-test and rollback, physicalPath changed by hand reported by the reconciliation, manual rollback without a new copy, keep_releases; Windows service over OpenSSH: binPath with the arguments on the release executable, executable that does not start with the deploy failed and the service back on the previous release |
| UC21 | Windows: ARR, NLB, MSI, scheduled tasks (windows plugin) | IIS application on two hosts behind an ARR web farm: one server at a time in Drain and back Available, drain, deploy and restore steps per batch; MSI package on the nodes of an NLB cluster: one node at a time drained and stopped, public properties to msiexec, MajorUpgrade, release read from the installed product, failed package (1603) with the host untouched, manual rollback with a downgrade after uninstalling; scheduled task: action on the release executable with arguments and working directory, stored password of the account, run_now, running instance that blocks the deploy and stop_task |
| UC22 | Azure (appservice, containerapps, azure-monitor) |
App Service with a staging slot: package through Kudu, health of the slot, swap, version travelling with the swap, faulty release that never reaches production, swap done by hand reported by the reconciliation, manual rollback with a new swap; without slot: production and tags; percentage rollout: 30% of the traffic to the slot (rampUpRules) with the gate on Log Analytics (KQL), swap, error rate over the threshold with production untouched; Container Apps: revision per deploy with the image pinned to the digest, unhealthy revision with the traffic back on the previous one |
| UC23 | Governance in Rego (Policies in Rego) | Lab on governance.engine: opa (every case runs on the default policy, which applies the rules of manent.yml); revision pushed by an author without policy.approve, tests run at push, impact on the real deploys, self-approval refused (four eyes), approval by an admin without restarts and revision in the metrics; approvers limited to the CAB with the admin refused, downgrade denied and shown by validate; freeze created at runtime between two waves with the rollout paused and resumed after its deletion; hard freeze that blocks the override, deletion refused without freeze.write and break-glass with manent-server admin; back to the default policy |
| UC24 | Tenants | Teams that own applications, with the registry in tenants.yml; payments and retail both have uc24-shop: without tenant the deploy is refused as ambiguous, with --tenant each release reads the configuration of its tenant ({tenant} in the template) and has its inventory entry; an application no tenant lists deployed as untenanted; the dedicated queue and the restrictions of a tenant; deploy IDs, after and releases across tenants refused; the implicit role of the members, refused on the homonym of retail named by mistake and on the untenanted applications; application tags in selectors and a runtime freeze on */uc24-*; admin move-application (dry-run); a Rego policy that reads the tenant tags; back to the default policy |
| UC25 | Permissions as a policy | Lab on rbac.engine: opa (every case runs on the default policy, which applies the bindings of manent.yml); OIDC users with the employee_type claim; a revision that gives a team its application and the applications of the tenants tagged owner=<team>, on-call shifts with until and a denial of production to contractors, simulated with policy access (gains against the active revision, denial with its reason, owned and other tenants) before a second person approves it; real deploys allowed and denied with the reason and the revision, audit with the grant used; a scheduled deploy cancelled by the on-call identity and not by the expired shift; a revision that removes every permission fixed by whoever has the static policy.*; back to the default policy |
| UC26 | Exports and imports | An infrastructure deploy whose custom step "creates" two VMs and exports their hosts (format: targets) and the release directory; the configuration deploy imports them as targets and dest_dir with after, with --import from the CLI and in a composite release that names the producer; an export outside its pattern that fails the producer and leaves its dependant in DEPENDENCY_FAILED; a missing export refused at submit; an environment that takes targets and parameters from the release in service (targets_from, params_from) and refuses targets from the descriptor; a promotion that drops the import of the source deploy |
| UC27 | Git repository | A second manent server reads the environment configuration from the lab Git server (smart HTTP with a token, commits signed with SSH): every deploy records the commit SHA; a new commit on main reaches the next deploy; an unsigned commit is refused at submission; --config-ref releases with the configuration of a tag, a branch outside allowed_refs and an unreachable SHA are refused; the manual rollback goes back to the SHA recorded by the deploy it restores, not to the head of main |
| UC28 | terraform | The external Terraform plugin with OpenTofu in the worker image releases a module with terraform_data resources in two states (one per region) of the pg backend on the lab Postgres: the dry-run shows the planned changes and writes no state; the deploy applies in both regions and exports the VMs as targets, on which uc28-app is released from the environment configuration (current import + targets_from); the same version again changes nothing; a version that destroys a VM is refused by max_destroy with a single plan (permanent error, no retries) and the inventory stays on the previous release |
| UC29 | ansible | A composite release chains infrastructure, configuration and application: the Terraform plugin exports two VMs with their labels, the Ansible plugin configures them on its own queue, served only by worker-ansible (Debian ansible-core; the distroless workers know the plugin only as a stub), with an inventory grouped by label, and exports the application directory with set_stats; remote-copy releases the application there (targetsFrom + paramsFrom). The release marker is written on the targets, the plugin queue shows as kind plugin, check mode changes nothing, and a host failing in the playbook fails the deploy with the per-host summary |
| UC30 | Parallel targets | Three SSH hosts in one batch with parallel: 100%, capped at 2 by plugins.remote-copy.max_parallel: the post_command logs show two hosts updated together and the third starting when a slot frees up, the deploy step reports parallel 2 and parallel_requested 3. Without parallel the hosts go one after the other; a descriptor can ask for it within descriptor_policy.max_parallel (4 in the lab, 422 beyond it) and cannot change the one of the environment. With a failed host the one already running finishes and the third never starts. In a wave rollout the canary wave keeps parallel: 1 and the next batch of two hosts is updated together |
| UC31 | Several providers | Three file providers in envconfig.providers: uc31-legacy serves the uc31-* applications with on_missing: next, so test is read there (the same environment in the default provider is never read) while prod, no longer in it, falls back to default; the config step reports provider and skipped. The uc31team tenant names uc31-team (tenant_only) in the registry: its deploy reads that provider, and an environment missing there does not fall back to the others (422, no targets) |
Each check prints ✔ or ✘. At the end the script summarizes the failures and saves the container logs in test/usecases/.work/compose.log.
Adding a case#
A case is a script in test/usecases/cases/ (ucN.sh), run in numeric order on the same lab. The functions in lib.sh create artifacts and descriptors, run the client with a role (as cab manent approve <id>), wait for states (wait_state, wait_rollout), read the targets (current, healthy) and inject faults (fault). Every case uses its own application, so it does not depend on the others.