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 `.` and the node's own facts // under `.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) }