Take mesh-sdk at 465beb4, which carries shares on an s3-bucket requirement (issue 1593)
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.20261020085642-ab0c16ddad26
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021093645-465beb4ea5e3
|
||||||
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
|
||||||
|
|||||||
@@ -1,11 +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 h1:E5stfIeCN6uYgA9TCv+4K8JgaURg3zQppvUgJLaDMP4=
|
||||||
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.20261018184557-238440d6f8c4 h1:yYDxFRJoH59kiCoEulD+FBwctyf9CrtKIxlEAsVL39Q=
|
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.20261018184557-238440d6f8c4/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
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.20261019091011-aa68b67389c9 h1:qHpN/fpTDqijfMVoepxUT6mPkfcydb4LiPKsIokv6iM=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019091011-aa68b67389c9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020085642-ab0c16ddad26 h1:oT1l3ZdjyF82MK14QMLKzoH2+iOFH/+UEYdlK3T2f7k=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020085642-ab0c16ddad26/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 ab0c16ddad260289006a40c5dc6fd4973e0b5e0b conformance/events/module-event.json
|
mesh-sdk 465beb4ea5e37eb36f6ba7fc60274de2d5a0d060 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:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BucketShare is one entry of `shares` on a module's requirement of `s3-bucket` (novox/hq ADR 0468 §8): who else may
|
||||||
|
// put or get in the consumer's bucket, and with which methods. Not the NFS shares of shares.go (ADR 0263).
|
||||||
|
//
|
||||||
|
// "requires": [{"name": "s3-bucket", "shares": [{"to": "seat:node-files", "methods": ["put"]},
|
||||||
|
// {"to": "print-and-scan", "methods": ["get"]}]}]
|
||||||
|
//
|
||||||
|
// `to` is a module name, or `seat:<seat name>`. `methods` is a non-empty subset of get and put, without a repeat.
|
||||||
|
// Shares are allowed on `s3-bucket` only, and no two entries name the same `to`.
|
||||||
|
type BucketShare struct {
|
||||||
|
To string `json:"to"`
|
||||||
|
Methods []string `json:"methods"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BucketRequirement is the requirement `shares` may be written on.
|
||||||
|
const BucketRequirement = "s3-bucket"
|
||||||
|
|
||||||
|
// RequireSharesKey is where a stored manifest keeps the shares of its requirements: beside `requires`, which stays a
|
||||||
|
// plain list of names, for the reason OptionalRequiresKey is beside it. An older controller sets the unknown key
|
||||||
|
// aside instead of failing the catalogue.
|
||||||
|
const RequireSharesKey = "requires-shares"
|
||||||
|
|
||||||
|
// SharesValue is the key of the value a consumer gives the provider of s3-bucket in its grant: the list of
|
||||||
|
// {to, methods} sorted by `to`. A consumer's own `contributes` may not name it.
|
||||||
|
const SharesValue = "shares"
|
||||||
|
|
||||||
|
var shareTarget = regexp.MustCompile(`^(seat:)?[a-z0-9][a-z0-9-]*$`)
|
||||||
|
|
||||||
|
// checkBucketShares refuses a malformed `shares` of one requirement, naming the entry and ADR 0468 §8.
|
||||||
|
func checkBucketShares(requirement string, shares []BucketShare) error {
|
||||||
|
if len(shares) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if requirement != BucketRequirement {
|
||||||
|
return fmt.Errorf("requires %s: `shares` is allowed on %s only (novox/hq ADR 0468 §8)", requirement, BucketRequirement)
|
||||||
|
}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for i, s := range shares {
|
||||||
|
at := fmt.Sprintf("requires %s: shares[%d] (to %q)", requirement, i, s.To)
|
||||||
|
if !shareTarget.MatchString(s.To) {
|
||||||
|
return fmt.Errorf("%s: `to` is a module name or seat:<seat name> (novox/hq ADR 0468 §8)", at)
|
||||||
|
}
|
||||||
|
if seen[s.To] {
|
||||||
|
return fmt.Errorf("%s: a second entry for the same `to` (novox/hq ADR 0468 §8)", at)
|
||||||
|
}
|
||||||
|
seen[s.To] = true
|
||||||
|
if len(s.Methods) == 0 {
|
||||||
|
return fmt.Errorf("%s: `methods` is empty; name get, put or both (novox/hq ADR 0468 §8)", at)
|
||||||
|
}
|
||||||
|
used := map[string]bool{}
|
||||||
|
for _, method := range s.Methods {
|
||||||
|
if method != "get" && method != "put" {
|
||||||
|
return fmt.Errorf("%s: method %q is not get or put (novox/hq ADR 0468 §8)", at, method)
|
||||||
|
}
|
||||||
|
if used[method] {
|
||||||
|
return fmt.Errorf("%s: method %q twice (novox/hq ADR 0468 §8)", at, method)
|
||||||
|
}
|
||||||
|
used[method] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readRequireShares reads the stored form's side key.
|
||||||
|
func readRequireShares(raw json.RawMessage) (map[string][]BucketShare, error) {
|
||||||
|
var out map[string][]BucketShare
|
||||||
|
if err := json.Unmarshal(raw, &out); err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: an object of requirement to a list of {to, methods}: %w", RequireSharesKey, err)
|
||||||
|
}
|
||||||
|
for requirement, shares := range out {
|
||||||
|
if err := checkBucketShares(requirement, shares); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sortedShares is a requirement's shares as they are handed to the provider: sorted by `to` (a file whose lines move
|
||||||
|
// looks changed), each entry's methods sorted, and a copy so the manifest is not reordered.
|
||||||
|
func sortedShares(shares []BucketShare) []BucketShare {
|
||||||
|
out := make([]BucketShare, 0, len(shares))
|
||||||
|
for _, s := range shares {
|
||||||
|
methods := append([]string(nil), s.Methods...)
|
||||||
|
sort.Strings(methods)
|
||||||
|
out = append(out, BucketShare{To: s.To, Methods: methods})
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i].To < out[j].To })
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// withShares adds this module's shares of the requirement to the values it gives the provider. A module whose own
|
||||||
|
// `contributes` already names `shares` is refused: the field is the only way to say it (ADR 0468 §8).
|
||||||
|
func withShares(m Manifest, requirement string, values map[string]any) (map[string]any, error) {
|
||||||
|
shares := m.RequireShares[requirement]
|
||||||
|
if _, named := values[SharesValue]; named && requirement == BucketRequirement {
|
||||||
|
return nil, fmt.Errorf("%s contributing to %s names `shares`; write it on its requirement instead (novox/hq ADR 0468 §8)",
|
||||||
|
m.Module, requirement)
|
||||||
|
}
|
||||||
|
if len(shares) == 0 {
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
|
out := make(map[string]any, len(values)+1)
|
||||||
|
for k, v := range values {
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
listed := []any{}
|
||||||
|
for _, s := range sortedShares(shares) {
|
||||||
|
methods := []any{}
|
||||||
|
for _, method := range s.Methods {
|
||||||
|
methods = append(methods, method)
|
||||||
|
}
|
||||||
|
listed = append(listed, map[string]any{"to": s.To, "methods": methods})
|
||||||
|
}
|
||||||
|
out[SharesValue] = listed
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@@ -1783,6 +1783,10 @@ func (r Resolution) contributions(settings SettingsBy, grants []Grant,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// What the module shares of its bucket reaches the provider with the rest (novox/hq ADR 0468 §8).
|
||||||
|
if values, err = withShares(m, to, values); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
out[to] = append(out[to], Contribution{From: m.Module, Values: values})
|
out[to] = append(out[to], Contribution{From: m.Module, Values: values})
|
||||||
}
|
}
|
||||||
// Several contributions to one requirement (ADR 0094's sibling for `contributes`): an
|
// Several contributions to one requirement (ADR 0094's sibling for `contributes`): an
|
||||||
@@ -2169,11 +2173,19 @@ func (r Resolution) ContributionsFrom(requirement, module string, settings Setti
|
|||||||
mine = append(mine, g.Values)
|
mine = append(mine, g.Values)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var asker Manifest
|
||||||
|
for _, m := range r.Modules {
|
||||||
|
if m.Module == module {
|
||||||
|
asker = m
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(mine) == 1 {
|
if len(mine) == 1 {
|
||||||
|
// Its shares are already in, put there where its contribution was composed (contributions).
|
||||||
return mine[0], true, nil
|
return mine[0], true, nil
|
||||||
}
|
}
|
||||||
if len(mine) > 1 {
|
if len(mine) > 1 {
|
||||||
return map[string]any{}, true, nil
|
values, err := withShares(asker, requirement, map[string]any{})
|
||||||
|
return values, err == nil, err
|
||||||
}
|
}
|
||||||
// It contributes no payload — but a require-only consumer of a parameterless provision (one whose
|
// It contributes no payload — but a require-only consumer of a parameterless provision (one whose
|
||||||
// `serves` names no consumer-supplied key: `redis-cache`, `amqp`) still ASKS for it and must be
|
// `serves` names no consumer-supplied key: `redis-cache`, `amqp`) still ASKS for it and must be
|
||||||
@@ -2186,7 +2198,8 @@ func (r Resolution) ContributionsFrom(requirement, module string, settings Setti
|
|||||||
}
|
}
|
||||||
for _, req := range m.Requires {
|
for _, req := range m.Requires {
|
||||||
if req == requirement {
|
if req == requirement {
|
||||||
return map[string]any{}, true, nil
|
values, err := withShares(m, requirement, map[string]any{})
|
||||||
|
return values, err == nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-2
@@ -494,6 +494,9 @@ type Manifest struct {
|
|||||||
// by UnmarshalJSON from the long form of a `requires` entry, {name, optional}, or from the stored
|
// by UnmarshalJSON from the long form of a `requires` entry, {name, optional}, or from the stored
|
||||||
// form's `optional-requires` list; never written by hand.
|
// form's `optional-requires` list; never written by hand.
|
||||||
OptionalRequires map[string]bool `json:"-"`
|
OptionalRequires map[string]bool `json:"-"`
|
||||||
|
// RequireShares is who else may use the bucket a module requires, by requirement (BucketShare, novox/hq ADR 0468
|
||||||
|
// §8): filled from the long form's `shares`, or from the stored form's `requires-shares`; never written by hand.
|
||||||
|
RequireShares map[string][]BucketShare `json:"-"`
|
||||||
|
|
||||||
// Emits are the events this module publishes, named **locally**: `order.placed`, not a subject
|
// Emits are the events this module publishes, named **locally**: `order.placed`, not a subject
|
||||||
// and not a routing key. The mesh derives where it lands (design 29 §1), so reorganising the
|
// and not a routing key. The mesh derives where it lands (design 29 §1), so reorganising the
|
||||||
@@ -1538,8 +1541,9 @@ func (m *Manifest) UnmarshalJSON(raw []byte) error {
|
|||||||
delete(keys, "contributes")
|
delete(keys, "contributes")
|
||||||
}
|
}
|
||||||
var optionalRequires map[string]bool
|
var optionalRequires map[string]bool
|
||||||
|
var requireShares map[string][]BucketShare
|
||||||
if requires, ok := keys["requires"]; ok && string(requires) != "null" {
|
if requires, ok := keys["requires"]; ok && string(requires) != "null" {
|
||||||
names, optional, err := readRequires(requires)
|
names, optional, shares, err := readRequires(requires)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1549,6 +1553,23 @@ func (m *Manifest) UnmarshalJSON(raw []byte) error {
|
|||||||
}
|
}
|
||||||
keys["requires"] = plainNames
|
keys["requires"] = plainNames
|
||||||
optionalRequires = optional
|
optionalRequires = optional
|
||||||
|
requireShares = shares
|
||||||
|
}
|
||||||
|
// The stored form's shares (RequireSharesKey).
|
||||||
|
if stored, ok := keys[RequireSharesKey]; ok {
|
||||||
|
if string(stored) != "null" {
|
||||||
|
stood, err := readRequireShares(stored)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for requirement, shares := range stood {
|
||||||
|
if requireShares == nil {
|
||||||
|
requireShares = map[string][]BucketShare{}
|
||||||
|
}
|
||||||
|
requireShares[requirement] = shares
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(keys, RequireSharesKey)
|
||||||
}
|
}
|
||||||
// The stored form: `requires` plain, and which are optional beside it (OptionalRequiresKey).
|
// The stored form: `requires` plain, and which are optional beside it (OptionalRequiresKey).
|
||||||
if stored, ok := keys[OptionalRequiresKey]; ok {
|
if stored, ok := keys[OptionalRequiresKey]; ok {
|
||||||
@@ -1599,6 +1620,7 @@ func (m *Manifest) UnmarshalJSON(raw []byte) error {
|
|||||||
m.unknown = unknown
|
m.unknown = unknown
|
||||||
m.forms = forms
|
m.forms = forms
|
||||||
m.OptionalRequires = optionalRequires
|
m.OptionalRequires = optionalRequires
|
||||||
|
m.RequireShares = requireShares
|
||||||
if len(plain) > 0 {
|
if len(plain) > 0 {
|
||||||
m.Secrets = plain
|
m.Secrets = plain
|
||||||
}
|
}
|
||||||
@@ -1611,6 +1633,21 @@ func (m *Manifest) UnmarshalJSON(raw []byte) error {
|
|||||||
if len(contributesMany) > 0 {
|
if len(contributesMany) > 0 {
|
||||||
m.ContributesMany = contributesMany
|
m.ContributesMany = contributesMany
|
||||||
}
|
}
|
||||||
|
if err := m.checkSharesNotContributed(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkSharesNotContributed refuses a module whose own `contributes` to s3-bucket names `shares` (ADR 0468 §8).
|
||||||
|
func (m Manifest) checkSharesNotContributed() error {
|
||||||
|
if len(m.RequireShares[BucketRequirement]) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, named := m.Contributes[BucketRequirement][SharesValue]; named {
|
||||||
|
return fmt.Errorf("contributes.%s names `shares` and so does its requirement; keep one, the requirement's (novox/hq ADR 0468 §8)",
|
||||||
|
BucketRequirement)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1655,7 +1692,7 @@ func (m Manifest) marshalFields() ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(m.SecretsMany) == 0 && len(m.ContributesMany) == 0 && len(m.OptionalRequires) == 0 {
|
if len(m.SecretsMany) == 0 && len(m.ContributesMany) == 0 && len(m.OptionalRequires) == 0 && len(m.RequireShares) == 0 {
|
||||||
return raw, nil
|
return raw, nil
|
||||||
}
|
}
|
||||||
var keys map[string]json.RawMessage
|
var keys map[string]json.RawMessage
|
||||||
@@ -1671,6 +1708,13 @@ func (m Manifest) marshalFields() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
keys[OptionalRequiresKey] = listed
|
keys[OptionalRequiresKey] = listed
|
||||||
}
|
}
|
||||||
|
if len(m.RequireShares) > 0 {
|
||||||
|
listed, err := json.Marshal(m.RequireShares)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keys[RequireSharesKey] = listed
|
||||||
|
}
|
||||||
if len(m.ContributesMany) > 0 {
|
if len(m.ContributesMany) > 0 {
|
||||||
mergedContributes := map[string]any{}
|
mergedContributes := map[string]any{}
|
||||||
for to, values := range m.Contributes {
|
for to, values := range m.Contributes {
|
||||||
|
|||||||
@@ -32,17 +32,20 @@ const OnlyWhenBound = "only-when-bound"
|
|||||||
type requirementForm struct {
|
type requirementForm struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Optional bool `json:"optional,omitempty"`
|
Optional bool `json:"optional,omitempty"`
|
||||||
|
// Shares is who else may use the consumer's bucket (BucketShare, novox/hq ADR 0468 §8), on s3-bucket only.
|
||||||
|
Shares []BucketShare `json:"shares,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// readRequires reads `requires` in both of its forms — a bare name, or {name, optional} — into the
|
// readRequires reads `requires` in both of its forms — a bare name, or {name, optional} — into the
|
||||||
// names and which of them are optional.
|
// names and which of them are optional.
|
||||||
func readRequires(raw json.RawMessage) ([]string, map[string]bool, error) {
|
func readRequires(raw json.RawMessage) ([]string, map[string]bool, map[string][]BucketShare, error) {
|
||||||
var entries []json.RawMessage
|
var entries []json.RawMessage
|
||||||
if err := json.Unmarshal(raw, &entries); err != nil {
|
if err := json.Unmarshal(raw, &entries); err != nil {
|
||||||
return nil, nil, fmt.Errorf("requires: a list of names, or of {name, optional}: %w", err)
|
return nil, nil, nil, fmt.Errorf("requires: a list of names, or of {name, optional}: %w", err)
|
||||||
}
|
}
|
||||||
var names []string
|
var names []string
|
||||||
optional := map[string]bool{}
|
optional := map[string]bool{}
|
||||||
|
var shares map[string][]BucketShare
|
||||||
for i, e := range entries {
|
for i, e := range entries {
|
||||||
var plain string
|
var plain string
|
||||||
if err := json.Unmarshal(e, &plain); err == nil {
|
if err := json.Unmarshal(e, &plain); err == nil {
|
||||||
@@ -53,12 +56,21 @@ func readRequires(raw json.RawMessage) ([]string, map[string]bool, error) {
|
|||||||
dec := json.NewDecoder(bytes.NewReader(e))
|
dec := json.NewDecoder(bytes.NewReader(e))
|
||||||
dec.DisallowUnknownFields()
|
dec.DisallowUnknownFields()
|
||||||
if err := dec.Decode(&full); err != nil {
|
if err := dec.Decode(&full); err != nil {
|
||||||
return nil, nil, fmt.Errorf("requires[%d]: a name, or {name, optional} (novox/hq ADR 0330): %w", i, err)
|
return nil, nil, nil, fmt.Errorf("requires[%d]: a name, or {name, optional} (novox/hq ADR 0330): %w", i, err)
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(full.Name) == "" {
|
if strings.TrimSpace(full.Name) == "" {
|
||||||
return nil, nil, fmt.Errorf("requires[%d] names no requirement: {name, optional}", i)
|
return nil, nil, nil, fmt.Errorf("requires[%d] names no requirement: {name, optional}", i)
|
||||||
}
|
}
|
||||||
names = append(names, full.Name)
|
names = append(names, full.Name)
|
||||||
|
if err := checkBucketShares(full.Name, full.Shares); err != nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("requires[%d]: %w", i, err)
|
||||||
|
}
|
||||||
|
if len(full.Shares) > 0 {
|
||||||
|
if shares == nil {
|
||||||
|
shares = map[string][]BucketShare{}
|
||||||
|
}
|
||||||
|
shares[full.Name] = full.Shares
|
||||||
|
}
|
||||||
if full.Optional {
|
if full.Optional {
|
||||||
optional[full.Name] = true
|
optional[full.Name] = true
|
||||||
}
|
}
|
||||||
@@ -66,7 +78,7 @@ func readRequires(raw json.RawMessage) ([]string, map[string]bool, error) {
|
|||||||
if len(optional) == 0 {
|
if len(optional) == 0 {
|
||||||
optional = nil
|
optional = nil
|
||||||
}
|
}
|
||||||
return names, optional, nil
|
return names, optional, shares, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OptionalRequiresKey is where a stored manifest keeps which of its requirements are optional: beside
|
// OptionalRequiresKey is where a stored manifest keeps which of its requirements are optional: beside
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ type BuildRecord struct {
|
|||||||
At time.Time `json:"at,omitzero"`
|
At time.Time `json:"at,omitzero"`
|
||||||
SourceFingerprint string `json:"source-fingerprint,omitempty"`
|
SourceFingerprint string `json:"source-fingerprint,omitempty"`
|
||||||
SourceInputs *SourceInputs `json:"source-inputs,omitempty"`
|
SourceInputs *SourceInputs `json:"source-inputs,omitempty"`
|
||||||
|
// Made is what the build made (Build.Made, records.go:110); the digest is the tail of an artifact's Reference.
|
||||||
|
Made []Artifact `json:"made,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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).
|
||||||
|
type Artifact struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Reference string `json:"reference"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildsBucket is the bucket the builds are kept in: mesh-controller internal/broker/controller_buckets.go:101,
|
// BuildsBucket is the bucket the builds are kept in: mesh-controller internal/broker/controller_buckets.go:101,
|
||||||
|
|||||||
+81
-14
@@ -377,15 +377,37 @@ func (c Candidate) check() error {
|
|||||||
// Manifest, the digest of the module's manifest, the key of the manifests bucket. A value written before has none of
|
// Manifest, the digest of the module's manifest, the key of the manifests bucket. A value written before has none of
|
||||||
// the three, and a reader of today's shape ignores them.
|
// the three, and a reader of today's shape ignores them.
|
||||||
type Module struct {
|
type Module struct {
|
||||||
V int `json:"v"`
|
V int `json:"v"`
|
||||||
FP string `json:"fp,omitempty"`
|
FP string `json:"fp,omitempty"`
|
||||||
None string `json:"none,omitempty"`
|
None string `json:"none,omitempty"`
|
||||||
FP2 string `json:"fp2,omitempty"`
|
FP2 string `json:"fp2,omitempty"`
|
||||||
None2 string `json:"none2,omitempty"`
|
None2 string `json:"none2,omitempty"`
|
||||||
Manifest string `json:"manifest,omitempty"`
|
Manifest string `json:"manifest,omitempty"`
|
||||||
Src uint64 `json:"src,omitempty"`
|
// Inputs2 are the new-form inputs the merge gave the module, kept so the indexer computes FP2 again when a build
|
||||||
Repository string `json:"repository"`
|
// module's toolchain versions appear, with no merge (novox/hq ADR 0465 clause 2). It needs Manifest.
|
||||||
Commit string `json:"commit"`
|
Inputs2 *KeptInputs2 `json:"inputs2,omitempty"`
|
||||||
|
Src uint64 `json:"src,omitempty"`
|
||||||
|
Repository string `json:"repository"`
|
||||||
|
Commit string `json:"commit"`
|
||||||
|
// Path is the module's directory in its repository, trimmed of slashes, empty when its manifest is the repository's
|
||||||
|
// root (novox/hq ADR 0478 clause 8): the build tools need it. A value written before has none.
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
type KeptInputs2 struct {
|
||||||
|
Tree string `json:"tree"`
|
||||||
|
Bases []string `json:"bases,omitempty"`
|
||||||
|
Builder string `json:"builder,omitempty"`
|
||||||
|
Contexts map[string]string `json:"contexts,omitempty"`
|
||||||
|
Locks map[string]string `json:"locks,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// With is the full inputs of module m: the kept ones and the given toolchains.
|
||||||
|
func (k KeptInputs2) With(module string, toolchains map[string]Toolchain2) SourceInputs2 {
|
||||||
|
return SourceInputs2{Module: module, Tree: k.Tree, Bases: k.Bases, Builder: k.Builder, Contexts: k.Contexts,
|
||||||
|
Locks: k.Locks, Toolchains: toolchains}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Module) check() error {
|
func (m Module) check() error {
|
||||||
@@ -399,6 +421,9 @@ 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")
|
||||||
}
|
}
|
||||||
|
if m.Inputs2 != nil && m.Manifest == "" {
|
||||||
|
return fmt.Errorf("delivery: a module keeps new-form inputs but has no manifest digest")
|
||||||
|
}
|
||||||
return checkNewForm(m.FP2, m.Manifest)
|
return checkNewForm(m.FP2, m.Manifest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,9 +446,14 @@ type Forge struct {
|
|||||||
Merge string `json:"merge,omitempty"`
|
Merge string `json:"merge,omitempty"`
|
||||||
Trees map[string]string `json:"trees,omitempty"`
|
Trees map[string]string `json:"trees,omitempty"`
|
||||||
Root string `json:"root,omitempty"`
|
Root string `json:"root,omitempty"`
|
||||||
Act string `json:"act,omitempty"`
|
// Manifests are the module.json bytes, exact, of each module directory: of the merge's trees on a merged message,
|
||||||
Words string `json:"words,omitempty"`
|
// of Dirs at the head on a pull message. Locks are, of each such directory, its lock files' hex sha256 by path
|
||||||
Open []OpenPull `json:"open,omitempty"`
|
// relative to it (novox/hq ADR 0465 clause 1). Older writers send neither.
|
||||||
|
Manifests map[string]string `json:"manifests,omitempty"`
|
||||||
|
Locks map[string]map[string]string `json:"locks,omitempty"`
|
||||||
|
Act string `json:"act,omitempty"`
|
||||||
|
Words string `json:"words,omitempty"`
|
||||||
|
Open []OpenPull `json:"open,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f Forge) check() error {
|
func (f Forge) check() error {
|
||||||
@@ -455,6 +485,13 @@ func (f Forge) check() error {
|
|||||||
if len(f.Dirs) > 0 && !f.DirsKnown {
|
if len(f.Dirs) > 0 && !f.DirsKnown {
|
||||||
return fmt.Errorf("delivery: a pull message with module directories must say it knows them (dirs-known)")
|
return fmt.Errorf("delivery: a pull message with module directories must say it knows them (dirs-known)")
|
||||||
}
|
}
|
||||||
|
dirs := map[string]bool{}
|
||||||
|
for _, d := range f.Dirs {
|
||||||
|
dirs[d] = true
|
||||||
|
}
|
||||||
|
if err := f.checkInputs(func(d string) bool { return dirs[d] }, "its module directories"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case ForgeMerged:
|
case ForgeMerged:
|
||||||
if len(f.Dirs) > 0 || f.DirsKnown {
|
if len(f.Dirs) > 0 || f.DirsKnown {
|
||||||
return fmt.Errorf("delivery: a merged message carries no module directories")
|
return fmt.Errorf("delivery: a merged message carries no module directories")
|
||||||
@@ -479,10 +516,16 @@ func (f Forge) check() error {
|
|||||||
return fmt.Errorf("delivery: a merged message's tree of the directory %q is empty", dir)
|
return fmt.Errorf("delivery: a merged message's tree of the directory %q is empty", dir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := f.checkInputs(func(d string) bool { _, ok := f.Trees[d]; return ok }, "its trees"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case ForgeRefused:
|
case ForgeRefused:
|
||||||
if len(f.Dirs) > 0 || f.DirsKnown {
|
if len(f.Dirs) > 0 || f.DirsKnown {
|
||||||
return fmt.Errorf("delivery: a refused message carries no module directories")
|
return fmt.Errorf("delivery: a refused message carries no module directories")
|
||||||
}
|
}
|
||||||
|
if len(f.Manifests) > 0 || len(f.Locks) > 0 {
|
||||||
|
return fmt.Errorf("delivery: a refused message carries no manifests or lock hashes")
|
||||||
|
}
|
||||||
if f.Act == "" {
|
if f.Act == "" {
|
||||||
return fmt.Errorf("delivery: a refused message needs the act it answers")
|
return fmt.Errorf("delivery: a refused message needs the act it answers")
|
||||||
}
|
}
|
||||||
@@ -493,11 +536,35 @@ func (f Forge) check() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkInputs refuses a manifest or a lock hash of a directory that known does not know (of is what it knows them
|
||||||
|
// by), an empty manifest and an empty lock path or hash, naming the directory.
|
||||||
|
func (f Forge) checkInputs(known func(dir string) bool, of string) error {
|
||||||
|
for dir, m := range f.Manifests {
|
||||||
|
if !known(dir) {
|
||||||
|
return fmt.Errorf("delivery: the manifest of the directory %q is not of %s", dir, of)
|
||||||
|
}
|
||||||
|
if m == "" {
|
||||||
|
return fmt.Errorf("delivery: the manifest of the directory %q is empty", dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for dir, locks := range f.Locks {
|
||||||
|
if !known(dir) {
|
||||||
|
return fmt.Errorf("delivery: the lock hashes of the directory %q are not of %s", dir, of)
|
||||||
|
}
|
||||||
|
for path, hash := range locks {
|
||||||
|
if path == "" || hash == "" {
|
||||||
|
return fmt.Errorf("delivery: the lock %q of the directory %q has an empty path or hash", path, dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// checkOpen refuses an open message that names a repository, a branch, a head or anything of the other kinds, or
|
// checkOpen refuses an open message that names a repository, a branch, a head or anything of the other kinds, or
|
||||||
// an open pull request that is not `<owner>/<name>` and a number.
|
// an open pull request that is not `<owner>/<name>` and a number.
|
||||||
func (f Forge) checkOpen() error {
|
func (f Forge) checkOpen() error {
|
||||||
if f.Repository != "" || f.Branch != "" || f.Number != 0 || f.Head != "" || f.Action != "" || len(f.Dirs) > 0 || f.DirsKnown ||
|
if f.Repository != "" || f.Branch != "" || f.Number != 0 || f.Head != "" || f.Action != "" || len(f.Dirs) > 0 || f.DirsKnown ||
|
||||||
f.Merge != "" || len(f.Trees) > 0 || f.Root != "" || f.Act != "" || f.Words != "" {
|
f.Merge != "" || len(f.Trees) > 0 || len(f.Manifests) > 0 || len(f.Locks) > 0 || f.Root != "" || f.Act != "" || f.Words != "" {
|
||||||
return fmt.Errorf("delivery: an open message carries only the open pull requests it names")
|
return fmt.Errorf("delivery: an open message carries only the open pull requests it names")
|
||||||
}
|
}
|
||||||
for _, p := range f.Open {
|
for _, p := range f.Open {
|
||||||
@@ -603,7 +670,7 @@ func SameSource(a, b Source) bool { return reflect.DeepEqual(a, b) }
|
|||||||
func SameCandidate(a, b Candidate) bool { return reflect.DeepEqual(a, b) }
|
func SameCandidate(a, b Candidate) bool { return reflect.DeepEqual(a, b) }
|
||||||
|
|
||||||
// SameModule says whether two modules are the same value.
|
// SameModule says whether two modules are the same value.
|
||||||
func SameModule(a, b Module) bool { return a == b }
|
func SameModule(a, b Module) bool { return reflect.DeepEqual(a, b) }
|
||||||
|
|
||||||
// SameForge says whether two forge messages are the same value.
|
// SameForge says whether two forge messages are the same value.
|
||||||
func SameForge(a, b Forge) bool { return reflect.DeepEqual(a, b) }
|
func SameForge(a, b Forge) bool { return reflect.DeepEqual(a, b) }
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package delivery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StepsState is the name of the state a build module keeps its long steps in; its bucket is `<module>_steps`, built
|
||||||
|
// by StepsBucket (novox/hq ADR 0469 clause 2; issue 1471). A tool call is cut at 30 s, so a fetch, a test level or a
|
||||||
|
// build answers at once with a step id and writes its progress and its end to the key `<node>.<step id>`.
|
||||||
|
const StepsState = "steps"
|
||||||
|
|
||||||
|
// StepsBucket is the bucket of a build module's steps.
|
||||||
|
func StepsBucket(module string) string { return module + "_" + StepsState }
|
||||||
|
|
||||||
|
// The states of a step. A step ends in passed, failed or refused; its key is written for each of them, and that
|
||||||
|
// write is the event the step's reader waits on.
|
||||||
|
const (
|
||||||
|
StepRunning = "running"
|
||||||
|
StepPassed = "passed"
|
||||||
|
StepFailed = "failed"
|
||||||
|
StepRefused = "refused"
|
||||||
|
)
|
||||||
|
|
||||||
|
var stepIDOK = regexp.MustCompile(`^[a-z0-9]+$`)
|
||||||
|
|
||||||
|
func checkStepID(id string) error {
|
||||||
|
if !stepIDOK.MatchString(id) {
|
||||||
|
return fmt.Errorf("delivery: the step id %q is not lower-case letters and digits", id)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StepKey is the key of a node's step: `<node>.<id>`. It refuses a node that is not a name and an id that is not
|
||||||
|
// lower-case letters and digits (one key token, never a dot).
|
||||||
|
func StepKey(node, id string) (string, error) {
|
||||||
|
if err := checkName("node", node); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := checkStepID(id); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return node + "." + id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseStepKey is the inverse of StepKey.
|
||||||
|
func ParseStepKey(key string) (node, id string, err error) {
|
||||||
|
node, id, ok := strings.Cut(key, ".")
|
||||||
|
if !ok {
|
||||||
|
return "", "", fmt.Errorf("delivery: %q is not a step key, <node>.<id>", key)
|
||||||
|
}
|
||||||
|
if _, err := StepKey(node, id); err != nil {
|
||||||
|
return "", "", fmt.Errorf("delivery: %q is not a step key: %w", key, err)
|
||||||
|
}
|
||||||
|
return node, id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step is the value of a steps key: which tool runs on which node, how far it is, when it started and ended (UTC, as
|
||||||
|
// the toolchains' At), why it was refused or failed, and what it returned.
|
||||||
|
type Step struct {
|
||||||
|
V int `json:"v"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Tool string `json:"tool"`
|
||||||
|
Node string `json:"node"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Started time.Time `json:"started"`
|
||||||
|
Ended time.Time `json:"ended,omitzero"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Result json.RawMessage `json:"result,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Step) check() error {
|
||||||
|
if err := checkStepID(s.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := checkName("node", s.Node); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s.Tool == "" {
|
||||||
|
return fmt.Errorf("delivery: a step needs its tool")
|
||||||
|
}
|
||||||
|
switch s.State {
|
||||||
|
case StepRunning, StepPassed, StepFailed, StepRefused:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("delivery: the step state %q is not running, passed, failed or refused", s.State)
|
||||||
|
}
|
||||||
|
if len(s.Result) > 0 && !json.Valid(s.Result) {
|
||||||
|
return fmt.Errorf("delivery: the step's result is not JSON")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeStep is s as JSON at Version; it refuses an id that is not one key token, a node that is not a name, an empty
|
||||||
|
// tool, a state outside the four and a result that is not JSON.
|
||||||
|
func EncodeStep(s Step) ([]byte, error) {
|
||||||
|
if err := s.check(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.V = Version
|
||||||
|
return json.Marshal(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeStep reads a steps value; it refuses an unknown version (ErrUnknownVersion) and what EncodeStep refuses.
|
||||||
|
func DecodeStep(data []byte) (Step, error) {
|
||||||
|
var s Step
|
||||||
|
if err := decode(data, &s); err != nil {
|
||||||
|
return Step{}, err
|
||||||
|
}
|
||||||
|
if err := s.check(); err != nil {
|
||||||
|
return Step{}, err
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package delivery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToolchainsState is the name of the state a build module keeps its toolchains in; its bucket is
|
||||||
|
// `<module>_toolchains`, built as the SDK names a module's state, by ToolchainsBucket (novox/hq ADR 0465 clause 2).
|
||||||
|
const ToolchainsState = "toolchains"
|
||||||
|
|
||||||
|
// ToolchainsBucket is the bucket of a build module's toolchains.
|
||||||
|
func ToolchainsBucket(module string) string { return module + "_" + ToolchainsState }
|
||||||
|
|
||||||
|
// ToolchainNoReport is the reason AgreedToolchain gives when no node has reported a language's toolchain.
|
||||||
|
const ToolchainNoReport = "toolchain not yet reported"
|
||||||
|
|
||||||
|
func checkToolchainLanguage(language string) error {
|
||||||
|
switch language {
|
||||||
|
case "go", "typescript":
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("delivery: the toolchain language %q is not go or typescript", language)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToolchainKey is the key of a node's toolchain of one language: `<node>.<language>`. It refuses a node that is not a
|
||||||
|
// name and a language other than go or typescript.
|
||||||
|
func ToolchainKey(node, language string) (string, error) {
|
||||||
|
if err := checkName("node", node); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := checkToolchainLanguage(language); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return node + "." + language, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseToolchainKey is the inverse of ToolchainKey.
|
||||||
|
func ParseToolchainKey(key string) (node, language string, err error) {
|
||||||
|
node, language, ok := strings.Cut(key, ".")
|
||||||
|
if !ok {
|
||||||
|
return "", "", fmt.Errorf("delivery: %q is not a toolchain key, <node>.<language>", key)
|
||||||
|
}
|
||||||
|
if _, err := ToolchainKey(node, language); err != nil {
|
||||||
|
return "", "", fmt.Errorf("delivery: %q is not a toolchain key: %w", key, err)
|
||||||
|
}
|
||||||
|
return node, language, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toolchain is the value of a toolchains key: the version of the language's toolchain installed on the node and the
|
||||||
|
// build module's recipe version, and when the module saw them.
|
||||||
|
type Toolchain struct {
|
||||||
|
V int `json:"v"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Recipe string `json:"recipe"`
|
||||||
|
At time.Time `json:"at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Toolchain) check() error {
|
||||||
|
if err := checkToolchainLanguage(t.Language); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if t.Version == "" || t.Recipe == "" {
|
||||||
|
return fmt.Errorf("delivery: a %s toolchain needs its version and its recipe", t.Language)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeToolchain is t as JSON at Version; it refuses a language other than go or typescript and an empty version or
|
||||||
|
// recipe.
|
||||||
|
func EncodeToolchain(t Toolchain) ([]byte, error) {
|
||||||
|
if err := t.check(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t.V = Version
|
||||||
|
return json.Marshal(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeToolchain reads a toolchains value; it refuses an unknown version (ErrUnknownVersion) and what EncodeToolchain
|
||||||
|
// refuses.
|
||||||
|
func DecodeToolchain(data []byte) (Toolchain, error) {
|
||||||
|
var t Toolchain
|
||||||
|
if err := decode(data, &t); err != nil {
|
||||||
|
return Toolchain{}, err
|
||||||
|
}
|
||||||
|
if err := t.check(); err != nil {
|
||||||
|
return Toolchain{}, err
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeToolchain is one node's entry of a language, as read from the toolchains bucket.
|
||||||
|
type NodeToolchain struct {
|
||||||
|
Node string
|
||||||
|
Toolchain
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgreedToolchain is the one Toolchain2 every node's entry of a language agrees on, or none and why: "toolchain not
|
||||||
|
// yet reported" with no entries, and "builders disagree" naming each differing version and recipe with the nodes
|
||||||
|
// reporting it when two differ (novox/hq ADR 0405 clause 2). The answer does not depend on the entries' order. The
|
||||||
|
// entries must all be of one language.
|
||||||
|
func AgreedToolchain(entries []NodeToolchain) (tc Toolchain2, none string) {
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return Toolchain2{}, ToolchainNoReport
|
||||||
|
}
|
||||||
|
groups := map[Toolchain2][]string{}
|
||||||
|
for _, e := range entries {
|
||||||
|
k := Toolchain2{Language: e.Language, Version: e.Version, Recipe: e.Recipe}
|
||||||
|
groups[k] = append(groups[k], e.Node)
|
||||||
|
}
|
||||||
|
if len(groups) == 1 {
|
||||||
|
for k := range groups {
|
||||||
|
return k, ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts := make([]string, 0, len(groups))
|
||||||
|
for k, nodes := range groups {
|
||||||
|
sort.Strings(nodes)
|
||||||
|
parts = append(parts, fmt.Sprintf("%s recipe %s on %s", k.Version, k.Recipe, strings.Join(nodes, ", ")))
|
||||||
|
}
|
||||||
|
sort.Strings(parts)
|
||||||
|
return Toolchain2{}, "builders disagree: " + strings.Join(parts, "; ")
|
||||||
|
}
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020085642-ab0c16ddad26
|
# git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021093645-465beb4ea5e3
|
||||||
## 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