Compare commits

..
Author SHA1 Message Date
jochen 52df577d8d Say that a provider's own name is not on its consumers' entries (hq ADR 0346)
mesh/merge-gate head 52df577d8d49 merged into trunk 6f1a05366c1d — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 52df577d8d49 merged into trunk 6f1a05366c1d — pass: its merge-check.sh passed
mesh/delivery ready: it delivers once merged
2026-10-13 16:54:19 +02:00
jochen 205f1541ef Let a provider serve its consumers its own composed names (hq ADR 0346, issue 624)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head 205f1541efa5 merged into trunk 6f1a05366c1d — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 205f1541efa5 merged into trunk 6f1a05366c1d — pass: its merge-check.sh passed
The forge writes every package tarball at its public name, and its
consumers had no source for that name but a second copy kept by hand,
though the mesh composes it for the forge's route. A served value may now
name the provider's own names; a name its machine does not compose leaves
the key out, and the plan says so.
2026-10-13 16:50:13 +02:00
mesh-admin 6f1a05366c Merge pull request 'Admit a port from the machine's own local network, and never a public subnet (issue 154, ADR 0340)' (#299) from fix/154-local-network into main 2026-10-13 13:07:11 +00:00
mesh-admin 0a1dc5f93f Merge pull request 'seat: a push to a named machine waits for the machine's answer, so its call says how it went (issue 629)' (#301) from fix/629-a-push-says-how-it-went into main 2026-10-13 12:44:51 +00:00
mesh-admin ae0b384497 push-wait is untrusted, so it is set in one call like any preference (issue 629)
mesh/merge-gate head ae0b384497c2 merged into trunk f3875a6b6019 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head ae0b384497c2 merged into trunk f3875a6b6019 — pass: its merge-check.sh passed
mesh/delivery delivered
The gate warned that the push file asks for a setting without saying whether it is trusted, which makes push-wait terminal-only.
2026-10-13 12:38:31 +00:00
mesh-admin f3875a6b60 Merge pull request 'catalogue: say in the comments that a package may be declared by several modules (issue 628)' (#300) from fix/628-packages-are-shared into main 2026-10-13 12:38:30 +00:00
mesh-admin f09c5ebdec push: the wait is the call's or the controller's setting push-wait, never a length in the code (issue 629)
mesh/merge-gate head f09c5ebdec6c merged into trunk 584aa2d7d7a5 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head f09c5ebdec6c merged into trunk 584aa2d7d7a5 — pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The operator's rule: a wait is never hard-coded, at most a default is configured. The push verb takes `wait`; without it the controller's setting `push-wait` (module default 2m) applies; a setting that cannot be read waits for nothing, as a push did before, and says so.
2026-10-13 12:09:43 +00:00
mesh-admin 6d2e4c299a seat: a push to a named machine waits for the machine's answer, so its call says how it went (issue 629)
mesh/merge-gate head 6d2e4c299a34 merged into trunk 584aa2d7d7a5 — pass: builds mesh-controller → novox; no bus step; every machine composes with the chang…
mesh/repo-check head 6d2e4c299a34 merged into trunk 584aa2d7d7a5 — pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
The seat ran `push <node> --wait 0`, so a push's call ended at "sent" and nobody learned whether the machine applied it. The caller is answered before a push starts (answersFirst, issue 265), so waiting holds nobody: the call now waits up to two minutes for the machine's own report and keeps it, applied, refused with the machine's reason, or each failed resource with its error.
2026-10-13 12:03:53 +00:00
mesh-admin b654b320a1 catalogue: say in the comments that a package may be declared by several modules (issue 628)
mesh/merge-gate head b654b320a18a merged into trunk 584aa2d7d7a5 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head b654b320a18a merged into trunk 584aa2d7d7a5 — pass: its merge-check.sh passed
mesh/delivery delivered
The resolver follows ADR 0303, but four comments still stated ADR 0210's retired rule that two modules declaring one package collide, and an agent took them as the rule. What stays exclusive is what a module owns: a path, a unit, a name, and a package declared present by one module and absent by another.
2026-10-13 11:52:17 +00:00
jochen 4cc8588afb Test that a global IPv6 subnet is public and never counts as the local network (issue 154, ADR 0340)
mesh/merge-gate head 4cc8588afb87 merged into trunk 0a1dc5f93fcb — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 4cc8588afb87 merged into trunk 0a1dc5f93fcb — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/154-local-network delivered: every member is delivered
The review asked for the IPv6 side of the rule that local-network cannot open a port to the internet.
2026-10-13 13:20:50 +02:00
mesh-admin 584aa2d7d7 Merge pull request 'Never make a credential only a person can give, and give it once per provider (hq issue 164, ADR 0337)' (#298) from fix/164-an-accepted-credential-is-never-minted into main 2026-10-13 09:52:07 +00:00
jochen c9dd07f919 Never make a credential only a person can give, and give it once per provider (hq issue 164, ADR 0337)
mesh/merge-gate head c9dd07f919a2 merged into trunk 404b4409e70e — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head c9dd07f919a2 merged into trunk 404b4409e70e — pass: its merge-check.sh passed (run 2)
mesh/delivery delivered
A Servarr app's API key, an indexer's key and a downloader's password are values the provider's
software holds; the pair path made a random value for every consumer of them, sealed it to both ends
and reported success, and the provider refused it. An offer may now say `"credential": {"issued-by":
"outside"}`: SecretFor never makes a value for it, the plan reads such a pair as given or not given
and sends the consumer no credential file until a person gives it, without refusing its machine;
remakeShared and SharedSecretFor never make a provider's own secret issued outside either.
`secret accept <provider's node> <provider module> <provision>` seals one value to every consumer that
binds the provision from that provider, the provider and the operator, under one generation; the
per-pair form is refused for such a provision, and rotate names the accept. A consumer may wait for a
provision's credential: the controller excuses the wait only when the store bears it out, and its
needs-operator condition names the one accept line.

Adds the replay of issue 164 (TestReplay164), written with only what the controller had before.
2026-10-13 11:37:31 +02:00
mesh-admin 404b4409e7 Merge pull request 'A built module keeps its build edges: a new SDK plans the toolchain and its TypeScript bundles (hq issue 614)' (#296) from fix/614-a-built-module-keeps-its-build-edges into main 2026-10-13 09:35:26 +00:00
jochen 6e6cd5e26d Admit a port from the machine's own local network, and never a public subnet (issue 154, ADR 0340)
mesh/delivery-group group fix/154-local-network checking: 0 of 2 member(s) ready
mesh/merge-gate out of date: judged against trunk 1826b68a22b5, the trunk is now 404b4409e70e; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 1826b68a22b5, the trunk is now 404b4409e70e; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
A local service could only be declared public, which says the internet. local-network is a source and a reach: the private network and the private networks the machine reported on a link facing outside, judged here, shown by plan and node, and said in the rule set when there are none.
2026-10-13 11:20:22 +02:00
jochen e658a14296 Read a built module's build edges from what is kept of it, so a new SDK rebuilds its toolchain and bundles (hq issue 614)
mesh/merge-gate head e658a14296f9 merged into trunk 1826b68a22b5 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head e658a14296f9 merged into trunk 1826b68a22b5 — pass: its merge-check.sh passed
mesh/delivery delivered
A registered manifest carries no build, so mesh-tools' build.on naming the
SDK's package and every bundle's toolchain were edges only before a module was
first built: mesh-sdk 0.1.14 planned "sent nowhere". The relation now reads a
package base from the recorded name@version against the package the mesh holds,
and the toolchain from the bundles the resolved manifest keeps. Only records the
mesh already keeps are read, so it holds for every module built before it and a
rollback reads the same records. Replay R614.
2026-10-13 11:08:17 +02:00
55 changed files with 2647 additions and 232 deletions
+145
View File
@@ -0,0 +1,145 @@
package main
import (
"context"
"fmt"
"sort"
"strings"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
)
// A provision whose credential only a person outside the mesh can give (novox/hq ADR 0337, issue 164, issue 165):
// given once for its provider, at the controller's terminal, and sealed in one act to every consumer that binds the
// provision from that provider now. The value is never read back, printed or kept.
// outsideOffer is the provider module's own name when `secret accept <node> <module> <name>` names a provision that
// module offers with a credential issued outside the mesh, and "" when it names anything else (an own secret, or a
// pair with --provider).
func outsideOffer(ctx context.Context, module, name, provider string) (string, error) {
if provider != "" {
return "", nil
}
open, err := openStores(ctx)
if err != nil {
return "", err
}
defer open.Close()
shelf, err := open.inventory.Catalogue(ctx)
if err != nil {
return "", err
}
if m, known := shelf[module]; known && m.IssuedOutsideOf(name) {
return module, nil
}
return "", nil
}
// offeredOutsideBy is a module that offers the provision with a credential issued outside the mesh, or "".
func offeredOutsideBy(ctx context.Context, provision string) (string, error) {
open, err := openStores(ctx)
if err != nil {
return "", err
}
defer open.Close()
shelf, err := open.inventory.Catalogue(ctx)
if err != nil {
return "", err
}
module, _ := catalogue.OffersIssuedOutside(shelf, provision)
return module, nil
}
// consumersFrom is every consumer that binds a provision from one provider now, found by resolving every machine as
// its next send would, and the machines that could not be resolved, with why.
func consumersFrom(ctx context.Context, open *stores, provider, providerModule, provision string) (
[]inventory.PairEnd, []string, error) {
nodes, err := open.inventory.Nodes(ctx)
if err != nil {
return nil, nil, err
}
var out []inventory.PairEnd
var unresolved []string
for _, n := range nodes {
plan, _, err := planFor(ctx, open, n.Name)
if err != nil {
unresolved = append(unresolved, fmt.Sprintf("%s: %s", n.Name, oneLine(err.Error())))
continue
}
out = append(out, bindsFrom(plan, n.Name, provider, providerModule, provision)...)
}
return out, unresolved, nil
}
// bindsFrom is every consumer on one machine whose resolution answers the provision from that provider. Pure.
func bindsFrom(plan catalogue.Resolution, machine, provider, providerModule, provision string) []inventory.PairEnd {
var out []inventory.PairEnd
for _, need := range plan.Needs {
if need.ByRecord || need.Name != provision || need.From != provider {
continue
}
if need.Module != "" && need.Module != providerModule {
continue
}
out = append(out, inventory.PairEnd{Consumer: machine, ConsumerModule: need.For, Local: need.Local})
}
sort.Slice(out, func(i, j int) bool {
return out[i].ConsumerModule+"\x00"+out[i].Local < out[j].ConsumerModule+"\x00"+out[j].Local
})
return out
}
// acceptForProvision gives a provision's credential issued outside the mesh, once for its provider (ADR 0337 §3).
func acceptForProvision(ctx context.Context, provider, providerModule, provision, value string) error {
open, err := openStores(ctx)
if err != nil {
return err
}
defer open.Close()
consumers, unresolved, err := consumersFrom(ctx, open, provider, providerModule, provision)
if err != nil {
return err
}
accepted, err := open.inventory.AcceptSecretForProvision(ctx, provision, provider, providerModule, value, consumers)
value = ""
if err != nil {
return err
}
fmt.Print(acceptedSaid(provider, providerModule, provision, accepted, unresolved))
if len(accepted.Sealed) == 0 {
return nil
}
if err := sendTo(ctx, open, accepted.Machines); err != nil {
return fmt.Errorf("%s is sealed and could not be sent: %w; `push %s` sends it", provision, err,
strings.Join(accepted.Machines, "`, `push "))
}
fmt.Printf(" sent %s; each consumer starts again on it\n", strings.Join(accepted.Machines, ", "))
return nil
}
// acceptedSaid is what one accept for a provider did, in words that never carry the value. Pure.
func acceptedSaid(provider, providerModule, provision string, a inventory.ProvisionAccepted, unresolved []string) string {
var b strings.Builder
if len(a.Sealed) == 0 {
fmt.Fprintf(&b, "%s from %s on %s: no consumer binds it now, so nothing was sealed; a consumer that binds "+
"later waits for it, and this line given again seals it to every consumer then\n", provision,
providerModule, provider)
} else {
var names []string
for _, c := range a.Sealed {
names = append(names, fmt.Sprintf("%s on %s%s", c.ConsumerModule, c.Consumer, asLocal(c.Local)))
}
fmt.Fprintf(&b, "%s from %s on %s is given, sealed to %s and to %s\n", provision, providerModule, provider,
strings.Join(names, ", "), provider)
fmt.Fprintf(&b, " the mesh cannot read it back, never makes one of its own for it, and will not rotate it\n")
}
for _, c := range a.Unsealable {
fmt.Fprintf(&b, " not sealed to %s on %s: no sealing key on that machine; give it again once it has one\n",
c.ConsumerModule, c.Consumer)
}
for _, u := range unresolved {
fmt.Fprintf(&b, " not known whether a consumer on %s binds it, which did not resolve; give it again once it does\n", u)
}
return b.String()
}
+3 -2
View File
@@ -203,8 +203,9 @@ func seatDependenciesOnAssign(ctx context.Context, open *stores, node string, mo
if _, err := catalogue.AssignRefusal(shelf, node, assigned, adding); err != nil {
return nil, nil, err
}
// Two modules declaring one package, path or unit is refused before anything is recorded
// (novox/hq ADR 0210, 04-ISSUES/235): kept, the node would not resolve until one came off again.
// Two modules declaring one path or unit, or one package present and absent, is refused before anything
// is recorded (novox/hq ADR 0210, ADR 0303, 04-ISSUES/235): kept, the node would not resolve until one came
// off again. Two modules declaring one package present is not a collision (ADR 0303, issue 628).
if err := catalogue.CollisionRefusal(shelf, node, assigned, adding); err != nil {
return nil, nil, err
}
+15 -1
View File
@@ -722,7 +722,7 @@ func converge(ctx context.Context, open *stores, node string, yes bool, digest s
return "", err
}
derived := derivedFilter{rules: rules, foundation: with.Foundation, mesh: with.Mesh,
outward: plan.PublicDomain != "", outwardLinks: with.OutwardLinks}
outward: plan.PublicDomain != "", outwardLinks: with.OutwardLinks, local: with.LocalNetworks}
filtering, err := inv.FilteringOf(ctx, node)
if err != nil {
return "", err
@@ -930,6 +930,9 @@ type derivedFilter struct {
// The filter constrains what arrives on them; everything arriving elsewhere is this machine's
// own guest and is not filtered.
outwardLinks []string
// local is the machine's local networks, which a port from `local-network` stays open to (novox/hq
// ADR 0340).
local []string
}
// closesOutside is what a narrowing from everywhere to the private network is called: it closes.
@@ -977,6 +980,17 @@ func (d derivedFilter) fate(r inventory.Reach) string {
if !onMesh {
return fmt.Sprintf("%s — declared by %s from the mesh only", closesOutside, by)
}
case catalogue.FromLocalNetwork:
// The private network and this machine's local networks (novox/hq ADR 0340): it closes to
// everything else outside, and says which local networks stay.
if !onMesh {
if len(d.local) == 0 {
return fmt.Sprintf("%s — declared by %s from the local network, and this machine "+
"reported none", closesOutside, by)
}
return fmt.Sprintf("WILL CLOSE to everything outside the private network and %s — declared "+
"by %s from the local network", strings.Join(d.local, ", "), by)
}
}
return fmt.Sprintf("declared by %s (from %s)", by, rule.From)
}
+1 -1
View File
@@ -55,7 +55,7 @@ func TestARepairByHandCarriesItsReason(t *testing.T) {
want string
}{
{"push", map[string]any{"node": "anchor", "why": "stuck", "cause": "sent-not-reported"},
"push anchor --wait 0 --why stuck --cause sent-not-reported"},
"push anchor --wait 0s --why stuck --cause sent-not-reported"},
{"plans", map[string]any{"close": "plan-1", "why": "the report will not come"},
"plans close plan-1 --why the report will not come"},
{"plans", map[string]any{"retry": "plan-1"}, "plans retry plan-1"},
+67
View File
@@ -0,0 +1,67 @@
package main
import (
"context"
"fmt"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/overlay"
)
// localNetworksOf is a machine's attached networks judged (novox/hq ADR 0340): which count as its local
// network, from what it last said of itself and the links it said face outside it. A machine that has
// said none judges to nothing, and a port reached from `local-network` is then admitted from the private
// network only, and said so.
func localNetworksOf(ctx context.Context, inv *inventory.Inventory, node string) ([]catalogue.LocalNetwork, error) {
account, had, err := inv.MachineOf(ctx, node)
if err != nil {
return nil, err
}
outward, err := inv.OutwardLinksOf(ctx, node)
if err != nil {
return nil, err
}
return judgedNetworks(account, had, outward, overlay.Interface), nil
}
// judgedNetworks is the judging alone, from what was read.
func judgedNetworks(account inventory.MachineAccount, had bool, outward []string, tunnel string) []catalogue.LocalNetwork {
if !had || account.Machine == nil {
return nil
}
attached := make([]catalogue.AttachedNetwork, 0, len(account.Machine.Networks))
for _, n := range account.Machine.Networks {
attached = append(attached, catalogue.AttachedNetwork{Link: n.Link, Network: n.Network})
}
return catalogue.JudgeLocalNetworks(attached, outward, tunnel)
}
// localNetworkLines is what `node` shows of a machine's attached networks (ADR 0340 §8): each one, with
// whether it counts as the local network and why, or why none is shown.
func localNetworkLines(account inventory.MachineAccount, had bool, judged []catalogue.LocalNetwork) []string {
out := []string{" its local network (what a port reached from local-network is admitted from):"}
switch {
case !had || account.Machine == nil:
return append(out, " not said — this machine has not reported what it is attached to")
case len(account.Machine.Networks) == 0:
why := account.Machine.Unknown["networks"]
if why == "" {
why = "its node-engine is older than the one that says it"
}
return append(out, " not said — "+why)
}
counted := 0
for _, j := range judged {
verdict := "not counted"
if j.Counts {
verdict = "counts"
counted++
}
out = append(out, fmt.Sprintf(" %-22s on %-10s %s: %s", j.Network, j.Link, verdict, j.Why))
}
if counted == 0 {
out = append(out, " none counts: a port reached from local-network is admitted from the private network only")
}
return out
}
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
)
// **node shows what the local network resolved to** (novox/hq ADR 0340 §8): every attached network,
// whether it counts, and why the others do not — the public subnet named as never counted.
func TestNodeShowsWhichAttachedNetworksCountAndWhy(t *testing.T) {
account := inventory.MachineAccount{Architecture: "amd64", Kernel: "linux", Machine: &inventory.MadeOf{
Networks: []inventory.AttachedNetwork{
{Link: "eth0", Network: "192.168.1.10/24"},
{Link: "eth1", Network: "203.0.113.7/24"},
{Link: "wg-mesh", Network: "10.10.0.4/24"},
}}}
judged := judgedNetworks(account, true, []string{"eth0", "eth1"}, "wg-mesh")
text := strings.Join(localNetworkLines(account, true, judged), "\n")
for _, want := range []string{
"192.168.1.0/24", "counts: in a private range (RFC 1918, 192.168.0.0/16)",
"203.0.113.0/24", "not counted: public, never counted",
"10.10.0.0/24", "the private network's own link",
} {
if !strings.Contains(text, want) {
t.Errorf("node does not say %q:\n%s", want, text)
}
}
}
// A machine that said none, or whose node-engine is older, is said as that — never as an empty list
// that reads like a machine on no network.
func TestNodeSaysWhenNoNetworkWasSaid(t *testing.T) {
older := inventory.MachineAccount{Machine: &inventory.MadeOf{}}
if text := strings.Join(localNetworkLines(older, true, nil), "\n"); !strings.Contains(text, "older than the one that says it") {
t.Errorf("an older node-engine is not said:\n%s", text)
}
public := inventory.MachineAccount{Machine: &inventory.MadeOf{
Networks: []inventory.AttachedNetwork{{Link: "eth0", Network: "198.51.100.9/24"}}}}
judged := judgedNetworks(public, true, []string{"eth0"}, "wg-mesh")
if text := strings.Join(localNetworkLines(public, true, judged), "\n"); !strings.Contains(text, "none counts") {
t.Errorf("a machine on a public network only is not said to have no local network:\n%s", text)
}
if text := strings.Join(localNetworkLines(inventory.MachineAccount{}, false, nil), "\n"); !strings.Contains(text, "not said") {
t.Errorf("a machine that never reported is not said:\n%s", text)
}
}
// The preview of a flip says what a port from the local network keeps: the local networks it stays
// open to, or that the machine reported none (ADR 0340 §6).
func TestTheFlipPreviewSaysWhatALocalNetworkPortKeeps(t *testing.T) {
rules := []catalogue.Rule{{Port: 1883, Protocol: "tcp", From: catalogue.FromLocalNetwork, Because: []string{"mosquitto"}}}
at := inventory.Reach{Protocol: "tcp", Address: "0.0.0.0", Port: 1883}
with := derivedFilter{rules: rules, mesh: []string{"10.10.0.1"}, outwardLinks: []string{"eth0"},
local: []string{"192.168.1.0/24"}}
if fate := with.fate(at); !strings.Contains(fate, "192.168.1.0/24") {
t.Errorf("the preview says %q, without the local network it stays open to", fate)
}
with.local = nil
if fate := with.fate(at); !strings.Contains(fate, "reported none") {
t.Errorf("the preview says %q, without saying no local network was reported", fate)
}
}
+1 -1
View File
@@ -75,7 +75,7 @@ func stateHealth(ctx context.Context, inv *inventory.Inventory, k *conditions.Ke
State: r.State, Reason: r.Reason, Since: r.Since, Streak: r.Streak, Restarts: r.Restarts,
Check: r.Check, Needs: r.Needs, Account: r.Account, Root: r.Root}
for _, w := range r.Waits {
kept.Waits = append(kept.Waits, inventory.Wait{Part: w.Part, Secret: w.Secret, Setting: w.Setting, What: w.What})
kept.Waits = append(kept.Waits, inventory.Wait{Part: w.Part, Secret: w.Secret, Setting: w.Setting, Provision: w.Provision, What: w.What})
}
resources = append(resources, kept)
}
+11
View File
@@ -19,6 +19,7 @@ import (
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
"github.com/novox/mesh-controller/internal/overlay"
"github.com/novox/mesh-controller/internal/token"
)
@@ -884,6 +885,16 @@ func showNode(ctx context.Context, stored *stores, name string) error {
for _, line := range machineLines(account, had, time.Now()) {
fmt.Println(line)
}
// And which of the networks it is attached to count as its local network, and why the others
// do not (novox/hq ADR 0340 §8).
outward, err := inv.OutwardLinksOf(ctx, name)
if err != nil {
fmt.Printf(" its local network could NOT be judged: %v\n", err)
} else {
for _, line := range localNetworkLines(account, had, judgedNetworks(account, had, outward, overlay.Interface)) {
fmt.Println(line)
}
}
}
held, err := inv.Profile(ctx, name)
+139 -14
View File
@@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"slices"
"sort"
"strings"
"time"
@@ -44,6 +45,28 @@ func needsOperatorKey(module, node string) string {
type operatorWaitFacts struct {
manifests map[string]catalogue.Manifest
given map[string]map[string]time.Time
// outside is, per provision some module offers with a credential issued outside the mesh, that provider module
// and the machines it is assigned to (novox/hq ADR 0337); pairsGiven, per "<module>@<machine>", the provisions
// it holds a value a person gave for, sealed to the keys both ends have now. Absent is not known: no wait for a
// provision of it is excused.
outside map[string]outsideProvider
pairsGiven map[string]map[string]bool
}
// outsideProvider is the module offering a provision whose credential only a person gives, and where it runs.
type outsideProvider struct {
Module string
Machines []string
}
// acceptLine is the one line that gives an outside provision's credential for every consumer of its provider: on
// the provider's machine when it runs on one, named as <machine> when on several or none.
func (p outsideProvider) acceptLine(provision string) string {
machine := "<its machine>"
if len(p.Machines) == 1 {
machine = p.Machines[0]
}
return inventory.AcceptLine(machine, p.Module, provision)
}
// checkWait is nil when a wait is excused, and otherwise why not, in words. Pure.
@@ -52,9 +75,18 @@ func checkWait(module, machine string, w inventory.Wait, f operatorWaitFacts) er
if !known {
return fmt.Errorf("says it waits for %s, and the mesh holds no manifest of %s to check it against", waitNames(w), module)
}
named := 0
for _, n := range []string{w.Secret, w.Setting, w.Provision} {
if n != "" {
named++
}
}
switch {
case w.Secret != "" && w.Setting != "", w.Secret == "" && w.Setting == "":
return fmt.Errorf("says it waits, naming %s, where a wait names one secret or one setting", waitNames(w))
case named != 1:
return fmt.Errorf("says it waits, naming %s, where a wait names one secret, one setting or one provision",
waitNames(w))
case w.Provision != "":
return checkProvisionWait(module, machine, w, m, f)
case w.Setting != "":
if _, declared := m.Settings[w.Setting]; !declared {
return fmt.Errorf("says it waits for the setting %s, which %s does not declare", w.Setting, module)
@@ -80,20 +112,46 @@ func checkWait(module, machine string, w inventory.Wait, f operatorWaitFacts) er
return nil
}
// checkProvisionWait excuses a wait for a provision's credential only when the store bears it out (novox/hq ADR 0337
// §6): the module requires the provision, a provider offers it with a credential issued outside the mesh, and the
// module on this machine holds no value a person gave for it under the keys both ends have now.
func checkProvisionWait(module, machine string, w inventory.Wait, m catalogue.Manifest, f operatorWaitFacts) error {
if !slices.Contains(m.Requires, w.Provision) {
return fmt.Errorf("says it waits for the provision %s, which %s does not require", w.Provision, module)
}
if _, outside := f.outside[w.Provision]; !outside {
return fmt.Errorf("says it waits for the provision %s, whose credential the mesh gives itself: only a "+
"credential issued outside the mesh waits for the operator", w.Provision)
}
given, readable := f.pairsGiven[module+"@"+machine]
if !readable {
return fmt.Errorf("says it waits for the provision %s, and what was given on %s could not be read", w.Provision, machine)
}
if given[w.Provision] {
return fmt.Errorf("says it waits for the provision %s, whose credential was given", w.Provision)
}
return nil
}
// waitRefusedPrefix opens every reason checkWait gives, so the words of a refused wait are told from a check's own.
const waitRefusedPrefix = "says it waits"
// waitNames is what a wait names, as "the secret x" or "the setting y".
func waitNames(w inventory.Wait) string {
switch {
case w.Secret != "" && w.Setting != "":
return "the secret " + w.Secret + " and the setting " + w.Setting
case w.Secret != "":
return "the secret " + w.Secret
case w.Setting != "":
return "the setting " + w.Setting
var names []string
if w.Secret != "" {
names = append(names, "the secret "+w.Secret)
}
return "nothing"
if w.Setting != "" {
names = append(names, "the setting "+w.Setting)
}
if w.Provision != "" {
names = append(names, "the provision "+w.Provision)
}
if len(names) == 0 {
return "nothing"
}
return strings.Join(names, " and ")
}
// checkWaiting reads one machine's resources against the facts: every waiting resource whose waits all check out is
@@ -107,11 +165,17 @@ func checkWaiting(machine string, rs []inventory.ResourceHealth, f operatorWaitF
if len(r.Waits) == 0 {
why = fmt.Errorf("says it waits, and names nothing it waits for")
}
waits := make([]inventory.Wait, 0, len(r.Waits))
for _, w := range r.Waits {
if why == nil {
why = checkWait(r.Module, machine, w, f)
}
if p, outside := f.outside[w.Provision]; outside && w.Provision != "" {
w.Act = p.acceptLine(w.Provision)
}
waits = append(waits, w)
}
r.Waits = waits
if why != nil {
r.State, r.Reason = link.StateUnhealthy, why.Error()
}
@@ -150,6 +214,10 @@ func operatorWaitSaid(module, machine string, waits []inventory.Wait) string {
if w.Secret != "" {
part += fmt.Sprintf(", given with `nox secret ask %s %s %s`", machine, module, w.Secret)
}
if w.Provision != "" && w.Act != "" {
// One line for the provider, which seals it to every consumer of it (ADR 0337 §3, §7).
part += fmt.Sprintf(", given for every consumer with `%s`", w.Act)
}
parts = append(parts, part+")")
}
return fmt.Sprintf("%s on %s waits for the operator: %s", module, machine, strings.Join(parts, "; "))
@@ -172,7 +240,7 @@ func needsOperatorObservation(module, node string, waits []inventory.Wait, rs []
func needsOperatorWords(module, node string, waits []inventory.Wait) words {
var acts []string
seen := map[string]bool{}
secret := false
secret, provision := false, false
for _, w := range waits {
var act string
switch {
@@ -180,6 +248,9 @@ func needsOperatorWords(module, node string, waits []inventory.Wait) words {
act, secret = fmt.Sprintf("type %s at %s's desk prompt", w.What, node), true
case w.Setting != "":
act = fmt.Sprintf("approve %s of %s on %s when it is proposed to you", w.Setting, module, node)
case w.Provision != "":
// ADR 0337 §7: "give <what> of <provider> on <machine>"; the line is in the summary.
act, provision = "give "+w.What, true
}
if act != "" && !seen[act] {
seen[act] = true
@@ -197,6 +268,10 @@ func needsOperatorWords(module, node string, waits []inventory.Wait) words {
explanation += " A hidden prompt opens at the desk when the secret is asked for, and what you type there " +
"is sealed to the machine."
}
if provision {
explanation += " A key only you can give is given once for the app that issued it, and reaches every " +
"module that uses it."
}
explanation += " Its update is in place and nothing was undone; it carries on by itself once it is given."
return words{
Headline: fmt.Sprintf("%s waits for you on %s", module, node),
@@ -217,6 +292,12 @@ func readWaitFacts(ctx context.Context, inv *inventory.Inventory, machine string
if f.given == nil {
f.given = map[string]map[string]time.Time{}
}
if f.outside == nil {
f.outside = map[string]outsideProvider{}
}
if f.pairsGiven == nil {
f.pairsGiven = map[string]map[string]bool{}
}
if inv == nil {
return
}
@@ -238,15 +319,59 @@ func readWaitFacts(ctx context.Context, inv *inventory.Inventory, machine string
}
}
}
if _, read := f.given[module+"@"+machine]; read {
if _, read := f.given[module+"@"+machine]; !read {
if given, err := inv.GivenOwnSecrets(ctx, machine, module); err == nil {
f.given[module+"@"+machine] = given
}
}
if _, read := f.pairsGiven[module+"@"+machine]; !read {
if given, err := inv.GivenPairsOf(ctx, machine, module); err == nil {
f.pairsGiven[module+"@"+machine] = given
}
}
// The provisions it requires whose credential only a person gives, and where their provider runs (ADR 0337).
m, known := f.manifests[module]
if !known {
continue
}
if given, err := inv.GivenOwnSecrets(ctx, machine, module); err == nil {
f.given[module+"@"+machine] = given
for _, provision := range m.Requires {
if _, read := f.outside[provision]; read {
continue
}
if shelf == nil {
var err error
if shelf, err = inv.Catalogue(ctx); err != nil {
shelf = map[string]catalogue.Manifest{}
}
}
provider, outside := catalogue.OffersIssuedOutside(shelf, provision)
if !outside {
continue
}
f.outside[provision] = outsideProvider{Module: provider, Machines: machinesRunning(ctx, inv, provider)}
}
}
}
// machinesRunning is every machine a module is assigned to, by name; a read that fails is left out.
func machinesRunning(ctx context.Context, inv *inventory.Inventory, module string) []string {
nodes, err := inv.Nodes(ctx)
if err != nil {
// Only the summary's accept line reads it, which then names the provider's machine "<its machine>".
// empty-on-error: no wait is excused or refused by it
return nil
}
var out []string
for _, n := range nodes {
assigned, err := inv.Assigned(ctx, n.Name)
if err == nil && slices.Contains(assigned, module) {
out = append(out, n.Name)
}
}
sort.Strings(out)
return out
}
// waitingModules is every module with a waiting resource in a statement.
func waitingModules(rs []inventory.ResourceHealth) []string {
seen := map[string]bool{}
+2 -2
View File
@@ -55,8 +55,8 @@ func TestAWaitIsExcusedOnlyWhenItChecksOut(t *testing.T) {
{"a secret the mesh makes", inventory.Wait{Part: "p", Secret: "made", What: "w"}, factsGiven(nil), "mesh makes itself"},
{"the bus account", inventory.Wait{Part: "p", Secret: "broker", What: "w"}, factsGiven(nil), "mesh makes itself"},
{"a setting not declared", inventory.Wait{Part: "p", Setting: "logins", What: "w"}, factsGiven(nil), "does not declare"},
{"both", inventory.Wait{Part: "p", Secret: "licence", Setting: "smb-users", What: "w"}, factsGiven(nil), "one secret or one setting"},
{"neither", inventory.Wait{Part: "p", What: "w"}, factsGiven(nil), "one secret or one setting"},
{"both", inventory.Wait{Part: "p", Secret: "licence", Setting: "smb-users", What: "w"}, factsGiven(nil), "one secret, one setting or one provision"},
{"neither", inventory.Wait{Part: "p", What: "w"}, factsGiven(nil), "one secret, one setting or one provision"},
{"no manifest known", passwordWait, operatorWaitFacts{}, "no manifest"},
{"what was given cannot be read", passwordWait,
operatorWaitFacts{manifests: map[string]catalogue.Manifest{"mounts": mountsManifest}}, "could not be read"},
@@ -0,0 +1,83 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
)
// One accept for a provider reaches every consumer that binds the provision from it, and each consumer's send
// places it; the per-pair form naming a consumer is refused for such a provision, naming the provider's form; a
// value the mesh made before the rule is not placed (novox/hq ADR 0337 §2, §3, §4, issue 164, issue 165). Needs the
// store (MESH_TEST_POSTGRES), and is named in testdata/store-tests.
func TestOneAcceptForAProviderIsPlacedForEveryConsumer(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
// Before the rule: tv's offer said nothing of its credential, and the send made one per pair.
register(t, open, catalogue.Manifest{Module: "tv", Version: "1",
Provides: []catalogue.Offer{{Name: "tv-api", Scope: catalogue.ScopeMesh}}})
for _, m := range []string{"requests", "indexer"} {
register(t, open, catalogue.Manifest{Module: m, Version: "1", Requires: []string{"tv-api"},
Secrets: map[string]string{"tv-api": "/var/lib/mesh/" + m + "/tv-api.secret"}})
}
for _, a := range [][2]string{{"anchor", "tv"}, {"laptop", "requests"}, {"laptop", "indexer"}} {
if _, err := assign(ctx, open, a[0], a[1]); err != nil {
t.Fatal(err)
}
}
placed := func(module string) bool {
t.Helper()
plan, settings, err := planFor(ctx, open, "laptop")
if err != nil {
t.Fatalf("laptop was refused: %v", err)
}
declared, err := declarationFor(ctx, open, "laptop", plan, settings)
if err != nil {
t.Fatalf("laptop did not compose: %v", err)
}
for _, r := range declared.Resources {
if r["path"] == "/var/lib/mesh/"+module+"/tv-api.secret" && r["sealed"] != nil {
return true
}
}
return false
}
if !placed("requests") || !placed("indexer") {
t.Fatal("before the rule, the send made and placed a credential per pair")
}
// The offer now says only a person gives it: the values the mesh made are not placed.
register(t, open, catalogue.Manifest{Module: "tv", Version: "2",
Provides: []catalogue.Offer{{Name: "tv-api", Scope: catalogue.ScopeMesh,
Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}}})
if placed("requests") || placed("indexer") {
t.Fatal("a credential nobody gave was placed")
}
// The per-pair form is refused, naming the provider's form, and reads nothing.
file := filepath.Join(t.TempDir(), "key")
if err := os.WriteFile(file, []byte("the app's key\n"), 0o600); err != nil {
t.Fatal(err)
}
err := secretCommand(ctx, []string{"accept", "laptop", "requests", "tv-api", "--provider", "anchor", "--from", file})
if err == nil || !strings.Contains(err.Error(), "secret accept anchor tv tv-api") {
t.Fatalf("the per-pair form was not refused naming the provider's: %v", err)
}
consumers, unresolved, err := consumersFrom(ctx, open, "anchor", "tv", "tv-api")
if err != nil || len(unresolved) != 0 {
t.Fatalf("the consumers of tv on anchor could not be found: %v %v", err, unresolved)
}
if len(consumers) != 2 {
t.Fatalf("the consumers of tv on anchor are %+v; want requests and indexer on laptop", consumers)
}
if _, err := open.inventory.AcceptSecretForProvision(ctx, "tv-api", "anchor", "tv", "the app's key", consumers); err != nil {
t.Fatal(err)
}
if !placed("requests") || !placed("indexer") {
t.Fatal("the value given once for the provider was not placed for every consumer")
}
}
+113
View File
@@ -0,0 +1,113 @@
package main
import (
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
// A consumer may wait for a provision's credential only a person gives (novox/hq ADR 0337 §5–§7, issue 164).
var requestsManifest = catalogue.Manifest{Module: "requests", Version: "1", Requires: []string{"tv-api", "db"},
Secrets: map[string]string{"tv-api": "/s/tv-api", "db": "/s/db"}}
var tvKeyWait = inventory.Wait{Part: "the tv library", Provision: "tv-api", What: "the API key of tv"}
func outsideFacts(given map[string]bool) operatorWaitFacts {
return operatorWaitFacts{manifests: map[string]catalogue.Manifest{"requests": requestsManifest},
outside: map[string]outsideProvider{"tv-api": {Module: "tv", Machines: []string{"media"}}},
pairsGiven: map[string]map[string]bool{"requests@laptop": given}}
}
// §6: excused only when the module requires the provision, its provider says it is issued outside, and no value a
// person gave is held for the pair under the keys both ends have now.
func TestAWaitForAProvisionIsExcusedOnlyWhenTheStoreBearsItOut(t *testing.T) {
for _, c := range []struct {
name string
w inventory.Wait
f operatorWaitFacts
says string
}{
{"required, issued outside, not given", tvKeyWait, outsideFacts(nil), ""},
{"given", tvKeyWait, outsideFacts(map[string]bool{"tv-api": true}), "was given"},
{"not required", inventory.Wait{Part: "p", Provision: "music-api", What: "w"}, outsideFacts(nil), "does not require"},
{"a credential the mesh gives itself", inventory.Wait{Part: "p", Provision: "db", What: "w"}, outsideFacts(nil),
"mesh gives itself"},
{"what was given cannot be read", tvKeyWait, operatorWaitFacts{
manifests: map[string]catalogue.Manifest{"requests": requestsManifest},
outside: map[string]outsideProvider{"tv-api": {Module: "tv"}}}, "could not be read"},
{"a provision and a secret", inventory.Wait{Part: "p", Provision: "tv-api", Secret: "x", What: "w"},
outsideFacts(nil), "one secret, one setting or one provision"},
} {
err := checkWait("requests", "laptop", c.w, c.f)
switch {
case c.says == "" && err != nil:
t.Errorf("%s: refused: %v", c.name, err)
case c.says != "" && (err == nil || !strings.Contains(err.Error(), c.says)):
t.Errorf("%s: %v; want a refusal saying %q", c.name, err, c.says)
}
}
}
// §7: an excused wait is the consumer's needs-operator condition, in plain words, its summary naming the one line
// that gives the credential for every consumer of the provider; the wait as kept is not changed.
func TestAWaitForAProvisionNamesTheOneAcceptForItsProvider(t *testing.T) {
r := inventory.ResourceHealth{Module: "requests", Resource: "requests.app", Kind: "container", Target: "requests",
State: link.StateWaiting, Check: "tool", Waits: []inventory.Wait{tvKeyWait}}
kept := []inventory.ResourceHealth{r}
got := checkWaiting("laptop", kept, outsideFacts(nil))
if got[0].State != link.StateWaiting {
t.Fatalf("an excused wait for a provision was judged: %+v", got[0])
}
if kept[0].Waits[0].Act != "" {
t.Fatal("checking the wait changed the statement as kept")
}
waits, waiting := operatorWait("requests", got)
if !waiting {
t.Fatal("the module does not read as waiting for the operator")
}
o := needsOperatorObservation("requests", "laptop", waits, got)
if !strings.Contains(o.Summary, "secret accept media tv tv-api") {
t.Fatalf("the summary does not name the accept for the provider: %q", o.Summary)
}
if !strings.HasPrefix(o.Needs, "give the API key of tv") {
t.Fatalf("the needs do not name the act: %q", o.Needs)
}
if why, ok := conditions.PlainWords(conditions.Words{Headline: o.Headline, Explanation: o.Explanation,
Resolved: o.Resolved, Needs: o.Needs, Actions: o.Actions}, "laptop"); !ok {
t.Fatalf("the condition is not in plain words: %s", why)
}
// A provider on several machines, or none: the line names its machine as a place to fill.
if line := (outsideProvider{Module: "tv", Machines: []string{"a", "b"}}).acceptLine("tv-api"); line !=
"secret accept <its machine> tv tv-api" {
t.Fatalf("a provider on two machines reads %q", line)
}
}
// §3: the consumers one accept reaches are those whose resolution answers the provision from that provider.
func TestTheConsumersOfAProviderAreThoseItAnswers(t *testing.T) {
plan := catalogue.Resolution{Needs: []catalogue.Needed{
{Name: "tv-api", From: "media", Module: "tv", For: "requests"},
{Name: "tv-api", From: "media", Module: "tv", For: "indexer", Local: "main"},
{Name: "tv-api", From: "other", Module: "tv", For: "elsewhere"},
{Name: "tv-api", From: "media", Module: "another-tv", For: "not-this-module"},
{Name: "db", From: "media", Module: "tv", For: "requests"},
{Name: "tv-api", ByRecord: true, For: "by-record"},
}}
got := bindsFrom(plan, "laptop", "media", "tv", "tv-api")
if len(got) != 2 || got[0].ConsumerModule != "indexer" || got[0].Local != "main" || got[1].ConsumerModule != "requests" {
t.Fatalf("the consumers from tv on media are %+v", got)
}
said := acceptedSaid("media", "tv", "tv-api", inventory.ProvisionAccepted{
Sealed: []inventory.PairEnd{{Consumer: "laptop", ConsumerModule: "requests"}},
Unsealable: []inventory.PairEnd{{Consumer: "new", ConsumerModule: "indexer"}}}, []string{"down: refused"})
for _, want := range []string{"sealed to requests on laptop", "no sealing key", "down: refused"} {
if !strings.Contains(said, want) {
t.Errorf("what the accept says lacks %q:\n%s", want, said)
}
}
}
+70 -5
View File
@@ -182,12 +182,21 @@ func planFor(ctx context.Context, open *stores, nodeName string) (catalogue.Reso
}
var secret inventory.Secret
var err error
if n.SharedOwn != "" {
if outsideModule, outside := issuedOutsideBy(shelf, n); outside {
// Only a person can give it (novox/hq ADR 0337, issue 164): read as given, never made.
secret, err = inv.GivenPairSecret(ctx, n.Name, nodeName, n.For, n.From, outsideModule, n.Local)
} else if n.SharedOwn != "" {
// The provider's one credential, sealed to this consumer too (novox/hq ADR 0158).
secret, err = inv.SharedSecretFor(ctx, n.Name, nodeName, n.For, n.From, providerModuleOf(resolved, open, ctx, n), n.Local, n.SharedOwn)
} else {
secret, err = inv.SecretFor(ctx, n.Name, nodeName, n.For, n.From, n.Local)
}
if inventory.IsNotGiven(err) {
// **Not given, and the rest of the consumer runs** (ADR 0337 §4): no credential file, so the module can
// tell it was not given and says it waits for it; the machine is not refused for what only a person can
// give, and nothing the mesh made stands in for it.
continue
}
if err != nil {
// Said rather than skipped. A machine that resolves cleanly and receives no
// credential is one that will fail to authenticate at some later, less obvious
@@ -280,9 +289,15 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory,
continue
}
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.
domain, err := inv.PublicDomainOf(ctx, n.Name)
if err != nil {
return catalogue.World{}, err
}
others = append(others, candidate{
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{}
@@ -305,8 +320,18 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory,
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.
layers, err := inv.SettingsFor(ctx, o.node.Name, m.Module)
if err != nil {
return catalogue.World{}, err
}
serves, leftOut, err := got.ServedOwnNames(m, name, serves, catalogue.WithDefaults(m, layers))
if err != nil {
return catalogue.World{}, err
}
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})
}
}
}
@@ -442,6 +467,12 @@ func declarationWith(ctx context.Context, open *stores, node string,
if err != nil {
return sendable{}, err
}
// Which ports the filter admits from the local network, and from which networks, for plan to say
// (novox/hq ADR 0340 §8). The same rules the composition rendered, never sent.
rules, err := plan.Rules(with)
if err != nil {
return sendable{}, err
}
// And what was taken on it, said in every declaration it is sent from this one place.
adoption, err := adoptionOf(ctx, open.inventory, record, plan, composed)
if err != nil {
@@ -451,7 +482,8 @@ func declarationWith(ctx context.Context, open *stores, node string,
Received: composed.Received, Mesh: with.Mesh, BusUsers: with.BusUsers,
LeftOut: sortedKeysOf(composed.LeftOut), leftOutWhy: composed.LeftOut, withheld: with.Withheld,
unbound: with.Unbound, foreseen: composed.Foreseen, unplaced: composed.Unplaced,
modules: namedModules(plan, composed.LeftOut)}
modules: namedModules(plan, composed.LeftOut),
localNetwork: catalogue.LocalNetworkPorts(rules, with.LocalNetworks)}
// And which build of each module it carries, for the send to record (novox/hq issue 259, ADR
// 0221). Read only on the send path: a question about what would be sent records nothing.
if choosing == Allocating {
@@ -960,6 +992,12 @@ func renderingFor(ctx context.Context, open *stores, node string,
if err != nil {
return catalogue.Rendering{}, inventory.Node{}, err
}
// And which of the networks it is attached to count as its local network (novox/hq ADR 0340):
// private ranges only, on a link facing outside it. Reported by the machine, never set.
local, err := localNetworksOf(ctx, inv, node)
if err != nil {
return catalogue.Rendering{}, inventory.Node{}, err
}
// Where this machine reaches each mesh seat's holder, for ${seat:<seat>:reach} (novox/hq ADR
// 0222): the artifact store as this network reaches it, which the container runtime is told to
// trust (ADR 0082). The same address composed into every reference the mesh built.
@@ -984,7 +1022,8 @@ func renderingFor(ctx context.Context, open *stores, node string,
Machines: machines, Zones: zones, Holders: replicas,
Suffix: overlay.Suffix(), MeshRange: meshRange, TunnelInterface: overlay.Interface, Accounts: accounts, Foundation: foundation,
Kept: kept, Adopted: record.Adopted, OutwardLinks: outwardLinks,
Given: given, Taken: taken, Seats: seats, ArtifactStore: artifactStore, SeatReach: reach, Built: built,
LocalNetworks: catalogue.CountedLocalNetworks(local),
Given: given, Taken: taken, Seats: seats, ArtifactStore: artifactStore, SeatReach: reach, Built: built,
BusUsers: busUsers, Withheld: withheld, Unbound: unbound,
}, record, nil
}
@@ -1403,6 +1442,16 @@ func planCommand(ctx context.Context, args []string) error {
// in this output would have told anybody that.
for _, n := range plan.Needs {
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) {
fmt.Println(line)
@@ -1417,6 +1466,11 @@ func planCommand(ctx context.Context, args []string) error {
fmt.Printf(" %-20s settings from %s\n", module, layer.From)
}
}
// What each port reached from the local network resolved to on this machine (novox/hq ADR 0340 §8),
// or that it resolved to none.
for _, p := range declared.localNetwork {
fmt.Println(" " + p.Said())
}
fmt.Printf("\n%d resource(s)\n", len(resources))
if *show {
@@ -1742,6 +1796,17 @@ func busUserList(ctx context.Context, inv *inventory.Inventory,
// providerModuleOf is which module answers a need on the providing node: the one in this node's
// own set when the provider is here, else the one the catalogue says offers it.
// issuedOutsideBy is whether a need's provision is offered by its provider with a credential issued outside the
// mesh (novox/hq ADR 0337), and that provider module: the one answering it when known, else any that offers it so.
func issuedOutsideBy(shelf map[string]catalogue.Manifest, n catalogue.Needed) (string, bool) {
if n.Module != "" {
if m, known := shelf[n.Module]; known {
return n.Module, m.IssuedOutsideOf(n.Name)
}
}
return catalogue.OffersIssuedOutside(shelf, n.Name)
}
func providerModuleOf(resolved catalogue.Resolution, open *stores, ctx context.Context, n catalogue.Needed) string {
for _, m := range resolved.Modules {
if _, shared := m.SharedCredentialOf(n.Name); shared {
+8 -1
View File
@@ -740,7 +740,14 @@ func waitForApplied(ctx context.Context, inv *inventory.Inventory, node, digest
fmt.Printf("%s applied it\n", node)
return nil
case inventory.OutcomeFailed:
return fmt.Errorf("%s applied what it was sent but %d resource(s) failed", node, len(doing.Failed))
// Each failed resource by id with the machine's own words, so the answer says what to fix
// without a second look (novox/hq issue 629).
var each []string
for _, f := range doing.Failed {
each = append(each, f.ID+": "+f.Error)
}
return fmt.Errorf("%s applied what it was sent but %d resource(s) failed: %s", node, len(doing.Failed),
strings.Join(each, "; "))
case inventory.OutcomeRefused:
return fmt.Errorf("%s refused what it was sent: %s", node, doing.Refused)
}
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
)
// TestReplay164 replays novox/hq issue 164 (found provisioning the media machine's modules, re-checked
// 2026-10-08): a Servarr app's API key, an indexer's key and a downloader's password are values the provider's
// software holds and only a person can give, and for a consumer of such a provision the send made a random pair
// credential, sealed it to both ends and placed it; the provider refused it, and a consumer retrying it could be
// banned by the provider. The outcome asserted (ADR 0337): for a provision whose offer says its credential is
// issued outside the mesh, the send makes no value and places none, the consumer's machine still composes, and a
// value a person gave for the pair is placed. Written with only what the controller had before its fix —
// register, assign, planFor, declarationFor, AcceptSecretForPair and the inventory's secret reads — so it is laid
// over the commit before. Needs the store (MESH_TEST_POSTGRES), and is named in testdata/store-tests.
func TestReplay164(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
const secretFile = "/var/lib/mesh/requests/tv-api.secret"
register(t, open, catalogue.Manifest{Module: "tv", Version: "1",
Provides: []catalogue.Offer{{Name: "tv-api", Scope: catalogue.ScopeMesh,
Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}},
Serves: map[string]map[string]any{"tv-api": {"port": 8989}}})
register(t, open, catalogue.Manifest{Module: "requests", Version: "1", Requires: []string{"tv-api"},
Secrets: map[string]string{"tv-api": secretFile}})
for _, a := range [][2]string{{"anchor", "tv"}, {"laptop", "requests"}} {
if _, err := assign(ctx, open, a[0], a[1]); err != nil {
t.Fatal(err)
}
}
sent := func() []map[string]any {
t.Helper()
plan, settings, err := planFor(ctx, open, "laptop")
if err != nil {
t.Fatalf("the consumer's machine was refused for a credential only a person gives: %v", err)
}
declared, err := declarationFor(ctx, open, "laptop", plan, settings)
if err != nil {
t.Fatalf("the send to laptop did not compose: %v", err)
}
return declared.Resources
}
placed := func(resources []map[string]any) map[string]any {
for _, r := range resources {
if r["path"] == secretFile && r["sealed"] != nil {
return r
}
}
return nil
}
if got := placed(sent()); got != nil {
t.Errorf("the send placed a credential for tv-api (%v), which only a person can give and nobody gave", got["id"])
}
held, err := open.inventory.SecretsOf(ctx, "laptop", "requests")
if err != nil {
t.Fatal(err)
}
for _, s := range held {
if s.Name == "tv-api" {
t.Fatalf("the mesh holds a value for tv-api that nobody gave (origin %q)", s.Origin)
}
}
// Given by a person for the pair, it is placed, as given.
if err := open.inventory.AcceptSecretForPair(ctx, "tv-api", "laptop", "requests", "anchor", "", "the app's key"); err != nil {
t.Fatal(err)
}
if placed(sent()) == nil {
t.Fatal("the value a person gave for tv-api was not placed")
}
}
+71
View File
@@ -0,0 +1,71 @@
package main
import (
"reflect"
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
)
// **A new SDK plans the toolchain, and then every bundle compiled in it** (novox/hq issue 614), reasoned
// as `mesh-controller.delivery-plan` reasons it: the modules registered as a build registers them —
// resolved, so carrying no `build` — their builds recorded with what they stood on and what they made,
// the graph read by theGraph and the change scoped by pullScope. On 2026-10-13 mesh-sdk 0.1.14 planned
// "builds mesh-sdk, sent nowhere": mesh-tools' `build.on` naming the SDK's package and the providers'
// TypeScript bundles were edges only while their manifests were unbuilt.
//
// **The replay of issue 614** (mesh-lab R614): written only with what the controller had before its fix — a
// store, modules registered and builds recorded, theGraph and pullScope — so it is laid over the commit before
// and fails there with the plan of that day, [[mesh-sdk]]. Needs a store: MESH_TEST_POSTGRES.
func TestReplay614ANewSDKPlansTheToolchainAndThenItsBundles(t *testing.T) {
inv := inventory.ForTest(t)
ctx := t.Context()
asked := time.Now().Add(-time.Hour)
digest := func(c string) string { return "@sha256:" + strings.Repeat(c, 64) }
register := func(m catalogue.Manifest, repository, path string, against []string, made []inventory.Artifact) {
t.Helper()
if err := inv.RegisterModule(ctx, m, inventory.Source{Repository: repository, Seat: "git", Path: path,
Ref: "main", BuiltFrom: "old", Asked: asked}); err != nil {
t.Fatal(err)
}
if err := inv.RecordBuild(ctx, inventory.Build{ID: "build-" + m.Module, Repository: repository, Ref: "main",
Module: m.Module, Commit: "old", On: "builder", Path: path, Against: against, Made: made, Asked: asked}); err != nil {
t.Fatal(err)
}
}
bundle := func(module, language string) catalogue.Manifest {
return catalogue.Manifest{Module: module, Version: "1", Bundles: []catalogue.Bundle{{
Name: "code", Language: language, Source: catalogue.ArtifactStoreScheme + module + "/code" + digest("2"),
Entrypoints: []string{"tools/index.js"}}}}
}
register(catalogue.Manifest{Module: "build-agent", Version: "1",
Claims: []catalogue.Claim{{Name: "node-build-agent", Scope: catalogue.ScopeNode}}},
"novox/mesh-catalog", "modules/build-agent", nil, nil)
register(catalogue.Manifest{Module: "mesh-sdk", Version: "1"}, "novox/mesh-sdk", "", nil,
[]inventory.Artifact{{Name: "lib", Kind: "package", Reference: "@novox/mesh-sdk@0.1.13"}})
register(catalogue.Manifest{Module: "mesh-tools", Version: "1"}, "novox/mesh-tools", "",
[]string{"@novox/mesh-sdk@0.1.13", catalogue.ArtifactStoreScheme + "upstream/docker.io/library/node" + digest("0")},
[]inventory.Artifact{{Name: "build", Kind: "image", Reference: catalogue.ArtifactStoreScheme + "mesh-tools/build" + digest("1")}})
register(catalogue.Manifest{Module: "mesh-tools-go", Version: "1"}, "novox/mesh-tools", "go", nil, nil)
register(bundle("minio", "typescript"), "novox/mesh-catalog", "modules/minio", nil, nil)
register(bundle("umami", "typescript"), "novox/mesh-catalog", "modules/umami", nil, nil)
register(bundle("dnsmasq", "go"), "novox/mesh-catalog", "modules/dnsmasq", nil, nil)
entries, read, edges, err := theGraph(ctx, inv)
if err != nil {
t.Fatal(err)
}
m := orderMember{ID: "novox/mesh-sdk@new", Repository: "novox/mesh-sdk", Head: "new",
Paths: []string{"src/provisioner/index.ts", "package.json"}}
s := pullScope(m.pull(), entries, read, edges)
want := [][]string{{"mesh-sdk"}, {"mesh-tools"}, {"minio", "umami"}}
if !reflect.DeepEqual(s.Reach.Plan.Tiers, want) {
t.Fatalf("a new SDK plans %v, want %v (edges %v)", s.Reach.Plan.Tiers, want, edges)
}
if !reflect.DeepEqual(s.Dependents, []string{"mesh-tools", "minio", "umami"}) {
t.Errorf("dependents %v", s.Dependents)
}
}
+54 -4
View File
@@ -13,6 +13,7 @@ import (
"slices"
"sort"
"strings"
"time"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/catalogue"
@@ -519,9 +520,13 @@ func (a *verbArguments) commandLine() ([]string, error) {
}
return []string{"unpin", str("node"), str("provision")}, nil
case "push":
// Sent and not waited for: the asker reads `status` for what the machine did, which is
// what a person at a shell does too. A tool call that blocked for a push's whole apply would
// time out on every machine that takes a minute, and say nothing about the ones that did not.
// A push to a named machine waits for that machine's own answer, so the call's record says
// how it went: applied, refused with the machine's reason, or which resources failed and why
// (novox/hq issue 629). How long is the call's `wait`, else the controller's setting `push-wait`;
// neither given waits for nothing, as before. The caller is answered before the push starts
// (answersFirst) and the machines are released once sent, so the wait holds nobody; it is what
// `calls` then shows. A sweep (--behind) does not wait: the slowest machine would hold back
// what is said of the others.
// A push through the seat is a push by hand, and says why (novox/hq to-be 45 §7).
if err := need("why"); err != nil {
return nil, fmt.Errorf("%w: a push by hand is a repair, recorded in the hand-act log with why", err)
@@ -534,7 +539,11 @@ func (a *verbArguments) commandLine() ([]string, error) {
// behind is not read here: given with a machine, it is refused as passed over — naming
// a machine and asking for every machine behind are two requests, and guessing one
// would push a machine nobody named, or not push one somebody did.
argv := []string{"push", n, "--wait", "0"}
wait, err := pushWait(str("wait"))
if err != nil {
return nil, err
}
argv := []string{"push", n, "--wait", wait.String()}
// A running module's data moving is sent only when said, per module (novox/hq ADR 0217).
if m := str("move"); m != "" {
argv = append(argv, "--move", m)
@@ -1328,6 +1337,47 @@ var inProcess = map[string]bool{"tools": true, "calls": true, "doctor": true,
// 0259 §8): the router asks it before an approval.
"root-free": true}
// pushFileVar names the file the controller's push settings are written to (module.json, `push-wait`).
const pushFileVar = "MESH_PUSH_FILE"
// pushWait is how long a push to a named machine waits for its answer (novox/hq issue 629): the call's own
// `wait` when given, else the controller's setting `push-wait`, else nothing. No length is written here: the
// setting's default is the module's, and a setting that cannot be read waits for nothing, as a push did
// before, and is said.
func pushWait(given string) (time.Duration, error) {
if strings.TrimSpace(given) != "" {
d, ok := durationSetting(given)
if !ok {
return 0, fmt.Errorf("push: wait %q is no duration: say it as \"90s\", \"2m\", or \"0\" to wait for nothing", given)
}
return d, nil
}
path := os.Getenv(pushFileVar)
if path == "" {
return 0, nil
}
raw, err := os.ReadFile(path)
if err != nil {
fmt.Fprintf(os.Stderr, "the push settings cannot be read (%v); a push waits for nothing\n", err)
return 0, nil
}
var values map[string]any
if err := json.Unmarshal(raw, &values); err != nil {
fmt.Fprintf(os.Stderr, "the push settings are not JSON (%v); a push waits for nothing\n", err)
return 0, nil
}
v, has := values["push-wait"]
if !has || v == "" {
return 0, nil
}
d, ok := durationSetting(v)
if !ok {
fmt.Fprintf(os.Stderr, "the setting push-wait=%v is no duration; a push waits for nothing\n", v)
return 0, nil
}
return d, nil
}
// answersFirst is a command line whose caller is answered before it runs: a push, by its verb or
// through `command`. A push sends the machine holding the bus first when its user list changed, the
// broker reloads, and a reload forgets every answer the bus was about to permit — so an answer
+1 -1
View File
@@ -146,7 +146,7 @@ func TestAnArgumentAVerbDoesNotDeclareIsRefused(t *testing.T) {
// naming one beside behind is refused rather than one of the two guessed.
func TestAPushIsOneMachineOrSaysItIsTheWholeMesh(t *testing.T) {
argv, err := argvFor("push", map[string]any{"node": "g1", "why": "w"})
if err != nil || strings.Join(argv, " ") != "push g1 --wait 0 --why w" {
if err != nil || strings.Join(argv, " ") != "push g1 --wait 0s --why w" {
t.Fatalf("a named push: %v %v", argv, err)
}
for _, args := range []map[string]any{{"why": "w"}, {"behind": "true", "why": "w"}} {
+45 -4
View File
@@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"github.com/novox/mesh-controller/internal/link"
"os"
"path/filepath"
"strings"
"testing"
@@ -164,12 +166,14 @@ func TestAVerbMissingWhatItNeedsIsRefused(t *testing.T) {
}
}
// A push and a build are sent, not waited for: the asker reads status, or the build's log by its
// id, for what happened. A repository given as a forge path is said to be one (issue 176).
// Acts do not block the call. A build is sent, not waited for: the asker reads the build's log by its id.
// A push answers its caller before it starts (answersFirst), so a named push waits for the machine's own
// answer without holding anybody, and the call's record says how it went (novox/hq issue 629). A
// repository given as a forge path is said to be one (issue 176).
func TestActsDoNotBlockTheCall(t *testing.T) {
argv, _ := argvFor("push", map[string]any{"node": "one", "why": "w"})
if strings.Join(argv, " ") != "push one --wait 0 --why w" {
t.Fatalf("push waits: %v", argv)
if strings.Join(argv, " ") != "push one --wait 0s --why w" || !answersFirst(argv) {
t.Fatalf("a named push with no setting waits, or holds its caller: %v", argv)
}
argv, _ = argvFor("build", map[string]any{"repository": "novox/x", "path": "modules/x"})
if strings.Join(argv, " ") != "build novox/x --wait 0 --self --path modules/x" {
@@ -460,3 +464,40 @@ func TestAQuotedValueRoundTrips(t *testing.T) {
}
}
}
// A named push waits as long as the call says, else as long as the controller's setting push-wait says, else
// not at all: no length is the code's own (novox/hq issue 629).
func TestAPushWaitsAsTheCallOrTheSettingSays(t *testing.T) {
file := filepath.Join(t.TempDir(), "push.json")
if err := os.WriteFile(file, []byte(`{"push-wait": "2m"}`), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv(pushFileVar, file)
argv, err := argvFor("push", map[string]any{"node": "one", "why": "w"})
if err != nil || strings.Join(argv, " ") != "push one --wait 2m0s --why w" {
t.Fatalf("the setting was not the default: %v %v", argv, err)
}
argv, err = argvFor("push", map[string]any{"node": "one", "why": "w", "wait": "30s"})
if err != nil || strings.Join(argv, " ") != "push one --wait 30s --why w" {
t.Fatalf("the call's wait did not win over the setting: %v %v", argv, err)
}
argv, err = argvFor("push", map[string]any{"node": "one", "why": "w", "wait": "0"})
if err != nil || strings.Join(argv, " ") != "push one --wait 0s --why w" {
t.Fatalf("a wait of 0 did not wait for nothing: %v %v", argv, err)
}
if _, err := argvFor("push", map[string]any{"node": "one", "why": "w", "wait": "soon"}); err == nil ||
!strings.Contains(err.Error(), "no duration") {
t.Fatalf("a wait that is no duration was taken: %v", err)
}
if err := os.WriteFile(file, []byte(`{"push-wait": "later"}`), 0o600); err != nil {
t.Fatal(err)
}
argv, err = argvFor("push", map[string]any{"node": "one", "why": "w"})
if err != nil || strings.Join(argv, " ") != "push one --wait 0s --why w" {
t.Fatalf("an unreadable setting did not fall back to waiting for nothing: %v %v", argv, err)
}
argv, _ = argvFor("push", map[string]any{"why": "w"})
if strings.Join(argv, " ") != "push --behind --wait 0 --why w" {
t.Fatalf("a sweep waits: %v", argv)
}
}
+32
View File
@@ -79,6 +79,37 @@ func secretCommand(ctx context.Context, args []string) error {
return fmt.Errorf("a secret's value is given at the controller's terminal or at the desk (`give`), never "+
"through a verb (this line came through %q): nothing was read or sealed", verb)
}
// **A provision's credential issued outside the mesh is given once, for its provider** (novox/hq ADR 0337 §3,
// issue 165): `secret accept <provider's machine> <provider module> <provision>`, sealed to every consumer of
// that provider; the per-pair form naming a consumer is refused for it, naming this one.
outsideAt, err := outsideOffer(ctx, module, name, *provider)
if err != nil {
return err
}
if outsideAt == "" && *provider != "" {
if offered, err := offeredOutsideBy(ctx, name); err != nil {
return err
} else if offered != "" {
return fmt.Errorf("%s is issued outside the mesh and given once for its provider, which seals it to "+
"every consumer of it (ADR 0337): `secret accept %s %s %s`; nothing was read or sealed",
name, *provider, offered, name)
}
}
if outsideAt != "" {
if *desk != "" || *local != "" {
return errors.New("a provision's credential issued outside the mesh is given at the controller's " +
"terminal, for every consumer of its provider; it takes neither --at-desk nor --local")
}
value, err := valueFor(node, module, name, *from)
if err != nil {
return err
}
value = asSupplied(value)
if value == "" {
return errors.New("there is nothing to seal")
}
return acceptForProvision(ctx, node, module, name, value)
}
if *desk != "" {
if *from != "" || *provider != "" {
return errors.New("--at-desk gives a module's own secret, and takes neither --from nor --provider")
@@ -173,6 +204,7 @@ const secretUsage = "secret rotate <node> <module> <name> [--why <text> [--cause
"secret ask <node> <module> <name> [--at <machine>]\n" +
"secret issued <provision>\n" +
"secret accept <node> <module> <name> [--from <file> | --at-desk <machine>] [--provider <node> [--local <name>]]\n" +
"secret accept <provider's node> <provider module> <provision> [--from <file>] (a credential issued outside the mesh, for every consumer)\n" +
"secret recover <node> <module> <name> --key <operator-key> [--out <file>] [--from-export <file>] [--provider <node>]\n" +
"secret export [--out <file>]"
+3
View File
@@ -40,6 +40,9 @@ type sendable struct {
actingEpoch uint64
// modules are the modules this declaration names, for the record of the send; never on the wire.
modules []string
// localNetwork is every port admitted from the local network and what it resolved to (novox/hq ADR
// 0340 §8), for plan to say; never on the wire.
localNetwork []catalogue.LocalNetworkPort
// Adoption is nil for a converged node, and then the body is byte for byte what it was before
// adoption existed: an older host parses the envelope strictly and would refuse the key.
Adoption *adoptionEnvelope
@@ -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)
}
}
+1 -1
View File
@@ -160,7 +160,7 @@ func TestTheVerbsCarryReadReplaceAndMove(t *testing.T) {
{"settings", map[string]any{"module": "plex", "history": "true"}, []string{"settings", "show", "plex", "--history"}},
{"settings", map[string]any{"module": "plex", "values": "{}", "replace": "true"}, []string{"settings", "set", "plex", "{}", "--replace"}},
{"push", map[string]any{"node": "home", "move": "plex", "why": "w"},
[]string{"push", "home", "--wait", "0", "--move", "plex", "--why", "w"}},
[]string{"push", "home", "--wait", "0s", "--move", "plex", "--why", "w"}},
{"push", map[string]any{"why": "w"}, []string{"push", "--behind", "--wait", "0", "--why", "w"}},
{"plan", map[string]any{"node": "home", "diff": "true"}, []string{"plan", "home", "--diff"}},
} {
+5
View File
@@ -87,6 +87,11 @@ func Openings(rules []Rule, foundation []int, published map[string]map[int]int)
widen(key{rule.Protocol, rule.Port}, OpeningFromEverywhere)
case FromMesh:
widen(key{rule.Protocol, rule.Port}, OpeningFromMesh)
case FromLocalNetwork:
// The private network's part only (novox/hq ADR 0340). An adopted node's found firewall
// stays, and it is what admits the machine's local network; an opening has no word for a
// local network, and widening it to everywhere would open the port to the internet.
widen(key{rule.Protocol, rule.Port}, OpeningFromMesh)
}
}
for _, port := range foundation {
+1
View File
@@ -127,6 +127,7 @@ func boundNeed(n Needed, bound Chosen, catalogue map[string]Manifest, node Node,
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.LeftOut = p.LeftOut
return held, true, ""
}
@@ -143,6 +143,8 @@ func CheckServes(m Manifest) []string {
problems = append(problems, fmt.Sprintf(
"%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
// bound has to keep the identity inside one (ADR 0225).
if strings.Contains(text, "${consumer:as:dns}") {
+17 -5
View File
@@ -135,6 +135,12 @@ type Rendering struct {
// composes no filter for it rather than writing a rule around a link with no name.
OutwardLinks []string
// LocalNetworks is this machine's local networks (novox/hq ADR 0340): the networks it reported on a
// link facing outside it that lie wholly in a private range (JudgeLocalNetworks), which a port
// reached from `local-network` is admitted from beside the private network. Empty when it reported
// none, and the rule set then says so rather than widening the port or dropping it.
LocalNetworks []string
// TunnelInterface is the interface the mesh's private network runs on, named here rather than
// imported because the overlay package rests on this one. Traffic arriving on it is the mesh's,
// not this machine's own guest, so the filter admits it only by a rule.
@@ -527,8 +533,8 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
"`node show %s` says whether it has. Until then %s is not sent, and the machine "+
"keeps the filter it has", r.Node, r.Node, filters)
}
filtering := AsNftables(rules, with.Mesh, r.PublicDomain != "", with.Foundation,
with.OutwardLinks, with.TunnelInterface)
filtering := AsNftablesWithLocal(rules, with.Mesh, r.PublicDomain != "", with.Foundation,
with.OutwardLinks, with.TunnelInterface, with.LocalNetworks)
// **A variable two modules set is refused whether or not anything places it** (novox/hq ADR
// 0203 §5): the account has one environment, and a machine whose holder arrives later should not
@@ -2033,7 +2039,13 @@ func (r Resolution) servedOnThisMachine(provision string, with Rendering) (map[s
if err != nil {
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
}
@@ -2551,8 +2563,8 @@ func onlyGroups(r map[string]any) bool {
// generatedHere is what each computed module's generator answers for this machine, by module —
// absent for a module whose machine is not yet part of what it generates — held to the rule every
// module is held to: no two modules on a machine declare one path, unit, name or package (novox/hq
// issue 190, step 5; ADR 0222).
// module is held to: no two modules on a machine declare one path, unit or name, nor one package present
// and absent (novox/hq issue 190, step 5; ADR 0222, ADR 0303).
//
// Resolution checks the catalogue's manifests, and a computed module's manifest has none of the
// resources it will declare — they exist only once its generator has answered for this machine,
+63 -6
View File
@@ -152,9 +152,10 @@ func Exposure(m Manifest, layers []Layer) (map[int]string, error) {
m.Module, port)
}
source, ok := value.(string)
if !ok || (source != FromMesh && source != FromEverywhere && source != FromMachine) {
return nil, fmt.Errorf("%s exposes port %d as %v; it is %q, %q or %q",
m.Module, port, value, FromMesh, FromEverywhere, FromMachine)
if !ok || (source != FromMesh && source != FromEverywhere && source != FromMachine &&
source != FromLocalNetwork) {
return nil, fmt.Errorf("%s exposes port %d as %v; it is %q, %q, %q or %q",
m.Module, port, value, FromMesh, FromEverywhere, FromMachine, FromLocalNetwork)
}
out[port] = source
}
@@ -172,7 +173,9 @@ func Exposure(m Manifest, layers []Layer) (map[int]string, error) {
// the appearance-of-restriction this mechanism exists to remove. The narrower rule's sources are
// carried onto the wider one, so the module that wanted it is still named.
func widest(rules []Rule) []Rule {
reach := map[string]int{FromMachine: 0, FromMesh: 1, FromEverywhere: 2}
// The local network is the private network and the machine's own local networks (novox/hq ADR
// 0340 §4), so it covers `mesh` and is covered by `anywhere`.
reach := map[string]int{FromMachine: 0, FromMesh: 1, FromLocalNetwork: 2, FromEverywhere: 3}
var out []Rule
for _, rule := range rules {
covered := false
@@ -232,6 +235,15 @@ const SSHPort = 22
// any module asks for, and neither may be derived away.
func AsNftables(rules []Rule, mesh []string, outward bool, foundation []int,
outwardLinks []string, tunnel string) string {
return AsNftablesWithLocal(rules, mesh, outward, foundation, outwardLinks, tunnel, nil)
}
// AsNftablesWithLocal is AsNftables with the machine's local networks: the networks it reported on a
// link facing outside that count (novox/hq ADR 0340, JudgeLocalNetworks), which a rule from
// `local-network` admits on its own port alone, beside the private network. None is not widened to
// anywhere and not dropped silently: the rule set says the port wanted the local network and found none.
func AsNftablesWithLocal(rules []Rule, mesh []string, outward bool, foundation []int,
outwardLinks []string, tunnel string, local []string) string {
// The links that are not this machine's own: the ones facing outside, and the mesh's tunnel.
// Traffic arriving on any of them is admitted only by a rule below; traffic arriving anywhere
// else is this machine's own guest and is not something the mesh has a position on.
@@ -394,6 +406,31 @@ func AsNftables(rules []Rule, mesh []string, outward bool, foundation []int,
b.WriteString(fmt.Sprintf("\t\tip6 saddr { %s } %s dport %d accept\n",
strings.Join(six, ", "), rule.Protocol, rule.Port))
}
case FromLocalNetwork:
// The private network and the machine's own local networks, on this port alone (novox/hq
// ADR 0340 §5).
four, six := byFamily(mesh)
lfour, lsix := byFamily(local)
four, six = append(four, lfour...), append(six, lsix...)
if len(lfour) == 0 && len(lsix) == 0 {
b.WriteString(fmt.Sprintf(
"\t\t# %s/%d wanted the local network, and this machine reported no private network "+
"on a link facing outside; admitted from no local network\n", rule.Protocol, rule.Port))
}
if len(four) == 0 && len(six) == 0 {
b.WriteString(fmt.Sprintf(
"\t\t# %s/%d wanted the local network, and this node knows neither mesh addresses nor a "+
"local network; closed\n", rule.Protocol, rule.Port))
break
}
if len(four) > 0 {
b.WriteString(fmt.Sprintf("\t\tip saddr { %s } %s dport %d accept\n",
strings.Join(four, ", "), rule.Protocol, rule.Port))
}
if len(six) > 0 {
b.WriteString(fmt.Sprintf("\t\tip6 saddr { %s } %s dport %d accept\n",
strings.Join(six, ", "), rule.Protocol, rule.Port))
}
case FromEverywhere:
b.WriteString(fmt.Sprintf("\t\t%s dport %d accept\n", rule.Protocol, rule.Port))
}
@@ -487,6 +524,18 @@ func AsNftables(rules []Rule, mesh []string, outward bool, foundation []int,
b.WriteString(fmt.Sprintf("\t\tip6 saddr { %s } ct original proto-dst %d accept\n",
strings.Join(six, ", "), rule.Port))
}
case FromLocalNetwork:
four, six := byFamily(mesh)
lfour, lsix := byFamily(local)
four, six = append(four, lfour...), append(six, lsix...)
if len(four) > 0 {
b.WriteString(fmt.Sprintf("\t\tip saddr { %s } ct original proto-dst %d accept\n",
strings.Join(four, ", "), rule.Port))
}
if len(six) > 0 {
b.WriteString(fmt.Sprintf("\t\tip6 saddr { %s } ct original proto-dst %d accept\n",
strings.Join(six, ", "), rule.Port))
}
case FromEverywhere:
b.WriteString(fmt.Sprintf("\t\tct original proto-dst %d accept\n", rule.Port))
}
@@ -793,10 +842,14 @@ const (
// are a subset of anywhere. What differs is the names, which is the whole reason reach is not
// simply the filter's vocabulary with nicer words.
ReachBoth = "both"
// ReachLocalNetwork is `internal` with the machine's own local networks added to what the filter
// opens (novox/hq ADR 0340 §4): the internal name and not the public one, the mesh's own authority,
// and the port opened to the private network and to the private networks the machine is attached to.
ReachLocalNetwork = "local-network"
)
// reaches is every value, in the order a refusal lists them.
var reaches = []string{ReachMachine, ReachInternal, ReachPublic, ReachBoth}
var reaches = []string{ReachMachine, ReachInternal, ReachPublic, ReachBoth, ReachLocalNetwork}
// RoutedPorts are the ports a module serves through a proxy, taken from its route contributions.
//
@@ -845,6 +898,8 @@ func FilterSource(reach string) (string, bool) {
return FromMesh, true
case ReachPublic, ReachBoth:
return FromEverywhere, true
case ReachLocalNetwork:
return FromLocalNetwork, true
default:
return "", false
}
@@ -854,7 +909,9 @@ func FilterSource(reach string) (string, bool) {
func WantsPublicName(reach string) bool { return reach == ReachPublic || reach == ReachBoth }
// WantsInternalName is whether a reach asks for the route's internal name to be composed.
func WantsInternalName(reach string) bool { return reach == ReachInternal || reach == ReachBoth }
func WantsInternalName(reach string) bool {
return reach == ReachInternal || reach == ReachBoth || reach == ReachLocalNetwork
}
// Reaches reads a module's per-node reach settings: declared port → how far it reaches.
//
@@ -6,7 +6,8 @@ import (
)
// novox/hq issue 190, step 5 (ADR 0222): what the mesh computes for a module is held to the rule
// every module is — no two modules on a machine declare one path, unit, name or package. The
// every module is — no two modules on a machine declare one path, unit or name (a package may be declared by
// several, novox/hq ADR 0303). The
// private network once declared the container runtime's file and service beside the runtime's own
// module, and nothing refused it, because the collision check only ever saw catalogue manifests.
+2 -2
View File
@@ -25,13 +25,13 @@ func TestAnOfferMaySayItsSoftwareIssuesTheCredential(t *testing.T) {
}
for want, raw := range map[string]string{
"the one issuer an offer may name": `{"module":"d","version":"1",
"the issuers an offer may name": `{"module":"d","version":"1",
"provides":[{"name":"d-api","credential":{"issued-by":"vendor","tool":"d_token"}}]}`,
"names no tool that answers it": `{"module":"d","version":"1",
"provides":[{"name":"d-api","credential":{"issued-by":"software"}}]}`,
"names its own secret": `{"module":"d","version":"1","own-secrets":{"token":{"path":"/p","taken":"at-start"}},
"provides":[{"name":"d-api","credential":{"issued-by":"software","tool":"d_token","own":"token"}}]}`,
"the one issuer an offer may name ": `{"module":"d","version":"1",
"the issuers an offer may name ": `{"module":"d","version":"1",
"provides":[{"name":"d-api","credential":{"tool":"d_token"}}]}`,
} {
want = strings.TrimSpace(want)
+46
View File
@@ -0,0 +1,46 @@
package catalogue
import (
"strings"
"testing"
)
// A provision's credential may be one only a person outside the mesh can give (novox/hq ADR 0337 §1, issue 164):
// the offer says `issued-by: outside` and names neither an own secret nor a tool, and the parser refuses it beside
// either. An offer that says `software` keeps ADR 0305's shape.
func TestAnOfferMaySayAPersonOutsideTheMeshIssuesItsCredential(t *testing.T) {
m, err := ParseManifest([]byte(`{"module":"tv","version":"1",
"provides":[{"name":"tv-api","scope":"mesh","credential":{"issued-by":"outside"}}],
"serves":{"tv-api":{"port":8989}}}`))
if err != nil {
t.Fatal(err)
}
if !m.IssuedOutsideOf("tv-api") {
t.Fatal("the offer's credential issued outside was not read")
}
if m.IssuedOutsideOf("other") {
t.Fatal("a provision the module does not offer reads as issued outside")
}
if _, issued := m.IssuedCredentialOf("tv-api"); issued {
t.Fatal("a credential issued outside was read as issued by the software")
}
if _, shared := m.SharedCredentialOf("tv-api"); shared {
t.Fatal("a credential issued outside was read as an own secret shared")
}
if who, offered := OffersIssuedOutside(map[string]Manifest{"tv": m}, "tv-api"); !offered || who != "tv" {
t.Fatalf("the catalogue does not say tv offers tv-api issued outside: %q %v", who, offered)
}
for want, raw := range map[string]string{
"names its own secret": `{"module":"d","version":"1","own-secrets":{"key":{"path":"/p","taken":"at-start"}},
"provides":[{"name":"d-api","credential":{"issued-by":"outside","own":"key"}}]}`,
"names the tool": `{"module":"d","version":"1",
"provides":[{"name":"d-api","credential":{"issued-by":"outside","tool":"d_key"}}]}`,
"the issuers an offer may name": `{"module":"d","version":"1",
"provides":[{"name":"d-api","credential":{"issued-by":"a-person"}}]}`,
} {
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("expected a refusal saying %q, got %v", want, err)
}
}
}
+154
View File
@@ -0,0 +1,154 @@
package catalogue
import (
"fmt"
"net/netip"
"slices"
"sort"
"strings"
)
// AttachedNetwork is one network a machine said it is directly attached to: the link it is on and the
// network as address and prefix (novox/hq ADR 0340 §7), as the node-engine reports it among its facts.
type AttachedNetwork struct {
Link string `json:"link"`
Network string `json:"network"`
}
// LocalNetwork is one attached network judged: whether it counts as this machine's local network for a
// port reached from `local-network`, and why — said for one that counts as well as for one that does
// not, so `node` can show which counted and why the others did not (ADR 0340 §8).
type LocalNetwork struct {
Link string
Network string
Counts bool
Why string
}
// privateRanges are the only ranges a local network may lie in (ADR 0340 §2): RFC 1918 for IPv4 and the
// unique local addresses for IPv6. A network counts only when it lies wholly inside one of them.
var privateRanges = []struct {
prefix netip.Prefix
name string
}{
{netip.MustParsePrefix("10.0.0.0/8"), "a private range (RFC 1918, 10.0.0.0/8)"},
{netip.MustParsePrefix("172.16.0.0/12"), "a private range (RFC 1918, 172.16.0.0/12)"},
{netip.MustParsePrefix("192.168.0.0/16"), "a private range (RFC 1918, 192.168.0.0/16)"},
{netip.MustParsePrefix("fc00::/7"), "the IPv6 unique local range (fc00::/7)"},
}
// JudgeLocalNetworks says, for each network a machine reported, whether it counts as its local network.
//
// **A public subnet never counts** (ADR 0340 §2): a network counts only when it lies wholly in a private
// range, so `local-network` can never open a port to the internet — a machine sitting on a public /24 has
// that /24 judged and refused, never rendered. A link-local network does not count either. And only a
// link the machine reported as facing outside it counts (§3): the private network's own link and a
// container bridge are this machine's own, not its local network, whatever their addresses.
//
// Sorted by link and then network, one entry per (link, network), so what `node` shows and what the
// filter is rendered from do not depend on the order a machine happened to say them in.
func JudgeLocalNetworks(attached []AttachedNetwork, outward []string, tunnel string) []LocalNetwork {
seen := map[[2]string]bool{}
var out []LocalNetwork
for _, a := range attached {
key := [2]string{a.Link, a.Network}
if seen[key] {
continue
}
seen[key] = true
out = append(out, judgeOne(a, outward, tunnel))
}
sort.Slice(out, func(i, j int) bool {
if out[i].Link != out[j].Link {
return out[i].Link < out[j].Link
}
return out[i].Network < out[j].Network
})
return out
}
func judgeOne(a AttachedNetwork, outward []string, tunnel string) LocalNetwork {
j := LocalNetwork{Link: a.Link, Network: a.Network}
prefix, err := netip.ParsePrefix(strings.TrimSpace(a.Network))
if err != nil {
j.Why = "not a network the mesh can read (" + err.Error() + ")"
return j
}
prefix = prefix.Masked()
j.Network = prefix.String()
switch {
case tunnel != "" && a.Link == tunnel:
j.Why = "the private network's own link, not the local network"
return j
case !slices.Contains(outward, a.Link):
j.Why = fmt.Sprintf("%s does not face outside this machine (a bridge or a link of its own)", a.Link)
return j
case prefix.Addr().IsLoopback():
j.Why = "loopback"
return j
case prefix.Addr().IsLinkLocalUnicast():
j.Why = "link-local, never counted"
return j
}
for _, r := range privateRanges {
if r.prefix.Contains(prefix.Addr()) && prefix.Bits() >= r.prefix.Bits() {
j.Counts = true
j.Why = "in " + r.name
return j
}
}
if prefix.Addr().IsPrivate() {
// Overlaps a private range without lying wholly inside it (a /7 around 10.0.0.0/8, say).
j.Why = "not wholly in a private range, so never counted"
return j
}
j.Why = "public, never counted: local-network may not open a port to the internet"
return j
}
// CountedLocalNetworks is the networks that count, as the filter renders them: sorted, without repeats.
func CountedLocalNetworks(judged []LocalNetwork) []string {
var out []string
for _, j := range judged {
if j.Counts && !slices.Contains(out, j.Network) {
out = append(out, j.Network)
}
}
sort.Strings(out)
return out
}
// LocalNetworkPort is one port the filter admits from `local-network`, and what that resolved to on this
// machine (ADR 0340 §8): what `plan` shows for it.
type LocalNetworkPort struct {
Port int
Protocol string
Because []string
// Networks is the local networks it is admitted from; empty when the machine reported none (§6).
Networks []string
}
// LocalNetworkPorts is every rule admitted from `local-network`, with the networks it resolved to.
func LocalNetworkPorts(rules []Rule, networks []string) []LocalNetworkPort {
var out []LocalNetworkPort
for _, rule := range rules {
if rule.From != FromLocalNetwork {
continue
}
out = append(out, LocalNetworkPort{Port: rule.Port, Protocol: rule.Protocol,
Because: rule.Because, Networks: networks})
}
return out
}
// Said is how `plan` says one such port: the networks, or that there are none and the port is admitted
// from the private network only.
func (p LocalNetworkPort) Said() string {
by := strings.Join(p.Because, ", ")
if len(p.Networks) == 0 {
return fmt.Sprintf("%s/%d (%s) from the local network: none — this machine reported no private "+
"network on a link facing outside, so it is admitted from the private network only", p.Protocol, p.Port, by)
}
return fmt.Sprintf("%s/%d (%s) from the local network: %s, and the private network", p.Protocol, p.Port, by,
strings.Join(p.Networks, ", "))
}
+195
View File
@@ -0,0 +1,195 @@
package catalogue
import (
"strings"
"testing"
)
// The home server's case (novox/hq issue 154): it sits on 192.168.1.0/24, and also on a public /24 a
// provider handed it, beside the private network's own link and a container bridge.
var homeServer = []AttachedNetwork{
{Link: "eth0", Network: "192.168.1.10/24"},
{Link: "eth1", Network: "203.0.113.7/24"},
{Link: "eth0", Network: "fe80::1/64"},
{Link: "eth0", Network: "fd12:3456:789a::5/64"},
{Link: "wg-mesh", Network: "10.10.0.4/24"},
{Link: "docker0", Network: "172.17.0.1/16"},
}
func judged(t *testing.T) []LocalNetwork {
t.Helper()
return JudgeLocalNetworks(homeServer, []string{"eth0", "eth1"}, "wg-mesh")
}
// **A public subnet never counts** (ADR 0340 §2), and only a private one on a link facing outside does
// (§3): the tunnel's and a bridge's are this machine's own, whatever their addresses.
func TestOnlyAPrivateNetworkOnALinkFacingOutsideCounts(t *testing.T) {
got := CountedLocalNetworks(judged(t))
want := []string{"192.168.1.0/24", "fd12:3456:789a::/64"}
if strings.Join(got, " ") != strings.Join(want, " ") {
t.Fatalf("counted %v, want %v", got, want)
}
for _, j := range judged(t) {
if j.Why == "" {
t.Errorf("%s on %s is judged without a why", j.Network, j.Link)
}
switch j.Network {
case "203.0.113.0/24":
if j.Counts || !strings.Contains(j.Why, "public") {
t.Errorf("the public subnet is judged %v (%s), want never counted as public", j.Counts, j.Why)
}
case "fe80::/64":
if j.Counts || !strings.Contains(j.Why, "link-local") {
t.Errorf("a link-local network is judged %v (%s)", j.Counts, j.Why)
}
case "10.10.0.0/24", "172.17.0.0/16":
if j.Counts {
t.Errorf("%s on %s counts, and it is not the local network", j.Network, j.Link)
}
}
}
}
// A network overlapping a private range without lying wholly inside it does not count either, so a
// wide prefix around a private range cannot carry the internet with it.
func TestANetworkNotWhollyInAPrivateRangeDoesNotCount(t *testing.T) {
for _, n := range []string{"10.0.0.0/7", "172.0.0.0/8", "100.64.0.0/10", "0.0.0.0/0", "::/0"} {
got := JudgeLocalNetworks([]AttachedNetwork{{Link: "eth0", Network: n}}, []string{"eth0"}, "")
if len(got) != 1 || got[0].Counts {
t.Errorf("%s counts as a local network: %+v", n, got)
}
}
}
// **The filter admits the local network on that port's rule alone** (§5), beside the private network,
// in both chains, and never the public subnet.
func TestALocalNetworkPortIsAdmittedFromItsNetworksAndTheMesh(t *testing.T) {
rules := []Rule{
{Port: 1883, Protocol: "tcp", From: FromLocalNetwork, Because: []string{"mosquitto"}},
{Port: 5432, Protocol: "tcp", From: FromMesh, Because: []string{"postgres"}},
}
local := CountedLocalNetworks(judged(t))
out := AsNftablesWithLocal(rules, []string{"10.10.0.1", "10.10.0.4"}, false, nil,
[]string{"eth0", "eth1"}, "wg-mesh", local)
for _, want := range []string{
"ip saddr { 10.10.0.1, 10.10.0.4, 192.168.1.0/24 } tcp dport 1883 accept",
"ip6 saddr { fd12:3456:789a::/64 } tcp dport 1883 accept",
"ip saddr { 10.10.0.1, 10.10.0.4, 192.168.1.0/24 } ct original proto-dst 1883 accept",
// The mesh-only port is not widened by the machine having a local network.
"ip saddr { 10.10.0.1, 10.10.0.4 } tcp dport 5432 accept",
} {
if !strings.Contains(out, want) {
t.Errorf("the rule set lacks %q:\n%s", want, out)
}
}
if strings.Contains(out, "203.0.113") {
t.Errorf("the public subnet reached the rule set:\n%s", out)
}
if strings.Contains(out, "tcp dport 1883 accept\n") && !strings.Contains(out, "saddr") {
t.Errorf("1883 is open to anywhere:\n%s", out)
}
for _, line := range strings.Split(out, "\n") {
if strings.TrimSpace(line) == "tcp dport 1883 accept" {
t.Fatalf("1883 is open to anywhere:\n%s", out)
}
}
}
// **Nothing resolved is closed and said** (§6): a machine that reported no private network admits the
// port from the private network only, and the rule set says it found none — never widened to anywhere.
func TestNoLocalNetworkIsSaidAndNotWidened(t *testing.T) {
rules := []Rule{{Port: 1883, Protocol: "tcp", From: FromLocalNetwork, Because: []string{"mosquitto"}}}
out := AsNftablesWithLocal(rules, []string{"10.10.0.1"}, false, nil, []string{"eth0"}, "wg-mesh", nil)
if !strings.Contains(out, "1883 wanted the local network, and this machine reported no private network") {
t.Errorf("the rule set does not say the local network resolved to nothing:\n%s", out)
}
if !strings.Contains(out, "ip saddr { 10.10.0.1 } tcp dport 1883 accept") {
t.Errorf("the port is not admitted from the private network:\n%s", out)
}
for _, line := range strings.Split(out, "\n") {
if strings.TrimSpace(line) == "tcp dport 1883 accept" {
t.Fatalf("an unresolved local network was widened to anywhere:\n%s", out)
}
}
said := LocalNetworkPorts(rules, nil)[0].Said()
if !strings.Contains(said, "none") {
t.Errorf("plan says %q, want it to say no local network was found", said)
}
}
// `local-network` is a source a manifest may name (§1, changing ADR 0045 §3), and an unknown source is
// still refused.
func TestAManifestMayListenFromTheLocalNetwork(t *testing.T) {
if _, err := ParseManifest([]byte(`{"module":"mosquitto","version":"1",
"listens":[{"port":1883,"from":"local-network"}]}`)); err != nil {
t.Fatalf("a listen from the local network is refused: %v", err)
}
_, err := ParseManifest([]byte(`{"module":"mosquitto","version":"1",
"listens":[{"port":1883,"from":"lan"}]}`))
if err == nil || !strings.Contains(err.Error(), `from "lan"`) {
t.Fatalf("a source the mesh cannot render is not refused: %v", err)
}
}
// **A fourth reach** (§4, changing ADR 0138 §4): `internal` with the local networks added to what the
// filter opens — the internal name, not the public one.
func TestTheLocalNetworkReachIsInternalWithTheLocalNetworkAdded(t *testing.T) {
public, internal := namesFor(t, aRoutedWeb(), reachSet(ReachLocalNetwork))
if public != "" || internal != "app.anchor.internal" {
t.Fatalf("names are %q and %q, want the internal name only", public, internal)
}
bare := aRoutedWeb()
bare.Contributes = nil
r := Resolution{Node: "anchor", Modules: []Manifest{bare}}
rules, err := r.Rules(Rendering{Settings: reachSet(ReachLocalNetwork)})
if err != nil {
t.Fatalf("rules: %v", err)
}
if len(rules) != 1 || rules[0].From != FromLocalNetwork {
t.Fatalf("reach local-network made the filter say %+v", rules)
}
}
// Two modules wanting one port, one from the mesh and one from the local network, render one rule from
// the local network: it covers the mesh, and anywhere covers it.
func TestTheLocalNetworkSitsBetweenTheMeshAndAnywhere(t *testing.T) {
got := widest([]Rule{
{Port: 53, Protocol: "udp", From: FromMesh, Because: []string{"a"}},
{Port: 53, Protocol: "udp", From: FromLocalNetwork, Because: []string{"b"}},
})
if len(got) != 1 || got[0].From != FromLocalNetwork || len(got[0].Because) != 2 {
t.Fatalf("got %+v", got)
}
got = widest(append(got, Rule{Port: 53, Protocol: "udp", From: FromEverywhere, Because: []string{"c"}}))
if len(got) != 1 || got[0].From != FromEverywhere {
t.Fatalf("got %+v", got)
}
}
// On an adopted node the opening is the private network's part only: the found firewall admits the
// local network, and an opening that widened to everywhere would open the port to the internet.
func TestAnAdoptedNodeOpensTheLocalNetworkPortToTheMeshOnly(t *testing.T) {
out := Openings([]Rule{{Port: 1883, Protocol: "tcp", From: FromLocalNetwork}}, nil, nil)
if len(out) != 1 || out[0]["from"] != OpeningFromMesh {
t.Fatalf("openings %v", out)
}
}
// A global unicast IPv6 subnet on a link facing outside is public, and never counts: the IPv6 side of the
// rule that `local-network` cannot open a port to the internet (ADR 0340 §2).
func TestAGlobalIPv6SubnetIsPublicAndNeverCounts(t *testing.T) {
got := JudgeLocalNetworks([]AttachedNetwork{{Link: "eth0", Network: "2001:db8:1234::7/48"}}, []string{"eth0"}, "wg-mesh")
if len(got) != 1 {
t.Fatalf("judged %+v", got)
}
if got[0].Counts {
t.Fatalf("the global IPv6 subnet %s counts as a local network", got[0].Network)
}
if got[0].Network != "2001:db8:1234::/48" || !strings.Contains(got[0].Why, "public, never counted") {
t.Fatalf("judged %s with why %q, want it named public and never counted", got[0].Network, got[0].Why)
}
if n := CountedLocalNetworks(got); len(n) != 0 {
t.Fatalf("counted %v", n)
}
}
+58 -9
View File
@@ -269,7 +269,8 @@ func KeepsConsumerData(catalogue map[string]Manifest, provision string) bool {
func (o Offer) MachineReach() bool { return o.Reach == ReachMachine }
// OfferCredential names which of the provider's own secrets a provision's consumers receive — or, with
// IssuedBy, that the provider's software issues the provision's one credential itself (novox/hq ADR 0305,
// IssuedBy "outside", that only a person can give the provision's credential (novox/hq ADR 0337, issue 164) — or,
// with IssuedBy "software", that the provider's software issues the provision's one credential itself (novox/hq ADR 0305,
// issue 526): a media server's sign-in token from its vendor, which the mesh can neither make nor rotate.
// Tool is then the provider's own tool that answers that value, sealed to a key made for the controller's
// one call, and to the controller alone.
@@ -293,6 +294,34 @@ func (m Manifest) IssuedCredentialOf(provision string) (string, bool) {
return "", false
}
// IssuedOutsideOf is whether this module's offer of a provision says only a person outside the mesh can give its
// credential (novox/hq ADR 0337): the mesh never makes a value for it, and a consumer holds one only once a person
// gave it with `secret accept`.
func (m Manifest) IssuedOutsideOf(provision string) bool {
for _, o := range m.Provides {
if o.Name == provision && o.Credential != nil && o.Credential.IssuedBy == IssuedOutside {
return true
}
}
return false
}
// OffersIssuedOutside is whether any module of a catalogue offers the provision with a credential issued outside
// the mesh (novox/hq ADR 0337), and the first such module by name.
func OffersIssuedOutside(shelf map[string]Manifest, provision string) (string, bool) {
var names []string
for name, m := range shelf {
if m.IssuedOutsideOf(provision) {
names = append(names, name)
}
}
if len(names) == 0 {
return "", false
}
sort.Strings(names)
return names[0], true
}
// SharedCredentialOf is the own secret an offer of this module names as the provision's credential,
// and whether it names one.
func (m Manifest) SharedCredentialOf(provision string) (string, bool) {
@@ -1136,6 +1165,12 @@ const (
FromEverywhere = "anywhere"
// FromMachine is this machine only — a port bound for something else on the same host.
FromMachine = "machine"
// FromLocalNetwork is the private network and the networks this machine is directly attached to
// through its uplink, as it reports them, and only those wholly in a private range (novox/hq ADR
// 0340): a local service reached by devices that will never be nodes, said as exactly that rather
// than as `anywhere`. A public subnet the machine sits on never counts, so it cannot open a port to
// the internet.
FromLocalNetwork = "local-network"
)
// At is this port's protocol, with the default applied.
@@ -1647,14 +1682,28 @@ func ParseManifest(raw []byte) (Manifest, error) {
"makes is %d", m.Module, p, id.Max, shortest))
}
}
if c := offer.Credential; c != nil && (c.IssuedBy != "" || c.Tool != "") {
if c := offer.Credential; c != nil && c.IssuedBy == IssuedOutside {
// Only a person can give it (novox/hq ADR 0337 §1, issue 164): the software holds a value the mesh can
// neither make nor fetch, so the offer names nothing that would answer it — no own secret, no tool.
switch {
case c.Own != "":
problems = append(problems, fmt.Sprintf(
"%s provides %q with a credential issued outside the mesh and names its own secret %q too; a "+
"person gives the value with `secret accept`, so the offer names no own secret (ADR 0337)",
m.Module, p, c.Own))
case c.Tool != "":
problems = append(problems, fmt.Sprintf(
"%s provides %q with a credential issued outside the mesh and names the tool %q; only a "+
"person gives it, so the offer names no tool (ADR 0337)", m.Module, p, c.Tool))
}
} else if c := offer.Credential; c != nil && (c.IssuedBy != "" || c.Tool != "") {
// The software issues it (novox/hq ADR 0305): named by the tool that answers it, never by an own
// secret, which would be a second copy for the mesh to keep in step with the software's.
switch {
case c.IssuedBy != IssuedBySoftware:
problems = append(problems, fmt.Sprintf(
"%s provides %q with a credential issued by %q; the one issuer an offer may name is %q (ADR 0305)",
m.Module, p, c.IssuedBy, IssuedBySoftware))
"%s provides %q with a credential issued by %q; the issuers an offer may name are %q and %q "+
"(ADR 0305, ADR 0337)", m.Module, p, c.IssuedBy, IssuedBySoftware, IssuedOutside))
case c.Own != "":
problems = append(problems, fmt.Sprintf(
"%s provides %q with a credential its software issues and names its own secret %q too; the "+
@@ -1886,17 +1935,17 @@ func ParseManifest(raw []byte) (Manifest, error) {
"%s listens on port %d, which is not a port", m.Module, l.Port))
}
switch l.From {
case FromMesh, FromEverywhere, FromMachine:
case FromMesh, FromEverywhere, FromMachine, FromLocalNetwork:
case "":
// The fault this field exists to prevent. A rule with no source is open, and a
// manifest that omitted it would read as a restriction and be none.
problems = append(problems, fmt.Sprintf(
"%s listens on %d and does not say from where; it is %q, %q or %q",
m.Module, l.Port, FromMesh, FromEverywhere, FromMachine))
"%s listens on %d and does not say from where; it is %q, %q, %q or %q",
m.Module, l.Port, FromMesh, FromEverywhere, FromMachine, FromLocalNetwork))
default:
problems = append(problems, fmt.Sprintf(
"%s listens on %d from %q; it is %q, %q or %q",
m.Module, l.Port, l.From, FromMesh, FromEverywhere, FromMachine))
"%s listens on %d from %q; it is %q, %q, %q or %q",
m.Module, l.Port, l.From, FromMesh, FromEverywhere, FromMachine, FromLocalNetwork))
}
if p := l.At(); p != "tcp" && p != "udp" {
problems = append(problems, fmt.Sprintf(
+7 -1
View File
@@ -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
// providing the same thing (ADR 0110).
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.
@@ -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
// licence's manager; empty for every consumer.
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
// 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).
@@ -582,7 +588,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
bound = pm.IdentityBoundOf(want)
}
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 {
case world.Unchecked:
+3 -2
View File
@@ -331,12 +331,13 @@ func UnassignRefusal(catalogue map[string]Manifest, node string, assigned, remov
}
// CollisionRefusal is why assigning `adding` beside `assigned` is refused for what two modules
// would both declare, or nothing (novox/hq ADR 0210 §1, 04-ISSUES/235).
// would both declare, or nothing (novox/hq ADR 0210 §1, 04-ISSUES/235): one path, unit or name, or a package one
// declares present and another absent. A package several modules declare present is no collision (ADR 0303).
//
// **Refused, not kept like an unresolved provision.** An assignment is otherwise kept when the
// node does not resolve, because assignment is not an ordering: a consumer's provider can follow.
// A collision is not an order anything can complete — no further assignment makes two owners of one
// package one owner — and kept, it leaves the node unresolvable, so the next push of anything drops
// file one owner — and kept, it leaves the node unresolvable, so the next push of anything drops
// it from the mesh. Only collisions involving a module being added are refused; one already on the
// node is `status`'s, and refusing an unrelated assignment for it would block its own remedy.
func CollisionRefusal(catalogue map[string]Manifest, node string, assigned, adding []string) error {
-3
View File
@@ -322,9 +322,6 @@ var defaultSeats = append([]Seat{
// node-mounts writes a mount and an automount unit per share on a machine that asks (shares.go).
{Name: NFSServerSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0263", Serves: nfsServerVerbs()},
{Name: MountsSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0263", Serves: mountsVerbs()},
// A machine's SMB shares for the devices on its own network (novox/hq ADR 0341, smb.go): its holder
// writes the SMB configuration, serves 445 alone and maps every client to the shared folder's owner.
{Name: SMBServerSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0341", Serves: smbServerVerbs()},
// A node's files and accounts, read (novox/hq ADR 0310, reads.go): by an account of the holder's own that holds
// no secret, on every node, so the reads ADR 0268 §5 takes over from the login shell's execute have a tool.
{Name: ReadsSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0310", Serves: readsVerbs()},
+3 -3
View File
@@ -48,7 +48,7 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
delivered[s.Delivers] = s.Name
}
}
// Forty-two with node-smb-server (novox/hq ADR 0341); forty-one with node-reads (novox/hq ADR 0310); forty with node-nfs-server and node-mounts (novox/hq ADR 0263); 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
// Forty-one with node-reads (novox/hq ADR 0310); forty with node-nfs-server and node-mounts (novox/hq ADR 0263); 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);
@@ -59,8 +59,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()) != 42 {
t.Errorf("the mesh defines %d seats rather than 42; the set is closed, so a change here is "+
if len(Seats()) != 41 {
t.Errorf("the mesh defines %d seats rather than 41; the set is closed, so a change here is "+
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
}
}
+173
View File
@@ -0,0 +1,173 @@
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
}
+163
View File
@@ -0,0 +1,163 @@
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)
}
}
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)
}
}
+5 -8
View File
@@ -31,10 +31,9 @@ func nfsServerVerbs() []Verb {
{Name: "exports", Description: "Every share this machine exports: its name, its path, read-write or " +
"read-only, the owner every client is mapped to (uid and gid), each node it is exported to with that " +
"node's private address and access (only the nodes the server grants it to and that ask for it), " +
"the subnets of this machine's local network it is exported to when the server grants it " +
"local-network (novox/hq ADR 0341), what is granted and not asked for or asked for and not " +
"granted, and whether the kernel holds it now. Also the exports found that are not the mesh's: a " +
"dataset's sharenfs property, a line in /etc/exports.",
"what is granted and not asked for or asked for and not granted, and whether the kernel holds it " +
"now. Also the exports found that are not the mesh's: a dataset's sharenfs property, a line in " +
"/etc/exports.",
Input: schema(map[string]string{}, nil),
Replaces: []string{"exportfs -v", "cat /etc/exports", "zfs get sharenfs"}},
{Name: "clients", Description: "Which machines have mounted which share now, as the NFS server " +
@@ -56,10 +55,8 @@ func nfsServerVerbs() []Verb {
Input: schema(map[string]string{}, nil),
Replaces: []string{"exportfs -ra"}},
{Name: "adopt", Description: "Take over an export a person made by hand: clear a dataset's sharenfs " +
"property for a path the mesh's own export now serves — only when that export is live and covers " +
"every client the property exported to: for a property exporting to the local network, the " +
"mesh's local-network export of the path (novox/hq ADR 0341). Without confirm, says what it " +
"would do and changes nothing.",
"property for a path the mesh's own export now serves — only when that export is live. Without " +
"confirm, says what it would do and changes nothing.",
Input: schema(map[string]string{
"path": "the shared path whose hand-made export is taken over",
"confirm": "\"true\": change it (needs why); anything else is a dry run",
-53
View File
@@ -1,53 +0,0 @@
package catalogue
// A machine's SMB shares, served to the devices on its own network that are not nodes (novox/hq
// ADR 0341).
//
// **One holder per machine.** Two modules writing one machine's Samba configuration is the conflict a
// seat exists to refuse, so the role is a node seat. It delivers nothing: its clients are devices that
// are not nodes, and a node that wants the same files mounts the NFS share (ADR 0263), so no provision
// is answered through it.
//
// **The data is the operator's** (ADR 0051). The holder never creates, chowns or removes anything under
// a shared path, and every client acts as the shared folder's owner. Its verbs read, and the one that
// takes over the configuration a person wrote by hand does it only on a person's word, keeping a dated
// copy of what it replaces.
// SMBServerSeat is the role of the machine that shares folders over SMB with its local network
// (novox/hq ADR 0341).
const SMBServerSeat = "node-smb-server"
// smbServerVerbs is the contract every holder of node-smb-server serves (novox/hq ADR 0341).
func smbServerVerbs() []Verb {
return []Verb{
{Name: "shares", Description: "Every SMB share this machine serves: its name, its path, read-write " +
"or read-only, the owner every client acts as (uid and gid), the logins allowed and whether each " +
"login's password was given or is kept as found, and whether the service serves it now. Also " +
"whether the configuration is the mesh's or still the hand-written one, and each hand-written " +
"share with whether the settings name it. A password is never shown.",
Input: schema(map[string]string{}, nil),
Replaces: []string{"testparm -s", "cat /etc/samba/smb.conf", "pdbedit -L"}},
{Name: "sessions", Description: "Which devices are connected to which share now, with the login and " +
"the protocol version each uses, as the SMB service knows them.",
Input: schema(map[string]string{}, nil),
Replaces: []string{"smbstatus"}},
{Name: "test", Description: "Whether this machine serves one share now and whether its SMB service " +
"is up and listening on 445: what a device on the local network is answered.",
Input: schema(map[string]string{"share": "the share, by its name"}, []string{"share"}),
Replaces: []string{"smbclient -L"}},
{Name: "reload", Description: "Have the SMB service read its configuration again now (smbcontrol " +
"reload-config) and answer the shares as it then serves them. The module's own process writes the " +
"configuration; this is for after a change made outside it. Changes no shared path.",
Input: schema(map[string]string{}, nil),
Replaces: []string{"smbcontrol smbd reload-config", "systemctl reload smb"}},
{Name: "adopt", Description: "Take over the SMB configuration a person wrote by hand: keep a dated " +
"copy of it beside it and write the mesh's own in its place — only when every hand-written share " +
"has its counterpart in the settings, so no share disappears. Without confirm, says what it would " +
"do and changes nothing: each hand-written share beside the mesh's rendering of it.",
Input: schema(map[string]string{
"confirm": "\"true\": change it (needs why); anything else is a dry run",
"why": "why, for the record",
}, nil, "confirm"),
Replaces: []string{"edit /etc/samba/smb.conf"}},
}
}
-95
View File
@@ -1,95 +0,0 @@
package catalogue
import (
"strings"
"testing"
)
// Defends novox/hq ADR 0341: a machine's SMB shares are one node seat, node-smb-server, with its verbs
// required of every holder, delivering nothing — its clients are devices that are not nodes.
func TestTheSMBServerIsANodeSeatWithItsVerbs(t *testing.T) {
s, ok := SeatNamed(SMBServerSeat)
if !ok {
t.Fatalf("%s is not in the mesh's set", SMBServerSeat)
}
if s.Scope != ScopeNode || s.Decision != "novox/hq ADR 0341" || s.Delivers != "" || s.Replicated {
t.Errorf("%s is %+v; a node seat under ADR 0341 that delivers nothing", SMBServerSeat, 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", SMBServerSeat, 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", SMBServerSeat, v.Name)
}
}
if strings.Join(got, " ") != "shares sessions test reload adopt" {
t.Errorf("%s serves %v", SMBServerSeat, got)
}
}
// Taking over the hand-written configuration is a person's act (ADR 0341 clause 7): a dry run unless the
// string switch says "true", and it says it keeps a dated copy and refuses to drop a share.
func TestTheSMBAdoptIsADryRunUnlessConfirmed(t *testing.T) {
s, _ := SeatNamed(SMBServerSeat)
for _, v := range s.Serves {
if v.Name != "adopt" {
continue
}
props, _ := v.Input["properties"].(map[string]any)
confirm, _ := props["confirm"].(map[string]any)
if confirm == nil || confirm["enum"] == nil || confirm["type"] != "string" {
t.Errorf("adopt has no string confirm switch: %v", v.Input)
}
for _, says := range []string{"Without confirm, says what it would do and changes nothing", "dated copy", "no share disappears"} {
if !strings.Contains(v.Description, says) {
t.Errorf("adopt does not say %q: %s", says, v.Description)
}
}
return
}
t.Error("node-smb-server has no adopt")
}
// A holder serving every verb holds the seat; a verb too many, one too few or the mesh's scope is refused,
// as the catalogue's smb-server module claims it.
func TestAHolderOfTheSMBSeatServesEveryVerbAndNothingElse(t *testing.T) {
seat, _ := SeatNamed(SMBServerSeat)
verbs := []string{"shares", "sessions", "test", "reload", "adopt"}
holder := Manifest{Module: "smb-server", Version: "1",
Claims: []Claim{{Name: SMBServerSeat, Scope: ScopeNode, Serves: verbs}}}
if err := CanHold(holder, seat); err != nil {
t.Errorf("smb-server serving every verb is refused: %v", err)
}
short := holder
short.Claims = []Claim{{Name: SMBServerSeat, Scope: ScopeNode, Serves: verbs[:4]}}
if err := CanHold(short, seat); err == nil || !strings.Contains(err.Error(), "adopt") {
t.Errorf("leaving adopt out was accepted: %v", err)
}
mesh := holder
mesh.Claims = []Claim{{Name: SMBServerSeat, Scope: ScopeMesh, Serves: verbs}}
if err := CanHold(mesh, seat); err == nil {
t.Error("claiming a node seat at the mesh's scope was accepted")
}
}
// node-nfs-server's adopt clears sharenfs only once the mesh's export covers every client the property
// exported to, the local network included (ADR 0341 clause 10), and exports names the local-network grant.
func TestTheNFSVerbsSayTheLocalNetworkGrant(t *testing.T) {
s, _ := SeatNamed(NFSServerSeat)
for _, v := range s.Serves {
switch v.Name {
case "adopt":
if !strings.Contains(v.Description, "covers every client the property exported to") {
t.Errorf("nfs adopt does not wait for the export to cover every client: %s", v.Description)
}
case "exports":
if !strings.Contains(v.Description, "local-network") {
t.Errorf("nfs exports does not say the local-network grant: %s", v.Description)
}
}
}
}
+4 -2
View File
@@ -217,14 +217,16 @@ var ControllerVerbs = []Verb{
Input: schema(map[string]string{"node": "the machine's name", "provision": "the provision"}, []string{"node", "provision"})},
{Name: "push", Description: "Send one machine everything it should be. With no machine named it is a push of the " +
"WHOLE mesh — every machine that is behind — and the answer says so first; behind says that outright. " +
"Answers at once that it is running, with a call id: `calls` with that id says what it sent " +
"(a push can reload the bus, which then refuses any answer still to come). A push by hand is a repair, " +
"Answers at once that it is running, with a call id: `calls` with that id says what it sent and, for a " +
"machine named, how that machine applied it — applied, refused with its reason, or each failed resource — " +
"once it reports within wait (a push can reload the bus, which then refuses any answer still to come). A push by hand is a repair, " +
"and says why: recorded in the hand-act log (novox/hq to-be 45 §7). A machine whose push would give a " +
"running module's data another directory is held and named, the others sent (novox/hq ADR 0217): read " +
"`plan` with diff, and name the machine with move to send it.",
Input: schema(map[string]string{
"node": "the machine's name; without it, every machine that is behind",
"move": "with node: the modules whose data may move with this push, comma-separated (optional)",
"wait": "with node: how long the call waits for the machine to report how it applied what it was sent, as \"90s\" or \"2m\"; \"0\" waits for nothing; without it, the controller's setting push-wait (optional)",
"behind": "\"true\": every machine that is behind, the whole mesh — the same as naming none, said outright; not with node",
"why": "why this is pushed by hand: recorded in the hand-act log",
"cause": "the cause in a word, or a condition's kind — the word a second push for the same reason uses (optional)",
+64 -1
View File
@@ -58,7 +58,42 @@ func (i *Inventory) Dependencies(ctx context.Context) ([]Edge, error) {
if err != nil {
return nil, err
}
return dependenciesOf(entries, against, nil), nil
held, err := i.Held(ctx)
if err != nil {
return nil, err
}
return dependenciesWith(entries, against, held), nil
}
// packagesHeld is, by package name, the module whose artifact the mesh holds as that package: a
// `package` artifact's reference is `<name>@<version>` (the builder's publishPackage), never an
// artifact-store reference, so a build that stood on it recorded the package's name and version.
func packagesHeld(held map[string]string) map[string]string {
out := map[string]string{}
for key, reference := range held {
module, _, ok := strings.Cut(key, "/")
if !ok {
continue
}
if name := packageName(reference); name != "" {
out[name] = module
}
}
return out
}
// packageName is the name of a `<name>@<version>` package reference, or "" for anything else: a
// kept or pinned image (`…@sha256:…`), a URL, or a reference with no version.
func packageName(reference string) string {
if strings.HasPrefix(reference, catalogue.ArtifactStoreScheme) || strings.Contains(reference, "@sha256:") ||
strings.Contains(reference, "://") {
return ""
}
at := strings.LastIndex(reference, "@")
if at <= 0 || at == len(reference)-1 {
return ""
}
return reference[:at]
}
// dependenciesOf is Dependencies over what was read, so a test can hand it a catalogue.
@@ -66,6 +101,20 @@ func (i *Inventory) Dependencies(ctx context.Context) ([]Edge, error) {
// `read` draws no edge (novox/hq ADR 0267 rule 4): what a build read moves it through its build source, in
// the planner, never through the relation.
func dependenciesOf(entries []Entry, against map[string][]string, _ map[string][]ReadRepository) []Edge {
return dependenciesWith(entries, against, nil)
}
// dependenciesWith is the relation with the artifacts the mesh holds, "<module>/<artifact>" to its
// reference (Held), so a package a build stood on is read as its module's.
//
// **A built manifest carries no `build`** (catalogue.Resolve drops it), so what a module declared it
// is built on is read from what is kept of it once it is built (novox/hq issue 614): the bases its
// newest build was handed (`against`) — an artifact-store reference, or a package's `<name>@<version>`
// — and the language of every bundle it compiled (`Bundles`). Read only from records the mesh already
// keeps, so it holds for every module built before it, and a controller rolled back to one without it
// reads the same records as before.
func dependenciesWith(entries []Entry, against map[string][]string, held map[string]string) []Edge {
packages := packagesHeld(held)
known := map[string]bool{}
var builders []string
for _, e := range entries {
@@ -111,6 +160,13 @@ func dependenciesOf(entries []Entry, against map[string][]string, _ map[string][
}
}
}
// **And once it is built**, from the bundles its resolved manifest keeps (novox/hq issue 614): the
// `build` it was declared with is gone by then, and a toolchain that moved rebuilt none of them.
for _, b := range e.Manifest.Bundles {
if chain, err := builder.ToolchainFor(b.Language); err == nil {
add(name, chain.Base, EdgeStandsOn)
}
}
for _, ref := range against[name] {
if rest, ok := strings.CutPrefix(ref, catalogue.ArtifactStoreScheme); ok {
// A copy of an upstream image is no module's artifact (novox/hq ADR 0257): its
@@ -121,6 +177,13 @@ func dependenciesOf(entries []Entry, against map[string][]string, _ map[string][
if base, _, found := strings.Cut(rest, "/"); found {
add(name, base, EdgeStandsOn)
}
continue
}
// A package base (`build.on` naming a module's `package` artifact) is recorded as the
// package's name and version (novox/hq issue 614): the module that publishes that package
// is what it stood on, whichever version the mesh holds now.
if base, ok := packages[packageName(ref)]; ok {
add(name, base, EdgeStandsOn)
}
}
if e.Source.Repository != "" {
+72
View File
@@ -1,6 +1,7 @@
package inventory
import (
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
@@ -174,3 +175,74 @@ func TestACopyOfAnUpstreamImageIsNoEdge(t *testing.T) {
}
}
}
// **A built module keeps its build edges** (novox/hq issue 614). A manifest the mesh registers from a
// build carries no `build` (catalogue.Resolve drops it), so mesh-tools' `build.on` naming mesh-sdk's
// `lib` package and a provider's TypeScript bundle drew no edge once built, and a new SDK rebuilt
// neither. The package base is read from what the build was handed (`@novox/mesh-sdk@0.1.13`, the
// package's name whichever version the mesh holds now), the toolchain from the bundles it keeps.
func TestABuiltModuleKeepsItsBuildEdges(t *testing.T) {
built := func(name string, bundles ...catalogue.Bundle) Entry {
return Entry{Manifest: catalogue.Manifest{Module: name, Bundles: bundles},
Source: Source{Repository: "http://forge/novox/" + name + ".git"}}
}
entries := []Entry{
built("mesh-sdk"),
built("mesh-tools"),
built("mesh-tools-go"),
built("minio", catalogue.Bundle{Name: "code", Language: "typescript"}),
built("umami", catalogue.Bundle{Name: "code", Language: "TypeScript"}),
built("dnsmasq", catalogue.Bundle{Name: "tools", Language: "go"}),
built("plain"),
}
against := map[string][]string{
"mesh-tools": {"@novox/mesh-sdk@0.1.13",
catalogue.ArtifactStoreScheme + "upstream/docker.io/library/node@sha256:" + strings.Repeat("0", 64)},
// A package nobody in the mesh publishes is no edge.
"plain": {"left-pad@1.3.0"},
}
held := map[string]string{
"mesh-sdk/lib": "@novox/mesh-sdk@0.1.14",
"mesh-tools/runtime": catalogue.ArtifactStoreScheme + "mesh-tools/runtime@sha256:" + strings.Repeat("1", 64),
}
got := dependenciesWith(entries, against, held)
has := func(from, to string) bool {
for _, e := range got {
if e.From == from && e.To == to && e.Kind == EdgeStandsOn {
return true
}
}
return false
}
for _, want := range [][2]string{
{"mesh-tools", "mesh-sdk"},
{"minio", "mesh-tools"},
{"umami", "mesh-tools"},
{"dnsmasq", "mesh-tools-go"},
} {
if !has(want[0], want[1]) {
t.Errorf("no %s stands-on %s: %+v", want[0], want[1], got)
}
}
for _, e := range got {
if e.From == "plain" && e.Kind != EdgeBuiltBy {
t.Errorf("an unknown package drew an edge: %+v", e)
}
}
}
func TestAPackageNameIsReadOnlyFromAPackageReference(t *testing.T) {
for ref, want := range map[string]string{
"@novox/mesh-sdk@0.1.14": "@novox/mesh-sdk",
"left-pad@1.3.0": "left-pad",
"@novox/mesh-sdk": "",
"novox.internal:5100/mesh-tools/build@sha256:" + strings.Repeat("0", 64): "",
catalogue.ArtifactStoreScheme + "mesh-tools/build@sha256:0": "",
"http://store/v2/x/blobs/sha256:0": "",
"": "",
} {
if got := packageName(ref); got != want {
t.Errorf("packageName(%q) = %q, want %q", ref, got, want)
}
}
}
+7 -1
View File
@@ -44,7 +44,13 @@ type Wait struct {
Part string `json:"part"`
Secret string `json:"secret,omitempty"`
Setting string `json:"setting,omitempty"`
What string `json:"what"`
// Provision is a provision the module requires whose credential only a person can give and nobody gave yet
// (novox/hq ADR 0337 §5): a wait names exactly one of Secret, Setting or Provision.
Provision string `json:"provision,omitempty"`
What string `json:"what"`
// Act is the line that gives a provision's credential, filled by the controller when it excuses the wait
// (ADR 0337 §7); never said by a machine.
Act string `json:"act,omitempty"`
}
// NodeHealth is a machine's newest statement, as kept.
+10 -1
View File
@@ -36,7 +36,16 @@ type MadeOf struct {
BootedAt *time.Time `json:"booted_at"`
Timezone string `json:"timezone"`
Filesystems []MountedFilesystem `json:"filesystems"`
Unknown map[string]string `json:"unknown"`
// Networks is every network the machine is directly attached to on a link facing outside it, as
// link and network (novox/hq ADR 0340 §7): what a port reached from `local-network` resolves from.
Networks []AttachedNetwork `json:"networks"`
Unknown map[string]string `json:"unknown"`
}
// AttachedNetwork is one network a machine said it is attached to, and the link it is on.
type AttachedNetwork struct {
Link string `json:"link"`
Network string `json:"network"`
}
// MountedFilesystem is one local filesystem a machine has mounted.
+15
View File
@@ -40,3 +40,18 @@ func TestAProfileWithoutTheFactsIsAnOlderNodeEngineNotNothing(t *testing.T) {
t.Error("a node that never reported read as having reported")
}
}
// The networks a machine is attached to (novox/hq ADR 0340 §7), in the node-engine's words: what a port
// reached from `local-network` resolves from.
func TestAMachinesAttachedNetworksAreRead(t *testing.T) {
a, had, err := machineFrom([]byte(`{"architecture":"amd64","kernel":"linux",
"machine":{"observed_at":"2026-10-13T09:00:00Z",
"networks":[{"link":"eth0","network":"192.168.1.0/24"},{"link":"eth1","network":"203.0.113.0/24"}]}}`))
if err != nil || !had || a.Machine == nil {
t.Fatalf("%+v had %v err %v", a, had, err)
}
n := a.Machine.Networks
if len(n) != 2 || n[0].Link != "eth0" || n[0].Network != "192.168.1.0/24" || n[1].Network != "203.0.113.0/24" {
t.Fatalf("networks read as %+v", n)
}
}
+252
View File
@@ -0,0 +1,252 @@
package inventory
import (
"context"
"errors"
"fmt"
"sort"
"github.com/jackc/pgx/v5"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/secrets"
)
// A provision whose credential only a person outside the mesh can give (novox/hq ADR 0337, issue 164): a Servarr
// app's API key, an indexer's key, a downloader's password. The provider's software holds the value; the mesh can
// neither make one that would work nor fetch it. So the mesh never makes one (§2), a person gives it once per
// provider and it is sealed to every consumer of that provider (§3), and until then a consumer holds no credential
// for it and is told so (§4), and the rest of it runs.
// NotGivenError is a pair credential only a person can give, which no person gave for this pair under the keys its
// two ends have now: the consumer receives no credential for it, and waits (ADR 0337 §4, §8). It is not a fault of
// the machine, so the plan does not refuse the machine for it.
type NotGivenError struct {
Provision, Consumer, ConsumerModule, Provider, ProviderModule, Local string
// Own is set when the credential is the provider's own secret issued outside (ADR 0158, ADR 0228), and names it.
Own string
// Why is why no value is held, in words that never carry one.
Why string
}
func (e *NotGivenError) Error() string {
if e.Own != "" {
return fmt.Sprintf("%s on %s holds no %s from %s: %s; give it with `secret accept %s %s %s`",
e.ConsumerModule, e.Consumer, e.Provision, e.Provider, e.Why, e.Provider, e.ProviderModule, e.Own)
}
return fmt.Sprintf("%s on %s holds no %s from %s: %s; it is issued outside the mesh and given with `%s`",
e.ConsumerModule, e.Consumer, e.Provision, e.Provider, e.Why, acceptLine(e.Provider, e.ProviderModule, e.Provision))
}
// IsNotGiven is whether an error is a credential only a person gives, not given yet.
func IsNotGiven(err error) bool {
var ng *NotGivenError
return errors.As(err, &ng)
}
// acceptLine is the one line that gives a provision's credential issued outside, for every consumer of a provider.
func acceptLine(provider, providerModule, provision string) string {
if providerModule == "" {
providerModule = "<provider module>"
}
return fmt.Sprintf("secret accept %s %s %s", provider, providerModule, provision)
}
// AcceptLine is acceptLine, for the controller's words.
func AcceptLine(provider, providerModule, provision string) string {
return acceptLine(provider, providerModule, provision)
}
// issuedOutside is whether a module in the catalogue offers the provision with a credential issued outside the
// mesh, and which.
func (i *Inventory) issuedOutside(ctx context.Context, provision string) (string, bool, error) {
shelf, err := i.Catalogue(ctx)
if err != nil {
return "", false, err
}
module, outside := catalogue.OffersIssuedOutside(shelf, provision)
return module, outside, nil
}
// ownIssuedOutside is whether a provider's own secret says only an outside party issues it (ADR 0228).
func (i *Inventory) ownIssuedOutside(ctx context.Context, providerModule, own string) (string, bool, error) {
if providerModule == "" {
return own, false, nil
}
m, err := i.declared(ctx, providerModule)
if err != nil {
return own, false, err
}
return own, m.OwnSecrets[own].IssuedBy == catalogue.IssuedOutside, nil
}
// GivenPairSecret is the credential of a pair whose provision is issued outside the mesh, as a person gave it, and
// never one the mesh makes (ADR 0337 §2, §4). It writes nothing. A value is answered only when a person gave it
// (`accepted`) and it is sealed to the keys both ends have now; otherwise a NotGivenError says why — never given, or
// given under a key one end no longer has (§8), or a value the mesh made before the rule, which is not given and
// never delivered again (§2).
func (i *Inventory) GivenPairSecret(ctx context.Context, name, consumer, consumerModule, provider, providerModule,
local string) (Secret, error) {
consumerKey, err := i.SealingKeyOf(ctx, consumer)
if err != nil {
return Secret{}, err
}
providerKey, err := i.SealingKeyOf(ctx, provider)
if err != nil {
return Secret{}, err
}
consumerNode, err := i.NodeByName(ctx, consumer)
if err != nil {
return Secret{}, err
}
providerNode, err := i.NodeByName(ctx, provider)
if err != nil {
return Secret{}, err
}
notGiven := func(why string) (Secret, error) {
return Secret{}, &NotGivenError{Provision: name, Consumer: consumer, ConsumerModule: consumerModule,
Provider: provider, ProviderModule: providerModule, Local: local, Why: why}
}
var held Secret
err = i.store.Pool().QueryRow(ctx,
`select for_consumer, for_provider, consumer_key, provider_key, origin from secret
where name = $1 and consumer = $2 and consumer_module = $3 and provider = $4 and local = $5`,
name, consumerNode.ID, consumerModule, providerNode.ID, local).
Scan(&held.ForConsumer, &held.ForProvider, &held.ConsumerKey, &held.ProviderKey, &held.Origin)
switch {
case errors.Is(err, pgx.ErrNoRows):
return notGiven("nobody gave it")
case err != nil:
return Secret{}, err
case held.Origin != OriginAccepted:
return notGiven("the value held was made by the mesh, which " + providerModule + "'s software would refuse")
case consumerKey == "" || providerKey == "" || held.ConsumerKey != consumerKey || held.ProviderKey != providerKey:
return notGiven("it was given under a sealing key one end no longer has")
}
held.Name, held.Consumer, held.Provider = name, consumer, provider
held.ConsumerModule, held.Local = consumerModule, local
return held, nil
}
// PairEnd is one consumer of a provision from one provider: the machine, the module there and its local name.
type PairEnd struct {
Consumer, ConsumerModule, Local string
}
// ProvisionAccepted is what one accept for a provider sealed (ADR 0337 §3): the consumers it reached, those it could
// not (no sealing key on their machine), and the machines to send.
type ProvisionAccepted struct {
Sealed, Unsealable []PairEnd
Machines []string
}
// AcceptSecretForProvision seals one value a person gave for a provision issued outside the mesh to the provider's
// machine, to every consumer named, and to the operator, in one act under one generation, each row `accepted`
// (ADR 0337 §3, issue 165); the plaintext is not kept. The consumers are every one that binds the provision from
// this provider now, which the caller finds by resolving every machine. A consumer whose machine has no sealing key
// is not sealed to, and said.
func (i *Inventory) AcceptSecretForProvision(ctx context.Context, provision, provider, providerModule, value string,
consumers []PairEnd) (ProvisionAccepted, error) {
m, err := i.declared(ctx, providerModule)
if err != nil {
return ProvisionAccepted{}, err
}
if !m.IssuedOutsideOf(provision) {
return ProvisionAccepted{}, fmt.Errorf("%s does not offer %q with a credential issued outside the mesh "+
"(\"credential\": {\"issued-by\": \"outside\"}, ADR 0337), so its consumers' credentials are not given "+
"once for the provider", providerModule, provision)
}
if value == "" {
return ProvisionAccepted{}, errors.New("there is nothing to seal")
}
providerNode, err := i.NodeByName(ctx, provider)
if err != nil {
return ProvisionAccepted{}, err
}
providerKey, err := i.SealingKeyOf(ctx, provider)
if err != nil {
return ProvisionAccepted{}, err
}
if providerKey == "" {
return ProvisionAccepted{}, fmt.Errorf("%s has no sealing key, so nothing can be sealed to it; a node joins to get one", provider)
}
forOperator, operatorKey, err := i.operatorSeal(ctx, value)
if err != nil {
return ProvisionAccepted{}, err
}
generation := secrets.Stamp()
var out ProvisionAccepted
machines := map[string]bool{provider: true}
tx, err := i.store.Pool().Begin(ctx)
if err != nil {
return ProvisionAccepted{}, err
}
defer func() { _ = tx.Rollback(ctx) }()
for _, c := range consumers {
consumerNode, err := i.NodeByName(ctx, c.Consumer)
if err != nil {
return ProvisionAccepted{}, err
}
consumerKey, err := i.SealingKeyOf(ctx, c.Consumer)
if err != nil {
return ProvisionAccepted{}, err
}
if consumerKey == "" {
out.Unsealable = append(out.Unsealable, c)
continue
}
sealed, err := secrets.Accept(value, consumerKey, providerKey)
if err != nil {
return ProvisionAccepted{}, err
}
if _, err := tx.Exec(ctx,
`insert into secret (name, consumer, consumer_module, provider, for_consumer, for_provider,
consumer_key, provider_key, operator_sealed, operator_key, origin, local, generation)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
on conflict (name, local, consumer, consumer_module, provider) do update set
for_consumer = excluded.for_consumer, for_provider = excluded.for_provider,
consumer_key = excluded.consumer_key, provider_key = excluded.provider_key,
created_at = now(), origin = excluded.origin, generation = excluded.generation,
operator_sealed = excluded.operator_sealed, operator_key = excluded.operator_key`,
provision, consumerNode.ID, c.ConsumerModule, providerNode.ID,
sealed.ForConsumer, sealed.ForProvider, sealed.ConsumerKey, sealed.ProviderKey,
forOperator, operatorKey, OriginAccepted, c.Local, generation); err != nil {
return ProvisionAccepted{}, err
}
out.Sealed = append(out.Sealed, c)
machines[c.Consumer] = true
}
if err := tx.Commit(ctx); err != nil {
return ProvisionAccepted{}, err
}
for n := range machines {
out.Machines = append(out.Machines, n)
}
sort.Strings(out.Machines)
return out, nil
}
// GivenPairsOf is every provision a module on a machine holds a value a person gave for, sealed to the keys both
// ends have now: what a wait for a provision's credential is checked against (ADR 0337 §6).
func (i *Inventory) GivenPairsOf(ctx context.Context, machine, module string) (map[string]bool, error) {
rows, err := i.store.Pool().Query(ctx,
`select s.name
from secret s
join node c on c.id = s.consumer
join node p on p.id = s.provider
where c.name = $1 and s.consumer_module = $2 and s.origin = 'accepted'
and s.consumer_key = c.sealing_key and s.provider_key = p.sealing_key`, machine, module)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]bool{}
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
out[name] = true
}
return out, rows.Err()
}
+195
View File
@@ -0,0 +1,195 @@
package inventory
import (
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
)
// outsideProvision registers a provider offering tv-api with a credential issued outside the mesh (novox/hq ADR
// 0337 §1) and two consumers requiring it, on twoNodesWithKeys' machines.
func outsideProvision(t *testing.T, inv *Inventory) {
t.Helper()
ctx := t.Context()
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "tv", Version: "1",
Provides: []catalogue.Offer{{Name: "tv-api", Scope: catalogue.ScopeMesh,
Credential: &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside}}}}, Source{}); err != nil {
t.Fatal(err)
}
for _, m := range []string{"requests", "indexer"} {
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: m, Version: "1", Requires: []string{"tv-api"},
Secrets: map[string]string{"tv-api": "/run/tv-api.secret"}}, Source{}); err != nil {
t.Fatal(err)
}
}
}
// pairRows is how many pair rows the store holds for tv-api.
func pairRows(t *testing.T, inv *Inventory) int {
t.Helper()
var n int
if err := inv.store.Pool().QueryRow(t.Context(), `select count(*) from secret where name = 'tv-api'`).Scan(&n); err != nil {
t.Fatal(err)
}
return n
}
// A credential only a person can give is never made (ADR 0337 §2, issue 164): not for a pair with no row, which is
// what a newly bound consumer has, and not after a key change; a value the mesh made before the rule is not given.
func TestACredentialIssuedOutsideIsNeverMade(t *testing.T) {
inv, ctx := twoNodesWithKeys(t)
outsideProvision(t, inv)
_, err := inv.SecretFor(ctx, "tv-api", "consumer", "requests", "provider", "")
if !IsNotGiven(err) {
t.Fatalf("SecretFor made or refused a credential only a person gives, rather than saying it was not given: %v", err)
}
if !strings.Contains(err.Error(), "secret accept provider tv tv-api") {
t.Fatalf("not given does not name the one line that gives it: %v", err)
}
if n := pairRows(t, inv); n != 0 {
t.Fatalf("the store holds %d rows for tv-api, which nobody gave", n)
}
if _, err := inv.GivenPairSecret(ctx, "tv-api", "consumer", "requests", "provider", "tv", ""); !IsNotGiven(err) {
t.Fatalf("a pair nobody gave read as given: %v", err)
}
if n := pairRows(t, inv); n != 0 {
t.Fatalf("reading the pair wrote %d rows", n)
}
// A value the mesh made before the rule (the minted value of issue 164) is not given.
if _, err := inv.store.Pool().Exec(ctx, `insert into secret (name, consumer, consumer_module, provider,
for_consumer, for_provider, consumer_key, provider_key, local)
select 'tv-api', c.id, 'indexer', p.id, 'x', 'y', c.sealing_key, p.sealing_key, ''
from node c, node p where c.name = 'consumer' and p.name = 'provider'`); err != nil {
t.Fatal(err)
}
if _, err := inv.GivenPairSecret(ctx, "tv-api", "consumer", "indexer", "provider", "tv", ""); !IsNotGiven(err) ||
!strings.Contains(err.Error(), "made by the mesh") {
t.Fatalf("a value the mesh made before the rule read as given: %v", err)
}
// Given, then a key changes: not given, and nothing is made in its place (ADR 0337 §8).
if _, err := inv.AcceptSecretForProvision(ctx, "tv-api", "provider", "tv", "the app's key",
[]PairEnd{{Consumer: "consumer", ConsumerModule: "requests"}}); err != nil {
t.Fatal(err)
}
if _, err := inv.GivenPairSecret(ctx, "tv-api", "consumer", "requests", "provider", "tv", ""); err != nil {
t.Fatalf("the value given does not read as given: %v", err)
}
cn, _ := inv.NodeByName(ctx, "consumer")
key, _ := aSealingKey(t)
if err := inv.RecordSealingKey(ctx, cn.ID, key); err != nil {
t.Fatal(err)
}
if _, err := inv.GivenPairSecret(ctx, "tv-api", "consumer", "requests", "provider", "tv", ""); !IsNotGiven(err) ||
!strings.Contains(err.Error(), "sealing key") {
t.Fatalf("a value given under a key the consumer no longer has read as given: %v", err)
}
var origin string
if err := inv.store.Pool().QueryRow(ctx, `select origin from secret where name = 'tv-api' and consumer_module = 'requests'`).
Scan(&origin); err != nil || origin != OriginAccepted {
t.Fatalf("the given row was replaced (origin %q, %v)", origin, err)
}
// Rotate refuses it, naming the accept (ADR 0337 §9).
err = inv.RotateSecret(ctx, "tv-api", "consumer", "requests", "provider", "")
if err == nil || !strings.Contains(err.Error(), "secret accept provider tv tv-api") {
t.Fatalf("rotate of a credential issued outside was not refused naming the accept: %v", err)
}
}
// One accept for a provider is sealed to every consumer named, to the provider and to the operator, under one
// generation, each row accepted (ADR 0337 §3, issue 165); a consumer with no sealing key is said, not sealed to.
func TestOneAcceptForAProviderIsSealedToEveryConsumer(t *testing.T) {
inv, ctx := twoNodesWithKeys(t)
outsideProvision(t, inv)
operatorKey, operatorOpens := aSealingKey(t)
if _, err := inv.SetOperatorKey(ctx, operatorKey); err != nil {
t.Fatal(err)
}
consumerKey, consumerOpens := aSealingKey(t)
cn, _ := inv.NodeByName(ctx, "consumer")
if err := inv.RecordSealingKey(ctx, cn.ID, consumerKey); err != nil {
t.Fatal(err)
}
if _, err := inv.AddNode(ctx, "keyless"); err != nil {
t.Fatal(err)
}
accepted, err := inv.AcceptSecretForProvision(ctx, "tv-api", "provider", "tv", "the app's key", []PairEnd{
{Consumer: "consumer", ConsumerModule: "requests"}, {Consumer: "consumer", ConsumerModule: "indexer"},
{Consumer: "keyless", ConsumerModule: "requests"}})
if err != nil {
t.Fatal(err)
}
if len(accepted.Sealed) != 2 || len(accepted.Unsealable) != 1 || accepted.Unsealable[0].Consumer != "keyless" {
t.Fatalf("the accept did not seal to both consumers with keys and say the keyless one: %+v", accepted)
}
if strings.Join(accepted.Machines, ",") != "consumer,provider" {
t.Fatalf("the machines to send are the provider's and every consumer's sealed to: %v", accepted.Machines)
}
rows, err := inv.store.Pool().Query(ctx, `select consumer_module, origin, coalesce(generation, ''), for_consumer,
coalesce(operator_sealed, '') from secret where name = 'tv-api'`)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
generations := map[string]bool{}
seen := 0
for rows.Next() {
var module, origin, generation, forConsumer, forOperator string
if err := rows.Scan(&module, &origin, &generation, &forConsumer, &forOperator); err != nil {
t.Fatal(err)
}
seen++
if origin != OriginAccepted || generation == "" {
t.Fatalf("%s's row is not accepted under a generation (origin %q)", module, origin)
}
generations[generation] = true
if opened, ok := consumerOpens(forConsumer); !ok || string(opened) != "the app's key" {
t.Fatalf("%s's copy does not open to the value given", module)
}
if opened, ok := operatorOpens(forOperator); !ok || string(opened) != "the app's key" {
t.Fatalf("%s's operator copy does not open to the value given", module)
}
}
if seen != 2 || len(generations) != 1 {
t.Fatalf("%d rows under %d generations; want 2 under one", seen, len(generations))
}
given, err := inv.GivenPairsOf(ctx, "consumer", "indexer")
if err != nil || !given["tv-api"] {
t.Fatalf("the provision given does not read as given for indexer (%v)", err)
}
// Only an offer that says so is given this way.
if _, err := inv.AcceptSecretForProvision(ctx, "secret", "provider", "gitea", "x", nil); err == nil {
t.Fatal("a provision not issued outside was given once for its provider")
}
}
// The provider's own secret issued outside the mesh, named as a provision's shared credential (ADR 0158, ADR 0228),
// is never made for a consumer that binds: the consumer is told it was not given (ADR 0337 §2, §4).
func TestASharedOwnSecretIssuedOutsideIsNeverMade(t *testing.T) {
inv, ctx := twoNodesWithKeys(t)
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "downloader", Version: "1",
OwnSecrets: catalogue.OwnSecrets{"password": {Path: "/run/password", Taken: catalogue.TakenAtStart,
IssuedBy: catalogue.IssuedOutside}},
Provides: []catalogue.Offer{{Name: "downloader-api", Scope: catalogue.ScopeMesh,
Credential: &catalogue.OfferCredential{Own: "password"}}}}, Source{}); err != nil {
t.Fatal(err)
}
_, err := inv.SharedSecretFor(ctx, "downloader-api", "consumer", "gitea", "provider", "downloader", "", "password")
if !IsNotGiven(err) {
t.Fatalf("a shared own secret issued outside was made or refused rather than not given: %v", err)
}
var n int
if err := inv.store.Pool().QueryRow(ctx, `select count(*) from module_secret where module = 'downloader'`).Scan(&n); err != nil || n != 0 {
t.Fatalf("the mesh made %d values of an own secret issued outside (%v)", n, err)
}
if err := inv.remakeShared(ctx, nil, "", "downloader", "password", "", nil, "", "", ""); err == nil ||
!strings.Contains(err.Error(), "issued outside") {
t.Fatalf("remakeShared made a value of an own secret issued outside: %v", err)
}
}
+39
View File
@@ -104,6 +104,21 @@ func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModul
consumerModule, name, provider, consumer, consumerModule, name, provider, localFlag(local))
}
// **Never made where only a person can give it** (novox/hq ADR 0337 §2, issue 164): a provision whose offer
// says `issued-by: outside` gets no value of the mesh's, which its provider's software would refuse. Asked
// here, on the one path that makes a value, so no caller can have one made by not knowing; the plan reads
// such a pair with GivenPairSecret, which never makes one and delivers only a value a person gave.
why := "nobody gave it"
if err == nil {
why = "the value held was sealed to a key one end no longer has"
}
if providerModule, outside, oerr := i.issuedOutside(ctx, name); oerr != nil {
return Secret{}, oerr
} else if outside {
return Secret{}, &NotGivenError{Provision: name, Consumer: consumer, ConsumerModule: consumerModule,
Provider: provider, ProviderModule: providerModule, Local: local, Why: why}
}
// And to the operator, when the mesh has one (novox/hq ADR 0085, amended): the third copy that
// makes a vault-provided secret recoverable, and nothing the mesh can open.
operator, err := i.OperatorKey(ctx)
@@ -239,6 +254,16 @@ func (i *Inventory) RotateSecret(ctx context.Context, name, consumer, consumerMo
`select origin from secret where name = $1 and consumer = $2 and consumer_module = $3
and provider = $4 and local = $5`,
name, consumerNode.ID, consumerModule, providerNode.ID, local).Scan(&origin)
if providerModule, outside, oerr := i.issuedOutside(ctx, name); oerr != nil {
return oerr
} else if outside {
// Only a person can give it, so only a person can give its next value (novox/hq ADR 0337 §9, ADR 0092 §7):
// deleting the row would leave the consumer waiting for nothing new.
return fmt.Errorf(
"%s is issued outside the mesh: %s's software holds it and the mesh cannot make its replacement. "+
"Give the new value with `%s`, which seals it to every consumer of %s on %s",
name, providerModule, acceptLine(provider, providerModule, name), providerModule, provider)
}
if err == nil && origin == OriginAccepted {
return fmt.Errorf(
"%s's %q credential from %s was accepted from a person, and the mesh cannot make "+
@@ -886,6 +911,15 @@ func (i *Inventory) SharedSecretFor(ctx context.Context, provision, consumer, co
held.ConsumerModule, held.Local = consumerModule, local
return held, nil
}
if own, outside, err := i.ownIssuedOutside(ctx, providerModule, own); err != nil {
return Secret{}, err
} else if outside && (ownOrigin == nil || *ownOrigin != OriginAccepted) {
// The provider's own secret only an outside party issues, which nobody gave (ADR 0228, issue 401): the
// mesh makes no value for it here either, and the consumer is told it was not given (ADR 0337 §4).
return Secret{}, &NotGivenError{Provision: provision, Consumer: consumer, ConsumerModule: consumerModule,
Provider: provider, ProviderModule: providerModule, Local: local, Own: own,
Why: "nobody gave " + providerModule + "'s own secret " + own + ", which is issued outside the mesh"}
}
if ownOrigin != nil && *ownOrigin == OriginAccepted {
return Secret{}, fmt.Errorf(
"%s on %s needs %s from %s, whose credential is %s's own secret %q — a value given to the "+
@@ -912,6 +946,11 @@ func (i *Inventory) remakeShared(ctx context.Context, providerID any, providerKe
if len(provisions) == 0 {
return fmt.Errorf("%s names no provision whose credential is its own secret %q", providerModule, own)
}
if m.OwnSecrets[own].IssuedBy == catalogue.IssuedOutside {
// Never made (novox/hq ADR 0228, ADR 0337 §2): only a person can give it, with `secret accept`.
return fmt.Errorf("%s's own secret %q is issued outside the mesh, so the mesh makes no value for it; a "+
"person gives it with `secret accept <machine> %s %s`", providerModule, own, providerModule, own)
}
operator, err := i.OperatorKey(ctx)
if err != nil {
return err
+4 -1
View File
@@ -577,7 +577,10 @@ type Wait struct {
Part string `json:"part"`
Secret string `json:"secret,omitempty"`
Setting string `json:"setting,omitempty"`
What string `json:"what"`
// Provision is a provision the module requires whose credential only a person can give and nobody gave yet
// (novox/hq ADR 0337 §5): a wait names exactly one of Secret, Setting or Provision.
Provision string `json:"provision,omitempty"`
What string `json:"what"`
}
// ResourceHealth is one long-running resource's state.
+16 -1
View File
@@ -35,6 +35,11 @@
"kind": "preference",
"default": "10m",
"why": "a busy period never holds a batch longer than this after its first merge (novox/hq ADR 0276)"
},
"push-wait": {
"kind": "preference",
"default": "2m",
"why": "how long a push to a named machine waits for that machine's answer, so its call says how it went; a call's own wait overrides it, and 0 waits for nothing (novox/hq issue 629)"
}
},
"tools": [
@@ -120,6 +125,15 @@
"owner": "mesh-controller",
"content": "{\n \"merge-window\": \"${setting:merge-window}\",\n \"merge-window-at-most\": \"${setting:merge-window-at-most}\"\n}\n"
},
{
"id": "push",
"trusted": false,
"type": "file",
"path": "${dir:mesh-state}/push.json",
"mode": "0600",
"owner": "mesh-controller",
"content": "{\n \"push-wait\": \"${setting:push-wait}\"\n}\n"
},
{
"id": "controller",
"type": "process",
@@ -144,7 +158,8 @@
"MESH_BROKER_ADDRESS_PORT": "${seat:mesh-broker:5671}",
"MESH_BUS_NATS_FILE": "${dir:mesh-state}/bus",
"MESH_CONTROLLER_VERSION": "${version}",
"MESH_MERGE_WINDOW_FILE": "${dir:mesh-state}/merge-window.json"
"MESH_MERGE_WINDOW_FILE": "${dir:mesh-state}/merge-window.json",
"MESH_PUSH_FILE": "${dir:mesh-state}/push.json"
},
"replaces": [
"server"
+6
View File
@@ -45,3 +45,9 @@
./cmd/mesh-controller TestReplay348AFaultFromBeforeTheSendFailsNoGate
./cmd/mesh-controller TestReplay239AModuleNameIsNotTakenOverByAnotherRepository
./cmd/mesh-controller TestAWalkThatMovedNothingSaysWhichModulesMatchedWhatRuns
./cmd/mesh-controller TestReplay614ANewSDKPlansTheToolchainAndThenItsBundles
./internal/inventory TestACredentialIssuedOutsideIsNeverMade
./internal/inventory TestOneAcceptForAProviderIsSealedToEveryConsumer
./internal/inventory TestASharedOwnSecretIssuedOutsideIsNeverMade
./cmd/mesh-controller TestOneAcceptForAProviderIsPlacedForEveryConsumer
./cmd/mesh-controller TestReplay164