Files
sslh-multiplex-lab/internal/docker/manager.go
2026-01-29 00:03:02 +00:00

167 lines
4.3 KiB
Go

package docker
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
type Manager struct {
ClientDir string
}
func NewManager(clientDir string) *Manager {
return &Manager{
ClientDir: clientDir,
}
}
func getComposeCommand() ([]string, error) {
// Try docker compose (v2) first
cmd := exec.Command("docker", "compose", "version")
if err := cmd.Run(); err == nil {
return []string{"docker", "compose"}, nil
}
// Fall back to docker-compose (v1)
cmd = exec.Command("docker-compose", "--version")
if err := cmd.Run(); err == nil {
return []string{"docker-compose"}, nil
}
return nil, fmt.Errorf("neither 'docker compose' nor 'docker-compose' is available. Please install Docker Compose")
}
func (m *Manager) Build() error {
dockerfilePath := filepath.Join(m.ClientDir, "Dockerfile")
if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) {
return fmt.Errorf("Dockerfile not found at %s", dockerfilePath)
}
// Use docker-compose build to ensure it respects docker-compose.yml configuration
// and rebuilds when Dockerfile changes
composeCmd, err := getComposeCommand()
if err != nil {
// Fallback to docker build if compose is not available
cmd := exec.Command("docker", "build", "--no-cache", "-t", "sslh-lab-client", m.ClientDir)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to build Docker image: %w", err)
}
return nil
}
// Use docker-compose build which will rebuild if Dockerfile changed
args := append(composeCmd, "-f", filepath.Join(m.ClientDir, "docker-compose.yml"), "build", "--no-cache")
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = m.ClientDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to build Docker image: %w", err)
}
return nil
}
func (m *Manager) Run() error {
composeFile := filepath.Join(m.ClientDir, "docker-compose.yml")
if _, err := os.Stat(composeFile); os.IsNotExist(err) {
return fmt.Errorf("docker-compose.yml not found at %s", composeFile)
}
composeCmd, err := getComposeCommand()
if err != nil {
return err
}
args := append(composeCmd, "-f", composeFile, "up", "-d")
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = m.ClientDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to run Docker container: %w", err)
}
return nil
}
func (m *Manager) Stop() error {
composeFile := filepath.Join(m.ClientDir, "docker-compose.yml")
if _, err := os.Stat(composeFile); os.IsNotExist(err) {
return fmt.Errorf("docker-compose.yml not found at %s", composeFile)
}
composeCmd, err := getComposeCommand()
if err != nil {
return err
}
args := append(composeCmd, "-f", composeFile, "down")
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = m.ClientDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to stop Docker container: %w", err)
}
return nil
}
func (m *Manager) Status() (string, error) {
cmd := exec.Command("docker", "ps", "--filter", "name=sslh-lab-client", "--format", "{{.Status}}")
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to check container status: %w", err)
}
return string(output), nil
}
func (m *Manager) IsRunning() bool {
cmd := exec.Command("docker", "ps", "--filter", "name=sslh-lab-client", "--format", "{{.Names}}")
output, err := cmd.Output()
if err != nil {
return false
}
return len(output) > 0 && string(output) == "sslh-lab-client\n"
}
func (m *Manager) Connect(shell string) error {
if !m.IsRunning() {
return fmt.Errorf("container is not running. Start it first with 'sslh-lab client'")
}
cmd := exec.Command("docker", "exec", "-it", "sslh-lab-client", shell)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
// Exit code 255 is common when user exits normally (Ctrl+D or 'exit' command)
// Exit code 130 is common when user presses Ctrl+C
// These are not errors, just normal ways to disconnect
if exitError, ok := err.(*exec.ExitError); ok {
exitCode := exitError.ExitCode()
if exitCode == 255 || exitCode == 130 || exitCode == 0 {
return nil
}
}
// If err is nil (exit code 0), return nil
if err == nil {
return nil
}
// For other errors, return them
return err
}