Adoption mode: a node in use is adopted before it is converged (hq ADR 0100–0103) #20

Merged
jschoubben merged 52 commits from feat/adoption-mode into main 2026-09-22 19:01:47 +00:00
2 changed files with 51 additions and 1 deletions
Showing only changes of commit c7ff0b9026 - Show all commits
+29
View File
@@ -3,6 +3,7 @@ package bootstrap
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"strconv"
@@ -133,6 +134,10 @@ func PlaceOnTheNetwork(ctx context.Context, o Options, control controlPlane,
return fmt.Errorf("the private network needs an endpoint other machines can dial, and " +
"nothing said one: pass --endpoint, or --broker-address so one can be derived")
}
endpoint, err = endpointAgrees(endpoint, o.Ports.orDefaults().Hub)
if err != nil {
return err
}
if _, err := control.tell(ctx, "assign", o.Node, module); err != nil {
return err
@@ -210,6 +215,30 @@ func builds(manifest []byte) bool {
return m.Build != nil && len(m.Build.Artifacts) > 0
}
// endpointAgrees holds the endpoint other machines dial to the port this node gave the private
// network's hub (novox/hq ADR 0100): the hub binds what --hub-port says, so an endpoint naming
// another port is an address nothing answers on. A host alone takes the hub's port.
func endpointAgrees(endpoint string, hub int) (string, error) {
_, portText, err := net.SplitHostPort(endpoint)
var missing *net.AddrError
if errors.As(err, &missing) && missing.Err == "missing port in address" {
return net.JoinHostPort(strings.Trim(endpoint, "[]"), strconv.Itoa(hub)), nil
}
if err != nil {
return "", fmt.Errorf("--endpoint %q is not host:port: %w", endpoint, err)
}
port, err := strconv.Atoi(portText)
if err != nil {
return "", fmt.Errorf("--endpoint %q does not end in a port", endpoint)
}
if port != hub {
return "", fmt.Errorf("--endpoint %s names port %d and the private network's hub binds %d "+
"(--hub-port): other machines would dial a port nothing answers on. Give one port for the "+
"hub; nothing was changed", endpoint, port, hub)
}
return endpoint, nil
}
// derivedEndpoint is the default place other machines dial for the private network: the same host
// they already dial for the broker, on WireGuard's ordinary port. One fact, not two.
func derivedEndpoint(brokerAddress string, hub int) string {
+22 -1
View File
@@ -1,6 +1,9 @@
package bootstrap
import "testing"
import (
"strings"
"testing"
)
// The endpoint other machines dial defaults to the host they already dial — the broker's — on
// WireGuard's port. One fact, not two that drift.
@@ -23,3 +26,21 @@ func TestOnlyAManifestWithArtifactsBuilds(t *testing.T) {
t.Fatal("a manifest with nothing to build was built anyway")
}
}
// Defends novox/hq ADR 0100: the hub's port is the node's, and the endpoint other machines dial
// must name it — an endpoint on another port is an address nothing answers on.
func TestTheEndpointAgreesWithTheHubsPort(t *testing.T) {
if got, err := endpointAgrees("192.0.2.10:51820", 51820); err != nil || got != "192.0.2.10:51820" {
t.Errorf("an endpoint on the hub's port: %q %v", got, err)
}
if got, err := endpointAgrees("192.0.2.10", 51821); err != nil || got != "192.0.2.10:51821" {
t.Errorf("a host alone did not take the hub's port: %q %v", got, err)
}
_, err := endpointAgrees("192.0.2.10:51820", 51821)
if err == nil || !strings.Contains(err.Error(), "--hub-port") {
t.Errorf("two ports for one hub were accepted: %v", err)
}
if _, err := endpointAgrees("192.0.2.10:not-a-port", 51820); err == nil {
t.Error("an endpoint whose port is not a number was accepted")
}
}