Skip to content

Commit 807808d

Browse files
committed
feat: add ansible task testing infrastructure based on Docker and pytest
This complements the existing AMI tests in testinfra by providing a faster feedback loops for Ansible development without requiring a full VM. We are also using testinfra to validate that the Ansible tasks have the desired effect. It is based on Docker, it can be run locally (e.g. macOS) or in CI. Note that this approach is not intended to replace the AMI tests, but rather to provide a more efficient way to test Ansible tasks during development. You can run the tests using `nix run -L .\#ansible-test`
1 parent 578be5d commit 807808d

File tree

9 files changed

+312
-0
lines changed

9 files changed

+312
-0
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
name: Ansible Test Image CI
2+
3+
on:
4+
push:
5+
branches:
6+
- develop
7+
pull_request:
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
id-token: write
13+
14+
jobs:
15+
build-and-push:
16+
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
17+
strategy:
18+
matrix:
19+
arch: [amd64, arm64]
20+
runs-on: ${{ matrix.arch == 'amd64' && 'blacksmith-16vcpu-ubuntu-2404' || 'blacksmith-16vcpu-ubuntu-2404-arm' }}
21+
steps:
22+
- name: Checkout Repo
23+
uses: supabase/postgres/.github/actions/shared-checkout@HEAD
24+
25+
- name: Install Nix
26+
uses: ./.github/actions/nix-install-ephemeral
27+
with:
28+
push-to-cache: true
29+
env:
30+
DEV_AWS_ROLE: ${{ secrets.DEV_AWS_ROLE }}
31+
NIX_SIGN_SECRET_KEY: ${{ secrets.NIX_SIGN_SECRET_KEY }}
32+
33+
- name: Login to Docker Hub
34+
uses: docker/login-action@v3
35+
with:
36+
username: ${{ secrets.DOCKER_USERNAME }}
37+
password: ${{ secrets.DOCKER_PASSWORD }}
38+
39+
- name: Build Docker image with Nix
40+
run: |
41+
echo "Building ansible-test Docker image for ${{ matrix.arch }}..."
42+
IMAGE_PATH=$(nix build .#docker-ansible-test --print-out-paths)
43+
echo "IMAGE_PATH=$IMAGE_PATH" >> "$GITHUB_ENV"
44+
45+
- name: Load and push Docker image
46+
run: |
47+
echo "Loading Docker image..."
48+
docker load < "$IMAGE_PATH"
49+
docker tag supabase/ansible-test:latest supabase/ansible-test:latest-${{ matrix.arch }}
50+
docker push supabase/ansible-test:latest-${{ matrix.arch }}
51+
52+
create-manifest:
53+
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
54+
needs: build-and-push
55+
runs-on: 'blacksmith-4vcpu-ubuntu-2404'
56+
steps:
57+
- name: Login to Docker Hub
58+
uses: docker/login-action@v3
59+
with:
60+
username: ${{ secrets.DOCKER_USERNAME }}
61+
password: ${{ secrets.DOCKER_PASSWORD }}
62+
63+
- name: Create and push multi-arch manifest
64+
run: |
65+
docker manifest create supabase/ansible-test:latest \
66+
supabase/ansible-test:latest-amd64 \
67+
supabase/ansible-test:latest-arm64
68+
docker manifest push supabase/ansible-test:latest
69+
70+
run-ansible-tests:
71+
if: github.event_name == 'pull_request' || success()
72+
needs: create-manifest
73+
runs-on: 'blacksmith-16vcpu-ubuntu-2404'
74+
steps:
75+
- name: Checkout Repo
76+
uses: supabase/postgres/.github/actions/shared-checkout@HEAD
77+
78+
- name: Install Nix
79+
uses: ./.github/actions/nix-install-ephemeral
80+
with:
81+
push-to-cache: true
82+
env:
83+
DEV_AWS_ROLE: ${{ secrets.DEV_AWS_ROLE }}
84+
NIX_SIGN_SECRET_KEY: ${{ secrets.NIX_SIGN_SECRET_KEY }}
85+
86+
- name: Login to Docker Hub
87+
uses: docker/login-action@v3
88+
with:
89+
username: ${{ secrets.DOCKER_USERNAME }}
90+
password: ${{ secrets.DOCKER_PASSWORD }}
91+
92+
- name: Run Ansible tests
93+
env:
94+
PY_COLORS: '1'
95+
ANSIBLE_FORCE_COLOR: '1'
96+
run: |
97+
docker pull supabase/ansible-test:latest &
98+
nix run .#ansible-test

ansible/tasks/files

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../files

ansible/tests/conftest.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import pytest
2+
import subprocess
3+
import testinfra
4+
from rich.console import Console
5+
6+
console = Console()
7+
8+
9+
def pytest_addoption(parser):
10+
parser.addoption(
11+
"--flake-dir",
12+
action="store",
13+
help="Directory containing the current flake",
14+
)
15+
16+
parser.addoption(
17+
"--docker-image",
18+
action="store",
19+
help="Docker image and tag to use for testing",
20+
)
21+
22+
23+
@pytest.fixture(scope="module")
24+
def host(request):
25+
flake_dir = request.config.getoption("--flake-dir")
26+
if not flake_dir:
27+
pytest.fail("--flake-dir option is required")
28+
docker_image = request.config.getoption("--docker-image")
29+
docker_id = (
30+
subprocess.check_output(
31+
[
32+
"docker",
33+
"run",
34+
"--privileged",
35+
"--cap-add",
36+
"SYS_ADMIN",
37+
"--security-opt",
38+
"seccomp=unconfined",
39+
"--cgroup-parent=docker.slice",
40+
"--cgroupns",
41+
"private",
42+
"-v",
43+
f"{flake_dir}:/flake",
44+
"-d",
45+
docker_image,
46+
]
47+
)
48+
.decode()
49+
.strip()
50+
)
51+
yield testinfra.get_host("docker://" + docker_id)
52+
subprocess.check_call(["docker", "rm", "-f", docker_id], stdout=subprocess.DEVNULL)
53+
54+
55+
@pytest.fixture(scope="module")
56+
def run_ansible_playbook(host):
57+
def _run_playbook(playbook_name, verbose=False):
58+
cmd = [
59+
"ANSIBLE_HOST_KEY_CHECKING=False",
60+
"ansible-playbook",
61+
"--connection=local",
62+
]
63+
if verbose:
64+
cmd.append("-vvv")
65+
cmd.extend(
66+
[
67+
"-i",
68+
"localhost,",
69+
"--extra-vars",
70+
"@/flake/ansible/vars.yml",
71+
f"/flake/ansible/tests/{playbook_name}",
72+
]
73+
)
74+
result = host.run(" ".join(cmd))
75+
if result.failed:
76+
console.log(result.stdout)
77+
console.log(result.stderr)
78+
pytest.fail(
79+
f"Ansible playbook {playbook_name} failed with return code {result.rc}"
80+
)
81+
82+
return _run_playbook

ansible/tests/nginx.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
- hosts: localhost
3+
tasks:
4+
- name: Install dependencies
5+
apt:
6+
pkg:
7+
- build-essential
8+
update_cache: yes
9+
- import_tasks: ../tasks/setup-nginx.yml
10+
- name: Start Nginx service
11+
service:
12+
name: nginx
13+
state: started
14+
enabled: yes

ansible/tests/test_nginx.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import pytest
2+
3+
4+
@pytest.fixture(scope="module", autouse=True)
5+
def run_ansible(run_ansible_playbook):
6+
run_ansible_playbook("nginx.yaml")
7+
8+
9+
def test_nginx_service(host):
10+
assert host.service("nginx.service").is_valid
11+
assert host.service("nginx.service").is_running

nix/packages/ansible-test.nix

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{ self, pkgs }:
2+
pkgs.writeShellApplication {
3+
name = "ansible-test";
4+
runtimeInputs = with pkgs; [
5+
(python3.withPackages (
6+
ps: with ps; [
7+
requests
8+
pytest
9+
pytest-testinfra
10+
pytest-xdist
11+
rich
12+
]
13+
))
14+
];
15+
text = ''
16+
echo "Running Ansible tests..."
17+
FLAKE_DIR=${self}
18+
pytest -x -p no:cacheprovider -s -v "$@" $FLAKE_DIR/ansible/tests --flake-dir=$FLAKE_DIR --docker-image=supabase/ansible-test:latest "$@"
19+
'';
20+
meta = {
21+
description = "Ansible test runner";
22+
};
23+
}

nix/packages/default.nix

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,13 @@
3030
packages = (
3131
{
3232
build-test-ami = pkgs.callPackage ./build-test-ami.nix { };
33+
ansible-test = pkgs.callPackage ./ansible-test.nix { inherit self; };
3334
cleanup-ami = pkgs.callPackage ./cleanup-ami.nix { };
3435
dbmate-tool = pkgs.callPackage ./dbmate-tool.nix { inherit (self.supabase) defaults; };
36+
docker-ansible-test = pkgs.callPackage ./docker-ansible-test.nix {
37+
inherit (self'.packages) docker-image-ubuntu;
38+
};
39+
docker-image-ubuntu = pkgs.callPackage ./docker-ubuntu.nix { };
3540
docs = pkgs.callPackage ./docs.nix { };
3641
supabase-groonga = pkgs.callPackage ./groonga { };
3742
http-mock-server = pkgs.callPackage ./http-mock-server.nix { };
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
pkgs,
3+
lib,
4+
docker-image-ubuntu,
5+
}:
6+
let
7+
tools = [ pkgs.ansible ];
8+
in
9+
pkgs.dockerTools.buildLayeredImage {
10+
name = "supabase/ansible-test";
11+
tag = "latest";
12+
maxLayers = 30;
13+
fromImage = docker-image-ubuntu;
14+
compressor = "zstd";
15+
config = {
16+
Env = [
17+
"PATH=${lib.makeBinPath tools}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
18+
];
19+
Cmd = [ "/lib/systemd/systemd" ];
20+
};
21+
}

nix/packages/docker-ubuntu.nix

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
runCommand,
3+
dockerTools,
4+
xz,
5+
buildEnv,
6+
stdenv,
7+
}:
8+
let
9+
ubuntu-cloudimg =
10+
let
11+
12+
cloudImg =
13+
if stdenv.hostPlatform.system == "x86_64-linux" then
14+
builtins.fetchurl {
15+
url = "https://cloud-images.ubuntu.com/releases/noble/release-20251026/ubuntu-24.04-server-cloudimg-amd64-root.tar.xz";
16+
sha256 = "0y3d55f5qy7bxm3mfmnxzpmwp88d7iiszc57z5b9npc6xgwi28np";
17+
}
18+
else
19+
builtins.fetchurl {
20+
url = "https://cloud-images.ubuntu.com/releases/noble/release-20251026/ubuntu-24.04-server-cloudimg-arm64-root.tar.xz";
21+
sha256 = "1l4l0llfffspzgnmwhax0fcnjn8ih8n4azhfaghng2hh1xvr4a17";
22+
};
23+
in
24+
runCommand "ubuntu-cloudimg" { nativeBuildInputs = [ xz ]; } ''
25+
mkdir -p $out
26+
tar --exclude='dev/*' \
27+
--exclude='etc/systemd/system/network-online.target.wants/systemd-networkd-wait-online.service' \
28+
--exclude='etc/systemd/system/multi-user.target.wants/systemd-resolved.service' \
29+
--exclude='usr/lib/systemd/system/tpm-udev.service' \
30+
--exclude='usr/lib/systemd/system/systemd-remount-fs.service' \
31+
--exclude='usr/lib/systemd/system/systemd-resolved.service' \
32+
--exclude='usr/lib/systemd/system/proc-sys-fs-binfmt_misc.automount' \
33+
--exclude='usr/lib/systemd/system/sys-kernel-*' \
34+
--exclude='var/lib/apt/lists/*' \
35+
-xJf ${cloudImg} -C $out
36+
rm -f $out/bin $out/lib $out/lib64 $out/sbin
37+
mkdir -p $out/run/systemd && echo 'docker' > $out/run/systemd/container
38+
mkdir $out/var/lib/apt/lists/partial
39+
'';
40+
in
41+
dockerTools.buildImage {
42+
name = "ubuntu-cloudimg";
43+
tag = "24.04";
44+
created = "now";
45+
extraCommands = ''
46+
ln -s usr/bin
47+
ln -s usr/lib
48+
ln -s usr/lib64
49+
ln -s usr/sbin
50+
'';
51+
copyToRoot = buildEnv {
52+
name = "image-root";
53+
pathsToLink = [ "/" ];
54+
paths = [ ubuntu-cloudimg ];
55+
};
56+
config.Cmd = [ "/lib/systemd/systemd" ];
57+
}

0 commit comments

Comments
 (0)