mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
`GET /image/{name}/json` now supports `platform` parameter allowing to
specify which platform variant of a multi-platform image to inspect.
For servers that do not use containerd image store integration, this
option will cause an error if the requested platform doesn't match the
image's actual platform
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
574 lines
17 KiB
Go
574 lines
17 KiB
Go
package image // import "github.com/docker/docker/api/server/router/image"
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/containerd/platforms"
|
|
"github.com/distribution/reference"
|
|
"github.com/docker/docker/api"
|
|
"github.com/docker/docker/api/server/httputils"
|
|
"github.com/docker/docker/api/types/backend"
|
|
"github.com/docker/docker/api/types/filters"
|
|
imagetypes "github.com/docker/docker/api/types/image"
|
|
"github.com/docker/docker/api/types/registry"
|
|
"github.com/docker/docker/api/types/versions"
|
|
"github.com/docker/docker/builder/remotecontext"
|
|
"github.com/docker/docker/dockerversion"
|
|
"github.com/docker/docker/errdefs"
|
|
"github.com/docker/docker/image"
|
|
"github.com/docker/docker/pkg/ioutils"
|
|
"github.com/docker/docker/pkg/progress"
|
|
"github.com/docker/docker/pkg/streamformatter"
|
|
"github.com/opencontainers/go-digest"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// Creates an image from Pull or from Import
|
|
func (ir *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
var (
|
|
img = r.Form.Get("fromImage")
|
|
repo = r.Form.Get("repo")
|
|
tag = r.Form.Get("tag")
|
|
comment = r.Form.Get("message")
|
|
progressErr error
|
|
output = ioutils.NewWriteFlusher(w)
|
|
platform *ocispec.Platform
|
|
)
|
|
defer output.Close()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
version := httputils.VersionFromContext(ctx)
|
|
if versions.GreaterThanOrEqualTo(version, "1.32") {
|
|
if p := r.FormValue("platform"); p != "" {
|
|
sp, err := platforms.Parse(p)
|
|
if err != nil {
|
|
return errdefs.InvalidParameter(err)
|
|
}
|
|
platform = &sp
|
|
}
|
|
}
|
|
|
|
if img != "" { // pull
|
|
metaHeaders := map[string][]string{}
|
|
for k, v := range r.Header {
|
|
if strings.HasPrefix(k, "X-Meta-") {
|
|
metaHeaders[k] = v
|
|
}
|
|
}
|
|
|
|
// Special case: "pull -a" may send an image name with a
|
|
// trailing :. This is ugly, but let's not break API
|
|
// compatibility.
|
|
imgName := strings.TrimSuffix(img, ":")
|
|
|
|
ref, err := reference.ParseNormalizedNamed(imgName)
|
|
if err != nil {
|
|
return errdefs.InvalidParameter(err)
|
|
}
|
|
|
|
// TODO(thaJeztah) this could use a WithTagOrDigest() utility
|
|
if tag != "" {
|
|
// The "tag" could actually be a digest.
|
|
var dgst digest.Digest
|
|
dgst, err = digest.Parse(tag)
|
|
if err == nil {
|
|
ref, err = reference.WithDigest(reference.TrimNamed(ref), dgst)
|
|
} else {
|
|
ref, err = reference.WithTag(ref, tag)
|
|
}
|
|
if err != nil {
|
|
return errdefs.InvalidParameter(err)
|
|
}
|
|
}
|
|
|
|
if err := validateRepoName(ref); err != nil {
|
|
return errdefs.Forbidden(err)
|
|
}
|
|
|
|
// For a pull it is not an error if no auth was given. Ignore invalid
|
|
// AuthConfig to increase compatibility with the existing API.
|
|
authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
|
|
progressErr = ir.backend.PullImage(ctx, ref, platform, metaHeaders, authConfig, output)
|
|
} else { // import
|
|
src := r.Form.Get("fromSrc")
|
|
|
|
tagRef, err := httputils.RepoTagReference(repo, tag)
|
|
if err != nil {
|
|
return errdefs.InvalidParameter(err)
|
|
}
|
|
|
|
if len(comment) == 0 {
|
|
comment = "Imported from " + src
|
|
}
|
|
|
|
var layerReader io.ReadCloser
|
|
defer r.Body.Close()
|
|
if src == "-" {
|
|
layerReader = r.Body
|
|
} else {
|
|
if len(strings.Split(src, "://")) == 1 {
|
|
src = "http://" + src
|
|
}
|
|
u, err := url.Parse(src)
|
|
if err != nil {
|
|
return errdefs.InvalidParameter(err)
|
|
}
|
|
|
|
resp, err := remotecontext.GetWithStatusError(u.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
output.Write(streamformatter.FormatStatus("", "Downloading from %s", u))
|
|
progressOutput := streamformatter.NewJSONProgressOutput(output, true)
|
|
layerReader = progress.NewProgressReader(resp.Body, progressOutput, resp.ContentLength, "", "Importing")
|
|
defer layerReader.Close()
|
|
}
|
|
|
|
var id image.ID
|
|
id, progressErr = ir.backend.ImportImage(ctx, tagRef, platform, comment, layerReader, r.Form["changes"])
|
|
|
|
if progressErr == nil {
|
|
_, _ = output.Write(streamformatter.FormatStatus("", "%v", id.String()))
|
|
}
|
|
}
|
|
if progressErr != nil {
|
|
if !output.Flushed() {
|
|
return progressErr
|
|
}
|
|
_, _ = output.Write(streamformatter.FormatError(progressErr))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ir *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
metaHeaders := map[string][]string{}
|
|
for k, v := range r.Header {
|
|
if strings.HasPrefix(k, "X-Meta-") {
|
|
metaHeaders[k] = v
|
|
}
|
|
}
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
var authConfig *registry.AuthConfig
|
|
if authEncoded := r.Header.Get(registry.AuthHeader); authEncoded != "" {
|
|
// the new format is to handle the authConfig as a header. Ignore invalid
|
|
// AuthConfig to increase compatibility with the existing API.
|
|
authConfig, _ = registry.DecodeAuthConfig(authEncoded)
|
|
} else {
|
|
// the old format is supported for compatibility if there was no authConfig header
|
|
var err error
|
|
authConfig, err = registry.DecodeAuthConfigBody(r.Body)
|
|
if err != nil {
|
|
return errors.Wrap(err, "bad parameters and missing X-Registry-Auth")
|
|
}
|
|
}
|
|
|
|
output := ioutils.NewWriteFlusher(w)
|
|
defer output.Close()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
img := vars["name"]
|
|
tag := r.Form.Get("tag")
|
|
|
|
var ref reference.Named
|
|
|
|
// Tag is empty only in case PushOptions.All is true.
|
|
if tag != "" {
|
|
r, err := httputils.RepoTagReference(img, tag)
|
|
if err != nil {
|
|
return errdefs.InvalidParameter(err)
|
|
}
|
|
ref = r
|
|
} else {
|
|
r, err := reference.ParseNormalizedNamed(img)
|
|
if err != nil {
|
|
return errdefs.InvalidParameter(err)
|
|
}
|
|
ref = r
|
|
}
|
|
|
|
var platform *ocispec.Platform
|
|
// Platform is optional, and only supported in API version 1.46 and later.
|
|
// However the PushOptions struct previously was an alias for the PullOptions struct
|
|
// which also contained a Platform field.
|
|
// This means that older clients may be sending a platform field, even
|
|
// though it wasn't really supported by the server.
|
|
// Don't break these clients and just ignore the platform field on older APIs.
|
|
if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.46") {
|
|
if formPlatform := r.Form.Get("platform"); formPlatform != "" {
|
|
p, err := httputils.DecodePlatform(formPlatform)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
platform = p
|
|
}
|
|
}
|
|
|
|
if err := ir.backend.PushImage(ctx, ref, platform, metaHeaders, authConfig, output); err != nil {
|
|
if !output.Flushed() {
|
|
return err
|
|
}
|
|
_, _ = output.Write(streamformatter.FormatError(err))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ir *imageRouter) getImagesGet(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/x-tar")
|
|
|
|
output := ioutils.NewWriteFlusher(w)
|
|
defer output.Close()
|
|
var names []string
|
|
if name, ok := vars["name"]; ok {
|
|
names = []string{name}
|
|
} else {
|
|
names = r.Form["names"]
|
|
}
|
|
|
|
var platform *ocispec.Platform
|
|
if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.48") {
|
|
if formPlatforms := r.Form["platform"]; len(formPlatforms) > 1 {
|
|
// TODO(thaJeztah): remove once we support multiple platforms: see https://github.com/moby/moby/issues/48759
|
|
return errdefs.InvalidParameter(errors.New("multiple platform parameters not supported"))
|
|
}
|
|
if formPlatform := r.Form.Get("platform"); formPlatform != "" {
|
|
p, err := httputils.DecodePlatform(formPlatform)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
platform = p
|
|
}
|
|
}
|
|
|
|
if err := ir.backend.ExportImage(ctx, names, platform, output); err != nil {
|
|
if !output.Flushed() {
|
|
return err
|
|
}
|
|
_, _ = output.Write(streamformatter.FormatError(err))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ir *imageRouter) postImagesLoad(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
var platform *ocispec.Platform
|
|
if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.48") {
|
|
if formPlatforms := r.Form["platform"]; len(formPlatforms) > 1 {
|
|
// TODO(thaJeztah): remove once we support multiple platforms: see https://github.com/moby/moby/issues/48759
|
|
return errdefs.InvalidParameter(errors.New("multiple platform parameters not supported"))
|
|
}
|
|
if formPlatform := r.Form.Get("platform"); formPlatform != "" {
|
|
p, err := httputils.DecodePlatform(formPlatform)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
platform = p
|
|
}
|
|
}
|
|
quiet := httputils.BoolValueOrDefault(r, "quiet", true)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
output := ioutils.NewWriteFlusher(w)
|
|
defer output.Close()
|
|
if err := ir.backend.LoadImage(ctx, r.Body, platform, output, quiet); err != nil {
|
|
_, _ = output.Write(streamformatter.FormatError(err))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type missingImageError struct{}
|
|
|
|
func (missingImageError) Error() string {
|
|
return "image name cannot be blank"
|
|
}
|
|
|
|
func (missingImageError) InvalidParameter() {}
|
|
|
|
func (ir *imageRouter) deleteImages(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
name := vars["name"]
|
|
|
|
if strings.TrimSpace(name) == "" {
|
|
return missingImageError{}
|
|
}
|
|
|
|
force := httputils.BoolValue(r, "force")
|
|
prune := !httputils.BoolValue(r, "noprune")
|
|
|
|
list, err := ir.backend.ImageDelete(ctx, name, force, prune)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return httputils.WriteJSON(w, http.StatusOK, list)
|
|
}
|
|
|
|
func (ir *imageRouter) getImagesByName(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
var manifests bool
|
|
if r.Form.Get("manifests") != "" && versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.48") {
|
|
manifests = httputils.BoolValue(r, "manifests")
|
|
}
|
|
|
|
var platform *ocispec.Platform
|
|
if r.Form.Get("platform") != "" && versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.49") {
|
|
p, err := httputils.DecodePlatform(r.Form.Get("platform"))
|
|
if err != nil {
|
|
return errdefs.InvalidParameter(err)
|
|
}
|
|
platform = p
|
|
}
|
|
|
|
if manifests && platform != nil {
|
|
return errdefs.InvalidParameter(errors.New("conflicting options: manifests and platform options cannot both be set"))
|
|
}
|
|
|
|
imageInspect, err := ir.backend.ImageInspect(ctx, vars["name"], backend.ImageInspectOpts{
|
|
Manifests: manifests,
|
|
Platform: platform,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Make sure we output empty arrays instead of nil. While Go nil slice is functionally equivalent to an empty slice,
|
|
// it matters for the JSON representation.
|
|
if imageInspect.RepoTags == nil {
|
|
imageInspect.RepoTags = []string{}
|
|
}
|
|
if imageInspect.RepoDigests == nil {
|
|
imageInspect.RepoDigests = []string{}
|
|
}
|
|
|
|
version := httputils.VersionFromContext(ctx)
|
|
if versions.LessThan(version, "1.44") {
|
|
imageInspect.VirtualSize = imageInspect.Size //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.44.
|
|
|
|
if imageInspect.Created == "" {
|
|
// backwards compatibility for Created not existing returning "0001-01-01T00:00:00Z"
|
|
// https://github.com/moby/moby/issues/47368
|
|
imageInspect.Created = time.Time{}.Format(time.RFC3339Nano)
|
|
}
|
|
}
|
|
if versions.GreaterThanOrEqualTo(version, "1.45") {
|
|
imageInspect.Container = "" //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.45.
|
|
imageInspect.ContainerConfig = nil //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.45.
|
|
}
|
|
if versions.LessThan(version, "1.48") {
|
|
imageInspect.Descriptor = nil
|
|
}
|
|
return httputils.WriteJSON(w, http.StatusOK, imageInspect)
|
|
}
|
|
|
|
func (ir *imageRouter) getImagesJSON(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
imageFilters, err := filters.FromJSON(r.Form.Get("filters"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
version := httputils.VersionFromContext(ctx)
|
|
if versions.LessThan(version, "1.41") {
|
|
// NOTE: filter is a shell glob string applied to repository names.
|
|
filterParam := r.Form.Get("filter")
|
|
if filterParam != "" {
|
|
imageFilters.Add("reference", filterParam)
|
|
}
|
|
}
|
|
|
|
var sharedSize bool
|
|
if versions.GreaterThanOrEqualTo(version, "1.42") {
|
|
// NOTE: Support for the "shared-size" parameter was added in API 1.42.
|
|
sharedSize = httputils.BoolValue(r, "shared-size")
|
|
}
|
|
|
|
var manifests bool
|
|
if versions.GreaterThanOrEqualTo(version, "1.47") {
|
|
manifests = httputils.BoolValue(r, "manifests")
|
|
}
|
|
|
|
images, err := ir.backend.Images(ctx, imagetypes.ListOptions{
|
|
All: httputils.BoolValue(r, "all"),
|
|
Filters: imageFilters,
|
|
SharedSize: sharedSize,
|
|
Manifests: manifests,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
useNone := versions.LessThan(version, "1.43")
|
|
withVirtualSize := versions.LessThan(version, "1.44")
|
|
noDescriptor := versions.LessThan(version, "1.48")
|
|
for _, img := range images {
|
|
if useNone {
|
|
if len(img.RepoTags) == 0 && len(img.RepoDigests) == 0 {
|
|
img.RepoTags = append(img.RepoTags, "<none>:<none>")
|
|
img.RepoDigests = append(img.RepoDigests, "<none>@<none>")
|
|
}
|
|
} else {
|
|
if img.RepoTags == nil {
|
|
img.RepoTags = []string{}
|
|
}
|
|
if img.RepoDigests == nil {
|
|
img.RepoDigests = []string{}
|
|
}
|
|
}
|
|
if withVirtualSize {
|
|
img.VirtualSize = img.Size //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.44.
|
|
}
|
|
if noDescriptor {
|
|
img.Descriptor = nil
|
|
}
|
|
}
|
|
|
|
return httputils.WriteJSON(w, http.StatusOK, images)
|
|
}
|
|
|
|
func (ir *imageRouter) getImagesHistory(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
var platform *ocispec.Platform
|
|
if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.48") {
|
|
if formPlatform := r.Form.Get("platform"); formPlatform != "" {
|
|
p, err := httputils.DecodePlatform(formPlatform)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
platform = p
|
|
}
|
|
}
|
|
history, err := ir.backend.ImageHistory(ctx, vars["name"], platform)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return httputils.WriteJSON(w, http.StatusOK, history)
|
|
}
|
|
|
|
func (ir *imageRouter) postImagesTag(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
ref, err := httputils.RepoTagReference(r.Form.Get("repo"), r.Form.Get("tag"))
|
|
if ref == nil || err != nil {
|
|
return errdefs.InvalidParameter(err)
|
|
}
|
|
|
|
refName := reference.FamiliarName(ref)
|
|
if refName == string(digest.Canonical) {
|
|
return errdefs.InvalidParameter(errors.New("refusing to create an ambiguous tag using digest algorithm as name"))
|
|
}
|
|
|
|
img, err := ir.backend.GetImage(ctx, vars["name"], backend.GetImageOpts{})
|
|
if err != nil {
|
|
return errdefs.NotFound(err)
|
|
}
|
|
|
|
if err := ir.backend.TagImage(ctx, img.ID(), ref); err != nil {
|
|
return err
|
|
}
|
|
w.WriteHeader(http.StatusCreated)
|
|
return nil
|
|
}
|
|
|
|
func (ir *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
var limit int
|
|
if r.Form.Get("limit") != "" {
|
|
var err error
|
|
limit, err = strconv.Atoi(r.Form.Get("limit"))
|
|
if err != nil || limit < 0 {
|
|
return errdefs.InvalidParameter(errors.Wrap(err, "invalid limit specified"))
|
|
}
|
|
}
|
|
searchFilters, err := filters.FromJSON(r.Form.Get("filters"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// For a search it is not an error if no auth was given. Ignore invalid
|
|
// AuthConfig to increase compatibility with the existing API.
|
|
authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
|
|
|
|
headers := http.Header{}
|
|
for k, v := range r.Header {
|
|
k = http.CanonicalHeaderKey(k)
|
|
if strings.HasPrefix(k, "X-Meta-") {
|
|
headers[k] = v
|
|
}
|
|
}
|
|
headers.Set("User-Agent", dockerversion.DockerUserAgent(ctx))
|
|
res, err := ir.searcher.Search(ctx, searchFilters, r.Form.Get("term"), limit, authConfig, headers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return httputils.WriteJSON(w, http.StatusOK, res)
|
|
}
|
|
|
|
func (ir *imageRouter) postImagesPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
pruneFilters, err := filters.FromJSON(r.Form.Get("filters"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pruneReport, err := ir.backend.ImagesPrune(ctx, pruneFilters)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return httputils.WriteJSON(w, http.StatusOK, pruneReport)
|
|
}
|
|
|
|
// validateRepoName validates the name of a repository.
|
|
func validateRepoName(name reference.Named) error {
|
|
familiarName := reference.FamiliarName(name)
|
|
if familiarName == api.NoBaseImageSpecifier {
|
|
return fmt.Errorf("'%s' is a reserved name", familiarName)
|
|
}
|
|
return nil
|
|
}
|