Skip to content

fix(systemtest): the Hetzner lane picks its location at run time (ankra-ews9qk) - #511

Merged
Myothas merged 2 commits into
masterfrom
fix/ankra-ews9qk-hetzner-location-at-runtime
Oct 14, 2026
Merged

Myothas merged 2 commits into
masterfrom
fix/ankra-ews9qk-hetzner-location-at-runtime

Conversation

@Myothas

@Myothas Myothas commented Oct 14, 2026

Copy link
Copy Markdown
Member

What & why

This is a follow-up to #510. The live run 38573723784, dispatched right after #510 merged, showed two things:

  • The new reporting works. Both lanes now say exactly why they failed.
  • The fsn1 default was itself refused: Server type 'cpx32' is not available in location 'fsn1'.

Hetzner's shared-vCPU stock moves by the minute. On 2026-10-14 cpx32 left nbg1, then fsn1, then came back to nbg1, all within the hour. No fixed location keeps the lane creatable.

So the lane now picks its location at run time:

  • Default: HETZNER_LOCATION defaults to empty.
  • Candidates: the lane takes the first of HETZNER_LOCATION_CANDIDATES (fsn1 nbg1 hel1) that offers every type it uses right now: control plane, worker, bastion and the resize type. It reads them from ankra cluster hetzner server-types --available-only with the lane's own credential.
  • Nothing fits: the lane fails with "no Hetzner location in '' offers right now".
  • Pinning: setting HETZNER_LOCATION still pins it.

The lane keeps the cheap shared-vCPU types (cpx22 / cpx32) instead of moving to dedicated ccx.

Test plan

  • TestHetznerLanePicksALocationThatOffersItsTypesNow covers:
    • the empty default;
    • hetzner_types_from_table on the CLI's table: header skipped, unavailable rows dropped;
    • pick_hetzner_location with stubbed availability: skips a location missing a type, keeps the candidate order, fails when none fits.
  • Run live against Hetzner just now: fsn1 lacked cpx32, and the picker chose nbg1.
  • The pre-commit gate passed: full go test ./... and golangci-lint run with 0 issues.
  • A lifecycle run will be dispatched after merge.

Docs checklist

  • No user-facing command/flag changes — no docs needed. This changes only the system test.
  • Command/flag changes — the CLI reference regenerates automatically on release (tools/gendocs); ensure Short/Long/Example on new commands are written for docs readers
  • Broader behaviour changes — docs updated in ankra-docs (link the PR):

Bead: ankra-ews9qk

🤖 Generated with Claude Code

The fsn1 default from #510 was refused within the hour: Hetzner's
shared-vCPU stock moves by the minute (on 2026-10-14 cpx32 left nbg1, then
fsn1, then came back to nbg1), so no fixed location keeps the lane
creatable. HETZNER_LOCATION now defaults to empty and the lane takes the
first of HETZNER_LOCATION_CANDIDATES (fsn1 nbg1 hel1) that offers every
type it uses right now (control plane, worker, bastion and the resize
type), read from 'ankra cluster hetzner server-types --available-only' with
the lane's own credential. When none does, the lane fails saying exactly
that. Setting HETZNER_LOCATION still pins it.

Bead: ankra-ews9qk

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ankra-platform

ankra-platform Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Ankra AI review

Verdict: looks good.

The lane now probes each candidate location at run time and, importantly, distinguishes 'read and lacking' from 'could not read' - an unreadable location is never treated as a negative answer, and the all-unreadable case is reported as a credential/API problem rather than a stock problem. The Go test exercises both the table parser and the picker against a stubbed reader, including the failure-wording cases. Only a minor note about the picked location being reused for the rest of the run.

Findings

  • [low] Picked location is pinned for the rest of the run (systemtest/lifecycle_systemtest.sh:1965)
    Assigning HETZNER_LOCATION here mutates the global, so any later hetzner lane in the same script run reuses this pick without re-probing. That is probably desirable (a mid-run location switch would be surprising), but if the intent was per-lane freshness, the assignment should be scoped or re-picked per lane. Worth a one-line comment either way.

Reviewed commit f20d958. This review is read-only and advisory.

Comment thread systemtest/lifecycle_systemtest.sh Outdated
pick_hetzner_location() {
local location needed available is_complete
for location in $HETZNER_LOCATION_CANDIDATES; do
available="$(hetzner_available_types "$location" || true)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[high] Failed server-types read is indistinguishable from 'location offers nothing'

available="$(hetzner_available_types "$location" || true)" swallows any failure of the CLI call (bad credential, network error, changed table format that makes awk print nothing). An unobserved location then looks exactly like an observed location missing a type, and if every read fails the lane dies with 'no Hetzner location ... offers ... right now', which is UNKNOWN being reported as a negative answer. Capture the exit status separately: on read failure, log it and either fail immediately with a 'could not list server types' message or skip the candidate while recording that it was never observed.

Ankra AI review. Read-only and advisory.

Comment thread systemtest/lifecycle_systemtest.sh Outdated
# hetzner_types_from_table reads a server-types table on stdin and prints the
# names whose AVAILABLE column says yes.
hetzner_types_from_table() {
awk 'NR > 2 && NF >= 9 && $NF == "yes" { print $1 }'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] awk parser assumes a fixed two-line preamble in the table output

NR > 2 skips exactly two lines, assuming the CLI always prints the 'Server types for location X:' banner plus a header row. The diff does not show ank_out's output format; if the banner is omitted (or a second header/wrapping line appears), the parser silently drops a real data row or keeps a header, again producing an empty/wrong answer that looks like 'type not offered'. Match on the row shape instead (e.g. require $NF == "yes" and $1 to look like a type name) rather than counting lines.

Ankra AI review. Read-only and advisory.

for location in $HETZNER_LOCATION_CANDIDATES; do
available="$(hetzner_available_types "$location" || true)"
is_complete=1
for needed in "$HETZNER_CP_TYPE" "$HETZNER_WORKER_TYPE" "$HETZNER_BASTION_TYPE" "$HETZNER_BIGGER_TYPE"; do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] Empty HETZNER_*_TYPE variables silently make every location incomplete

If any of HETZNER_CP_TYPE / HETZNER_WORKER_TYPE / HETZNER_BASTION_TYPE / HETZNER_BIGGER_TYPE is unset or empty, grep -qx "$needed" never matches and every candidate is rejected, surfacing as the misleading 'no location offers the types' failure. The diff does not show where these are defaulted; if they can be empty, validate them up front and fail with a configuration error instead.

Ankra AI review. Read-only and advisory.

…ing a type

Review of #511: a failed or empty server-types read was indistinguishable
from a location that lacks a type, so a credential or API problem could
surface as 'no location offers the types'. pick_hetzner_location now names
unread locations apart, says so when none could be read, and reports empty
type variables as a configuration error; the table parser matches the row
shape instead of skipping a fixed preamble.

Bead: ankra-ews9qk

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Myothas

Myothas commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

Review addressed in the latest commit: [high] a failed or empty server-types read now marks the location unread, never lacking; the failure says 'could not read: ' alongside 'lacking', and 'could not list Hetzner server types for any of ...' when nothing was readable (credential/API, not stock); hetzner_available_types returns the CLI's own failure. [medium] the parser matches the row shape (type-like name first, yes last), tested with and without the banner and header. [low] empty HETZNER_*_TYPE is a configuration error. All pinned in TestHetznerLanePicksALocationThatOffersItsTypesNow.

fail "$label create ($picked)"
return
fi
HETZNER_LOCATION="$picked"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] Picked location is pinned for the rest of the run

Assigning HETZNER_LOCATION here mutates the global, so any later hetzner lane in the same script run reuses this pick without re-probing. That is probably desirable (a mid-run location switch would be surprising), but if the intent was per-lane freshness, the assignment should be scoped or re-picked per lane. Worth a one-line comment either way.

Ankra AI review. Read-only and advisory.

@Myothas
Myothas merged commit cb302db into master Oct 14, 2026
3 checks passed
@Myothas
Myothas deleted the fix/ankra-ews9qk-hetzner-location-at-runtime branch October 14, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant