Merge pull request 'L3: the seat mesh-catalog's contract and a merged message with no trees; no graph code in mesh-sdk (ADR 0603, ADR 0604, issue 1367)' (#165) from feat/l3b-bus into main
This commit was merged in pull request #165.
This commit is contained in:
@@ -30,6 +30,9 @@ func ControllerLeftoverConsumersOn(nodes []string) []string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// endedController is the name the ended controller had as a module: its buckets are named for it (ADR 0590 §6).
|
||||||
|
const endedController = "mesh-controller"
|
||||||
|
|
||||||
// ControllerLeftoverBuckets are the ended controller's buckets, each by its local name (`KV_mesh-controller_<name>`).
|
// ControllerLeftoverBuckets are the ended controller's buckets, each by its local name (`KV_mesh-controller_<name>`).
|
||||||
var ControllerLeftoverBuckets = []string{"asked", "assignments", "builds", "calls", "checks-running", "composed",
|
var ControllerLeftoverBuckets = []string{"asked", "assignments", "builds", "calls", "checks-running", "composed",
|
||||||
"condition-history", "conditions", "declared-config", "declared-packages", "delivery-merges", "grants", "hand-acts",
|
"condition-history", "conditions", "declared-config", "declared-packages", "delivery-merges", "grants", "hand-acts",
|
||||||
@@ -40,7 +43,7 @@ var ControllerLeftoverBuckets = []string{"asked", "assignments", "builds", "call
|
|||||||
func ControllerLeftoverStreams(nodes []string) []string {
|
func ControllerLeftoverStreams(nodes []string) []string {
|
||||||
out := []string{"CONTROL", AssignmentsStream}
|
out := []string{"CONTROL", AssignmentsStream}
|
||||||
for _, b := range ControllerLeftoverBuckets {
|
for _, b := range ControllerLeftoverBuckets {
|
||||||
out = append(out, "KV_"+BucketName(catalogue.EndedControllerName, b))
|
out = append(out, "KV_"+BucketName(endedController, b))
|
||||||
}
|
}
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
if safeSubject.MatchString(n) {
|
if safeSubject.MatchString(n) {
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package catalogue
|
package catalogue
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"go/ast"
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -18,6 +22,7 @@ import (
|
|||||||
// design 28 task 5.4, not converted.
|
// design 28 task 5.4, not converted.
|
||||||
func TestNoCatalogueManifestNamesAmqp(t *testing.T) {
|
func TestNoCatalogueManifestNamesAmqp(t *testing.T) {
|
||||||
modules, err := filepath.Glob(filepath.Join(beside.Catalogue(t), "*", "module.json"))
|
modules, err := filepath.Glob(filepath.Join(beside.Catalogue(t), "*", "module.json"))
|
||||||
|
modules0 := modules
|
||||||
if err != nil || len(modules) == 0 {
|
if err != nil || len(modules) == 0 {
|
||||||
t.Fatalf("no manifests in the catalogue, so this proved nothing: %v", err)
|
t.Fatalf("no manifests in the catalogue, so this proved nothing: %v", err)
|
||||||
}
|
}
|
||||||
@@ -31,4 +36,44 @@ func TestNoCatalogueManifestNamesAmqp(t *testing.T) {
|
|||||||
filepath.Base(filepath.Dir(path)))
|
filepath.Base(filepath.Dir(path)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Nor does this package's code name any module the catalogue holds (ADR 0601 §4): what a module is, its manifest
|
||||||
|
// says. The names are read from the catalogue's module directories, never from a list here; every string literal of
|
||||||
|
// the package's non-test files is read, and a module's name is refused unless it is also a seat's name or one of the
|
||||||
|
// words below, which name something else.
|
||||||
|
t.Run("the package names no module", func(t *testing.T) {
|
||||||
|
modules := map[string]bool{}
|
||||||
|
for _, path := range modules0 {
|
||||||
|
modules[filepath.Base(filepath.Dir(path))] = true
|
||||||
|
}
|
||||||
|
// Words that are a module's name and name something else here: a language, an environment's rendering, a package.
|
||||||
|
elsewhere := map[string]bool{"go": true, "systemd": true, "nftables": true}
|
||||||
|
fset := token.NewFileSet()
|
||||||
|
files, _ := filepath.Glob("*.go")
|
||||||
|
for _, f := range files {
|
||||||
|
// kept_until_delivered.go keeps, deprecated, the names removed while running parts still use them; the second
|
||||||
|
// group removes it and this exemption together.
|
||||||
|
if strings.HasSuffix(f, "_test.go") || f == "kept_until_delivered.go" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parsed, err := parser.ParseFile(fset, f, nil, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ast.Inspect(parsed, func(n ast.Node) bool {
|
||||||
|
lit, ok := n.(*ast.BasicLit)
|
||||||
|
if !ok || lit.Kind != token.STRING {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
word, err := strconv.Unquote(lit.Value)
|
||||||
|
if err != nil || !modules[word] || elsewhere[word] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, seat := SeatNamed(word); seat {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
t.Errorf("%s names the module %s", fset.Position(lit.Pos()), word)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
// CatalogSeat is the seat of the catalog: what the mesh holds of every module (novox/hq ADR 0604 §1). Its states are
|
||||||
|
// its holder's: the modules the trunk holds, their manifests by digest and the module graph (the indexer's states
|
||||||
|
// `modules`, `manifests` and `graph`), announced with who may read them; a reader that only watches reads them and calls
|
||||||
|
// no verb. It names no event. What must be built again is no verb: a module whose fingerprint differs from its newest
|
||||||
|
// build, which the delivery view derives. The verbs' answers are CatalogModules, CatalogOne, CatalogProvides and
|
||||||
|
// CatalogDependents, whose field names are those of the catalog module's tools they replace.
|
||||||
|
const CatalogSeat = "mesh-catalog"
|
||||||
|
|
||||||
|
// catalogVerbs are the seat's four verbs, each read from its holder's states. Each is optional until the holder that
|
||||||
|
// runs serves it (design 33 §7): the module that claimed the seat before serves none of them, and a judge holding them
|
||||||
|
// required would read it as outgrown for as long as both claims stand (novox/hq ADR 0604 §6).
|
||||||
|
func catalogVerbs() []Verb {
|
||||||
|
return []Verb{
|
||||||
|
{Name: "modules", Optional: true, Description: "Every module the trunk holds, each with its repository, its " +
|
||||||
|
"directory there, the merge commit it was last changed at and its fingerprint (or why it has none): the " +
|
||||||
|
"catalog of the mesh, read from its holder's state. Reads only.",
|
||||||
|
Input: schema(map[string]string{}, nil)},
|
||||||
|
{Name: "module", Optional: true, Description: "One module whole: its repository, directory, commit and " +
|
||||||
|
"fingerprint, and its manifest as the trunk holds it. Older versions are in its repository's history. Reads only.",
|
||||||
|
Input: schema(map[string]string{"module": "the module's name"}, []string{"module"})},
|
||||||
|
{Name: "provides", Optional: true, Description: "Which modules provide a provision (a module provides its own " +
|
||||||
|
"name too) and which require it, from the module graph's provision edges. Reads only.",
|
||||||
|
Input: schema(map[string]string{"provision": "the provision, such as postgres-database"}, []string{"provision"})},
|
||||||
|
{Name: "dependents", Optional: true, Description: "The modules whose build edges reach a module, directly or " +
|
||||||
|
"through others (a base, a mesh package it publishes, the build module that builds them): what a change of it " +
|
||||||
|
"builds again, from the module graph. Reads only.",
|
||||||
|
Input: schema(map[string]string{"module": "the module's name"}, []string{"module"})},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CatalogEntry is one module of the catalog: its repository, its directory there ("." for the root), the merge commit it
|
||||||
|
// was last changed at, and its new-form fingerprint or why it has none.
|
||||||
|
type CatalogEntry struct {
|
||||||
|
Module string `json:"module"`
|
||||||
|
Repository string `json:"repository"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Commit string `json:"commit"`
|
||||||
|
FP string `json:"fp,omitempty"`
|
||||||
|
None string `json:"none,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CatalogModule is one module whole: its entry and its manifest as the trunk holds it.
|
||||||
|
type CatalogModule struct {
|
||||||
|
CatalogEntry
|
||||||
|
Manifest json.RawMessage `json:"manifest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CatalogEnd is a module that provides, requires or depends on something, with its commit; Scope is a provider's scope
|
||||||
|
// (a module provides its own name at node scope).
|
||||||
|
type CatalogEnd struct {
|
||||||
|
Module string `json:"module"`
|
||||||
|
Commit string `json:"commit"`
|
||||||
|
Scope string `json:"scope,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CatalogModules is the answer of the verb modules: every module the trunk holds, sorted by name.
|
||||||
|
type CatalogModules struct {
|
||||||
|
Modules []CatalogEntry `json:"modules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CatalogOne is the answer of the verb module.
|
||||||
|
type CatalogOne struct {
|
||||||
|
Module CatalogModule `json:"module"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CatalogProvides is the answer of the verb provides: the modules that provide the provision and those that require it.
|
||||||
|
type CatalogProvides struct {
|
||||||
|
Provision string `json:"provision"`
|
||||||
|
Provides []CatalogEnd `json:"provides"`
|
||||||
|
Requires []CatalogEnd `json:"requires"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CatalogDependents is the answer of the verb dependents: the modules whose build edges reach the module.
|
||||||
|
type CatalogDependents struct {
|
||||||
|
Module string `json:"module"`
|
||||||
|
Dependents []CatalogEnd `json:"dependents"`
|
||||||
|
}
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package catalogue
|
|
||||||
|
|
||||||
import (
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Issue 1126 B1/AC1, B2/AC1 (ADR 0590 clause 5): the seat set holds no mesh-controller, and the dependence check refuses
|
|
||||||
// every manifest that reads (the lease too), invokes, consumes, waits on, uses, claims, requires or is mesh-controller.
|
|
||||||
func TestNothingMayNameTheController(t *testing.T) {
|
|
||||||
for _, s := range DefaultSeats() {
|
|
||||||
if s.Name == EndedControllerName {
|
|
||||||
t.Errorf("the seat set still holds %s", s.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, ok := SeatNamed(EndedControllerName); ok {
|
|
||||||
t.Errorf("SeatNamed still finds %s", EndedControllerName)
|
|
||||||
}
|
|
||||||
for _, row := range []struct {
|
|
||||||
name, manifest string
|
|
||||||
want []string // "<kind> <what>"
|
|
||||||
}{
|
|
||||||
{"a read of the lease", `{"module":"mod-l","version":"1","reads":["mesh-controller.lease"]}`, []string{"read mesh-controller.lease"}},
|
|
||||||
{"the controller's own manifest", `{"module":"mesh-controller","version":"1"}`, []string{"is mesh-controller"}},
|
|
||||||
{"a claim of its seat", `{"module":"a-controller","version":"1","claims":[{"name":"mesh-controller","scope":"mesh"}]}`,
|
|
||||||
[]string{"claims mesh-controller"}},
|
|
||||||
{"each declaration kind; every tool (*) is none",
|
|
||||||
`{"module":"mod-i","version":"1","invokes":["seat:mesh-controller.root-free","mesh-controller.status","*","nats.nats_users"],` +
|
|
||||||
`"consumes":["mesh-controller.artifacts-gone","mod-x.thing"],"uses":["mesh-controller"],"waits":["mesh-controller.checked"],` +
|
|
||||||
`"requires":["mesh-controller"]}`,
|
|
||||||
[]string{"consume mesh-controller.artifacts-gone", "invoke mesh-controller.status", "invoke seat:mesh-controller.root-free",
|
|
||||||
"requires mesh-controller", "uses mesh-controller", "waits mesh-controller.checked"}},
|
|
||||||
{"a process's own invoke", `{"module":"mod-p","version":"1","resources":[{"id":"account","type":"user","name":"mod-p"},` +
|
|
||||||
`{"id":"worker","type":"process","run":["./w"],"user":"mod-p","bus":{"invokes":["seat:mesh-controller.held"]}}]}`,
|
|
||||||
[]string{"invoke seat:mesh-controller.held"}},
|
|
||||||
{"a module on its own seats", `{"module":"mod-ok","version":"1","reads":["mesh-grants.accounts"],"invokes":["*"]}`, nil},
|
|
||||||
} {
|
|
||||||
m, err := Decode([]byte(row.manifest))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s: %v", row.name, err)
|
|
||||||
}
|
|
||||||
var got []string
|
|
||||||
for _, d := range Dependencies(map[string]Manifest{m.Module: m}) {
|
|
||||||
got = append(got, d.Kind+" "+d.What)
|
|
||||||
}
|
|
||||||
if !slices.Equal(got, row.want) {
|
|
||||||
t.Errorf("%s: dependencies %q, want %q", row.name, got, row.want)
|
|
||||||
}
|
|
||||||
refused := ManifestRefusals(map[string]Manifest{m.Module: m})
|
|
||||||
if len(refused) != len(row.want) || len(refused) > 0 && !strings.Contains(refused[0], "ADR 0590") {
|
|
||||||
t.Errorf("%s: refused %q", row.name, refused)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if got := ManifestRefusals(map[string]Manifest{}); len(got) != 1 || !strings.Contains(got[0], "no manifest") {
|
|
||||||
t.Errorf("no manifest given: %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,11 +4,10 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CoreCredentialModules are the modules whose own secrets are the core's credentials: the bus, mesh-provisioner and
|
// CarriesCoreCredentials says whether a module's own secrets are the core's credentials, as its manifest says
|
||||||
// its client. The node-engine and the controller hold no own secret of a module's. **Fixed here, in the controller's
|
// (Manifest.CoreCredentials): the vault seals them to the machine, and the node-engine and the vault hold no list of
|
||||||
// code, as the clause says** — and the vault keeps the same list (mesh-vault's CoreModules), answering the controller
|
// such modules.
|
||||||
// for these alone.
|
func CarriesCoreCredentials(m Manifest) bool { return m.CoreCredentials }
|
||||||
var CoreCredentialModules = []string{"mesh-provisioner", "mesh-provisioner-client", "nats"}
|
|
||||||
|
|
||||||
// VaultSealedField is the field of a file the vault sealed to its machine (mesh-host's File.VaultSealed).
|
// VaultSealedField is the field of a file the vault sealed to its machine (mesh-host's File.VaultSealed).
|
||||||
const VaultSealedField = "vault-sealed"
|
const VaultSealedField = "vault-sealed"
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ type Rendering struct {
|
|||||||
// makes. Nil on every composition that is sent, and on every other question, which then refuse
|
// makes. Nil on every composition that is sent, and on every other question, which then refuse
|
||||||
// a secret not made with a *NotMadeError.
|
// a secret not made with a *NotMadeError.
|
||||||
Foreseen map[string]map[string]bool
|
Foreseen map[string]map[string]bool
|
||||||
// VaultSealed is, for a module on the core's list (CoreCredentialModules), each own secret as the vault sealed it
|
// VaultSealed is, for a module whose manifest says its own secrets are the core's credentials (core-credentials), each own secret as the vault sealed it
|
||||||
// to this machine (the mesh's sealed box, as the vault answered it), keyed like Needed (novox/hq ADR 0349 §16,
|
// to this machine (the mesh's sealed box, as the vault answered it), keyed like Needed (novox/hq ADR 0349 §16,
|
||||||
// issue 755). Given only for a machine whose engine opens the box (VaultBoxContract): its file then carries the
|
// issue 755). Given only for a machine whose engine opens the box (VaultBoxContract): its file then carries the
|
||||||
// box and never the controller's own sealed value. Nil everywhere else, and the composition is as it was.
|
// box and never the controller's own sealed value. Nil everywhere else, and the composition is as it was.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package catalogue
|
package catalogue
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,4 +17,56 @@ func TestAManifestMaySayWhatItInvokes(t *testing.T) {
|
|||||||
if len(m.Invokes) != 2 || m.Invokes[1] != "*" {
|
if len(m.Invokes) != 2 || m.Invokes[1] != "*" {
|
||||||
t.Fatalf("invokes not read: %v", m.Invokes)
|
t.Fatalf("invokes not read: %v", m.Invokes)
|
||||||
}
|
}
|
||||||
|
// And that its own secrets are the core's credentials (issue 774): read, and written back as said.
|
||||||
|
for raw, core := range map[string]bool{`{"module":"bus","version":"1","core-credentials":true}`: true, `{"module":"app","version":"1"}`: false} {
|
||||||
|
m, err := decodeForTest([]byte(raw))
|
||||||
|
if err != nil || m.CoreCredentials != core || m.UnknownField() != "" {
|
||||||
|
t.Errorf("%s: core credentials %v, %v, %q", raw, m.CoreCredentials, err, m.UnknownField())
|
||||||
|
}
|
||||||
|
back, _ := json.Marshal(m)
|
||||||
|
if strings.Contains(string(back), "core-credentials") != core {
|
||||||
|
t.Errorf("%s written back as %s", raw, back)
|
||||||
|
}
|
||||||
|
if CarriesCoreCredentials(m) != core {
|
||||||
|
t.Errorf("%s: carries the core's credentials %v", raw, !core)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// And that it is the tool runner (issue 774): read, written back, and found among others by that alone.
|
||||||
|
runner, err := decodeForTest([]byte(`{"module":"any-name","version":"1","tool-runner":true}`))
|
||||||
|
if err != nil || !IsToolRunner(runner) || runner.UnknownField() != "" {
|
||||||
|
t.Fatalf("a tool runner: %v %q", err, runner.UnknownField())
|
||||||
|
}
|
||||||
|
if back, _ := json.Marshal(runner); !strings.Contains(string(back), `"tool-runner":true`) {
|
||||||
|
t.Errorf("a tool runner written back as %s", back)
|
||||||
|
}
|
||||||
|
other, _ := decodeForTest([]byte(`{"module":"node-tools","version":"1"}`))
|
||||||
|
if got, ok := ToolRunnerOf([]Manifest{other, runner}); !ok || got.Module != "any-name" {
|
||||||
|
t.Errorf("the tool runner found is %q, %v: by what it says, never by its name", got.Module, ok)
|
||||||
|
}
|
||||||
|
// A module's seats are derived from what it declares, whatever its name: a service on a machine is the service
|
||||||
|
// manager's, the node-engine's name exempts nothing; a module whose resources are computed depends on none.
|
||||||
|
host, _ := decodeForTest([]byte(`{"module":"mesh-host","version":"1","resources":[{"id":"s","type":"service","name":"s.service"}]}`))
|
||||||
|
if got := DependsOn(nil, host); len(got) != 1 || got[0] != ServiceManagerSeat {
|
||||||
|
t.Errorf("a service by the node-engine's name depends on %v", got)
|
||||||
|
}
|
||||||
|
if got := DependsOn(nil, Manifest{Module: "net", Computed: "net", Resources: host.Resources}); len(got) != 0 {
|
||||||
|
t.Errorf("a computed module depends on %v", got)
|
||||||
|
}
|
||||||
|
// And its upgrade policy, which holds for the bus as for any module: providing the bus derives nothing (issue 774).
|
||||||
|
for raw, want := range map[string]string{
|
||||||
|
`{"module":"bus","version":"1","provides":[{"name":"mesh-bus"}],"upgrade":{"policy":"record","why":"its streams"}}`: PolicyRecord + " " + FromModule,
|
||||||
|
`{"module":"bus","version":"1","provides":[{"name":"mesh-bus"}]}`: PolicyRoll + " " + FromDefault,
|
||||||
|
} {
|
||||||
|
m, err := decodeForTest([]byte(raw))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if policy, from, _ := DerivedUpgrade(m); policy+" "+from != want {
|
||||||
|
t.Errorf("%s: derived %s %s, want %s", raw, policy, from, want)
|
||||||
|
}
|
||||||
|
chosen := &PersonPolicy{Policy: PolicyTogether, Why: "w", By: "a person"}
|
||||||
|
if policy, from, _, _ := Upgrade(m, chosen); policy != PolicyTogether || from != FromPerson {
|
||||||
|
t.Errorf("%s: a person's choice gave %s %s", raw, policy, from)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
package catalogue
|
package catalogue
|
||||||
|
|
||||||
|
// This file keeps, deprecated, the exported names feat/l3b-bus removed from this package while a running part still
|
||||||
|
// uses them: an exported name is removed only after every user of it is delivered. The checker builds the running
|
||||||
|
// node-engine's composer (mesh-host main) beside this mesh-sdk (ADR 0548 §6), and mesh-host main, mesh-delivery main
|
||||||
|
// (the indexer's judge, the assigner's and mesh-grants' code and tests) and mesh-tools main (node-tools' tests) name
|
||||||
|
// them. Nothing in L3's own heads uses any of them. The second group removes this file, and its exemption in
|
||||||
|
// TestNoCatalogueManifestNamesAmqp, once mesh-host a24a1d72 and L3's mesh-delivery and mesh-tools heads run.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -7,10 +14,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Deprecated: kept until its users are delivered (the head of this file).
|
||||||
|
//
|
||||||
// EndedControllerName is the name the ended controller had, as its module and as its seat: no manifest may name it.
|
// EndedControllerName is the name the ended controller had, as its module and as its seat: no manifest may name it.
|
||||||
const EndedControllerName = "mesh-controller"
|
const EndedControllerName = "mesh-controller"
|
||||||
|
|
||||||
// The kinds of a declaration that names the controller.
|
// The kinds of a declaration that names the controller.
|
||||||
|
//
|
||||||
|
// Deprecated: kept until their users are delivered (the head of this file).
|
||||||
const (
|
const (
|
||||||
DependsRead = "read"
|
DependsRead = "read"
|
||||||
DependsInvoke = "invoke"
|
DependsInvoke = "invoke"
|
||||||
@@ -22,6 +33,8 @@ const (
|
|||||||
DependsIs = "is"
|
DependsIs = "is"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Deprecated: kept until its users are delivered (the head of this file).
|
||||||
|
//
|
||||||
// Dependency is one declaration of a manifest that names the controller.
|
// Dependency is one declaration of a manifest that names the controller.
|
||||||
type Dependency struct {
|
type Dependency struct {
|
||||||
Module string `json:"module"`
|
Module string `json:"module"`
|
||||||
@@ -43,6 +56,8 @@ func invokesController(invoke string) bool {
|
|||||||
return onController(invoke)
|
return onController(invoke)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated: kept until its users are delivered (the head of this file).
|
||||||
|
//
|
||||||
// Dependencies are every declaration naming the controller in the manifests given, by module.
|
// Dependencies are every declaration naming the controller in the manifests given, by module.
|
||||||
func Dependencies(manifests map[string]Manifest) []Dependency {
|
func Dependencies(manifests map[string]Manifest) []Dependency {
|
||||||
var out []Dependency
|
var out []Dependency
|
||||||
@@ -118,6 +133,8 @@ func Dependencies(manifests map[string]Manifest) []Dependency {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated: kept until its users are delivered (the head of this file).
|
||||||
|
//
|
||||||
// ManifestRefusals is the check over a repository's manifests, by module name: one reason per declaration naming the
|
// ManifestRefusals is the check over a repository's manifests, by module name: one reason per declaration naming the
|
||||||
// controller, none when it passes. None given fails it, since nothing read can hide a declaration.
|
// controller, none when it passes. None given fails it, since nothing read can hide a declaration.
|
||||||
func ManifestRefusals(manifests map[string]Manifest) []string {
|
func ManifestRefusals(manifests map[string]Manifest) []string {
|
||||||
@@ -135,3 +152,37 @@ func ManifestRefusals(manifests map[string]Manifest) []string {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated: a manifest says it (Manifest.CoreCredentials, CarriesCoreCredentials); kept until its users are
|
||||||
|
// delivered (the head of this file).
|
||||||
|
//
|
||||||
|
// CoreCredentialModules are the modules whose own secrets are the core's credentials: the bus, mesh-provisioner and
|
||||||
|
// its client.
|
||||||
|
var CoreCredentialModules = []string{"mesh-provisioner", "mesh-provisioner-client", "nats"}
|
||||||
|
|
||||||
|
// Deprecated: a manifest says it (Manifest.ToolRunner, IsToolRunner); kept until its users are delivered (the head
|
||||||
|
// of this file).
|
||||||
|
//
|
||||||
|
// RuntimeModule is the module that is the node's tool runtime.
|
||||||
|
const RuntimeModule = "node-tools"
|
||||||
|
|
||||||
|
// Deprecated: the kept streams are the indexer's judge's; kept until its users are delivered (the head of this file).
|
||||||
|
//
|
||||||
|
// StreamsKeptUntilMoved are the streams declared before the check that a module keeps its data in the bus's
|
||||||
|
// predefined forms, each allowed by name until its module moves it.
|
||||||
|
var StreamsKeptUntilMoved = map[string][]string{
|
||||||
|
"brother": {"device-asks"},
|
||||||
|
"mesh-build-go": {"toolchains-go"},
|
||||||
|
"mesh-build-node": {"toolchains-node"},
|
||||||
|
"mesh-mail": {"mail", "mail-sends"},
|
||||||
|
"only-office": {"convert-asks"},
|
||||||
|
"mesh-delivery-assigner": {"placement"},
|
||||||
|
"mesh-delivery-checker": {"checks"},
|
||||||
|
"mesh-delivery-forge-bridge": {"forge"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: nothing derives it now (DerivedUpgrade reads the bus's own manifest); kept until its users are
|
||||||
|
// delivered (the head of this file).
|
||||||
|
//
|
||||||
|
// FromBus is the source an older DerivedUpgrade gave the bus's policy.
|
||||||
|
const FromBus = "the bus"
|
||||||
@@ -640,7 +640,7 @@ type Manifest struct {
|
|||||||
|
|
||||||
// Upgrade is how this module's new builds reach its machines (novox/hq ADR 0236): rolled out one
|
// Upgrade is how this module's new builds reach its machines (novox/hq ADR 0236): rolled out one
|
||||||
// machine first and gated when unsaid; `together`, or `record` — wait for a person's push — with
|
// machine first and gated when unsaid; `together`, or `record` — wait for a person's push — with
|
||||||
// why. A person's choice through the `upgrade` verb stands over it; the bus records whatever it says.
|
// why. A person's choice through the `upgrade` verb stands over it. The bus says `record` itself.
|
||||||
Upgrade *UpgradePolicy `json:"upgrade,omitempty"`
|
Upgrade *UpgradePolicy `json:"upgrade,omitempty"`
|
||||||
|
|
||||||
// Unhealthy is how loud this module's unhealthy condition is (novox/hq ADR 0355): a warning when unsaid;
|
// Unhealthy is how loud this module's unhealthy condition is (novox/hq ADR 0355): a warning when unsaid;
|
||||||
@@ -801,6 +801,17 @@ type Manifest struct {
|
|||||||
// start, and is then replaced.
|
// start, and is then replaced.
|
||||||
OwnSecrets OwnSecrets `json:"own-secrets,omitempty"`
|
OwnSecrets OwnSecrets `json:"own-secrets,omitempty"`
|
||||||
|
|
||||||
|
// ToolRunner says this module is the node's tool runner (issue 774): the one program on each node that loads every
|
||||||
|
// assigned module's tools bundle, served by its runtime process and the only module that may declare `waits`. Said by
|
||||||
|
// the module, never by a name in mesh-sdk.
|
||||||
|
ToolRunner bool `json:"tool-runner,omitempty"`
|
||||||
|
|
||||||
|
// CoreCredentials says this module's own secrets are the core's credentials (novox/hq ADR 0349 §16, issue 755): the
|
||||||
|
// vault seals each to the machine, and the node-engine writes the vault's box into its file, never a value of its
|
||||||
|
// own (Rendering.VaultSealed). Said by the module, never by a list in mesh-sdk (the operator, 2026-11-03: mesh-sdk names
|
||||||
|
// no module): the bus and the provisioner and its client say it.
|
||||||
|
CoreCredentials bool `json:"core-credentials,omitempty"`
|
||||||
|
|
||||||
// SecretsOwner is who the files holding this module's secrets belong to on the machine —
|
// SecretsOwner is who the files holding this module's secrets belong to on the machine —
|
||||||
// `uid:gid`, or a name — when its process is not root.
|
// `uid:gid`, or a name — when its process is not root.
|
||||||
//
|
//
|
||||||
|
|||||||
+13
-3
@@ -1,8 +1,18 @@
|
|||||||
package catalogue
|
package catalogue
|
||||||
|
|
||||||
// RuntimeModule is the module that is the node's tool runtime. Mirrored in the broker package,
|
// IsToolRunner says whether a manifest is the node's tool runner's, as it says itself (`tool-runner`, issue 774): the
|
||||||
// which composes a principal of its own for it; the agreement test there holds the two to one string.
|
// one program on each node that loads every assigned module's tools bundle. Never known here by its name.
|
||||||
const RuntimeModule = "node-tools"
|
func IsToolRunner(m Manifest) bool { return m.ToolRunner }
|
||||||
|
|
||||||
|
// ToolRunnerOf is the module among manifests that says it is the tool runner, and whether one does.
|
||||||
|
func ToolRunnerOf(manifests []Manifest) (Manifest, bool) {
|
||||||
|
for _, m := range manifests {
|
||||||
|
if IsToolRunner(m) {
|
||||||
|
return m, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Manifest{}, false
|
||||||
|
}
|
||||||
|
|
||||||
// BundleRoot is where a machine keeps the tools bundles the mesh delivers to it: under the mesh's
|
// BundleRoot is where a machine keeps the tools bundles the mesh delivers to it: under the mesh's
|
||||||
// own directory, beside the daemons the host unpacks there, and never where a package manager also
|
// own directory, beside the daemons the host unpacks there, and never where a package manager also
|
||||||
|
|||||||
@@ -24,24 +24,12 @@ var appliedThrough = map[string]string{
|
|||||||
"container": ContainerRuntimeSeat,
|
"container": ContainerRuntimeSeat,
|
||||||
}
|
}
|
||||||
|
|
||||||
// foundationModules are the pieces genesis lays before any module exists (novox/hq ADR 0207 §5):
|
// isFoundation is whether a module's declarations are the foundation's rather than its own: a module whose resources are
|
||||||
// the host and the private network. Registered as modules so they can be assigned, but what they
|
// computed (`computed`, the private network's peer list written per node) is the mesh's by construction. Read from the
|
||||||
// declare is the installation's, not a module's, so it is never judged. The third piece, the
|
// manifest alone, never from its name (issue 774, the operator 2026-11-03): the node-engine's own manifest declares no
|
||||||
// bootstrap container runtime, is not a module at all: its package and service are in the genesis
|
// resource a seat applies, so it depends on none by the rules below.
|
||||||
// bundle the host applies itself, and never pass through a resolution here.
|
|
||||||
//
|
|
||||||
// The private network's module is overlay.Name, written out because the overlay package composes
|
|
||||||
// on top of this one; its resources are computed, which exempts it by the rule below as well.
|
|
||||||
var foundationModules = map[string]bool{
|
|
||||||
"mesh-host": true,
|
|
||||||
"mesh-wireguard": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// isFoundation is whether a module's declarations are the foundation's rather than its own. A
|
|
||||||
// module whose resources are computed is the mesh's by construction — the private network's peer
|
|
||||||
// list and its tools are the controller's, written per node — so it counts whatever its name.
|
|
||||||
func isFoundation(m Manifest) bool {
|
func isFoundation(m Manifest) bool {
|
||||||
return foundationModules[m.Module] || m.Computed != ""
|
return m.Computed != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// DependsOn is every seat a module needs held on its node: derived from the resource types it
|
// DependsOn is every seat a module needs held on its node: derived from the resource types it
|
||||||
|
|||||||
@@ -272,7 +272,8 @@ var defaultSeats = append([]Seat{
|
|||||||
// images and archives, by digest — which is why the provision is the artifact store and not an
|
// images and archives, by digest — which is why the provision is the artifact store and not an
|
||||||
// image registry.
|
// image registry.
|
||||||
{Name: "mesh-artifact-store", Scope: ScopeMesh, Delivers: "artifact-store", Decision: "novox/hq ADR 0075"},
|
{Name: "mesh-artifact-store", Scope: ScopeMesh, Delivers: "artifact-store", Decision: "novox/hq ADR 0075"},
|
||||||
{Name: "mesh-catalog", Scope: ScopeMesh, Decision: "novox/hq ADR 0121"},
|
// The catalog (novox/hq ADR 0604): what the mesh holds of every module, its verbs read from its holder's states (CatalogSeat).
|
||||||
|
{Name: CatalogSeat, Scope: ScopeMesh, Serves: catalogVerbs(), Decision: "novox/hq ADR 0604"},
|
||||||
// The delivery's owner (novox/hq ADR 0239): one commit's journey from its pull request's head to every
|
// The delivery's owner (novox/hq ADR 0239): one commit's journey from its pull request's head to every
|
||||||
// machine, and a group of them, recorded and ordered by its holder, which asks the controller for each act.
|
// machine, and a group of them, recorded and ordered by its holder, which asks the controller for each act.
|
||||||
// What it says it says as its module (`mesh-delivery.transition`), through the runtime that launches it.
|
// What it says it says as its module (`mesh-delivery.transition`), through the runtime that launches it.
|
||||||
|
|||||||
@@ -370,19 +370,3 @@ func uncovered(patterns []string) []string {
|
|||||||
slices.Sort(out)
|
slices.Sort(out)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// **A module keeps its data in the bus's predefined forms** (the lead and the operator, 2026-10-29; the operator's rule of
|
|
||||||
// 2026-10-22, "data storage is done using our predefined nats bus options"): a state, a log or an object store, which the
|
|
||||||
// runtime reads and writes for its tools and other modules read by a declared `reads`. A stream of its own is outside
|
|
||||||
// them and refused, naming it. StreamsKeptUntilMoved are the streams declared before this check, each allowed by name
|
|
||||||
// until its module moves it; the list only shrinks.
|
|
||||||
var StreamsKeptUntilMoved = map[string][]string{
|
|
||||||
"brother": {"device-asks"},
|
|
||||||
"mesh-build-go": {"toolchains-go"},
|
|
||||||
"mesh-build-node": {"toolchains-node"},
|
|
||||||
"mesh-mail": {"mail", "mail-sends"},
|
|
||||||
"only-office": {"convert-asks"},
|
|
||||||
"mesh-delivery-assigner": {"placement"},
|
|
||||||
"mesh-delivery-checker": {"checks"},
|
|
||||||
"mesh-delivery-forge-bridge": {"forge"},
|
|
||||||
}
|
|
||||||
|
|||||||
+4
-11
@@ -30,25 +30,19 @@ type UpgradePolicy struct {
|
|||||||
const (
|
const (
|
||||||
FromPerson = "person"
|
FromPerson = "person"
|
||||||
FromModule = "module"
|
FromModule = "module"
|
||||||
FromBus = "the bus"
|
|
||||||
FromData = "irreplaceable data"
|
FromData = "irreplaceable data"
|
||||||
FromDefault = "default"
|
FromDefault = "default"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DerivedUpgrade is the policy a module's manifest gives it when no person has chosen one, with where
|
// DerivedUpgrade is the policy a module's manifest gives it when no person has chosen one, with where
|
||||||
// it came from and why (ADR 0236):
|
// it came from and why (ADR 0236), from what the manifest says alone (the operator, 2026-11-03: no module is
|
||||||
|
// treated apart here; the bus says `record` in its own manifest):
|
||||||
//
|
//
|
||||||
// - **the bus is never rolled**: a module that provides the mesh's bus records, whatever it says — its
|
|
||||||
// upgrade is a planned step a person starts (to-be 45 §8);
|
|
||||||
// - a module that says its policy has it;
|
// - a module that says its policy has it;
|
||||||
// - a module that keeps irreplaceable data records — sending the previous build cannot undo what a new
|
// - a module that keeps irreplaceable data records — sending the previous build cannot undo what a new
|
||||||
// one did to data that cannot be had again, so a person takes it, after a backup;
|
// one did to data that cannot be had again, so a person takes it, after a backup;
|
||||||
// - everything else rolls out, one machine first.
|
// - everything else rolls out, one machine first.
|
||||||
func DerivedUpgrade(m Manifest) (policy, from, why string) {
|
func DerivedUpgrade(m Manifest) (policy, from, why string) {
|
||||||
if ProvidesBus(m) {
|
|
||||||
return PolicyRecord, FromBus, "the bus is replaced only as a planned step a person starts (`bus upgrade`): " +
|
|
||||||
"its streams are snapshotted first and checked after"
|
|
||||||
}
|
|
||||||
if m.Upgrade != nil && m.Upgrade.Policy != "" {
|
if m.Upgrade != nil && m.Upgrade.Policy != "" {
|
||||||
return m.Upgrade.Policy, FromModule, m.Upgrade.Why
|
return m.Upgrade.Policy, FromModule, m.Upgrade.Why
|
||||||
}
|
}
|
||||||
@@ -126,11 +120,10 @@ func (p PersonPolicy) Check() error {
|
|||||||
|
|
||||||
// Upgrade is a module's policy and where it comes from (ADR 0565 clause 7): the one derivation every reader takes,
|
// Upgrade is a module's policy and where it comes from (ADR 0565 clause 7): the one derivation every reader takes,
|
||||||
// `mesh-wants.upgrade` and each node agent's judgement of a move alike. A person's choice over the module's word, its
|
// `mesh-wants.upgrade` and each node agent's judgement of a move alike. A person's choice over the module's word, its
|
||||||
// irreplaceable data and the default; the bus is never rolled, whoever says so (ADR 0236). By names the person when one
|
// irreplaceable data and the default. By names the person when one chose.
|
||||||
// chose.
|
|
||||||
func Upgrade(m Manifest, chosen *PersonPolicy) (policy, from, why, by string) {
|
func Upgrade(m Manifest, chosen *PersonPolicy) (policy, from, why, by string) {
|
||||||
policy, from, why = DerivedUpgrade(m)
|
policy, from, why = DerivedUpgrade(m)
|
||||||
if from == FromBus || chosen == nil {
|
if chosen == nil {
|
||||||
return policy, from, why, ""
|
return policy, from, why, ""
|
||||||
}
|
}
|
||||||
return chosen.Policy, FromPerson, chosen.Why, chosen.By
|
return chosen.Policy, FromPerson, chosen.Why, chosen.By
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package catalogue
|
package catalogue
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -50,6 +51,53 @@ func TestHoldingAMeshSeatRequiresServingItsVerbs(t *testing.T) {
|
|||||||
t.Errorf("a holder saying it serves mint: %v", err)
|
t.Errorf("a holder saying it serves mint: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// L3: the seat mesh-catalog promises four verbs, each optional while two claimants stand (ADR 0604 §1, §6).
|
||||||
|
catalog, ok := SeatNamed(CatalogSeat)
|
||||||
|
if !ok || catalog.Scope != ScopeMesh || catalog.Decision != "novox/hq ADR 0604" || len(catalog.Serves) != 4 {
|
||||||
|
t.Fatalf("mesh-catalog: in the set %v, %+v", ok, catalog)
|
||||||
|
}
|
||||||
|
four := []string{"modules", "module", "provides", "dependents"}
|
||||||
|
for i, v := range catalog.Serves {
|
||||||
|
if v.Name != four[i] || !v.Optional || v.Input == nil {
|
||||||
|
t.Errorf("mesh-catalog's verb %d is %+v, want %s, optional, with its input", i, v, four[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The answers keep the field names of the catalog module's tools they replace (catalog_modules, catalog_module,
|
||||||
|
// catalog_provides, catalog_dependents, as mesh_call answered them on 2026-11-03).
|
||||||
|
for _, c := range []struct {
|
||||||
|
answer any
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{CatalogModules{Modules: []CatalogEntry{{}}}, []string{`"modules":[{"module":`, `"repository":`, `"path":`, `"commit":`}},
|
||||||
|
{CatalogOne{Module: CatalogModule{Manifest: json.RawMessage(`{}`)}}, []string{`"module":{"module":`, `"manifest":{}`}},
|
||||||
|
{CatalogProvides{Provides: []CatalogEnd{{Scope: "mesh"}}, Requires: []CatalogEnd{{}}}, []string{`"provides":[{"module":"","commit":"","scope":"mesh"}]`, `"requires":[{"module":"","commit":""}]`}},
|
||||||
|
{CatalogDependents{Module: "m", Dependents: []CatalogEnd{}}, []string{`"module":"m"`, `"dependents":[]`}},
|
||||||
|
} {
|
||||||
|
raw, _ := json.Marshal(c.answer)
|
||||||
|
for _, w := range c.want {
|
||||||
|
if !strings.Contains(string(raw), w) {
|
||||||
|
t.Errorf("the answer %s does not hold %s", raw, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cataloguer := func(serves ...string) Manifest {
|
||||||
|
return Manifest{Module: "a-catalog", Tools: serves, Claims: []Claim{{Name: CatalogSeat, Scope: ScopeMesh, Serves: serves}}}
|
||||||
|
}
|
||||||
|
for name, c := range map[string]struct {
|
||||||
|
serves []string
|
||||||
|
word string
|
||||||
|
}{
|
||||||
|
"serving the four": {four, ""},
|
||||||
|
"serving three, optional": {four[:3], ""},
|
||||||
|
"serving none, the old one": {nil, ""},
|
||||||
|
"serving a fifth": {append(slices.Clone(four), "rebuild"), "rebuild"},
|
||||||
|
} {
|
||||||
|
err := CanHold(cataloguer(c.serves...), catalog)
|
||||||
|
if (c.word == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.word)) {
|
||||||
|
t.Errorf("a mesh-catalog holder %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
was := Seats()
|
was := Seats()
|
||||||
t.Cleanup(func() { UseSeats(was) })
|
t.Cleanup(func() { UseSeats(was) })
|
||||||
UseSeats([]Seat{{Name: "mesh-controller", Scope: ScopeMesh, Decision: "test",
|
UseSeats([]Seat{{Name: "mesh-controller", Scope: ScopeMesh, Decision: "test",
|
||||||
|
|||||||
+10
-6
@@ -135,13 +135,17 @@ type ManifestRead struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Machine is one machine composed: whether it composes (resolves, at DepthResolve), why not, the node-engine
|
// Machine is one machine composed: whether it composes (resolves, at DepthResolve), why not, the node-engine
|
||||||
// validator's problems and the composition's refusals (DepthCompose), and its declared paths when asked.
|
// validator's problems and the composition's refusals (DepthCompose), and its declared paths when asked. NotJudged says,
|
||||||
|
// in words, why the composer did not judge the machine (an older node-engine's input that does not say how it composes,
|
||||||
|
// novox/hq ADR 0593 §9): the machine is then neither composed nor refused, and a reader decides by this field alone,
|
||||||
|
// never by the words of Reason.
|
||||||
type Machine struct {
|
type Machine struct {
|
||||||
Composes bool `json:"composes"`
|
Composes bool `json:"composes"`
|
||||||
Reason string `json:"reason,omitempty"`
|
NotJudged string `json:"not_judged,omitempty"`
|
||||||
Problems []string `json:"problems,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
Refusals []string `json:"refusals,omitempty"`
|
Problems []string `json:"problems,omitempty"`
|
||||||
Declared []Path `json:"declared,omitempty"`
|
Refusals []string `json:"refusals,omitempty"`
|
||||||
|
Declared []Path `json:"declared,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path is one path a declaration puts on the machine, with its kind (the resource's type), owner, group and mode as
|
// Path is one path a declaration puts on the machine, with its kind (the resource's type), owner, group and mode as
|
||||||
|
|||||||
@@ -193,6 +193,10 @@ type Want struct {
|
|||||||
// of policy record that the node agent judged to recreate a container or move its data is applied only when its
|
// of policy record that the node agent judged to recreate a container or move its data is applied only when its
|
||||||
// want names the composition so allowed. Empty when none was asked.
|
// want names the composition so allowed. Empty when none was asked.
|
||||||
Yes string `json:"yes,omitempty"`
|
Yes string `json:"yes,omitempty"`
|
||||||
|
// Release names the release whose Approve answered the record ask of this want's build (novox/hq ADR 0601 §1): the
|
||||||
|
// group of pull requests mesh-delivery's gate merged on that Approve. Set with the yes it gave; empty when a person's
|
||||||
|
// answer to a record ask gave it, or none was needed.
|
||||||
|
Release string `json:"release,omitempty"`
|
||||||
// Self says the node composes this module itself (ADR 0547 §2): the want names the build alone, and no composition.
|
// Self says the node composes this module itself (ADR 0547 §2): the want names the build alone, and no composition.
|
||||||
// A want that is not Self names the one composition it allows (ADR 0539 clause 1), as before.
|
// A want that is not Self names the one composition it allows (ADR 0539 clause 1), as before.
|
||||||
Self bool `json:"self,omitempty"`
|
Self bool `json:"self,omitempty"`
|
||||||
@@ -200,7 +204,7 @@ type Want struct {
|
|||||||
|
|
||||||
func (w Want) check() error {
|
func (w Want) check() error {
|
||||||
if w.Removed {
|
if w.Removed {
|
||||||
if w.FP != "" || w.Composed != "" || w.Newest != "" || w.Rule != 0 || w.Backup != "" || w.Put || w.Hold || w.Self || w.Yes != "" {
|
if w.FP != "" || w.Composed != "" || w.Newest != "" || w.Rule != 0 || w.Backup != "" || w.Put || w.Hold || w.Self || w.Yes != "" || w.Release != "" {
|
||||||
return fmt.Errorf("delivery: a removed want names no fingerprint, composition, rule, backup answer or put")
|
return fmt.Errorf("delivery: a removed want names no fingerprint, composition, rule, backup answer or put")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -230,6 +234,8 @@ func (w Want) check() error {
|
|||||||
if err := checkDigest(w.Yes, "the digest a want's yes allows"); err != nil {
|
if err := checkDigest(w.Yes, "the digest a want's yes allows"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
} else if w.Release != "" {
|
||||||
|
return fmt.Errorf("delivery: the want of %s names the release %s and no yes it gave", w.FP, w.Release)
|
||||||
}
|
}
|
||||||
switch w.Backup {
|
switch w.Backup {
|
||||||
case "", BackupAgain, BackupSkip:
|
case "", BackupAgain, BackupSkip:
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ const (
|
|||||||
// starting): no module is named, Refused says why in words, and the checker takes it as a node that did not answer
|
// starting): no module is named, Refused says why in words, and the checker takes it as a node that did not answer
|
||||||
// (ComposeNoAnswer, ADR 0593 §9), deciding by this verdict and never by the words.
|
// (ComposeNoAnswer, ADR 0593 §9), deciding by this verdict and never by the words.
|
||||||
ComposeCandidateUnready = "unready"
|
ComposeCandidateUnready = "unready"
|
||||||
|
// ComposeCandidateNotJudged is the verdict for a node the checker's sandbox composed and could not judge (an older
|
||||||
|
// node-engine's input that does not say how it composes, novox/hq ADR 0593 §9): no module is named, Refused says why in
|
||||||
|
// words, and the checker names the node with them and passes the check with that warning, never silent and never a
|
||||||
|
// failure of the change. The checker decides by this verdict, never by the words (ADR 0601 §4's kind: a contract is a
|
||||||
|
// field, not a phrase).
|
||||||
|
ComposeCandidateNotJudged = "not-judged"
|
||||||
|
|
||||||
// The states of one module's composition with the candidate: composed (Digest set), refused by the candidate
|
// The states of one module's composition with the candidate: composed (Digest set), refused by the candidate
|
||||||
// (Words set), or held, refused without the candidate too (Words set), which counts toward no verdict.
|
// (Words set), or held, refused without the candidate too (Words set), which counts toward no verdict.
|
||||||
@@ -115,13 +121,13 @@ func (a ComposeCandidateAnswer) check() error {
|
|||||||
if a.Took < 0 {
|
if a.Took < 0 {
|
||||||
return fmt.Errorf("delivery: the answer of %s took %d ms", a.Node, a.Took)
|
return fmt.Errorf("delivery: the answer of %s took %d ms", a.Node, a.Took)
|
||||||
}
|
}
|
||||||
if a.Verdict == ComposeCandidateUnready && a.Refused == "" {
|
if (a.Verdict == ComposeCandidateUnready || a.Verdict == ComposeCandidateNotJudged) && a.Refused == "" {
|
||||||
return fmt.Errorf("delivery: %s is not ready to compose and its answer says no why", a.Node)
|
return fmt.Errorf("delivery: %s's answer is %s and says no why", a.Node, a.Verdict)
|
||||||
}
|
}
|
||||||
if a.Refused != "" {
|
if a.Refused != "" {
|
||||||
if len(a.Modules) > 0 || a.Verdict != ComposeCandidateRefused && a.Verdict != ComposeCandidateUnready {
|
if len(a.Modules) > 0 || a.Verdict != ComposeCandidateRefused && a.Verdict != ComposeCandidateUnready && a.Verdict != ComposeCandidateNotJudged {
|
||||||
return fmt.Errorf("delivery: %s could not compose the candidate (%s), and its answer names modules or is neither %s nor %s",
|
return fmt.Errorf("delivery: %s could not compose the candidate (%s), and its answer names modules or is neither %s, %s nor %s",
|
||||||
a.Node, a.Refused, ComposeCandidateRefused, ComposeCandidateUnready)
|
a.Node, a.Refused, ComposeCandidateRefused, ComposeCandidateUnready, ComposeCandidateNotJudged)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,23 @@ func TestComposeCandidateAndComposedPlanRoundTripAndRefuse(t *testing.T) {
|
|||||||
if _, err := EncodeComposeCandidateAnswer(unready); err == nil {
|
if _, err := EncodeComposeCandidateAnswer(unready); err == nil {
|
||||||
t.Error("an unready answer naming modules was encoded")
|
t.Error("an unready answer naming modules was encoded")
|
||||||
}
|
}
|
||||||
|
// A node the sandbox did not judge: its own verdict, with words and no module (ADR 0593 §9, ADR 0601 §4).
|
||||||
|
notJudged := ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: ComposeCandidateNotJudged, Refused: "its node-engine sends no running state"}
|
||||||
|
if raw, err := EncodeComposeCandidateAnswer(notJudged); err != nil {
|
||||||
|
t.Errorf("a not-judged answer was not encoded: %v", err)
|
||||||
|
} else if back, err := DecodeComposeCandidateAnswer(raw); err != nil || back.Verdict != ComposeCandidateNotJudged {
|
||||||
|
t.Errorf("a not-judged answer read back as %+v, %v", back, err)
|
||||||
|
}
|
||||||
|
for name, mutate := range map[string]func(*ComposeCandidateAnswer){
|
||||||
|
"no why": func(a *ComposeCandidateAnswer) { a.Refused = "" },
|
||||||
|
"modules too": func(a *ComposeCandidateAnswer) { a.Modules = good.Modules },
|
||||||
|
} {
|
||||||
|
a := notJudged
|
||||||
|
mutate(&a)
|
||||||
|
if _, err := EncodeComposeCandidateAnswer(a); err == nil {
|
||||||
|
t.Errorf("a not-judged answer with %s was encoded", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
if _, err := DecodeComposeCandidateAnswer([]byte(strings.Replace(string(raw), `"took"`, `"other":1,"took"`, 1))); err == nil {
|
if _, err := DecodeComposeCandidateAnswer([]byte(strings.Replace(string(raw), `"took"`, `"other":1,"took"`, 1))); err == nil {
|
||||||
t.Error("an answer with an unknown key was read")
|
t.Error("an answer with an unknown key was read")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,8 +164,9 @@ type ComposedDigest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The upgrade policies and their sources a composition's value names: catalogue.PolicyRoll, PolicyTogether and
|
// The upgrade policies and their sources a composition's value names: catalogue.PolicyRoll, PolicyTogether and
|
||||||
// PolicyRecord, and catalogue.FromPerson, FromModule, FromBus, FromData and FromDefault, the same words (the compose
|
// PolicyRecord, and catalogue.FromPerson, FromModule, FromData and FromDefault, the same words (the compose package's
|
||||||
// package's test holds them equal; this package does not import the catalogue).
|
// test holds them equal; this package does not import the catalogue). "the bus" is the word an older reader derived
|
||||||
|
// for the bus module, still read from a composition it wrote; nothing derives it now.
|
||||||
var (
|
var (
|
||||||
composedPolicies = []string{"roll", "together", "record"}
|
composedPolicies = []string{"roll", "together", "record"}
|
||||||
composedPolicyFroms = []string{"person", "module", "the bus", "irreplaceable data", "default"}
|
composedPolicyFroms = []string{"person", "module", "the bus", "irreplaceable data", "default"}
|
||||||
|
|||||||
+11
-2
@@ -351,6 +351,15 @@ type Have struct {
|
|||||||
// composition that waits for a person's yes (ADR 0547 §3); empty otherwise.
|
// composition that waits for a person's yes (ADR 0547 §3); empty otherwise.
|
||||||
Judgement Judgement `json:"judgement,omitempty"`
|
Judgement Judgement `json:"judgement,omitempty"`
|
||||||
Waits string `json:"waits,omitempty"`
|
Waits string `json:"waits,omitempty"`
|
||||||
|
// BuildAlone says, in phase waiting-yes, that the move changes the module's build alone (novox/hq ADR 0601 §1): the
|
||||||
|
// node composed the build it runs again with today's settings and secrets and got the composition it runs, so the
|
||||||
|
// release that carries the new build answers the yes. False when a setting or a secret changed too, or the node could
|
||||||
|
// not tell: the move is then asked of a person as before.
|
||||||
|
BuildAlone bool `json:"build-alone,omitempty"`
|
||||||
|
// Kept says, in words, what the node keeps of the module as it runs because its manifest does not yet say what shared
|
||||||
|
// code no longer infers (that it is the tool runner, that its own secrets are the core's credentials; novox/hq ADR 0601
|
||||||
|
// §4): the watchman raises a condition from it, cleared once the have says none. Empty in every other have.
|
||||||
|
Kept []string `json:"kept,omitempty"`
|
||||||
// Sealed is, for a module the node composes itself, each value its running composition carries sealed to the node's
|
// Sealed is, for a module the node composes itself, each value its running composition carries sealed to the node's
|
||||||
// key (a secret, a credential, a vault box), as applied (ADR 0547 §3, issue 1126): what the node's composer reuses
|
// key (a secret, a credential, a vault box), as applied (ADR 0547 §3, issue 1126): what the node's composer reuses
|
||||||
// after a restart while it opens to the vault's value, so a value sealed again moves no composition. Openable by the
|
// after a restart while it opens to the vault's value, so a value sealed again moves no composition. Openable by the
|
||||||
@@ -476,8 +485,8 @@ func decode(data []byte, into any) error {
|
|||||||
func (h Have) check() error {
|
func (h Have) check() error {
|
||||||
switch h.Phase {
|
switch h.Phase {
|
||||||
case PhaseBackingUp, PhaseApplying, PhaseChecking, PhaseRunning, PhaseFailed, PhaseRemoved, PhaseHeld:
|
case PhaseBackingUp, PhaseApplying, PhaseChecking, PhaseRunning, PhaseFailed, PhaseRemoved, PhaseHeld:
|
||||||
if h.Judgement != "" || h.Waits != "" {
|
if h.Judgement != "" || h.Waits != "" || h.BuildAlone {
|
||||||
return fmt.Errorf("delivery: only a have waiting for a yes names a judgement and the composition waiting")
|
return fmt.Errorf("delivery: only a have waiting for a yes names a judgement, the composition waiting and whether it changes the build alone")
|
||||||
}
|
}
|
||||||
case PhaseWaitingYes:
|
case PhaseWaitingYes:
|
||||||
if h.Judgement != JudgedRecreates && h.Judgement != JudgedMovesData && h.Judgement != JudgedCannot {
|
if h.Judgement != JudgedRecreates && h.Judgement != JudgedMovesData && h.Judgement != JudgedCannot {
|
||||||
|
|||||||
@@ -380,10 +380,25 @@ func testPhaseHealthRefused(t *testing.T) {
|
|||||||
} else if back, err := DecodeHave(raw); err != nil || back.Judgement != JudgedRecreates || back.Waits != waits.Waits {
|
} else if back, err := DecodeHave(raw); err != nil || back.Judgement != JudgedRecreates || back.Waits != waits.Waits {
|
||||||
t.Errorf("a have waiting for a yes came back %+v, %v", back, err)
|
t.Errorf("a have waiting for a yes came back %+v, %v", back, err)
|
||||||
}
|
}
|
||||||
|
// ADR 0601 §1: and whether the move changes the build alone, read back as said.
|
||||||
|
alone := waits
|
||||||
|
alone.BuildAlone = true
|
||||||
|
if raw, err := EncodeHave(alone); err != nil {
|
||||||
|
t.Errorf("a have waiting for a yes to its build alone: %v", err)
|
||||||
|
} else if back, err := DecodeHave(raw); err != nil || !back.BuildAlone {
|
||||||
|
t.Errorf("a have waiting for a yes to its build alone came back %+v, %v", back, err)
|
||||||
|
}
|
||||||
|
// ADR 0601 §4: what the node keeps as it runs, read back as said.
|
||||||
|
if raw, err := EncodeHave(Have{Phase: PhaseRunning, Health: HealthOK, Kept: []string{"runs the tool runner"}}); err != nil {
|
||||||
|
t.Errorf("a have keeping what runs: %v", err)
|
||||||
|
} else if back, err := DecodeHave(raw); err != nil || len(back.Kept) != 1 {
|
||||||
|
t.Errorf("a have keeping what runs came back %+v, %v", back, err)
|
||||||
|
}
|
||||||
for name, h := range map[string]Have{
|
for name, h := range map[string]Have{
|
||||||
"neither judged": {Phase: PhaseWaitingYes, Health: HealthOK, Judgement: JudgedNeither, Waits: waits.Waits},
|
"running build alone": {Phase: PhaseRunning, Health: HealthOK, BuildAlone: true},
|
||||||
"no waiting digest": {Phase: PhaseWaitingYes, Health: HealthOK, Judgement: JudgedMovesData},
|
"neither judged": {Phase: PhaseWaitingYes, Health: HealthOK, Judgement: JudgedNeither, Waits: waits.Waits},
|
||||||
"running judged": {Phase: PhaseRunning, Health: HealthOK, Judgement: JudgedRecreates},
|
"no waiting digest": {Phase: PhaseWaitingYes, Health: HealthOK, Judgement: JudgedMovesData},
|
||||||
|
"running judged": {Phase: PhaseRunning, Health: HealthOK, Judgement: JudgedRecreates},
|
||||||
} {
|
} {
|
||||||
if _, err := EncodeHave(h); err == nil {
|
if _, err := EncodeHave(h); err == nil {
|
||||||
t.Errorf("%s: encoded", name)
|
t.Errorf("%s: encoded", name)
|
||||||
|
|||||||
+22
-7
@@ -404,10 +404,15 @@ type Module struct {
|
|||||||
Manifest string `json:"manifest,omitempty"`
|
Manifest string `json:"manifest,omitempty"`
|
||||||
// Inputs2 are the new-form inputs the merge gave the module, kept so the indexer computes FP2 again when a build
|
// Inputs2 are the new-form inputs the merge gave the module, kept so the indexer computes FP2 again when a build
|
||||||
// module's toolchain versions appear, with no merge (novox/hq ADR 0465 clause 2). It needs Manifest.
|
// module's toolchain versions appear, with no merge (novox/hq ADR 0465 clause 2). It needs Manifest.
|
||||||
Inputs2 *KeptInputs2 `json:"inputs2,omitempty"`
|
Inputs2 *KeptInputs2 `json:"inputs2,omitempty"`
|
||||||
Src uint64 `json:"src,omitempty"`
|
Src uint64 `json:"src,omitempty"`
|
||||||
Repository string `json:"repository"`
|
// FPSrc is the source revision at which FP2 last changed (novox/hq ADR 0601 §1): Src when a merge of the module's own
|
||||||
Commit string `json:"commit"`
|
// repository moved it, and 0 when a recompute with no merge moved it (a toolchain, a package, a base), since no release
|
||||||
|
// carries that build. The release that moved the source to it is the one that carries the build; 0 is not known, and no
|
||||||
|
// release is taken to carry it. Src and Commit stay where the module was last merged and say nothing of that.
|
||||||
|
FPSrc uint64 `json:"fp-src,omitempty"`
|
||||||
|
Repository string `json:"repository"`
|
||||||
|
Commit string `json:"commit"`
|
||||||
// Path is the module's directory in its repository, trimmed of slashes, and RootPath (".") when its manifest is the
|
// Path is the module's directory in its repository, trimmed of slashes, and RootPath (".") when its manifest is the
|
||||||
// repository's root (novox/hq ADR 0478 clause 9, issue 1615): the build tools need it. Empty means not known: a value
|
// repository's root (novox/hq ADR 0478 clause 9, issue 1615): the build tools need it. Empty means not known: a value
|
||||||
// written before the indexer learned paths has none.
|
// written before the indexer learned paths has none.
|
||||||
@@ -431,6 +436,10 @@ type Module struct {
|
|||||||
// that state. Empty in a value written before the indexer judged, which a reader takes as accepted (TakeManifestVerdict).
|
// that state. Empty in a value written before the indexer judged, which a reader takes as accepted (TakeManifestVerdict).
|
||||||
Verdict string `json:"verdict,omitempty"`
|
Verdict string `json:"verdict,omitempty"`
|
||||||
Problems []string `json:"problems,omitempty"`
|
Problems []string `json:"problems,omitempty"`
|
||||||
|
// Unknown are the fields of Manifest the judge does not know. A manifest that waits for them (VerdictManifestWaits) is
|
||||||
|
// written here as a refusal whose Problems say what it waits on and whose Unknown names the fields: a reader pinned
|
||||||
|
// before the verdict waits holds it as a refused one, and VerdictOf gives a reader that knows it the verdict waits.
|
||||||
|
Unknown []string `json:"unknown,omitempty"`
|
||||||
// Warnings are the problems of an accepted manifest a stricter judge outgrew (novox/hq ADR 0579 §1): Verdict is then
|
// Warnings are the problems of an accepted manifest a stricter judge outgrew (novox/hq ADR 0579 §1): Verdict is then
|
||||||
// empty (accepted), and the watchman raises delivery.ManifestOutgrown until the manifest is mended.
|
// empty (accepted), and the watchman raises delivery.ManifestOutgrown until the manifest is mended.
|
||||||
Warnings []string `json:"warnings,omitempty"`
|
Warnings []string `json:"warnings,omitempty"`
|
||||||
@@ -444,6 +453,9 @@ func (m Module) VerdictOf(module string) *ManifestVerdict {
|
|||||||
if m.Verdict == "" {
|
if m.Verdict == "" {
|
||||||
return &ManifestVerdict{Module: module, State: VerdictAccepted, Warnings: m.Warnings}
|
return &ManifestVerdict{Module: module, State: VerdictAccepted, Warnings: m.Warnings}
|
||||||
}
|
}
|
||||||
|
if m.Verdict == VerdictRefused && len(m.Unknown) > 0 {
|
||||||
|
return &ManifestVerdict{Module: module, State: VerdictManifestWaits, Unknown: m.Unknown}
|
||||||
|
}
|
||||||
return &ManifestVerdict{Module: module, State: m.Verdict, Problems: m.Problems}
|
return &ManifestVerdict{Module: module, State: m.Verdict, Problems: m.Problems}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,6 +550,8 @@ func (m Module) check() error {
|
|||||||
switch {
|
switch {
|
||||||
case m.Verdict != "" && m.Verdict != VerdictAccepted && m.Verdict != VerdictRefused:
|
case m.Verdict != "" && m.Verdict != VerdictAccepted && m.Verdict != VerdictRefused:
|
||||||
return fmt.Errorf("delivery: the module's verdict %q is neither %s nor %s", m.Verdict, VerdictAccepted, VerdictRefused)
|
return fmt.Errorf("delivery: the module's verdict %q is neither %s nor %s", m.Verdict, VerdictAccepted, VerdictRefused)
|
||||||
|
case len(m.Unknown) > 0 && m.Verdict != VerdictRefused:
|
||||||
|
return fmt.Errorf("delivery: a module's unknown fields are named only with the refusal a wait is written as")
|
||||||
case m.Verdict != "" && m.Manifest == "":
|
case m.Verdict != "" && m.Manifest == "":
|
||||||
return fmt.Errorf("delivery: a module carries a verdict and no manifest digest")
|
return fmt.Errorf("delivery: a module carries a verdict and no manifest digest")
|
||||||
case (m.Verdict == VerdictRefused) != (len(m.Problems) > 0):
|
case (m.Verdict == VerdictRefused) != (len(m.Problems) > 0):
|
||||||
@@ -674,9 +688,10 @@ func (f Forge) check() error {
|
|||||||
if f.Root == "" {
|
if f.Root == "" {
|
||||||
return fmt.Errorf("delivery: a merged message needs the repository's root tree")
|
return fmt.Errorf("delivery: a merged message needs the repository's root tree")
|
||||||
}
|
}
|
||||||
if len(f.Trees) == 0 {
|
// No trees is a merge that leaves the repository with no module.json anywhere (a documents repository, or one
|
||||||
return fmt.Errorf("delivery: a merged message needs the tree of at least one directory holding a module.json")
|
// whose last module was removed): it is written all the same, so the indexer takes the merge in and drops the
|
||||||
}
|
// repository's modules (novox/hq issue 1126). An older writer always sent at least one tree, so none is unambiguous;
|
||||||
|
// such a message carries no manifests, locks or paths (checkInputs below: each names a directory of its trees).
|
||||||
for dir, tree := range f.Trees {
|
for dir, tree := range f.Trees {
|
||||||
if tree == "" {
|
if tree == "" {
|
||||||
return fmt.Errorf("delivery: a merged message's tree of the directory %q is empty", dir)
|
return fmt.Errorf("delivery: a merged message's tree of the directory %q is empty", dir)
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ func TestSourceValuesRoundTrip(t *testing.T) { // 1200/AC3
|
|||||||
t.Errorf("candidate round trip: %+v %v", got, err)
|
t.Errorf("candidate round trip: %+v %v", got, err)
|
||||||
}
|
}
|
||||||
for _, m := range []Module{
|
for _, m := range []Module{
|
||||||
{FP: "src1:abc", Src: 5, Repository: "novox/mesh-sdk", Commit: "cccc"},
|
{FP: "src1:abc", Src: 5, FPSrc: 5, Repository: "novox/mesh-sdk", Commit: "cccc"},
|
||||||
{None: "a build that resolves packages when built", Repository: "novox/mesh-sdk", Commit: "cccc"},
|
{None: "a build that resolves packages when built", Repository: "novox/mesh-sdk", Commit: "cccc"},
|
||||||
} {
|
} {
|
||||||
data, err = EncodeModule(m)
|
data, err = EncodeModule(m)
|
||||||
@@ -272,9 +272,15 @@ func TestSourceValuesRefused(t *testing.T) { // 1200/AC4
|
|||||||
_, err = EncodeForge(f)
|
_, err = EncodeForge(f)
|
||||||
wantErr("merged without root", err, "root")
|
wantErr("merged without root", err, "root")
|
||||||
f = sampleForges()[1]
|
f = sampleForges()[1]
|
||||||
f.Trees = nil
|
f.Trees, f.Manifests, f.Locks, f.Paths = nil, nil, nil, nil
|
||||||
|
if data, err := EncodeForge(f); err != nil {
|
||||||
|
t.Errorf("a merged message of a repository with no module.json is refused: %v", err)
|
||||||
|
} else if got, err := DecodeForge(data); err != nil || len(got.Trees) != 0 {
|
||||||
|
t.Errorf("a merged message with no trees comes back as %+v, %v", got, err)
|
||||||
|
}
|
||||||
|
f.Manifests = map[string]string{"modules/a": "{}"}
|
||||||
_, err = EncodeForge(f)
|
_, err = EncodeForge(f)
|
||||||
wantErr("merged without trees", err, "tree")
|
wantErr("merged with a manifest and no trees", err, "modules/a")
|
||||||
f = sampleForges()[1]
|
f = sampleForges()[1]
|
||||||
f.Trees = map[string]string{"modules/a": ""}
|
f.Trees = map[string]string{"modules/a": ""}
|
||||||
_, err = EncodeForge(f)
|
_, err = EncodeForge(f)
|
||||||
|
|||||||
+57
-7
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
@@ -26,6 +27,14 @@ const (
|
|||||||
// VerdictRefused is the state of a manifest the judge found a problem in; Problems says each.
|
// VerdictRefused is the state of a manifest the judge found a problem in; Problems says each.
|
||||||
VerdictRefused = "refused"
|
VerdictRefused = "refused"
|
||||||
|
|
||||||
|
// VerdictManifestWaits is the state of a MANIFEST's verdict (not a proposal's: VerdictWaits, in placement_proposal.go,
|
||||||
|
// has the same word for a different thing, which is why this one carries Manifest in its name). It is the state of a
|
||||||
|
// manifest that carries a field the indexer's judge does not know (novox/hq ADR 0578 §1, amended): the manifest is not
|
||||||
|
// refused, because a newer judge may know the field; Unknown names each field. A reader holds a module whose manifest
|
||||||
|
// waits as it holds a refused one (ManifestVerdict.Held) and keeps what runs. In a modules value it is written as a
|
||||||
|
// refusal naming the fields (Module.Unknown), for readers that do not know this state.
|
||||||
|
VerdictManifestWaits = "waits"
|
||||||
|
|
||||||
// ManifestRefusedEvent is the indexer's event saying a manifest was refused (ManifestRefused), so the watchman
|
// ManifestRefusedEvent is the indexer's event saying a manifest was refused (ManifestRefused), so the watchman
|
||||||
// raises it as a condition beside the indexer's finding.
|
// raises it as a condition beside the indexer's finding.
|
||||||
ManifestRefusedEvent = "manifest-refused"
|
ManifestRefusedEvent = "manifest-refused"
|
||||||
@@ -39,6 +48,10 @@ const (
|
|||||||
// (novox/hq ADR 0579 §1): it stays accepted, its problems are the verdict's Warnings, and the watchman raises it as a
|
// (novox/hq ADR 0579 §1): it stays accepted, its problems are the verdict's Warnings, and the watchman raises it as a
|
||||||
// warning until the manifest is mended.
|
// warning until the manifest is mended.
|
||||||
ManifestOutgrown = "manifest-outgrown"
|
ManifestOutgrown = "manifest-outgrown"
|
||||||
|
|
||||||
|
// ManifestWaits is the finding of a manifest whose verdict waits (VerdictManifestWaits): the watchman raises it as a warning
|
||||||
|
// until a judge knows the fields or the manifest is mended.
|
||||||
|
ManifestWaits = "manifest-waits"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ManifestVerdict is the value of a verdicts key: the module whose manifest it is, accepted or refused, every problem the judge
|
// ManifestVerdict is the value of a verdicts key: the module whose manifest it is, accepted or refused, every problem the judge
|
||||||
@@ -48,6 +61,8 @@ type ManifestVerdict struct {
|
|||||||
Module string `json:"module"`
|
Module string `json:"module"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
Problems []string `json:"problems,omitempty"`
|
Problems []string `json:"problems,omitempty"`
|
||||||
|
// Unknown are the fields of a manifest the judge does not know, named when the state is VerdictManifestWaits and nowhere else.
|
||||||
|
Unknown []string `json:"unknown,omitempty"`
|
||||||
// Warnings are the problems a stricter judge found in a manifest it accepted before, digest unchanged (novox/hq ADR
|
// Warnings are the problems a stricter judge found in a manifest it accepted before, digest unchanged (novox/hq ADR
|
||||||
// 0579 §1): the verdict stays accepted.
|
// 0579 §1): the verdict stays accepted.
|
||||||
Warnings []string `json:"warnings,omitempty"`
|
Warnings []string `json:"warnings,omitempty"`
|
||||||
@@ -58,16 +73,36 @@ type ManifestVerdict struct {
|
|||||||
// Refused is whether v refuses its manifest.
|
// Refused is whether v refuses its manifest.
|
||||||
func (v ManifestVerdict) Refused() bool { return v.State == VerdictRefused }
|
func (v ManifestVerdict) Refused() bool { return v.State == VerdictRefused }
|
||||||
|
|
||||||
// Same is whether v and o say the same of a manifest: the module, the state and the problems, whoever judged and when.
|
// Waits is whether v says the manifest waits for a judge that knows its fields.
|
||||||
|
func (v ManifestVerdict) Waits() bool { return v.State == VerdictManifestWaits }
|
||||||
|
|
||||||
|
// Held is whether a reader holds the module as refused: the manifest is refused or waits.
|
||||||
|
func (v ManifestVerdict) Held() bool { return v.Refused() || v.Waits() }
|
||||||
|
|
||||||
|
// AsRefused is v as a reader pinned before the verdict waits takes it: a waiting verdict is a refusal naming what it waits
|
||||||
|
// on, any other verdict is v. For a part that hands a verdict to a program it does not deliver with itself (the checker's
|
||||||
|
// sandbox composer).
|
||||||
|
func (v ManifestVerdict) AsRefused() ManifestVerdict {
|
||||||
|
if !v.Waits() {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
v.State, v.Problems, v.Unknown = VerdictRefused, []string{v.WaitsOn()}, nil
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same is whether v and o say the same of a manifest: the module, the state, the problems and the unknown fields, whoever judged and when.
|
||||||
// The indexer writes a verdict only when it is not the same as the one it holds.
|
// The indexer writes a verdict only when it is not the same as the one it holds.
|
||||||
func (v ManifestVerdict) Same(o ManifestVerdict) bool {
|
func (v ManifestVerdict) Same(o ManifestVerdict) bool {
|
||||||
return v.Module == o.Module && v.State == o.State && slices.Equal(v.Problems, o.Problems) && slices.Equal(v.Warnings, o.Warnings)
|
return v.Module == o.Module && v.State == o.State && slices.Equal(v.Problems, o.Problems) && slices.Equal(v.Unknown, o.Unknown) && slices.Equal(v.Warnings, o.Warnings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v ManifestVerdict) check() error {
|
func (v ManifestVerdict) check() error {
|
||||||
if err := checkName("module", v.Module); err != nil {
|
if err := checkName("module", v.Module); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if v.State != VerdictManifestWaits && len(v.Unknown) > 0 {
|
||||||
|
return fmt.Errorf("delivery: the verdict on %s names unknown fields and does not wait", v.Module)
|
||||||
|
}
|
||||||
switch v.State {
|
switch v.State {
|
||||||
case VerdictAccepted:
|
case VerdictAccepted:
|
||||||
if len(v.Problems) > 0 {
|
if len(v.Problems) > 0 {
|
||||||
@@ -80,8 +115,12 @@ func (v ManifestVerdict) check() error {
|
|||||||
if len(v.Warnings) > 0 {
|
if len(v.Warnings) > 0 {
|
||||||
return fmt.Errorf("delivery: the verdict on %s refuses its manifest and warns: a refusal names problems", v.Module)
|
return fmt.Errorf("delivery: the verdict on %s refuses its manifest and warns: a refusal names problems", v.Module)
|
||||||
}
|
}
|
||||||
|
case VerdictManifestWaits:
|
||||||
|
if len(v.Unknown) == 0 || len(v.Problems) > 0 || len(v.Warnings) > 0 {
|
||||||
|
return fmt.Errorf("delivery: the verdict on %s waits and names no unknown field, or names problems or warnings: a wait names the fields", v.Module)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("delivery: the verdict on %s is %q, neither %s nor %s", v.Module, v.State, VerdictAccepted, VerdictRefused)
|
return fmt.Errorf("delivery: the verdict on %s is %q, none of %s, %s and %s", v.Module, v.State, VerdictAccepted, VerdictRefused, VerdictManifestWaits)
|
||||||
}
|
}
|
||||||
if v.Judge == "" {
|
if v.Judge == "" {
|
||||||
return fmt.Errorf("delivery: the verdict on %s names no judge", v.Module)
|
return fmt.Errorf("delivery: the verdict on %s names no judge", v.Module)
|
||||||
@@ -120,26 +159,37 @@ func DecodeManifestVerdict(data []byte) (ManifestVerdict, error) {
|
|||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TakeManifestVerdict is how a reader takes the verdict on a manifest (ADR 0578 §3): refused, with its problems, only when the
|
// TakeManifestVerdict is how a reader takes the verdict on a manifest (ADR 0578 §3): held, with its problems, when the
|
||||||
// indexer wrote a refusal. A missing verdict (nil) is taken as accepted, because every manifest the indexer holds before
|
// indexer wrote a refusal, or, for a manifest that waits, with the words that say which fields it waits on. A missing verdict (nil) is taken as accepted, because every manifest the indexer holds before
|
||||||
// it has judged it was taken in under the judges that ran until then; that lasts until the indexer's first pass after its
|
// it has judged it was taken in under the judges that ran until then; that lasts until the indexer's first pass after its
|
||||||
// delivery has written every verdict. A reader holds a refused module and names it with Why, and reads the rest.
|
// delivery has written every verdict. A reader holds a refused module and names it with Why, and reads the rest.
|
||||||
func TakeManifestVerdict(v *ManifestVerdict) (refused bool, problems []string) {
|
func TakeManifestVerdict(v *ManifestVerdict) (refused bool, problems []string) {
|
||||||
if v == nil || !v.Refused() {
|
if v == nil || !v.Held() {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
if v.Waits() {
|
||||||
|
return true, []string{v.WaitsOn()}
|
||||||
|
}
|
||||||
return true, v.Problems
|
return true, v.Problems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WaitsOn says, in words, what a waiting manifest waits on.
|
||||||
|
func (v ManifestVerdict) WaitsOn() string {
|
||||||
|
return "it carries " + strings.Join(v.Unknown, ", ") + ", which the indexer's judge does not know yet"
|
||||||
|
}
|
||||||
|
|
||||||
// Why is a refusal in a reader's words: the module, that the indexer's judge refused its manifest, and each problem.
|
// Why is a refusal in a reader's words: the module, that the indexer's judge refused its manifest, and each problem.
|
||||||
func (v ManifestVerdict) Why() string {
|
func (v ManifestVerdict) Why() string {
|
||||||
if !v.Refused() {
|
if !v.Held() {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
judge := "the indexer's judge"
|
judge := "the indexer's judge"
|
||||||
if v.Judge != "" {
|
if v.Judge != "" {
|
||||||
judge += " (" + v.Judge + ")"
|
judge += " (" + v.Judge + ")"
|
||||||
}
|
}
|
||||||
|
if v.Waits() {
|
||||||
|
return fmt.Sprintf("%s holds the manifest of %s: %s", judge, v.Module, v.WaitsOn())
|
||||||
|
}
|
||||||
why := fmt.Sprintf("%s refused the manifest of %s:", judge, v.Module)
|
why := fmt.Sprintf("%s refused the manifest of %s:", judge, v.Module)
|
||||||
for _, p := range v.Problems {
|
for _, p := range v.Problems {
|
||||||
why += " " + p + ";"
|
why += " " + p + ";"
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ func TestAManifestVerdictIsTheContractReadersTake(t *testing.T) {
|
|||||||
"refused, no problem": {Module: "m", State: VerdictRefused, Judge: "j"},
|
"refused, no problem": {Module: "m", State: VerdictRefused, Judge: "j"},
|
||||||
"accepted, a problem": {Module: "m", State: VerdictAccepted, Problems: []string{"x"}, Judge: "j"},
|
"accepted, a problem": {Module: "m", State: VerdictAccepted, Problems: []string{"x"}, Judge: "j"},
|
||||||
"refused, a warning": {Module: "m", State: VerdictRefused, Problems: []string{"x"}, Warnings: []string{"y"}, Judge: "j"},
|
"refused, a warning": {Module: "m", State: VerdictRefused, Problems: []string{"x"}, Warnings: []string{"y"}, Judge: "j"},
|
||||||
|
"waits, no field": {Module: "m", State: VerdictManifestWaits, Judge: "j"},
|
||||||
|
"waits, a problem": {Module: "m", State: VerdictManifestWaits, Unknown: []string{"f"}, Problems: []string{"x"}, Judge: "j"},
|
||||||
|
"fields, accepted": {Module: "m", State: VerdictAccepted, Unknown: []string{"f"}, Judge: "j"},
|
||||||
} {
|
} {
|
||||||
if _, err := EncodeManifestVerdict(bad); err == nil {
|
if _, err := EncodeManifestVerdict(bad); err == nil {
|
||||||
t.Errorf("%s: encoded", name)
|
t.Errorf("%s: encoded", name)
|
||||||
@@ -100,4 +103,38 @@ func TestAManifestVerdictIsTheContractReadersTake(t *testing.T) {
|
|||||||
if id := heard.EventID(); id != "manifest-accepted-"+digest+"-src2:j" {
|
if id := heard.EventID(); id != "manifest-accepted-"+digest+"-src2:j" {
|
||||||
t.Fatalf("the accepted event's id is %q", id)
|
t.Fatalf("the accepted event's id is %q", id)
|
||||||
}
|
}
|
||||||
|
// A manifest with a field the judge does not know waits (clause 3 of the record on a release's Approve): readers hold it
|
||||||
|
// as a refused one, it round-trips through a modules value, an old value decodes unchanged, and it is not a refusal.
|
||||||
|
waits := ManifestVerdict{Module: "nats", State: VerdictManifestWaits, Unknown: []string{"core-credentials", "tool-runner"}, Judge: "j"}
|
||||||
|
data, err = EncodeManifestVerdict(waits)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if back, err = DecodeManifestVerdict(data); err != nil || !back.Same(waits) || back.Same(ManifestVerdict{Module: "nats", State: VerdictManifestWaits, Unknown: []string{"x"}, Judge: "j"}) {
|
||||||
|
t.Fatalf("a waiting verdict: %+v %v", back, err)
|
||||||
|
}
|
||||||
|
if r, p := TakeManifestVerdict(&waits); !r || waits.Refused() || !waits.Held() || !strings.Contains(p[0], "core-credentials, tool-runner") ||
|
||||||
|
!strings.Contains(waits.Why(), "holds the manifest of nats") {
|
||||||
|
t.Fatalf("a waiting verdict is held like a refusal: %v %v %q", r, p, waits.Why())
|
||||||
|
}
|
||||||
|
if r := waits.AsRefused(); !r.Refused() || r.Waits() || len(r.Problems) != 1 || len(r.Unknown) != 0 || refused.AsRefused().State != VerdictRefused {
|
||||||
|
t.Fatalf("a waiting verdict as a reader before it takes it: %+v", r)
|
||||||
|
}
|
||||||
|
// In a modules value a wait is written as a refusal naming the fields (an older reader holds it as refused).
|
||||||
|
w := Module{FP2: "src2:" + strings.Repeat("b", 64), Manifest: strings.Repeat("c", 64), Repository: "novox/x", Commit: "c",
|
||||||
|
Verdict: VerdictRefused, Problems: []string{waits.WaitsOn()}, Unknown: []string{"f"}}
|
||||||
|
data, err = EncodeModule(w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got, err = DecodeModule(data); err != nil || got.VerdictOf("x").Unknown[0] != "f" || !got.VerdictOf("x").Waits() || got.VerdictOf("x").Refused() {
|
||||||
|
t.Fatalf("the module's waiting verdict: %+v %v", got, err)
|
||||||
|
}
|
||||||
|
if r, p := TakeManifestVerdict(got.VerdictOf("x")); !r || !strings.Contains(p[0], "which the indexer's judge does not know yet") {
|
||||||
|
t.Fatalf("a waiting module is held: %v %v", r, p)
|
||||||
|
}
|
||||||
|
w.Verdict, w.Problems = "", nil
|
||||||
|
if _, err := EncodeModule(w); err == nil {
|
||||||
|
t.Fatal("unknown fields with no wait were encoded")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ func TestAWantNamesItsComposition(t *testing.T) {
|
|||||||
// ADR 0547 §2, §3 (issue 1126): from the second milestone a want names the build alone, and the yes a person gave.
|
// ADR 0547 §2, §3 (issue 1126): from the second milestone a want names the build alone, and the yes a person gave.
|
||||||
{FP: fp2of('a'), Self: true},
|
{FP: fp2of('a'), Self: true},
|
||||||
{FP: fp2of('a'), Self: true, Yes: hashOf('3')},
|
{FP: fp2of('a'), Self: true, Yes: hashOf('3')},
|
||||||
|
// ADR 0601 §1: the yes the release's Approve gave names the release.
|
||||||
|
{FP: fp2of('a'), Self: true, Yes: hashOf('3'), Release: "feat/l3b-bus"},
|
||||||
} {
|
} {
|
||||||
raw, err := EncodeWant(w)
|
raw, err := EncodeWant(w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -43,6 +45,8 @@ func TestAWantNamesItsComposition(t *testing.T) {
|
|||||||
{Want{FP: fp2of('a'), Self: true, Composed: hashOf('1')}, "names a composition"},
|
{Want{FP: fp2of('a'), Self: true, Composed: hashOf('1')}, "names a composition"},
|
||||||
{Want{FP: fp2of('a'), Self: true, Yes: "sha256:12"}, "a want's yes allows"},
|
{Want{FP: fp2of('a'), Self: true, Yes: "sha256:12"}, "a want's yes allows"},
|
||||||
{Want{Removed: true, Self: true}, "removed want names no"},
|
{Want{Removed: true, Self: true}, "removed want names no"},
|
||||||
|
{Want{Removed: true, Release: "feat/l3b-bus"}, "removed want names no"},
|
||||||
|
{Want{FP: fp2of('a'), Self: true, Release: "feat/l3b-bus"}, "and no yes it gave"},
|
||||||
{Want{FP: fp2of('a'), Composed: "sha256:12"}, "not sha256"},
|
{Want{FP: fp2of('a'), Composed: "sha256:12"}, "not sha256"},
|
||||||
{Want{Removed: true, Composed: hashOf('1')}, "removed want names no"},
|
{Want{Removed: true, Composed: hashOf('1')}, "removed want names no"},
|
||||||
{Want{FP: fp2of('a'), Composed: hashOf('1'), Backup: "later"}, `backup answer "later"`},
|
{Want{FP: fp2of('a'), Composed: hashOf('1'), Backup: "later"}, `backup answer "later"`},
|
||||||
|
|||||||
@@ -482,7 +482,7 @@ func (h *Harness) retirementBody(change string, consumers []map[string]any, extr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RetirementTools are the four tools every provider serves for retirement, the same names in every
|
// RetirementTools are the four tools every provider serves for retirement, the same names in every
|
||||||
// module: the controller's `retire` and `cleanup` verbs ask them on the provider's machine.
|
// module: a caller asks them on the provider's machine, with the operator's answer named in why and by.
|
||||||
func RetirementTools(h *Harness) []stdio.Tool {
|
func RetirementTools(h *Harness) []stdio.Tool {
|
||||||
if h == nil {
|
if h == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -493,7 +493,7 @@ func RetirementTools(h *Harness) []stdio.Tool {
|
|||||||
who := map[string]any{
|
who := map[string]any{
|
||||||
"why": map[string]any{"type": "string", "description": "why — required, kept in the hand-act log"},
|
"why": map[string]any{"type": "string", "description": "why — required, kept in the hand-act log"},
|
||||||
"by": map[string]any{"type": "string", "description": "who decided"},
|
"by": map[string]any{"type": "string", "description": "who decided"},
|
||||||
"via": map[string]any{"type": "string", "description": "mesh-controller when asked through its verbs"},
|
"via": map[string]any{"type": "string", "description": "the part or session that asks, when not the caller itself"},
|
||||||
}
|
}
|
||||||
withSet := map[string]any{"consumers": map[string]any{"type": "array", "items": map[string]any{"type": "string"},
|
withSet := map[string]any{"consumers": map[string]any{"type": "array", "items": map[string]any{"type": "string"},
|
||||||
"description": "the set, exactly as provisioner_retirement names it"}}
|
"description": "the set, exactly as provisioner_retirement names it"}}
|
||||||
@@ -518,7 +518,8 @@ func RetirementTools(h *Harness) []stdio.Tool {
|
|||||||
}},
|
}},
|
||||||
{Name: "provisioner_retire_approve",
|
{Name: "provisioner_retire_approve",
|
||||||
Description: "Retire exactly the set waiting for a person (or the set rejected earlier): access disabled, data " +
|
Description: "Retire exactly the set waiting for a person (or the set rejected earlier): access disabled, data " +
|
||||||
"kept and marked to delete. Use the controller's `retire approve`, which records the hand act.",
|
"kept and marked to delete. It asks nobody: the operator's answer on the operator channel to the provisioner's " +
|
||||||
|
"ask (novox/hq ADR 0570 §5) is named in why and by, which the hand-act log keeps.",
|
||||||
Input: withSet,
|
Input: withSet,
|
||||||
Run: func(args map[string]any) (any, error) {
|
Run: func(args map[string]any) (any, error) {
|
||||||
ctx, cancel := ask()
|
ctx, cancel := ask()
|
||||||
@@ -530,8 +531,9 @@ func RetirementTools(h *Harness) []stdio.Tool {
|
|||||||
return map[string]any{"retired": orEmptyList(done)}, nil
|
return map[string]any{"retired": orEmptyList(done)}, nil
|
||||||
}},
|
}},
|
||||||
{Name: "provisioner_retire_reject",
|
{Name: "provisioner_retire_reject",
|
||||||
Description: "Keep the set waiting for a person active. Use the controller's `retire reject`.",
|
Description: "Keep the set waiting for a person active. It asks nobody: the operator's answer on the operator channel " +
|
||||||
Input: withSet,
|
"(novox/hq ADR 0570 §5) is named in why and by, which the hand-act log keeps.",
|
||||||
|
Input: withSet,
|
||||||
Run: func(args map[string]any) (any, error) {
|
Run: func(args map[string]any) (any, error) {
|
||||||
kept, err := h.Reject(strs(args["consumers"]), argString(args, "why"), argString(args, "by"), argString(args, "via"))
|
kept, err := h.Reject(strs(args["consumers"]), argString(args, "why"), argString(args, "by"), argString(args, "via"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -541,7 +543,8 @@ func RetirementTools(h *Harness) []stdio.Tool {
|
|||||||
}},
|
}},
|
||||||
{Name: "provisioner_delete",
|
{Name: "provisioner_delete",
|
||||||
Description: "Delete one RETIRED consumer — its login and its data — for good. Refused for anything active or " +
|
Description: "Delete one RETIRED consumer — its login and its data — for good. Refused for anything active or " +
|
||||||
"asked for. Use the controller's `cleanup delete`, which records the hand act.",
|
"asked for. It asks nobody: ask the operator on the operator channel first, and name that answer in why and " +
|
||||||
|
"by, which the hand-act log keeps (novox/hq ADR 0604 §8).",
|
||||||
Input: withOne,
|
Input: withOne,
|
||||||
Run: func(args map[string]any) (any, error) {
|
Run: func(args map[string]any) (any, error) {
|
||||||
consumer := argString(args, "consumer")
|
consumer := argString(args, "consumer")
|
||||||
|
|||||||
@@ -482,6 +482,10 @@ func TestTheToolsAnswer(t *testing.T) {
|
|||||||
tools := map[string]func(map[string]any) (any, error){}
|
tools := map[string]func(map[string]any) (any, error){}
|
||||||
for _, tool := range RetirementTools(w.h) {
|
for _, tool := range RetirementTools(w.h) {
|
||||||
tools[tool.Name] = tool.Run
|
tools[tool.Name] = tool.Run
|
||||||
|
// L3: the delete, approve and reject name no removed controller verb, and say where the operator answers (ADR 0604 §8).
|
||||||
|
if tool.Name != "provisioner_retirement" && (strings.Contains(tool.Description, "controller") || !strings.Contains(tool.Description, "operator channel")) {
|
||||||
|
t.Errorf("%s says %q", tool.Name, tool.Description)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(tools) != 4 {
|
if len(tools) != 4 {
|
||||||
t.Fatalf("%d tools", len(tools))
|
t.Fatalf("%d tools", len(tools))
|
||||||
|
|||||||
Reference in New Issue
Block a user