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"` // 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 (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: 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(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, 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 *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 }