package hostops import ( "context" "encoding/json" "errors" "strings" "testing" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/moves" "github.com/carlosframework/platform/internal/provision" ) // Plan 6 Task 3 fix rounds 3 and 4: the recovery pass's move log writer // (Recovery.MoveLogs) delivers the console's queued entries whatever their // moves now are. The entry names a host, a move, the instance incarnation the // move's evidence carried, and an entry name; the account and app come from // the host's claim, whose holder must be that incarnation, and the move must // be one its record names (live, or closed as its last). The name is one // segment and the entry's identity; the entry is at most MoveLogEntryMax // bytes and this move's and host's; the write is create-only, a replay of the // same bytes a no-op and other bytes ErrMoveLogTaken. It answers only during // its hook, inside a recovery pass. func TestMoveLogWriterWritesOnlyVerifiedEntries(t *testing.T) { w := newC7World(t, func(c *hostclaim.Claim, r *provision.Record, _ *AppAdmission) {}) ctx := context.Background() entry := func(op, host, id, actor string) []byte { b, err := json.Marshal(moves.LogEvent{V: 1, Op: op, Host: host, Event: "x", Identity: id, Actor: actor, At: w.now}) if err != nil { t.Fatal(err) } return b } good := MoveLogEntry{Host: w.host, Op: "move", Incarnation: w.inc, Name: "done", Body: entry("move", w.host, "done", "console")} with := func(f func(*MoveLogEntry)) MoveLogEntry { e := good; f(&e); return e } big := moves.LogEvent{V: 1, Op: "move", Host: w.host, Event: "x", Identity: "done", Fields: map[string]string{"pad": strings.Repeat("a", MoveLogEntryMax)}} bigBody, _ := json.Marshal(big) var saved *MoveLogWriter var created, again bool got := map[string]error{} err := w.control.RunRecovery(ctx, func(ctx context.Context) error { return runMoveLogDrain(ctx, func(ctx context.Context, lw *MoveLogWriter) error { saved = lw var err error if created, err = lw.WriteMoveLog(ctx, good); err != nil { return err } if again, err = lw.WriteMoveLog(ctx, good); err != nil { return err } for what, e := range map[string]MoveLogEntry{ "other bytes": with(func(e *MoveLogEntry) { e.Body = entry("move", w.host, "done", "target") }), "a stale incarnation (recreated under the same names)": with(func(e *MoveLogEntry) { e.Incarnation = strings.Repeat("ef", 32) }), "a move its record does not name": with(func(e *MoveLogEntry) { e.Op = "forged"; e.Body = entry("forged", w.host, "done", "console") }), "another move's entry": with(func(e *MoveLogEntry) { e.Body = entry("other", w.host, "done", "console") }), "another host's entry": with(func(e *MoveLogEntry) { e.Body = entry("move", "elsewhere.example", "done", "console") }), "a name that is not the identity": with(func(e *MoveLogEntry) { e.Name = "opened" }), "a path": with(func(e *MoveLogEntry) { e.Name = "../source" }), "a move id that is a path": with(func(e *MoveLogEntry) { e.Op = "../move" }), "a host with no claim": with(func(e *MoveLogEntry) { e.Host = "gone.example" e.Body = entry("move", "gone.example", "done", "console") }), "too big": with(func(e *MoveLogEntry) { e.Body = bigBody }), } { _, got[what] = lw.WriteMoveLog(ctx, e) } return nil }) }) if err != nil { t.Fatal(err) } if !created || again { t.Fatalf("created=%v, a replay of the same bytes created=%v", created, again) } for what, err := range got { want := ErrIdentity if what == "other bytes" { want = ErrMoveLogTaken } if !errors.Is(err, want) { t.Errorf("%s: %v, want %v", what, err, want) } } prefix := moves.ReportPrefix("bab", "app", w.host, "move") raw, _, err := w.raw.Get(ctx, moves.LogKeyFor(prefix, "done")) if err != nil || string(raw) != string(good.Body) { t.Fatalf("the object is not the entry's bytes: %s %v", raw, err) } if logged, err := moves.ReadLog(ctx, w.raw, prefix); err != nil || len(logged) != 1 { t.Fatalf("log %+v %v", logged, err) } if _, err := saved.WriteMoveLog(ctx, with(func(e *MoveLogEntry) { e.Name = "late"; e.Body = entry("move", w.host, "late", "console") })); !errors.Is(err, ErrPending) { t.Fatalf("a writer kept past its hook wrote: %v", err) } if err := runMoveLogDrain(ctx, func(context.Context, *MoveLogWriter) error { return nil }); !errors.Is(err, ErrContract) { t.Fatalf("a writer minted outside a recovery pass: %v", err) } } // A closed move is named by its record's last move alone, so the claim // holder's incarnation is what refuses a stale row there: the instance // recreated under the same names, whose record may still close the same // move id. func TestMoveLogWriterRefusesAStaleIncarnationOfAClosedMove(t *testing.T) { w := newC7World(t, func(c *hostclaim.Claim, r *provision.Record, _ *AppAdmission) { r.LastMove = &provision.MoveOutcome{ID: r.Move.ID, Outcome: "done", At: r.CreatedAt} r.Move = nil }) ctx := context.Background() body, err := json.Marshal(moves.LogEvent{V: 1, Op: "move", Host: w.host, Event: "done", Identity: "done", Actor: "console", At: w.now}) if err != nil { t.Fatal(err) } var stale, current error err = w.control.RunRecovery(ctx, func(ctx context.Context) error { return runMoveLogDrain(ctx, func(ctx context.Context, lw *MoveLogWriter) error { _, stale = lw.WriteMoveLog(ctx, MoveLogEntry{Host: w.host, Op: "move", Incarnation: strings.Repeat("ef", 32), Name: "done", Body: body}) _, current = lw.WriteMoveLog(ctx, MoveLogEntry{Host: w.host, Op: "move", Incarnation: w.inc, Name: "done", Body: body}) return nil }) }) if err != nil { t.Fatal(err) } if !errors.Is(stale, ErrIdentity) || current != nil { t.Fatalf("stale incarnation: %v (want refused); current: %v", stale, current) } }