A container may name networks it also joins once created, for the per-machine setting that keeps a found network while a neighbour still resolves it there: joined after the run, part of the spec, refused when it cannot be joined. A declaration may say which modules the mesh left out because a stored setting cannot compose with its definition. Absence used to read as removal; a left-out module's records are kept and said, and its holds are not released. Genesis raises the bootstrap forge under the gitea module's container name, with its image digest and its data directory mounted at /data, so the module holds it by the found rule instead of raising a second forge beside it (issue 090). The network is the one difference left for a take to say. Before this the forge had no volume: its repositories were the container's, lost with it.
137 lines
4.6 KiB
Go
137 lines
4.6 KiB
Go
package apply
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// A taken container keeps a found network by a per-machine setting (novox/hq ADR 0163, rule 4):
|
|
// joined once it runs, part of its spec, and refused when it cannot be joined.
|
|
func TestAContainerJoinsTheNetworksItKeeps(t *testing.T) {
|
|
var ran []string
|
|
connectFails := false
|
|
run := func(_ context.Context, name string, args ...string) (string, error) {
|
|
if name != "docker" {
|
|
return "", errors.New("not installed")
|
|
}
|
|
ran = append(ran, strings.Join(args, " "))
|
|
switch args[0] {
|
|
case "info":
|
|
return "29.0.0\n", nil
|
|
case "container":
|
|
if len(ran) > 2 {
|
|
return "true\t" + specOfLast, nil
|
|
}
|
|
return "false\t\n", errors.New("no such container")
|
|
case "run":
|
|
return "deadbeef\n", nil
|
|
case "network":
|
|
if connectFails {
|
|
return "", errors.New("network predecessor_default not found")
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|
|
d := parseTrusted(t, `{"declaration":1,"resources":[
|
|
{"id":"app","type":"container","name":"app","image":"`+pinned+`",
|
|
"networks":["predecessor_default"]}
|
|
]}`)
|
|
specOfLast = containerSpec(d.Resources[0].(*declaration.Container), inputs{})
|
|
alone := *d.Resources[0].(*declaration.Container)
|
|
alone.Networks = nil
|
|
if specOfLast == containerSpec(&alone, inputs{}) {
|
|
t.Fatal("the kept network is not part of the container's spec: kept or let go, the container would be left alone")
|
|
}
|
|
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
joined := false
|
|
for i, line := range ran {
|
|
if line == "network connect predecessor_default app" {
|
|
joined = true
|
|
if ran[i-1] != "container inspect --format {{.State.Running}}\t{{index .Config.Labels \""+specLabel+"\"}} app" &&
|
|
!strings.HasPrefix(ran[i-1], "container inspect") {
|
|
t.Errorf("joined before the container was read back as running: %v", ran)
|
|
}
|
|
}
|
|
}
|
|
if !joined || report.Outcomes[0].Action != "created" {
|
|
t.Fatalf("the container did not join the kept network: %v\n%+v", ran, report.Outcomes)
|
|
}
|
|
|
|
connectFails, ran = true, nil
|
|
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil); err == nil ||
|
|
!strings.Contains(err.Error(), "could not join the kept network predecessor_default") {
|
|
t.Fatalf("a network that cannot be joined was passed over: %v", err)
|
|
}
|
|
}
|
|
|
|
var specOfLast string
|
|
|
|
// A module the mesh left out of a declaration is not a module the mesh removed (novox/hq ADR 0163,
|
|
// rule 6): what the host wrote for it stays, recorded and said; what it holds for it stays held.
|
|
// A module simply absent is removed as it always was.
|
|
func TestALeftOutModuleIsNeitherRemovedNorForgotten(t *testing.T) {
|
|
var removed []string
|
|
gone := map[string]bool{}
|
|
run := func(_ context.Context, name string, args ...string) (string, error) {
|
|
if name != "docker" {
|
|
return "", nil
|
|
}
|
|
switch args[0] {
|
|
case "info":
|
|
return "29.0.0\n", nil
|
|
case "rm":
|
|
removed = append(removed, args[len(args)-1])
|
|
gone[args[len(args)-1]] = true
|
|
case "container":
|
|
if gone[args[len(args)-1]] {
|
|
return "", errors.New("no such container")
|
|
}
|
|
return "true\tspec", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
known := store.State{
|
|
Resources: []store.Applied{
|
|
{ID: "web.server", Type: "container", Target: "web", Origin: store.OriginDeclared},
|
|
{ID: "old.server", Type: "container", Target: "old", Origin: store.OriginDeclared},
|
|
},
|
|
Held: []store.Held{{ID: "web.page", Module: "web", Kind: "file", Target: "/srv/web/index.html"}},
|
|
}
|
|
d := parse(t, `{"declaration":1,"left_out":["web"],"resources":[
|
|
{"id":"notes.conf","type":"file","path":"`+t.TempDir()+`/notes.conf","content":"x"}
|
|
]}`)
|
|
report, state, err := Apply(context.Background(), archHost(t), d, known, store.OriginDeclared, run, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(removed) != 1 || removed[0] != "old" {
|
|
t.Fatalf("removed %v; only the module that is absent goes", removed)
|
|
}
|
|
if _, kept := state.At("container", "web"); !kept {
|
|
t.Fatal("the left-out module's record was forgotten")
|
|
}
|
|
if _, held := state.HeldAt("web.page"); !held {
|
|
t.Fatal("the left-out module's hold was released")
|
|
}
|
|
said := false
|
|
for _, o := range report.Outcomes {
|
|
if o.ID == "web.server" && o.Action == "unchanged" && strings.Contains(o.Detail, "web was left out of this declaration by the mesh") {
|
|
said = true
|
|
}
|
|
if o.ID == "web.server" && o.Action != "unchanged" {
|
|
t.Errorf("the left-out module's container was %s", o.Action)
|
|
}
|
|
}
|
|
if !said {
|
|
t.Fatalf("keeping the left-out module's container was not said: %+v", report.Outcomes)
|
|
}
|
|
}
|