Skip to content

Commit 338f34a

Browse files
authored
Check Docker Compose services are running (#17)
2 parents f72b1ca + 02ae9f6 commit 338f34a

4 files changed

Lines changed: 131 additions & 3 deletions

File tree

internal/check/compose.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package check
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"context"
7+
"encoding/json"
8+
"fmt"
9+
"os/exec"
10+
"strings"
11+
)
12+
13+
type ComposeCheck struct {
14+
runner func() ([]byte, error)
15+
}
16+
17+
func (c *ComposeCheck) Name() string {
18+
return "Docker Compose services running"
19+
}
20+
21+
type composeService struct {
22+
Name string `json:"Name"`
23+
State string `json:"State"`
24+
}
25+
26+
func (c *ComposeCheck) Run(_ context.Context) Result {
27+
run := c.runner
28+
if run == nil {
29+
run = func() ([]byte, error) {
30+
return exec.Command("docker", "compose", "ps", "--format", "json").Output()
31+
}
32+
}
33+
34+
out, err := run()
35+
if err != nil {
36+
return Result{
37+
Name: c.Name(),
38+
Status: StatusFail,
39+
Message: "could not run docker compose ps",
40+
Fix: "make sure Docker is running and you are in the project directory",
41+
}
42+
}
43+
44+
// docker compose ps --format json outputs one JSON object per line
45+
var stopped []string
46+
scanner := bufio.NewScanner(bytes.NewReader(out))
47+
for scanner.Scan() {
48+
line := strings.TrimSpace(scanner.Text())
49+
if line == "" {
50+
continue
51+
}
52+
var svc composeService
53+
if err := json.Unmarshal([]byte(line), &svc); err != nil {
54+
continue
55+
}
56+
if svc.State != "running" {
57+
stopped = append(stopped, svc.Name)
58+
}
59+
}
60+
61+
if len(stopped) > 0 {
62+
return Result{
63+
Name: c.Name(),
64+
Status: StatusFail,
65+
Message: fmt.Sprintf("services not running: %s", strings.Join(stopped, ", ")),
66+
Fix: "run docker compose up -d to start them",
67+
}
68+
}
69+
70+
return Result{
71+
Name: c.Name(),
72+
Status: StatusPass,
73+
Message: "all services are running",
74+
}
75+
}

internal/check/compose_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package check
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
)
8+
9+
func TestComposeCheck_AllRunning(t *testing.T) {
10+
c := &ComposeCheck{runner: func() ([]byte, error) {
11+
return []byte(`{"Name":"app-db-1","State":"running"}` + "\n" +
12+
`{"Name":"app-web-1","State":"running"}` + "\n"), nil
13+
}}
14+
result := c.Run(context.Background())
15+
if result.Status != StatusPass {
16+
t.Errorf("expected pass, got %v: %s", result.Status, result.Message)
17+
}
18+
}
19+
20+
func TestComposeCheck_SomeStopped(t *testing.T) {
21+
c := &ComposeCheck{runner: func() ([]byte, error) {
22+
return []byte(`{"Name":"app-db-1","State":"running"}` + "\n" +
23+
`{"Name":"app-web-1","State":"exited"}` + "\n"), nil
24+
}}
25+
result := c.Run(context.Background())
26+
if result.Status != StatusFail {
27+
t.Errorf("expected fail, got %v", result.Status)
28+
}
29+
}
30+
31+
func TestComposeCheck_CommandFails(t *testing.T) {
32+
c := &ComposeCheck{runner: func() ([]byte, error) {
33+
return nil, errors.New("docker not found")
34+
}}
35+
result := c.Run(context.Background())
36+
if result.Status != StatusFail {
37+
t.Errorf("expected fail, got %v", result.Status)
38+
}
39+
}
40+
41+
func TestComposeCheck_NoServices(t *testing.T) {
42+
c := &ComposeCheck{runner: func() ([]byte, error) {
43+
return []byte(""), nil
44+
}}
45+
result := c.Run(context.Background())
46+
if result.Status != StatusPass {
47+
t.Errorf("expected pass, got %v: %s", result.Status, result.Message)
48+
}
49+
}

internal/check/registry.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ func Build(stack detector.DetectedStack) []Check {
3535
cs = append(cs, &BinaryCheck{Binary: "docker"})
3636
cs = append(cs, &DockerDaemonCheck{})
3737
}
38+
if stack.DockerCompose {
39+
cs = append(cs, &ComposeCheck{})
40+
}
3841
if stack.Postgres {
3942
cs = append(cs, &PostgresCheck{URL: os.Getenv("DATABASE_URL")})
4043
}

internal/detector/detector.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ type DetectedStack struct {
1313
Java bool
1414
Maven bool
1515
Gradle bool
16-
Docker bool
16+
Docker bool
17+
DockerCompose bool
1718
Postgres bool
1819
Redis bool
1920
MySQL bool
@@ -31,9 +32,9 @@ func Detect(dir string) DetectedStack {
3132
stack.Maven = fileExists(filepath.Join(dir, "pom.xml"))
3233
stack.Gradle = fileExists(filepath.Join(dir, "build.gradle"))
3334
stack.Java = stack.Maven || stack.Gradle
34-
stack.Docker = fileExists(filepath.Join(dir, "Dockerfile")) ||
35-
fileExists(filepath.Join(dir, "docker-compose.yml")) ||
35+
stack.DockerCompose = fileExists(filepath.Join(dir, "docker-compose.yml")) ||
3636
fileExists(filepath.Join(dir, "docker-compose.yaml"))
37+
stack.Docker = fileExists(filepath.Join(dir, "Dockerfile")) || stack.DockerCompose
3738

3839
dbURL := os.Getenv("DATABASE_URL")
3940
stack.Postgres = strings.Contains(dbURL, "postgres")

0 commit comments

Comments
 (0)