github.com/toplink-cn/moby@v0.0.0-20240305205811-460b4aebdf81/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  		if imageInspect.Created == "" {
   304  			// backwards compatibility for Created not existing returning "0001-01-01T00:00:00Z"
   305  			// https://github.com/moby/moby/issues/47368
   306  			imageInspect.Created = time.Time{}.Format(time.RFC3339Nano)
   307  		}
   308  	}
   309  	return httputils.WriteJSON(w, http.StatusOK, imageInspect)
   310  }
   311  
   312  func (ir *imageRouter) toImageInspect(img *image.Image) (*types.ImageInspect, error) {
   313  	var repoTags, repoDigests []string
   314  	for _, ref := range img.Details.References {
   315  		switch ref.(type) {
   316  		case reference.NamedTagged:
   317  			repoTags = append(repoTags, reference.FamiliarString(ref))
   318  		case reference.Canonical:
   319  			repoDigests = append(repoDigests, reference.FamiliarString(ref))
   320  		}
   321  	}
   322  
   323  	comment := img.Comment
   324  	if len(comment) == 0 && len(img.History) > 0 {
   325  		comment = img.History[len(img.History)-1].Comment
   326  	}
   327  
   328  	// Make sure we output empty arrays instead of nil.
   329  	if repoTags == nil {
   330  		repoTags = []string{}
   331  	}
   332  	if repoDigests == nil {
   333  		repoDigests = []string{}
   334  	}
   335  
   336  	var created string
   337  	if img.Created != nil {
   338  		created = img.Created.Format(time.RFC3339Nano)
   339  	}
   340  
   341  	return &types.ImageInspect{
   342  		ID:              img.ID().String(),
   343  		RepoTags:        repoTags,
   344  		RepoDigests:     repoDigests,
   345  		Parent:          img.Parent.String(),
   346  		Comment:         comment,
   347  		Created:         created,
   348  		Container:       img.Container,        //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.45.
   349  		ContainerConfig: &img.ContainerConfig, //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.45.
   350  		DockerVersion:   img.DockerVersion,
   351  		Author:          img.Author,
   352  		Config:          img.Config,
   353  		Architecture:    img.Architecture,
   354  		Variant:         img.Variant,
   355  		Os:              img.OperatingSystem(),
   356  		OsVersion:       img.OSVersion,
   357  		Size:            img.Details.Size,
   358  		GraphDriver: types.GraphDriverData{
   359  			Name: img.Details.Driver,
   360  			Data: img.Details.Metadata,
   361  		},
   362  		RootFS: rootFSToAPIType(img.RootFS),
   363  		Metadata: imagetypes.Metadata{
   364  			LastTagTime: img.Details.LastUpdated,
   365  		},
   366  	}, nil
   367  }
   368  
   369  func rootFSToAPIType(rootfs *image.RootFS) types.RootFS {
   370  	var layers []string
   371  	for _, l := range rootfs.DiffIDs {
   372  		layers = append(layers, l.String())
   373  	}
   374  	return types.RootFS{
   375  		Type:   rootfs.Type,
   376  		Layers: layers,
   377  	}
   378  }
   379  
   380  func (ir *imageRouter) getImagesJSON(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   381  	if err := httputils.ParseForm(r); err != nil {
   382  		return err
   383  	}
   384  
   385  	imageFilters, err := filters.FromJSON(r.Form.Get("filters"))
   386  	if err != nil {
   387  		return err
   388  	}
   389  
   390  	version := httputils.VersionFromContext(ctx)
   391  	if versions.LessThan(version, "1.41") {
   392  		// NOTE: filter is a shell glob string applied to repository names.
   393  		filterParam := r.Form.Get("filter")
   394  		if filterParam != "" {
   395  			imageFilters.Add("reference", filterParam)
   396  		}
   397  	}
   398  
   399  	var sharedSize bool
   400  	if versions.GreaterThanOrEqualTo(version, "1.42") {
   401  		// NOTE: Support for the "shared-size" parameter was added in API 1.42.
   402  		sharedSize = httputils.BoolValue(r, "shared-size")
   403  	}
   404  
   405  	images, err := ir.backend.Images(ctx, imagetypes.ListOptions{
   406  		All:        httputils.BoolValue(r, "all"),
   407  		Filters:    imageFilters,
   408  		SharedSize: sharedSize,
   409  	})
   410  	if err != nil {
   411  		return err
   412  	}
   413  
   414  	useNone := versions.LessThan(version, "1.43")
   415  	withVirtualSize := versions.LessThan(version, "1.44")
   416  	for _, img := range images {
   417  		if useNone {
   418  			if len(img.RepoTags) == 0 && len(img.RepoDigests) == 0 {
   419  				img.RepoTags = append(img.RepoTags, "<none>:<none>")
   420  				img.RepoDigests = append(img.RepoDigests, "<none>@<none>")
   421  			}
   422  		} else {
   423  			if img.RepoTags == nil {
   424  				img.RepoTags = []string{}
   425  			}
   426  			if img.RepoDigests == nil {
   427  				img.RepoDigests = []string{}
   428  			}
   429  		}
   430  		if withVirtualSize {
   431  			img.VirtualSize = img.Size //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.44.
   432  		}
   433  	}
   434  
   435  	return httputils.WriteJSON(w, http.StatusOK, images)
   436  }
   437  
   438  func (ir *imageRouter) getImagesHistory(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   439  	history, err := ir.backend.ImageHistory(ctx, vars["name"])
   440  	if err != nil {
   441  		return err
   442  	}
   443  
   444  	return httputils.WriteJSON(w, http.StatusOK, history)
   445  }
   446  
   447  func (ir *imageRouter) postImagesTag(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   448  	if err := httputils.ParseForm(r); err != nil {
   449  		return err
   450  	}
   451  
   452  	ref, err := httputils.RepoTagReference(r.Form.Get("repo"), r.Form.Get("tag"))
   453  	if ref == nil || err != nil {
   454  		return errdefs.InvalidParameter(err)
   455  	}
   456  
   457  	refName := reference.FamiliarName(ref)
   458  	if refName == string(digest.Canonical) {
   459  		return errdefs.InvalidParameter(errors.New("refusing to create an ambiguous tag using digest algorithm as name"))
   460  	}
   461  
   462  	img, err := ir.backend.GetImage(ctx, vars["name"], backend.GetImageOpts{})
   463  	if err != nil {
   464  		return errdefs.NotFound(err)
   465  	}
   466  
   467  	if err := ir.backend.TagImage(ctx, img.ID(), ref); err != nil {
   468  		return err
   469  	}
   470  	w.WriteHeader(http.StatusCreated)
   471  	return nil
   472  }
   473  
   474  func (ir *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   475  	if err := httputils.ParseForm(r); err != nil {
   476  		return err
   477  	}
   478  
   479  	var limit int
   480  	if r.Form.Get("limit") != "" {
   481  		var err error
   482  		limit, err = strconv.Atoi(r.Form.Get("limit"))
   483  		if err != nil || limit < 0 {
   484  			return errdefs.InvalidParameter(errors.Wrap(err, "invalid limit specified"))
   485  		}
   486  	}
   487  	searchFilters, err := filters.FromJSON(r.Form.Get("filters"))
   488  	if err != nil {
   489  		return err
   490  	}
   491  
   492  	// For a search it is not an error if no auth was given. Ignore invalid
   493  	// AuthConfig to increase compatibility with the existing API.
   494  	authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
   495  
   496  	headers := http.Header{}
   497  	for k, v := range r.Header {
   498  		k = http.CanonicalHeaderKey(k)
   499  		if strings.HasPrefix(k, "X-Meta-") {
   500  			headers[k] = v
   501  		}
   502  	}
   503  	headers.Set("User-Agent", dockerversion.DockerUserAgent(ctx))
   504  	res, err := ir.searcher.Search(ctx, searchFilters, r.Form.Get("term"), limit, authConfig, headers)
   505  	if err != nil {
   506  		return err
   507  	}
   508  	return httputils.WriteJSON(w, http.StatusOK, res)
   509  }
   510  
   511  func (ir *imageRouter) postImagesPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   512  	if err := httputils.ParseForm(r); err != nil {
   513  		return err
   514  	}
   515  
   516  	pruneFilters, err := filters.FromJSON(r.Form.Get("filters"))
   517  	if err != nil {
   518  		return err
   519  	}
   520  
   521  	pruneReport, err := ir.backend.ImagesPrune(ctx, pruneFilters)
   522  	if err != nil {
   523  		return err
   524  	}
   525  	return httputils.WriteJSON(w, http.StatusOK, pruneReport)
   526  }
   527  
   528  // validateRepoName validates the name of a repository.
   529  func validateRepoName(name reference.Named) error {
   530  	familiarName := reference.FamiliarName(name)
   531  	if familiarName == api.NoBaseImageSpecifier {
   532  		return fmt.Errorf("'%s' is a reserved name", familiarName)
   533  	}
   534  	return nil
   535  }