Merge pull request 'No repository names a node: placement files, particulars and per-node build defaults go (issue 1126, delivery 2 of 2)' (#552) from feat/1126-placement-seat into main
This commit was merged in pull request #552.
This commit is contained in:
@@ -36,7 +36,6 @@ modules/mesh-build-image/cmd/mesh-build-image/mesh-build-image
|
|||||||
/modules/mesh-build-go/mesh-build-go
|
/modules/mesh-build-go/mesh-build-go
|
||||||
/modules/mesh-delivery-gate/mesh-delivery-gate
|
/modules/mesh-delivery-gate/mesh-delivery-gate
|
||||||
modules/mesh-config/cmd/mesh-config/mesh-config
|
modules/mesh-config/cmd/mesh-config/mesh-config
|
||||||
checks/placement/placement
|
|
||||||
|
|
||||||
# Built by their modules' build step (issue 1577), never committed.
|
# Built by their modules' build step (issue 1577), never committed.
|
||||||
modules/forticlient/cmd/forticlient-tools/forticlient-tools
|
modules/forticlient/cmd/forticlient-tools/forticlient-tools
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module git.novox.be/novox/mesh-catalog/checks/noplacement
|
||||||
|
|
||||||
|
go 1.22
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// Command noplacement refuses what the repository path of placement left behind (novox/hq ADR 0567 §10, §12).
|
||||||
|
//
|
||||||
|
// Placement, which module runs on which node, is bus state held by the seat mesh-placement; no file of the
|
||||||
|
// catalogue says it, and no manifest reads it from the delivery indexer, which no longer keeps it. It refuses:
|
||||||
|
//
|
||||||
|
// 1. a `placement` directory (or file) at the root of the catalogue;
|
||||||
|
//
|
||||||
|
// 2. a `particulars` file at the root (the mesh's node names come from the checker's run, §16);
|
||||||
|
//
|
||||||
|
// 3. a manifest naming a state, stream, event or bucket of the indexer's placement (`mesh-delivery-indexer.placement…`
|
||||||
|
// or `mesh-delivery-indexer_placement…`) in any string value, such as `reads`.
|
||||||
|
//
|
||||||
|
// go run . <catalogue root>
|
||||||
|
//
|
||||||
|
// Exits 1 on a finding.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Prefixes are the names of the indexer's placement parts; a manifest string starting with one names them.
|
||||||
|
var Prefixes = []string{"mesh-delivery-indexer.placement", "mesh-delivery-indexer_placement"}
|
||||||
|
|
||||||
|
// Findings is every refusal in the catalogue at root, sorted.
|
||||||
|
func Findings(root string) ([]string, error) {
|
||||||
|
var out []string
|
||||||
|
if _, err := os.Lstat(filepath.Join(root, "placement")); err == nil {
|
||||||
|
out = append(out, "placement: the repository holds no placement; it is bus state of the seat mesh-placement (novox/hq ADR 0567 §10)")
|
||||||
|
}
|
||||||
|
if _, err := os.Lstat(filepath.Join(root, "particulars")); err == nil {
|
||||||
|
out = append(out, "particulars: the repository lists no node names; the checker hands them to the check (novox/hq ADR 0567 §16)")
|
||||||
|
}
|
||||||
|
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sort.Strings(manifests)
|
||||||
|
for _, file := range manifests {
|
||||||
|
raw, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var doc any
|
||||||
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", file, err)
|
||||||
|
}
|
||||||
|
module := filepath.Base(filepath.Dir(file))
|
||||||
|
for _, s := range named(doc, nil) {
|
||||||
|
out = append(out, fmt.Sprintf("%s: names %q, which the indexer no longer keeps; placement is the seat mesh-placement's (novox/hq ADR 0567 §12)", module, s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// named collects the strings of a decoded manifest that start with one of the Prefixes.
|
||||||
|
func named(v any, into []string) []string {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case string:
|
||||||
|
for _, p := range Prefixes {
|
||||||
|
if strings.HasPrefix(v, p) {
|
||||||
|
return append(into, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
for _, e := range v {
|
||||||
|
into = named(e, into)
|
||||||
|
}
|
||||||
|
case map[string]any:
|
||||||
|
for _, e := range v {
|
||||||
|
into = named(e, into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return into
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) != 2 {
|
||||||
|
fmt.Fprintln(os.Stderr, "usage: noplacement <catalogue root>")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
found, err := Findings(os.Args[1])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
for _, f := range found {
|
||||||
|
fmt.Println(f)
|
||||||
|
}
|
||||||
|
if len(found) > 0 {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("no placement directory, and no manifest reads the indexer's placement")
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFindingsRefuseWhatThePlacementPathLeft(t *testing.T) {
|
||||||
|
for _, row := range []struct {
|
||||||
|
name string
|
||||||
|
files map[string]string
|
||||||
|
want string // empty: clean
|
||||||
|
}{
|
||||||
|
{"clean", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-controller.assignments"]}`}, ""},
|
||||||
|
{"a placement directory", map[string]string{"placement/ace.json": `{}`}, "placement: the repository holds no placement"},
|
||||||
|
{"a particulars file", map[string]string{"particulars": "ace\n"}, "particulars: the repository lists no node names"},
|
||||||
|
{"a read of the indexer's placement", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-delivery-indexer.placement"]}`}, `a: names "mesh-delivery-indexer.placement"`},
|
||||||
|
{"a read of its candidates", map[string]string{"modules/b/module.json": `{"module":"b","reads":["mesh-delivery-indexer.placement-candidates"]}`}, "placement-candidates"},
|
||||||
|
{"its event", map[string]string{"modules/c/module.json": `{"module":"c","on":[{"event":"mesh-delivery-indexer.placement-changed"}]}`}, "placement-changed"},
|
||||||
|
{"the seat's own state is allowed", map[string]string{"modules/d/module.json": `{"module":"d","reads":["mesh-placement.placement"]}`}, ""},
|
||||||
|
} {
|
||||||
|
t.Run(row.name, func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
for path, text := range row.files {
|
||||||
|
p := filepath.Join(root, path)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(p, []byte(text), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
got, err := Findings(root)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if all := strings.Join(got, "\n"); (row.want == "") != (all == "") || !strings.Contains(all, row.want) {
|
||||||
|
t.Fatalf("findings = %q, want one containing %q", all, row.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2).
|
||||||
|
func TestTheCatalogueHoldsNoPlacementOfTheRepositoryPath(t *testing.T) {
|
||||||
|
got, err := Findings(filepath.Join("..", ".."))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, f := range got {
|
||||||
|
t.Error(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
||||||
|
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
||||||
|
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
||||||
|
# the root, for a placement directory, and every manifest, for a read of the indexer's placement states.
|
||||||
|
?placement
|
||||||
|
modules/*/module.json
|
||||||
@@ -2,8 +2,9 @@
|
|||||||
// issue 1838): a module is for any mesh, so no manifest, file, code string, identifier, comment, test name or
|
// issue 1838): a module is for any mesh, so no manifest, file, code string, identifier, comment, test name or
|
||||||
// fixture of a covered module names a node of this one.
|
// fixture of a covered module names a node of this one.
|
||||||
//
|
//
|
||||||
// The names are in the file `particulars` at the catalogue's root, one a line, `#` comments — the one place
|
// The names are the mesh's own, not the repository's: the checker hands them in the environment value
|
||||||
// they may be written. The modules covered are in `covered` at checks/particulars/covered, one a line. A name is matched
|
// MESH_NODE_NAMES, comma-separated, from the node records it reads (novox/hq ADR 0567 §16). A run with none fails
|
||||||
|
// loudly, since it would pass on anything. The modules covered are in `covered` at checks/particulars/covered, one a line. A name is matched
|
||||||
// whole and in any case; a hyphen, an underscore or a CamelCase boundary parts one, so a longer word holding a name is another word.
|
// whole and in any case; a hyphen, an underscore or a CamelCase boundary parts one, so a longer word holding a name is another word.
|
||||||
// The forge is not a node name: its host (git.novox.be) and the organisation path (novox/<repo>) are blanked
|
// The forge is not a node name: its host (git.novox.be) and the organisation path (novox/<repo>) are blanked
|
||||||
// before a line is read.
|
// before a line is read.
|
||||||
@@ -34,14 +35,28 @@ func main() {
|
|||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
root = os.Args[1]
|
root = os.Args[1]
|
||||||
}
|
}
|
||||||
os.Exit(check(root))
|
os.Exit(check(root, namesFromEnv()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamesEnv is the environment value that carries the mesh's node names (mesh-sdk delivery.NodeNamesEnv).
|
||||||
|
const NamesEnv = "MESH_NODE_NAMES"
|
||||||
|
|
||||||
|
// namesFromEnv is the names NamesEnv holds, comma-separated, trimmed, empty entries left out.
|
||||||
|
func namesFromEnv() []string {
|
||||||
|
var out []string
|
||||||
|
for _, n := range strings.Split(os.Getenv(NamesEnv), ",") {
|
||||||
|
if n = strings.TrimSpace(n); n != "" {
|
||||||
|
out = append(out, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// check runs the check over a catalogue and returns the exit code: 0 clean, 1 a finding, 2 a list unreadable.
|
// check runs the check over a catalogue and returns the exit code: 0 clean, 1 a finding, 2 a list unreadable.
|
||||||
func check(root string) int {
|
func check(root string, names []string) int {
|
||||||
names, err := readList(filepath.Join(root, "particulars"))
|
var err error
|
||||||
if err == nil && len(names) == 0 {
|
if len(names) == 0 {
|
||||||
err = fmt.Errorf("particulars lists no name — the check would pass on anything")
|
err = fmt.Errorf("no node names (%s is empty) — the check would pass on anything", NamesEnv)
|
||||||
}
|
}
|
||||||
var covered []string
|
var covered []string
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -74,26 +74,24 @@ func TestANodeNamedPathIsRefusedThroughRun(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckReturnsOneOnAFindingAndTwoOnAnUnreadableList(t *testing.T) {
|
func TestCheckReturnsOneOnAFindingAndTwoOnNoNames(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
write := func(p, s string) {
|
write := func(p, s string) {
|
||||||
p = filepath.Join(root, p)
|
p = filepath.Join(root, p)
|
||||||
os.MkdirAll(filepath.Dir(p), 0o755)
|
os.MkdirAll(filepath.Dir(p), 0o755)
|
||||||
os.WriteFile(p, []byte(s), 0o644)
|
os.WriteFile(p, []byte(s), 0o644)
|
||||||
}
|
}
|
||||||
write("particulars", "ace\n")
|
|
||||||
write("checks/particulars/covered", "kernel\n")
|
write("checks/particulars/covered", "kernel\n")
|
||||||
write("modules/kernel/a", "clean\n")
|
write("modules/kernel/a", "clean\n")
|
||||||
if got := check(root); got != 0 {
|
if got := check(root, []string{"ace"}); got != 0 {
|
||||||
t.Errorf("clean = %d, want 0", got)
|
t.Errorf("clean = %d, want 0", got)
|
||||||
}
|
}
|
||||||
write("modules/kernel/a", "on ace\n")
|
write("modules/kernel/a", "on ace\n")
|
||||||
if got := check(root); got != 1 {
|
if got := check(root, []string{"ace"}); got != 1 {
|
||||||
t.Errorf("finding = %d, want 1", got)
|
t.Errorf("finding = %d, want 1", got)
|
||||||
}
|
}
|
||||||
os.Remove(filepath.Join(root, "particulars"))
|
if got := check(root, nil); got != 2 {
|
||||||
if got := check(root); got != 2 {
|
t.Errorf("no names = %d, want 2", got)
|
||||||
t.Errorf("no list = %d, want 2", got)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +115,10 @@ func TestOnlyCoveredModulesAreRead(t *testing.T) {
|
|||||||
|
|
||||||
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): no node name of this mesh in a covered module.
|
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): no node name of this mesh in a covered module.
|
||||||
func TestTheCoveredModulesNameNoNode(t *testing.T) {
|
func TestTheCoveredModulesNameNoNode(t *testing.T) {
|
||||||
if code := check("../.."); code != 0 {
|
if _, set := os.LookupEnv(NamesEnv); !set {
|
||||||
|
t.Skip("ADR 0567 §16: this checker hands no node names (before D14 is delivered); the skip goes once it is")
|
||||||
|
}
|
||||||
|
if code := check("../..", namesFromEnv()); code != 0 {
|
||||||
t.Fatalf("the check of the real catalogue exits %d (see what it printed above)", code)
|
t.Fatalf("the check of the real catalogue exits %d (see what it printed above)", code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
||||||
# check("../.."): the root's list and every file of the modules it names.
|
# check("../.."): every file of the modules the covered list names. The node names come from the run (MESH_NODE_NAMES).
|
||||||
particulars
|
|
||||||
modules
|
modules
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
module checks/placement
|
|
||||||
|
|
||||||
go 1.26.0
|
|
||||||
|
|
||||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027014310-cadd7b1b2f8f
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026210326-979644420117 h1:88CNZOoQgu7YPYKW16qkWT8Gw9Ucg07aTrIImA2nYUk=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026210326-979644420117/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012704-062c29c3a22f h1:AKzV973UTWe0OYFoIjgGu0f+B26xh8QsAHzCpsTIjM8=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012704-062c29c3a22f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027014310-cadd7b1b2f8f h1:mimAGERguQmCGYVNdRTrFcZ1dhpU6GWo+Q3EDhHOrVk=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027014310-cadd7b1b2f8f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
|
||||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
|
||||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
|
||||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
|
||||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
|
||||||
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
|
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
|
||||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
|
||||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
|
||||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
|
||||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
|
||||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
// Command placement is the catalogue's check of its placement files (novox/hq ADR 0549 §1, ADR 0555; issue 1126): every
|
|
||||||
// `placement/<node>.json` reads as a placement of the node its name says, and the files together are refused, every
|
|
||||||
// finding named at once, for two modules of one node on one host port, a pin, binding or holding naming a provider not
|
|
||||||
// placed where it says, a seat held once for the mesh held twice, two nodes with one address on the private network, or
|
|
||||||
// more than one hub (mesh-sdk delivery.CheckPlacements).
|
|
||||||
//
|
|
||||||
// A module the catalogue does not hold and a node not enrolled are the composition check's to refuse: placement names
|
|
||||||
// modules of every repository the indexer reads and the nodes the mesh enrolled, neither of which this tree holds.
|
|
||||||
//
|
|
||||||
// go run . <catalogue root>
|
|
||||||
//
|
|
||||||
// Exits 1 on a finding.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check reads every placement file under root and answers each finding; none is a catalogue whose placement holds. A
|
|
||||||
// catalogue with no placement directory has none to check.
|
|
||||||
func Check(root string) ([]string, error) {
|
|
||||||
dir := filepath.Join(root, delivery.PlacementDir)
|
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var found []string
|
|
||||||
placements := map[string]delivery.Placement{}
|
|
||||||
for _, e := range entries {
|
|
||||||
rel := delivery.PlacementDir + "/" + e.Name()
|
|
||||||
node, ok := delivery.PlacementFileNode(rel)
|
|
||||||
if e.IsDir() || !ok {
|
|
||||||
found = append(found, fmt.Sprintf("%s is not a placement file: the directory holds only <node>.json, one per node", rel))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
raw, err := os.ReadFile(filepath.Join(dir, e.Name()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p, err := delivery.DecodePlacement(raw)
|
|
||||||
if err != nil {
|
|
||||||
found = append(found, fmt.Sprintf("%s does not read as a placement: %v", rel, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if p.Revision != "" {
|
|
||||||
found = append(found, fmt.Sprintf("%s names a revision, which only the indexer writes into the key", rel))
|
|
||||||
}
|
|
||||||
placements[node] = p
|
|
||||||
}
|
|
||||||
all := func(string) bool { return true }
|
|
||||||
found = append(found, delivery.CheckPlacements(placements, all, all)...)
|
|
||||||
sort.Strings(found)
|
|
||||||
return found, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if len(os.Args) != 2 {
|
|
||||||
fmt.Fprintln(os.Stderr, "usage: placement <catalogue root>")
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
found, err := Check(os.Args[1])
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
for _, f := range found {
|
|
||||||
fmt.Println(f)
|
|
||||||
}
|
|
||||||
if len(found) > 0 {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The catalogue's placement files as the merge check reads them (ADR 0549 §1, ADR 0555): the real tree holds every
|
|
||||||
// rule, and a tree that breaks them is refused with every finding named at once.
|
|
||||||
func TestPlacementFiles(t *testing.T) {
|
|
||||||
found, err := Check("../..")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, f := range found {
|
|
||||||
t.Errorf("%s (novox/hq ADR 0549 §1)", f)
|
|
||||||
}
|
|
||||||
|
|
||||||
root := t.TempDir()
|
|
||||||
write := func(name, body string) {
|
|
||||||
t.Helper()
|
|
||||||
if err := os.MkdirAll(filepath.Join(root, "placement"), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(filepath.Join(root, "placement", name), []byte(body), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if found, err := Check(root); err != nil || len(found) != 0 {
|
|
||||||
t.Fatalf("a catalogue with no placement directory: %v, %v", found, err)
|
|
||||||
}
|
|
||||||
write("ace.json", `{"v":1,"node":"ace","decided":{"hub":true,"address":"10.77.0.1"},"modules":[{"module":"a","ports":{"80":8080}},{"module":"b","ports":{"81":8080}}]}`)
|
|
||||||
write("g14.json", `{"v":1,"node":"g14","decided":{"hub":true,"address":"10.77.0.1"},"modules":[{"module":"c","bindings":{"pg":{"node":"ace","module":"pg"}}}]}`)
|
|
||||||
write("novox.json", `{"v":1,"node":"shanks","modules":[]}`)
|
|
||||||
write("bad.json", `{"v":1,"node":"bad","modules":{}}`)
|
|
||||||
write("dated.json", `{"v":1,"node":"dated","revision":"abc","modules":[]}`)
|
|
||||||
write("notes.md", "x")
|
|
||||||
found, err = Check(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, want := range []string{"a and b are both published on host port 8080", "bound to ace/pg", "both have the address 10.77.0.1",
|
|
||||||
"ace, g14 are each placed as it", "placement file of novox names the node", "bad.json does not read", "dated.json names a revision",
|
|
||||||
"notes.md is not a placement file"} {
|
|
||||||
hit := false
|
|
||||||
for _, f := range found {
|
|
||||||
hit = hit || strings.Contains(f, want)
|
|
||||||
}
|
|
||||||
if !hit {
|
|
||||||
t.Errorf("no finding says %q; found %v", want, found)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
These tests read files outside this directory (the whole catalogue, the repository, or its root files): mesh-build-go never keeps this package's verdict (issue 1935).
|
|
||||||
+3
-19
@@ -560,7 +560,7 @@ export class GiteaClient {
|
|||||||
* **A listing the forge cut is no listing** (it says `truncated` on a page with nothing more to read, or the pages
|
* **A listing the forge cut is no listing** (it says `truncated` on a page with nothing more to read, or the pages
|
||||||
* run past a bound): the call fails naming the repository and the commit, never answering the directories it did
|
* run past a bound): the call fails naming the repository and the commit, never answering the directories it did
|
||||||
* see, since a directory left out would be taken for one without a module. Reads only. */
|
* see, since a directory left out would be taken for one without a module. Reads only. */
|
||||||
async commitTrees(owner: string, repo: string, sha: string, paths?: Record<string, string[]>, listed?: string[]): Promise<{ root: string; trees: Record<string, string>; paths?: Record<string, Record<string, string>>; listed?: Record<string, Record<string, string>> }> {
|
async commitTrees(owner: string, repo: string, sha: string, paths?: Record<string, string[]>): Promise<{ root: string; trees: Record<string, string>; paths?: Record<string, Record<string, string>> }> {
|
||||||
const base = `${repoPath(owner, repo)}/git/trees/${commitSegment(sha)}?recursive=true`;
|
const base = `${repoPath(owner, repo)}/git/trees/${commitSegment(sha)}?recursive=true`;
|
||||||
const cut = (why: string) => new Error(`the forge's tree listing of ${owner}/${repo} at ${sha} was cut (${why}); no trees are answered from a part`);
|
const cut = (why: string) => new Error(`the forge's tree listing of ${owner}/${repo} at ${sha} was cut (${why}); no trees are answered from a part`);
|
||||||
const entries: any[] = [];
|
const entries: any[] = [];
|
||||||
@@ -587,23 +587,7 @@ export class GiteaClient {
|
|||||||
if (!tree) throw new Error(`the forge's tree listing of ${owner}/${repo} at ${sha} holds ${e.path} but not the tree of ${dir}`);
|
if (!tree) throw new Error(`the forge's tree listing of ${owner}/${repo} at ${sha} holds ${e.path} but not the tree of ${dir}`);
|
||||||
trees[dir] = tree;
|
trees[dir] = tree;
|
||||||
}
|
}
|
||||||
// **Listed directories** (novox/hq ADR 0549 §2, issue 1126): each asked directory's files directly under it, by name
|
if (paths === undefined) return { root, trees };
|
||||||
// to blob id, from the same listing: how the forge bridge finds the catalogue's placement files, one per node, at a
|
|
||||||
// merge. A directory the commit lacks lists nothing (the catalogue before its first placement file).
|
|
||||||
let files: Record<string, Record<string, string>> | undefined;
|
|
||||||
if (listed !== undefined) {
|
|
||||||
files = {};
|
|
||||||
for (const dir of listed) {
|
|
||||||
files[dir] = {};
|
|
||||||
const prefix = dir === "" ? "" : `${dir}/`;
|
|
||||||
for (const e of entries) {
|
|
||||||
if (e?.type !== "blob" || typeof e.path !== "string" || !e.path.startsWith(prefix)) continue;
|
|
||||||
const name = e.path.slice(prefix.length);
|
|
||||||
if (name !== "" && !name.includes("/")) files[dir][name] = String(e.sha);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (paths === undefined) return files === undefined ? { root, trees } : { root, trees, listed: files };
|
|
||||||
// **Named paths** (novox/hq ADR 0499 §3, issue 1758): each asked path's git object id at the commit, a file's or a
|
// **Named paths** (novox/hq ADR 0499 §3, issue 1758): each asked path's git object id at the commit, a file's or a
|
||||||
// directory's, from the same listing, by directory and by path relative to it. A path the listing lacks fails
|
// directory's, from the same listing, by directory and by path relative to it. A path the listing lacks fails
|
||||||
// naming both, and nothing is answered in part.
|
// naming both, and nothing is answered in part.
|
||||||
@@ -618,7 +602,7 @@ export class GiteaClient {
|
|||||||
named[dir][p] = id;
|
named[dir][p] = id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return files === undefined ? { root, trees, paths: named } : { root, trees, paths: named, listed: files };
|
return { root, trees, paths: named };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether a repository holds a file at a commit: true, false for a 404, thrown when the forge cannot say. */
|
/** Whether a repository holds a file at a commit: true, false for a 404, thrown when the forge cannot say. */
|
||||||
|
|||||||
@@ -183,15 +183,6 @@ test("named paths: each asked path's git object id at the commit, a file's or a
|
|||||||
});
|
});
|
||||||
assert.deepEqual(got.trees, { "": ROOT, "modules/a": h("4"), "modules/b/c": h("8") });
|
assert.deepEqual(got.trees, { "": ROOT, "modules/a": h("4"), "modules/b/c": h("8") });
|
||||||
assert.equal((await client.commitTrees("novox", "hq", COMMIT)).paths, undefined, "no paths asked, none answered");
|
assert.equal((await client.commitTrees("novox", "hq", COMMIT)).paths, undefined, "no paths asked, none answered");
|
||||||
// Listed directories (ADR 0549 §2, issue 1126): the files directly under each, not deeper; a missing one lists none.
|
|
||||||
const listed = await client.commitTrees("novox", "hq", COMMIT, undefined, ["modules/a", "modules/b", "", "placement"]);
|
|
||||||
assert.deepEqual(listed.listed, {
|
|
||||||
"modules/a": { "main.go": h("6"), "module.json": h("5") },
|
|
||||||
"modules/b": {},
|
|
||||||
"": { "README.md": h("1"), "module.json": h("2") },
|
|
||||||
placement: {},
|
|
||||||
});
|
|
||||||
assert.equal((await client.commitTrees("novox", "hq", COMMIT)).listed, undefined, "no directory asked, none listed");
|
|
||||||
} finally {
|
} finally {
|
||||||
server.close();
|
server.close();
|
||||||
}
|
}
|
||||||
@@ -218,12 +209,7 @@ test("gitea_commit_trees takes paths as {directory: [paths]} and refuses another
|
|||||||
const { client, server, asked } = await serve([{ sha: COMMIT, url: "u", tree: LISTING, truncated: false, page: 1, total_count: LISTING.length }]);
|
const { client, server, asked } = await serve([{ sha: COMMIT, url: "u", tree: LISTING, truncated: false, page: 1, total_count: LISTING.length }]);
|
||||||
try {
|
try {
|
||||||
const tool = getGiteaTools(client).find((t) => t.name === "gitea_commit_trees")!;
|
const tool = getGiteaTools(client).find((t) => t.name === "gitea_commit_trees")!;
|
||||||
assert.ok("paths" in tool.input && "listed" in tool.input, "the tool names the arguments");
|
assert.ok("paths" in tool.input, "the tool names the arguments");
|
||||||
const listedAnswer: any = await tool.run({ owner: "novox", repo: "hq", sha: COMMIT, listed: ["modules/d"] } as any);
|
|
||||||
assert.deepEqual(listedAnswer.listed, { "modules/d": { "notes.md": h("b") } });
|
|
||||||
for (const bad of ["modules/d", ["../x"], ["/abs"], [1]]) {
|
|
||||||
await assert.rejects(tool.run({ owner: "novox", repo: "hq", sha: COMMIT, listed: bad } as any), /listed/, JSON.stringify(bad));
|
|
||||||
}
|
|
||||||
const got: any = await tool.run({ owner: "novox", repo: "hq", sha: COMMIT, paths: { "modules/a": ["main.go"] } } as any);
|
const got: any = await tool.run({ owner: "novox", repo: "hq", sha: COMMIT, paths: { "modules/a": ["main.go"] } } as any);
|
||||||
assert.deepEqual(got.paths, { "modules/a": { "main.go": h("6") } });
|
assert.deepEqual(got.paths, { "modules/a": { "main.go": h("6") } });
|
||||||
const before = asked.length;
|
const before = asked.length;
|
||||||
|
|||||||
@@ -79,17 +79,6 @@ function namedPaths(raw: unknown): Record<string, string[]> | undefined {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The `listed` argument of gitea_commit_trees: [directory], each plain (no `.` or `..` segment, not absolute, the root
|
|
||||||
* the empty name); anything else is refused before the forge is asked. Absent is absent. */
|
|
||||||
function listedDirs(raw: unknown): string[] | undefined {
|
|
||||||
if (raw === undefined || raw === null) return undefined;
|
|
||||||
if (!Array.isArray(raw) || raw.some((d) => typeof d !== "string")) throw new Error("listed is [directory, ...]");
|
|
||||||
for (const d of raw as string[]) {
|
|
||||||
if (d !== "" && d.split("/").some((seg) => seg === "" || seg === "." || seg === "..")) throw new Error(`listed: ${JSON.stringify(d)} is not a plain relative directory`);
|
|
||||||
}
|
|
||||||
return raw as string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getGiteaTools(gitea: GiteaClient, admin?: () => GiteaAdmin, source: SourceRead = sourceFromBus(state)): ToolDefinition[] {
|
export function getGiteaTools(gitea: GiteaClient, admin?: () => GiteaAdmin, source: SourceRead = sourceFromBus(state)): ToolDefinition[] {
|
||||||
return namesChecked(allGiteaTools(gitea, admin, source));
|
return namesChecked(allGiteaTools(gitea, admin, source));
|
||||||
}
|
}
|
||||||
@@ -733,20 +722,19 @@ function allGiteaTools(gitea: GiteaClient, admin: (() => GiteaAdmin) | undefined
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "gitea_commit_trees",
|
name: "gitea_commit_trees",
|
||||||
description: "A commit's trees from the forge's one recursive listing: the root tree, and the tree of every directory that holds a module.json (the root as an empty name), each by its git hash. What the forge bridge puts in a merge's message (novox/hq ADR 0437 §5). With paths, {directory: [path relative to it]}, the answer also carries paths, {directory: {path: git object id}} of each named file or directory at the commit (novox/hq ADR 0499 §3); a path the commit lacks fails naming the directory and the path. The commit is a full 40-hex sha. With listed, [directory], the answer also carries listed, {directory: {name: git blob id}} of the files directly under each (the catalog's placement files, novox/hq ADR 0549 §2). A listing the forge cut is refused, never answered in part. Reads only.",
|
description: "A commit's trees from the forge's one recursive listing: the root tree, and the tree of every directory that holds a module.json (the root as an empty name), each by its git hash. What the forge bridge puts in a merge's message (novox/hq ADR 0437 §5). With paths, {directory: [path relative to it]}, the answer also carries paths, {directory: {path: git object id}} of each named file or directory at the commit (novox/hq ADR 0499 §3); a path the commit lacks fails naming the directory and the path. The commit is a full 40-hex sha. A listing the forge cut is refused, never answered in part. Reads only.",
|
||||||
input: {
|
input: {
|
||||||
owner: { type: "string", description: "the repository owner" },
|
owner: { type: "string", description: "the repository owner" },
|
||||||
repo: { type: "string", description: "the repository name" },
|
repo: { type: "string", description: "the repository name" },
|
||||||
sha: { type: "string", description: "the commit, its full 40-hex sha" },
|
sha: { type: "string", description: "the commit, its full 40-hex sha" },
|
||||||
paths: { type: "object", description: "optional, {directory: [path relative to it]}: the git object id of each named file or directory is answered under paths; the root directory is the empty name" },
|
paths: { type: "object", description: "optional, {directory: [path relative to it]}: the git object id of each named file or directory is answered under paths; the root directory is the empty name" },
|
||||||
listed: { type: "array", items: { type: "string" }, description: "optional, [directory]: the files directly under each, {directory: {name: git blob id}}, are answered under listed; a directory the commit lacks lists none" },
|
|
||||||
},
|
},
|
||||||
run: async (args) => {
|
run: async (args) => {
|
||||||
const owner = String(args.owner ?? "").trim(), repo = String(args.repo ?? "").trim();
|
const owner = String(args.owner ?? "").trim(), repo = String(args.repo ?? "").trim();
|
||||||
const sha = String(args.sha ?? "").trim();
|
const sha = String(args.sha ?? "").trim();
|
||||||
if (!owner || !repo) throw new Error("name the repository: owner and repo");
|
if (!owner || !repo) throw new Error("name the repository: owner and repo");
|
||||||
if (!/^[0-9a-f]{40}$/i.test(sha)) throw new Error("sha is a commit's full 40-hex sha");
|
if (!/^[0-9a-f]{40}$/i.test(sha)) throw new Error("sha is a commit's full 40-hex sha");
|
||||||
return gitea.commitTrees(owner, repo, sha, namedPaths(args.paths), listedDirs(args.listed));
|
return gitea.commitTrees(owner, repo, sha, namedPaths(args.paths));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -87,13 +87,13 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
|
|||||||
var watch func(string, func(stdio.ProvisionChange) error) error
|
var watch func(string, func(stdio.ProvisionChange) error) error
|
||||||
// And the CPU bounds it (issue 1806's follow-up): the configured capacity is an upper bound, the node's build slice
|
// And the CPU bounds it (issue 1806's follow-up): the configured capacity is an upper bound, the node's build slice
|
||||||
// gives floor(its quota's cores / the module's cores-per-step) at once, read live; a test judges by its own Capacity.
|
// gives floor(its quota's cores / the module's cores-per-step) at once, read live; a test judges by its own Capacity.
|
||||||
// And the node's build hours (the operator, 2026-10-26: g14 builds only from 18:00 to 06:00): outside them it takes
|
// And the node's build hours (a node may build only in a window): outside them it takes
|
||||||
// nothing new.
|
// nothing new.
|
||||||
var cores func() float64
|
var cores func() float64
|
||||||
var hours func() (*buildstep.BuildWindow, string)
|
var hours func() (*buildstep.BuildWindow, string)
|
||||||
if capacity == nil {
|
if capacity == nil {
|
||||||
hours = buildstep.NodeBuildHours(m.Settings, m.Node, m.Say)
|
hours = buildstep.NodeBuildHours(m.Settings, m.Node, m.Say)
|
||||||
capacity = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, m.Say)
|
capacity = buildstep.NodeCapacity(m.Settings, m.Node, m.Say)
|
||||||
watch = buildstep.WatchConfigServed
|
watch = buildstep.WatchConfigServed
|
||||||
cores = buildstep.NodeCoresPerStep(m.Settings, buildstep.DefaultCoresPerStep[delivery.AskGo], m.Say)
|
cores = buildstep.NodeCoresPerStep(m.Settings, buildstep.DefaultCoresPerStep[delivery.AskGo], m.Say)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -126,7 +127,8 @@ func TestManifestClaimsTheGoBuildSeatAndReadsItsCapacity(t *testing.T) {
|
|||||||
raw, _ := os.ReadFile(moduleDir + "/module.json")
|
raw, _ := os.ReadFile(moduleDir + "/module.json")
|
||||||
var doc struct {
|
var doc struct {
|
||||||
Config map[string]struct {
|
Config map[string]struct {
|
||||||
Default json.RawMessage `json:"default"`
|
Default json.RawMessage `json:"default"`
|
||||||
|
Pattern, Description string
|
||||||
} `json:"config"`
|
} `json:"config"`
|
||||||
}
|
}
|
||||||
_ = json.Unmarshal(raw, &doc)
|
_ = json.Unmarshal(raw, &doc)
|
||||||
@@ -144,10 +146,20 @@ func TestManifestClaimsTheGoBuildSeatAndReadsItsCapacity(t *testing.T) {
|
|||||||
if w, fault := buildstep.NodeBuildHours(moduleDir+"/files/settings.json", "g14", t.Errorf)(); w != nil || fault != "" {
|
if w, fault := buildstep.NodeBuildHours(moduleDir+"/files/settings.json", "g14", t.Errorf)(); w != nil || fault != "" {
|
||||||
t.Errorf("the settings template holds build hours %v (%s)", w, fault)
|
t.Errorf("the settings template holds build hours %v (%s)", w, fault)
|
||||||
}
|
}
|
||||||
var capacity string
|
// ADR 0567 §15: build-hours names no node (one window, or empty for any hour) and capacity defaults to 0.
|
||||||
|
window := regexp.MustCompile("^(?:" + doc.Config[buildstep.BuildHoursSetting].Pattern + ")$")
|
||||||
|
for value, ok := range map[string]bool{"": true, "18:00-06:00": true, "g14=18:00-06:00": false, "18:00": false} {
|
||||||
|
if window.MatchString(value) != ok {
|
||||||
|
t.Errorf("build-hours pattern on %q: accepted %v, want %v", value, !ok, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(doc.Config[buildstep.BuildHoursSetting].Description, "=") {
|
||||||
|
t.Errorf("the build-hours description names a node pair")
|
||||||
|
}
|
||||||
|
var capacity float64
|
||||||
_ = json.Unmarshal(doc.Config[buildstep.CapacitySetting].Default, &capacity)
|
_ = json.Unmarshal(doc.Config[buildstep.CapacitySetting].Default, &capacity)
|
||||||
if capacity != buildstep.DefaultCapacities {
|
if capacity != 0 {
|
||||||
t.Errorf("the capacity's default = %q, want %q", capacity, buildstep.DefaultCapacities)
|
t.Errorf("the capacity's default = %g, want 0", capacity)
|
||||||
}
|
}
|
||||||
settings := resource(t, m, "settings")
|
settings := resource(t, m, "settings")
|
||||||
if settings["path"] != "${dir:state}/settings.json" {
|
if settings["path"] != "${dir:state}/settings.json" {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func (m *Module) cpuLimits() buildcpu.Limits {
|
|||||||
case m.Capacity != nil:
|
case m.Capacity != nil:
|
||||||
slots = m.Capacity()
|
slots = m.Capacity()
|
||||||
case m.Settings != "":
|
case m.Settings != "":
|
||||||
slots = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, nil)()
|
slots = buildstep.NodeCapacity(m.Settings, m.Node, nil)()
|
||||||
}
|
}
|
||||||
return buildstep.NodeLimits(slots)
|
return buildstep.NodeLimits(slots)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ const DigestVersion = "6"
|
|||||||
// depends on the siblings' commits.
|
// depends on the siblings' commits.
|
||||||
const besideVariable = "MESH_CHECK_BESIDE"
|
const besideVariable = "MESH_CHECK_BESIDE"
|
||||||
|
|
||||||
|
// namesVariable is the same for the mesh's node names: a package whose tests reach a file naming it depends on the node
|
||||||
|
// list the ask carries, so its digest holds the sorted names and a new node misses a kept verdict (ADR 0567 §15).
|
||||||
|
const namesVariable = "MESH_NODE_NAMES"
|
||||||
|
|
||||||
// ReadsOutsideMarker is the name of a file in a package's directory that says its tests read files outside it (a
|
// ReadsOutsideMarker is the name of a file in a package's directory that says its tests read files outside it (a
|
||||||
// repository's documents, a sibling directory), which the Go toolchain does not list (issue 1935). It lists, one per
|
// repository's documents, a sibling directory), which the Go toolchain does not list (issue 1935). It lists, one per
|
||||||
// line, what those tests read, as paths from the repository's root; a path may be a glob (filepath.Match), and a
|
// line, what those tests read, as paths from the repository's root; a path may be a glob (filepath.Match), and a
|
||||||
@@ -164,6 +168,8 @@ func (m *Module) digestTool() StepTool {
|
|||||||
"workspace": map[string]any{"type": "string", "description": "the workspace id build_go_fetch answered"},
|
"workspace": map[string]any{"type": "string", "description": "the workspace id build_go_fetch answered"},
|
||||||
"level": map[string]any{"type": "string", "enum": []string{LevelUnit, LevelIntegration}, "description": "the level"},
|
"level": map[string]any{"type": "string", "enum": []string{LevelUnit, LevelIntegration}, "description": "the level"},
|
||||||
"runner": runnerProperty,
|
"runner": runnerProperty,
|
||||||
|
delivery.NodeNamesArg: map[string]any{"type": "array", "items": map[string]any{"type": "string"},
|
||||||
|
"description": "optional, the names of the mesh's nodes: a package whose tests read " + delivery.NodeNamesEnv + " has them in its digest"},
|
||||||
},
|
},
|
||||||
"required": []string{"workspace", "level"},
|
"required": []string{"workspace", "level"},
|
||||||
},
|
},
|
||||||
@@ -178,6 +184,10 @@ func (m *Module) digest(ctx context.Context, args map[string]any) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
names, err := nodeNamesArg(args[delivery.NodeNamesArg])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if runner == RunnerWorkspace {
|
if runner == RunnerWorkspace {
|
||||||
if err := m.workspaceThere(workspace); err != nil {
|
if err := m.workspaceThere(workspace); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -191,7 +201,7 @@ func (m *Module) digest(ctx context.Context, args map[string]any) (any, error) {
|
|||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("the level %q is neither unit nor integration", level)
|
return nil, fmt.Errorf("the level %q is neither unit nor integration", level)
|
||||||
}
|
}
|
||||||
a, judged, err := m.asCandidate(ctx, levelSpec{Step: stepDigest, Level: level, Workspace: workspace})
|
a, judged, err := m.asCandidate(ctx, levelSpec{Step: stepDigest, Level: level, Workspace: workspace, NodeNames: names})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -201,7 +211,7 @@ func (m *Module) digest(ctx context.Context, args map[string]any) (any, error) {
|
|||||||
a.Digest.JudgedBy = judged
|
a.Digest.JudgedBy = judged
|
||||||
return *a.Digest, nil
|
return *a.Digest, nil
|
||||||
}
|
}
|
||||||
res, err := m.digestHere(ctx, workspace, level)
|
res, err := m.digestHere(ctx, workspace, level, names)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -210,7 +220,7 @@ func (m *Module) digest(ctx context.Context, args map[string]any) (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// digestHere is the digest step as this program runs it.
|
// digestHere is the digest step as this program runs it.
|
||||||
func (m *Module) digestHere(ctx context.Context, workspace, level string) (DigestResult, error) {
|
func (m *Module) digestHere(ctx context.Context, workspace, level string, names []string) (DigestResult, error) {
|
||||||
if !workspaceOK.MatchString(workspace) {
|
if !workspaceOK.MatchString(workspace) {
|
||||||
return DigestResult{}, fmt.Errorf("%q is not a workspace id", workspace)
|
return DigestResult{}, fmt.Errorf("%q is not a workspace id", workspace)
|
||||||
}
|
}
|
||||||
@@ -241,6 +251,14 @@ func (m *Module) digestHere(ctx context.Context, workspace, level string) (Diges
|
|||||||
besideHeader = append(besideHeader, "beside "+b)
|
besideHeader = append(besideHeader, "beside "+b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The node names, sorted, as a header line for a package that reads them.
|
||||||
|
sortedNames := append([]string{}, names...)
|
||||||
|
sort.Strings(sortedNames)
|
||||||
|
var namesHeader []string
|
||||||
|
if len(sortedNames) > 0 {
|
||||||
|
namesHeader = []string{"node-names " + strings.Join(sortedNames, ",")}
|
||||||
|
}
|
||||||
|
|
||||||
dirs, err := m.goModDirs(ctx, tree)
|
dirs, err := m.goModDirs(ctx, tree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return DigestResult{}, err
|
return DigestResult{}, err
|
||||||
@@ -251,7 +269,7 @@ func (m *Module) digestHere(ctx context.Context, workspace, level string) (Diges
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return DigestResult{}, err
|
return DigestResult{}, err
|
||||||
}
|
}
|
||||||
if err := digestModule(&res, pkgs, tree, header, besideHeader, level == LevelIntegration); err != nil {
|
if err := digestModule(&res, pkgs, tree, header, besideHeader, namesHeader, level == LevelIntegration); err != nil {
|
||||||
return DigestResult{}, err
|
return DigestResult{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,7 +320,7 @@ func parsePackages(out string) (map[string]*goPackage, error) {
|
|||||||
|
|
||||||
// digestModule adds to res the digest of each root package of one go.mod directory. A root is a package of the main
|
// digestModule adds to res the digest of each root package of one go.mod directory. A root is a package of the main
|
||||||
// module that is listed, not only a dependency, and not a variant of another.
|
// module that is listed, not only a dependency, and not a variant of another.
|
||||||
func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, header, beside []string, needs bool) error {
|
func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, header, beside, names []string, needs bool) error {
|
||||||
roots := []string{}
|
roots := []string{}
|
||||||
for path, p := range pkgs {
|
for path, p := range pkgs {
|
||||||
if p.ForTest == "" && !p.DepOnly && !p.Standard && !strings.HasSuffix(path, ".test") && p.Module != nil && p.Module.Main {
|
if p.ForTest == "" && !p.DepOnly && !p.Standard && !strings.HasSuffix(path, ".test") && p.Module != nil && p.Module.Main {
|
||||||
@@ -349,7 +367,7 @@ func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, he
|
|||||||
res.neverKept[path] = why
|
res.neverKept[path] = why
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
d, err := packageDigest(root, testMain, pkgs, tree, header, beside, declared)
|
d, err := packageDigest(root, testMain, pkgs, tree, header, beside, names, declared)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", path, err)
|
return fmt.Errorf("%s: %w", path, err)
|
||||||
}
|
}
|
||||||
@@ -361,9 +379,9 @@ func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, he
|
|||||||
// packageDigest is the digest of one package: a hash over a sorted text of the header, the files of every package its
|
// packageDigest is the digest of one package: a hash over a sorted text of the header, the files of every package its
|
||||||
// tests reach in the module's own tree (by their content, under their path in the tree), the version and checksum of
|
// tests reach in the module's own tree (by their content, under their path in the tree), the version and checksum of
|
||||||
// every module outside it, and the package's own test data. The beside lines (the siblings' commits) join the header
|
// every module outside it, and the package's own test data. The beside lines (the siblings' commits) join the header
|
||||||
// only when a file the tests reach names MESH_CHECK_BESIDE. A package of the standard library is the Go version's.
|
// only when a file the tests reach names MESH_CHECK_BESIDE, and the node names likewise for MESH_NODE_NAMES. A package of the standard library is the Go version's.
|
||||||
// The lines a package's ReadsOutsideMarker declares join them (declaredReads).
|
// The lines a package's ReadsOutsideMarker declares join them (declaredReads).
|
||||||
func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree string, header, beside, declared []string) (string, error) {
|
func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree string, header, beside, names, declared []string) (string, error) {
|
||||||
reached := map[string]*goPackage{root.ImportPath: root}
|
reached := map[string]*goPackage{root.ImportPath: root}
|
||||||
for _, dep := range testMain.Deps {
|
for _, dep := range testMain.Deps {
|
||||||
if p := pkgs[dep]; p != nil {
|
if p := pkgs[dep]; p != nil {
|
||||||
@@ -371,7 +389,7 @@ func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines := append([]string{}, declared...)
|
lines := append([]string{}, declared...)
|
||||||
readsBeside := false
|
readsBeside, readsNames := false, false
|
||||||
for _, p := range reached {
|
for _, p := range reached {
|
||||||
if p.Standard || p.Module == nil {
|
if p.Standard || p.Module == nil {
|
||||||
continue
|
continue
|
||||||
@@ -390,9 +408,10 @@ func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree s
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
lines = append(lines, l)
|
lines = append(lines, l)
|
||||||
if !readsBeside {
|
if !readsBeside || !readsNames {
|
||||||
if raw, err := os.ReadFile(filepath.Join(p.Dir, f)); err == nil && bytes.Contains(raw, []byte(besideVariable)) {
|
if raw, err := os.ReadFile(filepath.Join(p.Dir, f)); err == nil {
|
||||||
readsBeside = true
|
readsBeside = readsBeside || bytes.Contains(raw, []byte(besideVariable))
|
||||||
|
readsNames = readsNames || bytes.Contains(raw, []byte(namesVariable))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,6 +443,9 @@ func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree s
|
|||||||
if readsBeside {
|
if readsBeside {
|
||||||
header = append(append([]string{}, header...), beside...)
|
header = append(append([]string{}, header...), beside...)
|
||||||
}
|
}
|
||||||
|
if readsNames {
|
||||||
|
header = append(append([]string{}, header...), names...)
|
||||||
|
}
|
||||||
h := sha256.New()
|
h := sha256.New()
|
||||||
for _, l := range append(append([]string{}, header...), lines...) {
|
for _, l := range append(append([]string{}, header...), lines...) {
|
||||||
h.Write([]byte(l + "\n"))
|
h.Write([]byte(l + "\n"))
|
||||||
|
|||||||
@@ -172,8 +172,9 @@ func TestAModuleOutsideTheTreeCountsByVersionAndChecksum(t *testing.T) {
|
|||||||
root := &goPackage{ImportPath: "x/r", Dir: dir, Module: &goModule{Path: "x", Main: true}, GoFiles: []string{"r.go"}, TestGoFiles: []string{"r.go"}}
|
root := &goPackage{ImportPath: "x/r", Dir: dir, Module: &goModule{Path: "x", Main: true}, GoFiles: []string{"r.go"}, TestGoFiles: []string{"r.go"}}
|
||||||
testMain := &goPackage{ImportPath: "x/r.test", Deps: []string{ext.ImportPath}}
|
testMain := &goPackage{ImportPath: "x/r.test", Deps: []string{ext.ImportPath}}
|
||||||
pkgs[root.ImportPath] = root
|
pkgs[root.ImportPath] = root
|
||||||
|
var names []string
|
||||||
digest := func() string {
|
digest := func() string {
|
||||||
d, err := packageDigest(root, testMain, pkgs, dir, []string{"digest 1"}, nil, nil)
|
d, err := packageDigest(root, testMain, pkgs, dir, []string{"digest 1"}, nil, names, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -188,6 +189,20 @@ func TestAModuleOutsideTheTreeCountsByVersionAndChecksum(t *testing.T) {
|
|||||||
if d3 := digest(); d3 == d1 {
|
if d3 := digest(); d3 == d1 {
|
||||||
t.Errorf("a new version left the digest as it was")
|
t.Errorf("a new version left the digest as it was")
|
||||||
}
|
}
|
||||||
|
// A different node list misses the cache only for a package whose tests read MESH_NODE_NAMES (ADR 0567 §15).
|
||||||
|
names = []string{"node-names a"}
|
||||||
|
d4 := digest()
|
||||||
|
names = []string{"node-names a", "node-names b"}
|
||||||
|
if d5 := digest(); d5 != d4 {
|
||||||
|
t.Errorf("a package that does not read the node names changed with them")
|
||||||
|
}
|
||||||
|
os.WriteFile(filepath.Join(dir, "r.go"), []byte("package r\n// "+namesVariable+"\n"), 0o644)
|
||||||
|
names = []string{"node-names a"}
|
||||||
|
d6 := digest()
|
||||||
|
names = []string{"node-names a", "node-names b"}
|
||||||
|
if d7 := digest(); d7 == d6 {
|
||||||
|
t.Errorf("a new node left the digest of a package that reads the node names as it was")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue 1935, 1936: a package whose reads-outside marker lists nothing it can trust is never kept: its digest is one no
|
// Issue 1935, 1936: a package whose reads-outside marker lists nothing it can trust is never kept: its digest is one no
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func (m *Module) integrationHere(ctx context.Context, workspace, tree string, pa
|
|||||||
return nil, fmt.Errorf("the integration level's bus: %w", err)
|
return nil, fmt.Errorf("the integration level's bus: %w", err)
|
||||||
}
|
}
|
||||||
defer stopBus()
|
defer stopBus()
|
||||||
env := append([]string{"MESH_TEST_POSTGRES=" + postgres, "MESH_TEST_NATS=" + nats}, m.checkEnv(workspace)...)
|
env := append([]string{"MESH_TEST_POSTGRES=" + postgres, "MESH_TEST_NATS=" + nats}, m.checkEnv(workspace, nil)...)
|
||||||
// Compiling counts as setup: the tests are built once, here, with the addresses a TestMain may read, and the run
|
// Compiling counts as setup: the tests are built once, here, with the addresses a TestMain may read, and the run
|
||||||
// below finds them built. Whether they compile is for the run to say; its failure is not read here.
|
// below finds them built. Whether they compile is for the run to say; its failure is not read here.
|
||||||
// Only the directories the selected packages lie in compile (ADR 0536 §3); no selection is every directory.
|
// Only the directories the selected packages lie in compile (ADR 0536 §3); no selection is every directory.
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ type levelSpec struct {
|
|||||||
Run string `json:"run,omitempty"`
|
Run string `json:"run,omitempty"`
|
||||||
Setup time.Duration `json:"setup"`
|
Setup time.Duration `json:"setup"`
|
||||||
Bound time.Duration `json:"bound"`
|
Bound time.Duration `json:"bound"`
|
||||||
|
NodeNames []string `json:"node-names,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// levelAnswer is the one line the process prints: the level's result or the digest, or why it could not run.
|
// levelAnswer is the one line the process prints: the level's result or the digest, or why it could not run.
|
||||||
@@ -221,14 +222,14 @@ func (m *Module) answer(ctx context.Context, spec levelSpec) levelAnswer {
|
|||||||
tree := filepath.Join(m.Work, spec.Workspace, "tree")
|
tree := filepath.Join(m.Work, spec.Workspace, "tree")
|
||||||
switch spec.Step {
|
switch spec.Step {
|
||||||
case stepDigest:
|
case stepDigest:
|
||||||
d, err := m.digestHere(ctx, spec.Workspace, spec.Level)
|
d, err := m.digestHere(ctx, spec.Workspace, spec.Level, spec.NodeNames)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return levelAnswer{Error: err.Error()}
|
return levelAnswer{Error: err.Error()}
|
||||||
}
|
}
|
||||||
return levelAnswer{Digest: &d}
|
return levelAnswer{Digest: &d}
|
||||||
case stepUnit:
|
case stepUnit:
|
||||||
res, err := m.runLevel(ctx, tree, levelOpts{workspace: spec.Workspace, level: LevelUnit, packages: spec.Packages, run: spec.Run,
|
res, err := m.runLevel(ctx, tree, levelOpts{workspace: spec.Workspace, level: LevelUnit, packages: spec.Packages, run: spec.Run,
|
||||||
bounds: Bounds{Run: m.unitBound()}, env: m.checkEnv(spec.Workspace)})
|
bounds: Bounds{Run: m.unitBound()}, env: m.checkEnv(spec.Workspace, spec.NodeNames)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return levelAnswer{Error: err.Error()}
|
return levelAnswer{Error: err.Error()}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Issue 1309: a test that runs a step in bubblewrap is proof only where it runs. Where a build module judges (it hands
|
// Issue 1309: a test that runs a step in bubblewrap is proof only where it runs. Where a build module judges (it hands
|
||||||
@@ -29,8 +32,12 @@ func TestAMissingToolSkipsExceptBubblewrapOnABuildNode(t *testing.T) {
|
|||||||
// The levels a build module judges hand the tests the build node mark, with the siblings they read.
|
// The levels a build module judges hand the tests the build node mark, with the siblings they read.
|
||||||
func TestEveryLevelHandsTheTestsTheBuildNodeMark(t *testing.T) {
|
func TestEveryLevelHandsTheTestsTheBuildNodeMark(t *testing.T) {
|
||||||
m := &Module{Work: t.TempDir()}
|
m := &Module{Work: t.TempDir()}
|
||||||
if env := m.checkEnv("ws1"); !slices.Contains(env, BuildNodeEnv+"=1") {
|
if env := m.checkEnv("ws1", nil); !slices.Contains(env, BuildNodeEnv+"=1") || slices.ContainsFunc(env, func(e string) bool { return strings.HasPrefix(e, delivery.NodeNamesEnv) }) {
|
||||||
t.Errorf("env %v lacks %s=1", env, BuildNodeEnv)
|
t.Errorf("env %v lacks %s=1, or names nodes when none were asked", env, BuildNodeEnv)
|
||||||
|
}
|
||||||
|
// And the mesh's node names, when the ask carried them (ADR 0567 §12).
|
||||||
|
if env := m.checkEnv("ws1", []string{"a", "b"}); !slices.Contains(env, delivery.NodeNamesEnv+"=a,b") {
|
||||||
|
t.Errorf("env %v lacks the node names", env)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,7 +39,9 @@ func (m *Module) testTool() StepTool {
|
|||||||
"level": map[string]any{"type": "string", "enum": []string{LevelUnit, LevelIntegration}, "description": "the level"},
|
"level": map[string]any{"type": "string", "enum": []string{LevelUnit, LevelIntegration}, "description": "the level"},
|
||||||
"packages": map[string]any{"type": "array", "items": map[string]any{"type": "string"},
|
"packages": map[string]any{"type": "array", "items": map[string]any{"type": "string"},
|
||||||
"description": "package patterns of each module, ./... when none"},
|
"description": "package patterns of each module, ./... when none"},
|
||||||
"run": map[string]any{"type": "string", "description": "optional go test -run pattern: only the tests it matches run (a try's tests)"},
|
"run": map[string]any{"type": "string", "description": "optional go test -run pattern: only the tests it matches run (a try's tests)"},
|
||||||
|
delivery.NodeNamesArg: map[string]any{"type": "array", "items": map[string]any{"type": "string"},
|
||||||
|
"description": "optional, the names of the mesh's nodes: the tests get them comma-separated in " + delivery.NodeNamesEnv + " (a check that keeps node names out of the repository reads them there)"},
|
||||||
"runner": runnerProperty,
|
"runner": runnerProperty,
|
||||||
},
|
},
|
||||||
"required": []string{"workspace", "level"},
|
"required": []string{"workspace", "level"},
|
||||||
@@ -78,12 +81,16 @@ func (m *Module) test(ctx context.Context, args map[string]any) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
names, err := nodeNamesArg(args[delivery.NodeNamesArg])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
var res *TestResult
|
var res *TestResult
|
||||||
switch {
|
switch {
|
||||||
case runner == RunnerWorkspace && (level == LevelUnit || level == LevelIntegration):
|
case runner == RunnerWorkspace && (level == LevelUnit || level == LevelIntegration):
|
||||||
return m.testAsCandidate(ctx, workspace, level, packages, pattern)
|
return m.testAsCandidate(ctx, workspace, level, packages, pattern, names)
|
||||||
case level == LevelUnit:
|
case level == LevelUnit:
|
||||||
res, err = m.runLevel(ctx, tree, levelOpts{workspace: workspace, level: LevelUnit, packages: packages, run: pattern, bounds: Bounds{Run: m.unitBound()}, env: m.checkEnv(workspace)})
|
res, err = m.runLevel(ctx, tree, levelOpts{workspace: workspace, level: LevelUnit, packages: packages, run: pattern, bounds: Bounds{Run: m.unitBound()}, env: m.checkEnv(workspace, names)})
|
||||||
case level == LevelIntegration:
|
case level == LevelIntegration:
|
||||||
res, err = m.integration(ctx, workspace, tree, packages, pattern)
|
res, err = m.integration(ctx, workspace, tree, packages, pattern)
|
||||||
default:
|
default:
|
||||||
@@ -98,14 +105,14 @@ func (m *Module) test(ctx context.Context, args map[string]any) (any, error) {
|
|||||||
|
|
||||||
// testAsCandidate runs a level as the candidate's program (ADR 0535 §1): the integration level is refused first as the
|
// testAsCandidate runs a level as the candidate's program (ADR 0535 §1): the integration level is refused first as the
|
||||||
// installed program would refuse it (no postgres), then the whole level runs in the candidate's one shut-off process.
|
// installed program would refuse it (no postgres), then the whole level runs in the candidate's one shut-off process.
|
||||||
func (m *Module) testAsCandidate(ctx context.Context, workspace, level string, packages []string, pattern string) (*TestResult, error) {
|
func (m *Module) testAsCandidate(ctx context.Context, workspace, level string, packages []string, pattern string, names []string) (*TestResult, error) {
|
||||||
spec := levelSpec{Step: stepUnit, Workspace: workspace, Packages: packages, Run: pattern, Bound: m.unitBound()}
|
spec := levelSpec{Step: stepUnit, Workspace: workspace, Packages: packages, Run: pattern, Bound: m.unitBound(), NodeNames: names}
|
||||||
if level == LevelIntegration {
|
if level == LevelIntegration {
|
||||||
if _, err := m.checkPostgres(ctx); err != nil {
|
if _, err := m.checkPostgres(ctx); err != nil {
|
||||||
return nil, err // a *buildstep.Refusal: this level only
|
return nil, err // a *buildstep.Refusal: this level only
|
||||||
}
|
}
|
||||||
b := m.integrationBounds()
|
b := m.integrationBounds()
|
||||||
spec = levelSpec{Step: stepIntegration, Workspace: workspace, Packages: packages, Run: pattern, Setup: b.Setup, Bound: b.Run}
|
spec = levelSpec{Step: stepIntegration, Workspace: workspace, Packages: packages, Run: pattern, Setup: b.Setup, Bound: b.Run, NodeNames: names}
|
||||||
}
|
}
|
||||||
a, judged, err := m.asCandidate(ctx, spec)
|
a, judged, err := m.asCandidate(ctx, spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -132,9 +139,35 @@ func (m *Module) besideEnv(workspace string) []string {
|
|||||||
// and present, so a test that needs it fails where it is missing instead of skipping (issue 1309).
|
// and present, so a test that needs it fails where it is missing instead of skipping (issue 1309).
|
||||||
const BuildNodeEnv = "MESH_BUILD_NODE"
|
const BuildNodeEnv = "MESH_BUILD_NODE"
|
||||||
|
|
||||||
// checkEnv is what every level hands the tests it runs: the build node mark and the siblings (besideEnv).
|
// checkEnv is what every level hands the tests it runs: the build node mark, the siblings (besideEnv) and, when the
|
||||||
func (m *Module) checkEnv(workspace string) []string {
|
// ask carried them, the mesh's node names (delivery.NodeNamesEnv, novox/hq ADR 0567 §12).
|
||||||
return append([]string{BuildNodeEnv + "=1"}, m.besideEnv(workspace)...)
|
func (m *Module) checkEnv(workspace string, names []string) []string {
|
||||||
|
env := append([]string{BuildNodeEnv + "=1"}, m.besideEnv(workspace)...)
|
||||||
|
if len(names) > 0 {
|
||||||
|
env = append(env, delivery.NodeNamesEnv+"="+strings.Join(names, ","))
|
||||||
|
}
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
// nodeNamesArg reads the node-names argument: a list of names, absent when none.
|
||||||
|
func nodeNamesArg(raw any) ([]string, error) {
|
||||||
|
switch v := raw.(type) {
|
||||||
|
case nil:
|
||||||
|
return nil, nil
|
||||||
|
case []string:
|
||||||
|
return v, nil
|
||||||
|
case []any:
|
||||||
|
var out []string
|
||||||
|
for _, e := range v {
|
||||||
|
n, ok := e.(string)
|
||||||
|
if !ok || n == "" || strings.ContainsAny(n, ", \n") {
|
||||||
|
return nil, fmt.Errorf("%s is a list of node names, not %v", delivery.NodeNamesArg, e)
|
||||||
|
}
|
||||||
|
out = append(out, n)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%s is a list of node names", delivery.NodeNamesArg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// packagesArg reads the packages argument: a list of package patterns, as []any when it came over the bus as JSON and
|
// packagesArg reads the packages argument: a list of package patterns, as []any when it came over the bus as JSON and
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"capacity": "shanks=3,ace=2,g14=0,novox=0",
|
"capacity": 0,
|
||||||
"cores-per-step": 6,
|
"cores-per-step": 6,
|
||||||
"build-hours": ""
|
"build-hours": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module meshbuildgo
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388
|
||||||
github.com/distribution/distribution/v3 v3.1.2
|
github.com/distribution/distribution/v3 v3.1.2
|
||||||
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,5 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1 h1:9qyUcrvlklxDRBUCBqGH1L3oximOk554IfaPrTMgaTA=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||||
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||||
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=
|
||||||
|
|||||||
@@ -25,15 +25,15 @@
|
|||||||
],
|
],
|
||||||
"config": {
|
"config": {
|
||||||
"capacity": {
|
"capacity": {
|
||||||
"type": "string",
|
"type": "number",
|
||||||
"description": "How many asks of the Go build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.",
|
"description": "How many asks of the Go build seat this module runs at once on this node (novox/hq ADR 0525 §5); 0 pulls nothing. It is set per node in the mesh's configuration; a node with no value builds nothing.",
|
||||||
"pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*",
|
"minimum": 0,
|
||||||
"default": "shanks=3,ace=2,g14=0,novox=0"
|
"default": 0
|
||||||
},
|
},
|
||||||
"build-hours": {
|
"build-hours": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "When each node may take this module's build asks, as node=HH:MM-HH:MM pairs in the node's local time, separated by commas, such as g14=18:00-06:00 (an end before its start crosses midnight). Outside its window a node takes no new step and finishes the ones it runs; a node not named, or an empty value, builds at any hour. The operator, 2026-10-26: g14 builds only outside office hours.",
|
"description": "When this node may take this module's build asks: one window HH:MM-HH:MM in the node's local time (an end before its start crosses midnight, 18:00-06:00). Outside it the node takes no new step and finishes the ones it runs; an empty value builds at any hour. It is set per node in the mesh's configuration.",
|
||||||
"pattern": "|[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9](,[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9])*",
|
"pattern": "|([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9]",
|
||||||
"default": ""
|
"default": ""
|
||||||
},
|
},
|
||||||
"cores-per-step": {
|
"cores-per-step": {
|
||||||
|
|||||||
@@ -43,13 +43,13 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
|
|||||||
var watch func(string, func(stdio.ProvisionChange) error) error
|
var watch func(string, func(stdio.ProvisionChange) error) error
|
||||||
// And the CPU bounds it (issue 1806's follow-up): the configured capacity is an upper bound, the node's build slice
|
// And the CPU bounds it (issue 1806's follow-up): the configured capacity is an upper bound, the node's build slice
|
||||||
// gives floor(its quota's cores / the module's cores-per-step) at once, read live; a test judges by its own Capacity.
|
// gives floor(its quota's cores / the module's cores-per-step) at once, read live; a test judges by its own Capacity.
|
||||||
// And the node's build hours (the operator, 2026-10-26: g14 builds only from 18:00 to 06:00): outside them it takes
|
// And the node's build hours (a node may build only in a window): outside them it takes
|
||||||
// nothing new.
|
// nothing new.
|
||||||
var cores func() float64
|
var cores func() float64
|
||||||
var hours func() (*buildstep.BuildWindow, string)
|
var hours func() (*buildstep.BuildWindow, string)
|
||||||
if capacity == nil {
|
if capacity == nil {
|
||||||
hours = buildstep.NodeBuildHours(m.Settings, m.Node, m.Say)
|
hours = buildstep.NodeBuildHours(m.Settings, m.Node, m.Say)
|
||||||
capacity = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, m.Say)
|
capacity = buildstep.NodeCapacity(m.Settings, m.Node, m.Say)
|
||||||
watch = buildstep.WatchConfigServed
|
watch = buildstep.WatchConfigServed
|
||||||
cores = buildstep.NodeCoresPerStep(m.Settings, buildstep.DefaultCoresPerStep[delivery.AskImage], m.Say)
|
cores = buildstep.NodeCoresPerStep(m.Settings, buildstep.DefaultCoresPerStep[delivery.AskImage], m.Say)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func (m *Module) cpuLimits() buildcpu.Limits {
|
|||||||
case m.Capacity != nil:
|
case m.Capacity != nil:
|
||||||
slots = m.Capacity()
|
slots = m.Capacity()
|
||||||
case m.Settings != "":
|
case m.Settings != "":
|
||||||
slots = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, nil)()
|
slots = buildstep.NodeCapacity(m.Settings, m.Node, nil)()
|
||||||
}
|
}
|
||||||
return buildstep.NodeLimits(slots)
|
return buildstep.NodeLimits(slots)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -121,6 +123,30 @@ func TestManifestKeepsItsStatesClaimsItsBuildSeatAndDeclaresItsHealthTool(t *tes
|
|||||||
if strings.TrimSpace(string(tpl)) != "${seat:mesh-artifact-store:reach}" {
|
if strings.TrimSpace(string(tpl)) != "${seat:mesh-artifact-store:reach}" {
|
||||||
t.Errorf("registry template = %q", tpl)
|
t.Errorf("registry template = %q", tpl)
|
||||||
}
|
}
|
||||||
|
raw, _ = os.ReadFile(filepath.Join(moduleDir, "module.json"))
|
||||||
|
var doc struct {
|
||||||
|
Config map[string]struct {
|
||||||
|
Default json.RawMessage `json:"default"`
|
||||||
|
Pattern, Description string
|
||||||
|
} `json:"config"`
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal(raw, &doc)
|
||||||
|
var capacity float64 = -1
|
||||||
|
_ = json.Unmarshal(doc.Config["capacity"].Default, &capacity)
|
||||||
|
if capacity != 0 {
|
||||||
|
t.Errorf("the capacity's default = %g, want 0", capacity)
|
||||||
|
}
|
||||||
|
// ADR 0567 §15: build-hours names no node (one window, or empty for any hour) and capacity defaults to 0.
|
||||||
|
window := regexp.MustCompile("^(?:" + doc.Config["build-hours"].Pattern + ")$")
|
||||||
|
for value, ok := range map[string]bool{"": true, "18:00-06:00": true, "g14=18:00-06:00": false, "18:00": false} {
|
||||||
|
if window.MatchString(value) != ok {
|
||||||
|
t.Errorf("build-hours pattern on %q: accepted %v, want %v", value, !ok, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(doc.Config["build-hours"].Description, "=") {
|
||||||
|
t.Errorf("the build-hours description names a node pair")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every tool the manifest names is served, and every tool served is named.
|
// Every tool the manifest names is served, and every tool served is named.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"capacity": "shanks=3,ace=2,g14=0,novox=0",
|
"capacity": 0,
|
||||||
"cores-per-step": 2,
|
"cores-per-step": 2,
|
||||||
"build-hours": ""
|
"build-hours": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module meshbuildimage
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028115016-1618837ea9fe
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388
|
||||||
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,5 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028115016-1618837ea9fe h1:QDWi8k7zxaRPnJ+V/izoy6A19hZt1LTLl3uksZSlqOg=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028115016-1618837ea9fe/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/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/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -20,15 +20,15 @@
|
|||||||
],
|
],
|
||||||
"config": {
|
"config": {
|
||||||
"capacity": {
|
"capacity": {
|
||||||
"type": "string",
|
"type": "number",
|
||||||
"description": "How many asks of the image build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.",
|
"description": "How many asks of the image build seat this module runs at once on this node (novox/hq ADR 0525 §5); 0 pulls nothing. It is set per node in the mesh's configuration; a node with no value builds nothing.",
|
||||||
"pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*",
|
"minimum": 0,
|
||||||
"default": "shanks=3,ace=2,g14=0,novox=0"
|
"default": 0
|
||||||
},
|
},
|
||||||
"build-hours": {
|
"build-hours": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "When each node may take this module's build asks, as node=HH:MM-HH:MM pairs in the node's local time, separated by commas, such as g14=18:00-06:00 (an end before its start crosses midnight). Outside its window a node takes no new step and finishes the ones it runs; a node not named, or an empty value, builds at any hour. The operator, 2026-10-26: g14 builds only outside office hours.",
|
"description": "When this node may take this module's build asks: one window HH:MM-HH:MM in the node's local time (an end before its start crosses midnight, 18:00-06:00). Outside it the node takes no new step and finishes the ones it runs; an empty value builds at any hour. It is set per node in the mesh's configuration.",
|
||||||
"pattern": "|[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9](,[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9])*",
|
"pattern": "|([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9]",
|
||||||
"default": ""
|
"default": ""
|
||||||
},
|
},
|
||||||
"cores-per-step": {
|
"cores-per-step": {
|
||||||
|
|||||||
@@ -51,13 +51,13 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
|
|||||||
var watch func(string, func(stdio.ProvisionChange) error) error
|
var watch func(string, func(stdio.ProvisionChange) error) error
|
||||||
// And the CPU bounds it (issue 1806's follow-up): the configured capacity is an upper bound, the node's build slice
|
// And the CPU bounds it (issue 1806's follow-up): the configured capacity is an upper bound, the node's build slice
|
||||||
// gives floor(its quota's cores / the module's cores-per-step) at once, read live; a test judges by its own Capacity.
|
// gives floor(its quota's cores / the module's cores-per-step) at once, read live; a test judges by its own Capacity.
|
||||||
// And the node's build hours (the operator, 2026-10-26: g14 builds only from 18:00 to 06:00): outside them it takes
|
// And the node's build hours (a node may build only in a window): outside them it takes
|
||||||
// nothing new.
|
// nothing new.
|
||||||
var cores func() float64
|
var cores func() float64
|
||||||
var hours func() (*buildstep.BuildWindow, string)
|
var hours func() (*buildstep.BuildWindow, string)
|
||||||
if capacity == nil {
|
if capacity == nil {
|
||||||
hours = buildstep.NodeBuildHours(m.Settings, m.Node, m.Say)
|
hours = buildstep.NodeBuildHours(m.Settings, m.Node, m.Say)
|
||||||
capacity = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, m.Say)
|
capacity = buildstep.NodeCapacity(m.Settings, m.Node, m.Say)
|
||||||
watch = buildstep.WatchConfigServed
|
watch = buildstep.WatchConfigServed
|
||||||
cores = buildstep.NodeCoresPerStep(m.Settings, buildstep.DefaultCoresPerStep[delivery.AskNode], m.Say)
|
cores = buildstep.NodeCoresPerStep(m.Settings, buildstep.DefaultCoresPerStep[delivery.AskNode], m.Say)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -84,7 +85,8 @@ func TestManifestTakesTheBuildCapacity(t *testing.T) {
|
|||||||
raw, _ := os.ReadFile(moduleDir + "/module.json")
|
raw, _ := os.ReadFile(moduleDir + "/module.json")
|
||||||
var doc struct {
|
var doc struct {
|
||||||
Config map[string]struct {
|
Config map[string]struct {
|
||||||
Default json.RawMessage `json:"default"`
|
Default json.RawMessage `json:"default"`
|
||||||
|
Pattern, Description string
|
||||||
} `json:"config"`
|
} `json:"config"`
|
||||||
}
|
}
|
||||||
_ = json.Unmarshal(raw, &doc)
|
_ = json.Unmarshal(raw, &doc)
|
||||||
@@ -102,10 +104,20 @@ func TestManifestTakesTheBuildCapacity(t *testing.T) {
|
|||||||
if w, fault := buildstep.NodeBuildHours(moduleDir+"/files/settings.json", "g14", t.Errorf)(); w != nil || fault != "" {
|
if w, fault := buildstep.NodeBuildHours(moduleDir+"/files/settings.json", "g14", t.Errorf)(); w != nil || fault != "" {
|
||||||
t.Errorf("the settings template holds build hours %v (%s)", w, fault)
|
t.Errorf("the settings template holds build hours %v (%s)", w, fault)
|
||||||
}
|
}
|
||||||
var capacity string
|
// ADR 0567 §15: build-hours names no node (one window, or empty for any hour) and capacity defaults to 0.
|
||||||
|
window := regexp.MustCompile("^(?:" + doc.Config[buildstep.BuildHoursSetting].Pattern + ")$")
|
||||||
|
for value, ok := range map[string]bool{"": true, "18:00-06:00": true, "g14=18:00-06:00": false, "18:00": false} {
|
||||||
|
if window.MatchString(value) != ok {
|
||||||
|
t.Errorf("build-hours pattern on %q: accepted %v, want %v", value, !ok, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(doc.Config[buildstep.BuildHoursSetting].Description, "=") {
|
||||||
|
t.Errorf("the build-hours description names a node pair")
|
||||||
|
}
|
||||||
|
var capacity float64
|
||||||
_ = json.Unmarshal(doc.Config[buildstep.CapacitySetting].Default, &capacity)
|
_ = json.Unmarshal(doc.Config[buildstep.CapacitySetting].Default, &capacity)
|
||||||
if capacity != buildstep.DefaultCapacities {
|
if capacity != 0 {
|
||||||
t.Errorf("the capacity's default = %q", capacity)
|
t.Errorf("the capacity's default = %g, want 0", capacity)
|
||||||
}
|
}
|
||||||
if resource(t, m, "settings")["path"] != "${dir:state}/settings.json" {
|
if resource(t, m, "settings")["path"] != "${dir:state}/settings.json" {
|
||||||
t.Error("no settings file")
|
t.Error("no settings file")
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func (m *Module) cpuLimits() buildcpu.Limits {
|
|||||||
case m.Capacity != nil:
|
case m.Capacity != nil:
|
||||||
slots = m.Capacity()
|
slots = m.Capacity()
|
||||||
case m.Settings != "":
|
case m.Settings != "":
|
||||||
slots = buildstep.NodeCapacity(m.Settings, buildstep.DefaultCapacities, m.Node, nil)()
|
slots = buildstep.NodeCapacity(m.Settings, m.Node, nil)()
|
||||||
}
|
}
|
||||||
return buildstep.NodeLimits(slots)
|
return buildstep.NodeLimits(slots)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"capacity": "shanks=3,ace=2,g14=0,novox=0",
|
"capacity": 0,
|
||||||
"cores-per-step": 2,
|
"cores-per-step": 2,
|
||||||
"build-hours": ""
|
"build-hours": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module meshbuildnode
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388
|
||||||
github.com/distribution/distribution/v3 v3.1.2
|
github.com/distribution/distribution/v3 v3.1.2
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1 h1:9qyUcrvlklxDRBUCBqGH1L3oximOk554IfaPrTMgaTA=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||||
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||||
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=
|
||||||
|
|||||||
@@ -21,15 +21,15 @@
|
|||||||
],
|
],
|
||||||
"config": {
|
"config": {
|
||||||
"capacity": {
|
"capacity": {
|
||||||
"type": "string",
|
"type": "number",
|
||||||
"description": "How many asks of the Node build seat this module runs at once on each node, as node=asks pairs separated by commas, such as shanks=3,ace=2 (novox/hq ADR 0525 §5); a node set to 0 pulls nothing, a node not named runs one at a time. The control-node and the laptop build nothing.",
|
"description": "How many asks of the Node build seat this module runs at once on this node (novox/hq ADR 0525 §5); 0 pulls nothing. It is set per node in the mesh's configuration; a node with no value builds nothing.",
|
||||||
"pattern": "|[a-z0-9_-]+=[0-9]+(,[a-z0-9_-]+=[0-9]+)*",
|
"minimum": 0,
|
||||||
"default": "shanks=3,ace=2,g14=0,novox=0"
|
"default": 0
|
||||||
},
|
},
|
||||||
"build-hours": {
|
"build-hours": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "When each node may take this module's build asks, as node=HH:MM-HH:MM pairs in the node's local time, separated by commas, such as g14=18:00-06:00 (an end before its start crosses midnight). Outside its window a node takes no new step and finishes the ones it runs; a node not named, or an empty value, builds at any hour. The operator, 2026-10-26: g14 builds only outside office hours.",
|
"description": "When this node may take this module's build asks: one window HH:MM-HH:MM in the node's local time (an end before its start crosses midnight, 18:00-06:00). Outside it the node takes no new step and finishes the ones it runs; an empty value builds at any hour. It is set per node in the mesh's configuration.",
|
||||||
"pattern": "|[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9](,[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9])*",
|
"pattern": "|([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9]",
|
||||||
"default": ""
|
"default": ""
|
||||||
},
|
},
|
||||||
"cores-per-step": {
|
"cores-per-step": {
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ const (
|
|||||||
ProposalsState = "proposals"
|
ProposalsState = "proposals"
|
||||||
|
|
||||||
AssignmentsRead = "mesh-controller.assignments"
|
AssignmentsRead = "mesh-controller.assignments"
|
||||||
// PlacementRead is the indexer's placement, read from the catalogue at each merge (novox/hq ADR 0549 §2, §3): each
|
// PlacementRead is the seat mesh-placement's state, held by the assigner (novox/hq ADR 0567 §1, §8): each
|
||||||
// node's key `placement.<node>` with the modules placed there.
|
// node's key `placement.<node>` with the modules placed there.
|
||||||
PlacementRead = "mesh-delivery-indexer.placement"
|
PlacementRead = "mesh-delivery-assigner.placement"
|
||||||
DeclaredConfigRead = "mesh-controller.declared-config"
|
DeclaredConfigRead = "mesh-controller.declared-config"
|
||||||
PredefinedRead = "mesh-controller.predefined"
|
PredefinedRead = "mesh-controller.predefined"
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ func (c *Config) sources() (Sources, error) {
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return s, fmt.Errorf("the assignments cannot be read: %w", err)
|
return s, fmt.Errorf("the assignments cannot be read: %w", err)
|
||||||
}
|
}
|
||||||
// And every module the catalog's placement names on a node (ADR 0549 §3): until ADR 0547's third milestone both
|
// And every module the seat's placement names on a node (ADR 0549 §3): until ADR 0547's third milestone both
|
||||||
// sources stand (ADR 0549 §5), so a pair either names is resolved, and none loses its values while the two differ.
|
// sources stand (ADR 0549 §5), so a pair either names is resolved, and none loses its values while the two differ.
|
||||||
// Placement that cannot be read yet (the indexer's state not on the bus) leaves the assignments alone, said.
|
// Placement that cannot be read yet (the seat's state not on the bus) leaves the assignments alone, said.
|
||||||
standing := map[string]bool{}
|
standing := map[string]bool{}
|
||||||
for _, a := range s.Assignments {
|
for _, a := range s.Assignments {
|
||||||
standing[a.Node+"."+a.Module] = true
|
standing[a.Node+"."+a.Module] = true
|
||||||
@@ -57,7 +57,7 @@ func (c *Config) sources() (Sources, error) {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
problems = append(problems, fmt.Sprintf("the catalog's placement cannot be read, so the assignments alone are resolved: %v", err))
|
problems = append(problems, fmt.Sprintf("the seat's placement cannot be read, so the assignments alone are resolved: %v", err))
|
||||||
}
|
}
|
||||||
if err := c.each(DeclaredConfigRead, func(key string, raw []byte) error {
|
if err := c.each(DeclaredConfigRead, func(key string, raw []byte) error {
|
||||||
m, err := ParseModuleDeclarations(raw)
|
m, err := ParseModuleDeclarations(raw)
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ module mesh-config
|
|||||||
|
|
||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021075504-7bf4a8053a49
|
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388
|
||||||
|
|||||||
@@ -1,4 +1,2 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013235212-c8b07242a009 h1:I9rm8HhYRJ0+mzQQz+VAH0kgv2YJWkvctEc67Yd0PX8=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013235212-c8b07242a009/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021075504-7bf4a8053a49 h1:h1hE+H1TzmeWNFP3EnfhUXTZq2gyiyeND32Tsg83ygY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021075504-7bf4a8053a49/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
],
|
],
|
||||||
"reads": [
|
"reads": [
|
||||||
"mesh-controller.assignments",
|
"mesh-controller.assignments",
|
||||||
"mesh-delivery-indexer.placement",
|
"mesh-delivery-assigner.placement",
|
||||||
"mesh-controller.declared-config",
|
"mesh-controller.declared-config",
|
||||||
"mesh-controller.predefined"
|
"mesh-controller.predefined"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ type checkerVerdict struct {
|
|||||||
Words string `json:"words,omitempty"`
|
Words string `json:"words,omitempty"`
|
||||||
} `json:"failed,omitzero"`
|
} `json:"failed,omitzero"`
|
||||||
// Composition is what the check of the machines' composition made of it (mesh-sdk delivery.Composition): its state
|
// Composition is what the check of the machines' composition made of it (mesh-sdk delivery.Composition): its state
|
||||||
// not-run says the composition, and with it a placement change's answers from its nodes, was not judged.
|
// not-run says the composition was not judged.
|
||||||
Composition *struct {
|
Composition *struct {
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
Words string `json:"words,omitempty"`
|
Words string `json:"words,omitempty"`
|
||||||
@@ -341,7 +341,7 @@ func (h *Holder) checkAt(members []*Delivery) checkSaid {
|
|||||||
if ok {
|
if ok {
|
||||||
switch v.State {
|
switch v.State {
|
||||||
case checkerPass:
|
case checkerPass:
|
||||||
return checkSaid{Key: checkSaidPass, Words: "passed its check" + notComposed(v, members)}
|
return checkSaid{Key: checkSaidPass, Words: "passed its check" + notComposed(v)}
|
||||||
case checkerRunning:
|
case checkerRunning:
|
||||||
words := "its check has no verdict yet: it is running"
|
words := "its check has no verdict yet: it is running"
|
||||||
if since, held := h.runningSince[hash]; held && !since.IsZero() {
|
if since, held := h.runningSince[hash]; held && !since.IsZero() {
|
||||||
@@ -384,20 +384,13 @@ func (h *Holder) checkAt(members []*Delivery) checkSaid {
|
|||||||
return checkSaid{Key: checkSaidNone, Words: "its check has no verdict yet"}
|
return checkSaid{Key: checkSaidNone, Words: "its check has no verdict yet"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// notComposed is what a passing verdict adds when its composition was not judged (mesh-sdk delivery.CompositionNotRun): a
|
// notComposed is what a passing verdict adds when its composition was not judged (mesh-sdk delivery.CompositionNotRun).
|
||||||
// placement change's pass then holds no node's answer to its placement (ADR 0559 §5), which the ask says. Empty otherwise.
|
// Empty otherwise.
|
||||||
func notComposed(v checkerVerdict, members []*Delivery) string {
|
func notComposed(v checkerVerdict) string {
|
||||||
if v.Composition == nil || v.Composition.State != sdk.CompositionNotRun {
|
if v.Composition == nil || v.Composition.State != sdk.CompositionNotRun {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
why := firstOf(oneLine(v.Composition.Words), "it was not run")
|
why := firstOf(oneLine(v.Composition.Words), "it was not run")
|
||||||
for _, m := range members {
|
|
||||||
for _, p := range m.Paths {
|
|
||||||
if strings.HasPrefix(p, "placement/") && strings.HasSuffix(p, ".json") {
|
|
||||||
return ", but its composition was not judged (" + why + "), so no node answered its placement"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ", but its composition was not judged (" + why + ")"
|
return ", but its composition was not judged (" + why + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -465,7 +465,7 @@ const waitsCheckBeforeAsking = "waits its check before asking the operator's App
|
|||||||
// has no verdict (the operator, 2026-10-27 23:54: "if you don't want me to approve it, don't send the approval request").
|
// has no verdict (the operator, 2026-10-27 23:54: "if you don't want me to approve it, don't send the approval request").
|
||||||
// A pass or a failure is asked (a failure in its words: the operator may still merge it), and so is a check that has not
|
// A pass or a failure is asked (a failure in its words: the operator may still merge it), and so is a check that has not
|
||||||
// answered within askCheckBound of the heads becoming ready, which the ask says. For a group the check is the one of all its
|
// answered within askCheckBound of the heads becoming ready, which the ask says. For a group the check is the one of all its
|
||||||
// heads together; for a placement change it ends only once every node it places answered (ADR 0559 §5). Under the lock.
|
// heads together. Under the lock.
|
||||||
func (h *Holder) verdictWaits(d *Delivery) string {
|
func (h *Holder) verdictWaits(d *Delivery) string {
|
||||||
members := h.askMembers(d)
|
members := h.askMembers(d)
|
||||||
c := h.checkAt(members)
|
c := h.checkAt(members)
|
||||||
|
|||||||
@@ -75,12 +75,11 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
|
|||||||
// past which it is asked anyway, saying so, and asked again in new words once the check passes. The verdict is the
|
// past which it is asked anyway, saying so, and asked again in new words once the check passes. The verdict is the
|
||||||
// checker's verdicts bucket value as captured on the bus (mesh-delivery-checker, `<group>.<heads hash>`).
|
// checker's verdicts bucket value as captured on the bus (mesh-delivery-checker, `<group>.<heads hash>`).
|
||||||
for _, row := range []struct {
|
for _, row := range []struct {
|
||||||
name, state, failed, then, path string
|
name, state, failed, then string
|
||||||
gated bool
|
gated bool
|
||||||
notComposed bool
|
after time.Duration
|
||||||
after time.Duration
|
asks, open int
|
||||||
asks, open int
|
want, not []string
|
||||||
want, not []string
|
|
||||||
}{
|
}{
|
||||||
{name: "gated, passed", gated: true, state: "pass", asks: 1,
|
{name: "gated, passed", gated: true, state: "pass", asks: 1,
|
||||||
want: []string{"app#7 Fix it passed its check. Approve merges it into main at " + readyHead[:12] + ";"},
|
want: []string{"app#7 Fix it passed its check. Approve merges it into main at " + readyHead[:12] + ";"},
|
||||||
@@ -100,9 +99,6 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
|
|||||||
want: []string{waitsCheckBeforeAsking + "its check has no verdict yet: it is running (since 15:00 CEST); asked anyway at 16:00 CEST"}},
|
want: []string{waitsCheckBeforeAsking + "its check has no verdict yet: it is running (since 15:00 CEST); asked anyway at 16:00 CEST"}},
|
||||||
{name: "ungated, passed, its verdict gone past its bound", state: "pass", after: askCheckBound, then: "gone", asks: 1, open: -1,
|
{name: "ungated, passed, its verdict gone past its bound", state: "pass", after: askCheckBound, then: "gone", asks: 1, open: -1,
|
||||||
want: []string{waitsCheckBeforeAsking + "its check has no verdict yet; asked anyway at 16:00 CEST"}},
|
want: []string{waitsCheckBeforeAsking + "its check has no verdict yet; asked anyway at 16:00 CEST"}},
|
||||||
// A placement change's pass with its composition not judged holds no node's answer, and the ask says so (ADR 0559 §5).
|
|
||||||
{name: "ungated, placement passed uncomposed", state: "pass", path: "placement/node-a.json", notComposed: true, asks: 1,
|
|
||||||
want: []string{"passed its check, but its composition was not judged (the assigner is not running), so no node answered its placement"}},
|
|
||||||
{name: "ungated, failed", state: "fail", failed: "TestThing: wanted 1, got 2", asks: 1,
|
{name: "ungated, failed", state: "fail", failed: "TestThing: wanted 1, got 2", asks: 1,
|
||||||
want: []string{"its check failed: TestThing: wanted 1, got 2", "although its check failed"}, not: []string{"passed", "anyway"}},
|
want: []string{"its check failed: TestThing: wanted 1, got 2", "although its check failed"}, not: []string{"passed", "anyway"}},
|
||||||
} {
|
} {
|
||||||
@@ -112,7 +108,7 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
|
|||||||
ungated(v)
|
ungated(v)
|
||||||
}
|
}
|
||||||
v.op.auto = false
|
v.op.auto = false
|
||||||
p := pr("novox/app", 7, readyHead, "feat/x", firstOf(row.path, "src/a.go"))
|
p := pr("novox/app", 7, readyHead, "feat/x", "src/a.go")
|
||||||
p.Title = "Fix it"
|
p.Title = "Fix it"
|
||||||
v.h.PullUpdated(p)
|
v.h.PullUpdated(p)
|
||||||
head := headOf("novox/app", 7, readyHead)
|
head := headOf("novox/app", 7, readyHead)
|
||||||
@@ -121,11 +117,7 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
|
|||||||
}
|
}
|
||||||
v.settleAll()
|
v.settleAll()
|
||||||
if !row.gated && row.state != "" {
|
if !row.gated && row.state != "" {
|
||||||
var more map[string]any
|
checkerSaysWith(v, "g", row.state, row.failed, nil, head)
|
||||||
if row.notComposed {
|
|
||||||
more = map[string]any{"composition": map[string]string{"state": "not-run", "words": "the assigner is not running"}}
|
|
||||||
}
|
|
||||||
checkerSaysWith(v, "g", row.state, row.failed, more, head)
|
|
||||||
v.settleAll()
|
v.settleAll()
|
||||||
}
|
}
|
||||||
if row.after > 0 {
|
if row.after > 0 {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-delivery-gate
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027014310-cadd7b1b2f8f
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388
|
||||||
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,9 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026211913-68d65e6bbf61 h1:/c6ncICWjq55nBf8TlDM0ROe/PuH+qZIdnuVbtg8UuM=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026211913-68d65e6bbf61/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012704-062c29c3a22f h1:AKzV973UTWe0OYFoIjgGu0f+B26xh8QsAHzCpsTIjM8=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027012704-062c29c3a22f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027014310-cadd7b1b2f8f h1:mimAGERguQmCGYVNdRTrFcZ1dhpU6GWo+Q3EDhHOrVk=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027014310-cadd7b1b2f8f/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/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -437,10 +437,10 @@ func TestANodeEngineHandsTheVaultWhatRunsOnItsOwnMachine(t *testing.T) {
|
|||||||
later -= time.Hour
|
later -= time.Hour
|
||||||
// own_drift reads it from the node's latest shadow answer, mesh-wide; the id as a node runs it, its module's name
|
// own_drift reads it from the node's latest shadow answer, mesh-wide; the id as a node runs it, its module's name
|
||||||
// first (captured from novox's shadow answer, 2026-10-28: only-office.needs-jwt).
|
// first (captured from novox's shadow answer, 2026-10-28: only-office.needs-jwt).
|
||||||
answer := delivery.CandidateAnswer{Head: "h", Verdict: delivery.CandidateDiffers, Differs: []delivery.ModuleDiffers{{
|
answer := delivery.CandidateAnswer{Proposal: "pro", Revision: 1, Verdict: delivery.CandidateDiffers, Differs: []delivery.ModuleDiffers{{
|
||||||
Module: "postgres", Why: "the resource differs", Bodies: []delivery.ResourceBodies{{ID: "postgres.needs-superuser-password",
|
Module: "postgres", Why: "the resource differs", Bodies: []delivery.ResourceBodies{{ID: "postgres.needs-superuser-password",
|
||||||
Composed: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`, Running: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`}}}}}
|
Composed: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`, Running: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`}}}}}
|
||||||
have, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"g": answer},
|
have, err := delivery.EncodeHaveNode(delivery.HaveNode{Candidates: map[string]delivery.CandidateAnswer{"pro": answer},
|
||||||
Composes: delivery.ComposesController})
|
Composes: delivery.ComposesController})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -453,7 +453,7 @@ func TestANodeEngineHandsTheVaultWhatRunsOnItsOwnMachine(t *testing.T) {
|
|||||||
}
|
}
|
||||||
if len(drift.Secrets) != 1 || drift.Secrets[0].Secret != "own/anchor/postgres/superuser-password" ||
|
if len(drift.Secrets) != 1 || drift.Secrets[0].Secret != "own/anchor/postgres/superuser-password" ||
|
||||||
drift.Secrets[0].Generation != 2 || drift.Secrets[0].Previous != 1 || drift.Secrets[0].Takeable ||
|
drift.Secrets[0].Generation != 2 || drift.Secrets[0].Previous != 1 || drift.Secrets[0].Takeable ||
|
||||||
len(drift.Nodes) != 1 || drift.Nodes[0].ValuesDiffer["g"] != 1 || drift.Nodes[0].Composes != delivery.ComposesController {
|
len(drift.Nodes) != 1 || drift.Nodes[0].ValuesDiffer["pro"] != 1 || drift.Nodes[0].Composes != delivery.ComposesController {
|
||||||
t.Errorf("own_drift: %+v", drift)
|
t.Errorf("own_drift: %+v", drift)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module mesh-vault
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028090029-3028c7ec4ed2
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388
|
||||||
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
|
||||||
golang.org/x/crypto v0.57.0
|
golang.org/x/crypto v0.57.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028090029-3028c7ec4ed2 h1:eSwObLdxs3jxPrlcwdaMo9hzIDTUz2ogWw8cjX8To7I=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028090029-3028c7ec4ed2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/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/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# This mesh's node names (novox/hq ADR 0507 §9, issue 1838): the one place they may be written.
|
|
||||||
novox
|
|
||||||
ace
|
|
||||||
shanks
|
|
||||||
g14
|
|
||||||
Reference in New Issue
Block a user