package host import ( "context" "encoding/json" "errors" "fmt" "os" "path/filepath" "testing" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" execlease "github.com/carlosframework/platform/internal/hostexec" "github.com/carlosframework/platform/internal/registry" ) type staleClaimReader func(context.Context, string) (hostclaim.Observed, error) func (f staleClaimReader) Read(ctx context.Context, host string) (hostclaim.Observed, error) { return f(ctx, host) } func TestPlan9PreviousAgentRetiredParticipation(t *testing.T) { t.Parallel() for _, mode := range []string{"free", "free-lock-held", "free-without-history", "replacement", "replacement-artifact", "missing-retirement", "artifact", "launch", "fresh-retirement-missing", "fresh-absent", "root", "adopt"} { t.Run(mode, func(t *testing.T) { f := newRecoveryFixture(t) ctx := t.Context() old := f.read() inc := old.Claim.Holder.Incarnation if mode == "launch" { f.controller("boot-old", "controller-old", 901, "101").admit(t, f) } o, err := hostclaim.Update(ctx, f.claims, f.read(), hostclaim.BeginDelete("delete", "console", "record-etag", f.now, nil, nil, "")) if err != nil { t.Fatal(err) } o, err = hostclaim.Update(ctx, f.claims, o, hostclaim.ToParking("delete", nil, nil, f.newID)) if err != nil { t.Fatal(err) } for len(o.Claim.Obligations) > 0 { chore := o.Claim.Obligations[0] o, err = hostclaim.Update(ctx, f.claims, o, hostclaim.CompleteObligation(chore.Op, chore.ID)) if err != nil { t.Fatal(err) } } o, err = hostclaim.Update(ctx, f.claims, o, hostclaim.FreeAfterPark("delete")) if err != nil || !o.Claim.Free() { t.Fatalf("FreeAfterPark: %+v %v", o, err) } if mode == "free-without-history" { o, err = hostclaim.Update(ctx, f.claims, o, func(c *hostclaim.Claim) error { c.RetiredIncarnations = nil; return nil }) if err != nil { t.Fatal(err) } } if mode == "replacement" || mode == "replacement-artifact" || mode == "missing-retirement" || mode == "fresh-retirement-missing" { created := f.now.Add(time.Hour) in := hostclaim.NewHolder{Family: "instance", Key: old.Claim.Holder.Key, Account: "bab", App: "jam", Host: recoveryHost, Region: "ie", RecordRegion: "ie", CreatedAt: created} body := []byte(fmt.Sprintf(`{"schema":1,"host":%q,"account":"bab","app":"jam","region":"ie","created_at":%q}`, recoveryHost, created.Format(time.RFC3339))) o, err = hostclaim.Update(ctx, f.claims, o, hostclaim.BeginCreate(in, body, "create-next", "console", created)) if err != nil { t.Fatal(err) } o, err = hostclaim.Update(ctx, f.claims, o, hostclaim.CompleteCreate("create-next", "box-2")) if err != nil { t.Fatal(err) } if mode == "missing-retirement" { o, err = hostclaim.Update(ctx, f.claims, o, func(c *hostclaim.Claim) error { c.RetiredIncarnations = nil; return nil }) if err != nil { t.Fatal(err) } } } inventory, err := openLegacyInventory(t, filepath.Join(t.TempDir(), "inventory.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { inventory.Close() }) reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { reg.Close() }) census := &hostbox.EmptyInstallationCensus{Inventory: inventory, Registry: reg, Launches: f.journal, Box: "box-1", Enrollment: "enrollment-1", DirectoryRevision: "7", Roots: map[string]string{"data": f.guard.DataDir, "opt": t.TempDir(), "units": t.TempDir()}, SharedPaths: map[string][]string{"data": {".hostexec", "hostguard"}}} p := &productionClaimLocal{legacyParticipation: true, Resources: ClaimRuntimeResources{Mode: "agent", DataDir: f.guard.DataDir}, lifetime: ctx, assembly: &ClaimAssembly{Wake: &ClaimWakeAssembly{}, Launches: f.journal, Reconcile: &hostbox.ReconcileAdapter{Census: census}}} state := execlease.State{V: 1, Binding: execlease.Binding{Host: recoveryHost, Box: "box-1", Enrollment: "enrollment-1", Incarnation: inc, Op: "old-op", BootID: "old-boot", ControllerID: "old-controller"}, State: "lost", SafeUntil: 1, Expires: time.Now()} dir := filepath.Join(f.guard.DataDir, ".hostexec") if err = os.Mkdir(dir, 0700); err != nil { t.Fatal(err) } body, _ := json.Marshal(state) path := filepath.Join(dir, recoveryHost+".json") if err = os.WriteFile(path, body, 0600); err != nil { t.Fatal(err) } if mode == "artifact" || mode == "replacement-artifact" { // Even complete previous-installation metadata cannot bypass the full scan. if err = inventory.WriteStopInventory(ctx, hostbox.StopInventory{Complete: true, Host: recoveryHost, Incarnation: inc, InstallationStamp: old.Claim.Holder.Stamp, Box: "box-1", Enrollment: "enrollment-1", DirectoryRevision: "7"}); err != nil { t.Fatal(err) } if err = os.WriteFile(filepath.Join(census.Roots["opt"], "leftover"), []byte("data"), 0600); err != nil { t.Fatal(err) } } cfg := execlease.Config{Binding: state.Binding, Guard: f.guard, LockDir: t.TempDir()} if err = census.Initialize(ctx, o, hostbox.RecordSelection{Host: recoveryHost, Incarnation: inc}); !errors.Is(err, hostbox.ErrChanged) { t.Fatalf("cleanup scope initialized replacement inventory: %v", err) } if mode == "free-lock-held" { original := cfg.Guard.Claims competing := cfg cfg.Guard.Claims = staleClaimReader(func(ctx context.Context, host string) (hostclaim.Observed, error) { deadline, cancel := context.WithTimeout(ctx, 25*time.Millisecond) defer cancel() if err := execlease.ClearStale(deadline, competing); !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("fresh proof did not retain lifetime lock: %v", err) } return original.Read(ctx, host) }) } if mode == "fresh-retirement-missing" || mode == "fresh-absent" { cfg.Guard.Claims = staleClaimReader(func(ctx context.Context, host string) (hostclaim.Observed, error) { live, err := execlease.InspectLocal(f.guard.DataDir, host) if err != nil || live.State != "lost" { t.Fatalf("fresh proof without durable denial: %+v %v", live, err) } if mode == "fresh-absent" { return hostclaim.Observed{Host: host, Absent: true}, nil } fresh := o c := *o.Claim c.RetiredIncarnations = nil fresh.Claim = &c return fresh, nil }) } actor := hostbox.Actor{Box: "box-1", Enrollment: "enrollment-1"} if mode == "root" || mode == "adopt" { p.Resources.Mode = mode err = p.prepareWith(ctx, actor, o) } else { err = p.clearUntracked(ctx, actor, o, state, cfg) } wantOK := mode == "free" || mode == "free-lock-held" || mode == "free-without-history" || mode == "replacement" if wantOK { if err != nil { t.Fatalf("positive stale cleanup refused: %v", err) } if _, err = os.Stat(path); !errors.Is(err, os.ErrNotExist) { t.Fatalf("stale state retained: %v", err) } } else { if err == nil { t.Fatal("unproven stale cleanup accepted") } if _, err = os.Stat(path); err != nil { t.Fatalf("refusal removed local denial: %v", err) } } }) } }