github.com/rish1988/moby@v25.0.2+incompatible/api/server/router/image/image_routes.go (about)

     1  package image // import "github.com/docker/docker/api/server/router/image"
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"net/url"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/containerd/containerd/platforms"
    14  	"github.com/distribution/reference"
    15  	"github.com/docker/docker/api"
    16  	"github.com/docker/docker/api/server/httputils"
    17  	"github.com/docker/docker/api/types"
    18  	"github.com/docker/docker/api/types/backend"
    19  	"github.com/docker/docker/api/types/filters"
    20  	imagetypes "github.com/docker/docker/api/types/image"
    21  	"github.com/docker/docker/api/types/registry"
    22  	"github.com/docker/docker/api/types/versions"
    23  	"github.com/docker/docker/builder/remotecontext"
    24  	"github.com/docker/docker/dockerversion"
    25  	"github.com/docker/docker/errdefs"
    26  	"github.com/docker/docker/image"
    27  	"github.com/docker/docker/pkg/ioutils"
    28  	"github.com/docker/docker/pkg/progress"
    29  	"github.com/docker/docker/pkg/streamformatter"
    30  	"github.com/opencontainers/go-digest"
    31  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    32  	"github.com/pkg/errors"
    33  )
    34  
    35  // Creates an image from Pull or from Import
    36  func (ir *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    37  	if err := httputils.ParseForm(r); err != nil {
    38  		return err
    39  	}
    40  
    41  	var (
    42  		img         = r.Form.Get("fromImage")
    43  		repo        = r.Form.Get("repo")
    44  		tag         = r.Form.Get("tag")
    45  		comment     = r.Form.Get("message")
    46  		progressErr error
    47  		output      = ioutils.NewWriteFlusher(w)
    48  		platform    *ocispec.Platform
    49  	)
    50  	defer output.Close()
    51  
    52  	w.Header().Set("Content-Type", "application/json")
    53  
    54  	version := httputils.VersionFromContext(ctx)
    55  	if versions.GreaterThanOrEqualTo(version, "1.32") {
    56  		if p := r.FormValue("platform"); p != "" {
    57  			sp, err := platforms.Parse(p)
    58  			if err != nil {
    59  				return err
    60  			}
    61  			platform = &sp
    62  		}
    63  	}
    64  
    65  	if img != "" { // pull
    66  		metaHeaders := map[string][]string{}
    67  		for k, v := range r.Header {
    68  			if strings.HasPrefix(k, "X-Meta-") {
    69  				metaHeaders[k] = v
    70  			}
    71  		}
    72  
    73  		// Special case: "pull -a" may send an image name with a
    74  		// trailing :. This is ugly, but let's not break API
    75  		// compatibility.
    76  		imgName := strings.TrimSuffix(img, ":")
    77  
    78  		ref, err := reference.ParseNormalizedNamed(imgName)
    79  		if err != nil {
    80  			return errdefs.InvalidParameter(err)
    81  		}
    82  
    83  		// TODO(thaJeztah) this could use a WithTagOrDigest() utility
    84  		if tag != "" {
    85  			// The "tag" could actually be a digest.
    86  			var dgst digest.Digest
    87  			dgst, err = digest.Parse(tag)
    88  			if err == nil {
    89  				ref, err = reference.WithDigest(reference.TrimNamed(ref), dgst)
    90  			} else {
    91  				ref, err = reference.WithTag(ref, tag)
    92  			}
    93  			if err != nil {
    94  				return errdefs.InvalidParameter(err)
    95  			}
    96  		}
    97  
    98  		if err := validateRepoName(ref); err != nil {
    99  			return errdefs.Forbidden(err)
   100  		}
   101  
   102  		// For a pull it is not an error if no auth was given. Ignore invalid
   103  		// AuthConfig to increase compatibility with the existing API.
   104  		authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
   105  		progressErr = ir.backend.PullImage(ctx, ref, platform, metaHeaders, authConfig, output)
   106  	} else { // import
   107  		src := r.Form.Get("fromSrc")
   108  
   109  		tagRef, err := httputils.RepoTagReference(repo, tag)
   110  		if err != nil {
   111  			return errdefs.InvalidParameter(err)
   112  		}
   113  
   114  		if len(comment) == 0 {
   115  			comment = "Imported from " + src
   116  		}
   117  
   118  		var layerReader io.ReadCloser
   119  		defer r.Body.Close()
   120  		if src == "-" {
   121  			layerReader = r.Body
   122  		} else {
   123  			if len(strings.Split(src, "://")) == 1 {
   124  				src = "http://" + src
   125  			}
   126  			u, err := url.Parse(src)
   127  			if err != nil {
   128  				return errdefs.InvalidParameter(err)
   129  			}
   130  
   131  			resp, err := remotecontext.GetWithStatusError(u.String())
   132  			if err != nil {
   133  				return err
   134  			}
   135  			output.Write(streamformatter.FormatStatus("", "Downloading from %s", u))
   136  			progressOutput := streamformatter.NewJSONProgressOutput(output, true)
   137  			layerReader = progress.NewProgressReader(resp.Body, progressOutput, resp.ContentLength, "", "Importing")
   138  			defer layerReader.Close()
   139  		}
   140  
   141  		var id image.ID
   142  		id, progressErr = ir.backend.ImportImage(ctx, tagRef, platform, comment, layerReader, r.Form["changes"])
   143  
   144  		if progressErr == nil {
   145  			output.Write(streamformatter.FormatStatus("", id.String()))
   146  		}
   147  	}
   148  	if progressErr != nil {
   149  		if !output.Flushed() {
   150  			return progressErr
   151  		}
   152  		_, _ = output.Write(streamformatter.FormatError(progressErr))
   153  	}
   154  
   155  	return nil
   156  }
   157  
   158  func (ir *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   159  	metaHeaders := map[string][]string{}
   160  	for k, v := range r.Header {
   161  		if strings.HasPrefix(k, "X-Meta-") {
   162  			metaHeaders[k] = v
   163  		}
   164  	}
   165  	if err := httputils.ParseForm(r); err != nil {
   166  		return err
   167  	}
   168  
   169  	var authConfig *registry.AuthConfig
   170  	if authEncoded := r.Header.Get(registry.AuthHeader); authEncoded != "" {
   171  		// the new format is to handle the authConfig as a header. Ignore invalid
   172  		// AuthConfig to increase compatibility with the existing API.
   173  		authConfig, _ = registry.DecodeAuthConfig(authEncoded)
   174  	} else {
   175  		// the old format is supported for compatibility if there was no authConfig header
   176  		var err error
   177  		authConfig, err = registry.DecodeAuthConfigBody(r.Body)
   178  		if err != nil {
   179  			return errors.Wrap(err, "bad parameters and missing X-Registry-Auth")
   180  		}
   181  	}
   182  
   183  	output := ioutils.NewWriteFlusher(w)
   184  	defer output.Close()
   185  
   186  	w.Header().Set("Content-Type", "application/json")
   187  
   188  	img := vars["name"]
   189  	tag := r.Form.Get("tag")
   190  
   191  	var ref reference.Named
   192  
   193  	// Tag is empty only in case PushOptions.All is true.
   194  	if tag != "" {
   195  		r, err := httputils.RepoTagReference(img, tag)
   196  		if err != nil {
   197  			return errdefs.InvalidParameter(err)
   198  		}
   199  		ref = r
   200  	} else {
   201  		r, err := reference.ParseNormalizedNamed(img)
   202  		if err != nil {
   203  			return errdefs.InvalidParameter(err)
   204  		}
   205  		ref = r
   206  	}
   207  
   208  	if err := ir.backend.PushImage(ctx, ref, metaHeaders, authConfig, output); err != nil {
   209  		if !output.Flushed() {
   210  			return err
   211  		}
   212  		_, _ = output.Write(streamformatter.FormatError(err))
   213  	}
   214  	return nil
   215  }
   216  
   217  func (ir *imageRouter) getImagesGet(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   218  	if err := httputils.ParseForm(r); err != nil {
   219  		return err
   220  	}
   221  
   222  	w.Header().Set("Content-Type", "application/x-tar")
   223  
   224  	output := ioutils.NewWriteFlusher(w)
   225  	defer output.Close()
   226  	var names []string
   227  	if name, ok := vars["name"]; ok {
   228  		names = []string{name}
   229  	} else {
   230  		names = r.Form["names"]
   231  	}
   232  
   233  	if err := ir.backend.ExportImage(ctx, names, output); err != nil {
   234  		if !output.Flushed() {
   235  			return err
   236  		}
   237  		_, _ = output.Write(streamformatter.FormatError(err))
   238  	}
   239  	return nil
   240  }
   241  
   242  func (ir *imageRouter) postImagesLoad(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   243  	if err := httputils.ParseForm(r); err != nil {
   244  		return err
   245  	}
   246  	quiet := httputils.BoolValueOrDefault(r, "quiet", true)
   247  
   248  	w.Header().Set("Content-Type", "application/json")
   249  
   250  	output := ioutils.NewWriteFlusher(w)
   251  	defer output.Close()
   252  	if err := ir.backend.LoadImage(ctx, r.Body, output, quiet); err != nil {
   253  		_, _ = output.Write(streamformatter.FormatError(err))
   254  	}
   255  	return nil
   256  }
   257  
   258  type missingImageError struct{}
   259  
   260  func (missingImageError) Error() string {
   261  	return "image name cannot be blank"
   262  }
   263  
   264  func (missingImageError) InvalidParameter() {}
   265  
   266  func (ir *imageRouter) deleteImages(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   267  	if err := httputils.ParseForm(r); err != nil {
   268  		return err
   269  	}
   270  
   271  	name := vars["name"]
   272  
   273  	if strings.TrimSpace(name) == "" {
   274  		return missingImageError{}
   275  	}
   276  
   277  	force := httputils.BoolValue(r, "force")
   278  	prune := !httputils.BoolValue(r, "noprune")
   279  
   280  	list, err := ir.backend.ImageDelete(ctx, name, force, prune)
   281  	if err != nil {
   282  		return err
   283  	}
   284  
   285  	return httputils.WriteJSON(w, http.StatusOK, list)
   286  }
   287  
   288  func (ir *imageRouter) getImagesByName(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   289  	img, err := ir.backend.GetImage(ctx, vars["name"], backend.GetImageOpts{Details: true})
   290  	if err != nil {
   291  		return err
   292  	}
   293  
   294  	imageInspect, err := ir.toImageInspect(img)
   295  	if err != nil {
   296  		return err
   297  	}
   298  
   299  	version := httputils.VersionFromContext(ctx)
   300  	if versions.LessThan(version, "1.44") {
   301  		imageInspect.VirtualSize = imageInspect.Size //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.44.
   302  	}
   303  	return httputils.WriteJSON(w, http.StatusOK, imageInspect)
   304  }
   305  
   306  func (ir *imageRouter) toImageInspect(img *image.Image) (*types.ImageInspect, error) {
   307  	var repoTags, repoDigests []string
   308  	for _, ref := range img.Details.References {
   309  		switch ref.(type) {
   310  		case reference.NamedTagged:
   311  			repoTags = append(repoTags, reference.FamiliarString(ref))
   312  		case reference.Canonical:
   313  			repoDigests = append(repoDigests, reference.FamiliarString(ref))
   314  		}
   315  	}
   316  
   317  	comment := img.Comment
   318  	if len(comment) == 0 && len(img.History) > 0 {
   319  		comment = img.History[len(img.History)-1].Comment
   320  	}
   321  
   322  	// Make sure we output empty arrays instead of nil.
   323  	if repoTags == nil {
   324  		repoTags = []string{}
   325  	}
   326  	if repoDigests == nil {
   327  		repoDigests = []string{}
   328  	}
   329  
   330  	var created string
   331  	if img.Created != nil {
   332  		created = img.Created.Format(time.RFC3339Nano)
   333  	}
   334  
   335  	return &types.ImageInspect{
   336  		ID:              img.ID().String(),
   337  		RepoTags:        repoTags,
   338  		RepoDigests:     repoDigests,
   339  		Parent:          img.Parent.String(),
   340  		Comment:         comment,
   341  		Created:         created,
   342  		Container:       img.Container,        //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.45.
   343  		ContainerConfig: &img.ContainerConfig, //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.45.
   344  		DockerVersion:   img.DockerVersion,
   345  		Author:          img.Author,
   346  		Config:          img.Config,
   347  		Architecture:    img.Architecture,
   348  		Variant:         img.Variant,
   349  		Os:              img.OperatingSystem(),
   350  		OsVersion:       img.OSVersion,
   351  		Size:            img.Details.Size,
   352  		GraphDriver: types.GraphDriverData{
   353  			Name: img.Details.Driver,
   354  			Data: img.Details.Metadata,
   355  		},
   356  		RootFS: rootFSToAPIType(img.RootFS),
   357  		Metadata: imagetypes.Metadata{
   358  			LastTagTime: img.Details.LastUpdated,
   359  		},
   360  	}, nil
   361  }
   362  
   363  func rootFSToAPIType(rootfs *image.RootFS) types.RootFS {
   364  	var layers []string
   365  	for _, l := range rootfs.DiffIDs {
   366  		layers = append(layers, l.String())
   367  	}
   368  	return types.RootFS{
   369  		Type:   rootfs.Type,
   370  		Layers: layers,
   371  	}
   372  }
   373  
   374  func (ir *imageRouter) getImagesJSON(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   375  	if err := httputils.ParseForm(r); err != nil {
   376  		return err
   377  	}
   378  
   379  	imageFilters, err := filters.FromJSON(r.Form.Get("filters"))
   380  	if err != nil {
   381  		return err
   382  	}
   383  
   384  	version := httputils.VersionFromContext(ctx)
   385  	if versions.LessThan(version, "1.41") {
   386  		// NOTE: filter is a shell glob string applied to repository names.
   387  		filterParam := r.Form.Get("filter")
   388  		if filterParam != "" {
   389  			imageFilters.Add("reference", filterParam)
   390  		}
   391  	}
   392  
   393  	var sharedSize bool
   394  	if versions.GreaterThanOrEqualTo(version, "1.42") {
   395  		// NOTE: Support for the "shared-size" parameter was added in API 1.42.
   396  		sharedSize = httputils.BoolValue(r, "shared-size")
   397  	}
   398  
   399  	images, err := ir.backend.Images(ctx, imagetypes.ListOptions{
   400  		All:        httputils.BoolValue(r, "all"),
   401  		Filters:    imageFilters,
   402  		SharedSize: sharedSize,
   403  	})
   404  	if err != nil {
   405  		return err
   406  	}
   407  
   408  	useNone := versions.LessThan(version, "1.43")
   409  	withVirtualSize := versions.LessThan(version, "1.44")
   410  	for _, img := range images {
   411  		if useNone {
   412  			if len(img.RepoTags) == 0 && len(img.RepoDigests) == 0 {
   413  				img.RepoTags = append(img.RepoTags, "<none>:<none>")
   414  				img.RepoDigests = append(img.RepoDigests, "<none>@<none>")
   415  			}
   416  		} else {
   417  			if img.RepoTags == nil {
   418  				img.RepoTags = []string{}
   419  			}
   420  			if img.RepoDigests == nil {
   421  				img.RepoDigests = []string{}
   422  			}
   423  		}
   424  		if withVirtualSize {
   425  			img.VirtualSize = img.Size //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.44.
   426  		}
   427  	}
   428  
   429  	return httputils.WriteJSON(w, http.StatusOK, images)
   430  }
   431  
   432  func (ir *imageRouter) getImagesHistory(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   433  	history, err := ir.backend.ImageHistory(ctx, vars["name"])
   434  	if err != nil {
   435  		return err
   436  	}
   437  
   438  	return httputils.WriteJSON(w, http.StatusOK, history)
   439  }
   440  
   441  func (ir *imageRouter) postImagesTag(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   442  	if err := httputils.ParseForm(r); err != nil {
   443  		return err
   444  	}
   445  
   446  	ref, err := httputils.RepoTagReference(r.Form.Get("repo"), r.Form.Get("tag"))
   447  	if ref == nil || err != nil {
   448  		return errdefs.InvalidParameter(err)
   449  	}
   450  
   451  	refName := reference.FamiliarName(ref)
   452  	if refName == string(digest.Canonical) {
   453  		return errdefs.InvalidParameter(errors.New("refusing to create an ambiguous tag using digest algorithm as name"))
   454  	}
   455  
   456  	img, err := ir.backend.GetImage(ctx, vars["name"], backend.GetImageOpts{})
   457  	if err != nil {
   458  		return errdefs.NotFound(err)
   459  	}
   460  
   461  	if err := ir.backend.TagImage(ctx, img.ID(), ref); err != nil {
   462  		return err
   463  	}
   464  	w.WriteHeader(http.StatusCreated)
   465  	return nil
   466  }
   467  
   468  func (ir *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   469  	if err := httputils.ParseForm(r); err != nil {
   470  		return err
   471  	}
   472  
   473  	var limit int
   474  	if r.Form.Get("limit") != "" {
   475  		var err error
   476  		limit, err = strconv.Atoi(r.Form.Get("limit"))
   477  		if err != nil || limit < 0 {
   478  			return errdefs.InvalidParameter(errors.Wrap(err, "invalid limit specified"))
   479  		}
   480  	}
   481  	searchFilters, err := filters.FromJSON(r.Form.Get("filters"))
   482  	if err != nil {
   483  		return err
   484  	}
   485  
   486  	// For a search it is not an error if no auth was given. Ignore invalid
   487  	// AuthConfig to increase compatibility with the existing API.
   488  	authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
   489  
   490  	headers := http.Header{}
   491  	for k, v := range r.Header {
   492  		k = http.CanonicalHeaderKey(k)
   493  		if strings.HasPrefix(k, "X-Meta-") {
   494  			headers[k] = v
   495  		}
   496  	}
   497  	headers.Set("User-Agent", dockerversion.DockerUserAgent(ctx))
   498  	res, err := ir.searcher.Search(ctx, searchFilters, r.Form.Get("term"), limit, authConfig, headers)
   499  	if err != nil {
   500  		return err
   501  	}
   502  	return httputils.WriteJSON(w, http.StatusOK, res)
   503  }
   504  
   505  func (ir *imageRouter) postImagesPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   506  	if err := httputils.ParseForm(r); err != nil {
   507  		return err
   508  	}
   509  
   510  	pruneFilters, err := filters.FromJSON(r.Form.Get("filters"))
   511  	if err != nil {
   512  		return err
   513  	}
   514  
   515  	pruneReport, err := ir.backend.ImagesPrune(ctx, pruneFilters)
   516  	if err != nil {
   517  		return err
   518  	}
   519  	return httputils.WriteJSON(w, http.StatusOK, pruneReport)
   520  }
   521  
   522  // validateRepoName validates the name of a repository.
   523  func validateRepoName(name reference.Named) error {
   524  	familiarName := reference.FamiliarName(name)
   525  	if familiarName == api.NoBaseImageSpecifier {
   526  		return fmt.Errorf("'%s' is a reserved name", familiarName)
   527  	}
   528  	return nil
   529  }