package docker import ( "context" "fmt" "io" "os" "os/exec" "path/filepath" "github.com/manalejandro/buque/internal/models" ) // ComposeManager manages Docker Compose operations type ComposeManager struct { composeCommand string } // NewComposeManager creates a new Docker Compose manager func NewComposeManager() (*ComposeManager, error) { // Try to find docker compose command cmd := "docker" if err := exec.Command(cmd, "compose", "version").Run(); err == nil { return &ComposeManager{composeCommand: cmd}, nil } // Fallback to docker-compose cmd = "docker-compose" if err := exec.Command(cmd, "version").Run(); err == nil { return &ComposeManager{composeCommand: cmd}, nil } return nil, fmt.Errorf("docker compose not found. Please install Docker and Docker Compose") } // Up starts services in an environment func (cm *ComposeManager) Up(ctx context.Context, env models.Environment, detach bool) error { args := cm.buildComposeArgs(env) args = append(args, "up") if detach { args = append(args, "-d") } cmd := cm.createCommand(ctx, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Dir = env.Path return cmd.Run() } // Down stops and removes services in an environment func (cm *ComposeManager) Down(ctx context.Context, env models.Environment, removeVolumes bool) error { args := cm.buildComposeArgs(env) args = append(args, "down") if removeVolumes { args = append(args, "-v") } cmd := cm.createCommand(ctx, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Dir = env.Path return cmd.Run() } // Restart restarts services in an environment func (cm *ComposeManager) Restart(ctx context.Context, env models.Environment) error { args := cm.buildComposeArgs(env) args = append(args, "restart") cmd := cm.createCommand(ctx, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Dir = env.Path return cmd.Run() } // Pull pulls images for an environment func (cm *ComposeManager) Pull(ctx context.Context, env models.Environment) error { args := cm.buildComposeArgs(env) args = append(args, "pull") cmd := cm.createCommand(ctx, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Dir = env.Path return cmd.Run() } // PS lists services in an environment func (cm *ComposeManager) PS(ctx context.Context, env models.Environment) (string, error) { args := cm.buildComposeArgs(env) args = append(args, "ps", "--format", "json") cmd := cm.createCommand(ctx, args...) cmd.Dir = env.Path output, err := cmd.CombinedOutput() if err != nil { return "", fmt.Errorf("failed to list services: %w\n%s", err, string(output)) } return string(output), nil } // Logs retrieves logs from an environment func (cm *ComposeManager) Logs(ctx context.Context, env models.Environment, follow bool, tail string) error { args := cm.buildComposeArgs(env) args = append(args, "logs") if follow { args = append(args, "-f") } if tail != "" { args = append(args, "--tail", tail) } cmd := cm.createCommand(ctx, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Dir = env.Path return cmd.Run() } // Update updates images and recreates services func (cm *ComposeManager) Update(ctx context.Context, env models.Environment) error { // Pull latest images if err := cm.Pull(ctx, env); err != nil { return fmt.Errorf("failed to pull images: %w", err) } // Recreate services with new images args := cm.buildComposeArgs(env) args = append(args, "up", "-d", "--force-recreate") cmd := cm.createCommand(ctx, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Dir = env.Path return cmd.Run() } // Build builds images for an environment func (cm *ComposeManager) Build(ctx context.Context, env models.Environment) error { args := cm.buildComposeArgs(env) args = append(args, "build") cmd := cm.createCommand(ctx, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Dir = env.Path return cmd.Run() } // ValidateComposeFile checks if the compose file is valid func (cm *ComposeManager) ValidateComposeFile(env models.Environment) error { composeFilePath := filepath.Join(env.Path, env.ComposeFile) if _, err := os.Stat(composeFilePath); os.IsNotExist(err) { return fmt.Errorf("compose file not found: %s", composeFilePath) } args := cm.buildComposeArgs(env) args = append(args, "config", "--quiet") cmd := cm.createCommand(context.Background(), args...) cmd.Dir = env.Path if err := cmd.Run(); err != nil { return fmt.Errorf("invalid compose file: %w", err) } return nil } // GetConfig returns the resolved compose configuration func (cm *ComposeManager) GetConfig(ctx context.Context, env models.Environment) (string, error) { args := cm.buildComposeArgs(env) args = append(args, "config") cmd := cm.createCommand(ctx, args...) cmd.Dir = env.Path output, err := cmd.CombinedOutput() if err != nil { return "", fmt.Errorf("failed to get config: %w\n%s", err, string(output)) } return string(output), nil } // buildComposeArgs builds the base arguments for docker compose commands func (cm *ComposeManager) buildComposeArgs(env models.Environment) []string { args := []string{} if cm.composeCommand == "docker" { args = append(args, "compose") } if env.ComposeFile != "" && env.ComposeFile != "docker-compose.yml" { args = append(args, "-f", env.ComposeFile) } return args } // createCommand creates an exec.Cmd with the given arguments func (cm *ComposeManager) createCommand(ctx context.Context, args ...string) *exec.Cmd { if ctx == nil { ctx = context.Background() } if cm.composeCommand == "docker-compose" { return exec.CommandContext(ctx, cm.composeCommand, args...) } return exec.CommandContext(ctx, cm.composeCommand, args...) } // ExecInService executes a command in a running service func (cm *ComposeManager) ExecInService(ctx context.Context, env models.Environment, service string, command []string) error { args := cm.buildComposeArgs(env) args = append(args, "exec", service) args = append(args, command...) cmd := cm.createCommand(ctx, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin cmd.Dir = env.Path return cmd.Run() } // CopyLogs copies logs from a service to a writer func (cm *ComposeManager) CopyLogs(ctx context.Context, env models.Environment, service string, writer io.Writer) error { args := cm.buildComposeArgs(env) args = append(args, "logs", service) cmd := cm.createCommand(ctx, args...) cmd.Stdout = writer cmd.Stderr = writer cmd.Dir = env.Path return cmd.Run() }