//go:build linux package host import ( "context" "os" "path/filepath" "testing" "time" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/park" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" ) // bindingInstallWorld is one box's agent and root halves over one roster: the // agent's ReconcileAdapter pinned to a verified actor's binding, installing // through the remote journal into root's real commitRootInventory, and root's // inventory actor the production identity, fresh on every read (agent mode's // actorForRoot). type bindingInstallWorld struct { roster *bindingRosterFixture identity hostbox.IdentityProvider f *recoveryFixture rec provision.Record route registry.Route reg *registry.Registry journals *hostbox.JournalService roots map[string]string guard hostguard.Guard rootAuth *hostguard.RootAuthority // rebuilt by pin pinned hostbox.Actor root ClaimRootHTTP census *hostbox.EmptyInstallationCensus adapter hostbox.ReconcileAdapter remote *hostbox.StopInventoryDB source hostbox.WorkSource } func newBindingInstallWorld(t *testing.T) *bindingInstallWorld { t.Helper() w := &bindingInstallWorld{roster: newBindingRosterFixture(t, "box-1", "ie")} w.identity = w.roster.identity() w.f = newRecoveryFixture(t) obs := w.f.read() holder := obs.Claim.Holder w.rec = provision.Record{Schema: 1, Host: obs.Host, Account: holder.Account, App: holder.App, Channel: "stable", CreatedAt: w.f.now} plan14PutRecord(t, w.f.claims, w.rec) w.reg = restartTestRegistry(t) var err error if w.journals, err = hostbox.NewJournalService(w.f.journal); err != nil { t.Fatal(err) } w.rootAuth = hostguard.NewRootAuthority(&rollbackDisk{dir: t.TempDir()}) w.roots = map[string]string{"data": w.f.guard.DataDir, "opt": t.TempDir(), "units": t.TempDir()} w.route = registry.Route{Host: w.rec.Host, Account: w.rec.Account, App: w.rec.App, Kind: "instance", DBPath: provision.NestedDBPath(w.f.guard.DataDir, w.rec.Host), Socket: provision.NestedSocket(w.rec.Host)} w.start(t) return w } // start is an agent and root start: Verify once, pin the binding into the // agent's adapter and census exactly as ClaimAssembly.Build does // (claimassembly.go, Reconcile.DirectoryRevision and the census), and wire // root with the fresh production identity. func (w *bindingInstallWorld) start(t *testing.T) { t.Helper() pinned, err := w.identity.Verify(t.Context()) if err != nil { t.Fatal(err) } if !hostactors.ValidBinding(pinned.Binding) { t.Fatalf("pinned binding %q", pinned.Binding) } w.pinned = pinned rootGuard := w.f.guard rootGuard.Root = w.rootAuth w.census = &hostbox.EmptyInstallationCensus{Inventory: w.journals.Inventory, Registry: w.reg, Launches: w.f.journal, Box: pinned.Box, Enrollment: pinned.Enrollment, DirectoryRevision: pinned.Binding, Roots: w.roots, SharedPaths: map[string][]string{"data": {"hostguard"}}} w.root = ClaimRootHTTP{Journals: w.journals, Launches: w.f.journal, Roots: w.roots, InventoryActor: w.identity.Verify, Reconcile: hostbox.ReconcileAdapter{Registry: w.reg, Inventory: w.journals.Inventory, Census: w.census, Box: pinned.Box, Enrollment: pinned.Enrollment}, Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: rootGuard, Evidence: w.f.claims}}} root := w.root _, w.remote = hostbox.RemoteJournals(func(ctx context.Context, q hostbox.JournalRequest) (hostbox.JournalResponse, error) { if q.Op == "inventory-write" { return root.commitRootInventory(ctx, q) } return w.journals.Execute(ctx, pinned.Box+"/"+pinned.Enrollment, q) }) w.guard = w.f.guard w.guard.FenceView = w.rootAuth.View // The production policy, unit-shape refusal included (G10): an allow-all // policy here hid the refusal of every fresh install. policy, _ := witnessPolicy(t, w.reg) w.source = hostbox.WorkSource{Guard: w.guard, Box: pinned.Box, Region: "ie", Now: time.Now, Policy: policy, Local: func(context.Context, *hostclaim.Claim, hostbox.Selection) error { return nil }} w.adapter = hostbox.ReconcileAdapter{Registry: w.reg, Inventory: w.remote, Box: pinned.Box, Enrollment: pinned.Enrollment, DirectoryRevision: pinned.Binding, Census: w.census, Reconciler: hostbox.Reconciler{Claims: w.f.claims, Guard: w.guard, Box: pinned.Box, Region: "ie", Now: time.Now, Writes: hostwrite.Config{Enabled: true, Selector: w.source}, Local: park.Local{Guard: w.guard, Claims: w.f.claims, DataDir: w.guard.DataDir, Box: pinned.Box}, CheckRecord: func(context.Context, hostbox.RecordSelection) error { return nil }, CheckLocal: func(context.Context, hostclaim.Observed, hostbox.RecordSelection) error { return nil }}} } func (w *bindingInstallWorld) install(t *testing.T) { t.Helper() holder := w.f.read().Claim.Holder ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() writes := 0 err := w.adapter.Install(ctx, provision.ClaimRecord{Host: w.rec.Host, Key: holder.Key, Account: w.rec.Account, App: w.rec.App, Family: "instance", CreatedAt: w.rec.CreatedAt, Value: w.rec, Route: w.route}, func(owner registry.ClaimOwnership) error { writes++ w.route.ClaimOwnership = owner return w.reg.Upsert(w.route) }) if err != nil || writes != 1 { t.Fatalf("install: %v, effects=%d", err, writes) } } func (w *bindingInstallWorld) inventory(t *testing.T) (hostbox.StopInventory, uint64) { t.Helper() holder := w.f.read().Claim.Holder in, revision, err := w.journals.Inventory.Snapshot(t.Context(), w.rec.Host, hostguard.Captured{Incarnation: holder.Incarnation, Stamp: holder.Stamp}) if err != nil { t.Fatal(err) } return in, revision } // L2 and L3 (roster binding): install at the binding B0 through the remote // path into root's commitRootInventory, then six real roster writes that move // the write counter (hold reserve and release, activation reserve and cancel, // an enrollment attempt, a role change). Without a restart (L2), and after // the agent and root start again and re-pin (L3), every comparison still // passes: the agent's census, a second inventory write through root // (prepareStateRemote), the regional and move-park bound identity and // VerifyRegional, and Verify's fresh actor is SameBinding with the pin. // // Root's inventory actor is the fresh production identity, as in agent mode. // Instance-control mode's startup-pinned inventory actor becomes fresh in // phase B (task 5); L4 covers it there. func TestClaimBindingSurvivesRosterWritesWithoutRestart(t *testing.T) { testClaimBindingSurvivesRosterWrites(t, false) } func TestClaimBindingSurvivesRosterWritesAfterRestart(t *testing.T) { testClaimBindingSurvivesRosterWrites(t, true) } func testClaimBindingSurvivesRosterWrites(t *testing.T, restart bool) { ctx := t.Context() w := newBindingInstallWorld(t) b0 := w.pinned.Binding w.install(t) in, revision := w.inventory(t) if in.DirectoryRevision != b0 || revision != 3 { t.Fatalf("installed stamp %q (want the binding %q), inventory revision %d", in.DirectoryRevision, b0, revision) } before := w.roster.read().Roster.Revision w.roster.sixWrites() fresh, err := w.identity.Verify(ctx) if err != nil { t.Fatal("Verify after six roster writes:", err) } if fresh.Revision != before+6 || !hostbox.SameBinding(fresh, w.pinned) || fresh.Binding != b0 { t.Fatalf("fresh actor revision %d (pinned %d), binding %q (pinned %q)", fresh.Revision, w.pinned.Revision, fresh.Binding, b0) } pinned := w.pinned if restart { w.start(t) if w.pinned.Binding != b0 || w.pinned.Revision == pinned.Revision { t.Fatalf("restart pinned %q at revision %d", w.pinned.Binding, w.pinned.Revision) } } // A create's hold stays held through the comparisons below, as the // create's own hold does while its box installs. held := w.roster.reserveHold() if fresh, err = w.identity.Verify(ctx); err != nil || fresh.Binding != b0 { t.Fatalf("Verify with a hold held: binding %q, %v", fresh.Binding, err) } holder := w.f.read().Claim.Holder if err := w.census.CheckLocal(ctx, w.f.read(), hostbox.RecordSelection{Host: w.rec.Host, Incarnation: holder.Incarnation}); err != nil { t.Fatal("agent census after roster writes:", err) } // The second inventory write: state registration through root. if err := os.Mkdir(filepath.Join(w.guard.DataDir, "instances"), 0700); err != nil { t.Fatal(err) } work, err := w.source.Select(ctx, w.rec.Host, "spawn") if err != nil { t.Fatal(err) } roles := &ClaimRoleAssembly{Inventory: w.remote, ManagedRoots: w.roots, Guard: w.guard, Claims: w.f.claims} if err := roles.prepareStateRemote(ctx, w.f.read(), work, ClaimLaunchInputs{DBPath: w.route.DBPath}, fresh); err != nil { t.Fatal("state registration after roster writes:", err) } if in, revision = w.inventory(t); in.DirectoryRevision != b0 || revision != 5 { t.Fatalf("after state registration: stamp %q, revision %d", in.DirectoryRevision, revision) } // The regional request store's and the move parks' bound identity, and // the regional enrollment check, against the actor this start pinned. if err := claimBoundIdentity(w.identity.Verify, w.pinned)(ctx); err != nil { t.Fatal("bound identity after roster writes:", err) } if err := (claimRegionalEnrollment{identity: w.identity, actor: w.pinned}).VerifyRegional(ctx, w.pinned.Region, w.pinned.Bucket, hostbox.RequestLimit); err != nil { t.Fatal("VerifyRegional after roster writes:", err) } // The hold is released and more writes land: nothing pins the counter. w.roster.releaseHold(held) w.roster.holdChurn() if err := claimBoundIdentity(w.identity.Verify, w.pinned)(ctx); err != nil { t.Fatal("bound identity after a further hold:", err) } if err := w.census.CheckLocal(ctx, w.f.read(), hostbox.RecordSelection{Host: w.rec.Host, Incarnation: holder.Incarnation}); err != nil { t.Fatal("census after a further hold:", err) } // A binding change is still refused by the same comparisons: quarantine // and readmission (direct signed write, see readmit) move the binding. w.roster.quarantine("loss_1") w.roster.readmit() readmitted, err := w.identity.Verify(ctx) if err != nil { t.Fatal(err) } if hostbox.SameBinding(readmitted, w.pinned) { t.Fatal("readmitted actor is SameBinding with the startup pin") } if err := claimBoundIdentity(w.identity.Verify, w.pinned)(ctx); err == nil { t.Fatal("bound identity accepted a readmitted box") } if err := (claimRegionalEnrollment{identity: w.identity, actor: w.pinned}).VerifyRegional(ctx, w.pinned.Region, w.pinned.Bucket, hostbox.RequestLimit); err == nil { t.Fatal("VerifyRegional accepted a readmitted box") } }