package docker import ( "context" "encoding/json" "fmt" "os/exec" "regexp" "strings" ) // ComposeClient — pembungkus CLI compose (strategi hybrid PRD §3: operasi // compose dipercayakan ke CLI, bukan reimplementasi API). type ComposeClient struct { cmd []string // mis. ["docker","compose"] atau ["docker-compose"] } // NewCompose — spec "auto" memilih perintah sesuai engine & ketersediaan CLI; // spec eksplisit (mis. "docker compose") selalu menang. func NewCompose(spec, engine string) *ComposeClient { return &ComposeClient{cmd: pickCompose(spec, engine, exec.LookPath)} } // pickCompose — dipisah agar lookPath dapat di-stub di unit test. func pickCompose(spec, engine string, lookPath func(string) (string, error)) []string { if spec != "" && spec != "auto" { if f := strings.Fields(spec); len(f) > 0 { return f } } orders := [][]string{ {"docker", "compose"}, {"docker-compose"}, {"podman", "compose"}, {"podman-compose"}, } if engine == "podman" { orders = [][]string{ {"podman", "compose"}, {"podman-compose"}, {"docker", "compose"}, {"docker-compose"}, } } for _, o := range orders { if _, err := lookPath(o[0]); err == nil { return o } } return []string{"docker", "compose"} // tidak ada → error jelas saat dijalankan } // Command — perintah compose yang benar-benar dipakai (untuk doctor/log). func (c *ComposeClient) Command() string { return strings.Join(c.cmd, " ") } // ComposeProject — entri `docker compose ls --all --format json` // (format terverifikasi: {"Name","Status":"running(N)","ConfigFiles":"a,b"}). type ComposeProject struct { Name string `json:"Name"` Status string `json:"Status"` ConfigFiles string `json:"ConfigFiles"` } // ConfigFileList — ConfigFiles dipecah per koma. func (p ComposeProject) ConfigFileList() []string { var out []string for _, f := range strings.Split(p.ConfigFiles, ",") { if f = strings.TrimSpace(f); f != "" { out = append(out, f) } } return out } // List — daftar semua project compose (termasuk yang berhenti). func (c *ComposeClient) List(ctx context.Context) ([]ComposeProject, error) { out, err := c.Run(ctx, "", "ls", "--all", "--format", "json") if err != nil { return nil, err } var projects []ComposeProject if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &projects); err != nil { return nil, fmt.Errorf("compose ls: parse json: %w", err) } return projects, nil } // Version — versi compose ringkas (mis. "5.6.0"). // Provider (mis. `podman compose` → docker-compose) kadang menulis pesan // peringatan ke output; ambil baris terakhir non-kosong & bersihkan ANSI. func (c *ComposeClient) Version(ctx context.Context) (string, error) { out, err := c.Run(ctx, "", "version", "--short") if err != nil { return "", err } lines := strings.Split(out, "\n") for i := len(lines) - 1; i >= 0; i-- { if l := stripANSI(strings.TrimSpace(lines[i])); l != "" { return l, nil } } return "", nil } var ansiRE = regexp.MustCompile(`\x1b\[[0-9;?<>=]*[A-Za-z]`) func stripANSI(s string) string { return ansiRE.ReplaceAllString(s, "") } // --- operasi stack (dipakai manager & CLI companion) --- func (c *ComposeClient) Up(ctx context.Context, dir string) (string, error) { return c.Run(ctx, dir, "up", "-d", "--remove-orphans") } func (c *ComposeClient) Down(ctx context.Context, dir string) (string, error) { return c.Run(ctx, dir, "down") } func (c *ComposeClient) Stop(ctx context.Context, dir string) (string, error) { return c.Run(ctx, dir, "stop") } func (c *ComposeClient) Restart(ctx context.Context, dir string) (string, error) { return c.Run(ctx, dir, "restart") } func (c *ComposeClient) Pull(ctx context.Context, dir string) (string, error) { return c.Run(ctx, dir, "pull") } // Run — jalankan subperintah compose; dir = folder stack (working dir project). func (c *ComposeClient) Run(ctx context.Context, dir string, args ...string) (string, error) { full := append(append([]string{}, c.cmd[1:]...), args...) cmd := exec.CommandContext(ctx, c.cmd[0], full...) if dir != "" { cmd.Dir = dir } out, err := cmd.CombinedOutput() if err != nil { verb := "?" if len(args) > 0 { verb = args[0] } if ee, ok := err.(*exec.ExitError); ok { return string(out), fmt.Errorf("compose %s: exit %d", verb, ee.ExitCode()) } return string(out), err } return string(out), nil }