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
}