Add standalone managed ingress diagnostics
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user