diff --git a/.ansible-lint.yml b/.ansible-lint.yml new file mode 100644 index 000000000..e010e6090 --- /dev/null +++ b/.ansible-lint.yml @@ -0,0 +1,38 @@ +--- +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/ + +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/actionlint.yml b/.github/actionlint.yml new file mode 100644 index 000000000..9b6706dff --- /dev/null +++ b/.github/actionlint.yml @@ -0,0 +1,8 @@ +self-hosted-runner: + labels: + - aarch64-darwin + - aarch64-linux + - blacksmith-2vcpu-ubuntu-2404-arm + - blacksmith-4vcpu-ubuntu-2404 + - blacksmith-16vcpu-ubuntu-2404 + - blacksmith-32vcpu-ubuntu-2404 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/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/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/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 896c262ba..1552e1638 100644 --- a/nix/hooks.nix +++ b/nix/hooks.nix @@ -1,4 +1,8 @@ { inputs, ... }: +let + ghWorkflows = builtins.attrNames (builtins.readDir ../.github/workflows); + lintedWorkflows = [ "ansible-test.yml" ]; +in { imports = [ inputs.git-hooks.flakeModule ]; perSystem = @@ -8,9 +12,26 @@ check.enable = true; settings = { hooks = { + actionlint = { + enable = true; + excludes = builtins.filter (name: !builtins.elem name lintedWorkflows) ghWorkflows; + verbose = true; + }; + + ansible-lint = { + enable = true; + verbose = true; + settings = { + configPath = "${../.ansible-lint.yml}"; + subdir = "ansible/tests"; + }; + }; + treefmt = { enable = true; package = config.treefmt.build.wrapper; + pass_filenames = false; + verbose = true; }; }; }; 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 c8eb02ef0..2a58f95ad 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -30,8 +30,13 @@ packages = ( { 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 { }; supabase-groonga = pkgs.callPackage ./groonga { }; http-mock-server = pkgs.callPackage ./http-mock-server.nix { }; 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" ]; +}