Merge pull request 'Say a part waiting for the operator as needs-operator and pass its gate; add secret families (issue 386, ADR 0283)' (#209) from feat/386-a-wait-for-the-operator into main

This commit was merged in pull request #209.
This commit is contained in:
2026-10-10 19:49:29 +00:00
15 changed files with 1060 additions and 19 deletions
+1 -1
View File
@@ -155,7 +155,7 @@ func (i *Inventory) ReplaceGivenAfterStart(ctx context.Context, node, declared s
}
// The definition is asked again now, not only when the value was given: one that has since
// said the value is an outside party's, or applied, keeps it as given, and the mark stays gone.
if own, ok := m.OwnSecrets[d.name]; !ok || !own.MeshMayMake() {
if own, _, ok := m.OwnSecrets.Lookup(d.name); !ok || !own.MeshMayMake() {
continue
}
if err := i.remakeOwn(ctx, d.nodeID, key, m, d.module, d.name); err != nil {
+11
View File
@@ -34,6 +34,17 @@ type ResourceHealth struct {
// Root is "never" on an account verdict that judged whether the account can become root without a
// person (novox/hq ADR 0266).
Root string `json:"root,omitempty"`
// Waits is what a waiting resource waits for the operator to give (novox/hq ADR 0283).
Waits []Wait `json:"waits,omitempty"`
}
// Wait is one thing a waiting resource waits for the operator to give: exactly one of Secret and Setting
// (novox/hq ADR 0283), as link.Wait carries it.
type Wait struct {
Part string `json:"part"`
Secret string `json:"secret,omitempty"`
Setting string `json:"setting,omitempty"`
What string `json:"what"`
}
// NodeHealth is a machine's newest statement, as kept.
+76
View File
@@ -0,0 +1,76 @@
package inventory
import (
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
)
// A member of a secret family is given by its full name, at the desk too, and a name outside the family is refused
// (novox/hq ADR 0283).
func TestAMemberIsGivableAtTheDeskAndANameOutsideTheFamilyIsNot(t *testing.T) {
m := catalogue.Manifest{Module: "mounts", OwnSecrets: catalogue.OwnSecrets{
"smb-password-*": {Path: "/s/smb-password-*.secret", IssuedBy: catalogue.IssuedOutside},
}}
if err := GivableAtDesk(m, "smb-password-games"); err != nil {
t.Fatalf("a member was refused: %v", err)
}
for _, name := range []string{"smb-password-*", "smb-password-", "smb-password-a/b", "smb-password-A", "smb-credentials"} {
err := GivableAtDesk(m, name)
if err == nil {
t.Errorf("%q was givable", name)
} else if !strings.Contains(err.Error(), "smb-password-<name>") {
t.Errorf("%q: the refusal does not say the family's form: %v", name, err)
}
}
}
// A member given is read back as given, sealed to the machine's key; the mesh makes none, so nothing else is.
func TestAMemberGivenIsReadAsGivenAndNothingElseIs(t *testing.T) {
inv := fresh(t)
ctx := t.Context()
node, err := inv.AddNode(ctx, "workstation")
if err != nil {
t.Fatal(err)
}
key, _ := aSealingKey(t)
if err := inv.RecordSealingKey(ctx, node.ID, key); err != nil {
t.Fatal(err)
}
m := catalogue.Manifest{Module: "mounts", Version: "1", OwnSecrets: catalogue.OwnSecrets{
"smb-password-*": {Path: "/s/smb-password-*.secret", IssuedBy: catalogue.IssuedOutside},
"token": {Path: "/s/token", IssuedBy: catalogue.IssuedOutside},
}}
if err := inv.RegisterModule(ctx, m, Source{}); err != nil {
t.Fatal(err)
}
if err := inv.AcceptSecretForModule(ctx, "workstation", "mounts", "smb-password-games", "hunter2"); err != nil {
t.Fatalf("a member was refused: %v", err)
}
if err := inv.AcceptSecretForModule(ctx, "workstation", "mounts", "smb-pass", "x"); err == nil {
t.Fatal("a name of no family was accepted")
}
members, err := inv.GivenMembers(ctx, "workstation", "mounts", "smb-password-*")
if err != nil {
t.Fatal(err)
}
if len(members) != 1 || members[0].Name != "smb-password-games" || !members[0].Current || members[0].Sealed == "" ||
strings.Contains(members[0].Sealed, "hunter2") {
t.Fatalf("members: %+v", members)
}
given, err := inv.GivenOwnSecrets(ctx, "workstation", "mounts")
if err != nil {
t.Fatal(err)
}
if _, was := given["smb-password-games"]; !was || len(given) != 1 {
t.Fatalf("given: %v", given)
}
// A value the mesh made is not one a person gave.
if _, err := inv.SecretForModule(ctx, "workstation", "mounts", "token"); err != nil {
t.Fatal(err)
}
if given, _ := inv.GivenOwnSecrets(ctx, "workstation", "mounts"); len(given) != 1 {
t.Fatalf("a value the mesh made counted as given: %v", given)
}
}
+77 -4
View File
@@ -7,6 +7,7 @@ import (
"slices"
"sort"
"strings"
"time"
"github.com/jackc/pgx/v5"
@@ -446,7 +447,7 @@ func (i *Inventory) acceptOwn(ctx context.Context, node, module, name, value str
if err != nil {
return false, err
}
own, declared := m.OwnSecrets[name]
own, _, declared := m.OwnSecrets.Lookup(name)
if !declared {
return false, fmt.Errorf("%s does not declare %q as an own secret; %s — a secret it requires from a provider is accepted with `--provider <node> [--local <name>]`, the value the running service already uses (novox/hq ADR 0163)", module, name, declaresOwn(m))
}
@@ -583,7 +584,7 @@ const BrokerSecret = "broker"
// in place of a value given (catalogue.OwnSecret.MeshMayMake). The desk takes only what a person holds and
// the mesh cannot make — a bot's token — so nobody is asked to type the mesh's own credential into a prompt.
func GivableAtDesk(m catalogue.Manifest, name string) error {
own, ok := m.OwnSecrets[name]
own, _, ok := m.OwnSecrets.Lookup(name)
if !ok {
return fmt.Errorf("%s does not declare %q as an own secret; %s", m.Module, name, declaresOwn(m))
}
@@ -602,7 +603,79 @@ func declaresOwn(m catalogue.Manifest) string {
if len(m.OwnSecrets) == 0 {
return "it declares no own secrets"
}
return "it declares: " + strings.Join(sortedNames(m.OwnSecrets.Paths()), ", ")
names := sortedNames(m.OwnSecrets.Plain().Paths())
// A family is said as its members are given (novox/hq ADR 0283): one per part, by its full name.
for _, f := range m.OwnSecrets.Families() {
names = append(names, catalogue.FamilyPrefix(f)+"<name> (one per part, issued outside the mesh)")
}
return "it declares: " + strings.Join(names, ", ")
}
// GivenMember is one member of a secret family given on a machine (novox/hq ADR 0283): its full name, its value
// sealed to the machine, and whether it is sealed to the key the machine holds now.
type GivenMember struct {
Name string
Sealed string
Current bool
}
// GivenMembers is every member of a module's secret family given on a machine, by name. The mesh never makes a
// member, so only a value a person gave is one; a machine with no sealing key holds none.
func (i *Inventory) GivenMembers(ctx context.Context, node, module, family string) ([]GivenMember, error) {
key, err := i.SealingKeyOf(ctx, node)
if err != nil || key == "" {
return nil, err
}
record, err := i.NodeByName(ctx, node)
if err != nil {
return nil, err
}
prefix := catalogue.FamilyPrefix(family)
rows, err := i.store.Pool().Query(ctx,
`select name, sealed, node_key from module_secret
where node = $1 and module = $2 and origin = 'accepted' and left(name, length($3)) = $3
order by name`, record.ID, module, prefix)
if err != nil {
return nil, err
}
defer rows.Close()
var out []GivenMember
for rows.Next() {
var g GivenMember
var against string
if err := rows.Scan(&g.Name, &g.Sealed, &against); err != nil {
return nil, err
}
g.Current = against == key
out = append(out, g)
}
return out, rows.Err()
}
// GivenOwnSecrets is every own secret of a module a person gave on a machine, by name, with when (novox/hq ADR
// 0283): what the controller checks a module's wait for a secret against. A value the mesh made is not one.
func (i *Inventory) GivenOwnSecrets(ctx context.Context, node, module string) (map[string]time.Time, error) {
record, err := i.NodeByName(ctx, node)
if err != nil {
return nil, err
}
rows, err := i.store.Pool().Query(ctx,
`select name, coalesce(made_at, now()) from module_secret where node = $1 and module = $2 and origin = 'accepted'`,
record.ID, module)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]time.Time{}
for rows.Next() {
var name string
var at time.Time
if err := rows.Scan(&name, &at); err != nil {
return nil, err
}
out[name] = at
}
return out, rows.Err()
}
func sortedNames(of map[string]string) []string {
@@ -645,7 +718,7 @@ func (i *Inventory) RotateModuleSecret(ctx context.Context, node, module, name s
if err != nil {
return err
}
own, declared := m.OwnSecrets[name]
own, _, declared := m.OwnSecrets.Lookup(name)
if !declared {
return fmt.Errorf("%s does not declare %q as an own secret; %s — a secret it requires from a provider is accepted with `--provider <node> [--local <name>]`, the value the running service already uses (novox/hq ADR 0163)", module, name, declaresOwn(m))
}