carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package appconfig
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "errors"
8 "os"
9 "strings"
10 "testing"
11
12 "github.com/carlosframework/platform/internal/hostwrite"
13)
14
15// recordingWork is a claim selection stand-in: it records each identity it
16// is asked to write under and runs the effect only while open.
17type recordingWork struct {
18 ids []hostwrite.Identity
19 shut bool
20}
21
22func (w *recordingWork) With(_ context.Context, id hostwrite.Identity, fn func() error) error {
23 w.ids = append(w.ids, id)
24 if w.shut {
25 return hostwrite.ErrUnavailable
26 }
27 return fn()
28}
29
30// TestRenderHeldUnderWritesThroughTheClaimSelection (moves Plan 3 Task 5,
31// H13): with CARLOS_HOST_CLAIM on, the held render writes the config file
32// through the work its caller selects, after the merged versions are known
33// (the work is chosen from them), and acks the move as RenderHeld does.
34func TestRenderHeldUnderWritesThroughTheClaimSelection(t *testing.T) {
35 ctx, s, _, dd, privB64, _ := execKit(t)
36 priv, err := ParsePrivateKey(privB64, "test")
37 if err != nil {
38 t.Fatal(err)
39 }
40 def, env, overlay := heldFixture(t, ctx, s)
41 t.Setenv("CARLOS_HOST_CLAIM", "1")
42 rt := landingRow()
43 work := &recordingWork{}
44 var seen HeldRender
45 hr, err := RenderHeldUnder(ctx, s, priv, dd, rt, heldOp, heldBox, func(pre HeldRender) (hostwrite.Work, error) {
46 seen = pre
47 return work, nil
48 })
49 if err != nil {
50 t.Fatal(err)
51 }
52 if seen.BundleETags["default"] != def || seen.BundleETags["environment"] != env || seen.OverlayETag != overlay {
53 t.Fatalf("the work was chosen from %+v", seen)
54 }
55 if hr.BundleETags["default"] != def || hr.OverlayETag != overlay || hr.SHA256 == "" || hr.Path != ConfigEnvPath(dd, rt) {
56 t.Fatalf("render: %+v", hr)
57 }
58 if len(work.ids) == 0 {
59 t.Fatal("the file was not written through the selection")
60 }
61 if _, err := os.Stat(hr.Path); err != nil {
62 t.Fatal(err)
63 }
64 if a := readAck(t, ctx, s); a.MoveOp != heldOp || a.FileSHA256 != hr.SHA256 || a.Box != heldBox {
65 t.Fatalf("ack: %+v", a)
66 }
67}
68
69// A refused selection writes nothing, and a missing work is refused before
70// any write; the ownership checks are RenderHeld's.
71func TestRenderHeldUnderRefusals(t *testing.T) {
72 ctx, s, _, dd, privB64, _ := execKit(t)
73 priv, err := ParsePrivateKey(privB64, "test")
74 if err != nil {
75 t.Fatal(err)
76 }
77 heldFixture(t, ctx, s)
78 t.Setenv("CARLOS_HOST_CLAIM", "1")
79 rt := landingRow()
80 shut := &recordingWork{shut: true}
81 if _, err := RenderHeldUnder(ctx, s, priv, dd, rt, heldOp, heldBox, func(HeldRender) (hostwrite.Work, error) { return shut, nil }); !errors.Is(err, hostwrite.ErrUnavailable) {
82 t.Fatalf("a refused selection: %v", err)
83 }
84 selectErr := errors.New("no selection")
85 if _, err := RenderHeldUnder(ctx, s, priv, dd, rt, heldOp, heldBox, func(HeldRender) (hostwrite.Work, error) { return nil, selectErr }); !errors.Is(err, selectErr) {
86 t.Fatalf("a failed selection: %v", err)
87 }
88 if _, err := RenderHeldUnder(ctx, s, priv, dd, rt, heldOp, heldBox, nil); !errors.Is(err, hostwrite.ErrUnavailable) {
89 t.Fatalf("no selection: %v", err)
90 }
91 ready := rt
92 ready.Status = "ready"
93 if _, err := RenderHeldUnder(ctx, s, priv, dd, ready, heldOp, heldBox, func(HeldRender) (hostwrite.Work, error) { return &recordingWork{}, nil }); !errors.Is(err, ErrNotHeld) {
94 t.Fatalf("a row SyncExec owns: %v", err)
95 }
96 if _, err := os.Stat(ConfigEnvPath(dd, rt)); !os.IsNotExist(err) {
97 t.Fatalf("a file was written: %v", err)
98 }
99 if _, _, err := s.Get(ctx, ConfigAckKey(heldBox, heldHost)); !errors.Is(err, os.ErrNotExist) {
100 t.Fatalf("an ack was written: %v", err)
101 }
102}
103
104// orderedWork is a claim selection stand-in that records, in log, each
105// write it lets through.
106type orderedWork struct{ log *[]string }
107
108func (w orderedWork) With(_ context.Context, id hostwrite.Identity, fn func() error) error {
109 *w.log = append(*w.log, "write")
110 return fn()
111}
112
113// TestRenderHeldUnderBeforeJournalsBeforeTheWrite (moves after claim Plan
114// 12, Task 7; platform-1e's answer to Q2.2): the held render under the claim
115// keeps carry (c). The work is chosen from the merged versions, then before
116// learns the sha256 of exactly the bytes the render places, and only then
117// does the selection write them. A refusal from before writes nothing, sends
118// no ack and never enters the selection; a missing before or work is refused
119// before anything is read or written; the ownership checks are RenderHeld's.
120func TestRenderHeldUnderBeforeJournalsBeforeTheWrite(t *testing.T) {
121 ctx, s, _, dd, privB64, _ := execKit(t)
122 priv, err := ParsePrivateKey(privB64, "test")
123 if err != nil {
124 t.Fatal(err)
125 }
126 def, env, overlay := heldFixture(t, ctx, s)
127 t.Setenv("CARLOS_HOST_CLAIM", "1")
128 rt := landingRow()
129 path := ConfigEnvPath(dd, rt)
130 noAck := func(t *testing.T) {
131 t.Helper()
132 if _, err := os.Stat(path); !os.IsNotExist(err) {
133 t.Fatalf("a file was written: %v", err)
134 }
135 if _, _, err := s.Get(ctx, ConfigAckKey(heldBox, heldHost)); !errors.Is(err, os.ErrNotExist) {
136 t.Fatalf("an ack was written: %v", err)
137 }
138 }
139
140 t.Run("refusals", func(t *testing.T) {
141 pick := func(HeldRender) (hostwrite.Work, error) { return &recordingWork{}, nil }
142 if _, err := RenderHeldUnderBefore(ctx, s, priv, dd, rt, heldOp, heldBox, pick, nil); !errors.Is(err, hostwrite.ErrUnavailable) {
143 t.Fatalf("no before: %v", err)
144 }
145 if _, err := RenderHeldUnderBefore(ctx, s, priv, dd, rt, heldOp, heldBox, nil, func(string) error { return nil }); !errors.Is(err, hostwrite.ErrUnavailable) {
146 t.Fatalf("no work: %v", err)
147 }
148 ready := rt
149 ready.Status = "ready"
150 if _, err := RenderHeldUnderBefore(ctx, s, priv, dd, ready, heldOp, heldBox, pick, func(string) error { return nil }); !errors.Is(err, ErrNotHeld) {
151 t.Fatalf("a row SyncExec owns: %v", err)
152 }
153 other := rt
154 other.HoldOp = "mv-other"
155 if _, err := RenderHeldUnderBefore(ctx, s, priv, dd, other, heldOp, heldBox, pick, func(string) error { return nil }); !errors.Is(err, ErrNotHeld) {
156 t.Fatalf("a row another op holds: %v", err)
157 }
158 noAck(t)
159 })
160
161 t.Run("a refusing journal writes nothing", func(t *testing.T) {
162 var log []string
163 refused := errors.New("the journal refused")
164 _, err := RenderHeldUnderBefore(ctx, s, priv, dd, rt, heldOp, heldBox, func(HeldRender) (hostwrite.Work, error) {
165 log = append(log, "select")
166 return orderedWork{&log}, nil
167 }, func(string) error {
168 log = append(log, "journal")
169 return refused
170 })
171 if !errors.Is(err, refused) {
172 t.Fatalf("a refusing journal: %v", err)
173 }
174 if strings.Join(log, ",") != "select,journal" {
175 t.Fatalf("order %v: the selection wrote after a refused journal", log)
176 }
177 noAck(t)
178 })
179
180 t.Run("the journal comes before the write", func(t *testing.T) {
181 var (
182 log []string
183 seen HeldRender
184 told string
185 )
186 hr, err := RenderHeldUnderBefore(ctx, s, priv, dd, rt, heldOp, heldBox, func(pre HeldRender) (hostwrite.Work, error) {
187 log = append(log, "select")
188 seen = pre
189 return orderedWork{&log}, nil
190 }, func(sha string) error {
191 log = append(log, "journal")
192 if _, err := os.Stat(path); !os.IsNotExist(err) {
193 t.Errorf("the file was there before the journal: %v", err)
194 }
195 told = sha
196 return nil
197 })
198 if err != nil {
199 t.Fatal(err)
200 }
201 if len(log) < 3 || log[0] != "select" || log[1] != "journal" || log[2] != "write" {
202 t.Fatalf("order %v, want the selection, then the journal, then the writes", log)
203 }
204 if seen.BundleETags["default"] != def || seen.BundleETags["environment"] != env || seen.OverlayETag != overlay {
205 t.Fatalf("the work was chosen from %+v", seen)
206 }
207 body, err := os.ReadFile(path)
208 if err != nil {
209 t.Fatal(err)
210 }
211 sum := sha256.Sum256(body)
212 if want := hex.EncodeToString(sum[:]); told != want || hr.SHA256 != want {
213 t.Fatalf("journal %q, render %q, file %q", told, hr.SHA256, want)
214 }
215 if a := readAck(t, ctx, s); a.MoveOp != heldOp || a.FileSHA256 != told || a.Box != heldBox {
216 t.Fatalf("ack: %+v", a)
217 }
218 })
219}
220