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:
@@ -3,6 +3,33 @@
|
|||||||
All notable changes to **felhom-agent** are recorded here. Update on every code
|
All notable changes to **felhom-agent** are recorded here. Update on every code
|
||||||
change that gets pushed.
|
change that gets pushed.
|
||||||
|
|
||||||
|
## v0.19.0 — bootstrap contract v2: agent relays the hub retrieval passphrase (no host key in the guest) (2026-06-11)
|
||||||
|
|
||||||
|
Lockstep with `felhom-controller` v0.40.0. Fixes the onboarding 401: a freshly provisioned guest's
|
||||||
|
controller used to come up with the agent's **host** hub key baked in, which the hub's `/api/v1/report`
|
||||||
|
(customer-scoped auth) rejects. The agent now bakes a **v2 bootstrap** carrying only what the controller
|
||||||
|
needs to **pull** its own config from the hub — the agent never touches the customer-scoped key or CF
|
||||||
|
tokens.
|
||||||
|
|
||||||
|
### Changed — bootstrap contract `v1 → v2` (`internal/provision`)
|
||||||
|
- `SchemaV1 → SchemaV2 = "felhom.bootstrap/v2"`. **`DocCustomer`** drops `name`/`domain`/`email` (keeps
|
||||||
|
`id`). **`DocHub`** drops `api_key`/`host_id`, adds **`retrieval_password`** (the customer's hub
|
||||||
|
retrieval passphrase — SECRET). `DocLocalAPI` unchanged. The contract is byte-compatible with the
|
||||||
|
controller's `internal/bootstrap.Bootstrap` (cross-repo round-trip verified).
|
||||||
|
- `backhalf.go`: renders the v2 Doc; validation now requires `customer.id` + `hub.url` +
|
||||||
|
`hub.retrieval_password` (was `customer.id` + `customer.domain`). Write/0600/chown/`pct set` unchanged.
|
||||||
|
- `cmd/felhom-agent/main.go` `--selftest=provision`: **new required `-hub-password`** flag (the customer's
|
||||||
|
hub retrieval passphrase; the customer must already exist in the hub). Stops baking `cfg.Hub.APIKey` /
|
||||||
|
`cfg.Hub.HostID`. `-customer-domain/-name/-email` still accepted (bring-up may use them) but NOT baked.
|
||||||
|
|
||||||
|
### Changed — `configs/build-golden.sh`
|
||||||
|
- Default `CONTROLLER_IMAGE` bumped off the stale `:v0.35.0` → `:0.40.0` (matches the registry's no-`v`
|
||||||
|
tag convention; latent footgun fixed).
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- `doc_test.go`/`backhalf_test.go` updated to the v2 shape (assert no `api_key`/`host_id`,
|
||||||
|
`retrieval_password` present, `customer` carries only `id`). `go build ./... && go test ./...` green.
|
||||||
|
|
||||||
## v0.18.0 — slice 10D: DR capstone — identity escrow + restore-mode consumption (agent side) (2026-06-10)
|
## v0.18.0 — slice 10D: DR capstone — identity escrow + restore-mode consumption (agent side) (2026-06-10)
|
||||||
|
|
||||||
The agent half of the slice-10 DR capstone (closes slice 10). Grounded by both 10-series spikes
|
The agent half of the slice-10 DR capstone (closes slice 10). Grounded by both 10-series spikes
|
||||||
|
|||||||
+25
-21
@@ -42,7 +42,7 @@ import (
|
|||||||
|
|
||||||
// version is the agent version. Overridable at build time with
|
// version is the agent version. Overridable at build time with
|
||||||
// -ldflags "-X main.version=<v>"; defaults to the in-repo CHANGELOG version.
|
// -ldflags "-X main.version=<v>"; defaults to the in-repo CHANGELOG version.
|
||||||
var version = "0.18.0"
|
var version = "0.19.0"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var (
|
var (
|
||||||
@@ -58,10 +58,11 @@ func main() {
|
|||||||
paperkey bool
|
paperkey bool
|
||||||
offline bool
|
offline bool
|
||||||
upload bool
|
upload bool
|
||||||
custID string
|
custID string
|
||||||
custDomain string
|
custDomain string
|
||||||
custName string
|
custName string
|
||||||
custEmail string
|
custEmail string
|
||||||
|
hubPassword string
|
||||||
blobPath string
|
blobPath string
|
||||||
expectedFP string
|
expectedFP string
|
||||||
keyDest string
|
keyDest string
|
||||||
@@ -70,7 +71,7 @@ func main() {
|
|||||||
showVersion bool
|
showVersion bool
|
||||||
)
|
)
|
||||||
flag.StringVar(&cfgPath, "config", envOr("FELHOM_AGENT_CONFIG", "/etc/felhom-agent/agent.json"), "path to the agent config file (JSON)")
|
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.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.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)")
|
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(&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(&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(&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(&custID, "customer-id", "", "for --selftest=provision: the customer id — the hub config-pull target, baked into the guest's bootstrap")
|
||||||
flag.StringVar(&custDomain, "customer-domain", "", "for --selftest=provision: the customer domain to seed")
|
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(&custName, "customer-name", "", "for --selftest=provision: the customer display name to seed (optional)")
|
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(&custEmail, "customer-email", "", "for --selftest=provision: the customer email to seed (optional)")
|
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.BoolVar(&showVersion, "version", false, "print version and exit")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -132,7 +134,7 @@ func main() {
|
|||||||
case "provision":
|
case "provision":
|
||||||
os.Exit(runSelftestProvision(context.Background(), cfg, logger, provisionArgs{
|
os.Exit(runSelftestProvision(context.Background(), cfg, logger, provisionArgs{
|
||||||
archive: archive, vmid: vmid, hostname: hostname,
|
archive: archive, vmid: vmid, hostname: hostname,
|
||||||
customer: provision.DocCustomer{ID: custID, Domain: custDomain, Name: custName, Email: custEmail},
|
customerID: custID, hubPassword: hubPassword,
|
||||||
}))
|
}))
|
||||||
case "escrow-create":
|
case "escrow-create":
|
||||||
os.Exit(runSelftestEscrowCreate(context.Background(), cfg, logger, pbsStorage, paperkey, offline, upload, idBundlePath, directivePath))
|
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.
|
// provisionArgs bundles the --selftest=provision inputs.
|
||||||
type provisionArgs struct {
|
type provisionArgs struct {
|
||||||
archive string
|
archive string
|
||||||
vmid int
|
vmid int
|
||||||
hostname string
|
hostname string
|
||||||
customer provision.DocCustomer
|
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
|
// 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")
|
fmt.Fprintln(os.Stderr, "selftest=provision requires backup.restore_storage in config")
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
if a.customer.ID == "" || a.customer.Domain == "" {
|
if a.customerID == "" || a.hubPassword == "" {
|
||||||
fmt.Fprintln(os.Stderr, "selftest=provision requires -customer-id and -customer-domain (so the controller skips setup)")
|
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
|
return 2
|
||||||
}
|
}
|
||||||
if err := cfg.LocalAPI.Validate(); err != nil || !cfg.LocalAPI.Enabled() {
|
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,
|
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)
|
engine.Recover(ctx)
|
||||||
fmt.Printf(" --- front half: bring-up (provision) %s → vmid %d ---\n", a.archive, a.vmid)
|
fmt.Printf(" --- front half: bring-up (provision) %s → vmid %d ---\n", a.archive, a.vmid)
|
||||||
res := engine.RunBringUp(ctx, reconcile.BringUpSpec{
|
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 ---")
|
fmt.Println(" --- back half: mint per-guest token + populate bootstrap config mount ---")
|
||||||
pres, err := bh.Provision(ctx, provision.Input{
|
pres, err := bh.Provision(ctx, provision.Input{
|
||||||
VMID: a.vmid,
|
VMID: a.vmid,
|
||||||
Customer: a.customer,
|
Customer: provision.DocCustomer{ID: a.customerID},
|
||||||
Hub: provision.DocHub{URL: cfg.Hub.URL, APIKey: cfg.Hub.APIKey, HostID: cfg.Hub.HostID},
|
Hub: provision.DocHub{URL: cfg.Hub.URL, RetrievalPassword: a.hubPassword},
|
||||||
Endpoint: cfg.LocalAPI.ListenAddr,
|
Endpoint: cfg.LocalAPI.ListenAddr,
|
||||||
Fingerprint: fingerprint,
|
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)
|
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(" 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.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
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ TEMPLATE="${2:-local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst}"
|
|||||||
ROOTFS_STORAGE="${3:-local-lvm}"
|
ROOTFS_STORAGE="${3:-local-lvm}"
|
||||||
ARCHIVE_STORAGE="${4:-local}"
|
ARCHIVE_STORAGE="${4:-local}"
|
||||||
BRIDGE="${5:-vmbr0}"
|
BRIDGE="${5:-vmbr0}"
|
||||||
CONTROLLER_IMAGE="${6:-gitea.dooplex.hu/admin/felhom-controller:v0.35.0}"
|
CONTROLLER_IMAGE="${6:-gitea.dooplex.hu/admin/felhom-controller:0.40.0}"
|
||||||
REGISTRY_HOST="${CONTROLLER_IMAGE%%/*}"
|
REGISTRY_HOST="${CONTROLLER_IMAGE%%/*}"
|
||||||
|
|
||||||
echo "[golden] creating build LXC $VMID (nesting=1,keyctl=1, unprivileged) …"
|
echo "[golden] creating build LXC $VMID (nesting=1,keyctl=1, unprivileged) …"
|
||||||
|
|||||||
@@ -87,8 +87,11 @@ func (b *BackHalf) Provision(ctx context.Context, in Input) (Result, error) {
|
|||||||
if in.Endpoint == "" || in.Fingerprint == "" {
|
if in.Endpoint == "" || in.Fingerprint == "" {
|
||||||
return Result{}, fmt.Errorf("provision: needs the local-api endpoint and leaf fingerprint")
|
return Result{}, fmt.Errorf("provision: needs the local-api endpoint and leaf fingerprint")
|
||||||
}
|
}
|
||||||
if in.Customer.ID == "" || in.Customer.Domain == "" {
|
if in.Customer.ID == "" {
|
||||||
return Result{}, fmt.Errorf("provision: needs customer id and domain (so the controller skips setup)")
|
return Result{}, fmt.Errorf("provision: needs a customer id (the hub config-pull target)")
|
||||||
|
}
|
||||||
|
if in.Hub.URL == "" || in.Hub.RetrievalPassword == "" {
|
||||||
|
return Result{}, fmt.Errorf("provision: needs hub url + retrieval passphrase (so the controller can pull its config)")
|
||||||
}
|
}
|
||||||
guestPath := in.GuestPath
|
guestPath := in.GuestPath
|
||||||
if guestPath == "" {
|
if guestPath == "" {
|
||||||
@@ -109,7 +112,7 @@ func (b *BackHalf) Provision(ctx context.Context, in Input) (Result, error) {
|
|||||||
|
|
||||||
// 2. Render the stable bootstrap.json contract (with the token injected).
|
// 2. Render the stable bootstrap.json contract (with the token injected).
|
||||||
doc := Doc{
|
doc := Doc{
|
||||||
Schema: SchemaV1,
|
Schema: SchemaV2,
|
||||||
Customer: in.Customer,
|
Customer: in.Customer,
|
||||||
Hub: in.Hub,
|
Hub: in.Hub,
|
||||||
LocalAPI: DocLocalAPI{Endpoint: in.Endpoint, Fingerprint: in.Fingerprint, Token: tok},
|
LocalAPI: DocLocalAPI{Endpoint: in.Endpoint, Fingerprint: in.Fingerprint, Token: tok},
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ func testLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard,
|
|||||||
func newInput() Input {
|
func newInput() Input {
|
||||||
return Input{
|
return Input{
|
||||||
VMID: 8200,
|
VMID: 8200,
|
||||||
Customer: DocCustomer{ID: "cust-8200", Domain: "cust8200.felhom.eu", Name: "Teszt"},
|
Customer: DocCustomer{ID: "cust-8200"},
|
||||||
Hub: DocHub{URL: "https://hub.felhom.eu", APIKey: "HUBKEY", HostID: "demo-felhom-01"},
|
Hub: DocHub{URL: "https://hub.felhom.eu", RetrievalPassword: "five-word-passphrase"},
|
||||||
Endpoint: "192.168.0.162:8443",
|
Endpoint: "192.168.0.162:8443",
|
||||||
Fingerprint: "ab12cd",
|
Fingerprint: "ab12cd",
|
||||||
}
|
}
|
||||||
@@ -95,9 +95,12 @@ func TestProvision_WritesChownsAndAttaches(t *testing.T) {
|
|||||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||||
t.Fatalf("bootstrap not valid JSON: %v", err)
|
t.Fatalf("bootstrap not valid JSON: %v", err)
|
||||||
}
|
}
|
||||||
if doc.Schema != SchemaV1 || doc.Customer.ID != "cust-8200" || doc.LocalAPI.Token != "SECRET-TOKEN-XYZ" {
|
if doc.Schema != SchemaV2 || doc.Customer.ID != "cust-8200" || doc.LocalAPI.Token != "SECRET-TOKEN-XYZ" {
|
||||||
t.Fatalf("bootstrap content wrong: %+v", doc)
|
t.Fatalf("bootstrap content wrong: %+v", doc)
|
||||||
}
|
}
|
||||||
|
if doc.Hub.URL != "https://hub.felhom.eu" || doc.Hub.RetrievalPassword != "five-word-passphrase" {
|
||||||
|
t.Fatalf("hub wrong (want url + retrieval_password, no host key): %+v", doc.Hub)
|
||||||
|
}
|
||||||
if doc.LocalAPI.Endpoint != "192.168.0.162:8443" || doc.LocalAPI.Fingerprint != "ab12cd" {
|
if doc.LocalAPI.Endpoint != "192.168.0.162:8443" || doc.LocalAPI.Fingerprint != "ab12cd" {
|
||||||
t.Fatalf("local_api wrong: %+v", doc.LocalAPI)
|
t.Fatalf("local_api wrong: %+v", doc.LocalAPI)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-13
@@ -10,14 +10,20 @@ package provision
|
|||||||
|
|
||||||
import "encoding/json"
|
import "encoding/json"
|
||||||
|
|
||||||
// SchemaV1 is the stable agent→controller contract version. It MUST stay byte-compatible with the
|
// SchemaV2 is the stable agent→controller contract version. It MUST stay byte-compatible with the
|
||||||
// controller's internal/bootstrap.SchemaV1 / Bootstrap shape (cross-repo contract; doc_test.go
|
// controller's internal/bootstrap.SchemaV2 / Bootstrap shape (cross-repo contract; doc_test.go
|
||||||
// pins the key set, mirroring the controller's bootstrap_test.go).
|
// pins the key set, mirroring the controller's bootstrap_test.go). v2 changed the contract's
|
||||||
const SchemaV1 = "felhom.bootstrap/v1"
|
// MEANING — the controller now PULLS its full controller.yaml from the hub using a per-customer
|
||||||
|
// retrieval passphrase (which yields the CUSTOMER-scoped hub key), instead of the agent baking its
|
||||||
|
// HOST key. So the agent no longer puts the hub api_key / host id (or customer name/domain/email)
|
||||||
|
// into the guest; it relays only the customer id, the hub URL, the retrieval passphrase, and the
|
||||||
|
// per-guest local-API handle.
|
||||||
|
const SchemaV2 = "felhom.bootstrap/v2"
|
||||||
|
|
||||||
// Doc is the bootstrap.json the agent emits. Field names + json tags MUST match the controller's
|
// Doc is the bootstrap.json the agent emits. Field names + json tags MUST match the controller's
|
||||||
// internal/bootstrap.Bootstrap exactly. It carries ONLY what the controller needs to come up
|
// internal/bootstrap.Bootstrap exactly. It carries ONLY what the controller needs to PULL its config
|
||||||
// configured and reach the agent's local API — no registry credential (image is baked).
|
// (customer id + hub url + retrieval passphrase) and reach the agent's local API — no registry
|
||||||
|
// credential (image is baked), no customer-scoped hub key, no CF tokens (those come from the pull).
|
||||||
type Doc struct {
|
type Doc struct {
|
||||||
Schema string `json:"schema"`
|
Schema string `json:"schema"`
|
||||||
Customer DocCustomer `json:"customer"`
|
Customer DocCustomer `json:"customer"`
|
||||||
@@ -26,16 +32,12 @@ type Doc struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DocCustomer struct {
|
type DocCustomer struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"` // the pull target; the hub provides name/domain/email
|
||||||
Name string `json:"name"`
|
|
||||||
Domain string `json:"domain"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DocHub struct {
|
type DocHub struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
APIKey string `json:"api_key"`
|
RetrievalPassword string `json:"retrieval_password"` // SECRET — pulls the full config (incl. the customer key)
|
||||||
HostID string `json:"host_id"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DocLocalAPI struct {
|
type DocLocalAPI struct {
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
// bootstrap_test.go ingests the same shape. A drift here (or there) breaks provisioning.
|
// bootstrap_test.go ingests the same shape. A drift here (or there) breaks provisioning.
|
||||||
func TestDoc_ContractKeySet(t *testing.T) {
|
func TestDoc_ContractKeySet(t *testing.T) {
|
||||||
d := Doc{
|
d := Doc{
|
||||||
Schema: SchemaV1,
|
Schema: SchemaV2,
|
||||||
Customer: DocCustomer{ID: "c", Name: "n", Domain: "d", Email: "e"},
|
Customer: DocCustomer{ID: "c"},
|
||||||
Hub: DocHub{URL: "u", APIKey: "k", HostID: "h"},
|
Hub: DocHub{URL: "u", RetrievalPassword: "p"},
|
||||||
LocalAPI: DocLocalAPI{Endpoint: "ep", Fingerprint: "fp", Token: "tok"},
|
LocalAPI: DocLocalAPI{Endpoint: "ep", Fingerprint: "fp", Token: "tok"},
|
||||||
}
|
}
|
||||||
b, err := d.render()
|
b, err := d.render()
|
||||||
@@ -34,12 +34,13 @@ func TestDoc_ContractKeySet(t *testing.T) {
|
|||||||
if err := json.Unmarshal(b, &full); err != nil {
|
if err := json.Unmarshal(b, &full); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
assertKeys(t, "customer", full.Customer, []string{"id", "name", "domain", "email"})
|
// v2: customer carries only id; hub carries url + retrieval_password (NO api_key/host_id).
|
||||||
assertKeys(t, "hub", full.Hub, []string{"url", "api_key", "host_id"})
|
assertKeys(t, "customer", full.Customer, []string{"id"})
|
||||||
|
assertKeys(t, "hub", full.Hub, []string{"url", "retrieval_password"})
|
||||||
assertKeys(t, "local_api", full.LocalAPI, []string{"endpoint", "fingerprint", "token"})
|
assertKeys(t, "local_api", full.LocalAPI, []string{"endpoint", "fingerprint", "token"})
|
||||||
|
|
||||||
if SchemaV1 != "felhom.bootstrap/v1" {
|
if SchemaV2 != "felhom.bootstrap/v2" {
|
||||||
t.Fatalf("schema drift: %q", SchemaV1)
|
t.Fatalf("schema drift: %q", SchemaV2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user