package host import ( "context" "crypto/ed25519" "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "go/ast" "go/parser" "go/token" "strings" "testing" "time" "github.com/carlosframework/platform/internal/domains" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) func TestClaimBackfillRunsOnlyOnInstancesCadence(t *testing.T) { file, err := parser.ParseFile(token.NewFileSet(), "agent.go", nil, 0) if err != nil { t.Fatal(err) } for _, decl := range file.Decls { fn, ok := decl.(*ast.FuncDecl) if !ok { continue } switch fn.Name.Name { case "fastConvergeLoop": ast.Inspect(fn.Body, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return true } id, ok := call.Fun.(*ast.Ident) if ok && (id.Name == "reconcileBackfill" || id.Name == "newClaimBackfill") { t.Error("backfill runs on the serial promote path") } return true }) case "instancesLoop": constructors := 0 for _, stmt := range fn.Body.List { loop, inLoop := stmt.(*ast.ForStmt) var node ast.Node = stmt if inLoop { node = loop.Body } ast.Inspect(node, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return true } id, ok := call.Fun.(*ast.Ident) if ok && id.Name == "newClaimBackfill" { constructors++ if inLoop { t.Error("backfill controller constructed per tick") } } return true }) } if constructors != 1 { t.Errorf("instances loop has %d persistent backfill constructors, want 1", constructors) } } } } type backfillPassStore struct { release.Store gets, censusReads int failClaim error } func (s *backfillPassStore) Get(ctx context.Context, key string) ([]byte, string, error) { s.gets++ if strings.HasPrefix(key, "control/host-census/") { s.censusReads++ } return s.Store.Get(ctx, key) } func (s *backfillPassStore) Put(ctx context.Context, key string, body []byte, tag string) (string, error) { if strings.HasPrefix(key, "control/hosts/") && s.failClaim != nil { err := s.failClaim s.failClaim = nil return "", err } return s.Store.Put(ctx, key, body, tag) } func TestClaimBackfillPassInertAndMissingContract(t *testing.T) { store := &backfillPassStore{Store: release.NewDirStore(t.TempDir())} ctx := context.Background() if err := newClaimBackfill(hostwrite.Config{}, store, nil, "")(ctx); err != nil || store.gets != 0 { t.Fatal("flag off accessed backfill", err, store.gets) } for _, missing := range []string{"store", "key", "bucket"} { var s release.Store = store pub := make(ed25519.PublicKey, ed25519.PublicKeySize) bucket := "bucket" switch missing { case "store": s = nil case "key": pub = nil case "bucket": bucket = "" } if err := newClaimBackfill(hostwrite.Config{Enabled: true}, s, pub, bucket)(ctx); !errors.Is(err, hostops.ErrContract) || store.gets != 0 { t.Fatalf("missing %s: err=%v reads=%d", missing, err, store.gets) } } } func TestClaimBackfillPassKeepsCensusBetweenRetries(t *testing.T) { ctx := context.Background() store := &backfillPassStore{Store: release.NewDirStore(t.TempDir())} pub, key, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } boxPub, boxKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } now := time.Now().UTC() put := func(path string, value any, signed bool) { t.Helper() raw, err := json.Marshal(value) if signed { raw, err = release.SignRootEnvelope(release.RootSigner(key), value) } if err != nil { t.Fatal(err) } if _, err := store.Put(ctx, path, raw, ""); err != nil { t.Fatal(err) } } enrollment := strings.Repeat("a", 32) roster := hostactors.Roster{V: 1, DeploymentBucket: "bucket", Revision: 1, Regions: map[string]hostactors.Region{"ie": {}}, Boxes: map[string]hostactors.Box{"box-a": {CensusPublicKey: boxPub, Enrollment: enrollment, Namespace: "platform", BoxID: "box-a", Label: "box-a", Region: "ie", Pool: "default", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "ie", InstanceID: "i-box-a"}, EvidenceSHA256: strings.Repeat("c", 64), State: "active"}}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} put(hostactors.Key, roster, true) r, err := hostops.PrepareInstance(provision.Record{Schema: 1, Account: "bab", App: "jam", Host: "legacy.example.com", Slug: "shop", Channel: "edge", Region: "ie", Acquisition: "acquisition"}, now, "ie") if err != nil { t.Fatal(err) } if _, err := store.Put(ctx, r.Holder.Key, r.Body, ""); err != nil { t.Fatal(err) } r, err = hostops.ReadRecord(ctx, store, r.Holder.Key) if err != nil { t.Fatal(err) } inc, err := r.Incarnation() if err != nil { t.Fatal(err) } put(domains.ClaimKey(r.Holder.Host), domains.Claim{Schema: 1, Host: r.Holder.Host, Account: "bab", App: "jam", Acquisition: "acquisition", RecordIncarnation: inc, OwnershipState: "committed"}, false) report := hostops.CensusReport{Box: "box-a", Enrollment: enrollment, BootID: "boot", Reservation: "backfill", DirectoryRevision: 1, CapturedAt: now, Complete: true, Installations: []hostops.CensusInstallation{{Host: r.Holder.Host, Account: "bab", App: "jam", Family: "instance", Key: r.Holder.Key, Incarnation: inc, Local: true, Route: true}}} canonical, err := json.Marshal(report) if err != nil { t.Fatal(err) } reportPath := "control/host-census/backfill/box-a.json" put(reportPath, hostops.CensusEnvelope{Report: report, Signature: ed25519.Sign(boxKey, canonical)}, false) raw, _, err := store.Get(ctx, reportPath) if err != nil { t.Fatal(err) } digest := sha256.Sum256(raw) put(hostops.BackfillKey, hostops.BackfillPlan{V: 1, ID: "backfill", Phase: "adopting", DirectoryRevision: 1, CreatedAt: now, Reports: map[string]string{"box-a": hex.EncodeToString(digest[:])}, Hosts: map[string]hostops.BackfillHost{r.Holder.Host: {Key: r.Holder.Key, ETag: r.ETag, Acquisition: "acquisition", Region: "ie", Box: "box-a"}}}, true) store.censusReads = 0 pass := newClaimBackfill(hostwrite.Config{Enabled: true}, store, pub, "bucket") fail := errors.New("claim write failed") store.failClaim = fail if err := pass(ctx); !errors.Is(err, fail) { t.Fatalf("first pass: %v", err) } if err := pass(ctx); err != nil { t.Fatalf("retry: %v", err) } if store.censusReads != 1 { t.Fatalf("census reads=%d, want one across both passes", store.censusReads) } obs, err := hostclaim.Read(ctx, store, r.Holder.Host) if err != nil || obs.Absent || obs.Claim.Holder.Acquisition != "acquisition" || obs.Claim.Op.Exec != "box-a" { t.Fatalf("retry did not adopt: %+v %v", obs, err) } } func TestClaimBackfillPassQuietWithoutAdoptingPlan(t *testing.T) { for _, phase := range []string{"absent", "collecting", "done", "future"} { t.Run(phase, func(t *testing.T) { ctx := context.Background() pub, key, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } store := &backfillPassStore{Store: release.NewDirStore(t.TempDir())} if phase != "absent" { raw, err := release.SignRootEnvelope(release.RootSigner(key), hostops.BackfillPlan{V: 1, ID: "backfill", Phase: phase, CreatedAt: time.Now()}) if err != nil { t.Fatal(err) } if _, err := store.Put(ctx, hostops.BackfillKey, raw, ""); err != nil { t.Fatal(err) } } pass := newClaimBackfill(hostwrite.Config{Enabled: true}, store, pub, "bucket") if err := pass(ctx); err != nil || store.gets != 1 { t.Fatalf("inactive backfill: err=%v reads=%d", err, store.gets) } }) } }