v0.19.0: bootstrap contract v2 — relay hub retrieval passphrase (no host key in guest)
Lockstep with felhom-controller v0.40.0. The agent now bakes a v2 bootstrap.json
carrying only what the controller needs to PULL its config from the hub:
customer.id + hub.url + hub.retrieval_password + the per-guest local_api. Stops
baking the agent's host hub key/host_id (and customer name/domain/email) into the
guest — the controller gets the customer-scoped key from the hub pull.
- internal/provision/doc.go: SchemaV2; DocCustomer{id}; DocHub{url,retrieval_password}.
- backhalf.go: render v2; require customer.id + hub.url + hub.retrieval_password.
- cmd/.../main.go --selftest=provision: new required -hub-password flag; stop
baking APIKey/HostID; -customer-domain/name/email accepted but not baked.
- configs/build-golden.sh: default CONTROLLER_IMAGE off stale :v0.35.0 -> :0.40.0.
- doc_test.go/backhalf_test.go updated to v2 shape.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+25
-21
@@ -42,7 +42,7 @@ import (
|
||||
|
||||
// version is the agent version. Overridable at build time with
|
||||
// -ldflags "-X main.version=<v>"; defaults to the in-repo CHANGELOG version.
|
||||
var version = "0.18.0"
|
||||
var version = "0.19.0"
|
||||
|
||||
func main() {
|
||||
var (
|
||||
@@ -58,10 +58,11 @@ func main() {
|
||||
paperkey bool
|
||||
offline bool
|
||||
upload bool
|
||||
custID string
|
||||
custDomain string
|
||||
custName string
|
||||
custEmail string
|
||||
custID string
|
||||
custDomain string
|
||||
custName string
|
||||
custEmail string
|
||||
hubPassword string
|
||||
blobPath string
|
||||
expectedFP string
|
||||
keyDest string
|
||||
@@ -70,7 +71,7 @@ func main() {
|
||||
showVersion bool
|
||||
)
|
||||
flag.StringVar(&cfgPath, "config", envOr("FELHOM_AGENT_CONFIG", "/etc/felhom-agent/agent.json"), "path to the agent config file (JSON)")
|
||||
flag.Var(&selftest, "selftest", "run a self-test and exit: bare/`read` = read-only queries; `task` = reversible mutating exercise (needs -vmid); `hub` = one collect+report; `storage` = observe storage (+ -watch); `backup` = one-shot backup of -vmid; `restore-test` = restore→boot→verify→teardown of -archive (or newest backup); `pbs-verify` = trigger a PBS verify + print snapshot records; `bring-up` = restore→reset identity→size→start link-up of -archive into -vmid (needs -mode/-archive/-vmid; tears down unless -keep); `provision` = full slice-8A chain: bring-up provision + mint token + populate bootstrap config mount (needs -archive/-vmid/-customer-id/-customer-domain; keeps the guest)")
|
||||
flag.Var(&selftest, "selftest", "run a self-test and exit: bare/`read` = read-only queries; `task` = reversible mutating exercise (needs -vmid); `hub` = one collect+report; `storage` = observe storage (+ -watch); `backup` = one-shot backup of -vmid; `restore-test` = restore→boot→verify→teardown of -archive (or newest backup); `pbs-verify` = trigger a PBS verify + print snapshot records; `bring-up` = restore→reset identity→size→start link-up of -archive into -vmid (needs -mode/-archive/-vmid; tears down unless -keep); `provision` = full slice-8A chain: bring-up provision + mint token + populate bootstrap config mount (needs -archive/-vmid/-customer-id/-hub-password; keeps the guest)")
|
||||
flag.IntVar(&vmid, "vmid", 0, "guest VMID for --selftest=task|backup|bring-up")
|
||||
flag.DurationVar(&watch, "watch", 0, "for --selftest=storage: run the watchdog verbose for this duration (e.g. 3m) with the re-mount response live; 0 = observe pass only")
|
||||
flag.StringVar(&archive, "archive", "", "for --selftest=restore-test|bring-up: the backup volid to restore (restore-test: default newest on the local target)")
|
||||
@@ -86,10 +87,11 @@ func main() {
|
||||
flag.StringVar(&keyDest, "keydest", "", "for --selftest=escrow-consume: where to install the recovered key (0600)")
|
||||
flag.StringVar(&idBundlePath, "identity-bundle", "", "for --selftest=escrow-create: a 0600 JSON file {tunnel_token,pbs_token} to ALSO escrow under R (10D)")
|
||||
flag.StringVar(&directivePath, "directive", "", "for --selftest=escrow-create: a JSON file with the non-secret DR directive (pbs repo/ns, expected fingerprint, tunnel id)")
|
||||
flag.StringVar(&custID, "customer-id", "", "for --selftest=provision: the customer id to seed into the guest's bootstrap")
|
||||
flag.StringVar(&custDomain, "customer-domain", "", "for --selftest=provision: the customer domain to seed")
|
||||
flag.StringVar(&custName, "customer-name", "", "for --selftest=provision: the customer display name to seed (optional)")
|
||||
flag.StringVar(&custEmail, "customer-email", "", "for --selftest=provision: the customer email to seed (optional)")
|
||||
flag.StringVar(&custID, "customer-id", "", "for --selftest=provision: the customer id — the hub config-pull target, baked into the guest's bootstrap")
|
||||
flag.StringVar(&hubPassword, "hub-password", "", "for --selftest=provision: the customer's hub RETRIEVAL PASSPHRASE (SECRET) — baked into bootstrap.json so the controller pulls its config (and the customer-scoped hub key) from the hub. The customer must already exist in the hub.")
|
||||
flag.StringVar(&custDomain, "customer-domain", "", "for --selftest=provision: customer domain (accepted; used by bring-up only — NOT baked into v2 bootstrap, the hub provides it)")
|
||||
flag.StringVar(&custName, "customer-name", "", "for --selftest=provision: customer display name (accepted; NOT baked into v2 bootstrap)")
|
||||
flag.StringVar(&custEmail, "customer-email", "", "for --selftest=provision: customer email (accepted; NOT baked into v2 bootstrap)")
|
||||
flag.BoolVar(&showVersion, "version", false, "print version and exit")
|
||||
flag.Parse()
|
||||
|
||||
@@ -132,7 +134,7 @@ func main() {
|
||||
case "provision":
|
||||
os.Exit(runSelftestProvision(context.Background(), cfg, logger, provisionArgs{
|
||||
archive: archive, vmid: vmid, hostname: hostname,
|
||||
customer: provision.DocCustomer{ID: custID, Domain: custDomain, Name: custName, Email: custEmail},
|
||||
customerID: custID, hubPassword: hubPassword,
|
||||
}))
|
||||
case "escrow-create":
|
||||
os.Exit(runSelftestEscrowCreate(context.Background(), cfg, logger, pbsStorage, paperkey, offline, upload, idBundlePath, directivePath))
|
||||
@@ -957,10 +959,11 @@ func runSelftestBringUp(ctx context.Context, cfg config.Config, logger *slog.Log
|
||||
|
||||
// provisionArgs bundles the --selftest=provision inputs.
|
||||
type provisionArgs struct {
|
||||
archive string
|
||||
vmid int
|
||||
hostname string
|
||||
customer provision.DocCustomer
|
||||
archive string
|
||||
vmid int
|
||||
hostname string
|
||||
customerID string // baked into bootstrap (the hub config-pull target)
|
||||
hubPassword string // the customer's hub retrieval passphrase (SECRET) — baked into bootstrap
|
||||
}
|
||||
|
||||
// runSelftestProvision runs the FULL slice-8A provisioning chain on-demand: the slice-7 bring-up
|
||||
@@ -982,8 +985,8 @@ func runSelftestProvision(ctx context.Context, cfg config.Config, logger *slog.L
|
||||
fmt.Fprintln(os.Stderr, "selftest=provision requires backup.restore_storage in config")
|
||||
return 2
|
||||
}
|
||||
if a.customer.ID == "" || a.customer.Domain == "" {
|
||||
fmt.Fprintln(os.Stderr, "selftest=provision requires -customer-id and -customer-domain (so the controller skips setup)")
|
||||
if a.customerID == "" || a.hubPassword == "" {
|
||||
fmt.Fprintln(os.Stderr, "selftest=provision requires -customer-id and -hub-password (the customer's hub retrieval passphrase, so the controller pulls its config)")
|
||||
return 2
|
||||
}
|
||||
if err := cfg.LocalAPI.Validate(); err != nil || !cfg.LocalAPI.Enabled() {
|
||||
@@ -1030,7 +1033,7 @@ func runSelftestProvision(ctx context.Context, cfg config.Config, logger *slog.L
|
||||
API: px, Queue: queue, Journal: journal, Gate: gate, HostID: cfg.Hub.HostID, Logger: logger,
|
||||
})
|
||||
|
||||
fmt.Printf("=== felhom-agent %s selftest=provision (vmid=%d customer=%s) ===\n", version, a.vmid, a.customer.ID)
|
||||
fmt.Printf("=== felhom-agent %s selftest=provision (vmid=%d customer=%s) ===\n", version, a.vmid, a.customerID)
|
||||
engine.Recover(ctx)
|
||||
fmt.Printf(" --- front half: bring-up (provision) %s → vmid %d ---\n", a.archive, a.vmid)
|
||||
res := engine.RunBringUp(ctx, reconcile.BringUpSpec{
|
||||
@@ -1053,8 +1056,8 @@ func runSelftestProvision(ctx context.Context, cfg config.Config, logger *slog.L
|
||||
fmt.Println(" --- back half: mint per-guest token + populate bootstrap config mount ---")
|
||||
pres, err := bh.Provision(ctx, provision.Input{
|
||||
VMID: a.vmid,
|
||||
Customer: a.customer,
|
||||
Hub: provision.DocHub{URL: cfg.Hub.URL, APIKey: cfg.Hub.APIKey, HostID: cfg.Hub.HostID},
|
||||
Customer: provision.DocCustomer{ID: a.customerID},
|
||||
Hub: provision.DocHub{URL: cfg.Hub.URL, RetrievalPassword: a.hubPassword},
|
||||
Endpoint: cfg.LocalAPI.ListenAddr,
|
||||
Fingerprint: fingerprint,
|
||||
})
|
||||
@@ -1066,7 +1069,8 @@ func runSelftestProvision(ctx context.Context, cfg config.Config, logger *slog.L
|
||||
pres.MountKey, pres.GuestPath, pres.VMID, pres.HostDir)
|
||||
fmt.Printf(" local-api endpoint %s · leaf fp %s · token: minted (not printed)\n", cfg.LocalAPI.ListenAddr, fingerprint)
|
||||
fmt.Printf("=== selftest=provision OK — guest %d provisioned + bootstrap-mounted (KEPT) ===\n", a.vmid)
|
||||
fmt.Println(" next: the golden's baked controller-bootstrap unit deploys the controller from the mount on boot.")
|
||||
fmt.Println(" next: reboot the guest → the golden's baked controller-bootstrap unit deploys the controller,")
|
||||
fmt.Println(" which PULLS its controller.yaml from the hub (retrieval passphrase) and merges in this local_api.")
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user