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