Keep what an adopted node reports holding, the firewall it found and what is reachable on it (hq ADR 0100)
This commit is contained in:
@@ -29,8 +29,39 @@ func showMode(ctx context.Context, inv *inventory.Inventory, node inventory.Node
|
|||||||
} else {
|
} else {
|
||||||
fmt.Printf(" taken %s\n", strings.Join(taken, ", "))
|
fmt.Printf(" taken %s\n", strings.Join(taken, ", "))
|
||||||
}
|
}
|
||||||
|
said, err := inv.AdoptionOf(ctx, node.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if said.At.IsZero() {
|
||||||
|
fmt.Printf(" it has not yet said what it found\n")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
fmt.Printf(" firewall found %s\n", orNone(said.Firewall))
|
||||||
|
if len(said.Held) == 0 {
|
||||||
|
fmt.Printf(" holding nothing found\n")
|
||||||
|
}
|
||||||
|
for _, h := range said.Held {
|
||||||
|
// A held thing that changed is how a predecessor still writing is caught: said first.
|
||||||
|
line := fmt.Sprintf(" holds %-11s %s %s, for %s", h.Kind, h.Target, h.ID, h.Module)
|
||||||
|
if h.Changed != "" {
|
||||||
|
line += " — " + strings.ToUpper(h.Changed) + " by something other than the mesh"
|
||||||
|
}
|
||||||
|
fmt.Println(line)
|
||||||
|
if h.Kept != "" {
|
||||||
|
fmt.Printf(" %-17s original kept at %s\n", "", h.Kept)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf(" as of %s\n", said.At.Local().Format(time.DateTime))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func orNone(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return "none reported"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
// adoptedNodes are the names of every adopted node, in the order given.
|
// adoptedNodes are the names of every adopted node, in the order given.
|
||||||
func adoptedNodes(nodes []inventory.Node) []string {
|
func adoptedNodes(nodes []inventory.Node) []string {
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ package inventory
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A node is adopted or converged (novox/hq ADR 0100).
|
// A node is adopted or converged (novox/hq ADR 0100).
|
||||||
@@ -113,7 +117,9 @@ func (i *Inventory) Converge(ctx context.Context, nodeName string) ([]string, er
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, err := tx.Exec(ctx,
|
if _, err := tx.Exec(ctx,
|
||||||
`update node set adopted = false, adopted_since = null, converged_at = now() where id = $1`,
|
`update node set adopted = false, adopted_since = null, converged_at = now(),
|
||||||
|
held = null, reachable = null
|
||||||
|
where id = $1`,
|
||||||
node.ID); err != nil {
|
node.ID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -147,3 +153,94 @@ func (i *Inventory) Taken(ctx context.Context, nodeName string) ([]string, error
|
|||||||
}
|
}
|
||||||
return out, rows.Err()
|
return out, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Held is one file or container an adopted node found and keeps as it was until its module is
|
||||||
|
// taken. The node's own account, kept as it said it.
|
||||||
|
type Held struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Module string `json:"module"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
Since time.Time `json:"since"`
|
||||||
|
Changed string `json:"changed,omitempty"`
|
||||||
|
Kept string `json:"kept,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reach is one thing reachable on an adopted node: a listening socket or a published port.
|
||||||
|
type Reach struct {
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
By string `json:"by,omitempty"`
|
||||||
|
Published bool `json:"published,omitempty"`
|
||||||
|
ContainerPort int `json:"container-port,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adoption is what an adopted node last said about adoption, and when.
|
||||||
|
type Adoption struct {
|
||||||
|
Held []Held
|
||||||
|
Firewall string
|
||||||
|
Reachable []Reach
|
||||||
|
// At is when it said so; zero when it never has.
|
||||||
|
At time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordAdoption keeps what a node last reported about adoption, replacing what was there: the
|
||||||
|
// question is the machine as it is now.
|
||||||
|
func (i *Inventory) RecordAdoption(ctx context.Context, node string, held []Held, firewall string,
|
||||||
|
reachable []Reach) error {
|
||||||
|
heldRaw, err := json.Marshal(nonNil(held))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reachRaw, err := json.Marshal(nonNil(reachable))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = i.store.Pool().Exec(ctx,
|
||||||
|
`update node set held = $2, firewall = nullif($3, ''), reachable = $4,
|
||||||
|
adoption_reported = now(), last_seen = now()
|
||||||
|
where id = $1`, node, heldRaw, firewall, reachRaw)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func nonNil[T any](s []T) []T {
|
||||||
|
if s == nil {
|
||||||
|
return []T{}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return Adoption{}, fmt.Errorf("%w: %s", ErrNoSuchNode, name)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return Adoption{}, err
|
||||||
|
}
|
||||||
|
var out Adoption
|
||||||
|
if firewall != nil {
|
||||||
|
out.Firewall = *firewall
|
||||||
|
}
|
||||||
|
if at != nil {
|
||||||
|
out.At = *at
|
||||||
|
}
|
||||||
|
if len(heldRaw) > 0 {
|
||||||
|
if err := json.Unmarshal(heldRaw, &out.Held); err != nil {
|
||||||
|
return Adoption{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(reachRaw) > 0 {
|
||||||
|
if err := json.Unmarshal(reachRaw, &out.Reachable); err != nil {
|
||||||
|
return Adoption{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- What an adopted node last reported about adoption (novox/hq ADR 0100): the files and containers
|
||||||
|
-- it found and holds until their module is taken, the firewall it found, and what is reachable on
|
||||||
|
-- the machine now — which converging it previews, so nothing closes without being named first.
|
||||||
|
--
|
||||||
|
-- The last report, replaced, like what a node owns: the question is the machine as it is now.
|
||||||
|
-- Kept apart from node_report because a node reports it on its own schedule, when what it holds
|
||||||
|
-- changes, and not only after an apply.
|
||||||
|
|
||||||
|
alter table node add column held jsonb;
|
||||||
|
alter table node add column firewall text;
|
||||||
|
alter table node add column reachable jsonb;
|
||||||
|
alter table node add column adoption_reported timestamptz;
|
||||||
@@ -199,6 +199,27 @@ func (e Enrolment) Heard(ctx context.Context, report Report) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What an adopted node holds, which firewall it found, and what is reachable on it (novox/hq
|
||||||
|
// ADR 0100). Recorded whenever a report carries any of it — a node reports these on its own
|
||||||
|
// 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 {
|
||||||
|
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})
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A bare word that a node is there is not an account of what the machine did or holds: it
|
// A bare word that a node is there is not an account of what the machine did or holds: it
|
||||||
// moves last_seen and touches nothing else. This arrives every minute (link.AliveEvery),
|
// moves last_seen and touches nothing else. This arrives every minute (link.AliveEvery),
|
||||||
// while a real report is rare, so recording it as one would overwrite the node's last real
|
// while a real report is rare, so recording it as one would overwrite the node's last real
|
||||||
|
|||||||
@@ -175,3 +175,46 @@ func TestAFailureDoesNotBecomeTheAccountOfWhatTheMachineHolds(t *testing.T) {
|
|||||||
t.Fatalf("a partial report replaced the account of what the machine holds: %v", owned)
|
t.Fatalf("a partial report replaced the account of what the machine holds: %v", owned)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// novox/hq ADR 0100: what an adopted node holds, the firewall it found and what is reachable on it
|
||||||
|
// are kept from the report that carries them, and a bare word that the node is there wipes none.
|
||||||
|
func TestWhatAnAdoptedNodeHoldsIsKeptAndAnAliveWordDoesNotWipeIt(t *testing.T) {
|
||||||
|
inv, _, _ := heardFrom(t, link.Report{
|
||||||
|
Node: "anchor", Applied: []string{"hello-web.served"}, Firewall: "ufw",
|
||||||
|
Held: []link.Held{{ID: "hello-web.page", Module: "hello-web", Kind: "file",
|
||||||
|
Target: "/var/lib/hello-web/index.html", Changed: "rewritten", Kept: "/var/lib/mesh/kept/x"}},
|
||||||
|
Reachable: []link.Reach{{Protocol: "tcp", Address: "0.0.0.0", Port: 8080, By: "hello-web",
|
||||||
|
Published: true, ContainerPort: 80}},
|
||||||
|
})
|
||||||
|
ctx := context.Background()
|
||||||
|
check := func(when string) {
|
||||||
|
t.Helper()
|
||||||
|
got, err := inv.AdoptionOf(ctx, "anchor")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.Firewall != "ufw" || len(got.Held) != 1 || got.Held[0].Changed != "rewritten" ||
|
||||||
|
len(got.Reachable) != 1 || got.Reachable[0].ContainerPort != 80 || got.At.IsZero() {
|
||||||
|
t.Fatalf("%s: what the node said is not what was kept: %+v", when, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check("after the report")
|
||||||
|
|
||||||
|
if err := (link.Enrolment{Inventory: inv}).Heard(ctx, link.Report{Node: "anchor"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
check("after an alive word")
|
||||||
|
|
||||||
|
// A reconcile report carrying only adoption is recorded, though it applied nothing.
|
||||||
|
if err := (link.Enrolment{Inventory: inv}).Heard(ctx, link.Report{Node: "anchor",
|
||||||
|
Firewall: "ufw"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, err := inv.AdoptionOf(ctx, "anchor")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(got.Held) != 0 || got.Firewall != "ufw" {
|
||||||
|
t.Fatalf("a report from an adopted node holding nothing did not empty held: %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
// traffic between them, each receiving half of what it expects. That has happened here before.
|
// traffic between them, each receiving half of what it expects. That has happened here before.
|
||||||
package link
|
package link
|
||||||
|
|
||||||
import "encoding/base64"
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// Exchange is where nodes publish everything they have to say.
|
// Exchange is where nodes publish everything they have to say.
|
||||||
const Exchange = "mesh"
|
const Exchange = "mesh"
|
||||||
@@ -116,6 +119,43 @@ type Report struct {
|
|||||||
// Declared is the digest of the declaration this report is about — the same bytes, hashed
|
// Declared is the digest of the declaration this report is about — the same bytes, hashed
|
||||||
// the same way, as the `sent` digest the mesh recorded. Which declaration, not when.
|
// the same way, as the `sent` digest the mesh recorded. Which declaration, not when.
|
||||||
Declared string `json:"declared,omitempty"`
|
Declared string `json:"declared,omitempty"`
|
||||||
|
|
||||||
|
// Held is what an adopted node found and is keeping as it was until its module is taken
|
||||||
|
// (novox/hq ADR 0100). Without it an adopted node reads as converged.
|
||||||
|
Held []Held `json:"held,omitempty"`
|
||||||
|
// Firewall is the firewall found on the machine — "ufw" or "none" — and empty on a node that
|
||||||
|
// was never asked, which is every converged one.
|
||||||
|
Firewall string `json:"firewall,omitempty"`
|
||||||
|
// Reachable is what can be reached on the machine now: every listening socket and every
|
||||||
|
// published container port. Only an adopted node reports it; it is what converging previews.
|
||||||
|
Reachable []Reach `json:"reachable,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Held is one file or container found on an adopted node and kept as it was.
|
||||||
|
type Held struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Module string `json:"module"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
Since time.Time `json:"since"`
|
||||||
|
// Changed is what something other than the mesh did to it since — rewritten, stopped,
|
||||||
|
// replaced or gone — and empty while it is as found.
|
||||||
|
Changed string `json:"changed,omitempty"`
|
||||||
|
// Kept is where a file's original was kept.
|
||||||
|
Kept string `json:"kept,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reach is one thing reachable on the machine: a listening socket, or a published container port.
|
||||||
|
type Reach struct {
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
// By is what holds it — a process, or a container's name.
|
||||||
|
By string `json:"by,omitempty"`
|
||||||
|
// Published is a container port the runtime publishes, reached on the forwarded path; its
|
||||||
|
// container's own port is ContainerPort.
|
||||||
|
Published bool `json:"published,omitempty"`
|
||||||
|
ContainerPort int `json:"container-port,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnrolReply is what the mesh says back.
|
// EnrolReply is what the mesh says back.
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ func TestTheWireFormatIsExactlyTheseFieldNames(t *testing.T) {
|
|||||||
{Signed{Declaration: []byte("{}"), Signature: []byte("x")}, []string{"declaration", "signature"}},
|
{Signed{Declaration: []byte("{}"), Signature: []byte("x")}, []string{"declaration", "signature"}},
|
||||||
{Report{Node: "n", Applied: []string{"a"}, Failed: map[string]string{"k": "v"}, Refused: "r"},
|
{Report{Node: "n", Applied: []string{"a"}, Failed: map[string]string{"k": "v"}, Refused: "r"},
|
||||||
[]string{"node", "applied", "failed", "refused"}},
|
[]string{"node", "applied", "failed", "refused"}},
|
||||||
|
{Report{Node: "n", Held: []Held{{ID: "m.f"}}, Firewall: "ufw", Reachable: []Reach{{Port: 1}}},
|
||||||
|
[]string{"node", "held", "firewall", "reachable"}},
|
||||||
|
{Held{ID: "m.f", Module: "m", Kind: "file", Target: "/f", Changed: "rewritten", Kept: "/k"},
|
||||||
|
[]string{"id", "module", "kind", "target", "since", "changed", "kept"}},
|
||||||
|
{Reach{Protocol: "tcp", Address: "0.0.0.0", Port: 8080, By: "hello-web", Published: true,
|
||||||
|
ContainerPort: 80},
|
||||||
|
[]string{"protocol", "address", "port", "by", "published", "container-port"}},
|
||||||
{EnrolRequest{Node: "n", Secret: "s", PublicKey: []byte("k")},
|
{EnrolRequest{Node: "n", Secret: "s", PublicKey: []byte("k")},
|
||||||
[]string{"node", "secret", "public_key"}},
|
[]string{"node", "secret", "public_key"}},
|
||||||
} {
|
} {
|
||||||
|
|||||||
Reference in New Issue
Block a user