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) } } // A container's capabilities reach the runtime and are part of its spec (novox/hq ADR 0169). func TestACapabilityReachesTheRuntimeAndTheSpec(t *testing.T) { var ran []string run := func(_ context.Context, name string, args ...string) (string, error) { if name != "docker" { return "", errors.New("not installed") } switch args[0] { case "info": return "29.0.0\n", nil case "container": return "false\t\n", errors.New("no such container") case "run": ran = args return "deadbeef\n", nil } return "", nil } d := parseTrusted(t, `{"declaration":1,"resources":[ {"id":"fw","type":"container","name":"fw","image":"`+pinned+`","network":"host","capabilities":["NET_ADMIN"]} ]}`) _, _, _ = Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil) granted := false for i, a := range ran { if a == "--cap-add" && i+1 < len(ran) && ran[i+1] == "NET_ADMIN" { granted = true } } if !granted { t.Fatalf("the capability was not granted: %v", ran) } with := d.Resources[0].(*declaration.Container) without := *with without.Capabilities = nil if containerSpec(with, inputs{}) == containerSpec(&without, inputs{}) { t.Fatal("a capability is not part of the container's spec") } }