package host import ( "bytes" "context" "crypto/ed25519" "encoding/json" "errors" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/boxid" "github.com/carlosframework/platform/internal/boxid/boxidtest" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostenv" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" ) // rootJoinWorld is a deployment that has invited box "ada-hiking", whose // identity record says it is instance i-ada. type rootJoinWorld struct { j rootJoin store *release.DirStore sg release.Signer armed []string invite hostactors.Invitation } func newRootJoinWorld(t *testing.T, invite bool) *rootJoinWorld { t.Helper() ctx := context.Background() pub, key, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } w := &rootJoinWorld{store: release.NewDirStore(t.TempDir()), sg: release.RootSigner(key)} _, raw := boxidtest.NewSigner(t).Record(t, "ada-hiking", "123", "eu-west-1", "i-ada") if _, err := w.store.Put(ctx, boxid.Key("ada-hiking"), raw, ""); err != nil { t.Fatal(err) } if err := hostfloor.Publish(ctx, w.store, w.sg, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.0.0"}); err != nil { t.Fatal(err) } if invite { if w.invite, err = (hostactors.Directory{Store: w.store, PublicKey: pub, Bucket: "bucket"}).Invite(ctx, w.sg, "ada-hiking", raw); err != nil { t.Fatal(err) } } w.j = rootJoin{box: "ada-hiking", optDir: t.TempDir(), bucket: "bucket", store: w.store, pub: pub, instance: func() string { return "i-ada" }, armed: func() bool { return false }, arm: func(policy, identity, enrollment string) error { w.armed = []string{policy, identity, enrollment} return nil }} return w } func TestRootJoinArmsWithTheInvitationsEnrollment(t *testing.T) { t.Parallel() w := newRootJoinWorld(t, true) got, err := w.j.run(context.Background()) if err != nil || got != joinArmed || len(w.armed) != 3 { t.Fatalf("%s %v, armed %v", got, err, w.armed) } raw, err := os.ReadFile(w.armed[0]) if err != nil { t.Fatal(err) } var p hostfloor.Policy if json.Unmarshal(raw, &p) != nil || p.Mode != "armed" { t.Fatalf("policy %s", raw) } if _, err := p.Verify(w.j.pub, "bucket"); err != nil { t.Fatalf("the policy does not verify: %v", err) } if w.armed[1] != channelIdentityPath(w.j.optDir) { t.Fatalf("identity %s", w.armed[1]) } e, err := hostfloor.ReadEnrollment(w.armed[2]) if err != nil || e != (hostfloor.Enrollment{Box: "ada-hiking", Enrollment: w.invite.Enrollment, InstanceID: "i-ada"}) { t.Fatalf("enrollment %+v %v", e, err) } for _, p := range []string{w.armed[0], w.armed[2]} { if fi, err := os.Stat(p); err != nil || fi.Mode().Perm() != 0o600 { t.Fatalf("%s: %v %v", p, fi, err) } } } func TestRootJoinDoesNothingUntilInvitedOrOnceArmed(t *testing.T) { t.Parallel() w := newRootJoinWorld(t, false) if got, err := w.j.run(context.Background()); err != nil || got != joinNotInvited || w.armed != nil { t.Fatalf("not invited: %s %v %v", got, err, w.armed) } v := newRootJoinWorld(t, true) v.j.armed = func() bool { return true } v.j.store = nil // an armed box reads nothing if got, err := v.j.run(context.Background()); err != nil || got != joinArmedAlready || v.armed != nil { t.Fatalf("armed: %s %v %v", got, err, v.armed) } } func TestRootJoinRefusesAnInvitationThatIsNotThisBoxs(t *testing.T) { t.Parallel() for name, spoil := range map[string]func(*rootJoinWorld){ "another instance": func(w *rootJoinWorld) { w.j.instance = func() string { return "i-other" } }, "another record": func(w *rootJoinWorld) { _, raw := boxidtest.NewSigner(t).Record(t, "ada-hiking", "123", "eu-west-1", "i-ada") _, etag, _ := w.store.Get(context.Background(), boxid.Key("ada-hiking")) w.store.Put(context.Background(), boxid.Key("ada-hiking"), raw, etag) }, "another release key": func(w *rootJoinWorld) { other, _, _ := ed25519.GenerateKey(nil) w.j.pub = other }, "another deployment": func(w *rootJoinWorld) { w.j.bucket = "other-bucket" }, } { t.Run(name, func(t *testing.T) { w := newRootJoinWorld(t, true) spoil(w) if got, err := w.j.run(context.Background()); err == nil || w.armed != nil { t.Fatalf("%s %v, armed %v", got, err, w.armed) } }) } // A store that cannot be read is an error, never "not invited". w := newRootJoinWorld(t, true) w.j.store = joinOutageStore{} if got, err := w.j.run(context.Background()); err == nil || got == joinNotInvited { t.Fatalf("unreadable store: %s %v", got, err) } } type joinOutageStore struct{ release.Store } func (joinOutageStore) Get(context.Context, string) ([]byte, string, error) { return nil, "", errors.New("connection reset") } // The join units' census condition (platform-1e, region-ha-2b): the path // unit's file is only a trigger. Whatever the agent, or anything else, // writes into it, root's join reads none of it, takes no parameter from // it, and decides from the signed invitation, the published identity // record, the signed host floor and IMDS alone. func TestRootJoinTakesNothingFromItsTrigger(t *testing.T) { data := t.TempDir() t.Setenv("CARLOS_DATA_DIR", data) clean := newRootJoinWorld(t, true) want, err := clean.j.run(context.Background()) if err != nil || want != joinArmed { t.Fatalf("baseline: %s %v", want, err) } for name, body := range map[string]string{ "another box": `{"box":"grace-hiking","instance":"i-grace","enrollment":"ffffffffffffffffffffffffffffffff"}`, "a path": "../../etc/carlos/host-actor.json\n", "a policy": `{"schema":1,"mode":"armed","deployment_bucket":"other-bucket"}`, "nul and newlines": "a\x00b\n\n--policy=/tmp/x\n", "a megabyte of junk": strings.Repeat("x", 1<<20), } { t.Run(name, func(t *testing.T) { if err := os.WriteFile(filepath.Join(data, "system-join-requested"), []byte(body), 0o644); err != nil { t.Fatal(err) } w := newRootJoinWorld(t, true) got, err := w.j.run(context.Background()) if err != nil || got != want || len(w.armed) != 3 || w.armed[1] != channelIdentityPath(w.j.optDir) { t.Fatalf("%s %v, armed %v", got, err, w.armed) } e, err := hostfloor.ReadEnrollment(w.armed[2]) if err != nil || e != (hostfloor.Enrollment{Box: "ada-hiking", Enrollment: w.invite.Enrollment, InstanceID: "i-ada"}) { t.Fatalf("enrollment %+v %v", e, err) } }) } // And by construction: root's join names neither the trigger nor its // path anywhere in its source. src, err := os.ReadFile("joinroot.go") if err != nil { t.Fatal(err) } if bytes.Contains(src, []byte("joinRequestMarker")) || bytes.Contains(src, []byte("system-join-requested")) { t.Fatal("joinroot.go reads its trigger") } } // A group box names its invitation by its group reference. While IMDS has // not given it, there is no name, and the join says so; it never reads the // invitation of "" and reports "not invited". func TestRootJoinNamesAGroupBoxByItsReferenceOrFails(t *testing.T) { group := groupEnv(map[string]string{hostenv.BoxReference: hostenv.GroupReference}) withGroupIdentity(t, "", "", errors.New("IMDS answered 500")) 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" { t.Fatalf("no identity: %q %v", got, err) } withGroupIdentity(t, "123456789012", "i-0a61f580df1acfa65", nil) want := hostactors.GroupBoxReference("123456789012", "i-0a61f580df1acfa65") if got, err := joinBoxLabel(group, "machine"); err != nil || got != want { t.Fatalf("group box: %q %v, want %q", got, err, want) } // Any other box keeps metricsBoxLabel's name. other := groupEnv(nil) if got, err := joinBoxLabel(other, "machine"); err != nil || got != metricsBoxLabel(other, "machine") || got == "" { t.Fatalf("other box: %q %v", got, err) } // And a run with no name reads nothing. w := newRootJoinWorld(t, true) w.j.box = "" w.j.store = nil if got, err := w.j.run(context.Background()); err == nil || got == joinNotInvited || w.armed != nil { t.Fatalf("nameless: %s %v %v", got, err, w.armed) } }