207 lines
7.3 KiB
Go
207 lines
7.3 KiB
Go
package nodeengineclient
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
|
|
seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
|
"git.novox.be/novox/mesh-sdk/go/keys"
|
|
)
|
|
|
|
// The state have (novox/hq ADR 0539 clause 9): a module's have on a node under `<node>.<module>` and the node's own facts
|
|
// under `<node>.node`; their layouts are the seat's contract (seat.HaveByNodeModuleValue, seat.HaveByNodeNodeValue). This
|
|
// file is the codec the node-engine, the assigner, the watchman and every reader share: it writes the version, and refuses
|
|
// a phase, a health, a judgement or a digest outside the lists.
|
|
|
|
// The phases of a module's change on a node, as the have states them (the seat's constants, short).
|
|
const (
|
|
PhaseBackingUp = seat.HaveByNodeModuleValuePhaseBackingUp
|
|
PhaseApplying = seat.HaveByNodeModuleValuePhaseApplying
|
|
PhaseChecking = seat.HaveByNodeModuleValuePhaseChecking
|
|
PhaseRunning = seat.HaveByNodeModuleValuePhaseRunning
|
|
PhaseFailed = seat.HaveByNodeModuleValuePhaseFailed
|
|
PhaseRemoved = seat.HaveByNodeModuleValuePhaseRemoved
|
|
PhaseHeld = seat.HaveByNodeModuleValuePhaseHeld
|
|
PhaseWaitingYes = seat.HaveByNodeModuleValuePhaseWaitingYes
|
|
)
|
|
|
|
// The healths a have carries.
|
|
const (
|
|
HealthOK = seat.HaveByNodeModuleValueHealthOk
|
|
HealthFailed = seat.HaveByNodeModuleValueHealthFailed
|
|
HealthWaits = seat.HaveByNodeModuleValueHealthWaits
|
|
HealthUnknown = seat.HaveByNodeModuleValueHealthUnknown
|
|
)
|
|
|
|
// The judgements of a move that waits for a yes.
|
|
const (
|
|
JudgedRecreates = seat.HaveByNodeModuleValueJudgementRecreates
|
|
JudgedMovesData = seat.HaveByNodeModuleValueJudgementMovesData
|
|
JudgedCannot = seat.HaveByNodeModuleValueJudgementCannotJudge
|
|
)
|
|
|
|
// ComposesOwn is a node applying its own compositions (the node facts' composes).
|
|
const ComposesOwn = "own"
|
|
|
|
// composedByTheEndedController is what an engine from before the controller ended wrote in composes: read, never written.
|
|
const composedByTheEndedController = "controller"
|
|
|
|
var digestForm = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
|
|
|
func checkDigest(digest, of string) error {
|
|
if !digestForm.MatchString(digest) {
|
|
return fmt.Errorf("node-engine: %s %q is not sha256:<64 lowercase hex digits>", of, digest)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkHave(h seat.HaveByNodeModuleValue) error {
|
|
switch h.Phase {
|
|
case PhaseBackingUp, PhaseApplying, PhaseChecking, PhaseRunning, PhaseFailed, PhaseRemoved, PhaseHeld:
|
|
if h.Judgement != "" || h.Waits != "" || h.BuildAlone {
|
|
return fmt.Errorf("node-engine: only a have waiting for a yes names a judgement, the composition waiting and whether it changes the build alone")
|
|
}
|
|
if h.PullWait != nil && h.Phase != PhaseFailed {
|
|
return fmt.Errorf("node-engine: only a failed have names the pull it waits on, not one %s", h.Phase)
|
|
}
|
|
case PhaseWaitingYes:
|
|
if h.Judgement != JudgedRecreates && h.Judgement != JudgedMovesData && h.Judgement != JudgedCannot {
|
|
return fmt.Errorf("node-engine: a have waiting for a yes names the judgement %q, not recreates, moves-data or cannot-judge", h.Judgement)
|
|
}
|
|
if err := checkDigest(h.Waits, "the composition a have waits a yes for"); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("node-engine: phase %q is not backing-up, applying, checking, running, failed, removed, held or waiting-yes", h.Phase)
|
|
}
|
|
if w := h.PullWait; w != nil && (w.Image == "" || w.Holder == "") {
|
|
return fmt.Errorf("node-engine: a have's pull wait names no image or no holder")
|
|
}
|
|
if h.Composed != "" {
|
|
if err := checkDigest(h.Composed, "the have's composition digest"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
switch h.Health {
|
|
case HealthOK, HealthFailed, HealthWaits, HealthUnknown:
|
|
default:
|
|
return fmt.Errorf("node-engine: health %q is not ok, failed, waits or unknown", h.Health)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeHave is h as JSON at the current version; it refuses a phase or health outside the lists.
|
|
func EncodeHave(h seat.HaveByNodeModuleValue) ([]byte, error) {
|
|
if err := checkHave(h); err != nil {
|
|
return nil, err
|
|
}
|
|
h.V = keys.Version
|
|
return json.Marshal(h)
|
|
}
|
|
|
|
// DecodeHave reads a have; it refuses an unknown version (keys.ErrUnknownVersion), a field the contract does not know and
|
|
// what EncodeHave refuses.
|
|
func DecodeHave(data []byte) (seat.HaveByNodeModuleValue, error) {
|
|
if err := keys.CheckVersion(data); err != nil {
|
|
return seat.HaveByNodeModuleValue{}, err
|
|
}
|
|
h, err := seat.DecodeHaveByNodeModuleValue(data)
|
|
if err != nil {
|
|
return seat.HaveByNodeModuleValue{}, fmt.Errorf("node-engine: value does not fit its shape: %w", err)
|
|
}
|
|
if err := checkHave(h); err != nil {
|
|
return seat.HaveByNodeModuleValue{}, err
|
|
}
|
|
return h, nil
|
|
}
|
|
|
|
func checkHaveNode(n seat.HaveByNodeNodeValue) error {
|
|
switch n.Composes {
|
|
case "", ComposesOwn, composedByTheEndedController:
|
|
default:
|
|
return fmt.Errorf("node-engine: a node composes %q, not %s", n.Composes, ComposesOwn)
|
|
}
|
|
for group, a := range n.Candidates {
|
|
if group == "" {
|
|
return fmt.Errorf("node-engine: a node's candidate answer names no group")
|
|
}
|
|
switch a.Verdict {
|
|
case seat.CandidateAnswerVerdictSame, seat.CandidateAnswerVerdictDiffers, seat.CandidateAnswerVerdictRefused:
|
|
default:
|
|
return fmt.Errorf("node-engine: the answer to %s's candidate has the verdict %q", group, a.Verdict)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeHaveNode is n as JSON at the current version; it refuses a candidate answer with a verdict outside the lists.
|
|
func EncodeHaveNode(n seat.HaveByNodeNodeValue) ([]byte, error) {
|
|
if err := checkHaveNode(n); err != nil {
|
|
return nil, err
|
|
}
|
|
n.V = keys.Version
|
|
return json.Marshal(n)
|
|
}
|
|
|
|
// DecodeHaveNode reads a node's facts; it refuses an unknown version (keys.ErrUnknownVersion), a field the contract does
|
|
// not know and what EncodeHaveNode refuses.
|
|
func DecodeHaveNode(data []byte) (seat.HaveByNodeNodeValue, error) {
|
|
if err := keys.CheckVersion(data); err != nil {
|
|
return seat.HaveByNodeNodeValue{}, err
|
|
}
|
|
n, err := seat.DecodeHaveByNodeNodeValue(data)
|
|
if err != nil {
|
|
return seat.HaveByNodeNodeValue{}, fmt.Errorf("node-engine: value does not fit its shape: %w", err)
|
|
}
|
|
if err := checkHaveNode(n); err != nil {
|
|
return seat.HaveByNodeNodeValue{}, err
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// SameHave says whether two haves are the same value, so a writer writes only when they differ: times compare as
|
|
// instants, the user list's and the handed accounts' time as well.
|
|
func SameHave(a, b seat.HaveByNodeModuleValue) bool {
|
|
return sameJSON(utcHave(a), utcHave(b))
|
|
}
|
|
|
|
// SameHaveNode says whether two node facts are the same value, compared as their JSON (maps sorted, spacing dropped).
|
|
func SameHaveNode(a, b seat.HaveByNodeNodeValue) bool { return sameJSON(a, b) }
|
|
|
|
// SameNodeRecord says whether two records are the same value but for their version.
|
|
func SameNodeRecord(a, b seat.NodeRecord) bool {
|
|
a.V, b.V = 0, 0
|
|
return sameJSON(a, b)
|
|
}
|
|
|
|
func utcHave(h seat.HaveByNodeModuleValue) seat.HaveByNodeModuleValue {
|
|
if w := h.PullWait; w != nil {
|
|
c := *w
|
|
c.Began = c.Began.UTC()
|
|
if c.RetriedAt != nil {
|
|
t := c.RetriedAt.UTC()
|
|
c.RetriedAt = &t
|
|
}
|
|
h.PullWait = &c
|
|
}
|
|
if u := h.UserList; u != nil {
|
|
c := *u
|
|
c.At = c.At.UTC()
|
|
if c.Handed != nil {
|
|
hd := *c.Handed
|
|
hd.At = hd.At.UTC()
|
|
c.Handed = &hd
|
|
}
|
|
h.UserList = &c
|
|
}
|
|
return h
|
|
}
|
|
|
|
func sameJSON(a, b any) bool {
|
|
ra, errA := json.Marshal(a)
|
|
rb, errB := json.Marshal(b)
|
|
return errA == nil && errB == nil && bytes.Equal(ra, rb)
|
|
}
|