package host import ( "bytes" "context" "errors" "path/filepath" "reflect" "strings" "testing" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostenv" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/status" ) // withGroupIdentity makes IMDS answer account and instance (or fail) for // one test, with no reference kept from before. func withGroupIdentity(t *testing.T, account, instance string, err error) { t.Helper() was := groupIdentity groupIdentity = func() (string, string, error) { return account, instance, err } groupRef.Lock() groupRef.ref = "" groupRef.Unlock() t.Cleanup(func() { groupIdentity = was groupRef.Lock() groupRef.ref = "" groupRef.Unlock() }) } func groupEnv(vals map[string]string) func(string) string { return func(k string) string { return vals[k] } } // A box its region's group launched takes its group reference everywhere // it names itself: the identity record, mode selection, root's join. An // identity IMDS has not given is no reference, never a label in its place. func TestAGroupBoxTakesItsGroupReference(t *testing.T) { group := groupEnv(map[string]string{hostenv.BoxReference: hostenv.GroupReference}) withGroupIdentity(t, "", "", errors.New("IMDS answered 500")) if got := metricsBoxLabel(group, "machine"); got != "" { t.Fatalf("no identity: %q", got) } withGroupIdentity(t, "123456789012", "i-0a61f580df1acfa65", nil) want := hostactors.GroupBoxReference("123456789012", "i-0a61f580df1acfa65") if got := metricsBoxLabel(group, "machine"); got != want { t.Fatalf("group box: %q, want %q", got, want) } } // statusFixture is a registry with one route and a status store holding // another box's report under the label this box's machine id hashes to. func statusFixture(t *testing.T) (*registry.Registry, *release.DirStore, []byte) { t.Helper() reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } if err := reg.Upsert(registry.Route{Host: "a.example.com", Kind: "static"}); err != nil { t.Fatal(err) } s := release.NewDirStore(t.TempDir()) victim := []byte(`{"label":"victim","metrics":{"instance_id":"i-victim"}}`) if _, err := s.Put(context.Background(), "status/boxes/"+status.Label("machine-1")+".json", victim, ""); err != nil { t.Fatal(err) } return reg, s, victim } // T1 (census ruling region-ha-2b): a group box whose reference has not // resolved publishes nothing, whatever the claim and region HA switches // say; above all never under status.Label, where another box's report may // stand. The reference is asked again each tick, and once it resolves the // box publishes under it. Through statusLoop's own tick and WriteBox. func TestAGroupBoxPublishesOnlyUnderItsReference(t *testing.T) { for name, env := range map[string]map[string]string{ "switches on": {hostenv.BoxReference: hostenv.GroupReference, "CARLOS_HOST_CLAIM": "1", "CARLOS_REGION_HA": "1"}, "switches off": {hostenv.BoxReference: hostenv.GroupReference}, } { t.Run(name, func(t *testing.T) { ctx := context.Background() reg, s, victim := statusFixture(t) withGroupIdentity(t, "", "", errors.New("IMDS answered 500")) asked := 0 groupIdentity = func() (string, string, error) { asked++ if asked < 3 { return "", "", errors.New("IMDS answered 500") } return "123456789012", "i-0a61f580df1acfa65", nil } probes := status.Probes{MachineID: "machine-1"} for i := 0; i < 2; i++ { err := groupStatusTick(ctx, s, reg, probes, false, "", groupEnv(env)) if !errors.Is(err, errNoGroupReference) || !strings.Contains(err.Error(), "IMDS answered 500") { t.Fatalf("tick %d: %v, want errNoGroupReference with its cause", i, err) } } keys, _ := s.List(ctx, "status/boxes/") got, _, _ := s.Get(ctx, "status/boxes/"+status.Label("machine-1")+".json") if len(keys) != 1 || !bytes.Equal(got, victim) { t.Fatalf("published with no reference: %v, victim now %s", keys, got) } if err := groupStatusTick(ctx, s, reg, probes, false, "", groupEnv(env)); err != nil { t.Fatal(err) } ref := hostactors.GroupBoxReference("123456789012", "i-0a61f580df1acfa65") if _, _, err := s.Get(ctx, "status/boxes/"+ref+".json"); err != nil || asked != 3 { t.Fatalf("after recovery: %v, IMDS asked %d times", err, asked) } if got, _, _ := s.Get(ctx, "status/boxes/"+status.Label("machine-1")+".json"); !bytes.Equal(got, victim) { t.Fatalf("the victim's report was replaced: %s", got) } }) } } // The ruling's third test: a box that is not a group box keeps its label // and its status key byte for byte: statusLoop's tick is statusTick's, with // statusLoop's own label (CollectBox's pseudonym, or a fleet box's label). func TestABoxThatIsNotAGroupBoxKeepsItsLabelAndKey(t *testing.T) { withGroupIdentity(t, "123456789012", "i-0a61f580df1acfa65", nil) for name, env := range map[string]map[string]string{ "no marker": {}, "another value": {hostenv.BoxReference: "instance-id"}, "region HA, no marker": {"CARLOS_REGION_HA": "1", "CARLOS_HOST_CLAIM": "1"}, } { getenv := groupEnv(env) if got := metricsBoxLabel(getenv, "machine-1"); got != status.Label("machine-1") { t.Fatalf("%s: label %q, want %q", name, got, status.Label("machine-1")) } for _, label := range []string{"", "kitchen"} { ctx := context.Background() reg, _, _ := statusFixture(t) mine, theirs := release.NewDirStore(t.TempDir()), release.NewDirStore(t.TempDir()) probes := status.Probes{MachineID: "machine-1"} if err := groupStatusTick(ctx, mine, reg, probes, false, label, getenv); err != nil { t.Fatal(err) } if err := statusTick(ctx, theirs, reg, probes, false, label); err != nil { t.Fatal(err) } a, _ := mine.List(ctx, "status/boxes/") b, _ := theirs.List(ctx, "status/boxes/") if !reflect.DeepEqual(a, b) || len(a) != 1 { t.Fatalf("%s, label %q: keys %v, statusTick's %v", name, label, a, b) } } } fleet := groupEnv(map[string]string{"CARLOS_FLEET": "pi", "CARLOS_FLEET_BOX": "kitchen"}) if got := metricsBoxLabel(fleet, "machine-1"); got != "kitchen" { t.Fatalf("fleet box: %q", got) } } // Task 8's gate relies on this: a marked box's label is "" only until its // reference resolves, asked again on every call, and then the reference, // kept, even if IMDS fails afterwards. func TestAGroupBoxLabelIsEmptyOnlyUntilItsReferenceResolves(t *testing.T) { group := groupEnv(map[string]string{hostenv.BoxReference: hostenv.GroupReference}) withGroupIdentity(t, "", "", nil) asked := 0 groupIdentity = func() (string, string, error) { asked++ if asked != 3 { return "", "", errors.New("IMDS answered 500") } return "123456789012", "i-0a61f580df1acfa65", nil } want := hostactors.GroupBoxReference("123456789012", "i-0a61f580df1acfa65") for i, w := range []string{"", "", want, want, want} { if got := metricsBoxLabel(group, "machine-1"); got != w { t.Fatalf("call %d: %q, want %q", i, got, w) } } if asked != 3 { t.Fatalf("IMDS asked %d times, want 3 (a resolved reference is kept)", asked) } }