ADR 0578 §2 and §3: a case's accepted verdict, or none (the running checker drops the change's own), lifts the input's hold on a manifest the composer reads whole; a case's refusal and bytes that do not decode stay held. It is composed at the build the input carries (Input.Holds). Without that build the node is not judged and the verdict names the module, why it was held and that its build is awaited; where it cannot compose without it, it is composed held as its node-engine holds it, and said so. Never a silent pass (the lead's Q4 ruling). The issue 774 row now gives the runner the refused verdict the checker gives a manifest the change does not bring. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
520 lines
20 KiB
Go
520 lines
20 KiB
Go
// Package composer is the node-engine's composition as one function over a plain request (novox/hq ADR 0482 clause 4,
|
|
// ADR 0593 §7): what the command mesh-compose runs, so a group that changes the node-engine's composition is composed by
|
|
// the composer built at that group's head of the node-engine's repository, in a build module's sandbox.
|
|
//
|
|
// **It reads its request and nothing else.** No bus, store, network, file or environment: the manifests come inside the
|
|
// request (raw, as their repository holds them), the rest of the mesh is the request's input, and the answer is a value.
|
|
// The request's and the verdict's shapes are mesh-sdk's composer package; their logic, which composes, is here.
|
|
package composer
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
|
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
|
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
|
|
"maps"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
sdk "git.novox.be/novox/mesh-delivery/sdk/composer"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/keys"
|
|
"git.novox.be/novox/mesh-sdk/go/sdkversion"
|
|
|
|
"github.com/novox/mesh-host/internal/compose"
|
|
"github.com/novox/mesh-host/internal/compose/frombus"
|
|
)
|
|
|
|
// The shapes of mesh-sdk's composer package, by the names this package's code was written with.
|
|
type (
|
|
Request = sdk.Request
|
|
Case = sdk.Case
|
|
Manifest = sdk.Manifest
|
|
Verdict = sdk.Verdict
|
|
CaseVerdict = sdk.CaseVerdict
|
|
ManifestRead = sdk.ManifestRead
|
|
Machine = sdk.Machine
|
|
Path = sdk.Path
|
|
)
|
|
|
|
const (
|
|
Version = sdk.Version
|
|
DepthResolve = sdk.DepthResolve
|
|
DepthCompose = sdk.DepthCompose
|
|
)
|
|
|
|
// ErrUnfilled is a manifest named by a digest that nobody replaced by its bytes (sdk.ErrUnfilled).
|
|
var ErrUnfilled = sdk.ErrUnfilled
|
|
|
|
// inputOf is the request's input as this composer's composition input: the request carries it as JSON (mesh-sdk holds no
|
|
// composition), the same bytes either way.
|
|
func inputOf(r Request) (compose.Input, error) {
|
|
var in compose.Input
|
|
raw, err := json.Marshal(r.Input)
|
|
if err != nil {
|
|
return in, fmt.Errorf("composer: the request's input cannot be read: %w", err)
|
|
}
|
|
if len(raw) == 0 || string(raw) == "null" {
|
|
return in, nil
|
|
}
|
|
if err := json.Unmarshal(raw, &in); err != nil {
|
|
return in, fmt.Errorf("composer: the request's input is not a composition input: %w", err)
|
|
}
|
|
return in, nil
|
|
}
|
|
|
|
// Run composes every case of r and answers the verdict, its SDK this binary's mesh-sdk. An error is a request it
|
|
// cannot run (an unknown depth, a case named twice, a manifest left as a digest); a machine that does not compose is a
|
|
// verdict.
|
|
func Run(r Request) (Verdict, error) {
|
|
depth := r.Depth
|
|
if depth == "" {
|
|
depth = DepthCompose
|
|
}
|
|
if depth != DepthResolve && depth != DepthCompose {
|
|
return Verdict{}, fmt.Errorf("composer: the depth %q is not resolve or compose", r.Depth)
|
|
}
|
|
v := Verdict{V: Version, SDK: sdkversion.Version()}
|
|
seen := map[string]bool{}
|
|
for _, c := range r.Cases {
|
|
if seen[c.Name] {
|
|
return Verdict{}, fmt.Errorf("composer: the case %q is named twice", c.Name)
|
|
}
|
|
seen[c.Name] = true
|
|
cv, err := runCase(r, c, depth)
|
|
if err != nil {
|
|
return Verdict{}, err
|
|
}
|
|
v.Cases = append(v.Cases, cv)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// parse decodes every manifest leniently and takes its verdict (novox/hq ADR 0578 §3, §5): the composer judges none. A
|
|
// manifest refused by its verdict is held, composed as its stub without its items (as the bus reader holds one); bytes
|
|
// that do not decode are left out. Both are named in reads.
|
|
func parse(m map[string]Manifest, reads map[string]ManifestRead, verdicts map[string]json.RawMessage) (map[string]catalogue.Manifest, error) {
|
|
out := map[string]catalogue.Manifest{}
|
|
for name, man := range m {
|
|
if len(man.Raw) == 0 {
|
|
return nil, fmt.Errorf("%w: %s (digest %s)", ErrUnfilled, name, man.Digest)
|
|
}
|
|
decoded, err := catalogue.Decode(man.Raw)
|
|
read := ManifestRead{Accepted: err == nil, SetAside: decoded.SetAside()}
|
|
if err != nil {
|
|
read.Refused = fmt.Sprintf("the manifest of %s does not decode: %v", name, err)
|
|
} else if raw, ok := verdicts[name]; ok {
|
|
v, verr := caseVerdict(raw)
|
|
if verr != nil {
|
|
return nil, fmt.Errorf("composer: the verdict of %s is not one: %w", name, verr)
|
|
}
|
|
if refused, _ := meshcatalogclient.TakeVerdict(&v); refused {
|
|
if v.Module == "" {
|
|
v.Module = name
|
|
}
|
|
read = ManifestRead{Refused: meshcatalogclient.VerdictWhy(v)}
|
|
decoded.Items = nil
|
|
decoded = compose.Stub(decoded)
|
|
}
|
|
}
|
|
if reads != nil {
|
|
reads[name] = read
|
|
}
|
|
if err == nil {
|
|
out[name] = decoded
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// caseVerdict reads a verdict a case carries. It is not the indexer's kept value but the checker's account of it, made
|
|
// from the indexer's modules value: a checker of the trunk writes it with version 0 and no judge, one of the registry
|
|
// milestone with no version and no judge, and the composer of a group's head is asked by the checker that runs. So it is
|
|
// read in the kept value's form at version 0 (or none) and at keys.Version, its judge optional; a later version, a value
|
|
// that does not decode and a state no reader knows are refused, never taken as accepted.
|
|
func caseVerdict(raw json.RawMessage) (meshcatalog.VerdictsByDigestValue, error) {
|
|
var v meshcatalog.VerdictsByDigestValue
|
|
if err := json.Unmarshal(raw, &v); err != nil {
|
|
return v, fmt.Errorf("the value does not fit a verdict's shape: %w", err)
|
|
}
|
|
if v.V != 0 && v.V != keys.Version {
|
|
return v, fmt.Errorf("%w: the value is version %d, this reader reads 0 and %d", keys.ErrUnknownVersion, v.V, keys.Version)
|
|
}
|
|
switch v.State {
|
|
case meshcatalogclient.VerdictAccepted, meshcatalogclient.VerdictRefused, meshcatalogclient.VerdictManifestWaits:
|
|
default:
|
|
return v, fmt.Errorf("the state %q is none of %s, %s and %s", v.State, meshcatalogclient.VerdictAccepted,
|
|
meshcatalogclient.VerdictRefused, meshcatalogclient.VerdictManifestWaits)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
func runCase(r Request, c Case, depth string) (CaseVerdict, error) {
|
|
cv := CaseVerdict{Name: c.Name, Manifests: map[string]ManifestRead{}}
|
|
shelf, err := parse(c.Manifests, cv.Manifests, c.Verdicts)
|
|
if err != nil {
|
|
return CaseVerdict{}, err
|
|
}
|
|
base, err := inputOf(r)
|
|
if err != nil {
|
|
return CaseVerdict{}, err
|
|
}
|
|
held := base
|
|
base, awaited := overrule(base, c, cv.Manifests, shelf)
|
|
assignments := map[string]*meshmoduleassigner.Assignments{}
|
|
for node, raw := range c.Assignments {
|
|
if string(raw) == "null" {
|
|
assignments[node] = nil
|
|
continue
|
|
}
|
|
p, perr := meshmoduleassignerclient.DecodeAssignments(raw)
|
|
if perr != nil {
|
|
return CaseVerdict{}, fmt.Errorf("composer: the case %q: the assignment of %s is not one: %w", c.Name, node, perr)
|
|
}
|
|
assignments[node] = &p
|
|
}
|
|
in, err := frombus.Placed(base, assignments)
|
|
if err != nil {
|
|
return CaseVerdict{}, fmt.Errorf("composer: the case %q: %w", c.Name, err)
|
|
}
|
|
// The module the mesh provides itself (the private network) has no repository the indexer reads, so no case carries
|
|
// it: its manifest comes with mesh-sdk and always wins, as the bus reader has it (frombus readBase). Without it a node
|
|
// it is assigned to composed nothing (issue 1126, check of mesh-host #200: "nothing provides mesh-wireguard").
|
|
for _, name := range overlay.ProvidedModules() {
|
|
m, _, err := overlay.ProvidedManifest(name)
|
|
if err != nil {
|
|
return CaseVerdict{}, fmt.Errorf("composer: %w", err)
|
|
}
|
|
shelf[name] = m
|
|
}
|
|
// A module the input carries as its node-engine took it in from its build (SentInput) is composed so: the bus
|
|
// reader composes it from its build, never from the indexer's manifest a case gives. One the case overrules the
|
|
// node's hold on is composed at the build the node would compose it at (Input.Holds), else from the case's manifest.
|
|
for name, m := range base.Catalogue {
|
|
if _, over := awaited[name]; !over {
|
|
shelf[name] = m
|
|
}
|
|
}
|
|
for name, hold := range base.Holds {
|
|
if _, over := awaited[name]; over && hold.Built != nil {
|
|
shelf[name] = *hold.Built
|
|
}
|
|
}
|
|
// A module the input's node-engine holds as refused (compose.Input.Refused: its verdict, or bytes that do not decode) is
|
|
// held here as there, and its read says why, whatever the case's own verdicts: the check then names the cause the node
|
|
// holds it for, never a pass the node would not compose, nor a rule about its stub (issue 774, group check 278aa5f0ccbe).
|
|
for name, why := range base.Refused {
|
|
if read, carried := cv.Manifests[name]; carried && read.Accepted {
|
|
cv.Manifests[name] = ManifestRead{Refused: "its node-engine holds it: " + why}
|
|
}
|
|
}
|
|
in.Catalogue = shelf
|
|
in.Nodes = slices.Clone(in.Nodes)
|
|
in.Holdings = slices.Clone(in.Holdings)
|
|
for i := range in.Nodes {
|
|
if len(c.Kept[in.Nodes[i].Name]) == 0 {
|
|
continue // what the input keeps of this node, as its engine took it in, stands
|
|
}
|
|
in.Nodes[i].Kept = nil
|
|
if kept := c.Kept[in.Nodes[i].Name]; len(kept) > 0 {
|
|
k, err := parse(kept, nil, nil)
|
|
if err != nil {
|
|
return CaseVerdict{}, err
|
|
}
|
|
in.Nodes[i].Kept = k
|
|
}
|
|
}
|
|
if r.HoldingsFromClaims {
|
|
in.Holdings = append(in.Holdings, HoldingsOf(shelf, in.Nodes)...)
|
|
}
|
|
if len(r.Machines) == 0 {
|
|
return cv, nil
|
|
}
|
|
cv.Machines = map[string]Machine{}
|
|
for _, node := range r.Machines {
|
|
m := machine(in, node, depth, r.Declarations)
|
|
if words := awaitedOn(in, node, awaited); words != "" {
|
|
// Composed without the build it runs at: named per node and module, never a silent pass (issue 1921). The
|
|
// machine is then not judged, as for a node whose running state its engine does not send (ADR 0593 §9). Where
|
|
// it does not compose without that build (a tool runner's bundle), it is composed with the module held as its
|
|
// node-engine holds it, and named so; a node that refuses then too is the change's refusal.
|
|
how := "composed from the change's manifest without its build: "
|
|
if !m.Composes && m.NotJudged == "" {
|
|
if again := machine(heldAgain(in, held, awaited), node, depth, r.Declarations); again.Composes {
|
|
m, how = again, "composed with it held as its node-engine holds it, since it does not compose without its build: "
|
|
}
|
|
}
|
|
words = how + words
|
|
if m.Composes {
|
|
m = Machine{NotJudged: words, Declared: m.Declared}
|
|
} else if m.NotJudged == "" {
|
|
m.Reason += "; " + words
|
|
}
|
|
}
|
|
cv.Machines[node] = m
|
|
}
|
|
return cv, nil
|
|
}
|
|
|
|
// overrule lifts the hold of the input's node-engine on each module whose manifest the case carries and judges itself
|
|
// (novox/hq ADR 0578 §2): a module the input holds (Refused: the running indexer's verdict, or the running node-engine's
|
|
// decoder), whose bytes this composer reads whole, and that the case gives an accepted verdict or none (a reader takes a
|
|
// manifest with no verdict as accepted, ADR 0578 §3; the running checker drops the verdicts of the change's manifests,
|
|
// issue 1921). It is then no longer Refused nor held in how the node composes. A case's own refusal and bytes this
|
|
// composer does not decode stay held. awaited is each module lifted with no build the input carries: the words for the verdict, or
|
|
// "" where it is composed at its build.
|
|
func overrule(base compose.Input, c Case, reads map[string]ManifestRead, shelf map[string]catalogue.Manifest) (compose.Input, map[string]string) {
|
|
awaited := map[string]string{}
|
|
for name, why := range base.Refused {
|
|
hold := base.Holds[name]
|
|
if _, carried := c.Manifests[name]; !carried || !reads[name].Accepted || shelf[name].ReadNoFurther() {
|
|
continue
|
|
}
|
|
held := "held by the running node-engine"
|
|
switch hold.By {
|
|
case compose.HeldByVerdict:
|
|
held = "refused only by the running indexer"
|
|
case compose.HeldByDecoder:
|
|
held = "not read by the running node-engine's decoder"
|
|
}
|
|
judged := "accepted by the check's own validator"
|
|
if _, given := c.Verdicts[name]; !given {
|
|
judged = "given no verdict by the check, so taken as accepted (novox/hq ADR 0578 §3)"
|
|
}
|
|
awaited[name] = ""
|
|
if hold.Built == nil && compose.NamesBuild(shelf[name]) {
|
|
build := "its build"
|
|
if hold.Build != "" {
|
|
build += " " + hold.Build
|
|
}
|
|
awaited[name] = fmt.Sprintf("%s is %s (%s) and %s, and %s is awaited", name, held, why, judged, build)
|
|
}
|
|
}
|
|
if len(awaited) == 0 {
|
|
return base, awaited
|
|
}
|
|
base.Refused = maps.Clone(base.Refused)
|
|
for name := range awaited {
|
|
delete(base.Refused, name)
|
|
}
|
|
if how := base.ComposedHow; how != nil {
|
|
copied := *how
|
|
copied.Held = maps.Clone(how.Held)
|
|
for name := range awaited {
|
|
delete(copied.Held, name)
|
|
}
|
|
base.ComposedHow = &copied
|
|
}
|
|
return base, awaited
|
|
}
|
|
|
|
// heldAgain is in with each module overrule lifted and gave no build held again as held, the input before overrule,
|
|
// holds it: its stub, Refused and how the node composes.
|
|
func heldAgain(in, held compose.Input, awaited map[string]string) compose.Input {
|
|
in.Catalogue = maps.Clone(in.Catalogue)
|
|
in.Refused = maps.Clone(in.Refused)
|
|
if in.Refused == nil {
|
|
in.Refused = map[string]string{}
|
|
}
|
|
if how := in.ComposedHow; how != nil {
|
|
copied := *how
|
|
copied.Held = maps.Clone(how.Held)
|
|
in.ComposedHow = &copied
|
|
}
|
|
for name, words := range awaited {
|
|
if words == "" {
|
|
continue
|
|
}
|
|
if stub, ok := held.Catalogue[name]; ok {
|
|
in.Catalogue[name] = stub
|
|
} else {
|
|
in.Catalogue[name] = compose.Stub(in.Catalogue[name])
|
|
}
|
|
in.Refused[name] = held.Refused[name]
|
|
if in.ComposedHow != nil && held.ComposedHow != nil {
|
|
if why, was := held.ComposedHow.Held[name]; was {
|
|
if in.ComposedHow.Held == nil {
|
|
in.ComposedHow.Held = map[string]string{}
|
|
}
|
|
in.ComposedHow.Held[name] = why
|
|
}
|
|
}
|
|
}
|
|
return in
|
|
}
|
|
|
|
// awaitedOn is the words of each module placed on node that is composed without its build (overrule), joined, sorted.
|
|
func awaitedOn(in compose.Input, node string, awaited map[string]string) string {
|
|
var out []string
|
|
for _, n := range in.Nodes {
|
|
if n.Name != node {
|
|
continue
|
|
}
|
|
for _, m := range n.Assigned {
|
|
if words := awaited[m]; words != "" {
|
|
out = append(out, words)
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return strings.Join(out, "; ")
|
|
}
|
|
|
|
// HoldingsOf is every seat an assigned module claims, held on the node it is assigned to, in node order: the holdings
|
|
// of an input that has none of record (the assigner's, mesh-delivery-assigner internal/assign input).
|
|
func HoldingsOf(shelf map[string]catalogue.Manifest, nodes []compose.Node) []catalogue.Held {
|
|
var out []catalogue.Held
|
|
sorted := slices.Clone(nodes)
|
|
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Name < sorted[j].Name })
|
|
for _, n := range sorted {
|
|
for _, mod := range n.Assigned {
|
|
for _, cl := range shelf[mod].Claims {
|
|
scope := cl.Scope
|
|
if seat, ok := catalogue.SeatNamed(cl.Name); ok && scope == "" {
|
|
scope = seat.Scope
|
|
}
|
|
out = append(out, catalogue.Held{Claim: cl.Name, Scope: scope, Node: n.Name, Module: mod})
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func machine(in compose.Input, node, depth string, declarations bool) Machine {
|
|
if depth == DepthResolve {
|
|
if _, err := compose.Resolve(in, node); err != nil {
|
|
return Machine{Reason: err.Error()}
|
|
}
|
|
return Machine{Composes: true}
|
|
}
|
|
// Composed as the node-engine composes it, when its input says how (compose.Input.ComposedHow, issue 774): the same
|
|
// path, at the same builds, a module folded in its own composition kept as it runs.
|
|
if !declarations {
|
|
if _, how, err := frombus.ComposeSent(in, node); how {
|
|
if err != nil {
|
|
return Machine{Reason: err.Error()}
|
|
}
|
|
return Machine{Composes: true}
|
|
}
|
|
}
|
|
d, problems, err := compose.Compose(in, node)
|
|
if err != nil {
|
|
// An input from a node-engine before ComposedHow says nothing of what runs of the modules folded in the node's
|
|
// own composition: the sandbox cannot compose them as the node does, so it does not judge the node, and names
|
|
// it (ADR 0593 §9), never a failure of the change and never a silent pass.
|
|
if folded := foldedUnsaid(in, node); len(folded) > 0 && in.ComposedHow == nil {
|
|
return Machine{NotJudged: NotJudged + strings.Join(folded, ", ") +
|
|
" (its own composition carries them as they run, and the sandbox cannot): " + err.Error()}
|
|
}
|
|
return Machine{Reason: err.Error()}
|
|
}
|
|
m := Machine{Composes: len(problems) == 0, Problems: problems, Refusals: d.Refusals()}
|
|
if len(problems) > 0 {
|
|
m.Reason = "the node-engine's validator refuses the declaration"
|
|
}
|
|
if declarations {
|
|
m.Declared = pathsOf(d)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// NotJudged opens the words of a machine the sandbox does not judge (Machine.NotJudged, the contract's field, ADR 0593 §9):
|
|
// its node-engine sent an input that does not say how it composes (compose.Input.ComposedHow), and the node runs modules
|
|
// folded in its own composition. The machine is then neither composed nor refused; a reader decides by the field.
|
|
const NotJudged = "not judged: its node-engine sends no running state for "
|
|
|
|
// foldedUnsaid is each module node runs folded in its own composition (Node.InOwn) whose manifest names a build: what
|
|
// the node composes as it runs, and an input without ComposedHow cannot.
|
|
func foldedUnsaid(in compose.Input, node string) []string {
|
|
var out []string
|
|
for _, n := range in.Nodes {
|
|
if n.Name != node {
|
|
continue
|
|
}
|
|
for m, ok := range n.InOwn {
|
|
man, known := n.Kept[m]
|
|
if !known {
|
|
man, known = in.Catalogue[m]
|
|
}
|
|
if ok && known && compose.NamesBuild(man) {
|
|
out = append(out, m)
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// pathsOf is every resource of a declaration that names a path, with its owner, group and mode as declared, sorted by
|
|
// path.
|
|
func pathsOf(d compose.Declared) []Path {
|
|
var out []Path
|
|
str := func(r map[string]any, key string) string {
|
|
if v, ok := r[key]; ok && v != nil {
|
|
return fmt.Sprint(v)
|
|
}
|
|
return ""
|
|
}
|
|
for _, r := range d.Resources {
|
|
p := str(r, "path")
|
|
if p == "" {
|
|
continue
|
|
}
|
|
out = append(out, Path{Path: p, Kind: str(r, "type"), Owner: str(r, "owner"), Group: str(r, "group"),
|
|
Mode: str(r, "mode"), Module: d.ModuleOf[str(r, "id")]})
|
|
}
|
|
sort.Slice(out, func(i, j int) bool {
|
|
if out[i].Path != out[j].Path {
|
|
return out[i].Path < out[j].Path
|
|
}
|
|
return out[i].Kind < out[j].Kind
|
|
})
|
|
return out
|
|
}
|
|
|
|
// Package is the import path of the command, which a build module builds at the node-engine's repository's head of a
|
|
// group that changes the node-engine's composition (novox/hq ADR 0593 §7): mesh-sdk's composer.Package names the same.
|
|
const Package = "github.com/novox/mesh-host/cmd/mesh-compose"
|
|
|
|
// Decode reads a request (sdk.Decode); it refuses one that is not JSON or of another version.
|
|
func Decode(data []byte) (Request, error) { return sdk.Decode(data) }
|
|
|
|
// SentInput is the composition input a node-engine answers for the sandbox (nodeengine.ComposeCandidateInput.Input), from the
|
|
// input it composed node from: the one shape both read (issue 1126, checks of mesh-host #200). Every other node's modules
|
|
// are left to the cases (the indexer's manifests, as the bus reader reads them too); node's own placed modules stay as the
|
|
// bus reader took them in from their builds (their artifacts, the texts of their templates archive, what is handed to the
|
|
// provisioner), with what node keeps at another build, which no case can give: the sandbox has no build or archive to
|
|
// read. Without them the sandbox composed another node than the engine (restic's templates and artifacts unread, postgres's
|
|
// secret reader handed and the module not).
|
|
func SentInput(in compose.Input, node string) compose.Input {
|
|
own := map[string]bool{}
|
|
for _, n := range in.Nodes {
|
|
if n.Name == node {
|
|
for _, m := range n.Assigned {
|
|
own[m] = true
|
|
}
|
|
}
|
|
}
|
|
taken := in.Catalogue
|
|
in.Catalogue = nil
|
|
for name, m := range taken {
|
|
if own[name] {
|
|
if in.Catalogue == nil {
|
|
in.Catalogue = map[string]catalogue.Manifest{}
|
|
}
|
|
in.Catalogue[name] = m
|
|
}
|
|
}
|
|
in.Nodes = slices.Clone(in.Nodes)
|
|
for i := range in.Nodes {
|
|
if in.Nodes[i].Name != node {
|
|
in.Nodes[i].Kept = nil
|
|
}
|
|
}
|
|
return in
|
|
}
|