Resolve keeps its own address strip (kept, ADR 0155), which needs no seat name. The one seat name left is ManifestNeeds' store holder (ADR 0632 §2), beside its rule in manifest_reads.go. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
264 lines
14 KiB
Go
264 lines
14 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: kept(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". What the vault judges `own` by: the plan is what the
|
|
// node's last composition made, so it follows that node's rule (the manifest, its build and the machine) one
|
|
// composition later. Never the rule a node composing itself hands by: with no plan yet it would never make one (issue
|
|
// 1966); the node reads it only to say where the two disagree (Rendering.Assigned).
|
|
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}
|