github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/pkg/api/handlers/libpod/images_pull.go (about)

     1  package libpod
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/http"
     8  	"strings"
     9  
    10  	"github.com/containers/image/v5/docker"
    11  	"github.com/containers/image/v5/docker/reference"
    12  	"github.com/containers/image/v5/types"
    13  	"github.com/containers/podman/v2/libpod"
    14  	"github.com/containers/podman/v2/libpod/image"
    15  	"github.com/containers/podman/v2/pkg/api/handlers/utils"
    16  	"github.com/containers/podman/v2/pkg/auth"
    17  	"github.com/containers/podman/v2/pkg/channel"
    18  	"github.com/containers/podman/v2/pkg/domain/entities"
    19  	"github.com/containers/podman/v2/pkg/util"
    20  	"github.com/gorilla/schema"
    21  	"github.com/pkg/errors"
    22  	"github.com/sirupsen/logrus"
    23  )
    24  
    25  // ImagesPull is the v2 libpod endpoint for pulling images.  Note that the
    26  // mandatory `reference` must be a reference to a registry (i.e., of docker
    27  // transport or be normalized to one).  Other transports are rejected as they
    28  // do not make sense in a remote context.
    29  func ImagesPull(w http.ResponseWriter, r *http.Request) {
    30  	runtime := r.Context().Value("runtime").(*libpod.Runtime)
    31  	decoder := r.Context().Value("decoder").(*schema.Decoder)
    32  	query := struct {
    33  		Reference       string `schema:"reference"`
    34  		OverrideOS      string `schema:"overrideOS"`
    35  		OverrideArch    string `schema:"overrideArch"`
    36  		OverrideVariant string `schema:"overrideVariant"`
    37  		TLSVerify       bool   `schema:"tlsVerify"`
    38  		AllTags         bool   `schema:"allTags"`
    39  	}{
    40  		TLSVerify: true,
    41  	}
    42  
    43  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
    44  		utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
    45  			errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
    46  		return
    47  	}
    48  
    49  	if len(query.Reference) == 0 {
    50  		utils.InternalServerError(w, errors.New("reference parameter cannot be empty"))
    51  		return
    52  	}
    53  
    54  	imageRef, err := utils.ParseDockerReference(query.Reference)
    55  	if err != nil {
    56  		utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err)
    57  		return
    58  	}
    59  
    60  	// Trim the docker-transport prefix.
    61  	rawImage := strings.TrimPrefix(query.Reference, fmt.Sprintf("%s://", docker.Transport.Name()))
    62  
    63  	// all-tags doesn't work with a tagged reference, so let's check early
    64  	namedRef, err := reference.Parse(rawImage)
    65  	if err != nil {
    66  		utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
    67  			errors.Wrapf(err, "error parsing reference %q", rawImage))
    68  		return
    69  	}
    70  	if _, isTagged := namedRef.(reference.Tagged); isTagged && query.AllTags {
    71  		utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
    72  			errors.Errorf("reference %q must not have a tag for all-tags", rawImage))
    73  		return
    74  	}
    75  
    76  	authConf, authfile, key, err := auth.GetCredentials(r)
    77  	if err != nil {
    78  		utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String()))
    79  		return
    80  	}
    81  	defer auth.RemoveAuthfile(authfile)
    82  
    83  	// Setup the registry options
    84  	dockerRegistryOptions := image.DockerRegistryOptions{
    85  		DockerRegistryCreds: authConf,
    86  		OSChoice:            query.OverrideOS,
    87  		ArchitectureChoice:  query.OverrideArch,
    88  		VariantChoice:       query.OverrideVariant,
    89  	}
    90  	if _, found := r.URL.Query()["tlsVerify"]; found {
    91  		dockerRegistryOptions.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
    92  	}
    93  
    94  	sys := runtime.SystemContext()
    95  	if sys == nil {
    96  		sys = image.GetSystemContext("", authfile, false)
    97  	}
    98  	dockerRegistryOptions.DockerCertPath = sys.DockerCertPath
    99  	sys.DockerAuthConfig = authConf
   100  
   101  	// Prepare the images we want to pull
   102  	imagesToPull := []string{}
   103  	imageName := namedRef.String()
   104  
   105  	if !query.AllTags {
   106  		imagesToPull = append(imagesToPull, imageName)
   107  	} else {
   108  		tags, err := docker.GetRepositoryTags(context.Background(), sys, imageRef)
   109  		if err != nil {
   110  			utils.InternalServerError(w, errors.Wrap(err, "error getting repository tags"))
   111  			return
   112  		}
   113  		for _, tag := range tags {
   114  			imagesToPull = append(imagesToPull, fmt.Sprintf("%s:%s", imageName, tag))
   115  		}
   116  	}
   117  
   118  	writer := channel.NewWriter(make(chan []byte, 1))
   119  	defer writer.Close()
   120  
   121  	stderr := channel.NewWriter(make(chan []byte, 1))
   122  	defer stderr.Close()
   123  
   124  	images := make([]string, 0, len(imagesToPull))
   125  	runCtx, cancel := context.WithCancel(context.Background())
   126  	go func(imgs []string) {
   127  		defer cancel()
   128  		// Finally pull the images
   129  		for _, img := range imgs {
   130  			newImage, err := runtime.ImageRuntime().New(
   131  				runCtx,
   132  				img,
   133  				"",
   134  				authfile,
   135  				writer,
   136  				&dockerRegistryOptions,
   137  				image.SigningOptions{},
   138  				nil,
   139  				util.PullImageAlways)
   140  			if err != nil {
   141  				stderr.Write([]byte(err.Error() + "\n"))
   142  			} else {
   143  				images = append(images, newImage.ID())
   144  			}
   145  		}
   146  	}(imagesToPull)
   147  
   148  	flush := func() {
   149  		if flusher, ok := w.(http.Flusher); ok {
   150  			flusher.Flush()
   151  		}
   152  	}
   153  
   154  	w.WriteHeader(http.StatusOK)
   155  	w.Header().Add("Content-Type", "application/json")
   156  	flush()
   157  
   158  	enc := json.NewEncoder(w)
   159  	enc.SetEscapeHTML(true)
   160  	var failed bool
   161  loop: // break out of for/select infinite loop
   162  	for {
   163  		var report entities.ImagePullReport
   164  		select {
   165  		case e := <-writer.Chan():
   166  			report.Stream = string(e)
   167  			if err := enc.Encode(report); err != nil {
   168  				stderr.Write([]byte(err.Error()))
   169  			}
   170  			flush()
   171  		case e := <-stderr.Chan():
   172  			failed = true
   173  			report.Error = string(e)
   174  			if err := enc.Encode(report); err != nil {
   175  				logrus.Warnf("Failed to json encode error %q", err.Error())
   176  			}
   177  			flush()
   178  		case <-runCtx.Done():
   179  			if !failed {
   180  				// Send all image id's pulled in 'images' stanza
   181  				report.Images = images
   182  				if err := enc.Encode(report); err != nil {
   183  					logrus.Warnf("Failed to json encode error %q", err.Error())
   184  				}
   185  
   186  				report.Images = nil
   187  				// Pull last ID from list and publish in 'id' stanza.  This maintains previous API contract
   188  				report.ID = images[len(images)-1]
   189  				if err := enc.Encode(report); err != nil {
   190  					logrus.Warnf("Failed to json encode error %q", err.Error())
   191  				}
   192  
   193  				flush()
   194  			}
   195  			break loop // break out of for/select infinite loop
   196  		case <-r.Context().Done():
   197  			// Client has closed connection
   198  			break loop // break out of for/select infinite loop
   199  		}
   200  	}
   201  }