carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "path/filepath"
8 "reflect"
9 "strings"
10 "testing"
11
12 "github.com/carlosframework/platform/internal/hostactors"
13 "github.com/carlosframework/platform/internal/hostenv"
14 "github.com/carlosframework/platform/internal/registry"
15 "github.com/carlosframework/platform/internal/release"
16 "github.com/carlosframework/platform/internal/status"
17)
18
19// withGroupIdentity makes IMDS answer account and instance (or fail) for
20// one test, with no reference kept from before.
21func withGroupIdentity(t *testing.T, account, instance string, err error) {
22 t.Helper()
23 was := groupIdentity
24 groupIdentity = func() (string, string, error) { return account, instance, err }
25 groupRef.Lock()
26 groupRef.ref = ""
27 groupRef.Unlock()
28 t.Cleanup(func() {
29 groupIdentity = was
30 groupRef.Lock()
31 groupRef.ref = ""
32 groupRef.Unlock()
33 })
34}
35
36func groupEnv(vals map[string]string) func(string) string {
37 return func(k string) string { return vals[k] }
38}
39
40// A box its region's group launched takes its group reference everywhere
41// it names itself: the identity record, mode selection, root's join. An
42// identity IMDS has not given is no reference, never a label in its place.
43func TestAGroupBoxTakesItsGroupReference(t *testing.T) {
44 group := groupEnv(map[string]string{hostenv.BoxReference: hostenv.GroupReference})
45 withGroupIdentity(t, "", "", errors.New("IMDS answered 500"))
46 if got := metricsBoxLabel(group, "machine"); got != "" {
47 t.Fatalf("no identity: %q", got)
48 }
49 withGroupIdentity(t, "123456789012", "i-0a61f580df1acfa65", nil)
50 want := hostactors.GroupBoxReference("123456789012", "i-0a61f580df1acfa65")
51 if got := metricsBoxLabel(group, "machine"); got != want {
52 t.Fatalf("group box: %q, want %q", got, want)
53 }
54}
55
56// statusFixture is a registry with one route and a status store holding
57// another box's report under the label this box's machine id hashes to.
58func statusFixture(t *testing.T) (*registry.Registry, *release.DirStore, []byte) {
59 t.Helper()
60 reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
61 if err != nil {
62 t.Fatal(err)
63 }
64 if err := reg.Upsert(registry.Route{Host: "a.example.com", Kind: "static"}); err != nil {
65 t.Fatal(err)
66 }
67 s := release.NewDirStore(t.TempDir())
68 victim := []byte(`{"label":"victim","metrics":{"instance_id":"i-victim"}}`)
69 if _, err := s.Put(context.Background(), "status/boxes/"+status.Label("machine-1")+".json", victim, ""); err != nil {
70 t.Fatal(err)
71 }
72 return reg, s, victim
73}
74
75// T1 (census ruling region-ha-2b): a group box whose reference has not
76// resolved publishes nothing, whatever the claim and region HA switches
77// say; above all never under status.Label, where another box's report may
78// stand. The reference is asked again each tick, and once it resolves the
79// box publishes under it. Through statusLoop's own tick and WriteBox.
80func TestAGroupBoxPublishesOnlyUnderItsReference(t *testing.T) {
81 for name, env := range map[string]map[string]string{
82 "switches on": {hostenv.BoxReference: hostenv.GroupReference, "CARLOS_HOST_CLAIM": "1", "CARLOS_REGION_HA": "1"},
83 "switches off": {hostenv.BoxReference: hostenv.GroupReference},
84 } {
85 t.Run(name, func(t *testing.T) {
86 ctx := context.Background()
87 reg, s, victim := statusFixture(t)
88 withGroupIdentity(t, "", "", errors.New("IMDS answered 500"))
89 asked := 0
90 groupIdentity = func() (string, string, error) {
91 asked++
92 if asked < 3 {
93 return "", "", errors.New("IMDS answered 500")
94 }
95 return "123456789012", "i-0a61f580df1acfa65", nil
96 }
97 probes := status.Probes{MachineID: "machine-1"}
98 for i := 0; i < 2; i++ {
99 err := groupStatusTick(ctx, s, reg, probes, false, "", groupEnv(env))
100 if !errors.Is(err, errNoGroupReference) || !strings.Contains(err.Error(), "IMDS answered 500") {
101 t.Fatalf("tick %d: %v, want errNoGroupReference with its cause", i, err)
102 }
103 }
104 keys, _ := s.List(ctx, "status/boxes/")
105 got, _, _ := s.Get(ctx, "status/boxes/"+status.Label("machine-1")+".json")
106 if len(keys) != 1 || !bytes.Equal(got, victim) {
107 t.Fatalf("published with no reference: %v, victim now %s", keys, got)
108 }
109 if err := groupStatusTick(ctx, s, reg, probes, false, "", groupEnv(env)); err != nil {
110 t.Fatal(err)
111 }
112 ref := hostactors.GroupBoxReference("123456789012", "i-0a61f580df1acfa65")
113 if _, _, err := s.Get(ctx, "status/boxes/"+ref+".json"); err != nil || asked != 3 {
114 t.Fatalf("after recovery: %v, IMDS asked %d times", err, asked)
115 }
116 if got, _, _ := s.Get(ctx, "status/boxes/"+status.Label("machine-1")+".json"); !bytes.Equal(got, victim) {
117 t.Fatalf("the victim's report was replaced: %s", got)
118 }
119 })
120 }
121}
122
123// The ruling's third test: a box that is not a group box keeps its label
124// and its status key byte for byte: statusLoop's tick is statusTick's, with
125// statusLoop's own label (CollectBox's pseudonym, or a fleet box's label).
126func TestABoxThatIsNotAGroupBoxKeepsItsLabelAndKey(t *testing.T) {
127 withGroupIdentity(t, "123456789012", "i-0a61f580df1acfa65", nil)
128 for name, env := range map[string]map[string]string{
129 "no marker": {},
130 "another value": {hostenv.BoxReference: "instance-id"},
131 "region HA, no marker": {"CARLOS_REGION_HA": "1", "CARLOS_HOST_CLAIM": "1"},
132 } {
133 getenv := groupEnv(env)
134 if got := metricsBoxLabel(getenv, "machine-1"); got != status.Label("machine-1") {
135 t.Fatalf("%s: label %q, want %q", name, got, status.Label("machine-1"))
136 }
137 for _, label := range []string{"", "kitchen"} {
138 ctx := context.Background()
139 reg, _, _ := statusFixture(t)
140 mine, theirs := release.NewDirStore(t.TempDir()), release.NewDirStore(t.TempDir())
141 probes := status.Probes{MachineID: "machine-1"}
142 if err := groupStatusTick(ctx, mine, reg, probes, false, label, getenv); err != nil {
143 t.Fatal(err)
144 }
145 if err := statusTick(ctx, theirs, reg, probes, false, label); err != nil {
146 t.Fatal(err)
147 }
148 a, _ := mine.List(ctx, "status/boxes/")
149 b, _ := theirs.List(ctx, "status/boxes/")
150 if !reflect.DeepEqual(a, b) || len(a) != 1 {
151 t.Fatalf("%s, label %q: keys %v, statusTick's %v", name, label, a, b)
152 }
153 }
154 }
155 fleet := groupEnv(map[string]string{"CARLOS_FLEET": "pi", "CARLOS_FLEET_BOX": "kitchen"})
156 if got := metricsBoxLabel(fleet, "machine-1"); got != "kitchen" {
157 t.Fatalf("fleet box: %q", got)
158 }
159}
160
161// Task 8's gate relies on this: a marked box's label is "" only until its
162// reference resolves, asked again on every call, and then the reference,
163// kept, even if IMDS fails afterwards.
164func TestAGroupBoxLabelIsEmptyOnlyUntilItsReferenceResolves(t *testing.T) {
165 group := groupEnv(map[string]string{hostenv.BoxReference: hostenv.GroupReference})
166 withGroupIdentity(t, "", "", nil)
167 asked := 0
168 groupIdentity = func() (string, string, error) {
169 asked++
170 if asked != 3 {
171 return "", "", errors.New("IMDS answered 500")
172 }
173 return "123456789012", "i-0a61f580df1acfa65", nil
174 }
175 want := hostactors.GroupBoxReference("123456789012", "i-0a61f580df1acfa65")
176 for i, w := range []string{"", "", want, want, want} {
177 if got := metricsBoxLabel(group, "machine-1"); got != w {
178 t.Fatalf("call %d: %q, want %q", i, got, w)
179 }
180 }
181 if asked != 3 {
182 t.Fatalf("IMDS asked %d times, want 3 (a resolved reference is kept)", asked)
183 }
184}
185