An assignment issues its bus credential, a push refuses one nobody issued, and what reads a secret restarts on it (hq issue 203)
`assign` recorded a module and `push` sealed a random own secret where its bus credential belongs; the process crash-looped until a person ran `module issue` and pushed again, and the only warning was one line in a list printed on every push. Now assigning a module that declares a broker secret issues the credential in the same act — kept when one exists, so re-assigning rotates nothing — and when the bus cannot be reached from here the assignment says which verb to run. A push never seals a placeholder in a credential's place: a module whose bus user is unminted is refused by name, with the verb. The control plane's own user is the installer's, seeded at genesis, which the test now says. And what reads one of a module's own secrets is restarted when it changes — composed for a container or daemon that names the secret's path in its volumes, environment or env-files, so a manifest need not say it: the build machine ran on an hour-old credential because its manifest restarted it on its environment file alone (issue 206). A scheduled or run-once process is left alone; it reads afresh.
This commit is contained in:
@@ -872,6 +872,15 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
||||
if renamed := reflectsRenamed(m.Module, resource["reload-on"]); renamed != nil {
|
||||
copied["reload-on"] = renamed
|
||||
}
|
||||
// **What reads one of this module's own secrets is restarted when it changes** (novox/hq
|
||||
// issue 203, issue 206). A credential is re-issued by the mesh, and a container that
|
||||
// mounted the old file keeps the old one open: the build machine ran for an hour on a
|
||||
// credential the mesh had replaced, because its manifest restarted it on its
|
||||
// environment file and nobody had thought to name the credential too. Composed here so
|
||||
// no manifest has to say it, for a container or a daemon that names the secret's path.
|
||||
if reads := secretsReadBy(copied, m); len(reads) > 0 {
|
||||
copied["restart-on"] = withRestartOn(copied["restart-on"], reads)
|
||||
}
|
||||
// **A version prepares its state before it runs** (novox/hq ADR 0135). Derived from the
|
||||
// module's own resource rather than declared beside it: what prepares the state is the
|
||||
// module's own code, so what it is given has to be what that code is given — and a
|
||||
@@ -2079,3 +2088,87 @@ func portOfEndpoint(values map[string]any, ports map[string]int) {
|
||||
values["port"] = port
|
||||
}
|
||||
}
|
||||
|
||||
// secretsReadBy is the file resources of this module's own secrets that a container or a daemon reads
|
||||
// — named in its volumes, its environment or its env-files by the secret's placed path — as
|
||||
// restart-on ids. Nothing for other shapes, and nothing for a scheduled or run-once process, which
|
||||
// the host refuses a restart-on for (it runs again anyway, and reads the file afresh).
|
||||
func secretsReadBy(resource map[string]any, m Manifest) []string {
|
||||
kind := fmt.Sprint(resource["type"])
|
||||
if kind != "container" && kind != "process" {
|
||||
return nil
|
||||
}
|
||||
if resource["schedule"] != nil || resource["run-once"] == true {
|
||||
return nil
|
||||
}
|
||||
var mentioned []string
|
||||
for _, key := range []string{"volumes", "env", "env-file"} {
|
||||
mentioned = append(mentioned, stringsIn(resource[key])...)
|
||||
}
|
||||
var out []string
|
||||
for _, name := range sortedKeys(m.OwnSecrets) {
|
||||
path := m.OwnSecrets[name].Path
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
for _, s := range mentioned {
|
||||
// A volume is `source:destination[:mode]`; an env value or an env-file is the path itself.
|
||||
if s == path || strings.HasPrefix(s, path+":") {
|
||||
out = append(out, m.Module+"."+NeedID(name))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// stringsIn is every string in a list or a map's values; nothing for anything else.
|
||||
func stringsIn(v any) []string {
|
||||
switch x := v.(type) {
|
||||
case []any:
|
||||
var out []string
|
||||
for _, item := range x {
|
||||
if s, ok := item.(string); ok {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
case []string:
|
||||
return x
|
||||
case map[string]any:
|
||||
var out []string
|
||||
for _, k := range sortedKeys(x) {
|
||||
if s, ok := x[k].(string); ok {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
case map[string]string:
|
||||
var out []string
|
||||
for _, k := range sortedKeys(x) {
|
||||
out = append(out, x[k])
|
||||
}
|
||||
return out
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// withRestartOn is a resource's restart-on list with these ids added once each.
|
||||
func withRestartOn(have any, add []string) []any {
|
||||
var out []any
|
||||
seen := map[string]bool{}
|
||||
for _, id := range reflectsRenamed("", have) {
|
||||
s := fmt.Sprint(id)
|
||||
if !seen[s] {
|
||||
seen[s] = true
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
for _, id := range add {
|
||||
if !seen[id] {
|
||||
seen[id] = true
|
||||
out = append(out, id)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user