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 "encoding/json"
5 "errors"
6 "testing"
7
8 "github.com/carlosframework/platform/internal/registry"
9)
10
11// addrRoute is the shape this whole feature is about: an instance carlos
12// proxies to over TCP and neither spawns nor owns — the one SyncExec writes
13// a file for and deliberately never bounces.
14func addrRoute(host, app, acct, env string) registry.Route {
15 return registry.Route{
16 Host: host, Kind: "instance", App: app, Account: acct,
17 Status: "ready", Addr: "127.0.0.1:8080", Environment: env,
18 }
19}
20
21// TestAwaitingRestartNamesOptedInAddrRoute is the base case, and the one
22// the notice exists for: config written, file delivered, process none the
23// wiser.
24func TestAwaitingRestartNamesOptedInAddrRoute(t *testing.T) {
25 ctx, s, _, _, _, _ := execKit(t)
26 if err := SetExecDelivery(ctx, s, "bab", "relay", ExecDeliveryFile); err != nil {
27 t.Fatalf("SetExecDelivery: %v", err)
28 }
29 routes := []registry.Route{addrRoute("connect.example.com", "relay", "bab", "")}
30 got := AwaitingRestart(ctx, s, routes, "bab", "relay", "", []string{"CLIENT_ID"})
31 if len(got.Hosts) != 1 || got.Hosts[0].Host != "connect.example.com" {
32 t.Fatalf("Hosts = %+v, want just connect.example.com", got.Hosts)
33 }
34 if got.Hosts[0].Unit != "" {
35 t.Fatalf("Unit = %q, want empty for a route carlos cannot restart", got.Hosts[0].Unit)
36 }
37 if len(got.Unresolved) != 0 {
38 t.Fatalf("Unresolved = %v, want none", got.Unresolved)
39 }
40}
41
42// TestAwaitingRestartSkipsUnoptedAndSocketRoutes pins the two shapes that
43// must never appear in the notice, for opposite reasons: an un-opted addr
44// route never gets a file at all, and a socket route gets one AND gets
45// bounced, so telling someone to restart it by hand would be noise.
46func TestAwaitingRestartSkipsUnoptedAndSocketRoutes(t *testing.T) {
47 ctx, s, _, _, _, _ := execKit(t)
48 if err := SetExecDelivery(ctx, s, "bab", "relay", ExecDeliveryFile); err != nil {
49 t.Fatalf("SetExecDelivery: %v", err)
50 }
51 socket := registry.Route{
52 Host: "sock.example.com", Kind: "instance", App: "relay", Account: "bab",
53 Status: "ready", Socket: "/run/carlos/sock.sock",
54 }
55 unit := registry.Route{
56 Host: "unit.example.com", Kind: "instance", App: "relay", Account: "bab",
57 Status: "ready", Addr: "127.0.0.1:9090", Backing: registry.BackingUnit,
58 }
59 got := AwaitingRestart(ctx, s, []registry.Route{socket, unit}, "bab", "relay", "", []string{"K"})
60 if !got.Empty() {
61 t.Fatalf("got %+v, want nothing: a socket route is bounced and a unit route is the root pass's", got)
62 }
63
64 // The same addr route, with the app never opted in: no file was ever
65 // written for it, so there is nothing to restart over.
66 ctx2, s2, _, _, _, _ := execKit(t)
67 got = AwaitingRestart(ctx2, s2, []registry.Route{addrRoute("connect.example.com", "relay", "bab", "")},
68 "bab", "relay", "", []string{"K"})
69 if !got.Empty() {
70 t.Fatalf("got %+v, want nothing for an app that never opted in", got)
71 }
72}
73
74// TestAwaitingRestartCarriesUnit pins the field that decides which remedy
75// the CLI offers. An addr route naming its own unit (issue #110's consoles)
76// is the one addr shape `carlos restart` can actually cycle.
77func TestAwaitingRestartCarriesUnit(t *testing.T) {
78 ctx, s, _, _, _, _ := execKit(t)
79 if err := SetExecDelivery(ctx, s, "bab", "relay", ExecDeliveryFile); err != nil {
80 t.Fatalf("SetExecDelivery: %v", err)
81 }
82 rt := addrRoute("console.example.com", "relay", "bab", "")
83 rt.Unit = "console.service"
84 got := AwaitingRestart(ctx, s, []registry.Route{rt}, "bab", "relay", "", nil)
85 if len(got.Hosts) != 1 || got.Hosts[0].Unit != "console.service" {
86 t.Fatalf("Hosts = %+v, want console.service carried through", got.Hosts)
87 }
88}
89
90// TestAwaitingRestartDefaultWriteReachesBoundRoute: a named bundle is an
91// overlay ON the default, not a replacement for it, so a default-bundle
92// write does reach a route bound to an environment — for any key that
93// environment does not itself state.
94func TestAwaitingRestartDefaultWriteReachesBoundRoute(t *testing.T) {
95 ctx, s, _, _, _, _ := execKit(t)
96 if err := SetExecDelivery(ctx, s, "bab", "relay", ExecDeliveryFile); err != nil {
97 t.Fatalf("SetExecDelivery: %v", err)
98 }
99 if _, err := ApplyEnvironment(ctx, s, "bab", "relay", "prod",
100 map[string]Var{"OWN_KEY": {Kind: "env", Value: "v"}}, nil); err != nil {
101 t.Fatalf("ApplyEnvironment: %v", err)
102 }
103 routes := []registry.Route{addrRoute("connect.example.com", "relay", "bab", "prod")}
104
105 // A key the bound bundle does NOT state: the merge changes, the file
106 // changes, the process is behind.
107 got := AwaitingRestart(ctx, s, routes, "bab", "relay", "", []string{"OTHER_KEY"})
108 if len(got.Hosts) != 1 {
109 t.Fatalf("Hosts = %+v, want the bound route for an unshadowed key", got.Hosts)
110 }
111
112 // A key it DOES state: the overlay wins, the merged manifest is
113 // byte-identical, syncExecHost writes nothing. Naming the route here
114 // would send someone to restart a process for a value it was never
115 // going to see.
116 got = AwaitingRestart(ctx, s, routes, "bab", "relay", "", []string{"OWN_KEY"})
117 if !got.Empty() {
118 t.Fatalf("got %+v, want nothing: OWN_KEY is shadowed by the bound bundle", got)
119 }
120
121 // Mixed: one shadowed, one not — still affected.
122 got = AwaitingRestart(ctx, s, routes, "bab", "relay", "", []string{"OWN_KEY", "OTHER_KEY"})
123 if len(got.Hosts) != 1 {
124 t.Fatalf("Hosts = %+v, want the route when any key is unshadowed", got.Hosts)
125 }
126}
127
128// TestAwaitingRestartNamedWriteOnlyReachesItsOwnRoutes: the other
129// direction. A write to one environment must not name routes bound to
130// another, or to none.
131func TestAwaitingRestartNamedWriteOnlyReachesItsOwnRoutes(t *testing.T) {
132 ctx, s, _, _, _, _ := execKit(t)
133 if err := SetExecDelivery(ctx, s, "bab", "relay", ExecDeliveryFile); err != nil {
134 t.Fatalf("SetExecDelivery: %v", err)
135 }
136 for _, env := range []string{"prod", "staging"} {
137 if _, err := ApplyEnvironment(ctx, s, "bab", "relay", env,
138 map[string]Var{"K": {Kind: "env", Value: env}}, nil); err != nil {
139 t.Fatalf("ApplyEnvironment(%s): %v", env, err)
140 }
141 }
142 routes := []registry.Route{
143 addrRoute("prod.example.com", "relay", "bab", "prod"),
144 addrRoute("staging.example.com", "relay", "bab", "staging"),
145 addrRoute("plain.example.com", "relay", "bab", ""),
146 }
147 got := AwaitingRestart(ctx, s, routes, "bab", "relay", "prod", []string{"K"})
148 if len(got.Hosts) != 1 || got.Hosts[0].Host != "prod.example.com" {
149 t.Fatalf("Hosts = %+v, want only prod.example.com", got.Hosts)
150 }
151}
152
153// TestAwaitingRestartOtherAppsAndAccounts is the tenant boundary: routes
154// belong to one app of one account, and a notice that leaked a neighbour's
155// hostname would be a disclosure, not a nicety.
156func TestAwaitingRestartOtherAppsAndAccounts(t *testing.T) {
157 ctx, s, _, _, _, _ := execKit(t)
158 for _, app := range []string{"relay", "other"} {
159 if err := SetExecDelivery(ctx, s, "bab", app, ExecDeliveryFile); err != nil {
160 t.Fatalf("SetExecDelivery(%s): %v", app, err)
161 }
162 }
163 if err := SetExecDelivery(ctx, s, "zzz", "relay", ExecDeliveryFile); err != nil {
164 t.Fatalf("SetExecDelivery(zzz): %v", err)
165 }
166 routes := []registry.Route{
167 addrRoute("mine.example.com", "relay", "bab", ""),
168 addrRoute("otherapp.example.com", "other", "bab", ""),
169 addrRoute("otheracct.example.com", "relay", "zzz", ""),
170 }
171 got := AwaitingRestart(ctx, s, routes, "bab", "relay", "", nil)
172 if len(got.Hosts) != 1 || got.Hosts[0].Host != "mine.example.com" {
173 t.Fatalf("Hosts = %+v, want only this account's route for this app", got.Hosts)
174 }
175}
176
177// TestAwaitingRestartUnresolvedOnMissingBoundBundle: a route bound to a
178// bundle that does not exist resolves to an ERROR, not to the app default —
179// LoadMerged refuses it and SyncExec leaves the file exactly as it was.
180// Nothing was delivered, so "delivered" would be a lie; but the route is
181// not fine either, so it is not a skip.
182func TestAwaitingRestartUnresolvedOnMissingBoundBundle(t *testing.T) {
183 ctx, s, _, _, _, _ := execKit(t)
184 if err := SetExecDelivery(ctx, s, "bab", "relay", ExecDeliveryFile); err != nil {
185 t.Fatalf("SetExecDelivery: %v", err)
186 }
187 routes := []registry.Route{addrRoute("connect.example.com", "relay", "bab", "ghost")}
188 got := AwaitingRestart(ctx, s, routes, "bab", "relay", "", nil)
189 if len(got.Hosts) != 0 {
190 t.Fatalf("Hosts = %+v, want none: nothing was delivered", got.Hosts)
191 }
192 if len(got.Unresolved) != 1 || got.Unresolved[0] != "connect.example.com" {
193 t.Fatalf("Unresolved = %v, want connect.example.com", got.Unresolved)
194 }
195}
196
197// TestAwaitingRestartUnresolvedOnFetchFailure is the same distinction for a
198// transient blip. A read that failed is not a determination either way, and
199// reporting it as "nothing to restart" would be discussion #16's own bug
200// reappearing inside its fix.
201func TestAwaitingRestartUnresolvedOnFetchFailure(t *testing.T) {
202 ctx, s, _, _, _, _ := execKit(t)
203 if err := SetExecDelivery(ctx, s, "bab", "relay", ExecDeliveryFile); err != nil {
204 t.Fatalf("SetExecDelivery: %v", err)
205 }
206 blip := &failingKeyStore{Store: s, failKey: Key("bab", "relay"), err: errors.New("503 slow down")}
207 routes := []registry.Route{addrRoute("connect.example.com", "relay", "bab", "")}
208 got := AwaitingRestart(ctx, blip, routes, "bab", "relay", "", nil)
209 if len(got.Hosts) != 0 {
210 t.Fatalf("Hosts = %+v, want none when the bundle would not read", got.Hosts)
211 }
212 if len(got.Unresolved) != 1 || got.Unresolved[0] != "connect.example.com" {
213 t.Fatalf("Unresolved = %v, want connect.example.com", got.Unresolved)
214 }
215}
216
217// TestAwaitingRestartUnknownExecDeliveryIsOff pins the additive-discipline
218// case: a value a NEWER carlos wrote decides "not opted in" here, exactly as
219// it does in SyncExec, so the notice and the sweep agree about who gets a
220// file even across versions.
221func TestAwaitingRestartUnknownExecDeliveryIsOff(t *testing.T) {
222 ctx, s, _, _, _, _ := execKit(t)
223 // Written straight to the bucket rather than through a setter: the
224 // setters refuse this value by design, and the case being pinned is a
225 // manifest a FUTURE carlos wrote that this binary has to read without
226 // choking on.
227 raw, err := json.Marshal(&Manifest{Schema: 1, ExecDelivery: "socket-someday"})
228 if err != nil {
229 t.Fatalf("marshal: %v", err)
230 }
231 if _, err := s.Put(ctx, Key("bab", "relay"), raw, ""); err != nil {
232 t.Fatalf("Put: %v", err)
233 }
234 routes := []registry.Route{addrRoute("connect.example.com", "relay", "bab", "")}
235 if got := AwaitingRestart(ctx, s, routes, "bab", "relay", "", nil); !got.Empty() {
236 t.Fatalf("got %+v, want nothing for an exec_delivery value this binary does not understand", got)
237 }
238}
239
240// TestAwaitingRestartInstanceOverlayShadowsADefaultWrite: the third layer
241// shadows a default-bundle write the same way a named bundle does, and for
242// the same reason. The merged manifest does not move, so syncExecHost's
243// bytes.Equal writes nothing and there is no file for anyone to restart over.
244func TestAwaitingRestartInstanceOverlayShadowsADefaultWrite(t *testing.T) {
245 ctx, s, _, _, _, _ := execKit(t)
246 if err := SetExecDelivery(ctx, s, "bab", "relay", ExecDeliveryFile); err != nil {
247 t.Fatalf("SetExecDelivery: %v", err)
248 }
249 if _, _, err := ApplyInstance(ctx, s, "bab", "relay", "connect.example.com",
250 map[string]Var{"OWN_KEY": {Kind: "env", Value: "instance"}}, nil); err != nil {
251 t.Fatalf("ApplyInstance: %v", err)
252 }
253 routes := []registry.Route{addrRoute("connect.example.com", "relay", "bab", "")}
254
255 if got := AwaitingRestart(ctx, s, routes, "bab", "relay", "", []string{"OWN_KEY"}); !got.Empty() {
256 t.Fatalf("got %+v, want nothing: OWN_KEY is shadowed by this instance's own layer", got)
257 }
258 got := AwaitingRestart(ctx, s, routes, "bab", "relay", "", []string{"OTHER_KEY"})
259 if len(got.Hosts) != 1 || got.Hosts[0].Host != "connect.example.com" {
260 t.Fatalf("Hosts = %+v, want the route for a key its overlay does not state", got.Hosts)
261 }
262 got = AwaitingRestart(ctx, s, routes, "bab", "relay", "", []string{"OWN_KEY", "OTHER_KEY"})
263 if len(got.Hosts) != 1 {
264 t.Fatalf("Hosts = %+v, want the route when any key is unshadowed", got.Hosts)
265 }
266}
267
268// TestAwaitingRestartInstanceOverlayShadowsANamedBundleWrite is the case
269// that did not exist before the third layer: a write to the route's OWN
270// bound bundle used to be unshadowable, because nothing sat above it.
271// Something does now.
272func TestAwaitingRestartInstanceOverlayShadowsANamedBundleWrite(t *testing.T) {
273 ctx, s, _, _, _, _ := execKit(t)
274 if err := SetExecDelivery(ctx, s, "bab", "relay", ExecDeliveryFile); err != nil {
275 t.Fatalf("SetExecDelivery: %v", err)
276 }
277 if _, err := ApplyEnvironment(ctx, s, "bab", "relay", "prod",
278 map[string]Var{"K": {Kind: "env", Value: "environment"}}, nil); err != nil {
279 t.Fatalf("ApplyEnvironment: %v", err)
280 }
281 if _, _, err := ApplyInstance(ctx, s, "bab", "relay", "connect.example.com",
282 map[string]Var{"K": {Kind: "env", Value: "instance"}}, nil); err != nil {
283 t.Fatalf("ApplyInstance: %v", err)
284 }
285 routes := []registry.Route{addrRoute("connect.example.com", "relay", "bab", "prod")}
286
287 if got := AwaitingRestart(ctx, s, routes, "bab", "relay", "prod", []string{"K"}); !got.Empty() {
288 t.Fatalf("got %+v, want nothing: K is shadowed by this instance's own layer", got)
289 }
290 if got := AwaitingRestart(ctx, s, routes, "bab", "relay", "prod", []string{"OTHER"}); len(got.Hosts) != 1 {
291 t.Fatalf("Hosts = %+v, want the route for an unshadowed key", got.Hosts)
292 }
293}
294
295// TestAwaitingRestartUnresolvedWhenTheOverlayCannotBeRead: a read this
296// function could not make is not a determination, on this layer as on the
297// other two — the route is named as unresolved rather than reported either
298// way.
299func TestAwaitingRestartUnresolvedWhenTheOverlayCannotBeRead(t *testing.T) {
300 ctx, s, _, _, _, _ := execKit(t)
301 if err := SetExecDelivery(ctx, s, "bab", "relay", ExecDeliveryFile); err != nil {
302 t.Fatalf("SetExecDelivery: %v", err)
303 }
304 if _, _, err := ApplyInstance(ctx, s, "bab", "relay", "connect.example.com",
305 map[string]Var{"OWN_KEY": {Kind: "env", Value: "instance"}}, nil); err != nil {
306 t.Fatalf("ApplyInstance: %v", err)
307 }
308 blip := &failingKeyStore{Store: s, failKey: InstanceConfigKey("bab", "relay", "connect.example.com"),
309 err: errors.New("s3: transient 503 SlowDown")}
310 routes := []registry.Route{addrRoute("connect.example.com", "relay", "bab", "")}
311 got := AwaitingRestart(ctx, blip, routes, "bab", "relay", "", []string{"OWN_KEY"})
312 if len(got.Hosts) != 0 || len(got.Unresolved) != 1 || got.Unresolved[0] != "connect.example.com" {
313 t.Fatalf("got %+v, want the host as unresolved and nothing claimed either way", got)
314 }
315}
316
317// TestAwaitingRestartEmptyKeysNeverConsultsTheOverlay pins that an empty
318// keys (a write with no vars, e.g. flipping exec_delivery on) never asks the
319// instance layer anything: there is nothing for it to shadow, per keys' own
320// doc comment, so a blip reading it must not downgrade a route this call was
321// certain about into Unresolved over a question it was never going to ask.
322func TestAwaitingRestartEmptyKeysNeverConsultsTheOverlay(t *testing.T) {
323 ctx, s, _, _, _, _ := execKit(t)
324 if err := SetExecDelivery(ctx, s, "bab", "relay", ExecDeliveryFile); err != nil {
325 t.Fatalf("SetExecDelivery: %v", err)
326 }
327 if _, _, err := ApplyInstance(ctx, s, "bab", "relay", "connect.example.com",
328 map[string]Var{"OWN_KEY": {Kind: "env", Value: "instance"}}, nil); err != nil {
329 t.Fatalf("ApplyInstance: %v", err)
330 }
331 blip := &failingKeyStore{Store: s, failKey: InstanceConfigKey("bab", "relay", "connect.example.com"),
332 err: errors.New("s3: transient 503 SlowDown")}
333 routes := []registry.Route{addrRoute("connect.example.com", "relay", "bab", "")}
334 got := AwaitingRestart(ctx, blip, routes, "bab", "relay", "", nil)
335 if len(got.Hosts) != 1 || got.Hosts[0].Host != "connect.example.com" || len(got.Unresolved) != 0 {
336 t.Fatalf("got %+v, want the route named in Hosts: an unreadable overlay is irrelevant when keys is empty", got)
337 }
338}
339
340// TestAwaitingRestartSorted pins determinism: registry order is not stable,
341// and a notice whose lines reshuffle between identical runs is one people
342// stop reading.
343func TestAwaitingRestartSorted(t *testing.T) {
344 ctx, s, _, _, _, _ := execKit(t)
345 if err := SetExecDelivery(ctx, s, "bab", "relay", ExecDeliveryFile); err != nil {
346 t.Fatalf("SetExecDelivery: %v", err)
347 }
348 routes := []registry.Route{
349 addrRoute("zebra.example.com", "relay", "bab", ""),
350 addrRoute("alpha.example.com", "relay", "bab", ""),
351 addrRoute("middle.example.com", "relay", "bab", ""),
352 }
353 got := AwaitingRestart(ctx, s, routes, "bab", "relay", "", nil)
354 want := []string{"alpha.example.com", "middle.example.com", "zebra.example.com"}
355 if len(got.Hosts) != len(want) {
356 t.Fatalf("Hosts = %+v, want %d", got.Hosts, len(want))
357 }
358 for i, w := range want {
359 if got.Hosts[i].Host != w {
360 t.Fatalf("Hosts[%d] = %q, want %q", i, got.Hosts[i].Host, w)
361 }
362 }
363}
364