Take mesh-sdk at 863a981e, which carries the file references of ADR 0486 (issue 1696)
This commit is contained in:
@@ -3,7 +3,7 @@ module github.com/novox/mesh-controller
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021093645-465beb4ea5e3
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021124357-863a981e014e
|
||||||
github.com/jackc/pgx/v5 v5.10.0
|
github.com/jackc/pgx/v5 v5.10.0
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5 h1:E5stfIeCN6uYg
|
|||||||
git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5/go.mod h1:4wyoCr6IIIIarEUuROThWDj7DAvJvIidqMjWcpSnOwE=
|
git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5/go.mod h1:4wyoCr6IIIIarEUuROThWDj7DAvJvIidqMjWcpSnOwE=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021093645-465beb4ea5e3 h1:OKxU64kHN1yQ/GOa6LwoUwVmiQ74d83WMSZJRFPVm9c=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021093645-465beb4ea5e3 h1:OKxU64kHN1yQ/GOa6LwoUwVmiQ74d83WMSZJRFPVm9c=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021093645-465beb4ea5e3/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021093645-465beb4ea5e3/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021124357-863a981e014e h1:eq29ENikw3XfEXjxL5OdnTbavv5pu1/UnQhWWuf5V5I=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021124357-863a981e014e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
|||||||
Vendored
+1
-1
@@ -9,7 +9,7 @@ a second place to clean besides the catalogue itself.
|
|||||||
|
|
||||||
mesh-catalog e0bc919d4945bb5f13f971f74643a0b714cf45da modules/*/module.json, modules/nats/Dockerfile
|
mesh-catalog e0bc919d4945bb5f13f971f74643a0b714cf45da modules/*/module.json, modules/nats/Dockerfile
|
||||||
mesh-host 9d9e3a46c4f5943cfffb413d962ce48b75e2fd39 examples/foundation-first-node.lock
|
mesh-host 9d9e3a46c4f5943cfffb413d962ce48b75e2fd39 examples/foundation-first-node.lock
|
||||||
mesh-sdk 465beb4ea5e37eb36f6ba7fc60274de2d5a0d060 conformance/events/module-event.json
|
mesh-sdk 863a981e014e08ef5781eb8767024e7754792583 conformance/events/module-event.json
|
||||||
|
|
||||||
To move them, from this repository's root, with the two repositories checked out beside it:
|
To move them, from this repository's root, with the two repositories checked out beside it:
|
||||||
|
|
||||||
|
|||||||
@@ -2214,6 +2214,7 @@ func parseManifest(raw []byte, predefined PredefinedList) (Manifest, error) {
|
|||||||
problems = append(problems, replacesProblems(m)...)
|
problems = append(problems, replacesProblems(m)...)
|
||||||
problems = append(problems, UnitSettingProblems(m)...)
|
problems = append(problems, UnitSettingProblems(m)...)
|
||||||
problems = append(problems, OwnerSettingProblems(m)...)
|
problems = append(problems, OwnerSettingProblems(m)...)
|
||||||
|
problems = append(problems, GroupSettingProblems(m)...)
|
||||||
problems = append(problems, endpointNameProblems(m)...)
|
problems = append(problems, endpointNameProblems(m)...)
|
||||||
problems = append(problems, RouteProblems(m)...)
|
problems = append(problems, RouteProblems(m)...)
|
||||||
for _, port := range m.Guards {
|
for _, port := range m.Guards {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ var capabilityName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
|
|||||||
// names an `if-capability` may give (novox/hq ADR 0255). A name nothing detects would leave its
|
// names an `if-capability` may give (novox/hq ADR 0255). A name nothing detects would leave its
|
||||||
// contribution out on every machine, silently, so it is refused. A detector added there is added here.
|
// contribution out on every machine, silently, so it is refused. A detector added there is added here.
|
||||||
var KnownCapabilities = []string{
|
var KnownCapabilities = []string{
|
||||||
"battery", "container-runtime", "firewall", "graphical-session", "overlay", "package-manager",
|
"battery", "container-runtime", "file-references", "firewall", "graphical-session", "overlay", "package-manager",
|
||||||
"power-meter", "privileged", "seat", "service-manager", "uplink-dhcpcd", "uplink-networkmanager",
|
"power-meter", "privileged", "seat", "service-manager", "uplink-dhcpcd", "uplink-networkmanager",
|
||||||
"uplink-systemd-networkd", "user-namespaces", "virtualisation",
|
"uplink-systemd-networkd", "user-namespaces", "virtualisation",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ func settingInto(resource map[string]any, layers []Layer, module string) error {
|
|||||||
if fmt.Sprint(resource["type"]) == "directory" {
|
if fmt.Sprint(resource["type"]) == "directory" {
|
||||||
return settingIntoOwner(resource, layers, module)
|
return settingIntoOwner(resource, layers, module)
|
||||||
}
|
}
|
||||||
|
if fmt.Sprint(resource["type"]) == "container" {
|
||||||
|
return settingIntoGroups(resource, layers, module)
|
||||||
|
}
|
||||||
if fmt.Sprint(resource["type"]) != "file" {
|
if fmt.Sprint(resource["type"]) != "file" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -343,6 +346,12 @@ func settingKeysAskedOutsideOwners(m Manifest) map[string]bool {
|
|||||||
if unit, ok := r["unit"].(string); ok {
|
if unit, ok := r["unit"].(string); ok {
|
||||||
note(unit)
|
note(unit)
|
||||||
}
|
}
|
||||||
|
case "container":
|
||||||
|
for _, g := range groupsOf(r) {
|
||||||
|
if s, ok := g.(string); ok {
|
||||||
|
note(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inValues := func(values map[string]any) {
|
inValues := func(values map[string]any) {
|
||||||
@@ -365,3 +374,100 @@ func settingKeysAskedOutsideOwners(m Manifest) map[string]bool {
|
|||||||
}
|
}
|
||||||
return used
|
return used
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A container's supplementary groups may be an operator's value (novox/hq ADR 0480 clause 7, ADR 0348 §11, issue 1636):
|
||||||
|
// n8n joins the media library's group, whose number is the machine's, never the definition's (ADR 0112 §1). Each
|
||||||
|
// element of `groups` is a whole placeholder or a whole number; a placeholder is filled with the number the
|
||||||
|
// config value holds, and the list reaches the node-engine as numbers.
|
||||||
|
|
||||||
|
// groupSide is one element of a container's groups: a whole placeholder or a whole number.
|
||||||
|
var groupSide = regexp.MustCompile(`^(?:\$\{setting:[a-z0-9][a-z0-9_.-]*\}|[0-9]+)$`)
|
||||||
|
|
||||||
|
// groupsOf is a container resource's groups as the manifest wrote them (text or numbers), none when it names none.
|
||||||
|
func groupsOf(r map[string]any) []any {
|
||||||
|
groups, _ := r["groups"].([]any)
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupSettingProblems refuses, where a manifest is read, a container whose groups hold anything but whole
|
||||||
|
// placeholders and whole numbers, or a placeholder whose key the module neither declares nor asks for elsewhere.
|
||||||
|
func GroupSettingProblems(m Manifest) []string {
|
||||||
|
var problems []string
|
||||||
|
for _, r := range m.Resources {
|
||||||
|
if fmt.Sprint(r["type"]) != "container" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, g := range groupsOf(r) {
|
||||||
|
s, isText := g.(string)
|
||||||
|
if !isText {
|
||||||
|
continue // a number: the declaration judges its value
|
||||||
|
}
|
||||||
|
if !groupSide.MatchString(s) {
|
||||||
|
problems = append(problems, fmt.Sprintf("%s: the container %v's group %q is not a whole "+
|
||||||
|
"${config:key} or a whole number: a supplementary group is a number, and nothing else is "+
|
||||||
|
"written into it (novox/hq ADR 0480 clause 7)", m.Module, r["id"], s))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if m.Templated() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, key := range settingsUsed(s) {
|
||||||
|
if _, declared := m.Settings[key]; !declared && !m.DeclaresConfig() {
|
||||||
|
continue // the settings form judges its own keys
|
||||||
|
}
|
||||||
|
if _, declared := m.Settings[key]; !declared {
|
||||||
|
problems = append(problems, fmt.Sprintf("%s: the container %v's group says ${config:%s}, and the module "+
|
||||||
|
"declares no such value (novox/hq ADR 0348 §4)", m.Module, r["id"], key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return problems
|
||||||
|
}
|
||||||
|
|
||||||
|
// settingIntoGroups fills ${setting:…} in a container's groups, refusing — with the key and why, the resource left as
|
||||||
|
// it was — a key nothing sets and a value that is not a whole number above zero. A number is kept; the list that
|
||||||
|
// results is numbers alone.
|
||||||
|
func settingIntoGroups(resource map[string]any, layers []Layer, module string) error {
|
||||||
|
groups := groupsOf(resource)
|
||||||
|
if len(groups) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
filled := make([]any, 0, len(groups))
|
||||||
|
for _, g := range groups {
|
||||||
|
s, isText := g.(string)
|
||||||
|
if !isText {
|
||||||
|
if f, isNumber := g.(float64); isNumber && f == float64(int(f)) {
|
||||||
|
g = int(f) // a number the manifest wrote, as the numbers a placeholder gives
|
||||||
|
}
|
||||||
|
filled = append(filled, g)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keys := settingsUsed(s)
|
||||||
|
if len(keys) != 1 || settingRef.ReplaceAllString(s, "") != "" {
|
||||||
|
filled = append(filled, g) // not a whole placeholder: the manifest check refuses it
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := keys[0]
|
||||||
|
value, set := settingValue(layers, key)
|
||||||
|
if !set {
|
||||||
|
return &UnsetSettingError{Module: module, Setting: key,
|
||||||
|
where: fmt.Sprintf("a container %v whose groups", resource["id"]), setToday: orNoSettings(layers),
|
||||||
|
said: fmt.Sprintf(
|
||||||
|
"%s has a container %v whose groups say ${setting:%s}, and nothing sets %q for it — an operator's "+
|
||||||
|
"value is the assignment's, never the definition's (novox/hq ADR 0112): "+
|
||||||
|
"`settings set %s <file>` with {%q: …}%s",
|
||||||
|
module, resource["id"], key, key, module, key, orNoSettings(layers))}
|
||||||
|
}
|
||||||
|
v := plainly(value)
|
||||||
|
id, whole := wholeID(v)
|
||||||
|
if !whole || id == 0 {
|
||||||
|
return fmt.Errorf("%s: the setting %q is %q, which cannot be a group of the container %v: a supplementary "+
|
||||||
|
"group is a whole number above zero, never a name or other text (novox/hq ADR 0480 clause 7, ADR 0328 §15)",
|
||||||
|
module, key, v, resource["id"])
|
||||||
|
}
|
||||||
|
filled = append(filled, int(id))
|
||||||
|
}
|
||||||
|
resource["groups"] = filled
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ type filler struct {
|
|||||||
// seatInto, machineInto and contributionsInto. A pass that comes to read another field adds it here,
|
// seatInto, machineInto and contributionsInto. A pass that comes to read another field adds it here,
|
||||||
// or the sweep refuses the placeholder it would have filled.
|
// or the sweep refuses the placeholder it would have filled.
|
||||||
var fillers = []filler{
|
var fillers = []filler{
|
||||||
{namespace: "setting", patterns: []*regexp.Regexp{settingRef}, fields: map[string][]string{"file": {"content"}, "service": {"unit"}, "directory": {"owner"}}},
|
{namespace: "setting", patterns: []*regexp.Regexp{settingRef}, fields: map[string][]string{"file": {"content"}, "service": {"unit"}, "directory": {"owner"}, "container": {"groups"}}},
|
||||||
{namespace: "dir", patterns: []*regexp.Regexp{dirRef}, fields: map[string][]string{"*": {"path", "content", "volumes", "env", "env-file"}}},
|
{namespace: "dir", patterns: []*regexp.Regexp{dirRef}, fields: map[string][]string{"*": {"path", "content", "volumes", "env", "env-file"}}},
|
||||||
{namespace: "access", patterns: []*regexp.Regexp{accessRef}, fields: map[string][]string{"*": {"path", "content", "volumes", "env", "env-file"}}},
|
{namespace: "access", patterns: []*regexp.Regexp{accessRef}, fields: map[string][]string{"*": {"path", "content", "volumes", "env", "env-file"}}},
|
||||||
{namespace: "secret", patterns: []*regexp.Regexp{placeholder}, fields: map[string][]string{"file": {"content"}},
|
{namespace: "secret", patterns: []*regexp.Regexp{placeholder}, fields: map[string][]string{"file": {"content"}},
|
||||||
|
|||||||
@@ -324,6 +324,15 @@ type File struct {
|
|||||||
// depend on a field somewhere else.
|
// depend on a field somewhere else.
|
||||||
Bytes string `json:"bytes,omitempty"`
|
Bytes string `json:"bytes,omitempty"`
|
||||||
|
|
||||||
|
// Digest and Size say the content is not carried but fetched: a file over FileReferenceThreshold travels as
|
||||||
|
// the sha256 digest of its content, "sha256:<64 hex>", and its size in bytes, in place of Content or Bytes
|
||||||
|
// (novox/hq ADR 0486 §1). The node-engine fetches the object of that name from FileBucket, checks what it
|
||||||
|
// received against Digest and Size, and only then writes the file. Exclusive with Content, Sealed, Bytes,
|
||||||
|
// VaultSealed and Secrets; sent only to an engine that reports FileReferencesCapability, as an older engine
|
||||||
|
// refuses a field it does not know, whole.
|
||||||
|
Digest string `json:"digest,omitempty"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
|
||||||
// Owner is the user this belongs to, by name. Absent means root.
|
// Owner is the user this belongs to, by name. Absent means root.
|
||||||
Owner string `json:"owner,omitempty"`
|
Owner string `json:"owner,omitempty"`
|
||||||
|
|
||||||
@@ -465,6 +474,10 @@ func (f *File) validate(where string, _ bool) []string {
|
|||||||
problems = append(problems, where+": a file the vault sealed whole has no holes for secrets")
|
problems = append(problems, where+": a file the vault sealed whole has no holes for secrets")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
problems = append(problems, f.checkReference(where)...)
|
||||||
|
if f.Digest != "" || f.Size != 0 {
|
||||||
|
said = append(said, "digest")
|
||||||
|
}
|
||||||
if len(said) > 1 {
|
if len(said) > 1 {
|
||||||
sort.Strings(said)
|
sort.Strings(said)
|
||||||
problems = append(problems, where+
|
problems = append(problems, where+
|
||||||
@@ -1454,6 +1467,14 @@ type Container struct {
|
|||||||
// container runs as its image says. Its key is `identity`, the manifest's word; the field is not, because
|
// container runs as its image says. Its key is `identity`, the manifest's word; the field is not, because
|
||||||
// a resource's Identity is its id.
|
// a resource's Identity is its id.
|
||||||
RunAs *ContainerIdentity `json:"identity,omitempty"`
|
RunAs *ContainerIdentity `json:"identity,omitempty"`
|
||||||
|
|
||||||
|
// Groups are the supplementary groups the container's process joins, as numbers, each passed to the runtime
|
||||||
|
// as a group to add (novox/hq ADR 0480 clause 7, issue 1636): n8n writes the media library through the
|
||||||
|
// library's group, which a stock image knows nothing of. Each is above zero and below the kernel's "no group"
|
||||||
|
// (ADR 0328 §15: never root). Part of the spec, so a change recreates the container. Absent: none are added.
|
||||||
|
// An older node-engine refuses the key (strict decoding), so the controller sends it only to a node that
|
||||||
|
// reports it reads it.
|
||||||
|
Groups []int64 `json:"groups,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// The ways an image is told who to run as (novox/hq ADR 0325 §2).
|
// The ways an image is told who to run as (novox/hq ADR 0325 §2).
|
||||||
@@ -1521,6 +1542,30 @@ func (i *ContainerIdentity) problems(where string, env map[string]string) []stri
|
|||||||
return problems
|
return problems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// groupProblems refuses a supplementary group that is root's or below it, the kernel's "no group" number, or a
|
||||||
|
// number named twice (novox/hq ADR 0480 clause 7, ADR 0328 §15).
|
||||||
|
func (c *Container) groupProblems(where string) []string {
|
||||||
|
var problems []string
|
||||||
|
seen := map[int64]bool{}
|
||||||
|
for _, g := range c.Groups {
|
||||||
|
switch {
|
||||||
|
case g == 0:
|
||||||
|
problems = append(problems, fmt.Sprintf("%s: container %s adds group 0, and the mesh never makes or keeps anything "+
|
||||||
|
"as root: a supplementary group is never root's (novox/hq ADR 0328 §15)", where, c.Name))
|
||||||
|
case g < 0:
|
||||||
|
problems = append(problems, fmt.Sprintf("%s: container %s adds group %d, below zero: a group is a whole number "+
|
||||||
|
"above zero", where, c.Name, g))
|
||||||
|
case g >= 1<<32-1:
|
||||||
|
problems = append(problems, fmt.Sprintf("%s: container %s adds group %d, which is the kernel's \"no group\" "+
|
||||||
|
"number or beyond it", where, c.Name, g))
|
||||||
|
case seen[g]:
|
||||||
|
problems = append(problems, fmt.Sprintf("%s: container %s adds group %d twice", where, c.Name, g))
|
||||||
|
}
|
||||||
|
seen[g] = true
|
||||||
|
}
|
||||||
|
return problems
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Container) Identity() string { return c.ID }
|
func (c *Container) Identity() string { return c.ID }
|
||||||
func (c *Container) Kind() Type { return TypeContainer }
|
func (c *Container) Kind() Type { return TypeContainer }
|
||||||
func (c *Container) Target() string { return c.Name }
|
func (c *Container) Target() string { return c.Name }
|
||||||
@@ -1603,6 +1648,7 @@ func (c *Container) validate(where string, _ bool) []string {
|
|||||||
"runs and exits, and joins nothing afterwards")
|
"runs and exits, and joins nothing afterwards")
|
||||||
}
|
}
|
||||||
problems = append(problems, c.RunAs.problems(where+": container "+c.Name, c.Env)...)
|
problems = append(problems, c.RunAs.problems(where+": container "+c.Name, c.Env)...)
|
||||||
|
problems = append(problems, c.groupProblems(where)...)
|
||||||
return append(problems, checkImage(where, c.Image)...)
|
return append(problems, checkImage(where, c.Image)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package declaration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The three names of a file travelling by digest (novox/hq ADR 0486 §1, §2, §5), shared by the node-engine that
|
||||||
|
// reads them and the controller that writes them.
|
||||||
|
const (
|
||||||
|
// FileReferenceThreshold is the size above which a file travels by digest: a file of this many bytes or fewer
|
||||||
|
// stays in the declaration as it is.
|
||||||
|
FileReferenceThreshold = 64 * 1024
|
||||||
|
// FileBucket is the object bucket of the bus that holds the contents, each under the hex of its sha256 digest
|
||||||
|
// (ObjectName). The controller alone writes it; a node-engine reads an object by name.
|
||||||
|
FileBucket = "declaration-files"
|
||||||
|
// FileReferencesCapability is what a node-engine reports when it reads a file given as a digest and size.
|
||||||
|
FileReferencesCapability = "file-references"
|
||||||
|
)
|
||||||
|
|
||||||
|
var digestForm = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
||||||
|
|
||||||
|
// DigestOf is the digest a reference names for a content: "sha256:<64 lower-case hex>".
|
||||||
|
func DigestOf(content []byte) string {
|
||||||
|
sum := sha256.Sum256(content)
|
||||||
|
return "sha256:" + hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectName is the name of the object holding the content a digest names: the hex without its "sha256:" prefix.
|
||||||
|
func ObjectName(digest string) (string, error) {
|
||||||
|
if !digestForm.MatchString(digest) {
|
||||||
|
return "", fmt.Errorf("digest %q is not sha256:<64 lower-case hex characters>", digest)
|
||||||
|
}
|
||||||
|
return digest[len("sha256:"):], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsReference reports whether the file gives its content as a digest and size instead of carrying it.
|
||||||
|
func (f *File) IsReference() bool { return f.Digest != "" }
|
||||||
|
|
||||||
|
// checkReference judges the digest and size, and what a reference may not be given with.
|
||||||
|
func (f *File) checkReference(where string) []string {
|
||||||
|
if f.Digest == "" && f.Size == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var problems []string
|
||||||
|
if !digestForm.MatchString(f.Digest) {
|
||||||
|
problems = append(problems, fmt.Sprintf("%s: digest %q; a file by reference names its content as "+
|
||||||
|
"sha256:<64 lower-case hex characters>", where, f.Digest))
|
||||||
|
}
|
||||||
|
if f.Size <= 0 {
|
||||||
|
problems = append(problems, fmt.Sprintf("%s: size %d; a file by reference states its size in bytes, "+
|
||||||
|
"more than zero", where, f.Size))
|
||||||
|
}
|
||||||
|
if f.Content != "" || f.Bytes != "" || f.Sealed != "" || f.VaultSealed != nil || len(f.Secrets) > 0 {
|
||||||
|
problems = append(problems, where+": a file by reference carries its content in the object its digest "+
|
||||||
|
"names, so it says no content, bytes, sealed, vault-sealed or secrets")
|
||||||
|
}
|
||||||
|
if f.Into != "" {
|
||||||
|
problems = append(problems, where+": a file by reference is written whole; a file written into says its "+
|
||||||
|
"part in content")
|
||||||
|
}
|
||||||
|
return problems
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body is the bytes the file's content stands for: its content, or its bytes decoded.
|
||||||
|
func (f *File) Body() ([]byte, error) {
|
||||||
|
if f.Bytes == "" {
|
||||||
|
return []byte(f.Content), nil
|
||||||
|
}
|
||||||
|
b, err := base64.StdEncoding.DecodeString(f.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("file %s: bytes are not base64: %w", f.ID, err)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TravelsByDigest reports whether the controller may put this file's content in the bucket: it is plain content
|
||||||
|
// or bytes (not sealed, not filled with secrets, not written into another file) of more than FileReferenceThreshold
|
||||||
|
// bytes.
|
||||||
|
func (f *File) TravelsByDigest() bool {
|
||||||
|
if f.IsReference() || f.Sealed != "" || f.VaultSealed != nil || len(f.Secrets) > 0 || f.Into != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if f.Bytes != "" {
|
||||||
|
// DecodedLen counts padding, so the exact length is taken off the decoded bytes.
|
||||||
|
body, err := f.Body()
|
||||||
|
return err == nil && len(body) > FileReferenceThreshold
|
||||||
|
}
|
||||||
|
return len(f.Content) > FileReferenceThreshold
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsReference is the file as it is sent to an engine that reports FileReferencesCapability: a copy with the digest
|
||||||
|
// and size in place of the content, and the content the controller puts into the bucket under ObjectName(digest).
|
||||||
|
// A file that does not travel by digest is refused, not passed through, so a caller cannot send a small file empty.
|
||||||
|
func (f *File) AsReference() (File, []byte, error) {
|
||||||
|
if !f.TravelsByDigest() {
|
||||||
|
return File{}, nil, fmt.Errorf("file %s does not travel by digest", f.ID)
|
||||||
|
}
|
||||||
|
body, err := f.Body()
|
||||||
|
if err != nil {
|
||||||
|
return File{}, nil, err
|
||||||
|
}
|
||||||
|
ref := *f
|
||||||
|
ref.Content, ref.Bytes = "", ""
|
||||||
|
ref.Digest, ref.Size = DigestOf(body), int64(len(body))
|
||||||
|
return ref, body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify checks what was fetched against the digest and size the file names; the node-engine writes the file only
|
||||||
|
// after it returns nil. A mismatch is the apply's failure, never a partial apply (ADR 0486 §1).
|
||||||
|
func (f *File) Verify(body []byte) error {
|
||||||
|
if !f.IsReference() {
|
||||||
|
return fmt.Errorf("file %s is not given by reference", f.ID)
|
||||||
|
}
|
||||||
|
if int64(len(body)) != f.Size {
|
||||||
|
return fmt.Errorf("file %s: the object is %d bytes and the declaration says %d", f.ID, len(body), f.Size)
|
||||||
|
}
|
||||||
|
if got := DigestOf(body); got != f.Digest {
|
||||||
|
return fmt.Errorf("file %s: the object hashes to %s and the declaration names %s", f.ID, got, f.Digest)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ type BuildRecord struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Artifact is one thing a build made: mesh-controller internal/chain/records.go Artifact, json keys name, kind and
|
// Artifact is one thing a build made: mesh-controller internal/chain/records.go Artifact, json keys name, kind and
|
||||||
// reference, nothing else (novox/hq ADR 0478 clause 7).
|
// reference, nothing else (novox/hq ADR 0478 clause 8).
|
||||||
type Artifact struct {
|
type Artifact struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
|
|||||||
+16
-2
@@ -389,11 +389,20 @@ type Module struct {
|
|||||||
Src uint64 `json:"src,omitempty"`
|
Src uint64 `json:"src,omitempty"`
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Commit string `json:"commit"`
|
Commit string `json:"commit"`
|
||||||
// Path is the module's directory in its repository, trimmed of slashes, empty when its manifest is the repository's
|
// Path is the module's directory in its repository, trimmed of slashes, and RootPath (".") when its manifest is the
|
||||||
// root (novox/hq ADR 0478 clause 8): the build tools need it. A value written before has none.
|
// repository's root (novox/hq ADR 0478 clause 9, issue 1615): the build tools need it. Empty means not known: a value
|
||||||
|
// written before the indexer learned paths has none.
|
||||||
Path string `json:"path,omitempty"`
|
Path string `json:"path,omitempty"`
|
||||||
|
// Toolchains are the toolchains FP2 was computed with, by language (go, typescript): the version installed and the
|
||||||
|
// build module's recipe version the build nodes agreed on then (novox/hq ADR 0477 clause 2, ADR 0478 clause 3). A builder
|
||||||
|
// builds the fingerprint only with these. Empty when FP2 is empty or the module has no toolchain; a value written
|
||||||
|
// before has none (issue 1615).
|
||||||
|
Toolchains map[string]ToolchainUsed `json:"toolchains,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RootPath is Module.Path of a module whose manifest is its repository's root.
|
||||||
|
const RootPath = "."
|
||||||
|
|
||||||
// KeptInputs2 are SourceInputs2 without the module's name (the key has it) and without toolchains (those come from
|
// KeptInputs2 are SourceInputs2 without the module's name (the key has it) and without toolchains (those come from
|
||||||
// the build modules' toolchain keys when the fingerprint is computed): what a Module keeps of a merge.
|
// the build modules' toolchain keys when the fingerprint is computed): what a Module keeps of a merge.
|
||||||
type KeptInputs2 struct {
|
type KeptInputs2 struct {
|
||||||
@@ -421,6 +430,11 @@ func (m Module) check() error {
|
|||||||
case m.FP2 != "" && m.None2 != "":
|
case m.FP2 != "" && m.None2 != "":
|
||||||
return fmt.Errorf("delivery: a module has both fp2 and none2")
|
return fmt.Errorf("delivery: a module has both fp2 and none2")
|
||||||
}
|
}
|
||||||
|
for language, t := range m.Toolchains {
|
||||||
|
if t.Version == "" || t.Recipe == "" {
|
||||||
|
return fmt.Errorf("delivery: the module's toolchain of %q needs its version and its recipe version", language)
|
||||||
|
}
|
||||||
|
}
|
||||||
if m.Inputs2 != nil && m.Manifest == "" {
|
if m.Inputs2 != nil && m.Manifest == "" {
|
||||||
return fmt.Errorf("delivery: a module keeps new-form inputs but has no manifest digest")
|
return fmt.Errorf("delivery: a module keeps new-form inputs but has no manifest digest")
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021093645-465beb4ea5e3
|
# git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021124357-863a981e014e
|
||||||
## explicit; go 1.26.0
|
## explicit; go 1.26.0
|
||||||
git.novox.be/novox/mesh-sdk/go/asks
|
git.novox.be/novox/mesh-sdk/go/asks
|
||||||
git.novox.be/novox/mesh-sdk/go/catalogue
|
git.novox.be/novox/mesh-sdk/go/catalogue
|
||||||
|
|||||||
Reference in New Issue
Block a user