| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | "time" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/registry" |
| 12 | "github.com/carlosframework/platform/internal/release" |
| 13 | "github.com/carlosframework/platform/internal/rootstate" |
| 14 | ) |
| 15 | |
| 16 | func TestPlan14GenerationRetirementReleasesExactPin(t *testing.T) { |
| 17 | f, disk, _ := generationLaunchFixture(t) |
| 18 | permit, err := f.root.generations.reserve(t.Context(), f.root, f.peer, f.req) |
| 19 | if err != nil { |
| 20 | t.Fatal(err) |
| 21 | } |
| 22 | r := permit.reservation |
| 23 | opt := f.root.generations.optDir |
| 24 | stageSweepVersion(t, opt, "bap", "v2", "other", 48*time.Hour) |
| 25 | stageSweepVersion(t, opt, "bap", "v3", "pending", 48*time.Hour) |
| 26 | pending := r.Binding |
| 27 | pending.Attempt, pending.Invocation = "pending-v3", "pending-v3" |
| 28 | pending.Binding.Account, pending.Binding.App, pending.Version = "bap", "pending", "v3" |
| 29 | pending.Path = release.VersionPath(opt, "bap", "v3", "pending") |
| 30 | pending.Tree = filepath.Dir(pending.Path) |
| 31 | for path, dst := range map[string]*rootstate.FileIdentity{pending.Path: &pending.Executable, pending.Tree: &pending.TreeIdentity} { |
| 32 | file, err := os.Open(path) |
| 33 | if err != nil { |
| 34 | t.Fatal(err) |
| 35 | } |
| 36 | *dst, err = rootstate.Identity(file) |
| 37 | file.Close() |
| 38 | if err != nil { |
| 39 | t.Fatal(err) |
| 40 | } |
| 41 | } |
| 42 | p, err := permit.ledger.Reserve(pending) |
| 43 | if err != nil { |
| 44 | t.Fatal(err) |
| 45 | } |
| 46 | when := time.Now().Add(-48 * time.Hour) |
| 47 | if err := os.Chtimes(r.Binding.Path, when, when); err != nil { |
| 48 | t.Fatal(err) |
| 49 | } |
| 50 | source := generationPinSource{open: func() (rootstate.Store, error) { return disk, nil }, verify: func(context.Context, []release.GenerationReservation) error { return nil }} |
| 51 | // The injected verifier exercises the destructive consumer contract; this |
| 52 | // fixture does not establish production installation/kernel completeness. |
| 53 | rows := fakeLister{routes: []registry.Route{{Host: "other.example", Account: "bap", App: "other", Version: "v2"}}} |
| 54 | gen := noGenerations(t) |
| 55 | sweepStagedVersions(rows, opt, gen, 6*time.Hour, time.Now(), source) |
| 56 | if !staged(t, opt, "v1") { |
| 57 | t.Fatal("pending generation lost pin") |
| 58 | } |
| 59 | if err := permit.ledger.Retire(t.Context(), r, func(context.Context, release.GenerationReservation) error { return nil }, func(context.Context, release.GenerationReservation) (string, error) { return "", os.ErrPermission }); err == nil { |
| 60 | t.Fatal("retired without kernel witness") |
| 61 | } |
| 62 | sweepStagedVersions(rows, opt, gen, 6*time.Hour, time.Now(), source) |
| 63 | if !staged(t, opt, "v1") { |
| 64 | t.Fatal("incomplete retirement lost pin") |
| 65 | } |
| 66 | if err := permit.ledger.Retire(t.Context(), r, func(context.Context, release.GenerationReservation) error { return nil }, func(context.Context, release.GenerationReservation) (string, error) { |
| 67 | return strings.Repeat("a", 64), nil |
| 68 | }); err != nil { |
| 69 | t.Fatal(err) |
| 70 | } |
| 71 | sweepStagedVersions(rows, opt, gen, 72*time.Hour, time.Now(), source) |
| 72 | if !staged(t, opt, "v1") { |
| 73 | t.Fatal("retirement bypassed sweep grace") |
| 74 | } |
| 75 | sweepStagedVersions(rows, opt, gen, 6*time.Hour, time.Now(), source) |
| 76 | if staged(t, opt, "v1") || !staged(t, opt, "v2") || !staged(t, opt, "v3") { |
| 77 | t.Fatal("sweep did not release only the retired version") |
| 78 | } |
| 79 | if got, err := permit.ledger.Read(p.Binding); err != nil || got != p { |
| 80 | t.Fatal("pending V3 changed", err) |
| 81 | } |
| 82 | // Once swept, the retirement tombstone must still read without the old |
| 83 | // executable or tree, and must never permit the original launch again. |
| 84 | if _, err := source.StagedPins(t.Context(), opt); err != nil { |
| 85 | t.Fatal(err) |
| 86 | } |
| 87 | if _, err := permit.ledger.Reserve(r.Binding); err == nil { |
| 88 | t.Fatal("swept reservation replayed") |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | func TestPlan14InstalledPinClosure(t *testing.T) { |
| 93 | for _, state := range []string{"running", "controller-open", "closed"} { |
| 94 | t.Run(state, func(t *testing.T) { |
| 95 | f, disk, _ := generationLaunchFixture(t) |
| 96 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| 97 | t.Fatal(err) |
| 98 | } |
| 99 | r := readGenerationLaunch(t, disk) |
| 100 | if state != "running" { |
| 101 | if err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil { |
| 102 | t.Fatal(err) |
| 103 | } |
| 104 | } |
| 105 | if state == "closed" { |
| 106 | if err := f.journal.CloseWriters(t.Context(), f.target); err != nil { |
| 107 | t.Fatal(err) |
| 108 | } |
| 109 | } |
| 110 | calls := 0 |
| 111 | retire := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { |
| 112 | calls++ |
| 113 | return strings.Repeat("b", 64), nil |
| 114 | }} |
| 115 | ledger := &release.RootGenerations{Store: disk} |
| 116 | if err := retire.retire(t.Context(), ledger, r); err != nil { |
| 117 | t.Fatal(err) |
| 118 | } |
| 119 | got, err := ledger.Read(r.Binding) |
| 120 | if err != nil { |
| 121 | t.Fatal(err) |
| 122 | } |
| 123 | if (got.Retirement != "") != (state == "closed") || (calls > 0) != (state == "closed") { |
| 124 | t.Fatal("pin crossed incomplete closure", state, got, calls) |
| 125 | } |
| 126 | }) |
| 127 | } |
| 128 | } |
| 129 | |