package catalogue_test import ( "reflect" "strings" "testing" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/overlay" ) // novox/hq issue 128, held where the host will see it: the shipped networking module's names, // through the whole composition, and not only through FactsInto. // // Composition prefixes a fact's id with the module that asked for it and passes everything else // through; a step that dropped `into` on the way would send the region as a whole file, and the // host would write the machine's hosts file over again with every unit test above still green. // onTheNetwork stands in for the overlay's generator: the node is part of the private network, // and what the generator writes is not what is under test here. type onTheNetwork struct{} func (onTheNetwork) Resources(string) ([]map[string]any, bool, error) { return []map[string]any{{"id": "overlay-config", "type": "file", "path": "/etc/wireguard/mesh0.conf", "mode": "0600", "content": "[Interface]\n"}}, true, nil } func TestTheHostsRegionArrivesAsTheHostWillReadIt(t *testing.T) { shelf := provided(t) // A resolver restarting on the names another module put on the machine, and one resource it // only runs at start — neither of which composition has any business changing. resolver, err := catalogue.ParseManifest([]byte(`{ "module": "resolver", "version": "1", "requires": ["mesh-addressing"], "resources": [ {"id": "seed", "type": "file", "path": "/etc/resolver/seed", "mode": "0644", "content": "seed\n", "at": "start"}, {"id": "daemon", "type": "service", "unit": "resolver.service", "state": "running", "restart-on": ["seed", "mesh-wireguard.fact-node-names"]} ]}`)) if err != nil { t.Fatal(err) } shelf[resolver.Module] = resolver got, err := catalogue.Resolve(shelf, []string{overlay.Domain, "resolver"}, catalogue.Node{Name: "homer", At: "homer.internal"}, catalogue.World{}) if err != nil { t.Fatal(err) } names := map[string]string{"homer.internal": "10.42.0.1", "marge.internal": "10.42.0.2"} out, err := got.Declaration(catalogue.Rendering{ Names: names, Machines: names, Suffix: "internal", Generators: map[string]catalogue.Generator{overlay.Name: onTheNetwork{}}, }) if err != nil { t.Fatal(err) } ids := map[string]map[string]any{} for _, r := range out { ids[r["id"].(string)] = r } hosts := ids[overlay.Name+".fact-node-names"] if hosts == nil { t.Fatalf("no names reached the machine; the declaration has %v", keys(ids)) } if hosts["path"] != "/etc/hosts" || hosts["into"] != "block" { t.Fatalf("the hosts file is not written into as a region: %v", hosts) } content := hosts["content"].(string) if !strings.Contains(content, "10.42.0.1\thomer.internal\thomer\t# this machine\n") { t.Errorf("the region does not name the machine:\n%s", content) } for _, floor := range []string{"Generated by the mesh", "localhost", "127.0.1.1"} { if strings.Contains(content, floor) { t.Errorf("the region carries %q, which is the machine's:\n%s", floor, content) } } // The resolver's reference to it still names a resource the host will be sent. daemon := ids["resolver.daemon"] if daemon == nil { t.Fatalf("the resolver's service was not composed: %v", keys(ids)) } for _, named := range daemon["restart-on"].([]any) { if ids[named.(string)] == nil { t.Errorf("the resolver restarts on %v, which is nothing the host is sent", named) } } if !reflect.DeepEqual(daemon["restart-on"], []any{"resolver.seed", overlay.Name + ".fact-node-names"}) { t.Errorf("restart-on is %v", daemon["restart-on"]) } // And a resource's own `at` passes through as the manifest wrote it. if seed := ids["resolver.seed"]; seed == nil || seed["at"] != "start" { t.Errorf("a resource's at did not survive composition: %v", seed) } } func keys(m map[string]map[string]any) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } return out }