Skip to content

Commit 1f38037

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. Also as 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. Docker is used outside of the Nix sandbox for the moment. You can run the tests using `nix run -L .\#ansible-test`
1 parent 337be4e commit 1f38037

File tree

6 files changed

+179
-0
lines changed

6 files changed

+179
-0
lines changed

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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
def pytest_addoption(parser):
2+
parser.addoption(
3+
"--ansible-dir",
4+
action="store",
5+
help="Directory containing Ansible playbooks and roles",
6+
)
7+
8+
parser.addoption(
9+
"--docker-image",
10+
action="store",
11+
help="Docker image and tag to use for testing",
12+
)

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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import pytest
2+
import subprocess
3+
import testinfra
4+
from rich.console import Console
5+
6+
console = Console()
7+
8+
9+
@pytest.fixture(scope="session")
10+
def host(request):
11+
ansible_dir = request.config.getoption("--ansible-dir")
12+
docker_id = (
13+
subprocess.check_output(
14+
[
15+
"docker",
16+
"run",
17+
"--privileged",
18+
"--cap-add",
19+
"SYS_ADMIN",
20+
"--security-opt",
21+
"seccomp=unconfined",
22+
"--cgroup-parent=docker.slice",
23+
"--cgroupns",
24+
"private",
25+
"-v",
26+
f"{ansible_dir}/:/ansible/",
27+
"-d",
28+
"ubuntu-cloudimg-with-tools:0.1",
29+
]
30+
)
31+
.decode()
32+
.strip()
33+
)
34+
yield testinfra.get_host("docker://" + docker_id)
35+
subprocess.check_call(["docker", "rm", "-f", docker_id], stdout=subprocess.DEVNULL)
36+
37+
38+
@pytest.fixture(scope="session", autouse=True)
39+
def run_ansible(host):
40+
cmd = [
41+
"ANSIBLE_HOST_KEY_CHECKING=False",
42+
"ansible-playbook",
43+
"--connection=local",
44+
"-i",
45+
"localhost,",
46+
"--extra-vars",
47+
"@/ansible/vars.yml",
48+
"/ansible/tests/nginx.yaml",
49+
]
50+
result = host.run(" ".join(cmd))
51+
if result.failed:
52+
console.log(result.stdout)
53+
console.log(result.stderr)
54+
raise pytest.fail(
55+
"Ansible playbook nginx.yaml failed with return code {}".format(result.rc)
56+
)
57+
58+
59+
def test_nginx_service(host):
60+
assert host.service("nginx.service").is_valid
61+
assert host.service("nginx.service").is_running

nix/packages/ansible-test.nix

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
{
2+
pkgs,
3+
lib,
4+
}:
5+
let
6+
ubuntu-cloudimg =
7+
let
8+
cloudImg = builtins.fetchurl {
9+
url = "http://cloud-images-archive.ubuntu.com/releases/noble/release-20250430/ubuntu-24.04-server-cloudimg-amd64-root.tar.xz";
10+
sha256 = "sha256:0rfi3qqs0sqarixfic7pzjpx7d4vldv2d98c5zjv7b90mirznvf9";
11+
};
12+
in
13+
pkgs.runCommand "ubuntu-cloudimg" { nativeBuildInputs = [ pkgs.xz ]; } ''
14+
mkdir -p $out
15+
tar --exclude='dev/*' \
16+
--exclude='etc/systemd/system/network-online.target.wants/systemd-networkd-wait-online.service' \
17+
--exclude='etc/systemd/system/multi-user.target.wants/systemd-resolved.service' \
18+
--exclude='usr/lib/systemd/system/tpm-udev.service' \
19+
--exclude='usr/lib/systemd/system/systemd-remount-fs.service' \
20+
--exclude='usr/lib/systemd/system/systemd-resolved.service' \
21+
--exclude='var/lib/apt/lists/*' \
22+
-xJf ${cloudImg} -C $out
23+
rm $out/bin $out/lib $out/lib64 $out/sbin
24+
mkdir -p $out/run/systemd && echo 'docker' > $out/run/systemd/container
25+
mkdir $out/var/lib/apt/lists/partial
26+
'';
27+
28+
dockerImageUbuntu = pkgs.dockerTools.buildImage {
29+
name = "ubuntu-cloudimg";
30+
tag = "0.1";
31+
created = "now";
32+
extraCommands = ''
33+
ln -s usr/bin
34+
ln -s usr/lib
35+
ln -s usr/lib64
36+
ln -s usr/sbin
37+
'';
38+
copyToRoot = pkgs.buildEnv {
39+
name = "image-root";
40+
pathsToLink = [ "/" ];
41+
paths = [ ubuntu-cloudimg ];
42+
};
43+
config.Cmd = [ "/lib/systemd/systemd" ];
44+
};
45+
46+
dockerImageUbuntuWithTools =
47+
let
48+
tools = [ pkgs.ansible ];
49+
in
50+
pkgs.dockerTools.buildLayeredImage {
51+
name = "ubuntu-cloudimg-with-tools";
52+
tag = "0.1";
53+
created = "now";
54+
maxLayers = 30;
55+
fromImage = dockerImageUbuntu;
56+
config = {
57+
Env = [
58+
"PATH=${lib.makeBinPath tools}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
59+
];
60+
Cmd = [ "/lib/systemd/systemd" ];
61+
};
62+
};
63+
in
64+
pkgs.writeShellApplication {
65+
name = "ansible-test";
66+
runtimeInputs = with pkgs; [
67+
(python3.withPackages (
68+
ps: with ps; [
69+
requests
70+
pytest
71+
pytest-testinfra
72+
rich
73+
]
74+
))
75+
];
76+
text = ''
77+
echo "Running Ansible tests..."
78+
export DOCKER_IMAGE=${dockerImageUbuntuWithTools.imageName}:${dockerImageUbuntuWithTools.imageTag}
79+
if ! docker image inspect $DOCKER_IMAGE > /dev/null; then
80+
echo "Loading Docker image..."
81+
docker load < ${dockerImageUbuntuWithTools}
82+
fi
83+
ANSIBLE_DIR=${../../ansible}
84+
pytest -p no:cacheprovider -s -v "$@" $ANSIBLE_DIR/tests --ansible-dir=$ANSIBLE_DIR --docker-image=$DOCKER_IMAGE
85+
'';
86+
meta = with pkgs.lib; {
87+
description = "Ansible test runner";
88+
platforms = platforms.linux;
89+
};
90+
}

nix/packages/default.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
packages = (
3232
{
3333
build-test-ami = pkgs.callPackage ./build-test-ami.nix { };
34+
ansible-test = pkgs.callPackage ./ansible-test.nix { };
3435
cleanup-ami = pkgs.callPackage ./cleanup-ami.nix { };
3536
dbmate-tool = pkgs.callPackage ./dbmate-tool.nix { inherit (self.supabase) defaults; };
3637
docs = pkgs.callPackage ./docs.nix { };

0 commit comments

Comments
 (0)