Merge pull request 'A take is a comparison: the preview, its refusals, the strays, and the policy said at build (hq ADR 0163)' (#201) from feat/a-take-is-a-comparison into main
This commit was merged in pull request #201.
This commit is contained in:
@@ -109,10 +109,10 @@ var (
|
||||
|
||||
func TestTakingAModuleNotOnTheNodeIsRefused(t *testing.T) {
|
||||
open, _ := anAdoptedAnchor(t)
|
||||
if _, err := take(t.Context(), open, "anchor", "nftables"); !errors.Is(err, inventory.ErrNotAssigned) {
|
||||
if _, err := take(t.Context(), open, "anchor", "nftables", takeOptions{Yes: true}); !errors.Is(err, inventory.ErrNotAssigned) {
|
||||
t.Fatalf("taking an unassigned module gave %v", err)
|
||||
}
|
||||
if _, err := take(t.Context(), open, "laptop", "network"); !errors.Is(err, inventory.ErrNotAdopted) {
|
||||
if _, err := take(t.Context(), open, "laptop", "network", takeOptions{Yes: true}); !errors.Is(err, inventory.ErrNotAdopted) {
|
||||
t.Fatalf("taking on a converged node gave %v", err)
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@ func TestTheFlipIsRefusedWhileAFoundContainerIsHeld(t *testing.T) {
|
||||
func TestTakingNamesWhatItReplaces(t *testing.T) {
|
||||
open, _ := anAdoptedAnchor(t)
|
||||
reportsHolding(t, open, heldContainer, heldFile)
|
||||
said, err := take(t.Context(), open, "anchor", "hello-web")
|
||||
said, err := take(t.Context(), open, "anchor", "hello-web", takeOptions{Yes: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func TestTakingNamesWhatItReplaces(t *testing.T) {
|
||||
func TestConvergingPreviewsThenChangesAndAdoptingKeepsWhatWasTaken(t *testing.T) {
|
||||
open, sent := anAdoptedAnchor(t)
|
||||
ctx := t.Context()
|
||||
if _, err := take(ctx, open, "anchor", "hello-web"); err != nil {
|
||||
if _, err := take(ctx, open, "anchor", "hello-web", takeOptions{Yes: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reportsHolding(t, open, heldFile)
|
||||
@@ -330,7 +330,7 @@ func digestIn(t *testing.T, preview string) string {
|
||||
func TestTheFlipActsOnlyOnThePreviewTheOperatorSaw(t *testing.T) {
|
||||
open, sent := anAdoptedAnchor(t)
|
||||
ctx := t.Context()
|
||||
if _, err := take(ctx, open, "anchor", "hello-web"); err != nil {
|
||||
if _, err := take(ctx, open, "anchor", "hello-web", takeOptions{Yes: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reportsHolding(t, open, heldFile)
|
||||
@@ -396,7 +396,7 @@ func TestTheFlipActsOnlyOnThePreviewTheOperatorSaw(t *testing.T) {
|
||||
func TestTheFlipHoldsTheNodeWhileItSends(t *testing.T) {
|
||||
open, _ := anAdoptedAnchor(t)
|
||||
ctx := t.Context()
|
||||
if _, err := take(ctx, open, "anchor", "hello-web"); err != nil {
|
||||
if _, err := take(ctx, open, "anchor", "hello-web", takeOptions{Yes: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reportsHolding(t, open, heldFile)
|
||||
@@ -441,7 +441,7 @@ func TestTheFlipHoldsTheNodeWhileItSends(t *testing.T) {
|
||||
func TestThePreviewNamesEveryHeldKind(t *testing.T) {
|
||||
open, _ := anAdoptedAnchor(t)
|
||||
ctx := t.Context()
|
||||
if _, err := take(ctx, open, "anchor", "hello-web"); err != nil {
|
||||
if _, err := take(ctx, open, "anchor", "hello-web", takeOptions{Yes: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
since := time.Now()
|
||||
@@ -484,7 +484,7 @@ func TestThePreviewNamesEveryHeldKind(t *testing.T) {
|
||||
func TestTheFlipIsRefusedOnAnAccountNamingNothingReachable(t *testing.T) {
|
||||
open, sent := anAdoptedAnchor(t)
|
||||
ctx := t.Context()
|
||||
if _, err := take(ctx, open, "anchor", "hello-web"); err != nil {
|
||||
if _, err := take(ctx, open, "anchor", "hello-web", takeOptions{Yes: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Only a loopback listener: nothing off the machine, which is the same silence.
|
||||
@@ -512,7 +512,7 @@ func TestTheFlipIsRefusedOnAnAccountNamingNothingReachable(t *testing.T) {
|
||||
func TestAssigningWaitsForWhateverIsConvergingTheNode(t *testing.T) {
|
||||
open, _ := anAdoptedAnchor(t)
|
||||
ctx := t.Context()
|
||||
if _, err := take(ctx, open, "anchor", "hello-web"); err != nil {
|
||||
if _, err := take(ctx, open, "anchor", "hello-web", takeOptions{Yes: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reportsHolding(t, open, heldFile)
|
||||
|
||||
+194
-16
@@ -62,6 +62,15 @@ func showMode(ctx context.Context, inv *inventory.Inventory, node inventory.Node
|
||||
if h.Kept != "" {
|
||||
fmt.Printf(" %-17s original kept at %s\n", "", h.Kept)
|
||||
}
|
||||
for _, f := range comparisonLines(h) {
|
||||
fmt.Printf(" %-17s %s\n", "", f)
|
||||
}
|
||||
}
|
||||
if len(said.Strays) > 0 {
|
||||
fmt.Printf(" strays %d container(s) the mesh neither wrote nor holds:\n", len(said.Strays))
|
||||
for _, s := range said.Strays {
|
||||
fmt.Printf(" %-17s %s (%s)\n", "", s.Name, s.Detail)
|
||||
}
|
||||
}
|
||||
fmt.Printf(" as of %s\n", said.At.Local().Format(time.DateTime))
|
||||
return nil
|
||||
@@ -136,7 +145,14 @@ const DefaultFilter = "nftables"
|
||||
// take is a module's cutover on an adopted node: the operator's act, done when that module's data
|
||||
// has moved. From the next push its resources converge there like any other, replacing what the
|
||||
// node found and holds for it.
|
||||
func take(ctx context.Context, open *stores, node, module string) (string, error) {
|
||||
// takeOptions is what a take was told about the differences it may pass (novox/hq ADR 0163).
|
||||
type takeOptions struct {
|
||||
Yes bool
|
||||
Downgrade bool
|
||||
Replace map[string]bool
|
||||
}
|
||||
|
||||
func take(ctx context.Context, open *stores, node, module string, opts takeOptions) (string, error) {
|
||||
inv := open.inventory
|
||||
assigned, err := inv.Assigned(ctx, node)
|
||||
if err != nil {
|
||||
@@ -150,27 +166,170 @@ func take(ctx context.Context, open *stores, node, module string) (string, error
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := inv.Take(ctx, node, module); err != nil {
|
||||
return "", err
|
||||
}
|
||||
said := fmt.Sprintf("%s is taken on %s", module, node)
|
||||
// The comparison first (novox/hq ADR 0163): every held thing the module would replace, beside
|
||||
// what the module declares, and the differences that refuse unless named.
|
||||
reported, err := inv.AdoptionOf(ctx, node)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var replaces []string
|
||||
for _, h := range reported.Held {
|
||||
if h.Module == module {
|
||||
replaces = append(replaces, " "+heldLine(h))
|
||||
}
|
||||
preview, refusals := comparisonOf(reported.Held, module, opts)
|
||||
if len(refusals) > 0 {
|
||||
return "", fmt.Errorf("taking %s on %s is refused:\n %s\n%s", module, node,
|
||||
strings.Join(refusals, "\n "), preview)
|
||||
}
|
||||
if len(replaces) > 0 {
|
||||
said += "; the next push replaces what the node found and holds for it:\n" +
|
||||
strings.Join(replaces, "\n")
|
||||
if !opts.Yes {
|
||||
return preview + fmt.Sprintf("\nnothing taken; `take %s %s --yes` cuts it over as previewed", node, module), nil
|
||||
}
|
||||
if err := inv.Take(ctx, node, module); err != nil {
|
||||
return "", err
|
||||
}
|
||||
said := fmt.Sprintf("%s is taken on %s", module, node)
|
||||
if preview != "" {
|
||||
said += "; the next push replaces what the node found and holds for it:\n" + preview
|
||||
}
|
||||
return said + fmt.Sprintf("\n run `push %s` to cut it over", node), nil
|
||||
}
|
||||
|
||||
// comparisonOf is a take's preview: for every held thing of the module, what runs beside what the
|
||||
// module declares, and the refusals the differences earn unless the take named them
|
||||
// (novox/hq ADR 0163): an image older than the one running, a declared file that differs from the
|
||||
// found one. A narrowed port and a shared network are said and not refused.
|
||||
func comparisonOf(held []inventory.Held, module string, opts takeOptions) (string, []string) {
|
||||
var b strings.Builder
|
||||
var refusals []string
|
||||
for _, h := range held {
|
||||
if h.Module != module {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&b, " %s", heldLine(h))
|
||||
if h.Kept != "" {
|
||||
fmt.Fprintf(&b, ", original kept at %s", h.Kept)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
for _, line := range comparisonLines(h) {
|
||||
fmt.Fprintf(&b, " %s\n", line)
|
||||
}
|
||||
f := factsOf(h)
|
||||
if f.downgrade && !opts.Downgrade {
|
||||
refusals = append(refusals, fmt.Sprintf("%s: the module's image (%s, made %s) is older than the one running (%s, made %s) — "+
|
||||
"a service that migrated its data forward may not start on it; `--downgrade` to take it anyway",
|
||||
h.Target, f.declaredImage, day(f.declaredCreated), f.image, day(f.imageCreated)))
|
||||
}
|
||||
if f.differs && !opts.Replace[h.Target] && !opts.Replace["*"] {
|
||||
refusals = append(refusals, fmt.Sprintf("%s: the module's content differs from the file found; the lines above "+
|
||||
"marked - are lost by taking it; `--replace %s` to replace it anyway, or declare the file partially",
|
||||
h.Target, h.Target))
|
||||
}
|
||||
}
|
||||
return b.String(), refusals
|
||||
}
|
||||
|
||||
// facts is a held thing's facts as the preview reads them.
|
||||
type facts struct {
|
||||
image, imageCreated, declaredImage, declaredCreated string
|
||||
downgrade, differs bool
|
||||
networks map[string][]string
|
||||
mounts, ports, declaredPorts, declaredVolumes []string
|
||||
difference []string
|
||||
}
|
||||
|
||||
func factsOf(h inventory.Held) facts {
|
||||
var f facts
|
||||
if h.Facts == nil {
|
||||
return f
|
||||
}
|
||||
str := func(k string) string { s, _ := h.Facts[k].(string); return s }
|
||||
list := func(k string) []string {
|
||||
var out []string
|
||||
if raw, ok := h.Facts[k].([]any); ok {
|
||||
for _, x := range raw {
|
||||
if s, ok := x.(string); ok {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
f.image, f.imageCreated = str("image"), str("image_created")
|
||||
f.declaredImage, f.declaredCreated = str("declared_image"), str("declared_image_created")
|
||||
f.downgrade, _ = h.Facts["downgrade"].(bool)
|
||||
f.differs, _ = h.Facts["differs"].(bool)
|
||||
f.mounts, f.ports = list("mounts"), list("ports")
|
||||
f.declaredPorts, f.declaredVolumes, f.difference = list("declared_ports"), list("declared_volumes"), list("difference")
|
||||
if raw, ok := h.Facts["networks"].(map[string]any); ok {
|
||||
f.networks = map[string][]string{}
|
||||
for name, members := range raw {
|
||||
var out []string
|
||||
if ms, ok := members.([]any); ok {
|
||||
for _, m := range ms {
|
||||
if s, ok := m.(string); ok {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
f.networks[name] = out
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// comparisonLines says a held thing's facts the way a person weighs them.
|
||||
func comparisonLines(h inventory.Held) []string {
|
||||
f := factsOf(h)
|
||||
var out []string
|
||||
if f.image != "" || f.declaredImage != "" {
|
||||
line := fmt.Sprintf("runs %s", orNone(f.image))
|
||||
if f.imageCreated != "" {
|
||||
line += " (made " + day(f.imageCreated) + ")"
|
||||
}
|
||||
line += "; the module declares " + orNone(f.declaredImage)
|
||||
switch {
|
||||
case f.declaredCreated != "":
|
||||
line += " (made " + day(f.declaredCreated) + ")"
|
||||
case f.declaredImage != "":
|
||||
line += " (not on the machine yet, so its age is unknown)"
|
||||
}
|
||||
if f.downgrade {
|
||||
line += " — DOWNGRADE"
|
||||
}
|
||||
out = append(out, line)
|
||||
}
|
||||
names := make([]string, 0, len(f.networks))
|
||||
for n := range f.networks {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, n := range names {
|
||||
if members := f.networks[n]; len(members) > 0 {
|
||||
out = append(out, fmt.Sprintf("on the network %s with %s, which may reach it by name and will not once it moves to the module's own network",
|
||||
n, strings.Join(members, ", ")))
|
||||
}
|
||||
}
|
||||
if len(f.ports) > 0 || len(f.declaredPorts) > 0 {
|
||||
out = append(out, fmt.Sprintf("publishes %s; the module declares %s",
|
||||
orNone(strings.Join(f.ports, " ")), orNone(strings.Join(f.declaredPorts, " "))))
|
||||
}
|
||||
if len(f.mounts) > 0 || len(f.declaredVolumes) > 0 {
|
||||
out = append(out, fmt.Sprintf("mounts %s; the module declares %s",
|
||||
orNone(strings.Join(f.mounts, " ")), orNone(strings.Join(f.declaredVolumes, " "))))
|
||||
}
|
||||
if f.differs {
|
||||
out = append(out, "the declared content differs from the file found (- lost, + new):")
|
||||
for _, d := range f.difference {
|
||||
out = append(out, " "+d)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// day is a timestamp as a person reads it in a preview: its date.
|
||||
func day(stamp string) string {
|
||||
if len(stamp) >= 10 {
|
||||
return stamp[:10]
|
||||
}
|
||||
return stamp
|
||||
}
|
||||
|
||||
// reportFreshFor is how old a node's account of itself may be for the flip to act on it. A
|
||||
// variable so a test can age a report without waiting.
|
||||
var reportFreshFor = 15 * time.Minute
|
||||
@@ -596,12 +755,31 @@ func adopt(ctx context.Context, open *stores, node string) (string, error) {
|
||||
|
||||
// takeCommand, convergeCommand and adoptCommand are the command line's adapters to the acts above.
|
||||
func takeCommand(ctx context.Context, args []string) error {
|
||||
if len(args) != 2 {
|
||||
return errors.New("take <node> <module>")
|
||||
set := flag.NewFlagSet("take", flag.ContinueOnError)
|
||||
yes := set.Bool("yes", false, "cut over as previewed; without it the comparison is printed and nothing is taken")
|
||||
downgrade := set.Bool("downgrade", false, "take it although the module's image is older than the one running")
|
||||
var replace stringList
|
||||
set.Var(&replace, "replace", "a found file's path whose content the module may replace although it differs (repeatable; * for every one)")
|
||||
positionals, err := parseAround(set, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runAct(ctx, func(open *stores) (string, error) { return take(ctx, open, args[0], args[1]) })
|
||||
if len(positionals) != 2 {
|
||||
return errors.New("take <node> <module> [--yes] [--downgrade] [--replace <path>]...")
|
||||
}
|
||||
opts := takeOptions{Yes: *yes, Downgrade: *downgrade, Replace: map[string]bool{}}
|
||||
for _, r := range replace {
|
||||
opts.Replace[r] = true
|
||||
}
|
||||
return runAct(ctx, func(open *stores) (string, error) { return take(ctx, open, positionals[0], positionals[1], opts) })
|
||||
}
|
||||
|
||||
// stringList is a repeatable flag.
|
||||
type stringList []string
|
||||
|
||||
func (l *stringList) String() string { return strings.Join(*l, ",") }
|
||||
func (l *stringList) Set(v string) error { *l = append(*l, v); return nil }
|
||||
|
||||
func convergeCommand(ctx context.Context, args []string) error {
|
||||
set := flag.NewFlagSet("converge", flag.ContinueOnError)
|
||||
yes := set.String("yes", "", "do it, naming the digest the preview printed; without it, only "+
|
||||
|
||||
@@ -102,7 +102,7 @@ func commands(who Authenticator) http.Handler {
|
||||
}))
|
||||
// Adoption (novox/hq ADR 0100): the same acts as `take`, `converge` and `adopt`.
|
||||
mux.HandleFunc("POST /take", acting(who, true, func(ctx context.Context, open *stores, in request) (string, error) {
|
||||
return take(ctx, open, in.Node, in.Module)
|
||||
return take(ctx, open, in.Node, in.Module, takeOptions{Yes: true})
|
||||
}))
|
||||
mux.HandleFunc("POST /converge", acting(who, false, func(ctx context.Context, open *stores, in request) (string, error) {
|
||||
return converge(ctx, open, in.Node, in.Yes, in.Digest, in.Filter)
|
||||
|
||||
@@ -469,10 +469,25 @@ func buildOne(ctx context.Context, source buildSource, path, ref string, wait ti
|
||||
}
|
||||
fmt.Printf("\n%s %s, built on %s from %s\n",
|
||||
manifest.Module, manifest.Version, result.On, short(result.Commit))
|
||||
saysWhenThePolicyActs(ctx, open.inventory, manifest.Module)
|
||||
fmt.Printf(" run `assign <node> %s` to put it somewhere\n", manifest.Module)
|
||||
return nil
|
||||
}
|
||||
|
||||
// saysWhenThePolicyActs tells whoever built a module that its upgrade policy will send the
|
||||
// result on at once (novox/hq issue 126, ADR 0163): a person choreographing a data move must
|
||||
// know which module will not wait for them.
|
||||
func saysWhenThePolicyActs(ctx context.Context, inv *inventory.Inventory, module string) {
|
||||
if u, err := inv.UpgradeOf(ctx, module); err == nil && u.RollOut {
|
||||
how := "one machine at a time"
|
||||
if u.Together {
|
||||
how = "every machine at once"
|
||||
}
|
||||
fmt.Printf(" %s rolls out on build: the machines running it are sent this now, %s — "+
|
||||
"`upgrade %s record` first if something must move before it does\n", module, how, module)
|
||||
}
|
||||
}
|
||||
|
||||
// takeIn is what the mesh does with a build's outcome, whoever hears it: the waiting command and
|
||||
// the daemon that follows the role's events both come here (novox/hq issue 176), so a build's
|
||||
// result reaches the catalogue whether or not the asker was still listening.
|
||||
|
||||
@@ -270,6 +270,7 @@ func (b builds) Built(ctx context.Context, result link.BuildResult) error {
|
||||
}
|
||||
fmt.Printf("%s: %s %s registered, built on %s from %s\n",
|
||||
result.ID, manifest.Module, manifest.Version, result.On, short(result.Commit))
|
||||
saysWhenThePolicyActs(ctx, b.inv, manifest.Module)
|
||||
planBuilt(ctx, b.inv, manifest.Module, result.Commit, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
// A take is a comparison (novox/hq ADR 0163): the preview puts what runs beside what the module
|
||||
// declares, and an older image or a differing file refuses unless named.
|
||||
func TestATakePreviewsTheComparisonAndRefusesWhatIsNotNamed(t *testing.T) {
|
||||
held := []inventory.Held{
|
||||
{ID: "forge.server", Module: "forge", Kind: "container", Target: "forge", Facts: map[string]any{
|
||||
"image": "forge:1.27.3", "image_created": "2026-09-17T10:00:00Z",
|
||||
"declared_image": "forge:1.22.6", "declared_image_created": "2026-08-20T10:00:00Z", "downgrade": true,
|
||||
"networks": map[string]any{"predecessor_default": []any{"office", "db"}},
|
||||
"ports": []any{"3000/tcp>0.0.0.0:3000"}, "declared_ports": []any{"3000:3000"},
|
||||
}},
|
||||
{ID: "forge.config", Module: "forge", Kind: "file", Target: "/etc/forge/app.ini", Kept: "/var/lib/mesh/kept/app.ini",
|
||||
Facts: map[string]any{"differs": true, "difference": []any{"- private scope: local", "+ upstream: public"}}},
|
||||
{ID: "other.server", Module: "other", Kind: "container", Target: "other"},
|
||||
}
|
||||
preview, refusals := comparisonOf(held, "forge", takeOptions{})
|
||||
for _, want := range []string{"runs forge:1.27.3 (made 2026-09-17)", "declares forge:1.22.6 (made 2026-08-20)", "DOWNGRADE",
|
||||
"on the network predecessor_default with office, db", "publishes 3000/tcp>0.0.0.0:3000; the module declares 3000:3000",
|
||||
"- private scope: local", "original kept at /var/lib/mesh/kept/app.ini"} {
|
||||
if !strings.Contains(preview, want) {
|
||||
t.Errorf("the preview lacks %q:\n%s", want, preview)
|
||||
}
|
||||
}
|
||||
if strings.Contains(preview, "other") {
|
||||
t.Errorf("another module's held things are in the preview:\n%s", preview)
|
||||
}
|
||||
if len(refusals) != 2 || !strings.Contains(refusals[0], "--downgrade") || !strings.Contains(refusals[1], "--replace /etc/forge/app.ini") {
|
||||
t.Fatalf("the downgrade and the differing file refuse, each naming its override: %v", refusals)
|
||||
}
|
||||
// Named, they pass.
|
||||
if _, refusals := comparisonOf(held, "forge", takeOptions{Downgrade: true, Replace: map[string]bool{"/etc/forge/app.ini": true}}); len(refusals) != 0 {
|
||||
t.Fatalf("named differences still refused: %v", refusals)
|
||||
}
|
||||
if _, refusals := comparisonOf(held, "forge", takeOptions{Downgrade: true, Replace: map[string]bool{"*": true}}); len(refusals) != 0 {
|
||||
t.Fatalf("replace * did not cover the file: %v", refusals)
|
||||
}
|
||||
// A held thing with no facts yet — a host older than this — refuses nothing and says what it can.
|
||||
if preview, refusals := comparisonOf(held, "other", takeOptions{}); len(refusals) != 0 || !strings.Contains(preview, "container other") {
|
||||
t.Fatalf("a factless hold: %q %v", preview, refusals)
|
||||
}
|
||||
}
|
||||
@@ -164,6 +164,18 @@ type Held struct {
|
||||
Since time.Time `json:"since"`
|
||||
Changed string `json:"changed,omitempty"`
|
||||
Kept string `json:"kept,omitempty"`
|
||||
// Facts is what a take compares (novox/hq ADR 0163), as the host reported it: for a found
|
||||
// container its image and the image's date, the networks and their other members, mounts and
|
||||
// ports, beside the declared image, ports and volumes, and whether the declared image is the
|
||||
// older; for a found file whether the declared content differs and how.
|
||||
Facts map[string]any `json:"facts,omitempty"`
|
||||
}
|
||||
|
||||
// A Stray is a container a machine runs that the mesh neither wrote nor holds (ADR 0163).
|
||||
type Stray struct {
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// Reach is one thing reachable on an adopted node: a listening socket or a published port.
|
||||
@@ -181,6 +193,8 @@ type Adoption struct {
|
||||
Held []Held
|
||||
Firewall string
|
||||
Reachable []Reach
|
||||
// Strays is what the machine runs that nobody asked for, as last reported (ADR 0163).
|
||||
Strays []Stray
|
||||
// At is when it said so; zero when it never has.
|
||||
At time.Time
|
||||
}
|
||||
@@ -189,6 +203,16 @@ type Adoption struct {
|
||||
// question is the machine as it is now.
|
||||
func (i *Inventory) RecordAdoption(ctx context.Context, node string, held []Held, firewall string,
|
||||
reachable []Reach) error {
|
||||
return i.RecordAdoptionWithStrays(ctx, node, held, firewall, reachable, nil)
|
||||
}
|
||||
|
||||
// RecordAdoptionWithStrays is RecordAdoption with what the machine says strays on it (ADR 0163).
|
||||
func (i *Inventory) RecordAdoptionWithStrays(ctx context.Context, node string, held []Held, firewall string,
|
||||
reachable []Reach, strays []Stray) error {
|
||||
straysRaw, err := json.Marshal(nonNil(strays))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
heldRaw, err := json.Marshal(nonNil(held))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -198,9 +222,9 @@ func (i *Inventory) RecordAdoption(ctx context.Context, node string, held []Held
|
||||
return err
|
||||
}
|
||||
_, err = i.store.Pool().Exec(ctx,
|
||||
`update node set held = $2, firewall = nullif($3, ''), reachable = $4,
|
||||
`update node set held = $2, firewall = nullif($3, ''), reachable = $4, strays = $5,
|
||||
adoption_reported = now(), last_seen = now()
|
||||
where id = $1`, node, heldRaw, firewall, reachRaw)
|
||||
where id = $1`, node, heldRaw, firewall, reachRaw, straysRaw)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -213,12 +237,12 @@ func nonNil[T any](s []T) []T {
|
||||
|
||||
// AdoptionOf is what a node last reported about adoption.
|
||||
func (i *Inventory) AdoptionOf(ctx context.Context, name string) (Adoption, error) {
|
||||
var heldRaw, reachRaw []byte
|
||||
var heldRaw, reachRaw, straysRaw []byte
|
||||
var firewall *string
|
||||
var at *time.Time
|
||||
err := i.store.Pool().QueryRow(ctx,
|
||||
`select held, firewall, reachable, adoption_reported from node where name = $1`, name).
|
||||
Scan(&heldRaw, &firewall, &reachRaw, &at)
|
||||
`select held, firewall, reachable, adoption_reported, strays from node where name = $1`, name).
|
||||
Scan(&heldRaw, &firewall, &reachRaw, &at, &straysRaw)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return Adoption{}, fmt.Errorf("%w: %s", ErrNoSuchNode, name)
|
||||
}
|
||||
@@ -237,6 +261,11 @@ func (i *Inventory) AdoptionOf(ctx context.Context, name string) (Adoption, erro
|
||||
return Adoption{}, err
|
||||
}
|
||||
}
|
||||
if len(straysRaw) > 0 {
|
||||
if err := json.Unmarshal(straysRaw, &out.Strays); err != nil {
|
||||
return Adoption{}, err
|
||||
}
|
||||
}
|
||||
if len(reachRaw) > 0 {
|
||||
if err := json.Unmarshal(reachRaw, &out.Reachable); err != nil {
|
||||
return Adoption{}, err
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- What runs on a machine that the mesh neither wrote nor holds, as the host reports it with every
|
||||
-- apply (novox/hq ADR 0163): a container left behind by a cutover is seen the day it is left.
|
||||
alter table node add column strays jsonb;
|
||||
@@ -398,7 +398,7 @@ func (i *Inventory) AcceptSecretForModule(ctx context.Context, node, module, nam
|
||||
return err
|
||||
}
|
||||
if _, own := m.OwnSecrets[name]; !own {
|
||||
return fmt.Errorf("%s does not declare %q as an own secret; %s", module, name, declaresOwn(m))
|
||||
return fmt.Errorf("%s does not declare %q as an own secret; %s — a secret it requires from a provider is accepted with `--provider <node> [--local <name>]`, the value the running service already uses (novox/hq ADR 0163)", module, name, declaresOwn(m))
|
||||
}
|
||||
key, err := i.SealingKeyOf(ctx, node)
|
||||
if err != nil {
|
||||
@@ -546,7 +546,7 @@ func (i *Inventory) RotateModuleSecret(ctx context.Context, node, module, name s
|
||||
}
|
||||
own, declared := m.OwnSecrets[name]
|
||||
if !declared {
|
||||
return fmt.Errorf("%s does not declare %q as an own secret; %s", module, name, declaresOwn(m))
|
||||
return fmt.Errorf("%s does not declare %q as an own secret; %s — a secret it requires from a provider is accepted with `--provider <node> [--local <name>]`, the value the running service already uses (novox/hq ADR 0163)", module, name, declaresOwn(m))
|
||||
}
|
||||
switch own.Taken {
|
||||
case catalogue.TakenAtStart:
|
||||
|
||||
@@ -286,18 +286,22 @@ func (e Enrolment) Heard(ctx context.Context, report Report) (news bool, err err
|
||||
// schedule, when what it holds changes, not only after an apply — and never cleared by a
|
||||
// report that carries none, which is every bare word that the node is there. An adopted node
|
||||
// always names its firewall, so a report from one replaces all three, emptied held included.
|
||||
if len(report.Held) > 0 || report.Firewall != "" || len(report.Reachable) > 0 {
|
||||
if len(report.Held) > 0 || report.Firewall != "" || len(report.Reachable) > 0 || len(report.Strays) > 0 {
|
||||
held := make([]inventory.Held, 0, len(report.Held))
|
||||
for _, h := range report.Held {
|
||||
held = append(held, inventory.Held{ID: h.ID, Module: h.Module, Kind: h.Kind,
|
||||
Target: h.Target, Since: h.Since, Changed: h.Changed, Kept: h.Kept})
|
||||
Target: h.Target, Since: h.Since, Changed: h.Changed, Kept: h.Kept, Facts: h.Facts})
|
||||
}
|
||||
strays := make([]inventory.Stray, 0, len(report.Strays))
|
||||
for _, s := range report.Strays {
|
||||
strays = append(strays, inventory.Stray{Kind: s.Kind, Name: s.Name, Detail: s.Detail})
|
||||
}
|
||||
reachable := make([]inventory.Reach, 0, len(report.Reachable))
|
||||
for _, r := range report.Reachable {
|
||||
reachable = append(reachable, inventory.Reach{Protocol: r.Protocol, Address: r.Address,
|
||||
Port: r.Port, By: r.By, Published: r.Published, ContainerPort: r.ContainerPort})
|
||||
}
|
||||
if err := e.Inventory.RecordAdoption(ctx, node.ID, held, report.Firewall, reachable); err != nil {
|
||||
if err := e.Inventory.RecordAdoptionWithStrays(ctx, node.ID, held, report.Firewall, reachable, strays); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +194,9 @@ type Report struct {
|
||||
// not see coming.
|
||||
Host string `json:"host,omitempty"`
|
||||
|
||||
// Strays is what runs on the machine that the mesh neither wrote nor holds (ADR 0163).
|
||||
Strays []Stray `json:"strays,omitempty"`
|
||||
|
||||
// Profile is what the machine can do, detected again by this apply (novox/hq ADR 0161): the
|
||||
// same shape enrolment sends, so a machine that gained or lost a capability — switched its
|
||||
// network manager — is known at its next push and not at its next enrolment. Absent from a host
|
||||
@@ -270,6 +273,16 @@ type Held struct {
|
||||
Changed string `json:"changed,omitempty"`
|
||||
// Kept is where a file's original was kept.
|
||||
Kept string `json:"kept,omitempty"`
|
||||
// Facts is the found thing beside what the module declares — what a take compares (novox/hq
|
||||
// ADR 0163): the host's own shape, carried as data and read by the preview.
|
||||
Facts map[string]any `json:"facts,omitempty"`
|
||||
}
|
||||
|
||||
// A Stray is a container a machine runs that the mesh neither wrote nor holds (ADR 0163).
|
||||
type Stray struct {
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// Reach is one thing reachable on the machine: a listening socket, or a published container port.
|
||||
|
||||
Reference in New Issue
Block a user