Carry an adopted node's mode and taken modules in every declaration, from one marshaller (hq ADR 0100)

This commit is contained in:
2026-09-22 17:17:54 +02:00
parent 1c32af6a22
commit a86a6c2974
6 changed files with 357 additions and 49 deletions
+96
View File
@@ -0,0 +1,96 @@
package main
import (
"context"
"encoding/json"
"sort"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
)
// sendable is a declaration as a machine is sent it: its resources and, for an adopted node, its
// mode and which modules were taken on it (novox/hq ADR 0100).
//
// **Body is the only place the envelope is marshalled.** It was written by hand at every send site,
// in the digest the mesh compares, and in `plan --json`; a key added at one and not another would
// make a machine look out of date for ever, or send something `plan` never showed.
type sendable struct {
Resources []map[string]any
// Adoption is nil for a converged node, and then the body is byte for byte what it was before
// adoption existed: an older host parses the envelope strictly and would refuse the key.
Adoption *adoptionEnvelope
}
// adoptionEnvelope is what an adopted node is told about its mode. Taken is every module taken on
// it that it runs; Untaken is, for every module it runs that is not taken, the ids of that module's
// files and containers — the resources the host keeps as found until the module is taken. Ids
// rather than a rule to split them by, because a module's name may contain a dot.
type adoptionEnvelope struct {
Taken []string `json:"taken"`
Untaken map[string][]string `json:"untaken,omitempty"`
}
// Body is the declaration's bytes, as sent and as digested.
func (s sendable) Body() ([]byte, error) {
envelope := map[string]any{"declaration": 1, "resources": s.Resources}
if s.Adoption != nil {
envelope["adoption"] = s.Adoption
}
return json.Marshal(envelope)
}
// adoptionOf is the envelope for a node, nil when it is converged.
func adoptionOf(ctx context.Context, inv *inventory.Inventory, node string,
plan catalogue.Resolution, composed catalogue.Composed) (*adoptionEnvelope, error) {
record, err := inv.NodeByName(ctx, node)
if err != nil {
return nil, err
}
if !record.Adopted {
return nil, nil
}
taken, err := inv.Taken(ctx, node)
if err != nil {
return nil, err
}
return adoptionFor(plan, taken, composed), nil
}
// adoptionFor is the envelope computed from what was taken and who owns each resource.
//
// Every module the node runs that is not taken is untaken — including one pulled in by another
// rather than assigned: what is found is kept until its module is taken, whoever put it there.
func adoptionFor(plan catalogue.Resolution, taken []string,
composed catalogue.Composed) *adoptionEnvelope {
isTaken := map[string]bool{}
for _, m := range taken {
isTaken[m] = true
}
out := &adoptionEnvelope{Taken: []string{}}
runs := map[string]bool{}
for _, m := range plan.Modules {
runs[m.Module] = true
if isTaken[m.Module] {
out.Taken = append(out.Taken, m.Module)
}
}
sort.Strings(out.Taken)
for _, r := range composed.Resources {
id, _ := r["id"].(string)
module, owned := composed.Owner[id]
if !owned || !runs[module] || isTaken[module] {
continue
}
switch r["type"] {
case "file", "container":
default:
continue
}
if out.Untaken == nil {
out.Untaken = map[string][]string{}
}
out.Untaken[module] = append(out.Untaken[module], id)
}
return out
}