Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a86701891 | ||
|
|
d4c61ee8ff | ||
|
|
76f7c4d223 | ||
|
|
f609be7864 |
+114
-10
@@ -3,10 +3,13 @@ package buildstep
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
@@ -29,16 +32,25 @@ const Bubblewrap = "bwrap"
|
||||
// - the Workspace and Writable directories read-write, the ReadOnly ones (the toolchain, the modules the fetch step
|
||||
// downloaded) read-only;
|
||||
// - the shared warm Caches read-only under a throwaway writable layer (S18), or read-write for a trunk build alone
|
||||
// (WriteCaches), so a candidate's code never changes what a later build reads.
|
||||
// (WriteCaches), so a candidate's code never changes what a later build reads;
|
||||
// - with a CacheLayer, what one command writes into the caches kept for the later commands of the same ask (issue 1921,
|
||||
// the try speed-up): the vet, the unit tests and the integration compile of one ask compile each package once.
|
||||
type Isolation struct {
|
||||
Workspace string
|
||||
Writable []string
|
||||
ReadOnly []string
|
||||
Caches []string
|
||||
WriteCaches bool
|
||||
Env []string
|
||||
// CacheLayer is a directory of the ask's own, in its workspace and gone with it: each command writes the caches into
|
||||
// a layer of its own beneath it (overlayfs refuses an upper layer two mounts share, and an ask's commands run side by
|
||||
// side), which Isolated moves into the ask's kept layer when the command ends; every later command reads the kept
|
||||
// layer above the shared cache. The shared cache stays unchanged. Empty: what a command writes is thrown away.
|
||||
CacheLayer string
|
||||
Env []string
|
||||
// Hosts is the file bound as /etc/hosts; Isolated writes one naming only the loopback.
|
||||
Hosts string
|
||||
// run is the directory of this command's own writable layers under CacheLayer; Isolated makes one per command.
|
||||
run string
|
||||
}
|
||||
|
||||
// etcNeeded is what of /etc a program in the step may read: who the users are, where the libraries are, the
|
||||
@@ -91,7 +103,7 @@ func (iso Isolation) command(env []string, name string, args ...string) ([]strin
|
||||
for _, group := range []struct {
|
||||
what string
|
||||
dirs []string
|
||||
}{{"writable directory", iso.Writable}, {"read-only directory", iso.ReadOnly}, {"cache", iso.Caches}} {
|
||||
}{{"writable directory", iso.Writable}, {"read-only directory", iso.ReadOnly}, {"cache", iso.Caches}, {"cache layer", layerDirs(iso.CacheLayer)}} {
|
||||
for _, d := range group.dirs {
|
||||
if err := checkDir(group.what, d); err != nil {
|
||||
return nil, err
|
||||
@@ -113,7 +125,7 @@ func (iso Isolation) command(env []string, name string, args ...string) ([]strin
|
||||
for _, d := range iso.ReadOnly {
|
||||
argv = append(argv, "--ro-bind", d, d)
|
||||
}
|
||||
argv = append(argv, cacheArgs(iso.Caches, iso.WriteCaches)...)
|
||||
argv = append(argv, iso.cacheArgs()...)
|
||||
argv = append(argv, "--bind", iso.Workspace, iso.Workspace)
|
||||
for _, d := range iso.Writable {
|
||||
argv = append(argv, "--bind", d, d)
|
||||
@@ -134,19 +146,107 @@ func (iso Isolation) command(env []string, name string, args ...string) ([]strin
|
||||
|
||||
// cacheArgs binds the shared warm caches (S18): for a trunk build read-write, so the next build is warm; for any other
|
||||
// read-only beneath an overlay whose writes go to a tmpfs of the step's own, so the step writes as a build does and
|
||||
// nothing it wrote outlives it (bubblewrap's --tmp-overlay, an overlay mount in the step's user namespace).
|
||||
func cacheArgs(caches []string, write bool) []string {
|
||||
// nothing it wrote outlives it (bubblewrap's --tmp-overlay, an overlay mount in the step's user namespace). With a
|
||||
// CacheLayer the ask's kept layer lies above the shared cache and the command's writes go to its own layer on disk
|
||||
// (--overlay), which Isolated keeps for the ask when the command ends.
|
||||
func (iso Isolation) cacheArgs() []string {
|
||||
var argv []string
|
||||
for _, c := range caches {
|
||||
if write {
|
||||
for i, c := range iso.Caches {
|
||||
switch {
|
||||
case iso.WriteCaches:
|
||||
argv = append(argv, "--bind", c, c)
|
||||
continue
|
||||
case iso.CacheLayer != "":
|
||||
upper, work := iso.runLayer(i)
|
||||
argv = append(argv, "--overlay-src", c, "--overlay-src", keptLayer(iso.CacheLayer, i), "--overlay", upper, work, c)
|
||||
default:
|
||||
argv = append(argv, "--overlay-src", c, "--tmp-overlay", c)
|
||||
}
|
||||
argv = append(argv, "--overlay-src", c, "--tmp-overlay", c)
|
||||
}
|
||||
return argv
|
||||
}
|
||||
|
||||
func layerDirs(layer string) []string {
|
||||
if layer == "" {
|
||||
return nil
|
||||
}
|
||||
return []string{layer}
|
||||
}
|
||||
|
||||
// keptLayer is where the ask keeps what its commands wrote into the cache i.
|
||||
func keptLayer(layer string, i int) string { return filepath.Join(layer, "kept", strconv.Itoa(i)) }
|
||||
|
||||
// runLayer is the upper and work directory of this command's writes into the cache i. Command, which makes no
|
||||
// directory, names the layer "run"; Isolated gives each command one of its own.
|
||||
func (iso Isolation) runLayer(i int) (upper, work string) {
|
||||
run := iso.run
|
||||
if run == "" {
|
||||
run = filepath.Join(iso.CacheLayer, "run")
|
||||
}
|
||||
return filepath.Join(run, strconv.Itoa(i), "upper"), filepath.Join(run, strconv.Itoa(i), "work")
|
||||
}
|
||||
|
||||
// layerLocks hold one mutex per cache layer, so two commands ending at once keep their writes one after the other.
|
||||
var layerLocks sync.Map
|
||||
|
||||
// prepareLayer makes the kept layers and a run directory of the command's own under the cache layer.
|
||||
func (iso *Isolation) prepareLayer() error {
|
||||
if iso.CacheLayer == "" || iso.WriteCaches || len(iso.Caches) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(iso.CacheLayer, "runs"), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
run, err := os.MkdirTemp(filepath.Join(iso.CacheLayer, "runs"), "run-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
iso.run = run
|
||||
for i := range iso.Caches {
|
||||
upper, work := iso.runLayer(i)
|
||||
for _, d := range []string{keptLayer(iso.CacheLayer, i), upper, work} {
|
||||
if err := os.MkdirAll(d, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// keepLayer moves what the command wrote into the caches into the ask's kept layers, and removes its run directory.
|
||||
// A file the command wrote replaces the kept one (a cache's newest write wins); a whiteout (a file the command
|
||||
// deleted) is not kept, since a cache entry deleted by one command is no fault of a later one. A write that cannot be
|
||||
// kept only means a later command builds it again.
|
||||
func (iso Isolation) keepLayer() {
|
||||
if iso.run == "" {
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(iso.run)
|
||||
mu, _ := layerLocks.LoadOrStore(iso.CacheLayer, &sync.Mutex{})
|
||||
mu.(*sync.Mutex).Lock()
|
||||
defer mu.(*sync.Mutex).Unlock()
|
||||
for i := range iso.Caches {
|
||||
upper, _ := iso.runLayer(i)
|
||||
kept := keptLayer(iso.CacheLayer, i)
|
||||
filepath.WalkDir(upper, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil || p == upper {
|
||||
return nil
|
||||
}
|
||||
rel, _ := filepath.Rel(upper, p)
|
||||
to := filepath.Join(kept, rel)
|
||||
switch {
|
||||
case d.IsDir():
|
||||
if fi, err := os.Lstat(to); err == nil && !fi.IsDir() {
|
||||
os.Remove(to)
|
||||
}
|
||||
os.MkdirAll(to, 0o700)
|
||||
case d.Type().IsRegular():
|
||||
os.Rename(p, to)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// loopbackHosts is the hosts file of a step: the loopback and nothing else.
|
||||
const loopbackHosts = "127.0.0.1 localhost\n::1 localhost\n"
|
||||
|
||||
@@ -166,6 +266,10 @@ func Isolated(run forgekey.Runner, iso Isolation) forgekey.Runner {
|
||||
}
|
||||
iso := iso
|
||||
iso.Hosts = hosts
|
||||
if err := iso.prepareLayer(); err != nil {
|
||||
return forgekey.Ran{Status: 1, Err: "the step's cache layer: " + err.Error()}
|
||||
}
|
||||
defer iso.keepLayer()
|
||||
argv, err := iso.command(env, name, args...)
|
||||
if err != nil {
|
||||
return forgekey.Ran{Status: 1, Err: err.Error()}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package buildstep
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The shared warm caches are read under a throwaway writable layer (novox/hq ADR 0448; issue 1309): a candidate's step
|
||||
// writes into them as a build does, and nothing it writes outlives it or reaches a later build; only a trunk build
|
||||
@@ -27,4 +30,35 @@ func TestACandidatesStepWritesTheCachesOnlyIntoAThrowawayLayer(t *testing.T) {
|
||||
t.Errorf("a trunk build does not write the cache %s: %v", c, argv)
|
||||
}
|
||||
}
|
||||
// 1921/S2: with a cache layer, a command reads the ask's kept layer above the shared cache and writes into a layer
|
||||
// on disk of the ask's own, never into the shared cache and never into a throwaway tmpfs.
|
||||
iso = Isolation{Workspace: "/w", Caches: []string{"/cache/gomod", "/cache/gobuild"}, CacheLayer: "/w/cachelayer"}
|
||||
argv, err = iso.Command("go", "vet")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, c := range iso.Caches {
|
||||
if !holds(argv, "--overlay-src", c, "--overlay-src", keptLayer(iso.CacheLayer, i), "--overlay") {
|
||||
t.Errorf("the cache %s is not read beneath the ask's kept layer: %v", c, argv)
|
||||
}
|
||||
if up := overlayOf(argv, c); up == "" || !strings.HasPrefix(up, "/w/cachelayer/") {
|
||||
t.Errorf("the cache %s is written into %q, not a layer of the ask's own: %v", c, up, argv)
|
||||
}
|
||||
if holds(argv, "--tmp-overlay", c) || holds(argv, "--bind", c, c) {
|
||||
t.Errorf("the cache %s is thrown away or written: %v", c, argv)
|
||||
}
|
||||
}
|
||||
if _, err := (Isolation{Workspace: "/w", CacheLayer: "cachelayer"}).Command("go"); err == nil {
|
||||
t.Error("a relative cache layer was taken")
|
||||
}
|
||||
}
|
||||
|
||||
// overlayOf answers the upper directory a command line gives the cache c, "" when it gives none.
|
||||
func overlayOf(argv []string, c string) string {
|
||||
for i := 0; i+3 < len(argv); i++ {
|
||||
if argv[i] == "--overlay" && argv[i+3] == c {
|
||||
return argv[i+1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -63,4 +63,23 @@ func TestARunStepReachesNothingAndLeavesTheCachesUnchanged(t *testing.T) {
|
||||
if b, err := os.ReadFile(filepath.Join(workspace, "out")); r.Status != 0 || err != nil || string(b) != "built\n" {
|
||||
t.Fatalf("the step's write to its workspace did not stay (status %d, %s%s): %q %v", r.Status, r.Stderr, r.Err, b, err)
|
||||
}
|
||||
|
||||
// 1921/S2: with the ask's cache layer, what one command writes into the cache the next command of the ask reads,
|
||||
// above what the shared cache holds, and the shared cache stays unchanged.
|
||||
asked := Isolated(forgekey.ExecRunnerFor(0), Isolation{Workspace: workspace, Caches: []string{cacheDir},
|
||||
CacheLayer: filepath.Join(workspace, "cachelayer")})
|
||||
r = asked(context.Background(), []string{"PATH=/usr/bin:/bin"}, "/bin/sh", "-c", "cat "+filepath.Join(cacheDir, "warm")+" && mkdir -p "+filepath.Join(cacheDir, "ab")+" && echo compiled > "+filepath.Join(cacheDir, "ab", "one-d"))
|
||||
if r.Status != 0 || r.Err != "" || strings.TrimSpace(r.Stdout) != "warm" {
|
||||
t.Fatalf("the first command of the ask did not read the shared cache and write its own (status %d, %s): %s%s", r.Status, r.Err, r.Stdout, r.Stderr)
|
||||
}
|
||||
r = asked(context.Background(), []string{"PATH=/usr/bin:/bin"}, "/bin/cat", filepath.Join(cacheDir, "ab", "one-d"), filepath.Join(cacheDir, "warm"))
|
||||
if r.Status != 0 || r.Err != "" || r.Stdout != "compiled\nwarm\n" {
|
||||
t.Fatalf("the next command of the ask did not find what the first wrote (status %d, %s): %q %s", r.Status, r.Err, r.Stdout, r.Stderr)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(cacheDir, "ab")); !os.IsNotExist(err) {
|
||||
t.Fatalf("the ask's commands wrote the shared cache: %v", err)
|
||||
}
|
||||
if err := os.RemoveAll(filepath.Join(workspace, "cachelayer")); err != nil {
|
||||
t.Fatalf("the ask's cache layer cannot be removed with its workspace: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ package buildstep
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
@@ -112,4 +115,55 @@ func TestIsolatedRunsEachCommandThroughBubblewrap(t *testing.T) {
|
||||
if hosts == "" {
|
||||
t.Fatalf("no hosts file was bound: %v", ran)
|
||||
}
|
||||
|
||||
// 1921/S2: with a cache layer, what one command wrote into the caches is kept for the ask's later commands, the
|
||||
// shared cache stays unchanged, two commands at once write into layers of their own (overlayfs refuses an upper
|
||||
// layer two mounts share), and a command's own layer is gone when it ends.
|
||||
root := t.TempDir()
|
||||
shared := filepath.Join(root, "gobuild")
|
||||
if err := os.MkdirAll(shared, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
layer := filepath.Join(root, "ws", "cachelayer")
|
||||
var mu sync.Mutex
|
||||
uppers := map[string]bool{}
|
||||
gate := make(chan struct{})
|
||||
var started sync.WaitGroup
|
||||
started.Add(2)
|
||||
writer := func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
||||
up := overlayOf(args, shared)
|
||||
mu.Lock()
|
||||
uppers[up] = true
|
||||
mu.Unlock()
|
||||
started.Done()
|
||||
<-gate // both commands are running before either ends
|
||||
os.MkdirAll(filepath.Join(up, "ab"), 0o700)
|
||||
os.WriteFile(filepath.Join(up, "ab", args[len(args)-1]+"-d"), []byte("compiled"), 0o600)
|
||||
return forgekey.Ran{}
|
||||
}
|
||||
run := Isolated(writer, Isolation{Workspace: filepath.Join(root, "ws"), Caches: []string{shared}, CacheLayer: layer})
|
||||
var wg sync.WaitGroup
|
||||
for _, pkg := range []string{"one", "two"} {
|
||||
wg.Add(1)
|
||||
go func() { defer wg.Done(); run(context.Background(), nil, "go", "build", pkg) }()
|
||||
}
|
||||
started.Wait()
|
||||
close(gate)
|
||||
wg.Wait()
|
||||
if len(uppers) != 2 {
|
||||
t.Fatalf("two commands at once wrote into %d layers, want 2 of their own: %v", len(uppers), uppers)
|
||||
}
|
||||
for _, pkg := range []string{"one", "two"} {
|
||||
if b, err := os.ReadFile(filepath.Join(keptLayer(layer, 0), "ab", pkg+"-d")); err != nil || string(b) != "compiled" {
|
||||
t.Errorf("what the build of %s compiled is not kept for the ask: %q %v", pkg, b, err)
|
||||
}
|
||||
}
|
||||
for up := range uppers {
|
||||
if _, err := os.Stat(up); !os.IsNotExist(err) {
|
||||
t.Errorf("the command's own layer %s outlived it: %v", up, err)
|
||||
}
|
||||
}
|
||||
if entries, _ := os.ReadDir(shared); len(entries) != 0 {
|
||||
t.Errorf("the shared cache was written: %v", entries)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,6 +318,15 @@ func Stub(m catalogue.Manifest) catalogue.Manifest {
|
||||
m.OwnsPaths = catalogue.DeclaredPaths(m)
|
||||
}
|
||||
m.Build = nil
|
||||
// Nor is a template its build kept for its own files handed: a stub carries none of them (issue 1126: novox's node
|
||||
// refused "postgres: its build kept a template for consumers, which it does not declare" with postgres held). Its
|
||||
// items stay, and the templates they render through: a held module's contributions to other modules' collections
|
||||
// (seat contributions, shell blocks, seat data) still reach their collectors.
|
||||
if m.Handed != nil {
|
||||
h := *m.Handed
|
||||
h.Templates = nil
|
||||
m.Handed = &h
|
||||
}
|
||||
var dirs []map[string]any
|
||||
for _, r := range m.Resources {
|
||||
if r["type"] == "directory" {
|
||||
|
||||
@@ -209,21 +209,59 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st
|
||||
return a
|
||||
}
|
||||
|
||||
// heldAsToday moves each refusal of a module the node holds today (Scope.HeldToday) into Held: the candidate holds it as
|
||||
// the node does, which is no change (issue 1126: a seed reproduces today, and today holds it too).
|
||||
func heldAsToday(a *delivery.CandidateAnswer, today []string) {
|
||||
// heldBehind is why a module is held with a held one it reads (composeEach's dependents).
|
||||
func heldBehind(m string) string {
|
||||
return "it reads what " + m + " contributes or listens on, and " + m + " is held"
|
||||
}
|
||||
|
||||
// behind is the held module a refusal's words say it is held behind, or "".
|
||||
func behind(why string) string {
|
||||
rest, ok := strings.CutPrefix(why, "it reads what ")
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
m, _, ok := strings.Cut(rest, " contributes or listens on, and ")
|
||||
if !ok || why != heldBehind(m) {
|
||||
return ""
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// heldAsToday moves into Held each refusal of a module the node keeps today as it runs (Scope.HeldToday, with how:
|
||||
// held, failed, running a build of the old form), and of each module held only behind one of those: the candidate keeps
|
||||
// them as the node does, which is no change (issue 1126: a seed reproduces today, and today keeps them so). Each says
|
||||
// how it stands today (ModuleRefused.Today).
|
||||
func heldAsToday(a *delivery.CandidateAnswer, today map[string]string) {
|
||||
if len(today) == 0 {
|
||||
return
|
||||
}
|
||||
kept := a.Refusals[:0]
|
||||
for _, r := range a.Refusals {
|
||||
if slices.Contains(today, r.Module) {
|
||||
a.Held = append(a.Held, r)
|
||||
} else {
|
||||
kept = append(kept, r)
|
||||
how := map[string]string{}
|
||||
for m, w := range today {
|
||||
how[m] = w
|
||||
}
|
||||
for moved := true; moved; {
|
||||
moved = false
|
||||
kept := a.Refusals[:0]
|
||||
for _, r := range a.Refusals {
|
||||
w, keep := how[r.Module]
|
||||
if !keep {
|
||||
if m := behind(r.Why); m != "" {
|
||||
if _, behindKept := how[m]; behindKept {
|
||||
w, keep = "held behind "+m, true
|
||||
}
|
||||
}
|
||||
}
|
||||
if keep {
|
||||
r.Today = w
|
||||
how[r.Module] = w
|
||||
a.Held = append(a.Held, r)
|
||||
moved = true
|
||||
} else {
|
||||
kept = append(kept, r)
|
||||
}
|
||||
}
|
||||
a.Refusals = kept
|
||||
}
|
||||
a.Refusals = kept
|
||||
if len(a.Refusals) == 0 {
|
||||
a.Refusals = nil
|
||||
}
|
||||
@@ -313,7 +351,7 @@ func composeEach(in compose.Input, node string, fps map[string]string, applied b
|
||||
// What the others read of its manifest (what it contributes, what it listens on) is its new manifest's, not
|
||||
// necessarily the build's that runs: what reads it is held with it (review of the apply, round 2).
|
||||
for _, r := range dependents(in, node, src[m], failed) {
|
||||
hold(r, "it reads what "+m+" contributes or listens on, and "+m+" is held", false)
|
||||
hold(r, heldBehind(m), false)
|
||||
}
|
||||
}
|
||||
for _, m := range sortedKeys(failed) {
|
||||
|
||||
@@ -93,17 +93,25 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
||||
}
|
||||
// Issue 1126: a module the node holds today that the candidate holds too is held as today, no refusal: with every
|
||||
// refused module held today the verdict is no refusal, and each is named under Held.
|
||||
today := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), HeldToday: []string{"api", "spiegel", "web"}},
|
||||
today := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), HeldToday: map[string]string{"api": "held", "spiegel": "held", "web": "held"}},
|
||||
map[string]string{"lemurs": fp("e"), "web": fp("1"), "api": fp("2"), "spiegel": fp("3")}, nil, "p-h1", 3)
|
||||
if today.Verdict == delivery.CandidateRefused || len(today.Refusals) != 0 || len(today.Held) != 3 || today.Held[0].Why == "" {
|
||||
t.Errorf("held as today: %+v", today)
|
||||
}
|
||||
// A refused module the node does not hold today stays a refusal: holding only api today leaves spiegel and web refused.
|
||||
part := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), HeldToday: []string{"api"}},
|
||||
part := Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), HeldToday: map[string]string{"api": "held"}},
|
||||
map[string]string{"lemurs": fp("e"), "web": fp("1"), "api": fp("2"), "spiegel": fp("3")}, nil, "p-h1", 3)
|
||||
if part.Verdict != delivery.CandidateRefused || len(part.Refusals) != 2 || len(part.Held) != 1 || part.Held[0].Module != "api" {
|
||||
t.Errorf("held as today in part: %+v", part)
|
||||
}
|
||||
// A module held only behind one kept today is kept as today too, saying behind which (novox's mailu behind de-spiegel).
|
||||
if a := (delivery.CandidateAnswer{Refusals: []delivery.ModuleRefused{{Module: "de-spiegel", Why: "a value is unset"},
|
||||
{Module: "mailu", Why: heldBehind("de-spiegel")}, {Module: "proxy", Why: heldBehind("mailu")}, {Module: "web", Why: "no build"}}}); true {
|
||||
heldAsToday(&a, map[string]string{"de-spiegel": "runs a build of the old form"})
|
||||
if len(a.Refusals) != 1 || a.Refusals[0].Module != "web" || len(a.Held) != 3 || a.Held[2].Today != "held behind mailu" {
|
||||
t.Errorf("held behind a module kept today: %+v", a)
|
||||
}
|
||||
}
|
||||
|
||||
// The fold: spiegel runs inside the node's own composition at a build of the old form. Its resources are carried as
|
||||
// they run, each after the one it runs after: the node's own is the one it runs, and the node composes itself.
|
||||
@@ -286,6 +294,31 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
||||
if got, want := catalogue.DeclaredPaths(compose.Carried(f1, "f1-circuits", &f1runs)), catalogue.DeclaredPaths(f1); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("f1-circuits carried owns %v, want what the controller composes from its manifest, %v", got, want)
|
||||
}
|
||||
// Issue 1126: a held module hands no template its build kept for a file it no longer carries (novox's node:
|
||||
// "postgres: its build kept a template for consumers"), but keeps its items and their templates, so what it
|
||||
// contributes to other modules' collections still reaches them.
|
||||
for _, row := range []struct {
|
||||
name string
|
||||
handed *catalogue.Handed
|
||||
items []catalogue.Item
|
||||
}{
|
||||
{"templates and items", &catalogue.Handed{Templates: map[string]string{"consumers": "files/consumers.json"},
|
||||
Items: map[string]string{"routes": "files/routes.tmpl"}}, []catalogue.Item{{Collection: "routes", Content: "x"}}},
|
||||
{"items alone", nil, []catalogue.Item{{Collection: "routes", Content: "x"}}},
|
||||
} {
|
||||
kept := f1
|
||||
kept.Handed, kept.Items = row.handed, row.items
|
||||
st := compose.Stub(kept)
|
||||
if st.Handed != nil && st.Handed.Templates != nil {
|
||||
t.Errorf("%s: a stub hands templates %v", row.name, st.Handed.Templates)
|
||||
}
|
||||
if !reflect.DeepEqual(st.Items, row.items) {
|
||||
t.Errorf("%s: a stub collects %+v, want its items %+v", row.name, st.Items, row.items)
|
||||
}
|
||||
if row.handed != nil && (st.Handed == nil || !reflect.DeepEqual(st.Handed.Items, row.handed.Items) || row.handed.Templates == nil) {
|
||||
t.Errorf("%s: a stub hands item templates %+v, want %v and its manifest untouched", row.name, st.Handed, row.handed.Items)
|
||||
}
|
||||
}
|
||||
var k1, k2 any
|
||||
_ = json.Unmarshal(keptComposed, &k1)
|
||||
_ = json.Unmarshal(keptFile, &k2)
|
||||
|
||||
@@ -134,10 +134,10 @@ type Scope struct {
|
||||
Placements map[string]*delivery.Placement
|
||||
InOwn []string
|
||||
Running []delivery.Composed
|
||||
// HeldToday is, in an answer, the modules the node holds today ("node" for its own composition): their have's phase
|
||||
// is held, or they are held inside its own composition. A candidate that holds one too holds it as today, which is
|
||||
// said under CandidateAnswer.Held and is no refusal (issue 1126).
|
||||
HeldToday []string
|
||||
// HeldToday is, in an answer, the modules the node keeps today as they run ("node" for its own composition), each with
|
||||
// how ("held", "failed", "runs a build of the old form"): a candidate that cannot compose one, or holds one only
|
||||
// behind one of them, keeps it as today, which is said under CandidateAnswer.Held and is no refusal (issue 1126).
|
||||
HeldToday map[string]string
|
||||
// SameValue, in an answer, says whether two sealed values open to the same value (the node opens them with its own
|
||||
// key), so a differing resource shows a value sealed again as such (delivery.CompareComposedWith). Nil shows them
|
||||
// as differing.
|
||||
|
||||
@@ -258,9 +258,13 @@ func Brief(p delivery.PlacementProposal, v delivery.PlacementVerdict) string {
|
||||
if held := v.Answers[n].Held; len(held) > 0 {
|
||||
var names []string
|
||||
for _, h := range held {
|
||||
names = append(names, h.Module)
|
||||
today := h.Today
|
||||
if today == "" {
|
||||
today = "held"
|
||||
}
|
||||
names = append(names, h.Module+" ("+today+")")
|
||||
}
|
||||
fmt.Fprintf(&b, "; stays held as today: %s", strings.Join(names, ", "))
|
||||
fmt.Fprintf(&b, "; stays as today: %s", strings.Join(names, ", "))
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
|
||||
@@ -79,7 +79,7 @@ func TestJudge(t *testing.T) {
|
||||
t.Errorf("the difference is named %q", got)
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(row.name, "a module held as today") && !strings.Contains(Brief(q, v), "stays held as today: spiegel") {
|
||||
if strings.HasPrefix(row.name, "a module held as today") && !strings.Contains(Brief(q, v), "stays as today: spiegel (held)") {
|
||||
t.Errorf("%s: the brief does not show it: %s", row.name, Brief(q, v))
|
||||
}
|
||||
if row.marked != "" && (!slices.Contains(v.Unintended["anchor"], row.marked) || !strings.Contains(Brief(q, v), "not computed: "+row.marked)) {
|
||||
|
||||
@@ -190,10 +190,13 @@ type ModuleAdded struct {
|
||||
Unissued []string `json:"unissued,omitempty"`
|
||||
}
|
||||
|
||||
// ModuleRefused is a value missing for a module the node runs, in the words the composition stood in for it with.
|
||||
// ModuleRefused is a value missing for a module the node runs, in the words the composition stood in for it with. Today,
|
||||
// in CandidateAnswer.Held, is how the node keeps it today (held, failed, runs a build of the old form, held behind
|
||||
// another), which the candidate keeps.
|
||||
type ModuleRefused struct {
|
||||
Module string `json:"module"`
|
||||
Why string `json:"why"`
|
||||
Today string `json:"today,omitempty"`
|
||||
}
|
||||
|
||||
// VerdictOf is the verdict of an answer's lists: refused on any refusal, differs on any difference, addition or
|
||||
|
||||
Reference in New Issue
Block a user