merge testing->tomerge/main #7
@@ -79,7 +79,9 @@ DECNET_PROFILE_DIR: str = os.environ.get("DECNET_PROFILE_DIR", "profiles")
|
||||
# API Options
|
||||
DECNET_API_HOST: str = os.environ.get("DECNET_API_HOST", "127.0.0.1")
|
||||
DECNET_API_PORT: int = _port("DECNET_API_PORT", 8000)
|
||||
DECNET_JWT_SECRET: str = _require_env("DECNET_JWT_SECRET")
|
||||
# DECNET_JWT_SECRET is resolved lazily via module __getattr__ so that agent /
|
||||
# updater / swarmctl subcommands (which never touch auth) can start without
|
||||
# the master's JWT secret being present in the environment.
|
||||
DECNET_INGEST_LOG_FILE: str | None = os.environ.get("DECNET_INGEST_LOG_FILE", "/var/log/decnet/decnet.log")
|
||||
|
||||
# SWARM log pipeline — RFC 5425 syslog-over-TLS between worker forwarders
|
||||
@@ -124,3 +126,10 @@ _web_hostname: str = "localhost" if DECNET_WEB_HOST in _WILDCARD_ADDRS else DECN
|
||||
_cors_default: str = f"http://{_web_hostname}:{DECNET_WEB_PORT}"
|
||||
_cors_raw: str = os.environ.get("DECNET_CORS_ORIGINS", _cors_default)
|
||||
DECNET_CORS_ORIGINS: list[str] = [o.strip() for o in _cors_raw.split(",") if o.strip()]
|
||||
|
||||
|
||||
def __getattr__(name: str) -> str:
|
||||
"""Lazy resolution for secrets only the master web/api process needs."""
|
||||
if name == "DECNET_JWT_SECRET":
|
||||
return _require_env("DECNET_JWT_SECRET")
|
||||
raise AttributeError(f"module 'decnet.env' has no attribute {name!r}")
|
||||
|
||||
60
tests/test_env_lazy_jwt.py
Normal file
60
tests/test_env_lazy_jwt.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""The JWT secret must be lazy: agent/updater subcommands should import
|
||||
`decnet.env` without DECNET_JWT_SECRET being set."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _reimport_env(monkeypatch):
|
||||
monkeypatch.delenv("DECNET_JWT_SECRET", raising=False)
|
||||
for mod in list(sys.modules):
|
||||
if mod == "decnet.env" or mod.startswith("decnet.env."):
|
||||
sys.modules.pop(mod)
|
||||
return importlib.import_module("decnet.env")
|
||||
|
||||
|
||||
def test_env_imports_without_jwt_secret(monkeypatch):
|
||||
env = _reimport_env(monkeypatch)
|
||||
assert hasattr(env, "DECNET_API_PORT")
|
||||
|
||||
|
||||
def test_jwt_secret_access_returns_value_when_set(monkeypatch):
|
||||
monkeypatch.setenv("DECNET_JWT_SECRET", "x" * 32)
|
||||
env = _reimport_env(monkeypatch)
|
||||
monkeypatch.setenv("DECNET_JWT_SECRET", "x" * 32)
|
||||
assert env.DECNET_JWT_SECRET == "x" * 32
|
||||
|
||||
|
||||
def test_agent_cli_imports_without_jwt_secret(monkeypatch, tmp_path):
|
||||
"""Subprocess check: `decnet agent --help` must succeed with no
|
||||
DECNET_JWT_SECRET in the environment and no .env file in cwd."""
|
||||
import subprocess
|
||||
import pathlib
|
||||
clean_env = {
|
||||
k: v for k, v in os.environ.items()
|
||||
if not k.startswith("DECNET_") and not k.startswith("PYTEST")
|
||||
}
|
||||
clean_env["PATH"] = os.environ["PATH"]
|
||||
clean_env["HOME"] = str(tmp_path)
|
||||
repo = pathlib.Path(__file__).resolve().parent.parent
|
||||
binary = repo / ".venv" / "bin" / "decnet"
|
||||
result = subprocess.run(
|
||||
[str(binary), "agent", "--help"],
|
||||
cwd=str(tmp_path),
|
||||
env=clean_env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "worker agent" in result.stdout.lower()
|
||||
|
||||
|
||||
def test_unknown_attr_still_raises(monkeypatch):
|
||||
env = _reimport_env(monkeypatch)
|
||||
with pytest.raises(AttributeError):
|
||||
_ = env.DOES_NOT_EXIST
|
||||
Reference in New Issue
Block a user