From d1f4e574a5b6ca0564ce8fae0d7ecda25f2676e9 Mon Sep 17 00:00:00 2001 From: Toby Bellwood Date: Fri, 13 Mar 2026 19:04:29 +1100 Subject: [PATCH 1/5] test: add BATS tests --- .github/workflows/build_and_test.yml | 74 ++++++----- Makefile | 57 +++++++++ tests/image_structure.bats | 124 ++++++++++++++++++ tests/runtime.bats | 184 +++++++++++++++++++++++++++ 4 files changed, 408 insertions(+), 31 deletions(-) create mode 100644 Makefile create mode 100644 tests/image_structure.bats create mode 100644 tests/runtime.bats diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 98286d5..c8faf30 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -17,11 +17,11 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 with: # list of Docker images to use as base name for tags images: | @@ -32,26 +32,26 @@ jobs: org.opencontainers.image.description=dnsmasq DNS proxy, configured for use with the pygmy stack - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: . platforms: linux/amd64,linux/arm64 @@ -61,15 +61,15 @@ jobs: test: needs: docker - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # Establish some SSH keys. - name: Setup SSH run: | - eval $(ssh-agent); + eval "$(ssh-agent)"; ssh-keygen -t rsa -q -f "$HOME/.ssh/id_rsa" -N ""; ssh-keygen -t rsa -q -f "$HOME/.ssh/id_pwd" -N "passphrase"; ssh-add; @@ -77,7 +77,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 with: # list of Docker images to use as base name for tags images: | @@ -86,24 +86,36 @@ jobs: latest=false - name: Find and Replace - uses: jacobtomlinson/gha-find-replace@v3 - with: - find: "ghcr.io/pygmystack/haproxy:main" - replace: ${{ steps.meta.outputs.tags }} - include: "examples/**" - - - name: Show changes + env: + IMAGE_TAG: ${{ steps.meta.outputs.tags }} run: | + find examples/ -type f -exec sed -i.bak "s|ghcr.io/pygmystack/haproxy:main|${IMAGE_TAG}|g" {} \; + find examples/ -name "*.bak" -delete grep -n ghcr examples/* - - name: Install pygmy and dockerize via brew + name: Set up Homebrew + uses: Homebrew/actions/setup-homebrew@main + - + name: Install homebrew packages + env: + HOMEBREW_NO_AUTO_UPDATE: 1 + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 + HOMEBREW_NO_ENV_HINTS: 1 run: | - eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"; - brew tap pygmystack/pygmy; - brew install pygmy; + brew install bats-core; brew install dockerize; - echo "/home/linuxbrew/.linuxbrew/bin" >> $GITHUB_PATH; + brew install pygmystack/pygmy/pygmy; pygmy version; + - + name: Pull image for tests + run: docker pull ${{ steps.meta.outputs.tags }} + - + name: Run BATS tests + env: + IMAGE_NAME: ${{ steps.meta.outputs.tags }} + run: | + bats --tap tests/image_structure.bats + bats --tap tests/runtime.bats - name: Switch pygmy configs from vanilla to basic run: | @@ -123,7 +135,7 @@ jobs: pygmy --config examples/pygmy.basic.yml export -o ./exported-config.yml cat ./exported-config.yml echo "Checking image references in started containers..."; - docker container inspect amazeeio-haproxy | jq '.[].Config.Image' | grep '${{ steps.meta.outputs.tags }}'; + docker container inspect amazeeio-haproxy | jq '.[].Config.Image' | grep -F '${{ steps.meta.outputs.tags }}'; - name: Resolv file test run: | @@ -185,7 +197,7 @@ jobs: pygmy --config examples/pygmy.yml export -o ./exported-config-2.yml cat ./exported-config-2.yml echo "Checking image references in started containers..."; - docker container inspect amazeeio-haproxy | jq '.[].Config.Image' | grep '${{ steps.meta.outputs.tags }}'; + docker container inspect amazeeio-haproxy | jq '.[].Config.Image' | grep -F '${{ steps.meta.outputs.tags }}'; - name: SSH Key test run: | @@ -201,14 +213,14 @@ jobs: name: "[Example] Drupal Base" run: | cd lagoon-examples/drupal-base; - docker-compose -p drupal-base up -d; - docker-compose -p drupal-base exec -T cli composer install; + docker compose -p drupal-base up -d; + docker compose -p drupal-base exec -T cli composer install; dockerize -wait http://drupal-base.docker.amazee.io:80 -timeout 10s; - curl --HEAD http://drupal-base.docker.amazee.io; - curl --HEAD http://drupal-base.docker.amazee.io | grep -i "x-lagoon"; + curl --head http://drupal-base.docker.amazee.io; + curl --head http://drupal-base.docker.amazee.io | grep -i "x-lagoon"; pygmy --config examples/pygmy.yml status | grep '\- http://drupal-base.docker.amazee.io'; - docker-compose -p drupal-base down; - docker-compose -p drupal-base rm; + docker compose -p drupal-base down; + docker compose -p drupal-base rm -f; cd ../../; - name: Test the stop command diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3c18d65 --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +IMAGE_NAME ?= pygmystack/haproxy +IMAGE_TAG ?= test +FULL_IMAGE := $(IMAGE_NAME):$(IMAGE_TAG) + +.DEFAULT_GOAL := help + +.PHONY: help build test test-bats test-structure test-runtime validate-config up down shell clean + +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ + awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}' + +build: ## Build the Docker image + docker build --tag $(FULL_IMAGE) . + +test: build ## Build image and run all tests (requires: brew install bats-core) + @command -v bats >/dev/null 2>&1 || { \ + echo "Error: bats is not installed. Install with: brew install bats-core"; \ + exit 1; \ + } + IMAGE_NAME=$(FULL_IMAGE) bats --tap tests/ + +test-bats: ## Run all BATS tests without rebuilding the image + @command -v bats >/dev/null 2>&1 || { \ + echo "Error: bats is not installed. Install with: brew install bats-core"; \ + exit 1; \ + } + IMAGE_NAME=$(FULL_IMAGE) bats --tap tests/ + +test-structure: ## Run image structure tests only (no Docker socket required) + @command -v bats >/dev/null 2>&1 || { \ + echo "Error: bats is not installed. Install with: brew install bats-core"; \ + exit 1; \ + } + IMAGE_NAME=$(FULL_IMAGE) bats --tap tests/image_structure.bats + +test-runtime: ## Run container runtime and integration tests (Docker socket required) + @command -v bats >/dev/null 2>&1 || { \ + echo "Error: bats is not installed. Install with: brew install bats-core"; \ + exit 1; \ + } + IMAGE_NAME=$(FULL_IMAGE) bats --tap tests/runtime.bats + +validate-config: ## Validate the default haproxy.cfg syntax + docker run --rm $(FULL_IMAGE) haproxy -c -f /app/haproxy.cfg + +up: ## Start the stack with docker-compose + docker-compose up -d + +down: ## Stop the stack with docker-compose + docker-compose down + +shell: ## Open an interactive shell inside the container + docker run --rm -it --entrypoint bash $(FULL_IMAGE) + +clean: ## Remove the local test Docker image + docker rmi $(FULL_IMAGE) 2>/dev/null || true diff --git a/tests/image_structure.bats b/tests/image_structure.bats new file mode 100644 index 0000000..3e13b9b --- /dev/null +++ b/tests/image_structure.bats @@ -0,0 +1,124 @@ +#!/usr/bin/env bats +# Image structure tests — verify binaries, files, and configuration baked into +# the image. These tests run ephemeral containers and do not require access to +# the Docker socket. + +IMAGE="${IMAGE_NAME:-pygmystack/haproxy:test}" + +# --------------------------------------------------------------------------- +# Binaries +# --------------------------------------------------------------------------- + +@test "haproxy binary is available in PATH" { + run docker run --rm --entrypoint which "${IMAGE}" haproxy + [ "$status" -eq 0 ] + [ -n "$output" ] +} + +@test "haproxy version is 3.3.x" { + run docker run --rm "${IMAGE}" sh -c 'haproxy -v 2>&1' + [ "$status" -eq 0 ] + [[ "$output" =~ "3.3" ]] +} + +@test "docker-gen binary is available in PATH" { + run docker run --rm --entrypoint which "${IMAGE}" docker-gen + [ "$status" -eq 0 ] + [ -n "$output" ] +} + +@test "bash is installed" { + run docker run --rm "${IMAGE}" sh -c 'bash --version 2>&1' + [ "$status" -eq 0 ] + [[ "$output" =~ "GNU bash" ]] +} + +# --------------------------------------------------------------------------- +# Required files +# --------------------------------------------------------------------------- + +@test "/app/haproxy.cfg exists" { + run docker run --rm "${IMAGE}" test -f /app/haproxy.cfg + [ "$status" -eq 0 ] +} + +@test "/app/haproxy.tmpl exists" { + run docker run --rm "${IMAGE}" test -f /app/haproxy.tmpl + [ "$status" -eq 0 ] +} + +@test "/app/docker-entrypoint.sh is executable" { + run docker run --rm "${IMAGE}" test -x /app/docker-entrypoint.sh + [ "$status" -eq 0 ] +} + +@test "/app/haproxy_start.sh is executable" { + run docker run --rm "${IMAGE}" test -x /app/haproxy_start.sh + [ "$status" -eq 0 ] +} + +@test "/app/haproxy_reload.sh is executable" { + run docker run --rm "${IMAGE}" test -x /app/haproxy_reload.sh + [ "$status" -eq 0 ] +} + +# --------------------------------------------------------------------------- +# Container environment +# --------------------------------------------------------------------------- + +@test "working directory is /app" { + run docker run --rm --entrypoint sh "${IMAGE}" -c 'pwd' + [ "$status" -eq 0 ] + [ "$output" = "/app" ] +} + +@test "DOCKER_HOST is set to the expected unix socket path" { + run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' "${IMAGE}" + [ "$status" -eq 0 ] + [[ "$output" =~ "DOCKER_HOST=unix:///tmp/docker.sock" ]] +} + +# --------------------------------------------------------------------------- +# HAProxy config validation +# --------------------------------------------------------------------------- + +@test "haproxy config checker accepts a valid config" { + # The baked-in haproxy.cfg is an intentionally minimal bootstrap config + # (no backend) that docker-gen immediately replaces at runtime. Instead, + # verify that the haproxy binary itself correctly validates a well-formed + # config, proving the binary is functional. + run docker run --rm --entrypoint sh "${IMAGE}" -c ' +cat > /tmp/valid.cfg <&3 + return 0 + fi + + # Remove any leftover container from a previous (failed) run. + docker rm -f "${HAPROXY_CONTAINER}" 2>/dev/null || true + + docker run -d \ + --name "${HAPROXY_CONTAINER}" \ + --volume "${DOCKER_SOCKET}:/tmp/docker.sock" \ + -p "${TEST_PORT}:80" \ + "${IMAGE}" + + # Wait for docker-gen to run once and reload haproxy with the stats frontend. + # The template includes "stats uri /stats", so /stats becomes available only + # after the first docker-gen pass — give it up to 30 seconds. + local max_wait=30 + local waited=0 + until curl -sf "http://localhost:${TEST_PORT}/stats" >/dev/null 2>&1; do + sleep 1 + waited=$((waited + 1)) + if [ "$waited" -ge "$max_wait" ]; then + echo "# Timed out waiting for haproxy /stats endpoint" >&3 + docker logs "${HAPROXY_CONTAINER}" >&3 2>&3 + break + fi + done +} + +teardown_file() { + docker rm -f "${HAPROXY_CONTAINER}" 2>/dev/null || true + docker rm -f "${BACKEND_AMAZEEIO}" 2>/dev/null || true + docker rm -f "${BACKEND_LAGOON}" 2>/dev/null || true +} + +# --------------------------------------------------------------------------- +# Helper — skip the current test when the Docker socket is absent. +# --------------------------------------------------------------------------- + +_require_docker_socket() { + if [ ! -S "${DOCKER_SOCKET}" ]; then + skip "Docker socket not available at ${DOCKER_SOCKET}" + fi +} + +# --------------------------------------------------------------------------- +# Container lifecycle +# --------------------------------------------------------------------------- + +@test "container is running" { + _require_docker_socket + run docker inspect --format='{{.State.Status}}' "${HAPROXY_CONTAINER}" + [ "$status" -eq 0 ] + [ "$output" = "running" ] +} + +@test "haproxy process is alive (pidfile check)" { + _require_docker_socket + run docker exec "${HAPROXY_CONTAINER}" sh -c \ + 'kill -0 "$(cat /var/run/haproxy.pid)" 2>/dev/null' + [ "$status" -eq 0 ] +} + +@test "docker-gen process is running inside the container" { + _require_docker_socket + run docker exec "${HAPROXY_CONTAINER}" sh -c \ + 'ps aux 2>/dev/null | grep -c "[d]ocker-gen"' + [ "$status" -eq 0 ] + [ "$output" -ge 1 ] +} + +# --------------------------------------------------------------------------- +# HAProxy stats page — mirrors the GitHub Actions "haproxy test" step. +# --------------------------------------------------------------------------- + +@test "stats page is accessible on port 80" { + _require_docker_socket + run curl -sf "http://localhost:${TEST_PORT}/stats" + [ "$status" -eq 0 ] +} + +@test "stats page contains HAProxy version information" { + _require_docker_socket + run curl -s "http://localhost:${TEST_PORT}/stats" + [ "$status" -eq 0 ] + [[ "$output" =~ "HAProxy version" ]] +} + +@test "stats page contains the statistics table" { + _require_docker_socket + run curl -s "http://localhost:${TEST_PORT}/stats" + [ "$status" -eq 0 ] + [[ "$output" =~ "class=px" ]] +} + +# --------------------------------------------------------------------------- +# Integration — backend containers appear in the generated haproxy config. +# These tests mirror the "mailhog test" / backend discovery steps in CI. +# --------------------------------------------------------------------------- + +@test "backend with AMAZEEIO env is added to haproxy config" { + _require_docker_socket + + local backend_host="test-amazeeio.docker.amazee.io" + + docker rm -f "${BACKEND_AMAZEEIO}" 2>/dev/null || true + docker run -d \ + --name "${BACKEND_AMAZEEIO}" \ + -e AMAZEEIO=AMAZEEIO \ + -e "AMAZEEIO_URL=${backend_host}" \ + -e AMAZEEIO_HTTP_PORT=80 \ + -p 80 \ + nginx:alpine + + # Wait for docker-gen to detect the new container and reload haproxy. + local max_wait=20 + local waited=0 + until curl -s "http://localhost:${TEST_PORT}/stats" | grep -q "${backend_host}"; do + sleep 1 + waited=$((waited + 1)) + [ "$waited" -lt "$max_wait" ] || break + done + + run curl -s "http://localhost:${TEST_PORT}/stats" + [ "$status" -eq 0 ] + [[ "$output" =~ "${backend_host}" ]] + + docker rm -f "${BACKEND_AMAZEEIO}" 2>/dev/null || true +} + +@test "backend with LAGOON_LOCALDEV_HTTP_PORT env is added to haproxy config" { + _require_docker_socket + + local backend_host="lagoon-test.docker.amazee.io" + + docker rm -f "${BACKEND_LAGOON}" 2>/dev/null || true + docker run -d \ + --name "${BACKEND_LAGOON}" \ + -e LAGOON_LOCALDEV_HTTP_PORT=8080 \ + -e "LAGOON_ROUTE=http://${backend_host}" \ + -p 8080 \ + nginx:alpine + + # Wait for docker-gen to detect the new container and reload haproxy. + local max_wait=20 + local waited=0 + until curl -s "http://localhost:${TEST_PORT}/stats" | grep -q "${backend_host}"; do + sleep 1 + waited=$((waited + 1)) + [ "$waited" -lt "$max_wait" ] || break + done + + run curl -s "http://localhost:${TEST_PORT}/stats" + [ "$status" -eq 0 ] + [[ "$output" =~ "${backend_host}" ]] + + docker rm -f "${BACKEND_LAGOON}" 2>/dev/null || true +} From 563b15cedf5872483fbe39c1dfc0906ea0efc4e5 Mon Sep 17 00:00:00 2001 From: Toby Bellwood Date: Fri, 13 Mar 2026 19:10:26 +1100 Subject: [PATCH 2/5] chore: fix BATS test --- tests/image_structure.bats | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/image_structure.bats b/tests/image_structure.bats index 3e13b9b..ca8ae30 100644 --- a/tests/image_structure.bats +++ b/tests/image_structure.bats @@ -15,10 +15,10 @@ IMAGE="${IMAGE_NAME:-pygmystack/haproxy:test}" [ -n "$output" ] } -@test "haproxy version is 3.3.x" { +@test "haproxy version is 2.9.x" { run docker run --rm "${IMAGE}" sh -c 'haproxy -v 2>&1' [ "$status" -eq 0 ] - [[ "$output" =~ "3.3" ]] + [[ "$output" =~ "2.9" ]] } @test "docker-gen binary is available in PATH" { From 7b109dd16887f7bf95d7c189e849519b0ff7743b Mon Sep 17 00:00:00 2001 From: Toby Bellwood Date: Mon, 16 Mar 2026 12:29:33 +1100 Subject: [PATCH 3/5] fix: update BATS tests for dynamic container naming and port discovery --- .github/workflows/build_and_test.yml | 17 ++++++--- Makefile | 8 ++-- tests/runtime.bats | 55 +++++++++++++++++++++++----- 3 files changed, 60 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index c8faf30..628edf5 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -84,17 +84,22 @@ jobs: ghcr.io/${{ github.repository_owner }}/haproxy flavor: | latest=false + - + name: Set single image tag + id: single_tag + run: | + echo "tag=$(echo '${{ steps.meta.outputs.tags }}' | head -n1)" >> $GITHUB_OUTPUT - name: Find and Replace env: - IMAGE_TAG: ${{ steps.meta.outputs.tags }} + IMAGE_TAG: ${{ steps.single_tag.outputs.tag }} run: | find examples/ -type f -exec sed -i.bak "s|ghcr.io/pygmystack/haproxy:main|${IMAGE_TAG}|g" {} \; find examples/ -name "*.bak" -delete grep -n ghcr examples/* - name: Set up Homebrew - uses: Homebrew/actions/setup-homebrew@main + uses: Homebrew/actions/setup-homebrew@fb0fe15d936a80ac0ba4c6cee691bc3da3f00f4e # main - name: Install homebrew packages env: @@ -108,11 +113,11 @@ jobs: pygmy version; - name: Pull image for tests - run: docker pull ${{ steps.meta.outputs.tags }} + run: docker pull ${{ steps.single_tag.outputs.tag }} - name: Run BATS tests env: - IMAGE_NAME: ${{ steps.meta.outputs.tags }} + IMAGE_NAME: ${{ steps.single_tag.outputs.tag }} run: | bats --tap tests/image_structure.bats bats --tap tests/runtime.bats @@ -135,7 +140,7 @@ jobs: pygmy --config examples/pygmy.basic.yml export -o ./exported-config.yml cat ./exported-config.yml echo "Checking image references in started containers..."; - docker container inspect amazeeio-haproxy | jq '.[].Config.Image' | grep -F '${{ steps.meta.outputs.tags }}'; + docker container inspect amazeeio-haproxy | jq '.[].Config.Image' | grep -F '${{ steps.single_tag.outputs.tag }}'; - name: Resolv file test run: | @@ -197,7 +202,7 @@ jobs: pygmy --config examples/pygmy.yml export -o ./exported-config-2.yml cat ./exported-config-2.yml echo "Checking image references in started containers..."; - docker container inspect amazeeio-haproxy | jq '.[].Config.Image' | grep -F '${{ steps.meta.outputs.tags }}'; + docker container inspect amazeeio-haproxy | jq '.[].Config.Image' | grep -F '${{ steps.single_tag.outputs.tag }}'; - name: SSH Key test run: | diff --git a/Makefile b/Makefile index 3c18d65..700d474 100644 --- a/Makefile +++ b/Makefile @@ -44,11 +44,11 @@ test-runtime: ## Run container runtime and integration tests (Docker socket requ validate-config: ## Validate the default haproxy.cfg syntax docker run --rm $(FULL_IMAGE) haproxy -c -f /app/haproxy.cfg -up: ## Start the stack with docker-compose - docker-compose up -d +up: ## Start the stack with docker compose + docker compose up -d -down: ## Stop the stack with docker-compose - docker-compose down +down: ## Stop the stack with docker compose + docker compose down shell: ## Open an interactive shell inside the container docker run --rm -it --entrypoint bash $(FULL_IMAGE) diff --git a/tests/runtime.bats b/tests/runtime.bats index 887960e..eb187c4 100644 --- a/tests/runtime.bats +++ b/tests/runtime.bats @@ -12,17 +12,31 @@ bats_require_minimum_version 1.5.0 IMAGE="${IMAGE_NAME:-pygmystack/haproxy:test}" -HAPROXY_CONTAINER="haproxy-bats-test" -BACKEND_AMAZEEIO="haproxy-bats-backend-amazeeio" -BACKEND_LAGOON="haproxy-bats-backend-lagoon" + +# Container name variables are set in setup() by reading the suffix written by +# setup_file(). This ensures all tests share the same names despite BATS +# re-sourcing the file for each test. +HAPROXY_CONTAINER="" +BACKEND_AMAZEEIO="" +BACKEND_LAGOON="" DOCKER_SOCKET="/var/run/docker.sock" -TEST_PORT="18080" +# TEST_PORT is discovered dynamically in setup_file() using an ephemeral host port. +TEST_PORT="" # --------------------------------------------------------------------------- # File-level setup / teardown — container is started once for the entire file. # --------------------------------------------------------------------------- setup_file() { + # Generate a unique suffix once and persist it so every test in this run + # references the same container names (BATS re-sources the file per test). + local suffix + suffix="$(openssl rand -hex 4)" + echo "${suffix}" > "${BATS_SUITE_TMPDIR}/.suffix" + HAPROXY_CONTAINER="haproxy-bats-test-${suffix}" + BACKEND_AMAZEEIO="haproxy-bats-backend-amazeeio-${suffix}" + BACKEND_LAGOON="haproxy-bats-backend-lagoon-${suffix}" + if [ ! -S "${DOCKER_SOCKET}" ]; then echo "# Docker socket not found at ${DOCKER_SOCKET} – skipping runtime tests" >&3 return 0 @@ -34,19 +48,24 @@ setup_file() { docker run -d \ --name "${HAPROXY_CONTAINER}" \ --volume "${DOCKER_SOCKET}:/tmp/docker.sock" \ - -p "${TEST_PORT}:80" \ + -p 0:80 \ "${IMAGE}" + # Discover the ephemeral host port assigned by Docker. + local port + port="$(docker port "${HAPROXY_CONTAINER}" 80 | head -n1 | awk -F: '{print $NF}')" + echo "${port}" > "${BATS_SUITE_TMPDIR}/.port" + # Wait for docker-gen to run once and reload haproxy with the stats frontend. # The template includes "stats uri /stats", so /stats becomes available only # after the first docker-gen pass — give it up to 30 seconds. local max_wait=30 local waited=0 - until curl -sf "http://localhost:${TEST_PORT}/stats" >/dev/null 2>&1; do + until curl -sf "http://localhost:${port}/stats" >/dev/null 2>&1; do sleep 1 waited=$((waited + 1)) if [ "$waited" -ge "$max_wait" ]; then - echo "# Timed out waiting for haproxy /stats endpoint" >&3 + echo "# Timed out waiting for haproxy /stats endpoint (port ${port})" >&3 docker logs "${HAPROXY_CONTAINER}" >&3 2>&3 break fi @@ -54,9 +73,25 @@ setup_file() { } teardown_file() { - docker rm -f "${HAPROXY_CONTAINER}" 2>/dev/null || true - docker rm -f "${BACKEND_AMAZEEIO}" 2>/dev/null || true - docker rm -f "${BACKEND_LAGOON}" 2>/dev/null || true + local suffix + suffix="$(cat "${BATS_SUITE_TMPDIR}/.suffix" 2>/dev/null || true)" + docker rm -f "haproxy-bats-test-${suffix}" 2>/dev/null || true + docker rm -f "haproxy-bats-backend-amazeeio-${suffix}" 2>/dev/null || true + docker rm -f "haproxy-bats-backend-lagoon-${suffix}" 2>/dev/null || true +} + +# --------------------------------------------------------------------------- +# Per-test setup — restore container name variables from the stable suffix +# written by setup_file(), because BATS re-sources the file for every test. +# --------------------------------------------------------------------------- + +setup() { + local suffix + suffix="$(cat "${BATS_SUITE_TMPDIR}/.suffix" 2>/dev/null || true)" + HAPROXY_CONTAINER="haproxy-bats-test-${suffix}" + BACKEND_AMAZEEIO="haproxy-bats-backend-amazeeio-${suffix}" + BACKEND_LAGOON="haproxy-bats-backend-lagoon-${suffix}" + TEST_PORT="$(cat "${BATS_SUITE_TMPDIR}/.port" 2>/dev/null || true)" } # --------------------------------------------------------------------------- From 60fb41ecf433da27fcc05327cff782d96279f8b8 Mon Sep 17 00:00:00 2001 From: Toby Bellwood Date: Mon, 16 Mar 2026 12:48:41 +1100 Subject: [PATCH 4/5] fix: update runtime BATS tests to handle timeout and container exposure correctly --- tests/runtime.bats | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/runtime.bats b/tests/runtime.bats index eb187c4..18f2845 100644 --- a/tests/runtime.bats +++ b/tests/runtime.bats @@ -67,7 +67,7 @@ setup_file() { if [ "$waited" -ge "$max_wait" ]; then echo "# Timed out waiting for haproxy /stats endpoint (port ${port})" >&3 docker logs "${HAPROXY_CONTAINER}" >&3 2>&3 - break + return 1 fi done } @@ -125,7 +125,7 @@ _require_docker_socket() { @test "docker-gen process is running inside the container" { _require_docker_socket run docker exec "${HAPROXY_CONTAINER}" sh -c \ - 'ps aux 2>/dev/null | grep -c "[d]ocker-gen"' + 'pidof docker-gen >/dev/null 2>&1 && echo 1 || echo 0' [ "$status" -eq 0 ] [ "$output" -ge 1 ] } @@ -170,7 +170,7 @@ _require_docker_socket() { -e AMAZEEIO=AMAZEEIO \ -e "AMAZEEIO_URL=${backend_host}" \ -e AMAZEEIO_HTTP_PORT=80 \ - -p 80 \ + --expose 80 \ nginx:alpine # Wait for docker-gen to detect the new container and reload haproxy. @@ -199,7 +199,7 @@ _require_docker_socket() { --name "${BACKEND_LAGOON}" \ -e LAGOON_LOCALDEV_HTTP_PORT=8080 \ -e "LAGOON_ROUTE=http://${backend_host}" \ - -p 8080 \ + --expose 8080 \ nginx:alpine # Wait for docker-gen to detect the new container and reload haproxy. From 9f3a7596b6cab156cac63e016218fdcbdb6c3990 Mon Sep 17 00:00:00 2001 From: Toby Bellwood Date: Mon, 16 Mar 2026 13:33:55 +1100 Subject: [PATCH 5/5] fix: improve BATS tests for HAProxy stats page accessibility and output validation --- .github/workflows/build_and_test.yml | 2 +- tests/runtime.bats | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 628edf5..ef2c441 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -88,7 +88,7 @@ jobs: name: Set single image tag id: single_tag run: | - echo "tag=$(echo '${{ steps.meta.outputs.tags }}' | head -n1)" >> $GITHUB_OUTPUT + echo "tag=$(echo '${{ steps.meta.outputs.tags }}' | head -n1)" >> "$GITHUB_OUTPUT" - name: Find and Replace env: diff --git a/tests/runtime.bats b/tests/runtime.bats index 18f2845..8021f41 100644 --- a/tests/runtime.bats +++ b/tests/runtime.bats @@ -134,7 +134,7 @@ _require_docker_socket() { # HAProxy stats page — mirrors the GitHub Actions "haproxy test" step. # --------------------------------------------------------------------------- -@test "stats page is accessible on port 80" { +@test "stats page is accessible via published HTTP port" { _require_docker_socket run curl -sf "http://localhost:${TEST_PORT}/stats" [ "$status" -eq 0 ] @@ -176,7 +176,7 @@ _require_docker_socket() { # Wait for docker-gen to detect the new container and reload haproxy. local max_wait=20 local waited=0 - until curl -s "http://localhost:${TEST_PORT}/stats" | grep -q "${backend_host}"; do + until curl -s "http://localhost:${TEST_PORT}/stats" | grep -Fq -- "${backend_host}"; do sleep 1 waited=$((waited + 1)) [ "$waited" -lt "$max_wait" ] || break @@ -184,7 +184,7 @@ _require_docker_socket() { run curl -s "http://localhost:${TEST_PORT}/stats" [ "$status" -eq 0 ] - [[ "$output" =~ "${backend_host}" ]] + [[ "$output" == *"${backend_host}"* ]] docker rm -f "${BACKEND_AMAZEEIO}" 2>/dev/null || true } @@ -205,7 +205,7 @@ _require_docker_socket() { # Wait for docker-gen to detect the new container and reload haproxy. local max_wait=20 local waited=0 - until curl -s "http://localhost:${TEST_PORT}/stats" | grep -q "${backend_host}"; do + until curl -s "http://localhost:${TEST_PORT}/stats" | grep -Fq -- "${backend_host}"; do sleep 1 waited=$((waited + 1)) [ "$waited" -lt "$max_wait" ] || break @@ -213,7 +213,7 @@ _require_docker_socket() { run curl -s "http://localhost:${TEST_PORT}/stats" [ "$status" -eq 0 ] - [[ "$output" =~ "${backend_host}" ]] + [[ "$output" == *"${backend_host}"* ]] docker rm -f "${BACKEND_LAGOON}" 2>/dev/null || true }