package host import ( "crypto/ed25519" "encoding/json" "fmt" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" ) // bindingRosterFixture is a signed one-box roster in a release.DirStore, with // a published floor and the matching local floor policy, read through the // production roster adapter. Roster writes go through the real // hostactors.Directory writers unless a step says otherwise. type bindingRosterFixture struct { t *testing.T store release.Store signer release.Signer dir hostactors.Directory floorDir string box string churn int } const bindingFixtureBucket = "deployment" func newBindingRosterFixture(t *testing.T, box, region string) *bindingRosterFixture { t.Helper() key := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) s := release.NewDirStore(t.TempDir()) f := &bindingRosterFixture{t: t, store: s, signer: release.RootSigner(key), dir: hostactors.Directory{Store: s, PublicKey: key.Public().(ed25519.PublicKey), Bucket: bindingFixtureBucket}, floorDir: t.TempDir(), box: box} if err := hostfloor.Publish(t.Context(), s, f.signer, bindingFixtureBucket, hostfloor.Floor{Min: 1, MinRelease: "v0.1.0"}); err != nil { t.Fatalf("newBindingRosterFixture: %v", err) } raw, _, err := s.Get(t.Context(), hostfloor.Key) if err != nil { t.Fatalf("newBindingRosterFixture: %v", err) } auth, _, err := s.Get(t.Context(), hostfloor.AuthKey) if err != nil { t.Fatalf("newBindingRosterFixture: %v", err) } body, _ := json.Marshal(hostfloor.Policy{Schema: 1, DeploymentBucket: bindingFixtureBucket, Mode: "enforced", Floor: raw, Authorization: auth}) if err := os.WriteFile(filepath.Join(f.floorDir, "policy.json"), body, 0600); err != nil { t.Fatalf("newBindingRosterFixture: %v", err) } if err := f.dir.Initialize(t.Context(), f.signer); err != nil { t.Fatalf("newBindingRosterFixture: %v", err) } o := f.read() next := hostactors.Clone(o.Roster) next.RequestLimitNS, next.Publication, next.FloorSHA256 = int64(hostbox.RequestLimit), "published", hostfloor.Digest(raw) // A region HA group, so enrollmentAttempt can run the real writer. next.Regions = map[string]hostactors.Region{region: {Namespaces: []string{"platform"}, Pools: []string{"default"}, Account: "111122223333", AWSRegion: "eu-west-1", GroupName: "group", GroupARN: "arn:aws:autoscaling:eu-west-1:111122223333:autoScalingGroup:x:autoScalingGroupName/group"}} next.Boxes[box] = hostactors.Box{BoxID: box, Enrollment: strings.Repeat("a", 32), Namespace: "platform", Label: box, Region: region, Pool: "default", Cloud: hostactors.Cloud{Provider: "aws", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"}, EvidenceSHA256: strings.Repeat("c", 64), State: "active", LossIDs: []string{}} if _, err := f.dir.Replace(t.Context(), f.signer, o, next); err != nil { t.Fatalf("newBindingRosterFixture: %v", err) } return f } func (f *bindingRosterFixture) read() hostactors.Observed { f.t.Helper() o, err := f.dir.Read(f.t.Context()) if err != nil { f.t.Fatalf("read: %v", err) } return o } func (f *bindingRosterFixture) row() hostactors.Box { return f.read().Roster.Boxes[f.box] } // identity is the box's production identity: signed cloud evidence for the // row's instance and productionClaimRoster over this roster. func (f *bindingRosterFixture) identity() hostbox.IdentityProvider { f.t.Helper() row := f.row() identity := handoverRecoveryIdentity(f.t, hostbox.Actor{Box: f.box, Enrollment: row.Enrollment, Region: row.Region, ControlBucket: bindingFixtureBucket, Revision: 1}) identity.Roster = productionClaimRoster{Directory: f.dir, DataBucket: identity.Bucket, FloorDir: f.floorDir} return identity } func (f *bindingRosterFixture) binding() string { f.t.Helper() b, ok := hostactors.BoxBinding(f.read().Roster, f.box) if !ok { f.t.Fatal("box not in roster") } return b } // holdChurn is one console create's roster traffic: a real hold Reserve, then // the claim it names moves on and ReleaseFinishedHold drops the hold. The // roster write counter moves by two; this box's binding does not. func (f *bindingRosterFixture) holdChurn() { f.t.Helper() before := f.read().Roster.Revision f.releaseHold(f.reserveHold()) after := f.read() if after.Roster.Revision != before+2 || len(after.Roster.Holds) != 0 { f.t.Fatalf("hold churn: revision %d -> %d, holds %d", before, after.Roster.Revision, len(after.Roster.Holds)) } } // reserveHold is a create's real hold Reserve naming this box, left held. func (f *bindingRosterFixture) reserveHold() hostactors.Reservation { f.t.Helper() ctx := f.t.Context() f.churn++ host := fmt.Sprintf("churn-%d.example.com", f.churn) before := f.read() claim, err := hostclaim.Read(ctx, f.store, host) if err != nil { f.t.Fatalf("reserveHold: %v", err) } actor := "box:" + f.box enrollment := before.Roster.Boxes[f.box].Enrollment h := hostactors.Reservation{ID: fmt.Sprintf("hold_%d", f.churn), Host: host, OpID: "create", ExpectedClaimETag: claim.ETag, FloorSHA256: before.Roster.FloorSHA256, Actors: map[string]string{actor: enrollment}, Releases: map[string]hostactors.Report{actor: {Enrollment: enrollment}}} if _, err := f.dir.Reserve(ctx, f.signer, before, h, false); err != nil { f.t.Fatalf("reserveHold: %v", err) } return h } // releaseHold moves the hold's claim on, then ReleaseFinishedHold drops it. func (f *bindingRosterFixture) releaseHold(h hostactors.Reservation) { f.t.Helper() ctx := f.t.Context() claim, err := hostclaim.Read(ctx, f.store, h.Host) if err != nil { f.t.Fatalf("releaseHold: %v", err) } if _, err := hostclaim.Update(ctx, f.store, claim, func(*hostclaim.Claim) error { return nil }); err != nil { f.t.Fatalf("releaseHold: %v", err) } if err := f.dir.ReleaseFinishedHold(ctx, f.signer, h.ID); err != nil { f.t.Fatalf("releaseHold: %v", err) } } // bindCensusKey binds key's public half as the box's census key through the // real BindCensusKey writer, with the box's proof of possession. func (f *bindingRosterFixture) bindCensusKey(key ed25519.PrivateKey) { f.t.Helper() binding := hostactors.CensusKeyBinding{Bucket: bindingFixtureBucket, Box: f.box, Enrollment: f.row().Enrollment, PublicKey: key.Public().(ed25519.PublicKey)} raw, err := json.Marshal(binding) if err != nil { f.t.Fatal(err) } if _, err := f.dir.BindCensusKey(f.t.Context(), f.signer, f.read(), binding, ed25519.Sign(key, raw)); err != nil { f.t.Fatal("bind census key:", err) } } // quarantine is a real Directory.Quarantine of the box. func (f *bindingRosterFixture) quarantine(loss string) { f.t.Helper() o := f.read() if _, err := f.dir.Quarantine(f.t.Context(), f.signer, o, f.box, o.Roster.Boxes[f.box].Enrollment, loss); err != nil { f.t.Fatalf("quarantine: %v", err) } } // readmit returns the quarantined row to active by a DIRECT SIGNED WRITE. // Directory.Replace admits a quarantined -> active change on a published // roster only with a fresh box-signed census report (hostactors // checkMembership), which this fixture has no box to produce. The written // roster is exactly what hostresolve's readmission writes: the row's state // alone changes, its quarantine revision and loss ids stay, and Revision // moves by one. func (f *bindingRosterFixture) readmit() { f.t.Helper() o := f.read() next := hostactors.Clone(o.Roster) b := next.Boxes[f.box] if b.State != "quarantined" { f.t.Fatalf("readmit: state %q", b.State) } b.State = "active" next.Boxes[f.box] = b next.Revision++ wire, err := release.SignRootEnvelope(f.signer, next) if err != nil { f.t.Fatalf("readmit: %v", err) } if _, err := f.store.Put(f.t.Context(), hostactors.Key, wire, o.ETag); err != nil { f.t.Fatalf("readmit: %v", err) } } // activationChurn is one platform promote's roster traffic: a real activation // Reserve (with a prepared system publication) and CancelActivation. func (f *bindingRosterFixture) activationChurn() { f.t.Helper() ctx := f.t.Context() f.churn++ id := fmt.Sprintf("activation_%d", f.churn) platform := release.AdoptPlatform() rel := release.SystemRelease{Version: "v2.0.0", ReleaseTag: "v2.0.0", MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{platform: {Key: release.SystemArtifactKey("v2.0.0", platform), SHA256: strings.Repeat("a", 64)}}} pub, err := release.PrepareSystemPromotion(ctx, f.store, f.signer, "router", "stable", rel, release.SystemPromoteOptions{Actor: "operator"}, id) if err != nil { f.t.Fatalf("activationChurn: %v", err) } before := f.read() actor := "box:" + f.box enrollment := before.Roster.Boxes[f.box].Enrollment h := hostactors.Reservation{ID: id, Publication: &pub, RequestSHA256: strings.Repeat("e", 64), UpdateMode: "promote", FloorSHA256: before.Roster.FloorSHA256, Actors: map[string]string{actor: enrollment}, Releases: map[string]hostactors.Report{actor: {Enrollment: enrollment}}, Hashes: map[string][]string{actor: {"a", "b", "c"}}} if _, err := f.dir.Reserve(ctx, f.signer, before, h, true); err != nil { f.t.Fatalf("activationChurn: %v", err) } if err := f.dir.CancelActivation(ctx, f.signer, id); err != nil { f.t.Fatalf("activationChurn: %v", err) } if after := f.read(); after.Roster.Revision != before.Roster.Revision+2 || len(after.Roster.Activations) != 0 { f.t.Fatalf("activation churn: revision %d -> %d", before.Roster.Revision, after.Roster.Revision) } } // enrollmentAttempt is region HA autoscaling's real BeginEnrollmentAttempt // for another instance in the box's region. func (f *bindingRosterFixture) enrollmentAttempt() { f.t.Helper() f.churn++ o := f.read() ref := fmt.Sprintf("replacement-%d", f.churn) if _, _, err := f.dir.BeginEnrollmentAttempt(f.t.Context(), f.signer, o, hostactors.EnrollmentRequest{Reference: ref, Region: o.Roster.Boxes[f.box].Region}, []byte("evidence-"+ref), "i-0"+ref); err != nil { f.t.Fatalf("enrollmentAttempt: %v", err) } } // roleChange is the operator's real SetBoxRole on this box. func (f *bindingRosterFixture) roleChange(role string) { f.t.Helper() if _, err := f.dir.SetBoxRole(f.t.Context(), f.signer, f.read(), f.box, role); err != nil { f.t.Fatalf("roleChange: %v", err) } } // sixWrites is the design's L2 sequence: hold Reserve and ReleaseFinishedHold, // activation Reserve and CancelActivation, BeginEnrollmentAttempt and // SetBoxRole. The roster write counter moves by exactly six and this box's // binding does not move at all. func (f *bindingRosterFixture) sixWrites() { f.t.Helper() before, binding := f.read().Roster.Revision, f.binding() f.holdChurn() f.activationChurn() f.enrollmentAttempt() role := hostactors.RoleLegacy if f.row().Role == role { role = hostactors.RoleDefault } f.roleChange(role) if after := f.read().Roster.Revision; after != before+6 { f.t.Fatalf("six roster writes moved the revision %d -> %d", before, after) } if f.binding() != binding { f.t.Fatal("unrelated roster writes moved the box binding") } }