mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-08 03:01:29 -07:00
coderabbit changes
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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/
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user