diff --git a/api/types/types.go b/api/types/types.go index b13d9c4c7d..4cf9a95ff2 100644 --- a/api/types/types.go +++ b/api/types/types.go @@ -39,6 +39,7 @@ type ImageInspect struct { Author string Config *container.Config Architecture string + Variant string `json:",omitempty"` Os string OsVersion string `json:",omitempty"` Size int64 diff --git a/builder/dockerfile/imagecontext.go b/builder/dockerfile/imagecontext.go index 6e5ddb9bc2..9d9a6c618c 100644 --- a/builder/dockerfile/imagecontext.go +++ b/builder/dockerfile/imagecontext.go @@ -4,6 +4,7 @@ import ( "context" "runtime" + "github.com/containerd/containerd/platforms" "github.com/docker/docker/api/types/backend" "github.com/docker/docker/builder" dockerimage "github.com/docker/docker/image" @@ -56,7 +57,7 @@ func (m *imageSources) Get(idOrRef string, localOnly bool, platform *specs.Platf return nil, err } im := newImageMount(image, layer) - m.Add(im) + m.Add(im, platform) return im, nil } @@ -70,16 +71,26 @@ func (m *imageSources) Unmount() (retErr error) { return } -func (m *imageSources) Add(im *imageMount) { +func (m *imageSources) Add(im *imageMount, platform *specs.Platform) { switch im.image { case nil: - // set the OS for scratch images - os := runtime.GOOS + // Set the platform for scratch images + if platform == nil { + p := platforms.DefaultSpec() + platform = &p + } + // Windows does not support scratch except for LCOW + os := platform.OS if runtime.GOOS == "windows" { os = "linux" } - im.image = &dockerimage.Image{V1Image: dockerimage.V1Image{OS: os}} + + im.image = &dockerimage.Image{V1Image: dockerimage.V1Image{ + OS: os, + Architecture: platform.Architecture, + Variant: platform.Variant, + }} default: m.byImageID[im.image.ImageID()] = im } diff --git a/builder/dockerfile/imagecontext_test.go b/builder/dockerfile/imagecontext_test.go new file mode 100644 index 0000000000..d31c9c18c1 --- /dev/null +++ b/builder/dockerfile/imagecontext_test.go @@ -0,0 +1,106 @@ +package dockerfile // import "github.com/docker/docker/builder/dockerfile" + +import ( + "fmt" + "runtime" + "testing" + + "github.com/containerd/containerd/platforms" + "github.com/docker/docker/builder" + "github.com/docker/docker/image" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "gotest.tools/assert" +) + +func getMockImageSource(getImageImage builder.Image, getImageLayer builder.ROLayer, getImageError error) *imageSources { + return &imageSources{ + byImageID: make(map[string]*imageMount), + mounts: []*imageMount{}, + getImage: func(name string, localOnly bool, platform *ocispec.Platform) (builder.Image, builder.ROLayer, error) { + return getImageImage, getImageLayer, getImageError + }, + } +} + +func getMockImageMount() *imageMount { + return &imageMount{ + image: nil, + layer: nil, + } +} + +func TestAddScratchImageAddsToMounts(t *testing.T) { + is := getMockImageSource(nil, nil, fmt.Errorf("getImage is not implemented")) + im := getMockImageMount() + + // We are testing whether the imageMount is added to is.mounts + assert.Equal(t, len(is.mounts), 0) + is.Add(im, nil) + assert.Equal(t, len(is.mounts), 1) +} + +func TestAddFromScratchPopulatesPlatform(t *testing.T) { + is := getMockImageSource(nil, nil, fmt.Errorf("getImage is not implemented")) + + platforms := []*ocispec.Platform{ + { + OS: "linux", + Architecture: "amd64", + }, + { + OS: "linux", + Architecture: "arm64", + Variant: "v8", + }, + } + + for i, platform := range platforms { + im := getMockImageMount() + assert.Equal(t, len(is.mounts), i) + is.Add(im, platform) + image, ok := im.image.(*image.Image) + assert.Assert(t, ok) + assert.Equal(t, image.OS, platform.OS) + assert.Equal(t, image.Architecture, platform.Architecture) + assert.Equal(t, image.Variant, platform.Variant) + } +} + +func TestAddFromScratchDoesNotModifyArgPlatform(t *testing.T) { + is := getMockImageSource(nil, nil, fmt.Errorf("getImage is not implemented")) + im := getMockImageMount() + + platform := &ocispec.Platform{ + OS: "windows", + Architecture: "amd64", + } + argPlatform := *platform + + is.Add(im, &argPlatform) + // The way the code is written right now, this test + // really doesn't do much except on Windows. + assert.DeepEqual(t, *platform, argPlatform) +} + +func TestAddFromScratchPopulatesPlatformIfNil(t *testing.T) { + is := getMockImageSource(nil, nil, fmt.Errorf("getImage is not implemented")) + im := getMockImageMount() + is.Add(im, nil) + image, ok := im.image.(*image.Image) + assert.Assert(t, ok) + + expectedPlatform := platforms.DefaultSpec() + if runtime.GOOS == "windows" { + expectedPlatform.OS = "linux" + } + assert.Equal(t, expectedPlatform.OS, image.OS) + assert.Equal(t, expectedPlatform.Architecture, image.Architecture) + assert.Equal(t, expectedPlatform.Variant, image.Variant) +} + +func TestImageSourceGetAddsToMounts(t *testing.T) { + is := getMockImageSource(nil, nil, nil) + _, err := is.Get("test", false, nil) + assert.NilError(t, err) + assert.Equal(t, len(is.mounts), 1) +} diff --git a/builder/dockerfile/internals.go b/builder/dockerfile/internals.go index fa35c4f3c0..3376b0bcdd 100644 --- a/builder/dockerfile/internals.go +++ b/builder/dockerfile/internals.go @@ -26,6 +26,7 @@ import ( "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/system" "github.com/docker/go-connections/nat" + specs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -117,15 +118,21 @@ func (b *Builder) exportImage(state *dispatchState, layer builder.RWLayer, paren return err } - // add an image mount without an image so the layer is properly unmounted - // if there is an error before we can add the full mount with image - b.imageSources.Add(newImageMount(nil, newLayer)) - parentImage, ok := parent.(*image.Image) if !ok { return errors.Errorf("unexpected image type") } + platform := &specs.Platform{ + OS: parentImage.OS, + Architecture: parentImage.Architecture, + Variant: parentImage.Variant, + } + + // add an image mount without an image so the layer is properly unmounted + // if there is an error before we can add the full mount with image + b.imageSources.Add(newImageMount(nil, newLayer), platform) + newImage := image.NewChildImage(parentImage, image.ChildConfig{ Author: state.maintainer, ContainerConfig: runConfig, @@ -146,7 +153,7 @@ func (b *Builder) exportImage(state *dispatchState, layer builder.RWLayer, paren } state.imageID = exportedImage.ImageID() - b.imageSources.Add(newImageMount(exportedImage, newLayer)) + b.imageSources.Add(newImageMount(exportedImage, newLayer), platform) return nil } diff --git a/builder/dockerfile/internals_test.go b/builder/dockerfile/internals_test.go index b1ef6c80d8..6c8fe94f2e 100644 --- a/builder/dockerfile/internals_test.go +++ b/builder/dockerfile/internals_test.go @@ -11,8 +11,12 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/builder" "github.com/docker/docker/builder/remotecontext" + "github.com/docker/docker/image" + "github.com/docker/docker/layer" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/containerfs" "github.com/docker/go-connections/nat" + "github.com/opencontainers/go-digest" "gotest.tools/assert" is "gotest.tools/assert/cmp" "gotest.tools/skip" @@ -176,3 +180,45 @@ func TestDeepCopyRunConfig(t *testing.T) { copy.Shell[0] = "sh" assert.Check(t, is.DeepEqual(fullMutableRunConfig(), runConfig)) } + +type MockRWLayer struct{} + +func (l *MockRWLayer) Release() error { return nil } +func (l *MockRWLayer) Root() containerfs.ContainerFS { return nil } +func (l *MockRWLayer) Commit() (builder.ROLayer, error) { + return &MockROLayer{ + diffID: layer.DiffID(digest.Digest("sha256:1234")), + }, nil +} + +type MockROLayer struct { + diffID layer.DiffID +} + +func (l *MockROLayer) Release() error { return nil } +func (l *MockROLayer) NewRWLayer() (builder.RWLayer, error) { return nil, nil } +func (l *MockROLayer) DiffID() layer.DiffID { return l.diffID } + +func getMockBuildBackend() builder.Backend { + return &MockBackend{} +} + +func TestExportImage(t *testing.T) { + ds := newDispatchState(NewBuildArgs(map[string]*string{})) + layer := &MockRWLayer{} + parentImage := &image.Image{ + V1Image: image.V1Image{ + OS: "linux", + Architecture: "arm64", + Variant: "v8", + }, + } + runConfig := &container.Config{} + + b := &Builder{ + imageSources: getMockImageSource(nil, nil, nil), + docker: getMockBuildBackend(), + } + err := b.exportImage(ds, layer, parentImage, runConfig) + assert.NilError(t, err) +} diff --git a/builder/dockerfile/mockbackend_test.go b/builder/dockerfile/mockbackend_test.go index d4526eafad..06b3ffd6e0 100644 --- a/builder/dockerfile/mockbackend_test.go +++ b/builder/dockerfile/mockbackend_test.go @@ -82,7 +82,7 @@ func (m *MockBackend) MakeImageCache(cacheFrom []string) builder.ImageCache { } func (m *MockBackend) CreateImage(config []byte, parent string) (builder.Image, error) { - return nil, nil + return &mockImage{id: "test"}, nil } type mockImage struct { diff --git a/daemon/images/image_inspect.go b/daemon/images/image_inspect.go index 16c4c9b2dc..60a673d950 100644 --- a/daemon/images/image_inspect.go +++ b/daemon/images/image_inspect.go @@ -76,6 +76,7 @@ func (i *ImageService) LookupImage(name string) (*types.ImageInspect, error) { Author: img.Author, Config: img.Config, Architecture: img.Architecture, + Variant: img.Variant, Os: img.OperatingSystem(), OsVersion: img.OSVersion, Size: size, diff --git a/distribution/config.go b/distribution/config.go index 8e66d93cad..f96df7b94d 100644 --- a/distribution/config.go +++ b/distribution/config.go @@ -170,7 +170,7 @@ func (s *imageConfigStore) PlatformFromConfig(c []byte) (*specs.Platform, error) if !system.IsOSSupported(os) { return nil, system.ErrNotSupportedOperatingSystem } - return &specs.Platform{OS: os, Architecture: unmarshalledConfig.Architecture, OSVersion: unmarshalledConfig.OSVersion}, nil + return &specs.Platform{OS: os, Architecture: unmarshalledConfig.Architecture, Variant: unmarshalledConfig.Variant, OSVersion: unmarshalledConfig.OSVersion}, nil } type storeLayerProvider struct { diff --git a/image/image.go b/image/image.go index f043b042cf..29de613a3e 100644 --- a/image/image.go +++ b/image/image.go @@ -53,6 +53,8 @@ type V1Image struct { Config *container.Config `json:"config,omitempty"` // Architecture is the hardware that the image is built and runs on Architecture string `json:"architecture,omitempty"` + // Variant is the CPU architecture variant (presently ARM-only) + Variant string `json:"variant,omitempty"` // OS is the operating system used to build and run the image OS string `json:"os,omitempty"` // Size is the total size of the image including all layers it is composed of @@ -105,6 +107,13 @@ func (img *Image) BaseImgArch() string { return arch } +// BaseImgVariant returns the image's variant, whether populated or not. +// This avoids creating an inconsistency where the stored image variant +// is "greater than" (i.e. v8 vs v6) the actual image variant. +func (img *Image) BaseImgVariant() string { + return img.Variant +} + // OperatingSystem returns the image's operating system. If not populated, defaults to the host runtime OS. func (img *Image) OperatingSystem() string { os := img.OS @@ -167,6 +176,7 @@ func NewChildImage(img *Image, child ChildConfig, os string) *Image { DockerVersion: dockerversion.Version, Config: child.Config, Architecture: img.BaseImgArch(), + Variant: img.BaseImgVariant(), OS: os, Container: child.ContainerID, ContainerConfig: *child.ContainerConfig,