Skip to content

Commit 7fad1eb

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 00f7011 commit 7fad1eb

File tree

12 files changed

+375
-0
lines changed

12 files changed

+375
-0
lines changed

.github/actionlint.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
self-hosted-runner:
2+
labels:
3+
- aarch64-darwin
4+
- aarch64-linux
5+
- blacksmith-4vcpu-ubuntu-2404
6+
- blacksmith-16vcpu-ubuntu-2404
7+
- blacksmith-32vcpu-ubuntu-2404
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: 'Install Nix on ephemeral runners'
2+
description: 'Installs Nix and sets up AWS credentials to push to the Nix binary cache'
3+
inputs:
4+
push-to-cache:
5+
description: 'Whether to push build outputs to the Nix binary cache'
6+
required: false
7+
default: 'false'
8+
runs:
9+
using: 'composite'
10+
steps:
11+
- name: aws-creds
12+
uses: aws-actions/configure-aws-credentials@v4
13+
if: ${{ inputs.push-to-cache == 'true' }}
14+
with:
15+
role-to-assume: ${{ env.DEV_AWS_ROLE }}
16+
aws-region: "us-east-1"
17+
output-credentials: true
18+
role-duration-seconds: 7200
19+
- name: Setup AWS credentials for Nix
20+
if: ${{ inputs.push-to-cache == 'true' }}
21+
shell: bash
22+
run: |
23+
sudo -H aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID
24+
sudo -H aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY
25+
sudo -H aws configure set aws_session_token $AWS_SESSION_TOKEN
26+
sudo mkdir -p /etc/nix
27+
sudo -E python -c "import os; file = open('/etc/nix/nix-secret-key', 'w'); file.write(os.environ['NIX_SIGN_SECRET_KEY']); file.close()"
28+
cat << 'EOF' | sudo tee /etc/nix/upload-to-cache.sh > /dev/null
29+
#!/usr/bin/env bash
30+
set -euo pipefail
31+
set -f
32+
33+
export IFS=' '
34+
/nix/var/nix/profiles/default/bin/nix copy --to 's3://nix-postgres-artifacts?secret-key=/etc/nix/nix-secret-key' $OUT_PATHS
35+
EOF
36+
sudo chmod +x /etc/nix/upload-to-cache.sh
37+
env:
38+
NIX_SIGN_SECRET_KEY: ${{ env.NIX_SIGN_SECRET_KEY }}
39+
- name: Install nix
40+
uses: cachix/install-nix-action@v31
41+
with:
42+
install_url: https://releases.nixos.org/nix/nix-2.31.2/install
43+
extra_nix_config: |
44+
substituters = https://cache.nixos.org https://nix-postgres-artifacts.s3.amazonaws.com
45+
trusted-public-keys = nix-postgres-artifacts:dGZlQOvKcNEjvT7QEAJbcV6b6uk7VF/hWMjhYleiaLI= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
46+
${{ inputs.push-to-cache == 'true' && 'post-build-hook = /etc/nix/upload-to-cache.sh' || '' }}
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/hooks.nix

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
{ inputs, ... }:
2+
let
3+
ghWorkflows = builtins.attrNames (builtins.readDir ../.github/workflows);
4+
lintedWorkflows = [ "ansible-test.yml" ];
5+
in
26
{
37
imports = [ inputs.git-hooks.flakeModule ];
48
perSystem =
@@ -8,6 +12,12 @@
812
check.enable = true;
913
settings = {
1014
hooks = {
15+
actionlint = {
16+
enable = true;
17+
excludes = builtins.filter (name: !builtins.elem name lintedWorkflows) ghWorkflows;
18+
verbose = true;
19+
};
20+
1121
treefmt = {
1222
enable = true;
1323
package = config.treefmt.build.wrapper;

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 { };

0 commit comments

Comments
 (0)