carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package historyreadback
2
3import (
4 "bytes"
5 "context"
6 "crypto/ed25519"
7 "encoding/json"
8 "errors"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/carlosframework/platform/internal/hostactors"
17 "github.com/carlosframework/platform/internal/release"
18 "github.com/carlosframework/platform/internal/safefile"
19)
20
21type workflowLedger string
22
23func (s workflowLedger) Read(k string) ([]byte, error) {
24 return os.ReadFile(filepath.Join(string(s), k))
25}
26func (s workflowLedger) Write(k string, b []byte) error {
27 return safefile.WriteFile(filepath.Join(string(s), k), b, 0600)
28}
29func (s workflowLedger) Remove(k string) error { return os.Remove(filepath.Join(string(s), k)) }
30func (s workflowLedger) Names() ([]string, error) {
31 rows, e := os.ReadDir(string(s))
32 var names []string
33 for _, r := range rows {
34 names = append(names, r.Name())
35 }
36 return names, e
37}
38func (s workflowLedger) Lock(context.Context, string) (func(), error) { return func() {}, nil }
39func (s workflowLedger) Close() error { return nil }
40
41type workflowCrashStore struct {
42 release.Store
43 boundary string
44}
45
46func (s workflowCrashStore) Put(ctx context.Context, key string, raw []byte, tag string) (string, error) {
47 request := strings.HasSuffix(key, "/request.json")
48 if !request && s.boundary == "before grant" {
49 os.Exit(76)
50 }
51 got, err := s.Store.Put(ctx, key, raw, tag)
52 if err == nil && ((!request && s.boundary == "after grant") || (request && s.boundary == "after request")) {
53 os.Exit(76)
54 }
55 return got, err
56}
57
58func TestPlan14HistoryWorkflowProcessCrash(t *testing.T) {
59 if boundary := os.Getenv("PLAN14_WORKFLOW_CRASH"); boundary != "" {
60 dir := os.Getenv("PLAN14_WORKFLOW_DIR")
61 raw, err := os.ReadFile(filepath.Join(dir, "saved.json"))
62 if err != nil {
63 t.Fatal(err)
64 }
65 var saved struct {
66 Op Operation
67 Pub ed25519.PublicKey
68 Now time.Time
69 }
70 if err = json.Unmarshal(raw, &saved); err != nil {
71 t.Fatal(err)
72 }
73 d := hostactors.Directory{Store: workflowCrashStore{Store: release.NewDirStore(filepath.Join(dir, "store")), boundary: boundary}, PublicKey: saved.Pub, Bucket: "bucket"}
74 if err = saved.Op.Deliver(t.Context(), d, saved.Now); err != nil {
75 t.Fatal(err)
76 }
77 t.Fatal("missed crash")
78 }
79 for _, boundary := range []string{"before grant", "after grant", "after request"} {
80 t.Run(boundary, func(t *testing.T) {
81 f := newFixture(t)
82 dir := t.TempDir()
83 store := release.NewDirStore(filepath.Join(dir, "store"))
84 roster, _, err := f.directory.Store.Get(t.Context(), hostactors.Key)
85 if err != nil {
86 t.Fatal(err)
87 }
88 if _, err = store.Put(t.Context(), hostactors.Key, roster, ""); err != nil {
89 t.Fatal(err)
90 }
91 f.directory.Store = store
92 ledger := workflowLedger(t.TempDir())
93 h := &release.RootHistory{Store: ledger}
94 resolve := func(release.HistoryBinding) (string, error) { return "canonical", nil }
95 before, err := h.MigrationStatus(f.scope.Binding, resolve)
96 if err != nil {
97 t.Fatal(err)
98 }
99 g := release.HistoryReconstruction{Schema: 1, Kind: "carlos-release-history", Identity: before.Identity, Revision: before.Revision, Operation: "operator", IssuedAt: f.now, Account: "abc", App: "web", Hosts: []release.HistoryReconstructionHost{before.Hosts[0].HistoryReconstructionHost}, Holds: map[string]release.HistoryReconstructionHold{"stable": {Window: time.Hour, SetAt: f.now}}}
100 op, err := PrepareOperation(t.Context(), f.directory, f.signer, f.scope, &g, nil, f.now)
101 if err != nil {
102 t.Fatal(err)
103 }
104 saved := struct {
105 Op Operation
106 Pub ed25519.PublicKey
107 Now time.Time
108 }{op, f.directory.PublicKey, f.now}
109 raw, err := json.Marshal(saved)
110 if err != nil {
111 t.Fatal(err)
112 }
113 path := filepath.Join(dir, "saved.json")
114 if err = safefile.WriteFile(path, raw, 0600); err != nil {
115 t.Fatal(err)
116 }
117 cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^TestPlan14HistoryWorkflowProcessCrash$", "-test.count=1")
118 cmd.Dir = t.TempDir()
119 cmd.Env = append(os.Environ(), "PLAN14_WORKFLOW_CRASH="+boundary, "PLAN14_WORKFLOW_DIR="+dir)
120 out, err := cmd.CombinedOutput()
121 var exit *exec.ExitError
122 if !errors.As(err, &exit) || exit.ExitCode() != 76 {
123 t.Fatalf("child %v: %s", err, out)
124 }
125 // A new operator process reads the saved messages; a new root opener reads
126 // its own ledger. Neither depends on an in-memory successful response.
127 again, err := os.ReadFile(path)
128 if err != nil {
129 t.Fatal(err)
130 }
131 if !bytes.Equal(raw, again) {
132 t.Fatal("crash changed saved authorization")
133 }
134 if err = json.Unmarshal(again, &saved); err != nil {
135 t.Fatal(err)
136 }
137 if err = saved.Op.Deliver(t.Context(), f.directory, f.now); err != nil {
138 t.Fatal(err)
139 }
140 if _, err = saved.Op.Read(t.Context(), f.directory, f.now); err == nil {
141 t.Fatal("delivery completed operation")
142 }
143 h = &release.RootHistory{Store: ledger}
144 if err = h.ReceiveReconstruction(t.Context(), store, f.directory.PublicKey, f.scope.Binding, f.now, resolve); err != nil {
145 t.Fatal(err)
146 }
147 if err = Respond(t.Context(), f.directory, f.scope, f.key, f.now, func(context.Context) (release.HistoryMigrationStatus, error) {
148 return h.MigrationStatus(f.scope.Binding, resolve)
149 }); err != nil {
150 t.Fatal(err)
151 }
152 got, err := saved.Op.Read(t.Context(), f.directory, f.now)
153 if err != nil {
154 t.Fatal(err)
155 }
156 if !got.Status.AppComplete || got.Status.Hosts[0].Programs.Complete {
157 t.Fatal("wrong category migration", got.Status)
158 }
159 if err = saved.Op.Deliver(t.Context(), f.directory, f.now); err != nil {
160 t.Fatal(err)
161 }
162 if err = h.ReceiveReconstruction(t.Context(), store, f.directory.PublicKey, f.scope.Binding, f.now, resolve); err != nil {
163 t.Fatal(err)
164 }
165 final, err := h.ReconstructionStatus()
166 if err != nil || final.Revision != g.Revision+1 {
167 t.Fatal("replay advanced revision", final, err)
168 }
169 })
170 }
171}
172