Merge pull request 'The machine's own resolver is a node seat, and the uplink steps back from the resolver file where it is held (hq ADR 0247)' (#120) from feat/node-resolver-seat into main
This commit was merged in pull request #120.
This commit is contained in:
@@ -1035,7 +1035,9 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
||||
// plane's; making a name resolve is the module's software. Emitted as ordinary files under
|
||||
// this module's name, so they are applied, reported and removed exactly as anything else
|
||||
// it declares.
|
||||
given, err := FactsFrom(m, r, with)
|
||||
// The resolver file is the node-resolver holder's where one is held here, and the uplink's
|
||||
// holder steps back from it (novox/hq ADR 0247).
|
||||
given, err := FactsFrom(stepsBack(m, r.Modules), r, with)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package catalogue
|
||||
|
||||
// The machine's own resolver (novox/hq ADR 0247).
|
||||
//
|
||||
// **Most machines have none.** Every machine lists the mesh's resolvers in /etc/resolv.conf and nothing
|
||||
// else, and the module holding its uplink writes that file (ADR 0223). A machine with a VPN client that
|
||||
// pushes its own resolvers for its own domains needs a third answer: those domains to the VPN's servers,
|
||||
// over the VPN's link, and every other name to the mesh's resolvers as before. One file cannot list both
|
||||
// sets of servers — musl takes the first reply, glibc the first server's "no such name" — so the domains
|
||||
// are routed by a resolver on the machine itself: the `node-resolver` seat's holder.
|
||||
//
|
||||
// **Where it is held, it owns the resolver file.** The file then names the machine's own resolver, which
|
||||
// routes; the uplink's holder steps back from writing it on that machine, by the rule below and not by
|
||||
// two modules writing one path. There are two uplink holders (NetworkManager's and systemd-networkd's),
|
||||
// and the resolver is its own module so it is written once, not once in each.
|
||||
//
|
||||
// **It knows nothing of any VPN.** Its verbs route a set of domains to a set of servers over one link,
|
||||
// list what is routed and remove a route. A module wrapping a VPN client that writes /etc/resolv.conf
|
||||
// itself carries its own adapter and calls those verbs; a VPN that tells systemd-resolved its link's DNS
|
||||
// itself needs none.
|
||||
|
||||
// ResolverSeat is the machine's own resolver (novox/hq ADR 0247).
|
||||
const ResolverSeat = "node-resolver"
|
||||
|
||||
// ResolverFile is the machine's resolver file: the uplink holder's, or the node-resolver holder's where
|
||||
// one is held.
|
||||
const ResolverFile = "/etc/resolv.conf"
|
||||
|
||||
// resolverVerbs is the contract every holder of node-resolver serves (novox/hq ADR 0247): what is routed
|
||||
// where, route a set of domains, and take a route away. The same verbs are served on the machine itself to
|
||||
// the modules there, so what a VPN pushed never crosses the bus to be routed; on the mesh they are the
|
||||
// operator's way to read and correct the same.
|
||||
func resolverVerbs() []Verb {
|
||||
return []Verb{
|
||||
{Name: "routes", Description: "What this machine's own resolver sends where: the mesh's resolvers, " +
|
||||
"which answer every name not routed elsewhere, and each link given servers of its own with the " +
|
||||
"domains routed to them. Also the resolver file's outside writes it kept, newest first: when, who " +
|
||||
"wrote it as far as the file says, whether a module took it, and when the resolver's own file was " +
|
||||
"put back.",
|
||||
Input: schema(map[string]string{}, nil),
|
||||
Replaces: []string{"resolvectl status", "resolvectl dns", "resolvectl domain"}},
|
||||
{Name: "route", Description: "Send these domains, and every name under them, to these servers over " +
|
||||
"this link — and only them: the link is never the machine's default route for names, and the " +
|
||||
"mesh's own domain is refused. Replaces whatever the link was given before. A link that goes away " +
|
||||
"takes its route with it.",
|
||||
Input: schema(map[string]string{
|
||||
"link": "the network link the servers are reached over, by name (a VPN's tunnel interface)",
|
||||
"domains": "the domains to route there, separated by spaces or commas",
|
||||
"servers": "the servers' addresses, separated by spaces or commas",
|
||||
}, []string{"link", "domains", "servers"}),
|
||||
Replaces: []string{"resolvectl dns", "resolvectl domain", "resolvectl default-route"}},
|
||||
{Name: "unroute", Description: "Take one link's route away: its domains go back to the mesh's " +
|
||||
"resolvers. Nothing changes when the link has none.",
|
||||
Input: schema(map[string]string{"link": "the network link, by name"}, []string{"link"}),
|
||||
Replaces: []string{"resolvectl revert"}},
|
||||
}
|
||||
}
|
||||
|
||||
// holdsSeat says whether a module claims a seat, by its current name.
|
||||
func holdsSeat(m Manifest, seat string) bool {
|
||||
for _, c := range m.Claims {
|
||||
if canonicalSeat(c.Name) == seat {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// rendersResolverFile says whether a module asks the mesh to render the machine's resolver file.
|
||||
func rendersResolverFile(m Manifest) bool {
|
||||
for _, f := range m.Facts {
|
||||
if !f.Home && f.Path == ResolverFile {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ResolverFileOwner is the module that writes the machine's resolver file among the modules on one
|
||||
// machine (novox/hq ADR 0247): the holder of node-resolver when one renders it, else nobody is named here
|
||||
// and the file is whoever's it always was — the uplink holder's (ADR 0223).
|
||||
func ResolverFileOwner(modules []Manifest) string {
|
||||
for _, m := range modules {
|
||||
if holdsSeat(m, ResolverSeat) && rendersResolverFile(m) {
|
||||
return m.Module
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// stepsBack is the module as it composes on a machine whose resolver file is the node-resolver holder's:
|
||||
// **the uplink holder's rendering of that file is left out**, and nothing else of it changes. Only the
|
||||
// uplink's holder steps back — any other module rendering the file beside the resolver's is still two
|
||||
// owners of one path, and is refused as before.
|
||||
func stepsBack(m Manifest, modules []Manifest) Manifest {
|
||||
if !holdsSeat(m, "node-uplink") || !rendersResolverFile(m) {
|
||||
return m
|
||||
}
|
||||
owner := ResolverFileOwner(modules)
|
||||
if owner == "" || owner == m.Module {
|
||||
return m
|
||||
}
|
||||
facts := make(map[string]RosterFile, len(m.Facts))
|
||||
for name, f := range m.Facts {
|
||||
if !f.Home && f.Path == ResolverFile {
|
||||
continue
|
||||
}
|
||||
facts[name] = f
|
||||
}
|
||||
m.Facts = facts
|
||||
return m
|
||||
}
|
||||
|
||||
// steppedBack is every module of one machine as it composes there (stepsBack, each).
|
||||
func steppedBack(modules []Manifest) []Manifest {
|
||||
if ResolverFileOwner(modules) == "" {
|
||||
return modules
|
||||
}
|
||||
out := make([]Manifest, len(modules))
|
||||
for i, m := range modules {
|
||||
out[i] = stepsBack(m, modules)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Defends novox/hq ADR 0247: a machine's own resolver is a node seat, held only where something requires
|
||||
// `split-dns`; where it is held it writes the resolver file and the uplink's holder steps back from it.
|
||||
|
||||
// The seat: node-scoped, decided by ADR 0247, and its three verbs required of every holder — a holder
|
||||
// exists only once the module serving them does, so nothing has to be optional while it catches up.
|
||||
func TestTheMachinesOwnResolverIsANodeSeatWithItsVerbs(t *testing.T) {
|
||||
s, ok := SeatNamed(ResolverSeat)
|
||||
if !ok {
|
||||
t.Fatalf("%s is not in the mesh's set", ResolverSeat)
|
||||
}
|
||||
if s.Scope != ScopeNode || s.Decision != "novox/hq ADR 0247" || s.Delivers != "" || s.Replicated {
|
||||
t.Errorf("%s is %+v; a node seat under ADR 0247 that delivers nothing", ResolverSeat, s)
|
||||
}
|
||||
var got []string
|
||||
for _, v := range s.Serves {
|
||||
got = append(got, v.Name)
|
||||
if v.Optional {
|
||||
t.Errorf("%s.%s is optional; its first holder serves it", ResolverSeat, v.Name)
|
||||
}
|
||||
if v.Description == "" || v.Input["type"] != "object" || len(v.Replaces) == 0 {
|
||||
t.Errorf("%s.%s has no description, no object schema or says it replaces nothing", ResolverSeat, v.Name)
|
||||
}
|
||||
}
|
||||
if strings.Join(got, " ") != "routes route unroute" {
|
||||
t.Errorf("%s serves %v, not routes, route and unroute", ResolverSeat, got)
|
||||
}
|
||||
// The verbs name a link, domains and servers, and never a VPN: the resolver knows nothing of one.
|
||||
for _, v := range s.Serves {
|
||||
if strings.Contains(strings.ToLower(v.Description), "forti") {
|
||||
t.Errorf("%s.%s names a VPN client: %s", ResolverSeat, v.Name, v.Description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// localResolver is a stand-in holder: it claims the seat and renders the resolver file naming the
|
||||
// machine's own address. What is under test is the controller's rule, not the catalogue's module.
|
||||
func localResolver() Manifest {
|
||||
return Manifest{Module: "local-resolver", Version: "1",
|
||||
Claims: []Claim{{Name: ResolverSeat, Scope: ScopeNode}},
|
||||
Facts: map[string]RosterFile{"resolvers": {Path: ResolverFile,
|
||||
Template: "# Managed by the mesh\n{{range .Machines}}{{if eq .Name $.Node}}nameserver {{.Address}}\n{{end}}{{end}}"}}}
|
||||
}
|
||||
|
||||
func splitDNSLaptop() Node {
|
||||
return Node{Name: "laptop", At: "laptop.internal", Capabilities: map[string]bool{
|
||||
"package-manager": true, "service-manager": true, "uplink-systemd-networkd": true}}
|
||||
}
|
||||
|
||||
// Where a module holds node-resolver, the machine is composed one resolver file, the holder's, naming
|
||||
// the machine's own address; the uplink's holder composes everything else it declares, and not that file.
|
||||
func TestWhereTheResolverIsHeldItWritesTheFileAndTheUplinkStepsBack(t *testing.T) {
|
||||
shelf := resolverShelf(t)
|
||||
shelf["local-resolver"] = localResolver()
|
||||
got, err := Resolve(shelf, []string{"dnsmasq", "systemd-networkd", "local-resolver"}, splitDNSLaptop(), World{})
|
||||
if err != nil {
|
||||
t.Fatalf("the resolver's holder and the uplink's were refused together: %v", err)
|
||||
}
|
||||
out, err := got.Declaration(Rendering{Names: twoMachines, Machines: twoMachines, Suffix: "internal",
|
||||
Holders: map[string]map[string]string{"mesh-dns-resolver": {"anchor.internal": "10.42.0.1"}},
|
||||
Needed: map[string]map[string]string{"dnsmasq": {"broker": "sealed"}},
|
||||
Settings: SettingsBy{"dnsmasq": {{From: "the mesh", Values: map[string]any{"listen-addresses": "127.0.0.1"}}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var files []string
|
||||
for _, r := range out {
|
||||
if r["path"] == ResolverFile {
|
||||
files = append(files, r["id"].(string))
|
||||
}
|
||||
}
|
||||
if strings.Join(files, " ") != "local-resolver.fact-resolvers" {
|
||||
t.Fatalf("the resolver file is composed as %v; once, the resolver's", files)
|
||||
}
|
||||
content := byID(out)["local-resolver.fact-resolvers"]["content"].(string)
|
||||
if !strings.Contains(content, "nameserver 10.42.0.2\n") || strings.Contains(content, "10.42.0.1") {
|
||||
t.Errorf("the resolver file does not name this machine's own resolver alone:\n%s", content)
|
||||
}
|
||||
// The uplink's holder is still composed: only the one file moved.
|
||||
uplinkComposed := false
|
||||
for _, r := range out {
|
||||
if id, _ := r["id"].(string); strings.HasPrefix(id, "systemd-networkd.") {
|
||||
uplinkComposed = true
|
||||
}
|
||||
}
|
||||
if !uplinkComposed {
|
||||
t.Errorf("the uplink's holder composed nothing once the resolver was held")
|
||||
}
|
||||
}
|
||||
|
||||
// Where nobody holds it, nothing changes: the uplink's holder writes the file, listing the mesh's
|
||||
// resolvers (ADR 0223) — every machine but the one that requires split-dns.
|
||||
func TestWithoutTheResolverTheUplinkWritesTheFileAsBefore(t *testing.T) {
|
||||
got, err := Resolve(resolverShelf(t), []string{"dnsmasq", "systemd-networkd"}, splitDNSLaptop(), World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := got.Declaration(Rendering{Names: twoMachines, Machines: twoMachines, Suffix: "internal",
|
||||
Holders: map[string]map[string]string{"mesh-dns-resolver": {"anchor.internal": "10.42.0.1"}},
|
||||
Needed: map[string]map[string]string{"dnsmasq": {"broker": "sealed"}},
|
||||
Settings: SettingsBy{"dnsmasq": {{From: "the mesh", Values: map[string]any{"listen-addresses": "127.0.0.1"}}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r := byID(out)["systemd-networkd.fact-resolvers"]; r == nil || r["path"] != ResolverFile {
|
||||
t.Fatalf("without the resolver, the uplink's holder no longer writes the resolver file: %v", r)
|
||||
}
|
||||
}
|
||||
|
||||
// Only the uplink's holder steps back. A third module rendering or declaring the file beside the
|
||||
// resolver's is two owners of one path, refused as before; and a module rendering it without holding
|
||||
// the seat is not the resolver, so the uplink's holder does not step back for it.
|
||||
func TestOnlyTheUplinkStepsBackForTheResolver(t *testing.T) {
|
||||
uplink := Manifest{Module: "uplink", Version: "1", Claims: []Claim{{Name: "node-uplink"}},
|
||||
Facts: map[string]RosterFile{"resolvers": {Path: ResolverFile, Template: "nameserver 10.42.0.1\n"}}}
|
||||
if p := checkResources([]Manifest{uplink, localResolver()}); len(p) != 0 {
|
||||
t.Errorf("the uplink's holder and the resolver's were refused together: %v", p)
|
||||
}
|
||||
other := Manifest{Module: "other", Version: "1",
|
||||
Facts: map[string]RosterFile{"mine": {Path: ResolverFile, Template: "nameserver 10.42.0.9\n"}}}
|
||||
if p := checkResources([]Manifest{uplink, localResolver(), other}); len(p) == 0 {
|
||||
t.Error("a third module wrote the resolver file beside the resolver's")
|
||||
}
|
||||
if p := checkResources([]Manifest{uplink, other}); len(p) == 0 {
|
||||
t.Error("a module that does not hold node-resolver took the resolver file from the uplink's holder")
|
||||
}
|
||||
declared := Manifest{Module: "declared", Version: "1", Resources: []map[string]any{
|
||||
{"id": "mine", "type": "file", "path": ResolverFile, "content": "nameserver 10.42.0.9\n"}}}
|
||||
if p := checkResources([]Manifest{uplink, localResolver(), declared}); len(p) == 0 {
|
||||
t.Error("a module declaring the resolver file was let beside the resolver's")
|
||||
}
|
||||
// What steps back is the one file: the uplink's other facts stay.
|
||||
uplink.Facts["hosts"] = RosterFile{Path: "/etc/elsewhere", Template: "x"}
|
||||
if got := stepsBack(uplink, []Manifest{uplink, localResolver()}); len(got.Facts) != 1 || got.Facts["hosts"].Path == "" {
|
||||
t.Errorf("the uplink's holder lost more than the resolver file: %v", got.Facts)
|
||||
}
|
||||
if got := stepsBack(uplink, []Manifest{uplink}); len(got.Facts) != 2 {
|
||||
t.Errorf("the uplink's holder stepped back with no resolver held: %v", got.Facts)
|
||||
}
|
||||
}
|
||||
|
||||
// The catalogue as it is: every holder of node-resolver renders the resolver file as the mesh's own
|
||||
// (its header is how the uplink's verb and the node-engine read a file as the mesh's), and provides
|
||||
// split-dns at the machine's reach — a requirement is answered only on the same machine and never pulls
|
||||
// the resolver in. Skipped while the catalogue has no holder.
|
||||
func TestTheCataloguesResolverHoldersWriteTheFileAndProvideSplitDNS(t *testing.T) {
|
||||
held := 0
|
||||
for _, m := range theCatalogue(t) {
|
||||
if !holdsSeat(m, ResolverSeat) {
|
||||
continue
|
||||
}
|
||||
held++
|
||||
f, ok := m.Facts["resolvers"]
|
||||
if !ok || f.Path != ResolverFile || !strings.HasPrefix(f.Template, "# Managed by the mesh") {
|
||||
t.Errorf("%s holds %s and does not render the resolver file as the mesh's: %+v", m.Module, ResolverSeat, f)
|
||||
}
|
||||
provides := false
|
||||
for _, o := range m.Provides {
|
||||
if o.Name == "split-dns" && o.Reach == ReachMachine {
|
||||
provides = true
|
||||
}
|
||||
}
|
||||
if !provides {
|
||||
t.Errorf("%s holds %s and does not provide split-dns at the machine's reach", m.Module, ResolverSeat)
|
||||
}
|
||||
}
|
||||
if held == 0 {
|
||||
t.Skip("no module of the catalogue holds node-resolver yet")
|
||||
}
|
||||
}
|
||||
|
||||
// The catalogue's systemd-resolved, composed on a machine beside its uplink: the resolver file names the
|
||||
// machine's own private address alone, its kept copy is the same file, and resolved is given every mesh
|
||||
// resolver as its default route and the private address to listen on. Skipped without the catalogue.
|
||||
func TestTheCataloguesResolverComposesOnAMachine(t *testing.T) {
|
||||
shelf := resolverShelf(t)
|
||||
shelf["systemd-resolved"] = catalogueManifest(t, "systemd-resolved")
|
||||
got, err := Resolve(shelf, []string{"dnsmasq", "systemd-networkd", "systemd-resolved"}, splitDNSLaptop(), World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := got.Declaration(Rendering{Names: twoMachines, Machines: twoMachines, 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"}},
|
||||
Settings: SettingsBy{"dnsmasq": {{From: "the mesh", Values: map[string]any{"listen-addresses": "127.0.0.1"}}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ids := byID(out)
|
||||
resolv, _ := ids["systemd-resolved.fact-resolvers"]["content"].(string)
|
||||
kept, _ := ids["systemd-resolved.fact-kept"]["content"].(string)
|
||||
if !strings.Contains(resolv, "\nnameserver 10.42.0.2\noptions timeout:1 attempts:2 edns0\n") || resolv != kept {
|
||||
t.Errorf("the resolver file does not name this machine's own resolver alone, or its copy differs:\n%s", resolv)
|
||||
}
|
||||
if ids["systemd-networkd.fact-resolvers"] != nil {
|
||||
t.Error("the uplink's holder still writes the resolver file beside the resolver's")
|
||||
}
|
||||
conf, _ := ids["systemd-resolved.fact-resolved"]["content"].(string)
|
||||
for _, want := range []string{"\nDNS=10.42.0.1 10.42.0.3 \n", "\nDNSStubListenerExtra=10.42.0.2\n", "\nFallbackDNS=\n"} {
|
||||
if !strings.Contains(conf, want) {
|
||||
t.Errorf("resolved's drop-in lacks %q:\n%s", want, conf)
|
||||
}
|
||||
}
|
||||
if s, _ := ids["systemd-resolved.fact-suffix"]["content"].(string); s != "internal\n" {
|
||||
t.Errorf("the mesh's domain is rendered as %q", s)
|
||||
}
|
||||
}
|
||||
@@ -930,6 +930,9 @@ func canonicalSeat(name string) string {
|
||||
// resolves with no refusal. What is refused is the contradiction — a path one module owns and
|
||||
// another merely accesses — because shared data is the operator's and nobody's to own.
|
||||
func checkResources(modules []Manifest) []string {
|
||||
// Judged as the machine composes them: where the node-resolver holder writes the resolver file, the
|
||||
// uplink holder's rendering of it is not composed, so it is not a second owner (novox/hq ADR 0247).
|
||||
modules = steppedBack(modules)
|
||||
var problems []string
|
||||
owner := map[string]string{}
|
||||
ownedPath := map[string]string{} // path → owning module, for the access check below
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestTheResolverConfigSeatIsGone(t *testing.T) {
|
||||
}
|
||||
|
||||
// The catalogue as it is: nothing claims the retired seat, and every manager the mesh knows holds the
|
||||
// uplink and writes the resolver file.
|
||||
// uplink and writes the resolver file; beside them only a holder of node-resolver does (ADR 0247).
|
||||
func TestTheCataloguesUplinksWriteTheResolverFileAndNothingElseDoes(t *testing.T) {
|
||||
cat := map[string]Manifest{}
|
||||
for _, m := range theCatalogue(t) {
|
||||
@@ -41,8 +41,10 @@ func TestTheCataloguesUplinksWriteTheResolverFileAndNothingElseDoes(t *testing.T
|
||||
isUplink = isUplink || u == name
|
||||
}
|
||||
for _, f := range m.Facts {
|
||||
if f.Path == "/etc/resolv.conf" && !isUplink {
|
||||
t.Errorf("%s writes /etc/resolv.conf and does not hold the uplink", name)
|
||||
// Or the machine's own resolver's, where it is held (novox/hq ADR 0247): the uplink's holder
|
||||
// steps back from the file there, and nowhere else.
|
||||
if f.Path == "/etc/resolv.conf" && !isUplink && !holdsSeat(m, ResolverSeat) {
|
||||
t.Errorf("%s writes /etc/resolv.conf and holds neither the uplink nor the machine's resolver", name)
|
||||
}
|
||||
}
|
||||
for _, r := range m.Resources {
|
||||
|
||||
@@ -292,6 +292,11 @@ var defaultSeats = append([]Seat{
|
||||
// the manager knows for it — the same for every holder, so a question about a machine's names is asked
|
||||
// the same way whatever manages its network. Optional until both holders serve them.
|
||||
{Name: "node-uplink", Scope: ScopeNode, Decision: "novox/hq ADR 0117", Serves: uplinkVerbs()},
|
||||
// The machine's own resolver (novox/hq ADR 0247): held only where something requires `split-dns` — a
|
||||
// VPN client whose domains must go to its own servers while every other name goes to the mesh's.
|
||||
// Where it is held, its holder writes the resolver file and the uplink's holder steps back from it
|
||||
// (node_resolver.go). It knows nothing of any VPN: its verbs route domains to servers over a link.
|
||||
{Name: ResolverSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0247", Serves: resolverVerbs()},
|
||||
},
|
||||
// The graphical session's roles (novox/hq ADR 0208), last because they are a workstation's.
|
||||
graphicalSessionSeats()...)
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
||||
delivered[s.Delivers] = s.Name
|
||||
}
|
||||
}
|
||||
// Thirty-seven with mesh-delivery (novox/hq ADR 0239); thirty-six since node-resolver-config retired
|
||||
// Thirty-eight with node-resolver (novox/hq ADR 0247); thirty-seven with mesh-delivery (novox/hq ADR 0239); thirty-six since node-resolver-config retired
|
||||
// into node-uplink (novox/hq ADR 0223); thirty-seven
|
||||
// since the retired node-dns-resolver went (novox/hq ADR 0220); thirty-eight with
|
||||
// node-backup (novox/hq ADR 0214); thirty-seven with node-message-bus (novox/hq ADR 0215);
|
||||
@@ -57,8 +57,8 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
||||
// node-container-runtime (ADR 0207); nineteen with node-environment and node-login-shell (ADR 0203,
|
||||
// ADR 0204); seventeen with node-build-agent (ADR 0190). One fewer once the retired
|
||||
// mesh-build-machine row goes, when no registered manifest claims it.
|
||||
if len(Seats()) != 37 {
|
||||
t.Errorf("the mesh defines %d seats rather than 37; the set is closed, so a change here is "+
|
||||
if len(Seats()) != 38 {
|
||||
t.Errorf("the mesh defines %d seats rather than 38; the set is closed, so a change here is "+
|
||||
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user