Add variant to image.Image and legacy builder
This commit adds the image variant to the image.(Image) type and updates related functionality. Images built from another will inherit the OS, architecture and variant. Note that if a base image does not specify an architecture, the local machine's architecture is used for inherited images. On the other hand, the variant is set equal to the parent image's variant, even when the parent image's variant is unset. The legacy builder is also updated to allow the user to specify a '--platform' argument on the command line when creating an image FROM scratch. A complete platform specification, including variant, is supported. The built image will include the variant, as will any derived images. Signed-off-by: Chris Price <chris.price@docker.com>
This commit is contained in:
parent
30c5ec4365
commit
c21a3cf432
9 changed files with 194 additions and 12 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
106
builder/dockerfile/imagecontext_test.go
Normal file
106
builder/dockerfile/imagecontext_test.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue