Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .ansible-lint.yml
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +7 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find and read the .ansible-lint.yml file
find . -name ".ansible-lint.yml" -type f | head -5

Repository: supabase/postgres

Length of output: 80


🏁 Script executed:

# If it exists, read its full content
if [ -f ".ansible-lint.yml" ]; then
  cat -n ".ansible-lint.yml"
fi

Repository: supabase/postgres

Length of output: 1467


🏁 Script executed:

# Check if ansible/tasks/ directory exists
if [ -d "ansible/tasks" ]; then
  echo "=== Directory exists ==="
  find ansible/tasks -type f -name "*.yml" -o -name "*.yaml" | sort
else
  echo "ansible/tasks/ directory does not exist"
fi

Repository: supabase/postgres

Length of output: 1231


Remove the ansible/tasks/ exclusion to enable task linting.

The configuration excludes ansible/tasks/ from linting while also defining a kinds pattern for task files in the same directory. This contradiction prevents ansible-lint from checking any task files. If this PR aims to validate tasks, remove the ansible/tasks/ line from exclude_paths.

Additionally, note that the kinds pattern ansible/tasks/*.yml only matches top-level files and will miss nested tasks in ansible/tasks/internal/ (e.g., admin-api.yml, install-salt.yml). Consider updating the pattern to ansible/tasks/**/*.yml if nested tasks should be linted.

Suggested fix
 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

And optionally update the kinds pattern:

 kinds:
-  - tasks: "ansible/tasks/*.yml"
+  - tasks: "ansible/tasks/**/*.yml"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
exclude_paths:
- .cache/ # implicit unless exclude_paths is defined in config
- .github/
- ansible/files/
- ansible/manifest-playbook.yml
- ansible/playbook.yml
- audit-specs/
- nix/mkdocs.yml
🤖 Prompt for AI Agents
In @.ansible-lint.yml around lines 7 - 15, Remove the "ansible/tasks/" entry
from the exclude_paths list so task files are not skipped by ansible-lint, and
update the kinds pattern from "ansible/tasks/*.yml" to "ansible/tasks/**/*.yml"
to include nested task files (e.g., ansible/tasks/internal/*.yml); ensure you
modify the exclude_paths block and the kinds pattern reference in the config
accordingly.


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"
3 changes: 0 additions & 3 deletions .envrc.recommended

This file was deleted.

1 change: 1 addition & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ self-hosted-runner:
- aarch64-darwin
- aarch64-linux
- blacksmith-32vcpu-ubuntu-2404
- blacksmith-16vcpu-ubuntu-2404
- blacksmith-2vcpu-ubuntu-2404
- blacksmith-2vcpu-ubuntu-2404-arm
- blacksmith-4vcpu-ubuntu-2404
Expand Down
4 changes: 0 additions & 4 deletions .github/actionlint.yml

This file was deleted.

98 changes: 98 additions & 0 deletions .github/workflows/ansible-tests.yml
Original file line number Diff line number Diff line change
@@ -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: ${{ !failure() && !cancelled() }}
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
1 change: 1 addition & 0 deletions ansible/tasks/files
70 changes: 37 additions & 33 deletions ansible/tasks/setup-nginx.yml
Original file line number Diff line number Diff line change
@@ -1,78 +1,82 @@
- 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
- libssl-dev
- 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

Expand Down
84 changes: 84 additions & 0 deletions ansible/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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")
if not docker_image:
pytest.fail("--docker-image option is required")
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)
Comment on lines +23 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n -- "--flake-dir" -A 2 -B 2

Repository: supabase/postgres

Length of output: 230


🏁 Script executed:

cat -n ansible/tests/conftest.py | head -60

Repository: supabase/postgres

Length of output: 2045


🏁 Script executed:

git ls-files | grep -E "(conftest|pytest)" | head -20

Repository: supabase/postgres

Length of output: 86


🏁 Script executed:

rg -n "flake-dir"

Repository: supabase/postgres

Length of output: 450


🏁 Script executed:

rg -n "flake.dir\|flake_dir" ansible/tests/

Repository: supabase/postgres

Length of output: 43


🏁 Script executed:

find . -type f -name "*.sh" -o -name "*.yml" -o -name "*.yaml" | head -20

Repository: supabase/postgres

Length of output: 807


🏁 Script executed:

cat -n nix/packages/ansible-test.nix

Repository: supabase/postgres

Length of output: 754


🏁 Script executed:

cat -n .github/workflows/ansible-tests.yml

Repository: supabase/postgres

Length of output: 3842


🏁 Script executed:

rg -n "FLAKE_DIR" nix/

Repository: supabase/postgres

Length of output: 285


🏁 Script executed:

rg -n "from pathlib\|import pathlib" ansible/

Repository: supabase/postgres

Length of output: 43


🏁 Script executed:

rg -n "Path\|pathlib" ansible/tests/

Repository: supabase/postgres

Length of output: 43


🏁 Script executed:

rg -n "is_dir\|exists()" ansible/ | head -20

Repository: supabase/postgres

Length of output: 43


🏁 Script executed:

cat -n ansible/tests/conftest.py | sed -n '54,80p'

Repository: supabase/postgres

Length of output: 1059


🏁 Script executed:

rg -n "docker.*-v" ansible/

Repository: supabase/postgres

Length of output: 43


🏁 Script executed:

python3 << 'EOF'
import pathlib
# Check if pathlib is available and works as expected
p = pathlib.Path("/tmp").expanduser().resolve()
print(f"Path exists: {p.is_dir()}")
print(f"Path type: {type(p)}")
EOF

Repository: supabase/postgres

Length of output: 117


🏁 Script executed:

# Check if there are any other fixtures or functions that validate paths
rg -n "pytest.fail\|is_dir\|exists" ansible/tests/conftest.py

Repository: supabase/postgres

Length of output: 43


🏁 Script executed:

# Verify the exact Docker behavior with non-existent paths in this context
# by checking if there's any error handling for Docker mount issues
rg -n "docker.*run\|subprocess" ansible/tests/conftest.py -A 5

Repository: supabase/postgres

Length of output: 43


🏁 Script executed:

# Check what happens when docker run is called with non-existent mount path
# by looking at Docker documentation or testing behavior
python3 << 'EOF'
import subprocess
import tempfile

# Simulate what Docker does with non-existent paths
# Docker typically just creates an empty directory if the source doesn't exist

# Let's verify the concern: checking if Docker silently creates empty dirs
# by examining the current setup
print("Docker behavior with non-existent source paths:")
print("- Docker will mount a volume even if source doesn't exist (creates empty dir)")
print("- This would cause confusing errors later in the test execution")
print("- Early validation would catch this and fail explicitly")
EOF

Repository: supabase/postgres

Length of output: 306


🏁 Script executed:

# Check if there are any CI logs or error handling that shows this is a real issue
rg -n "mount\|volume" ansible/

Repository: supabase/postgres

Length of output: 43


🏁 Script executed:

# Verify the proposed fix doesn't break anything by checking all imports
head -10 ansible/tests/conftest.py

Repository: supabase/postgres

Length of output: 217


Add validation that --flake-dir is an existing directory.

Docker will silently mount an empty directory if the source path doesn't exist, causing confusing test failures later. Validating early provides immediate, explicit feedback.

Suggested implementation
+from pathlib import Path
     flake_dir = request.config.getoption("--flake-dir")
     if not flake_dir:
         pytest.fail("--flake-dir option is required")
+    flake_dir_path = Path(flake_dir).expanduser().resolve()
+    if not flake_dir_path.is_dir():
+        pytest.fail(f"--flake-dir must be an existing directory: {flake_dir}")
     docker_image = request.config.getoption("--docker-image")
     if not docker_image:
         pytest.fail("--docker-image option is required")
@@
-                f"{flake_dir}:/flake",
+                f"{flake_dir_path}:/flake",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@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")
if not docker_image:
pytest.fail("--docker-image option is required")
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 host(request):
from pathlib import Path
flake_dir = request.config.getoption("--flake-dir")
if not flake_dir:
pytest.fail("--flake-dir option is required")
flake_dir_path = Path(flake_dir).expanduser().resolve()
if not flake_dir_path.is_dir():
pytest.fail(f"--flake-dir must be an existing directory: {flake_dir}")
docker_image = request.config.getoption("--docker-image")
if not docker_image:
pytest.fail("--docker-image option is required")
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_path}:/flake",
"-d",
docker_image,
]
)
.decode()
.strip()
)
yield testinfra.get_host("docker://" + docker_id)
subprocess.check_call(["docker", "rm", "-f", docker_id], stdout=subprocess.DEVNULL)
🧰 Tools
🪛 Ruff (0.14.14)

[error] 32-32: subprocess call: check for execution of untrusted input

(S603)


[error] 33-48: Starting a process with a partial executable path

(S607)


[error] 54-54: subprocess call: check for execution of untrusted input

(S603)


[error] 54-54: Starting a process with a partial executable path

(S607)

🤖 Prompt for AI Agents
In `@ansible/tests/conftest.py` around lines 23 - 54, The host fixture currently
reads --flake-dir into flake_dir but doesn't verify it exists, causing docker to
silently mount an empty dir; update the host fixture to validate that flake_dir
is an existing directory (use os.path.exists and os.path.isdir on flake_dir) and
call pytest.fail with a clear message if it is missing or not a directory; keep
the same behavior for docker_image validation and use the existing flake_dir
variable when constructing the docker run volume argument.



@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
16 changes: 16 additions & 0 deletions ansible/tests/nginx.yaml
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions ansible/tests/test_nginx.py
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading