Add support for platform (os and architecture) on image import
fixes https://github.com/moby/moby/issues/42813
fixes https://github.com/docker/cli/issues/3408

Commit 0380fbff37922cadf294851b1546f4c212c7f364 (part of https://github.com/moby/moby/pull/34642) added the ability to pass a `--platform flag` on `docker import` when importing an archive. The intent of that commit was to allow importing a Linux rootfs on a Windows daemon (as part of the experimental LCOW feature).

A later commit (337ba71fc1124603302e28d94e2f08674e31a756, part of https://github.com/moby/moby/pull/37350) changed some of this code to take both OS and Architecture into account (for `docker build` and `docker pull`), but did not yet update the `docker image import`.

This patch updates the import endpoitn to allow passing both OS and Architecture. Note that currently only matching OSes are accepted, and an error will be produced when (e.g.) specifying `linux` on Windows and vice-versa.

**- How to verify it**

A test has been added, which can be run with:

```bash
make DOCKER_GRAPHDRIVER=vfs TEST_SKIP_INTEGRATION_CLI=1 TESTFLAGS='-test.run TestImportWithCustomPlatform' test-integration

...

INFO: Testing against a local daemon
=== RUN   TestImportWithCustomPlatform
=== PAUSE TestImportWithCustomPlatform
=== CONT  TestImportWithCustomPlatform
=== RUN   TestImportWithCustomPlatform/#00
=== RUN   TestImportWithCustomPlatform/_______
=== RUN   TestImportWithCustomPlatform//
=== RUN   TestImportWithCustomPlatform/linux
=== RUN   TestImportWithCustomPlatform/LINUX
=== RUN   TestImportWithCustomPlatform/linux/sparc64
=== RUN   TestImportWithCustomPlatform/macos
=== RUN   TestImportWithCustomPlatform/macos/arm64
=== RUN   TestImportWithCustomPlatform/nintendo64
--- PASS: TestImportWithCustomPlatform (1.44s)
    --- PASS: TestImportWithCustomPlatform/#00 (0.14s)
    --- PASS: TestImportWithCustomPlatform/_______ (0.00s)
    --- PASS: TestImportWithCustomPlatform// (0.00s)
    --- PASS: TestImportWithCustomPlatform/linux (0.13s)
    --- PASS: TestImportWithCustomPlatform/LINUX (0.13s)
    --- PASS: TestImportWithCustomPlatform/linux/sparc64 (0.13s)
    --- PASS: TestImportWithCustomPlatform/macos (0.03s)
    --- PASS: TestImportWithCustomPlatform/macos/arm64 (0.00s)
    --- PASS: TestImportWithCustomPlatform/nintendo64 (0.00s)
PASS
```

**- Description for the changelog**
<!--
Write a short (one line) summary that describes the changes in this
pull request for inclusion in the changelog:
-->


**- A picture of a cute animal (not mandatory but encouraged)**


daemon/images/image_import.go
package images // import "github.com/docker/docker/daemon/images"

import (
	"encoding/json"
	"io"
	"net/http"
	"net/url"
	"runtime"
	"strings"
	"time"

	"github.com/docker/distribution/reference"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/builder/dockerfile"
	"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/layer"
	"github.com/docker/docker/pkg/archive"
	"github.com/docker/docker/pkg/progress"
	"github.com/docker/docker/pkg/streamformatter"
	"github.com/pkg/errors"
)

// ImportImage imports an image, getting the archived layer data either from
// inConfig (if src is "-"), or from a URI specified in src. Progress output is
// written to outStream. Repository and tag names can optionally be given in
// the repo and tag arguments, respectively.
func (i *ImageService) ImportImage(src string, repository, os string, tag string, msg string, inConfig io.ReadCloser, outStream io.Writer, changes []string) error {
	var (
		rc     io.ReadCloser
		resp   *http.Response
		newRef reference.Named
	)

	// Default the operating system if not supplied.
	if os == "" {
		os = runtime.GOOS
	}

	if repository != "" {
		var err error
		newRef, err = reference.ParseNormalizedNamed(repository)
		if err != nil {
			return errdefs.InvalidParameter(err)
		}
		if _, isCanonical := newRef.(reference.Canonical); isCanonical {
			return errdefs.InvalidParameter(errors.New("cannot import digest reference"))
		}

		if tag != "" {
			newRef, err = reference.WithTag(newRef, tag)
			if err != nil {
				return errdefs.InvalidParameter(err)
			}
		}
	}

	config, err := dockerfile.BuildFromConfig(&container.Config{}, changes, os)
	if err != nil {
		return err
	}
	if src == "-" {
		rc = inConfig
	} else {
		inConfig.Close()
		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
		}
		outStream.Write(streamformatter.FormatStatus("", "Downloading from %s", u))
		progressOutput := streamformatter.NewJSONProgressOutput(outStream, true)
		rc = progress.NewProgressReader(resp.Body, progressOutput, resp.ContentLength, "", "Importing")
	}

	defer rc.Close()
	if len(msg) == 0 {
		msg = "Imported from " + src
	}

	inflatedLayerData, err := archive.DecompressStream(rc)
	if err != nil {
		return err
	}
	l, err := i.layerStore.Register(inflatedLayerData, "")
	if err != nil {
		return err
	}
	defer layer.ReleaseAndLog(i.layerStore, l)

	created := time.Now().UTC()
	imgConfig, err := json.Marshal(&image.Image{
		V1Image: image.V1Image{
			DockerVersion: dockerversion.Version,
			Config:        config,
			Architecture:  runtime.GOARCH,
			OS:            os,
			Created:       created,
			Comment:       msg,
		},
		RootFS: &image.RootFS{
			Type:    "layers",
			DiffIDs: []layer.DiffID{l.DiffID()},
		},
		History: []image.History{{
			Created: created,
			Comment: msg,
		}},
	})
	if err != nil {
		return err
	}

	id, err := i.imageStore.Create(imgConfig)
	if err != nil {
		return err
	}

	// FIXME: connect with commit code and call refstore directly
	if newRef != nil {
		if err := i.TagImageWithReference(id, newRef); err != nil {
			return err
		}
	}

	i.LogImageEvent(id.String(), id.String(), "import")
	outStream.Write(streamformatter.FormatStatus("", id.String()))
	return nil
}

package images // import "github.com/docker/docker/daemon/images"

import (
	"encoding/json"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/containerd/containerd/platforms"
	"github.com/docker/distribution/reference"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/builder/dockerfile"
	"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/layer"
	"github.com/docker/docker/pkg/archive"
	"github.com/docker/docker/pkg/progress"
	"github.com/docker/docker/pkg/streamformatter"
	specs "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/pkg/errors"
)

// ImportImage imports an image, getting the archived layer data either from
// inConfig (if src is "-"), or from a URI specified in src. Progress output is
// written to outStream. Repository and tag names can optionally be given in
// the repo and tag arguments, respectively.
func (i *ImageService) ImportImage(src string, repository string, platform *specs.Platform, tag string, msg string, inConfig io.ReadCloser, outStream io.Writer, changes []string) error {
	var (
		rc     io.ReadCloser
		resp   *http.Response
		newRef reference.Named
	)

	if repository != "" {
		var err error
		newRef, err = reference.ParseNormalizedNamed(repository)
		if err != nil {
			return errdefs.InvalidParameter(err)
		}
		if _, isCanonical := newRef.(reference.Canonical); isCanonical {
			return errdefs.InvalidParameter(errors.New("cannot import digest reference"))
		}

		if tag != "" {
			newRef, err = reference.WithTag(newRef, tag)
			if err != nil {
				return errdefs.InvalidParameter(err)
			}
		}
	}

	// Normalize platform - default to the operating system and architecture if not supplied.
	if platform == nil {
		p := platforms.DefaultSpec()
		platform = &p
	}

	config, err := dockerfile.BuildFromConfig(&container.Config{}, changes, platform.OS)
	if err != nil {
		return err
	}
	if src == "-" {
		rc = inConfig
	} else {
		inConfig.Close()
		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
		}
		outStream.Write(streamformatter.FormatStatus("", "Downloading from %s", u))
		progressOutput := streamformatter.NewJSONProgressOutput(outStream, true)
		rc = progress.NewProgressReader(resp.Body, progressOutput, resp.ContentLength, "", "Importing")
	}

	defer rc.Close()
	if len(msg) == 0 {
		msg = "Imported from " + src
	}

	inflatedLayerData, err := archive.DecompressStream(rc)
	if err != nil {
		return err
	}
	l, err := i.layerStore.Register(inflatedLayerData, "")
	if err != nil {
		return err
	}
	defer layer.ReleaseAndLog(i.layerStore, l)

	created := time.Now().UTC()
	imgConfig, err := json.Marshal(&image.Image{
		V1Image: image.V1Image{
			DockerVersion: dockerversion.Version,
			Config:        config,
			Architecture:  platform.Architecture,
			Variant:       platform.Variant,
			OS:            platform.OS,
			Created:       created,
			Comment:       msg,
		},
		RootFS: &image.RootFS{
			Type:    "layers",
			DiffIDs: []layer.DiffID{l.DiffID()},
		},
		History: []image.History{{
			Created: created,
			Comment: msg,
		}},
	})
	if err != nil {
		return err
	}

	id, err := i.imageStore.Create(imgConfig)
	if err != nil {
		return err
	}

	// FIXME: connect with commit code and call refstore directly
	if newRef != nil {
		if err := i.TagImageWithReference(id, newRef); err != nil {
			return err
		}
	}

	i.LogImageEvent(id.String(), id.String(), "import")
	outStream.Write(streamformatter.FormatStatus("", id.String()))
	return nil
}

what case is this?