Skip to content

e2e: a transient apiserver error makes the whole suite fall back to the public Talos factory, cached suite-wide #3668

Description

Summary

resolve_talos_image_factory_url in hack/e2e-chainsaw/_lib/talos-image-cache.sh decides whether to use the in-cluster Talos image mirror with if kubectl -n kube-system get deploy talos-image-cache >/dev/null 2>&1 (line 158). Any non-zero exit is read as "the mirror is not deployed", including a transient connection-refused, an Unauthorized, or an apiserver blip, not only a genuine absence. The result is written to _TALOS_IMAGE_FACTORY_DECISION_FILE and cached for the whole suite (lines 153-154, 182), so one bad check pins the decision for every worker that follows.

Why it matters

This is the happy path, and the decision it makes is high-blast-radius: when it wrongly concludes "no mirror", every tenant worker in the suite falls back to the public Image Factory over live egress, which is exactly the dependency the mirror exists to remove. A transient apiserver hiccup during this one check therefore turns into suite-wide public-factory pulls, and a slow or throttled public factory is a plausible contributor to the node-join / image-pull timeouts the e2e suite flakes on. The failure is silent: the suite runs, just against the flaky dependency, so it reads as environment noise rather than a decision bug.

Same class as the image-cache gate defect just fixed on the failure path (a kubectl get exit code does not distinguish "absent" from "could not ask"), but here on the happy path where the cost is a whole-suite fallback rather than one diagnostic line.

Fix shape

Distinguish "deployment absent" from "could not determine": kubectl get deploy ... --ignore-not-found -o name gives exit 0 plus empty output for a real absence and a non-zero exit for a refused/unauthorized/timed-out check. Treat only the empty-output case as "no mirror"; on an indeterminate result, either retry briefly or fail loud rather than silently pinning the public-factory decision for the suite. Do not cache an indeterminate result.

Two more sites in the same function, not covered by the fix in flight

Checked against fix/talos-factory-decision-indeterminate, both still present there.

_apply_talos_image_cache_egress_policy || true discards its return value, so a failed policy apply changes neither the decision nor whether it is cached. The endpoint is committed and cached while the tenant default-deny drops traffic to it, which is the same shape as the unanswered lookup this issue is about: an outcome the function cannot observe is treated as a good one. The sibling helper on the ghcr side takes the opposite branch, refusing to cache when its own allow fails.

The decision file is written with a bare printf ... > "$file". The same file is read from another suite in the shared sandbox, so a concurrent reader can see a partially written decision. The ghcr helper writes atomically.

Both belong here rather than in a separate issue: same function, same decision, same cache. Splitting them off would make it easy to fix half and close the ticket.

Activity

  1. added
    kind/bugCategorizes issue or PR as related to a bug
    area/ciIssues or PRs related to CI workflows, GitHub Actions, automation
    area/testingIssues or PRs related to testing (e2e, bats, unit tests)
    on Aug 8, 2026
  2. lexfrei commented on Aug 8, 2026

    @lexfrei
    ContributorAuthor

    Status note so the fix and the issue do not get confused with each other: the change in flight closes the FIRST door only, the kubectl get deploy at the top of resolve_talos_image_factory_url. This issue is not fully closed by it, and should stay open afterwards.

    The reason is that the decision is cached once, at the end of the function, regardless of which inner check produced the negative answer. Inside the "deployment is present" branch every probe still conflates "the mirror does not work" with "the probe could not be run": the kubectl rollout status wait, and the three reads in _talos_image_cache_reachable_from_tenant (the pod lookup, the exec ... find, and the deploy image read). A transient apiserver error in any of them still reaches the cache write with an empty URL, which is exactly the suite-wide fallback this issue is named after. The fix shape is the same one the in-flight change uses, applied a layer down: either cache only a positive decision and a proven negative, or introduce one "the probe never ran" signal and return on it without writing the file. Note that rollout status has no indicator distinguishing "not ready" from "could not ask" other than its own timeout, so it needs its own treatment.

    Two related facts measured on the same file while confirming the above. The pod lookup uses -o jsonpath='{.items[0].metadata.name}', which is a hard error on an empty list rather than empty output, so "no Running mirror pod" and "the apiserver refused" are again the same non-zero exit; {.items[*]...} with a head -n1, or --ignore-not-found -o name, separates them. And none of the file's kubectl calls carried --request-timeout before this change (0 of 17), while the sibling run-kubernetes.sh uses it on 18 calls and documents why: the flag bounds the HTTP request, an outer timeout bounds the client's retries. The kubectl default is 0, meaning no timeout at all, so each of these probes is unbounded on a wedged apiserver.

  3. added
    triage/acceptedIndicates an issue is ready to be actively worked on
    priority/backlogGeneral backlog priority. Lower than priority/important-longterm
    and removed
    triage/needs-triageIndicates an issue needs triage by a maintainer
    on Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/ciIssues or PRs related to CI workflows, GitHub Actions, automationarea/testingIssues or PRs related to testing (e2e, bats, unit tests)kind/bugCategorizes issue or PR as related to a bugpriority/backlogGeneral backlog priority. Lower than priority/important-longtermtriage/acceptedIndicates an issue is ready to be actively worked on

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions