Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/registry-facade/pkg/registry/manifest_test.go
2499 views
1
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package registry
6
7
import (
8
"context"
9
"encoding/json"
10
"errors"
11
"fmt"
12
"io"
13
"strings"
14
"syscall"
15
"testing"
16
"time"
17
18
"github.com/containerd/containerd/content"
19
"github.com/containerd/containerd/errdefs"
20
"github.com/containerd/containerd/remotes"
21
"github.com/opencontainers/go-digest"
22
"github.com/opencontainers/image-spec/specs-go"
23
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
24
"github.com/stretchr/testify/assert"
25
"github.com/stretchr/testify/require"
26
"k8s.io/apimachinery/pkg/util/wait"
27
)
28
29
func TestDownloadManifest(t *testing.T) {
30
tests := []struct {
31
Name string
32
Store BlobStore
33
FetchMF bool
34
}{
35
{
36
Name: "no store",
37
},
38
{
39
Name: "accepts nothing store",
40
Store: &alwaysNotFoundStore{},
41
},
42
{
43
Name: "misbehaving store",
44
Store: &misbehavingStore{},
45
},
46
{
47
Name: "fetch mf no store",
48
FetchMF: true,
49
},
50
{
51
Name: "fetch mf with store",
52
Store: &alwaysNotFoundStore{},
53
FetchMF: true,
54
},
55
}
56
57
for _, test := range tests {
58
t.Run(test.Name, func(t *testing.T) {
59
cfg, err := json.Marshal(ociv1.ImageConfig{})
60
if err != nil {
61
t.Fatal(err)
62
}
63
cfgDgst := digest.FromBytes(cfg)
64
65
mf, err := json.Marshal(ociv1.Manifest{
66
Versioned: specs.Versioned{SchemaVersion: 1},
67
MediaType: ociv1.MediaTypeImageManifest,
68
Config: ociv1.Descriptor{
69
MediaType: ociv1.MediaTypeImageConfig,
70
Digest: cfgDgst,
71
Size: int64(len(cfg)),
72
},
73
})
74
if err != nil {
75
t.Fatal(err)
76
}
77
mfDgst := digest.FromBytes(mf)
78
mfDesc := ociv1.Descriptor{
79
MediaType: ociv1.MediaTypeImageManifest,
80
Digest: mfDgst,
81
Size: int64(len(mf)),
82
}
83
84
mfl, err := json.Marshal(ociv1.Index{
85
MediaType: ociv1.MediaTypeImageIndex,
86
Manifests: []ociv1.Descriptor{mfDesc},
87
})
88
if err != nil {
89
t.Fatal(err)
90
}
91
mflDgst := digest.FromBytes(mfl)
92
mflDesc := ociv1.Descriptor{
93
MediaType: ociv1.MediaTypeImageIndex,
94
Digest: mflDgst,
95
Size: int64(len(mfl)),
96
}
97
98
fetcher := AsFetcherFunc(&fakeFetcher{
99
Content: map[string][]byte{
100
mflDgst.Encoded(): mfl,
101
mfDgst.Encoded(): mf,
102
cfgDgst.Encoded(): cfg,
103
},
104
})
105
desc := mflDesc
106
if test.FetchMF {
107
desc = mfDesc
108
}
109
_, _, err = DownloadManifest(context.Background(), fetcher, desc, WithStore(test.Store))
110
if err != nil {
111
t.Fatal(err)
112
}
113
})
114
}
115
}
116
117
type alwaysNotFoundStore struct{}
118
119
func (fbs *alwaysNotFoundStore) ReaderAt(ctx context.Context, desc ociv1.Descriptor) (content.ReaderAt, error) {
120
return nil, errdefs.ErrNotFound
121
}
122
123
// Some implementations require WithRef to be included in opts.
124
func (fbs *alwaysNotFoundStore) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {
125
return nil, errdefs.ErrAlreadyExists
126
}
127
128
// Info will return metadata about content available in the content store.
129
//
130
// If the content is not present, ErrNotFound will be returned.
131
func (fbs *alwaysNotFoundStore) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
132
return content.Info{}, errdefs.ErrNotFound
133
}
134
135
type misbehavingStore struct{}
136
137
func (fbs *misbehavingStore) ReaderAt(ctx context.Context, desc ociv1.Descriptor) (content.ReaderAt, error) {
138
return nil, fmt.Errorf("foobar")
139
}
140
141
// Some implementations require WithRef to be included in opts.
142
func (fbs *misbehavingStore) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {
143
return nil, fmt.Errorf("some error")
144
}
145
146
// Info will return metadata about content available in the content store.
147
//
148
// If the content is not present, ErrNotFound will be returned.
149
func (fbs *misbehavingStore) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
150
return content.Info{}, fmt.Errorf("you wish")
151
}
152
153
// manifestFailingReader is a reader that fails after a certain point.
154
type manifestFailingReader struct {
155
reader io.Reader
156
failAfterBytes int
157
failError error
158
bytesRead int
159
}
160
161
func (fr *manifestFailingReader) Read(p []byte) (n int, err error) {
162
if fr.bytesRead >= fr.failAfterBytes {
163
return 0, fr.failError
164
}
165
n, err = fr.reader.Read(p)
166
if err != nil {
167
return n, err
168
}
169
fr.bytesRead += n
170
if fr.bytesRead >= fr.failAfterBytes {
171
// Return the error, but also the bytes read in this call.
172
return n, fr.failError
173
}
174
return n, nil
175
}
176
177
func (fr *manifestFailingReader) Close() error {
178
return nil
179
}
180
181
type mockFetcher struct {
182
// How many times Fetch should fail before succeeding.
183
failCount int
184
// The error to return on failure.
185
failError error
186
187
// Internal counter for calls.
188
callCount int
189
// The data to return on success.
190
successData string
191
192
// Whether to use a reader that fails mid-stream on the first call.
193
failReaderOnFirstCall bool
194
// The number of bytes to read successfully before the reader fails.
195
failAfterBytes int
196
}
197
198
func (m *mockFetcher) Fetch(ctx context.Context, desc ociv1.Descriptor) (io.ReadCloser, error) {
199
m.callCount++
200
if m.callCount <= m.failCount {
201
return nil, m.failError
202
}
203
204
if m.failReaderOnFirstCall && m.callCount == 1 {
205
return &manifestFailingReader{
206
reader: strings.NewReader(m.successData),
207
failAfterBytes: m.failAfterBytes,
208
failError: m.failError,
209
}, nil
210
}
211
212
return io.NopCloser(strings.NewReader(m.successData)), nil
213
}
214
215
func TestDownloadManifest_RetryOnReadAll(t *testing.T) {
216
// Arrange
217
mockFetcher := &mockFetcher{
218
failCount: 0, // Fetch succeeds immediately
219
failReaderOnFirstCall: true,
220
failAfterBytes: 5,
221
failError: syscall.EPIPE,
222
successData: `{"schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json"}`,
223
}
224
225
fetcherFunc := func() (remotes.Fetcher, error) {
226
return mockFetcher, nil
227
}
228
229
// Use short backoff for testing
230
originalBackoff := fetcherBackoffParams
231
fetcherBackoffParams = wait.Backoff{
232
Duration: 1 * time.Millisecond,
233
Steps: 3,
234
}
235
defer func() { fetcherBackoffParams = originalBackoff }()
236
237
// Act
238
_, _, err := DownloadManifest(context.Background(), fetcherFunc, ociv1.Descriptor{MediaType: ociv1.MediaTypeImageManifest})
239
240
// Assert
241
require.NoError(t, err)
242
assert.Equal(t, 2, mockFetcher.callCount, "Expected Fetch to be called twice (1st succeeds, read fails, 2nd succeeds)")
243
}
244
245
func TestDownloadConfig_RetryOnReadAll(t *testing.T) {
246
// Arrange
247
mockFetcher := &mockFetcher{
248
failCount: 0, // Fetch succeeds immediately
249
failReaderOnFirstCall: true,
250
failAfterBytes: 5,
251
failError: syscall.EPIPE,
252
successData: `{"architecture": "amd64", "os": "linux"}`,
253
}
254
255
fetcherFunc := func() (remotes.Fetcher, error) {
256
return mockFetcher, nil
257
}
258
259
// Use short backoff for testing
260
originalBackoff := fetcherBackoffParams
261
fetcherBackoffParams = wait.Backoff{
262
Duration: 1 * time.Millisecond,
263
Steps: 3,
264
}
265
defer func() { fetcherBackoffParams = originalBackoff }()
266
267
// Act
268
_, err := DownloadConfig(context.Background(), fetcherFunc, "ref", ociv1.Descriptor{MediaType: ociv1.MediaTypeImageConfig})
269
270
// Assert
271
require.NoError(t, err)
272
assert.Equal(t, 2, mockFetcher.callCount, "Expected Fetch to be called twice (1st succeeds, read fails, 2nd succeeds)")
273
}
274
275
func TestDownloadManifest_RetryOnFetch(t *testing.T) {
276
// Arrange
277
mockFetcher := &mockFetcher{
278
failCount: 2,
279
failError: errors.New("transient network error"),
280
successData: `{"schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json"}`,
281
}
282
283
fetcherFunc := func() (remotes.Fetcher, error) {
284
return mockFetcher, nil
285
}
286
287
// Use short backoff for testing
288
originalBackoff := fetcherBackoffParams
289
fetcherBackoffParams = wait.Backoff{
290
Duration: 1 * time.Millisecond,
291
Steps: 3,
292
}
293
defer func() { fetcherBackoffParams = originalBackoff }()
294
295
// Act
296
_, _, err := DownloadManifest(context.Background(), fetcherFunc, ociv1.Descriptor{MediaType: ociv1.MediaTypeImageManifest})
297
298
// Assert
299
require.NoError(t, err)
300
assert.Equal(t, 3, mockFetcher.callCount, "Expected Fetch to be called 3 times (2 failures + 1 success)")
301
}
302
303