Vendor every dependency, so no build fetches the host's validator (hq to-be 45 D1)
The controller imports mesh-host/validate through a replace onto the forge that holds it, and every build — the build agent's go build in a fresh toolchain container, the Dockerfile's go mod download — would have fetched it through the public proxy and checksum database at build time: a merge breaking main on the network, the class Phase 1 removes. vendor/ is committed; go builds from it with nothing fetched, and refuses to build when it and go.mod disagree, so a pin moved without go mod vendor fails at once. The Dockerfile copies vendor/ and builds with GOPROXY=off.
This commit is contained in:
+182
@@ -0,0 +1,182 @@
|
||||
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
|
||||
}
|
||||
+1811
File diff suppressed because it is too large
Load Diff
+38
@@ -0,0 +1,38 @@
|
||||
// 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