Files
mesh-catalog/modules/docker-compose/cmd/docker-compose-tools/compose_test.go
T
jochen 45befbbd02 docker-compose: the package, and its projects as tools (hq to-be 42 phase 2.9)
Compose and nothing else, for the two workstations, where it is already
installed by hand; the runtime, buildx and the group stay the docker
module's. Nine Go tools: projects (with their directories from the
containers' labels), ps, logs, a rendered config with secret-looking values
redacted, and up, down, restart and pull by directory or name. Acts run as
jobs inside the bundle, waited on for 18 s and followed with
docker_compose_job, because an up that pulls outlasts a call. down never
removes volumes.
2026-10-04 13:02:23 +02:00

223 lines
8.4 KiB
Go

package main
import (
"encoding/json"
"strings"
"testing"
)
func TestTheManifestIsComposesPackageAndNothingElse(t *testing.T) {
m := readManifest(t)
holdsTheBundle(t, m, "docker_compose")
if got := strings.Join(m.packages(), ","); got != "docker-compose" {
t.Errorf("packages %s: buildx and the runtime are the docker module's", got)
}
if len(m.Resources) != 1 {
t.Errorf("one resource, the package: %v", m.Resources)
}
}
const lsJSON = `[{"Name":"anton-lavinmq","Status":"running(1)","ConfigFiles":"/home/op/hub/lavinmq/docker-compose.yml"},{"Name":"old","Status":"exited(2)","ConfigFiles":"/srv/old/compose.yaml,/srv/old/compose.override.yaml"}]`
const psLabels = "anton-lavinmq\t/home/op/hub/lavinmq\t/home/op/hub/lavinmq/docker-compose.yml\tlavinmq\trunning\n" +
"old\t/srv/old\t/srv/old/compose.yaml,/srv/old/compose.override.yaml\tweb\texited\n" +
"old\t/srv/old\t/srv/old/compose.yaml,/srv/old/compose.override.yaml\tdb\texited\n"
// aDocker answers compose ls and the labelled ps, and hands every other line to rest.
func aDocker(t *testing.T, rest func(line string) Result) *fake {
return using(t, func(line string, c Cmd) Result {
switch {
case strings.HasPrefix(line, "docker compose ls"):
return ok(lsJSON)
case strings.HasPrefix(line, "docker ps -a --filter label=com.docker.compose.project"):
return ok(psLabels)
}
if rest != nil {
return rest(line)
}
return ok("")
})
}
func onDisk(t *testing.T, dirs, files []string) {
t.Helper()
wasD, wasF := statDir, statFile
in := func(set []string) func(string) bool {
return func(p string) bool {
for _, s := range set {
if s == p {
return true
}
}
return false
}
}
statDir, statFile = in(dirs), in(files)
t.Cleanup(func() { statDir, statFile = wasD, wasF })
}
func TestProjectsMergesComposesListWithTheContainersLabels(t *testing.T) {
aDocker(t, nil)
got, err := Projects()
if err != nil || got.Count != 2 {
t.Fatalf("%+v %v", got, err)
}
p := got.Projects[0]
if p.Name != "anton-lavinmq" || p.WorkingDir != "/home/op/hub/lavinmq" || p.Running != 1 || p.Containers != 1 || p.Status != "running(1)" {
t.Errorf("%+v", p)
}
o := got.Projects[1]
if strings.Join(o.Services, ",") != "db,web" || len(o.ConfigFiles) != 2 || o.Running != 0 || o.Containers != 2 {
t.Errorf("%+v", o)
}
}
func TestProjectsNamesADaemonTheAccountCannotReach(t *testing.T) {
using(t, func(string, Cmd) Result {
return Result{Status: 1, Stderr: "permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock"}
})
if _, err := Projects(); err == nil || !strings.Contains(err.Error(), "docker group") {
t.Fatalf("%v", err)
}
}
func TestATargetIsAKnownProjectWithItsFilesOrADirectoryHoldingAComposeFile(t *testing.T) {
aDocker(t, nil)
onDisk(t, []string{"/home/op/hub/lavinmq", "/srv/new", "/srv/empty", "/srv/old"},
[]string{"/home/op/hub/lavinmq/docker-compose.yml", "/srv/new/compose.yaml"})
got, err := targetOf(map[string]any{"dir": "/home/op/hub/lavinmq/"}, true)
if err != nil || got.Project != "anton-lavinmq" || len(got.Files) != 1 {
t.Fatalf("a known directory: %+v %v", got, err)
}
if a := strings.Join(got.args(), " "); a != "compose --project-directory /home/op/hub/lavinmq -f /home/op/hub/lavinmq/docker-compose.yml -p anton-lavinmq" {
t.Errorf("args %s", a)
}
got, err = targetOf(map[string]any{"dir": "/srv/new"}, true)
if err != nil || got.Project != "" || got.Dir != "/srv/new" {
t.Fatalf("a new directory: %+v %v", got, err)
}
got, err = targetOf(map[string]any{"project": "old"}, false)
if err != nil || got.Dir != "/srv/old" || got.Files != nil {
t.Fatalf("a project whose files are gone, for an act that needs none: %+v %v", got, err)
}
if _, err := targetOf(map[string]any{"project": "old"}, true); err == nil || !strings.Contains(err.Error(), "no longer there") {
t.Errorf("its files are needed: %v", err)
}
for _, bad := range []map[string]any{{}, {"dir": "rel/path"}, {"dir": "/nope"}, {"dir": "/srv/empty"}, {"project": "Bad Name"}, {"project": "unknown"}} {
if _, err := targetOf(bad, false); err == nil {
t.Errorf("%v was accepted", bad)
}
}
}
func TestPsReadsEitherJSONShapeAndKeepsOnlyPublishedPorts(t *testing.T) {
aDocker(t, func(line string) Result {
return ok(`{"Name":"a-web-1","Service":"web","State":"running","Status":"Up 2 hours","Health":"healthy","ExitCode":0,"Image":"nginx","Publishers":[{"URL":"0.0.0.0","TargetPort":80,"PublishedPort":8080,"Protocol":"tcp"},{"URL":"","TargetPort":443,"PublishedPort":0,"Protocol":"tcp"}]}
{"Name":"a-db-1","Service":"db","State":"exited","Status":"Exited (1)","ExitCode":1,"Image":"postgres","Publishers":[]}`)
})
got, err := Ps(Target{Project: "a"})
if err != nil || len(got.Containers) != 2 {
t.Fatalf("%+v %v", got, err)
}
if c := got.Containers[0]; strings.Join(c.Ports, ",") != "0.0.0.0:8080->80/tcp" || c.Health != "healthy" {
t.Errorf("%+v", c)
}
var arr []map[string]any
if err := jsonObjects(`[{"Name":"x"}]`, &arr); err != nil || len(arr) != 1 {
t.Errorf("an array: %v %v", arr, err)
}
}
func TestLogsAreBoundedAndRefuseAnOptionInAName(t *testing.T) {
f := aDocker(t, func(line string) Result { return Result{Stdout: "web-1 | a\nweb-1 | b\n", Truncated: true} })
got, err := Logs(Target{Project: "a"}, []string{"web"}, 50, "10m")
if err != nil || len(got.Lines) != 2 || !got.Truncated {
t.Fatalf("%+v %v", got, err)
}
if l := f.lines()[0]; l != "docker compose -p a logs --no-color --timestamps --tail 50 --since 10m web" {
t.Errorf("%s", l)
}
if _, err := Logs(Target{Project: "a"}, []string{"--follow"}, 5, ""); err == nil {
t.Error("an option as a service")
}
if _, err := Logs(Target{Project: "a"}, nil, 5, "1h; rm"); err == nil {
t.Error("a since that is neither")
}
}
func TestConfigRedactsSecretLookingValuesAndInlineContent(t *testing.T) {
aDocker(t, func(string) Result {
return ok(`{"name":"a","services":{"web":{"image":"nginx","environment":{"DB_PASSWORD":"hunter2","PORT":"80","API_TOKEN":"t"},"build":{"args":{"NPM_TOKEN":"n","NODE_ENV":"production"}}}},"secrets":{"s":{"content":"raw"}}}`)
})
got, err := Config(Target{Dir: "/srv/a"})
if err != nil || got.Redacted != 4 || strings.Join(got.Services, ",") != "web" {
t.Fatalf("%+v %v", got, err)
}
raw, _ := json.Marshal(got.Rendered)
for _, secret := range []string{"hunter2", `"t"`, `"n"`, "raw"} {
if strings.Contains(string(raw), secret) {
t.Errorf("%s shown: %s", secret, raw)
}
}
for _, kept := range []string{`"PORT":"80"`, `"NODE_ENV":"production"`, `"image":"nginx"`} {
if !strings.Contains(string(raw), kept) {
t.Errorf("%s hidden: %s", kept, raw)
}
}
}
func TestConfigAnInvalidFileIsComposesError(t *testing.T) {
aDocker(t, func(string) Result {
return Result{Status: 15, Stderr: "services.web Additional property foo is not allowed"}
})
if _, err := Config(Target{Dir: "/srv/a"}); err == nil || !strings.Contains(err.Error(), "Additional property") {
t.Fatalf("%v", err)
}
}
func TestActsAreJobsAsTheAccountKeepVolumesAndCheckTheirArguments(t *testing.T) {
f := aDocker(t, nil)
tg := Target{Dir: "/srv/a", Project: "a"}
if got, err := Up(tg, []string{"web"}, true, "always"); err != nil || got.Job.Running || got.Act != "up" {
t.Fatalf("%+v %v", got, err)
}
if _, err := Down(tg); err != nil {
t.Fatal(err)
}
if _, err := Restart(tg, nil); err != nil {
t.Fatal(err)
}
if _, err := Pull(tg, nil); err != nil {
t.Fatal(err)
}
want := []string{
"docker compose --project-directory /srv/a -p a up --detach --pull always --build web",
"docker compose --project-directory /srv/a -p a down",
"docker compose --project-directory /srv/a -p a restart",
"docker compose --project-directory /srv/a -p a pull",
}
if got := strings.Join(f.lines(), "\n"); got != strings.Join(want, "\n") {
t.Errorf("asked\n%s\nwant\n%s", got, strings.Join(want, "\n"))
}
for _, l := range f.lines() {
if strings.Contains(l, "sudo") || strings.Contains(l, "-v") || strings.Contains(l, "--volumes") {
t.Errorf("%s", l)
}
}
if _, err := Up(tg, nil, false, "sometimes"); err == nil {
t.Error("an unknown pull policy")
}
if _, err := Restart(tg, []string{"-t"}); err == nil {
t.Error("an option as a service")
}
}
func TestAFailedActIsAnError(t *testing.T) {
aDocker(t, func(string) Result {
return Result{Status: 1, Stderr: "Error response from daemon: port is already allocated"}
})
if _, err := Up(Target{Dir: "/srv/a"}, nil, false, "missing"); err == nil || !strings.Contains(err.Error(), "already allocated") {
t.Fatalf("%v", err)
}
}