Browse code

c8d/list: Add TestImageList

Add unit test for `Images` implementation.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>

Paweł Gronowski authored on 2024/02/27 01:43:41
Showing 3 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,243 @@
0
+package containerd
1
+
2
+import (
3
+	"context"
4
+	"fmt"
5
+	"os"
6
+	"path/filepath"
7
+	"sort"
8
+	"testing"
9
+
10
+	"github.com/containerd/containerd"
11
+	"github.com/containerd/containerd/content"
12
+	"github.com/containerd/containerd/images"
13
+	"github.com/containerd/containerd/metadata"
14
+	"github.com/containerd/containerd/namespaces"
15
+	"github.com/containerd/containerd/snapshots"
16
+	"github.com/containerd/log/logtest"
17
+	imagetypes "github.com/docker/docker/api/types/image"
18
+	daemonevents "github.com/docker/docker/daemon/events"
19
+	"github.com/docker/docker/internal/testutils/specialimage"
20
+	"github.com/opencontainers/go-digest"
21
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
22
+	"gotest.tools/v3/assert"
23
+	is "gotest.tools/v3/assert/cmp"
24
+)
25
+
26
+func imagesFromIndex(index ...*ocispec.Index) []images.Image {
27
+	var imgs []images.Image
28
+	for _, idx := range index {
29
+		for _, desc := range idx.Manifests {
30
+			imgs = append(imgs, images.Image{
31
+				Name:   desc.Annotations["io.containerd.image.name"],
32
+				Target: desc,
33
+			})
34
+		}
35
+	}
36
+	return imgs
37
+}
38
+
39
+func TestImageList(t *testing.T) {
40
+	ctx := namespaces.WithNamespace(context.TODO(), "testing")
41
+
42
+	blobsDir := t.TempDir()
43
+
44
+	multilayer, err := specialimage.MultiLayer(blobsDir)
45
+	assert.NilError(t, err)
46
+
47
+	twoplatform, err := specialimage.TwoPlatform(blobsDir)
48
+	assert.NilError(t, err)
49
+
50
+	cs := &blobsDirContentStore{blobs: filepath.Join(blobsDir, "blobs/sha256")}
51
+
52
+	snapshotter := &testSnapshotterService{}
53
+
54
+	for _, tc := range []struct {
55
+		name   string
56
+		images []images.Image
57
+		opts   imagetypes.ListOptions
58
+
59
+		check func(*testing.T, []*imagetypes.Summary) // Change the type of the check function
60
+	}{
61
+		{
62
+			name:   "one multi-layer image",
63
+			images: imagesFromIndex(multilayer),
64
+			check: func(t *testing.T, all []*imagetypes.Summary) { // Change the type of the check function
65
+				assert.Check(t, is.Len(all, 1))
66
+
67
+				assert.Check(t, is.Equal(all[0].ID, multilayer.Manifests[0].Digest.String()))
68
+				assert.Check(t, is.DeepEqual(all[0].RepoTags, []string{"multilayer:latest"}))
69
+			},
70
+		},
71
+		{
72
+			name:   "one image with two platforms is still one entry",
73
+			images: imagesFromIndex(twoplatform),
74
+			check: func(t *testing.T, all []*imagetypes.Summary) { // Change the type of the check function
75
+				assert.Check(t, is.Len(all, 1))
76
+
77
+				assert.Check(t, is.Equal(all[0].ID, twoplatform.Manifests[0].Digest.String()))
78
+				assert.Check(t, is.DeepEqual(all[0].RepoTags, []string{"twoplatform:latest"}))
79
+			},
80
+		},
81
+		{
82
+			name:   "two images are two entries",
83
+			images: imagesFromIndex(multilayer, twoplatform),
84
+			check: func(t *testing.T, all []*imagetypes.Summary) { // Change the type of the check function
85
+				assert.Check(t, is.Len(all, 2))
86
+
87
+				assert.Check(t, is.Equal(all[0].ID, multilayer.Manifests[0].Digest.String()))
88
+				assert.Check(t, is.DeepEqual(all[0].RepoTags, []string{"multilayer:latest"}))
89
+
90
+				assert.Check(t, is.Equal(all[1].ID, twoplatform.Manifests[0].Digest.String()))
91
+				assert.Check(t, is.DeepEqual(all[1].RepoTags, []string{"twoplatform:latest"}))
92
+			},
93
+		},
94
+	} {
95
+		tc := tc
96
+		t.Run(tc.name, func(t *testing.T) {
97
+			ctx := logtest.WithT(ctx, t)
98
+			mdb := newTestDB(ctx, t)
99
+
100
+			snapshotters := map[string]snapshots.Snapshotter{
101
+				containerd.DefaultSnapshotter: snapshotter,
102
+			}
103
+
104
+			service := &ImageService{
105
+				images:              metadata.NewImageStore(mdb),
106
+				containers:          emptyTestContainerStore(),
107
+				content:             cs,
108
+				eventsService:       daemonevents.New(),
109
+				snapshotterServices: snapshotters,
110
+				snapshotter:         containerd.DefaultSnapshotter,
111
+			}
112
+
113
+			// containerd.Image gets the services directly from containerd.Client
114
+			// so we need to create a "fake" containerd.Client with the test services.
115
+			c8dCli, err := containerd.New("", containerd.WithServices(
116
+				containerd.WithImageStore(service.images),
117
+				containerd.WithContentStore(cs),
118
+				containerd.WithSnapshotters(snapshotters),
119
+			))
120
+			assert.NilError(t, err)
121
+
122
+			service.client = c8dCli
123
+
124
+			for _, img := range tc.images {
125
+				_, err := service.images.Create(ctx, img)
126
+				assert.NilError(t, err)
127
+			}
128
+
129
+			all, err := service.Images(ctx, tc.opts)
130
+			assert.NilError(t, err)
131
+
132
+			sort.Slice(all, func(i, j int) bool {
133
+				firstTag := func(idx int) string {
134
+					if len(all[idx].RepoTags) > 0 {
135
+						return all[idx].RepoTags[0]
136
+					}
137
+					return ""
138
+				}
139
+				return firstTag(i) < firstTag(j)
140
+			})
141
+
142
+			tc.check(t, all)
143
+		})
144
+	}
145
+
146
+}
147
+
148
+type blobsDirContentStore struct {
149
+	blobs string
150
+}
151
+
152
+type fileReaderAt struct {
153
+	*os.File
154
+}
155
+
156
+func (f *fileReaderAt) Size() int64 {
157
+	fi, err := f.Stat()
158
+	if err != nil {
159
+		return -1
160
+	}
161
+	return fi.Size()
162
+}
163
+
164
+func (s *blobsDirContentStore) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) {
165
+	p := filepath.Join(s.blobs, desc.Digest.Encoded())
166
+	r, err := os.Open(p)
167
+	if err != nil {
168
+		return nil, err
169
+	}
170
+	return &fileReaderAt{r}, nil
171
+}
172
+
173
+func (s *blobsDirContentStore) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {
174
+	return nil, fmt.Errorf("read-only")
175
+}
176
+
177
+func (s *blobsDirContentStore) Status(ctx context.Context, _ string) (content.Status, error) {
178
+	return content.Status{}, fmt.Errorf("not implemented")
179
+}
180
+
181
+func (s *blobsDirContentStore) Delete(ctx context.Context, dgst digest.Digest) error {
182
+	return fmt.Errorf("read-only")
183
+}
184
+
185
+func (s *blobsDirContentStore) ListStatuses(ctx context.Context, filters ...string) ([]content.Status, error) {
186
+	return nil, nil
187
+}
188
+
189
+func (s *blobsDirContentStore) Abort(ctx context.Context, ref string) error {
190
+	return fmt.Errorf("not implemented")
191
+}
192
+
193
+func (s *blobsDirContentStore) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error {
194
+	entries, err := os.ReadDir(s.blobs)
195
+	if err != nil {
196
+		return err
197
+	}
198
+
199
+	for _, e := range entries {
200
+		if e.IsDir() {
201
+			continue
202
+		}
203
+
204
+		d := digest.FromString(e.Name())
205
+		if d == "" {
206
+			continue
207
+		}
208
+
209
+		stat, err := e.Info()
210
+		if err != nil {
211
+			return err
212
+		}
213
+
214
+		if err := fn(content.Info{Digest: d, Size: stat.Size()}); err != nil {
215
+			return err
216
+		}
217
+	}
218
+
219
+	return nil
220
+}
221
+
222
+func (s *blobsDirContentStore) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
223
+	f, err := os.Open(filepath.Join(s.blobs, dgst.Encoded()))
224
+	if err != nil {
225
+		return content.Info{}, err
226
+	}
227
+	defer f.Close()
228
+
229
+	stat, err := f.Stat()
230
+	if err != nil {
231
+		return content.Info{}, err
232
+	}
233
+
234
+	return content.Info{
235
+		Digest: dgst,
236
+		Size:   stat.Size(),
237
+	}, nil
238
+}
239
+
240
+func (s *blobsDirContentStore) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) {
241
+	return content.Info{}, fmt.Errorf("read-only")
242
+}
... ...
@@ -10,6 +10,7 @@ import (
10 10
 	"github.com/containerd/containerd/images"
11 11
 	"github.com/containerd/containerd/metadata"
12 12
 	"github.com/containerd/containerd/namespaces"
13
+	"github.com/containerd/containerd/snapshots"
13 14
 	"github.com/containerd/log/logtest"
14 15
 	"github.com/distribution/reference"
15 16
 	dockerimages "github.com/docker/docker/daemon/images"
... ...
@@ -296,3 +297,15 @@ func newTestDB(ctx context.Context, t *testing.T) *metadata.DB {
296 296
 
297 297
 	return mdb
298 298
 }
299
+
300
+type testSnapshotterService struct {
301
+	snapshots.Snapshotter
302
+}
303
+
304
+func (s *testSnapshotterService) Stat(ctx context.Context, key string) (snapshots.Info, error) {
305
+	return snapshots.Info{}, nil
306
+}
307
+
308
+func (s *testSnapshotterService) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
309
+	return snapshots.Usage{}, nil
310
+}
299 311
new file mode 100644
... ...
@@ -0,0 +1,117 @@
0
+package specialimage
1
+
2
+import (
3
+	"os"
4
+	"path/filepath"
5
+
6
+	"github.com/containerd/containerd/platforms"
7
+	"github.com/distribution/reference"
8
+	"github.com/opencontainers/go-digest"
9
+	"github.com/opencontainers/image-spec/specs-go"
10
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
11
+)
12
+
13
+func TwoPlatform(dir string) (*ocispec.Index, error) {
14
+	const imageRef = "twoplatform:latest"
15
+
16
+	layer1Desc, err := writeLayerWithOneFile(dir, "bash", []byte("layer1"))
17
+	if err != nil {
18
+		return nil, err
19
+	}
20
+	layer2Desc, err := writeLayerWithOneFile(dir, "bash", []byte("layer2"))
21
+	if err != nil {
22
+		return nil, err
23
+	}
24
+
25
+	config1Desc, err := writeJsonBlob(dir, ocispec.MediaTypeImageConfig, ocispec.Image{
26
+		Platform: platforms.MustParse("linux/amd64"),
27
+		Config: ocispec.ImageConfig{
28
+			Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
29
+		},
30
+		RootFS: ocispec.RootFS{
31
+			Type:    "layers",
32
+			DiffIDs: []digest.Digest{layer1Desc.Digest},
33
+		},
34
+	})
35
+	if err != nil {
36
+		return nil, err
37
+	}
38
+
39
+	manifest1Desc, err := writeJsonBlob(dir, ocispec.MediaTypeImageManifest, ocispec.Manifest{
40
+		MediaType: ocispec.MediaTypeImageManifest,
41
+		Config:    config1Desc,
42
+		Layers:    []ocispec.Descriptor{layer1Desc},
43
+	})
44
+	if err != nil {
45
+		return nil, err
46
+	}
47
+
48
+	config2Desc, err := writeJsonBlob(dir, ocispec.MediaTypeImageConfig, ocispec.Image{
49
+		Platform: platforms.MustParse("linux/arm64"),
50
+		Config: ocispec.ImageConfig{
51
+			Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
52
+		},
53
+		RootFS: ocispec.RootFS{
54
+			Type:    "layers",
55
+			DiffIDs: []digest.Digest{layer1Desc.Digest},
56
+		},
57
+	})
58
+	if err != nil {
59
+		return nil, err
60
+	}
61
+
62
+	manifest2Desc, err := writeJsonBlob(dir, ocispec.MediaTypeImageManifest, ocispec.Manifest{
63
+		MediaType: ocispec.MediaTypeImageManifest,
64
+		Config:    config2Desc,
65
+		Layers:    []ocispec.Descriptor{layer2Desc},
66
+	})
67
+	if err != nil {
68
+		return nil, err
69
+	}
70
+
71
+	index := ocispec.Index{
72
+		Versioned: specs.Versioned{SchemaVersion: 2},
73
+		MediaType: ocispec.MediaTypeImageIndex,
74
+		Manifests: []ocispec.Descriptor{manifest1Desc, manifest2Desc},
75
+	}
76
+
77
+	ref, err := reference.ParseNormalizedNamed(imageRef)
78
+	if err != nil {
79
+		return nil, err
80
+	}
81
+	return multiPlatformImage(dir, ref, index)
82
+}
83
+
84
+func multiPlatformImage(dir string, ref reference.Named, target ocispec.Index) (*ocispec.Index, error) {
85
+	targetDesc, err := writeJsonBlob(dir, ocispec.MediaTypeImageIndex, target)
86
+	if err != nil {
87
+		return nil, err
88
+	}
89
+
90
+	if ref != nil {
91
+		targetDesc.Annotations = map[string]string{
92
+			"io.containerd.image.name": ref.String(),
93
+		}
94
+
95
+		if tagged, ok := ref.(reference.Tagged); ok {
96
+			targetDesc.Annotations[ocispec.AnnotationRefName] = tagged.Tag()
97
+		}
98
+	}
99
+
100
+	index := ocispec.Index{
101
+		Versioned: specs.Versioned{SchemaVersion: 2},
102
+		MediaType: ocispec.MediaTypeImageIndex,
103
+		Manifests: []ocispec.Descriptor{targetDesc},
104
+	}
105
+
106
+	if err := writeJson(index, filepath.Join(dir, "index.json")); err != nil {
107
+		return nil, err
108
+	}
109
+
110
+	err = os.WriteFile(filepath.Join(dir, "oci-layout"), []byte(`{"imageLayoutVersion": "1.0.0"}`), 0o644)
111
+	if err != nil {
112
+		return nil, err
113
+	}
114
+
115
+	return &index, nil
116
+}