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/v3/libpod" "github.com/containers/podman/v3/libpod/image" "github.com/containers/podman/v3/pkg/api/handlers/utils" "github.com/containers/podman/v3/pkg/auth" "github.com/containers/podman/v3/pkg/channel" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/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"` OS string `schema:"OS"` Arch string `schema:"Arch"` Variant string `schema:"Variant"` 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, err) 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, key, err := auth.GetCredentials(r) if err != nil { utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String())) return } defer auth.RemoveAuthfile(authfile) // Setup the registry options dockerRegistryOptions := image.DockerRegistryOptions{ DockerRegistryCreds: authConf, OSChoice: query.OS, ArchitectureChoice: query.Arch, VariantChoice: query.Variant, } 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)) defer writer.Close() stderr := channel.NewWriter(make(chan []byte)) 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 { // Send all image id's pulled in 'images' stanza report.Images = images if err := enc.Encode(report); err != nil { logrus.Warnf("Failed to json encode error %q", err.Error()) } report.Images = nil // Pull last ID from list and publish in 'id' stanza. This maintains previous API contract report.ID = images[len(images)-1] 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 } } }