slice 8A (controller half): bootstrap.json ingestion + pinned agent local-API client (v0.35.0)
internal/bootstrap: first-run bootstrap.json ingestion (decision (c)) — seed controller.yaml + skip setup; idempotent + fail-safe. internal/agentapi: minimal pinned local-API client (leaf-cert SHA-256 pin, fails closed). config LocalAPIConfig; startup /storage connectivity probe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||
)
|
||||
|
||||
func testLogger() *log.Logger { return log.New(io.Discard, "", 0) }
|
||||
|
||||
const goodBootstrap = `{
|
||||
"schema": "felhom.bootstrap/v1",
|
||||
"customer": {"id": "cust-8200", "name": "Teszt", "domain": "cust8200.felhom.eu", "email": "a@b.hu"},
|
||||
"hub": {"url": "https://hub.felhom.eu", "api_key": "HUBKEY", "host_id": "demo-felhom-01"},
|
||||
"local_api": {"endpoint": "192.168.0.162:8443", "fingerprint": "ab12", "token": "PERGUESTTOKEN"}
|
||||
}`
|
||||
|
||||
// A present bootstrap on an unconfigured controller seeds controller.yaml and skips setup.
|
||||
func TestMaybeIngest_SeedsWhenUnconfigured(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
bpath := filepath.Join(dir, "bootstrap.json")
|
||||
cfgPath := filepath.Join(dir, "controller.yaml")
|
||||
if err := os.WriteFile(bpath, []byte(goodBootstrap), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("FELHOM_BOOTSTRAP_PATH", bpath)
|
||||
|
||||
got := MaybeIngest(cfgPath, config.Default(), testLogger())
|
||||
if got.Customer.ID != "cust-8200" || got.Customer.Domain != "cust8200.felhom.eu" {
|
||||
t.Fatalf("customer not seeded: %+v", got.Customer)
|
||||
}
|
||||
if got.LocalAPI.Endpoint != "192.168.0.162:8443" || got.LocalAPI.Token != "PERGUESTTOKEN" || got.LocalAPI.Fingerprint != "ab12" {
|
||||
t.Fatalf("local_api not seeded: %+v", got.LocalAPI)
|
||||
}
|
||||
if !got.Hub.Enabled || got.Hub.URL != "https://hub.felhom.eu" || got.Hub.APIKey != "HUBKEY" {
|
||||
t.Fatalf("hub not seeded: %+v", got.Hub)
|
||||
}
|
||||
// controller.yaml must now exist on disk (so a restart reads it directly).
|
||||
if _, err := os.Stat(cfgPath); err != nil {
|
||||
t.Fatalf("controller.yaml not written: %v", err)
|
||||
}
|
||||
// And it must reload as configured (not setup).
|
||||
reloaded, err := config.LoadPermissive(cfgPath)
|
||||
if err != nil || reloaded.Customer.ID != "cust-8200" {
|
||||
t.Fatalf("seeded controller.yaml does not reload configured: %v / %+v", err, reloaded.Customer)
|
||||
}
|
||||
}
|
||||
|
||||
// An already-configured controller is NEVER clobbered (idempotent).
|
||||
func TestMaybeIngest_DoesNotClobberConfigured(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
bpath := filepath.Join(dir, "bootstrap.json")
|
||||
cfgPath := filepath.Join(dir, "controller.yaml")
|
||||
if err := os.WriteFile(bpath, []byte(goodBootstrap), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("FELHOM_BOOTSTRAP_PATH", bpath)
|
||||
|
||||
existing := config.Default()
|
||||
existing.Customer.ID = "already-here"
|
||||
existing.Customer.Domain = "existing.felhom.eu"
|
||||
|
||||
got := MaybeIngest(cfgPath, existing, testLogger())
|
||||
if got.Customer.ID != "already-here" {
|
||||
t.Fatalf("configured controller was clobbered by bootstrap: %+v", got.Customer)
|
||||
}
|
||||
if _, err := os.Stat(cfgPath); err == nil {
|
||||
t.Fatal("controller.yaml was written despite an already-configured controller")
|
||||
}
|
||||
}
|
||||
|
||||
// A malformed bootstrap leaves the controller in setup mode (cfg unchanged), no crash.
|
||||
func TestMaybeIngest_MalformedStaysInSetup(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
bpath := filepath.Join(dir, "bootstrap.json")
|
||||
cfgPath := filepath.Join(dir, "controller.yaml")
|
||||
if err := os.WriteFile(bpath, []byte("{not json"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("FELHOM_BOOTSTRAP_PATH", bpath)
|
||||
|
||||
got := MaybeIngest(cfgPath, config.Default(), testLogger())
|
||||
if got.Customer.ID != "" {
|
||||
t.Fatalf("malformed bootstrap seeded a config: %+v", got.Customer)
|
||||
}
|
||||
if _, err := os.Stat(cfgPath); err == nil {
|
||||
t.Fatal("controller.yaml written from malformed bootstrap")
|
||||
}
|
||||
}
|
||||
|
||||
// A bootstrap missing the minimum identity is rejected (stays in setup).
|
||||
func TestMaybeIngest_MissingIdentityStaysInSetup(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
bpath := filepath.Join(dir, "bootstrap.json")
|
||||
cfgPath := filepath.Join(dir, "controller.yaml")
|
||||
if err := os.WriteFile(bpath, []byte(`{"schema":"felhom.bootstrap/v1","local_api":{"endpoint":"x:1"}}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("FELHOM_BOOTSTRAP_PATH", bpath)
|
||||
|
||||
got := MaybeIngest(cfgPath, config.Default(), testLogger())
|
||||
if got.Customer.ID != "" {
|
||||
t.Fatal("seeded despite missing customer identity")
|
||||
}
|
||||
}
|
||||
|
||||
// An absent bootstrap is a no-op (normal setup).
|
||||
func TestMaybeIngest_AbsentIsNoop(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("FELHOM_BOOTSTRAP_PATH", filepath.Join(dir, "nope.json"))
|
||||
got := MaybeIngest(filepath.Join(dir, "controller.yaml"), config.Default(), testLogger())
|
||||
if got.Customer.ID != "" {
|
||||
t.Fatal("seeded with no bootstrap present")
|
||||
}
|
||||
}
|
||||
|
||||
// An unsupported schema is rejected.
|
||||
func TestMaybeIngest_UnsupportedSchema(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
bpath := filepath.Join(dir, "bootstrap.json")
|
||||
if err := os.WriteFile(bpath, []byte(`{"schema":"felhom.bootstrap/v999","customer":{"id":"x","domain":"y"}}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("FELHOM_BOOTSTRAP_PATH", bpath)
|
||||
got := MaybeIngest(filepath.Join(dir, "controller.yaml"), config.Default(), testLogger())
|
||||
if got.Customer.ID != "" {
|
||||
t.Fatal("seeded from an unsupported schema")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user