package host import ( "context" "os" "path/filepath" "strings" "testing" "time" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) func TestPlan14GenerationRetirementReleasesExactPin(t *testing.T) { f, disk, _ := generationLaunchFixture(t) permit, err := f.root.generations.reserve(t.Context(), f.root, f.peer, f.req) if err != nil { t.Fatal(err) } r := permit.reservation opt := f.root.generations.optDir stageSweepVersion(t, opt, "bap", "v2", "other", 48*time.Hour) stageSweepVersion(t, opt, "bap", "v3", "pending", 48*time.Hour) pending := r.Binding pending.Attempt, pending.Invocation = "pending-v3", "pending-v3" pending.Binding.Account, pending.Binding.App, pending.Version = "bap", "pending", "v3" pending.Path = release.VersionPath(opt, "bap", "v3", "pending") pending.Tree = filepath.Dir(pending.Path) for path, dst := range map[string]*rootstate.FileIdentity{pending.Path: &pending.Executable, pending.Tree: &pending.TreeIdentity} { file, err := os.Open(path) if err != nil { t.Fatal(err) } *dst, err = rootstate.Identity(file) file.Close() if err != nil { t.Fatal(err) } } p, err := permit.ledger.Reserve(pending) if err != nil { t.Fatal(err) } when := time.Now().Add(-48 * time.Hour) if err := os.Chtimes(r.Binding.Path, when, when); err != nil { t.Fatal(err) } source := generationPinSource{open: func() (rootstate.Store, error) { return disk, nil }, verify: func(context.Context, []release.GenerationReservation) error { return nil }} // The injected verifier exercises the destructive consumer contract; this // fixture does not establish production installation/kernel completeness. rows := fakeLister{routes: []registry.Route{{Host: "other.example", Account: "bap", App: "other", Version: "v2"}}} gen := noGenerations(t) sweepStagedVersions(rows, opt, gen, 6*time.Hour, time.Now(), source) if !staged(t, opt, "v1") { t.Fatal("pending generation lost pin") } 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 { t.Fatal("retired without kernel witness") } sweepStagedVersions(rows, opt, gen, 6*time.Hour, time.Now(), source) if !staged(t, opt, "v1") { t.Fatal("incomplete retirement lost pin") } if err := permit.ledger.Retire(t.Context(), r, func(context.Context, release.GenerationReservation) error { return nil }, func(context.Context, release.GenerationReservation) (string, error) { return strings.Repeat("a", 64), nil }); err != nil { t.Fatal(err) } sweepStagedVersions(rows, opt, gen, 72*time.Hour, time.Now(), source) if !staged(t, opt, "v1") { t.Fatal("retirement bypassed sweep grace") } sweepStagedVersions(rows, opt, gen, 6*time.Hour, time.Now(), source) if staged(t, opt, "v1") || !staged(t, opt, "v2") || !staged(t, opt, "v3") { t.Fatal("sweep did not release only the retired version") } if got, err := permit.ledger.Read(p.Binding); err != nil || got != p { t.Fatal("pending V3 changed", err) } // Once swept, the retirement tombstone must still read without the old // executable or tree, and must never permit the original launch again. if _, err := source.StagedPins(t.Context(), opt); err != nil { t.Fatal(err) } if _, err := permit.ledger.Reserve(r.Binding); err == nil { t.Fatal("swept reservation replayed") } } func TestPlan14InstalledPinClosure(t *testing.T) { for _, state := range []string{"running", "controller-open", "closed"} { t.Run(state, func(t *testing.T) { f, disk, _ := generationLaunchFixture(t) if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { t.Fatal(err) } r := readGenerationLaunch(t, disk) if state != "running" { if err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil { t.Fatal(err) } } if state == "closed" { if err := f.journal.CloseWriters(t.Context(), f.target); err != nil { t.Fatal(err) } } calls := 0 retire := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { calls++ return strings.Repeat("b", 64), nil }} ledger := &release.RootGenerations{Store: disk} if err := retire.retire(t.Context(), ledger, r); err != nil { t.Fatal(err) } got, err := ledger.Read(r.Binding) if err != nil { t.Fatal(err) } if (got.Retirement != "") != (state == "closed") || (calls > 0) != (state == "closed") { t.Fatal("pin crossed incomplete closure", state, got, calls) } }) } }