package catalogue import ( "strings" "testing" ) // Where the answer to a requirement is allowed to live. // // A shell, a display server and a private network have to be on the machine that needs them. A // database does not — it runs somewhere and is reached over the network. Both were written // `requires`, so both were answered the same way, and the second answer was to install PostgreSQL // on every machine that runs a web application. func brokeredShelf() map[string]Manifest { return shelf( Manifest{Module: "postgres", Version: "1", Provides: FromAnywhere("database")}, Manifest{Module: "meshboard", Version: "1", Requires: []string{"database"}}, ) } func TestADatabaseIsNotInstalledOnEveryMachineThatUsesOne(t *testing.T) { // The fault this whole distinction exists for. got, err := Resolve(brokeredShelf(), []string{"meshboard"}, workstation(), World{Offered: map[string][]string{"database": {"anchor"}}}) if err != nil { t.Fatal(err) } if have := strings.Join(names(got), " "); strings.Contains(have, "postgres") { t.Fatalf("using a database installed one here: %s", have) } } func TestWhatAMachineTakesFromElsewhereIsRecorded(t *testing.T) { // It is the only part of a node's set that stops working when a *different* machine goes // away, and it is where a credential will have to be handed back. got, err := Resolve(brokeredShelf(), []string{"meshboard"}, workstation(), World{Offered: map[string][]string{"database": {"anchor"}}}) if err != nil { t.Fatal(err) } if len(got.Needs) != 1 { t.Fatalf("got %v", got.Needs) } if got.Needs[0].Name != "database" || got.Needs[0].From != "anchor" { t.Fatalf("got %v", got.Needs[0]) } if got.Needs[0].For != "meshboard" { t.Fatalf("it does not say what wanted it: %v", got.Needs[0]) } } func TestNothingInTheMeshProvidingItIsRefusedWithSomewhereToPutIt(t *testing.T) { // Refused rather than installed here. Choosing a machine to put a database on is a decision // with consequences, and nothing resolving a web application should make it silently. _, err := Resolve(brokeredShelf(), []string{"meshboard"}, workstation(), World{}) if err == nil { t.Fatal("a database was found in a mesh that has none") } if !strings.Contains(err.Error(), "assign") || !strings.Contains(err.Error(), "postgres") { t.Fatalf("the refusal does not say what to do: %v", err) } } func TestTwoNodesProvidingItIsRefusedRatherThanPicked(t *testing.T) { // Same rule as everywhere else. Picking one would be a guess about which database a person // meant, and the wrong guess is somebody's data in the wrong place. _, err := Resolve(brokeredShelf(), []string{"meshboard"}, workstation(), World{Offered: map[string][]string{"database": {"anchor", "archive"}}}) if err == nil { t.Fatal("one of two databases was picked silently") } for _, want := range []string{"anchor", "archive", "pin"} { if !strings.Contains(err.Error(), want) { t.Fatalf("the refusal does not name %s: %v", want, err) } } } func TestSayingWhichOneSettlesIt(t *testing.T) { got, err := Resolve(brokeredShelf(), []string{"meshboard"}, workstation(), World{ Offered: map[string][]string{"database": {"anchor", "archive"}}, Pinned: map[string]string{"database": "archive"}, }) if err != nil { t.Fatal(err) } if len(got.Needs) != 1 || got.Needs[0].From != "archive" { t.Fatalf("the choice was not taken: %v", got.Needs) } } func TestBeingPointedAtAMachineThatDoesNotProvideItIsRefused(t *testing.T) { // Rather than falling back to one that does. A fallback would quietly move somebody's data to // a machine they did not choose, which is the whole reason the question is asked. _, err := Resolve(brokeredShelf(), []string{"meshboard"}, workstation(), World{ Offered: map[string][]string{"database": {"anchor", "archive"}}, Pinned: map[string]string{"database": "somewhere-else"}, }) if err == nil { t.Fatal("a machine was silently given a different database from the one chosen") } if !strings.Contains(err.Error(), "somewhere-else") { t.Fatalf("the refusal does not say what was chosen: %v", err) } } func TestOneProviderDoesNotOverruleAChoice(t *testing.T) { // A single answer is normally taken silently. Not when somebody said they wanted a different // one -- that is the mesh overruling a person, which it does nowhere else. _, err := Resolve(brokeredShelf(), []string{"meshboard"}, workstation(), World{ Offered: map[string][]string{"database": {"anchor"}}, Pinned: map[string]string{"database": "archive"}, }) if err == nil { t.Fatal("the only database was used although another was chosen") } if !strings.Contains(err.Error(), "only anchor provides it") { t.Fatalf("the refusal does not say what is available: %v", err) } } func TestACatalogueThatDisagreesAboutScopeIsRefused(t *testing.T) { // If one module says a database is local and another says it is anywhere, the same // requirement means two things depending on which one happens to answer it. _, err := Resolve(shelf( Manifest{Module: "postgres", Version: "1", Provides: FromAnywhere("database")}, Manifest{Module: "sqlite", Version: "1", Provides: Offers("database")}, Manifest{Module: "meshboard", Version: "1", Requires: []string{"database"}}, ), []string{"meshboard"}, workstation(), World{}) if err == nil { t.Fatal("a catalogue that disagrees about where a database lives was accepted") } if !strings.Contains(err.Error(), "two things") { t.Fatalf("unhelpful refusal: %v", err) } } func TestALocalRequirementIsStillAnsweredLocally(t *testing.T) { // The change must not have made everything brokered. A shell is still installed here. got, err := Resolve(shelf( Manifest{Module: "zsh", Version: "1", Provides: Offers("shell")}, Manifest{Module: "tools", Version: "1", Requires: []string{"shell"}}, ), []string{"tools"}, workstation(), World{}) if err != nil { t.Fatal(err) } if have := strings.Join(names(got), " "); !strings.Contains(have, "zsh") { t.Fatalf("a shell was not installed on the machine that needs one: %s", have) } } func TestTheShortFormStillMeansHere(t *testing.T) { // `"provides": ["shell"]` must keep meaning what it meant, or every existing manifest // silently changes meaning. m, err := ParseManifest([]byte(`{"module":"zsh","version":"1","provides":["shell"]}`)) if err != nil { t.Fatal(err) } if len(m.Provides) != 1 || m.Provides[0].At() != ScopeNode { t.Fatalf("a plain name is no longer node-scoped: %v", m.Provides) } } func TestASiteScopedProvisionIsRefused(t *testing.T) { // Meaningful for a claim — one DHCP server per segment — and not yet meaningful for a // provision, because nothing knows how to reach "the one at my site". _, err := ParseManifest([]byte( `{"module":"dns","version":"1","provides":[{"name":"resolver","scope":"site"}]}`)) if err == nil { t.Fatal("a site-scoped provision was accepted") } if !strings.Contains(err.Error(), "site") { t.Fatalf("unhelpful refusal: %v", err) } }