fix(e2e): keep a failed image-mirror lookup from pinning the public factory - #3677
Conversation
📝 WalkthroughWalkthroughThe Talos image cache lookup now uses bounded retries and tri-state deployment results. Unknown status avoids caching fallback decisions. Tests cover command stubbing, retries, deadlines, caching, resolution, and diagnostics. ChangesTalos image cache lookup
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant ImageFactoryResolver
participant Timeout
participant Kubectl
ImageFactoryResolver->>Timeout: wrap deployment query when available
Timeout->>Kubectl: query deployment with --ignore-not-found
Kubectl-->>Timeout: return deployment status or failure
ImageFactoryResolver->>ImageFactoryResolver: retry failed queries
ImageFactoryResolver-->>ImageFactoryResolver: cache established state or use uncached fallback
Possibly related issues
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
hack/e2e-chainsaw/_lib/talos-image-cache.sh (1)
262-273: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider a shorter lookup budget for the diagnose path.
talos_image_cache_diagnosetreatspresentandunknownthe same: both continue to the dump. Onlyabsentchanges behavior. The retries therefore change the outcome only when a later attempt returns a confirmed absence, which is rare on this path.With the defaults, a wedged apiserver costs about 3 × 30s plus 2 × 5s before the dump starts, and the dump then issues more
kubectlcalls that also stall. Diagnose runs on an already-failing node-join, so this delay is added to a failure path.You can keep the shared helper and lower the budget for this caller only.
♻️ Proposed change to bound the diagnose lookup
talos_image_cache_diagnose() { local state - state=$(_talos_image_cache_deploy_state) + # One bounded attempt is enough here: present and unknown both dump, so only a + # confirmed absence changes the outcome. + state=$(_TALOS_IMAGE_CACHE_QUERY_TRIES=1 _talos_image_cache_deploy_state) if [ "$state" = absent ]; thenNote that
_TALOS_IMAGE_CACHE_QUERY_TRIESis currently a plain variable, not alocalin the helper, so the prefix assignment above works only because the helper reads it at call time. Confirm the override does not leak into later calls in the same shell.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hack/e2e-chainsaw/_lib/talos-image-cache.sh` around lines 262 - 273, In talos_image_cache_diagnose, use a caller-scoped override to shorten the _talos_image_cache_deploy_state lookup budget while preserving the shared helper. Ensure the temporary _TALOS_IMAGE_CACHE_QUERY_TRIES value is restored after the lookup so it cannot affect later calls in the same shell, while retaining the existing absent and unknown handling.hack/talos-image-cache_test.bats (1)
162-168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMake the
timeoutstub fail loudly when it cannot find the wrapped command.The stub shifts until
$1equals the literal stringkubectl. If a future caller wraps any other binary withtimeout, the loop consumes every argument andexec "$@"runs with no arguments.shthen exits 0 and produces no output._talos_image_cache_deploy_statereads that asabsent, which is the exact outcome the tri-state lookup exists to prevent, and the test still passes.A generic skip of the leading option arguments is both more faithful to real
timeoutand fails loudly on a mismatch.♻️ Proposed change to the `timeout` stub
printf '%s\n' \ '#!/bin/sh' \ 'printf "%s\n" "$*" >> "$STUB_TIMEOUT_CALLS"' \ - 'while [ $# -gt 0 ] && [ "$1" != kubectl ]; do shift; done' \ + '# Skip the options and the duration, the way real timeout does.' \ + 'while [ $# -gt 0 ]; do' \ + ' case "$1" in -*|[0-9]*) shift ;; *) break ;; esac' \ + 'done' \ + '[ $# -gt 0 ] || { echo "stub timeout: no command to run" >&2; exit 125; }' \ 'exec "$@"' \ > "$1/bin/timeout"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hack/talos-image-cache_test.bats` around lines 162 - 168, Update the timeout stub in the test setup to skip leading option arguments generically, then require a wrapped command before executing it; do not search specifically for the literal kubectl argument. Make the stub exit nonzero with a clear diagnostic when no command remains, preserving normal execution for timeout invocations that include options followed by a command.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@hack/e2e-chainsaw/_lib/talos-image-cache.sh`:
- Around line 262-273: In talos_image_cache_diagnose, use a caller-scoped
override to shorten the _talos_image_cache_deploy_state lookup budget while
preserving the shared helper. Ensure the temporary
_TALOS_IMAGE_CACHE_QUERY_TRIES value is restored after the lookup so it cannot
affect later calls in the same shell, while retaining the existing absent and
unknown handling.
In `@hack/talos-image-cache_test.bats`:
- Around line 162-168: Update the timeout stub in the test setup to skip leading
option arguments generically, then require a wrapped command before executing
it; do not search specifically for the literal kubectl argument. Make the stub
exit nonzero with a clear diagnostic when no command remains, preserving normal
execution for timeout invocations that include options followed by a command.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: be268ba5-b1ea-4044-ba08-dde2622675fa
📒 Files selected for processing (2)
hack/e2e-chainsaw/_lib/talos-image-cache.shhack/talos-image-cache_test.bats
1106227 to
3598f2c
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
hack/e2e-chainsaw/_lib/talos-image-cache.sh (2)
198-204: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe bounds here are hardcoded while every other read in this file uses the shared knobs.
Lines 199-203 pin
timeout -k 5 30and--request-timeout=30s. Lines 435-440 and 460-470 use_TALOS_IMAGE_CACHE_READ_GRACEand_TALOS_IMAGE_CACHE_READ_TIMEOUT, which_talos_image_cache_secondsvalidates and which the comment at lines 271-284 says exist so the outer and inner bounds cannot drift apart.Two consequences. A test that lowers
_TALOS_IMAGE_CACHE_READ_TIMEOUTstill waits the full path here: three attempts of 30s plus two delays, about 115s. An operator reading the file sees two different bounding conventions for the same kind of read.Reuse the existing knobs, or add a named default beside
_TALOS_IMAGE_CACHE_READ_TIMEOUT_DEFAULTand state in the comment why this read needs a longer bound than the diagnostic reads.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hack/e2e-chainsaw/_lib/talos-image-cache.sh` around lines 198 - 204, The talos-image-cache deployment read in the command -v timeout branch uses hardcoded timeout values instead of the shared bounds. Update this read to use _TALOS_IMAGE_CACHE_READ_GRACE and _TALOS_IMAGE_CACHE_READ_TIMEOUT, preserving the existing _talos_image_cache_seconds validation and keeping both timeout paths consistent; only introduce a separate named default if this read intentionally requires a longer bound and document that distinction.
434-445: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe code does not match two claims in the PR description.
The description states that "the factory decision and diagnostic paths share the same helper". They do not. Lines 434-441 are a second inline copy of the query shape used at lines 198-204. Only the bounds and the retry policy differ. The comment at lines 158-162 argues the separation is deliberate, so the code may be right and the description wrong.
The description also states that "diagnostics dump available data when the lookup remains unanswered". Lines 442-445 return early on an unanswered lookup and dump nothing.
Confirm which behavior you intend. If the separation is intended, correct the description. If a shared helper is intended, extract a single-attempt primitive that takes the grace, the timeout and the request timeout as parameters, and let
_talos_image_cache_deploy_statewrap it in the retry loop.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hack/e2e-chainsaw/_lib/talos-image-cache.sh` around lines 434 - 445, The diagnostic lookup in the relevant Talos image-cache flow duplicates the factory query and returns without dumping available data when the lookup fails. Confirm the intended behavior: either update the PR description to accurately document the deliberate separation and early return, or refactor the query into a shared single-attempt helper parameterized by grace, timeout, and request timeout, then have _talos_image_cache_deploy_state use it for retries and diagnostics dump data after an unanswered lookup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@hack/e2e-chainsaw/_lib/talos-image-cache.sh`:
- Around line 192-193: Validate _TALOS_IMAGE_CACHE_QUERY_TRIES and
_TALOS_IMAGE_CACHE_QUERY_DELAY inside the query function, at call time rather
than during their top-level initialization. Reuse _talos_image_cache_seconds for
delay validation and add equivalent integer validation for the retry count
before entering the loop; invalid overrides must follow the function’s existing
fallback/error path instead of allowing repeated test and sleep failures.
---
Nitpick comments:
In `@hack/e2e-chainsaw/_lib/talos-image-cache.sh`:
- Around line 198-204: The talos-image-cache deployment read in the command -v
timeout branch uses hardcoded timeout values instead of the shared bounds.
Update this read to use _TALOS_IMAGE_CACHE_READ_GRACE and
_TALOS_IMAGE_CACHE_READ_TIMEOUT, preserving the existing
_talos_image_cache_seconds validation and keeping both timeout paths consistent;
only introduce a separate named default if this read intentionally requires a
longer bound and document that distinction.
- Around line 434-445: The diagnostic lookup in the relevant Talos image-cache
flow duplicates the factory query and returns without dumping available data
when the lookup fails. Confirm the intended behavior: either update the PR
description to accurately document the deliberate separation and early return,
or refactor the query into a shared single-attempt helper parameterized by
grace, timeout, and request timeout, then have _talos_image_cache_deploy_state
use it for retries and diagnostics dump data after an unanswered lookup.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 70086e04-fdf6-4288-a499-20e6d7da7179
📒 Files selected for processing (2)
hack/e2e-chainsaw/_lib/talos-image-cache.shhack/talos-image-cache_test.bats
🚧 Files skipped from review as they are similar to previous changes (1)
- hack/talos-image-cache_test.bats
| _TALOS_IMAGE_CACHE_QUERY_TRIES="${_TALOS_IMAGE_CACHE_QUERY_TRIES:-3}" | ||
| _TALOS_IMAGE_CACHE_QUERY_DELAY="${_TALOS_IMAGE_CACHE_QUERY_DELAY:-5}" |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Validate the retry knobs, or a non-integer override loops forever.
_TALOS_IMAGE_CACHE_QUERY_TRIES and _TALOS_IMAGE_CACHE_QUERY_DELAY come straight from the environment with no check. The rest of this file routes every duration through _talos_image_cache_seconds for exactly this reason.
If _TALOS_IMAGE_CACHE_QUERY_TRIES is not a bare integer, [ "$try" -ge "$_TALOS_IMAGE_CACHE_QUERY_TRIES" ] fails with "integer expression expected" and returns non-zero on every pass. The loop then never reaches the unknown exit, and sleep also errors each pass, so the loop spins without delay. That wedges the Chainsaw op rather than falling back.
Validate at call time inside the function. Do not validate at line 192, because _talos_image_cache_seconds is defined further down at line 303 and is not yet available when line 192 runs.
🛡️ Proposed fix
_talos_image_cache_deploy_state() {
local out rc try=1
+ local tries delay
+ tries=$(_talos_image_cache_seconds "${_TALOS_IMAGE_CACHE_QUERY_TRIES-}" 3 _TALOS_IMAGE_CACHE_QUERY_TRIES positive)
+ delay=$(_talos_image_cache_seconds "${_TALOS_IMAGE_CACHE_QUERY_DELAY-}" 5 _TALOS_IMAGE_CACHE_QUERY_DELAY)
while :; do
@@
- if [ "$try" -ge "$_TALOS_IMAGE_CACHE_QUERY_TRIES" ]; then
+ if [ "$try" -ge "$tries" ]; then
printf 'unknown'
return 0
fi
try=$((try + 1))
- sleep "$_TALOS_IMAGE_CACHE_QUERY_DELAY"
+ sleep "$delay"
done
}Also applies to: 213-218
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@hack/e2e-chainsaw/_lib/talos-image-cache.sh` around lines 192 - 193, Validate
_TALOS_IMAGE_CACHE_QUERY_TRIES and _TALOS_IMAGE_CACHE_QUERY_DELAY inside the
query function, at call time rather than during their top-level initialization.
Reuse _talos_image_cache_seconds for delay validation and add equivalent integer
validation for the retry count before entering the loop; invalid overrides must
follow the function’s existing fallback/error path instead of allowing repeated
test and sleep failures.
3598f2c to
4ed7d68
Compare
The Talos image factory decision asks the apiserver once whether the in-cluster mirror Deployment exists, reads any non-zero exit as an absence, and caches that answer for the whole suite. A refused, throttled or timed-out lookup therefore sends every tenant worker in the run to the public Image Factory over live egress -- the dependency the mirror exists to remove -- and the run reads as ordinary environment noise rather than as a decision that was never actually made. Route that question through one helper and pin what the helper owes its caller: the three outcomes it must tell apart, the flags that make the three-way answer possible at all, a deadline on every attempt, the caching rule -- a real absence is resolved once and cached, an unanswered lookup falls back for its own caller alone -- and that a missing local timeout binary leaves the lookup asking the apiserver rather than answering unknown three times over. Two of those need saying carefully, because both concern what the caller is told when the lookup fails. A killed attempt gets its own case, driven by a wrapper that exits without running kubectl at all, so the lookup has to account for why it gave up rather than relay someone else's words: pinning the reason on a kubectl that failed loudly, as the sibling case does, pins it only where it was never in doubt. And the wall-clock bound is pinned as strictly greater than the request deadline beside it, which is the property that lets kubectl reach its own deadline and say what happened; level with it, the kill always lands first. The helper is a placeholder in this commit and claims present unconditionally, so the cases that need the distinction fail. The stubs are files on PATH rather than shell functions because the lookup runs under an external timeout binary, which execs a real file and would never see a shell override. xtrace stays off for the rest of a test that captures stderr: turned back on, a failing assertion wedges bats instead of reporting it. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
resolve_talos_image_factory_url asked whether the in-cluster Talos image mirror exists with a plain `kubectl get deploy`, where a connection refused, an Unauthorized and a real absence all arrive as the same non-zero exit. It then cached that reading for the whole suite, so one unlucky moment sent every tenant worker to the public Image Factory over live egress -- the dependency the mirror exists to remove -- while the run still looked ordinary. Ask with --ignore-not-found -o name instead: a real absence answers exit 0 with no output, which leaves a non-zero exit meaning only that the question failed, and re-ask a couple of times before giving up. Only the empty answer turns the suite toward the public factory. An unanswered lookup warns, falls back for its own caller alone, and is not written to the decision file, so the next tenant test asks again rather than inheriting a guess. Each attempt is bounded twice, and the two bounds are deliberately not equal. kubectl defaults --request-timeout to 0, so a connection that establishes and then stalls has no deadline of its own; the flag alone is not a wall-clock bound either, because kubectl retries discovery several times before giving up. Measured against a stub apiserver, the three ways this call fails behave differently: a refused port answers in under a second and names the refusal, one that stalls mid-TLS gets kubectl's own handshake deadline and still produces text, and a blackholed address produces nothing until some deadline expires. With both bounds at the same value the expiry that arrives first is always the wall-clock kill, so --request-timeout can never fire and that last case stays silent. The wrapper therefore sits ten seconds above it: the request deadline ends the attempt and kubectl says why, and the wrapper remains the backstop for a client that overruns its own deadline. Where timeout is not installed the read runs unwrapped rather than through it: its exit 127 would otherwise count as an attempt that did not answer, three times over, and a missing local binary would pin the whole suite to the public factory while every message blamed the apiserver. kubectl's stderr is left alone, because it is the only thing separating refused from unauthorized from stalled. It is still not guaranteed, since a kill can land before the client has said anything, so the lookup also names the last attempt's exit status before giving up, in the vocabulary the diagnostic reads use for 124 and 137. The diagnostic path asks the same question and keeps its own answer to it. That one gives up after a single attempt, under a phase budget where re-asking spends what the snapshot behind it needs; this one runs on the happy path, where a re-ask costs a suite nothing and buys it the mirror. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
4ed7d68 to
5400f16
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@hack/talos-image-cache_test.bats`:
- Around line 280-305: Extend the test at hack/talos-image-cache_test.bats lines
280-305 by calling resolve_talos_image_factory_url a second time and asserting
the stub counter reaches six, proving an unknown decision triggers three fresh
lookup attempts; retain the existing warning and uncached assertions. Extend the
test at hack/talos-image-cache_test.bats lines 333-347 by calling
resolve_talos_image_factory_url again and asserting the stub counter remains
one, proving a confirmed absence reuses the saved decision.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 983456a8-2dac-4a5d-aa46-1944dda0076a
📒 Files selected for processing (2)
hack/e2e-chainsaw/_lib/talos-image-cache.shhack/talos-image-cache_test.bats
🚧 Files skipped from review as they are similar to previous changes (1)
- hack/e2e-chainsaw/_lib/talos-image-cache.sh
| @test "an undecidable lookup warns and is not cached for the rest of the suite" { | ||
| d=$(mktemp -d) | ||
| _TALOS_IMAGE_FACTORY_DECISION_FILE="$d/decision" | ||
| . hack/e2e-chainsaw/_lib/talos-image-cache.sh | ||
| _TALOS_IMAGE_CACHE_QUERY_DELAY=0 | ||
| _stub_kubectl_dir "$d" 99 'deployment.apps/talos-image-cache' | ||
| # cozytest.sh runs each test under `set -x`, and xtrace shares the stderr | ||
| # this captures. Off for the call, so the assertions below read what the | ||
| # function said rather than an echo of the commands it ran, and left off | ||
| # afterwards: a test that turns it back on and then fails wedges `bats` | ||
| # instead of reporting, which costs a reader the failure they came for. | ||
| set +x | ||
| url=$(resolve_talos_image_factory_url 2>"$d/err") | ||
| cached=no | ||
| if [ -f "$d/decision" ]; then cached=yes; fi | ||
| err=$(cat "$d/err") | ||
| rm -rf "$d" | ||
| # Falling back for this one caller is fine; pinning that fallback for every | ||
| # later tenant test on the strength of a failed question is not. | ||
| [ -z "$url" ] | ||
| [ "$cached" = no ] | ||
| printf '%s' "$err" | grep -q 'could not determine' || { echo "an undecidable lookup must say so: [$err]" >&2; exit 1; } | ||
| # And it must say why. "could not determine" alone leaves an operator with a | ||
| # new silent failure in place of the old one: refused, Unauthorized and | ||
| # timed-out all read the same until kubectl's own message reaches the log. | ||
| printf '%s' "$err" | grep -q 'the server was unable to return a response' || { echo "the reason the lookup failed must reach the log: [$err]" >&2; exit 1; } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Exercise cache behavior with a second resolver call.
Both tests inspect $d/decision after one call. They do not verify behavior for the next caller.
hack/talos-image-cache_test.bats#L280-L305: Callresolve_talos_image_factory_urlagain. Assert that the stub counter reaches six, so the second unknown result performs three new lookup attempts.hack/talos-image-cache_test.bats#L333-L347: Callresolve_talos_image_factory_urlagain. Assert that the stub counter remains one, so confirmed absence uses the saved decision.
📍 Affects 1 file
hack/talos-image-cache_test.bats#L280-L305(this comment)hack/talos-image-cache_test.bats#L333-L347
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@hack/talos-image-cache_test.bats` around lines 280 - 305, Extend the test at
hack/talos-image-cache_test.bats lines 280-305 by calling
resolve_talos_image_factory_url a second time and asserting the stub counter
reaches six, proving an unknown decision triggers three fresh lookup attempts;
retain the existing warning and uncached assertions. Extend the test at
hack/talos-image-cache_test.bats lines 333-347 by calling
resolve_talos_image_factory_url again and asserting the stub counter remains
one, proving a confirmed absence reuses the saved decision.
What this PR does
The tenant Kubernetes e2e suites decide once whether worker image pulls go to the in-cluster Talos mirror or to the public Image Factory, and that decision came from a plain
kubectl get deploy talos-image-cache. A plaingetexits non-zero for a connection refused, an Unauthorized and a real absence alike, so any blip during that one call read as "no mirror", and the answer went straight into the decision file every later tenant test reads. One unlucky moment sent every worker in the run to the public factory over live egress, which is the dependency the mirror exists to remove, and nothing in the log said so. The run went ahead against the flaky thing and the failures read as environment noise.The lookup now asks with
--ignore-not-found -o name. A real absence answers exit 0 with no output, which leaves a non-zero exit meaning only that the question failed, and the helper re-asks a couple of times before giving up. Only the empty answer turns the suite toward the public factory. An unanswered lookup warns, falls back for its own caller alone, and is deliberately not cached, so the next tenant test asks again instead of inheriting a guess.The warning has to be backed by something, and kubectl's stderr alone is not enough. When the wall-clock bound kills an attempt, neither the bound nor the SIGTERMed kubectl writes a word, and that stall is precisely the failure the re-ask was written against, so a warning that sent the reader to kubectl's message would be pointing at an empty stretch of log exactly when the lookup mattered. The lookup therefore names the last attempt's exit status before giving up, in the vocabulary the diagnostic reads already use for 124 and 137, and kubectl's own text stays on stderr for the cases where there is any.
Each attempt is bounded twice, and the two bounds are deliberately not equal:
timeout -k 5 40around--request-timeout=30s. kubectl defaults the request timeout to 0, so a connection that establishes and then stalls has no deadline at all, and the flag on its own is not a wall-clock bound either, because kubectl retries discovery several times before it gives up. Re-asking an unbounded call three times would have multiplied a hang instead of riding out a blip.The ten seconds between the two are load-bearing, and I only found out why by measuring rather than reasoning. Driven against a stub apiserver the three failure modes are not alike: a refused port answers in under a second and names the refusal, one that completes TCP but stalls mid-TLS hits kubectl's own handshake deadline and still produces text despite being killed, and a blackholed address (SYN dropped, no RST, which is what a wedged node or a netpol looks like) produces nothing at all until some deadline expires. That last mode is the one the headroom buys back. With both bounds at the same value the expiry that arrives first is always the wall-clock kill, so the blackhole case ends as a bare exit 124; with the wrapper ten seconds above, the request deadline ends the attempt and kubectl says what happened, and the wrapper stays as the backstop for a client that overruns its own deadline. Controlled A/B, same target and same inner bound, only the outer differs: 30 over 30 gives exit 124 and zero bytes, 40 over 30 gives exit 1 at the thirty second mark and a message naming the cause. The cost is ten more seconds per attempt on a path where the apiserver is already unreachable, inside a 50m op.
Worth stating separately, because it is a different question with a different answer: before this change the inner flag was not doing anything at all.
--request-timeout=30ssat under atimeoutof the same 30 seconds, and across all three measured modes it never once fired. The refused case never reached it, the TLS stall was ended by kubectl's own shorter handshake deadline, and the blackhole was ended by the wall-clock kill landing at the same instant. So the flag was decoration, and the comment above it claimed otherwise. That is a separate defect from the lost diagnostics, and it is fixed by the same separation rather than by adding anything.Where
timeoutis not installed the read runs unwrapped instead of through it. Its exit 127 would otherwise count as an attempt that did not answer, three times over, so a missing local binary would pin the whole suite to the public factory while every message blamed the apiserver.talos_image_cache_diagnoseasks the same question on the node-join failure path, and #3676 landed its own three-way gate there while this branch was open. The two stay separate rather than sharing this helper. That one runs under a phase budget where re-asking spends what the tenant snapshot behind it needs, so it answers on a single attempt; this one runs on the happy path, where a re-ask costs a suite nothing and buys back the mirror. This branch leaves that gate as it was merged.Unit tests put a throwaway
kubectlandtimeouton PATH and drive all three outcomes, the re-ask, the caching rule, thetimeout-absent path and the killed-attempt path where nothing but the lookup itself can explain the failure:hack/cozytest.sh hack/talos-image-cache_test.bats. The stubs are files rather than shell functions because an externaltimeoutbinary execs a real file and would never see a shell override.The re-ask is a retry loop, which
docs/agents/e2e-testing.mdrestricts. It falls under the exemption there rather than the prohibition: what is retried is a network read with no product or test logic under it, and the deterministic steps the rule protects are untouched.This relates to #3668 and closes the first door only. The decision is cached once at the end of the function regardless of which inner check produced the negative answer, and inside the "mirror is present" branch the
rollout statuswait and the three reads in_talos_image_cache_reachable_from_tenantstill conflate "the mirror does not work" with "the probe could not be run", with the same suite-wide effect one layer down. The issue's own status comment says it stays open after this change, so there is no closing keyword here.Three weaknesses this PR does not remove, named because each is a real thing a later reader will hit. The
absentcase inresolve_talos_image_factory_urlis reached byelsefall-through rather than by matching the token, so a fourth state added to the helper later would land in the branch that turns the suite toward the public factory rather than in the safe one; a test pins thepresenttoken against exactly that drift, but the shape stays fall-through. The lookup's own30and5are literals while the diagnostic reads next door run off a validated, overridable pair, and that is deliberate rather than an oversight: those knobs are named and documented for the dump budget, so wiring the lookup to them would let a test that turns the dumps down silently change which image factory a whole suite pulls from. And_TALOS_IMAGE_CACHE_QUERY_TRIESis the one knob in the file without that validation. A non-numeric value spins the loop forever rather than failing: the comparison sits in anifcondition, soset -eis exempt, the branch is simply never taken, and the job burns to its ceiling with no output. Nothing outside the tests sets it, which is why it is here and not in the diff. Worth recording for whoever does fix it: the obvious remedy, reusing this file's own_talos_image_cache_seconds, does not work where the knob is assigned, because the validator is defined a hundred lines further down and the assignment runs at source time. The call resolves to nothing, the substitution yields an empty string, and an empty bound spins the same loop. A real fix has to move the definition up or validate inside the function at call time.Screenshots
Not a UI change.
Downstream repositories
Walked the trigger map against the diff. It touches
hack/e2e-chainsaw/_lib/talos-image-cache.shandhack/talos-image-cache_test.batsand nothing else: no file underhack/is moved or renamed, no make target changes behaviour, and no package, chart, CRD or values key is involved. Thecozystack/ccpentry is the closest one and it does not fire.Release note