Unify trunk on main: initialization → main #3

Merged
jschoubben merged 58 commits from initialization into main 2026-09-05 01:13:33 +00:00
4 changed files with 191 additions and 5 deletions
Showing only changes of commit 4a43e21794 - Show all commits
+59
View File
@@ -247,6 +247,8 @@ func applyOne(ctx context.Context, sys system.System, r declaration.Resource, ru
return applyArchive(ctx, res, previous)
case *declaration.Action:
return applyAction(ctx, res, run)
case *declaration.Network:
return applyNetwork(ctx, res, run)
default:
// Unreachable: the declaration refused this already. Present because "unreachable"
// stops being true the moment someone adds a kind and forgets this switch.
@@ -657,6 +659,24 @@ func remove(ctx context.Context, sys system.System, a store.Applied, run Runner)
// to whatever it acted on.
return "forgotten", "an action leaves nothing the host owns", nil
case declaration.TypeNetwork:
// **The reason this is a shape at all** (novox/hq ADR 0029). Orphans are removed in
// reverse declaration order, so a network written before the containers that join it is
// removed after they are gone — and a runtime refusing to remove one still in use is
// reported rather than swallowed, because that means something the mesh did not declare
// is holding it.
cri, err := containerRuntime(ctx, run)
if err != nil {
return "", "", fmt.Errorf("%w, so the network %q cannot be removed", err, a.Target)
}
if _, err := run(ctx, cri, "network", "inspect", a.Target); err != nil {
return "forgotten", "no longer there", nil
}
if _, err := run(ctx, cri, "network", "rm", a.Target); err != nil {
return "", "", fmt.Errorf("cannot remove the network %q: %w", a.Target, err)
}
return "removed", "no longer declared", nil
default:
return "", "", fmt.Errorf("no way to remove a %q", a.Type)
}
@@ -775,6 +795,45 @@ func containerState(ctx context.Context, name string, run Runner) (state struct
// There is no "update" for a container: a container's configuration is fixed when it is
// created, so any change is a replacement. Saying that plainly is better than a partial
// in-place update that leaves the running thing half-declared.
// applyNetwork creates a named network if the machine does not already have one.
//
// **Existence is the whole of the state.** A network the mesh declared and a network somebody
// made by hand are indistinguishable by name, and that is deliberate: the mesh owns the name, not
// the thing, so it will not tear down and rebuild one that is already there and working. What it
// records is that this resource is now present, which is what lets it be removed later.
//
// Nothing is reconciled beyond presence. A driver or a subnet changed underneath would not be
// noticed — and is not declarable either (novox/hq ADR 0029), so there is nothing to disagree
// with.
func applyNetwork(ctx context.Context, r *declaration.Network, run Runner) (Outcome, error) {
out := begin(r)
cri, err := containerRuntime(ctx, run)
if err != nil {
return out, fmt.Errorf("%w, so nothing can be said about the network %q", err, r.Name)
}
if _, err := run(ctx, cri, "network", "inspect", r.Name); err == nil {
out.Action = "unchanged"
out.Detail = "already there"
return out, nil
}
if _, err := run(ctx, cri, "network", "create", r.Name); err != nil {
return out, fmt.Errorf("cannot create the network %q: %w", r.Name, err)
}
// Read back rather than trusting the exit status (novox/hq ADR 0018). A runtime that reports
// success and made nothing leaves every container that joins it failing to start, with the
// cause one step away.
if _, err := run(ctx, cri, "network", "inspect", r.Name); err != nil {
return out, fmt.Errorf(
"the network %q was created and is not there afterwards: %w", r.Name, err)
}
out.Action = "created"
out.Detail = "a network for this module's own containers"
return out, nil
}
func applyContainer(ctx context.Context, r *declaration.Container, run Runner) (Outcome, error) {
out := begin(r)
want := containerSpec(r)
+69
View File
@@ -9,6 +9,7 @@ import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
@@ -248,3 +249,71 @@ func TestAnArchiveMustBePinned(t *testing.T) {
t.Fatalf("unhelpful refusal: %v", err)
}
}
// Defends novox/hq ADR 0029: a network is a shape so that it can be removed.
//
// The whole argument for widening the vocabulary is lifecycle — an action could create one and
// nothing could ever take it away — so removal is the assertion that matters, not creation.
func TestANetworkIsCreatedAndThenRemovedWhenNoLongerDeclared(t *testing.T) {
var calls []string
there := map[string]bool{}
run := func(_ context.Context, name string, args ...string) (string, error) {
calls = append(calls, name+" "+strings.Join(args, " "))
if name != "docker" || len(args) < 2 || args[0] != "network" {
return "", nil // the runtime probe
}
switch args[1] {
case "inspect":
if !there[args[2]] {
return "", fmt.Errorf("no such network")
}
case "create":
there[args[2]] = true
case "rm":
delete(there, args[2])
}
return "", nil
}
d := declare(t, `{"id":"private","type":"network","name":"mail"}`)
_, state, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginDeclared, run, nil, nil)
if err != nil {
t.Fatal(err)
}
if !there["mail"] {
t.Fatal("the network was not created")
}
// The module is unassigned: the mesh now declares nothing.
empty := declare(t, `{"id":"unrelated","type":"directory","path":"`+t.TempDir()+`"}`)
if _, _, err := Apply(context.Background(), archHost(t), empty, state,
store.OriginDeclared, run, nil, nil); err != nil {
t.Fatal(err)
}
if there["mail"] {
t.Fatal("the network outlived the module that declared it, which is the entire reason " +
"this is a shape rather than an action")
}
}
// A network is created once and left alone when it is already there.
func TestANetworkAlreadyThereIsNotRebuilt(t *testing.T) {
var created int
run := func(_ context.Context, name string, args ...string) (string, error) {
if name == "docker" && len(args) > 1 && args[0] == "network" && args[1] == "create" {
created++
}
return "", nil // inspect succeeds: it is already there
}
d := declare(t, `{"id":"private","type":"network","name":"mail"}`)
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginDeclared, run, nil, nil); err != nil {
t.Fatal(err)
}
if created != 0 {
t.Fatalf("a network that was already there was created %d time(s); the mesh owns the "+
"name and not the thing, so it does not tear one down and rebuild it", created)
}
}
+45 -2
View File
@@ -42,6 +42,12 @@ const (
// of files; inlining them would make every declaration enormous and rewrite the lot whenever
// one changed.
TypeArchive Type = "archive"
// TypeNetwork is a named network on this machine, for a module whose containers must reach
// each other by name. Created if absent, removed when no longer declared — which is the whole
// reason it is a shape rather than an action, because an action leaves nothing the host can
// undo and the network would outlive the module (novox/hq ADR 0029).
TypeNetwork Type = "network"
)
// Resource is one thing that should be true of the machine.
@@ -182,6 +188,41 @@ type User struct {
Home string `json:"home,omitempty"`
}
// Network is a named network on this machine.
//
// **A name and nothing else.** Not a driver, a subnet or a gateway: each of those is something a
// module would have to know about the machine it lands on, and a module naming a subnet is a
// module that collides with whatever else chose the same one. The runtime picks; the mesh names
// (novox/hq ADR 0029).
type Network struct {
ID string `json:"id"`
Type Type `json:"type"`
Name string `json:"name"`
}
func (n *Network) Identity() string { return n.ID }
func (n *Network) Kind() Type { return TypeNetwork }
func (n *Network) Target() string { return n.Name }
func (n *Network) validate(where string, _ bool) []string {
var problems []string
if n.Name == "" {
problems = append(problems, where+": a network needs a name")
}
// The runtimes accept more than this, and the mesh does not: a name with a slash or a colon
// in it reads as a reference to something else entirely wherever it is later printed.
for _, r := range n.Name {
if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && (r < '0' || r > '9') &&
r != '-' && r != '_' && r != '.' {
problems = append(problems, where+
": a network name is letters, digits, dashes, underscores and dots, and "+
n.Name+" is not")
break
}
}
return problems
}
func (u *User) Identity() string { return u.ID }
func (u *User) Kind() Type { return TypeUser }
func (u *User) Target() string { return u.Name }
@@ -429,6 +470,8 @@ func newOf(t Type) Resource {
return &Container{}
case TypeAction:
return &Action{}
case TypeNetwork:
return &Network{}
case TypeUser:
return &User{}
case TypeArchive:
@@ -440,8 +483,8 @@ func newOf(t Type) Resource {
// Vocabulary is every kind this host speaks.
func Vocabulary() []Type {
return []Type{
TypeAction, TypeArchive, TypeContainer, TypeDirectory, TypeFile, TypePackage,
TypeService, TypeUser,
TypeAction, TypeArchive, TypeContainer, TypeDirectory, TypeFile, TypeNetwork,
TypePackage, TypeService, TypeUser,
}
}
+18 -3
View File
@@ -256,7 +256,7 @@ func TestTheVocabularyIsTheEightShapesTheMeshNeeds(t *testing.T) {
}
for _, want := range []Type{
TypeDirectory, TypeFile, TypeService, TypePackage, TypeContainer, TypeAction,
TypeUser, TypeArchive,
TypeUser, TypeArchive, TypeNetwork,
} {
if !speaks[want] {
t.Errorf("the host no longer speaks %q", want)
@@ -265,8 +265,11 @@ func TestTheVocabularyIsTheEightShapesTheMeshNeeds(t *testing.T) {
t.Errorf("%q is in the vocabulary and cannot be constructed", want)
}
}
if len(speaks) != 8 {
t.Errorf("the vocabulary is %d shapes rather than 8; every addition widens what a compromised "+
// `network` is the ninth, and novox/hq ADR 0029 is the decision that made it one: an action
// could create a network and nothing could remove it, because an action leaves no footprint
// the host can undo — so the network would outlive every module that was ever unassigned.
if len(speaks) != 9 {
t.Errorf("the vocabulary is %d shapes rather than 9; every addition widens what a compromised "+
"control plane can express, so a change here is a decision: %s",
len(speaks), vocabulary())
}
@@ -344,3 +347,15 @@ func TestSomethingAfterTheDeclarationIsRefused(t *testing.T) {
t.Fatalf("a declaration with a trailing newline was refused: %v", err)
}
}
// A name that would read as a reference to something else is refused before it reaches a runtime.
func TestANetworkNameIsRefusedIfItIsNotOne(t *testing.T) {
for _, name := range []string{"", "mail/private", "host:mail", "a b"} {
refusal := refusalFor(t, `{"declaration":1,"resources":[
{"id":"private","type":"network","name":"`+name+`"}
]}`)
if len(refusal.Problems) == 0 {
t.Errorf("a network named %q was accepted", name)
}
}
}