A network is a shape, so that it can be removed
novox/hq ADR 0029, and work breakdown 1.3. A module of several containers had no way to let them reach each other by name: a container declaration could join a network and nothing could create one. An action was the obvious alternative and is refused on removal — "an action has no footprint the host can undo", so a network made that way outlives every module that is ever unassigned, and the mesh cannot tell. A resource the mesh can create and never clean up is one it should not create. A name and nothing else. Not a driver, a subnet or a gateway: each is something a module would have to know about the machine it lands on, and a module naming a subnet collides with whatever else chose the same one. It needs no new ordering rule. Resources apply in declaration order and orphans are removed in reverse, so a network written before the containers that join it is created first and removed last — after they are gone. A runtime refusing to remove one still in use is reported rather than swallowed, because that means something undeclared is holding it. The vocabulary guard fired on the change, as designed, and now names the record instead of a number: nine shapes, with the argument beside the count. Creation reads back rather than trusting an exit status (ADR 0018): a runtime that reports success and made nothing leaves every container that joins it failing to start, one step from the cause.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user