Merge pull request 'A provider serves its consumers its own composed names (ADR 0346, issue 624)' (#306) from feat/346-a-provider-serves-its-own-names into main
This commit was merged in pull request #306.
This commit is contained in:
@@ -684,7 +684,7 @@ func artifactStoreOnNetwork(ctx context.Context, inv *inventory.Inventory,
|
|||||||
if !offers {
|
if !offers {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
serves, err := servedOnNode(ctx, inv, machine, m, catalogue.ArtifactStoreProvision)
|
serves, _, err := servedOnNode(ctx, inv, machine, m, catalogue.ArtifactStoreProvision)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", false, fmt.Errorf("reading where %s puts the artifact store: %w", machine, err)
|
return "", "", false, fmt.Errorf("reading where %s puts the artifact store: %w", machine, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -289,9 +289,14 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
caps, _ := inv.ProfileOf(ctx, n.Name)
|
caps, _ := inv.ProfileOf(ctx, n.Name)
|
||||||
|
// Its public domain too (novox/hq ADR 0346): a provider elsewhere serves its own composed names,
|
||||||
|
// and composes them as its own plan does, under its own domain.
|
||||||
|
// Read as its profile is: a machine whose domain cannot be read (removed since the list was read)
|
||||||
|
// composes no public name, which leaves the key out and says so, and fails no other machine's plan.
|
||||||
|
domain, _ := inv.PublicDomainOf(ctx, n.Name)
|
||||||
others = append(others, candidate{
|
others = append(others, candidate{
|
||||||
catalogue.Node{Name: n.Name, Site: siteOf[n.Name], Capabilities: caps,
|
catalogue.Node{Name: n.Name, Site: siteOf[n.Name], Capabilities: caps,
|
||||||
At: onNetwork[n.Name]}, theirs})
|
At: onNetwork[n.Name], PublicDomain: domain}, theirs})
|
||||||
}
|
}
|
||||||
|
|
||||||
offered := map[string][]catalogue.Provider{}
|
offered := map[string][]catalogue.Provider{}
|
||||||
@@ -310,12 +315,18 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory,
|
|||||||
// What that module says a consumer needs to know, with that node's settings on
|
// What that module says a consumer needs to know, with that node's settings on
|
||||||
// it: a port somebody moved on the provider is a port its consumers must be told
|
// it: a port somebody moved on the provider is a port its consumers must be told
|
||||||
// about, and the two coming from different places is how they come to disagree.
|
// about, and the two coming from different places is how they come to disagree.
|
||||||
serves, err := servedOnNode(ctx, inv, o.node.Name, m, name)
|
serves, layers, err := servedOnNode(ctx, inv, o.node.Name, m, name)
|
||||||
|
if err != nil {
|
||||||
|
return catalogue.World{}, err
|
||||||
|
}
|
||||||
|
// And its own composed names, as that node composes them (novox/hq ADR 0346): a name it
|
||||||
|
// does not compose leaves the key out, said under each consumer's need.
|
||||||
|
serves, leftOut, err := got.ServedOwnNames(m, name, serves, catalogue.WithDefaults(m, layers))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return catalogue.World{}, err
|
return catalogue.World{}, err
|
||||||
}
|
}
|
||||||
offered[name] = append(offered[name], catalogue.Provider{
|
offered[name] = append(offered[name], catalogue.Provider{
|
||||||
Node: o.node.Name, At: o.node.At, Serves: serves, Module: m.Module})
|
Node: o.node.Name, At: o.node.At, Serves: serves, Module: m.Module, LeftOut: leftOut})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1426,6 +1437,16 @@ func planCommand(ctx context.Context, args []string) error {
|
|||||||
// in this output would have told anybody that.
|
// in this output would have told anybody that.
|
||||||
for _, n := range plan.Needs {
|
for _, n := range plan.Needs {
|
||||||
fmt.Printf(" needs %s from %s, for %s\n", n.Name, n.From, n.For)
|
fmt.Printf(" needs %s from %s, for %s\n", n.Name, n.From, n.For)
|
||||||
|
// And a served key the provider left out, because its machine composes no name it asks for
|
||||||
|
// (novox/hq ADR 0346 §3).
|
||||||
|
for _, line := range n.LeftOut {
|
||||||
|
fmt.Printf(" %s\n", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// What the providers on this machine leave out of what they serve, and why: said where the remedy
|
||||||
|
// is (novox/hq ADR 0346 §3).
|
||||||
|
for _, line := range plan.ServedLeftOut(settings) {
|
||||||
|
fmt.Printf("%s: %s\n", args[0], line)
|
||||||
}
|
}
|
||||||
for _, line := range unboundLines(plan) {
|
for _, line := range unboundLines(plan) {
|
||||||
fmt.Println(line)
|
fmt.Println(line)
|
||||||
@@ -1566,19 +1587,20 @@ func managerPublicKeyFor(
|
|||||||
// artifact store's trust and the references composed through it (04-ISSUES/102). Unreadable
|
// artifact store's trust and the references composed through it (04-ISSUES/102). Unreadable
|
||||||
// given ports are that node's refusal to report, not this reader's.
|
// given ports are that node's refusal to report, not this reader's.
|
||||||
func servedOnNode(ctx context.Context, inv *inventory.Inventory, node string,
|
func servedOnNode(ctx context.Context, inv *inventory.Inventory, node string,
|
||||||
m catalogue.Manifest, provision string) (map[string]any, error) {
|
m catalogue.Manifest, provision string) (map[string]any, []catalogue.Layer, error) {
|
||||||
ports, layers, err := portsGivenOn(ctx, inv, node, m)
|
ports, layers, err := portsGivenOn(ctx, inv, node, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
serves := catalogue.ServedOn(m, provision, ports)
|
serves := catalogue.ServedOn(m, provision, ports)
|
||||||
if len(serves) > 0 {
|
if len(serves) > 0 {
|
||||||
serves, err = catalogue.Settle(serves, catalogue.WithDefaults(m, layers))
|
serves, err = catalogue.Settle(serves, catalogue.WithDefaults(m, layers))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return serves, nil
|
// The layers too, read once, for what is filled after (novox/hq ADR 0346).
|
||||||
|
return serves, layers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// portsGivenOn is where a node puts a module's ports — assigned, then given over them — and the
|
// portsGivenOn is where a node puts a module's ports — assigned, then given over them — and the
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
|
)
|
||||||
|
|
||||||
|
// novox/hq ADR 0346, issue 624: a provider on another machine serves its consumers its own composed
|
||||||
|
// names, as that machine composes them; one it does not compose is left out, said under the need.
|
||||||
|
func TestAProviderElsewhereServesItsOwnComposedName(t *testing.T) {
|
||||||
|
open := aMesh(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
forge, err := catalogue.ParseManifest([]byte(`{"module":"forge","version":"1","requires":["route"],
|
||||||
|
"provides":[{"name":"npm-package-registry","scope":"mesh"}],
|
||||||
|
"contributes":{"route":{"web":{"label":"git","port":3000}}},
|
||||||
|
"serves":{"npm-package-registry":{"scheme":"http","port":3000,"npm-path":"/api/packages/novox/npm/",
|
||||||
|
"public-root":"https://${bound:route:name-web}/api/packages/novox/npm/"}}}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
register(t, open, catalogue.Manifest{Module: "proxy", Version: "1", Provides: catalogue.FromAnywhere("route"),
|
||||||
|
Receives: map[string]string{"route": "/var/lib/proxy/routes.json"}})
|
||||||
|
register(t, open, forge)
|
||||||
|
register(t, open, catalogue.Manifest{Module: "lab", Version: "1", Requires: []string{"npm-package-registry"},
|
||||||
|
Binds: map[string]string{"npm-package-registry": "/var/lib/lab/package-registry.json"}})
|
||||||
|
for _, a := range [][2]string{{"anchor", "proxy"}, {"anchor", "forge"}, {"laptop", "lab"}} {
|
||||||
|
if _, err := assign(ctx, open, a[0], a[1]); err != nil {
|
||||||
|
t.Fatalf("assign %s %s: %v", a[0], a[1], err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := open.inventory.SetPublicDomain(ctx, "anchor", "example.test"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
plan, _, err := planFor(ctx, open, "laptop")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
n := need(t, plan, "lab", "npm-package-registry")
|
||||||
|
if n.Serves["public-root"] != "https://git.example.test/api/packages/novox/npm/" || len(n.LeftOut) != 0 {
|
||||||
|
t.Fatalf("the consumer elsewhere is served the forge's public root: %v, left out %v", n.Serves, n.LeftOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := open.inventory.SetPublicDomain(ctx, "anchor", ""); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
plan, _, err = planFor(ctx, open, "laptop")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
n = need(t, plan, "lab", "npm-package-registry")
|
||||||
|
if _, there := n.Serves["public-root"]; there {
|
||||||
|
t.Fatalf("anchor composes no public name, so the key is left out: %v", n.Serves)
|
||||||
|
}
|
||||||
|
if len(n.LeftOut) != 1 || !strings.Contains(n.LeftOut[0], `without "public-root"`) {
|
||||||
|
t.Fatalf("and the need says so: %v", n.LeftOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -127,6 +127,7 @@ func boundNeed(n Needed, bound Chosen, catalogue map[string]Manifest, node Node,
|
|||||||
identity = pm.IdentityBoundOf(n.Name)
|
identity = pm.IdentityBoundOf(n.Name)
|
||||||
}
|
}
|
||||||
held.From, held.At, held.Module, held.Serves, held.Identity = p.Node, p.At, p.Module, p.Serves, identity
|
held.From, held.At, held.Module, held.Serves, held.Identity = p.Node, p.At, p.Module, p.Serves, identity
|
||||||
|
held.LeftOut = p.LeftOut
|
||||||
return held, true, ""
|
return held, true, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,9 @@ func CheckServes(m Manifest) []string {
|
|||||||
for _, key := range sortedAnyKeys(m.Serves[provision]) {
|
for _, key := range sortedAnyKeys(m.Serves[provision]) {
|
||||||
text, ok := m.Serves[provision][key].(string)
|
text, ok := m.Serves[provision][key].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
// A provider's own name is filled only in a value of its own (novox/hq ADR 0346): one
|
||||||
|
// nested in a list or an object would be served as the placeholder, unsaid.
|
||||||
|
problems = append(problems, nestedOwnNames(m, provision, key, m.Serves[provision][key])...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// A probe identity, because what is checked is the shape of the statement and not
|
// A probe identity, because what is checked is the shape of the statement and not
|
||||||
@@ -143,6 +146,8 @@ func CheckServes(m Manifest) []string {
|
|||||||
problems = append(problems, fmt.Sprintf(
|
problems = append(problems, fmt.Sprintf(
|
||||||
"%s serves %s, and the value it serves as %q %s", m.Module, provision, key, err))
|
"%s serves %s, and the value it serves as %q %s", m.Module, provision, key, err))
|
||||||
}
|
}
|
||||||
|
// And the provider's own names it may serve (novox/hq ADR 0346).
|
||||||
|
problems = append(problems, checkServedOwnNames(m, provision, key, text)...)
|
||||||
// A label longer than DNS keeps is not truncated here (asDNSLabel), so the offer's own
|
// A label longer than DNS keeps is not truncated here (asDNSLabel), so the offer's own
|
||||||
// bound has to keep the identity inside one (ADR 0225).
|
// bound has to keep the identity inside one (ADR 0225).
|
||||||
if strings.Contains(text, "${consumer:as:dns}") {
|
if strings.Contains(text, "${consumer:as:dns}") {
|
||||||
|
|||||||
@@ -2039,7 +2039,13 @@ func (r Resolution) servedOnThisMachine(provision string, with Rendering) (map[s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf("%s serving %s: %w", m.Module, provision, err)
|
return nil, false, fmt.Errorf("%s serving %s: %w", m.Module, provision, err)
|
||||||
}
|
}
|
||||||
return settled, true, nil
|
// And the provider's own names, as this machine composes them (novox/hq ADR 0346); one it does
|
||||||
|
// not compose leaves its key out, said by the plan on this machine (ServedLeftOut).
|
||||||
|
named, _, err := r.ServedOwnNames(m, provision, settled, WithDefaults(m, with.Settings[m.Module]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("%s serving %s: %w", m.Module, provision, err)
|
||||||
|
}
|
||||||
|
return named, true, nil
|
||||||
}
|
}
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ type Provider struct {
|
|||||||
// (novox/hq to-be 23), and the pair is what tells the holder of a seat apart from another module
|
// (novox/hq to-be 23), and the pair is what tells the holder of a seat apart from another module
|
||||||
// providing the same thing (ADR 0110).
|
// providing the same thing (ADR 0110).
|
||||||
Module string
|
Module string
|
||||||
|
// LeftOut says each served key left out of Serves because that node composes no name it asks
|
||||||
|
// for (novox/hq ADR 0346 §3): said under each consumer's need.
|
||||||
|
LeftOut []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Held is a claim somebody already has, used for the scopes wider than one node.
|
// Held is a claim somebody already has, used for the scopes wider than one node.
|
||||||
@@ -230,6 +233,9 @@ type Needed struct {
|
|||||||
// state, not a consumer missing its key. Set by the plan, which is the only layer that knows a
|
// state, not a consumer missing its key. Set by the plan, which is the only layer that knows a
|
||||||
// licence's manager; empty for every consumer.
|
// licence's manager; empty for every consumer.
|
||||||
Manager bool
|
Manager bool
|
||||||
|
// LeftOut says each served key the provider left out, because its machine composes no name the
|
||||||
|
// value asks for (novox/hq ADR 0346 §3).
|
||||||
|
LeftOut []string
|
||||||
// Module is the providing module on From, empty for a need answered by a record or by nothing
|
// Module is the providing module on From, empty for a need answered by a record or by nothing
|
||||||
// that serves it. A provider is a (node, module) pair (novox/hq to-be 23), and a binding to data
|
// that serves it. A provider is a (node, module) pair (novox/hq to-be 23), and a binding to data
|
||||||
// is a binding to the pair (ADR 0232).
|
// is a binding to the pair (ADR 0232).
|
||||||
@@ -582,7 +588,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
|
|||||||
bound = pm.IdentityBoundOf(want)
|
bound = pm.IdentityBoundOf(want)
|
||||||
}
|
}
|
||||||
needs = append(needs, Needed{Name: want, From: p.Node, At: p.At, Module: p.Module,
|
needs = append(needs, Needed{Name: want, From: p.Node, At: p.At, Module: p.Module,
|
||||||
Serves: p.Serves, For: because[want], SharedOwn: shared, Identity: bound})
|
Serves: p.Serves, For: because[want], SharedOwn: shared, Identity: bound, LeftOut: p.LeftOut})
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case world.Unchecked:
|
case world.Unchecked:
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A provider may tell its consumers its own composed names (novox/hq ADR 0346, issue 624).
|
||||||
|
//
|
||||||
|
// **The name is already the mesh's.** A module contributes a label to a requirement — a route — and
|
||||||
|
// the mesh joins it with the providing machine's domains; the module reads the result in its own files
|
||||||
|
// as `${bound:<requirement>:name}` (novox/hq 04-ISSUES/122). Its consumers could not be told it: a
|
||||||
|
// served value named only the consumer (ADR 0202) and settings. The forge's package registry writes
|
||||||
|
// every tarball at its public name, and a consumer that must trust that name had no source for it but a
|
||||||
|
// second copy kept by hand. So a served value may say `${bound:<requirement>:<name>}` for a requirement
|
||||||
|
// the provider contributes to, and the mesh fills it from the same composition the provider's own files
|
||||||
|
// and its proxy receive.
|
||||||
|
//
|
||||||
|
// **A name the providing machine does not compose leaves the key out** (ADR 0346 §3): a machine with
|
||||||
|
// no public domain has no public name to give, and an empty value or a refusal would be worse than a
|
||||||
|
// consumer told nothing — a refused provider machine takes every provision it offers down with it. The
|
||||||
|
// plan says which key was left out, and why, on the provider's machine and under each consumer's need.
|
||||||
|
|
||||||
|
// ownNameFacts are what a served value may name of the provider's own composition: the names, as its
|
||||||
|
// own files read them (withOwnNames).
|
||||||
|
var ownNameFacts = []string{"name", "internal-name"}
|
||||||
|
|
||||||
|
// requirementsContributedTo is every requirement m contributes to, sorted.
|
||||||
|
func requirementsContributedTo(m Manifest) []string {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for to := range m.Contributes {
|
||||||
|
seen[to] = true
|
||||||
|
}
|
||||||
|
for to := range m.ContributesMany {
|
||||||
|
seen[to] = true
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(seen))
|
||||||
|
for to := range seen {
|
||||||
|
out = append(out, to)
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ownNameKeys is every name a served value of m may say for one requirement it contributes to:
|
||||||
|
// `name` and `internal-name`, and `name-<local>` and `internal-name-<local>` for each local name it
|
||||||
|
// contributes under.
|
||||||
|
func ownNameKeys(m Manifest, to string) []string {
|
||||||
|
keys := append([]string{}, ownNameFacts...)
|
||||||
|
for _, local := range sortedKeys(m.ContributesMany[to]) {
|
||||||
|
for _, fact := range ownNameFacts {
|
||||||
|
keys = append(keys, fact+"-"+local)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServedOwnNames fills the provider's own composed names into what it serves one provision's
|
||||||
|
// consumers, from this resolution's composition of the provider's machine (ADR 0346).
|
||||||
|
//
|
||||||
|
// A value naming a name this machine does not compose is left out, with its key, and said. The map is
|
||||||
|
// copied only when something changes, as ServedTo does: the caller's may be the manifest's.
|
||||||
|
func (r Resolution) ServedOwnNames(m Manifest, provision string, serves map[string]any, layers []Layer) (
|
||||||
|
map[string]any, []string, error) {
|
||||||
|
var asking []string
|
||||||
|
for _, key := range sortedAnyKeys(serves) {
|
||||||
|
if text, ok := serves[key].(string); ok && bound.MatchString(text) {
|
||||||
|
asking = append(asking, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(asking) == 0 {
|
||||||
|
return serves, nil, nil
|
||||||
|
}
|
||||||
|
own := map[string]map[string]string{}
|
||||||
|
for _, to := range requirementsContributedTo(m) {
|
||||||
|
names, err := r.ownNames(m, to, layers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
values := map[string]string{}
|
||||||
|
withOwnNames(values, names)
|
||||||
|
own[to] = values
|
||||||
|
}
|
||||||
|
out := make(map[string]any, len(serves))
|
||||||
|
for k, v := range serves {
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
var said []string
|
||||||
|
for _, key := range asking {
|
||||||
|
text := serves[key].(string)
|
||||||
|
var missing string
|
||||||
|
filled := bound.ReplaceAllStringFunc(text, func(token string) string {
|
||||||
|
parts := bound.FindStringSubmatch(token)
|
||||||
|
if value := own[parts[1]][parts[2]]; value != "" && parts[3] == "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
if missing == "" {
|
||||||
|
missing = token
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
})
|
||||||
|
if missing != "" {
|
||||||
|
delete(out, key)
|
||||||
|
said = append(said, fmt.Sprintf(
|
||||||
|
"%s serves %s without %q: it names %s, and %s composes no such name for %s (it has no "+
|
||||||
|
"public domain, or the endpoint is not reached publicly) (novox/hq ADR 0346)",
|
||||||
|
m.Module, provision, key, missing, orHere(r.Node), m.Module))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[key] = filled
|
||||||
|
}
|
||||||
|
return out, said, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func orHere(node string) string {
|
||||||
|
if node == "" {
|
||||||
|
return "this machine"
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServedLeftOut is what the providers in this node's own set leave out of what they serve, because
|
||||||
|
// this machine composes no name a served value asks for (ADR 0346 §3): said on the provider's machine,
|
||||||
|
// where the remedy is.
|
||||||
|
func (r Resolution) ServedLeftOut(settings SettingsBy) []string {
|
||||||
|
var said []string
|
||||||
|
for _, m := range r.Modules {
|
||||||
|
for _, provision := range sortedServes(m.Serves) {
|
||||||
|
_, lines, err := r.ServedOwnNames(m, provision, m.Serves[provision], WithDefaults(m, settings[m.Module]))
|
||||||
|
if err != nil {
|
||||||
|
said = append(said, fmt.Sprintf("%s serves %s, and its own names could not be composed: %v",
|
||||||
|
m.Module, provision, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
said = append(said, lines...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return said
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkServedOwnNames refuses a served `${bound:…}` that names a requirement the module does not
|
||||||
|
// contribute to, a fact other than its own names, or a local name it does not contribute under; and
|
||||||
|
// one with a default, since a served value is left out rather than given one (ADR 0346 §3, §4).
|
||||||
|
func checkServedOwnNames(m Manifest, provision, key, text string) []string {
|
||||||
|
var problems []string
|
||||||
|
contributes := requirementsContributedTo(m)
|
||||||
|
for _, parts := range bound.FindAllStringSubmatch(text, -1) {
|
||||||
|
to, fact, fallback := parts[1], parts[2], parts[3]
|
||||||
|
says := fmt.Sprintf("%s serves %s, and the value it serves as %q says %s", m.Module, provision, key, parts[0])
|
||||||
|
switch {
|
||||||
|
case fallback != "":
|
||||||
|
problems = append(problems, says+": a served name takes no default; one the machine does not "+
|
||||||
|
"compose leaves the key out (novox/hq ADR 0346)")
|
||||||
|
case !oneOf(contributes, to):
|
||||||
|
problems = append(problems, fmt.Sprintf(
|
||||||
|
"%s, and %s contributes to %s: a served value names only the provider's own names "+
|
||||||
|
"(novox/hq ADR 0346)", says, m.Module, orNothing(quoted(contributes))))
|
||||||
|
case !oneOf(ownNameKeys(m, to), fact):
|
||||||
|
problems = append(problems, fmt.Sprintf("%s, and what %s is called through %s is %s (novox/hq ADR 0346)",
|
||||||
|
says, m.Module, to, strings.Join(ownNameKeys(m, to), ", ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return problems
|
||||||
|
}
|
||||||
|
|
||||||
|
func quoted(values []string) []string {
|
||||||
|
out := make([]string, len(values))
|
||||||
|
for i, v := range values {
|
||||||
|
out[i] = fmt.Sprintf("%q", v)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// nestedOwnNames refuses a `${bound:…}` inside a served list or object: only a string value of its own is
|
||||||
|
// filled (ServedOwnNames), so a nested one would reach the consumer as the placeholder (ADR 0346 §5).
|
||||||
|
func nestedOwnNames(m Manifest, provision, key string, value any) []string {
|
||||||
|
var found []string
|
||||||
|
var walk func(v any)
|
||||||
|
walk = func(v any) {
|
||||||
|
switch x := v.(type) {
|
||||||
|
case string:
|
||||||
|
found = append(found, bound.FindAllString(x, -1)...)
|
||||||
|
case []any:
|
||||||
|
for _, e := range x {
|
||||||
|
walk(e)
|
||||||
|
}
|
||||||
|
case map[string]any:
|
||||||
|
for _, k := range sortedAnyKeys(x) {
|
||||||
|
walk(x[k])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(value)
|
||||||
|
if len(found) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{fmt.Sprintf(
|
||||||
|
"%s serves %s, and the value it serves as %q holds %s inside a list or an object: a provider's own "+
|
||||||
|
"name is filled only in a string value of its own (novox/hq ADR 0346)",
|
||||||
|
m.Module, provision, key, strings.Join(found, ", "))}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// novox/hq ADR 0346, issue 624: the forge's package registry writes every tarball at its public name,
|
||||||
|
// and the lab, its consumer, must trust that name and no other. The name is the mesh's own composition
|
||||||
|
// of the forge's route, which the forge's files read as ${bound:route:name-web}; a consumer can now be
|
||||||
|
// served it.
|
||||||
|
|
||||||
|
const aRegistryForge = `{"module":"forge","version":"1","requires":["route"],
|
||||||
|
"provides":[{"name":"npm-package-registry","scope":"mesh"}],
|
||||||
|
"contributes":{"route":{"web":{"label":"git","port":3000}}},
|
||||||
|
"serves":{"npm-package-registry":{"scheme":"http","port":3000,"npm-path":"/api/packages/novox/npm/",
|
||||||
|
"public-root":"https://${bound:route:name-web}/api/packages/novox/npm/"}}}`
|
||||||
|
|
||||||
|
func aForgeManifest(t *testing.T) Manifest {
|
||||||
|
t.Helper()
|
||||||
|
m, err := ParseManifest([]byte(aRegistryForge))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAProviderServesItsOwnComposedName(t *testing.T) {
|
||||||
|
m := aForgeManifest(t)
|
||||||
|
r := Resolution{Node: "home", PublicDomain: "example.org"}
|
||||||
|
got, said, err := r.ServedOwnNames(m, "npm-package-registry", m.Serves["npm-package-registry"], nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got["public-root"] != "https://git.example.org/api/packages/novox/npm/" || len(said) != 0 {
|
||||||
|
t.Fatalf("the public root is the route's composed name: %v, said %v", got, said)
|
||||||
|
}
|
||||||
|
if got["npm-path"] != "/api/packages/novox/npm/" || got["port"] != float64(3000) {
|
||||||
|
t.Fatalf("the rest is served as it was: %v", got)
|
||||||
|
}
|
||||||
|
if m.Serves["npm-package-registry"]["public-root"] != "https://${bound:route:name-web}/api/packages/novox/npm/" {
|
||||||
|
t.Fatal("the manifest's own map was rewritten")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestANameTheMachineDoesNotComposeLeavesTheKeyOutAndSaysSo(t *testing.T) {
|
||||||
|
m := aForgeManifest(t)
|
||||||
|
r := Resolution{Node: "home"} // no public domain
|
||||||
|
got, said, err := r.ServedOwnNames(m, "npm-package-registry", m.Serves["npm-package-registry"], nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, there := got["public-root"]; there {
|
||||||
|
t.Fatalf("a name not composed is left out, never written as the placeholder or empty: %v", got)
|
||||||
|
}
|
||||||
|
if got["npm-path"] == nil {
|
||||||
|
t.Fatalf("only the key that names it is left out: %v", got)
|
||||||
|
}
|
||||||
|
if len(said) != 1 || !strings.Contains(said[0], `forge serves npm-package-registry without "public-root"`) ||
|
||||||
|
!strings.Contains(said[0], "home composes no such name") {
|
||||||
|
t.Fatalf("said: %v", said)
|
||||||
|
}
|
||||||
|
if lines := (Resolution{Node: "home", Modules: []Manifest{m}}).ServedLeftOut(nil); len(lines) != 1 {
|
||||||
|
t.Fatalf("the plan on the provider's machine says it: %v", lines)
|
||||||
|
}
|
||||||
|
if lines := (Resolution{Node: "home", PublicDomain: "example.org", Modules: []Manifest{m}}).ServedLeftOut(nil); len(lines) != 0 {
|
||||||
|
t.Fatalf("nothing to say when the name is composed: %v", lines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On the provider's own machine the consumer is served the same name (ADR 0346 §2).
|
||||||
|
func TestAConsumerBesideTheProviderIsServedItsName(t *testing.T) {
|
||||||
|
m := aForgeManifest(t)
|
||||||
|
consumer := Manifest{Module: "lab", Version: "1", Requires: []string{"npm-package-registry"},
|
||||||
|
Binds: map[string]string{"npm-package-registry": "/var/lib/lab/package-registry.json"}}
|
||||||
|
node := workstation()
|
||||||
|
node.PublicDomain = "example.org"
|
||||||
|
got, err := Resolve(shelf(minimalRouteProxy(), m, consumer), []string{"route-proxy", "forge", "lab"}, node, World{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
served, answered, err := got.servedOnThisMachine("npm-package-registry", Rendering{})
|
||||||
|
if err != nil || !answered {
|
||||||
|
t.Fatalf("answered %v: %v", answered, err)
|
||||||
|
}
|
||||||
|
if served["public-root"] != "https://git.example.org/api/packages/novox/npm/" {
|
||||||
|
t.Fatalf("served beside the provider: %v", served)
|
||||||
|
}
|
||||||
|
out, err := got.Declaration(Rendering{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var binding string
|
||||||
|
for _, r := range out {
|
||||||
|
if r["path"] == "/var/lib/lab/package-registry.json" {
|
||||||
|
binding, _ = r["content"].(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(binding, `"public-root": "https://git.example.org/api/packages/novox/npm/"`) &&
|
||||||
|
!strings.Contains(binding, `"public-root":"https://git.example.org/api/packages/novox/npm/"`) {
|
||||||
|
t.Fatalf("the consumer's binding carries the name:\n%s", binding)
|
||||||
|
}
|
||||||
|
if strings.Contains(binding, "${bound:") {
|
||||||
|
t.Fatalf("a placeholder reached the consumer:\n%s", binding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// From another machine, the provider's served values carry what that machine composed, and a key it
|
||||||
|
// left out is said on the consumer's need (ADR 0346 §2, §3).
|
||||||
|
func TestAConsumerElsewhereIsToldWhatTheProviderLeftOut(t *testing.T) {
|
||||||
|
consumer := Manifest{Module: "lab", Version: "1", Requires: []string{"npm-package-registry"},
|
||||||
|
Binds: map[string]string{"npm-package-registry": "/var/lib/lab/package-registry.json"}}
|
||||||
|
node := workstation()
|
||||||
|
node.At = "workstation.internal"
|
||||||
|
world := World{Offered: map[string][]Provider{"npm-package-registry": {{Node: "home", At: "home.internal",
|
||||||
|
Module: "forge", Serves: map[string]any{"port": 3000},
|
||||||
|
LeftOut: []string{`forge serves npm-package-registry without "public-root"`}}}}}
|
||||||
|
got, err := Resolve(shelf(aForgeManifest(t), consumer), []string{"lab"}, node, world)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, n := range got.Needs {
|
||||||
|
if n.Name == "npm-package-registry" {
|
||||||
|
if len(n.LeftOut) != 1 {
|
||||||
|
t.Fatalf("the need carries what was left out: %+v", n)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("no need: %+v", got.Needs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAServedNameIsCheckedWhenTheDefinitionIsRead(t *testing.T) {
|
||||||
|
for bad, want := range map[string]string{
|
||||||
|
`"public-root":"https://${bound:route:name-api}/x"`: "what forge is called through route is name, internal-name, name-web, internal-name-web",
|
||||||
|
`"public-root":"https://${bound:postgres-database:name}/x"`: `forge contributes to "route"`,
|
||||||
|
`"public-root":"https://${bound:route:name-web|default:x}/x"`: "takes no default",
|
||||||
|
`"public-root":"https://${bound:route:at}/x"`: "what forge is called through route",
|
||||||
|
} {
|
||||||
|
raw := strings.Replace(aRegistryForge, `"public-root":"https://${bound:route:name-web}/api/packages/novox/npm/"`, bad, 1)
|
||||||
|
_, err := ParseManifest([]byte(raw))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), want) {
|
||||||
|
t.Errorf("%s: want a refusal saying %q, got %v", bad, want, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nested := strings.Replace(aRegistryForge, `"public-root":"https://${bound:route:name-web}/api/packages/novox/npm/"`,
|
||||||
|
`"roots":["https://${bound:route:name-web}/x"]`, 1)
|
||||||
|
if _, err := ParseManifest([]byte(nested)); err == nil || !strings.Contains(err.Error(), "inside a list or an object") {
|
||||||
|
t.Errorf("a nested own name is refused, not served as the placeholder: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := ParseManifest([]byte(aRegistryForge)); err != nil {
|
||||||
|
t.Fatalf("its own name is accepted: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The provider's contributions file carries no own name on a consumer's entry (ADR 0346 §4): only what
|
||||||
|
// is derived for that consumer is there (ADR 0202 §5), and the provider reads its own names in its files.
|
||||||
|
func TestAProvidersOwnNameIsNotOnItsConsumersEntry(t *testing.T) {
|
||||||
|
m := aForgeManifest(t)
|
||||||
|
r := Resolution{Node: "home", PublicDomain: "example.org", Modules: []Manifest{m}}
|
||||||
|
derived, err := r.derivedFor("npm-package-registry", "lab_home", "lab", "", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if derived != nil {
|
||||||
|
t.Fatalf("a consumer's entry carries only what was derived for it: %v", derived)
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1
@@ -58,3 +58,4 @@
|
|||||||
./cmd/mesh-controller TestASettingsWalkStopsBeforeADataMoveAndPassesOverAMachineAway
|
./cmd/mesh-controller TestASettingsWalkStopsBeforeADataMoveAndPassesOverAMachineAway
|
||||||
./cmd/mesh-controller TestACrossingSendSupersedesASettingsGate
|
./cmd/mesh-controller TestACrossingSendSupersedesASettingsGate
|
||||||
./cmd/mesh-controller TestASettingsWalkFailsOnARefusalThatCannotClear
|
./cmd/mesh-controller TestASettingsWalkFailsOnARefusalThatCannotClear
|
||||||
|
./cmd/mesh-controller TestAProviderElsewhereServesItsOwnComposedName
|
||||||
|
|||||||
Reference in New Issue
Block a user