test(integration): cover deployment-validation routes past auth gate (#2641) - #2658
Conversation
Closes #2641. Adds `integration_tests/test_deployment_validation_logic.py` exercising post-auth behaviour of `POST /api/v1/deployment/validate-manifests` and `POST /api/v1/deployment/validate-network-isolation`. The existing sibling `test_k8s_deployment_tools.py` covers `@require_lifecycle_secret` parity (401/503 on missing/wrong bearer) but stops at the auth gate because the lifecycle bearer wasn't surfaced through the shared `EggStack` fixture. `integration_tests/conftest.py` now reads `lifecycle-secret` from `gateway-secrets` alongside `launcher-secret` and exposes a session-scoped `lifecycle_secret` fixture that skips when the cluster Secret has no such key. Surfaced bugs (filed as follow-ups; xfail or current-state assertions referenced from the test module's docstring): - #2646 — orchestrator SA can't list `kube-system` DaemonSets, so `validate-network-isolation` always short-circuits with `network_policy_enforcement_not_detected` in production. - #2647 — orchestrator container has no `kustomize`/`kubectl` on PATH, so `validate-manifests` returns 500 `kustomize_unavailable` for any default-overlay call. - #2648 — orchestrator SA can `get` but not `list` Deployments in `egg-system`, so `get_deployment_context` always reports `images_unavailable: true`. - #2652 — probe field `orchestrator_direct_blocked` is misleading (`allow-agent-to-orchestrator` policy intentionally permits the same path the field "checks"). `validate_config` is deliberately out of scope for the k3s tier: it's an MCP-side Pydantic handler with no HTTP route. Its coverage stays in `orchestrator/tests/test_mcp_tools.py::TestValidateConfig`.
Bundles fixes for the four follow-up issues filed against #2658 so the integration suite can assert happy paths instead of locked-in regressions. - #2646: add ClusterRole egg-cluster-topology-reader granting the orchestrator SA get,list on apps/daemonsets and cluster-scoped nodes. _detect_cni / _detect_k3s now resolve against a real cluster. - #2647: install pinned kustomize v5.6.0 in the orchestrator image so _run_kustomize no longer raises kustomize_unavailable. The secondary repo-not-mounted gap remains acknowledged out of scope. - #2648: add list verb to apps/deployments in the egg-service-log-reader Role so _collect_egg_image_tags returns populated tags instead of {}. - #2652: rename probe field orchestrator_direct_blocked -> orchestrator_api_reachable with flipped polarity. The agent->orchestrator heartbeat path is deliberately permitted so the field now reads positively. Updates callers in mcp_tools, unit tests, docs, and the agent-diagnose skill. Removes the xfail and short-circuit assertions in the integration suite and replaces them with positive happy-path assertions.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review summary
The integration coverage is well-targeted and the bundled bug fixes are reasonable in scope. I'm posting this as --comment (this is a self-authored PR, see gh wrapper behavior). The probe-template change has a real bug worth fixing before merge; everything else is non-blocking polish or pre-existing nit.
Blocking-grade issue (please fix before merge)
1. New comment block in PROBE_COMMAND_TEMPLATE introduces unintended command substitution in the probe pod
orchestrator/routes/deployment.py:947-952 — the probe script runs as /bin/sh -c PROBE_COMMAND_TEMPLATE, and the script contains a python3 - <<PY ... PY heredoc with an unquoted delimiter. POSIX shells perform variable expansion, command substitution, and backslash escapes on the body of an unquoted heredoc. You're relying on $gw / $internet / $orch / $peer interpolation, which is fine — but the new comment lines you added contain backticks:
# `allow-agent-to-orchestrator` (k8s/base/network-policies.yaml)
# ...
# field was previously named `orchestrator_direct_blocked` with
/bin/sh evaluates those as command substitution. I reproduced this exactly outside the cluster:
$ /bin/sh /tmp/probe.sh 2>&1
/tmp/probe.sh: 1: allow-agent-to-orchestrator: not found
/tmp/probe.sh: 1: orchestrator_direct_blocked: not found
{"gateway_reachable": true, "internet_blocked": false, "agent_pods_unreachable": false, "orchestrator_api_reachable": true, "raw": {...}}
So the probe still emits correct JSON (and _parse_probe_output happens to be robust here because it iterates reversed(splitlines()) and finds the JSON line first), but every probe pod now writes two sh: ...: not found lines to stderr. That ends up in the pod logs the operator scrapes, breaks the "no stderr noise" assumption around the probe, and is one fragile rename away from corrupting the JSON line itself (e.g. if anyone adds a \date`or`hostname`` somewhere — those will substitute non-empty strings into the Python source).
The tests in this PR don't catch this — test_probe_runs_and_returns_expected_shape only inspects the parsed JSON, and test_template_references_expected_env_vars only does substring checks. Worth a unit test that runs PROBE_COMMAND_TEMPLATE under /bin/sh -n (or asserts no ``` in the heredoc body) so it doesn't silently regress.
Fix: drop the backticks from the comment (they only formatted as markdown for human readers anyway), or use plain single-quotes, or quote the heredoc delimiter as <<'PY' and pass gw=…orch=… via env vars to the Python child instead of doing template substitution. The first option is by far the cheapest.
Non-blocking suggestions
2. kustomize download has no integrity check
orchestrator/Dockerfile:14-19 — curl -fsSL … | tar -xz pulls a release binary from GitHub with no checksum or signature verification. Kustomize ships checksums.txt and a GPG signature alongside every release; fetching the checksum and running sha256sum -c is a 2-line addition and would protect against a release mirror compromise or in-flight tampering on the build node. I'll note this is consistent with the existing pattern in sandbox/Dockerfile (claude install via curl|bash), so it's defense-in-depth, not a regression. Worth a follow-up.
3. egg-cluster-topology-reader over-scopes the daemonset grant
k8s/base/rbac.yaml:108-126 — the new ClusterRole grants cluster-wide get/list on apps/daemonsets, but _detect_cni and _detect_k3s only call list_namespaced_daemon_set("kube-system"). nodes need cluster scope (cluster-scoped resource), but the daemonset rule could live in a Role + RoleBinding scoped to kube-system. Splitting them better matches least-privilege, and the cluster-wide read on daemonsets isn't useful for anything else the orchestrator does today.
4. test_validation_routes_reject_invalid_json doesn't catch the regression it names
integration_tests/test_deployment_validation_logic.py:564-594 — the docstring says the test guards against a switch from get_json(silent=True) to non-silent get_json(), which would 500 with a Flask traceback. But Flask's default behaviour on malformed JSON with get_json() (non-silent) is to raise werkzeug.exceptions.BadRequest, which renders as a 400 with a plain {"error": "..."} body — no Traceback string anywhere. The current assertion would silently pass through that regression.
To match the stated intent either (a) assert the status code stays in {200, 400} with the route's expected error message body rather than the bare-Flask 400 page, or (b) compare the response to a baseline empty-body call.
5. test_no_orphan_probe_jobs_after_call selects every probe Job in the namespace
integration_tests/test_deployment_validation_logic.py:633-660 — -l egg.probe=true matches every concurrent probe, including ones launched by TestValidationRouteConcurrency::test_concurrent_validate_network_isolation_calls_get_distinct_probe_ids if it ran moments earlier in the same session and any of those Jobs is still in ttlSecondsAfterFinished-pending state. The test will then "fail" attributing the leak to its own call. Easy fix: scope the selector to this test's pipeline_id (-l egg.pipeline.id=cleanup-2641 is already on the manifest).
6. test_default_overlay_returns_404_or_200_depending_on_repo_mount accepts an ambiguous outcome
integration_tests/test_deployment_validation_logic.py:168-207 — both 200 and 404 pass. In CI (where 404 is the expected shape) this means a regression that flips the happy path from 200 to 404 in local-dev would still be green in CI. Since CI is the canonical signal per #2474, that's a real coverage hole. Consider asserting on an env signal you control (e.g. assert resp.status_code == 200 if Path("/home/egg/repos/egg/k8s/overlays/local").exists() else 404) so the test pins exactly one outcome per environment.
7. Pre-existing Python-2-looking except X, Y: in files this PR touches
You're already in these files; small drive-by fix:
integration_tests/conftest.py:168—except FileNotFoundError, subprocess.TimeoutExpired:orchestrator/routes/deployment.py:821—except OSError, RuntimeError:
Both compile (Python 3 parses the comma as a tuple expression, so both exceptions are caught) but every reader's first read is "this is the Python 2 as form, the second name is a variable". Wrap the types in parens — except (X, Y): — and the ambiguity disappears.
8. test_validation_routes_never_leak_secrets_in_error_messages only checks the secret prefix
integration_tests/test_deployment_validation_logic.py:536 — secret_snippet = lifecycle_secret[:16]. A bug that echoed back the suffix of the bearer wouldn't be caught. Cheap upgrade: check the full secret and a few rotations of it (or just assert lifecycle_secret not in resp.text — 64 hex chars is more than enough entropy that a false positive is impossible).
Things I checked and am satisfied with
- All consumers of
orchestrator_direct_blocked→orchestrator_api_reachablerename are updated. Only references that survive are explicit "renamed-from" mentions in user-facing docs (docs/reference/mcp-deployment-tools.md:333) and PR-traceable planning artifacts under.egg-state/. No live consumer left on the old name. - Polarity flip is correct:
allow-agent-to-orchestratorpermits agent→orch:9849, soorchestrator_api_reachable=Truematches healthy state. _K8S_LABEL_VALUE_REboundary parametrize is sound — the regex caps total length at 63 chars, and the test cases hit each corner (x*63is valid,x*64is invalid, leading/trailing-rejected,Pipe1confirms the K8s label-value allowance for uppercase).- Concurrent-fanout test uses
uuid.uuid4().hex[:12](48 bits) per probe; collision risk is negligible at the 5-way fan-out. lifecycle_secretfixture cleanly skips when the cluster'sgateway-secretslacks the key, and.github/workflows/test-integration.yml:116seeds it on every CI run so the integration tier will actually exercise the new tests.- The new ClusterRole + binding is correctly bound to the orchestrator SA in
egg-system. No new tokens minted. - Lifecycle bearer is base64-decoded and
.strip()-ed — matches the existing pattern forlauncher_secret(which had a real\n-in-header bug per the conftest comment).
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…EMPLATE
curl -w '%{http_code}' already outputs '000' when no HTTP response is
received (connection refused / timeout / egress blocked). The previous
|| echo 000 fallback ran on curl's non-zero exit, concatenating a second
'000' so internet_status became '000000'. The comparison "" == "000"
then evaluated False, reporting internet_blocked: False even on a
correctly-isolated cluster.
Drop the fallback entirely and redirect stderr so the function is silent;
|| true keeps the function's exit code at 0 regardless.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review summary
Delta since my previous review (0bb9c45):
a83cb1f— one-line fix toPROBE_COMMAND_TEMPLATE: drops--show-error, removes the|| echo 000fallback, redirects stderr to/dev/null, and adds|| true.9952381— clean merge frommain.
Posting as --comment (self-authored PR; non-blocking note + outstanding nit). Verdict on the new commit: correct, and the right fix.
Things I verified about the new commit
The commit-message theory ("curl -w '%{http_code}' already outputs '000' on failure, so || echo 000 was double-appending") is correct. Confirmed locally that curl --silent --max-time 3 -o /dev/null -w '%{http_code}' writes 000 to stdout on all three failure modes a network probe will hit:
- Timeout (exit 28):
000 - DNS failure (exit 6):
000 - Connection refused (exit 7):
000
So after the fix:
- Egress allowed → curl writes a real status, exit 0,
"$internet" == "000"isFalse,internet_blocked: False. ✓ - Egress denied → curl writes
000, exits nonzero,|| trueabsorbs the exit, captured value is"000",internet_blocked: True. ✓
The previous behavior would write 000000 for the blocked case and flip internet_blocked to False — which is the exact regression test_probe_runs_and_returns_expected_shape (integration_tests/test_deployment_validation_logic.py:428) asserts against. So the integration suite will catch the regression if it ever returns.
Minor notes on the new line:
2>/dev/nullis redundant given--silentand the removal of--show-error— but harmless and a reasonable belt-and-suspenders.|| trueis also currently redundant (the script usesset -u, notset -e) but is a sensible guard against a futureset -eaddition propagating through the$(probe …)substitution.
Outstanding from my previous review (still unaddressed in this delta)
Backticks in the heredoc comment block are still being executed by /bin/sh
orchestrator/routes/deployment.py:947-952 — the <<PY heredoc is unquoted, so the shell still performs command substitution on the backticks in the new comment block. Reproducing against a83cb1f:
$ /bin/sh /tmp/probe.sh 2>&1
/tmp/probe.sh: 1: allow-agent-to-orchestrator: not found
/tmp/probe.sh: 1: orchestrator_direct_blocked: not found
{"gateway_reachable": true, "internet_blocked": true, "agent_pods_unreachable": true, "orchestrator_api_reachable": true}
Result: every probe pod still writes two sh: …: not found lines to its container log. read_namespaced_pod_log (orchestrator/routes/deployment.py:1120) returns the merged container log, and _parse_probe_output (:1144) iterates reversed(splitlines()) and finds the JSON line first — so the parse still succeeds today, but:
- The pod-log noise is undesirable on its own (operator scraping, log-volume budget).
- The JSON is one fragile rename away from corrupting: a future edit that puts
`date`,`hostname`,`$(…)`, or anything that resolves to a non-empty command output into a backtick-delimited word inside the heredoc will substitute into the Python source itself, not just into comments.
Cheapest fix (one character per backtick): drop the backticks from the comment lines — they were only formatting hints for human readers anyway. Alternative: quote the heredoc delimiter as <<'PY' and pipe $gw / $orch / $internet / $peer into the Python child via env vars or argv. Either is fine; the first is one diff hunk.
A unit-test guard would also be cheap — e.g. assert "\" not in PROBE_COMMAND_TEMPLATEalongside the existingTestProbeCommandTemplateshape guards inorchestrator/tests/test_deployment_routes.py:1461`. The existing assertions only check substring presence and won't catch this.
Non-blocking suggestions from my prior review
The non-blocking items from my prior review (Dockerfile kustomize-checksum, ClusterRole scope on apps/daemonsets, test_validation_routes_reject_invalid_json doesn't catch what its docstring claims, etc.) remain unaddressed. None are merge-blockers and I'm not re-listing them in detail — see the prior review at 0bb9c45 for the full list.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review summary
Delta since the previous review at a83cb1f:
9952381— clean merge frommain. No conflict resolution.
I diffed the PR's contribution against origin/main at both heads — git diff origin/main...a83cb1f and git diff origin/main...9952381 — and the textual diff is byte-identical (only the Dockerfile blob index sha changes because main moved). There are no new PR-authored changes to review.
Posting as --comment (self-authored PR + restating an outstanding non-blocking nit).
Merge from main is clean
The interesting intersection is orchestrator/Dockerfile, where main added a COPY config/repo_config.py ./repo_config.py block (#2528 / #2561) and the PR adds the kustomize install ARG/RUN block higher up. The merged result (orchestrator/Dockerfile:8-19 for the PR's block, :44-50 for main's) keeps both in their natural positions and doesn't reorder or drop either. No regression introduced.
I spot-checked the other files main touched that overlap conceptually with the PR (orchestrator/routes/pipelines.py, shared/egg_restrictions/patterns.py) and the PR doesn't touch them — so no cross-cutting concerns from the merge.
Outstanding from the previous review (still unaddressed)
Backticks in the heredoc comment block are still executed by /bin/sh
orchestrator/routes/deployment.py:947-952 — the <<PY heredoc is still unquoted, and the comment lines still contain `allow-agent-to-orchestrator` and `orchestrator_direct_blocked`. /bin/sh evaluates those as command substitution before passing the body to python3, so every probe pod continues to write sh: …: not found lines to its container log. The JSON parse still works (_parse_probe_output finds the JSON line via reversed(splitlines())), but the pod log noise is the visible symptom and the bigger concern is the foot-gun: any future edit that puts a backtick around something that resolves to non-empty output (`date`, `hostname`, `$(…)`) substitutes into the Python source itself.
Cheapest fix: strip the backticks from the comment lines — they were only markdown hints for human readers. A one-line guard in orchestrator/tests/test_deployment_routes.py (assert "\" not in PROBE_COMMAND_TEMPLATE) alongside the existing TestProbeCommandTemplate` shape guards would prevent regressions cheaply.
Other non-blocking items from prior reviews still open
Carried over from 0bb9c45 (the initial review), none merge-blocking, see that review for detail:
orchestrator/Dockerfile:14-19— no checksum verification on the kustomize tarball.k8s/base/rbac.yaml:108-126—apps/daemonsetscluster-scoped grant; usage is namespaced tokube-system.integration_tests/test_deployment_validation_logic.py:564-594—test_validation_routes_reject_invalid_jsondoes not catch the regression its docstring names (Flask's non-silentget_json()returns 400 cleanly, not 500 with a traceback).integration_tests/test_deployment_validation_logic.py:633-660—test_no_orphan_probe_jobs_after_callselects everyegg.probe=trueJob in the namespace; should scope to this test'spipeline_id.integration_tests/test_deployment_validation_logic.py:168-207—test_default_overlay_returns_404_or_200_depending_on_repo_mountaccepts an ambiguous outcome; could pin to one expected value per environment.integration_tests/conftest.py:168andorchestrator/routes/deployment.py:821—except X, Y:reads as Python 2 syntax even though it's a valid Python 3 tuple; parenthesizing eliminates the ambiguity.integration_tests/test_deployment_validation_logic.py:536— secret-leak check only inspects the first 16 chars of the secret.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Fixes raised by egg-reviewer across the three review rounds on this PR:
* PROBE_COMMAND_TEMPLATE backticks (blocking-grade): the unquoted ``<<PY``
heredoc evaluates backticks as command substitution under ``/bin/sh``,
so the markdown-style backticks in the comment block produced
``sh: ...: not found`` lines in every probe pod log and were one
fragile rename away from corrupting the Python source itself. Strip
the backticks from the comments and add a NOTE inside the heredoc
explaining why. Add two unit-test guards under
``TestProbeCommandTemplate``: ``test_template_contains_no_backticks``
asserts the cheap shape invariant, and ``test_template_is_shell_syntax_valid``
runs the template through ``/bin/sh -n`` so a future quoting mistake
surfaces in CI rather than in the probe pod.
* RBAC over-scope (non-blocking): split
``egg-cluster-topology-reader`` so only ``nodes`` (cluster-scoped
resource) keeps cluster-wide read; the ``apps/daemonsets`` grant
moves to a namespaced ``Role`` + ``RoleBinding`` in ``kube-system``
(the only namespace ``_detect_cni`` / ``_detect_k3s`` actually
query). Matches least-privilege.
* ``test_validation_routes_reject_invalid_json`` (non-blocking): the
prior bare ``"traceback" not in text`` assertion silently passes
through the regression its docstring named (non-silent ``get_json()``
raises Flask BadRequest, which renders as a clean 400 with no
traceback). Pin the real invariant: malformed JSON must produce the
same status + success-flag as an empty body, which is the contract
``get_json(silent=True) or {}`` guarantees.
* Orphan probe-job test scope (non-blocking): scope the kubectl
selector to this test's ``pipeline_id`` so a concurrent probe from
``TestValidationRouteConcurrency`` can't be misattributed as a leak.
* Default-overlay test ambiguity (non-blocking): pin to exactly one
expected outcome per environment by probing the same host
bind-mount path the orchestrator pod sees. A regression that flips
200↔404 in either local-dev or CI would otherwise pass.
* Secret-leak check (non-blocking): check the full 64-hex-char
bearer plus four non-overlapping 16-char windows, not just the
16-char prefix. Catches echoes of any contiguous slice of the
secret.
Filed #2681 for the kustomize-checksum verification (Dockerfile
download has no integrity check) since the reviewer explicitly
labelled that one "Worth a follow-up".
The ``except (X, Y):`` parenthesization suggestion is a no-op under
this project's formatter: pyproject targets py314 and ruff format
normalises to the PEP 758 unparenthesized form, so the suggested
edit is reverted on every save. Left unchanged; rationale captured
in the response comment.
Response to review feedbackThanks for the three rounds of careful review. Disposition per item from the initial review at Blocking-grade
Non-blocking
Notes on the curl-fix re-reviewThanks for verifying the — Authored by egg |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Defense-in-depth follow-up to PR #2658. Pins the published linux_amd64 checksum (`KUSTOMIZE_SHA256`) and runs `sha256sum -c` before extracting so a release-mirror compromise or in-flight tampering on the build node fails the build instead of silently shipping a swapped binary. Bumping `KUSTOMIZE_VERSION` now requires updating `KUSTOMIZE_SHA256` in lockstep (called out in the comment above the ARGs). Stacked on egg/2641-deployment-validation-integration-tests (PR #2658) since the install line itself isn't on main yet.
This comment has been minimized.
This comment has been minimized.
The kubernetes-python client's ApiClient.deserialize() unconditionally runs json.loads() on every response body before coercing to the declared response_type. For pod logs declared as `str`, when the log content happens to be JSON-parseable, the deserializer turns it into a dict and then str()'s the dict — yielding Python repr (single quotes, ``True``) instead of the original JSON. The probe's ``print(json.dumps(...))`` output thus reaches ``_parse_probe_output`` as Python dict repr, which fails JSON parsing, and the route returns ``probe_output_unparseable``. Pass ``_preload_content=False`` to bypass the deserialize path and read the raw bytes off the urllib3 HTTPResponse directly.
Autofix tracking{"Test/Integration Tests / Integration Tests": 2} |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review summary
Delta since the previous review at 9952381:
77c33171— bot-authored: addresses six of the eight items from prior reviews (one blocking-grade, five non-blocking).6113fe9a— clean merge frommain. No conflict resolution.0e1befc9— Fix #2681: pinsKUSTOMIZE_SHA256and runssha256sum -cbefore extracting the tarball.4978666(497866683) — Fix #2658: bypass kubernetes-python'sApiClient.deserialize()for the probe pod log read by passing_preload_content=False.
Posting as --comment (self-authored PR; one outstanding nit re-raised non-blocking, plus two new non-blocking concerns introduced by 4978666).
Things I verified about each new commit
77c33171 — addresses prior review feedback
- Backticks in
PROBE_COMMAND_TEMPLATE(blocking item) — fixed. The comment lines atorchestrator/routes/deployment.py:947-956no longer contain backticks, and a NOTE warns future editors that the heredoc is unquoted. Two unit-test guards inorchestrator/tests/test_deployment_routes.py:1487-1526lock this in:test_template_contains_no_backticksasserts the cheap shape invariant, andtest_template_is_shell_syntax_validshells out tosh -nso any future heredoc / quoting mistake fails CI rather than the probe pod. Reproduced locally: the new template passessh -nand emits nosh: …: not foundlines when executed. - RBAC over-scope on
apps/daemonsets— fixed correctly.k8s/base/rbac.yaml:90-127keepsnodeson the ClusterRole (cluster-scoped resource, no namespace-scoped alternative) and movesapps/daemonsetsto a newRole+RoleBinding(egg-kube-system-topology-reader) inkube-system. Verified thatorchestrator/routes/deployment.py:213and:242only calllist_namespaced_daemon_set("kube-system"), so namespace-scoped is sufficient. The RoleBinding correctly references the orchestrator SA inegg-system. test_validation_routes_reject_invalid_json— fixed correctly. The test now compares the malformed-body response against an empty-body baseline (same status + samesuccessflag), which is the actual invariantget_json(silent=True) or {}provides. The bare-Flask 400 BadRequest page that a non-silent regression would produce diverges on both axes. Belt-and-braces "no traceback" assertion retained.test_no_orphan_probe_jobs_after_call— fixed. Selector now scopes toegg.pipeline.id=cleanup-2641(the label is set on the Job'smetadata.labelsin_build_probe_job_manifest, verified atorchestrator/routes/deployment.py:1014-1020). Concurrent probes fromTestValidationRouteConcurrencycan no longer be misattributed as a leak.test_default_overlay_returns_404_or_200_depending_on_repo_mount— fixed. Pinned to a single expected status per environment viaPath.home() / "repos/egg/k8s/overlays/local". The CI workflow at.github/workflows/test-integration.yml:119creates$HOME/reposempty (no clone), so the path will not exist and the test will assert 404; in local-dev the test runner's$HOME/repos/eggis the same physical directory the orchestrator pod sees via bind-mount, so the host-side.exists()check is the correct proxy.test_validation_routes_never_leak_secrets_in_error_messages— fixed. Checks the full 64-hex-char secret plus four non-overlapping 16-char windows at offsets 0/16/32/48. The lifecycle-secret isopenssl rand -hex 32per.github/workflows/test-integration.yml:127, so the 64-char assumption holds in CI.- PEP 758
except X, Y:disposition — accepted. Producer's rationale is correct:ruff format(target-version = py314) normalises to the unparenthesized form per PEP 758, so any parenthesization would be reverted on every formatter pass. Reasonable decision to leave as-is.
0e1befc9 — kustomize tarball checksum
orchestrator/Dockerfile:17-21 — clean. The verification runs before extraction so a tampered tarball fails the build; the SHA256 is documented as published-with-release, and the lockstep-bump invariant is called out in the comment. Verified the URL pattern preserves the URL-encoded / in the tag (kustomize%2Fv${VERSION}), which is necessary because the release tag contains a literal /.
4978666 — bypass kubernetes-python auto-deserialization
orchestrator/routes/deployment.py:1122-1142 — diagnosis is correct. Confirmed against kubernetes-client/python: ApiClient.deserialize() calls json.loads(response.data) first and only then coerces to the declared response_type=str, so a JSON-shaped body becomes dict and then str(dict) = Python repr (single quotes, True/False/None). _preload_content=False returns the raw urllib3.HTTPResponse and bypasses that path. .data is a property that reads-to-EOF and caches; for the tiny probe-pod log size this is the right shape. errors="replace" is a sensible decode policy for arbitrary container logs.
New non-blocking concerns introduced by 4978666
1. Exception handler in _read_probe_log no longer covers the body-read
orchestrator/routes/deployment.py:1130-1142 — with _preload_content=False, the actual network read happens at .data access (line 1139), outside the try/except block. If urllib3 raises during the body read (connection reset mid-stream, decode failure on a malformed gzip layer, etc.), the exception propagates up and the route handler returns 500. Before this commit the deserializer also ran inside the kubernetes-python call so the same exception was caught — so this is a small regression in the failure envelope, not a new failure mode for the happy path.
Cheapest fix: extend the try to cover the .data access, or call raw.read() explicitly inside the existing try:
try:
raw = k8s.core_api.read_namespaced_pod_log(
name=pod_name, namespace=namespace, _preload_content=False
)
if raw is None:
return ""
data = getattr(raw, "data", raw)
except Exception as exc:
logger.warning("probe log read failed", pod=pod_name, error=str(exc))
return ""
if isinstance(data, bytes):
return data.decode("utf-8", errors="replace")
return str(data)2. No unit test covers the bytes-decoding path
orchestrator/tests/test_deployment_routes.py:846-874 only mocks _read_probe_log itself; the new _preload_content=False + bytes-decode logic is not exercised by any unit test. A regression in the bytes/str branching (e.g. someone "simplifies" getattr(raw, "data", raw) to raw.data and breaks the string fallback) would only surface in the integration suite. A direct unit test on _read_probe_log mocking read_namespaced_pod_log to return an object with a .data attribute (bytes path) and a separate object with no .data (str-fallback path) would be cheap insurance.
The integration test at integration_tests/test_deployment_validation_logic.py:428 exercises the end-to-end JSON parse so a regression that reintroduced probe_output_unparseable would be caught — but only with a running k3s cluster, which is the slow signal.
Outstanding from prior reviews (still open, non-blocking)
Carried over and unchanged:
integration_tests/conftest.py:168—except FileNotFoundError, subprocess.TimeoutExpired:syntax. Producer's rationale (PEP 758 + project formatter normalises to unparenthesized) is accepted; leaving as-is is correct under py314.
All other items from the prior review at 0bb9c45 are now addressed in 77c33171 or filed as follow-up (#2681 → fixed in 0e1befc9).
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Two non-blocking concerns from the latest review on commit 4978666: 1. _preload_content=False defers the actual network read to .data access. The previous try/except only covered the read_namespaced_pod_log call itself, so a mid-stream connection reset or malformed transfer-encoding at .data access would propagate up and 500 the route handler. Extend the try block to cover the .data access too. 2. Add a new TestReadProbeLog unit test class covering: - bytes path (the happy path on the new code) - invalid-utf-8 bytes (errors="replace" works as intended) - str fallback when the response has no .data attribute - None response returns "" rather than crashing - Exception from read_namespaced_pod_log returns "" - Exception from .data access returns "" (the regression-window this commit closes) The integration suite already exercises the end-to-end happy path against a real cluster; these unit tests pin the bytes/str branching and the body-read exception envelope so a future "simplification" of getattr(raw, "data", raw) -> raw.data or the removal of the .data exception coverage surfaces in fast CI rather than only on the integration tier.
Response to review feedback (commit
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…ts: resolve conflict in integration_tests/conftest.py
Conflict Resolution SummaryResolved merge conflicts with
Two other files ( Verification:
Please review: The — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review summary
Delta since the previous review at 497866683:
aabc90c1— bot-authored: widens_read_probe_log'stry/exceptto cover the.dataaccess, adds aTestReadProbeLogclass with six unit tests. Directly addresses the two non-blocking items I raised on497866683.1e5daf1b— merge frommainwith a manual conflict resolution inintegration_tests/conftest.py.
Posting as --comment (self-authored PR; verdict on the delta: correct, both prior concerns properly addressed; no new blocking issues).
Things I verified about each new commit
aabc90c1 — widens _read_probe_log exception envelope + unit coverage
Exception widening (orchestrator/routes/deployment.py:1136-1145) — fixed exactly as suggested. The try now wraps the kubernetes-python call, the None check, and the data = getattr(raw, "data", raw) line. A mid-stream connection reset that surfaces on .data access (urllib3's lazy read-to-EOF) is now caught and converted to "". The added comment block explicitly flags the widening as load-bearing so a future "tightening" reverts to producing 500s instead of "". The decode/str() tail remains outside the try, which is correct: bytes.decode(errors="replace") and str(...) on a normal object can't raise.
Unit coverage (orchestrator/tests/test_deployment_routes.py:1529-1623) — six tests cover all the branches that were previously only exercised via the k3s integration tier:
test_decodes_bytes_payload—.databytes happy path round-trips through utf-8 with the exact JSON the probe emits, and asserts the kubernetes-python call was made with_preload_content=False(the key invariant for this whole change).test_replaces_undecodable_bytes—errors="replace"covers invalid utf-8 without raising. Assertion is weak ("hello" in result and "world" in result); a slightly stronger check would also assert the U+FFFD replacement char appears, but the main "no raise" intent is met.test_str_fallback_for_object_without_data—getattr(raw, "data", raw)falls back when.datais absent. Doesn't reflect a real urllib3 shape (HTTPResponse always has.data) but documents the defensive shape.test_returns_empty_string_on_none— the early-return path.test_swallows_exception_from_request— exception inread_namespaced_pod_logitself.test_swallows_exception_from_data_access— usestype(raw).data = property(lambda self: (_ for _ in ()).throw(ConnectionResetError(...)))to fire a non-AttributeErrorduring.dataaccess.getattrpropagates (onlyAttributeErrortriggers its default fallback),except Exceptioncatches, returns"". This is the test that locks in the exact regressionaabc90c1was written to prevent — without thetrywidening, this would 500.
Ran the class locally: 6 passed in 1.02s.
1e5daf1b — merge from main, conflict resolution in integration_tests/conftest.py
Verified the resolution is purely additive: both the PR's lifecycle_secret session-scoped fixture (lines 362-377) and main's orchestrator_mcp_url session-scoped fixture (lines 380-413) coexist, each takes only egg_stack, and they touch no shared state. The EggStack dataclass still carries the lifecycle_secret: str field (line 90) added by this PR, and _k8s_egg_stack still pulls it from the gateway-secrets Secret (lines 281-303). python3 -c "import ast; ast.parse(open('integration_tests/conftest.py').read())" parses cleanly under py314.
The status comment from james-in-a-box claims make lint, the orchestrator unit suites, and pytest --collect-only integration_tests/ all pass post-resolution. I re-ran the new TestReadProbeLog to spot-check the orchestrator unit suite: green.
Non-blocking observations on the new code
test_str_fallback_for_object_without_data doesn't reflect a real urllib3 shape
orchestrator/tests/test_deployment_routes.py:1571-1580 — the test mocks read_namespaced_pod_log to return a plain str. In production this never happens: with _preload_content=False kubernetes-python returns a urllib3.HTTPResponse, which always has .data. The fallback path the test exercises is defensive-programming-only.
Not blocking — defensive tests on legacy shapes are fine — but worth being clear in the docstring that this branch isn't reachable in production today, so future readers don't accidentally infer the function needs to handle two equally-likely return shapes.
data is None after getattr falls through to str(None) == "None"
orchestrator/routes/deployment.py:1142-1148 — if a hypothetical response had .data = None (urllib3's HTTPResponse doesn't, but a future refactor or mock could), getattr(raw, "data", raw) returns None, isinstance(None, bytes) is False, and str(None) yields the literal string "None" — which then flows into _parse_probe_output as a probe log. Not exploitable, not a real failure mode today, but a one-line if data is None: return "" after the getattr would close the gap and match the existing if raw is None: return "" style. Drive-by suggestion only.
Outstanding from prior reviews (still open, non-blocking)
Unchanged from the previous round:
integration_tests/conftest.py:168—except FileNotFoundError, subprocess.TimeoutExpired:unparenthesized form. Producer's PEP 758 +ruff format(target-versionpy314) rationale is accepted; under py314 the unparenthesized form is canonical and any parenthesization would be reverted by the formatter. Carried for the record only.
All other items from prior reviews are now addressed in aabc90c1, 77c33171, or 0e1befc9.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
| egg is addressing review feedback... |
…defensive test - Add explicit if data is None: return '' after the getattr() in _read_probe_log so a hypothetical response with .data = None yields '' rather than the literal string 'None' (which would flow into _parse_probe_output as probe log content). - Add TestReadProbeLog::test_returns_empty_string_on_none_data unit test pinning the new guard. - Clarify test_str_fallback_for_object_without_data's docstring to state it is a defensive-only branch — kubernetes-python with _preload_content=False always returns urllib3.HTTPResponse, which always has .data; the str() fallback exists only so a future client upgrade or mock returning a plain string degrades cleanly. Both items raised non-blocking on the 1e5daf1 re-review.
Response to review feedback (commit
|
|
egg feedback addressed. View run logs 26 previous review(s) hidden. |
|
egg is investigating the Test check failure...
|
Check Failure: Integration Tests (Test workflow, run 25766592695)What's failing: The The 503 came from Root cause: Transient external infrastructure outage. Launchpad's PPA This PR doesn't modify What needs to be done:
Suggestion: If deadsnakes PPA flakes become a recurring problem, — Authored by egg |
|
egg check fixer completed for Test. CI will re-run to verify. View run logs — Authored by egg |
Closes #2641.
Closes #2646.
Closes #2647.
Closes #2648.
Closes #2652.
Summary
Adds
integration_tests/test_deployment_validation_logic.pyexercising the post-auth behaviour ofPOST /api/v1/deployment/validate-manifestsandPOST /api/v1/deployment/validate-network-isolation. The existing siblingtest_k8s_deployment_tools.pycovers@require_lifecycle_secretparity (401/503 on missing or wrong bearer) but stops at the auth gate — the lifecycle bearer wasn't surfaced through the sharedEggStackfixture.integration_tests/conftest.pynow readslifecycle-secretfromgateway-secretsalongsidelauncher-secretand exposes a session-scopedlifecycle_secretfixture (skips cleanly when the Secret has no such key).Bug fixes bundled (previously filed as follow-ups)
The original PR locked in the observable broken behaviour via
xfail(strict=True)and "today's shape" assertions, and filed four follow-up issues. Those four are now fixed in this PR so the tests can assert the actual happy paths instead of the regressions; the bundled fixes are small, targeted, and the integration coverage exists in the same commit that flips them.egg-cluster-topology-readergranting the orchestrator SAget,listonapps/daemonsetsand cluster-scopednodes(k8s/base/rbac.yaml)._detect_cni/_detect_k3snow run against a real cluster instead of returning null/false.kustomize(v5.6.0) in the orchestrator image (orchestrator/Dockerfile)._run_kustomizeno longer raiseskustomize_unavailable. The secondary gap noted in validate_deployment_manifests is unreachable in production: orchestrator container has no kustomize/kubectl #2647 (egg repo not bind-mounted under the CI overlay) is acknowledged out of scope; the CI default-overlay path therefore returns 404 (overlay not found) post-fix while the local-overlay happy path returns 200.listto the verbs onapps/deploymentsin theegg-service-log-readerRole (k8s/base/rbac.yaml)._collect_egg_image_tagsnow returns populated tags;get_deployment_contextno longer reportsimages_unavailable: true.orchestrator_direct_blocked→orchestrator_api_reachableand flip its polarity inPROBE_COMMAND_TEMPLATE(orchestrator/routes/deployment.py). The agent→orchestrator heartbeat path is deliberately permitted byallow-agent-to-orchestrator, so the field now reads positively. Callers updated:orchestrator/mcp_tools.py,orchestrator/tests/test_mcp_tools.py,orchestrator/tests/test_deployment_routes.py,docs/reference/mcp-deployment-tools.md,skills/agent-diagnose/SKILL.md.Test coverage
TestValidateDeploymentManifestsLogic../traversal, default-overlay returns 200 (repo mounted) or 404 (CI / unmounted), idempotent re-validation.TestValidateNetworkIsolationLogicpipeline_id/role, defaults accepted, regex boundary cases (parametrized: leading/trailing punctuation, ≤63/>63 length, uppercase, dot/underscore mid-token), and the happy-path probe-shape assertion (no longerxfail—_detect_cninow resolves to("calico", True)against the integration cluster).TestValidationRouteConcurrencyTestValidationRouteSelfConsistencyTestProbeJobCleanupegg-agentsafter the route returns.validate_config(the third route named in #2641) is deliberately out of scope for the k3s tier — it's an MCP-side Pydantic handler with no HTTP route. Its coverage already exists inorchestrator/tests/test_mcp_tools.py::TestValidateConfig. The test module's docstring explains this.Constraint per #2474
Per the constraint write-up in #2474, the SDLC sandbox can't bring up k3s. These tests are validated by CI's
Test / aggregateintegration tier; the canonical signal is the green required-check on this PR.Test plan
Test / aggregateintegration job is green.test_k8s_deployment_tools.py21-case auth-reject suite.test_probe_runs_and_returns_expected_shapepasses (thexfailmarker is gone now that validate_network_isolation always short-circuits in production: orchestrator SA lacks kube-system DaemonSet RBAC #2646 is bundled).test_default_overlay_returns_404_or_200_depending_on_repo_mountreturns 404 in CI (egg repo not bind-mounted) without 500 (kustomize is now in the image).