package bootstrap import ( "context" "errors" "strings" "testing" "time" "github.com/novox/mesh-host/internal/declaration" ) // Retirement is destruction by omission, which is the host's ordinary behaviour: it owns what it // applied and removes what it owns and is no longer declared. These tests defend the bundle // surgery that expresses it, because a bundle that came out of it unparseable would be found by // the apply — after the file on the machine had already been replaced. func produced(t *testing.T) []byte { t.Helper() out, err := Rewrite(theRealBundle(t), held) if err != nil { t.Fatal(err) } return out.Bundle } // The temporary control plane leaves the bundle, everything else stays, and what is left parses. func TestTheTemporaryControlPlaneLeavesTheBundleAndNothingElseDoes(t *testing.T) { before, err := declaration.ParseFileTrusted(produced(t)) if err != nil { t.Fatal(err) } shorter, err := removeResource(produced(t), ControlPlaneID) if err != nil { t.Fatal(err) } after, err := declaration.ParseFileTrusted(shorter) if err != nil { t.Fatalf("the bundle without the control plane does not parse: %v\n%s", err, shorter) } if len(after.Resources) != len(before.Resources)-1 { t.Fatalf("the bundle went from %d resources to %d, and one was removed", len(before.Resources), len(after.Resources)) } if _, err := controlPlaneIn(after); err == nil { t.Error("the bundle still declares a control plane") } // The store and the broker are still exactly what they were. A retirement that took the // substrate with it would leave the machine with a module and nothing under it. for id, name := range containerNames(before) { if id == ControlPlaneID { continue } if containerNames(after)[id] != name { t.Errorf("%s was lost or renamed by the retirement", id) } } } // **A `//` inside a string is not a comment.** The substrate's own bundle carries // `postgres://…` several times, and a scanner that read the rest of those lines as a comment // would lose braces and cut the wrong thing out — silently, because what it produced would still // look like a file. func TestASchemeInsideAStringIsNotReadAsAComment(t *testing.T) { shorter, err := removeResource(produced(t), ControlPlaneID) if err != nil { t.Fatal(err) } after, err := declaration.ParseFileTrusted(shorter) if err != nil { t.Fatal(err) } // The migration action, which is the resource holding the most `://` of anything here, is // still whole. found := false for _, r := range after.Resources { if r.Identity() == "context-schemas" { found = true } } if !found { t.Error("the resource full of connection strings did not survive the removal") } } // Removing the FIRST element takes the comma after it rather than the comma before it, because // there is no comma before it. Getting this wrong produces a leading comma, which is JSON nothing // parses — and the file would already have been written. func TestRemovingTheFirstResourceLeavesAValidList(t *testing.T) { shorter, err := removeResource(produced(t), "container-runtime") if err != nil { t.Fatal(err) } after, err := declaration.ParseFileTrusted(shorter) if err != nil { t.Fatalf("removing the first resource broke the bundle: %v\n%s", err, shorter) } for _, r := range after.Resources { if r.Identity() == "container-runtime" { t.Error("the first resource is still there") } } } // A bundle that declares no such resource is refused rather than silently returned unchanged. A // removal that removed nothing and reported success would leave the apply below with nothing to // do and the installer claiming a pivot it did not finish. func TestRemovingSomethingThatIsNotThereIsRefused(t *testing.T) { if _, err := removeResource(produced(t), "nothing-of-the-sort"); err == nil { t.Fatal("a bundle was reported to have had a resource removed that it never declared") } } // A re-run after the retirement finds a bundle with no control plane in it and says so, rather // than failing. This is the idempotence of the last step, and it is the one a person is most // likely to exercise: the pivot ends here, so a re-run to check ends here too. func TestRetiringABundleThatAlreadyHasNoControlPlaneIsAlreadyDone(t *testing.T) { shorter, err := removeResource(produced(t), ControlPlaneID) if err != nil { t.Fatal(err) } var said []string out, err := RetireTheTemporaryControlPlane(context.Background(), Options{}, nil, shorter, nil, func(line string) { said = append(said, line) }) if err != nil { t.Fatal(err) } if !out.Already { t.Error("a bundle with no control plane in it was not reported as already retired") } if !strings.Contains(strings.Join(said, "\n"), "already dropped") { t.Errorf("the run does not say it was already done: %v", said) } } // A container the runtime still knows about after the apply is a pivot that did not finish. Two // control planes on one mesh are both correct and neither is wrong — but the temporary one was // supposed to go, and saying it went when it did not is the fault this project keeps naming. func TestAContainerStillThereAfterRemovalIsNotGone(t *testing.T) { previous := answerEvery answerEvery = time.Millisecond defer func() { answerEvery = previous }() stillThere := &asked{answer: func(_ string, _ []string) (string, error) { return "true running\n", nil }} gone, err := isGone(context.Background(), stillThere.run, time.Second, 0, "temp-mesh-control") if err != nil { t.Fatal(err) } if gone { t.Error("a container the runtime still describes was reported gone") } removed := &asked{answer: func(_ string, _ []string) (string, error) { return "", errors.New("No such object: temp-mesh-control") }} gone, err = isGone(context.Background(), removed.run, time.Second, 0, "temp-mesh-control") if err != nil { t.Fatal(err) } if !gone { t.Error("a container the runtime does not know about was not reported gone") } }