node public-domain: asking what it is no longer takes it away
`node public-domain <name>` cleared the domain. It reads like a question — it is exactly what anybody types to find out what the answer is — and it silently took every routed name the node had. There is no output that makes up for that: by the time it prints, the fact is gone, and the mesh cannot tell a person what a domain used to be. The bare form reports now. Clearing is still a real thing to want — a machine that stops facing the outside composes no names, and lab-versus-production is this one setting (novox/hq ADR 0056) — so it keeps a way to be said, by name: `--clear`. A domain and `--clear` together are refused rather than one of them silently winning. The other `node` subcommands were checked. `add`, `list` and `show` write nothing they were not asked to, so there is nothing to make consistent with. `overlay place <node>` with no flags has the same shape — it clears the endpoint, the site and the hub flag — and is deliberately left alone here. It is a verb rather than a question and every caller passes flags, so the fix is a different judgement and belongs in its own change. The usage text gains the three forms, and `module forget`'s new flag, neither of which it named before. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
@@ -123,6 +123,9 @@ func usage() {
|
|||||||
node add <name> create a node record
|
node add <name> create a node record
|
||||||
node list the nodes this mesh knows about
|
node list the nodes this mesh knows about
|
||||||
node show <name> what one machine reported it can do, and why
|
node show <name> what one machine reported it can do, and why
|
||||||
|
node public-domain <name> the domain it composes its routed names under
|
||||||
|
node public-domain <name> <d> ...set it to d
|
||||||
|
node public-domain <name> --clear ...it faces the outside no longer
|
||||||
token issue --node <name> a one-time right to join, for an existing record
|
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
|
token issue --new <name> create the record and issue for it
|
||||||
identity show this control plane's signing key
|
identity show this control plane's signing key
|
||||||
@@ -134,7 +137,8 @@ func usage() {
|
|||||||
module add <file> register a module from its manifest
|
module add <file> register a module from its manifest
|
||||||
module list what modules this mesh knows about
|
module list what modules this mesh knows about
|
||||||
module moved <name> <commit> the source has a newer commit than the mesh built
|
module moved <name> <commit> the source has a newer commit than the mesh built
|
||||||
module forget <name> remove one, unless a node is running it
|
module forget <name> remove one, unless a node runs it or the mesh holds things for it
|
||||||
|
module forget <name> --and-what-it-holds ...and discard its settings, secrets and ports too
|
||||||
module issue <name> --node <m> a broker account for a module, scoped to its emits and consumes
|
module issue <name> --node <m> a broker account for a module, scoped to its emits and consumes
|
||||||
status [--json] what is wrong, what is quiet, and what is out of date
|
status [--json] what is wrong, what is quiet, and what is out of date
|
||||||
board [--listen ADDR] the same three questions, as a page that holds nothing
|
board [--listen ADDR] the same three questions, as a page that holds nothing
|
||||||
|
|||||||
+73
-21
@@ -22,7 +22,7 @@ import (
|
|||||||
|
|
||||||
func nodeCommand(ctx context.Context, args []string) error {
|
func nodeCommand(ctx context.Context, args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return errors.New("node add <name>, node list, node show <name>, or node public-domain <name> [domain]")
|
return errors.New("node add <name>, node list, node show <name>, or " + publicDomainUsage)
|
||||||
}
|
}
|
||||||
open, err := openStores(ctx)
|
open, err := openStores(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -66,32 +66,84 @@ func nodeCommand(ctx context.Context, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
|
|
||||||
case "public-domain":
|
case "public-domain":
|
||||||
// The domain this node composes its routed names under (novox/hq ADR 0056). Given a domain,
|
// The domain this node composes its routed names under (novox/hq ADR 0056).
|
||||||
// it is set; given nothing, it is cleared — a node that stops facing the outside composes no
|
//
|
||||||
// names. Lab-versus-production is this one setting and nothing else (see the ADR).
|
// **The form with no argument reports; clearing is asked for by name.** It used to clear —
|
||||||
if len(args) < 2 || len(args) > 3 {
|
// so `node public-domain anchor`, which reads like a question and is what anybody types to
|
||||||
return errors.New(
|
// find out what the answer is, silently took every routed name the node had. A read-shaped
|
||||||
"node public-domain <name> [domain] — a domain sets it, nothing clears it")
|
// invocation must never be a destructive write: there is no output that makes up for it,
|
||||||
}
|
// because the damage is already done by the time it prints.
|
||||||
domain := ""
|
return publicDomain(ctx, inv, args[1:])
|
||||||
if len(args) == 3 {
|
|
||||||
domain = args[2]
|
|
||||||
}
|
|
||||||
if err := inv.SetPublicDomain(ctx, args[1], domain); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if domain == "" {
|
|
||||||
fmt.Printf("%s has no public domain, so it composes no routed names\n", args[1])
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s composes its routed names under %s\n", args[1], domain)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("node has no %q; it has add, list, show and public-domain", args[0])
|
return fmt.Errorf("node has no %q; it has add, list, show and public-domain", args[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// publicDomainUsage is the one description of the three forms, so a refusal and the help agree.
|
||||||
|
const publicDomainUsage = "node public-domain <name> — what it is now; " +
|
||||||
|
"<name> <domain> to set it; <name> --clear to take it away"
|
||||||
|
|
||||||
|
// publicDomain reads, sets or clears the domain a node composes its routed names under.
|
||||||
|
//
|
||||||
|
// Three forms, and the destructive one is the only one that has to be asked for. Clearing is a
|
||||||
|
// real thing to want — a machine that stops facing the outside composes no names, and
|
||||||
|
// lab-versus-production is this one setting (novox/hq ADR 0056) — so it keeps a way to say it.
|
||||||
|
// What it does not keep is being the thing that happens when nothing was said at all.
|
||||||
|
func publicDomain(ctx context.Context, inv *inventory.Inventory, args []string) error {
|
||||||
|
set := flag.NewFlagSet("node public-domain", flag.ContinueOnError)
|
||||||
|
clear := set.Bool("clear", false, "take the domain away; it composes no routed names after")
|
||||||
|
positionals, err := parseAround(set, args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(positionals) == 0 || len(positionals) > 2 {
|
||||||
|
return errors.New(publicDomainUsage)
|
||||||
|
}
|
||||||
|
node := positionals[0]
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case *clear && len(positionals) == 2:
|
||||||
|
// Both, which cannot be meant. Refused rather than one of them silently winning.
|
||||||
|
return fmt.Errorf("give %s a domain or --clear, not both: %q and --clear say opposite "+
|
||||||
|
"things and the mesh will not choose between them", node, positionals[1])
|
||||||
|
|
||||||
|
case *clear:
|
||||||
|
if err := inv.SetPublicDomain(ctx, node, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("%s has no public domain, so it composes no routed names\n", node)
|
||||||
|
fmt.Printf(" run `push %s` to take them off it\n", node)
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case len(positionals) == 2:
|
||||||
|
if err := inv.SetPublicDomain(ctx, node, positionals[1]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("%s composes its routed names under %s\n", node, positionals[1])
|
||||||
|
fmt.Printf(" run `push %s` to send it\n", node)
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Asked, so answered. NodeByName first, so a name the mesh has never heard of is a refusal
|
||||||
|
// rather than "it has no public domain", which is true of that name and says nothing.
|
||||||
|
if _, err := inv.NodeByName(ctx, node); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
domain, err := inv.PublicDomainOf(ctx, node)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if domain == "" {
|
||||||
|
fmt.Printf("%s has no public domain, so it composes no routed names\n", node)
|
||||||
|
fmt.Printf(" `node public-domain %s <domain>` gives it one\n", node)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Printf("%s composes its routed names under %s\n", node, domain)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func tokenCommand(ctx context.Context, args []string) error {
|
func tokenCommand(ctx context.Context, args []string) error {
|
||||||
if len(args) == 0 || args[0] != "issue" {
|
if len(args) == 0 || args[0] != "issue" {
|
||||||
return errors.New("token issue --node <name>, or token issue --new <name>")
|
return errors.New("token issue --node <name>, or token issue --new <name>")
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// **A read-shaped invocation is never a destructive write.**
|
||||||
|
//
|
||||||
|
// `node public-domain anchor` used to clear the domain. It reads like a question — it is what
|
||||||
|
// anybody types to find out what the answer is — and it silently took every routed name the node
|
||||||
|
// had. There is no output that makes up for that: by the time it prints, the fact is gone.
|
||||||
|
func TestAskingForANodesPublicDomainDoesNotTakeItAway(t *testing.T) {
|
||||||
|
open := aMesh(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
if err := open.inventory.SetPublicDomain(ctx, "anchor", "example.test"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := publicDomain(ctx, open.inventory, []string{"anchor"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
domain, err := open.inventory.PublicDomainOf(ctx, "anchor")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if domain != "example.test" {
|
||||||
|
t.Fatalf("asking what the domain is took it away: %q", domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clearing is a real thing to want — a machine that stops facing the outside composes no names —
|
||||||
|
// so it keeps a way to be said. What it stops being is what happens when nothing was said.
|
||||||
|
func TestClearingANodesPublicDomainIsAskedForByName(t *testing.T) {
|
||||||
|
open := aMesh(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
if err := open.inventory.SetPublicDomain(ctx, "anchor", "example.test"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := publicDomain(ctx, open.inventory, []string{"anchor", "--clear"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
domain, err := open.inventory.PublicDomainOf(ctx, "anchor")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if domain != "" {
|
||||||
|
t.Fatalf("--clear did not clear it: %q", domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A domain sets it, as it always did.
|
||||||
|
func TestGivingANodeAPublicDomainSetsIt(t *testing.T) {
|
||||||
|
open := aMesh(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
if err := publicDomain(ctx, open.inventory, []string{"anchor", "example.test"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
domain, err := open.inventory.PublicDomainOf(ctx, "anchor")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if domain != "example.test" {
|
||||||
|
t.Fatalf("got %q", domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A domain and --clear say opposite things. Refused rather than one of them silently winning.
|
||||||
|
func TestADomainAndClearTogetherIsRefused(t *testing.T) {
|
||||||
|
open := aMesh(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
if err := open.inventory.SetPublicDomain(ctx, "anchor", "example.test"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err := publicDomain(ctx, open.inventory, []string{"anchor", "other.test", "--clear"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("a domain and --clear together were accepted")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "not both") {
|
||||||
|
t.Fatalf("the refusal does not say why: %v", err)
|
||||||
|
}
|
||||||
|
// And neither half happened.
|
||||||
|
domain, err := open.inventory.PublicDomainOf(ctx, "anchor")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if domain != "example.test" {
|
||||||
|
t.Fatalf("a refused command changed something: %q", domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asking about a name the mesh has never heard of is a refusal, not "it has no public domain" —
|
||||||
|
// which is true of that name and says nothing.
|
||||||
|
func TestAskingAboutAMachineTheMeshHasNeverHeardOfIsRefused(t *testing.T) {
|
||||||
|
open := aMesh(t)
|
||||||
|
if err := publicDomain(t.Context(), open.inventory, []string{"nowhere"}); err == nil {
|
||||||
|
t.Fatal("a name the mesh does not know was answered as if it were a machine")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user