Vendor mesh-sdk's go/compose and drop the node-engine's validator from vendor/ (issues 1381, 1382)
go mod vendor writes go/compose (copied on the forge from mesh-sdk 14fd5cb68fd1, equal to its blobs) now that the adapter imports it, and no longer writes github.com/novox/mesh-host/validate and internal/declaration, which nothing imports since the validator comes from mesh-sdk (issue 1381; the swap commits left those five files behind). vendor/modules.txt follows with the adapter.
This commit is contained in:
-87
@@ -1,87 +0,0 @@
|
||||
package declaration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Busy is how a long-running container says it holds work that would be lost if it were stopped now
|
||||
// (novox/hq ADR 0335 §1): a command run inside it, by its shell, as an exec health check is, that exits 0
|
||||
// while it is busy and non-zero when it is idle. Being asked is also the container's notice that it is
|
||||
// about to be replaced: from the first ask it takes no new work, so its answer ends once the work in hand
|
||||
// ends.
|
||||
//
|
||||
// **Not part of what the container is.** Busy is not in the container's spec: a changed command changes
|
||||
// when the container is replaced, never whether — a container is not recreated for its busy command alone.
|
||||
type Busy struct {
|
||||
Command string `json:"command"`
|
||||
// Timeout is how long one ask may take; empty is BusyTimeoutDefault.
|
||||
Timeout string `json:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
// The bounds of an ask (novox/hq ADR 0335).
|
||||
const (
|
||||
BusyTimeoutDefault = 10 * time.Second
|
||||
// BusyTimeoutCeiling is the longest one ask may take: an ask runs inside an apply, and an apply waits
|
||||
// for nothing longer than a look.
|
||||
BusyTimeoutCeiling = time.Minute
|
||||
// BusyBound is how long a container that says busy is left running after the first apply that would
|
||||
// have replaced it (ADR 0335 §3). The node-engine's, not the module's: no module holds its own
|
||||
// replacement for ever.
|
||||
BusyBound = 15 * time.Minute
|
||||
)
|
||||
|
||||
// UnmarshalJSON reads a busy strictly, as everything a declaration carries is read.
|
||||
func (b *Busy) UnmarshalJSON(raw []byte) error {
|
||||
type plain Busy
|
||||
var p plain
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&p); err != nil {
|
||||
return fmt.Errorf("busy: %w", err)
|
||||
}
|
||||
*b = Busy(p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Within is how long one ask may take. Validated on arrival, so an unreadable timeout is the default.
|
||||
func (b *Busy) Within() time.Duration {
|
||||
if d, err := time.ParseDuration(b.Timeout); err == nil && d > 0 {
|
||||
return d
|
||||
}
|
||||
return BusyTimeoutDefault
|
||||
}
|
||||
|
||||
// problems holds a container's busy to its shape: a one-line command, a timeout within its bounds, and only
|
||||
// on a container that stays up.
|
||||
func (b *Busy) problems(where string, longRunning bool) []string {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
var problems []string
|
||||
say := func(format string, args ...any) {
|
||||
problems = append(problems, where+": "+fmt.Sprintf(format, args...))
|
||||
}
|
||||
if !longRunning {
|
||||
say("busy is asked of a container that stays up; a step or a scheduled run is never replaced mid-run")
|
||||
}
|
||||
if strings.TrimSpace(b.Command) == "" {
|
||||
say("busy needs a command")
|
||||
}
|
||||
if strings.ContainsAny(b.Command, "\n\r") {
|
||||
say("busy's command is one line")
|
||||
}
|
||||
if b.Timeout != "" {
|
||||
d, err := time.ParseDuration(b.Timeout)
|
||||
switch {
|
||||
case err != nil:
|
||||
say("busy's timeout is a duration, not %q", b.Timeout)
|
||||
case d <= 0 || d > BusyTimeoutCeiling:
|
||||
say("an ask of busy takes more than nothing and at most %s, not %s", BusyTimeoutCeiling, d)
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
-182
@@ -1,182 +0,0 @@
|
||||
package declaration
|
||||
|
||||
// A minimal five-field cron, enough to say when a scheduled step is due (novox/hq ADR 0053).
|
||||
//
|
||||
// **Written rather than pulled in.** The host depends on nothing it does not have to
|
||||
// (novox/hq ADR 0005), and a scheduled step needs exactly two questions answered — *is this minute
|
||||
// a match* and *when is the next one* — over the ordinary five fields (minute, hour, day-of-month,
|
||||
// month, day-of-week) with `*`, lists (`,`), ranges (`-`) and steps (`/`). That is small enough to
|
||||
// keep in the vocabulary the host already owns, and a cron library would be a dependency carried
|
||||
// for the parts of it nobody here uses.
|
||||
//
|
||||
// **The host both validates and evaluates.** The control plane refuses a malformed cron near its
|
||||
// author (novox/hq ADR 0053), and the host refuses it again on arrival for the same near-versus-far
|
||||
// reason every other field is checked here: a declaration the host does not fully understand is
|
||||
// refused whole rather than half-applied. The evaluation half — `Next` — is what the scheduler
|
||||
// fires on.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cron is a parsed five-field expression, each field held as a bitset of the values it permits.
|
||||
//
|
||||
// The two day fields carry a flag for whether they were written as a bare `*`, because standard
|
||||
// cron gives them a special rule: when day-of-month and day-of-week are *both* restricted, a day
|
||||
// matches if *either* does; when one is `*`, only the other is consulted. Getting that wrong is the
|
||||
// classic cron surprise, so the flag is kept rather than rediscovered.
|
||||
type Cron struct {
|
||||
minute uint64
|
||||
hour uint64
|
||||
dom uint64
|
||||
month uint64
|
||||
dow uint64
|
||||
domStar bool
|
||||
dowStar bool
|
||||
}
|
||||
|
||||
// ParseCron reads a five-field cron expression, or says why it is not one.
|
||||
func ParseCron(expr string) (*Cron, error) {
|
||||
fields := strings.Fields(expr)
|
||||
if len(fields) != 5 {
|
||||
return nil, fmt.Errorf(
|
||||
"a schedule is a five-field cron expression (minute hour day-of-month month "+
|
||||
"day-of-week), and %q has %d field(s)", expr, len(fields))
|
||||
}
|
||||
|
||||
minute, _, err := parseCronField(fields[0], 0, 59)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("schedule minute field: %w", err)
|
||||
}
|
||||
hour, _, err := parseCronField(fields[1], 0, 23)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("schedule hour field: %w", err)
|
||||
}
|
||||
dom, domStar, err := parseCronField(fields[2], 1, 31)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("schedule day-of-month field: %w", err)
|
||||
}
|
||||
month, _, err := parseCronField(fields[3], 1, 12)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("schedule month field: %w", err)
|
||||
}
|
||||
// Day-of-week is 0-6 with Sunday at 0, and 7 is also accepted for Sunday — the convention every
|
||||
// cron keeps, so a crontab copied from elsewhere is not refused for saying 7.
|
||||
dow, dowStar, err := parseCronField(fields[4], 0, 7)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("schedule day-of-week field: %w", err)
|
||||
}
|
||||
if dow&(1<<7) != 0 {
|
||||
dow |= 1 << 0
|
||||
dow &^= 1 << 7
|
||||
}
|
||||
|
||||
return &Cron{
|
||||
minute: minute, hour: hour, dom: dom, month: month, dow: dow,
|
||||
domStar: domStar, dowStar: dowStar,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseCronField turns one field into the bitset of values it permits, and reports whether it was a
|
||||
// bare `*` (which the day fields treat specially).
|
||||
func parseCronField(spec string, min, max int) (uint64, bool, error) {
|
||||
if spec == "" {
|
||||
return 0, false, fmt.Errorf("is empty")
|
||||
}
|
||||
star := spec == "*"
|
||||
|
||||
var bits uint64
|
||||
for _, part := range strings.Split(spec, ",") {
|
||||
if part == "" {
|
||||
return 0, false, fmt.Errorf("%q has an empty element between commas", spec)
|
||||
}
|
||||
|
||||
// A step may follow either `*` or a range: `*/15`, `0-30/5`.
|
||||
step := 1
|
||||
rangePart := part
|
||||
if slash := strings.IndexByte(part, '/'); slash >= 0 {
|
||||
rangePart = part[:slash]
|
||||
n, err := strconv.Atoi(part[slash+1:])
|
||||
if err != nil || n < 1 {
|
||||
return 0, false, fmt.Errorf("step in %q is not a positive number", part)
|
||||
}
|
||||
step = n
|
||||
}
|
||||
|
||||
lo, hi := min, max
|
||||
switch {
|
||||
case rangePart == "*":
|
||||
// Full range, already set.
|
||||
case strings.IndexByte(rangePart, '-') >= 0:
|
||||
dash := strings.IndexByte(rangePart, '-')
|
||||
a, errA := strconv.Atoi(rangePart[:dash])
|
||||
b, errB := strconv.Atoi(rangePart[dash+1:])
|
||||
if errA != nil || errB != nil {
|
||||
return 0, false, fmt.Errorf("range %q is not two numbers", rangePart)
|
||||
}
|
||||
lo, hi = a, b
|
||||
default:
|
||||
n, err := strconv.Atoi(rangePart)
|
||||
if err != nil {
|
||||
return 0, false, fmt.Errorf("%q is not a number", rangePart)
|
||||
}
|
||||
lo, hi = n, n
|
||||
}
|
||||
|
||||
if lo < min || hi > max || lo > hi {
|
||||
return 0, false, fmt.Errorf(
|
||||
"%q is outside the allowed range %d-%d", part, min, max)
|
||||
}
|
||||
for v := lo; v <= hi; v += step {
|
||||
bits |= 1 << uint(v)
|
||||
}
|
||||
}
|
||||
return bits, star, nil
|
||||
}
|
||||
|
||||
// Matches reports whether a scheduled step is due at this minute.
|
||||
func (c *Cron) Matches(t time.Time) bool {
|
||||
if c.minute&(1<<uint(t.Minute())) == 0 {
|
||||
return false
|
||||
}
|
||||
if c.hour&(1<<uint(t.Hour())) == 0 {
|
||||
return false
|
||||
}
|
||||
if c.month&(1<<uint(int(t.Month()))) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
domMatch := c.dom&(1<<uint(t.Day())) != 0
|
||||
dowMatch := c.dow&(1<<uint(int(t.Weekday()))) != 0
|
||||
// The standard day rule: both restricted means either may match; one as `*` defers to the other.
|
||||
switch {
|
||||
case c.domStar && c.dowStar:
|
||||
return true
|
||||
case c.domStar:
|
||||
return dowMatch
|
||||
case c.dowStar:
|
||||
return domMatch
|
||||
default:
|
||||
return domMatch || dowMatch
|
||||
}
|
||||
}
|
||||
|
||||
// Next is the first minute strictly after t at which the step is due, and false if there is none
|
||||
// within a bound generous enough to cover a once-a-year, leap-day schedule.
|
||||
//
|
||||
// Minute-by-minute rather than a closed form: the day rule above makes a closed form fiddly and
|
||||
// error-prone, and this is called once per fire — rarely — so simple and obviously correct wins.
|
||||
func (c *Cron) Next(after time.Time) (time.Time, bool) {
|
||||
t := after.Truncate(time.Minute).Add(time.Minute)
|
||||
// Five years of minutes: enough that "29 2 * * *" (Feb 29) always finds its next leap year.
|
||||
for i := 0; i < 5*366*24*60; i++ {
|
||||
if c.Matches(t) {
|
||||
return t, true
|
||||
}
|
||||
t = t.Add(time.Minute)
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
-2607
File diff suppressed because it is too large
Load Diff
-188
@@ -1,188 +0,0 @@
|
||||
package declaration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Health is how a long-running resource is ready, as the controller composed it from the module's
|
||||
// `health` (novox/hq ADR 0240 rule 2, to-be 48 §2–§3, Phase B): one kind and its timing, the endpoint
|
||||
// already the port this machine published it on.
|
||||
//
|
||||
// **The node-engine runs every kind and owns every verdict.** http and tcp it makes itself, from the
|
||||
// machine to the port; unit it reads from the service manager it already reads; exec and runtime it hands
|
||||
// to the container runtime as the container's own check, with this timing, and reads the state; tool it
|
||||
// asks of its own node tools. Nothing else on the machine sets a container's check.
|
||||
//
|
||||
// Refused here as the controller refuses it near the author, in the same bounds: an engine that took a
|
||||
// check it could not judge would say a module ready that nothing looked at.
|
||||
type Health struct {
|
||||
Kind string `json:"kind"`
|
||||
// Endpoint is the module's name for what Port is: for the words a verdict is said in.
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Status int `json:"status,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
Scheme string `json:"scheme,omitempty"`
|
||||
Command string `json:"command,omitempty"`
|
||||
Tool string `json:"tool,omitempty"`
|
||||
Interval string `json:"interval"`
|
||||
Timeout string `json:"timeout"`
|
||||
Looks int `json:"looks"`
|
||||
Grace string `json:"grace"`
|
||||
// Needs is the provision the check exercises (to-be 48 §6): said with every verdict, so the
|
||||
// controller can hold what it finds under the provider's own condition.
|
||||
Needs string `json:"needs,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON reads a health strictly, as everything a declaration carries is read: a field this host
|
||||
// does not know is a part of the check the controller believes it asked for, and nothing would look at it.
|
||||
func (h *Health) UnmarshalJSON(raw []byte) error {
|
||||
type plain Health
|
||||
var p plain
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&p); err != nil {
|
||||
return fmt.Errorf("health: %w", err)
|
||||
}
|
||||
*h = Health(p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// The kinds.
|
||||
const (
|
||||
HealthRuntime = "runtime"
|
||||
HealthHTTP = "http"
|
||||
HealthTCP = "tcp"
|
||||
HealthExec = "exec"
|
||||
HealthUnit = "unit"
|
||||
HealthTool = "tool"
|
||||
)
|
||||
|
||||
// The bounds (ADR 0240 rule 2) — the controller's, held again here.
|
||||
const (
|
||||
HealthIntervalFloor = 10 * time.Second
|
||||
HealthLooksFloor = 2
|
||||
HealthWithin = 5 * time.Minute
|
||||
)
|
||||
|
||||
// Every, Within and GraceOf are the timing, read. Validated on arrival, so a parse error here is
|
||||
// impossible on a declaration that was accepted; it reads as zero.
|
||||
func (h *Health) Every() time.Duration { d, _ := time.ParseDuration(h.Interval); return d }
|
||||
func (h *Health) Within() time.Duration { d, _ := time.ParseDuration(h.Timeout); return d }
|
||||
func (h *Health) GraceOf() time.Duration { d, _ := time.ParseDuration(h.Grace); return d }
|
||||
|
||||
// RunByRuntime says the container runtime runs this check as the container's own: exec and runtime.
|
||||
func (h *Health) RunByRuntime() bool {
|
||||
return h != nil && (h.Kind == HealthExec || h.Kind == HealthRuntime)
|
||||
}
|
||||
|
||||
// Words is the check in a few words, as a verdict is said: "http /healthz on web".
|
||||
func (h *Health) Words() string {
|
||||
switch h.Kind {
|
||||
case HealthHTTP:
|
||||
return "http " + h.Path + " on " + orPort(h.Endpoint, h.Port)
|
||||
case HealthTCP:
|
||||
return "tcp on " + orPort(h.Endpoint, h.Port)
|
||||
case HealthTool:
|
||||
return "its tool " + h.Tool
|
||||
case HealthRuntime:
|
||||
return "its image's own check"
|
||||
case HealthExec:
|
||||
return "its command"
|
||||
case HealthUnit:
|
||||
return "its unit"
|
||||
}
|
||||
return h.Kind
|
||||
}
|
||||
|
||||
func orPort(endpoint string, port int) string {
|
||||
if endpoint != "" {
|
||||
return endpoint
|
||||
}
|
||||
return fmt.Sprint(port)
|
||||
}
|
||||
|
||||
// problems holds a resource's health to its kind and bounds. container says whether the resource is a
|
||||
// container; longRunning whether it stays up.
|
||||
func (h *Health) problems(where string, container, longRunning bool) []string {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
var problems []string
|
||||
say := func(format string, args ...any) {
|
||||
problems = append(problems, where+": "+fmt.Sprintf(format, args...))
|
||||
}
|
||||
if !longRunning {
|
||||
say("health is judged on what stays up; a step or a scheduled run is judged by its own outcome")
|
||||
}
|
||||
switch h.Kind {
|
||||
case HealthHTTP, HealthTCP:
|
||||
if h.Port < 1 || h.Port > 65535 {
|
||||
say("a %s check needs the port it looks at", h.Kind)
|
||||
}
|
||||
case HealthExec:
|
||||
if !container {
|
||||
say("an exec check runs inside a container")
|
||||
}
|
||||
if strings.TrimSpace(h.Command) == "" {
|
||||
say("an exec check needs a command")
|
||||
}
|
||||
case HealthRuntime:
|
||||
if !container {
|
||||
say("a runtime check is a container image's own")
|
||||
}
|
||||
case HealthUnit:
|
||||
if container {
|
||||
say("a unit check is a service's or a process's own")
|
||||
}
|
||||
case HealthTool:
|
||||
if strings.TrimSpace(h.Tool) == "" {
|
||||
say("a tool check names the tool")
|
||||
}
|
||||
default:
|
||||
say("health of kind %q; it is runtime, http, tcp, exec, unit or tool", h.Kind)
|
||||
}
|
||||
if h.Kind == HealthHTTP {
|
||||
if !strings.HasPrefix(h.Path, "/") {
|
||||
say("an http check asks a path starting with /")
|
||||
}
|
||||
if h.Status != 0 && (h.Status < 100 || h.Status > 599) {
|
||||
say("an http check expects status %d, which is not one", h.Status)
|
||||
}
|
||||
if h.Scheme != "" && h.Scheme != "http" && h.Scheme != "https" {
|
||||
say("an http check is over http or https, not %q", h.Scheme)
|
||||
}
|
||||
}
|
||||
if strings.ContainsAny(h.Command, "\n\r") {
|
||||
say("an exec check's command is one line")
|
||||
}
|
||||
every, everyErr := time.ParseDuration(h.Interval)
|
||||
within, withinErr := time.ParseDuration(h.Timeout)
|
||||
grace, graceErr := time.ParseDuration(h.Grace)
|
||||
switch {
|
||||
case everyErr != nil || withinErr != nil || graceErr != nil:
|
||||
say("health's interval, timeout and grace are durations")
|
||||
default:
|
||||
if every < HealthIntervalFloor {
|
||||
say("a check looks no more often than every %s, not every %s", HealthIntervalFloor, every)
|
||||
}
|
||||
if within <= 0 || within >= every {
|
||||
say("a look takes more than nothing and less than its interval")
|
||||
}
|
||||
if grace < 0 {
|
||||
say("a grace is not negative")
|
||||
}
|
||||
if h.Looks >= HealthLooksFloor && grace+time.Duration(h.Looks)*every > HealthWithin {
|
||||
say("a grace and the failing looks take at most %s", HealthWithin)
|
||||
}
|
||||
}
|
||||
if h.Looks < HealthLooksFloor {
|
||||
say("a check is unhealthy after at least %d failing looks, not %d", HealthLooksFloor, h.Looks)
|
||||
}
|
||||
return problems
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
// Package validate is the node-engine's own judgement of a declaration, for whoever must know before
|
||||
// a declaration is sent whether a machine would take it (novox/hq to-be 45 §4, D1; §9, the merge gate).
|
||||
//
|
||||
// **One validator.** The controller composed declarations the host then refused whole — a manifest
|
||||
// the catalogue check passed (novox/hq issue 236), a consumer's identity too long for the machine's
|
||||
// names (issue 263) — because the only validator was the one the host runs as it applies. A second,
|
||||
// written in the controller from the host's rules, would drift from them the first time either
|
||||
// changed. So the host's own parsing is exported here, unchanged: what the controller's self-check and
|
||||
// the merge gate run is what every machine runs.
|
||||
//
|
||||
// It judges a declaration as it arrives over the link: actions are refused, as the host refuses them
|
||||
// from the link (novox/hq ADR 0005). Nothing here touches a machine.
|
||||
package validate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
)
|
||||
|
||||
// Declaration is every problem the node-engine would refuse a declaration for, each in its own words;
|
||||
// nil when it would take it. The body is the declaration as the controller composes it — the bytes a
|
||||
// signature is made over — not the signed envelope.
|
||||
func Declaration(raw []byte) []string {
|
||||
_, err := declaration.Parse(raw)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var refused *declaration.RefusalError
|
||||
if errors.As(err, &refused) {
|
||||
return refused.Problems
|
||||
}
|
||||
return []string{err.Error()}
|
||||
}
|
||||
|
||||
// Version is the declaration vocabulary this validator speaks: a declaration of another version is
|
||||
// refused whole by Declaration, as by the host.
|
||||
const Version = declaration.Version
|
||||
Reference in New Issue
Block a user