internal/agent/bootstrap/bootstrap.go
Ref: Size: 7.5 KiB History
// Package bootstrap installs the agent's runtime — the cloud-hypervisor
// binary and its UEFI guest firmware (CLOUDHV.fd) — the first time it is
// missing on a host, by fetching sha-pinned artifacts from the eitri.sh
// release manifest (internal/relmanifest). It runs once at agent startup,
// before the first reconcile.
//
// Existing fleets and airgapped hosts never touch the network: if both files
// are already present, Ensure returns immediately with zero HTTP requests.
// An empty manifest URL disables bootstrap entirely — the operator manages
// the runtime by hand.
//
// Ensure is idempotent per file: a file that already exists is never
// touched. There is no version comparison — upgrading an already-installed
// runtime is a separate concern from bootstrapping a missing one.
package bootstrap
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"path/filepath"
"runtime"
"time"
"github.com/a73x/eitri/internal/relmanifest"
)
// manifestMaxBytes bounds the manifest fetch: it is a small, hand-authored
// JSON document, so anything vastly larger indicates a misconfigured or
// hostile URL rather than a legitimate manifest.
const manifestMaxBytes = 1 << 20
// Bootstrapper installs cloud-hypervisor and CLOUDHV.fd if either is missing
// on disk. The zero value works in production against a configured
// ManifestURL; tests inject the seams.
type Bootstrapper struct {
// HTTP is the manifest+artifact download client (nil ⇒ a 5-minute-timeout
// default, mirroring selfupdate's seam style).
HTTP *http.Client
// Log receives progress and warning lines (nil ⇒ slog.Default()).
Log *slog.Logger
// CHPath and FirmwarePath are the destination paths this agent was
// started with (the same values as its --ch-bin/--firmware flags).
CHPath, FirmwarePath string
// ManifestURL is the eitri.sh release manifest to fetch from. "" disables
// bootstrap entirely: the operator manages the runtime by hand.
ManifestURL string
}
func (b *Bootstrapper) httpClient() *http.Client {
if b.HTTP != nil {
return b.HTTP
}
return &http.Client{Timeout: 5 * time.Minute}
}
func (b *Bootstrapper) log() *slog.Logger {
if b.Log != nil {
return b.Log
}
return slog.Default()
}
// Ensure installs whichever of cloud-hypervisor / CLOUDHV.fd is missing at
// CHPath / FirmwarePath, fetching the manifest from ManifestURL. It is meant
// to run once, before the first reconcile.
func (b *Bootstrapper) Ensure(ctx context.Context) error {
chMissing := !fileExists(b.CHPath)
fwMissing := !fileExists(b.FirmwarePath)
if !chMissing && !fwMissing {
return nil
}
if b.ManifestURL == "" {
b.log().Info("bootstrap: runtime file(s) missing and no manifest URL configured — operator manages cloud-hypervisor/firmware by hand",
"ch_missing", chMissing, "firmware_missing", fwMissing)
return nil
}
man, err := b.fetchManifest(ctx)
if err != nil {
return fmt.Errorf("bootstrap: fetch manifest: %w", err)
}
platform := runtime.GOOS + "/" + runtime.GOARCH
if chMissing {
art, ok := man.Artifacts["cloud-hypervisor"][platform]
if !ok {
return fmt.Errorf("bootstrap: manifest has no cloud-hypervisor artifact for platform %s; the agent cannot run without it", platform)
}
if err := b.install(ctx, art, b.CHPath, 0o755); err != nil {
return fmt.Errorf("bootstrap: install cloud-hypervisor: %w", err)
}
b.log().Info("bootstrap: installed cloud-hypervisor", "path", b.CHPath, "version", man.Version)
}
if fwMissing {
art, ok := man.Artifacts["firmware"][platform]
if !ok {
b.log().Warn("bootstrap: manifest has no firmware artifact for this platform; VM creates will fail until it is installed by hand",
"platform", platform, "path", b.FirmwarePath)
} else if err := b.install(ctx, art, b.FirmwarePath, 0o644); err != nil {
return fmt.Errorf("bootstrap: install firmware: %w", err)
} else {
b.log().Info("bootstrap: installed firmware", "path", b.FirmwarePath, "version", man.Version)
}
}
return nil
}
// fetchManifest GETs and decodes the release manifest.
func (b *Bootstrapper) fetchManifest(ctx context.Context) (relmanifest.Manifest, error) {
var man relmanifest.Manifest
req, err := http.NewRequestWithContext(ctx, http.MethodGet, b.ManifestURL, nil)
if err != nil {
return man, err
}
resp, err := b.httpClient().Do(req)
if err != nil {
return man, fmt.Errorf("GET %s: %w", b.ManifestURL, err)
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
return man, fmt.Errorf("GET %s: HTTP %d", b.ManifestURL, resp.StatusCode)
}
if err := json.NewDecoder(io.LimitReader(resp.Body, manifestMaxBytes)).Decode(&man); err != nil {
return man, fmt.Errorf("decode manifest from %s: %w", b.ManifestURL, err)
}
return man, nil
}
// install downloads art to dest, streaming its sha256 and verifying it
// against art.SHA256 before the file is ever visible at dest. It mirrors
// selfupdate's durable-install idiom: a temp file beside the destination (so
// the final rename is atomic on the same filesystem), fsync before close,
// then an atomic rename plus a best-effort parent-dir fsync.
func (b *Bootstrapper) install(ctx context.Context, art relmanifest.Artifact, dest string, perm os.FileMode) error {
dir := filepath.Dir(dest)
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("create parent dir %s: %w", dir, err)
}
// Sweep temps a crashed prior run left behind; they never block a fresh
// install (dest-existence gates Ensure) but would accumulate.
if stale, err := filepath.Glob(filepath.Join(dir, ".eitri-bootstrap-*")); err == nil {
for _, s := range stale {
os.Remove(s)
}
}
tmp, err := os.CreateTemp(dir, ".eitri-bootstrap-*")
if err != nil {
return fmt.Errorf("create temp beside %s: %w", dest, err)
}
defer os.Remove(tmp.Name()) // no-op after a successful rename
req, err := http.NewRequestWithContext(ctx, http.MethodGet, art.URL, nil)
if err != nil {
tmp.Close()
return err
}
resp, err := b.httpClient().Do(req)
if err != nil {
tmp.Close()
return fmt.Errorf("download %s: %w", art.URL, err)
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
tmp.Close()
return fmt.Errorf("download %s: HTTP %d", art.URL, resp.StatusCode)
}
h := sha256.New()
if _, err := io.Copy(io.MultiWriter(tmp, h), resp.Body); err != nil {
tmp.Close()
return fmt.Errorf("download %s: %w", art.URL, err)
}
// Durability: fsync the downloaded bytes before Close, same rationale as
// selfupdate — a torn install can't heal itself on the next Ensure (the
// file would already "exist" and be skipped).
if err := tmp.Sync(); err != nil {
tmp.Close()
return fmt.Errorf("sync downloaded temp: %w", err)
}
if err := tmp.Close(); err != nil {
return err
}
if got := hex.EncodeToString(h.Sum(nil)); got != art.SHA256 {
return fmt.Errorf("sha256 mismatch for %s: got %s want %s", art.URL, got, art.SHA256)
}
if err := os.Chmod(tmp.Name(), perm); err != nil {
return err
}
if err := os.Rename(tmp.Name(), dest); err != nil {
return fmt.Errorf("install %s: %w", dest, err)
}
// Durability: fsync the directory entry itself — see selfupdate's note on
// why a rename alone is not durable on every filesystem. Best-effort: the
// install already happened either way.
if df, derr := os.Open(dir); derr == nil {
_ = df.Sync()
_ = df.Close()
}
return nil
}
// fileExists reports whether path names an existing file (any stat error,
// including permission errors, is treated as "missing" — Ensure's job is to
// put a working file there, not to diagnose why one isn't visible).
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}