Skip to content

Commit

Permalink
Refactor remote pull to provide progress
Browse files Browse the repository at this point in the history
podman and podman-remote do not exactly match as the lower layer code
checks if the output is destined for a  TTY before creating the progress
bars.  A future PR for containers/images could change this behavior.

Fixes containers#7543

Tested with:

$ (echo '# start'; podman-remote pull nginx ) 2>&1 | ts '[%Y-%m-%d %H:%M:%.S]'
$ (echo '# start'; podman pull nginx ) 2>&1 | ts '[%Y-%m-%d %H:%M:%.S]'

Signed-off-by: Jhon Honce <[email protected]>
  • Loading branch information
jwhonce committed Sep 16, 2020
1 parent acf86ef commit 222cf74
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 179 deletions.
13 changes: 5 additions & 8 deletions cmd/podman/images/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ var (
// child of the images command.
imagesPullCmd = &cobra.Command{
Use: pullCmd.Use,
Args: pullCmd.Args,
Short: pullCmd.Short,
Long: pullCmd.Long,
RunE: pullCmd.RunE,
Args: cobra.ExactArgs(1),
Example: `podman image pull imageName
podman image pull fedora:latest`,
}
Expand Down Expand Up @@ -77,21 +77,18 @@ func init() {
// pullFlags set the flags for the pull command.
func pullFlags(flags *pflag.FlagSet) {
flags.BoolVar(&pullOptions.AllTags, "all-tags", false, "All tagged images in the repository will be pulled")
flags.StringVar(&pullOptions.Authfile, "authfile", auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override")
flags.StringVar(&pullOptions.CertDir, "cert-dir", "", "`Pathname` of a directory containing TLS certificates and keys")
flags.StringVar(&pullOptions.CredentialsCLI, "creds", "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry")
flags.StringVar(&pullOptions.OverrideArch, "override-arch", "", "Use `ARCH` instead of the architecture of the machine for choosing images")
flags.StringVar(&pullOptions.OverrideOS, "override-os", "", "Use `OS` instead of the running OS for choosing images")
flags.StringVar(&pullOptions.OverrideVariant, "override-variant", "", " use VARIANT instead of the running architecture variant for choosing images")
flags.Bool("disable-content-trust", false, "This is a Docker specific option and is a NOOP")
flags.BoolVarP(&pullOptions.Quiet, "quiet", "q", false, "Suppress output information when pulling images")
flags.StringVar(&pullOptions.SignaturePolicy, "signature-policy", "", "`Pathname` of signature policy file (not usually used)")
flags.BoolVar(&pullOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries")

if registry.IsRemote() {
_ = flags.MarkHidden("authfile")
_ = flags.MarkHidden("cert-dir")
_ = flags.MarkHidden("tls-verify")
if !registry.IsRemote() {
flags.StringVar(&pullOptions.Authfile, "authfile", auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override")
flags.StringVar(&pullOptions.CertDir, "cert-dir", "", "`Pathname` of a directory containing TLS certificates and keys")
flags.BoolVar(&pullOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries")
}
_ = flags.MarkHidden("signature-policy")
}
Expand Down
120 changes: 0 additions & 120 deletions pkg/api/handlers/libpod/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import (
"strings"

"github.com/containers/buildah"
"github.com/containers/image/v5/docker"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/manifest"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v2/libpod"
Expand All @@ -25,7 +23,6 @@ import (
"github.com/containers/podman/v2/pkg/domain/entities"
"github.com/containers/podman/v2/pkg/domain/infra/abi"
"github.com/containers/podman/v2/pkg/errorhandling"
"github.com/containers/podman/v2/pkg/util"
utils2 "github.com/containers/podman/v2/utils"
"github.com/gorilla/schema"
"github.com/pkg/errors"
Expand Down Expand Up @@ -400,123 +397,6 @@ func ImagesImport(w http.ResponseWriter, r *http.Request) {
utils.WriteResponse(w, http.StatusOK, entities.ImageImportReport{Id: importedImage})
}

// ImagesPull is the v2 libpod endpoint for pulling images. Note that the
// mandatory `reference` must be a reference to a registry (i.e., of docker
// transport or be normalized to one). Other transports are rejected as they
// do not make sense in a remote context.
func ImagesPull(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value("runtime").(*libpod.Runtime)
decoder := r.Context().Value("decoder").(*schema.Decoder)
query := struct {
Reference string `schema:"reference"`
OverrideOS string `schema:"overrideOS"`
OverrideArch string `schema:"overrideArch"`
OverrideVariant string `schema:"overrideVariant"`
TLSVerify bool `schema:"tlsVerify"`
AllTags bool `schema:"allTags"`
}{
TLSVerify: true,
}

if err := decoder.Decode(&query, r.URL.Query()); err != nil {
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
return
}

if len(query.Reference) == 0 {
utils.InternalServerError(w, errors.New("reference parameter cannot be empty"))
return
}

imageRef, err := utils.ParseDockerReference(query.Reference)
if err != nil {
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
errors.Wrapf(err, "image destination %q is not a docker-transport reference", query.Reference))
return
}

// Trim the docker-transport prefix.
rawImage := strings.TrimPrefix(query.Reference, fmt.Sprintf("%s://", docker.Transport.Name()))

// all-tags doesn't work with a tagged reference, so let's check early
namedRef, err := reference.Parse(rawImage)
if err != nil {
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
errors.Wrapf(err, "error parsing reference %q", rawImage))
return
}
if _, isTagged := namedRef.(reference.Tagged); isTagged && query.AllTags {
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
errors.Errorf("reference %q must not have a tag for all-tags", rawImage))
return
}

authConf, authfile, err := auth.GetCredentials(r)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse %q header for %s", auth.XRegistryAuthHeader, r.URL.String()))
return
}
defer auth.RemoveAuthfile(authfile)

// Setup the registry options
dockerRegistryOptions := image.DockerRegistryOptions{
DockerRegistryCreds: authConf,
OSChoice: query.OverrideOS,
ArchitectureChoice: query.OverrideArch,
VariantChoice: query.OverrideVariant,
}
if _, found := r.URL.Query()["tlsVerify"]; found {
dockerRegistryOptions.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
}

sys := runtime.SystemContext()
if sys == nil {
sys = image.GetSystemContext("", authfile, false)
}
dockerRegistryOptions.DockerCertPath = sys.DockerCertPath
sys.DockerAuthConfig = authConf

// Prepare the images we want to pull
imagesToPull := []string{}
res := []handlers.LibpodImagesPullReport{}
imageName := namedRef.String()

if !query.AllTags {
imagesToPull = append(imagesToPull, imageName)
} else {
tags, err := docker.GetRepositoryTags(context.Background(), sys, imageRef)
if err != nil {
utils.InternalServerError(w, errors.Wrap(err, "error getting repository tags"))
return
}
for _, tag := range tags {
imagesToPull = append(imagesToPull, fmt.Sprintf("%s:%s", imageName, tag))
}
}

// Finally pull the images
for _, img := range imagesToPull {
newImage, err := runtime.ImageRuntime().New(
context.Background(),
img,
"",
authfile,
os.Stderr,
&dockerRegistryOptions,
image.SigningOptions{},
nil,
util.PullImageAlways)
if err != nil {
utils.InternalServerError(w, err)
return
}
res = append(res, handlers.LibpodImagesPullReport{ID: newImage.ID()})
}

utils.WriteResponse(w, http.StatusOK, res)
}

// PushImage is the handler for the compat http endpoint for pushing images.
func PushImage(w http.ResponseWriter, r *http.Request) {
decoder := r.Context().Value("decoder").(*schema.Decoder)
Expand Down
193 changes: 193 additions & 0 deletions pkg/api/handlers/libpod/images_pull.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package libpod

import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"

"github.com/containers/image/v5/docker"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v2/libpod"
"github.com/containers/podman/v2/libpod/image"
"github.com/containers/podman/v2/pkg/api/handlers/utils"
"github.com/containers/podman/v2/pkg/auth"
"github.com/containers/podman/v2/pkg/channel"
"github.com/containers/podman/v2/pkg/domain/entities"
"github.com/containers/podman/v2/pkg/util"
"github.com/gorilla/schema"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

// ImagesPull is the v2 libpod endpoint for pulling images. Note that the
// mandatory `reference` must be a reference to a registry (i.e., of docker
// transport or be normalized to one). Other transports are rejected as they
// do not make sense in a remote context.
func ImagesPull(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value("runtime").(*libpod.Runtime)
decoder := r.Context().Value("decoder").(*schema.Decoder)
query := struct {
Reference string `schema:"reference"`
OverrideOS string `schema:"overrideOS"`
OverrideArch string `schema:"overrideArch"`
OverrideVariant string `schema:"overrideVariant"`
TLSVerify bool `schema:"tlsVerify"`
AllTags bool `schema:"allTags"`
}{
TLSVerify: true,
}

if err := decoder.Decode(&query, r.URL.Query()); err != nil {
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
return
}

if len(query.Reference) == 0 {
utils.InternalServerError(w, errors.New("reference parameter cannot be empty"))
return
}

imageRef, err := utils.ParseDockerReference(query.Reference)
if err != nil {
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
errors.Wrapf(err, "image destination %q is not a docker-transport reference", query.Reference))
return
}

// Trim the docker-transport prefix.
rawImage := strings.TrimPrefix(query.Reference, fmt.Sprintf("%s://", docker.Transport.Name()))

// all-tags doesn't work with a tagged reference, so let's check early
namedRef, err := reference.Parse(rawImage)
if err != nil {
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
errors.Wrapf(err, "error parsing reference %q", rawImage))
return
}
if _, isTagged := namedRef.(reference.Tagged); isTagged && query.AllTags {
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
errors.Errorf("reference %q must not have a tag for all-tags", rawImage))
return
}

authConf, authfile, err := auth.GetCredentials(r)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse %q header for %s", auth.XRegistryAuthHeader, r.URL.String()))
return
}
defer auth.RemoveAuthfile(authfile)

// Setup the registry options
dockerRegistryOptions := image.DockerRegistryOptions{
DockerRegistryCreds: authConf,
OSChoice: query.OverrideOS,
ArchitectureChoice: query.OverrideArch,
VariantChoice: query.OverrideVariant,
}
if _, found := r.URL.Query()["tlsVerify"]; found {
dockerRegistryOptions.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
}

sys := runtime.SystemContext()
if sys == nil {
sys = image.GetSystemContext("", authfile, false)
}
dockerRegistryOptions.DockerCertPath = sys.DockerCertPath
sys.DockerAuthConfig = authConf

// Prepare the images we want to pull
imagesToPull := []string{}
imageName := namedRef.String()

if !query.AllTags {
imagesToPull = append(imagesToPull, imageName)
} else {
tags, err := docker.GetRepositoryTags(context.Background(), sys, imageRef)
if err != nil {
utils.InternalServerError(w, errors.Wrap(err, "error getting repository tags"))
return
}
for _, tag := range tags {
imagesToPull = append(imagesToPull, fmt.Sprintf("%s:%s", imageName, tag))
}
}

writer := channel.NewWriter(make(chan []byte, 1))
defer writer.Close()

stderr := channel.NewWriter(make(chan []byte, 1))
defer stderr.Close()

images := make([]string, 0, len(imagesToPull))
runCtx, cancel := context.WithCancel(context.Background())
go func(imgs []string) {
defer cancel()
// Finally pull the images
for _, img := range imgs {
newImage, err := runtime.ImageRuntime().New(
runCtx,
img,
"",
authfile,
writer,
&dockerRegistryOptions,
image.SigningOptions{},
nil,
util.PullImageAlways)
if err != nil {
stderr.Write([]byte(err.Error() + "\n"))
} else {
images = append(images, newImage.ID())
}
}
}(imagesToPull)

flush := func() {
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
}

w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "application/json")
flush()

enc := json.NewEncoder(w)
enc.SetEscapeHTML(true)
var failed bool
loop: // break out of for/select infinite loop
for {
var report entities.ImagePullReport
select {
case e := <-writer.Chan():
report.Stream = string(e)
if err := enc.Encode(report); err != nil {
stderr.Write([]byte(err.Error()))
}
flush()
case e := <-stderr.Chan():
failed = true
report.Error = string(e)
if err := enc.Encode(report); err != nil {
logrus.Warnf("Failed to json encode error %q", err.Error())
}
flush()
case <-runCtx.Done():
if !failed {
report.Images = images
if err := enc.Encode(report); err != nil {
logrus.Warnf("Failed to json encode error %q", err.Error())
}
flush()
}
break loop // break out of for/select infinite loop
case <-r.Context().Done():
// Client has closed connection
break loop // break out of for/select infinite loop
}
}
}
Loading

0 comments on commit 222cf74

Please sign in to comment.