From 03c0a5ffcbfe4945ff71dc543fe86c9278164de4 Mon Sep 17 00:00:00 2001 From: saratomaz Date: Tue, 17 Feb 2026 17:06:45 +0000 Subject: [PATCH 1/5] Add docker environment for containerized tests --- docker/Dockerfile | 23 ++++++++++++ docker/Dockerfile.dockerignore | 66 ++++++++++++++++++++++++++++++++++ docker/README.md | 59 ++++++++++++++++++++++++++++++ docker/docker-compose.yaml | 30 ++++++++++++++++ 4 files changed, 178 insertions(+) create mode 100644 docker/Dockerfile create mode 100644 docker/Dockerfile.dockerignore create mode 100644 docker/README.md create mode 100644 docker/docker-compose.yaml diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..7d2d10a9d --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,23 @@ +# Dockerfile for cardano-node-tests (Antithesis/Moog driver image) +# +# Minimal image: nix is configured and the repo is copied in. +# All heavy setup (cardano binaries, Python venv) happens at runtime +# via regression.sh, which manages its own nix environment through its shebang. +# +# Build and push to GHCR before submitting to Moog: +# docker build -f docker/Dockerfile -t ghcr.io/intersectmbo/cardano-node-tests-antithesis:latest . +# docker push ghcr.io/intersectmbo/cardano-node-tests-antithesis:latest + +FROM nixos/nix:2.25.5 + +ARG GIT_REVISION +ENV GIT_REVISION=${GIT_REVISION} + +RUN mkdir -p /etc/nix && \ + echo "extra-substituters = https://cache.iog.io" >> /etc/nix/nix.conf && \ + echo "extra-trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ=" >> /etc/nix/nix.conf && \ + echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf && \ + echo "accept-flake-config = true" >> /etc/nix/nix.conf + +WORKDIR /work +COPY . /work/ diff --git a/docker/Dockerfile.dockerignore b/docker/Dockerfile.dockerignore new file mode 100644 index 000000000..ea2e6c752 --- /dev/null +++ b/docker/Dockerfile.dockerignore @@ -0,0 +1,66 @@ +# Ignore unnecessary files during Docker build + +# Git +.git/ +.gitignore + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ +build/ +*.egg + +# Virtual environments +.venv/ +venv/ +ENV/ +env/ + +# Testing artifacts +run_workdir/ +.artifacts/ +.cli_coverage/ +.reports/ +allure-results/ +allure-results.tar.xz +testrun-report.* +*.log +*.json.log + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Nix +result +result-* + +# Documentation +docs/_build/ +*.md + +# Temporary files +*.tmp +*.bak +.DS_Store + +# Scripts output +scripts/destination/ +scripts/destination_working/ + +# Coverage +.coverage +htmlcov/ +cli_coverage.json +requirements_coverage.json + +# CI specific +.bin/ diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..f9b78d17c --- /dev/null +++ b/docker/README.md @@ -0,0 +1,59 @@ +# Docker setup for cardano-node-tests (Antithesis/Moog) + +This directory contains the driver image and compose file for submitting +`cardano-node-tests` to Antithesis via the Moog platform. + +## How it works + +- `Dockerfile` โ€” minimal image: configures Nix and copies the repo into + `/work/`. No binaries are pre-built; `regression.sh` handles all setup at + runtime via its own Nix shebang. Requires `GIT_REVISION` build arg (the + current commit hash) so pytest can identify the revision without a `.git` + directory inside the image. +- `docker-compose.yaml` โ€” single `driver` service for Moog submission. + +## Workflow + +### 1. Build and push the image + +```bash +docker build -f docker/Dockerfile \ + --build-arg GIT_REVISION=$(git rev-parse HEAD) \ + -t ghcr.io/intersectmbo/cardano-node-tests-antithesis:latest . + +docker push ghcr.io/intersectmbo/cardano-node-tests-antithesis:latest +``` + +### 2. Validate the compose locally + +```bash +docker compose -f docker/docker-compose.yaml config +docker compose -f docker/docker-compose.yaml up --build +``` + +### 3. Submit to Moog + +```bash +moog requester create-test \ + --platform github \ + --username saratomaz \ + --repository IntersectMBO/cardano-node-tests \ + --directory ./docker \ + --commit $(git rev-parse HEAD) \ + --try 1 \ + --duration 2 +``` + +## Environment variables + +| Variable | Default | Description | +|-------------------|------------|------------------------------------------| +| `NODE_REV` | `master` | cardano-node git revision | +| `CARDANO_CLI_REV` | (built-in) | cardano-cli revision, empty = use node's | +| `DBSYNC_REV` | (disabled) | db-sync revision, empty = disabled | +| `RUN_TARGET` | `tests` | `tests`, `testpr`, or `testnets` | +| `MARKEXPR` | | pytest marker expression | +| `CLUSTERS_COUNT` | | number of local cluster instances | +| `CLUSTER_ERA` | | e.g. `conway` | +| `PROTOCOL_VERSION`| | e.g. `11` | +| `UTXO_BACKEND` | | e.g. `disk`, `mem` | diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 000000000..06f73d541 --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,30 @@ +# Docker Compose for Antithesis/Moog test submission. +# +# Submit to Moog: +# moog requester create-test \ +# --platform github --username \ +# --repository IntersectMBO/cardano-node-tests \ +# --directory ./docker \ +# --commit --try 1 --duration 2 +# +# Validate locally before submitting: +# docker compose -f docker/docker-compose.yaml config +# docker compose -f docker/docker-compose.yaml up --build + +services: + driver: + image: ghcr.io/intersectmbo/cardano-node-tests-antithesis:latest + build: + context: .. + dockerfile: docker/Dockerfile + command: ["/work/.github/regression.sh"] + environment: + - NODE_REV=${NODE_REV:-master} + - CARDANO_CLI_REV=${CARDANO_CLI_REV:-} + - DBSYNC_REV=${DBSYNC_REV:-} + - RUN_TARGET=${RUN_TARGET:-tests} + - MARKEXPR=${MARKEXPR:-} + - CLUSTERS_COUNT=${CLUSTERS_COUNT:-} + - CLUSTER_ERA=${CLUSTER_ERA:-} + - PROTOCOL_VERSION=${PROTOCOL_VERSION:-} + - UTXO_BACKEND=${UTXO_BACKEND:-} From c37efbb40ecd8b4ad55e9fa0acf189eb120fb4c1 Mon Sep 17 00:00:00 2001 From: saratomaz Date: Fri, 27 Mar 2026 12:57:39 +0000 Subject: [PATCH 2/5] Adapt docker to antithesis --- .github/regression.sh | 38 ++++++++++++++------ docker/Dockerfile | 74 ++++++++++++++++++++++++++++++++------ docker/Dockerfile.config | 13 +++++++ docker/README.md | 73 +++++++++++++++++++++++++------------ docker/antithesis_run.sh | 49 +++++++++++++++++++++++++ docker/docker-compose.yaml | 26 ++++++++------ 6 files changed, 217 insertions(+), 56 deletions(-) create mode 100644 docker/Dockerfile.config create mode 100755 docker/antithesis_run.sh diff --git a/.github/regression.sh b/.github/regression.sh index 88fa20b53..2ce4650bb 100755 --- a/.github/regression.sh +++ b/.github/regression.sh @@ -115,16 +115,25 @@ case "${CARDANO_CLI_REV:-}" in esac # setup cardano-node binaries -case "${NODE_REV:-}" in - "" | "none" ) - NODE_REV=master - ;; -esac -# shellcheck disable=SC1091 -. .github/source_cardano_node.sh -cardano_bins_build_all "$NODE_REV" "${CARDANO_CLI_REV:-}" -PATH_PREPEND="$(cardano_bins_print_path_prepend "${CARDANO_CLI_REV:-}")${PATH_PREPEND}" -export PATH_PREPEND +if [ -n "${CARDANO_PREBUILT_DIR:-}" ]; then + # Pre-built binaries were baked into the image (e.g. for Antithesis). + # Skip all nix builds and point PATH_PREPEND at the pre-built directories. + _d="${CARDANO_PREBUILT_DIR}" + PATH_PREPEND="${_d}/cardano-node/bin:${_d}/cardano-submit-api/bin:${_d}/cardano-cli/bin:${_d}/bech32/bin:${PATH_PREPEND}" + export PATH_PREPEND + unset _d +else + case "${NODE_REV:-}" in + "" | "none" ) + NODE_REV=master + ;; + esac + # shellcheck disable=SC1091 + . .github/source_cardano_node.sh + cardano_bins_build_all "$NODE_REV" "${CARDANO_CLI_REV:-}" + PATH_PREPEND="$(cardano_bins_print_path_prepend "${CARDANO_CLI_REV:-}")${PATH_PREPEND}" + export PATH_PREPEND +fi # optimize nix store if running in GitHub Actions if [ -n "${GITHUB_ACTIONS:-}" ]; then @@ -254,7 +263,14 @@ nix develop --accept-flake-config .#testenv --command bash -c ' echo "::group::Python venv setup" printf "start: %(%H:%M:%S)T\n" -1 - . .github/setup_venv.sh clean + # When _VENV_DIR points to a pre-built venv (e.g. baked into the image for + # Antithesis), skip the `clean` flag so the existing venv is reused as-is + # without re-downloading packages. + if [ -n "${_VENV_DIR:-}" ] && [ -e "${_VENV_DIR}" ]; then + . .github/setup_venv.sh + else + . .github/setup_venv.sh clean + fi echo "::endgroup::" # end group for "Python venv setup" echo "::group::๐Ÿงช Testrun" diff --git a/docker/Dockerfile b/docker/Dockerfile index 7d2d10a9d..0bae1dfe6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,23 +1,75 @@ -# Dockerfile for cardano-node-tests (Antithesis/Moog driver image) +# Dockerfile for cardano-node-tests (Antithesis-compatible driver image) # -# Minimal image: nix is configured and the repo is copied in. -# All heavy setup (cardano binaries, Python venv) happens at runtime -# via regression.sh, which manages its own nix environment through its shebang. +# All heavy dependencies are baked in at image build time so the container +# runs without any network access (required by Antithesis environments). # -# Build and push to GHCR before submitting to Moog: -# docker build -f docker/Dockerfile -t ghcr.io/intersectmbo/cardano-node-tests-antithesis:latest . -# docker push ghcr.io/intersectmbo/cardano-node-tests-antithesis:latest +# Build args: +# GIT_REVISION โ€” git commit hash stored as $GIT_REVISION in the image +# NODE_REV โ€” cardano-node git ref to pre-build (default: master) +# +# Build and push to GHCR before submitting to Antithesis: +# docker build -f docker/Dockerfile \ +# --build-arg GIT_REVISION=$(git rev-parse HEAD) \ +# --build-arg NODE_REV=master \ +# -t ghcr.io/saratomaz/cardano-node-tests-antithesis:latest . +# docker push ghcr.io/saratomaz/cardano-node-tests-antithesis:latest FROM nixos/nix:2.25.5 ARG GIT_REVISION +ARG NODE_REV=master + ENV GIT_REVISION=${GIT_REVISION} +# Store the baked-in node revision for reference at runtime. +ENV BAKED_NODE_REV=${NODE_REV} +# Configure Nix with IOG binary cache and required experimental features. RUN mkdir -p /etc/nix && \ - echo "extra-substituters = https://cache.iog.io" >> /etc/nix/nix.conf && \ - echo "extra-trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ=" >> /etc/nix/nix.conf && \ - echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf && \ - echo "accept-flake-config = true" >> /etc/nix/nix.conf + printf 'extra-substituters = https://cache.iog.io\n\ +extra-trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ=\n\ +experimental-features = nix-command flakes\n\ +accept-flake-config = true\n' >> /etc/nix/nix.conf WORKDIR /work COPY . /work/ + +# Pre-build cardano-node, cardano-submit-api, cardano-cli, and bech32 into /opt/cardano/. +# NODE_REV is fixed at image build time โ€” no network access is needed at runtime. +RUN mkdir -p /opt/cardano && \ + nix build \ + --accept-flake-config --no-write-lock-file \ + "github://github.com/IntersectMBO/cardano-node?ref=${NODE_REV}#cardano-node" \ + -o /opt/cardano/cardano-node && \ + nix build \ + --accept-flake-config --no-write-lock-file \ + "github://github.com/IntersectMBO/cardano-node?ref=${NODE_REV}#cardano-submit-api" \ + -o /opt/cardano/cardano-submit-api && \ + nix build \ + --accept-flake-config --no-write-lock-file \ + "github://github.com/IntersectMBO/cardano-node?ref=${NODE_REV}#cardano-cli" \ + -o /opt/cardano/cardano-cli && \ + nix build \ + --accept-flake-config --no-write-lock-file \ + "github://github.com/IntersectMBO/cardano-node?ref=${NODE_REV}#bech32" \ + -o /opt/cardano/bech32 + +# Pre-warm the testenv dev shell (pulls nixpkgs, postgres, uv, python313 into the +# nix store) and create the Python venv at /opt/tests-venv with all project +# dependencies installed. This is the same step regression.sh does at runtime +# but done here so no pip/uv network calls are needed in the Antithesis env. +RUN nix develop --accept-flake-config .#testenv --command \ + bash -c 'python3 -m venv /opt/tests-venv --prompt tests-venv && \ + . /opt/tests-venv/bin/activate && \ + cd /work && \ + uv sync --active --no-dev' + +# Pre-warm the base dev shell (bash, coreutils, git, jq, โ€ฆ) so its store +# paths are cached and the regression.sh shebang resolves offline. +RUN nix develop --accept-flake-config .#base --command true + +# Create the Antithesis test driver directory and install the entry-point. +# singleton_driver_* files are run once per test run by Antithesis. +RUN mkdir -p /opt/antithesis/test/v1/quickstart && \ + cp /work/docker/antithesis_run.sh \ + /opt/antithesis/test/v1/quickstart/singleton_driver_regression.sh && \ + chmod +x /opt/antithesis/test/v1/quickstart/singleton_driver_regression.sh diff --git a/docker/Dockerfile.config b/docker/Dockerfile.config new file mode 100644 index 000000000..ce0461dd7 --- /dev/null +++ b/docker/Dockerfile.config @@ -0,0 +1,13 @@ +# Config image for Antithesis. +# +# Contains only the docker-compose.yaml that tells Antithesis how to run +# the services. Must be pushed to the Antithesis registry alongside the +# driver image. +# +# Build: +# docker build -f docker/Dockerfile.config \ +# -t us-central1-docker.pkg.dev//antithesis/config:latest . +# docker push us-central1-docker.pkg.dev//antithesis/config:latest + +FROM scratch +COPY docker/docker-compose.yaml /docker-compose.yaml diff --git a/docker/README.md b/docker/README.md index f9b78d17c..2cb54c300 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,54 +1,81 @@ -# Docker setup for cardano-node-tests (Antithesis/Moog) +# Docker setup for cardano-node-tests (Antithesis) -This directory contains the driver image and compose file for submitting -`cardano-node-tests` to Antithesis via the Moog platform. +This directory contains the driver image and compose files for submitting +`cardano-node-tests` to Antithesis. ## How it works -- `Dockerfile` โ€” minimal image: configures Nix and copies the repo into - `/work/`. No binaries are pre-built; `regression.sh` handles all setup at - runtime via its own Nix shebang. Requires `GIT_REVISION` build arg (the - current commit hash) so pytest can identify the revision without a `.git` - directory inside the image. -- `docker-compose.yaml` โ€” single `driver` service for Moog submission. +Antithesis environments have **no internet access** at runtime, so all +dependencies are baked into the image at build time: + +- `Dockerfile` โ€” builds the driver image. At build time it: + 1. Pre-builds `cardano-node`, `cardano-submit-api`, `cardano-cli`, and + `bech32` from `NODE_REV` into `/opt/cardano/` via `nix build`. + 2. Pre-warms the `testenv` dev shell and creates the Python venv at + `/opt/tests-venv/` with all project dependencies installed. + 3. Pre-warms the `base` dev shell so the `regression.sh` shebang resolves + from the local nix store without network access. + 4. Installs `antithesis_run.sh` as the Antithesis test driver at + `/opt/antithesis/test/v1/quickstart/singleton_driver_regression.sh`. + +- `antithesis_run.sh` โ€” container entrypoint that: + 1. Forces nix into offline mode (`offline = true`). + 2. Exports `CARDANO_PREBUILT_DIR=/opt/cardano` and `_VENV_DIR=/opt/tests-venv` + so `regression.sh` skips all downloads and uses the pre-built artefacts. + 3. Emits the Antithesis `setup_complete` lifecycle signal. + 4. Hands off to `.github/regression.sh`. + +- `Dockerfile.config` โ€” builds the Antithesis config image (`FROM scratch`) + containing only `docker-compose.yaml`. + +- `docker-compose.yaml` โ€” single `driver` service. ## Workflow -### 1. Build and push the image +### 1. Build and push the driver image ```bash docker build -f docker/Dockerfile \ --build-arg GIT_REVISION=$(git rev-parse HEAD) \ - -t ghcr.io/intersectmbo/cardano-node-tests-antithesis:latest . + --build-arg NODE_REV=master \ + -t ghcr.io/saratomaz/cardano-node-tests-antithesis:latest . -docker push ghcr.io/intersectmbo/cardano-node-tests-antithesis:latest +docker push ghcr.io/saratomaz/cardano-node-tests-antithesis:latest ``` -### 2. Validate the compose locally +`NODE_REV` is locked at build time โ€” the same binaries are used every run +regardless of what is on the `master` branch when the container starts. + +### 2. Build and push the config image + +```bash +docker build -f docker/Dockerfile.config \ + -t us-central1-docker.pkg.dev//antithesis/config:latest . + +docker push us-central1-docker.pkg.dev//antithesis/config:latest +``` + +### 3. Validate locally (internet-connected build, isolated network at runtime) ```bash docker compose -f docker/docker-compose.yaml config docker compose -f docker/docker-compose.yaml up --build ``` -### 3. Submit to Moog +To fully simulate the Antithesis no-internet constraint, run inside an +isolated network namespace on Linux: ```bash -moog requester create-test \ - --platform github \ - --username saratomaz \ - --repository IntersectMBO/cardano-node-tests \ - --directory ./docker \ - --commit $(git rev-parse HEAD) \ - --try 1 \ - --duration 2 +unshare -n docker compose -f docker/docker-compose.yaml up ``` ## Environment variables +`NODE_REV` is baked into the image at build time and must **not** be set at +runtime. All other variables are passed through docker-compose as before. + | Variable | Default | Description | |-------------------|------------|------------------------------------------| -| `NODE_REV` | `master` | cardano-node git revision | | `CARDANO_CLI_REV` | (built-in) | cardano-cli revision, empty = use node's | | `DBSYNC_REV` | (disabled) | db-sync revision, empty = disabled | | `RUN_TARGET` | `tests` | `tests`, `testpr`, or `testnets` | diff --git a/docker/antithesis_run.sh b/docker/antithesis_run.sh new file mode 100755 index 000000000..d4a6282e3 --- /dev/null +++ b/docker/antithesis_run.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Antithesis entrypoint for cardano-node-tests. +# +# Runs the full test suite without any network access by: +# 1. Forcing nix into offline mode (all store paths were pre-built into +# the image by docker/Dockerfile). +# 2. Pointing regression.sh at the pre-built cardano binaries and Python +# venv so it skips all download / build steps. +# 3. Emitting the Antithesis `setup_complete` lifecycle signal before +# starting pytest. +# +# This file is installed at: +# /opt/antithesis/test/v1/quickstart/singleton_driver_regression.sh +# and is also usable directly as the docker-compose `command`. + +set -Eeuo pipefail + +# --------------------------------------------------------------------------- +# 1. Force nix offline โ€” all required store paths were pre-built into the +# image. This prevents nix from attempting any network calls at runtime, +# which would fail inside the Antithesis environment. +# --------------------------------------------------------------------------- +echo "offline = true" >> /etc/nix/nix.conf + +# --------------------------------------------------------------------------- +# 2. Tell regression.sh to use the pre-built binaries and Python venv that +# were baked into the image at docker build time. +# --------------------------------------------------------------------------- +export CARDANO_PREBUILT_DIR=/opt/cardano +export _VENV_DIR=/opt/tests-venv + +# --------------------------------------------------------------------------- +# 3. Emit the Antithesis setup_complete signal. +# Written as JSONL to $ANTITHESIS_OUTPUT_DIR/sdk.jsonl. +# Antithesis begins fault injection / test orchestration after receiving +# this message. +# --------------------------------------------------------------------------- +_output_dir="${ANTITHESIS_OUTPUT_DIR:-/tmp/antithesis}" +mkdir -p "$_output_dir" +printf '{"antithesis_setup": {"status": "complete", "details": {"info": ["cardano-node-tests driver ready, node_rev=%s"]}}}\n' \ + "${BAKED_NODE_REV:-unknown}" >> "$_output_dir/sdk.jsonl" +unset _output_dir + +# --------------------------------------------------------------------------- +# 4. Hand off to regression.sh. The shebang in that script will invoke +# `nix develop .#base` which now resolves entirely from the local nix +# store (offline = true). +# --------------------------------------------------------------------------- +exec /work/.github/regression.sh diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 06f73d541..d8e82da32 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -1,25 +1,29 @@ -# Docker Compose for Antithesis/Moog test submission. +# Docker Compose for Antithesis test submission. # -# Submit to Moog: -# moog requester create-test \ -# --platform github --username \ -# --repository IntersectMBO/cardano-node-tests \ -# --directory ./docker \ -# --commit --try 1 --duration 2 +# The driver image must be pre-built with all cardano binaries and the Python +# venv baked in (see docker/Dockerfile). No internet access is available at +# runtime inside the Antithesis environment. # -# Validate locally before submitting: +# Push images to the Antithesis registry before submitting: +# docker push us-central1-docker.pkg.dev//antithesis/cardano-node-tests:latest +# docker push us-central1-docker.pkg.dev//antithesis/config:latest +# +# Validate locally (requires internet โ€” use an isolated netns to simulate +# the Antithesis environment): # docker compose -f docker/docker-compose.yaml config # docker compose -f docker/docker-compose.yaml up --build services: driver: - image: ghcr.io/intersectmbo/cardano-node-tests-antithesis:latest + image: ghcr.io/saratomaz/cardano-node-tests-antithesis:latest build: context: .. dockerfile: docker/Dockerfile - command: ["/work/.github/regression.sh"] + # antithesis_run.sh sets nix offline, exports pre-built paths, emits + # setup_complete, then hands off to regression.sh. + command: ["/work/docker/antithesis_run.sh"] environment: - - NODE_REV=${NODE_REV:-master} + # NODE_REV is baked into the image at build time; do not override here. - CARDANO_CLI_REV=${CARDANO_CLI_REV:-} - DBSYNC_REV=${DBSYNC_REV:-} - RUN_TARGET=${RUN_TARGET:-tests} From a3409994592b70076b36921a2b3be67816a1f1d4 Mon Sep 17 00:00:00 2001 From: saratomaz Date: Mon, 13 Apr 2026 15:43:03 +0100 Subject: [PATCH 3/5] Fix exit code from antithesis script --- docker/antithesis_run.sh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docker/antithesis_run.sh b/docker/antithesis_run.sh index d4a6282e3..613f898d8 100755 --- a/docker/antithesis_run.sh +++ b/docker/antithesis_run.sh @@ -45,5 +45,15 @@ unset _output_dir # 4. Hand off to regression.sh. The shebang in that script will invoke # `nix develop .#base` which now resolves entirely from the local nix # store (offline = true). -# --------------------------------------------------------------------------- -exec /work/.github/regression.sh +# +# Do not exec directly: Antithesis treats any non-zero container exit +# code (other than 137/143) as an error property violation. Test +# failures are expected and communicated via SDK assertions, not the +# process exit code. Always exit 0 so the container is not flagged. +# --------------------------------------------------------------------------- +set +e +/work/.github/regression.sh +_rc=$? +set -e +echo "regression.sh finished with exit code ${_rc}" +exit 0 From cd2e42238a6efa33058b9331872b4717ff0147cc Mon Sep 17 00:00:00 2001 From: saratomaz Date: Thu, 16 Apr 2026 10:30:55 +0100 Subject: [PATCH 4/5] fix docker antithesis, add antithesis-net bridge network to compose --- docker/docker-compose.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index d8e82da32..d30f59fbe 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -13,6 +13,10 @@ # docker compose -f docker/docker-compose.yaml config # docker compose -f docker/docker-compose.yaml up --build +networks: + antithesis-net: + driver: bridge + services: driver: image: ghcr.io/saratomaz/cardano-node-tests-antithesis:latest @@ -22,6 +26,8 @@ services: # antithesis_run.sh sets nix offline, exports pre-built paths, emits # setup_complete, then hands off to regression.sh. command: ["/work/docker/antithesis_run.sh"] + networks: + - antithesis-net environment: # NODE_REV is baked into the image at build time; do not override here. - CARDANO_CLI_REV=${CARDANO_CLI_REV:-} From efa4b95eec611f50464d70c07e38b31b76bd4f67 Mon Sep 17 00:00:00 2001 From: saratomaz Date: Thu, 16 Apr 2026 16:03:13 +0100 Subject: [PATCH 5/5] Docker antithesis - split into node and driver containers --- .github/regression.sh | 3 +- docker/Dockerfile | 5 +- docker/README.md | 6 +- docker/antithesis_run.sh | 79 ++++++++++++++++++++++---- docker/docker-compose.yaml | 56 +++++++++++++++++-- docker/node_run.sh | 112 +++++++++++++++++++++++++++++++++++++ 6 files changed, 241 insertions(+), 20 deletions(-) create mode 100755 docker/node_run.sh diff --git a/.github/regression.sh b/.github/regression.sh index 2ce4650bb..aa42e0e7b 100755 --- a/.github/regression.sh +++ b/.github/regression.sh @@ -75,7 +75,8 @@ elif is_truthy "${CI_BYRON_CLUSTER:-}"; then export TESTNET_VARIANT="${CLUSTER_ERA:-conway}_slow" fi -export CARDANO_NODE_SOCKET_PATH_CI="$WORKDIR/state-cluster0/bft1.socket" +CARDANO_NODE_SOCKET_PATH_CI="${CARDANO_NODE_SOCKET_PATH_CI:-$WORKDIR/state-cluster0/bft1.socket}" +export CARDANO_NODE_SOCKET_PATH_CI # assume we run tests on testnet when `BOOTSTRAP_DIR` is set if [ -n "${BOOTSTRAP_DIR:-}" ]; then diff --git a/docker/Dockerfile b/docker/Dockerfile index 0bae1dfe6..81f2b4f24 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -67,9 +67,10 @@ RUN nix develop --accept-flake-config .#testenv --command \ # paths are cached and the regression.sh shebang resolves offline. RUN nix develop --accept-flake-config .#base --command true -# Create the Antithesis test driver directory and install the entry-point. +# Create the Antithesis test driver directory and install the entry-points. # singleton_driver_* files are run once per test run by Antithesis. RUN mkdir -p /opt/antithesis/test/v1/quickstart && \ cp /work/docker/antithesis_run.sh \ /opt/antithesis/test/v1/quickstart/singleton_driver_regression.sh && \ - chmod +x /opt/antithesis/test/v1/quickstart/singleton_driver_regression.sh + chmod +x /opt/antithesis/test/v1/quickstart/singleton_driver_regression.sh && \ + chmod +x /work/docker/node_run.sh diff --git a/docker/README.md b/docker/README.md index 2cb54c300..c38088a94 100644 --- a/docker/README.md +++ b/docker/README.md @@ -28,7 +28,11 @@ dependencies are baked into the image at build time: - `Dockerfile.config` โ€” builds the Antithesis config image (`FROM scratch`) containing only `docker-compose.yaml`. -- `docker-compose.yaml` โ€” single `driver` service. +- `docker-compose.yaml` โ€” two services: `node` (cardano-node cluster) and + `driver` (pytest). Both share a `cluster-state` Docker volume so the + driver accesses the node sockets without going over the network. An HTTP + health check on port 8090 provides cross-container traffic that satisfies + the Antithesis "Containers joined the Antithesis network" property. ## Workflow diff --git a/docker/antithesis_run.sh b/docker/antithesis_run.sh index 613f898d8..bcfdf4f69 100755 --- a/docker/antithesis_run.sh +++ b/docker/antithesis_run.sh @@ -1,17 +1,27 @@ #!/usr/bin/env bash -# Antithesis entrypoint for cardano-node-tests. +# Antithesis driver container entrypoint. # # Runs the full test suite without any network access by: # 1. Forcing nix into offline mode (all store paths were pre-built into # the image by docker/Dockerfile). # 2. Pointing regression.sh at the pre-built cardano binaries and Python # venv so it skips all download / build steps. -# 3. Emitting the Antithesis `setup_complete` lifecycle signal before -# starting pytest. +# 3. When NODE_HOST is set (multi-container mode): waiting for the node +# container's health check on port 8090 before running tests, and +# setting DEV_CLUSTER_RUNNING=1 so pytest uses the pre-running cluster +# instead of starting its own. +# 4. Emitting the Antithesis setup_complete lifecycle signal. +# 5. Handing off to regression.sh. +# +# Multi-container environment variables (set in docker-compose): +# NODE_HOST Hostname of the node container (default: unset). +# NODE_PORT Health check port on the node container (default: 8090). +# CLUSTER_STATE_DIR Mount point of the shared cluster-state volume +# (default: /cluster-state). # # This file is installed at: # /opt/antithesis/test/v1/quickstart/singleton_driver_regression.sh -# and is also usable directly as the docker-compose `command`. +# and is also usable directly as the docker-compose command. set -Eeuo pipefail @@ -29,20 +39,67 @@ echo "offline = true" >> /etc/nix/nix.conf export CARDANO_PREBUILT_DIR=/opt/cardano export _VENV_DIR=/opt/tests-venv -# --------------------------------------------------------------------------- -# 3. Emit the Antithesis setup_complete signal. -# Written as JSONL to $ANTITHESIS_OUTPUT_DIR/sdk.jsonl. -# Antithesis begins fault injection / test orchestration after receiving -# this message. -# --------------------------------------------------------------------------- _output_dir="${ANTITHESIS_OUTPUT_DIR:-/tmp/antithesis}" mkdir -p "$_output_dir" + +# --------------------------------------------------------------------------- +# 3. Multi-container mode: wait for the node container and configure the +# driver to use the pre-running cluster. +# +# When NODE_HOST is set the driver polls the node's HTTP health endpoint +# (port 8090) until it responds "ready". This HTTP traffic is what makes +# both containers visible on the Antithesis network bridge. +# +# DEV_CLUSTER_RUNNING=1 tells pytest to skip cluster startup/shutdown and +# use the cluster already started by the node container. +# CARDANO_NODE_SOCKET_PATH_CI is pre-set to the shared volume socket path +# so regression.sh does not override it with its default workdir path. +# --------------------------------------------------------------------------- +if [ -n "${NODE_HOST:-}" ]; then + _node_port="${NODE_PORT:-8090}" + echo "Waiting for ${NODE_HOST}:${_node_port} to report ready..." + + _ready=0 + for _i in $(seq 1 120); do + # Use the venv's Python directly โ€” python3 is not in PATH outside a nix shell. + _resp="$("${_VENV_DIR}/bin/python3" -c " +import urllib.request, sys +try: + r = urllib.request.urlopen('http://${NODE_HOST}:${_node_port}/', timeout=5) + sys.stdout.write(r.read().decode()) +except Exception: + pass +" 2>/dev/null || true)" + if [ "$_resp" = "ready" ]; then + _ready=1 + break + fi + echo " attempt ${_i}/120: node reports '${_resp:-no response}', retrying in 5s..." + sleep 5 + done + + if [ "$_ready" -ne 1 ]; then + echo "ERROR: node container did not become ready within 10 minutes" >&2 + exit 1 + fi + echo "Node is ready." + + CLUSTER_STATE_DIR="${CLUSTER_STATE_DIR:-/cluster-state}" + export DEV_CLUSTER_RUNNING=1 + export CLUSTERS_COUNT="${CLUSTERS_COUNT:-1}" + # Pre-set so regression.sh does not overwrite with its default workdir path. + export CARDANO_NODE_SOCKET_PATH_CI="${CLUSTER_STATE_DIR}/state-cluster0/bft1.socket" +fi + +# --------------------------------------------------------------------------- +# 4. Emit the Antithesis setup_complete signal. +# --------------------------------------------------------------------------- printf '{"antithesis_setup": {"status": "complete", "details": {"info": ["cardano-node-tests driver ready, node_rev=%s"]}}}\n' \ "${BAKED_NODE_REV:-unknown}" >> "$_output_dir/sdk.jsonl" unset _output_dir # --------------------------------------------------------------------------- -# 4. Hand off to regression.sh. The shebang in that script will invoke +# 5. Hand off to regression.sh. The shebang in that script will invoke # `nix develop .#base` which now resolves entirely from the local nix # store (offline = true). # diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index d30f59fbe..a86e1facb 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -1,7 +1,18 @@ # Docker Compose for Antithesis test submission. # -# The driver image must be pre-built with all cardano binaries and the Python -# venv baked in (see docker/Dockerfile). No internet access is available at +# Two services share a cluster-state volume: +# +# node โ€” starts the cardano-node cluster (system under test). +# Serves a health check on port 8090 so the driver can detect +# when the cluster is ready. The traffic between driver and node +# over the antithesis-net bridge satisfies the Antithesis +# "Containers joined the Antithesis network" property. +# +# driver โ€” waits for the node health check, then runs the pytest test +# suite against the pre-running cluster via DEV_CLUSTER_RUNNING=1. +# +# Both images must be pre-built with all cardano binaries and the Python venv +# baked in (see docker/Dockerfile). No internet access is available at # runtime inside the Antithesis environment. # # Push images to the Antithesis registry before submitting: @@ -17,24 +28,59 @@ networks: antithesis-net: driver: bridge +volumes: + cluster-state: + services: + node: + image: ghcr.io/saratomaz/cardano-node-tests-antithesis:latest + build: + context: .. + dockerfile: docker/Dockerfile + command: ["/work/docker/node_run.sh"] + networks: + - antithesis-net + volumes: + - cluster-state:/cluster-state + environment: + - CLUSTER_STATE_DIR=/cluster-state + - TESTNET_VARIANT=${TESTNET_VARIANT:-conway_fast} + healthcheck: + test: + - "CMD" + - "/opt/tests-venv/bin/python3" + - "-c" + - "import urllib.request; exit(0 if urllib.request.urlopen('http://localhost:8090/', timeout=5).read() == b'ready' else 1)" + interval: 15s + timeout: 6s + retries: 60 + start_period: 60s + driver: image: ghcr.io/saratomaz/cardano-node-tests-antithesis:latest build: context: .. dockerfile: docker/Dockerfile - # antithesis_run.sh sets nix offline, exports pre-built paths, emits - # setup_complete, then hands off to regression.sh. + # antithesis_run.sh sets nix offline, waits for the node health check, + # exports DEV_CLUSTER_RUNNING=1, emits setup_complete, then hands off + # to regression.sh. command: ["/work/docker/antithesis_run.sh"] networks: - antithesis-net + depends_on: + - node + volumes: + - cluster-state:/cluster-state environment: + - CLUSTER_STATE_DIR=/cluster-state + - NODE_HOST=node + - NODE_PORT=8090 # NODE_REV is baked into the image at build time; do not override here. - CARDANO_CLI_REV=${CARDANO_CLI_REV:-} - DBSYNC_REV=${DBSYNC_REV:-} - RUN_TARGET=${RUN_TARGET:-tests} - MARKEXPR=${MARKEXPR:-} - - CLUSTERS_COUNT=${CLUSTERS_COUNT:-} + - CLUSTERS_COUNT=${CLUSTERS_COUNT:-1} - CLUSTER_ERA=${CLUSTER_ERA:-} - PROTOCOL_VERSION=${PROTOCOL_VERSION:-} - UTXO_BACKEND=${UTXO_BACKEND:-} diff --git a/docker/node_run.sh b/docker/node_run.sh new file mode 100755 index 000000000..4d6ef6ab8 --- /dev/null +++ b/docker/node_run.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# Antithesis node container entrypoint. +# +# 1. Starts the cardano-node cluster on the shared 'cluster-state' volume so +# the driver container can reach the node sockets without going over the +# network (Unix socket on a shared Docker volume). +# 2. Serves a lightweight HTTP health check on port 8090 over the Antithesis +# network bridge. Returns "ready" once the cluster socket exists. +# This cross-container HTTP traffic satisfies the Antithesis +# "Containers joined the Antithesis network" property. +# +# Environment variables: +# CLUSTER_STATE_DIR Mount point of the shared cluster-state volume +# (default: /cluster-state). +# TESTNET_VARIANT Cluster variant passed to prepare_cluster_scripts +# (default: conway_fast). + +set -Eeuo pipefail + +# --------------------------------------------------------------------------- +# 1. Force nix offline โ€” all store paths are pre-built into the image. +# --------------------------------------------------------------------------- +echo "offline = true" >> /etc/nix/nix.conf + +# --------------------------------------------------------------------------- +# 2. Point at pre-built binaries and Python venv. +# All variables are exported so the inner nix shell inherits them. +# --------------------------------------------------------------------------- +export CARDANO_PREBUILT_DIR=/opt/cardano +export _VENV_DIR=/opt/tests-venv +export _PATH_PREPEND="/opt/cardano/cardano-node/bin:/opt/cardano/cardano-submit-api/bin:/opt/cardano/cardano-cli/bin:/opt/cardano/bech32/bin" + +# --------------------------------------------------------------------------- +# 3. Cluster state lives on the shared volume so the driver can read sockets. +# --------------------------------------------------------------------------- +CLUSTER_STATE_DIR="${CLUSTER_STATE_DIR:-/cluster-state}" +export _INSTANCE_NUM=0 +export _STATE_CLUSTER="${CLUSTER_STATE_DIR}/state-cluster${_INSTANCE_NUM}" +export _SCRIPTS_DEST="${CLUSTER_STATE_DIR}/startup_scripts" +export CLUSTER_STATE_DIR + +# Local clusters (conway_fast, etc.) use bft1.socket. +export CARDANO_NODE_SOCKET_PATH="${_STATE_CLUSTER}/bft1.socket" + +export _output_dir="${ANTITHESIS_OUTPUT_DIR:-/tmp/antithesis}" +mkdir -p "$_output_dir" "${CLUSTER_STATE_DIR}" + +# --------------------------------------------------------------------------- +# 4. Health check server on port 8090 (Antithesis network bridge traffic). +# Returns HTTP 200 "ready" once the cluster socket file exists, +# 503 "starting" while the cluster is still coming up. +# Uses the venv Python directly โ€” python3 is not in PATH outside a nix shell. +# --------------------------------------------------------------------------- +"${_VENV_DIR}/bin/python3" -c " +import os, socket as _s +_sock_path = os.environ.get('CARDANO_NODE_SOCKET_PATH', '') +server = _s.socket(_s.AF_INET, _s.SOCK_STREAM) +server.setsockopt(_s.SOL_SOCKET, _s.SO_REUSEADDR, 1) +server.bind(('0.0.0.0', 8090)) +server.listen(64) +while True: + conn, _ = server.accept() + ready = os.path.exists(_sock_path) + body = b'ready' if ready else b'starting' + status = b'200 OK' if ready else b'503 Service Unavailable' + conn.sendall(b'HTTP/1.1 ' + status + b'\r\nContent-Length: ' + str(len(body)).encode() + b'\r\n\r\n' + body) + conn.close() +" & +_health_pid=$! +trap 'kill "$_health_pid" 2>/dev/null || true' EXIT + +# --------------------------------------------------------------------------- +# 5. Prepare cluster startup scripts and run the cluster. +# The inner script uses single quotes so the outer shell does NOT expand +# variables โ€” the nix shell inherits all exported vars above and the inner +# bash expands them from its environment. This avoids PATH corruption from +# nested quoting (single quotes inside a double-quoted string are literal +# and prevent $PATH from being expanded in the inner shell). +# --------------------------------------------------------------------------- +export _testnet_variant="${TESTNET_VARIANT:-conway_fast}" + +set +e +# shellcheck disable=SC2016 +nix develop --accept-flake-config .#testenv --command bash -c ' + set -euo pipefail + . "$_VENV_DIR/bin/activate" + export PATH="$_PATH_PREPEND:$PATH" + + # Instantiate cluster scripts for instance $_INSTANCE_NUM into the + # shared volume. --clean removes any previous attempt. + python3 -m cardano_node_tests.prepare_cluster_scripts \ + --dest-dir "$_SCRIPTS_DEST" \ + --testnet-variant "$_testnet_variant" \ + --instance-num "$_INSTANCE_NUM" \ + --clean + + # start-cluster must run from the parent of the state-cluster directory. + cd "$CLUSTER_STATE_DIR" + "$_SCRIPTS_DEST/start-cluster" + + # shellcheck disable=SC2016 + printf '"'"'{"antithesis_setup": {"status": "complete", "details": {"info": ["cardano-node cluster ready, socket=%s"]}}}\n'"'"' \ + "$CARDANO_NODE_SOCKET_PATH" >> "$_output_dir/sdk.jsonl" + + # Keep the cluster alive until the container is stopped. + tail -f /dev/null +' +_rc=$? +set -e + +echo "node_run.sh exiting with code ${_rc}" +exit 0