Keep what a machine said about itself, not just the yes
novox/hq ADR 0009: a capability's presence gates an assignment and its detail carries a value — seat: card1-DP-1, an architecture, an amount of memory. So 'can this run here' and 'what should it be configured as' are one fact read two ways, and the mesh was keeping the first read and discarding the second. The reason an absent capability is absent went the same way, which is the case a person most needs: 'this machine has no container runtime' is the answer and 'docker is not installed' is why, and only the machine knows why. `node show` says it back. Never reported and reported nothing stay different things there — one machine has not run the host, the other ran it and can do nothing, and those send a person to different places.
This commit is contained in:
@@ -129,6 +129,7 @@ func usage() {
|
||||
migrate bring each context's schema up to date
|
||||
node add <name> create a node record
|
||||
node list the nodes this mesh knows about
|
||||
node show <name> what one machine reported it can do, and why
|
||||
token issue --node <name> a one-time right to join, for an existing record
|
||||
token issue --new <name> create the record and issue for it
|
||||
identity show this control plane's signing key
|
||||
@@ -267,7 +268,7 @@ func openInventory(ctx context.Context) (*inventory.Inventory, error) {
|
||||
|
||||
func nodeCommand(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("node add <name>, or node list")
|
||||
return errors.New("node add <name>, node list, or node show <name>")
|
||||
}
|
||||
inv, err := openInventory(ctx)
|
||||
if err != nil {
|
||||
@@ -276,6 +277,11 @@ func nodeCommand(ctx context.Context, args []string) error {
|
||||
defer inv.Close()
|
||||
|
||||
switch args[0] {
|
||||
case "show":
|
||||
if len(args) != 2 {
|
||||
return errors.New("node show <name>")
|
||||
}
|
||||
return showNode(ctx, inv, args[1])
|
||||
case "add":
|
||||
if len(args) != 2 {
|
||||
return errors.New("node add <name>")
|
||||
@@ -2560,3 +2566,56 @@ func theThreeQuestions(ctx context.Context, inv *inventory.Inventory) (answers,
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// showNode says what one machine reported about itself, in its own words.
|
||||
//
|
||||
// **A capability is detected and never assumed** (novox/hq ADR 0009), so the only account of what
|
||||
// a machine can do is the one it gave — and its detail is half of that account. The mesh was
|
||||
// keeping the yes and discarding the reason, which makes *this machine has no seat* an answer with
|
||||
// nowhere to go: a person told a machine lacks something wants to know what the detector saw.
|
||||
//
|
||||
// It is also where "what should it be configured as" is read. The same line that gates an
|
||||
// assignment carries `card1-DP-1`, and a person composing settings for that machine needs it.
|
||||
func showNode(ctx context.Context, inv *inventory.Inventory, name string) error {
|
||||
node, err := inv.NodeByName(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s\n", node.Name)
|
||||
fmt.Printf(" last heard from %s\n", heardFrom(node))
|
||||
|
||||
held, err := inv.Profile(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if held == nil {
|
||||
// Never reported is not the same as reported nothing, and the remedy differs: one is a
|
||||
// machine that has not run the host yet, the other is a machine that ran it and can do
|
||||
// nothing.
|
||||
fmt.Printf("\n this machine has never said what it can do, so everything requiring a\n" +
|
||||
" capability is refused here — run the host on it\n")
|
||||
return nil
|
||||
}
|
||||
if len(held) == 0 {
|
||||
fmt.Printf("\n it reported no capabilities at all\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("\n what it can do, as it reported:\n")
|
||||
for _, c := range held {
|
||||
mark := "no "
|
||||
if c.Present {
|
||||
mark = "yes"
|
||||
}
|
||||
fmt.Printf(" %s %-20s %s\n", mark, c.Name, c.Detail)
|
||||
}
|
||||
|
||||
assigned, err := inv.Assigned(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(assigned) > 0 {
|
||||
fmt.Printf("\n assigned: %s\n", strings.Join(assigned, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -331,16 +331,11 @@ func (i *Inventory) ProfileOf(ctx context.Context, nodeName string) (map[string]
|
||||
// machine", which is wrong but visible. Better than assuming it can do everything.
|
||||
return out, nil
|
||||
}
|
||||
var reported struct {
|
||||
Capabilities []struct {
|
||||
Name string `json:"name"`
|
||||
Present bool `json:"present"`
|
||||
} `json:"capabilities"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &reported); err != nil {
|
||||
held, err := profileFrom(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, c := range reported.Capabilities {
|
||||
for _, c := range held {
|
||||
if c.Present {
|
||||
out[c.Name] = true
|
||||
}
|
||||
@@ -348,6 +343,57 @@ func (i *Inventory) ProfileOf(ctx context.Context, nodeName string) (map[string]
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Capability is one named fact a machine reported about itself.
|
||||
//
|
||||
// **Detected and never assumed** (novox/hq ADR 0009). The detail is the part that was being
|
||||
// thrown away: a capability's presence gates an assignment and *its detail can also carry a
|
||||
// value* — `seat: card1-DP-1`, an architecture, an amount of memory. So *can this run here* and
|
||||
// *what should it be configured as* are the same fact read two ways, and keeping only the first
|
||||
// read makes the second unanswerable.
|
||||
//
|
||||
// It is also what a refusal should quote. "This machine has no seat" is the right answer and
|
||||
// "no graphics devices are present" is the reason, and only the machine knows the reason.
|
||||
type Capability struct {
|
||||
Name string `json:"name"`
|
||||
Present bool `json:"present"`
|
||||
// Detail is what the detector observed, in its own words — including when it found nothing,
|
||||
// which is the case a person most needs explaining.
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// Profile is everything a machine last reported about what it can do.
|
||||
//
|
||||
// The same read ProfileOf uses, unreduced. Two functions parsing one column would be two things
|
||||
// to keep agreeing about what a profile is.
|
||||
func (i *Inventory) Profile(ctx context.Context, nodeName string) ([]Capability, error) {
|
||||
var raw []byte
|
||||
err := i.store.Pool().QueryRow(ctx,
|
||||
`select profile from node where name = $1`, nodeName).Scan(&raw)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNoSuchNode, nodeName)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return profileFrom(raw)
|
||||
}
|
||||
|
||||
func profileFrom(raw []byte) ([]Capability, error) {
|
||||
if len(raw) == 0 {
|
||||
// A machine that has never reported. Not an error and not an empty machine: nil says
|
||||
// "nothing is known", where an empty slice would say "it reported having nothing", and
|
||||
// those send a person to different places.
|
||||
return nil, nil
|
||||
}
|
||||
var reported struct {
|
||||
Capabilities []Capability `json:"capabilities"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &reported); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return reported.Capabilities, nil
|
||||
}
|
||||
|
||||
// SetSettings records what somebody wants a module's configuration to say.
|
||||
//
|
||||
// An empty node name means the whole mesh. Replacing rather than merging what is already there:
|
||||
|
||||
@@ -517,3 +517,90 @@ func TestACatalogueEntryKnowsWhetherItIsBehind(t *testing.T) {
|
||||
t.Fatal("a module whose source moved reported itself current")
|
||||
}
|
||||
}
|
||||
|
||||
// A capability's detail is half of what the machine said, and it was being thrown away.
|
||||
//
|
||||
// novox/hq ADR 0009: a capability's presence gates an assignment and its detail carries a value —
|
||||
// `seat: card1-DP-1`. So *can this run here* and *what should it be configured as* are one fact
|
||||
// read two ways, and keeping only the first read makes the second unanswerable.
|
||||
func TestWhatAMachineSaidAboutItselfIsKeptWhole(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
ctx := t.Context()
|
||||
node, err := inv.AddNode(ctx, "workstation")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RecordProfile(ctx, node.ID, map[string]any{"capabilities": []map[string]any{
|
||||
{"name": "seat", "present": true, "detail": "card1-DP-1, card1-HDMI-A-1"},
|
||||
{"name": "container-runtime", "present": false, "detail": "docker is not installed"},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
held, err := inv.Profile(ctx, "workstation")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(held) != 2 {
|
||||
t.Fatalf("the machine reported two things and the mesh kept %d", len(held))
|
||||
}
|
||||
found := map[string]Capability{}
|
||||
for _, c := range held {
|
||||
found[c.Name] = c
|
||||
}
|
||||
if found["seat"].Detail != "card1-DP-1, card1-HDMI-A-1" {
|
||||
t.Fatalf("the value the machine reported was discarded: %+v", found["seat"])
|
||||
}
|
||||
// And the absent one keeps its reason, which is the case a person most needs explaining:
|
||||
// "this machine has no container runtime" is the answer and "docker is not installed" is why.
|
||||
if found["container-runtime"].Present {
|
||||
t.Fatal("something the machine said it does not have was recorded as present")
|
||||
}
|
||||
if found["container-runtime"].Detail != "docker is not installed" {
|
||||
t.Fatalf("the reason a capability is absent was discarded: %+v", found["container-runtime"])
|
||||
}
|
||||
|
||||
// The gating read is unchanged, and takes only what is present.
|
||||
gates, err := inv.ProfileOf(ctx, "workstation")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !gates["seat"] || gates["container-runtime"] {
|
||||
t.Fatalf("the gating read disagrees with what the machine said: %+v", gates)
|
||||
}
|
||||
}
|
||||
|
||||
// Never reported is not the same as reported nothing: one machine has not run the host, the other
|
||||
// ran it and can do nothing, and the remedies are different.
|
||||
func TestNeverReportedAndReportedNothingAreDifferentProfiles(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
ctx := t.Context()
|
||||
silent, err := inv.AddNode(ctx, "silent")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
empty, err := inv.AddNode(ctx, "empty")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RecordProfile(ctx, empty.ID,
|
||||
map[string]any{"capabilities": []map[string]any{}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = silent
|
||||
|
||||
never, err := inv.Profile(ctx, "silent")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if never != nil {
|
||||
t.Fatalf("a machine that never reported looks like one that reported nothing: %+v", never)
|
||||
}
|
||||
nothing, err := inv.Profile(ctx, "empty")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if nothing == nil {
|
||||
t.Fatal("a machine that reported nothing looks like one that never reported")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user