carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package hostops
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "strings"
8 "testing"
9
10 "github.com/carlosframework/platform/internal/hostclaim"
11 "github.com/carlosframework/platform/internal/moves"
12 "github.com/carlosframework/platform/internal/provision"
13)
14
15// Plan 6 Task 3 fix rounds 3 and 4: the recovery pass's move log writer
16// (Recovery.MoveLogs) delivers the console's queued entries whatever their
17// moves now are. The entry names a host, a move, the instance incarnation the
18// move's evidence carried, and an entry name; the account and app come from
19// the host's claim, whose holder must be that incarnation, and the move must
20// be one its record names (live, or closed as its last). The name is one
21// segment and the entry's identity; the entry is at most MoveLogEntryMax
22// bytes and this move's and host's; the write is create-only, a replay of the
23// same bytes a no-op and other bytes ErrMoveLogTaken. It answers only during
24// its hook, inside a recovery pass.
25func TestMoveLogWriterWritesOnlyVerifiedEntries(t *testing.T) {
26 w := newC7World(t, func(c *hostclaim.Claim, r *provision.Record, _ *AppAdmission) {})
27 ctx := context.Background()
28 entry := func(op, host, id, actor string) []byte {
29 b, err := json.Marshal(moves.LogEvent{V: 1, Op: op, Host: host, Event: "x", Identity: id, Actor: actor, At: w.now})
30 if err != nil {
31 t.Fatal(err)
32 }
33 return b
34 }
35 good := MoveLogEntry{Host: w.host, Op: "move", Incarnation: w.inc, Name: "done", Body: entry("move", w.host, "done", "console")}
36 with := func(f func(*MoveLogEntry)) MoveLogEntry { e := good; f(&e); return e }
37 big := moves.LogEvent{V: 1, Op: "move", Host: w.host, Event: "x", Identity: "done", Fields: map[string]string{"pad": strings.Repeat("a", MoveLogEntryMax)}}
38 bigBody, _ := json.Marshal(big)
39 var saved *MoveLogWriter
40 var created, again bool
41 got := map[string]error{}
42 err := w.control.RunRecovery(ctx, func(ctx context.Context) error {
43 return runMoveLogDrain(ctx, func(ctx context.Context, lw *MoveLogWriter) error {
44 saved = lw
45 var err error
46 if created, err = lw.WriteMoveLog(ctx, good); err != nil {
47 return err
48 }
49 if again, err = lw.WriteMoveLog(ctx, good); err != nil {
50 return err
51 }
52 for what, e := range map[string]MoveLogEntry{
53 "other bytes": with(func(e *MoveLogEntry) { e.Body = entry("move", w.host, "done", "target") }),
54 "a stale incarnation (recreated under the same names)": with(func(e *MoveLogEntry) { e.Incarnation = strings.Repeat("ef", 32) }),
55 "a move its record does not name": with(func(e *MoveLogEntry) { e.Op = "forged"; e.Body = entry("forged", w.host, "done", "console") }),
56 "another move's entry": with(func(e *MoveLogEntry) { e.Body = entry("other", w.host, "done", "console") }),
57 "another host's entry": with(func(e *MoveLogEntry) { e.Body = entry("move", "elsewhere.example", "done", "console") }),
58 "a name that is not the identity": with(func(e *MoveLogEntry) { e.Name = "opened" }),
59 "a path": with(func(e *MoveLogEntry) { e.Name = "../source" }),
60 "a move id that is a path": with(func(e *MoveLogEntry) { e.Op = "../move" }),
61 "a host with no claim": with(func(e *MoveLogEntry) {
62 e.Host = "gone.example"
63 e.Body = entry("move", "gone.example", "done", "console")
64 }),
65 "too big": with(func(e *MoveLogEntry) { e.Body = bigBody }),
66 } {
67 _, got[what] = lw.WriteMoveLog(ctx, e)
68 }
69 return nil
70 })
71 })
72 if err != nil {
73 t.Fatal(err)
74 }
75 if !created || again {
76 t.Fatalf("created=%v, a replay of the same bytes created=%v", created, again)
77 }
78 for what, err := range got {
79 want := ErrIdentity
80 if what == "other bytes" {
81 want = ErrMoveLogTaken
82 }
83 if !errors.Is(err, want) {
84 t.Errorf("%s: %v, want %v", what, err, want)
85 }
86 }
87 prefix := moves.ReportPrefix("bab", "app", w.host, "move")
88 raw, _, err := w.raw.Get(ctx, moves.LogKeyFor(prefix, "done"))
89 if err != nil || string(raw) != string(good.Body) {
90 t.Fatalf("the object is not the entry's bytes: %s %v", raw, err)
91 }
92 if logged, err := moves.ReadLog(ctx, w.raw, prefix); err != nil || len(logged) != 1 {
93 t.Fatalf("log %+v %v", logged, err)
94 }
95 if _, err := saved.WriteMoveLog(ctx, with(func(e *MoveLogEntry) { e.Name = "late"; e.Body = entry("move", w.host, "late", "console") })); !errors.Is(err, ErrPending) {
96 t.Fatalf("a writer kept past its hook wrote: %v", err)
97 }
98 if err := runMoveLogDrain(ctx, func(context.Context, *MoveLogWriter) error { return nil }); !errors.Is(err, ErrContract) {
99 t.Fatalf("a writer minted outside a recovery pass: %v", err)
100 }
101}
102
103// A closed move is named by its record's last move alone, so the claim
104// holder's incarnation is what refuses a stale row there: the instance
105// recreated under the same names, whose record may still close the same
106// move id.
107func TestMoveLogWriterRefusesAStaleIncarnationOfAClosedMove(t *testing.T) {
108 w := newC7World(t, func(c *hostclaim.Claim, r *provision.Record, _ *AppAdmission) {
109 r.LastMove = &provision.MoveOutcome{ID: r.Move.ID, Outcome: "done", At: r.CreatedAt}
110 r.Move = nil
111 })
112 ctx := context.Background()
113 body, err := json.Marshal(moves.LogEvent{V: 1, Op: "move", Host: w.host, Event: "done", Identity: "done", Actor: "console", At: w.now})
114 if err != nil {
115 t.Fatal(err)
116 }
117 var stale, current error
118 err = w.control.RunRecovery(ctx, func(ctx context.Context) error {
119 return runMoveLogDrain(ctx, func(ctx context.Context, lw *MoveLogWriter) error {
120 _, stale = lw.WriteMoveLog(ctx, MoveLogEntry{Host: w.host, Op: "move", Incarnation: strings.Repeat("ef", 32), Name: "done", Body: body})
121 _, current = lw.WriteMoveLog(ctx, MoveLogEntry{Host: w.host, Op: "move", Incarnation: w.inc, Name: "done", Body: body})
122 return nil
123 })
124 })
125 if err != nil {
126 t.Fatal(err)
127 }
128 if !errors.Is(stale, ErrIdentity) || current != nil {
129 t.Fatalf("stale incarnation: %v (want refused); current: %v", stale, current)
130 }
131}
132