Reading ace's node composition gave four resources twice and no way to see that one revision named no provisioner client's account. The verb now shows, for module node, the revision's provisioner account beside what runs names, its adoption, how many identities, groups and libraries it names, and each module the composer's last pass held with why. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
254 lines
9.4 KiB
Go
254 lines
9.4 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"github.com/novox/mesh-host/internal/judge"
|
|
)
|
|
|
|
// 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 []judge.ResourceChange `json:"diff,omitempty"`
|
|
// Own is, of the node's own composition (module node), its own fields beside what runs names (issue 774: ace's
|
|
// own composition dropped the provisioner client's account, and nothing read could show it).
|
|
Own *OwnView `json:"own,omitempty"`
|
|
}
|
|
|
|
// OwnView is the node's own fields of a composition (delivery.NodeOwn) beside what runs of them, and what the node's
|
|
// composer held in its last pass.
|
|
type OwnView struct {
|
|
// Provisioner is the account the provisioner's client runs as, as the revision read names it; RunsProvisioner as
|
|
// what runs names it. Empty: the declaration names none, and every filled write is refused.
|
|
Provisioner string `json:"provisioner_account"`
|
|
RunsProvisioner string `json:"runs_provisioner_account,omitempty"`
|
|
// Adoption is the revision's adoption: the node adopted, and which of its modules are taken; nil for a converged node.
|
|
Adoption *declaration.Adoption `json:"adoption,omitempty"`
|
|
// Identities, Groups and Libraries count what the revision names of each.
|
|
Identities int `json:"identities,omitempty"`
|
|
Groups int `json:"groups,omitempty"`
|
|
Libraries int `json:"libraries,omitempty"`
|
|
// Held is each module the node's composer held in its last pass, with why.
|
|
Held map[string]string `json:"held,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
|
|
}
|
|
if module == NodeModule {
|
|
o := read.C.Own
|
|
view.Own = &OwnView{Provisioner: o.Provisioner, Adoption: o.Adoption, Identities: len(o.Identities), Groups: len(o.Groups),
|
|
Libraries: len(o.Libraries)}
|
|
if running != nil {
|
|
view.Own.RunsProvisioner = running.Own.Provisioner
|
|
}
|
|
if a.env.Held != nil {
|
|
view.Own.Held = a.env.Held()
|
|
}
|
|
}
|
|
j := judge.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 = judge.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 state keeps, oldest first, each with its digest (unsigned, ADR
|
|
// 0593 §6), 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 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
|
|
}
|
|
dig, err := delivery.ComposedDigestOf(c) // unsigned: a record, never verified (ADR 0593 §6)
|
|
out = append(out, compNow{Raw: u.Value, C: c, Dig: dig, Rev: u.Revision, Err: err})
|
|
}
|
|
return out, nil
|
|
}
|