Say a part waiting for the operator as needs-operator and pass its gate; add secret families (issue 386, hq ADR 0283)
A module waiting for the operator's secret failed its first-node gate, held every later walk and was said as 'nothing for you to do'. The node-engine's new waiting state is checked against the manifest and the secrets given, read by the gate as a wait for a person, and raised as needs-operator naming the act. A secret family gives each part its own one-line secret, which the mesh never makes, so the desk prompt can take each password.
This commit is contained in:
@@ -606,7 +606,18 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
||||
"mode": "0600",
|
||||
})
|
||||
}
|
||||
for _, name := range sortedKeys(m.OwnSecrets) {
|
||||
// **A secret family's members, as given** (novox/hq ADR 0283): one file each at the family's path, and
|
||||
// nothing for a member not given — the mesh never makes one, and the module says it waits for it.
|
||||
for _, name := range sortedKeys(with.Needed[m.Module]) {
|
||||
own, family, ok := m.OwnSecrets.Lookup(name)
|
||||
if !ok || family == "" || with.Needed[m.Module][name] == "" {
|
||||
continue
|
||||
}
|
||||
first = append(first, ownedBy(m.SecretsOwner, map[string]any{
|
||||
"id": NeedID(name), "type": "file", "path": own.Path, "sealed": with.Needed[m.Module][name],
|
||||
}))
|
||||
}
|
||||
for _, name := range sortedKeys(m.OwnSecrets.Plain()) {
|
||||
sealed := with.Needed[m.Module][name]
|
||||
if sealed == "" && with.Foreseen[m.Module][name] {
|
||||
// Not made, and the next send makes it: composed with a stand-in so that whatever
|
||||
@@ -2385,7 +2396,7 @@ func secretsReadBy(resource map[string]any, m Manifest) []string {
|
||||
mentioned = append(mentioned, stringsIn(resource[key])...)
|
||||
}
|
||||
var out []string
|
||||
for _, name := range sortedKeys(m.OwnSecrets) {
|
||||
for _, name := range sortedKeys(m.OwnSecrets.Plain()) {
|
||||
path := m.OwnSecrets[name].Path
|
||||
if path == "" {
|
||||
continue
|
||||
|
||||
@@ -1942,6 +1942,37 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
problems = append(problems, whileStoppedProblems(m, r, hasSchedule(r))...)
|
||||
}
|
||||
for name, own := range m.OwnSecrets {
|
||||
// **A family is issued outside the mesh, and lands one file per member** (novox/hq ADR 0283): the mesh
|
||||
// never makes a member, so a family the mesh may make would be one nothing ever fills.
|
||||
if IsFamily(name) {
|
||||
if !memberRest.MatchString(strings.TrimSuffix(FamilyPrefix(name), "-")) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s declares the secret family %q, whose prefix is not a name", m.Module, name))
|
||||
}
|
||||
if own.IssuedBy != IssuedOutside {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s declares the secret family %q without \"issued-by\": %q; the mesh never makes a "+
|
||||
"member of a family, so only a party outside the mesh can fill one (novox/hq ADR 0283)",
|
||||
m.Module, name, IssuedOutside))
|
||||
}
|
||||
if strings.Count(own.Path, "*") != 1 {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s keeps the secret family %q at %q, which does not hold exactly one *: each member lands "+
|
||||
"where the * is replaced by its name (novox/hq ADR 0283)", m.Module, name, own.Path))
|
||||
}
|
||||
for other := range m.OwnSecrets {
|
||||
if other != name && !IsFamily(other) {
|
||||
if rest := strings.TrimPrefix(other, FamilyPrefix(name)); rest != other && memberRest.MatchString(rest) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s declares the secret %q, which is also a member of its family %q — a name names one",
|
||||
m.Module, other, name))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(name, "*") {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s declares the secret %q: a * only ends a family's name, as \"<prefix>-*\"", m.Module, name))
|
||||
}
|
||||
if !placedOrAbsolute(own.Path) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s needs %q at %q, which is neither an absolute path nor a placed one", m.Module, name, own.Path))
|
||||
@@ -2549,6 +2580,69 @@ func (o OwnSecrets) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(entries)
|
||||
}
|
||||
|
||||
// A secret family (novox/hq ADR 0283): an own secret declared under a name ending in FamilySuffix is one
|
||||
// member per part the module's settings name — `smb-password-*` holds `smb-password-games`,
|
||||
// `smb-password-library` — each given by its full name, placed at the family's path with its one `*` replaced
|
||||
// by what follows the prefix. The mesh never makes a member: a family is issued outside the mesh, and a member
|
||||
// not given is no file.
|
||||
const FamilySuffix = "-*"
|
||||
|
||||
// memberRest is what may follow a family's prefix: a name's characters.
|
||||
var memberRest = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
|
||||
|
||||
// IsFamily says an own secret's declared name is a family's.
|
||||
func IsFamily(name string) bool { return strings.HasSuffix(name, FamilySuffix) }
|
||||
|
||||
// FamilyPrefix is what every member of a family begins with: the declared name without its `*`.
|
||||
func FamilyPrefix(family string) string { return strings.TrimSuffix(family, "*") }
|
||||
|
||||
// Lookup resolves an own secret by the name it is given under: declared by that name, or a member of a family
|
||||
// (the longest prefix that fits), with the family's path filled for the member. family is the family's
|
||||
// declared name, or "" for a secret declared by name.
|
||||
func (o OwnSecrets) Lookup(name string) (s OwnSecret, family string, ok bool) {
|
||||
if s, ok := o[name]; ok && !IsFamily(name) {
|
||||
return s, "", true
|
||||
}
|
||||
for declared, f := range o {
|
||||
if !IsFamily(declared) {
|
||||
continue
|
||||
}
|
||||
prefix := FamilyPrefix(declared)
|
||||
rest := strings.TrimPrefix(name, prefix)
|
||||
if !strings.HasPrefix(name, prefix) || !memberRest.MatchString(rest) {
|
||||
continue
|
||||
}
|
||||
if family == "" || len(declared) > len(family) {
|
||||
s, family, ok = OwnSecret{Path: strings.Replace(f.Path, "*", rest, 1), Taken: f.Taken, IssuedBy: f.IssuedBy}, declared, true
|
||||
}
|
||||
}
|
||||
return s, family, ok
|
||||
}
|
||||
|
||||
// Plain is every own secret declared by its own name: what the mesh makes when not given, and places by
|
||||
// name. A family is not one, and is never made.
|
||||
func (o OwnSecrets) Plain() OwnSecrets {
|
||||
out := make(OwnSecrets, len(o))
|
||||
for name, s := range o {
|
||||
if !IsFamily(name) {
|
||||
out[name] = s
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Families is every family's declared name, sorted.
|
||||
func (o OwnSecrets) Families() []string {
|
||||
var out []string
|
||||
for name := range o {
|
||||
if IsFamily(name) {
|
||||
out = append(out, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Paths is each own secret's path by name — the shape every placement and file walk reads.
|
||||
func (o OwnSecrets) Paths() map[string]string {
|
||||
out := make(map[string]string, len(o))
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A secret family (novox/hq ADR 0283): one own secret per part the settings name, issued outside the mesh, each
|
||||
// given by its full name, never made by the mesh.
|
||||
|
||||
func familyManifest(t *testing.T, ownSecrets string) (Manifest, error) {
|
||||
t.Helper()
|
||||
return ParseManifest([]byte(`{"module":"mounts","version":"1","own-secrets":{` + ownSecrets + `}}`))
|
||||
}
|
||||
|
||||
func TestAFamilyIsDeclaredIssuedOutsideWithOneStar(t *testing.T) {
|
||||
m, err := familyManifest(t, `"smb-password-*":{"path":"/var/lib/mounts/smb-password-*.secret","issued-by":"outside"}`)
|
||||
if err != nil {
|
||||
t.Fatalf("a well-formed family was refused: %v", err)
|
||||
}
|
||||
if got := m.OwnSecrets.Families(); len(got) != 1 || got[0] != "smb-password-*" {
|
||||
t.Fatalf("families: %v", got)
|
||||
}
|
||||
if len(m.OwnSecrets.Plain()) != 0 {
|
||||
t.Fatalf("a family counted as a secret declared by name: %v", m.OwnSecrets.Plain())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAMalformedFamilyIsRefused(t *testing.T) {
|
||||
for name, c := range map[string]struct{ own, says string }{
|
||||
"made by the mesh": {`"smb-password-*":{"path":"/s/smb-password-*.secret","taken":"at-start"}`, "issued-by"},
|
||||
"no star in its path": {`"smb-password-*":{"path":"/s/smb-password.secret","issued-by":"outside"}`,
|
||||
"exactly one *"},
|
||||
"two stars in its path": {`"smb-password-*":{"path":"/s/*/smb-password-*.secret","issued-by":"outside"}`,
|
||||
"exactly one *"},
|
||||
"a star inside a name": {`"smb*password":{"path":"/s/x","issued-by":"outside"}`, "only ends a family"},
|
||||
"a name that is also a member": {`"smb-password-*":{"path":"/s/p-*","issued-by":"outside"},
|
||||
"smb-password-games":{"path":"/s/games","issued-by":"outside"}`, "also a member"},
|
||||
} {
|
||||
if _, err := familyManifest(t, c.own); err == nil || !strings.Contains(err.Error(), c.says) {
|
||||
t.Errorf("%s: %v; want a refusal saying %q", name, err, c.says)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAMemberIsFoundByItsFullNameAndLandsWhereTheStarIs(t *testing.T) {
|
||||
o := OwnSecrets{
|
||||
"smb-password-*": {Path: "/s/smb-password-*.secret", IssuedBy: IssuedOutside},
|
||||
"smb-password-big-*": {Path: "/big/*.secret", IssuedBy: IssuedOutside},
|
||||
"token": {Path: "/s/token", IssuedBy: IssuedOutside},
|
||||
}
|
||||
s, family, ok := o.Lookup("smb-password-games")
|
||||
if !ok || family != "smb-password-*" || s.Path != "/s/smb-password-games.secret" || s.IssuedBy != IssuedOutside {
|
||||
t.Fatalf("a member: %+v %q %v", s, family, ok)
|
||||
}
|
||||
if s, family, ok := o.Lookup("smb-password-big-one"); !ok || family != "smb-password-big-*" || s.Path != "/big/one.secret" {
|
||||
t.Fatalf("the longest family that fits: %+v %q %v", s, family, ok)
|
||||
}
|
||||
if s, family, ok := o.Lookup("token"); !ok || family != "" || s.Path != "/s/token" {
|
||||
t.Fatalf("a secret declared by name: %+v %q %v", s, family, ok)
|
||||
}
|
||||
for _, name := range []string{"smb-password-*", "smb-password-", "smb-password-../etc", "smb-password-a/b",
|
||||
"smb-password-a.b", "smb-password-Games", "smb-password--x", "other"} {
|
||||
if _, _, ok := o.Lookup(name); ok {
|
||||
t.Errorf("%q was found as a secret", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A member given is one file at its path; one not given is nothing, and the machine still composes — the mesh never
|
||||
// makes a member.
|
||||
func TestAMemberGivenIsPlacedAndOneNotGivenIsNothing(t *testing.T) {
|
||||
m, err := familyManifest(t, `"smb-password-*":{"path":"/var/lib/mounts/smb-password-*.secret","issued-by":"outside"}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := Resolution{Node: "workstation", Modules: []Manifest{m}}
|
||||
out, err := r.Declaration(Rendering{Needed: map[string]map[string]string{"mounts": {"smb-password-games": "sealed"}}})
|
||||
if err != nil {
|
||||
t.Fatalf("a module with one member given did not compose: %v", err)
|
||||
}
|
||||
var paths []string
|
||||
for _, res := range out {
|
||||
if res["sealed"] != nil {
|
||||
paths = append(paths, res["path"].(string))
|
||||
}
|
||||
}
|
||||
if len(paths) != 1 || paths[0] != "/var/lib/mounts/smb-password-games.secret" {
|
||||
t.Fatalf("placed: %v", paths)
|
||||
}
|
||||
out, err = r.Declaration(Rendering{})
|
||||
if err != nil {
|
||||
t.Fatalf("a module with no member given did not compose: %v", err)
|
||||
}
|
||||
for _, res := range out {
|
||||
if res["sealed"] != nil {
|
||||
t.Fatalf("a member nobody gave was placed: %v", res)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func secretsUsed(content string) []string {
|
||||
// name would end that, to save writing a file.
|
||||
func sealedFor(m Manifest, needs []Needed, with Rendering) (map[string]string, error) {
|
||||
sealed := map[string]string{}
|
||||
for name := range m.OwnSecrets {
|
||||
for name := range m.OwnSecrets.Plain() {
|
||||
if value := with.Needed[m.Module][name]; value != "" {
|
||||
sealed[name] = value
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -556,8 +556,21 @@ const (
|
||||
StateStarting = "starting"
|
||||
StateHeld = "held"
|
||||
StateUnknown = "unknown"
|
||||
// StateWaiting is a resource whose module's tool check says it waits for the operator: a named own secret
|
||||
// or setting not given yet (novox/hq ADR 0283). Not a fault; the controller checks the wait before it
|
||||
// excuses it.
|
||||
StateWaiting = "waiting"
|
||||
)
|
||||
|
||||
// Wait is one thing a waiting resource waits for the operator to give: exactly one of Secret and Setting, the
|
||||
// part that waits and what the operator gives, in words (novox/hq ADR 0283; mesh-sdk go/health's shape).
|
||||
type Wait struct {
|
||||
Part string `json:"part"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
Setting string `json:"setting,omitempty"`
|
||||
What string `json:"what"`
|
||||
}
|
||||
|
||||
// ResourceHealth is one long-running resource's state.
|
||||
type ResourceHealth struct {
|
||||
Module string `json:"module"`
|
||||
@@ -581,6 +594,9 @@ type ResourceHealth struct {
|
||||
// without a person (novox/hq ADR 0266): the engine judged that too, and a healthy verdict says it cannot.
|
||||
// Empty from an engine older than RootContract, and on every other verdict.
|
||||
Root string `json:"root,omitempty"`
|
||||
// Waits is what a resource in StateWaiting waits for (novox/hq ADR 0283). Empty otherwise, and from an
|
||||
// engine older than that.
|
||||
Waits []Wait `json:"waits,omitempty"`
|
||||
}
|
||||
|
||||
// HealthSaid is the health event's body: the machine and its statement. The machine is read from the
|
||||
|
||||
Reference in New Issue
Block a user