package catalogue import ( "strings" "testing" ) // The mesh has two resolvers (novox/hq ADR 0223): `mesh-dns-resolver` is replicated, held on the // anchor and on the home server, each answering the same names; every machine's resolver file lists // every holder — its own first when it is one — and no public resolver. ADR 0196 listed the mesh's // resolver then a public one, and musl asks both at once and takes the first reply: from the home // server the public "no such name" for the anchor's mesh name won, every time, in every Alpine build. // resolverMachines is the anchor and the home server holding the resolver, and a laptop holding nothing. var resolverMachines = map[string]string{ "anchor.internal": "10.42.0.1", "laptop.internal": "10.42.0.2", "home.internal": "10.42.0.3"} // bothResolvers is the two holders on record, as `seat mesh-dns-resolver --add` leaves them. var bothResolvers = []Held{ {Claim: "mesh-dns-resolver", Scope: ScopeMesh, Node: "anchor", Module: "dnsmasq"}, {Claim: "mesh-dns-resolver", Scope: ScopeMesh, Node: "home", Module: "dnsmasq"}, } // twoResolverShelf is the resolver, what asks it, and a stand-in answering `mesh-addressing`. func twoResolverShelf(t *testing.T) map[string]Manifest { t.Helper() return map[string]Manifest{ "net": {Module: "net", Version: "1", Provides: []Offer{{Name: "mesh-addressing"}}}, "dnsmasq": catalogueManifest(t, "dnsmasq"), "resolv-conf": catalogueManifest(t, "resolv-conf"), } } // worldWithout is the rest of the mesh as a plan for one machine sees it: every other holder's claim // and offer, and both holders on record. func worldWithout(node string) World { w := World{Holdings: bothResolvers, Offered: map[string][]Provider{}} for _, h := range bothResolvers { if h.Node == node { continue } w.Held = append(w.Held, h) w.Offered["wildcard-resolution"] = append(w.Offered["wildcard-resolution"], Provider{Node: h.Node, At: h.Node + ".internal", Module: h.Module}) } return w } // resolvConfOn resolves and composes one machine and answers with the nameservers its resolver file // lists, in order, and the file. func resolvConfOn(t *testing.T, node string, assigned []string) ([]string, string) { t.Helper() got, err := Resolve(twoResolverShelf(t), assigned, Node{Name: node, At: node + ".internal"}, worldWithout(node)) if err != nil { t.Fatalf("%s does not resolve with two resolvers on record: %v", node, err) } out, err := got.Declaration(Rendering{ Names: resolverMachines, Machines: resolverMachines, Suffix: "internal", Holders: map[string]map[string]string{"mesh-dns-resolver": { "anchor.internal": "10.42.0.1", "home.internal": "10.42.0.3"}}, Needed: map[string]map[string]string{"dnsmasq": {"broker": "sealed"}}, }) if err != nil { t.Fatalf("%s does not compose: %v", node, err) } file := byID(out)["resolv-conf.fact-resolvers"] if file == nil || file["path"] != "/etc/resolv.conf" { t.Fatalf("%s was given no resolver file: %v", node, file) } content, _ := file["content"].(string) var servers []string for _, line := range strings.Split(content, "\n") { if strings.HasPrefix(line, "nameserver ") { servers = append(servers, strings.TrimPrefix(line, "nameserver ")) } } return servers, content } func TestTwoResolversComposeOnBothHoldersAndEachListsItselfFirst(t *testing.T) { for node, want := range map[string][]string{ "anchor": {"10.42.0.1", "10.42.0.3"}, "home": {"10.42.0.3", "10.42.0.1"}, } { servers, content := resolvConfOn(t, node, []string{"dnsmasq", "resolv-conf"}) if strings.Join(servers, " ") != strings.Join(want, " ") { t.Errorf("%s lists %v; itself first, then the other holder: %v\n%s", node, servers, want, content) } } } func TestAMachineHoldingNoResolverListsBothAndNoPublicOne(t *testing.T) { servers, content := resolvConfOn(t, "laptop", []string{"resolv-conf"}) if strings.Join(servers, " ") != "10.42.0.1 10.42.0.3" { t.Errorf("the laptop lists %v; every holder, by name, and nothing else:\n%s", servers, content) } for _, public := range []string{"1.1.1.1", "8.8.8.8", "9.9.9.9"} { if strings.Contains(content, public) { t.Errorf("a public resolver is listed beside the mesh's (ADR 0223):\n%s", content) } } } // A holder answers its own requirement, even though the other holder sorts first (issue 258 kept). func TestAHolderAnswersItsOwnRequirement(t *testing.T) { got, err := Resolve(twoResolverShelf(t), []string{"dnsmasq", "resolv-conf"}, Node{Name: "home", At: "home.internal"}, worldWithout("home")) if err != nil { t.Fatal(err) } for _, n := range got.Needs { if n.Name == "wildcard-resolution" && n.From != "home" { t.Errorf("the home server's resolver configuration is bound to %s; it holds the seat itself", n.From) } } } // A seat held once is still held once: a second claimant on another machine is refused while // nothing is on record, and a store recording two holders of it is refused, naming the seat. func TestASingleHolderMeshSeatStillRefusesASecondHolder(t *testing.T) { store := shelf(mod("postgres", nil, nil, nil, Claim{Name: "mesh-store", Scope: ScopeMesh})) other := Held{Claim: "mesh-store", Scope: ScopeMesh, Node: "anchor", Module: "postgres"} if _, err := Resolve(store, []string{"postgres"}, workstation(), World{Held: []Held{other}}); err == nil || !strings.Contains(err.Error(), "one per mesh") { t.Errorf("a second claimant of a seat held once was not refused: %v", err) } here := Held{Claim: "mesh-store", Scope: ScopeMesh, Node: workstation().Name, Module: "postgres"} _, err := Resolve(store, []string{"postgres"}, workstation(), World{Held: []Held{other}, Holdings: []Held{other, here}}) if err == nil || !strings.Contains(err.Error(), "on record as held by 2") { t.Errorf("two holders on record for a seat held once were not refused: %v", err) } } // Replicated is not "whoever is assigned": two claimants with nothing on record are refused, as for // any mesh seat, and each holder is added by an act. func TestTwoUnrecordedClaimantsOfTheReplicatedSeatAreRefused(t *testing.T) { w := worldWithout("home") w.Holdings = nil _, err := Resolve(twoResolverShelf(t), []string{"dnsmasq"}, Node{Name: "home", At: "home.internal"}, w) if err == nil || !strings.Contains(err.Error(), "seat mesh-dns-resolver --to") { t.Errorf("a second resolver with nothing on record was not refused, naming the handover: %v", err) } } // Only the mesh's resolver is replicated: a seat being replicated is a decision, recorded. func TestOnlyTheResolverIsReplicated(t *testing.T) { for _, s := range Seats() { if s.Replicated != (s.Name == "mesh-dns-resolver") { t.Errorf("%s replicated = %v; only mesh-dns-resolver is (ADR 0223)", s.Name, s.Replicated) } } UseSeats([]Seat{{Name: "mesh-dns-resolver", Scope: ScopeMesh, Delivers: "wildcard-resolution"}}) defer UseSeats(DefaultSeats()) if s, _ := SeatNamed("mesh-dns-resolver"); !s.Replicated { t.Error("loading the set from the store, which has no column for it, lost the resolver's replication") } } // Every consumer is bound to the same holder whatever order the mesh resolved its machines in. func TestTheFirstHolderIsTheFirstProvider(t *testing.T) { providers := []Provider{{Node: "anchor", Module: "dnsmasq"}, {Node: "home", Module: "dnsmasq"}} for _, held := range [][]Held{bothResolvers, {bothResolvers[1], bothResolvers[0]}} { if p, ok := HolderAmong("wildcard-resolution", providers, held); !ok || p.Node != "anchor" { t.Errorf("held in order %v answered %v", held, p) } } } // One host record per machine, beside its wildcard (novox/hq issue 262): a name with a host record // says it exists and has no IPv6 address, where the wildcard alone said there is no such name, and // musl reads that as final. func TestTheResolverHasOneHostRecordPerMachine(t *testing.T) { got, err := Resolve(twoResolverShelf(t), []string{"dnsmasq"}, Node{Name: "anchor", At: "anchor.internal"}, World{}) if err != nil { t.Fatal(err) } out, err := got.Declaration(Rendering{Names: resolverMachines, Machines: resolverMachines, Suffix: "internal", Needed: map[string]map[string]string{"dnsmasq": {"broker": "sealed"}}}) if err != nil { t.Fatal(err) } content, _ := byID(out)["dnsmasq.fact-node-zones"]["content"].(string) records := map[string]int{} for _, line := range strings.Split(content, "\n") { if strings.HasPrefix(line, "host-record=") { records[strings.TrimPrefix(line, "host-record=")]++ } } for name, at := range resolverMachines { if records[name+","+at] != 1 { t.Errorf("%s has %d host records at %s, and has one:\n%s", name, records[name+","+at], at, content) } } if len(records) != len(resolverMachines) { t.Errorf("%d host records for %d machines:\n%s", len(records), len(resolverMachines), content) } }