Take the mesh-sdk that holds the container-groups capability rebased on main (issue 1638)

This commit is contained in:
2026-10-21 15:04:37 +02:00
parent d8d6d196a4
commit 6ef78580ce
7 changed files with 145 additions and 6 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ module github.com/novox/mesh-controller
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021122745-e0e1a91496de
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021130332-b5d8eb03cea3
github.com/jackc/pgx/v5 v5.10.0
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
+2 -2
View File
@@ -1,7 +1,7 @@
git.novox.be/novox/mesh-host v0.0.0-20261018171808-9d9e3a46c4f5 h1:E5stfIeCN6uYgA9TCv+4K8JgaURg3zQppvUgJLaDMP4=
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.20261021122745-e0e1a91496de h1:0iw6Xg/DzsvjIOGq2NE/YeLULuVG0Dv6eUKKMQS+4ys=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021122745-e0e1a91496de/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021130332-b5d8eb03cea3 h1:1LLMeQbvmmVPMQjqo6fFd/WtIx4e6v8TGn55dyB4wps=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021130332-b5d8eb03cea3/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/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+1 -1
View File
@@ -9,7 +9,7 @@ a second place to clean besides the catalogue itself.
mesh-catalog e0bc919d4945bb5f13f971f74643a0b714cf45da modules/*/module.json, modules/nats/Dockerfile
mesh-host 9d9e3a46c4f5943cfffb413d962ce48b75e2fd39 examples/foundation-first-node.lock
mesh-sdk e0e1a91496de28becd8387ac7acad4cdb85fdda2 conformance/events/module-event.json
mesh-sdk b5d8eb03cea33fd78e9962a0f226f46c25d4143f conformance/events/module-event.json
To move them, from this repository's root, with the two repositories checked out beside it:
@@ -66,7 +66,8 @@ 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
// contribution out on every machine, silently, so it is refused. A detector added there is added here.
var KnownCapabilities = []string{
"battery", "container-groups", "container-runtime", "firewall", "graphical-session", "overlay", "package-manager",
"battery", "container-groups", "container-runtime", "file-references", "firewall", "graphical-session", "overlay",
"package-manager",
"power-meter", "privileged", "seat", "service-manager", "uplink-dhcpcd", "uplink-networkmanager",
"uplink-systemd-networkd", "user-namespaces", "virtualisation",
}
@@ -324,6 +324,15 @@ type File struct {
// depend on a field somewhere else.
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 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, f.checkReference(where)...)
if f.Digest != "" || f.Size != 0 {
said = append(said, "digest")
}
if len(said) > 1 {
sort.Strings(said)
problems = append(problems, where+
@@ -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
}
+1 -1
View File
@@ -1,4 +1,4 @@
# git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021122745-e0e1a91496de
# git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021130332-b5d8eb03cea3
## explicit; go 1.26.0
git.novox.be/novox/mesh-sdk/go/asks
git.novox.be/novox/mesh-sdk/go/catalogue