6.0 KiB
Compose Deployer
ComposeDeployer runs each node as a Docker Compose service generated from your deployment descriptor.
The compose deployer lives in the testing-framework-runner-compose crate. It generates a compose file per run, brings the stack up, discovers the host ports Docker assigned, probes readiness, and hands control to the scenario runner. It requires a running Docker daemon; otherwise deployment returns ComposeRunnerError::DockerUnavailable.
use kvstore_runtime_ext::KvComposeDeployer; // = ComposeDeployer<KvEnv>
use testing_framework_core::scenario::Deployer;
use testing_framework_runner_compose::ComposeRunnerError;
let deployer = KvComposeDeployer::new();
let runner = match deployer.deploy(&scenario).await {
Ok(runner) => runner,
Err(ComposeRunnerError::DockerUnavailable) => return Ok(()), // skip without Docker
Err(error) => return Err(error.into()),
};
runner.run(&mut scenario).await?;
Run the demonstration binary with cargo run -p kvstore-examples --bin kvstore_compose_convergence.
Deployment Pipeline
flowchart LR
A[Workspace<br/>tempdir] --> B[Write configs<br/>+ cfgsync.yaml]
B --> C[Render<br/>compose.generated.yml]
C --> D[docker compose<br/>create + up]
D --> E[Port discovery<br/>docker compose port]
E --> F[Readiness<br/>probes]
F --> G[Node clients<br/>+ Runner]
- Workspace. A temporary
ComposeWorkspaceis created; the app'sComposeDeployEnv::prepare_compose_configswrites per-node config files (forComposeBinaryAppenvironments, one static config per node understack/configs/, rewritten for service hostnamesnode-0,node-1, ...). - cfgsync. If the environment enables
ComposeConfigServerMode::Docker, a cfgsync config server container is started on an ephemeral port and the deployer waits for it to accept TCP connections before proceeding. The default mode isDisabled. See Static Artifacts and cfgsync. - Compose file. The env's
compose_descriptor(image, entrypoint, volumes, ports, environment, optional platform per service) is rendered through the Tera template attesting-framework/deployers/compose/assets/docker-compose.yml.teraintocompose.generated.yml. The template is resolved relative to the repository root (CARGO_WORKSPACE_DIRoverride respected). Required images are checked withdocker image inspectup front. The deployer never builds or pulls them; a missing image fails the deploy withMissingImage. - Bring-up.
docker compose createanddocker compose uprun under a unique project name (compose-stack-<uuid>). On failure, container logs are dumped before cleanup. - Ports. Container ports map to ephemeral host ports; the deployer resolves each with
docker compose portand records them asNodeHostPorts { api, testing }. The host defaults to127.0.0.1and can be overridden withCOMPOSE_RUNNER_HOST. - Readiness. Per the env's
ComposeReadinessProbe: HTTP GET againstApplication::node_readiness_path()on each mapped API port, or raw TCP reachability. Gated byDeploymentPolicy.readiness_enabledand the deployer's ownwith_readiness(bool)switch; when disabled, the stack gets a short fixed grace period instead. See Readiness, Retry, and Artifact Preservation. - Clients.
build_node_clientruns against the discovered host/port pairs, producing the scenario's typed node clients.
Node Control
With with_node_control() on the builder, the deployer installs a ComposeNodeControl handle bound to the generated compose file and project. It supports restart only: restart_node(name) shells out to docker compose restart <service>. Start and stop of individual services are not wired for managed compose scenarios. The openraft_kv failover scenario runs on this backend: cargo run -p openraft-kv-examples --bin openraft_kv_compose_failover.
Attaching to an Existing Stack
The compose deployer fully supports existing-cluster mode. A scenario built with with_existing_cluster(ExistingCluster::for_compose_project("my-project")) skips workspace generation entirely: services are discovered from the running project (or taken from for_compose_services), each container's labeled API port is inspected, and clients are built through Application::external_node_client. In this mode node control gains stop_node in addition to restart_node, implemented with docker container stop / docker container restart against discovered container IDs.
deploy_with_metadata returns ComposeDeploymentMetadata alongside the runner; its existing_cluster() / IntoExistingCluster impl lets a later scenario attach to the stack this one deployed. See Existing and External Clusters.
Observability
Compose resolves ObservabilityInputs by merging LOGOS_BLOCKCHAIN_METRICS_QUERY_URL, LOGOS_BLOCKCHAIN_METRICS_OTLP_INGEST_URL, and LOGOS_BLOCKCHAIN_GRAFANA_URL env vars with the scenario's observability capability (capability values win). The OTLP ingest URL is passed into config preparation so node configs can point at your collector; the metrics query URL becomes the run's Prometheus-backed Metrics handle. Setting TESTNET_PRINT_ENDPOINTS prints Prometheus/Grafana endpoints and per-node pprof profile URLs to stdout. See Telemetry and External Observability.
Cleanup
The runner's cleanup guard runs docker compose down, shuts down the cfgsync container if one was started, and removes the workspace. Setting COMPOSE_RUNNER_PRESERVE (or TESTNET_RUNNER_PRESERVE) keeps the stack running and persists the workspace directory for post-mortem inspection; the preserved path is logged.
Requirements recap:
| Requirement | Why |
|---|---|
| Docker daemon running | ensure_docker_available gates every deploy |
| Node container images | Must exist locally before deploy; missing images fail with MissingImage |
| Repository checkout | The compose Tera template is read from the repo tree |