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/.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/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/.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 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/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/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/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..0054819c1 --- /dev/null +++ b/ansible/tests/nginx.yaml @@ -0,0 +1,16 @@ +--- +- name: Setup Nginx Server + hosts: localhost + tasks: + - 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/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_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/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/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..11854ff55 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,8 @@ nix/nixpkgs.nix nix/packages nix/overlays + nix/systemModules + nix/systemConfigs.nix ]; }); } diff --git a/nix/hooks.nix b/nix/hooks.nix index 9b78b8a5d..f0d5bc5d7 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" @@ -24,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; diff --git a/nix/packages/ansible-test.nix b/nix/packages/ansible-test.nix new file mode 100644 index 000000000..b8d6ef46d --- /dev/null +++ b/nix/packages/ansible-test.nix @@ -0,0 +1,52 @@ +{ + 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; [ + (python3.withPackages ( + ps: with ps; [ + requests + pytest + pytest-testinfra + pytest-xdist + rich + ] + )) + ]; + text = '' + echo "Running Ansible tests..." + 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 7977a0af9..78d369cb0 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -35,8 +35,15 @@ { 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; + }; 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; @@ -74,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/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" ]; +} 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 new file mode 100644 index 000000000..4810dff46 --- /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..104171a57 --- /dev/null +++ b/nix/systemModules/tests/default.nix @@ -0,0 +1,63 @@ +{ self, ... }: +{ + perSystem = + { + lib, + pkgs, + self', + ... + }: + { + packages = lib.optionalAttrs (pkgs.stdenv.hostPlatform.isLinux) { + check-system-manager = + let + lib = pkgs.lib; + systemManagerConfig = self.systemConfigs.${pkgs.system}.default; + + 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