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 }