Feature 57 (config and provisioning): release mesh-controller, the reviewed release branch merged with main, on top of main
This commit is contained in:
+169
-3
@@ -19,6 +19,8 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
)
|
||||
|
||||
// Version is the vocabulary this host speaks. A declaration naming any other version is
|
||||
@@ -205,6 +207,12 @@ type File struct {
|
||||
// by looking rather than by knowing which field won.
|
||||
Sealed string `json:"sealed,omitempty"`
|
||||
|
||||
// VaultSealed is content the vault sealed to this node's sealing key, in the mesh's sealed box (novox/hq ADR
|
||||
// 0206): a core module's own credential, which the controller carries in the declaration and cannot open
|
||||
// (ADR 0349 §16, issue 755). The same promise as Sealed, from the vault rather than the controller; sent only to
|
||||
// an engine that states VaultBoxContract. Exclusive with Content, Sealed and Bytes.
|
||||
VaultSealed *seal.Box `json:"vault-sealed,omitempty"`
|
||||
|
||||
// Trusted is the catalogue's word on whether the settings this file's content took are trusted (novox/hq
|
||||
// issue 339). It is the controller's to act on, and a controller takes it out before it sends a declaration.
|
||||
// Accepted here, and nothing is done with it, so that a controller that passes it on — an older one, or one
|
||||
@@ -237,11 +245,48 @@ type File struct {
|
||||
|
||||
// Owner is the user this belongs to, by name. Absent means root.
|
||||
Owner string `json:"owner,omitempty"`
|
||||
|
||||
// OwnerAtRuntime says who owns the file once it is placed (novox/hq ADR 0349 §5): "owned", the mesh's
|
||||
// whole, written back whenever it differs; "seed", written once and then the program's; or "managed",
|
||||
// only its named keys or its marked block the mesh's, the program keeping the rest.
|
||||
//
|
||||
// **Three words for what the forms above already do.** Owned is an ordinary file, seed is create-once
|
||||
// and managed is a file written into, so the word changes no write: a file that says it is applied
|
||||
// exactly as its old form is, and one that does not say is owned by its old form (Ownership). The word
|
||||
// is what the provisioner's files say of themselves, which the old forms only implied. Seed is enough
|
||||
// alone; managed still needs `into`, which says the format. A word that contradicts the form is refused.
|
||||
OwnerAtRuntime string `json:"owner-at-runtime,omitempty"`
|
||||
}
|
||||
|
||||
// The words a file says who owns it at runtime with (novox/hq ADR 0349 §5).
|
||||
const (
|
||||
RuntimeOwned = "owned"
|
||||
RuntimeSeed = "seed"
|
||||
RuntimeManaged = "managed"
|
||||
)
|
||||
|
||||
// Ownership is who owns the file at runtime: the word it says, or the one its old form implies — a file
|
||||
// written into is managed, one created once is a seed, and any other is owned.
|
||||
func (f *File) Ownership() string {
|
||||
switch {
|
||||
case f.OwnerAtRuntime != "":
|
||||
return f.OwnerAtRuntime
|
||||
case f.Into != "":
|
||||
return RuntimeManaged
|
||||
case f.CreateOnce:
|
||||
return RuntimeSeed
|
||||
default:
|
||||
return RuntimeOwned
|
||||
}
|
||||
}
|
||||
|
||||
// Seeded reports whether the file is written only when absent: created once, by create-once or by the
|
||||
// word seed.
|
||||
func (f *File) Seeded() bool { return f.CreateOnce || f.OwnerAtRuntime == RuntimeSeed }
|
||||
|
||||
// Secret reports whether this file arrived sealed, which is what decides both that it must be
|
||||
// opened before writing and that its contents must never appear in a report.
|
||||
func (f *File) Secret() bool { return f.Sealed != "" }
|
||||
func (f *File) Secret() bool { return f.Sealed != "" || f.VaultSealed != nil }
|
||||
|
||||
func (f *File) Identity() string { return f.ID }
|
||||
func (f *File) Kind() Type { return TypeFile }
|
||||
@@ -314,12 +359,13 @@ func (f *File) validate(where string, _ bool) []string {
|
||||
where, f.At))
|
||||
}
|
||||
if f.Into == IntoJSON || f.Into == IntoBlock {
|
||||
if f.Sealed != "" || f.Bytes != "" || len(f.Secrets) > 0 || f.CreateOnce {
|
||||
if f.Secret() || f.Bytes != "" || len(f.Secrets) > 0 || f.CreateOnce {
|
||||
problems = append(problems, where+
|
||||
": a file written into says only its part, in content — not sealed, bytes, "+
|
||||
"secrets or create-once")
|
||||
}
|
||||
}
|
||||
problems = append(problems, f.checkOwnership(where)...)
|
||||
var said []string
|
||||
for name, value := range map[string]string{
|
||||
"content": f.Content, "sealed": f.Sealed, "bytes": f.Bytes,
|
||||
@@ -328,6 +374,16 @@ func (f *File) validate(where string, _ bool) []string {
|
||||
said = append(said, name)
|
||||
}
|
||||
}
|
||||
if f.VaultSealed != nil {
|
||||
said = append(said, "vault-sealed")
|
||||
if f.VaultSealed.V != seal.Version {
|
||||
problems = append(problems, fmt.Sprintf("%s: vault-sealed is a sealed box of version %d, and this "+
|
||||
"node-engine opens version %d", where, f.VaultSealed.V, seal.Version))
|
||||
}
|
||||
if len(f.Secrets) > 0 {
|
||||
problems = append(problems, where+": a file the vault sealed whole has no holes for secrets")
|
||||
}
|
||||
}
|
||||
if len(said) > 1 {
|
||||
sort.Strings(said)
|
||||
problems = append(problems, where+
|
||||
@@ -361,6 +417,42 @@ func (f *File) validate(where string, _ bool) []string {
|
||||
return append(problems, checkMode(where, f.Mode)...)
|
||||
}
|
||||
|
||||
// checkOwnership refuses a word that contradicts the file's form (novox/hq ADR 0349 §5): owned is a whole
|
||||
// file written back, so not created once nor written into; a seed is a whole file created once, so not
|
||||
// written into; managed is a file written into, which says its format. Managed and created once together
|
||||
// is already refused by the rule that a file written into is not created once.
|
||||
func (f *File) checkOwnership(where string) []string {
|
||||
switch f.OwnerAtRuntime {
|
||||
case "":
|
||||
case RuntimeOwned:
|
||||
var also []string
|
||||
if f.CreateOnce {
|
||||
also = append(also, "create-once")
|
||||
}
|
||||
if f.Into != "" {
|
||||
also = append(also, "into")
|
||||
}
|
||||
if len(also) > 0 {
|
||||
return []string{fmt.Sprintf("%s: owned at runtime is the mesh's whole file, written back whenever it "+
|
||||
"differs, and this also says %s", where, strings.Join(also, " and "))}
|
||||
}
|
||||
case RuntimeSeed:
|
||||
if f.Into != "" {
|
||||
return []string{where + ": a seed is a whole file written once, and this says into; a file the " +
|
||||
"program shares with the mesh is managed"}
|
||||
}
|
||||
case RuntimeManaged:
|
||||
if f.Into == "" {
|
||||
return []string{where + ": a managed file names the format its keys or block are written into, " +
|
||||
"with into \"json\" or \"block\""}
|
||||
}
|
||||
default:
|
||||
return []string{fmt.Sprintf("%s: owner-at-runtime %q; a placed file is %q, %q or %q, or omits it to be "+
|
||||
"owned by its form", where, f.OwnerAtRuntime, RuntimeOwned, RuntimeSeed, RuntimeManaged)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// User is a login on the machine.
|
||||
//
|
||||
// The thing that makes a shell, a chat client or a desktop expressible at all: each is a package
|
||||
@@ -1565,6 +1657,71 @@ type Declaration struct {
|
||||
// this is its home, whether the operator's warrant to take it was carried out, the sub-paths its modules ask for,
|
||||
// and the tool runner's account to put in its group (§11). Absent is a controller older than ADR 0328.
|
||||
Libraries []Library
|
||||
|
||||
// Filled is the paths of the files the provisioner fills on this machine (novox/hq ADR 0349 §4, §5): each a
|
||||
// file this declaration declares, which says who owns it at runtime. The provisioner's client has the
|
||||
// node-engine write only these. Absent is a controller that hands no file to the provisioner, and every
|
||||
// file is written from the declaration as before.
|
||||
Filled []string
|
||||
|
||||
// Provisioner is the account the provisioner's client runs as on this machine, by name (novox/hq ADR 0349 §4):
|
||||
// the only account whose write of a filled file the node-engine's local write socket accepts. Absent is a
|
||||
// controller that names none, and every such write is refused.
|
||||
Provisioner string
|
||||
}
|
||||
|
||||
// checkProvisioner refuses a provisioner client's account that is no account name, or is root: the account whose
|
||||
// writes the node-engine accepts must be one that could not write the file without it.
|
||||
func checkProvisioner(name string) []string {
|
||||
switch {
|
||||
case name == "":
|
||||
return nil
|
||||
case !accountName.MatchString(name):
|
||||
return []string{fmt.Sprintf("provisioner_account %q is not an account name", name)}
|
||||
case name == "root":
|
||||
return []string{"provisioner_account is root; the provisioner's client runs as an account of its own"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fills reports whether the provisioner fills the file at path on this machine.
|
||||
func (d *Declaration) Fills(path string) bool { return slices.Contains(d.Filled, path) }
|
||||
|
||||
// checkFilled refuses a filled path that is not a declared file saying who owns it at runtime, or that is named
|
||||
// twice or empty. Every file declared at the path must say it: the provisioner writes the path, and a file there
|
||||
// the mesh writes some other way would be a second writer of it (ADR 0349 §5).
|
||||
func checkFilled(filled []string, resources []Resource) []string {
|
||||
var problems []string
|
||||
files := map[string][]*File{}
|
||||
for _, r := range resources {
|
||||
if f, ok := r.(*File); ok {
|
||||
files[f.Path] = append(files[f.Path], f)
|
||||
}
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, path := range filled {
|
||||
switch {
|
||||
case path == "":
|
||||
problems = append(problems, "filled_files names a file with no path")
|
||||
continue
|
||||
case seen[path]:
|
||||
problems = append(problems, fmt.Sprintf("filled_files names %q twice", path))
|
||||
continue
|
||||
}
|
||||
seen[path] = true
|
||||
if len(files[path]) == 0 {
|
||||
problems = append(problems, fmt.Sprintf("filled_files names %q, and this declaration declares no "+
|
||||
"file there", path))
|
||||
continue
|
||||
}
|
||||
for _, f := range files[path] {
|
||||
if f.OwnerAtRuntime == "" {
|
||||
problems = append(problems, fmt.Sprintf("resource %q: the provisioner fills %q, and a file it "+
|
||||
"fills says who owns it at runtime with owner-at-runtime", f.ID, path))
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// Identity is one module's identity, as the controller allocated it (ADR 0328 §4): its user `mesh-<module>` with
|
||||
@@ -1904,6 +2061,12 @@ type envelope struct {
|
||||
// sends them only to a host whose health says link.LibraryContract.
|
||||
Groups []Group `json:"groups,omitempty"`
|
||||
Libraries []Library `json:"libraries,omitempty"`
|
||||
// Filled is optional too (novox/hq ADR 0349 §5), as is a file's `owner-at-runtime`. **An older host refuses
|
||||
// these keys**; a controller sends them only to a host whose health says link.OwnershipContract.
|
||||
Filled []string `json:"filled_files,omitempty"`
|
||||
// Provisioner is optional too (novox/hq ADR 0349 §4). **An older host refuses this key**; a controller sends it
|
||||
// only to a host whose health says link.ProvisionerContract.
|
||||
Provisioner string `json:"provisioner_account,omitempty"`
|
||||
}
|
||||
|
||||
func parse(raw []byte, allowActions bool) (*Declaration, error) {
|
||||
@@ -1922,8 +2085,10 @@ func parse(raw []byte, allowActions bool) (*Declaration, error) {
|
||||
|
||||
d := &Declaration{Version: env.Version, For: env.For, Adoption: env.Adoption, Sequence: env.Sequence,
|
||||
Epoch: env.Epoch, LeftOut: env.LeftOut, Generation: env.Generation, PutBack: env.PutBack,
|
||||
Identities: env.Identities, Groups: env.Groups, Libraries: env.Libraries}
|
||||
Identities: env.Identities, Groups: env.Groups, Libraries: env.Libraries, Filled: env.Filled,
|
||||
Provisioner: env.Provisioner}
|
||||
problems := checkIdentities(env.Identities, env.Groups)
|
||||
problems = append(problems, checkProvisioner(env.Provisioner)...)
|
||||
problems = append(problems, checkLibraries(env.Groups, env.Libraries)...)
|
||||
if env.Generation < 0 {
|
||||
// Below zero would read as "none claimed" and pass every refusal of an older generation.
|
||||
@@ -2015,6 +2180,7 @@ func parse(raw []byte, allowActions bool) (*Declaration, error) {
|
||||
problems = append(problems, checkWhileStopped(d.Resources)...)
|
||||
problems = append(problems, checkAdoption(env.Adoption, d.Resources, allowActions)...)
|
||||
problems = append(problems, checkReplaces(d.Resources)...)
|
||||
problems = append(problems, checkFilled(env.Filled, d.Resources)...)
|
||||
if env.Adoption == nil {
|
||||
for _, r := range d.Resources {
|
||||
if r.Kind() == TypeOpening {
|
||||
|
||||
Reference in New Issue
Block a user