Skip to content

Collapse the target / build-host duplication - #15

Merged
Loa212 merged 4 commits into
mainfrom
refactor/collapse-target-build-host-duplication
Jul 31, 2026
Merged

Collapse the target / build-host duplication#15
Loa212 merged 4 commits into
mainfrom
refactor/collapse-target-build-host-duplication

Conversation

@Loa212

@Loa212 Loa212 commented Jul 30, 2026

Copy link
Copy Markdown
Owner

What this is

A thermo-nuclear-refactor pass over the whole repo. Behaviour is unchanged
throughout: make check passes, and the full Docker-backed e2e suite passes
14/14 — which matters here, because the largest change is to the ssh host-key
path those tests exercise directly.

The dominant finding is that the target / build-host parallel was implemented
by copy-paste at four layers
. Everything below follows from pulling on that.


1. The host-key state machine was written twice

targets and build_hosts carry the same six host-key columns with the same
meaning. Migration 0007 says so outright — "identical in shape and meaning to
the columns added to targets in 0006"
— and then the code over those columns
was duplicated anyway:

  • row_to_host_key was byte-identical in both stores.
  • pin / record_pending / clear_pending / forget existed once per table,
    eight methods for four operations.
  • The connect-and-record logic was copied a third and fourth time, inlined
    into the deploy engine's connect_build_host and into BuildHosts.Check,
    while the target equivalents already delegated to a shared helper.

It now lives once in store/host_keys.rs, keyed on an SshHost enum naming the
table. Engine::connect and ssh_target_for take either kind of host through
one SshHostRef borrow, so connect_build_host and ssh_target_for_build_host
are gone and BuildHosts.Check stops open-coding what Targets.Check already
delegated.

This is the change that matters most: it is what makes "a host presenting the
pinned key clears a pending change" true of build hosts because it is true of
targets, rather than true of whichever copy was edited last.

2. Two selects per table, and a test to police them

Each host store hand-copied its ~24 column names into a list select and a by-id
select. build_hosts carried a third copy as a test-only const plus a test
asserting the two selects agreed — a test that existed only because the
duplication existed. The by-id reads now append WHERE id = ?1 to the one
select, the same way the list reads already appended ORDER BY. The guard test
is retired with the thing it guarded.

3. A field mask meant one statement per field

update_target and update_build_host issued up to eight UPDATEs inside a
transaction that existed to make the set of them atomic. store/field_mask.rs
collects the named columns and writes them in one statement, so the update is
atomic on its own and the transaction goes away with the round trips. Which
columns a blank value writes is still per-field policy, now visible in one list.

4. set_*_status branched into two statements to do one thing

Both stores matched on whether the probe succeeded and ran one of two SQL
statements. set_ingress_status in the same file already expressed exactly this
("only success advances the timestamp") as a single COALESCE. Converged on
that form.

5. Five one-line wrappers around the canonical vocabulary

nudo-format is the shared crate for unit states, artifact sources and log
levels, but five callers still went through a wrapper around it —
server::units::status_label, server::logs::priority_label,
cli::units_label, and three of mcp::presentation's four functions. Two were
dead outside their own tests, and two carried doc comments describing a
duplication or a sharing that had stopped being true. Deleted; the exhaustive
assertions moved to sit with the logic, gaining cases neither copy covered.

6. The preflight-checks card was rendered twice

Identical markup in the target and build-host renderers, differing only in a
trailing warnings block. The two proto Check messages are distinct Rust types,
so they borrow into one Probe view and share one preflight_card. Rendered
HTML is byte-identical — all 230 web tests pass untouched.

7. The dashboard asked for seven things in a row

Four gRPC reads, each opening its own connection, then two database reads — in
series, with statement order the only thing indicating which depended on which.
None of the first five did. They now go out together, with a second group for
the two that need their results.

unit_statuses deliberately stays sequential. Each call opens an ssh
connection to the target, and making a dashboard refresh arrive at a
latency-critical box as a burst of simultaneous connections would trade the
property this tool exists to protect for a faster page.

8. tests/e2e.rs was 2358 lines

Three unrelated areas — deploying, building, ingress — over one shared container
fixture, with wait_for_async defined in the build-host section and used by the
deploy tests. Split into tests/e2e/{main,fixture,deploy,build_hosts,ingress}.rs:
still one binary named e2e, so the CI invocation and the Makefile target are
unchanged, and all 14 tests keep their names.

Splitting it also put the file under clippy for the first time — CI lints without
the e2e feature, so it never had been — which surfaced three lints. Fixed.


Verification

  • make check — fmt, clippy (-D warnings), 999 tests, release scripts. Green.
  • cargo test --features e2e --test e2e -- --test-threads=114/14 against
    real Docker containers
    , 362s. Exercises host-key pinning on first use, a
    refused changed key, accepting a reviewed key, build-host pinning, deploys and
    rollback — i.e. precisely the code section 1 rewrote.
  • Diffed the full test-name set against main: the only two tests that no longer
    exist are the drift guard from §2 and the label test from §5, both retired with
    the duplication they existed for. Every other test on main still runs.

Deliberately left alone

  • unit_statuses — see §7. The obvious parallelisation is not safe here.
  • accept_host_key / forget_host_key in the API layer. They look like near
    duplicates across targets and build hosts, but the audit strings and the
    validation differ deliberately, and the target path requires a non-empty
    fingerprint where the build-host path does not. Collapsing them would have to
    pick one behaviour; that is a semantic decision, not a refactor.
  • The targets / build_hosts tables themselves. Kept apart on purpose — it
    is what makes "a build host is never deployed to" structural. Only the host-key
    column family, which is genuinely one concept, was unified.
  • logs::is_problem — unused, but real logic rather than delegation, so it is
    kept and its doc corrected to stop claiming a caller it does not have.

Loa212 added 4 commits July 30, 2026 20:32
targets and build_hosts carry the same six host-key columns with the same
meaning — migration 0007 says so outright. The trust-on-first-use state
machine over them was written twice: row_to_host_key byte-identical in both
stores, four mutation methods each, and the connect-and-record path copied a
third and fourth time into the deploy engine and the build-host check RPC.

Collapse it onto one SshHost enum naming the table:

  - store/host_keys.rs owns row_to_host_key and pin/record_pending/
    clear_pending/forget, once each.
  - set_target_status and set_build_host_status stop branching into two SQL
    statements and use the COALESCE form set_ingress_status already used.
  - Engine::connect and ssh_target_for take either kind of host through one
    SshHostRef borrow, so connect_build_host and ssh_target_for_build_host
    disappear and BuildHosts.Check stops open-coding what Targets.Check
    already delegated.

Also derive the by-id selects from the list selects instead of hand-copying
24 column names, which retires the test that existed only to police the two
copies for drift.
…pers

tests/e2e.rs was 2358 lines covering three unrelated areas — deploying,
building on a build host, and ingress — over one shared container fixture,
with wait_for_async defined in the build-host section and used by the deploy
tests. It becomes tests/e2e/{main,fixture,deploy,build_hosts,ingress}.rs:
still one binary named e2e, so the CI invocation and the Makefile target are
unchanged, and all 14 tests keep their names.

Splitting it also put the file under clippy for the first time (CI lints
without the e2e feature), which surfaced three lints; fixed.

Separately, nudo-format is the canonical vocabulary for unit states, artifact
sources and log levels, but four callers still went through a one-line
wrapper around it. server::units::status_label was dead outside its own
tests and its doc claimed a sharing that no longer happened; cli::units_label
claimed to be a duplicate it no longer was; three of mcp::presentation's four
functions were pure delegation. All deleted in favour of calling nudo_format
directly, and the exhaustive label assertions moved to sit with the logic.
The dashboard opened four gRPC connections to the control plane one after
another, then read two more things out of the database, all in series —
statement order was the only thing saying which of the seven reads depended
on another, and none of the first five did.

Group them: one join for the independent reads, one for the two that need
their results. Same on the deployments list and the build-host pages.

unit_statuses deliberately stays sequential. Each call opens an ssh
connection to the target, so making a dashboard refresh arrive at a
latency-critical box as a burst of simultaneous connections would trade the
property this tool exists to protect for a faster page.
server::logs::priority_label was a one-line call through to
nudo_format::journal_priority_label with no caller outside its own test. Its
assertions move to nudo_format, where the mapping actually lives, and gain
the four severities neither copy covered.

is_problem stays: it is real logic rather than delegation, and the dashboard's
priority_class is a different question (three CSS classes, not a boolean). Its
doc no longer claims a caller it does not have.
@Loa212
Loa212 merged commit 041ae5f into main Jul 31, 2026
3 checks passed
@Loa212
Loa212 deleted the refactor/collapse-target-build-host-duplication branch July 31, 2026 10:45
Loa212 added a commit that referenced this pull request Jul 31, 2026
Comment-only. #15 justified batching the dashboard's independent reads with
each one opening its own connection to the control plane; #16 made that false
by giving all three clients one held, multiplexed channel. The batching is
still right — sharing a connection is what makes asking at once cheap rather
than contended.
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