Files
tia-love/internal/docker/compose.go
T

147 lines
4.3 KiB
Go

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
}