fix(macos): put the gateway address in the Docker CLI proxy config
tracker-policy-pr / check-pr (pull_request) Successful in 10s
test / integration-docker (pull_request) Successful in 16s
lint / lint (push) Successful in 49s
test / unit (pull_request) Successful in 1m40s
test / integration-firecracker (pull_request) Failing after 2m50s
test / coverage (pull_request) Has been skipped
test / publish-infra (pull_request) Has been skipped
tracker-policy-pr / check-pr (pull_request) Successful in 10s
test / integration-docker (pull_request) Successful in 16s
lint / lint (push) Successful in 49s
test / unit (pull_request) Successful in 1m40s
test / integration-firecracker (pull_request) Failing after 2m50s
test / coverage (pull_request) Has been skipped
test / publish-infra (pull_request) Has been skipped
This is the layer that actually decides it, and it was in the script from the start: ~/.docker/config.json's proxies block is what the Docker CLI copies into every container it starts. Being client-side it beats the podman service outright, which is why containers.conf `env`, http_proxy=false, and the service environment each looked correct on disk and changed nothing a nested container saw. Also corrects the comment on the service-env substitution, which claimed to be the one place the address sticks. It is not; it covers podman's own pulls and non-CLI API clients. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -156,16 +156,28 @@ PY
|
|||||||
# Registry pulls egress through the bottle's proxy like everything else. The
|
# Registry pulls egress through the bottle's proxy like everything else. The
|
||||||
# token-bearing proxy URL is already in the agent's environment; persisting it
|
# token-bearing proxy URL is already in the agent's environment; persisting it
|
||||||
# inside this disposable VM does not broaden its authority.
|
# inside this disposable VM does not broaden its authority.
|
||||||
python3 - <<'PY'
|
#
|
||||||
|
# This file is also what the Docker CLI copies into every container it starts,
|
||||||
|
# and being client-side it beats anything the podman service does — it is why
|
||||||
|
# containers.conf `env`, `http_proxy=false`, and the service's own environment
|
||||||
|
# all failed to change what a nested container saw. The address goes in here
|
||||||
|
# for the same reason it goes everywhere else: `bot-bottle-gateway` resolves
|
||||||
|
# in the bottle, never inside a nested container.
|
||||||
|
GATEWAY_NAME="$GATEWAY_NAME" GATEWAY_IP="$gateway_ip" python3 - <<'PY'
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
name = os.environ["GATEWAY_NAME"]
|
||||||
|
ip = os.environ["GATEWAY_IP"]
|
||||||
|
|
||||||
proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy", "")
|
proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy", "")
|
||||||
|
# NO_PROXY keeps the name: it is matched against what a client asks for, and
|
||||||
|
# code inside a nested container still says "bot-bottle-gateway".
|
||||||
no_proxy = os.environ.get("NO_PROXY") or os.environ.get("no_proxy", "")
|
no_proxy = os.environ.get("NO_PROXY") or os.environ.get("no_proxy", "")
|
||||||
config = {"proxies": {"default": {
|
config = {"proxies": {"default": {
|
||||||
"httpProxy": proxy,
|
"httpProxy": proxy.replace(name, ip),
|
||||||
"httpsProxy": proxy,
|
"httpsProxy": proxy.replace(name, ip),
|
||||||
"noProxy": no_proxy,
|
"noProxy": no_proxy,
|
||||||
}}}
|
}}}
|
||||||
path = Path.home() / ".docker" / "config.json"
|
path = Path.home() / ".docker" / "config.json"
|
||||||
@@ -178,14 +190,13 @@ if docker info >/dev/null 2>&1; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The service's own environment is the last word on what a nested container
|
# Belt to the ~/.docker/config.json braces above, which is what actually
|
||||||
# gets. podman's Docker-compatible API injects proxy settings from the daemon
|
# decides this for `docker run`. The service environment is what podman falls
|
||||||
# environment *after* containers.conf is applied, so neither the `env` list
|
# back to for anything the CLI does not stamp — its own registry pulls, and
|
||||||
# below nor `http_proxy=false` can override it — the same blind spot the
|
# containers created through the API by something other than the Docker CLI.
|
||||||
# compat path has for `hosts_file`. Substituting here, before the service
|
# Cheap, and it keeps the address consistent across both paths.
|
||||||
# starts, is therefore the only place the address actually sticks.
|
|
||||||
#
|
#
|
||||||
# Assigned via command substitution, never echoed: these carry the bottle's
|
# Assigned via parameter expansion, never echoed: these carry the bottle's
|
||||||
# identity token.
|
# identity token.
|
||||||
for var in HTTP_PROXY HTTPS_PROXY http_proxy https_proxy; do
|
for var in HTTP_PROXY HTTPS_PROXY http_proxy https_proxy; do
|
||||||
eval "value=\${$var:-}"
|
eval "value=\${$var:-}"
|
||||||
|
|||||||
@@ -174,12 +174,19 @@ class TestInitScript(unittest.TestCase):
|
|||||||
self.assertIn('$2 == name { print $1; exit }', self.script)
|
self.assertIn('$2 == name { print $1; exit }', self.script)
|
||||||
self.assertIn('value.replace(name, ip)', self.script)
|
self.assertIn('value.replace(name, ip)', self.script)
|
||||||
|
|
||||||
|
def test_docker_cli_proxy_config_uses_the_address(self) -> None:
|
||||||
|
"""The Docker CLI copies ~/.docker/config.json's proxies block into
|
||||||
|
every container it starts. Being client-side it beats the podman
|
||||||
|
service, so this — not containers.conf — is what decides whether a
|
||||||
|
nested container can reach the proxy."""
|
||||||
|
block = self.script[self.script.index('"proxies"'):]
|
||||||
|
self.assertIn('"httpProxy": proxy.replace(name, ip)', block)
|
||||||
|
self.assertIn('"httpsProxy": proxy.replace(name, ip)', block)
|
||||||
|
self.assertIn('"noProxy": no_proxy', block)
|
||||||
|
|
||||||
def test_substitutes_the_address_into_the_service_environment(self) -> None:
|
def test_substitutes_the_address_into_the_service_environment(self) -> None:
|
||||||
"""The compat API injects proxy settings from the daemon environment
|
"""Covers what the Docker CLI does not stamp: podman's own registry
|
||||||
after containers.conf is applied, so neither the env list nor
|
pulls, and containers created through the API by another client."""
|
||||||
http_proxy=false can override it — the same blind spot it has for
|
|
||||||
hosts_file. Substituting before the service starts is the only place
|
|
||||||
the address sticks."""
|
|
||||||
launch = self.script[self.script.index("podman system service"):]
|
launch = self.script[self.script.index("podman system service"):]
|
||||||
self.assertNotIn("$GATEWAY_NAME", launch) # substitution precedes it
|
self.assertNotIn("$GATEWAY_NAME", launch) # substitution precedes it
|
||||||
setup = self.script[:self.script.index("podman system service")]
|
setup = self.script[:self.script.index("podman system service")]
|
||||||
@@ -196,9 +203,11 @@ class TestInitScript(unittest.TestCase):
|
|||||||
def test_keeps_the_gateway_name_in_no_proxy(self) -> None:
|
def test_keeps_the_gateway_name_in_no_proxy(self) -> None:
|
||||||
"""NO_PROXY is matched against what a client asks for, and code inside
|
"""NO_PROXY is matched against what a client asks for, and code inside
|
||||||
a nested container still says bot-bottle-gateway."""
|
a nested container still says bot-bottle-gateway."""
|
||||||
no_proxy = self.script[self.script.index('for var in ("NO_PROXY"'):]
|
start = self.script.index('for var in ("NO_PROXY"')
|
||||||
self.assertIn('entries.append(f"{var}={value}")', no_proxy)
|
loop = self.script[start:self.script.index("path = Path(", start)]
|
||||||
self.assertNotIn("replace(name, ip)", no_proxy)
|
self.assertIn('entries.append(f"{var}={value}")', loop)
|
||||||
|
self.assertNotIn("replace(name, ip)", loop)
|
||||||
|
self.assertIn('"noProxy": no_proxy', self.script)
|
||||||
|
|
||||||
def test_fails_closed_without_a_gateway_address(self) -> None:
|
def test_fails_closed_without_a_gateway_address(self) -> None:
|
||||||
self.assertIn('[ -n "$gateway_ip" ] || {', self.script)
|
self.assertIn('[ -n "$gateway_ip" ] || {', self.script)
|
||||||
|
|||||||
Reference in New Issue
Block a user