mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-06 17:15:38 -08:00
Merge branch 'main' of https://github.com/jokob-sk/NetAlertX
This commit is contained in:
@@ -80,8 +80,9 @@ ENV SYSTEM_SERVICES=/services
|
||||
ENV SYSTEM_SERVICES_SCRIPTS=${SYSTEM_SERVICES}/scripts
|
||||
ENV SYSTEM_SERVICES_CONFIG=${SYSTEM_SERVICES}/config
|
||||
ENV SYSTEM_NGINX_CONFIG=${SYSTEM_SERVICES_CONFIG}/nginx
|
||||
ENV SYSTEM_NGINX_CONFIG_FILE=${SYSTEM_NGINX_CONFIG}/nginx.conf
|
||||
ENV SYSTEM_NGINX_CONFIG_TEMPLATE=${SYSTEM_NGINX_CONFIG}/netalertx.conf.template
|
||||
ENV SYSTEM_SERVICES_ACTIVE_CONFIG=/tmp/nginx/active-config
|
||||
ENV SYSTEM_SERVICES_ACTIVE_CONFIG_FILE=${SYSTEM_SERVICES_ACTIVE_CONFIG}/nginx.conf
|
||||
ENV SYSTEM_SERVICES_PHP_FOLDER=${SYSTEM_SERVICES_CONFIG}/php
|
||||
ENV SYSTEM_SERVICES_PHP_FPM_D=${SYSTEM_SERVICES_PHP_FOLDER}/php-fpm.d
|
||||
ENV SYSTEM_SERVICES_CROND=${SYSTEM_SERVICES_CONFIG}/crond
|
||||
@@ -138,6 +139,9 @@ RUN install -d -o ${NETALERTX_USER} -g ${NETALERTX_GROUP} -m 700 ${READ_WRITE_FO
|
||||
sh -c "find ${NETALERTX_APP} -type f \( -name '*.sh' -o -name 'speedtest-cli' \) \
|
||||
-exec chmod 750 {} \;"
|
||||
|
||||
# Copy version information into the image
|
||||
COPY --chown=${NETALERTX_USER}:${NETALERTX_GROUP} .VERSION ${NETALERTX_APP}/.VERSION
|
||||
|
||||
# Copy the virtualenv from the builder stage
|
||||
COPY --from=builder --chown=20212:20212 ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
# DO NOT MODIFY THIS FILE DIRECTLY. IT IS AUTO-GENERATED BY .devcontainer/scripts/generate-configs.sh
|
||||
# Generated from: install/production-filesystem/services/config/nginx/netalertx.conf.template
|
||||
|
||||
# Set number of worker processes automatically based on number of CPU cores.
|
||||
worker_processes auto;
|
||||
|
||||
# Enables the use of JIT for regular expressions to speed-up their processing.
|
||||
pcre_jit on;
|
||||
|
||||
# Configures default error logger.
|
||||
error_log /tmp/log/nginx-error.log warn;
|
||||
|
||||
pid /tmp/run/nginx.pid;
|
||||
|
||||
events {
|
||||
# The maximum number of simultaneous connections that can be opened by
|
||||
# a worker process.
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
# Mapping of temp paths for various nginx modules.
|
||||
client_body_temp_path /tmp/nginx/client_body;
|
||||
proxy_temp_path /tmp/nginx/proxy;
|
||||
fastcgi_temp_path /tmp/nginx/fastcgi;
|
||||
uwsgi_temp_path /tmp/nginx/uwsgi;
|
||||
scgi_temp_path /tmp/nginx/scgi;
|
||||
|
||||
# Includes mapping of file name extensions to MIME types of responses
|
||||
# and defines the default type.
|
||||
include /services/config/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Name servers used to resolve names of upstream servers into addresses.
|
||||
# It's also needed when using tcpsocket and udpsocket in Lua modules.
|
||||
#resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001];
|
||||
|
||||
# Don't tell nginx version to the clients. Default is 'on'.
|
||||
server_tokens off;
|
||||
|
||||
# Specifies the maximum accepted body size of a client request, as
|
||||
# indicated by the request header Content-Length. If the stated content
|
||||
# length is greater than this size, then the client receives the HTTP
|
||||
# error code 413. Set to 0 to disable. Default is '1m'.
|
||||
client_max_body_size 1m;
|
||||
|
||||
# Sendfile copies data between one FD and other from within the kernel,
|
||||
# which is more efficient than read() + write(). Default is off.
|
||||
sendfile on;
|
||||
|
||||
# Causes nginx to attempt to send its HTTP response head in one packet,
|
||||
# instead of using partial frames. Default is 'off'.
|
||||
tcp_nopush on;
|
||||
|
||||
|
||||
# Enables the specified protocols. Default is TLSv1 TLSv1.1 TLSv1.2.
|
||||
# TIP: If you're not obligated to support ancient clients, remove TLSv1.1.
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
|
||||
# Path of the file with Diffie-Hellman parameters for EDH ciphers.
|
||||
# TIP: Generate with: `openssl dhparam -out /etc/ssl/nginx/dh2048.pem 2048`
|
||||
#ssl_dhparam /etc/ssl/nginx/dh2048.pem;
|
||||
|
||||
# Specifies that our cipher suits should be preferred over client ciphers.
|
||||
# Default is 'off'.
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
# Enables a shared SSL cache with size that can hold around 8000 sessions.
|
||||
# Default is 'none'.
|
||||
ssl_session_cache shared:SSL:2m;
|
||||
|
||||
# Specifies a time during which a client may reuse the session parameters.
|
||||
# Default is '5m'.
|
||||
ssl_session_timeout 1h;
|
||||
|
||||
# Disable TLS session tickets (they are insecure). Default is 'on'.
|
||||
ssl_session_tickets off;
|
||||
|
||||
|
||||
# Enable gzipping of responses.
|
||||
gzip on;
|
||||
|
||||
# Set the Vary HTTP header as defined in the RFC 2616. Default is 'off'.
|
||||
gzip_vary on;
|
||||
|
||||
|
||||
# Specifies the main log format.
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
# Sets the path, format, and configuration for a buffered log write.
|
||||
access_log /tmp/log/nginx-access.log main;
|
||||
|
||||
|
||||
# Virtual host config
|
||||
server {
|
||||
listen 0.0.0.0:20211 default_server;
|
||||
large_client_header_buffers 4 16k;
|
||||
root /app/front;
|
||||
index index.php;
|
||||
add_header X-Forwarded-Prefix "/app" always;
|
||||
|
||||
location ~* \.php$ {
|
||||
# Set Cache-Control header to prevent caching on the first load
|
||||
add_header Cache-Control "no-store";
|
||||
fastcgi_pass unix:/tmp/run/php.sock;
|
||||
include /services/config/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||
|
||||
fastcgi_param PHP_VALUE "xdebug.remote_enable=1";
|
||||
fastcgi_connect_timeout 75;
|
||||
fastcgi_send_timeout 600;
|
||||
fastcgi_read_timeout 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,33 +30,4 @@ cat "${DEVCONTAINER_DIR}/resources/devcontainer-Dockerfile" >> "$OUT_FILE"
|
||||
|
||||
echo "Generated $OUT_FILE using root dir $ROOT_DIR" >&2
|
||||
|
||||
# Generate devcontainer nginx config from production template
|
||||
echo "Generating devcontainer nginx config"
|
||||
NGINX_TEMPLATE="${ROOT_DIR}/install/production-filesystem/services/config/nginx/netalertx.conf.template"
|
||||
NGINX_OUT="${DEVCONTAINER_DIR}/resources/devcontainer-overlay/services/config/nginx/netalertx.conf.template"
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
mkdir -p "$(dirname "$NGINX_OUT")"
|
||||
|
||||
# Start with header comment
|
||||
cat > "$NGINX_OUT" << 'EOF'
|
||||
# DO NOT MODIFY THIS FILE DIRECTLY. IT IS AUTO-GENERATED BY .devcontainer/scripts/generate-configs.sh
|
||||
# Generated from: install/production-filesystem/services/config/nginx/netalertx.conf.template
|
||||
|
||||
EOF
|
||||
|
||||
# Process the template: replace listen directive and inject Xdebug params
|
||||
sed 's/${LISTEN_ADDR}:${PORT}/0.0.0.0:20211/g' "$NGINX_TEMPLATE" | \
|
||||
awk '
|
||||
/fastcgi_param SCRIPT_NAME \$fastcgi_script_name;/ {
|
||||
print $0
|
||||
print ""
|
||||
print " fastcgi_param PHP_VALUE \"xdebug.remote_enable=1\";"
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' >> "$NGINX_OUT"
|
||||
|
||||
echo "Generated $NGINX_OUT from $NGINX_TEMPLATE" >&2
|
||||
|
||||
echo "Done."
|
||||
@@ -50,9 +50,6 @@ sudo chmod 777 /tmp/log /tmp/api /tmp/run /tmp/nginx
|
||||
|
||||
|
||||
|
||||
sudo rm -rf "${SYSTEM_NGINX_CONFIG}/conf.active"
|
||||
sudo ln -s "${SYSTEM_SERVICES_ACTIVE_CONFIG}" "${SYSTEM_NGINX_CONFIG}/conf.active"
|
||||
|
||||
sudo rm -rf /entrypoint.d
|
||||
sudo ln -s "${SOURCE_DIR}/install/production-filesystem/entrypoint.d" /entrypoint.d
|
||||
|
||||
@@ -67,6 +64,7 @@ for dir in \
|
||||
"${SYSTEM_SERVICES_RUN_LOG}" \
|
||||
"${SYSTEM_SERVICES_ACTIVE_CONFIG}" \
|
||||
"${NETALERTX_PLUGINS_LOG}" \
|
||||
"${SYSTEM_SERVICES_RUN_TMP}" \
|
||||
"/tmp/nginx/client_body" \
|
||||
"/tmp/nginx/proxy" \
|
||||
"/tmp/nginx/fastcgi" \
|
||||
@@ -75,9 +73,6 @@ for dir in \
|
||||
sudo install -d -m 777 "${dir}"
|
||||
done
|
||||
|
||||
# Create nginx temp subdirs with permissions
|
||||
sudo mkdir -p "${SYSTEM_SERVICES_RUN_TMP}/client_body" "${SYSTEM_SERVICES_RUN_TMP}/proxy" "${SYSTEM_SERVICES_RUN_TMP}/fastcgi" "${SYSTEM_SERVICES_RUN_TMP}/uwsgi" "${SYSTEM_SERVICES_RUN_TMP}/scgi"
|
||||
sudo chmod -R 777 "${SYSTEM_SERVICES_RUN_TMP}"
|
||||
|
||||
for var in "${LOG_FILES[@]}"; do
|
||||
path=${!var}
|
||||
|
||||
@@ -77,8 +77,9 @@ ENV SYSTEM_SERVICES=/services
|
||||
ENV SYSTEM_SERVICES_SCRIPTS=${SYSTEM_SERVICES}/scripts
|
||||
ENV SYSTEM_SERVICES_CONFIG=${SYSTEM_SERVICES}/config
|
||||
ENV SYSTEM_NGINX_CONFIG=${SYSTEM_SERVICES_CONFIG}/nginx
|
||||
ENV SYSTEM_NGINX_CONFIG_FILE=${SYSTEM_NGINX_CONFIG}/nginx.conf
|
||||
ENV SYSTEM_NGINX_CONFIG_TEMPLATE=${SYSTEM_NGINX_CONFIG}/netalertx.conf.template
|
||||
ENV SYSTEM_SERVICES_ACTIVE_CONFIG=/tmp/nginx/active-config
|
||||
ENV SYSTEM_SERVICES_ACTIVE_CONFIG_FILE=${SYSTEM_SERVICES_ACTIVE_CONFIG}/nginx.conf
|
||||
ENV SYSTEM_SERVICES_PHP_FOLDER=${SYSTEM_SERVICES_CONFIG}/php
|
||||
ENV SYSTEM_SERVICES_PHP_FPM_D=${SYSTEM_SERVICES_PHP_FOLDER}/php-fpm.d
|
||||
ENV SYSTEM_SERVICES_CROND=${SYSTEM_SERVICES_CONFIG}/crond
|
||||
|
||||
2
front/php/templates/language/ar_ar.json
Executable file → Normal file
2
front/php/templates/language/ar_ar.json
Executable file → Normal file
@@ -761,4 +761,4 @@
|
||||
"settings_system_label": "تسمية النظام",
|
||||
"settings_update_item_warning": "تحذير تحديث العنصر",
|
||||
"test_event_tooltip": "تلميح اختبار الحدث"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
#!/bin/bash
|
||||
echo "Initializing php-fpm..."
|
||||
# Set up PHP-FPM directories and socket configuration
|
||||
install -d -o netalertx -g netalertx /services/config/run
|
||||
|
||||
|
||||
echo "php-fpm initialized."
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/tmp/nginx/active-config
|
||||
@@ -5,8 +5,6 @@ set -euo pipefail
|
||||
LOG_DIR=${NETALERTX_LOG}
|
||||
RUN_DIR=${SYSTEM_SERVICES_RUN}
|
||||
TMP_DIR=/tmp/nginx
|
||||
SYSTEM_NGINX_CONFIG_TEMPLATE="/services/config/nginx/netalertx.conf.template"
|
||||
SYSTEM_NGINX_CONFIG_FILE="/services/config/nginx/conf.active/netalertx.conf"
|
||||
|
||||
# Create directories if they don't exist
|
||||
mkdir -p "${LOG_DIR}" "${RUN_DIR}" "${TMP_DIR}"
|
||||
@@ -33,9 +31,9 @@ done
|
||||
|
||||
TEMP_CONFIG_FILE=$(mktemp "${TMP_DIR}/netalertx.conf.XXXXXX")
|
||||
if envsubst '${LISTEN_ADDR} ${PORT}' < "${SYSTEM_NGINX_CONFIG_TEMPLATE}" > "${TEMP_CONFIG_FILE}" 2>/dev/null; then
|
||||
mv "${TEMP_CONFIG_FILE}" "${SYSTEM_NGINX_CONFIG_FILE}"
|
||||
mv "${TEMP_CONFIG_FILE}" "${SYSTEM_SERVICES_ACTIVE_CONFIG_FILE}"
|
||||
else
|
||||
echo "Note: Unable to write to ${SYSTEM_NGINX_CONFIG_FILE}. Using default configuration."
|
||||
echo "Note: Unable to write to ${SYSTEM_SERVICES_ACTIVE_CONFIG_FILE}. Using default configuration."
|
||||
rm -f "${TEMP_CONFIG_FILE}"
|
||||
fi
|
||||
|
||||
@@ -49,10 +47,10 @@ chmod -R 777 "/tmp/nginx" 2>/dev/null || true
|
||||
|
||||
# Execute nginx with overrides
|
||||
# echo the full nginx command then run it
|
||||
echo "Starting /usr/sbin/nginx -p \"${RUN_DIR}/\" -c \"${SYSTEM_NGINX_CONFIG_FILE}\" -g \"error_log /dev/stderr; error_log ${NETALERTX_LOG}/nginx-error.log; daemon off;\" &"
|
||||
echo "Starting /usr/sbin/nginx -p \"${RUN_DIR}/\" -c \"${SYSTEM_SERVICES_ACTIVE_CONFIG_FILE}\" -g \"error_log /dev/stderr; error_log ${NETALERTX_LOG}/nginx-error.log; daemon off;\" &"
|
||||
/usr/sbin/nginx \
|
||||
-p "${RUN_DIR}/" \
|
||||
-c "${SYSTEM_NGINX_CONFIG_FILE}" \
|
||||
-c "${SYSTEM_SERVICES_ACTIVE_CONFIG_FILE}" \
|
||||
-g "error_log /dev/stderr; error_log ${NETALERTX_LOG}/nginx-error.log; daemon off;" &
|
||||
nginx_pid=$!
|
||||
|
||||
|
||||
@@ -9,7 +9,13 @@ import copy
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from collections.abc import Callable, Iterable
|
||||
|
||||
from _pytest.outcomes import Skipped
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
@@ -29,6 +35,55 @@ CONTAINER_PATHS = {
|
||||
|
||||
TMPFS_ROOT = "/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
|
||||
|
||||
DEFAULT_HTTP_PORT = int(os.environ.get("NETALERTX_DEFAULT_HTTP_PORT", "20211"))
|
||||
COMPOSE_PORT_WAIT_TIMEOUT = int(os.environ.get("NETALERTX_COMPOSE_PORT_WAIT_TIMEOUT", "180"))
|
||||
COMPOSE_SETTLE_WAIT_SECONDS = int(os.environ.get("NETALERTX_COMPOSE_SETTLE_WAIT", "15"))
|
||||
PREFERRED_CUSTOM_PORTS = (22111, 22112)
|
||||
HOST_ADDR_ENV = os.environ.get("NETALERTX_HOST_ADDRS", "")
|
||||
|
||||
|
||||
def _discover_host_addresses() -> tuple[str, ...]:
|
||||
"""Return candidate loopback addresses for reaching host-mode containers."""
|
||||
|
||||
candidates: list[str] = ["127.0.0.1"]
|
||||
|
||||
if HOST_ADDR_ENV:
|
||||
env_addrs = [addr.strip() for addr in HOST_ADDR_ENV.split(",") if addr.strip()]
|
||||
candidates.extend(env_addrs)
|
||||
|
||||
ip_cmd = shutil.which("ip")
|
||||
if ip_cmd:
|
||||
try:
|
||||
route_proc = subprocess.run(
|
||||
[ip_cmd, "-4", "route", "show", "default"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=5,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
route_proc = None
|
||||
if route_proc and route_proc.returncode == 0 and route_proc.stdout:
|
||||
match = re.search(r"default\s+via\s+(?P<gateway>\S+)", route_proc.stdout)
|
||||
if match:
|
||||
gateway = match.group("gateway")
|
||||
candidates.append(gateway)
|
||||
|
||||
# Deduplicate while preserving order
|
||||
seen: set[str] = set()
|
||||
deduped: list[str] = []
|
||||
for addr in candidates:
|
||||
if addr not in seen:
|
||||
deduped.append(addr)
|
||||
seen.add(addr)
|
||||
|
||||
return tuple(deduped)
|
||||
|
||||
|
||||
HOST_ADDRESS_CANDIDATES = _discover_host_addresses()
|
||||
LAST_PORT_SUCCESSES: dict[int, str] = {}
|
||||
|
||||
pytestmark = [pytest.mark.docker, pytest.mark.compose]
|
||||
|
||||
IMAGE = os.environ.get("NETALERTX_TEST_IMAGE", "netalertx-test")
|
||||
@@ -151,12 +206,142 @@ def _extract_conflict_container_name(output: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _port_is_free(port: int) -> bool:
|
||||
"""Return True if a TCP port is available on localhost."""
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
sock.bind(("127.0.0.1", port))
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _wait_for_ports(ports: Iterable[int], timeout: int = COMPOSE_PORT_WAIT_TIMEOUT) -> None:
|
||||
"""Block until every port in the iterable accepts TCP connections or timeout expires."""
|
||||
|
||||
remaining = set(ports)
|
||||
deadline = time.time() + timeout
|
||||
last_errors: dict[int, dict[str, BaseException]] = {port: {} for port in remaining}
|
||||
|
||||
while remaining and time.time() < deadline:
|
||||
ready: list[int] = []
|
||||
for port in list(remaining):
|
||||
for addr in HOST_ADDRESS_CANDIDATES:
|
||||
try:
|
||||
with socket.create_connection((addr, port), timeout=2):
|
||||
ready.append(port)
|
||||
LAST_PORT_SUCCESSES[port] = addr
|
||||
break
|
||||
except OSError as exc:
|
||||
last_errors.setdefault(port, {})[addr] = exc
|
||||
else:
|
||||
continue
|
||||
for port in ready:
|
||||
remaining.discard(port)
|
||||
if remaining:
|
||||
time.sleep(1)
|
||||
|
||||
if remaining:
|
||||
details: list[str] = []
|
||||
for port in sorted(remaining):
|
||||
addr_errors = last_errors.get(port, {})
|
||||
if addr_errors:
|
||||
error_summary = ", ".join(f"{addr}: {err}" for addr, err in addr_errors.items())
|
||||
else:
|
||||
error_summary = "no connection attempts recorded"
|
||||
details.append(f"{port} -> {error_summary}")
|
||||
raise TimeoutError(
|
||||
"Ports did not become ready before timeout: " + "; ".join(details)
|
||||
)
|
||||
|
||||
|
||||
def _select_custom_ports() -> tuple[int, int]:
|
||||
"""Choose a pair of non-default ports, preferring the standard high test pair when free."""
|
||||
preferred_http, preferred_graphql = PREFERRED_CUSTOM_PORTS
|
||||
if _port_is_free(preferred_http) and _port_is_free(preferred_graphql):
|
||||
return preferred_http, preferred_graphql
|
||||
|
||||
# Fall back to scanning ephemeral range for the first free consecutive pair.
|
||||
for port in range(30000, 60000, 2):
|
||||
if _port_is_free(port) and _port_is_free(port + 1):
|
||||
return port, port + 1
|
||||
|
||||
raise RuntimeError("Unable to locate two free high ports for compose testing")
|
||||
|
||||
|
||||
def _make_port_check_hook(ports: tuple[int, ...]) -> Callable[[], None]:
|
||||
"""Return a callback that waits for the provided ports to accept TCP connections."""
|
||||
|
||||
def _hook() -> None:
|
||||
for port in ports:
|
||||
LAST_PORT_SUCCESSES.pop(port, None)
|
||||
time.sleep(COMPOSE_SETTLE_WAIT_SECONDS)
|
||||
_wait_for_ports(ports, timeout=COMPOSE_PORT_WAIT_TIMEOUT)
|
||||
|
||||
return _hook
|
||||
|
||||
|
||||
def _write_normal_startup_compose(
|
||||
base_dir: pathlib.Path,
|
||||
project_name: str,
|
||||
env_overrides: dict[str, str] | None,
|
||||
) -> pathlib.Path:
|
||||
"""Generate a compose file for the normal startup scenario with optional environment overrides."""
|
||||
|
||||
compose_config = copy.deepcopy(COMPOSE_CONFIGS["normal_startup"])
|
||||
service = compose_config["services"]["netalertx"]
|
||||
|
||||
data_volume_name = f"{project_name}_data"
|
||||
service["volumes"][0]["source"] = data_volume_name
|
||||
|
||||
if env_overrides:
|
||||
service_env = service.setdefault("environment", {})
|
||||
service_env.update(env_overrides)
|
||||
|
||||
compose_config["volumes"] = {data_volume_name: {}}
|
||||
|
||||
compose_file = base_dir / "docker-compose.yml"
|
||||
with open(compose_file, "w") as f:
|
||||
yaml.dump(compose_config, f)
|
||||
|
||||
return compose_file
|
||||
|
||||
|
||||
def _assert_ports_ready(
|
||||
result: subprocess.CompletedProcess,
|
||||
project_name: str,
|
||||
ports: tuple[int, ...],
|
||||
) -> str:
|
||||
"""Validate the post-up hook succeeded and return sanitized compose logs for further assertions."""
|
||||
|
||||
post_error = getattr(result, "post_up_error", None)
|
||||
clean_output = ANSI_ESCAPE.sub("", result.output)
|
||||
port_hosts = {port: LAST_PORT_SUCCESSES.get(port) for port in ports}
|
||||
result.port_hosts = port_hosts # type: ignore[attr-defined]
|
||||
|
||||
if post_error:
|
||||
pytest.fail(
|
||||
"Port readiness check failed for project"
|
||||
f" {project_name} on ports {ports}: {post_error}\n"
|
||||
f"Compose logs:\n{clean_output}"
|
||||
)
|
||||
|
||||
port_summary = ", ".join(
|
||||
f"{port}@{addr if addr else 'unresolved'}" for port, addr in port_hosts.items()
|
||||
)
|
||||
print(f"[compose port hosts] {project_name}: {port_summary}")
|
||||
|
||||
return clean_output
|
||||
|
||||
|
||||
def _run_docker_compose(
|
||||
compose_file: pathlib.Path,
|
||||
project_name: str,
|
||||
timeout: int = 5,
|
||||
env_vars: dict | None = None,
|
||||
detached: bool = False,
|
||||
post_up: Callable[[], None] | None = None,
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""Run docker compose up and capture output."""
|
||||
cmd = [
|
||||
@@ -219,10 +404,21 @@ def _run_docker_compose(
|
||||
continue
|
||||
return proc
|
||||
|
||||
post_up_exc: BaseException | None = None
|
||||
skip_exc: Skipped | None = None
|
||||
|
||||
try:
|
||||
if detached:
|
||||
up_result = _run_with_conflict_retry(up_cmd, timeout)
|
||||
|
||||
if post_up:
|
||||
try:
|
||||
post_up()
|
||||
except Skipped as exc:
|
||||
skip_exc = exc
|
||||
except BaseException as exc: # noqa: BLE001 - bubble the root cause through the result payload
|
||||
post_up_exc = exc
|
||||
|
||||
logs_cmd = cmd + ["logs"]
|
||||
logs_result = subprocess.run(
|
||||
logs_cmd,
|
||||
@@ -255,6 +451,9 @@ def _run_docker_compose(
|
||||
|
||||
# Combine stdout and stderr
|
||||
result.output = result.stdout + result.stderr
|
||||
result.post_up_error = post_up_exc # type: ignore[attr-defined]
|
||||
if skip_exc is not None:
|
||||
raise skip_exc
|
||||
|
||||
# Surface command context and IO for any caller to aid debugging
|
||||
print("\n[compose command]", " ".join(up_cmd))
|
||||
@@ -339,43 +538,34 @@ def test_normal_startup_no_warnings_compose(tmp_path: pathlib.Path) -> None:
|
||||
"""
|
||||
base_dir = tmp_path / "normal_startup"
|
||||
base_dir.mkdir()
|
||||
default_http_port = DEFAULT_HTTP_PORT
|
||||
default_ports = (default_http_port,)
|
||||
if not _port_is_free(default_http_port):
|
||||
pytest.skip(
|
||||
"Default NetAlertX ports are already bound on this host; "
|
||||
"skipping compose normal-startup validation."
|
||||
)
|
||||
|
||||
project_name = "netalertx-normal"
|
||||
default_dir = base_dir / "default"
|
||||
default_dir.mkdir()
|
||||
default_project = "netalertx-normal-default"
|
||||
|
||||
# Create compose file mirroring production docker-compose.yml
|
||||
compose_config = copy.deepcopy(COMPOSE_CONFIGS["normal_startup"])
|
||||
service = compose_config["services"]["netalertx"]
|
||||
default_compose_file = _write_normal_startup_compose(default_dir, default_project, None)
|
||||
default_result = _run_docker_compose(
|
||||
default_compose_file,
|
||||
default_project,
|
||||
timeout=60,
|
||||
detached=True,
|
||||
post_up=_make_port_check_hook(default_ports),
|
||||
)
|
||||
default_output = _assert_ports_ready(default_result, default_project, default_ports)
|
||||
|
||||
data_volume_name = f"{project_name}_data"
|
||||
|
||||
service["volumes"][0]["source"] = data_volume_name
|
||||
|
||||
service.setdefault("environment", {})
|
||||
service["environment"].update({
|
||||
"PORT": "22111",
|
||||
"GRAPHQL_PORT": "22112",
|
||||
})
|
||||
|
||||
compose_config["volumes"] = {
|
||||
data_volume_name: {},
|
||||
}
|
||||
|
||||
compose_file = base_dir / "docker-compose.yml"
|
||||
with open(compose_file, 'w') as f:
|
||||
yaml.dump(compose_config, f)
|
||||
|
||||
# Run docker compose
|
||||
result = _run_docker_compose(compose_file, project_name, detached=True)
|
||||
|
||||
clean_output = ANSI_ESCAPE.sub("", result.output)
|
||||
|
||||
# Check that startup completed without critical issues and mounts table shows success
|
||||
assert "Startup pre-checks" in clean_output
|
||||
assert "❌" not in clean_output
|
||||
assert "Startup pre-checks" in default_output
|
||||
assert "❌" not in default_output
|
||||
|
||||
data_line = ""
|
||||
data_parts: list[str] = []
|
||||
for line in clean_output.splitlines():
|
||||
for line in default_output.splitlines():
|
||||
if CONTAINER_PATHS['data'] not in line or '|' not in line:
|
||||
continue
|
||||
parts = [segment.strip() for segment in line.split('|')]
|
||||
@@ -387,15 +577,46 @@ def test_normal_startup_no_warnings_compose(tmp_path: pathlib.Path) -> None:
|
||||
break
|
||||
|
||||
assert data_line, "Expected /data row in mounts table"
|
||||
assert data_parts[1] == CONTAINER_PATHS['data'], f"Unexpected path column in /data row: {data_parts}"
|
||||
assert data_parts[2] == "✅" and data_parts[3] == "✅", (
|
||||
f"Unexpected mount row values for /data: {data_parts[2:4]}"
|
||||
)
|
||||
|
||||
parts = data_parts
|
||||
assert parts[1] == CONTAINER_PATHS['data'], f"Unexpected path column in /data row: {parts}"
|
||||
assert parts[2] == "✅" and parts[3] == "✅", f"Unexpected mount row values for /data: {parts[2:4]}"
|
||||
assert "Write permission denied" not in default_output
|
||||
assert "CRITICAL" not in default_output
|
||||
assert "⚠️" not in default_output
|
||||
|
||||
# Ensure no critical errors or permission problems surfaced
|
||||
assert "Write permission denied" not in clean_output
|
||||
assert "CRITICAL" not in clean_output
|
||||
assert "⚠️" not in clean_output
|
||||
custom_http, custom_graphql = _select_custom_ports()
|
||||
assert custom_http != default_http_port
|
||||
custom_ports = (custom_http,)
|
||||
|
||||
custom_dir = base_dir / "custom"
|
||||
custom_dir.mkdir()
|
||||
custom_project = "netalertx-normal-custom"
|
||||
|
||||
custom_compose_file = _write_normal_startup_compose(
|
||||
custom_dir,
|
||||
custom_project,
|
||||
{
|
||||
"PORT": str(custom_http),
|
||||
"GRAPHQL_PORT": str(custom_graphql),
|
||||
},
|
||||
)
|
||||
|
||||
custom_result = _run_docker_compose(
|
||||
custom_compose_file,
|
||||
custom_project,
|
||||
timeout=60,
|
||||
detached=True,
|
||||
post_up=_make_port_check_hook(custom_ports),
|
||||
)
|
||||
custom_output = _assert_ports_ready(custom_result, custom_project, custom_ports)
|
||||
|
||||
assert "Startup pre-checks" in custom_output
|
||||
assert "❌" not in custom_output
|
||||
assert "Write permission denied" not in custom_output
|
||||
assert "CRITICAL" not in custom_output
|
||||
assert "⚠️" not in custom_output
|
||||
|
||||
|
||||
def test_ram_disk_mount_analysis_compose(tmp_path: pathlib.Path) -> None:
|
||||
|
||||
Reference in New Issue
Block a user