Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85ba0a9a70 | ||
|
|
73c8f15151 |
@@ -2,10 +2,11 @@ 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.
|
||||
// node-engine's composer (mesh-host main) beside this mesh-sdk (ADR 0548 §6), and mesh-host main names them
|
||||
// (RuntimeModule, CoreCredentialModules, FromBus). Nothing in the final L3 release's heads uses any of them, but that
|
||||
// release still builds beside mesh-host 798d73e, so it keeps this file. The next release that changes mesh-sdk, once
|
||||
// the final L3 release's node-engine runs on every node, removes this file and its exemption in
|
||||
// TestNoCatalogueManifestNamesAmqp.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A reader capability is one thing of a manifest a reader of manifests (a node-engine) can read (novox/hq ADR 0614 §4):
|
||||
// a manifest word, a form, or a seat name of the shared contract it is built with. Each is a string of one of three
|
||||
// kinds:
|
||||
//
|
||||
// - `word:<path>`, a key the lenient decoder reads, by its path of keys from the top of the manifest: `word:claims`,
|
||||
// `word:claims.serves`. The key of a map (a setting's name, a requirement's) is no part of a path, and an entry of
|
||||
// a list is read as the list: `word:settings.default`, `word:provides.credential`.
|
||||
// - `form:<name>`, a form a manifest may be written in: the config form (`form:config`, ADR 0348 §1) and each
|
||||
// reference form a value may hold, `${<name>:…}` (`form:setting`, `form:dir`).
|
||||
// - `seat:<name>`, a seat of the contract's seat table, as a manifest names one in a claim, a use, an invoke or a
|
||||
// bucket's share.
|
||||
//
|
||||
// ManifestReads is what this contract reads; ManifestNeeds what one manifest's own words, forms and seat names need;
|
||||
// BaselineManifestReads what a node-engine that states none is read as stating. What a module's build reads once it
|
||||
// runs is never asked.
|
||||
const (
|
||||
ReadWord = "word:"
|
||||
ReadForm = "form:"
|
||||
ReadSeat = "seat:"
|
||||
)
|
||||
|
||||
// referenceForms are the reference forms a manifest's values may hold, `${<name>:…}`, as this contract fills them. A new
|
||||
// reference form is added here in the change that teaches the contract to read it, so a node-engine built with it
|
||||
// states it (ADR 0612 §2's new reference form is one).
|
||||
var referenceForms = []string{"access", "backup", "bound", "collected", "config", "consumer", "consumers",
|
||||
"contribution", "dir", "machine", "port", "seat", "secret", "setting"}
|
||||
|
||||
// manifestTopWords are the top-level keys Manifest.UnmarshalJSON reads apart from the fields of manifestFields: each
|
||||
// read whole, its inside a map of the module's own names (requirements, local names) and values, no words of the
|
||||
// contract's beneath it. `requires` and `config` are read as their fields are (manifestNeeds).
|
||||
var manifestTopWords = []string{"secrets", "contributes", OptionalRequiresKey, RequireSharesKey}
|
||||
|
||||
// wireKeys are the keys of a block whose decoder reads its object by keys of its own, not by its fields' tags (its
|
||||
// UnmarshalJSON): the keys as that decoder reads them, none of them a block.
|
||||
var wireKeys = map[reflect.Type][]string{
|
||||
reflect.TypeOf(OfferIdentity{}): {"max", "in"},
|
||||
reflect.TypeOf(DataBackup{}): {"dump", "into"},
|
||||
reflect.TypeOf(OwnSecret{}): {"path", "taken", "issued-by", "prompt", "owner"},
|
||||
}
|
||||
|
||||
// ManifestReads is every reader capability of this contract, sorted: what a node-engine built with it states in its
|
||||
// node record (delivery.NodeRecord.ManifestReads).
|
||||
func ManifestReads() []string {
|
||||
set := map[string]bool{}
|
||||
for _, w := range contractWords() {
|
||||
set[ReadWord+w] = true
|
||||
}
|
||||
for _, f := range referenceForms {
|
||||
set[ReadForm+f] = true
|
||||
}
|
||||
for _, s := range DefaultSeats() {
|
||||
set[ReadSeat+s.Name] = true
|
||||
}
|
||||
return sortedSet(set)
|
||||
}
|
||||
|
||||
// baselineManifestReads is the baseline, generated once from mesh-sdk f02f142 (the contract every node-engine ran
|
||||
// before any stated what it reads) by ManifestReads as this file computes it: one reader capability a line.
|
||||
//
|
||||
//go:embed manifest_reads_baseline.txt
|
||||
var baselineManifestReads string
|
||||
|
||||
// BaselineManifestReads is what a node-engine that states no reader capability is read as stating (novox/hq ADR 0614
|
||||
// §4): the words, forms and seat names of the shared contract as it stood before any node-engine stated them.
|
||||
func BaselineManifestReads() []string {
|
||||
var out []string
|
||||
for _, line := range strings.Split(baselineManifestReads, "\n") {
|
||||
if line = strings.TrimSpace(line); line != "" {
|
||||
out = append(out, line)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// StatedManifestReads is what a node-engine's statement is read as: stated itself, or the baseline when it states none
|
||||
// (an engine built before the statement existed).
|
||||
func StatedManifestReads(stated []string) []string {
|
||||
if len(stated) == 0 {
|
||||
return BaselineManifestReads()
|
||||
}
|
||||
return stated
|
||||
}
|
||||
|
||||
// MissingManifestReads is each of needs that stated, read as StatedManifestReads reads it, lacks: sorted, nil when it
|
||||
// lacks none.
|
||||
func MissingManifestReads(needs, stated []string) []string {
|
||||
have := map[string]bool{}
|
||||
for _, s := range StatedManifestReads(stated) {
|
||||
have[s] = true
|
||||
}
|
||||
var missing []string
|
||||
for _, n := range needs {
|
||||
if !have[n] {
|
||||
missing = append(missing, n)
|
||||
}
|
||||
}
|
||||
sort.Strings(missing)
|
||||
return slices.Compact(missing)
|
||||
}
|
||||
|
||||
// ManifestNeeds is every reader capability a manifest's own words, forms and seat names need, sorted, read as the
|
||||
// lenient decoder walks the manifest: only what this contract knows is named, since what it does not know is set aside
|
||||
// (Manifest.SetAside). An error is only bytes that are no JSON object.
|
||||
func ManifestNeeds(raw []byte) ([]string, error) {
|
||||
var top map[string]any
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
if err := dec.Decode(&top); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
used := map[string]bool{}
|
||||
for _, f := range referenceForms {
|
||||
if bytes.Contains(raw, []byte("${"+f+":")) {
|
||||
used[ReadForm+f] = true
|
||||
}
|
||||
}
|
||||
if _, has := top[ConfigField]; has || takesConfigKey.Match(raw) {
|
||||
used[ReadForm+"config"] = true
|
||||
}
|
||||
words := map[string]bool{}
|
||||
fields := structFields(reflect.TypeOf(manifestFields{}))
|
||||
for _, k := range sortedAnyKeys(top) {
|
||||
v := top[k]
|
||||
switch {
|
||||
case k == ConfigField:
|
||||
// The config form is the settings form's words under another key (config.go).
|
||||
words[SettingsField] = true
|
||||
walkValue(v, fields[SettingsField].Type, SettingsField, words)
|
||||
case k == "requires":
|
||||
words[k] = true
|
||||
for _, e := range asList(v) {
|
||||
obj, isObj := e.(map[string]any)
|
||||
if !isObj {
|
||||
continue
|
||||
}
|
||||
if _, bus := obj["storage"]; bus {
|
||||
// A bus-state entry of `requires` (BusStateEntry): the stored form's `bus-state`.
|
||||
words[BusStateField] = true
|
||||
walkValue(obj, reflect.TypeOf(BusStateEntry{}), BusStateField, words)
|
||||
continue
|
||||
}
|
||||
walkValue(obj, reflect.TypeOf(requirementForm{}), k, words)
|
||||
}
|
||||
case slices.Contains(manifestTopWords, k):
|
||||
words[k] = true
|
||||
default:
|
||||
if f, known := fields[k]; known {
|
||||
words[k] = true
|
||||
walkValue(v, f.Type, k, words)
|
||||
}
|
||||
}
|
||||
}
|
||||
for w := range words {
|
||||
used[ReadWord+w] = true
|
||||
}
|
||||
for _, s := range seatNamesUsed(top) {
|
||||
used[ReadSeat+s] = true
|
||||
}
|
||||
return sortedSet(used), nil
|
||||
}
|
||||
|
||||
// seatNamesUsed is each seat of the contract's table the manifest names: in a claim, a use, an invoke of a seat's verb,
|
||||
// and a bucket's share with a seat.
|
||||
func seatNamesUsed(top map[string]any) []string {
|
||||
known := map[string]bool{}
|
||||
for _, s := range DefaultSeats() {
|
||||
known[s.Name] = true
|
||||
}
|
||||
set := map[string]bool{}
|
||||
add := func(name string) {
|
||||
if known[name] {
|
||||
set[name] = true
|
||||
}
|
||||
}
|
||||
for _, c := range asList(top["claims"]) {
|
||||
if obj, ok := c.(map[string]any); ok {
|
||||
name, _ := obj["name"].(string)
|
||||
add(name)
|
||||
}
|
||||
}
|
||||
for _, u := range asList(top["uses"]) {
|
||||
name, _ := u.(string)
|
||||
add(name)
|
||||
}
|
||||
for _, i := range asList(top["invokes"]) {
|
||||
text, _ := i.(string)
|
||||
if seat, _, ok := SeatInvoke(text); ok {
|
||||
add(seat)
|
||||
}
|
||||
}
|
||||
for _, r := range asList(top["requires"]) {
|
||||
obj, _ := r.(map[string]any)
|
||||
for _, share := range asList(obj["shares"]) {
|
||||
text, _ := share.(string)
|
||||
if seat, ok := strings.CutPrefix(text, "seat:"); ok {
|
||||
add(seat)
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortedSet(set)
|
||||
}
|
||||
|
||||
// contractWords is every word of this contract, by path (ReadWord): the fields of manifestFields, the keys the
|
||||
// manifest's own decoder reads beside them, and the long form of a requirement.
|
||||
func contractWords() []string {
|
||||
set := map[string]bool{}
|
||||
walkType(reflect.TypeOf(manifestFields{}), "", set, map[reflect.Type]bool{})
|
||||
set["requires"] = true
|
||||
walkType(reflect.TypeOf(requirementForm{}), "requires", set, map[reflect.Type]bool{})
|
||||
for _, w := range manifestTopWords {
|
||||
set[w] = true
|
||||
}
|
||||
return sortedSet(set)
|
||||
}
|
||||
|
||||
// walkType adds every word of t at path to set: a struct's fields by their keys, a list's and a map's elements as
|
||||
// themselves; onPath stops a type that holds itself.
|
||||
func walkType(t reflect.Type, path string, set map[string]bool, onPath map[reflect.Type]bool) {
|
||||
t = deref(t)
|
||||
if keys, custom := wireKeys[t]; custom {
|
||||
for _, k := range keys {
|
||||
set[joinWord(path, k)] = true
|
||||
}
|
||||
return
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Struct:
|
||||
if onPath[t] {
|
||||
return
|
||||
}
|
||||
onPath[t] = true
|
||||
defer delete(onPath, t)
|
||||
for name, f := range structFields(t) {
|
||||
word := joinWord(path, name)
|
||||
set[word] = true
|
||||
walkType(f.Type, word, set, onPath)
|
||||
}
|
||||
case reflect.Slice, reflect.Array, reflect.Map:
|
||||
if t.Elem().Kind() == reflect.Uint8 {
|
||||
return // bytes: a value, never a block
|
||||
}
|
||||
walkType(t.Elem(), path, set, onPath)
|
||||
}
|
||||
}
|
||||
|
||||
// walkValue adds to used every word of t that v, a value decoded from JSON, has at path.
|
||||
func walkValue(v any, t reflect.Type, path string, used map[string]bool) {
|
||||
t = deref(t)
|
||||
if keys, custom := wireKeys[t]; custom {
|
||||
obj, _ := v.(map[string]any)
|
||||
for _, k := range keys {
|
||||
if _, has := obj[k]; has {
|
||||
used[joinWord(path, k)] = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Struct:
|
||||
obj, isObj := v.(map[string]any)
|
||||
if !isObj {
|
||||
return // a short form (a name for an offer): no words inside it
|
||||
}
|
||||
fields := structFields(t)
|
||||
for _, k := range sortedAnyKeys(obj) {
|
||||
f, known := fields[k]
|
||||
if !known {
|
||||
continue // set aside by the decoder, so no word of this contract
|
||||
}
|
||||
word := joinWord(path, k)
|
||||
used[word] = true
|
||||
walkValue(obj[k], f.Type, word, used)
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
if t.Elem().Kind() == reflect.Uint8 {
|
||||
return
|
||||
}
|
||||
for _, item := range asList(v) {
|
||||
walkValue(item, t.Elem(), path, used)
|
||||
}
|
||||
case reflect.Map:
|
||||
obj, _ := v.(map[string]any)
|
||||
for _, k := range sortedAnyKeys(obj) {
|
||||
walkValue(obj[k], t.Elem(), path, used)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// structFields are a struct's fields by the key the JSON decoder reads each by: its tag's name, else its own name; an
|
||||
// untagged embedded struct's fields as the struct's own; never one tagged "-" or unexported.
|
||||
func structFields(t reflect.Type) map[string]reflect.StructField {
|
||||
out := map[string]reflect.StructField{}
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
f := t.Field(i)
|
||||
tag := f.Tag.Get("json")
|
||||
name, _, _ := strings.Cut(tag, ",")
|
||||
if name == "-" {
|
||||
continue
|
||||
}
|
||||
if f.Anonymous && name == "" && deref(f.Type).Kind() == reflect.Struct {
|
||||
for k, inner := range structFields(deref(f.Type)) {
|
||||
if _, shadowed := out[k]; !shadowed {
|
||||
out[k] = inner
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !f.IsExported() {
|
||||
continue
|
||||
}
|
||||
if name == "" {
|
||||
name = f.Name
|
||||
}
|
||||
out[name] = f
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// untaggedBlocks are the struct types among the contract's words whose fields carry no key of their own and which
|
||||
// wireKeys does not name: their words would be the Go names, which no decoder reads. A row keeps it empty.
|
||||
func untaggedBlocks() []string {
|
||||
seen := map[reflect.Type]bool{}
|
||||
var out []string
|
||||
var walk func(t reflect.Type)
|
||||
walk = func(t reflect.Type) {
|
||||
t = deref(t)
|
||||
if seen[t] {
|
||||
return
|
||||
}
|
||||
seen[t] = true
|
||||
if _, custom := wireKeys[t]; custom {
|
||||
return
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Struct:
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
f := t.Field(i)
|
||||
if f.IsExported() && !f.Anonymous && f.Tag.Get("json") == "" {
|
||||
out = append(out, t.String())
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, f := range structFields(t) {
|
||||
walk(f.Type)
|
||||
}
|
||||
case reflect.Slice, reflect.Array, reflect.Map:
|
||||
walk(t.Elem())
|
||||
}
|
||||
}
|
||||
walk(reflect.TypeOf(manifestFields{}))
|
||||
walk(reflect.TypeOf(requirementForm{}))
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func deref(t reflect.Type) reflect.Type {
|
||||
for t.Kind() == reflect.Pointer {
|
||||
t = t.Elem()
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func joinWord(path, key string) string {
|
||||
if path == "" {
|
||||
return key
|
||||
}
|
||||
return path + "." + key
|
||||
}
|
||||
|
||||
func asList(v any) []any {
|
||||
list, _ := v.([]any)
|
||||
return list
|
||||
}
|
||||
|
||||
func sortedSet(set map[string]bool) []string {
|
||||
out := make([]string, 0, len(set))
|
||||
for k := range set {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
form:access
|
||||
form:backup
|
||||
form:bound
|
||||
form:collected
|
||||
form:config
|
||||
form:consumer
|
||||
form:consumers
|
||||
form:contribution
|
||||
form:dir
|
||||
form:machine
|
||||
form:port
|
||||
form:seat
|
||||
form:secret
|
||||
form:setting
|
||||
seat:git
|
||||
seat:mesh-artifact-store
|
||||
seat:mesh-artifact-sweep
|
||||
seat:mesh-broker
|
||||
seat:mesh-catalog
|
||||
seat:mesh-conditions
|
||||
seat:mesh-config
|
||||
seat:mesh-delivery
|
||||
seat:mesh-delivery-checker
|
||||
seat:mesh-dns-resolver
|
||||
seat:mesh-enrolment
|
||||
seat:mesh-hand-acts
|
||||
seat:mesh-healers
|
||||
seat:mesh-placement
|
||||
seat:mesh-provisioner
|
||||
seat:mesh-self-check
|
||||
seat:mesh-store
|
||||
seat:mesh-vault
|
||||
seat:mesh-wants
|
||||
seat:mesh-watchdogs
|
||||
seat:node-backup
|
||||
seat:node-bar
|
||||
seat:node-build-go
|
||||
seat:node-build-image
|
||||
seat:node-build-node
|
||||
seat:node-clipboard
|
||||
seat:node-compositor
|
||||
seat:node-container-runtime
|
||||
seat:node-display-server
|
||||
seat:node-display-session
|
||||
seat:node-engine
|
||||
seat:node-environment
|
||||
seat:node-files
|
||||
seat:node-hostname
|
||||
seat:node-hotkeys
|
||||
seat:node-intrusion-prevention
|
||||
seat:node-launcher
|
||||
seat:node-lock-screen
|
||||
seat:node-login-manager
|
||||
seat:node-login-shell
|
||||
seat:node-message-bus
|
||||
seat:node-mounts
|
||||
seat:node-nfs-server
|
||||
seat:node-notifier
|
||||
seat:node-package-manager
|
||||
seat:node-packet-filter
|
||||
seat:node-power
|
||||
seat:node-provisioner
|
||||
seat:node-reads
|
||||
seat:node-resolver
|
||||
seat:node-secret-service
|
||||
seat:node-service-manager
|
||||
seat:node-terminal-emulator
|
||||
seat:node-uplink
|
||||
seat:npm-package-registry
|
||||
seat:the-private-network
|
||||
word:accesses
|
||||
word:accesses.id
|
||||
word:accesses.mode
|
||||
word:accesses.path
|
||||
word:binds
|
||||
word:build
|
||||
word:build.artifacts
|
||||
word:build.artifacts.binary
|
||||
word:build.artifacts.bus
|
||||
word:build.artifacts.bus.asks
|
||||
word:build.artifacts.bus.emits
|
||||
word:build.artifacts.bus.invokes
|
||||
word:build.artifacts.bus.queues
|
||||
word:build.artifacts.bus.removes
|
||||
word:build.artifacts.bus.state
|
||||
word:build.artifacts.compiles
|
||||
word:build.artifacts.context
|
||||
word:build.artifacts.context.ref
|
||||
word:build.artifacts.context.repository
|
||||
word:build.artifacts.context.seat
|
||||
word:build.artifacts.entrypoints
|
||||
word:build.artifacts.env
|
||||
word:build.artifacts.external
|
||||
word:build.artifacts.file
|
||||
word:build.artifacts.from
|
||||
word:build.artifacts.kind
|
||||
word:build.artifacts.language
|
||||
word:build.artifacts.loads
|
||||
word:build.artifacts.name
|
||||
word:build.artifacts.names-on-purpose
|
||||
word:build.artifacts.paths
|
||||
word:build.artifacts.sha256
|
||||
word:build.artifacts.system
|
||||
word:build.artifacts.target
|
||||
word:build.artifacts.url
|
||||
word:build.artifacts.version
|
||||
word:build.artifacts.within
|
||||
word:build.on
|
||||
word:build.on.arg
|
||||
word:build.on.artifact
|
||||
word:build.on.image
|
||||
word:build.on.module
|
||||
word:bundles
|
||||
word:bundles.binary
|
||||
word:bundles.bus
|
||||
word:bundles.bus.asks
|
||||
word:bundles.bus.emits
|
||||
word:bundles.bus.invokes
|
||||
word:bundles.bus.queues
|
||||
word:bundles.bus.removes
|
||||
word:bundles.bus.state
|
||||
word:bundles.digest
|
||||
word:bundles.entrypoints
|
||||
word:bundles.env
|
||||
word:bundles.language
|
||||
word:bundles.launchers
|
||||
word:bundles.loads
|
||||
word:bundles.name
|
||||
word:bundles.source
|
||||
word:bus-users
|
||||
word:capabilities
|
||||
word:certificate
|
||||
word:certificate.authority
|
||||
word:certificate.into
|
||||
word:claims
|
||||
word:claims.capabilities
|
||||
word:claims.kind
|
||||
word:claims.name
|
||||
word:claims.renders
|
||||
word:claims.scope
|
||||
word:claims.serves
|
||||
word:collections
|
||||
word:collections.comment
|
||||
word:collections.name
|
||||
word:computed
|
||||
word:consumes
|
||||
word:contributes
|
||||
word:contributions
|
||||
word:contributions.content
|
||||
word:contributions.data
|
||||
word:contributions.if-capability
|
||||
word:contributions.kind
|
||||
word:contributions.seat
|
||||
word:data
|
||||
word:data.consumers
|
||||
word:data.consumers.class
|
||||
word:data.consumers.in
|
||||
word:data.consumers.why
|
||||
word:data.kept-by
|
||||
word:data.kept-by.class
|
||||
word:data.kept-by.why
|
||||
word:data.own
|
||||
word:data.own.active
|
||||
word:data.own.backup
|
||||
word:data.own.backup.dump
|
||||
word:data.own.backup.into
|
||||
word:data.own.class
|
||||
word:data.own.id
|
||||
word:data.own.measure
|
||||
word:data.own.path
|
||||
word:data.own.redundancy
|
||||
word:data.own.why
|
||||
word:data.own.within
|
||||
word:emits
|
||||
word:environment
|
||||
word:environment.path
|
||||
word:environment.path.at
|
||||
word:environment.path.entry
|
||||
word:environment.variables
|
||||
word:facts
|
||||
word:facts.home
|
||||
word:facts.path
|
||||
word:facts.shared
|
||||
word:facts.template
|
||||
word:filtering
|
||||
word:filtering.into
|
||||
word:grants
|
||||
word:guards
|
||||
word:handed
|
||||
word:handed.digest
|
||||
word:handed.items
|
||||
word:handed.source
|
||||
word:handed.templates
|
||||
word:instances
|
||||
word:invokes
|
||||
word:items
|
||||
word:items.attributes
|
||||
word:items.collection
|
||||
word:items.content
|
||||
word:items.if-capability
|
||||
word:items.name
|
||||
word:items.template
|
||||
word:jailing
|
||||
word:jailing.filter-into
|
||||
word:jailing.into
|
||||
word:jails
|
||||
word:jails.failregex
|
||||
word:jails.jail
|
||||
word:jails.name
|
||||
word:keeps
|
||||
word:listens
|
||||
word:listens.fixed
|
||||
word:listens.from
|
||||
word:listens.name
|
||||
word:listens.port
|
||||
word:listens.protocol
|
||||
word:listens.why
|
||||
word:logs
|
||||
word:logs.max-age-days
|
||||
word:logs.max-mib
|
||||
word:logs.name
|
||||
word:module
|
||||
word:moved
|
||||
word:objects
|
||||
word:objects.grant
|
||||
word:objects.grant.config
|
||||
word:objects.max-mib
|
||||
word:objects.name
|
||||
word:optional-requires
|
||||
word:own-secrets
|
||||
word:own-secrets.issued-by
|
||||
word:own-secrets.owner
|
||||
word:own-secrets.path
|
||||
word:own-secrets.prompt
|
||||
word:own-secrets.taken
|
||||
word:prepares
|
||||
word:provides
|
||||
word:provides.answered-by
|
||||
word:provides.credential
|
||||
word:provides.credential.form
|
||||
word:provides.credential.issued-by
|
||||
word:provides.credential.own
|
||||
word:provides.credential.tool
|
||||
word:provides.identity
|
||||
word:provides.identity.in
|
||||
word:provides.identity.max
|
||||
word:provides.keeps-consumer-data
|
||||
word:provides.name
|
||||
word:provides.reach
|
||||
word:provides.scope
|
||||
word:reads
|
||||
word:receives
|
||||
word:replaces
|
||||
word:requires
|
||||
word:requires-shares
|
||||
word:requires.name
|
||||
word:requires.optional
|
||||
word:requires.shares
|
||||
word:requires.shares.methods
|
||||
word:requires.shares.to
|
||||
word:resources
|
||||
word:runs-as
|
||||
word:runtime-health
|
||||
word:seats
|
||||
word:seats.accepts
|
||||
word:seats.by-caller
|
||||
word:seats.emits
|
||||
word:seats.kinded
|
||||
word:seats.name
|
||||
word:seats.needs
|
||||
word:seats.paths
|
||||
word:seats.proofs
|
||||
word:seats.records
|
||||
word:seats.retain-seconds
|
||||
word:seats.scope
|
||||
word:seats.serves
|
||||
word:seats.serves.description
|
||||
word:seats.serves.input
|
||||
word:seats.serves.name
|
||||
word:seats.serves.output
|
||||
word:seats.serves.replaces
|
||||
word:secrets
|
||||
word:secrets-owner
|
||||
word:serves
|
||||
word:settings
|
||||
word:settings.default
|
||||
word:settings.description
|
||||
word:settings.enum
|
||||
word:settings.kind
|
||||
word:settings.maximum
|
||||
word:settings.minimum
|
||||
word:settings.pattern
|
||||
word:settings.predefined
|
||||
word:settings.required
|
||||
word:settings.runtime
|
||||
word:settings.trusted
|
||||
word:settings.type
|
||||
word:settings.why
|
||||
word:shell
|
||||
word:shell.code
|
||||
word:shell.for
|
||||
word:shell.slot
|
||||
word:slug
|
||||
word:state
|
||||
word:state.history
|
||||
word:state.key-ttl
|
||||
word:state.name
|
||||
word:state.per-machine
|
||||
word:state.ttl-seconds
|
||||
word:streams
|
||||
word:streams.duplicates
|
||||
word:streams.events
|
||||
word:streams.grant
|
||||
word:streams.grant.config
|
||||
word:streams.grant.events
|
||||
word:streams.max-age-days
|
||||
word:streams.max-deliveries
|
||||
word:streams.max-mib
|
||||
word:streams.name
|
||||
word:streams.retention
|
||||
word:templates
|
||||
word:tools
|
||||
word:unhealthy
|
||||
word:unhealthy.severity
|
||||
word:unhealthy.why
|
||||
word:upgrade
|
||||
word:upgrade.policy
|
||||
word:upgrade.why
|
||||
word:uses
|
||||
word:version
|
||||
word:waits
|
||||
word:zone
|
||||
word:zone.listen
|
||||
word:zone.name
|
||||
@@ -2,6 +2,7 @@ package catalogue
|
||||
|
||||
import (
|
||||
"os"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -26,4 +27,71 @@ func TestOwnManifestUsesNoWordTheRunningControllerRefuses(t *testing.T) {
|
||||
t.Errorf("artifact %q names paths %v: the running controller refuses the word until R4e is delivered", a.Name, a.Paths)
|
||||
}
|
||||
}
|
||||
|
||||
// What a reader reads (novox/hq ADR 0614 §4): the baseline is the contract of mesh-sdk f02f142, which every running
|
||||
// node-engine was built with, and this contract reads all of it; the words added since are the only ones it lacks.
|
||||
reads, baseline := ManifestReads(), BaselineManifestReads()
|
||||
var added []string
|
||||
for _, r := range reads {
|
||||
if !slices.Contains(baseline, r) {
|
||||
added = append(added, r)
|
||||
}
|
||||
}
|
||||
for _, b := range baseline {
|
||||
if !slices.Contains(reads, b) {
|
||||
t.Errorf("the baseline's %s is no reader capability of this contract", b)
|
||||
}
|
||||
}
|
||||
for _, want := range []string{"word:tool-runner", "word:core-credentials", "word:bus-state", "word:bus-state.storage"} {
|
||||
if !slices.Contains(added, want) {
|
||||
t.Errorf("%s is in the baseline, or not read: added since it are %v", want, added)
|
||||
}
|
||||
}
|
||||
for _, want := range []string{"word:claims.serves", "word:settings.default", "form:config", "form:setting", "seat:mesh-catalog"} {
|
||||
if !slices.Contains(baseline, want) {
|
||||
t.Errorf("the baseline lacks %s", want)
|
||||
}
|
||||
}
|
||||
if blocks := untaggedBlocks(); len(blocks) != 0 {
|
||||
t.Errorf("blocks whose words are their Go names, no decoder's keys: %v (name their keys in wireKeys)", blocks)
|
||||
}
|
||||
// What a manifest's own words, forms and seat names need, beyond the baseline.
|
||||
for _, row := range []struct {
|
||||
why, raw string
|
||||
needs []string // among its needs
|
||||
missing []string // what the baseline lacks of them
|
||||
}{
|
||||
{"a tool runner", `{"module":"r","version":"1","tool-runner":true}`, []string{"word:tool-runner"}, []string{"word:tool-runner"}},
|
||||
{"a manifest of the baseline's words", `{"module":"a","version":"1","claims":[{"name":"mesh-catalog","scope":"mesh",` +
|
||||
`"serves":["modules"]}],"invokes":["seat:mesh-delivery.show"],"config":{"x":{"type":"string","default":""}},` +
|
||||
`"resources":[{"id":"f","type":"file","content":"${config:x} ${count:-0}"}],"provides":["plain",{"name":"p",` +
|
||||
`"identity":{"max":20,"in":"x"}}]}`,
|
||||
[]string{"word:claims.serves", "word:settings.type", "form:config", "seat:mesh-catalog", "seat:mesh-delivery",
|
||||
"word:provides.identity.max", "word:resources"}, nil},
|
||||
{"a bus-state entry of requires", `{"module":"b","version":"1","requires":["mesh-bus",{"name":"jobs","storage":"kv"}]}`,
|
||||
[]string{"word:requires", "word:bus-state.storage"}, []string{"word:bus-state", "word:bus-state.name", "word:bus-state.storage"}},
|
||||
{"a word this contract does not know", `{"module":"u","version":"1","newer-word":{"inside":1}}`, []string{"word:module"}, nil},
|
||||
} {
|
||||
needs, err := ManifestNeeds([]byte(row.raw))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", row.why, err)
|
||||
}
|
||||
for _, n := range row.needs {
|
||||
if !slices.Contains(needs, n) {
|
||||
t.Errorf("%s: needs %v, not %s", row.why, needs, n)
|
||||
}
|
||||
}
|
||||
if slices.ContainsFunc(needs, func(n string) bool { return n == "word:newer-word" || n == "form:count" }) {
|
||||
t.Errorf("%s: needs %v, naming what the contract does not read", row.why, needs)
|
||||
}
|
||||
if got := MissingManifestReads(needs, nil); !slices.Equal(got, row.missing) {
|
||||
t.Errorf("%s: an engine stating nothing lacks %v, want %v", row.why, got, row.missing)
|
||||
}
|
||||
if got := MissingManifestReads(needs, reads); len(got) != 0 {
|
||||
t.Errorf("%s: this contract lacks %v", row.why, got)
|
||||
}
|
||||
}
|
||||
if got := MissingManifestReads([]string{"seat:mesh-catalog", "word:module"}, []string{"word:module"}); !slices.Equal(got, []string{"seat:mesh-catalog"}) {
|
||||
t.Errorf("an engine stating its own reads is read by them alone: lacks %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,9 +146,15 @@ const (
|
||||
RuleProvisions = 8
|
||||
)
|
||||
|
||||
// ruleRead is the highest rule a reader takes (novox/hq ADR 0614 §2, §4): 9 and 10 are kept for the verb wait and the
|
||||
// capability hold, which a later release gives numbers of their own, so a want of that release reads on every node
|
||||
// that runs this one. Nothing writes 9 or 10 yet: the verb wait is held under RuleProvisions and the capability hold
|
||||
// under RuleComposed, each with words naming its edge.
|
||||
const ruleRead = 10
|
||||
|
||||
func checkRule(rule int, of string) error {
|
||||
if rule < RuleBuildDone || rule > RuleProvisions {
|
||||
return fmt.Errorf("delivery: %s names the rule %d, which is not one of the eight (1 to 8)", of, rule)
|
||||
if rule < RuleBuildDone || rule > ruleRead {
|
||||
return fmt.Errorf("delivery: %s names the rule %d, which is not one of 1 to %d", of, rule, ruleRead)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ func TestAssignerValuesRoundTrip(t *testing.T) {
|
||||
{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: RuleCanary, Words: "the canary ace does not run it healthy yet"},
|
||||
{Removed: true, Words: "the module left g14"},
|
||||
{FP: fp2of('a'), Composed: hashOf('1'), Put: true, Hold: true, Words: "put by the operator: the bus's user list"},
|
||||
// A rule a later release numbers (9, 10: the verb wait, the capability hold, ADR 0614) reads here already.
|
||||
{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: 10, Words: "the verb x.y it invokes is served by no running build"},
|
||||
}
|
||||
for _, w := range wants {
|
||||
data, err := EncodeWant(w)
|
||||
@@ -147,7 +149,7 @@ func TestAssignerValuesRefused(t *testing.T) {
|
||||
{Removed: true, FP: fp2of('a')},
|
||||
{FP: fp2of('a'), Newest: fp2of('b')},
|
||||
{FP: fp2of('a'), Newest: fp2of('a'), Rule: 3},
|
||||
{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: 9},
|
||||
{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: 11},
|
||||
{FP: fp2of('a'), Composed: hashOf('1'), Hold: true},
|
||||
{FP: fp2of('a'), Composed: hashOf('1'), Put: true, Newest: fp2of('b'), Rule: 3},
|
||||
{Removed: true, Put: true},
|
||||
@@ -164,7 +166,7 @@ func TestAssignerValuesRefused(t *testing.T) {
|
||||
{Want: fp2of('a'), Sent: fp2of('b'), Rule: 3, Unexplained: true},
|
||||
{Want: fp2of('a'), Sent: fp2of('a'), Rule: 3},
|
||||
{},
|
||||
{Want: fp2of('a'), Sent: fp2of('b'), Rule: 9},
|
||||
{Want: fp2of('a'), Sent: fp2of('b'), Rule: 11},
|
||||
{WantRemoved: true, Want: fp2of('a'), Rule: 1},
|
||||
} {
|
||||
if _, err := EncodeDifference(d); err == nil {
|
||||
|
||||
@@ -59,6 +59,10 @@ type NodeRecord struct {
|
||||
// key until the lease is handed away, and the vault's key and authority once it reads them from `signers`. The gate
|
||||
// before the lease write reads every node's from the bus.
|
||||
Trusts []string `json:"trusts,omitempty"`
|
||||
// ManifestReads are the reader capabilities of the shared contract this machine's node-engine is built with
|
||||
// (catalogue.ManifestReads, novox/hq ADR 0614 §4): the manifest words, forms and seat names it reads. Empty for an
|
||||
// engine built before it said them, which a reader takes as the baseline (catalogue.StatedManifestReads).
|
||||
ManifestReads []string `json:"manifest-reads,omitempty"`
|
||||
}
|
||||
|
||||
func (r NodeRecord) check() error {
|
||||
@@ -70,6 +74,11 @@ func (r NodeRecord) check() error {
|
||||
return fmt.Errorf("delivery: the record of %s names an empty capability", r.Node)
|
||||
}
|
||||
}
|
||||
for _, c := range r.ManifestReads {
|
||||
if c == "" {
|
||||
return fmt.Errorf("delivery: the record of %s names an empty reader capability", r.Node)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ var secretNamed = regexp.MustCompile(`(?i)(secret|passw|token|api[-_]?key|privat
|
||||
func TestNodeRecord(t *testing.T) {
|
||||
hub := NodeRecord{Node: "novox", TunnelKey: "k", Capabilities: []string{"docker"}, SealingKey: "age1x", ServingKey: "pub",
|
||||
Certificate: "cert", OutwardLinks: []string{"eth0"}, Networks: []catalogue.AttachedNetwork{{Link: "eth0", Network: "192.0.2.0/24"}},
|
||||
NetworksSaid: true, Contract: 3, Trusts: []string{"controller key sha256:aa", "vault key sha256:bb"}}
|
||||
NetworksSaid: true, Contract: 3, Trusts: []string{"controller key sha256:aa", "vault key sha256:bb"},
|
||||
ManifestReads: []string{"seat:mesh-catalog", "word:tool-runner"}}
|
||||
raw, err := EncodeNodeRecord(hub)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -45,6 +46,7 @@ func TestNodeRecord(t *testing.T) {
|
||||
|
||||
for name, r := range map[string]NodeRecord{
|
||||
"empty capability": {Node: "ace", Capabilities: []string{""}},
|
||||
"empty reader capability": {Node: "ace", ManifestReads: []string{""}},
|
||||
"node that is not a name": {Node: "a.b"},
|
||||
} {
|
||||
if _, err := EncodeNodeRecord(r); err == nil {
|
||||
|
||||
@@ -443,6 +443,10 @@ type Module struct {
|
||||
// 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.
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
// Needs are the reader capabilities the manifest's own words, forms and seat names need (catalogue.ManifestNeeds,
|
||||
// novox/hq ADR 0614 §4), written by the judge with its verdict: the assigner holds a node's want while that node's
|
||||
// engine does not state one of them. Empty in a value written before, which needs nothing.
|
||||
Needs []string `json:"needs,omitempty"`
|
||||
}
|
||||
|
||||
// VerdictOf is the verdict a modules value carries, or nil when it carries none (taken as accepted, TakeManifestVerdict).
|
||||
|
||||
@@ -181,6 +181,8 @@ func TestSourceValuesRoundTrip(t *testing.T) { // 1200/AC3
|
||||
for _, m := range []Module{
|
||||
{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"},
|
||||
// The reader capabilities its manifest needs (ADR 0614 §4), written with the verdict.
|
||||
{FP: "src1:abc", Repository: "novox/mesh-sdk", Commit: "cccc", Needs: []string{"seat:mesh-catalog", "word:tool-runner"}},
|
||||
} {
|
||||
data, err = EncodeModule(m)
|
||||
check("module", data, err)
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestAWantNamesItsComposition(t *testing.T) {
|
||||
{Want{Removed: true, Composed: hashOf('1')}, "removed want names no"},
|
||||
{Want{FP: fp2of('a'), Composed: hashOf('1'), Backup: "later"}, `backup answer "later"`},
|
||||
{Want{Removed: true, Backup: BackupAgain}, "removed want names no"},
|
||||
{Want{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: 9}, "rule 9"},
|
||||
{Want{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: 11}, "rule 11"},
|
||||
}
|
||||
for _, r := range refusals {
|
||||
if _, err := EncodeWant(r.w); err == nil || !strings.Contains(err.Error(), r.why) {
|
||||
|
||||
Reference in New Issue
Block a user