Merge pull request 'A build not done yet holds its module, never every module of a node, and a refusal names what was held (issue 774)' (#140) from fix/build-not-done-holds-one-module into main
Reviewed-on: #140
This commit was merged in pull request #140.
This commit is contained in:
@@ -41,7 +41,7 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st
|
|||||||
// Every module it cannot compose is named, not the first only (issue 1126): a build it cannot read or take in, and
|
// Every module it cannot compose is named, not the first only (issue 1126): a build it cannot read or take in, and
|
||||||
// below, a module the composition refuses, each composed as a stub so the rest is composed and compared.
|
// below, a module the composition refuses, each composed as a stub so the rest is composed and compared.
|
||||||
failed := map[string]string{}
|
failed := map[string]string{}
|
||||||
in, err := input(ctx, r, mesh, s, func(module string, err error) { failed[module] = err.Error() })
|
in, err := input(ctx, r, mesh, s, func(module string, err error) { failed[module] = err.Error() }, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return refuse(err)
|
return refuse(err)
|
||||||
}
|
}
|
||||||
@@ -373,6 +373,16 @@ func composeEach(in compose.Input, node string, fps map[string]string, applied b
|
|||||||
return out, skipped, stood, nil
|
return out, skipped, stood, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// blames says the composition with m's stub (again) makes m the culprit of err: it composes, or fails otherwise. The
|
||||||
|
// runtime's stub is never a runtime (no build is taken in, so it carries no bundle) and always fails otherwise: it is
|
||||||
|
// the culprit only where its stub composes (issue 774, 2026-10-30 19:30: node-tools held for any error naming it).
|
||||||
|
func blames(m string, again, err error) bool {
|
||||||
|
if m == catalogue.RuntimeModule {
|
||||||
|
return again == nil
|
||||||
|
}
|
||||||
|
return again == nil || again.Error() != err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
// leaveAll leaves out the whole of a held module's offer and its own secrets (a bare stub); any other word leaves out
|
// leaveAll leaves out the whole of a held module's offer and its own secrets (a bare stub); any other word leaves out
|
||||||
// what it contributes to that provision alone.
|
// what it contributes to that provision alone.
|
||||||
const leaveAll = "*"
|
const leaveAll = "*"
|
||||||
@@ -419,7 +429,7 @@ func culprit(in compose.Input, node string, fps map[string]string, mode compose.
|
|||||||
}
|
}
|
||||||
changes := func(m, leave string) bool {
|
changes := func(m, leave string) bool {
|
||||||
_, _, _, again := compose.CompositionsIn(stubbed(in, node, m, leave), node, fps, mode)
|
_, _, _, again := compose.CompositionsIn(stubbed(in, node, m, leave), node, fps, mode)
|
||||||
return again == nil || again.Error() != err.Error()
|
return blames(m, again, err)
|
||||||
}
|
}
|
||||||
for _, m := range order {
|
for _, m := range order {
|
||||||
if _, done := failed[m]; done {
|
if _, done := failed[m]; done {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -177,7 +178,7 @@ func InOwn(running []delivery.Composed, p delivery.Placement) []string {
|
|||||||
|
|
||||||
// Input is the composition input for s, read through r.
|
// Input is the composition input for s, read through r.
|
||||||
func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, error) {
|
func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, error) {
|
||||||
return input(ctx, r, mesh, s, nil)
|
return input(ctx, r, mesh, s, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// input is Input; with refuse set, a module placed on the composing node whose build cannot be read or taken in, or whose
|
// input is Input; with refuse set, a module placed on the composing node whose build cannot be read or taken in, or whose
|
||||||
@@ -185,7 +186,7 @@ func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, er
|
|||||||
// rather than ending the read, so a node's answer names every module it cannot compose (issue 1126, issue 774); a
|
// rather than ending the read, so a node's answer names every module it cannot compose (issue 1126, issue 774); a
|
||||||
// module placed elsewhere whose manifest the mesh refuses is read for its offer alone (unusable). A manifest the bus does
|
// module placed elsewhere whose manifest the mesh refuses is read for its offer alone (unusable). A manifest the bus does
|
||||||
// not answer for still ends the read: that is the bus's, not the module's.
|
// not answer for still ends the read: that is the bus's, not the module's.
|
||||||
func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module string, err error)) (compose.Input, error) {
|
func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module string, err error), wanted map[string]string) (compose.Input, error) {
|
||||||
placements, err := r.Placements(ctx)
|
placements, err := r.Placements(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return compose.Input{}, fmt.Errorf("frombus: placement: %w", err)
|
return compose.Input{}, fmt.Errorf("frombus: placement: %w", err)
|
||||||
@@ -397,7 +398,7 @@ func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module
|
|||||||
if _, ok := records[s.Node]; !ok {
|
if _, ok := records[s.Node]; !ok {
|
||||||
return compose.Input{}, fmt.Errorf("frombus: %s has written no record of itself yet", s.Node)
|
return compose.Input{}, fmt.Errorf("frombus: %s has written no record of itself yet", s.Node)
|
||||||
}
|
}
|
||||||
if err := fromBuilds(ctx, r, &in, s, placements[s.Node], modules, read, parse, refuse); err != nil {
|
if err := fromBuilds(ctx, r, &in, s, placements[s.Node], modules, read, parse, refuse, wanted); err != nil {
|
||||||
return compose.Input{}, err
|
return compose.Input{}, err
|
||||||
}
|
}
|
||||||
// Its own modules' secrets, sealed to its own key by the vault: a composition carries them sealed (ADR 0550 §5).
|
// Its own modules' secrets, sealed to its own key by the vault: a composition carries them sealed (ADR 0550 §5).
|
||||||
@@ -429,7 +430,18 @@ func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module
|
|||||||
// elsewhere, a stand-in for the node's own) refuses the node, err.
|
// elsewhere, a stand-in for the node's own) refuses the node, err.
|
||||||
func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (out map[string]delivery.Composed, skipped []string, held map[string]string, err error) {
|
func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (out map[string]delivery.Composed, skipped []string, held map[string]string, err error) {
|
||||||
held = map[string]string{}
|
held = map[string]string{}
|
||||||
in, err := input(ctx, r, mesh, s, func(module string, err error) { held[module] = err.Error() })
|
// A node refused whole says what it held on the way, since the refusal can be of a held module's stub (issue 774,
|
||||||
|
// 2026-10-30 19:30: node-tools held, then "its build produced 0 bundle(s)" with its own refusal unsaid).
|
||||||
|
defer func() {
|
||||||
|
if err != nil && len(held) > 0 {
|
||||||
|
var said []string
|
||||||
|
for _, m := range sortedKeys(held) {
|
||||||
|
said = append(said, m+": "+held[m])
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("%w (held before: %s)", err, strings.Join(said, "; "))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
in, err := input(ctx, r, mesh, s, func(module string, err error) { held[module] = err.Error() }, fps)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, held, err
|
return nil, nil, held, err
|
||||||
}
|
}
|
||||||
@@ -467,7 +479,7 @@ func Plans(ctx context.Context, r Reader, mesh Mesh, s Scope) (map[string]catalo
|
|||||||
// err. The seat mesh-provisioner's holder composes so: a node with one bad manifest still has every other module's plan.
|
// err. The seat mesh-provisioner's holder composes so: a node with one bad manifest still has every other module's plan.
|
||||||
func PlansHeld(ctx context.Context, r Reader, mesh Mesh, s Scope) (plans map[string]catalogue.Plan, held map[string]string, err error) {
|
func PlansHeld(ctx context.Context, r Reader, mesh Mesh, s Scope) (plans map[string]catalogue.Plan, held map[string]string, err error) {
|
||||||
held = map[string]string{}
|
held = map[string]string{}
|
||||||
in, err := input(ctx, r, mesh, s, func(module string, err error) { held[module] = err.Error() })
|
in, err := input(ctx, r, mesh, s, func(module string, err error) { held[module] = err.Error() }, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, held, err
|
return nil, held, err
|
||||||
}
|
}
|
||||||
@@ -607,7 +619,7 @@ func ranOf(ctx context.Context, r Reader, mesh Mesh, running []delivery.Composed
|
|||||||
// (issue 1126).
|
// (issue 1126).
|
||||||
func fromBuilds(ctx context.Context, r Reader, in *compose.Input, s Scope, p delivery.Placement, modules map[string]delivery.Module,
|
func fromBuilds(ctx context.Context, r Reader, in *compose.Input, s Scope, p delivery.Placement, modules map[string]delivery.Module,
|
||||||
read func(module, digest string) (catalogue.Manifest, error), parse func([]byte) (catalogue.Manifest, error),
|
read func(module, digest string) (catalogue.Manifest, error), parse func([]byte) (catalogue.Manifest, error),
|
||||||
refuse func(module string, err error)) error {
|
refuse func(module string, err error), wanted map[string]string) error {
|
||||||
// stub keeps a refused module in the composition as a stub for this node alone (compose.Stub): what it provides
|
// stub keeps a refused module in the composition as a stub for this node alone (compose.Stub): what it provides
|
||||||
// and requires still shapes the rest.
|
// and requires still shapes the rest.
|
||||||
stub := func(module string, err error) {
|
stub := func(module string, err error) {
|
||||||
@@ -667,9 +679,27 @@ func fromBuilds(ctx context.Context, r Reader, in *compose.Input, s Scope, p del
|
|||||||
}
|
}
|
||||||
return resolved, nil
|
return resolved, nil
|
||||||
}
|
}
|
||||||
|
// A module wanted at another build than the trunk's is composed from the build its want names (issue 774), as one
|
||||||
|
// the scope keeps at another build is.
|
||||||
|
keptAt := maps.Clone(s.KeptAt)
|
||||||
|
for _, pm := range p.Modules {
|
||||||
|
if m, ok := modules[pm.Module]; ok && m.FP2 != "" && wanted[pm.Module] != "" && wanted[pm.Module] != m.FP2 &&
|
||||||
|
keptAt[pm.Module] == "" && !slices.Contains(s.InOwn, pm.Module) {
|
||||||
|
if keptAt == nil {
|
||||||
|
keptAt = map[string]string{}
|
||||||
|
}
|
||||||
|
keptAt[pm.Module] = wanted[pm.Module]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runs := map[string]bool{}
|
||||||
|
for _, c := range s.Running {
|
||||||
|
if c.Module != "" {
|
||||||
|
runs[c.Module] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, pm := range p.Modules {
|
for _, pm := range p.Modules {
|
||||||
m, ok := modules[pm.Module]
|
m, ok := modules[pm.Module]
|
||||||
if !ok || m.FP2 == "" || s.KeptAt[pm.Module] != "" || slices.Contains(s.InOwn, pm.Module) {
|
if !ok || m.FP2 == "" || keptAt[pm.Module] != "" || slices.Contains(s.InOwn, pm.Module) {
|
||||||
// A module it runs inside its own composition is composed there as before: no build of the new form is what
|
// A module it runs inside its own composition is composed there as before: no build of the new form is what
|
||||||
// it runs (Scope.InOwn).
|
// it runs (Scope.InOwn).
|
||||||
continue
|
continue
|
||||||
@@ -687,7 +717,14 @@ func fromBuilds(ctx context.Context, r Reader, in *compose.Input, s Scope, p del
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if b == nil || b.State != delivery.BuildDone {
|
if b == nil || b.State != delivery.BuildDone {
|
||||||
continue // no done build at the trunk's: composed from the indexer's manifest, as before
|
if refuse != nil && runs[pm.Module] {
|
||||||
|
// A module that runs is never composed from the indexer's manifest, which names no build's artifacts (no
|
||||||
|
// runtime's bundle): it is held, keeping what runs, until its build lands (issue 774, 2026-10-30 19:30:
|
||||||
|
// node-tools on ace, g14 and shanks, "its build produced 0 bundle(s)", and every module refused).
|
||||||
|
stub(pm.Module, fmt.Errorf("frombus: %s is wanted at %s, of which the builder holds no done build yet; it "+
|
||||||
|
"keeps running what it runs", pm.Module, m.FP2))
|
||||||
|
}
|
||||||
|
continue // nothing of it runs: composed from the indexer's manifest, as before
|
||||||
}
|
}
|
||||||
resolved, err := built(pm.Module, m.FP2, b)
|
resolved, err := built(pm.Module, m.FP2, b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -703,8 +740,8 @@ func fromBuilds(ctx context.Context, r Reader, in *compose.Input, s Scope, p del
|
|||||||
if in.Nodes[i].Name != s.Node {
|
if in.Nodes[i].Name != s.Node {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, module := range sortedKeys(s.KeptAt) {
|
for _, module := range sortedKeys(keptAt) {
|
||||||
fp := s.KeptAt[module]
|
fp := keptAt[module]
|
||||||
b, err := r.Build(ctx, module, fp)
|
b, err := r.Build(ctx, module, fp)
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
|
|||||||
@@ -138,6 +138,10 @@ func TestInput(t *testing.T) {
|
|||||||
// saying why (answer_rows_test.go).
|
// saying why (answer_rows_test.go).
|
||||||
t.Run("every refusal, and an old build left to the node", answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode)
|
t.Run("every refusal, and an old build left to the node", answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode)
|
||||||
t.Run("no store reached says why", noStoreReachedSaysWhy)
|
t.Run("no store reached says why", noStoreReachedSaysWhy)
|
||||||
|
// Issue 774: a node refused whole says what it held on the way.
|
||||||
|
t.Run("a node refused whole says what it held", aNodeRefusedWholeSaysWhatItHeld)
|
||||||
|
t.Run("the runtime is blamed only where its stub composes", theRuntimeIsBlamedOnlyWhereItsStubComposes)
|
||||||
|
t.Run("a running module waits for its build", aRunningModuleWaitsForItsBuild)
|
||||||
t.Run("another node not resolved is named", otherNodeNotResolvedIsNamed)
|
t.Run("another node not resolved is named", otherNodeNotResolvedIsNamed)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
xterm, err := os.ReadFile("../../catalogue/testdata/catalogue-835bf22b-xterm.module.json")
|
xterm, err := os.ReadFile("../../catalogue/testdata/catalogue-835bf22b-xterm.module.json")
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package frombus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// aNode is a bus with n1 the hub, each of manifests placed on it and held at the indexer, in the order given.
|
||||||
|
func aNode(t *testing.T, manifests ...string) (*bus, map[string]string) {
|
||||||
|
t.Helper()
|
||||||
|
p := delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}}
|
||||||
|
b := &bus{records: map[string]delivery.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t), SealingKey: "sk", Capabilities: []string{"systemd"}}},
|
||||||
|
modules: map[string]delivery.Module{}, manifests: map[string][]byte{}, builds: map[string]*delivery.BuilderBuild{}}
|
||||||
|
fps := map[string]string{}
|
||||||
|
for i, raw := range manifests {
|
||||||
|
module := strings.SplitN(strings.TrimPrefix(raw, `{"module":"`), `"`, 2)[0]
|
||||||
|
p.Modules = append(p.Modules, delivery.PlacedModule{Module: module})
|
||||||
|
b.modules[module] = delivery.Module{Manifest: delivery.ManifestDigest([]byte(raw))}
|
||||||
|
b.manifests[delivery.ManifestDigest([]byte(raw))] = []byte(raw)
|
||||||
|
fps[module] = "src2:" + strings.Repeat(string(rune('1'+i)), 64)
|
||||||
|
}
|
||||||
|
b.placements = map[string]delivery.Placement{"n1": p}
|
||||||
|
return b, fps
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue 774, 2026-10-30 19:30: a node refused whole after holding a module says what it held and why, since the last
|
||||||
|
// refusal can be of the held module's stub (node-tools held, then "its build produced 0 bundle(s)", its own why unsaid).
|
||||||
|
func aNodeRefusedWholeSaysWhatItHeld(t *testing.T) {
|
||||||
|
refused := `{"module":"power","version":"1","items":[{"collection":"contribution:node-bar:block","attributes":{"shows":"state"},"template":"files/a"}]}`
|
||||||
|
needs := `{"module":"lamp","version":"1","requires":["nothing-provides-this"]}`
|
||||||
|
b, fps := aNode(t, refused, needs)
|
||||||
|
_, _, held, err := Compose(context.Background(), b, Mesh{}, Scope{Node: "n1"}, fps)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "nothing provides") || !strings.Contains(err.Error(), `held before: power: `) ||
|
||||||
|
!strings.Contains(err.Error(), `"shows" is none of them`) {
|
||||||
|
t.Errorf("the node refused whole with %v (held %v), want lamp's refusal and power held before, with its why", err, held)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue 774: the culprit rule takes a module whose stub composes or fails otherwise; the runtime only where its stub
|
||||||
|
// composes, since its stub always fails otherwise.
|
||||||
|
func theRuntimeIsBlamedOnlyWhereItsStubComposes(t *testing.T) {
|
||||||
|
err := errors.New("lamp refuses")
|
||||||
|
for _, row := range []struct {
|
||||||
|
module string
|
||||||
|
again error
|
||||||
|
blamed bool
|
||||||
|
}{
|
||||||
|
{"lamp", nil, true},
|
||||||
|
{"lamp", errors.New("other"), true},
|
||||||
|
{"lamp", errors.New("lamp refuses"), false},
|
||||||
|
{catalogue.RuntimeModule, nil, true},
|
||||||
|
{catalogue.RuntimeModule, errors.New("node-tools is assigned to n1 and its build produced 0 bundle(s)"), false},
|
||||||
|
} {
|
||||||
|
if got := blames(row.module, row.again, err); got != row.blamed {
|
||||||
|
t.Errorf("%s with %v: blamed %v, want %v", row.module, row.again, got, row.blamed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue 774, 2026-10-30 19:30: the trunk's node-tools moved to a build not yet done, and ace, g14 and shanks composed
|
||||||
|
// it from the indexer's manifest, which names no bundle: "its build produced 0 bundle(s)", every module refused. A
|
||||||
|
// module that runs is now held until its build lands, the runtime composed as what runs of it; and a module wanted at
|
||||||
|
// another build than the trunk's is composed from the build its want names.
|
||||||
|
func aRunningModuleWaitsForItsBuild(t *testing.T) {
|
||||||
|
dg := func(c string) string { return "sha256:" + strings.Repeat(c, 64) }
|
||||||
|
tools := `{"module":"node-tools","version":"1","own-secrets":{"broker":"/var/lib/mesh/node-tools/broker"},` +
|
||||||
|
`"build":{"artifacts":[{"name":"runtime","kind":"bundle","language":"go","system":"arch","from":"cmd/node-tools"}]}}`
|
||||||
|
lamp := `{"module":"lamp","version":"1","resources":[{"id":"service","type":"service","unit":"lamp.service","state":"running","boot":"enabled"}]}`
|
||||||
|
store := `{"module":"artifacts","version":"1","provides":[{"name":"artifact-store","scope":"mesh"}],"serves":{"artifact-store":{"port":5100}}}`
|
||||||
|
old, next := "src2:"+strings.Repeat("a", 64), "src2:"+strings.Repeat("b", 64)
|
||||||
|
running := []delivery.Composed{{Node: "n1", Module: catalogue.RuntimeModule, FP: old, Resources: []json.RawMessage{
|
||||||
|
json.RawMessage(`{"id":"node-tools.runtime","module":"node-tools","type":"process","name":"node-tools","env":{"MESH_TOOL_MODULES":""}}`),
|
||||||
|
json.RawMessage(`{"id":"node-tools.bundle-runtime","module":"node-tools","type":"archive","source":"artifact-store://node-tools/runtime/blobs/` +
|
||||||
|
dg("1") + `","digest":"` + dg("1") + `","path":"` + catalogue.BundlePath("node-tools", "runtime") + `"}`)}}}
|
||||||
|
for _, row := range []struct {
|
||||||
|
name string
|
||||||
|
want string // the build node-tools' want names
|
||||||
|
done bool // the trunk's build is done
|
||||||
|
held string // words node-tools' hold says; empty: composed
|
||||||
|
digest string // the runtime bundle's digest composed
|
||||||
|
}{
|
||||||
|
{name: "the trunk's build not done yet", want: next, held: "no done build yet"},
|
||||||
|
{name: "wanted at the build that runs, the trunk's not done", want: old, digest: dg("1")},
|
||||||
|
{name: "the trunk's build done", want: next, done: true, digest: dg("2")},
|
||||||
|
} {
|
||||||
|
b, fps := aNode(t, store, tools, lamp)
|
||||||
|
b.own = []compose.OwnSecretRow{{Node: "n1", Module: "node-tools", Name: "broker", Origin: "made", SealedTo: "sk", Sealed: `"box"`}}
|
||||||
|
mod := b.modules["node-tools"]
|
||||||
|
mod.FP2 = next
|
||||||
|
b.modules["node-tools"] = mod
|
||||||
|
b.builds["node-tools "+old] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: mod.Manifest,
|
||||||
|
Artifacts: []delivery.BuiltArtifact{{Name: "runtime", Kind: "bundle", Reference: "artifact-store://node-tools/runtime/blobs/" + dg("1"), Digest: dg("1")}}}
|
||||||
|
b.builds["node-tools "+next] = &delivery.BuilderBuild{State: "building", Manifest: mod.Manifest}
|
||||||
|
if row.done {
|
||||||
|
b.builds["node-tools "+next] = &delivery.BuilderBuild{State: delivery.BuildDone, Manifest: mod.Manifest,
|
||||||
|
Artifacts: []delivery.BuiltArtifact{{Name: "runtime", Kind: "bundle", Reference: "artifact-store://node-tools/runtime/blobs/" + dg("2"), Digest: dg("2")}}}
|
||||||
|
}
|
||||||
|
fps["node-tools"] = row.want
|
||||||
|
out, _, held, err := Compose(context.Background(), b, Mesh{}, Scope{Node: "n1", Running: running}, fps)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: the node is refused: %v", row.name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lampKey, _ := delivery.ComposedKey("n1", "lamp", fps["lamp"]); out[lampKey].Module != "lamp" {
|
||||||
|
t.Errorf("%s: lamp is not composed (held %v)", row.name, held)
|
||||||
|
}
|
||||||
|
if why := held["node-tools"]; (row.held == "") != (why == "") || !strings.Contains(why, row.held) {
|
||||||
|
t.Errorf("%s: node-tools held %q, want %q", row.name, why, row.held)
|
||||||
|
}
|
||||||
|
if row.digest != "" {
|
||||||
|
key, _ := delivery.ComposedKey("n1", "node-tools", row.want)
|
||||||
|
if c := out[key]; !strings.Contains(fmt.Sprint(rawStrings(c.Resources)), row.digest) {
|
||||||
|
t.Errorf("%s: node-tools is composed as %v, want its bundle %s", row.name, rawStrings(c.Resources), row.digest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func rawStrings(rs []json.RawMessage) []string {
|
||||||
|
var out []string
|
||||||
|
for _, r := range rs {
|
||||||
|
out = append(out, string(r))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user