Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions pkg/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -558,13 +558,37 @@ func (c *ContainerClient) ImagePullWithRetries(ctx context.Context, imageRef str

// Note: ImagePull does not seem to return an error if the private registries authentication fails
// so after pulling the image, check if it is loaded to confirm everything worked as expected
out, err := c.Client.ImagePull(ctx, imageRef, pullOptions)
if err != nil {
return nil, err
useDockerPull := false

// try podman api first and but fallback to docker pull API fails
// Note: Podman 4.4 was observed to have an issue pulling images via the docker API where the only reported error is:
// "write /dev/stderr: input/output error"
podmanLib := NewDefaultLibPodHTTPClient()
if podmanLib.Test(ctx) == nil {
slog.Info("Using podman API to pull image")
libpodErr := podmanLib.PullImages(ctx, imageRef, alwaysPull, PodmanPullOptions{
PullOptions: pullOptions,
Quiet: false,
})

// Don't fail as it is unclear how stable the libpod API is
// and a check for the image is done afterwards anyway
if libpodErr != nil {
slog.Warn("podman (libpod) pull images failed but error will be ignored.", "err", libpodErr)
useDockerPull = true
}
}
defer out.Close()
if _, ioErr := io.Copy(os.Stderr, out); ioErr != nil {
slog.Warn("Could not write to stderr.", "err", ioErr)

if useDockerPull {
slog.Info("Trying to pull image using docker API")
out, err := c.Client.ImagePull(ctx, imageRef, pullOptions)
if err != nil {
return nil, err
}
defer out.Close()
if _, ioErr := io.Copy(os.Stderr, out); ioErr != nil {
slog.Warn("Could not write to stderr.", "err", ioErr)
}
}

//
Expand Down Expand Up @@ -1234,12 +1258,12 @@ func (c *ContainerClient) ImagesPruneUnused(ctx context.Context) (image.PruneRep

// Note: Due to a bug in podman <= 4.8, the above call will fail, so the direct libpod is used instead
// Reference: https://github.com/containers/podman/issues/20469
// NewDefaultLibPodHTTPClient

slog.Info("Prune images failed. This is expected when using podman < 4.8.", "err", apiErr)

slog.Info("Using podman api to prune unused images")
socketAddr := findContainerEngineSocket()
libpod := NewLibPodHTTPClient(socketAddr)
report, libpodErr := libpod.PruneImages(nil)
report, libpodErr := NewDefaultLibPodHTTPClient().PruneImages(nil)

// Don't fail as it is unclear how stable the libpod api is
if libpodErr != nil {
Expand Down
87 changes: 87 additions & 0 deletions pkg/container/socket_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ package container
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"os"
"strings"

"github.com/docker/docker/api/types/image"
"github.com/google/go-querystring/query"
)

type ResponsePruneImage struct {
Expand All @@ -21,6 +26,10 @@ type SocketClient struct {
Client *http.Client
}

func NewDefaultLibPodHTTPClient() *SocketClient {
return NewLibPodHTTPClient(findContainerEngineSocket())
}

func NewLibPodHTTPClient(sock string) *SocketClient {
httpc := http.Client{
Transport: &http.Transport{
Expand All @@ -40,6 +49,19 @@ func (c *SocketClient) resolveURL(path string) string {
return strings.Join([]string{c.BaseURL, strings.TrimPrefix(path, "/")}, "/")
}

var ErrPodmanAPIError = errors.New("podman api not available")

func (c *SocketClient) Test(ctx context.Context) error {
r, err := c.Client.Get(c.resolveURL("info"))
if err != nil {
return err
}
if r.StatusCode != 200 {
return ErrPodmanAPIError
}
return nil
}

// Prune all images and return object in same format as the docker prune response
func (c *SocketClient) PruneImages(body io.Reader) (report image.PruneReport, err error) {
r, err := c.Client.Post(c.resolveURL("images/prune?all=true"), "application/json", body)
Expand Down Expand Up @@ -69,3 +91,68 @@ func (c *SocketClient) PruneImages(body io.Reader) (report image.PruneReport, er
report.SpaceReclaimed = spaceReclaimed
return
}

type PodmanAPIPullOptions struct {
AllTags *bool `url:"allTags,omitempty"`
Quiet *bool `url:"quiet,omitempty"`
Policy string `url:"policy,omitempty"`
Reference string `url:"reference"`
}

func (po *PodmanAPIPullOptions) WithPolicy(v string) *PodmanAPIPullOptions {
po.Policy = v
return po
}

func (po *PodmanAPIPullOptions) WithAllTags(v bool) *PodmanAPIPullOptions {
po.AllTags = &v
return po
}

func (po *PodmanAPIPullOptions) WithQuiet(v bool) *PodmanAPIPullOptions {
po.Quiet = &v
return po
}

type PodmanPullOptions struct {
image.PullOptions

Quiet bool
}

func (c *SocketClient) PullImages(ctx context.Context, imageRef string, alwaysPull bool, pullOptions PodmanPullOptions) error {
options := PodmanAPIPullOptions{
Reference: imageRef,
}
options.WithQuiet(pullOptions.Quiet)
if alwaysPull {
options.WithPolicy("always")
}

queryParams, err := query.Values(options)
if err != nil {
return err
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.resolveURL(fmt.Sprintf("images/pull?%s", queryParams.Encode())), nil)
if err != nil {
return err
}

if pullOptions.RegistryAuth != "" {
req.Header.Set("X-Registry-Auth", pullOptions.RegistryAuth)
}

r, err := c.Client.Do(req)
if err != nil {
return err
}

defer r.Body.Close()
if _, ioErr := io.Copy(os.Stderr, r.Body); ioErr != nil {
slog.Warn("Could not write to stderr.", "err", ioErr)
}

slog.Info("Podman API response was successful.", "status", r.Status)
return nil
}