Skip to content

Commit daf45c0

Browse files
committed
feat: deploy nginx using system manager
And use docker to run tests
1 parent ed0bfa5 commit daf45c0

File tree

8 files changed

+284
-0
lines changed

8 files changed

+284
-0
lines changed

flake.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
url = "github:cachix/git-hooks.nix";
3232
inputs.nixpkgs.follows = "nixpkgs";
3333
};
34+
system-manager = {
35+
url = "github:numtide/system-manager";
36+
inputs.nixpkgs.follows = "nixpkgs";
37+
};
3438
};
3539

3640
outputs =
@@ -53,6 +57,7 @@
5357
nix/nixpkgs.nix
5458
nix/packages
5559
nix/overlays
60+
nix/systemModules
5661
];
5762
});
5863
}

nix/systemModules/default.nix

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
flake-parts-lib,
3+
withSystem,
4+
self,
5+
...
6+
}:
7+
{
8+
imports = [ ./tests ];
9+
flake = {
10+
systemModules = {
11+
nginx = flake-parts-lib.importApply ./nginx.nix { inherit withSystem self; };
12+
};
13+
};
14+
}

nix/systemModules/nginx.nix

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{ ... }:
2+
{ config, ... }:
3+
{
4+
config = {
5+
system-manager.preActivationAssertions = {
6+
waitForSystemd = {
7+
enable = true;
8+
script = ''
9+
TIMEOUT=20
10+
while ! systemctl is-system-running --quiet; do
11+
if [ $TIMEOUT -le 0 ]; then
12+
echo "Systemd did not start in time."
13+
exit 1
14+
fi
15+
sleep 0.1;
16+
echo "Waiting for systemd to start..."
17+
TIMEOUT=$((TIMEOUT - 1))
18+
done
19+
'';
20+
};
21+
};
22+
23+
services.nginx.enable = true;
24+
};
25+
}

nix/systemModules/tests/conftest.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import pytest
2+
import shutil
3+
import subprocess
4+
import testinfra
5+
import time
6+
from rich.console import Console
7+
8+
console = Console()
9+
10+
11+
def pytest_addoption(parser):
12+
parser.addoption(
13+
"--image-name",
14+
action="store",
15+
help="Docker image and tag to use for testing",
16+
)
17+
parser.addoption(
18+
"--image-path",
19+
action="store",
20+
help="Compressed Docker image to load for testing",
21+
)
22+
23+
parser.addoption(
24+
"--force-docker",
25+
action="store_true",
26+
help="Force using Docker instead of Podman for testing",
27+
)
28+
29+
30+
@pytest.fixture(scope="session")
31+
def host(request):
32+
force_docker = request.config.getoption("--force-docker")
33+
image_name = request.config.getoption("--image-name")
34+
image_path = request.config.getoption("--image-path")
35+
if not force_docker and shutil.which("podman"):
36+
console.log("Using Podman for testing")
37+
with console.status("Loading image..."):
38+
subprocess.check_output(["podman", "load", "-q", "-i", image_path])
39+
podman_id = (
40+
subprocess.check_output(
41+
[
42+
"podman",
43+
"run",
44+
"--cap-add",
45+
"SYS_ADMIN",
46+
"-d",
47+
image_name,
48+
]
49+
)
50+
.decode()
51+
.strip()
52+
)
53+
yield testinfra.get_host("podman://" + podman_id)
54+
with console.status("Cleaning up..."):
55+
subprocess.check_call(
56+
["podman", "rm", "-f", podman_id], stdout=subprocess.DEVNULL
57+
)
58+
else:
59+
console.log("Using Docker for testing")
60+
with console.status("Loading image..."):
61+
subprocess.check_output(["docker", "load", "-q", "-i", image_path])
62+
docker_id = (
63+
subprocess.check_output(
64+
[
65+
"docker",
66+
"run",
67+
"--privileged",
68+
"--cap-add",
69+
"SYS_ADMIN",
70+
"--security-opt",
71+
"seccomp=unconfined",
72+
"--cgroup-parent=docker.slice",
73+
"--cgroupns",
74+
"private",
75+
"-d",
76+
image_name,
77+
]
78+
)
79+
.decode()
80+
.strip()
81+
)
82+
yield testinfra.get_host("docker://" + docker_id)
83+
with console.status("Cleaning up..."):
84+
subprocess.check_call(
85+
["docker", "rm", "-f", docker_id], stdout=subprocess.DEVNULL
86+
)
87+
88+
89+
def wait_for_target(host, target, timeout=60):
90+
start_time = time.time()
91+
while time.time() - start_time < timeout:
92+
result = host.run(f"systemctl is-active {target}")
93+
if result.rc == 0:
94+
return True
95+
time.sleep(1)
96+
return False
97+
98+
99+
@pytest.fixture(scope="session", autouse=True)
100+
def activate_system_manager(host):
101+
with console.status("Waiting systemd to be ready..."):
102+
assert wait_for_target(host, "multi-user.target")
103+
result = host.run("activate")
104+
console.log(result.stdout)
105+
console.log(result.stderr)
106+
if result.failed:
107+
raise pytest.fail(
108+
"System manager activation failed with return code {}".format(result.rc)
109+
)

nix/systemModules/tests/default.nix

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{ self, ... }:
2+
{
3+
perSystem =
4+
{ lib, pkgs, ... }:
5+
{
6+
packages = lib.optionalAttrs (pkgs.stdenv.hostPlatform.isLinux) {
7+
check-system-manager-nginx = (import ./nginx.nix { inherit self pkgs; });
8+
};
9+
};
10+
}

nix/systemModules/tests/nginx.nix

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

nix/systemModules/tests/test_nginx.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def test_nginx_service(host):
2+
assert host.service("nginx.service").is_valid
3+
assert host.service("nginx.service").is_running

0 commit comments

Comments
 (0)