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 "encoding/json"
8 "errors"
9 "os"
10 "testing"
11
12 "github.com/carlosframework/platform/internal/registry"
13 "github.com/carlosframework/platform/internal/release"
14)
15
16const (
17 heldOp = "mv_abcdefghijklmnopqrstuvwxyz"
18 heldBox = "uk-box"
19 heldHost = "held.example.com"
20)
21
22// heldFixture writes all three config layers for bab/eleven and returns the
23// etag the dir store reports for each object.
24func heldFixture(t *testing.T, ctx context.Context, s release.Store) (def, env, overlay string) {
25 t.Helper()
26 if err := SetEnv(ctx, s, "bab", "eleven", map[string]string{"FROM_DEFAULT": "d", "SHARED": "default"}); err != nil {
27 t.Fatalf("SetEnv: %v", err)
28 }
29 if _, err := ApplyEnvironment(ctx, s, "bab", "eleven", "prod", map[string]Var{"SHARED": {Kind: "env", Value: "environment"}}, nil); err != nil {
30 t.Fatalf("ApplyEnvironment: %v", err)
31 }
32 if _, _, err := ApplyInstance(ctx, s, "bab", "eleven", heldHost, map[string]Var{"FROM_INSTANCE": {Kind: "env", Value: "i"}}, nil); err != nil {
33 t.Fatalf("ApplyInstance: %v", err)
34 }
35 etagOf := func(key string) string {
36 _, etag, err := s.Get(ctx, key)
37 if err != nil {
38 t.Fatalf("Get %s: %v", key, err)
39 }
40 return etag
41 }
42 return etagOf(Key("bab", "eleven")), etagOf(EnvironmentKey("bab", "eleven", "prod")), etagOf(InstanceConfigKey("bab", "eleven", heldHost))
43}
44
45func landingRow() registry.Route {
46 return registry.Route{Host: heldHost, Kind: "instance", App: "eleven", Account: "bab", Environment: "prod",
47 Status: "landing", HoldOp: heldOp, Socket: "/run/carlos/held.sock"}
48}
49
50// TestLoadMergedHostVersionsReportsTheMergedObjects: the etag of every
51// object the merge actually read comes back with the manifest, and an
52// absent layer reports "".
53func TestLoadMergedHostVersionsReportsTheMergedObjects(t *testing.T) {
54 ctx := context.Background()
55 s := release.NewDirStore(t.TempDir())
56 def, env, overlay := heldFixture(t, ctx, s)
57
58 m, v, err := LoadMergedHostVersions(ctx, s, "bab", "eleven", "prod", heldHost)
59 if err != nil {
60 t.Fatalf("LoadMergedHostVersions: %v", err)
61 }
62 if v != (MergedVersions{Default: def, Environment: env, Overlay: overlay}) {
63 t.Fatalf("versions = %+v, want default %q environment %q overlay %q", v, def, env, overlay)
64 }
65 for k, want := range map[string]string{"FROM_DEFAULT": "d", "SHARED": "environment", "FROM_INSTANCE": "i"} {
66 if got := m.Vars[k].Value; got != want {
67 t.Errorf("%s = %q, want %q", k, got, want)
68 }
69 }
70 legacy, etag, err := LoadMergedHost(ctx, s, "bab", "eleven", "prod", heldHost)
71 if err != nil || etag != overlay || len(legacy.Vars) != len(m.Vars) {
72 t.Fatalf("LoadMergedHost = %d vars, %q, %v; want the same merge and the overlay etag %q", len(legacy.Vars), etag, err, overlay)
73 }
74
75 // Unbound, no overlay: only the default is read, the other two are "".
76 _, v2, err := LoadMergedHostVersions(ctx, s, "bab", "eleven", "", "other.example.com")
77 if err != nil {
78 t.Fatalf("LoadMergedHostVersions (unbound): %v", err)
79 }
80 if v2 != (MergedVersions{Default: def}) {
81 t.Fatalf("unbound versions = %+v, want only the default %q", v2, def)
82 }
83}
84
85func readAck(t *testing.T, ctx context.Context, s release.Store) ConfigAck {
86 t.Helper()
87 raw, _, err := s.Get(ctx, ConfigAckKey(heldBox, heldHost))
88 if err != nil {
89 t.Fatalf("ack: %v", err)
90 }
91 var a ConfigAck
92 if err := json.Unmarshal(raw, &a); err != nil {
93 t.Fatal(err)
94 }
95 return a
96}
97
98// TestRenderHeldWritesALandingRowSyncExecSkips: SyncExec leaves a landing
99// row alone (execRouteBase), RenderHeld renders it through the same writer
100// and acks it bound to the move: the op, the file's sha256 and the etags of
101// the default and environment objects the dir store reports.
102func TestRenderHeldWritesALandingRowSyncExecSkips(t *testing.T) {
103 ctx, s, reg, dd, privB64, _ := execKit(t)
104 priv, err := ParsePrivateKey(privB64, "test")
105 if err != nil {
106 t.Fatal(err)
107 }
108 def, env, overlay := heldFixture(t, ctx, s)
109 rt := landingRow()
110 if err := reg.Upsert(rt); err != nil {
111 t.Fatal(err)
112 }
113 if err := SyncExec(ctx, reg, s, priv, dd, nil, "", nil); err != nil {
114 t.Fatalf("SyncExec: %v", err)
115 }
116 path := ConfigEnvPath(dd, rt)
117 if _, err := os.Stat(path); !os.IsNotExist(err) {
118 t.Fatalf("SyncExec wrote a landing row's file: %v", err)
119 }
120
121 got, err := RenderHeld(ctx, s, priv, dd, rt, heldOp, heldBox)
122 if err != nil {
123 t.Fatalf("RenderHeld: %v", err)
124 }
125 body, err := os.ReadFile(path)
126 if err != nil {
127 t.Fatalf("RenderHeld wrote no file: %v", err)
128 }
129 sum := sha256.Sum256(body)
130 wantSHA := hex.EncodeToString(sum[:])
131 wantTags := map[string]string{"default": def, "environment": env}
132 if got.Path != path || got.SHA256 != wantSHA || got.OverlayETag != overlay || len(got.BundleETags) != 2 || got.BundleETags["default"] != def || got.BundleETags["environment"] != env {
133 t.Fatalf("HeldRender = %+v, want path %s sha %s bundles %v overlay %q", got, path, wantSHA, wantTags, overlay)
134 }
135 m, _, err := LoadMergedHostVersions(ctx, s, "bab", "eleven", "prod", heldHost)
136 if err != nil {
137 t.Fatal(err)
138 }
139 want, err := RenderEnv(m, priv, routeBinding(rt))
140 if err != nil {
141 t.Fatal(err)
142 }
143 if string(body) != string(want) {
144 t.Fatalf("file = %q, want the three-layer render %q", body, want)
145 }
146
147 a := readAck(t, ctx, s)
148 if a.MoveOp != heldOp || a.FileSHA256 != wantSHA || a.Box != heldBox || a.Host != heldHost || a.InstanceConfigETag != overlay || a.Schema != ConfigAckSchema || a.Shape != "socket-exec" {
149 t.Fatalf("ack = %+v", a)
150 }
151 if len(a.BundleETags) != 2 || a.BundleETags["default"] != def || a.BundleETags["environment"] != env {
152 t.Fatalf("ack bundle etags = %v, want %v", a.BundleETags, wantTags)
153 }
154}
155
156// TestRenderHeldAcksASecondCallWithNoChange: nothing changed, so the file
157// is not rewritten, but the ack is: T3 reads it for this round.
158func TestRenderHeldAcksASecondCallWithNoChange(t *testing.T) {
159 ctx, s, _, dd, privB64, _ := execKit(t)
160 priv, err := ParsePrivateKey(privB64, "test")
161 if err != nil {
162 t.Fatal(err)
163 }
164 heldFixture(t, ctx, s)
165 rt := landingRow()
166 first, err := RenderHeld(ctx, s, priv, dd, rt, heldOp, heldBox)
167 if err != nil {
168 t.Fatalf("RenderHeld: %v", err)
169 }
170 if err := s.Delete(ctx, ConfigAckKey(heldBox, heldHost)); err != nil {
171 t.Fatal(err)
172 }
173 second, err := RenderHeld(ctx, s, priv, dd, rt, heldOp, heldBox)
174 if err != nil {
175 t.Fatalf("second RenderHeld: %v", err)
176 }
177 if second.SHA256 != first.SHA256 || second.Path != first.Path {
178 t.Fatalf("second = %+v, first = %+v", second, first)
179 }
180 if a := readAck(t, ctx, s); a.MoveOp != heldOp || a.FileSHA256 != first.SHA256 {
181 t.Fatalf("second call's ack = %+v", a)
182 }
183}
184
185// TestRenderHeldRefusesWhileHostClaimIsWired: ruling F10. RenderHeld writes
186// through legacyHostWork, so with CARLOS_HOST_CLAIM on it refuses and
187// writes nothing, neither file nor ack. Plan 2 adds the held-row selection.
188func TestRenderHeldRefusesWhileHostClaimIsWired(t *testing.T) {
189 ctx, s, _, dd, privB64, _ := execKit(t)
190 priv, err := ParsePrivateKey(privB64, "test")
191 if err != nil {
192 t.Fatal(err)
193 }
194 heldFixture(t, ctx, s)
195 t.Setenv("CARLOS_HOST_CLAIM", "1")
196 rt := landingRow()
197 if _, err := RenderHeld(ctx, s, priv, dd, rt, heldOp, heldBox); !errors.Is(err, ErrHeldClaimWired) {
198 t.Fatalf("RenderHeld with the claim wired: %v, want ErrHeldClaimWired", err)
199 }
200 if _, err := os.Stat(ConfigEnvPath(dd, rt)); !os.IsNotExist(err) {
201 t.Fatalf("a file was written: %v", err)
202 }
203 if _, _, err := s.Get(ctx, ConfigAckKey(heldBox, heldHost)); !errors.Is(err, os.ErrNotExist) {
204 t.Fatalf("an ack was written: %v", err)
205 }
206}
207
208// TestRenderHeldRefusesWhatItDoesNotOwn: a row SyncExec owns (ready, or
209// no status at all) would race SyncExec on one file, a row held by another
210// operation is not this move's, and a missing op or box cannot be acked.
211func TestRenderHeldRefusesWhatItDoesNotOwn(t *testing.T) {
212 ctx, s, _, dd, privB64, _ := execKit(t)
213 priv, err := ParsePrivateKey(privB64, "test")
214 if err != nil {
215 t.Fatal(err)
216 }
217 heldFixture(t, ctx, s)
218 for name, tc := range map[string]struct {
219 mutate func(*registry.Route)
220 op, box string
221 }{
222 "ready row": {func(r *registry.Route) { r.Status = "ready" }, heldOp, heldBox},
223 "statusless row": {func(r *registry.Route) { r.Status = "" }, heldOp, heldBox},
224 "held by another op": {func(r *registry.Route) { r.HoldOp = "mv_bcdefghijklmnopqrstuvwxyza" }, heldOp, heldBox},
225 "no op": {func(*registry.Route) {}, "", heldBox},
226 "no box": {func(*registry.Route) {}, heldOp, ""},
227 "unit-backed": {func(r *registry.Route) { r.Backing = registry.BackingUnit }, heldOp, heldBox},
228 "no socket": {func(r *registry.Route) { r.Socket = "" }, heldOp, heldBox},
229 "not a bare host name": {func(r *registry.Route) { r.Host = "../x" }, heldOp, heldBox},
230 } {
231 t.Run(name, func(t *testing.T) {
232 rt := landingRow()
233 tc.mutate(&rt)
234 if _, err := RenderHeld(ctx, s, priv, dd, rt, tc.op, tc.box); !errors.Is(err, ErrNotHeld) {
235 t.Fatalf("RenderHeld: %v, want ErrNotHeld", err)
236 }
237 if _, err := os.Stat(ConfigEnvPath(dd, landingRow())); !os.IsNotExist(err) {
238 t.Fatalf("a file was written: %v", err)
239 }
240 })
241 }
242}
243