package release import ( "context" "errors" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/rootstate" ) func TestPlan14GenerationRetirementReleasesExactPin(t *testing.T) { f := newReuseFixture(t) g, r := generationPinFixture(t, f.opt, "bab", "myapp", "v2") pending := r.Binding pending.Attempt, pending.Invocation = "pending-v3", "pending-v3" pending.Version = "v3" pending.Path = VersionPath(f.opt, "bap", "v3", "other") pending.Binding.Account, pending.Binding.App = "bap", "other" pending.Tree = filepath.Dir(pending.Path) if err := StageArtifact(f.opt, "bap", "v3", "other", []byte("pending")); err != nil { t.Fatal(err) } 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 := g.Reserve(pending) if err != nil { t.Fatal(err) } closed, verified := 0, 0 err = g.Retire(t.Context(), r, func(context.Context, GenerationReservation) error { closed++; return nil }, func(_ context.Context, got GenerationReservation) (string, error) { verified++ if !got.Closed || got.ID != r.ID { t.Fatal("kernel checked before durable close") } if err := g.Check(r); err == nil { t.Fatal("closed authority can launch") } return strings.Repeat("a", 64), nil }) if err != nil || closed != 1 || verified != 1 { t.Fatalf("retire: %v %d %d", err, closed, verified) } source := RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }} pins, err := source.StagedPins(t.Context(), f.opt) if err != nil || pins.Has(r.Binding.Path) || !pins.Has(p.Binding.Path) { t.Fatalf("wrong pins: %v %v", pins, err) } if got, err := g.Read(p.Binding); err != nil || got != p { t.Fatalf("pending reservation changed: %+v %v", got, err) } if _, err := g.Reserve(r.Binding); err == nil { t.Fatal("retired request replay authorized launch") } if err := g.Started(r, GenerationProcess{PID: 51, TenantPID: 52, Start: "51", TenantStart: "52", Group: "system.slice/run.service", Invocation: "unit", GroupIdentity: rootstate.FileIdentity{Device: 1, Inode: 2}}); err == nil { t.Fatal("retired reservation recorded a late launch") } f.pass(t, WithRootGenerationPins(source)) if got := f.stagedV2(t); got != "new v2" { t.Fatalf("exact retirement failed to release restage: %q", got) } } func TestPlan14GenerationRetirementRefusals(t *testing.T) { for _, attack := range []string{"join", "kernel", "close-sync", "retire-sync", "wrong-id", "wrong-binding", "no-close", "no-kernel", "bad-evidence"} { t.Run(attack, func(t *testing.T) { g, s, b := generationFixture(t) r, err := g.Reserve(b) if err != nil { t.Fatal(err) } expected := r close := func(context.Context, GenerationReservation) error { return nil } verify := func(context.Context, GenerationReservation) (string, error) { return strings.Repeat("b", 64), nil } switch attack { case "join": close = func(context.Context, GenerationReservation) error { return os.ErrPermission } case "kernel": verify = func(context.Context, GenerationReservation) (string, error) { return "", os.ErrPermission } case "close-sync": close = func(context.Context, GenerationReservation) error { s.fail = 1; return nil } case "retire-sync": verify = func(context.Context, GenerationReservation) (string, error) { s.fail = 1 return strings.Repeat("b", 64), nil } case "wrong-id": expected.ID = strings.Repeat("c", 64) case "wrong-binding": expected.Binding.ControllerStart = "other" case "no-close": close = nil case "no-kernel": verify = nil case "bad-evidence": verify = func(context.Context, GenerationReservation) (string, error) { return "complete", nil } } if err := g.Retire(t.Context(), expected, close, verify); err == nil { t.Fatal("retirement accepted incomplete evidence") } // A failed final sync may have made the evidence visible. It must be // recommitted before a reader can release its pin. if attack == "retire-sync" { s.fail = 1 if _, err := g.Read(b); err == nil { t.Fatal("uncommitted retirement granted read") } } got, err := g.Read(b) if err != nil { t.Fatal(err) } if attack != "retire-sync" && got.Retirement != "" { t.Fatal("unverified retirement released pin") } if got.ID != r.ID || got.Binding != r.Binding { t.Fatal("retirement changed identity") } if got.Closed && !errors.Is(g.Check(r), os.ErrPermission) { t.Fatal("closed request remained launchable") } }) } } func TestPlan14GenerationRetirementKeepsIndependentReference(t *testing.T) { f := newReuseFixture(t) g, r := generationPinFixture(t, f.opt, "bab", "myapp", "v2") b := r.Binding b.Attempt = "independent" b.Invocation = "independent" other, err := g.Reserve(b) if err != nil { t.Fatal(err) } if err := g.Retire(t.Context(), r, func(context.Context, GenerationReservation) error { return nil }, func(context.Context, GenerationReservation) (string, error) { return strings.Repeat("a", 64), nil }); err != nil { t.Fatal(err) } source := RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }} f.pass(t, WithRootGenerationPins(source)) if got := f.stagedV2(t); got != "old v2" { t.Fatalf("independent reference overwritten: %q", got) } if got, err := g.Read(other.Binding); err != nil || got != other { t.Fatal("other reference changed", err) } source.Verify = nil if _, err := source.StagedPins(t.Context(), f.opt); err == nil { t.Fatal("retirement supplied completeness") } } func TestPlan14GenerationClaimsOffRestageMatchesMain(t *testing.T) { for _, pinned := range []bool{false, true} { type result struct { bytes, live, version, previous string restarts int } run := func(withRoot bool) result { f := newReuseFixture(t) if pinned { if err := f.reg.Upsert(registry.Route{Host: "pending.example", Account: "bab", App: "myapp", Version: "v2", Status: "creating"}); err != nil { t.Fatal(err) } } var opts []PassOption if withRoot { opts = append(opts, WithRootGenerationPins(PinSourceFunc(func(context.Context, string) (StagedPaths, error) { return StagedPaths{}, nil }))) } restarts := 0 if err := Pass(t.Context(), f.reg, f.s, f.pub, f.opt, func(string) error { restarts++; return nil }, opts...); err != nil { t.Fatal(err) } live, err := LiveVersion(f.opt, "h1.example.com") if err != nil { t.Fatal(err) } row, err := f.reg.Get("h1.example.com") if err != nil || row == nil { t.Fatal(err) } return result{f.stagedV2(t), live, row.Version, row.PrevVersion, restarts} } main, branch := run(false), run(true) if main != branch { t.Fatalf("claims-off changed pinned=%v: main=%+v branch=%+v", pinned, main, branch) } if !pinned && (branch.bytes != "new v2" || branch.live != "v2" || branch.restarts != 1) { t.Fatalf("positive restage refused: %+v", branch) } } } func TestPlan14GenerationClosedReplayRefused(t *testing.T) { for _, started := range []bool{false, true} { g, _, b := generationFixture(t) r, err := g.Reserve(b) if err != nil { t.Fatal(err) } process := GenerationProcess{PID: 51, TenantPID: 52, Start: "51", TenantStart: "52", Group: "system.slice/run.service", Invocation: "unit", GroupIdentity: rootstate.FileIdentity{Device: 1, Inode: 2}} if started { if err := g.Started(r, process); err != nil { t.Fatal(err) } r, err = g.Read(b) if err != nil { t.Fatal(err) } } if err := g.Retire(t.Context(), r, func(context.Context, GenerationReservation) error { return nil }, func(_ context.Context, closed GenerationReservation) (string, error) { if err := g.Check(closed); err == nil { t.Fatal("closed reservation granted a spawn") } if err := g.Started(r, process); err == nil { t.Fatal("closed reservation accepted a started replay") } return strings.Repeat("a", 64), nil }); err != nil { t.Fatal(err) } } }