Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 48 additions & 31 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Comment thread
tobybellwood marked this conversation as resolved.
-
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: |
Expand All @@ -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
Expand All @@ -61,49 +61,66 @@ 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
Comment thread
tobybellwood marked this conversation as resolved.
# 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;
ssh-add -l;
-
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: |
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
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.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: Install pygmy and dockerize via brew
name: Set up Homebrew
uses: Homebrew/actions/setup-homebrew@fb0fe15d936a80ac0ba4c6cee691bc3da3f00f4e # 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.single_tag.outputs.tag }}
-
name: Run BATS tests
env:
IMAGE_NAME: ${{ steps.single_tag.outputs.tag }}
run: |
Comment thread
tobybellwood marked this conversation as resolved.
bats --tap tests/image_structure.bats
bats --tap tests/runtime.bats
-
name: Switch pygmy configs from vanilla to basic
run: |
Expand All @@ -123,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 '${{ steps.meta.outputs.tags }}';
docker container inspect amazeeio-haproxy | jq '.[].Config.Image' | grep -F '${{ steps.single_tag.outputs.tag }}';
-
name: Resolv file test
run: |
Expand Down Expand Up @@ -185,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 '${{ steps.meta.outputs.tags }}';
docker container inspect amazeeio-haproxy | jq '.[].Config.Image' | grep -F '${{ steps.single_tag.outputs.tag }}';
-
name: SSH Key test
run: |
Expand All @@ -201,14 +218,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
Expand Down
57 changes: 57 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
124 changes: 124 additions & 0 deletions tests/image_structure.bats
Original file line number Diff line number Diff line change
@@ -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 2.9.x" {
run docker run --rm "${IMAGE}" sh -c 'haproxy -v 2>&1'
[ "$status" -eq 0 ]
[[ "$output" =~ "2.9" ]]
}

@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 <<HPXY
global
daemon
maxconn 1024
defaults
mode http
timeout client 60s
timeout connect 60s
timeout server 60s
frontend http
bind :80
default_backend be
backend be
mode http
HPXY
haproxy -c -f /tmp/valid.cfg'
[ "$status" -eq 0 ]
}

# ---------------------------------------------------------------------------
# Exposed ports (image metadata)
# ---------------------------------------------------------------------------

@test "image exposes port 80" {
run docker inspect --format='{{json .Config.ExposedPorts}}' "${IMAGE}"
[ "$status" -eq 0 ]
[[ "$output" =~ "80/tcp" ]]
}

@test "image exposes port 443" {
run docker inspect --format='{{json .Config.ExposedPorts}}' "${IMAGE}"
[ "$status" -eq 0 ]
[[ "$output" =~ "443/tcp" ]]
}
Loading
Loading