Four providers waited for a yes on 2026-10-31 and nobody could read what waited: the have said only "where its data is kept". The read verb composition answers it from the engine: the latest revision of the want's build or the one asked by digest, every revision the bucket keeps, what runs and where that is known from, the want and the have, the move's judgement and whether it waits for a yes, and with diff the resources that differ, a received file naming its consumers. revisionNamed now reads the same revisions.
231 lines
8.0 KiB
Go
231 lines
8.0 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// The node-engine seat's read verb composition (issue 774): one module's composition beside what runs of it, so why a
|
|
// record module waits for a yes, or what its next apply would change, is read from the engine and not guessed from its
|
|
// have's words. It changes nothing.
|
|
|
|
// CompositionView is what the verb composition answers.
|
|
type CompositionView struct {
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
// Want is the module's want as the agent holds it; nil when it holds none.
|
|
Want *WantView `json:"want,omitempty"`
|
|
// Have is what the module's have key says now; nil when it says nothing.
|
|
Have *HaveView `json:"have,omitempty"`
|
|
// Composition is the revision read: the latest of the want's build, or the one whose digest was asked.
|
|
Composition RevisionView `json:"composition"`
|
|
// Revisions are the revisions of its key the bucket keeps, oldest first, each with its digest.
|
|
Revisions []RevisionView `json:"revisions,omitempty"`
|
|
// Runs is what runs of the module, and where that is known from; nil when nothing of it runs.
|
|
Runs *RunsView `json:"runs,omitempty"`
|
|
// Judged is the move from what runs to the composition read, as the agent judges it before an apply.
|
|
Judged JudgedView `json:"judged"`
|
|
// Diff is every resource that differs from what runs, when asked.
|
|
Diff []delivery.ResourceChange `json:"diff,omitempty"`
|
|
}
|
|
|
|
// WantView is a want as the verb shows it.
|
|
type WantView struct {
|
|
FP string `json:"fp"`
|
|
Composed string `json:"composed,omitempty"`
|
|
Yes string `json:"yes,omitempty"`
|
|
Removed bool `json:"removed,omitempty"`
|
|
}
|
|
|
|
// HaveView is a have as the verb shows it.
|
|
type HaveView struct {
|
|
FP string `json:"fp,omitempty"`
|
|
Composed string `json:"composed,omitempty"`
|
|
Phase string `json:"phase"`
|
|
Detail string `json:"detail,omitempty"`
|
|
Judgement string `json:"judgement,omitempty"`
|
|
Waits string `json:"waits,omitempty"`
|
|
}
|
|
|
|
// RevisionView is one revision of a composition key.
|
|
type RevisionView struct {
|
|
Key string `json:"key"`
|
|
Revision uint64 `json:"revision,omitempty"`
|
|
Digest string `json:"digest,omitempty"`
|
|
Resources int `json:"resources,omitempty"`
|
|
Latest bool `json:"latest,omitempty"`
|
|
// Refused is why the revision cannot be read or trusted.
|
|
Refused string `json:"refused,omitempty"`
|
|
}
|
|
|
|
// RunsView is what runs of a module.
|
|
type RunsView struct {
|
|
FP string `json:"fp,omitempty"`
|
|
Composed string `json:"composed,omitempty"`
|
|
Phase string `json:"phase,omitempty"`
|
|
// From is applied (what this engine applied), have (what its have named at the start) or kept (the declaration the
|
|
// node kept before the switch).
|
|
From string `json:"from"`
|
|
Resources int `json:"resources"`
|
|
}
|
|
|
|
// JudgedView is a move's judgement.
|
|
type JudgedView struct {
|
|
Judgement string `json:"judgement"`
|
|
Words string `json:"words"`
|
|
// WaitsForYes says the agent would wait for a person's yes before applying it: a module of policy record, a move that
|
|
// recreates a container, moves data or cannot be judged, and no yes in its want for this digest.
|
|
WaitsForYes bool `json:"waits_for_yes"`
|
|
}
|
|
|
|
// Composition reads one module's composition beside what runs of it: the want's build (else the have's), its latest
|
|
// revision or the one whose digest is given, and the move's judgement; with diff, the resources that differ.
|
|
func (a *Agent) Composition(ctx context.Context, module, digest string, diff bool) (CompositionView, error) {
|
|
if module == "" {
|
|
return CompositionView{}, errors.New("name the module, or node for the node's own composition")
|
|
}
|
|
view := CompositionView{Node: a.env.Node, Module: module}
|
|
var have *Have
|
|
if a.env.Haves != nil {
|
|
if h, ok := a.env.Haves.Read(ctx, module); ok {
|
|
have = &h
|
|
phase := h.Phase
|
|
if phase == "" {
|
|
phase = string(delivery.PhaseRunning)
|
|
}
|
|
view.Have = &HaveView{FP: h.FP, Composed: h.Composed, Phase: phase, Detail: h.Detail, Judgement: h.Judgement, Waits: h.Waits}
|
|
}
|
|
}
|
|
a.mu.Lock()
|
|
w := a.wants[module]
|
|
fp := ""
|
|
if w.Present {
|
|
fp = w.Want.FP
|
|
view.Want = &WantView{FP: w.Want.FP, Composed: w.Want.Composed, Yes: w.Want.Yes, Removed: w.Want.Removed}
|
|
} else if have != nil {
|
|
fp = have.FP
|
|
}
|
|
latest, haveLatest := a.compositionFor(module, fp)
|
|
r, ran := a.running[module]
|
|
from, known := a.runningComposition(module)
|
|
source := "applied"
|
|
if _, ok := a.applied[module]; !ok {
|
|
if _, ok := a.had[module]; ok {
|
|
source = "have"
|
|
} else {
|
|
source = "kept"
|
|
}
|
|
}
|
|
a.mu.Unlock()
|
|
if module != NodeModule && fp == "" {
|
|
return view, fmt.Errorf("%s has no want and no have on %s, so no build of it is composed here", module, a.env.Node)
|
|
}
|
|
key, err := a.compositionKey(module, fp)
|
|
if err != nil {
|
|
return view, err
|
|
}
|
|
revs, err := a.revisionsOf(ctx, key)
|
|
if err != nil {
|
|
return view, fmt.Errorf("the revisions of %s cannot be read: %w", key, err)
|
|
}
|
|
var read *compNow
|
|
for i := range revs {
|
|
c := revs[i]
|
|
rv := RevisionView{Key: key, Revision: c.Rev, Digest: c.Dig, Resources: len(c.C.Resources),
|
|
Latest: haveLatest && c.Rev == latest.Rev}
|
|
if c.Err != nil {
|
|
rv.Refused = c.Err.Error()
|
|
}
|
|
view.Revisions = append(view.Revisions, rv)
|
|
if digest != "" && c.Err == nil && c.Dig == digest {
|
|
read = &revs[i]
|
|
}
|
|
}
|
|
switch {
|
|
case digest != "" && read == nil && haveLatest && latest.Err == nil && latest.Dig == digest:
|
|
read = &latest
|
|
case digest != "" && read == nil:
|
|
return view, fmt.Errorf("no revision of %s the bucket keeps has the digest %s; revisions lists those it keeps", key, digest)
|
|
case read == nil && !haveLatest:
|
|
return view, fmt.Errorf("the bucket holds no composition of %s at %s", module, fp)
|
|
case read == nil:
|
|
read = &latest
|
|
}
|
|
view.Composition = RevisionView{Key: key, Revision: read.Rev, Digest: read.Dig, Resources: len(read.C.Resources),
|
|
Latest: haveLatest && read.Rev == latest.Rev}
|
|
if read.Err != nil {
|
|
view.Composition.Refused = read.Err.Error()
|
|
return view, nil
|
|
}
|
|
var running *delivery.Composed
|
|
if known {
|
|
running = &from
|
|
rv := &RunsView{From: source, Resources: len(from.Resources), FP: from.FP}
|
|
if ran {
|
|
rv.FP, rv.Composed, rv.Phase = r.FP, r.Composed, string(r.Phase)
|
|
}
|
|
view.Runs = rv
|
|
}
|
|
j := delivery.JudgeMove(running, read.C)
|
|
view.Judged = JudgedView{Judgement: string(j.Judgement), Words: j.Words}
|
|
if a.env.SelfComposed && a.env.Policy != nil && module != NodeModule && (!w.Present || w.Want.Yes != read.Dig) {
|
|
switch j.Judgement {
|
|
case delivery.JudgedRecreates, delivery.JudgedMovesData, delivery.JudgedCannot:
|
|
view.Judged.WaitsForYes = a.env.Policy(ctx, module) == catalogue.PolicyRecord
|
|
}
|
|
}
|
|
if diff {
|
|
view.Diff = delivery.ChangedResources(running, read.C)
|
|
}
|
|
return view, nil
|
|
}
|
|
|
|
// compositionKey is a module's composition key at a build, or the node's own.
|
|
func (a *Agent) compositionKey(module, fp string) (string, error) {
|
|
if module == NodeModule {
|
|
return delivery.ComposedNodeKey(a.env.Node)
|
|
}
|
|
return delivery.ComposedKey(a.env.Node, module, fp)
|
|
}
|
|
|
|
// revisionsOf is every revision of a composition key the bucket keeps, oldest first, each with its digest as this node
|
|
// trusts it (its own unsigned with SelfComposed, else signed by a trusted key), or why it is refused. Nil Revisions
|
|
// reads none.
|
|
func (a *Agent) revisionsOf(ctx context.Context, key string) ([]compNow, error) {
|
|
if a.env.Revisions == nil {
|
|
return nil, nil
|
|
}
|
|
revs, err := a.env.Revisions(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var trusted []ed25519.PublicKey
|
|
if a.env.Trusted != nil {
|
|
trusted = a.env.Trusted()
|
|
}
|
|
var out []compNow
|
|
for _, u := range revs {
|
|
if u.Deleted || u.Key != key {
|
|
continue
|
|
}
|
|
c, err := delivery.DecodeComposed(u.Value)
|
|
if err != nil {
|
|
out = append(out, compNow{Raw: u.Value, Rev: u.Revision, Err: err})
|
|
continue
|
|
}
|
|
var dig string
|
|
if a.env.SelfComposed {
|
|
dig, err = delivery.ComposedDigestOf(c) // the node's own: unsigned (ADR 0547 §2)
|
|
} else {
|
|
dig, err = delivery.VerifyComposed(c, trusted)
|
|
}
|
|
out = append(out, compNow{Raw: u.Value, C: c, Dig: dig, Rev: u.Revision, Err: err})
|
|
}
|
|
return out, nil
|
|
}
|