scripts.check_infra_drift
Reference › scripts › check_infra_drift
The infra drift gate, the mechanism for ADR-0077 item 4. It detects drift between the infrastructure code and the live Incus infrastructure. No module or test imports it; a human or a scheduled gate runs it.
Invocation: main takes --tf-dir (the Terraform or OpenTofu directory), --state (the state file) and --report (write a JSON verdict). It copies the process environment and sets TF_IN_AUTOMATION to 1 when unset, then calls run_gate.
resolve_binary finds a usable tofu or terraform binary, checking versions with _version_tuple. plan_drift runs a plan in the given directory (default timeout 300 seconds) and returns the plan's exit code, its changes and the raw output. bridge_expected_config reads the network bridge settings the code expects, and bridge_drift compares them with the live values obtained by incus_network_get, which calls the Incus CLI for one network key. finish prints the verdict and returns the exit code, writing the JSON report when asked. The commands go through _run, which applies a timeout.
At a glance
| Source | scripts/check_infra_drift.py |
| Lines of code | 291 |
| Classes | 0 |
| Functions | 9 |
| Imports (internal) | none |
| Imported by | none |
| Tests that import it | none |
Class diagram
No classes; see Functions.
Dependency diagram
No internal imports and no importers.
API
Functions
| Function | Signature | Summary |
|---|---|---|
_run |
_run(cmd: list[str], cwd: Path | None, env: Mapping[str, str], timeout: int, stdin: str | None = None) -> tuple[int, str, str] |
Run cmd and return (returncode, stdout, stderr); never raises. |
_version_tuple |
_version_tuple(binary: str, env: Mapping[str, str]) -> tuple[int, ...] | None |
|
resolve_binary |
resolve_binary(env: Mapping[str, str], home: Path | None = None) -> str | None |
$TOFU > tofu on PATH > ~/bin/tofu > a new-enough terraform. |
plan_drift |
plan_drift(tf_dir: Path, binary: str, env: Mapping[str, str], timeout: int = 300) -> tuple[int, list[str], str] |
plan -detailed-exitcode; returns (rc, action lines, stderr). |
bridge_expected_config |
bridge_expected_config(tf_dir: Path, binary: str, env: Mapping[str, str]) -> tuple[dict[str, dict[str, str]], str | None] |
The root's bridge_expected_config output, or ({}, reason). |
incus_network_get |
incus_network_get(network: str, key: str) -> str | None |
Live value of key on bridge network (project default), or None. |
bridge_drift |
bridge_drift(expected: Mapping[str, Mapping[str, str]], network_get: NetworkGet) -> list[str] |
One line per bridge key whose live value differs from the declaration. |
run_gate |
run_gate(tf_dir: Path, state_path: Path, env: Mapping[str, str], network_get: NetworkGet = incus_network_get, report_path: Path | None = None, out: Callable[[str], None] = print) -> int |
Run the whole gate; print the verdict lines; return the exit code. |
main |
main(argv: list[str] | None = None) -> int |
Source
#!/usr/bin/env python3
"""Infra drift gate — the mechanism for **ADR-0077 item 4**.
OpenTofu owns the Incus topology of all three environments (prod `default`,
`orchestrata-test`, `orchestrata-dev` — projects, bridges, ACLs, profiles and
every container), and the live fleets were IMPORTED into the host-owned state
at ``/var/lib/orchestrata/terraform/fleet.tfstate``. This gate is how drift is
detected instead of discovered:
1. ``tofu plan -detailed-exitcode`` over ``infra/terraform`` — exit 0 is
zero-diff, exit 2 is drift (a container, bridge, ACL or profile no longer
matches the declaration), anything else is a tool error.
2. The three bridge keys the lxc/incus provider never writes to state
(``ipv4.address``, ``ipv4.nat``, ``ipv6.address`` — see networks.tf) are
proven live with ``incus network get`` against the root's
``bridge_expected_config`` output, so the plan's silence on them is never
mistaken for proof.
Verdicts (exit code):
* ``0`` — zero-diff AND every bridge key matches live.
* ``1`` — FAIL: drift, a MISSING state file, no usable binary, or an init /
plan / output error. A missing state is a failure, never a skip: the
September state was lost to a worktree gc and nothing noticed.
* ``0`` with a ``NOTE`` — no IaC root at ``--tf-dir``: a checkout without the
IaC is not a fleet host (the health-check sandboxes), and the note is
printed so the pass is never silent.
Binary resolution: ``$TOFU``, else ``tofu`` on PATH, else ``~/bin/tofu``, else
``terraform`` only when its version satisfies the root's ``required_version``
floor (1.6.0 — terraform 1.5.7 on this host cannot even init the root).
Stdlib only; runs from the deployed tree on a user timer
(deploy/systemd/orchestrata-infra-drift.timer) and from
``scripts/health_check.py``.
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
import subprocess
import sys
from collections.abc import Callable, Mapping
from datetime import UTC, datetime
from pathlib import Path
RULE_ID = "infra-drift"
DEFAULT_TF_DIR = Path("infra/terraform")
DEFAULT_STATE = Path("/var/lib/orchestrata/terraform/fleet.tfstate")
REQUIRED_VERSION_FLOOR = (1, 6, 0)
BRIDGE_OUTPUT = "bridge_expected_config"
_DRIFT_MARKERS = (" will be ", " must be ", "forces replacement", "Plan:")
NetworkGet = Callable[[str, str], str | None]
def _run(
cmd: list[str],
cwd: Path | None,
env: Mapping[str, str],
timeout: int,
stdin: str | None = None,
) -> tuple[int, str, str]:
"""Run ``cmd`` and return ``(returncode, stdout, stderr)``; never raises."""
try:
proc = subprocess.run(
cmd,
cwd=cwd,
env=dict(env),
input=stdin,
capture_output=True,
text=True,
timeout=timeout,
)
except FileNotFoundError as exc:
return 127, "", str(exc)
except subprocess.TimeoutExpired:
return 124, "", f"{cmd[0]} timed out after {timeout}s"
return proc.returncode, proc.stdout, proc.stderr
def _version_tuple(binary: str, env: Mapping[str, str]) -> tuple[int, ...] | None:
rc, out, _ = _run([binary, "version", "-json"], None, env, 30)
if rc != 0:
return None
try:
raw = json.loads(out)["terraform_version"]
except ValueError, KeyError, TypeError:
return None
digits = raw.split("-", 1)[0].split(".")
try:
return tuple(int(d) for d in digits)
except ValueError:
return None
def resolve_binary(env: Mapping[str, str], home: Path | None = None) -> str | None:
"""``$TOFU`` > ``tofu`` on PATH > ``~/bin/tofu`` > a new-enough ``terraform``."""
explicit = env.get("TOFU")
if explicit:
return explicit
path_env = env.get("PATH", "")
found = shutil.which("tofu", path=path_env)
if found:
return found
home_dir = home if home is not None else Path(env.get("HOME", str(Path.home())))
candidate = home_dir / "bin" / "tofu"
if candidate.is_file() and os.access(candidate, os.X_OK):
return str(candidate)
terraform = shutil.which("terraform", path=path_env)
if terraform is None:
return None
version = _version_tuple(terraform, env)
if version is not None and version >= REQUIRED_VERSION_FLOOR:
return terraform
return None
def plan_drift(
tf_dir: Path, binary: str, env: Mapping[str, str], timeout: int = 300
) -> tuple[int, list[str], str]:
"""``plan -detailed-exitcode``; returns ``(rc, action lines, stderr)``."""
rc, out, err = _run(
[
binary,
"plan",
"-detailed-exitcode",
"-input=false",
"-no-color",
"-lock=false",
],
tf_dir,
env,
timeout,
)
lines = [
line.strip()
for line in out.splitlines()
if any(marker in line for marker in _DRIFT_MARKERS)
]
return rc, lines, err.strip()
def bridge_expected_config(
tf_dir: Path, binary: str, env: Mapping[str, str]
) -> tuple[dict[str, dict[str, str]], str | None]:
"""The root's ``bridge_expected_config`` output, or ``({}, reason)``."""
rc, out, err = _run(
[binary, "output", "-no-color", "-json", BRIDGE_OUTPUT], tf_dir, env, 60
)
if rc != 0:
return {}, (err or out).strip() or f"output {BRIDGE_OUTPUT} unavailable"
try:
value = json.loads(out)
except ValueError as exc:
return {}, f"output {BRIDGE_OUTPUT} is not JSON: {exc}"
if not isinstance(value, dict):
return {}, f"output {BRIDGE_OUTPUT} is not a map"
return {
str(name): {str(k): str(v) for k, v in keys.items()}
for name, keys in value.items()
if isinstance(keys, dict)
}, None
def incus_network_get(network: str, key: str) -> str | None:
"""Live value of ``key`` on bridge ``network`` (project default), or None."""
rc, out, _ = _run(
["incus", "network", "get", "--project", "default", network, key],
None,
os.environ,
30,
)
if rc != 0:
return None
return out.strip()
def bridge_drift(
expected: Mapping[str, Mapping[str, str]], network_get: NetworkGet
) -> list[str]:
"""One line per bridge key whose live value differs from the declaration."""
findings: list[str] = []
for network in sorted(expected):
for key in sorted(expected[network]):
want = expected[network][key]
live = network_get(network, key)
if live != want:
shown = "<absent>" if live is None else repr(live)
findings.append(
f"BRIDGE DRIFT: {network} {key} declared {want!r}, live {shown}"
)
return findings
def run_gate(
tf_dir: Path,
state_path: Path,
env: Mapping[str, str],
network_get: NetworkGet = incus_network_get,
report_path: Path | None = None,
out: Callable[[str], None] = print,
) -> int:
"""Run the whole gate; print the verdict lines; return the exit code."""
report: dict[str, object] = {
"rule": RULE_ID,
"checked_at": datetime.now(UTC).isoformat(timespec="seconds"),
"tf_dir": str(tf_dir),
"state": str(state_path),
"drift": [],
"bridge_drift": [],
}
def finish(code: int, verdict: str, lines: list[str]) -> int:
report["verdict"] = verdict
report["exit"] = code
for line in lines:
out(line)
if report_path is not None:
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(json.dumps(report, indent=2) + "\n")
return code
if not tf_dir.is_dir():
return finish(0, "no-root", [f"NOTE: no IaC root at {tf_dir}; nothing to gate"])
if not state_path.is_file():
return finish(
1,
"missing-state",
[
f"MISSING STATE: {state_path} does not exist — the fleet is not "
"imported (run infra/terraform/import.sh; ADR-0077 items 3-4)"
],
)
binary = resolve_binary(env)
report["binary"] = binary
if binary is None:
return finish(
1,
"no-binary",
[
"no OpenTofu: set $TOFU, put `tofu` on PATH, or install a "
f"terraform >= {'.'.join(map(str, REQUIRED_VERSION_FLOOR))}"
],
)
if not (tf_dir / ".terraform").is_dir():
rc, _, err = _run(
[binary, "init", "-input=false", "-no-color"], tf_dir, env, 300
)
if rc != 0:
return finish(1, "init-error", [f"init failed: {err.strip()[:400]}"])
rc, lines, err = plan_drift(tf_dir, binary, env)
report["plan_exit"] = rc
if rc == 2:
report["drift"] = lines
return finish(1, "drift", ["DRIFT: " + line for line in lines] or ["DRIFT"])
if rc != 0:
return finish(1, "plan-error", [f"plan failed (exit {rc}): {err[:400]}"])
expected, reason = bridge_expected_config(tf_dir, binary, env)
if reason is not None:
report["bridge_note"] = reason
findings = bridge_drift(expected, network_get)
report["bridge_drift"] = findings
if findings:
return finish(1, "bridge-drift", findings)
note = f" ({reason})" if reason else ""
return finish(
0,
"zero-diff",
[f"OK: zero-diff; {len(expected)} bridge(s) verified live{note}"],
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0])
parser.add_argument("--tf-dir", type=Path, default=DEFAULT_TF_DIR)
parser.add_argument("--state", type=Path, default=DEFAULT_STATE)
parser.add_argument("--report", type=Path, default=None, help="write JSON verdict")
args = parser.parse_args(argv)
env = dict(os.environ)
env.setdefault("TF_IN_AUTOMATION", "1")
return run_gate(args.tf_dir, args.state, env, report_path=args.report)
if __name__ == "__main__":
sys.exit(main())