290 lines
12 KiB
Go
290 lines
12 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
|
engcomp "github.com/novox/mesh-host/internal/compositions"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
"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"`
|
|
// Attempt is what the agent holds of the module's last apply that did not end running: failed, failed for a moment
|
|
// of the bus, or held for a failed backup, with the want's revision that hold took as answered; nil when none
|
|
// (issue 1585: why a module stays held was not readable from the engine).
|
|
Attempt *AttemptView `json:"attempt,omitempty"`
|
|
// Started says the agent has looked at the module once since it started (its baseline): what runs was taken from its
|
|
// have, or it was applied.
|
|
Started bool `json:"started"`
|
|
// 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 (engcomp.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: as the agent holds it, at the revision of its key it last read.
|
|
type WantView struct {
|
|
FP string `json:"fp"`
|
|
Composed string `json:"composed,omitempty"`
|
|
Yes string `json:"yes,omitempty"`
|
|
Removed bool `json:"removed,omitempty"`
|
|
// Self says the node composes the module itself (ADR 0547 §2).
|
|
Self bool `json:"self,omitempty"`
|
|
// Backup is the operator's answer to a failed backup the want carries, again or skip (ADR 0539 §16); empty when none.
|
|
Backup string `json:"backup,omitempty"`
|
|
Words string `json:"words,omitempty"`
|
|
// Revision is the revision of the want's key the agent read last.
|
|
Revision uint64 `json:"revision"`
|
|
}
|
|
|
|
// AttemptView is the agent's record of a module's last apply that did not end running.
|
|
type AttemptView struct {
|
|
FP string `json:"fp"`
|
|
Composed string `json:"composed,omitempty"`
|
|
// WantRevision and CompositionRevision are the revisions of the want's and the composition's keys it was tried at.
|
|
WantRevision uint64 `json:"want_revision"`
|
|
CompositionRevision uint64 `json:"composition_revision"`
|
|
// Held says it is held for a failed backup; AnsweredRevision is the want's revision the hold took, so an answer
|
|
// (again or skip) in a later revision releases it.
|
|
Held bool `json:"held,omitempty"`
|
|
AnsweredRevision uint64 `json:"answered_revision,omitempty"`
|
|
// Transient says it failed because the bus did not answer for a moment, and is tried again by the clock.
|
|
Transient bool `json:"transient,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(nodeengineclient.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,
|
|
Self: w.Want.Self, Backup: w.Want.Backup, Words: w.Want.Words, Revision: w.Rev}
|
|
} else if have != nil {
|
|
fp = have.FP
|
|
}
|
|
if att, ok := a.attempt[module]; ok {
|
|
view.Attempt = &AttemptView{FP: att.FP, Composed: att.Composed, WantRevision: att.WantRev, CompositionRevision: att.CompRev,
|
|
Held: att.Held, AnsweredRevision: att.AnsweredRev, Transient: att.Transient}
|
|
}
|
|
view.Started = a.checked[module]
|
|
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 *engcomp.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 nodeengineclient.JudgedRecreates, nodeengineclient.JudgedMovesData, nodeengineclient.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 engcomp.NodeKey(a.env.Node)
|
|
}
|
|
return engcomp.Key(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 := engcomp.Decode(u.Value)
|
|
if err != nil {
|
|
out = append(out, compNow{Raw: u.Value, Rev: u.Revision, Err: err})
|
|
continue
|
|
}
|
|
dig, err := engcomp.DigestOf(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
|
|
}
|