248 lines
11 KiB
Go
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
|
|
}
|