Add standalone managed ingress diagnostics

This commit is contained in:
2026-08-03 19:59:12 +02:00
parent 6163c5992f
commit d107d94fec
4 changed files with 93 additions and 3 deletions
@@ -0,0 +1,44 @@
name: Runtime Ingress Drill
on:
workflow_dispatch:
inputs:
caddy_image:
description: Digest-pinned Caddy image
required: true
type: string
load_balancer_image:
description: Digest-pinned HAProxy image
required: true
type: string
probe_image:
description: Digest-pinned amd64 GovOPlaN API image
required: true
type: string
jobs:
managed-ingress:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
path: govoplan
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"
- name: Authenticate runtime image pull
env:
REGISTRY_USERNAME: ${{ secrets.GOVOPLAN_REGISTRY_USERNAME }}
REGISTRY_TOKEN: ${{ secrets.GOVOPLAN_REGISTRY_TOKEN }}
run: echo "$REGISTRY_TOKEN" | docker login git.add-ideas.de --username "$REGISTRY_USERNAME" --password-stdin
- name: Exercise the managed ingress boundary
working-directory: govoplan
env:
CADDY_IMAGE: ${{ inputs.caddy_image }}
LOAD_BALANCER_IMAGE: ${{ inputs.load_balancer_image }}
PROBE_IMAGE: ${{ inputs.probe_image }}
run: >-
python tools/checks/managed-ingress-drill.py
--caddy-image "$CADDY_IMAGE"
--load-balancer-image "$LOAD_BALANCER_IMAGE"
--probe-image "$PROBE_IMAGE"
@@ -181,6 +181,10 @@ host binding configuration, avoiding daemon-specific random-port shorthand
behavior. Because an Actions job and deployment containers may be Docker behavior. Because an Actions job and deployment containers may be Docker
siblings, functional HTTP/TLS checks run from the digest-pinned API image on siblings, functional HTTP/TLS checks run from the digest-pinned API image on
the deployment network instead of assuming the Docker host is job-local. the deployment network instead of assuming the Docker host is job-local.
The dispatch-only `Runtime Ingress Drill` workflow exposes the same bounded
check independently so ingress changes can be diagnosed before an immutable
runtime publication; it accepts only digest-pinned Caddy, HAProxy, and API
images and has no push trigger.
The bounded setup helper writes only generated public configuration as root so The bounded setup helper writes only generated public configuration as root so
it can initialize a new volume; the actual HAProxy process retains the image's it can initialize a new volume; the actual HAProxy process retains the image's
non-root identity and runs read-only with all capabilities dropped. non-root identity and runs read-only with all capabilities dropped.
+30
View File
@@ -122,6 +122,36 @@ class ManagedIngressDrillTests(unittest.TestCase):
self.assertIn("server_hostname=\"localhost\"", argv[-1]) self.assertIn("server_hostname=\"localhost\"", argv[-1])
self.assertNotIn("localhost:49152", argv[-1]) self.assertNotIn("localhost:49152", argv[-1])
def test_probe_diagnostics_include_container_stderr(self) -> None:
probe_failure = subprocess.CalledProcessError(1, ["docker", "run"])
state = subprocess.CompletedProcess([], 0, '{"Running":false}', "")
logs = subprocess.CompletedProcess([], 0, "", "caddy startup failed")
with patch.object(
INGRESS,
"_run",
side_effect=[probe_failure, state, logs],
), patch.object(INGRESS.sys, "stderr") as stderr:
with self.assertRaises(subprocess.CalledProcessError):
INGRESS._probe_ingress(
image="registry.example/runtime-api@sha256:" + "1" * 64,
network="deployment-network",
container="ingress",
)
rendered = "".join(call.args[0] for call in stderr.write.call_args_list)
self.assertIn('"Running":false', rendered)
self.assertIn("caddy startup failed", rendered)
def test_standalone_workflow_is_dispatch_only_and_digest_bounded(self) -> None:
workflow = (
ROOT / ".gitea/workflows/runtime-ingress-drill.yml"
).read_text(encoding="utf-8")
self.assertIn("workflow_dispatch:", workflow)
self.assertNotIn("\n push:", workflow)
self.assertIn("--probe-image \"$PROBE_IMAGE\"", workflow)
self.assertIn("GOVOPLAN_REGISTRY_TOKEN", workflow)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+15 -3
View File
@@ -200,12 +200,24 @@ if not location.startswith("https://localhost"):
] ]
) )
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
logs = _run(["docker", "logs", container], check=False).stdout.strip() _print_container_diagnostics(container)
if logs:
print(f"managed ingress logs:\n{logs}", file=sys.stderr)
raise raise
def _print_container_diagnostics(container: str) -> None:
state = _run(
["docker", "inspect", "--format", "{{json .State}}", container],
check=False,
)
state_detail = (state.stdout + state.stderr).strip()
if state_detail:
print(f"managed ingress state:\n{state_detail}", file=sys.stderr)
logs = _run(["docker", "logs", container], check=False)
log_detail = (logs.stdout + logs.stderr).strip()
if log_detail:
print(f"managed ingress logs:\n{log_detail}", file=sys.stderr)
def main() -> int: def main() -> int:
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--caddy-image", required=True) parser.add_argument("--caddy-image", required=True)