diff --git a/cmd/mesh-host/main.go b/cmd/mesh-host/main.go index 6fc4d63..39fbd3f 100644 --- a/cmd/mesh-host/main.go +++ b/cmd/mesh-host/main.go @@ -15,6 +15,7 @@ import ( "fmt" "os" "os/signal" + "sort" "strings" "syscall" "text/tabwriter" @@ -733,7 +734,7 @@ func applyAndKeep(ctx context.Context, opts options, raw []byte, signed *store.D saveErr.Error()} } - report := link.Report{} + report := link.Report{Carried: carriedPorts(updated)} for _, change := range outcome.Outcomes { report.Applied = append(report.Applied, change.ID) } @@ -798,3 +799,30 @@ func sealOpener(statePath string) apply.Unseal { return key.Unseal(sealed) } } + +// carriedPorts is every machine port held by what this host raised from its own bundle. +// +// **What the mesh must assign around** (novox/hq ADR 0038). The substrate is not a module: a node +// raises it before any mesh exists, so the control plane has never heard of the store or the +// broker. Told this, it can put a module somewhere else; not told, it hands out a port one of them +// holds and finds out from a container runtime. +// +// Only what was carried. What the mesh itself put here it already knows about, and reporting it +// back would make the machine an authority on the mesh's own bookkeeping. +func carriedPorts(state store.State) []int { + seen := map[int]bool{} + var out []int + for _, applied := range state.Resources { + if applied.Origin == store.OriginDeclared { + continue + } + for _, port := range applied.Holds { + if !seen[port] { + seen[port] = true + out = append(out, port) + } + } + } + sort.Ints(out) + return out +} diff --git a/internal/apply/apply.go b/internal/apply/apply.go index c610a2a..a87fcce 100644 --- a/internal/apply/apply.go +++ b/internal/apply/apply.go @@ -204,6 +204,7 @@ func Apply( ID: resource.Identity(), Type: string(resource.Kind()), Target: outcome.Target, AppliedAt: time.Now().UTC(), Wrote: outcome.wrote, + Holds: holds(resource), }) report.Outcomes = append(report.Outcomes, outcome) if outcome.Action != "unchanged" { @@ -1063,3 +1064,31 @@ func digestOf(content string) string { sum := sha256.Sum256([]byte(content)) return hex.EncodeToString(sum[:]) } + +// holds is the machine's own ports a resource occupies. +// +// **What the declaration binds, not what is open.** A machine's open ports are a moving target — +// something a person started, a connection the kernel handed out — and assigning around them would +// mean a port that was free when it was asked for and taken when it was used. What a resource +// declares is stable, and it is the half the mesh can be responsible for. +func holds(resource declaration.Resource) []int { + container, ok := resource.(*declaration.Container) + if !ok { + return nil + } + var out []int + for _, mapping := range container.Ports { + // "8080:80", or "127.0.0.1:8080:80" when an address was named. The machine's port is the + // one before the last colon; the last is inside the container and is not the machine's. + parts := strings.Split(mapping, ":") + if len(parts) < 2 { + continue + } + port, err := strconv.Atoi(strings.TrimSpace(parts[len(parts)-2])) + if err != nil { + continue + } + out = append(out, port) + } + return out +} diff --git a/internal/link/messages.go b/internal/link/messages.go index f82489c..ed57fcf 100644 --- a/internal/link/messages.go +++ b/internal/link/messages.go @@ -56,4 +56,15 @@ type Report struct { // Refused is set when the declaration was rejected whole rather than applied in part. Refused string `json:"refused,omitempty"` + + // Carried are the machine's ports held by what this host raised from its own bundle. + // + // **So the mesh can assign around what it did not put here** (novox/hq ADR 0038). A node + // raises its substrate before any mesh exists, so the control plane has never heard of the + // store or the broker — and would hand a module a port one of them holds, discovering it only + // when a container runtime refused to start. + // + // A node *states* and the mesh writes, which is the whole shape of this message: this is the + // machine saying what is true of it, not asking for anything. + Carried []int `json:"carried,omitempty"` } diff --git a/internal/store/store.go b/internal/store/store.go index 6b87884..e0669d7 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -43,6 +43,18 @@ type Applied struct { // Empty means carried, for state written before this field existed: everything a host had // applied at that point came from its bundle. Origin string `json:"origin,omitempty"` + + // Holds are the machine's own ports this resource occupies. + // + // **So the mesh can assign around what it did not put here** (novox/hq ADR 0038). A node + // raises its substrate from the bundle before any mesh exists, so the control plane has never + // heard of the store, the broker or the control plane's own container — and a module assigned + // afterwards would be given a port one of them already holds, and would be told so by a + // container runtime rather than by anything that could have prevented it. + // + // Recorded per resource rather than counted per machine, because what a machine happens to + // have open right now is a moving target, and what its declaration binds is not. + Holds []int `json:"holds,omitempty"` // Target is what was changed — a path, a unit — so removal knows what to undo without // re-reading a declaration that may no longer exist. Target string `json:"target"`