package runtime import ( "context" "encoding/json" "errors" "fmt" "os/exec" "strings" "time" ) // Podman implements the Runtime interface using the podman CLI. type Podman struct { Command string // path to podman binary, default "podman" } func (p *Podman) command() string { if p.Command != "" { return p.Command } return "podman" } // Pull pulls a container image. func (p *Podman) Pull(ctx context.Context, image string) error { cmd := exec.CommandContext(ctx, p.command(), "pull", image) //nolint:gosec // args built programmatically if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("podman pull %q: %w: %s", image, err, out) } return nil } // BuildRunArgs constructs the argument list for podman run. Exported for testing. func (p *Podman) BuildRunArgs(spec ContainerSpec) []string { args := []string{"run", "-d", "--name", spec.Name} if spec.Network != "" { args = append(args, "--network", spec.Network) } if spec.User != "" { args = append(args, "--user", spec.User) } if spec.Restart != "" { args = append(args, "--restart", spec.Restart) } for _, port := range spec.Ports { args = append(args, "-p", port) } for _, vol := range spec.Volumes { args = append(args, "-v", vol) } for _, env := range spec.Env { args = append(args, "-e", env) } args = append(args, spec.Image) args = append(args, spec.Cmd...) return args } // Run creates and starts a container from the given spec. func (p *Podman) Run(ctx context.Context, spec ContainerSpec) error { args := p.BuildRunArgs(spec) cmd := exec.CommandContext(ctx, p.command(), args...) //nolint:gosec // args built programmatically if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("podman run %q: %w: %s", spec.Name, err, out) } return nil } // Stop stops a running container. func (p *Podman) Stop(ctx context.Context, name string) error { cmd := exec.CommandContext(ctx, p.command(), "stop", name) //nolint:gosec // args built programmatically if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("podman stop %q: %w: %s", name, err, out) } return nil } // Remove removes a container. func (p *Podman) Remove(ctx context.Context, name string) error { cmd := exec.CommandContext(ctx, p.command(), "rm", name) //nolint:gosec // args built programmatically if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("podman rm %q: %w: %s", name, err, out) } return nil } // podmanPortBinding is a single port binding from the inspect output. type podmanPortBinding struct { HostIP string `json:"HostIp"` HostPort string `json:"HostPort"` } // podmanInspectResult is the subset of podman inspect JSON we parse. type podmanInspectResult struct { Name string `json:"Name"` Config struct { Image string `json:"Image"` Cmd []string `json:"Cmd"` User string `json:"User"` } `json:"Config"` State struct { Status string `json:"Status"` StartedAt string `json:"StartedAt"` } `json:"State"` HostConfig struct { RestartPolicy struct { Name string `json:"Name"` } `json:"RestartPolicy"` NetworkMode string `json:"NetworkMode"` } `json:"HostConfig"` NetworkSettings struct { Networks map[string]struct{} `json:"Networks"` Ports map[string][]podmanPortBinding `json:"Ports"` } `json:"NetworkSettings"` Mounts []struct { Source string `json:"Source"` Destination string `json:"Destination"` } `json:"Mounts"` } // Inspect retrieves information about a container. func (p *Podman) Inspect(ctx context.Context, name string) (ContainerInfo, error) { cmd := exec.CommandContext(ctx, p.command(), "inspect", name) //nolint:gosec // args built programmatically out, err := cmd.Output() if err != nil { return ContainerInfo{}, fmt.Errorf("podman inspect %q: %w", name, err) } var results []podmanInspectResult if err := json.Unmarshal(out, &results); err != nil { return ContainerInfo{}, fmt.Errorf("parse inspect output: %w", err) } if len(results) == 0 { return ContainerInfo{}, fmt.Errorf("podman inspect %q: no results", name) } r := results[0] info := ContainerInfo{ Name: strings.TrimPrefix(r.Name, "/"), Image: r.Config.Image, State: r.State.Status, User: r.Config.User, Restart: r.HostConfig.RestartPolicy.Name, Cmd: r.Config.Cmd, Version: ExtractVersion(r.Config.Image), } if t, err := time.Parse(time.RFC3339Nano, r.State.StartedAt); err == nil { info.Started = t } info.Network = r.HostConfig.NetworkMode if len(r.NetworkSettings.Networks) > 0 { for netName := range r.NetworkSettings.Networks { info.Network = netName break } } for containerPort, bindings := range r.NetworkSettings.Ports { for _, b := range bindings { port := strings.SplitN(containerPort, "/", 2)[0] // strip "/tcp" suffix mapping := b.HostPort + ":" + port if b.HostIP != "" && b.HostIP != "0.0.0.0" { mapping = b.HostIP + ":" + mapping } info.Ports = append(info.Ports, mapping) } } for _, m := range r.Mounts { info.Volumes = append(info.Volumes, m.Source+":"+m.Destination) } return info, nil } // Build builds a container image from a Dockerfile. func (p *Podman) Build(ctx context.Context, image, contextDir, dockerfile string) error { args := []string{"build", "-t", image, "-f", dockerfile, contextDir} cmd := exec.CommandContext(ctx, p.command(), args...) //nolint:gosec // args built programmatically cmd.Dir = contextDir if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("podman build %q: %w: %s", image, err, out) } return nil } // Push pushes a container image to a remote registry. func (p *Podman) Push(ctx context.Context, image string) error { cmd := exec.CommandContext(ctx, p.command(), "push", image) //nolint:gosec // args built programmatically if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("podman push %q: %w: %s", image, err, out) } return nil } // ImageExists checks whether an image tag exists in a remote registry. func (p *Podman) ImageExists(ctx context.Context, image string) (bool, error) { cmd := exec.CommandContext(ctx, p.command(), "manifest", "inspect", "docker://"+image) //nolint:gosec // args built programmatically if err := cmd.Run(); err != nil { // Exit code 1 means the manifest was not found. var exitErr *exec.ExitError if ok := errors.As(err, &exitErr); ok && exitErr.ExitCode() == 1 { return false, nil } return false, fmt.Errorf("podman manifest inspect %q: %w", image, err) } return true, nil } // podmanPSEntry is a single entry from podman ps --format json. type podmanPSEntry struct { Names []string `json:"Names"` Image string `json:"Image"` State string `json:"State"` Command []string `json:"Command"` } // List returns information about all containers. func (p *Podman) List(ctx context.Context) ([]ContainerInfo, error) { cmd := exec.CommandContext(ctx, p.command(), "ps", "-a", "--format", "json") //nolint:gosec // args built programmatically out, err := cmd.Output() if err != nil { return nil, fmt.Errorf("podman ps: %w", err) } var entries []podmanPSEntry if err := json.Unmarshal(out, &entries); err != nil { return nil, fmt.Errorf("parse ps output: %w", err) } infos := make([]ContainerInfo, 0, len(entries)) for _, e := range entries { name := "" if len(e.Names) > 0 { name = e.Names[0] } infos = append(infos, ContainerInfo{ Name: name, Image: e.Image, State: e.State, Version: ExtractVersion(e.Image), }) } return infos, nil }