Compare commits

..
Author SHA1 Message Date
jochen 41216e4d4c Give a bundle module's bound credential to the account its runtime runs as (hq issue 624)
mesh/merge-gate out of date: judged against trunk f3875a6b6019, the trunk is now 0a1dc5f93fcb; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk f3875a6b6019, the trunk is now 0a1dc5f93fcb; judged again on a recheck or when it is merged
mesh/delivery stopped: the pull request closed unmerged
A consumer's credential file was composed with the owner the module declared, root when it declared
none. A module whose code is a bundle the node's runtime runs reads it as the operator's account, and
a root-owned 0600 file is one it cannot read: the lab cloned the forge with no credential. The grant
side was fixed the same way for issue 225; this is the consumer's side. A declared owner still wins.
2026-10-13 14:39:20 +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 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
28 changed files with 1490 additions and 190 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()
}
+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)
}
+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)
}
}
}
+21 -1
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
@@ -1742,6 +1751,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 {
+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)
}
}
+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>]"
@@ -0,0 +1,57 @@
package catalogue
import "testing"
// A consumer's credential is read by the consumer's own code, and that stopped being root for every
// module whose code is a bundle the node's runtime runs (novox/hq issue 624, the consumer's side of
// issue 225). The lab requires the package registry and reads its bound credential to clone the forge;
// the credential file was composed with no owner, so root's and 0600, and the lab — running as the
// operator's account — cloned with no credential and was refused, "could not read Username".
// aConsumerWithABundle is the lab's shape: tools in a bundle, a requirement, its credential in a file.
func aConsumerWithABundle() Manifest {
return Manifest{
Module: "lab",
Requires: []string{"npm-package-registry"},
Secrets: map[string]string{"npm-package-registry": "/var/lib/lab/package-registry.secret"},
Bundles: []Bundle{{Name: "code", Language: "typescript"}},
}
}
func consumerSecret(t *testing.T, m Manifest, account string) map[string]any {
t.Helper()
r := Resolution{Node: "shanks", Account: account, Modules: []Manifest{m},
Needs: []Needed{{Name: "npm-package-registry", For: m.Module, From: "novox", Sealed: "c2VhbGVk"}}}
out, err := r.Declaration(Rendering{})
if err != nil {
t.Fatal(err)
}
for _, res := range out {
if res["type"] == "file" && res["path"] == "/var/lib/lab/package-registry.secret" {
return res
}
}
t.Fatalf("no credential file was composed: %v", out)
return nil
}
func TestABundlesBoundCredentialBelongsToTheAccountItsRuntimeRunsAs(t *testing.T) {
if got := consumerSecret(t, aConsumerWithABundle(), "jochen")["owner"]; got != "jochen" {
t.Fatalf("the lab's credential belongs to %v; its tools run as %q, and a root-owned 0600 file is one "+
"they cannot read", got, "jochen")
}
}
// A module that names its secrets' owner keeps it, and one with no bundle and no owner stays root's.
func TestAConsumersDeclaredSecretsOwnerStillWins(t *testing.T) {
m := aConsumerWithABundle()
m.SecretsOwner = "65534:65534"
if got := consumerSecret(t, m, "jochen")["owner"]; got != "65534:65534" {
t.Errorf("a declared owner was replaced by %v", got)
}
m = aConsumerWithABundle()
m.Bundles = nil
if got, has := consumerSecret(t, m, "jochen")["owner"]; has {
t.Errorf("a container consumer's credential was given the owner %v", got)
}
}
+10 -1
View File
@@ -730,7 +730,16 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
// worse than none: something would read it and fail authenticating.
continue
}
first = append(first, ownedBy(m.SecretsOwner, map[string]any{
// **Read by the module's own code, so owned by whoever runs it** (novox/hq issue 624, the
// consumer's side of issue 225): a module whose code is a bundle the node's runtime runs reads
// its binding's credential as the operator's account, and a root-owned 0600 file is one it
// cannot read — the lab cloned the forge with no credential and said nothing about why. A
// module that names its secrets' owner keeps it.
owner := m.SecretsOwner
if owner == "" {
owner = r.provisionsAs(m)
}
first = append(first, ownedBy(owner, map[string]any{
"id": SecretID(SecretLocal(to, file.Local)), "type": "file", "path": file.Path,
"sealed": found.Sealed,
}))
+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)
}
}
}
+47 -4
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) {
@@ -1647,14 +1676,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 "+
-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())
}
}
+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)
}
}
}
}
+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.
+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.
+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