| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "sort" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/hostclaim" |
| 12 | ) |
| 13 | |
| 14 | // retirementsByOp is how F3 finds a host's local retirements by op, one rule |
| 15 | // for both receipts a retirement can carry (K20): park.Local's .journal.json |
| 16 | // (Op) and the moves-owned rename's moves-retirement.json (op), each in the |
| 17 | // event directory .parked/<incarnation>/<host>/<retirement>/. It returns the |
| 18 | // retirement ids whose receipt names op, and those with no readable receipt. |
| 19 | // This is the one copy of the rule; internal/park's test checks only that |
| 20 | // park.Local's journal carries Op. |
| 21 | func retirementsByOp(t *testing.T, dataDir, incarnation, host, op string) (found, unreadable []string) { |
| 22 | t.Helper() |
| 23 | dir := filepath.Join(dataDir, ".parked", incarnation, host) |
| 24 | es, err := os.ReadDir(dir) |
| 25 | if err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | for _, e := range es { |
| 29 | var engine struct{ Op string } |
| 30 | var owned struct { |
| 31 | Op string `json:"op"` |
| 32 | } |
| 33 | named := "" |
| 34 | if b, err := os.ReadFile(filepath.Join(dir, e.Name(), ".journal.json")); err == nil && json.Unmarshal(b, &engine) == nil { |
| 35 | named = engine.Op |
| 36 | } else if b, err := os.ReadFile(filepath.Join(dir, e.Name(), localParkReceiptName)); err == nil && json.Unmarshal(b, &owned) == nil { |
| 37 | named = owned.Op |
| 38 | } else { |
| 39 | unreadable = append(unreadable, e.Name()) |
| 40 | continue |
| 41 | } |
| 42 | if named == op { |
| 43 | found = append(found, e.Name()) |
| 44 | } |
| 45 | } |
| 46 | sort.Strings(found) |
| 47 | return found, unreadable |
| 48 | } |
| 49 | |
| 50 | // TestMoveLocalRetirementsAreFindableByOp (moves spec §6.4, D6, K20): a |
| 51 | // move leaves two local receipts, not one. A registered source's |
| 52 | // finalization retires its installation through park.Local (.journal.json) |
| 53 | // and parks what the inventory does not list by the moves-owned rename |
| 54 | // (moves-retirement.json), each in its own retirement under the engine's |
| 55 | // layout. Both name the move's op, and nothing under the host's |
| 56 | // incarnation lacks a receipt, so F3 finds every local retirement of the |
| 57 | // move by op with one rule. |
| 58 | func TestMoveLocalRetirementsAreFindableByOp(t *testing.T) { |
| 59 | t.Parallel() |
| 60 | h := newFinalHarness(t) |
| 61 | dd := h.cfg.DataDir |
| 62 | for _, d := range []string{"hostguard", ".parked"} { |
| 63 | if err := os.MkdirAll(filepath.Join(dd, d), 0o770); err != nil { |
| 64 | t.Fatal(err) |
| 65 | } |
| 66 | } |
| 67 | rec := `{"v":1,"host":"` + p1Host + `","incarnation":"` + h.inc + `","active_stamp":1,"retired_stamps":[]}` |
| 68 | if err := os.WriteFile(filepath.Join(dd, "hostguard", p1Host+".json"), []byte(rec), 0o660); err != nil { |
| 69 | t.Fatal(err) |
| 70 | } |
| 71 | rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) { |
| 72 | if c.Inventory == nil { |
| 73 | c.Inventory = map[string]hostclaim.InventoryEntry{} |
| 74 | } |
| 75 | c.Inventory["ie-box"] = hostclaim.InventoryEntry{Layout: "flat", Paths: map[string]uint64{p1Host + ".db": 1, p1Host + ".db.gen": 1, p1Host: 1, p1Host + ".env": 1}} |
| 76 | }) |
| 77 | h.commit(t) |
| 78 | var chore hostclaim.Obligation |
| 79 | for _, o := range h.claim(t).Obligations { |
| 80 | if o.Kind == "park-local" { |
| 81 | chore = o |
| 82 | } |
| 83 | } |
| 84 | if chore.Op != p1ID { |
| 85 | t.Fatalf("the commit queued no park-local of the move: %+v", h.claim(t).Obligations) |
| 86 | } |
| 87 | h.sourcePass(t) |
| 88 | owned := h.parkedJournal(t).Steps[finalStepLocal] |
| 89 | if owned == "" || owned == chore.Retirement { |
| 90 | t.Fatalf("the moves-owned retirement %q, park.Local's %q", owned, chore.Retirement) |
| 91 | } |
| 92 | for _, receipt := range []string{filepath.Join(chore.Retirement, ".journal.json"), filepath.Join(owned, localParkReceiptName)} { |
| 93 | if _, err := os.Stat(filepath.Join(dd, ".parked", h.inc, p1Host, receipt)); err != nil { |
| 94 | t.Fatalf("the receipt %s: %v", receipt, err) |
| 95 | } |
| 96 | } |
| 97 | found, unreadable := retirementsByOp(t, dd, h.inc, p1Host, p1ID) |
| 98 | want := []string{chore.Retirement, owned} |
| 99 | sort.Strings(want) |
| 100 | if strings.Join(found, " ") != strings.Join(want, " ") || len(unreadable) != 0 { |
| 101 | t.Fatalf("by op %s: found %v (want %v), no receipt %v", p1ID, found, want, unreadable) |
| 102 | } |
| 103 | if other, _ := retirementsByOp(t, dd, h.inc, p1Host, "mv_zzzzzzzzzzzzzzzzzzzzzzzzzz"); len(other) != 0 { |
| 104 | t.Fatalf("another op found the move's retirements: %v", other) |
| 105 | } |
| 106 | } |
| 107 | |