package inventory import ( "errors" "reflect" "testing" ) // novox/hq ADR 0100: a node is adopted or converged, and the controller records which. func TestANodeAddedWithoutSayingIsConverged(t *testing.T) { inv := fresh(t) if _, err := inv.AddNode(t.Context(), "anchor"); err != nil { t.Fatal(err) } n, err := inv.NodeByName(t.Context(), "anchor") if err != nil { t.Fatal(err) } if n.Adopted || !n.AdoptedSince.IsZero() { t.Fatalf("a node nobody said anything about reads as adopted: %+v", n) } } func TestAnAdoptedNodeRoundTripsThroughEveryReading(t *testing.T) { inv := fresh(t) made, err := inv.AddNodeAs(t.Context(), "anchor", true) if err != nil { t.Fatal(err) } if !made.Adopted || made.AdoptedSince.IsZero() { t.Fatalf("added adopted, got %+v", made) } byName, err := inv.NodeByName(t.Context(), "anchor") if err != nil { t.Fatal(err) } all, err := inv.Nodes(t.Context()) if err != nil { t.Fatal(err) } if !byName.Adopted || len(all) != 1 || !all[0].Adopted { t.Fatalf("adoption did not survive reading back: %+v %+v", byName, all) } // And through a token: enrolment reads the node from the token it spends. issued, err := inv.IssueToken(t.Context(), "anchor", 60e9) if err != nil { t.Fatal(err) } claimed, err := inv.Claim(t.Context(), issued.Secret, "a-key", false) if err != nil { t.Fatal(err) } if !claimed.Adopted { t.Fatal("the node a token claims lost its mode") } } func TestTakingIsRefusedOnAConvergedNodeAndForAnUnassignedModule(t *testing.T) { inv := fresh(t) if err := inv.RegisterModule(t.Context(), manifest("hello-web", nil, nil), Source{}); err != nil { t.Fatal(err) } if _, err := inv.AddNode(t.Context(), "converged"); err != nil { t.Fatal(err) } if err := inv.Assign(t.Context(), "converged", "hello-web"); err != nil { t.Fatal(err) } if err := inv.Take(t.Context(), "converged", "hello-web"); !errors.Is(err, ErrNotAdopted) { t.Fatalf("taking on a converged node gave %v", err) } if _, err := inv.AddNodeAs(t.Context(), "anchor", true); err != nil { t.Fatal(err) } if err := inv.Take(t.Context(), "anchor", "hello-web"); !errors.Is(err, ErrNotAssigned) { t.Fatalf("taking an unassigned module gave %v", err) } } func TestATakenModuleOutlivesItsAssignmentAndReturningToAdopted(t *testing.T) { inv := fresh(t) for _, m := range []string{"hello-web", "postgres"} { if err := inv.RegisterModule(t.Context(), manifest(m, nil, nil), Source{}); err != nil { t.Fatal(err) } } if _, err := inv.AddNodeAs(t.Context(), "anchor", true); err != nil { t.Fatal(err) } for _, m := range []string{"hello-web", "postgres"} { if err := inv.Assign(t.Context(), "anchor", m); err != nil { t.Fatal(err) } } if err := inv.Take(t.Context(), "anchor", "postgres"); err != nil { t.Fatal(err) } if err := inv.Take(t.Context(), "anchor", "postgres"); err != nil { t.Fatalf("taking twice is not an error: %v", err) } if err := inv.Unassign(t.Context(), "anchor", "postgres"); err != nil { t.Fatal(err) } taken, err := inv.Taken(t.Context(), "anchor") if err != nil { t.Fatal(err) } if !reflect.DeepEqual(taken, []string{"postgres"}) { t.Fatalf("unassigning un-took it: %v", taken) } took, err := inv.Converge(t.Context(), "anchor") if err != nil { t.Fatal(err) } if !reflect.DeepEqual(took, []string{"hello-web"}) { t.Fatalf("converging took %v; it takes every assigned module not yet taken", took) } n, _ := inv.NodeByName(t.Context(), "anchor") if n.Adopted { t.Fatal("converged node still reads adopted") } if err := inv.SetAdopted(t.Context(), "anchor", true); err != nil { t.Fatal(err) } taken, _ = inv.Taken(t.Context(), "anchor") if !reflect.DeepEqual(taken, []string{"hello-web", "postgres"}) { t.Fatalf("returning to adopted lost what was taken: %v", taken) } } // Converging clears the whole of a node's account of itself: what it held, what was reachable, the // firewall it found and when it said so. Keeping any of it would have `node show` report an // adopted machine's account of a converged one. func TestConvergingClearsTheAccountTheNodeGave(t *testing.T) { inv := fresh(t) ctx := t.Context() made, err := inv.AddNodeAs(ctx, "anchor", true) if err != nil { t.Fatal(err) } if err := inv.RecordAdoption(ctx, made.ID, []Held{{ID: "notes.conf", Module: "notes", Kind: "file", Target: "/etc/notes.conf"}}, "ufw", []Reach{{Protocol: "tcp", Address: "0.0.0.0", Port: 22, By: "sshd"}}); err != nil { t.Fatal(err) } if _, err := inv.Converge(ctx, "anchor"); err != nil { t.Fatal(err) } said, err := inv.AdoptionOf(ctx, "anchor") if err != nil { t.Fatal(err) } if len(said.Held) != 0 || len(said.Reachable) != 0 || said.Firewall != "" || !said.At.IsZero() { t.Fatalf("converging kept the adopted machine's account: %+v", said) } }