deploy/upcloud/Cargo.toml
# gunnar's **UpCloud re-image**: the live appliance at `gunnar.rs` taken from an
# installer ISO on this disk to a serving box, over the UpCloud API 1.3, in Rust.
#
# ## Why this is a crate and not xtask code
#
# `xtask/Cargo.toml` declares **zero dependencies, deliberately** — it is the
# dependency guard, and a workspace member takes part in resolver-2 feature
# unification, so a dependency there could switch a feature on inside the
# appliance build. This work needs HTTPS and JSON. Both cannot live in xtask.
#
# Three options were weighed before this one was taken:
#
# a. **A normal crate that xtask RUNS.** What this is. `xtask/src/reinstall.rs`
# spawns `cargo run --manifest-path deploy/upcloud/Cargo.toml`, exactly the
# way `cargo xtask install-test` reaches `installtest/` and `cargo xtask
# iso-installer` reaches dwarves. Spawning cargo is not the shell-out the
# rule forbids — cargo is the build tool, and the door it opens is a Rust
# `main`, not a script. `isoinstaller.rs`'s header carries that argument.
# b. **Hand-roll HTTP and JSON inside xtask.** Rejected: it means a TLS stack in
# a crate whose whole purpose is to have no dependencies, and a hand-rolled
# TLS client is the one thing in this procedure that MUST not be homemade.
# Without TLS there is no talking to `api.upcloud.com` at all.
# c. **Put it in an existing crate.** Rejected: every `crates/*` member is in
# the appliance's resolved graph, so `ureq` + `rustls` + `serde_json` would
# be measured by `cargo xtask guard` as new appliance cost for a tool the
# appliance never runs. A detached workspace costs the graph nothing.
#
# `[workspace]` below makes this a workspace ROOT, so the parent's resolution and
# the parent `Cargo.lock` are untouched — the same mechanism `installtest/` and
# `<ops>/sitetest/` use, and for the same reason.
#
# ## The dependencies, and why each one
#
# * `upcloud-api` (edda) — **the constellation's ONE UpCloud surface**, since
# 2026-09-21. This crate carried its own `ureq` transport, its own URL
# builder and a hardcoded `api.upcloud.com` with no override, so a
# `private-gunnar-ops` `mock reimage --apply` chained into this binary and
# would have re-imaged the LIVE appliance while its banner said FAKE. Now
# every call goes through the trait, over ONE wire that builds the request
# for the account and for a `mock-upcloud` by the same code, and the cloud is
# chosen once in `main` from `--mock-api`. `ureq` (3, rustls) arrives through
# it; there is no second HTTP client here.
# * `serde_json` — the API speaks JSON and nothing else. Used through `Value`
# rather than derived structs: the bodies are five keys wide, and `Value` is
# what lets the scripted world in `tests/` hand the procedure a canned reply
# without a parallel set of wire types to keep in step with the real ones.
# * `nornir-hash` — `sha256_file`, streaming, 64 KiB at a time. The upload
# verification of step 3 is the one gate that cannot be skipped, and the
# constellation already owns exactly one SHA-256 primitive (edda). A local
# `Sha256::new(); hex(...)` here would be the seventh copy that leaf was
# written to delete.
[package]
name = "gunnar-upcloud"
description = "Re-image the live gunnar appliance on UpCloud from an installer ISO, over API 1.3, in pure Rust. Driven by `cargo xtask reinstall-appliance`."
version = "0.0.0"
edition = "2021"
publish = false
# MEASURED 2026-09-13: the moment `src/bin/vnc-capture.rs` gave this crate a
# SECOND binary, `cargo xtask reinstall-appliance` stopped working — it spawns
# `cargo run --release --manifest-path deploy/upcloud/Cargo.toml --` (see
# `xtask/src/reinstall.rs`), and cargo answered `could not determine which
# binary to run` and exited 101. The re-image door was dead, for a diagnostic
# tool that was only ever meant to watch. The verb has no `--bin` to give, so
# the manifest has to say which binary IS the crate: the procedure, never the
# observer.
default-run = "gunnar-upcloud"
[workspace]
[dependencies]
upcloud-api = { version = "0.1.5" }
serde_json = "1"
# ── THE FOUR edda LEAVES BELOW ARE crates.io VERSIONS, NOT SIBLING PATHS ─────
#
# They were `path = "../../../edda/crates/…"` until 2026-09-12, and the reason
# each one gave for it was the same sentence: *"this deploy door already
# REQUIRES the sibling `edda` checkout, so a second registry spelling would buy
# nothing."* That premise stopped being true one file at a time —
# `installtest/` deleted its `[patch.crates-io]` on 2026-09-10 when
# `korp-installer 0.1.2` published, `bench/` took its last sibling path to a
# version on 2026-09-09, and the root workspace took the six gunnar leaves to
# versions the same day. This manifest was the LAST tree in the repository
# requiring `../edda` on disk, and a requirement held by exactly one detached
# workspace is not a shared premise, it is a straggler.
#
# `cargo metadata` reads a path dependency's manifest at RESOLVE time, so while
# these four lines said `path`, `cargo xtask deploy-check` — and anything else
# that so much as asks this tree what it contains — was a hard error in a clone
# with nothing beside it. `xtask/tests/deploy_check.rs` is the guard.
#
# All four are published and the requirement here is the version this door was
# written against; cargo takes the newest compatible one.
nornir-hash = { version = "0.1.0" }
# `expand_home` — the ONE implementation, re-exported by `src/site.rs`. It was a
# copy there until 2026-09-10, one of FOUR in this repository that
# `nornir mimir analyze duplicates` clustered.
nornir-xtask-support = { version = "0.1.1" }
# * `nornir-build-thing` — THE ROSTER, and the reason this dependency is not
# optional. `target::presets` is the one table of named machines in the
# constellation, and each row says how its platform takes a medium
# (`MediumBring`). `src/machine.rs` derives this door's attach strategy from
# that word instead of remembering it: `upcloud` is `IsoCdrom`, so the ISO
# rides twice; `scaleway-instance` is `ImportQcow2`, so this verb refuses BY
# NAME rather than attempting an ISO attach a platform has no door for.
#
# Before this line the pair of attaches was a constant in `plan.rs` — the
# SIXTH copy of a fact draupnir, installtest, mother_kvm, this crate's header
# and the roster itself already carried, and the only one that was
# load-bearing. Reusing the declaration instead of restating it is the LAW
# ("reuse the nordisk repos"), and here it is also the bug fix.
#
# `default-features = false`: the default feature set is already dep-light,
# but naming it keeps `plugin-host`'s wasmtime out of this tree by
# declaration rather than by luck. A registry version, like `nornir-hash`
# above: `MediumBring` and `target::presets` are both in the published
# 0.1.17, and `Cargo.lock` beside this file resolves exactly that.
#
# **ONE version of the build system per repository**, and this line is the
# one that got away. efb6c29 took the root workspace, `installtest/` and this
# manifest to `0.1.17` together — but it committed the root and `installtest/`
# LOCKS and not this detached workspace's, leaving a lock pinned at `0.1.16`
# under a requirement of `^0.1.17`. An unsatisfiable lock is not a lock:
# cargo re-resolved on every `cargo xtask reimage`, took the newest compatible
# release (0.1.18 by 2026-09-17) and WROTE it here, and the next re-image
# refused with `[gunnar-dirty]` because the tree was modified. Three hand
# reverts on 2026-09-17 each restored the unsatisfiable state and bought one
# run. `xtask/src/reinstall.rs` now spawns with `--locked` so a resolve that
# would move this file is an error instead of a write, and
# `xtask/tests/lock_current.rs` keeps the file current so that flag never
# turns into a dead door.
#
# If this version moves, the root `Cargo.toml`'s workspace pin, `installtest/`
# and this line move TOGETHER, and every affected `Cargo.lock` goes in the
# SAME commit. Two versions of `nornir-build-thing` in one repository is how a
# `MediumBring` variant lands here that the appliance build never saw.
nornir-build-thing = { version = "0.2.6", default-features = false }
# * `nornir-git`, `default-features = false` — **THE SSH TRANSPORT**, for step 11:
# restarting the front's three services after the appliance underneath them was
# replaced. `nornir_git::ssh::connect_exec_with` is the russh dialler
# `gunnar-client`'s send-pack uses every day, with the connect / authenticate /
# `exec` / blocking-I/O-bridge / in-runtime teardown already written and tested
# against a real russh server on loopback. That teardown is not trivia: getting
# its `Drop` wrong panics with "there is no reactor running", a bug that repo
# found and fixed once.
#
# The alternative was spawning `ssh`, and that is the one dependency decision
# in this file that is a LAW question rather than a taste one. `cargo` is
# spawned by `xtask` because it is a BUILD TOOL opening a door onto a Rust
# `main`; `ssh` is not a build tool, it IS the work — transport,
# authentication and remote invocation are the whole of what step 11 does, and
# doing that in another program is exactly what "pure Rust, no shelling"
# refuses. `ssh2`/libssh2 would be FFI to C while a Rust library exists.
#
# `default-features = false` drops `porcelain`, i.e. gix and its ~57 `gix-*`
# crates plus the reqwest/rustls/icu tail, leaving russh + ssh-key + tokio: the
# transport half and nothing else. This is a DETACHED workspace, so none of it
# reaches the appliance's resolved graph or the parent `Cargo.lock`.
#
# `monetize-impl/deploy/front/src/remote.rs` made the same call the same night
# for the same box. What crosses the fence is this library, not that crate: a
# path dependency from the OPEN product into a private repository would make
# every gunnar build need a checkout nobody outside Vetra has. `src/front.rs`
# names the twin.
nornir-git = { version = "0.2.4", default-features = false }
# * `russh` + `tokio` — **the host key in the receipt**, and neither line adds a
# crate to this tree. `nornir-git` above already resolves both (its transport
# IS russh on tokio; `deploy/upcloud/Cargo.lock` has carried russh 0.61.2 until OSSF-NEXT (2026-10-30) moved it to the root's nornir-git 0.2 and russh 0.63,
# ssh-key 0.7.0-rc.10 and tokio 1.53.1 since that dependency landed). What
# these two lines buy is the right to NAME them: `src/banner.rs` opens one
# key exchange against the re-imaged box and records the host key it
# presented, which `nornir-git`'s API cannot hand back — its `check_server_key`
# prints a fingerprint and keeps it (`ssh.rs:353`).
#
# The feature sets are deliberately a SUBSET of what `nornir-git` already
# asks for ("macros", "rt-multi-thread", "io-std", "io-util", "sync", "fs",
# "time"), so resolver-2 unification changes nothing: no new crate, no new
# feature, no movement in the lock beyond this crate's own dependency list.
#
# Why it is worth a dependency line at all: `known_hosts` records no
# provenance. A key taken off the wire by `ssh-keyscan` and a key read off an
# install console are the same bytes once written, so the only thing that
# makes a forge push mean anything is that the fingerprint came from a record
# written BY the install. This crate IS that record's author.
russh = { version = "0.65" }
tokio = { version = "1", features = ["rt-multi-thread", "time"] }
[dev-dependencies]
# **UpCloud, for the order proofs** — the published fake of API 1.3, its
# MEASURED behaviours included (`BEHAVIOURS-LEDGER.md`). The flow tests ran
# on a scripted world this crate wrote until 2026-09-21; the owner's order was
# to run them on the mock. `test-inject` is its test-only door for a refusal
# the provider CAN give but no behaviour produces on demand; it claims nothing
# about UpCloud. A dev-dependency only: nothing of it reaches the binary.
mock-upcloud = { version = "0.1.3", features = ["test-inject"] }
# `tokio/net` for the TESTS only. `src/banner.rs`'s host-key proof starts a real
# russh server on loopback and reads its key over a real key exchange, which
# needs a listener; the binary itself only ever dials, so `net` is not widened
# on the normal dependency above. russh already pulls `net` into the graph on
# every non-wasm target, so this adds no crate and no build — it is the
# DECLARATION that this crate's tests use it.
tokio = { version = "1", features = ["net"] }
[lib]
name = "gunnar_upcloud"
path = "src/lib.rs"
[[bin]]
name = "gunnar-upcloud"
path = "src/main.rs"
# A SECOND bin, not a verb on the first. `gunnar-upcloud` is `reinstall`: a
# destructive procedure gated behind `--confirm-destroy`, whose parser refuses
# unknown flags precisely so nothing is dropped silently on a run that wipes a
# disk. Capturing the console is READ-ONLY observation and must stay reachable
# without touching that door — the day it was written (2026-09-13) the standing
# instruction on the lane was "observe only".
#
# `src/vnc.rs` behind it adds NO dependency: it asks the server for the Raw
# encoding and nothing else, which makes the wire format a rectangle of pixels,
# and it writes PNG with stored deflate blocks. See that module's header.
[[bin]]
name = "gunnar-vnc-capture"
path = "src/bin/vnc-capture.rs"
[lints.rust]
unsafe_code = "forbid"