Fix managed ingress loopback publication
This commit is contained in:
@@ -176,6 +176,8 @@ Ingress acceptance streams generated configuration into Docker-managed
|
|||||||
volumes before starting the read-only containers. It therefore also works when
|
volumes before starting the read-only containers. It therefore also works when
|
||||||
an Actions job reaches a host or remote Docker daemon through a mounted socket;
|
an Actions job reaches a host or remote Docker daemon through a mounted socket;
|
||||||
the drill never assumes that a job-container path is visible to that daemon.
|
the drill never assumes that a job-container path is visible to that daemon.
|
||||||
|
The drill allocates explicit loopback-only host ports and verifies Docker's
|
||||||
|
published mappings, avoiding daemon-specific random-port shorthand behavior.
|
||||||
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.
|
||||||
|
|||||||
@@ -60,6 +60,25 @@ class ManagedIngressDrillTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertNotIn("type=bind", source)
|
self.assertNotIn("type=bind", source)
|
||||||
self.assertIn('"--network-alias",\n "load-balancer"', source)
|
self.assertIn('"--network-alias",\n "load-balancer"', source)
|
||||||
|
self.assertNotIn('"127.0.0.1::8080"', source)
|
||||||
|
self.assertIn("requested_http_port", source)
|
||||||
|
self.assertIn("requested_https_port", source)
|
||||||
|
|
||||||
|
def test_published_port_reads_the_docker_mapping(self) -> None:
|
||||||
|
completed = subprocess.CompletedProcess(
|
||||||
|
[],
|
||||||
|
0,
|
||||||
|
"127.0.0.1:49152\n",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
with patch.object(INGRESS, "_run", return_value=completed) as run:
|
||||||
|
port = INGRESS._published_port("ingress", 8443)
|
||||||
|
|
||||||
|
self.assertEqual(49152, port)
|
||||||
|
self.assertEqual(
|
||||||
|
["docker", "port", "ingress", "8443/tcp"],
|
||||||
|
run.call_args.args[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import argparse
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -86,6 +87,16 @@ def _published_port(container: str, target: int) -> int:
|
|||||||
raise RuntimeError(f"cannot determine published port from {output!r}") from exc
|
raise RuntimeError(f"cannot determine published port from {output!r}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _available_loopback_port(*, exclude: frozenset[int] = frozenset()) -> int:
|
||||||
|
for _attempt in range(10):
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener:
|
||||||
|
listener.bind(("127.0.0.1", 0))
|
||||||
|
port = int(listener.getsockname()[1])
|
||||||
|
if port not in exclude:
|
||||||
|
return port
|
||||||
|
raise RuntimeError("cannot allocate distinct loopback ports for ingress drill")
|
||||||
|
|
||||||
|
|
||||||
def _curl(url: str, *, headers: bool = False) -> str:
|
def _curl(url: str, *, headers: bool = False) -> str:
|
||||||
argv = ["curl", "--silent", "--show-error", "--insecure"]
|
argv = ["curl", "--silent", "--show-error", "--insecure"]
|
||||||
if headers:
|
if headers:
|
||||||
@@ -253,6 +264,10 @@ def main() -> int:
|
|||||||
"/govoplan-config/Caddyfile",
|
"/govoplan-config/Caddyfile",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
requested_http_port = _available_loopback_port()
|
||||||
|
requested_https_port = _available_loopback_port(
|
||||||
|
exclude=frozenset({requested_http_port})
|
||||||
|
)
|
||||||
ingress_command = [
|
ingress_command = [
|
||||||
"docker",
|
"docker",
|
||||||
"run",
|
"run",
|
||||||
@@ -269,9 +284,9 @@ def main() -> int:
|
|||||||
"--cap-drop",
|
"--cap-drop",
|
||||||
"ALL",
|
"ALL",
|
||||||
"--publish",
|
"--publish",
|
||||||
"127.0.0.1::8080",
|
f"127.0.0.1:{requested_http_port}:8080/tcp",
|
||||||
"--publish",
|
"--publish",
|
||||||
"127.0.0.1::8443",
|
f"127.0.0.1:{requested_https_port}:8443/tcp",
|
||||||
"--mount",
|
"--mount",
|
||||||
(
|
(
|
||||||
"type=volume,"
|
"type=volume,"
|
||||||
@@ -291,6 +306,11 @@ def main() -> int:
|
|||||||
_run(ingress_command)
|
_run(ingress_command)
|
||||||
http_port = _published_port(ingress, 8080)
|
http_port = _published_port(ingress, 8080)
|
||||||
https_port = _published_port(ingress, 8443)
|
https_port = _published_port(ingress, 8443)
|
||||||
|
if (http_port, https_port) != (
|
||||||
|
requested_http_port,
|
||||||
|
requested_https_port,
|
||||||
|
):
|
||||||
|
raise RuntimeError("Docker published unexpected ingress ports")
|
||||||
body = _wait_for_https(https_port)
|
body = _wait_for_https(https_port)
|
||||||
if body.strip() != "proto=https":
|
if body.strip() != "proto=https":
|
||||||
raise RuntimeError(f"forwarded protocol was not normalized: {body!r}")
|
raise RuntimeError(f"forwarded protocol was not normalized: {body!r}")
|
||||||
|
|||||||
Reference in New Issue
Block a user