a73x

internal/agent/run/wire_darwin.go

Ref:   Size: 4.5 KiB   History

//go:build darwin

package run

import (
	"context"
	"fmt"
	"os"
	"path/filepath"

	"github.com/a73x/eitri/internal/agent/reconcile"
	"github.com/a73x/eitri/internal/agent/serialpump"
	"github.com/a73x/eitri/internal/agent/state"
	"github.com/a73x/eitri/internal/agent/vfkit"
)

// vfkit.Provisioner is what serve() drives through the reconcile.Provisioner
// seam. Pin the method-set match at compile time: a drift on either side (a
// renamed Boot, a changed Capabilities signature) fails the build here rather
// than silently at the wiring call below.
var _ reconcile.Provisioner = (*vfkit.Provisioner)(nil)

// platformProvisioner is what this host advertises to the server at join
// time — an opaque label the server stores and never interprets.
const platformProvisioner = "vfkit"

// proposeGuestCIDR is what this platform offers the fleet at enrollment. A Mac
// proposes an explicit NOTHING rather than no opinion: vmnet owns the guest
// network, so this host has neither a subnet of its own to state nor any use
// for one the fleet would allocate. It reports what it observes later instead.
func proposeGuestCIDR(Config) *string { none := ""; return &none }

// defaultStateDir is where a Mac agent keeps its state when nothing says
// otherwise: a dotdir in the running account's home, which is what the tools a
// Mac user already runs do (colima keeps ~/.colima, lima ~/.lima). Not
// /var/lib/eitri-agent, which is a Linux path an unprivileged account cannot
// create; and not ~/Library/Application Support, Apple's convention for
// application data, for a measured reason: vfkit's control socket lives under
// the state directory, macOS caps a unix socket path at 103 usable bytes, and
//
//	/Users/<user>/Library/Application Support/eitri-agent/vms/<32-hex>/vfkit.sock
//
// is exactly 103 for an eight-character username. It would work for the author
// of this line and fail for the next person. The dotdir leaves 27 bytes of
// headroom, and Preflight still refuses outright rather than failing later if a
// deeper --state-dir spends them.
//
// An unresolvable home is not worth guessing at: the caller sees the error and
// the operator passes --state-dir.
func defaultStateDir() (string, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return "", fmt.Errorf("locate home directory for the default --state-dir: %w", err)
	}
	return filepath.Join(home, ".eitri", "agent"), nil
}

// platform is this host's backend pair, mirroring wire_linux.go's contract:
// serve() is the sole consumer, wiring Prov and Pumps into the reconcile engine
// and the sync client.
type platform struct {
	Prov  reconcile.Provisioner
	Pumps *serialpump.Manager
	// GuestCIDR reports the subnet this host's guests are on, asked once per
	// report rather than resolved at startup. See the sync client's field.
	GuestCIDR func() string
	// HostNetworks is always empty here, and the field exists only because
	// serve() is platform-neutral. Attaching a guest to a named bridge is a
	// Linux tap on a Linux bridge; vmnet gives a Mac neither, so this host
	// advertises none and every create asking for one is refused above it.
	HostNetworks []string
}

// newPlatform builds the macOS backend: vfkit over Apple's
// Virtualization.framework, with the guest console on the PTY vfkit hands out.
//
// It does nothing before returning, and the two things it does not do are the
// whole difference from Linux. There is no host networking to set up: vmnet's
// NAT and its bootpd are already running, own the subnet, and hand out the
// addresses — so this host observes its guest network rather than building one.
// And there is no runtime to bootstrap: cloud-hypervisor is a binary the agent
// can fetch and install, while vfkit works only if it carries Apple's
// virtualization entitlement, and an entitlement lives in a signature we cannot
// produce. So a Mac without vfkit is not one the agent can fix at startup — it
// is one whose provisioner refuses at Preflight, in a sentence naming the
// install.
func newPlatform(_ context.Context, cfg Config, st *state.Store) (platform, error) {
	prov := vfkit.New(st, cfg.VfkitBin, hostRunner)
	pumps := serialpump.NewManager(vfkit.NewConsoleSource(prov.SocketPath), st.SerialLogPath)
	prov.Pumps = pumps

	// Asked once per report. One file read and an interface walk — no
	// subprocess, no privileges, and nothing worth caching over a ten-second
	// poll, where a cache would reintroduce exactly the staleness polling exists
	// to avoid.
	return platform{Prov: prov, Pumps: pumps, GuestCIDR: vfkit.GuestCIDR}, nil
}