diff --git a/.gitignore b/.gitignore index 3aa37f33..760bb78f 100755 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,5 @@ front/css/cloud_services.css docker-compose.yml.ffsb42 .env.omada.ffsb42 -.venv \ No newline at end of file +.venv +test_mounts/ diff --git a/test-script.sh b/test-script.sh deleted file mode 100755 index b1f6c904..00000000 --- a/test-script.sh +++ /dev/null @@ -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}" diff --git a/test/docker_tests/configurations/docker-compose.missing-caps.yml b/test/docker_tests/configurations/docker-compose.missing-caps.yml index d40bc46e..57e308e7 100644 --- a/test/docker_tests/configurations/docker-compose.missing-caps.yml +++ b/test/docker_tests/configurations/docker-compose.missing-caps.yml @@ -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 diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.cap_chown_missing.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.cap_chown_missing.yml index 9d488bdc..597a9131 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.cap_chown_missing.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.cap_chown_missing.yml @@ -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 diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_ramdisk.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_ramdisk.yml index f1c4b365..6803c6e5 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_ramdisk.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_ramdisk.yml @@ -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: diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_unwritable.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_unwritable.yml index 931f7043..c43c705b 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_unwritable.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_unwritable.yml @@ -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: \ No newline at end of file diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_mounted.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_mounted.yml index 447fb4e8..eb0786e5 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_mounted.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_mounted.yml @@ -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: diff --git a/test/docker_tests/conftest.py b/test/docker_tests/conftest.py index 14fce16c..2643016b 100644 --- a/test/docker_tests/conftest.py +++ b/test/docker_tests/conftest.py @@ -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") diff --git a/test/docker_tests/test_container_environment.py b/test/docker_tests/test_container_environment.py index b61f7451..15e6f057 100644 --- a/test/docker_tests/test_container_environment.py +++ b/test/docker_tests/test_container_environment.py @@ -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"} diff --git a/test/docker_tests/test_docker_compose_unit.py b/test/docker_tests/test_docker_compose_unit.py index cdbb08fc..b664f7bc 100644 --- a/test/docker_tests/test_docker_compose_unit.py +++ b/test/docker_tests/test_docker_compose_unit.py @@ -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: diff --git a/test/docker_tests/test_mount_diagnostics_pytest.py b/test/docker_tests/test_mount_diagnostics_pytest.py index ab0e5353..cb0613b9 100644 --- a/test/docker_tests/test_mount_diagnostics_pytest.py +++ b/test/docker_tests/test_mount_diagnostics_pytest.py @@ -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.