github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/api/server/router/image/image_routes.go (about)

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