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:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user