coderabbit changes

This commit is contained in:
Adam Outler
2026-01-03 20:13:01 +00:00
parent 850d93ed62
commit 3cf856f1c2
11 changed files with 104 additions and 37 deletions

View File

@@ -28,6 +28,32 @@ services:
APP_CONF_OVERRIDE: ${GRAPHQL_PORT:-20212}
ALWAYS_FRESH_INSTALL: ${ALWAYS_FRESH_INSTALL:-false}
NETALERTX_DEBUG: ${NETALERTX_DEBUG:-0}
# Environment variable: NETALERTX_CHECK_ONLY
#
# Purpose: Enables check-only mode for container startup diagnostics and capability testing.
#
# When set to 1 (enabled):
# - Container runs all startup checks and prints diagnostic information
# - Services are NOT started (container exits after checks complete)
# - Useful for testing configurations, auditing capabilities, or troubleshooting
#
# When set to 0 (disabled):
# - Normal operation: container starts all services after passing checks
#
# Default: 1 in this compose file (check-only mode for testing)
# Production default: 0 (full startup)
#
# Automatic behavior:
# - May be automatically set by root-entrypoint.sh when privilege drop fails
# - Triggers immediate exit path in entrypoint.sh after diagnostic output
#
# Usage examples:
# NETALERTX_CHECK_ONLY: 0 # Normal startup with services
# NETALERTX_CHECK_ONLY: 1 # Check-only mode (exits after diagnostics)
#
# Troubleshooting:
# If container exits immediately after startup checks, verify this variable is set to 0
# for production deployments. Check container logs for diagnostic output from startup checks.
NETALERTX_CHECK_ONLY: ${NETALERTX_CHECK_ONLY:-1}
mem_limit: 2048m

View File

@@ -14,6 +14,8 @@ services:
cap_add:
- SETUID
- SETGID
- NET_RAW
- NET_ADMIN
# Intentionally drop CHOWN to prove failure path while leaving defaults intact
environment:
LISTEN_ADDR: 0.0.0.0

View File

@@ -31,11 +31,11 @@ services:
target: /data/config
read_only: false
tmpfs:
- "/data/db:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/api:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/log:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/run:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/nginx/active-config:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/data/db:mode=1700,uid=0,gid=0,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/api:mode=1700,uid=0,gid=0,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/log:mode=1700,uid=0,gid=0,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/run:mode=1700,uid=0,gid=0,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/nginx/active-config:mode=1700,uid=0,gid=0,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:

View File

@@ -35,10 +35,10 @@ services:
target: /data/config
read_only: false
tmpfs:
- "/tmp/api:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/log:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/run:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/nginx/active-config:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/api:mode=1700,uid=0,gid=0,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/log:mode=1700,uid=0,gid=0,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/run:mode=1700,uid=0,gid=0,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/nginx/active-config:mode=1700,uid=0,gid=0,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
test_netalertx_db:

View File

@@ -39,9 +39,9 @@ services:
target: /tmp/log
read_only: false
tmpfs:
- "/tmp/api:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/run:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/nginx/active-config:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/api:mode=1700,uid=20211,gid=20211,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/run:mode=1700,uid=20211,gid=20211,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp/nginx/active-config:mode=1700,uid=20211,gid=20211,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
volumes:
netalertx_config:
netalertx_db:

View File

@@ -1,6 +1,7 @@
import os
import pathlib
import subprocess
import shutil
import pytest
@@ -13,8 +14,48 @@ def _announce(request: pytest.FixtureRequest, message: str) -> None:
print(message)
def _clean_test_mounts(project_root: pathlib.Path) -> None:
"""Clean up the test_mounts directory, handling root-owned files via Docker."""
mounts_dir = project_root / "test_mounts"
if not mounts_dir.exists():
return
# Try python removal first (faster)
try:
shutil.rmtree(mounts_dir)
except PermissionError:
# Fallback to docker for root-owned files
# We mount the parent directory to delete the directory itself
cmd = [
"docker", "run", "--rm",
"-v", f"{project_root}:/work",
"alpine:3.22",
"rm", "-rf", "/work/test_mounts"
]
subprocess.run(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False
)
@pytest.fixture(scope="session")
def cleanup_artifacts(request: pytest.FixtureRequest) -> None:
"""Ensure test artifacts are cleaned up before and after the session."""
project_root = pathlib.Path(__file__).resolve().parents[2]
_announce(request, "[docker-tests] Cleaning up previous test artifacts...")
_clean_test_mounts(project_root)
yield
_announce(request, "[docker-tests] Cleaning up test artifacts...")
_clean_test_mounts(project_root)
@pytest.fixture(scope="session", autouse=True)
def build_netalertx_test_image(request: pytest.FixtureRequest) -> None:
def build_netalertx_test_image(request: pytest.FixtureRequest, cleanup_artifacts: None) -> None:
"""Build the docker test image before running any docker-based tests."""
image = os.environ.get("NETALERTX_TEST_IMAGE", "netalertx-test")

View File

@@ -87,10 +87,11 @@ def _docker_visible_tmp_root() -> pathlib.Path:
Pytest's default tmp_path lives under /tmp inside the devcontainer, which may
not be visible to the Docker daemon that evaluates bind mount source paths.
We use /tmp/pytest-docker-mounts instead of the repo.
We use a directory under the repo root which is guaranteed to be shared.
"""
root = pathlib.Path("/tmp/pytest-docker-mounts")
# Use a directory inside the workspace to ensure visibility to Docker daemon
root = _repo_root() / "test_mounts"
root.mkdir(parents=True, exist_ok=True)
try:
root.chmod(0o777)
@@ -1259,6 +1260,8 @@ def test_mount_analysis_ram_disk_performance(tmp_path: pathlib.Path) -> None:
f"{VOLUME_MAP['app_db']}:uid=20211,gid=20211,mode=755",
"--tmpfs",
f"{VOLUME_MAP['app_config']}:uid=20211,gid=20211,mode=755",
"--tmpfs",
"/tmp/nginx:uid=20211,gid=20211,mode=755",
]
result = _run_container(
"ram-disk-mount", volumes=volumes, extra_args=extra_args, user="20211:20211"
@@ -1314,6 +1317,8 @@ def test_mount_analysis_dataloss_risk(tmp_path: pathlib.Path) -> None:
f"{VOLUME_MAP['app_db']}:uid=20211,gid=20211,mode=755",
"--tmpfs",
f"{VOLUME_MAP['app_config']}:uid=20211,gid=20211,mode=755",
"--tmpfs",
"/tmp/nginx:uid=20211,gid=20211,mode=755",
]
result = _run_container(
"dataloss-risk", volumes=volumes, extra_args=extra_args, user="20211:20211"
@@ -1354,16 +1359,16 @@ def test_restrictive_permissions_handling(tmp_path: pathlib.Path) -> None:
"""
paths = _setup_mount_tree(tmp_path, "restrictive_perms")
# Helper to chown without userns host (workaround for potential devcontainer hang)
def _chown_root_safe(host_path: pathlib.Path) -> None:
# Helper to chown/chmod without userns host (workaround for potential devcontainer hang)
def _setup_restrictive_dir(host_path: pathlib.Path) -> None:
cmd = [
"docker", "run", "--rm",
# "--userns", "host", # Removed to avoid hang
"--user", "0:0",
"--entrypoint", "/bin/chown",
"--entrypoint", "/bin/sh",
"-v", f"{host_path}:/mnt",
IMAGE,
"-R", "0:0", "/mnt",
"-c", "chown -R 0:0 /mnt && chmod 755 /mnt",
]
subprocess.run(
cmd,
@@ -1375,8 +1380,7 @@ def test_restrictive_permissions_handling(tmp_path: pathlib.Path) -> None:
# Set up a restrictive directory (root owned, 755)
target_dir = paths["app_db"]
_chown_root_safe(target_dir)
target_dir.chmod(0o755)
_setup_restrictive_dir(target_dir)
# Mount ALL volumes to avoid errors during permission checks
keys = {"data", "app_db", "app_config", "app_log", "app_api", "services_run", "nginx_conf"}

View File

@@ -17,12 +17,12 @@ def test_run_docker_compose_returns_output(monkeypatch, tmp_path):
subprocess.CompletedProcess([], 0, stdout="down-initial\n", stderr=""),
subprocess.CompletedProcess(["up"], 0, stdout="up-out\n", stderr=""),
subprocess.CompletedProcess(["logs"], 0, stdout="log-out\n", stderr=""),
# ps_proc: cause compose ps parsing to fail (no containers listed)
subprocess.CompletedProcess(["ps"], 0, stdout="", stderr="no containers"),
# ps_proc: return valid container entries
subprocess.CompletedProcess(["ps"], 0, stdout="test-container Running 0\n", stderr=""),
subprocess.CompletedProcess([], 0, stdout="down-final\n", stderr=""),
]
def fake_run(*args, **kwargs):
def fake_run(*_, **__):
try:
return cps.pop(0)
except IndexError:

View File

@@ -651,7 +651,7 @@ def test_mount_diagnostic(netalertx_test_image, test_scenario):
# Wait for container to be ready
import time
# Container is still running - validate the diagnostics already run at startup
# Container is still running - validate the diagnostics already run at startup
# Give entrypoint scripts a moment to finish outputting to logs
time.sleep(2)
@@ -727,7 +727,7 @@ def test_table_parsing():
@pytest.mark.docker
def test_cap_chown_required_when_caps_dropped(netalertx_test_image):
def test_cap_chown_required_when_caps_dropped():
"""Ensure startup warns (but runs) when CHOWN capability is removed."""
compose_file = CONFIG_DIR / "mount-tests" / "docker-compose.mount-test.cap_chown_missing.yml"
@@ -747,7 +747,7 @@ def test_cap_chown_required_when_caps_dropped(netalertx_test_image):
container_name = "netalertx-test-mount-cap_chown_missing"
result = subprocess.run(
base_cmd + ["down", "-v"], capture_output=True, text=True, timeout=30, env=compose_env
[*base_cmd, "down", "-v"], capture_output=True, text=True, timeout=30, env=compose_env
)
print(result.stdout) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.
print(result.stderr) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.
@@ -762,7 +762,7 @@ def test_cap_chown_required_when_caps_dropped(netalertx_test_image):
print(result.stdout) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.
print(result.stderr) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.
cmd_up = base_cmd + ["up", "-d"]
cmd_up = [*base_cmd, "up", "-d"]
try:
result_up = subprocess.run(
@@ -806,7 +806,7 @@ def test_cap_chown_required_when_caps_dropped(netalertx_test_image):
finally:
result = subprocess.run(
base_cmd + ["down", "-v"], capture_output=True, text=True, timeout=30, env=compose_env
[*base_cmd, "down", "-v"], capture_output=True, text=True, timeout=30, env=compose_env
)
print(result.stdout) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.
print(result.stderr) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.