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:
@@ -87,8 +87,11 @@ func (b *BackHalf) Provision(ctx context.Context, in Input) (Result, error) {
|
||||
if in.Endpoint == "" || in.Fingerprint == "" {
|
||||
return Result{}, fmt.Errorf("provision: needs the local-api endpoint and leaf fingerprint")
|
||||
}
|
||||
if in.Customer.ID == "" || in.Customer.Domain == "" {
|
||||
return Result{}, fmt.Errorf("provision: needs customer id and domain (so the controller skips setup)")
|
||||
if in.Customer.ID == "" {
|
||||
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
|
||||
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).
|
||||
doc := Doc{
|
||||
Schema: SchemaV1,
|
||||
Schema: SchemaV2,
|
||||
Customer: in.Customer,
|
||||
Hub: in.Hub,
|
||||
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 {
|
||||
return Input{
|
||||
VMID: 8200,
|
||||
Customer: DocCustomer{ID: "cust-8200", Domain: "cust8200.felhom.eu", Name: "Teszt"},
|
||||
Hub: DocHub{URL: "https://hub.felhom.eu", APIKey: "HUBKEY", HostID: "demo-felhom-01"},
|
||||
Customer: DocCustomer{ID: "cust-8200"},
|
||||
Hub: DocHub{URL: "https://hub.felhom.eu", RetrievalPassword: "five-word-passphrase"},
|
||||
Endpoint: "192.168.0.162:8443",
|
||||
Fingerprint: "ab12cd",
|
||||
}
|
||||
@@ -95,9 +95,12 @@ func TestProvision_WritesChownsAndAttaches(t *testing.T) {
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
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)
|
||||
}
|
||||
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" {
|
||||
t.Fatalf("local_api wrong: %+v", doc.LocalAPI)
|
||||
}
|
||||
|
||||
+15
-13
@@ -10,14 +10,20 @@ package provision
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// SchemaV1 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
|
||||
// pins the key set, mirroring the controller's bootstrap_test.go).
|
||||
const SchemaV1 = "felhom.bootstrap/v1"
|
||||
// SchemaV2 is the stable agent→controller contract version. It MUST stay byte-compatible with the
|
||||
// controller's internal/bootstrap.SchemaV2 / Bootstrap shape (cross-repo contract; doc_test.go
|
||||
// pins the key set, mirroring the controller's bootstrap_test.go). v2 changed the contract's
|
||||
// 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
|
||||
// internal/bootstrap.Bootstrap exactly. It carries ONLY what the controller needs to come up
|
||||
// configured and reach the agent's local API — no registry credential (image is baked).
|
||||
// internal/bootstrap.Bootstrap exactly. It carries ONLY what the controller needs to PULL its config
|
||||
// (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 {
|
||||
Schema string `json:"schema"`
|
||||
Customer DocCustomer `json:"customer"`
|
||||
@@ -26,16 +32,12 @@ type Doc struct {
|
||||
}
|
||||
|
||||
type DocCustomer struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Domain string `json:"domain"`
|
||||
Email string `json:"email"`
|
||||
ID string `json:"id"` // the pull target; the hub provides name/domain/email
|
||||
}
|
||||
|
||||
type DocHub struct {
|
||||
URL string `json:"url"`
|
||||
APIKey string `json:"api_key"`
|
||||
HostID string `json:"host_id"`
|
||||
URL string `json:"url"`
|
||||
RetrievalPassword string `json:"retrieval_password"` // SECRET — pulls the full config (incl. the customer key)
|
||||
}
|
||||
|
||||
type DocLocalAPI struct {
|
||||
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
// bootstrap_test.go ingests the same shape. A drift here (or there) breaks provisioning.
|
||||
func TestDoc_ContractKeySet(t *testing.T) {
|
||||
d := Doc{
|
||||
Schema: SchemaV1,
|
||||
Customer: DocCustomer{ID: "c", Name: "n", Domain: "d", Email: "e"},
|
||||
Hub: DocHub{URL: "u", APIKey: "k", HostID: "h"},
|
||||
Schema: SchemaV2,
|
||||
Customer: DocCustomer{ID: "c"},
|
||||
Hub: DocHub{URL: "u", RetrievalPassword: "p"},
|
||||
LocalAPI: DocLocalAPI{Endpoint: "ep", Fingerprint: "fp", Token: "tok"},
|
||||
}
|
||||
b, err := d.render()
|
||||
@@ -34,12 +34,13 @@ func TestDoc_ContractKeySet(t *testing.T) {
|
||||
if err := json.Unmarshal(b, &full); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertKeys(t, "customer", full.Customer, []string{"id", "name", "domain", "email"})
|
||||
assertKeys(t, "hub", full.Hub, []string{"url", "api_key", "host_id"})
|
||||
// v2: customer carries only id; hub carries url + retrieval_password (NO 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"})
|
||||
|
||||
if SchemaV1 != "felhom.bootstrap/v1" {
|
||||
t.Fatalf("schema drift: %q", SchemaV1)
|
||||
if SchemaV2 != "felhom.bootstrap/v2" {
|
||||
t.Fatalf("schema drift: %q", SchemaV2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user