package container import ( "context" "fmt" "io" "strconv" "strings" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" "github.com/rs/zerolog/log" "gordon/pkg/runtime" ) // DockerRuntime implements the Runtime interface using Docker API type DockerRuntime struct { client *client.Client } // NewDockerRuntime creates a new Docker runtime instance func NewDockerRuntime() (*DockerRuntime, error) { cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return nil, fmt.Errorf("failed to create Docker client: %w", err) } return &DockerRuntime{ client: cli, }, nil } // CreateContainer creates a new container func (d *DockerRuntime) CreateContainer(ctx context.Context, config *runtime.ContainerConfig) (*runtime.Container, error) { // Convert ports to Docker format exposedPorts := make(nat.PortSet) portBindings := make(nat.PortMap) for _, port := range config.Ports { containerPort := nat.Port(fmt.Sprintf("%d/tcp", port)) exposedPorts[containerPort] = struct{}{} // Bind to random available port on host portBindings[containerPort] = []nat.PortBinding{ { HostIP: "0.0.0.0", HostPort: "0", // Docker will assign a random available port }, } } // Create container configuration containerConfig := &container.Config{ Image: config.Image, Env: config.Env, ExposedPorts: exposedPorts, WorkingDir: config.WorkingDir, Cmd: config.Cmd, Labels: config.Labels, } hostConfig := &container.HostConfig{ PortBindings: portBindings, AutoRemove: config.AutoRemove, } // Create the container resp, err := d.client.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, config.Name) if err != nil { return nil, fmt.Errorf("failed to create container: %w", err) } log.Info().Str("id", resp.ID).Str("name", config.Name).Str("image", config.Image).Msg("Container created") // Inspect the created container to get full details return d.InspectContainer(ctx, resp.ID) } // StartContainer starts a container func (d *DockerRuntime) StartContainer(ctx context.Context, containerID string) error { err := d.client.ContainerStart(ctx, containerID, container.StartOptions{}) if err != nil { return fmt.Errorf("failed to start container %s: %w", containerID, err) } log.Info().Str("id", containerID).Msg("Container started") return nil } // StopContainer stops a container func (d *DockerRuntime) StopContainer(ctx context.Context, containerID string) error { timeout := 30 // 30 seconds timeout err := d.client.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout}) if err != nil { return fmt.Errorf("failed to stop container %s: %w", containerID, err) } log.Info().Str("id", containerID).Msg("Container stopped") return nil } // RestartContainer restarts a container func (d *DockerRuntime) RestartContainer(ctx context.Context, containerID string) error { timeout := 30 // 30 seconds timeout err := d.client.ContainerRestart(ctx, containerID, container.StopOptions{Timeout: &timeout}) if err != nil { return fmt.Errorf("failed to restart container %s: %w", containerID, err) } log.Info().Str("id", containerID).Msg("Container restarted") return nil } // RemoveContainer removes a container func (d *DockerRuntime) RemoveContainer(ctx context.Context, containerID string, force bool) error { err := d.client.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: force}) if err != nil { return fmt.Errorf("failed to remove container %s: %w", containerID, err) } log.Info().Str("id", containerID).Bool("force", force).Msg("Container removed") return nil } // ListContainers lists containers func (d *DockerRuntime) ListContainers(ctx context.Context, all bool) ([]*runtime.Container, error) { containers, err := d.client.ContainerList(ctx, container.ListOptions{All: all}) if err != nil { return nil, fmt.Errorf("failed to list containers: %w", err) } var result []*runtime.Container for _, c := range containers { // Extract ports var ports []int for _, port := range c.Ports { if port.PublicPort > 0 { ports = append(ports, int(port.PublicPort)) } } // Get the primary name (remove leading slash) name := "" if len(c.Names) > 0 { name = strings.TrimPrefix(c.Names[0], "/") } result = append(result, &runtime.Container{ ID: c.ID, Image: c.Image, Name: name, Status: c.Status, Ports: ports, Labels: c.Labels, }) } return result, nil } // InspectContainer inspects a container func (d *DockerRuntime) InspectContainer(ctx context.Context, containerID string) (*runtime.Container, error) { resp, err := d.client.ContainerInspect(ctx, containerID) if err != nil { return nil, fmt.Errorf("failed to inspect container %s: %w", containerID, err) } // Extract published ports var ports []int if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { for _, bindings := range resp.NetworkSettings.Ports { for _, binding := range bindings { if binding.HostPort != "" { if port, err := strconv.Atoi(binding.HostPort); err == nil { ports = append(ports, port) } } } } } // Get container name (remove leading slash) name := strings.TrimPrefix(resp.Name, "/") return &runtime.Container{ ID: resp.ID, Image: resp.Config.Image, Name: name, Status: resp.State.Status, Ports: ports, Labels: resp.Config.Labels, }, nil } // GetContainerLogs gets container logs func (d *DockerRuntime) GetContainerLogs(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) { logs, err := d.client.ContainerLogs(ctx, containerID, container.LogsOptions{ ShowStdout: true, ShowStderr: true, Follow: follow, Timestamps: true, }) if err != nil { return nil, fmt.Errorf("failed to get logs for container %s: %w", containerID, err) } return logs, nil } // PullImage pulls an image func (d *DockerRuntime) PullImage(ctx context.Context, imageRef string) error { log.Info().Str("image", imageRef).Msg("Pulling image") reader, err := d.client.ImagePull(ctx, imageRef, image.PullOptions{}) if err != nil { return fmt.Errorf("failed to pull image %s: %w", imageRef, err) } defer reader.Close() // Read the response to completion (this is required for the pull to complete) _, err = io.Copy(io.Discard, reader) if err != nil { return fmt.Errorf("failed to read pull response for image %s: %w", imageRef, err) } log.Info().Str("image", imageRef).Msg("Image pulled successfully") return nil } // RemoveImage removes an image func (d *DockerRuntime) RemoveImage(ctx context.Context, imageRef string, force bool) error { _, err := d.client.ImageRemove(ctx, imageRef, image.RemoveOptions{Force: force}) if err != nil { return fmt.Errorf("failed to remove image %s: %w", imageRef, err) } log.Info().Str("image", imageRef).Bool("force", force).Msg("Image removed") return nil } // ListImages lists images func (d *DockerRuntime) ListImages(ctx context.Context) ([]string, error) { images, err := d.client.ImageList(ctx, image.ListOptions{}) if err != nil { return nil, fmt.Errorf("failed to list images: %w", err) } var result []string for _, img := range images { for _, tag := range img.RepoTags { if tag != ":" { result = append(result, tag) } } } return result, nil } // Ping checks if Docker is responsive func (d *DockerRuntime) Ping(ctx context.Context) error { _, err := d.client.Ping(ctx) if err != nil { return fmt.Errorf("Docker ping failed: %w", err) } return nil } // Version returns Docker version func (d *DockerRuntime) Version(ctx context.Context) (string, error) { version, err := d.client.ServerVersion(ctx) if err != nil { return "", fmt.Errorf("failed to get Docker version: %w", err) } return version.Version, nil } // IsContainerRunning checks if a container is running func (d *DockerRuntime) IsContainerRunning(ctx context.Context, containerID string) (bool, error) { container, err := d.InspectContainer(ctx, containerID) if err != nil { return false, err } return container.Status == "running", nil } // GetContainerPort gets the host port for a container's internal port func (d *DockerRuntime) GetContainerPort(ctx context.Context, containerID string, internalPort int) (int, error) { resp, err := d.client.ContainerInspect(ctx, containerID) if err != nil { return 0, fmt.Errorf("failed to inspect container %s: %w", containerID, err) } if resp.NetworkSettings == nil || resp.NetworkSettings.Ports == nil { return 0, fmt.Errorf("no port mappings found for container %s", containerID) } containerPort := nat.Port(fmt.Sprintf("%d/tcp", internalPort)) bindings, exists := resp.NetworkSettings.Ports[containerPort] if !exists || len(bindings) == 0 { return 0, fmt.Errorf("port %d not mapped for container %s", internalPort, containerID) } hostPort, err := strconv.Atoi(bindings[0].HostPort) if err != nil { return 0, fmt.Errorf("invalid host port for container %s: %w", containerID, err) } return hostPort, nil } // GetImageExposedPorts gets the exposed ports from an image func (d *DockerRuntime) GetImageExposedPorts(ctx context.Context, imageRef string) ([]int, error) { imageInspect, err := d.client.ImageInspect(ctx, imageRef) if err != nil { return nil, fmt.Errorf("failed to inspect image %s: %w", imageRef, err) } var ports []int if imageInspect.Config != nil && imageInspect.Config.ExposedPorts != nil { for portSpec := range imageInspect.Config.ExposedPorts { // Parse port from format like "80/tcp" portStr := strings.Split(string(portSpec), "/")[0] if port, err := strconv.Atoi(portStr); err == nil { ports = append(ports, port) } } } // If no exposed ports found, return common web ports as fallback if len(ports) == 0 { log.Warn().Str("image", imageRef).Msg("No EXPOSE directives found in image, using common web ports") return []int{80, 8080, 3000}, nil } log.Info().Str("image", imageRef).Ints("exposed_ports", ports).Msg("Found exposed ports in image") return ports, nil } // GetContainerExposedPorts gets all exposed ports from a running container func (d *DockerRuntime) GetContainerExposedPorts(ctx context.Context, containerID string) ([]int, error) { resp, err := d.client.ContainerInspect(ctx, containerID) if err != nil { return nil, fmt.Errorf("failed to inspect container %s: %w", containerID, err) } var ports []int if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { for portSpec := range resp.NetworkSettings.Ports { // Parse port from format like "80/tcp" portStr := strings.Split(string(portSpec), "/")[0] if port, err := strconv.Atoi(portStr); err == nil { ports = append(ports, port) } } } return ports, nil } // GetContainerNetworkInfo gets container's internal IP and automatically detects the best port to use func (d *DockerRuntime) GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) { resp, err := d.client.ContainerInspect(ctx, containerID) if err != nil { return "", 0, fmt.Errorf("failed to inspect container %s: %w", containerID, err) } // Get container's internal IP address var containerIP string if resp.NetworkSettings != nil && resp.NetworkSettings.Networks != nil { // Use the first available network (usually bridge or custom network) for _, network := range resp.NetworkSettings.Networks { if network.IPAddress != "" { containerIP = network.IPAddress break } } } if containerIP == "" { return "", 0, fmt.Errorf("no IP address found for container %s", containerID) } // Get exposed ports and select the best one (Traefik-style logic) var exposedPorts []int if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { for portSpec := range resp.NetworkSettings.Ports { // Parse port from format like "80/tcp" portStr := strings.Split(string(portSpec), "/")[0] if port, err := strconv.Atoi(portStr); err == nil { exposedPorts = append(exposedPorts, port) } } } if len(exposedPorts) == 0 { return "", 0, fmt.Errorf("no exposed ports found for container %s", containerID) } // Sort ports and select the lowest one (Traefik strategy) var selectedPort int if len(exposedPorts) == 1 { selectedPort = exposedPorts[0] } else { // Find the lowest port number selectedPort = exposedPorts[0] for _, port := range exposedPorts { if port < selectedPort { selectedPort = port } } } log.Info(). Str("container_id", containerID). Str("ip", containerIP). Int("selected_port", selectedPort). Ints("available_ports", exposedPorts). Msg("Container network info detected") return containerIP, selectedPort, nil }