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 "crypto/ed25519"
7 "encoding/json"
8 "errors"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13
14 "github.com/carlosframework/platform/internal/boxid"
15 "github.com/carlosframework/platform/internal/boxid/boxidtest"
16 "github.com/carlosframework/platform/internal/hostactors"
17 "github.com/carlosframework/platform/internal/hostenv"
18 "github.com/carlosframework/platform/internal/hostfloor"
19 "github.com/carlosframework/platform/internal/release"
20)
21
22// rootJoinWorld is a deployment that has invited box "ada-hiking", whose
23// identity record says it is instance i-ada.
24type rootJoinWorld struct {
25 j rootJoin
26 store *release.DirStore
27 sg release.Signer
28 armed []string
29 invite hostactors.Invitation
30}
31
32func newRootJoinWorld(t *testing.T, invite bool) *rootJoinWorld {
33 t.Helper()
34 ctx := context.Background()
35 pub, key, err := ed25519.GenerateKey(nil)
36 if err != nil {
37 t.Fatal(err)
38 }
39 w := &rootJoinWorld{store: release.NewDirStore(t.TempDir()), sg: release.RootSigner(key)}
40 _, raw := boxidtest.NewSigner(t).Record(t, "ada-hiking", "123", "eu-west-1", "i-ada")
41 if _, err := w.store.Put(ctx, boxid.Key("ada-hiking"), raw, ""); err != nil {
42 t.Fatal(err)
43 }
44 if err := hostfloor.Publish(ctx, w.store, w.sg, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.0.0"}); err != nil {
45 t.Fatal(err)
46 }
47 if invite {
48 if w.invite, err = (hostactors.Directory{Store: w.store, PublicKey: pub, Bucket: "bucket"}).Invite(ctx, w.sg, "ada-hiking", raw); err != nil {
49 t.Fatal(err)
50 }
51 }
52 w.j = rootJoin{box: "ada-hiking", optDir: t.TempDir(), bucket: "bucket", store: w.store, pub: pub,
53 instance: func() string { return "i-ada" }, armed: func() bool { return false },
54 arm: func(policy, identity, enrollment string) error {
55 w.armed = []string{policy, identity, enrollment}
56 return nil
57 }}
58 return w
59}
60
61func TestRootJoinArmsWithTheInvitationsEnrollment(t *testing.T) {
62 t.Parallel()
63 w := newRootJoinWorld(t, true)
64 got, err := w.j.run(context.Background())
65 if err != nil || got != joinArmed || len(w.armed) != 3 {
66 t.Fatalf("%s %v, armed %v", got, err, w.armed)
67 }
68 raw, err := os.ReadFile(w.armed[0])
69 if err != nil {
70 t.Fatal(err)
71 }
72 var p hostfloor.Policy
73 if json.Unmarshal(raw, &p) != nil || p.Mode != "armed" {
74 t.Fatalf("policy %s", raw)
75 }
76 if _, err := p.Verify(w.j.pub, "bucket"); err != nil {
77 t.Fatalf("the policy does not verify: %v", err)
78 }
79 if w.armed[1] != channelIdentityPath(w.j.optDir) {
80 t.Fatalf("identity %s", w.armed[1])
81 }
82 e, err := hostfloor.ReadEnrollment(w.armed[2])
83 if err != nil || e != (hostfloor.Enrollment{Box: "ada-hiking", Enrollment: w.invite.Enrollment, InstanceID: "i-ada"}) {
84 t.Fatalf("enrollment %+v %v", e, err)
85 }
86 for _, p := range []string{w.armed[0], w.armed[2]} {
87 if fi, err := os.Stat(p); err != nil || fi.Mode().Perm() != 0o600 {
88 t.Fatalf("%s: %v %v", p, fi, err)
89 }
90 }
91}
92
93func TestRootJoinDoesNothingUntilInvitedOrOnceArmed(t *testing.T) {
94 t.Parallel()
95 w := newRootJoinWorld(t, false)
96 if got, err := w.j.run(context.Background()); err != nil || got != joinNotInvited || w.armed != nil {
97 t.Fatalf("not invited: %s %v %v", got, err, w.armed)
98 }
99 v := newRootJoinWorld(t, true)
100 v.j.armed = func() bool { return true }
101 v.j.store = nil // an armed box reads nothing
102 if got, err := v.j.run(context.Background()); err != nil || got != joinArmedAlready || v.armed != nil {
103 t.Fatalf("armed: %s %v %v", got, err, v.armed)
104 }
105}
106
107func TestRootJoinRefusesAnInvitationThatIsNotThisBoxs(t *testing.T) {
108 t.Parallel()
109 for name, spoil := range map[string]func(*rootJoinWorld){
110 "another instance": func(w *rootJoinWorld) { w.j.instance = func() string { return "i-other" } },
111 "another record": func(w *rootJoinWorld) {
112 _, raw := boxidtest.NewSigner(t).Record(t, "ada-hiking", "123", "eu-west-1", "i-ada")
113 _, etag, _ := w.store.Get(context.Background(), boxid.Key("ada-hiking"))
114 w.store.Put(context.Background(), boxid.Key("ada-hiking"), raw, etag)
115 },
116 "another release key": func(w *rootJoinWorld) {
117 other, _, _ := ed25519.GenerateKey(nil)
118 w.j.pub = other
119 },
120 "another deployment": func(w *rootJoinWorld) { w.j.bucket = "other-bucket" },
121 } {
122 t.Run(name, func(t *testing.T) {
123 w := newRootJoinWorld(t, true)
124 spoil(w)
125 if got, err := w.j.run(context.Background()); err == nil || w.armed != nil {
126 t.Fatalf("%s %v, armed %v", got, err, w.armed)
127 }
128 })
129 }
130 // A store that cannot be read is an error, never "not invited".
131 w := newRootJoinWorld(t, true)
132 w.j.store = joinOutageStore{}
133 if got, err := w.j.run(context.Background()); err == nil || got == joinNotInvited {
134 t.Fatalf("unreadable store: %s %v", got, err)
135 }
136}
137
138type joinOutageStore struct{ release.Store }
139
140func (joinOutageStore) Get(context.Context, string) ([]byte, string, error) {
141 return nil, "", errors.New("connection reset")
142}
143
144// The join units' census condition (platform-1e, region-ha-2b): the path
145// unit's file is only a trigger. Whatever the agent, or anything else,
146// writes into it, root's join reads none of it, takes no parameter from
147// it, and decides from the signed invitation, the published identity
148// record, the signed host floor and IMDS alone.
149func TestRootJoinTakesNothingFromItsTrigger(t *testing.T) {
150 data := t.TempDir()
151 t.Setenv("CARLOS_DATA_DIR", data)
152 clean := newRootJoinWorld(t, true)
153 want, err := clean.j.run(context.Background())
154 if err != nil || want != joinArmed {
155 t.Fatalf("baseline: %s %v", want, err)
156 }
157 for name, body := range map[string]string{
158 "another box": `{"box":"grace-hiking","instance":"i-grace","enrollment":"ffffffffffffffffffffffffffffffff"}`,
159 "a path": "../../etc/carlos/host-actor.json\n",
160 "a policy": `{"schema":1,"mode":"armed","deployment_bucket":"other-bucket"}`,
161 "nul and newlines": "a\x00b\n\n--policy=/tmp/x\n",
162 "a megabyte of junk": strings.Repeat("x", 1<<20),
163 } {
164 t.Run(name, func(t *testing.T) {
165 if err := os.WriteFile(filepath.Join(data, "system-join-requested"), []byte(body), 0o644); err != nil {
166 t.Fatal(err)
167 }
168 w := newRootJoinWorld(t, true)
169 got, err := w.j.run(context.Background())
170 if err != nil || got != want || len(w.armed) != 3 || w.armed[1] != channelIdentityPath(w.j.optDir) {
171 t.Fatalf("%s %v, armed %v", got, err, w.armed)
172 }
173 e, err := hostfloor.ReadEnrollment(w.armed[2])
174 if err != nil || e != (hostfloor.Enrollment{Box: "ada-hiking", Enrollment: w.invite.Enrollment, InstanceID: "i-ada"}) {
175 t.Fatalf("enrollment %+v %v", e, err)
176 }
177 })
178 }
179 // And by construction: root's join names neither the trigger nor its
180 // path anywhere in its source.
181 src, err := os.ReadFile("joinroot.go")
182 if err != nil {
183 t.Fatal(err)
184 }
185 if bytes.Contains(src, []byte("joinRequestMarker")) || bytes.Contains(src, []byte("system-join-requested")) {
186 t.Fatal("joinroot.go reads its trigger")
187 }
188}
189
190// A group box names its invitation by its group reference. While IMDS has
191// not given it, there is no name, and the join says so; it never reads the
192// invitation of "" and reports "not invited".
193func TestRootJoinNamesAGroupBoxByItsReferenceOrFails(t *testing.T) {
194 group := groupEnv(map[string]string{hostenv.BoxReference: hostenv.GroupReference})
195 withGroupIdentity(t, "", "", errors.New("IMDS answered 500"))
196 if got, err := joinBoxLabel(group, "machine"); err == nil || got != "" || err.Error() != "this box's AWS identity could not be read, so it cannot join yet: IMDS answered 500" {
197 t.Fatalf("no identity: %q %v", got, err)
198 }
199 withGroupIdentity(t, "123456789012", "i-0a61f580df1acfa65", nil)
200 want := hostactors.GroupBoxReference("123456789012", "i-0a61f580df1acfa65")
201 if got, err := joinBoxLabel(group, "machine"); err != nil || got != want {
202 t.Fatalf("group box: %q %v, want %q", got, err, want)
203 }
204 // Any other box keeps metricsBoxLabel's name.
205 other := groupEnv(nil)
206 if got, err := joinBoxLabel(other, "machine"); err != nil || got != metricsBoxLabel(other, "machine") || got == "" {
207 t.Fatalf("other box: %q %v", got, err)
208 }
209 // And a run with no name reads nothing.
210 w := newRootJoinWorld(t, true)
211 w.j.box = ""
212 w.j.store = nil
213 if got, err := w.j.run(context.Background()); err == nil || got == joinNotInvited || w.armed != nil {
214 t.Fatalf("nameless: %s %v %v", got, err, w.armed)
215 }
216}
217