Files
mesh-sdk/go/delivery/placement_candidate.go
T

248 lines
11 KiB
Go

package delivery
import (
"encoding/json"
"fmt"
"strings"
)
// Placement candidates (novox/hq ADR 0567 §3, ADR 0559 §4): for each node a proposal must hear from, the seat's holder
// writes one key `<proposal>.<node>` in its state `candidates`: the node's placement with the proposal laid over it for a
// node whose key it changes, or the node's current key for one whose composition it changes only through another node
// (Reached: a binding, a firewall or a resolver entry reaching it). It deletes them when the proposal ends. A state of its
// own, since the readers of `placement` refuse any key that is not `placement.<node>`.
//
// The node watches `*.<node>`, composes in shadow with every candidate of its proposal laid over the current placement
// (its own sealed values, the boxes it already runs) and compares the result with what it runs; it applies nothing. Its
// answer rides in its own have key, `<node>.node` (HaveNode.Candidates, by proposal), which that node alone writes; the
// holder reads it and judges (ADR 0567 §4). Event-driven both ways: the candidate key's write is the node's event, the
// `<node>.node` change the holder's.
const (
// PlacementCandidatesState is the seat's state of placement candidates, PlacementCandidatesBucket its bucket and
// PlacementCandidatesRead the name a reader's manifest declares.
PlacementCandidatesState = "candidates"
PlacementCandidatesBucket = AssignerModule + "_" + PlacementCandidatesState
PlacementCandidatesRead = AssignerModule + "." + PlacementCandidatesState
// PlacementCandidatesHistory is the revisions the bucket keeps per key.
PlacementCandidatesHistory = 5
)
// PlacementCandidateKey is the key, in PlacementCandidatesBucket, of one node's candidate in one proposal:
// `<proposal>.<node>`, so a node watches `*.<node>` and a proposal's keys are `<proposal>.*`.
func PlacementCandidateKey(proposal, node string) (string, error) {
if err := CheckProposalID(proposal); err != nil {
return "", err
}
if err := checkName("node", node); err != nil {
return "", err
}
return proposal + "." + node, nil
}
// ParsePlacementCandidateKey is the inverse of PlacementCandidateKey; it refuses any other key, naming it.
func ParsePlacementCandidateKey(key string) (proposal, node string, err error) {
proposal, node, ok := strings.Cut(key, ".")
if !ok || checkName("node", node) != nil || CheckProposalID(proposal) != nil {
return "", "", fmt.Errorf("delivery: %q is not a placement candidate key, <proposal>.<node>", key)
}
return proposal, node, nil
}
// PlacementCandidate is the value of a candidate key: the proposal it is of, and the node's placement with the proposal
// laid over it; nil Placement says the proposal takes the node's key away. Reached says the proposal changes not this
// node's key but its composition, through another node's: Placement is then its current key (nil when it has none).
type PlacementCandidate struct {
V int `json:"v"`
Proposal string `json:"proposal"`
Reached bool `json:"reached,omitempty"`
Placement *Placement `json:"placement,omitempty"`
}
func (c PlacementCandidate) check() error {
if err := CheckProposalID(c.Proposal); err != nil {
return err
}
if c.Placement != nil {
return c.Placement.check()
}
return nil
}
// EncodePlacementCandidate is c as JSON at Version; it refuses what DecodePlacementCandidate refuses.
func EncodePlacementCandidate(c PlacementCandidate) ([]byte, error) {
if err := c.check(); err != nil {
return nil, err
}
c.V = Version
if c.Placement != nil {
p := *c.Placement
p.V = Version
c.Placement = &p
}
return json.Marshal(c)
}
// DecodePlacementCandidate reads a candidate; it refuses an unknown version, a proposal id that is not one, and a
// placement DecodePlacement refuses.
func DecodePlacementCandidate(data []byte) (PlacementCandidate, error) {
var c PlacementCandidate
if err := decode(data, &c); err != nil {
return PlacementCandidate{}, err
}
if err := c.check(); err != nil {
return PlacementCandidate{}, err
}
return c, nil
}
// The verdicts of a node's answer to a candidate.
const (
// CandidateSame: every composition the node composes with the candidate is the one it runs, and it runs no other.
CandidateSame = "same"
// CandidateDiffers: some composition differs (Differs), is added by the candidate (Added) or removed by it (Removed);
// whether the change intends it is the assigner's to judge (ADR 0559 §5).
CandidateDiffers = "differs"
// CandidateRefused: a value is missing for a module the node runs (Refusals), or the node cannot compose with the
// candidate at all (Refused says why: an unbound requirement, a manifest it cannot read).
CandidateRefused = "refused"
)
// CandidateAnswer is a node's answer to its proposal's candidate keys, in HaveNode.Candidates (ADR 0567 §3): the
// proposal and the revision of the node's own candidate key it answered, its verdict, the modules composed the same, those that differ with both digests, those
// the candidate adds (naming the values not yet issued for each, which is no refusal: a module's credential and boxes
// are made only once it is placed, ADR 0549 §5) or removes, and each refusal. The node's own composition is named
// "node" (HaveNodeWord).
type CandidateAnswer struct {
Proposal string `json:"proposal"`
Revision uint64 `json:"revision"`
Verdict string `json:"verdict"`
Same []string `json:"same,omitempty"`
Differs []ModuleDiffers `json:"differs,omitempty"`
Added []ModuleAdded `json:"added,omitempty"`
Removed []string `json:"removed,omitempty"`
Refusals []ModuleRefused `json:"refusals,omitempty"`
// Refused is why the node could not compose with the candidate at all; Refusals is then empty.
Refused string `json:"refused,omitempty"`
// Returns is, for a proposal that takes the node's placement key away, what the node does then: it returns to the
// controller's compositions (ReturnsToController), as a node with no placement key composes none of its own. Same, Differs, Added and Removed then compare the controller's compositions at
// the node's wants with what it runs, and the verdict is theirs; a want the controller holds no composition for is
// refused (Refused, without Returns).
Returns string `json:"returns,omitempty"`
// Note is what the node says of how it answered, in words, beside the verdict: a pass that ran over a minute, inputs
// it waited for (issue 1126: the shadow shows its work). It changes no verdict.
Note string `json:"note,omitempty"`
// ValuesDiffer is how many shown bodies hold a value the node opened on both sides to two values (ValuesDiffer,
// MaskedAnotherValue): the vault's value and the one that runs drifted apart (issue 1126, cause C4). It changes no
// verdict.
ValuesDiffer int `json:"values_differ,omitempty"`
}
// ReturnsToController is what a node answers a proposal that takes its placement key away.
const ReturnsToController = "the proposal takes this node's placement away: it returns to the controller's compositions"
// ModuleDiffers is one module the node runs and the candidate still places whose composition is not the one it runs:
// both digests (Composed with the candidate, Running what runs), the first resource that differs by its id, or empty
// when every resource is equal, and why in words.
type ModuleDiffers struct {
Module string `json:"module"`
Composed string `json:"composed,omitempty"`
Running string `json:"running,omitempty"`
Resource string `json:"resource,omitempty"`
Why string `json:"why"`
// Bodies is every resource that differs, both sides, each value a secret could be in masked (issue 1126: the shadow
// shows its work, so why a module differs is read, not guessed). At most MaxBodies, each side cut at MaxBody bytes.
Bodies []ResourceBodies `json:"bodies,omitempty"`
}
// ResourceBodies is one differing resource of a module: its id, why, and its body composed and as it runs, masked
// (MaskedPair); a side that does not hold it is empty.
type ResourceBodies struct {
ID string `json:"id"`
Why string `json:"why"`
// Paths is every JSON path at which the two bodies differ (DifferingPaths: `sealed`, `content.url`, `links.0`), found
// on the whole bodies before they are cut, a JSON value written in a string read as JSON; a path names keys alone,
// never a value. At most MaxPaths, the last then saying how many more.
Paths []string `json:"paths,omitempty"`
Composed string `json:"composed,omitempty"`
Running string `json:"running,omitempty"`
}
// The bounds of a module's shown bodies, and of one answer as the node's own key holds it: an answer over
// MaxAnswerBytes is fitted (FitAnswer). The node's key (HaveBucket) takes a value of about 1 MiB
// (buscompose.ChainValueBytes) and holds every proposal's answer beside the node's facts, so one answer keeps to a sixth.
const (
MaxBodies = 20
MaxBody = 8192
MaxPaths = 64
MaxAnswerBytes = 160 << 10
)
// ModuleAdded is one module the candidate places on the node and the node does not run: its composition's digest when
// it composed, and the values not yet issued for it (its own secrets, its grants, a host port), each in the words the
// composition stood in for it with.
type ModuleAdded struct {
Module string `json:"module"`
Composed string `json:"composed,omitempty"`
Unissued []string `json:"unissued,omitempty"`
}
// ModuleRefused is a value missing for a module the node runs, in the words the composition stood in for it with.
type ModuleRefused struct {
Module string `json:"module"`
Why string `json:"why"`
}
// VerdictOf is the verdict of an answer's lists: refused on any refusal, differs on any difference, addition or
// removal, same otherwise.
func VerdictOf(a CandidateAnswer) string {
switch {
case a.Refused != "" || len(a.Refusals) > 0:
return CandidateRefused
case len(a.Differs)+len(a.Added)+len(a.Removed) > 0:
return CandidateDiffers
}
return CandidateSame
}
func (a CandidateAnswer) check() error {
if CheckProposalID(a.Proposal) != nil || a.Revision == 0 {
return fmt.Errorf("delivery: a candidate answer names no proposal or no revision of its candidate")
}
switch a.Verdict {
case CandidateSame, CandidateDiffers, CandidateRefused:
default:
return fmt.Errorf("delivery: the candidate verdict %q is not same, differs or refused", a.Verdict)
}
if got := VerdictOf(a); got != a.Verdict {
return fmt.Errorf("delivery: a candidate answered %s names what makes it %s", a.Verdict, got)
}
if a.Refused != "" && len(a.Same)+len(a.Differs)+len(a.Added)+len(a.Removed)+len(a.Refusals) > 0 {
return fmt.Errorf("delivery: a candidate the node could not compose names modules")
}
if a.Returns != "" && (a.Refused != "" || len(a.Refusals) > 0) {
return fmt.Errorf("delivery: a candidate the node returns to the controller's compositions on is compared, never refused: " +
"a composition the controller holds none of is refused without returning")
}
for _, d := range a.Differs {
if d.Module == "" || d.Why == "" {
return fmt.Errorf("delivery: a candidate difference names no module or no why")
}
}
for _, d := range a.Added {
if d.Module == "" {
return fmt.Errorf("delivery: a module the candidate adds is not named")
}
}
for _, d := range a.Refusals {
if d.Module == "" || d.Why == "" {
return fmt.Errorf("delivery: a candidate refusal names no module or no why")
}
}
for _, m := range a.Removed {
if m == "" {
return fmt.Errorf("delivery: a module the candidate removes is not named")
}
}
return nil
}