Browse code

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>

Chris Price authored on 2019/04/27 07:12:43
Showing 9 changed files
... ...
@@ -39,6 +39,7 @@ type ImageInspect struct {
39 39
 	Author          string
40 40
 	Config          *container.Config
41 41
 	Architecture    string
42
+	Variant         string `json:",omitempty"`
42 43
 	Os              string
43 44
 	OsVersion       string `json:",omitempty"`
44 45
 	Size            int64
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"context"
5 5
 	"runtime"
6 6
 
7
+	"github.com/containerd/containerd/platforms"
7 8
 	"github.com/docker/docker/api/types/backend"
8 9
 	"github.com/docker/docker/builder"
9 10
 	dockerimage "github.com/docker/docker/image"
... ...
@@ -56,7 +57,7 @@ func (m *imageSources) Get(idOrRef string, localOnly bool, platform *specs.Platf
56 56
 		return nil, err
57 57
 	}
58 58
 	im := newImageMount(image, layer)
59
-	m.Add(im)
59
+	m.Add(im, platform)
60 60
 	return im, nil
61 61
 }
62 62
 
... ...
@@ -70,16 +71,26 @@ func (m *imageSources) Unmount() (retErr error) {
70 70
 	return
71 71
 }
72 72
 
73
-func (m *imageSources) Add(im *imageMount) {
73
+func (m *imageSources) Add(im *imageMount, platform *specs.Platform) {
74 74
 	switch im.image {
75 75
 	case nil:
76
-		// set the OS for scratch images
77
-		os := runtime.GOOS
76
+		// Set the platform for scratch images
77
+		if platform == nil {
78
+			p := platforms.DefaultSpec()
79
+			platform = &p
80
+		}
81
+
78 82
 		// Windows does not support scratch except for LCOW
83
+		os := platform.OS
79 84
 		if runtime.GOOS == "windows" {
80 85
 			os = "linux"
81 86
 		}
82
-		im.image = &dockerimage.Image{V1Image: dockerimage.V1Image{OS: os}}
87
+
88
+		im.image = &dockerimage.Image{V1Image: dockerimage.V1Image{
89
+			OS:           os,
90
+			Architecture: platform.Architecture,
91
+			Variant:      platform.Variant,
92
+		}}
83 93
 	default:
84 94
 		m.byImageID[im.image.ImageID()] = im
85 95
 	}
86 96
new file mode 100644
... ...
@@ -0,0 +1,106 @@
0
+package dockerfile // import "github.com/docker/docker/builder/dockerfile"
1
+
2
+import (
3
+	"fmt"
4
+	"runtime"
5
+	"testing"
6
+
7
+	"github.com/containerd/containerd/platforms"
8
+	"github.com/docker/docker/builder"
9
+	"github.com/docker/docker/image"
10
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
11
+	"gotest.tools/assert"
12
+)
13
+
14
+func getMockImageSource(getImageImage builder.Image, getImageLayer builder.ROLayer, getImageError error) *imageSources {
15
+	return &imageSources{
16
+		byImageID: make(map[string]*imageMount),
17
+		mounts:    []*imageMount{},
18
+		getImage: func(name string, localOnly bool, platform *ocispec.Platform) (builder.Image, builder.ROLayer, error) {
19
+			return getImageImage, getImageLayer, getImageError
20
+		},
21
+	}
22
+}
23
+
24
+func getMockImageMount() *imageMount {
25
+	return &imageMount{
26
+		image: nil,
27
+		layer: nil,
28
+	}
29
+}
30
+
31
+func TestAddScratchImageAddsToMounts(t *testing.T) {
32
+	is := getMockImageSource(nil, nil, fmt.Errorf("getImage is not implemented"))
33
+	im := getMockImageMount()
34
+
35
+	// We are testing whether the imageMount is added to is.mounts
36
+	assert.Equal(t, len(is.mounts), 0)
37
+	is.Add(im, nil)
38
+	assert.Equal(t, len(is.mounts), 1)
39
+}
40
+
41
+func TestAddFromScratchPopulatesPlatform(t *testing.T) {
42
+	is := getMockImageSource(nil, nil, fmt.Errorf("getImage is not implemented"))
43
+
44
+	platforms := []*ocispec.Platform{
45
+		{
46
+			OS:           "linux",
47
+			Architecture: "amd64",
48
+		},
49
+		{
50
+			OS:           "linux",
51
+			Architecture: "arm64",
52
+			Variant:      "v8",
53
+		},
54
+	}
55
+
56
+	for i, platform := range platforms {
57
+		im := getMockImageMount()
58
+		assert.Equal(t, len(is.mounts), i)
59
+		is.Add(im, platform)
60
+		image, ok := im.image.(*image.Image)
61
+		assert.Assert(t, ok)
62
+		assert.Equal(t, image.OS, platform.OS)
63
+		assert.Equal(t, image.Architecture, platform.Architecture)
64
+		assert.Equal(t, image.Variant, platform.Variant)
65
+	}
66
+}
67
+
68
+func TestAddFromScratchDoesNotModifyArgPlatform(t *testing.T) {
69
+	is := getMockImageSource(nil, nil, fmt.Errorf("getImage is not implemented"))
70
+	im := getMockImageMount()
71
+
72
+	platform := &ocispec.Platform{
73
+		OS:           "windows",
74
+		Architecture: "amd64",
75
+	}
76
+	argPlatform := *platform
77
+
78
+	is.Add(im, &argPlatform)
79
+	// The way the code is written right now, this test
80
+	// really doesn't do much except on Windows.
81
+	assert.DeepEqual(t, *platform, argPlatform)
82
+}
83
+
84
+func TestAddFromScratchPopulatesPlatformIfNil(t *testing.T) {
85
+	is := getMockImageSource(nil, nil, fmt.Errorf("getImage is not implemented"))
86
+	im := getMockImageMount()
87
+	is.Add(im, nil)
88
+	image, ok := im.image.(*image.Image)
89
+	assert.Assert(t, ok)
90
+
91
+	expectedPlatform := platforms.DefaultSpec()
92
+	if runtime.GOOS == "windows" {
93
+		expectedPlatform.OS = "linux"
94
+	}
95
+	assert.Equal(t, expectedPlatform.OS, image.OS)
96
+	assert.Equal(t, expectedPlatform.Architecture, image.Architecture)
97
+	assert.Equal(t, expectedPlatform.Variant, image.Variant)
98
+}
99
+
100
+func TestImageSourceGetAddsToMounts(t *testing.T) {
101
+	is := getMockImageSource(nil, nil, nil)
102
+	_, err := is.Get("test", false, nil)
103
+	assert.NilError(t, err)
104
+	assert.Equal(t, len(is.mounts), 1)
105
+}
... ...
@@ -26,6 +26,7 @@ import (
26 26
 	"github.com/docker/docker/pkg/stringid"
27 27
 	"github.com/docker/docker/pkg/system"
28 28
 	"github.com/docker/go-connections/nat"
29
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
29 30
 	"github.com/pkg/errors"
30 31
 	"github.com/sirupsen/logrus"
31 32
 )
... ...
@@ -117,15 +118,21 @@ func (b *Builder) exportImage(state *dispatchState, layer builder.RWLayer, paren
117 117
 		return err
118 118
 	}
119 119
 
120
-	// add an image mount without an image so the layer is properly unmounted
121
-	// if there is an error before we can add the full mount with image
122
-	b.imageSources.Add(newImageMount(nil, newLayer))
123
-
124 120
 	parentImage, ok := parent.(*image.Image)
125 121
 	if !ok {
126 122
 		return errors.Errorf("unexpected image type")
127 123
 	}
128 124
 
125
+	platform := &specs.Platform{
126
+		OS:           parentImage.OS,
127
+		Architecture: parentImage.Architecture,
128
+		Variant:      parentImage.Variant,
129
+	}
130
+
131
+	// add an image mount without an image so the layer is properly unmounted
132
+	// if there is an error before we can add the full mount with image
133
+	b.imageSources.Add(newImageMount(nil, newLayer), platform)
134
+
129 135
 	newImage := image.NewChildImage(parentImage, image.ChildConfig{
130 136
 		Author:          state.maintainer,
131 137
 		ContainerConfig: runConfig,
... ...
@@ -146,7 +153,7 @@ func (b *Builder) exportImage(state *dispatchState, layer builder.RWLayer, paren
146 146
 	}
147 147
 
148 148
 	state.imageID = exportedImage.ImageID()
149
-	b.imageSources.Add(newImageMount(exportedImage, newLayer))
149
+	b.imageSources.Add(newImageMount(exportedImage, newLayer), platform)
150 150
 	return nil
151 151
 }
152 152
 
... ...
@@ -11,8 +11,12 @@ import (
11 11
 	"github.com/docker/docker/api/types/container"
12 12
 	"github.com/docker/docker/builder"
13 13
 	"github.com/docker/docker/builder/remotecontext"
14
+	"github.com/docker/docker/image"
15
+	"github.com/docker/docker/layer"
14 16
 	"github.com/docker/docker/pkg/archive"
17
+	"github.com/docker/docker/pkg/containerfs"
15 18
 	"github.com/docker/go-connections/nat"
19
+	"github.com/opencontainers/go-digest"
16 20
 	"gotest.tools/assert"
17 21
 	is "gotest.tools/assert/cmp"
18 22
 	"gotest.tools/skip"
... ...
@@ -176,3 +180,45 @@ func TestDeepCopyRunConfig(t *testing.T) {
176 176
 	copy.Shell[0] = "sh"
177 177
 	assert.Check(t, is.DeepEqual(fullMutableRunConfig(), runConfig))
178 178
 }
179
+
180
+type MockRWLayer struct{}
181
+
182
+func (l *MockRWLayer) Release() error                { return nil }
183
+func (l *MockRWLayer) Root() containerfs.ContainerFS { return nil }
184
+func (l *MockRWLayer) Commit() (builder.ROLayer, error) {
185
+	return &MockROLayer{
186
+		diffID: layer.DiffID(digest.Digest("sha256:1234")),
187
+	}, nil
188
+}
189
+
190
+type MockROLayer struct {
191
+	diffID layer.DiffID
192
+}
193
+
194
+func (l *MockROLayer) Release() error                       { return nil }
195
+func (l *MockROLayer) NewRWLayer() (builder.RWLayer, error) { return nil, nil }
196
+func (l *MockROLayer) DiffID() layer.DiffID                 { return l.diffID }
197
+
198
+func getMockBuildBackend() builder.Backend {
199
+	return &MockBackend{}
200
+}
201
+
202
+func TestExportImage(t *testing.T) {
203
+	ds := newDispatchState(NewBuildArgs(map[string]*string{}))
204
+	layer := &MockRWLayer{}
205
+	parentImage := &image.Image{
206
+		V1Image: image.V1Image{
207
+			OS:           "linux",
208
+			Architecture: "arm64",
209
+			Variant:      "v8",
210
+		},
211
+	}
212
+	runConfig := &container.Config{}
213
+
214
+	b := &Builder{
215
+		imageSources: getMockImageSource(nil, nil, nil),
216
+		docker:       getMockBuildBackend(),
217
+	}
218
+	err := b.exportImage(ds, layer, parentImage, runConfig)
219
+	assert.NilError(t, err)
220
+}
... ...
@@ -82,7 +82,7 @@ func (m *MockBackend) MakeImageCache(cacheFrom []string) builder.ImageCache {
82 82
 }
83 83
 
84 84
 func (m *MockBackend) CreateImage(config []byte, parent string) (builder.Image, error) {
85
-	return nil, nil
85
+	return &mockImage{id: "test"}, nil
86 86
 }
87 87
 
88 88
 type mockImage struct {
... ...
@@ -76,6 +76,7 @@ func (i *ImageService) LookupImage(name string) (*types.ImageInspect, error) {
76 76
 		Author:          img.Author,
77 77
 		Config:          img.Config,
78 78
 		Architecture:    img.Architecture,
79
+		Variant:         img.Variant,
79 80
 		Os:              img.OperatingSystem(),
80 81
 		OsVersion:       img.OSVersion,
81 82
 		Size:            size,
... ...
@@ -170,7 +170,7 @@ func (s *imageConfigStore) PlatformFromConfig(c []byte) (*specs.Platform, error)
170 170
 	if !system.IsOSSupported(os) {
171 171
 		return nil, system.ErrNotSupportedOperatingSystem
172 172
 	}
173
-	return &specs.Platform{OS: os, Architecture: unmarshalledConfig.Architecture, OSVersion: unmarshalledConfig.OSVersion}, nil
173
+	return &specs.Platform{OS: os, Architecture: unmarshalledConfig.Architecture, Variant: unmarshalledConfig.Variant, OSVersion: unmarshalledConfig.OSVersion}, nil
174 174
 }
175 175
 
176 176
 type storeLayerProvider struct {
... ...
@@ -53,6 +53,8 @@ type V1Image struct {
53 53
 	Config *container.Config `json:"config,omitempty"`
54 54
 	// Architecture is the hardware that the image is built and runs on
55 55
 	Architecture string `json:"architecture,omitempty"`
56
+	// Variant is the CPU architecture variant (presently ARM-only)
57
+	Variant string `json:"variant,omitempty"`
56 58
 	// OS is the operating system used to build and run the image
57 59
 	OS string `json:"os,omitempty"`
58 60
 	// Size is the total size of the image including all layers it is composed of
... ...
@@ -105,6 +107,13 @@ func (img *Image) BaseImgArch() string {
105 105
 	return arch
106 106
 }
107 107
 
108
+// BaseImgVariant returns the image's variant, whether populated or not.
109
+// This avoids creating an inconsistency where the stored image variant
110
+// is "greater than" (i.e. v8 vs v6) the actual image variant.
111
+func (img *Image) BaseImgVariant() string {
112
+	return img.Variant
113
+}
114
+
108 115
 // OperatingSystem returns the image's operating system. If not populated, defaults to the host runtime OS.
109 116
 func (img *Image) OperatingSystem() string {
110 117
 	os := img.OS
... ...
@@ -167,6 +176,7 @@ func NewChildImage(img *Image, child ChildConfig, os string) *Image {
167 167
 			DockerVersion:   dockerversion.Version,
168 168
 			Config:          child.Config,
169 169
 			Architecture:    img.BaseImgArch(),
170
+			Variant:         img.BaseImgVariant(),
170 171
 			OS:              os,
171 172
 			Container:       child.ContainerID,
172 173
 			ContainerConfig: *child.ContainerConfig,