f984021a
fix(web): the join flow hands you the bundle your host actually takes
a73x 2026-08-11 15:15
Commit message
docs/quickstart.md
| Old | New | ||
|---|---|---|---|
| @@ -30,6 +30,10 @@ tar xzf "eitri-server_${V}_linux_amd64.tar.gz" && cd "eitri-server_${V}_linux_am | |||
| 30 | `/dl/latest/` is the current release. Its filenames carry the version, so | 30 | `/dl/latest/` is the current release. Its filenames carry the version, so |
| 31 | `SHA256SUMS`—which you need to verify anyway—is where you read it from. | 31 | `SHA256SUMS`—which you need to verify anyway—is where you read it from. |
| 32 | 32 | ||
| 33 | The Linux host bundle is named for the `eitri-server` it also carries: a box | ||
| 34 | that only runs VMs installs the `eitri-agent` inside it and nothing else. On an | ||
| 35 | arm64 host, `_arm64` replaces `_amd64` throughout. | ||
| 36 | |||
| 33 | Then click **+ Add host** in the console and run the command it prints | 37 | Then click **+ Add host** in the console and run the command it prints |
| 34 | from the unpacked bundle—it installs the agent and joins this machine | 38 | from the unpacked bundle—it installs the agent and joins this machine |
| 35 | to your tenant: | 39 | to your tenant: |
| @@ -60,8 +64,20 @@ here: vfkit only works carrying Apple's virtualization entitlement, and an | |||
| 60 | entitlement lives in a code signature. Homebrew's copy is signed. Without it, | 64 | entitlement lives in a code signature. Homebrew's copy is signed. Without it, |
| 61 | VMs placed on this host fail at once, saying so. | 65 | VMs placed on this host fail at once, saying so. |
| 62 | 66 | ||
| 63 | Take the darwin bundle instead of the linux one. Nothing here needs `sudo`, and | 67 | Take the darwin bundle instead of the linux one: |
| 64 | nothing goes in a system directory: | 68 | `eitri-agent_<version>_darwin_arm64.tar.gz`, the agent and its LaunchAgent |
| 69 | installer, no server. macOS ships no `sha256sum`, so `shasum` checks the one | ||
| 70 | line of `SHA256SUMS` that names it: | ||
| 71 | |||
| 72 | ```sh | ||
| 73 | curl -fsSLO https://eitri.sh/dl/latest/SHA256SUMS | ||
| 74 | V=$(sed -n 's/.*eitri-agent_\(v[^_]*\)_darwin_arm64\.tar\.gz$/\1/p' SHA256SUMS) | ||
| 75 | curl -fsSLO "https://eitri.sh/dl/latest/eitri-agent_${V}_darwin_arm64.tar.gz" | ||
| 76 | grep " eitri-agent_${V}_darwin_arm64.tar.gz$" SHA256SUMS | shasum -a 256 -c - | ||
| 77 | tar xzf "eitri-agent_${V}_darwin_arm64.tar.gz" && cd "eitri-agent_${V}_darwin_arm64" | ||
| 78 | ``` | ||
| 79 | |||
| 80 | Nothing here needs `sudo`, and nothing goes in a system directory: | ||
| 65 | 81 | ||
| 66 | ```sh | 82 | ```sh |
| 67 | mkdir -p ~/.local/bin && cp eitri-agent ~/.local/bin/ | 83 | mkdir -p ~/.local/bin && cp eitri-agent ~/.local/bin/ |
| @@ -213,9 +229,12 @@ guest firmware itself on first start, sha-verified against the release. To | |||
| 213 | manage them by hand instead, disable it in `/etc/default/eitri-agent`: | 229 | manage them by hand instead, disable it in `/etc/default/eitri-agent`: |
| 214 | `EITRI_AGENT_FLAGS="--bootstrap-url="`. | 230 | `EITRI_AGENT_FLAGS="--bootstrap-url="`. |
| 215 | 231 | ||
| 216 | Tarballs live at <https://eitri.sh/dl/latest/>. The host bundle | 232 | Tarballs live at <https://eitri.sh/dl/latest/>. The Linux host bundle |
| 217 | (`eitri-server_<version>_linux_amd64.tar.gz`) has `eitri-server`, `eitri-agent`, and | 233 | (`eitri-server_<version>_linux_amd64.tar.gz`) has `eitri-server`, `eitri-agent`, and |
| 218 | their systemd units. The issuer bundle | 234 | their systemd units—a box that only runs VMs takes the same tarball and |
| 235 | installs just the agent from it. A Mac host has a bundle of its own | ||
| 236 | (`eitri-agent_<version>_darwin_arm64.tar.gz`): the agent and its LaunchAgent | ||
| 237 | installer, no server. The issuer bundle | ||
| 219 | (`eitri-oidc_<version>_linux_amd64.tar.gz`) has `eitri-oidc`—the bundled | 238 | (`eitri-oidc_<version>_linux_amd64.tar.gz`) has `eitri-oidc`—the bundled |
| 220 | sign-in provider—and its unit. The client bundle | 239 | sign-in provider—and its unit. The client bundle |
| 221 | (`eitri-cli_<version>_<os>_<arch>.tar.gz`) is the single `eitri` binary for | 240 | (`eitri-cli_<version>_<os>_<arch>.tar.gz`) is the single `eitri` binary for |
| @@ -391,8 +410,9 @@ sudo systemctl enable --now eitri-agent | |||
| 391 | The host goes **online** in the console. Logs: | 410 | The host goes **online** in the console. Logs: |
| 392 | `journalctl -u eitri-agent -f`. | 411 | `journalctl -u eitri-agent -f`. |
| 393 | 412 | ||
| 394 | A Mac joins the same fleet from the `darwin_arm64` bundle—`brew install vfkit` | 413 | A Mac joins the same fleet from its own bundle, |
| 395 | first, then `eitri-agent join`, then `eitri-agent-launchagent.sh install` in | 414 | `eitri-agent_<version>_darwin_arm64.tar.gz`—`brew install vfkit` first, then |
| 415 | `eitri-agent join`, then `eitri-agent-launchagent.sh install` in | ||
| 396 | place of the systemd unit. The differences are the same ones the hosted section covers above, and | 416 | place of the systemd unit. The differences are the same ones the hosted section covers above, and |
| 397 | the one extra thing self-hosting asks of you is an `arm64` entry in the | 417 | the one extra thing self-hosting asks of you is an `arm64` entry in the |
| 398 | server's `default_images`. | 418 | server's `default_images`. |
docs/upgrade.md
| Old | New | ||
|---|---|---|---|
| @@ -63,7 +63,8 @@ keeps, so rollback is the same either way. | |||
| 63 | 63 | ||
| 64 | On a Mac the binary lives in the running account's own space (e.g. | 64 | On a Mac the binary lives in the running account's own space (e.g. |
| 65 | `~/.local/bin/eitri-agent`). Overwrite it with `eitri-agent` from the darwin | 65 | `~/.local/bin/eitri-agent`). Overwrite it with `eitri-agent` from the darwin |
| 66 | bundle, then restart the LaunchAgent: | 66 | bundle (`eitri-agent_<version>_darwin_arm64.tar.gz`), then restart the |
| 67 | LaunchAgent: | ||
| 67 | 68 | ||
| 68 | ```sh | 69 | ```sh |
| 69 | launchctl kickstart -k gui/$(id -u)/sh.eitri.agent | 70 | launchctl kickstart -k gui/$(id -u)/sh.eitri.agent |
web/src/lib/JoinHost.svelte
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,72 @@ | |||
| 1 | <script lang="ts"> | ||
| 2 | // The enrolment recipe for one join token. The token is the same word on | ||
| 3 | // every platform; nothing else about joining is, so the flow asks which | ||
| 4 | // machine this is rather than printing one recipe and hoping — the rules | ||
| 5 | // themselves live in fleet.svelte.ts, next to the release they describe. | ||
| 6 | import { HOST_PLATFORMS, hostBundle, joinCommands, type HostPlatform } from '$lib/fleet.svelte'; | ||
| 7 | |||
| 8 | let { blob, version }: { blob: string; version: string } = $props(); | ||
| 9 | |||
| 10 | // Linux amd64 leads because most hosts are one, and because the server's own | ||
| 11 | // box — the first host of most fleets — is whatever the server runs on. | ||
| 12 | let platform = $state<HostPlatform>('linux/amd64'); | ||
| 13 | const mac = $derived(platform === 'darwin/arm64'); | ||
| 14 | const bundle = $derived(hostBundle(platform, version)); | ||
| 15 | const commands = $derived(joinCommands(platform, version, blob)); | ||
| 16 | </script> | ||
| 17 | |||
| 18 | <div class="join"> | ||
| 19 | <label> | ||
| 20 | Host platform | ||
| 21 | <select bind:value={platform}> | ||
| 22 | {#each HOST_PLATFORMS as p (p.id)} | ||
| 23 | <option value={p.id}>{p.label}</option> | ||
| 24 | {/each} | ||
| 25 | </select> | ||
| 26 | </label> | ||
| 27 | |||
| 28 | <p> | ||
| 29 | Run these on the new host. | ||
| 30 | {#if mac} | ||
| 31 | <code>{bundle.file}</code> is the macOS host bundle: the agent and its LaunchAgent installer, no | ||
| 32 | server. Apple silicon only, and nothing here needs <code>sudo</code>. | ||
| 33 | {:else} | ||
| 34 | <code>{bundle.file}</code> is the Linux host bundle. It is named for the | ||
| 35 | <code>eitri-server</code> it also carries; a box that only runs VMs installs the | ||
| 36 | <code>eitri-agent</code> inside it and nothing else. | ||
| 37 | {/if} | ||
| 38 | </p> | ||
| 39 | |||
| 40 | <div class="enroll"> | ||
| 41 | {#each commands as c (c)} | ||
| 42 | <code>{c}</code> | ||
| 43 | {/each} | ||
| 44 | </div> | ||
| 45 | </div> | ||
| 46 | |||
| 47 | <style> | ||
| 48 | /* Top margin only: this sits under a line of prose in both callers, and the | ||
| 49 | box below it is the last thing in the onboarding card. */ | ||
| 50 | .join { | ||
| 51 | margin-top: 0.5em; | ||
| 52 | } | ||
| 53 | label { | ||
| 54 | display: flex; | ||
| 55 | align-items: center; | ||
| 56 | gap: 0.75em; | ||
| 57 | } | ||
| 58 | .join p { | ||
| 59 | margin: 0.5em 0; | ||
| 60 | } | ||
| 61 | /* One command per line, unwrapped: a join blob is one long word and breaking | ||
| 62 | it mid-token would be a command nobody can paste. */ | ||
| 63 | .enroll { | ||
| 64 | border: 1px solid var(--hairline); | ||
| 65 | padding: 1em; | ||
| 66 | overflow-x: auto; | ||
| 67 | } | ||
| 68 | .enroll code { | ||
| 69 | display: block; | ||
| 70 | word-break: break-all; | ||
| 71 | } | ||
| 72 | </style> | ||
web/src/lib/fleet.svelte.ts
| Old | New | ||
|---|---|---|---|
| @@ -387,6 +387,94 @@ export function vmsForHost(id: string): VM[] { | |||
| 387 | return fleet.vms.filter((v) => v.host_id === id); | 387 | return fleet.vms.filter((v) => v.host_id === id); |
| 388 | } | 388 | } |
| 389 | 389 | ||
| 390 | // Host enrolment — which release artifact a new host takes, and what it runs. | ||
| 391 | |||
| 392 | /** HostPlatform is a machine a host can enrol on, spelled the way | ||
| 393 | * scripts/release.sh spells the artifact for it. darwin/amd64 is absent | ||
| 394 | * because no release carries an agent for it: shipping one would put an | ||
| 395 | * enrolment recipe in front of an Intel Mac that has never booted a guest. */ | ||
| 396 | export type HostPlatform = 'linux/amd64' | 'linux/arm64' | 'darwin/arm64'; | ||
| 397 | |||
| 398 | /** HOST_PLATFORMS is what the join flow offers, in the order it offers it. */ | ||
| 399 | export const HOST_PLATFORMS: { id: HostPlatform; label: string }[] = [ | ||
| 400 | { id: 'linux/amd64', label: 'Linux · amd64' }, | ||
| 401 | { id: 'linux/arm64', label: 'Linux · arm64' }, | ||
| 402 | { id: 'darwin/arm64', label: 'macOS · Apple silicon' } | ||
| 403 | ]; | ||
| 404 | |||
| 405 | /** HostBundle is the one release artifact a host of a given platform enrols | ||
| 406 | * from: the tarball to fetch and the directory it unpacks into. */ | ||
| 407 | export type HostBundle = { file: string; dir: string }; | ||
| 408 | |||
| 409 | /** hostBundle names that artifact. | ||
| 410 | * | ||
| 411 | * A Linux host takes `eitri-server_<v>_linux_<arch>.tar.gz`. The tarball is | ||
| 412 | * named for the server it also carries, and that is the whole of the trap | ||
| 413 | * this names its way out of: the `eitri-agent` inside it is the binary a host | ||
| 414 | * installs, whether or not that box ever runs a server. A Mac takes | ||
| 415 | * `eitri-agent_<v>_darwin_arm64.tar.gz`, which carries no server at all — | ||
| 416 | * a Mac joins a fleet only as a host. | ||
| 417 | * | ||
| 418 | * An empty version is a plane that has not read the release manifest yet. The | ||
| 419 | * names then carry `${V}`, a shell variable the recipe reads out of | ||
| 420 | * SHA256SUMS, because the release's own filenames are the only place the | ||
| 421 | * version is written: `/dl/latest/` is a directory alias, and | ||
| 422 | * `eitri-server_latest_linux_amd64.tar.gz` is a URL that 404s. */ | ||
| 423 | export function hostBundle(platform: HostPlatform, version: string): HostBundle { | ||
| 424 | const arch = platform.split('/')[1]; | ||
| 425 | const v = version || '${V}'; | ||
| 426 | const dir = | ||
| 427 | platform === 'darwin/arm64' | ||
| 428 | ? `eitri-agent_${v}_darwin_arm64` | ||
| 429 | : `eitri-server_${v}_linux_${arch}`; | ||
| 430 | return { file: `${dir}.tar.gz`, dir }; | ||
| 431 | } | ||
| 432 | |||
| 433 | /** joinCommands is the enrolment recipe for one join token on one platform: | ||
| 434 | * fetch, verify, install, join, start. It is the same sequence docs/quickstart | ||
| 435 | * prints, with this fleet's release in it. | ||
| 436 | * | ||
| 437 | * The two platforms differ in more than a filename, which is why the flow asks | ||
| 438 | * rather than guessing. A Linux host installs into system paths under sudo and | ||
| 439 | * runs the agent under systemd; a Mac installs into the running account's own | ||
| 440 | * space with no sudo anywhere, runs it as a LaunchAgent, and needs vfkit from | ||
| 441 | * Homebrew first — the agent bootstraps cloud-hypervisor for itself but cannot | ||
| 442 | * do the same for vfkit, which only works carrying Apple's virtualization | ||
| 443 | * entitlement. macOS also ships no `sha256sum`, so verification there is | ||
| 444 | * `shasum` over the one line of SHA256SUMS that names this bundle. */ | ||
| 445 | export function joinCommands(platform: HostPlatform, version: string, blob: string): string[] { | ||
| 446 | const { file, dir } = hostBundle(platform, version); | ||
| 447 | const base = `https://eitri.sh/dl/${version || 'latest'}`; | ||
| 448 | const mac = platform === 'darwin/arm64'; | ||
| 449 | const stem = mac ? 'eitri-agent' : 'eitri-server'; | ||
| 450 | const suffix = mac ? 'darwin_arm64' : `linux_${platform.split('/')[1]}`; | ||
| 451 | |||
| 452 | const cmds = mac ? ['brew install vfkit'] : []; | ||
| 453 | cmds.push(`curl -fsSLO ${base}/SHA256SUMS`); | ||
| 454 | if (!version) { | ||
| 455 | cmds.push(`V=$(sed -n 's/.*${stem}_\\(v[^_]*\\)_${suffix}\\.tar\\.gz$/\\1/p' SHA256SUMS)`); | ||
| 456 | } | ||
| 457 | cmds.push(`curl -fsSLO "${base}/${file}"`); | ||
| 458 | cmds.push( | ||
| 459 | mac | ||
| 460 | ? `grep " ${file}$" SHA256SUMS | shasum -a 256 -c -` | ||
| 461 | : 'sha256sum -c SHA256SUMS --ignore-missing' | ||
| 462 | ); | ||
| 463 | cmds.push(`tar xzf ${file} && cd ${dir}`); | ||
| 464 | if (mac) { | ||
| 465 | cmds.push('mkdir -p ~/.local/bin && cp eitri-agent ~/.local/bin/'); | ||
| 466 | cmds.push(`~/.local/bin/eitri-agent join ${blob}`); | ||
| 467 | cmds.push('./eitri-agent-launchagent.sh install ~/.local/bin/eitri-agent'); | ||
| 468 | } else { | ||
| 469 | cmds.push('sudo install -m 0755 eitri-agent /usr/local/bin/eitri-agent'); | ||
| 470 | cmds.push('sudo install -m 0644 eitri-agent.service /etc/systemd/system/eitri-agent.service'); | ||
| 471 | cmds.push(`sudo eitri-agent --state-dir /var/lib/eitri-agent join ${blob}`); | ||
| 472 | cmds.push('sudo systemctl daemon-reload'); | ||
| 473 | cmds.push('sudo systemctl enable --now eitri-agent'); | ||
| 474 | } | ||
| 475 | return cmds; | ||
| 476 | } | ||
| 477 | |||
| 390 | // VM display derivations — the canonical "what phase / power / address is this | 478 | // VM display derivations — the canonical "what phase / power / address is this |
| 391 | // VM" rules, shared by every view so they cannot drift. | 479 | // VM" rules, shared by every view so they cannot drift. |
| 392 | 480 | ||
web/src/lib/fleet.test.ts
| Old | New | ||
|---|---|---|---|
| @@ -2,6 +2,8 @@ import { beforeEach, describe, expect, test } from 'vitest'; | |||
| 2 | import { | 2 | import { |
| 3 | capacityReading, | 3 | capacityReading, |
| 4 | fleet, | 4 | fleet, |
| 5 | hostBundle, | ||
| 6 | joinCommands, | ||
| 5 | upgradeAge, | 7 | upgradeAge, |
| 6 | upgradeStuck, | 8 | upgradeStuck, |
| 7 | upgradeStuckNote, | 9 | upgradeStuckNote, |
| @@ -244,3 +246,91 @@ describe('capacityReading', () => { | |||
| 244 | expect(capacityReading(4, 0)).toEqual({ pct: 0, free: 0, over: 0, level: 'ok' }); | 246 | expect(capacityReading(4, 0)).toEqual({ pct: 0, free: 0, over: 0, level: 'ok' }); |
| 245 | }); | 247 | }); |
| 246 | }); | 248 | }); |
| 249 | |||
| 250 | describe('hostBundle', () => { | ||
| 251 | test('a Linux host takes the tarball named for the server it also carries', () => { | ||
| 252 | expect(hostBundle('linux/amd64', 'v0.0.6')).toEqual({ | ||
| 253 | file: 'eitri-server_v0.0.6_linux_amd64.tar.gz', | ||
| 254 | dir: 'eitri-server_v0.0.6_linux_amd64' | ||
| 255 | }); | ||
| 256 | expect(hostBundle('linux/arm64', 'v0.0.6').file).toBe( | ||
| 257 | 'eitri-server_v0.0.6_linux_arm64.tar.gz' | ||
| 258 | ); | ||
| 259 | }); | ||
| 260 | |||
| 261 | test('a Mac takes the agent bundle—no release carries a server for it', () => { | ||
| 262 | expect(hostBundle('darwin/arm64', 'v0.0.6')).toEqual({ | ||
| 263 | file: 'eitri-agent_v0.0.6_darwin_arm64.tar.gz', | ||
| 264 | dir: 'eitri-agent_v0.0.6_darwin_arm64' | ||
| 265 | }); | ||
| 266 | }); | ||
| 267 | |||
| 268 | test('an unknown release leaves the version to the shell, never the word latest', () => { | ||
| 269 | const b = hostBundle('linux/amd64', ''); | ||
| 270 | expect(b.file).toBe('eitri-server_${V}_linux_amd64.tar.gz'); | ||
| 271 | expect(b.file).not.toContain('latest'); | ||
| 272 | }); | ||
| 273 | }); | ||
| 274 | |||
| 275 | describe('joinCommands', () => { | ||
| 276 | const blob = 'eitri_join_abc123'; | ||
| 277 | |||
| 278 | test('a Linux recipe fetches its bundle, verifies it, and starts the unit', () => { | ||
| 279 | const cmds = joinCommands('linux/amd64', 'v0.0.6', blob); | ||
| 280 | expect(cmds).toContain( | ||
| 281 | 'curl -fsSLO "https://eitri.sh/dl/v0.0.6/eitri-server_v0.0.6_linux_amd64.tar.gz"' | ||
| 282 | ); | ||
| 283 | expect(cmds).toContain('sha256sum -c SHA256SUMS --ignore-missing'); | ||
| 284 | expect(cmds).toContain(`sudo eitri-agent --state-dir /var/lib/eitri-agent join ${blob}`); | ||
| 285 | expect(cmds).toContain('sudo systemctl enable --now eitri-agent'); | ||
| 286 | expect(cmds.some((c) => c.includes('SHA256SUMS'))).toBe(true); | ||
| 287 | }); | ||
| 288 | |||
| 289 | test('SHA256SUMS is fetched before the bundle it verifies', () => { | ||
| 290 | const cmds = joinCommands('linux/amd64', 'v0.0.6', blob); | ||
| 291 | expect(cmds.findIndex((c) => c.endsWith('SHA256SUMS'))).toBeLessThan( | ||
| 292 | cmds.findIndex((c) => c.includes('.tar.gz"')) | ||
| 293 | ); | ||
| 294 | }); | ||
| 295 | |||
| 296 | test('a Mac recipe takes the darwin agent bundle and asks for sudo nowhere', () => { | ||
| 297 | const cmds = joinCommands('darwin/arm64', 'v0.0.6', blob); | ||
| 298 | expect(cmds.join('\n')).toContain('eitri-agent_v0.0.6_darwin_arm64.tar.gz'); | ||
| 299 | expect(cmds.join('\n')).not.toContain('eitri-server'); | ||
| 300 | expect(cmds.join('\n')).not.toContain('sudo'); | ||
| 301 | expect(cmds).toContain('brew install vfkit'); | ||
| 302 | expect(cmds).toContain('./eitri-agent-launchagent.sh install ~/.local/bin/eitri-agent'); | ||
| 303 | }); | ||
| 304 | |||
| 305 | test('a Mac verifies with shasum—macOS ships no sha256sum', () => { | ||
| 306 | const cmds = joinCommands('darwin/arm64', 'v0.0.6', blob); | ||
| 307 | expect(cmds).toContain( | ||
| 308 | 'grep " eitri-agent_v0.0.6_darwin_arm64.tar.gz$" SHA256SUMS | shasum -a 256 -c -' | ||
| 309 | ); | ||
| 310 | expect(cmds.join('\n')).not.toContain('sha256sum'); | ||
| 311 | }); | ||
| 312 | |||
| 313 | test('no systemd on a Mac, and no LaunchAgent on Linux', () => { | ||
| 314 | expect(joinCommands('darwin/arm64', 'v0.0.6', blob).join('\n')).not.toContain('systemctl'); | ||
| 315 | expect(joinCommands('linux/amd64', 'v0.0.6', blob).join('\n')).not.toContain('launchagent'); | ||
| 316 | }); | ||
| 317 | |||
| 318 | test('an unknown release reads the version out of SHA256SUMS, per platform', () => { | ||
| 319 | const linux = joinCommands('linux/arm64', '', blob); | ||
| 320 | expect(linux).toContain( | ||
| 321 | "V=$(sed -n 's/.*eitri-server_\\(v[^_]*\\)_linux_arm64\\.tar\\.gz$/\\1/p' SHA256SUMS)" | ||
| 322 | ); | ||
| 323 | expect(linux).toContain('curl -fsSLO https://eitri.sh/dl/latest/SHA256SUMS'); | ||
| 324 | const mac = joinCommands('darwin/arm64', '', blob); | ||
| 325 | expect(mac).toContain( | ||
| 326 | "V=$(sed -n 's/.*eitri-agent_\\(v[^_]*\\)_darwin_arm64\\.tar\\.gz$/\\1/p' SHA256SUMS)" | ||
| 327 | ); | ||
| 328 | }); | ||
| 329 | |||
| 330 | test('a known release is fetched from its own immutable directory', () => { | ||
| 331 | const cmds = joinCommands('linux/amd64', 'v0.0.6', blob).join('\n'); | ||
| 332 | expect(cmds).toContain('https://eitri.sh/dl/v0.0.6/'); | ||
| 333 | expect(cmds).not.toContain('/dl/latest/'); | ||
| 334 | expect(cmds).not.toContain('${V}'); | ||
| 335 | }); | ||
| 336 | }); | ||
web/src/routes/+page.svelte
| Old | New | ||
|---|---|---|---|
| @@ -22,6 +22,7 @@ | |||
| 22 | type CreateVMRequest, | 22 | type CreateVMRequest, |
| 23 | type VM | 23 | type VM |
| 24 | } from '$lib/fleet.svelte'; | 24 | } from '$lib/fleet.svelte'; |
| 25 | import JoinHost from '$lib/JoinHost.svelte'; | ||
| 25 | 26 | ||
| 26 | let showCreate = $state(false); | 27 | let showCreate = $state(false); |
| 27 | let advanced = $state(false); | 28 | let advanced = $state(false); |
| @@ -32,9 +33,6 @@ | |||
| 32 | // Case-insensitive substring match over the row's visible fields, so a | 33 | // Case-insensitive substring match over the row's visible fields, so a |
| 33 | // large fleet can be narrowed live from one box. | 34 | // large fleet can be narrowed live from one box. |
| 34 | const needle = $derived(filter.trim().toLowerCase()); | 35 | const needle = $derived(filter.trim().toLowerCase()); |
| 35 | // The /dl/ path for the printed join instructions: the fleet's known release, | ||
| 36 | // falling back to the stable latest alias before the manifest is fetched. | ||
| 37 | const dlVersion = $derived(fleet.latest_version || 'latest'); | ||
| 38 | const shownHosts = $derived( | 36 | const shownHosts = $derived( |
| 39 | fleet.hosts.filter( | 37 | fleet.hosts.filter( |
| 40 | (h) => | 38 | (h) => |
| @@ -193,41 +191,19 @@ | |||
| 193 | <div class="onboard"> | 191 | <div class="onboard"> |
| 194 | <h3>Add your first host</h3> | 192 | <h3>Add your first host</h3> |
| 195 | <p class="hint"> | 193 | <p class="hint"> |
| 196 | A host is any Linux box that runs your VMs—the server's own box counts. Mint a one-time | 194 | A host is any Linux box or Mac that runs your VMs—the server's own box counts. Mint a |
| 197 | join token, then run the printed command on the box; it comes online here the moment it | 195 | one-time join token, then run the printed command on the box; it comes online here the |
| 198 | enrolls. | 196 | moment it enrolls. |
| 199 | </p> | 197 | </p> |
| 200 | {#if joinBlob} | 198 | {#if joinBlob} |
| 201 | <p>Download and verify the host bundle, then run on the new host:</p> | 199 | <JoinHost blob={joinBlob} version={fleet.latest_version} /> |
| 202 | <div class="enroll"> | ||
| 203 | <code>curl -fsSLO "https://eitri.sh/dl/{dlVersion}/eitri-server_{dlVersion}_linux_amd64.tar.gz"</code> | ||
| 204 | <code>curl -fsSLO "https://eitri.sh/dl/{dlVersion}/SHA256SUMS"</code> | ||
| 205 | <code>sha256sum -c SHA256SUMS --ignore-missing</code> | ||
| 206 | <code>tar xzf eitri-server_{dlVersion}_linux_amd64.tar.gz && cd eitri-server_{dlVersion}_linux_amd64</code> | ||
| 207 | <code>sudo install -m 0755 eitri-agent /usr/local/bin/eitri-agent</code> | ||
| 208 | <code>sudo install -m 0644 eitri-agent.service /etc/systemd/system/eitri-agent.service</code> | ||
| 209 | <code>sudo eitri-agent --state-dir /var/lib/eitri-agent join {joinBlob}</code> | ||
| 210 | <code>sudo systemctl daemon-reload</code> | ||
| 211 | <code>sudo systemctl enable --now eitri-agent</code> | ||
| 212 | </div> | ||
| 213 | {:else} | 200 | {:else} |
| 214 | <button onclick={addHost}>Add your first host</button> | 201 | <button onclick={addHost}>Add your first host</button> |
| 215 | {/if} | 202 | {/if} |
| 216 | </div> | 203 | </div> |
| 217 | {:else if joinBlob || shownHosts.length === 0} | 204 | {:else if joinBlob || shownHosts.length === 0} |
| 218 | {#if joinBlob} | 205 | {#if joinBlob} |
| 219 | <div class="enroll"> | 206 | <JoinHost blob={joinBlob} version={fleet.latest_version} /> |
| 220 | Download and verify the host bundle, then run on the new host: | ||
| 221 | <code>curl -fsSLO "https://eitri.sh/dl/{dlVersion}/eitri-server_{dlVersion}_linux_amd64.tar.gz"</code> | ||
| 222 | <code>curl -fsSLO "https://eitri.sh/dl/{dlVersion}/SHA256SUMS"</code> | ||
| 223 | <code>sha256sum -c SHA256SUMS --ignore-missing</code> | ||
| 224 | <code>tar xzf eitri-server_{dlVersion}_linux_amd64.tar.gz && cd eitri-server_{dlVersion}_linux_amd64</code> | ||
| 225 | <code>sudo install -m 0755 eitri-agent /usr/local/bin/eitri-agent</code> | ||
| 226 | <code>sudo install -m 0644 eitri-agent.service /etc/systemd/system/eitri-agent.service</code> | ||
| 227 | <code>sudo eitri-agent --state-dir /var/lib/eitri-agent join {joinBlob}</code> | ||
| 228 | <code>sudo systemctl daemon-reload</code> | ||
| 229 | <code>sudo systemctl enable --now eitri-agent</code> | ||
| 230 | </div> | ||
| 231 | {/if} | 207 | {/if} |
| 232 | {#if shownHosts.length === 0} | 208 | {#if shownHosts.length === 0} |
| 233 | <p class="hint">No hosts match “{filter}”.</p> | 209 | <p class="hint">No hosts match “{filter}”.</p> |
| @@ -522,16 +498,6 @@ | |||
| 522 | display: flex; | 498 | display: flex; |
| 523 | gap: 0.5em; | 499 | gap: 0.5em; |
| 524 | } | 500 | } |
| 525 | .enroll { | ||
| 526 | border: 1px solid var(--hairline); | ||
| 527 | padding: 1em; | ||
| 528 | margin: 0.5em 0; | ||
| 529 | overflow-x: auto; | ||
| 530 | } | ||
| 531 | .enroll code { | ||
| 532 | display: block; | ||
| 533 | word-break: break-all; | ||
| 534 | } | ||
| 535 | .update-banner { | 501 | .update-banner { |
| 536 | display: inline-block; | 502 | display: inline-block; |
| 537 | border: 1px solid var(--hairline); | 503 | border: 1px solid var(--hairline); |
| @@ -617,7 +583,4 @@ | |||
| 617 | .onboard p { | 583 | .onboard p { |
| 618 | margin-bottom: 0.5em; | 584 | margin-bottom: 0.5em; |
| 619 | } | 585 | } |
| 620 | .onboard .enroll { | ||
| 621 | margin-bottom: 0; | ||
| 622 | } | ||
| 623 | </style> | 586 | </style> |