Files

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)
}