| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/sha256" |
| 6 | "encoding/hex" |
| 7 | "fmt" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/hostbox" |
| 11 | "github.com/carlosframework/platform/internal/hostclaim" |
| 12 | "github.com/carlosframework/platform/internal/hostguard" |
| 13 | "github.com/carlosframework/platform/internal/provision" |
| 14 | "github.com/carlosframework/platform/internal/registry" |
| 15 | ) |
| 16 | |
| 17 | func TestPlan14RootTimerInventory(t *testing.T) { |
| 18 | for _, attack := range []string{"unrecorded", "intended", "missing", "omitted-timer", "row identity", "owner", "path", "root", "shape"} { |
| 19 | t.Run(attack, func(t *testing.T) { |
| 20 | t.Setenv("CARLOS_HOST_CLAIM", "1") |
| 21 | f := newRecoveryFixture(t) |
| 22 | o := f.read() |
| 23 | h := o.Claim.Holder |
| 24 | plan14PutRecord(t, f.claims, provision.Record{Schema: 1, Host: o.Host, Account: h.Account, App: h.App, Channel: "stable", CreatedAt: f.now}) |
| 25 | _, etag, _ := f.claims.Get(t.Context(), h.Key) |
| 26 | journals, err := hostbox.NewJournalService(f.journal) |
| 27 | if err != nil { |
| 28 | t.Fatal(err) |
| 29 | } |
| 30 | reg := restartTestRegistry(t) |
| 31 | sum := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", o.Host, h.Incarnation, h.Stamp, "box-1"))) |
| 32 | owner := registry.ClaimOwnership{ClaimIncarnation: h.Incarnation, ClaimStamp: h.Stamp, ClaimOwner: hex.EncodeToString(sum[:]), ClaimRowID: "route"} |
| 33 | row := registry.Route{Host: o.Host, Account: h.Account, App: h.App, DBPath: provision.NestedDBPath(f.guard.DataDir, o.Host), ClaimOwnership: owner} |
| 34 | if err = reg.Upsert(row); err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | if err = reg.UpsertTimer(registry.Timer{Host: o.Host, Name: "daily", Kind: "cron", Spec: "0 0 * * *", Path: "/daily", ClaimOwnership: owner}); err != nil { |
| 38 | t.Fatal(err) |
| 39 | } |
| 40 | timer, err := reg.GetTimer(o.Host, "daily") |
| 41 | if err != nil { |
| 42 | t.Fatal(err) |
| 43 | } |
| 44 | if !timer.ClaimOwnership.Valid() { |
| 45 | t.Fatal("timer fixture has no ownership") |
| 46 | } |
| 47 | in := hostbox.StopInventory{Complete: true, Host: o.Host, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, Box: "box-1", Enrollment: "en", DirectoryRevision: "1", Entries: []hostbox.StopInventoryEntry{{Kind: "route", ArtifactID: "route", ManagedRoot: "registry", RelativePath: o.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"}}} |
| 48 | if err = journals.Inventory.WriteStopInventory(t.Context(), in); err != nil { |
| 49 | t.Fatal(err) |
| 50 | } |
| 51 | g := f.guard |
| 52 | g.Root = hostguard.NewRootAuthority(&rollbackDisk{dir: t.TempDir()}) |
| 53 | if err = g.EstablishRoot(t.Context(), hostguard.RootInstallation{Artifacts: hostguard.InstallationArtifacts{Schema: 1, StateDirectory: true}, Host: o.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Account: h.Account, App: h.App, Record: h.Key, Box: "box-1", Enrollment: "en", Migration: 1, InventoryRevision: 1}, func(*hostclaim.Claim) error { return nil }); err != nil { |
| 54 | t.Fatal(err) |
| 55 | } |
| 56 | actor := hostbox.Actor{Box: "box-1", Enrollment: "en", Revision: 1, Binding: "1"} |
| 57 | root := ClaimRootHTTP{Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: g, Evidence: f.claims}}, Journals: journals, InventoryActor: func(context.Context) (hostbox.Actor, error) { return actor, nil }, Roots: map[string]string{"data": g.DataDir}, Reconcile: hostbox.ReconcileAdapter{Registry: reg, Census: &hostbox.EmptyInstallationCensus{}}} |
| 58 | e := hostbox.StopInventoryEntry{Kind: "timer", ArtifactID: "timer", ManagedRoot: "registry", RelativePath: o.Host + "/daily", RowIdentity: timer.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"} |
| 59 | switch attack { |
| 60 | case "missing", "intended": |
| 61 | if err = reg.DeleteTimer(o.Host, "daily"); err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | if attack == "intended" { |
| 65 | e.Materialization = "intended" |
| 66 | } |
| 67 | case "row identity": |
| 68 | e.RowIdentity = "other" |
| 69 | case "owner": |
| 70 | e.Ownership = "other" |
| 71 | case "path": |
| 72 | e.RelativePath = "other.example/daily" |
| 73 | case "root": |
| 74 | e.ManagedRoot = "data" |
| 75 | case "shape": |
| 76 | e.Inode = 1 |
| 77 | } |
| 78 | if attack != "omitted-timer" { |
| 79 | in.Entries = append(in.Entries, e) |
| 80 | } |
| 81 | _, client := hostbox.RemoteJournals(root.commitRootInventory) |
| 82 | _, err = client.WriteExpected(t.Context(), in, hostbox.InventoryExpectation{Revision: 1, RecordKey: h.Key, RecordETag: etag, Target: hostguard.FenceTarget{Host: o.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Session: h.Session, Box: actor.Box, Enrollment: actor.Enrollment, ClaimETag: o.ETag, Op: o.Claim.Op.ID, Phase: o.Claim.Op.Phase}}) |
| 83 | valid := attack == "intended" |
| 84 | if valid && err != nil || !valid && err == nil { |
| 85 | t.Fatal(attack, err) |
| 86 | } |
| 87 | _, revision, _ := journals.Inventory.Snapshot(t.Context(), o.Host, hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}) |
| 88 | want := uint64(1) |
| 89 | if valid { |
| 90 | want = 2 |
| 91 | } |
| 92 | if valid { |
| 93 | views, readErr := g.Root.GenerationInstallations(t.Context()) |
| 94 | if readErr != nil || len(views) != 1 || views[0].Installation.Artifacts.Timers != "daily" { |
| 95 | t.Fatalf("timer obligation not retained: %+v %v", views, readErr) |
| 96 | } |
| 97 | } |
| 98 | if revision != want { |
| 99 | t.Fatal(revision, want) |
| 100 | } |
| 101 | }) |
| 102 | } |
| 103 | } |
| 104 | |