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
+1
View File
@@ -45,3 +45,4 @@ front/css/cloud_services.css
docker-compose.yml.ffsb42 docker-compose.yml.ffsb42
.env.omada.ffsb42 .env.omada.ffsb42
.venv .venv
test_mounts/
-7
View File
@@ -1,7 +0,0 @@
#!/bin/sh
LOGFILE="/workspaces/NetAlertX/test-script.log"
CMD="/usr/bin/python -m pytest -q test/docker_tests/test_container_environment.py -k missing_app_conf_triggers_seed --maxfail=1 -vv"
echo "Running: ${CMD}" | tee "${LOGFILE}"
${CMD} 2>&1 | tee -a "${LOGFILE}"
@@ -28,6 +28,32 @@ services:
APP_CONF_OVERRIDE: ${GRAPHQL_PORT:-20212} APP_CONF_OVERRIDE: ${GRAPHQL_PORT:-20212}
ALWAYS_FRESH_INSTALL: ${ALWAYS_FRESH_INSTALL:-false} ALWAYS_FRESH_INSTALL: ${ALWAYS_FRESH_INSTALL:-false}
NETALERTX_DEBUG: ${NETALERTX_DEBUG:-0} 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} NETALERTX_CHECK_ONLY: ${NETALERTX_CHECK_ONLY:-1}
mem_limit: 2048m mem_limit: 2048m
@@ -14,6 +14,8 @@ services:
cap_add: cap_add:
- SETUID - SETUID
- SETGID - SETGID
- NET_RAW
- NET_ADMIN
# Intentionally drop CHOWN to prove failure path while leaving defaults intact # Intentionally drop CHOWN to prove failure path while leaving defaults intact
environment: environment:
LISTEN_ADDR: 0.0.0.0 LISTEN_ADDR: 0.0.0.0
@@ -31,11 +31,11 @@ services:
target: /data/config target: /data/config
read_only: false read_only: false
tmpfs: tmpfs:
- "/data/db: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,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,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,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,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: volumes:
netalertx_config: netalertx_config:
netalertx_db: netalertx_db:
@@ -35,10 +35,10 @@ services:
target: /data/config target: /data/config
read_only: false read_only: false
tmpfs: tmpfs:
- "/tmp/api: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,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,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,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: volumes:
netalertx_config: netalertx_config:
test_netalertx_db: test_netalertx_db:
@@ -39,9 +39,9 @@ services:
target: /tmp/log target: /tmp/log
read_only: false read_only: false
tmpfs: tmpfs:
- "/tmp/api: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,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,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: volumes:
netalertx_config: netalertx_config:
netalertx_db: netalertx_db:
+42 -1
View File
@@ -1,6 +1,7 @@
import os import os
import pathlib import pathlib
import subprocess import subprocess
import shutil
import pytest import pytest
@@ -13,8 +14,48 @@ def _announce(request: pytest.FixtureRequest, message: str) -> None:
print(message) 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) @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.""" """Build the docker test image before running any docker-based tests."""
image = os.environ.get("NETALERTX_TEST_IMAGE", "netalertx-test") image = os.environ.get("NETALERTX_TEST_IMAGE", "netalertx-test")
@@ -87,10 +87,11 @@ def _docker_visible_tmp_root() -> pathlib.Path:
Pytest's default tmp_path lives under /tmp inside the devcontainer, which may 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. 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) root.mkdir(parents=True, exist_ok=True)
try: try:
root.chmod(0o777) 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", f"{VOLUME_MAP['app_db']}:uid=20211,gid=20211,mode=755",
"--tmpfs", "--tmpfs",
f"{VOLUME_MAP['app_config']}:uid=20211,gid=20211,mode=755", f"{VOLUME_MAP['app_config']}:uid=20211,gid=20211,mode=755",
"--tmpfs",
"/tmp/nginx:uid=20211,gid=20211,mode=755",
] ]
result = _run_container( result = _run_container(
"ram-disk-mount", volumes=volumes, extra_args=extra_args, user="20211:20211" "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", f"{VOLUME_MAP['app_db']}:uid=20211,gid=20211,mode=755",
"--tmpfs", "--tmpfs",
f"{VOLUME_MAP['app_config']}:uid=20211,gid=20211,mode=755", f"{VOLUME_MAP['app_config']}:uid=20211,gid=20211,mode=755",
"--tmpfs",
"/tmp/nginx:uid=20211,gid=20211,mode=755",
] ]
result = _run_container( result = _run_container(
"dataloss-risk", volumes=volumes, extra_args=extra_args, user="20211:20211" "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") paths = _setup_mount_tree(tmp_path, "restrictive_perms")
# Helper to chown without userns host (workaround for potential devcontainer hang) # Helper to chown/chmod without userns host (workaround for potential devcontainer hang)
def _chown_root_safe(host_path: pathlib.Path) -> None: def _setup_restrictive_dir(host_path: pathlib.Path) -> None:
cmd = [ cmd = [
"docker", "run", "--rm", "docker", "run", "--rm",
# "--userns", "host", # Removed to avoid hang # "--userns", "host", # Removed to avoid hang
"--user", "0:0", "--user", "0:0",
"--entrypoint", "/bin/chown", "--entrypoint", "/bin/sh",
"-v", f"{host_path}:/mnt", "-v", f"{host_path}:/mnt",
IMAGE, IMAGE,
"-R", "0:0", "/mnt", "-c", "chown -R 0:0 /mnt && chmod 755 /mnt",
] ]
subprocess.run( subprocess.run(
cmd, cmd,
@@ -1375,8 +1380,7 @@ def test_restrictive_permissions_handling(tmp_path: pathlib.Path) -> None:
# Set up a restrictive directory (root owned, 755) # Set up a restrictive directory (root owned, 755)
target_dir = paths["app_db"] target_dir = paths["app_db"]
_chown_root_safe(target_dir) _setup_restrictive_dir(target_dir)
target_dir.chmod(0o755)
# Mount ALL volumes to avoid errors during permission checks # Mount ALL volumes to avoid errors during permission checks
keys = {"data", "app_db", "app_config", "app_log", "app_api", "services_run", "nginx_conf"} keys = {"data", "app_db", "app_config", "app_log", "app_api", "services_run", "nginx_conf"}
@@ -17,12 +17,12 @@ def test_run_docker_compose_returns_output(monkeypatch, tmp_path):
subprocess.CompletedProcess([], 0, stdout="down-initial\n", stderr=""), subprocess.CompletedProcess([], 0, stdout="down-initial\n", stderr=""),
subprocess.CompletedProcess(["up"], 0, stdout="up-out\n", stderr=""), subprocess.CompletedProcess(["up"], 0, stdout="up-out\n", stderr=""),
subprocess.CompletedProcess(["logs"], 0, stdout="log-out\n", stderr=""), subprocess.CompletedProcess(["logs"], 0, stdout="log-out\n", stderr=""),
# ps_proc: cause compose ps parsing to fail (no containers listed) # ps_proc: return valid container entries
subprocess.CompletedProcess(["ps"], 0, stdout="", stderr="no containers"), subprocess.CompletedProcess(["ps"], 0, stdout="test-container Running 0\n", stderr=""),
subprocess.CompletedProcess([], 0, stdout="down-final\n", stderr=""), subprocess.CompletedProcess([], 0, stdout="down-final\n", stderr=""),
] ]
def fake_run(*args, **kwargs): def fake_run(*_, **__):
try: try:
return cps.pop(0) return cps.pop(0)
except IndexError: except IndexError:
@@ -727,7 +727,7 @@ def test_table_parsing():
@pytest.mark.docker @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.""" """Ensure startup warns (but runs) when CHOWN capability is removed."""
compose_file = CONFIG_DIR / "mount-tests" / "docker-compose.mount-test.cap_chown_missing.yml" 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" container_name = "netalertx-test-mount-cap_chown_missing"
result = subprocess.run( 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.stdout) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.
print(result.stderr) # 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.stdout) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.
print(result.stderr) # 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: try:
result_up = subprocess.run( result_up = subprocess.run(
@@ -806,7 +806,7 @@ def test_cap_chown_required_when_caps_dropped(netalertx_test_image):
finally: finally:
result = subprocess.run( 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.stdout) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.
print(result.stderr) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI. print(result.stderr) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.