mesh/delivery-group group feat/sdk1 rejected: composed together: the checker's verdict at the members' heads (428d048dd50a): unit: git.novox.be/novox/mesh-sdk/…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: unit: git.novox.be/novox/mesh-sdk/go/process.TestStatePutWritesAKeyWhole: a Put through a short blackout: 0, mesh-mail's state send…
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
262 lines
13 KiB
Go
262 lines
13 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Handed is what a moved module's build kept for the provisioner (novox/hq ADR 0349 §2): its templates archive, as
|
|
// recorded (store-relative, routed at composition like any archive), and its digest; and, for each file resource
|
|
// filled from a template, that template's path inside the archive. Written by Resolve, never by hand.
|
|
type Handed struct {
|
|
Source string `json:"source"`
|
|
Digest string `json:"digest"`
|
|
Templates map[string]string `json:"templates"`
|
|
// Items is, for each item the module contributes, by ItemKey (its collection, and `/<name>` for a named one),
|
|
// that item's template inside the archive (collections.go, design 57 L1).
|
|
Items map[string]string `json:"items,omitempty"`
|
|
}
|
|
|
|
// Plan is a moved module's plan on one node: what the node's provisioner client fills its files from. The shape
|
|
// of the assignments mirror's `provisioning` (mesh-controller internal/inventory Provisioning).
|
|
type Plan struct {
|
|
Templates PlanTemplates `json:"templates"`
|
|
Files []PlanFile `json:"files"`
|
|
Placement map[string]string `json:"placement,omitempty"`
|
|
// Secrets are the module's own secrets this declaration names (design 57 V4, issue 647): each its templates
|
|
// name, and each it keeps in a file of its own, with that file's path. By name, never a value: the client asks
|
|
// the vault for each, and the vault hands one only when the plan names it.
|
|
Secrets []PlanSecret `json:"secrets,omitempty"`
|
|
// Items are the module's items of collections (design 57 L1): each filled by the client like a file and recorded
|
|
// in mesh-provisioner_collect for its node. Collections are the collections its files name, as their collector,
|
|
// each with its comment. By template and name, never a contributor's text.
|
|
Items []PlanItem `json:"items,omitempty"`
|
|
Collections []PlanCollection `json:"collections,omitempty"`
|
|
// Provisions are what it requires here, each with the provider resolution chose to serve it (issue 760), so the
|
|
// vault sees a consumer bind to a provider and unbind from it (novox/hq ADR 0349 clause 8); and, for a
|
|
// credential the vault keeps for it alone, the name its files call it and the file holding it (issue 757). By
|
|
// name, never a value.
|
|
Provisions []PlanProvision `json:"provisions,omitempty"`
|
|
}
|
|
|
|
// Hands is whether the plan hands its module to the provisioner: it names something the node's provisioner client fills
|
|
// (a template archive, a file, an own secret or an item), and the node composes none of it. A plan naming provisions alone
|
|
// hands nothing (catalogue.Keyed, issue 774): the seat keys what a provider's helper answers of a module not handed, and
|
|
// that module's files and own secrets stay its node's composition. The one rule every reader of a plan asks; whether a
|
|
// plan exists is never it (on 2026-10-30 the vault read every route-only plan as handed and answered twenty modules'
|
|
// own secrets to nobody).
|
|
func (p Plan) Hands() bool {
|
|
return p.Templates.Source != "" || len(p.Files) > 0 || len(p.Secrets) > 0 || len(p.Items) > 0
|
|
}
|
|
|
|
// PlanItem is one item the module contributes: to which collection, and its template inside the archive.
|
|
//
|
|
// A named item has its Name and Attributes (design 57 L3, issue 777): the client records them with the item and
|
|
// places it under a placeholder selecting by them. An item whose `if-capability` the machine lacks is not in the plan.
|
|
type PlanItem struct {
|
|
Collection string `json:"collection"`
|
|
Name string `json:"name,omitempty"`
|
|
Attributes map[string]string `json:"attributes,omitempty"`
|
|
Template string `json:"template"`
|
|
}
|
|
|
|
// PlanCollection is one collection the module collects, and the comment introducing each contributor's item.
|
|
type PlanCollection struct {
|
|
Name string `json:"name"`
|
|
Comment string `json:"comment,omitempty"`
|
|
}
|
|
|
|
// PlanProvision is one provision the module requires, under one local name, and the provider serving it: the module
|
|
// (ProviderModule) on the machine (Provider) that resolution chose for this consumer. Where several providers offer
|
|
// a mesh-scoped provision, it is the one this consumer is bound to (a pin, the seat's holder, or where its data is),
|
|
// never the others (issue 760). Local is the name the consumer keeps the credential under where it keeps several
|
|
// from one requirement (ADR 0094), empty for the ordinary one; a consumer has one provider per provision and local
|
|
// name.
|
|
//
|
|
// Name and Path are set only for a credential the vault keeps for this holder alone, which its machine's client
|
|
// fills (issue 757): the value the provider's software issued for this module on this machine. Name is what the
|
|
// module's files call it (`${secret:<name>}`) and Path the file holding it alone.
|
|
//
|
|
// CredentialForm is the form the provider's offer declares for its credential (Manifest.CredentialFormOf, issue
|
|
// 768): the vault makes a first value in that form for a provider it holds no row of. Empty where the provider's
|
|
// manifest was not in hand, and the vault then makes nothing.
|
|
type PlanProvision struct {
|
|
Name string `json:"name,omitempty"`
|
|
Provision string `json:"provision"`
|
|
Local string `json:"local,omitempty"`
|
|
Provider string `json:"provider"`
|
|
ProviderModule string `json:"provider-module"`
|
|
CredentialForm string `json:"credential-form,omitempty"`
|
|
// CredentialTool is, for an offer whose credential the provider's software issues (issue 928), the provider's tool
|
|
// the vault asks for each holder's value; empty for any other form.
|
|
CredentialTool string `json:"credential-tool,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
// Contributes is what the consumer's manifest tells its provider for this provision (Manifest.Contributes: a
|
|
// database's name, its extensions), so the provider's helper learns it from the bus (issue 776). Nil where the
|
|
// manifest contributes nothing for it, and the entry is then as it was.
|
|
Contributes map[string]any `json:"contributes,omitempty"`
|
|
// At, From and As are what today's binding file tells the consumer of this provision (issue 788): where the
|
|
// provider is on the private network, the machine, and who the consumer is at its end (its slug first), so the
|
|
// provider's helper writes them into the consumer's entry and the client fills `${bound:<provision>:at|from|as}`
|
|
// from there. Empty where the resolution knew none.
|
|
At string `json:"at,omitempty"`
|
|
From string `json:"from,omitempty"`
|
|
As string `json:"as,omitempty"`
|
|
// Names are what the mesh calls the consumer through this provision (issue 792), keyed as its binding file names
|
|
// them (`name`, `internal-name`, `name-<local>`, `internal-name-<local>`): composed by the controller, which
|
|
// alone knows the node's domains, and carried unchanged by the provider's helper into the consumer's entry. Nil
|
|
// where the consumer contributes nothing to the provision, and the entry is then as it was.
|
|
Names map[string]string `json:"names,omitempty"`
|
|
// Contributed is what the consumer gives its provider through this provision, opaque here (issue 774, research
|
|
// 072 row 34): the bytes of a ContributedForm, each contribution composed as its provider receives it and where
|
|
// the consumer's machine is. What a contribution's values mean is the provider's own contract, published by the
|
|
// provider's repository (route-proxy's for a route), which decodes them; the seat, its plans, its client and the
|
|
// helper carry them unread. On the provision's first entry alone; nil where the consumer contributes nothing to it.
|
|
Contributed json.RawMessage `json:"contributed,omitempty"`
|
|
}
|
|
|
|
// ContributedVersion is the version of ContributedForm this SDK writes and reads.
|
|
const ContributedVersion = 1
|
|
|
|
// ContributedForm is the generic form of PlanProvision.Contributed: every contribution one consumer makes to one
|
|
// requirement, composed (settled with its node's settings, its endpoint's port filled in, its names joined to the
|
|
// node's domains: Resolution.composed, the one composition a received file was written from), and At, where the
|
|
// consumer's machine is on the private network, empty when it is on none. The provisioner knows no field of any
|
|
// contribution: Values are the bytes of an object in the provider's contract.
|
|
type ContributedForm struct {
|
|
V int `json:"v"`
|
|
At string `json:"at,omitempty"`
|
|
Each []ContributedOne `json:"each"`
|
|
}
|
|
|
|
// ContributedOne is one contribution: Local its local name among several (`contributes` naming several), empty for a
|
|
// requirement's one, and Values the contribution's object.
|
|
type ContributedOne struct {
|
|
Local string `json:"local,omitempty"`
|
|
Values json.RawMessage `json:"values"`
|
|
}
|
|
|
|
// DecodeContributed reads PlanProvision.Contributed strictly: this version, no field it does not know, at least one
|
|
// contribution, each local name once, each value a JSON object. What a value says is not judged here.
|
|
func DecodeContributed(raw json.RawMessage) (ContributedForm, error) {
|
|
var f ContributedForm
|
|
dec := json.NewDecoder(strings.NewReader(string(raw)))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&f); err != nil {
|
|
return ContributedForm{}, fmt.Errorf("what a consumer contributed is not readable: %w", err)
|
|
}
|
|
if dec.More() {
|
|
return ContributedForm{}, errors.New("what a consumer contributed carries more than one document")
|
|
}
|
|
if f.V != ContributedVersion {
|
|
return ContributedForm{}, fmt.Errorf("what a consumer contributed is of version %d, this SDK reads %d", f.V,
|
|
ContributedVersion)
|
|
}
|
|
if len(f.Each) == 0 {
|
|
return ContributedForm{}, errors.New("what a consumer contributed holds no contribution")
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, one := range f.Each {
|
|
if seen[one.Local] {
|
|
return ContributedForm{}, fmt.Errorf("what a consumer contributed names %q twice", one.Local)
|
|
}
|
|
seen[one.Local] = true
|
|
var object map[string]json.RawMessage
|
|
if err := json.Unmarshal(one.Values, &object); err != nil || object == nil {
|
|
return ContributedForm{}, fmt.Errorf("the contribution %q is not a JSON object", one.Local)
|
|
}
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// PlanSecret is one of the module's own secrets, by its name in the manifest; Path is the file holding it alone,
|
|
// when the manifest's own-secrets places one.
|
|
type PlanSecret struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path,omitempty"`
|
|
}
|
|
|
|
// PlanTemplates is where the templates archive is fetched from on this node, and what it hashes to.
|
|
type PlanTemplates struct {
|
|
Source string `json:"source"`
|
|
Digest string `json:"digest"`
|
|
}
|
|
|
|
// PlanFile is one file the client fills.
|
|
type PlanFile struct {
|
|
ID string `json:"id"`
|
|
Path string `json:"path"`
|
|
Template string `json:"template"`
|
|
OwnerAtRuntime string `json:"owner-at-runtime"`
|
|
Takes string `json:"takes,omitempty"`
|
|
}
|
|
|
|
// The words a placed file says who owns it at runtime with (novox/hq ADR 0349 §5).
|
|
const (
|
|
RuntimeOwned = "owned"
|
|
RuntimeSeed = "seed"
|
|
RuntimeManaged = "managed"
|
|
)
|
|
|
|
// OwnerAtRuntimeField is the key a declared file says who owns it at runtime by (the node-engine's
|
|
// `owner-at-runtime`).
|
|
const OwnerAtRuntimeField = "owner-at-runtime"
|
|
|
|
// handedBy is what a moved module's build keeps, from its templates archive as built, or nil for a module not
|
|
// moved.
|
|
func handedBy(m Manifest, archive Built) *Handed {
|
|
if !m.Moved {
|
|
return nil
|
|
}
|
|
dir := m.TemplatesDir()
|
|
h := &Handed{Source: Recorded(archive.Reference), Digest: archive.Digest, Templates: map[string]string{}}
|
|
for _, r := range m.Resources {
|
|
name, has, wrong := templateOf(m.Module, dir, r)
|
|
if !has || wrong != "" {
|
|
continue
|
|
}
|
|
h.Templates[fmt.Sprint(r["id"])] = strings.TrimPrefix(name, dir+"/")
|
|
}
|
|
for _, it := range m.Items {
|
|
if it.Template == "" || itemTemplateProblem(m, it.Template) != "" {
|
|
continue
|
|
}
|
|
if h.Items == nil {
|
|
h.Items = map[string]string{}
|
|
}
|
|
h.Items[it.key()] = strings.TrimPrefix(it.Template, dir+"/")
|
|
}
|
|
return h
|
|
}
|
|
|
|
// AssignmentHands is whether a module's value in the seat mesh-provisioner's plans (the assignments mirror's shape,
|
|
// `{"provisioning": <Plan>}`) hands its module to the provisioner: Plan.Hands of the plan it carries, false with no
|
|
// plan. A value that does not read is an error, never "not handed". The one rule the vault judges `own` by and a node
|
|
// composing itself follows (Rendering.Assigned, issue 774).
|
|
func AssignmentHands(raw []byte) (bool, error) {
|
|
var a struct {
|
|
Provisioning *Plan `json:"provisioning"`
|
|
}
|
|
if err := json.Unmarshal(raw, &a); err != nil {
|
|
return false, err
|
|
}
|
|
return a.Provisioning != nil && a.Provisioning.Hands(), nil
|
|
}
|
|
|
|
// HandedProvisionField marks, between composing a handed module's credential file for something it requires and
|
|
// placing it, one the vault keeps for this holder alone (issue 757): its value is the PlanProvision without its path.
|
|
// Never on the wire.
|
|
const HandedProvisionField = "handed-provision"
|
|
|
|
// HandedSecretField marks, between composing a module's own secret files and placing them, the one a handing
|
|
// composition hands to the provisioner: its value is the secret's name. Never on the wire.
|
|
const HandedSecretField = "handed-secret"
|
|
|
|
// SettingField is the settings form's word for a config placeholder's namespace.
|
|
const SettingField = "setting"
|
|
|
|
// PlacementKinds are the placeholder namespaces placement answers (ADR 0349 clause 2), as a plan's keys begin.
|
|
var PlacementKinds = []string{"dir", "access", "port", "seat", "machine", BackupNamespace}
|