From 2dbcb5a1567f4a9edde53cf74008fb9edc71ab3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Mon, 4 Aug 2025 21:12:53 +0200 Subject: [PATCH 1/6] 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` --- .github/workflows/ansible-tests.yml | 98 ++++++++++++++++++++++++++++ ansible/tasks/files | 1 + ansible/tests/conftest.py | 82 +++++++++++++++++++++++ ansible/tests/nginx.yaml | 14 ++++ ansible/tests/test_nginx.py | 11 ++++ nix/hooks.nix | 1 + nix/packages/ansible-test.nix | 23 +++++++ nix/packages/default.nix | 5 ++ nix/packages/docker-ansible-test.nix | 21 ++++++ nix/packages/docker-ubuntu.nix | 57 ++++++++++++++++ 10 files changed, 313 insertions(+) create mode 100644 .github/workflows/ansible-tests.yml create mode 120000 ansible/tasks/files create mode 100644 ansible/tests/conftest.py create mode 100644 ansible/tests/nginx.yaml create mode 100644 ansible/tests/test_nginx.py create mode 100644 nix/packages/ansible-test.nix create mode 100644 nix/packages/docker-ansible-test.nix create mode 100644 nix/packages/docker-ubuntu.nix diff --git a/.github/workflows/ansible-tests.yml b/.github/workflows/ansible-tests.yml new file mode 100644 index 000000000..8cc82a5a1 --- /dev/null +++ b/.github/workflows/ansible-tests.yml @@ -0,0 +1,98 @@ +name: Ansible Test Image CI + +on: + push: + branches: + - develop + pull_request: + workflow_dispatch: + +permissions: + contents: read + id-token: write + +jobs: + build-and-push: + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' + strategy: + matrix: + arch: [amd64, arm64] + runs-on: ${{ matrix.arch == 'amd64' && 'blacksmith-16vcpu-ubuntu-2404' || 'blacksmith-16vcpu-ubuntu-2404-arm' }} + steps: + - name: Checkout Repo + uses: supabase/postgres/.github/actions/shared-checkout@HEAD + + - name: Install Nix + uses: ./.github/actions/nix-install-ephemeral + with: + push-to-cache: true + env: + DEV_AWS_ROLE: ${{ secrets.DEV_AWS_ROLE }} + NIX_SIGN_SECRET_KEY: ${{ secrets.NIX_SIGN_SECRET_KEY }} + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build Docker image with Nix + run: | + echo "Building ansible-test Docker image for ${{ matrix.arch }}..." + IMAGE_PATH=$(nix build .#docker-ansible-test --print-out-paths) + echo "IMAGE_PATH=$IMAGE_PATH" >> "$GITHUB_ENV" + + - name: Load and push Docker image + run: | + echo "Loading Docker image..." + docker load < "$IMAGE_PATH" + docker tag supabase/ansible-test:latest supabase/ansible-test:latest-${{ matrix.arch }} + docker push supabase/ansible-test:latest-${{ matrix.arch }} + + create-manifest: + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' + needs: build-and-push + runs-on: 'blacksmith-4vcpu-ubuntu-2404' + steps: + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Create and push multi-arch manifest + run: | + docker manifest create supabase/ansible-test:latest \ + supabase/ansible-test:latest-amd64 \ + supabase/ansible-test:latest-arm64 + docker manifest push supabase/ansible-test:latest + + run-ansible-tests: + if: github.event_name == 'pull_request' || success() + needs: create-manifest + runs-on: 'blacksmith-16vcpu-ubuntu-2404' + steps: + - name: Checkout Repo + uses: supabase/postgres/.github/actions/shared-checkout@HEAD + + - name: Install Nix + uses: ./.github/actions/nix-install-ephemeral + with: + push-to-cache: true + env: + DEV_AWS_ROLE: ${{ secrets.DEV_AWS_ROLE }} + NIX_SIGN_SECRET_KEY: ${{ secrets.NIX_SIGN_SECRET_KEY }} + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Run Ansible tests + env: + PY_COLORS: '1' + ANSIBLE_FORCE_COLOR: '1' + run: | + docker pull supabase/ansible-test:latest & + nix run .#ansible-test diff --git a/ansible/tasks/files b/ansible/tasks/files new file mode 120000 index 000000000..feb122881 --- /dev/null +++ b/ansible/tasks/files @@ -0,0 +1 @@ +../files \ No newline at end of file diff --git a/ansible/tests/conftest.py b/ansible/tests/conftest.py new file mode 100644 index 000000000..28544f786 --- /dev/null +++ b/ansible/tests/conftest.py @@ -0,0 +1,82 @@ +import pytest +import subprocess +import testinfra +from rich.console import Console + +console = Console() + + +def pytest_addoption(parser): + parser.addoption( + "--flake-dir", + action="store", + help="Directory containing the current flake", + ) + + parser.addoption( + "--docker-image", + action="store", + help="Docker image and tag to use for testing", + ) + + +@pytest.fixture(scope="module") +def host(request): + flake_dir = request.config.getoption("--flake-dir") + if not flake_dir: + pytest.fail("--flake-dir option is required") + docker_image = request.config.getoption("--docker-image") + docker_id = ( + subprocess.check_output( + [ + "docker", + "run", + "--privileged", + "--cap-add", + "SYS_ADMIN", + "--security-opt", + "seccomp=unconfined", + "--cgroup-parent=docker.slice", + "--cgroupns", + "private", + "-v", + f"{flake_dir}:/flake", + "-d", + docker_image, + ] + ) + .decode() + .strip() + ) + yield testinfra.get_host("docker://" + docker_id) + subprocess.check_call(["docker", "rm", "-f", docker_id], stdout=subprocess.DEVNULL) + + +@pytest.fixture(scope="module") +def run_ansible_playbook(host): + def _run_playbook(playbook_name, verbose=False): + cmd = [ + "ANSIBLE_HOST_KEY_CHECKING=False", + "ansible-playbook", + "--connection=local", + ] + if verbose: + cmd.append("-vvv") + cmd.extend( + [ + "-i", + "localhost,", + "--extra-vars", + "@/flake/ansible/vars.yml", + f"/flake/ansible/tests/{playbook_name}", + ] + ) + result = host.run(" ".join(cmd)) + if result.failed: + console.log(result.stdout) + console.log(result.stderr) + pytest.fail( + f"Ansible playbook {playbook_name} failed with return code {result.rc}" + ) + + return _run_playbook diff --git a/ansible/tests/nginx.yaml b/ansible/tests/nginx.yaml new file mode 100644 index 000000000..720e79679 --- /dev/null +++ b/ansible/tests/nginx.yaml @@ -0,0 +1,14 @@ +--- +- hosts: localhost + tasks: + - name: Install dependencies + apt: + pkg: + - build-essential + update_cache: yes + - import_tasks: ../tasks/setup-nginx.yml + - name: Start Nginx service + service: + name: nginx + state: started + enabled: yes diff --git a/ansible/tests/test_nginx.py b/ansible/tests/test_nginx.py new file mode 100644 index 000000000..ec68e82a9 --- /dev/null +++ b/ansible/tests/test_nginx.py @@ -0,0 +1,11 @@ +import pytest + + +@pytest.fixture(scope="module", autouse=True) +def run_ansible(run_ansible_playbook): + run_ansible_playbook("nginx.yaml") + + +def test_nginx_service(host): + assert host.service("nginx.service").is_valid + assert host.service("nginx.service").is_running diff --git a/nix/hooks.nix b/nix/hooks.nix index 9b78b8a5d..74ea506c9 100644 --- a/nix/hooks.nix +++ b/nix/hooks.nix @@ -2,6 +2,7 @@ let ghWorkflows = builtins.attrNames (builtins.readDir ../.github/workflows); lintedWorkflows = [ + "ansible-test.yml" "nix-eval.yml" "nix-build.yml" "testinfra-ami-build.yml" diff --git a/nix/packages/ansible-test.nix b/nix/packages/ansible-test.nix new file mode 100644 index 000000000..618ecc1c9 --- /dev/null +++ b/nix/packages/ansible-test.nix @@ -0,0 +1,23 @@ +{ self, pkgs }: +pkgs.writeShellApplication { + name = "ansible-test"; + runtimeInputs = with pkgs; [ + (python3.withPackages ( + ps: with ps; [ + requests + pytest + pytest-testinfra + pytest-xdist + rich + ] + )) + ]; + text = '' + echo "Running Ansible tests..." + FLAKE_DIR=${self} + pytest -x -p no:cacheprovider -s -v "$@" $FLAKE_DIR/ansible/tests --flake-dir=$FLAKE_DIR --docker-image=supabase/ansible-test:latest "$@" + ''; + meta = { + description = "Ansible test runner"; + }; +} diff --git a/nix/packages/default.nix b/nix/packages/default.nix index 7977a0af9..0731596eb 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -35,8 +35,13 @@ { build-ami = pkgs.callPackage ./build-ami.nix { packer = self'.packages.packer; }; build-test-ami = pkgs.callPackage ./build-test-ami.nix { }; + ansible-test = pkgs.callPackage ./ansible-test.nix { inherit self; }; cleanup-ami = pkgs.callPackage ./cleanup-ami.nix { }; dbmate-tool = pkgs.callPackage ./dbmate-tool.nix { inherit (self.supabase) defaults; }; + docker-ansible-test = pkgs.callPackage ./docker-ansible-test.nix { + inherit (self'.packages) docker-image-ubuntu; + }; + docker-image-ubuntu = pkgs.callPackage ./docker-ubuntu.nix { }; docs = pkgs.callPackage ./docs.nix { }; github-matrix = pkgs.callPackage ./github-matrix { nix-eval-jobs = inputs'.nix-eval-jobs.packages.default; diff --git a/nix/packages/docker-ansible-test.nix b/nix/packages/docker-ansible-test.nix new file mode 100644 index 000000000..15a1d2320 --- /dev/null +++ b/nix/packages/docker-ansible-test.nix @@ -0,0 +1,21 @@ +{ + pkgs, + lib, + docker-image-ubuntu, +}: +let + tools = [ pkgs.ansible ]; +in +pkgs.dockerTools.buildLayeredImage { + name = "supabase/ansible-test"; + tag = "latest"; + maxLayers = 30; + fromImage = docker-image-ubuntu; + compressor = "zstd"; + config = { + Env = [ + "PATH=${lib.makeBinPath tools}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ]; + Cmd = [ "/lib/systemd/systemd" ]; + }; +} diff --git a/nix/packages/docker-ubuntu.nix b/nix/packages/docker-ubuntu.nix new file mode 100644 index 000000000..c492380f9 --- /dev/null +++ b/nix/packages/docker-ubuntu.nix @@ -0,0 +1,57 @@ +{ + runCommand, + dockerTools, + xz, + buildEnv, + stdenv, +}: +let + ubuntu-cloudimg = + let + + cloudImg = + if stdenv.hostPlatform.system == "x86_64-linux" then + builtins.fetchurl { + url = "https://cloud-images.ubuntu.com/releases/noble/release-20251026/ubuntu-24.04-server-cloudimg-amd64-root.tar.xz"; + sha256 = "0y3d55f5qy7bxm3mfmnxzpmwp88d7iiszc57z5b9npc6xgwi28np"; + } + else + builtins.fetchurl { + url = "https://cloud-images.ubuntu.com/releases/noble/release-20251026/ubuntu-24.04-server-cloudimg-arm64-root.tar.xz"; + sha256 = "1l4l0llfffspzgnmwhax0fcnjn8ih8n4azhfaghng2hh1xvr4a17"; + }; + in + runCommand "ubuntu-cloudimg" { nativeBuildInputs = [ xz ]; } '' + mkdir -p $out + tar --exclude='dev/*' \ + --exclude='etc/systemd/system/network-online.target.wants/systemd-networkd-wait-online.service' \ + --exclude='etc/systemd/system/multi-user.target.wants/systemd-resolved.service' \ + --exclude='usr/lib/systemd/system/tpm-udev.service' \ + --exclude='usr/lib/systemd/system/systemd-remount-fs.service' \ + --exclude='usr/lib/systemd/system/systemd-resolved.service' \ + --exclude='usr/lib/systemd/system/proc-sys-fs-binfmt_misc.automount' \ + --exclude='usr/lib/systemd/system/sys-kernel-*' \ + --exclude='var/lib/apt/lists/*' \ + -xJf ${cloudImg} -C $out + rm -f $out/bin $out/lib $out/lib64 $out/sbin + mkdir -p $out/run/systemd && echo 'docker' > $out/run/systemd/container + mkdir $out/var/lib/apt/lists/partial + ''; +in +dockerTools.buildImage { + name = "ubuntu-cloudimg"; + tag = "24.04"; + created = "now"; + extraCommands = '' + ln -s usr/bin + ln -s usr/lib + ln -s usr/lib64 + ln -s usr/sbin + ''; + copyToRoot = buildEnv { + name = "image-root"; + pathsToLink = [ "/" ]; + paths = [ ubuntu-cloudimg ]; + }; + config.Cmd = [ "/lib/systemd/systemd" ]; +} From b8ffeff261d50bcb251c4e72d59404ddf3349c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 11 Nov 2025 15:14:18 +0100 Subject: [PATCH 2/6] feat: add ansible-lint validation for test playbooks Configure pre-commit hook to run ansible-lint on test playbooks and their dependencies. Since test playbooks include tasks from existing task files, ansible-lint automatically validates those dependencies as well. --- .ansible-lint.yml | 44 ++++++++++++++++++++++ ansible/tasks/setup-nginx.yml | 70 ++++++++++++++++++----------------- ansible/tests/nginx.yaml | 26 +++++++------ nix/hooks.nix | 9 +++++ 4 files changed, 104 insertions(+), 45 deletions(-) create mode 100644 .ansible-lint.yml diff --git a/.ansible-lint.yml b/.ansible-lint.yml new file mode 100644 index 000000000..512c3751c --- /dev/null +++ b/.ansible-lint.yml @@ -0,0 +1,44 @@ +--- +profile: production + +# exclude_paths included in this file are parsed relative to this file's location +# and not relative to the CWD of execution. CLI arguments passed to the --exclude +# option are parsed relative to the CWD of execution. +exclude_paths: + - .cache/ # implicit unless exclude_paths is defined in config + - .github/ + - ansible/files/ + - ansible/manifest-playbook.yml + - ansible/playbook.yml + - ansible/tasks/ + - audit-specs/ + - nix/mkdocs.yml + +use_default_rules: true +enable_list: + - args + - empty-string-compare + - no-log-password + - no-same-owner +warn_list: + - experimental +skip_list: + - name[casing] + - name[prefix] + - yaml[line-length] + - var-naming[no-role-prefix] + +# Offline mode disables installation of requirements.yml +offline: false + +# Make the output more readable +parseable: true + +# Define required Ansible's variables to satisfy syntax check +# extra_vars: + +# List of additional kind:pattern to be added at the top of the default +# match list, first match determines the file kind. +kinds: + - tasks: "ansible/tasks/*.yml" + - vars: "ansible/vars.yml" diff --git a/ansible/tasks/setup-nginx.yml b/ansible/tasks/setup-nginx.yml index 1f10ceec2..58986419a 100644 --- a/ansible/tasks/setup-nginx.yml +++ b/ansible/tasks/setup-nginx.yml @@ -1,10 +1,11 @@ -- name: nginx - system user +--- +- name: Nginx - system user ansible.builtin.user: - name: 'nginx' - state: 'present' + name: nginx + state: present # Kong installation steps from http://archive.vn/3HRQx -- name: nginx - system dependencies +- name: Nginx - system dependencies ansible.builtin.apt: pkg: - libpcre3-dev @@ -12,67 +13,70 @@ - openssl - zlib1g-dev -- name: nginx - download source +- name: Nginx - download source ansible.builtin.get_url: checksum: "{{ nginx_release_checksum }}" - dest: '/tmp/nginx-{{ nginx_release }}.tar.gz' - url: "https://nginx.org/download/nginx-{{ nginx_release }}.tar.gz" + dest: /tmp/nginx-{{ nginx_release }}.tar.gz + url: https://nginx.org/download/nginx-{{ nginx_release }}.tar.gz + mode: '0640' -- name: nginx - unpack archive +- name: Nginx - unpack archive ansible.builtin.unarchive: - dest: '/tmp' + dest: /tmp remote_src: true - src: "/tmp/nginx-{{ nginx_release }}.tar.gz" + src: /tmp/nginx-{{ nginx_release }}.tar.gz -- name: nginx - configure +- name: Nginx - configure ansible.builtin.command: argv: - - ./configure - - --prefix=/usr/local/nginx - - --conf-path=/etc/nginx/nginx.conf - - --with-http_ssl_module - - --with-http_realip_module + - ./configure + - --prefix=/usr/local/nginx + - --conf-path=/etc/nginx/nginx.conf + - --with-http_ssl_module + - --with-http_realip_module - --with-threads + creates: /tmp/nginx-{{ nginx_release }}/Makefile args: - chdir: "/tmp/nginx-{{ nginx_release }}" + chdir: /tmp/nginx-{{ nginx_release }} become: true -- name: nginx - build and install +- name: Nginx - build and install community.general.make: - chdir: "/tmp/nginx-{{ nginx_release }}" + chdir: /tmp/nginx-{{ nginx_release }} jobs: "{{ parallel_jobs | default(omit) }}" target: "{{ make_target }}" become: true loop: - - 'build' - - 'install' + - build + - install loop_control: - loop_var: 'make_target' + loop_var: make_target -- name: nginx - hand over ownership of /etc/nginx and /usr/local/nginx to user nginx +- name: Nginx - hand over ownership of /etc/nginx and /usr/local/nginx to user nginx ansible.builtin.file: - owner: 'nginx' + owner: nginx path: "{{ nginx_dir_item }}" recurse: true loop: - /etc/nginx - /usr/local/nginx loop_control: - loop_var: 'nginx_dir_item' + loop_var: nginx_dir_item # [warn] ulimit is currently set to "1024". For better performance set it to at least # "4096" using "ulimit -n" -- name: nginx - bump up ulimit +- name: Nginx - bump up ulimit community.general.pam_limits: - domain: 'nginx' - limit_item: 'nofile' - limit_type: 'soft' - value: '4096' + domain: nginx + limit_item: nofile + limit_type: soft + value: "4096" -- name: nginx - create service file +- name: Nginx - create service file ansible.builtin.template: - dest: '/etc/systemd/system/nginx.service' - src: 'files/nginx.service.j2' + dest: /etc/systemd/system/nginx.service + src: files/nginx.service.j2 + mode: '0644' # Keep it dormant for the timebeing diff --git a/ansible/tests/nginx.yaml b/ansible/tests/nginx.yaml index 720e79679..0054819c1 100644 --- a/ansible/tests/nginx.yaml +++ b/ansible/tests/nginx.yaml @@ -1,14 +1,16 @@ --- -- hosts: localhost +- name: Setup Nginx Server + hosts: localhost tasks: - - name: Install dependencies - apt: - pkg: - - build-essential - update_cache: yes - - import_tasks: ../tasks/setup-nginx.yml - - name: Start Nginx service - service: - name: nginx - state: started - enabled: yes + - name: Install dependencies + ansible.builtin.apt: + pkg: + - build-essential + update_cache: true + - name: Setup Nginx using existing task file + ansible.builtin.import_tasks: ../tasks/setup-nginx.yml + - name: Start Nginx service + ansible.builtin.service: + name: nginx + state: started + enabled: true diff --git a/nix/hooks.nix b/nix/hooks.nix index 74ea506c9..f0d5bc5d7 100644 --- a/nix/hooks.nix +++ b/nix/hooks.nix @@ -25,6 +25,15 @@ in verbose = true; }; + ansible-lint = { + enable = true; + verbose = true; + settings = { + configPath = "${../.ansible-lint.yml}"; + subdir = "ansible/tests"; + }; + }; + treefmt = { enable = true; package = config.treefmt.build.wrapper; From b5eb5ff20dedaa91687cf00ba2327865eafea32f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Mon, 4 Aug 2025 21:27:39 +0200 Subject: [PATCH 3/6] feat: deploy nginx using system manager And use docker to run tests --- flake.lock | 188 ++++++++++++++++++++++++++ flake.nix | 3 + nix/systemModules/default.nix | 14 ++ nix/systemModules/tests/conftest.py | 109 +++++++++++++++ nix/systemModules/tests/default.nix | 70 ++++++++++ nix/systemModules/tests/test_nginx.py | 3 + 6 files changed, 387 insertions(+) create mode 100644 nix/systemModules/default.nix create mode 100644 nix/systemModules/tests/conftest.py create mode 100644 nix/systemModules/tests/default.nix create mode 100644 nix/systemModules/tests/test_nginx.py diff --git a/flake.lock b/flake.lock index 02d9a0986..f6fa46186 100644 --- a/flake.lock +++ b/flake.lock @@ -16,6 +16,22 @@ "type": "github" } }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1747046372, + "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-parts": { "inputs": { "nixpkgs-lib": "nixpkgs-lib" @@ -34,6 +50,28 @@ "type": "github" } }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": [ + "system-manager", + "userborn", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1756770412, + "narHash": "sha256-+uWLQZccFHwqpGqr2Yt5VsW/PbeJVTn9Dk6SHWhNRPw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "4524271976b625a4a605beefd893f270620fd751", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -95,6 +133,29 @@ "type": "github" } }, + "gitignore_2": { + "inputs": { + "nixpkgs": [ + "system-manager", + "userborn", + "pre-commit-hooks-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, "nix": { "flake": false, "locked": { @@ -236,6 +297,50 @@ "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" } }, + "nixpkgs_3": { + "locked": { + "lastModified": 1757745802, + "narHash": "sha256-hLEO2TPj55KcUFUU1vgtHE9UEIOjRcH/4QbmfHNF820=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c23193b943c6c689d70ee98ce3128239ed9e32d1", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks-nix": { + "inputs": { + "flake-compat": [ + "system-manager", + "userborn", + "flake-compat" + ], + "gitignore": "gitignore_2", + "nixpkgs": [ + "system-manager", + "userborn", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1757588530, + "narHash": "sha256-tJ7A8mID3ct69n9WCvZ3PzIIl3rXTdptn/lZmqSS95U=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "b084b2c2b6bc23e83bbfe583b03664eb0b18c411", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, "root": { "inputs": { "flake-parts": "flake-parts", @@ -247,6 +352,7 @@ "nixpkgs": "nixpkgs_2", "nixpkgs-oldstable": "nixpkgs-oldstable", "rust-overlay": "rust-overlay", + "system-manager": "system-manager", "treefmt-nix": "treefmt-nix" } }, @@ -270,6 +376,50 @@ "type": "github" } }, + "sops-nix": { + "inputs": { + "nixpkgs": [ + "system-manager", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1768709255, + "narHash": "sha256-aigyBfxI20FRtqajVMYXHtj5gHXENY2gLAXEhfJ8/WM=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "5e8fae80726b66e9fec023d21cd3b3e638597aa9", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" + } + }, + "system-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "sops-nix": "sops-nix", + "userborn": "userborn" + }, + "locked": { + "lastModified": 1768847146, + "narHash": "sha256-ALJX+yXIHN9he7lRRG45E+NW5Hf0TY+0FI9uIvblDGA=", + "owner": "numtide", + "repo": "system-manager", + "rev": "98bafdee7bdbb3499559c151a944adce37c3af3d", + "type": "github" + }, + "original": { + "owner": "numtide", + "ref": "secrets", + "repo": "system-manager", + "type": "github" + } + }, "systems": { "locked": { "lastModified": 1681028828, @@ -285,6 +435,21 @@ "type": "github" } }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "treefmt-nix": { "inputs": { "nixpkgs": [ @@ -304,6 +469,29 @@ "repo": "treefmt-nix", "type": "github" } + }, + "userborn": { + "inputs": { + "flake-compat": "flake-compat_2", + "flake-parts": "flake-parts_2", + "nixpkgs": "nixpkgs_3", + "pre-commit-hooks-nix": "pre-commit-hooks-nix", + "systems": "systems_2" + }, + "locked": { + "lastModified": 1762107051, + "narHash": "sha256-8bvUPwdiUnqgBnNAuPJlbNFGProAIzlDzjiaqQugPJY=", + "owner": "JulienMalka", + "repo": "userborn", + "rev": "6e8f0d00e683049ac727b626552d5eba7f3471ff", + "type": "github" + }, + "original": { + "owner": "JulienMalka", + "ref": "stateful-users", + "repo": "userborn", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 595cd2d96..4c4593ba0 100644 --- a/flake.nix +++ b/flake.nix @@ -27,6 +27,8 @@ rust-overlay.url = "github:oxalica/rust-overlay"; treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; treefmt-nix.url = "github:numtide/treefmt-nix"; + system-manager.inputs.nixpkgs.follows = "nixpkgs"; + system-manager.url = "github:numtide/system-manager/secrets"; }; outputs = @@ -48,6 +50,7 @@ nix/nixpkgs.nix nix/packages nix/overlays + nix/systemModules ]; }); } diff --git a/nix/systemModules/default.nix b/nix/systemModules/default.nix new file mode 100644 index 000000000..5a3c69be2 --- /dev/null +++ b/nix/systemModules/default.nix @@ -0,0 +1,14 @@ +{ + flake-parts-lib, + withSystem, + self, + ... +}: +{ + imports = [ ./tests ]; + flake = { + systemModules = { +nginx = flake-parts-lib.importApply ./nginx.nix { inherit withSystem self; }; + }; + }; +} diff --git a/nix/systemModules/tests/conftest.py b/nix/systemModules/tests/conftest.py new file mode 100644 index 000000000..581c2dea1 --- /dev/null +++ b/nix/systemModules/tests/conftest.py @@ -0,0 +1,109 @@ +import pytest +import shutil +import subprocess +import testinfra +import time +from rich.console import Console + +console = Console() + + +def pytest_addoption(parser): + parser.addoption( + "--image-name", + action="store", + help="Docker image and tag to use for testing", + ) + parser.addoption( + "--image-path", + action="store", + help="Compressed Docker image to load for testing", + ) + + parser.addoption( + "--force-docker", + action="store_true", + help="Force using Docker instead of Podman for testing", + ) + + +@pytest.fixture(scope="session") +def host(request): + force_docker = request.config.getoption("--force-docker") + image_name = request.config.getoption("--image-name") + image_path = request.config.getoption("--image-path") + if not force_docker and shutil.which("podman"): + console.log("Using Podman for testing") + with console.status("Loading image..."): + subprocess.check_output(["podman", "load", "-q", "-i", image_path]) + podman_id = ( + subprocess.check_output( + [ + "podman", + "run", + "--cap-add", + "SYS_ADMIN", + "-d", + image_name, + ] + ) + .decode() + .strip() + ) + yield testinfra.get_host("podman://" + podman_id) + with console.status("Cleaning up..."): + subprocess.check_call( + ["podman", "rm", "-f", podman_id], stdout=subprocess.DEVNULL + ) + else: + console.log("Using Docker for testing") + with console.status("Loading image..."): + subprocess.check_output(["docker", "load", "-q", "-i", image_path]) + docker_id = ( + subprocess.check_output( + [ + "docker", + "run", + "--privileged", + "--cap-add", + "SYS_ADMIN", + "--security-opt", + "seccomp=unconfined", + "--cgroup-parent=docker.slice", + "--cgroupns", + "private", + "-d", + image_name, + ] + ) + .decode() + .strip() + ) + yield testinfra.get_host("docker://" + docker_id) + with console.status("Cleaning up..."): + subprocess.check_call( + ["docker", "rm", "-f", docker_id], stdout=subprocess.DEVNULL + ) + + +def wait_for_target(host, target, timeout=60): + start_time = time.time() + while time.time() - start_time < timeout: + result = host.run(f"systemctl is-active {target}") + if result.rc == 0: + return True + time.sleep(0.2) + return False + + +@pytest.fixture(scope="session", autouse=True) +def activate_system_manager(host): + with console.status("Waiting systemd to be ready..."): + assert wait_for_target(host, "multi-user.target") + result = host.run("activate") + console.log(result.stdout) + console.log(result.stderr) + if result.failed: + raise pytest.fail( + "System manager activation failed with return code {}".format(result.rc) + ) diff --git a/nix/systemModules/tests/default.nix b/nix/systemModules/tests/default.nix new file mode 100644 index 000000000..cb489b714 --- /dev/null +++ b/nix/systemModules/tests/default.nix @@ -0,0 +1,70 @@ +{ self, ... }: +{ + perSystem = + { + lib, + pkgs, + self', + ... + }: + { + packages = lib.optionalAttrs (pkgs.stdenv.hostPlatform.isLinux) { + check-system-manager = + let + lib = pkgs.lib; + systemManagerConfig = self.inputs.system-manager.lib.makeSystemConfig { + modules = [ + ({ + services.nginx.enable = true; + nixpkgs.hostPlatform = pkgs.system; + }) + ]; + }; + + dockerImageUbuntuWithTools = + let + tools = [ systemManagerConfig ]; + in + pkgs.dockerTools.buildLayeredImage { + name = "ubuntu-cloudimg-with-tools"; + tag = "0.2"; + created = "now"; + maxLayers = 30; + fromImage = self'.packages.docker-image-ubuntu; + compressor = "zstd"; + config = { + Env = [ + "PATH=${lib.makeBinPath tools}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ]; + Cmd = [ "/lib/systemd/systemd" ]; + }; + }; + in + pkgs.writeShellApplication { + name = "system-manager-test"; + passthru = { + inherit systemManagerConfig dockerImageUbuntuWithTools; + }; + runtimeInputs = with pkgs; [ + (python3.withPackages ( + ps: with ps; [ + requests + pytest + pytest-testinfra + rich + ] + )) + ]; + text = '' + export DOCKER_IMAGE=${dockerImageUbuntuWithTools.imageName}:${dockerImageUbuntuWithTools.imageTag} + TEST_DIR=${./.} + pytest -p no:cacheprovider -s -v "$@" $TEST_DIR --image-name=$DOCKER_IMAGE --image-path=${dockerImageUbuntuWithTools} + ''; + meta = with pkgs.lib; { + description = "Test deployment with system-manager"; + platforms = platforms.linux; + }; + }; + }; + }; +} diff --git a/nix/systemModules/tests/test_nginx.py b/nix/systemModules/tests/test_nginx.py new file mode 100644 index 000000000..047d07956 --- /dev/null +++ b/nix/systemModules/tests/test_nginx.py @@ -0,0 +1,3 @@ +def test_nginx_service(host): + assert host.service("nginx.service").is_valid + assert host.service("nginx.service").is_running From 847835fb36a17df1377acfda9b9bb594c1f331e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 12 Aug 2025 20:11:40 +0200 Subject: [PATCH 4/6] Test system manager deployment triggered by Ansible --- ansible/tasks/setup-nix.yml | 11 ++++++++ ansible/tasks/setup-system-manager.yml | 7 ++++++ ansible/tests/nix.yaml | 5 ++++ ansible/tests/test_nix.py | 10 ++++++++ nix/packages/ansible-test.nix | 35 +++++++++++++++++++++++--- nix/packages/default.nix | 2 +- 6 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 ansible/tasks/setup-nix.yml create mode 100644 ansible/tasks/setup-system-manager.yml create mode 100644 ansible/tests/nix.yaml create mode 100644 ansible/tests/test_nix.py diff --git a/ansible/tasks/setup-nix.yml b/ansible/tasks/setup-nix.yml new file mode 100644 index 000000000..9675677dd --- /dev/null +++ b/ansible/tasks/setup-nix.yml @@ -0,0 +1,11 @@ +--- +- name: Check if nix is installed + ansible.builtin.command: which nix + register: nix_installed + failed_when: nix_installed.rc != 0 + ignore_errors: true + +- name: Install nix + ansible.builtin.shell: curl --proto '=https' --tlsv1.2 -sSf -L https://artifacts.nixos.org/experimental-installer | sh -s -- install --no-confirm --extra-conf 'substituters = https://cache.nixos.org https://nix-postgres-artifacts.s3.amazonaws.com' --extra-conf 'trusted-public-keys = nix-postgres-artifacts:dGZlQOvKcNEjvT7QEAJbcV6b6uk7VF/hWMjhYleiaLI=% cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=' + when: nix_installed.rc != 0 + become: true diff --git a/ansible/tasks/setup-system-manager.yml b/ansible/tasks/setup-system-manager.yml new file mode 100644 index 000000000..990af5120 --- /dev/null +++ b/ansible/tasks/setup-system-manager.yml @@ -0,0 +1,7 @@ +--- +- name: Deploy system manager + ansible.builtin.shell: | + . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh + cd /tmp + nix run --accept-flake-config /flake#system-manager -- switch --flake /flake + become: true diff --git a/ansible/tests/nix.yaml b/ansible/tests/nix.yaml new file mode 100644 index 000000000..4effc67dd --- /dev/null +++ b/ansible/tests/nix.yaml @@ -0,0 +1,5 @@ +--- +- hosts: localhost + tasks: + - import_tasks: ../tasks/setup-nix.yml + - import_tasks: ../tasks/setup-system-manager.yml diff --git a/ansible/tests/test_nix.py b/ansible/tests/test_nix.py new file mode 100644 index 000000000..606aa5a26 --- /dev/null +++ b/ansible/tests/test_nix.py @@ -0,0 +1,10 @@ +import pytest + + +@pytest.fixture(scope="module", autouse=True) +def run_ansible(run_ansible_playbook): + run_ansible_playbook("nix.yaml", verbose=True) + + +def test_nix_service(host): + assert host.service("nix-daemon.service").is_running diff --git a/nix/packages/ansible-test.nix b/nix/packages/ansible-test.nix index 618ecc1c9..b8d6ef46d 100644 --- a/nix/packages/ansible-test.nix +++ b/nix/packages/ansible-test.nix @@ -1,4 +1,28 @@ -{ self, pkgs }: +{ + pkgs, + lib, + docker-image-ubuntu, +}: +let + dockerImageUbuntuWithTools = + let + tools = [ pkgs.ansible ]; + in + pkgs.dockerTools.buildLayeredImage { + name = "ubuntu-cloudimg-with-tools"; + tag = "0.1"; + created = "now"; + maxLayers = 30; + fromImage = docker-image-ubuntu; + compressor = "zstd"; + config = { + Env = [ + "PATH=${lib.makeBinPath tools}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ]; + Cmd = [ "/lib/systemd/systemd" ]; + }; + }; +in pkgs.writeShellApplication { name = "ansible-test"; runtimeInputs = with pkgs; [ @@ -14,8 +38,13 @@ pkgs.writeShellApplication { ]; text = '' echo "Running Ansible tests..." - FLAKE_DIR=${self} - pytest -x -p no:cacheprovider -s -v "$@" $FLAKE_DIR/ansible/tests --flake-dir=$FLAKE_DIR --docker-image=supabase/ansible-test:latest "$@" + export DOCKER_IMAGE=${dockerImageUbuntuWithTools.imageName}:${dockerImageUbuntuWithTools.imageTag} + if ! docker image inspect $DOCKER_IMAGE > /dev/null; then + echo "Loading Docker image..." + docker load < ${dockerImageUbuntuWithTools} + fi + FLAKE_DIR=${../..} + pytest -x -p no:cacheprovider -s -v "$@" $FLAKE_DIR/ansible/tests --flake-dir=$FLAKE_DIR --docker-image=$DOCKER_IMAGE ''; meta = { description = "Ansible test runner"; diff --git a/nix/packages/default.nix b/nix/packages/default.nix index 0731596eb..057631fe0 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -35,7 +35,7 @@ { build-ami = pkgs.callPackage ./build-ami.nix { packer = self'.packages.packer; }; build-test-ami = pkgs.callPackage ./build-test-ami.nix { }; - ansible-test = pkgs.callPackage ./ansible-test.nix { inherit self; }; + ansible-test = pkgs.callPackage ./ansible-test.nix { inherit (self'.packages) docker-image-ubuntu; }; cleanup-ami = pkgs.callPackage ./cleanup-ami.nix { }; dbmate-tool = pkgs.callPackage ./dbmate-tool.nix { inherit (self.supabase) defaults; }; docker-ansible-test = pkgs.callPackage ./docker-ansible-test.nix { From cf8ddeff33d6ae91fa805dd0be48682dbca0b625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 12 Aug 2025 20:11:40 +0200 Subject: [PATCH 5/6] Create default system manager configuration --- flake.nix | 1 + nix/packages/default.nix | 5 ++++- nix/systemConfigs.nix | 30 +++++++++++++++++++++++++++++ nix/systemModules/default.nix | 4 ++-- nix/systemModules/tests/default.nix | 9 +-------- 5 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 nix/systemConfigs.nix diff --git a/flake.nix b/flake.nix index 4c4593ba0..11854ff55 100644 --- a/flake.nix +++ b/flake.nix @@ -51,6 +51,7 @@ nix/packages nix/overlays nix/systemModules + nix/systemConfigs.nix ]; }); } diff --git a/nix/packages/default.nix b/nix/packages/default.nix index 057631fe0..78d369cb0 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -35,7 +35,9 @@ { build-ami = pkgs.callPackage ./build-ami.nix { packer = self'.packages.packer; }; build-test-ami = pkgs.callPackage ./build-test-ami.nix { }; - ansible-test = pkgs.callPackage ./ansible-test.nix { inherit (self'.packages) docker-image-ubuntu; }; + ansible-test = pkgs.callPackage ./ansible-test.nix { + inherit (self'.packages) docker-image-ubuntu; + }; cleanup-ami = pkgs.callPackage ./cleanup-ami.nix { }; dbmate-tool = pkgs.callPackage ./dbmate-tool.nix { inherit (self.supabase) defaults; }; docker-ansible-test = pkgs.callPackage ./docker-ansible-test.nix { @@ -79,6 +81,7 @@ inherit (self'.packages) overlayfs-on-package; }; sync-exts-versions = pkgs.callPackage ./sync-exts-versions.nix { inherit (inputs') nix-editor; }; + system-manager = inputs'.system-manager.packages.default; trigger-nix-build = pkgs.callPackage ./trigger-nix-build.nix { }; update-readme = pkgs.callPackage ./update-readme.nix { }; inherit (pkgs.callPackage ./wal-g.nix { }) wal-g-2; diff --git a/nix/systemConfigs.nix b/nix/systemConfigs.nix new file mode 100644 index 000000000..7f50ded93 --- /dev/null +++ b/nix/systemConfigs.nix @@ -0,0 +1,30 @@ +{ self, inputs, ... }: +let + mkModules = system: [ + ({ + services.nginx.enable = true; + nixpkgs.hostPlatform = system; + }) + ]; + + systems = [ + "aarch64-linux" + "x86_64-linux" + ]; + + mkSystemConfig = system: { + name = system; + value.default = inputs.system-manager.lib.makeSystemConfig { + modules = mkModules system; + extraSpecialArgs = { + inherit self; + inherit system; + }; + }; + }; +in +{ + flake = { + systemConfigs = builtins.listToAttrs (map mkSystemConfig systems); + }; +} diff --git a/nix/systemModules/default.nix b/nix/systemModules/default.nix index 5a3c69be2..4810dff46 100644 --- a/nix/systemModules/default.nix +++ b/nix/systemModules/default.nix @@ -7,8 +7,8 @@ { imports = [ ./tests ]; flake = { - systemModules = { -nginx = flake-parts-lib.importApply ./nginx.nix { inherit withSystem self; }; + systemModules = { + nginx = flake-parts-lib.importApply ./nginx.nix { inherit withSystem self; }; }; }; } diff --git a/nix/systemModules/tests/default.nix b/nix/systemModules/tests/default.nix index cb489b714..104171a57 100644 --- a/nix/systemModules/tests/default.nix +++ b/nix/systemModules/tests/default.nix @@ -12,14 +12,7 @@ check-system-manager = let lib = pkgs.lib; - systemManagerConfig = self.inputs.system-manager.lib.makeSystemConfig { - modules = [ - ({ - services.nginx.enable = true; - nixpkgs.hostPlatform = pkgs.system; - }) - ]; - }; + systemManagerConfig = self.systemConfigs.${pkgs.system}.default; dockerImageUbuntuWithTools = let From f8ba3defdcd7309f0e9785190739acf664987897 Mon Sep 17 00:00:00 2001 From: Yvan Sraka Date: Fri, 19 Sep 2025 10:16:56 +0200 Subject: [PATCH 6/6] chores: add nix run .#ansible-test and .#check-system-module to github actions workflows --- .github/workflows/ansible-test.yml | 27 ++++++++++++++++++++++ .github/workflows/check-system-manager.yml | 27 ++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 .github/workflows/ansible-test.yml create mode 100644 .github/workflows/check-system-manager.yml diff --git a/.github/workflows/ansible-test.yml b/.github/workflows/ansible-test.yml new file mode 100644 index 000000000..2b75b5a22 --- /dev/null +++ b/.github/workflows/ansible-test.yml @@ -0,0 +1,27 @@ +name: Ansible Test + +on: + pull_request: + merge_group: + workflow_dispatch: + +permissions: + id-token: write + +jobs: + ansible-test: + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: supabase/postgres/.github/actions/shared-checkout@HEAD + + - name: Install nix + uses: ./.github/actions/nix-install-ephemeral + with: + push-to-cache: 'true' + env: + DEV_AWS_ROLE: ${{ secrets.DEV_AWS_ROLE }} + NIX_SIGN_SECRET_KEY: ${{ secrets.NIX_SIGN_SECRET_KEY }} + + - name: Run Ansible Test + run: nix run .#ansible-test diff --git a/.github/workflows/check-system-manager.yml b/.github/workflows/check-system-manager.yml new file mode 100644 index 000000000..3da3a3ce1 --- /dev/null +++ b/.github/workflows/check-system-manager.yml @@ -0,0 +1,27 @@ +name: Check System Manager + +on: + pull_request: + merge_group: + workflow_dispatch: + +permissions: + id-token: write + +jobs: + check-system-manager: + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: supabase/postgres/.github/actions/shared-checkout@HEAD + + - name: Install nix + uses: ./.github/actions/nix-install-ephemeral + with: + push-to-cache: 'true' + env: + DEV_AWS_ROLE: ${{ secrets.DEV_AWS_ROLE }} + NIX_SIGN_SECRET_KEY: ${{ secrets.NIX_SIGN_SECRET_KEY }} + + - name: Run check-system-manager + run: nix run .#check-system-manager