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

3
.gitignore vendored
View File

@@ -44,4 +44,5 @@ front/css/cloud_services.css
docker-compose.yml.ffsb42 docker-compose.yml.ffsb42
.env.omada.ffsb42 .env.omada.ffsb42
.venv .venv
test_mounts/

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}"

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

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")

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 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"}

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([], 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:

View File

@@ -651,7 +651,7 @@ def test_mount_diagnostic(netalertx_test_image, test_scenario):
# Wait for container to be ready # Wait for container to be ready
import time 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 # Give entrypoint scripts a moment to finish outputting to logs
time.sleep(2) time.sleep(2)
@@ -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.