166a1c8bcb
The golden bakes --onboot 0 (template safety) and the back-half never overrode it, so every provisioned customer guest was onboot:0 -> after a host reboot/power-cut the customer's whole home-server stayed stopped until a manual pct start. Add a fatal 'pct set <vmid> -onboot 1' step to BackHalf.Provision (right after the config-mount attach), mirroring the existing pct set ops. No startup/boot-order: the v0.75 mountpoint-gate covers the drive-bind race at boot. Golden build-golden.sh unchanged (templates must not auto-start). Unit-tested (TestProvision_SetsOnbootOne + red-proof). RUNBOOK note added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FpBYrZCt9sFDqLgbG5GRGD
204 lines
6.3 KiB
Go
204 lines
6.3 KiB
Go
package provision
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
// recRunner records every command issued (to assert chown + pct set ran with correct args).
|
|
type recRunner struct {
|
|
mu sync.Mutex
|
|
cmds [][]string
|
|
fail string // if a command's name == fail, return an error
|
|
}
|
|
|
|
func (r *recRunner) Run(_ context.Context, name string, args ...string) ([]byte, []byte, error) {
|
|
r.mu.Lock()
|
|
r.cmds = append(r.cmds, append([]string{name}, args...))
|
|
r.mu.Unlock()
|
|
if name == r.fail {
|
|
return nil, []byte("boom"), io.ErrUnexpectedEOF
|
|
}
|
|
return nil, nil, nil
|
|
}
|
|
|
|
func (r *recRunner) find(name string) []string {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
for _, c := range r.cmds {
|
|
if c[0] == name {
|
|
return c
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// hasExact reports whether any recorded command matches the given args exactly (name + all args).
|
|
// Needed because several `pct` invocations are recorded; find() only returns the first.
|
|
func (r *recRunner) hasExact(want ...string) bool {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
for _, c := range r.cmds {
|
|
if len(c) != len(want) {
|
|
continue
|
|
}
|
|
match := true
|
|
for i := range c {
|
|
if c[i] != want[i] {
|
|
match = false
|
|
break
|
|
}
|
|
}
|
|
if match {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// mintMinter returns a fixed token and records the vmid it was minted for.
|
|
type mintMinter struct {
|
|
token string
|
|
vmids []int
|
|
}
|
|
|
|
func (m *mintMinter) Mint(vmid int) (string, error) { m.vmids = append(m.vmids, vmid); return m.token, nil }
|
|
|
|
func testLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) }
|
|
|
|
func newInput() Input {
|
|
return Input{
|
|
VMID: 8200,
|
|
Customer: DocCustomer{ID: "cust-8200"},
|
|
Hub: DocHub{URL: "https://hub.felhom.eu", RetrievalPassword: "five-word-passphrase"},
|
|
Endpoint: "192.168.0.162:8443",
|
|
Fingerprint: "ab12cd",
|
|
}
|
|
}
|
|
|
|
func TestProvision_WritesChownsAndAttaches(t *testing.T) {
|
|
dir := t.TempDir()
|
|
runner := &recRunner{}
|
|
minter := &mintMinter{token: "SECRET-TOKEN-XYZ"}
|
|
bh := NewBackHalf(minter, runner, dir, testLogger())
|
|
|
|
res, err := bh.Provision(context.Background(), newInput())
|
|
if err != nil {
|
|
t.Fatalf("provision: %v", err)
|
|
}
|
|
|
|
// token minted for the right guest
|
|
if len(minter.vmids) != 1 || minter.vmids[0] != 8200 {
|
|
t.Fatalf("mint vmids: %v", minter.vmids)
|
|
}
|
|
|
|
// bootstrap.json written 0600, contains the token + customer, valid contract
|
|
bootPath := filepath.Join(res.HostDir, "bootstrap.json")
|
|
info, err := os.Stat(bootPath)
|
|
if err != nil {
|
|
t.Fatalf("stat bootstrap: %v", err)
|
|
}
|
|
// Unix perms are not modeled on Windows; the 0600 is enforced on the Linux target (where the
|
|
// agent runs). Assert only where the OS honors it.
|
|
if runtime.GOOS != "windows" {
|
|
if perm := info.Mode().Perm(); perm != 0o600 {
|
|
t.Fatalf("bootstrap perms: got %o want 600", perm)
|
|
}
|
|
}
|
|
raw, _ := os.ReadFile(bootPath)
|
|
var doc Doc
|
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
|
t.Fatalf("bootstrap not valid JSON: %v", err)
|
|
}
|
|
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)
|
|
}
|
|
|
|
// chown to the mapped guest-root ran on the host dir
|
|
chown := runner.find("chown")
|
|
if chown == nil || chown[1] != "-R" || chown[2] != "100000:100000" || chown[3] != res.HostDir {
|
|
t.Fatalf("chown command wrong: %v", chown)
|
|
}
|
|
|
|
// pct set attached the read-only bind mount at the default high slot
|
|
pct := runner.find("pct")
|
|
if pct == nil {
|
|
t.Fatal("pct set not called")
|
|
}
|
|
joined := strings.Join(pct, " ")
|
|
if !strings.Contains(joined, "set 8200 -mp9") || !strings.Contains(joined, res.HostDir+",mp=/etc/felhom-bootstrap,ro=1") {
|
|
t.Fatalf("pct set command wrong: %v", pct)
|
|
}
|
|
if res.MountKey != "mp9" || res.GuestPath != "/etc/felhom-bootstrap" {
|
|
t.Fatalf("result placement wrong: %+v", res)
|
|
}
|
|
}
|
|
|
|
// F3: the provisioned customer guest must be set onboot:1 so it auto-starts after a host
|
|
// reboot/power-cut (the golden bakes onboot:0 as a template). Assert the exact pct invocation.
|
|
// Companion red-proof: removing the `b.run(... -onboot 1)` call in Provision makes this FAIL
|
|
// (no such invocation recorded) — re-applying the call turns it green.
|
|
func TestProvision_SetsOnbootOne(t *testing.T) {
|
|
dir := t.TempDir()
|
|
runner := &recRunner{}
|
|
bh := NewBackHalf(&mintMinter{token: "t"}, runner, dir, testLogger())
|
|
|
|
if _, err := bh.Provision(context.Background(), newInput()); err != nil {
|
|
t.Fatalf("provision: %v", err)
|
|
}
|
|
if !runner.hasExact("pct", "set", "8200", "-onboot", "1") {
|
|
t.Fatalf("expected `pct set 8200 -onboot 1` to be issued; recorded: %v", runner.cmds)
|
|
}
|
|
}
|
|
|
|
// The Result must never carry the token, and the token must not appear in any field returned to
|
|
// the caller (secret discipline — only the 0600 file + the store hash hold it).
|
|
func TestProvision_ResultHasNoToken(t *testing.T) {
|
|
dir := t.TempDir()
|
|
bh := NewBackHalf(&mintMinter{token: "SECRET-TOKEN-XYZ"}, &recRunner{}, dir, testLogger())
|
|
res, err := bh.Provision(context.Background(), newInput())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
blob, _ := json.Marshal(res)
|
|
if strings.Contains(string(blob), "SECRET-TOKEN-XYZ") {
|
|
t.Fatalf("token leaked into the Result: %s", blob)
|
|
}
|
|
}
|
|
|
|
func TestProvision_RejectsIncompleteInput(t *testing.T) {
|
|
dir := t.TempDir()
|
|
bh := NewBackHalf(&mintMinter{token: "t"}, &recRunner{}, dir, testLogger())
|
|
bad := Input{VMID: 8200} // no endpoint/fingerprint/customer
|
|
if _, err := bh.Provision(context.Background(), bad); err == nil {
|
|
t.Fatal("expected an error for incomplete input")
|
|
}
|
|
}
|
|
|
|
// A failed chown surfaces an error (and does not proceed to attach).
|
|
func TestProvision_ChownFailureStops(t *testing.T) {
|
|
dir := t.TempDir()
|
|
runner := &recRunner{fail: "chown"}
|
|
bh := NewBackHalf(&mintMinter{token: "t"}, runner, dir, testLogger())
|
|
if _, err := bh.Provision(context.Background(), newInput()); err == nil {
|
|
t.Fatal("expected chown failure to surface")
|
|
}
|
|
if runner.find("pct") != nil {
|
|
t.Fatal("pct set ran despite a chown failure")
|
|
}
|
|
}
|