@@ -4,11 +4,13 @@ import (
4
4
"bytes"
5
5
"crypto/sha256"
6
6
"fmt"
7
+ "io/ioutil"
7
8
"net/http"
8
9
"net/http/httptest"
9
10
"net/url"
10
11
"os"
11
12
"strconv"
13
+ "strings"
12
14
"testing"
13
15
"time"
14
16
@@ -214,6 +216,168 @@ func TestPullthroughServeBlob(t *testing.T) {
214
216
}
215
217
}
216
218
219
+ func TestPullthroughServeNotSeekableBlob (t * testing.T ) {
220
+ namespace , name := "user" , "app"
221
+ repoName := fmt .Sprintf ("%s/%s" , namespace , name )
222
+ log .SetLevel (log .DebugLevel )
223
+ installFakeAccessController (t )
224
+ setPassthroughBlobDescriptorServiceFactory ()
225
+
226
+ testImage , err := registrytest .NewImageForManifest (repoName , registrytest .SampleImageManifestSchema1 , "" , false )
227
+ if err != nil {
228
+ t .Fatal (err )
229
+ }
230
+ client := & testclient.Fake {}
231
+ client .AddReactor ("get" , "images" , registrytest .GetFakeImageGetHandler (t , * testImage ))
232
+
233
+ // TODO: get rid of those nasty global vars
234
+ backupRegistryClient := DefaultRegistryClient
235
+ DefaultRegistryClient = makeFakeRegistryClient (client , fake .NewSimpleClientset ())
236
+ defer func () {
237
+ // set it back once this test finishes to make other unit tests working again
238
+ DefaultRegistryClient = backupRegistryClient
239
+ }()
240
+
241
+ reader , dgst , err := registrytest .CreateRandomTarFile ()
242
+ if err != nil {
243
+ t .Fatalf ("unexpected error generating test layer file: %v" , err )
244
+ }
245
+
246
+ blob1Content , err := ioutil .ReadAll (reader )
247
+ if err != nil {
248
+ t .Fatalf ("failed to read blob content: %v" , err )
249
+ }
250
+
251
+ blob1Storage := map [digest.Digest ][]byte {dgst : blob1Content }
252
+
253
+ // start regular HTTP server
254
+ remoteRegistryServer := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
255
+ t .Logf ("External registry got %s %s" , r .Method , r .URL .Path )
256
+
257
+ w .Header ().Set ("Docker-Distribution-API-Version" , "registry/2.0" )
258
+
259
+ switch r .URL .Path {
260
+ case "/v2/" :
261
+ w .Write ([]byte (`{}` ))
262
+ case "/v2/" + repoName + "/tags/list" :
263
+ w .Write ([]byte ("{\" name\" : \" " + repoName + "\" , \" tags\" : [\" latest\" ]}" ))
264
+ case "/v2/" + repoName + "/manifests/latest" , "/v2/" + repoName + "/manifests/" + etcdDigest :
265
+ if r .Method == "HEAD" {
266
+ w .Header ().Set ("Content-Length" , fmt .Sprintf ("%d" , len (etcdManifest )))
267
+ w .Header ().Set ("Docker-Content-Digest" , etcdDigest )
268
+ w .WriteHeader (http .StatusOK )
269
+ } else {
270
+ w .Write ([]byte (etcdManifest ))
271
+ }
272
+ default :
273
+ if strings .HasPrefix (r .URL .Path , "/v2/" + repoName + "/blobs/" ) {
274
+ for dgst , payload := range blob1Storage {
275
+ if r .URL .Path != "/v2/" + repoName + "/blobs/" + dgst .String () {
276
+ continue
277
+ }
278
+ w .Header ().Set ("Content-Length" , fmt .Sprintf ("%d" , len (payload )))
279
+ if r .Method == "HEAD" {
280
+ w .Header ().Set ("Docker-Content-Digest" , dgst .String ())
281
+ w .WriteHeader (http .StatusOK )
282
+ return
283
+ } else {
284
+ // Important!
285
+ //
286
+ // We need to return any return code between 200 and 399, expept 200 and 206.
287
+ // https://github.com/docker/distribution/blob/master/registry/client/transport/http_reader.go#L192
288
+ //
289
+ // In this case the docker client library will make a not truly
290
+ // seekable response.
291
+ // https://github.com/docker/distribution/blob/master/registry/client/transport/http_reader.go#L239
292
+ w .WriteHeader (http .StatusAccepted )
293
+ }
294
+ w .Write (payload )
295
+ return
296
+ }
297
+ }
298
+ t .Fatalf ("unexpected request %s: %#v" , r .URL .Path , r )
299
+ }
300
+ }))
301
+
302
+ serverURL , err := url .Parse (remoteRegistryServer .URL )
303
+ if err != nil {
304
+ t .Fatalf ("error parsing server url: %v" , err )
305
+ }
306
+ os .Setenv ("DOCKER_REGISTRY_URL" , serverURL .Host )
307
+ testImage .DockerImageReference = fmt .Sprintf ("%s/%s@%s" , serverURL .Host , repoName , testImage .Name )
308
+
309
+ testImageStream := registrytest .TestNewImageStreamObject (namespace , name , "latest" , testImage .Name , testImage .DockerImageReference )
310
+ if testImageStream .Annotations == nil {
311
+ testImageStream .Annotations = make (map [string ]string )
312
+ }
313
+ testImageStream .Annotations [imageapi .InsecureRepositoryAnnotation ] = "true"
314
+
315
+ client .AddReactor ("get" , "imagestreams" , imagetest .GetFakeImageStreamGetHandler (t , * testImageStream ))
316
+
317
+ localBlobStore := newTestBlobStore (nil )
318
+
319
+ ctx := WithTestPassthroughToUpstream (context .Background (), false )
320
+ repo := newTestRepositoryForPullthrough (t , ctx , nil , namespace , name , client , true )
321
+ ptbs := & pullthroughBlobStore {
322
+ BlobStore : localBlobStore ,
323
+ repo : repo ,
324
+ }
325
+
326
+ req , err := http .NewRequest ("GET" , fmt .Sprintf ("http://example.org/v2/user/app/blobs/%s" , dgst ), nil )
327
+ if err != nil {
328
+ t .Fatalf ("failed to create http request: %v" , err )
329
+ }
330
+ w := httptest .NewRecorder ()
331
+
332
+ if _ , err = ptbs .Stat (ctx , dgst ); err != nil {
333
+ t .Fatalf ("Stat returned unexpected error: %#+v" , err )
334
+ }
335
+
336
+ if err = ptbs .ServeBlob (ctx , w , req , dgst ); err != nil {
337
+ t .Fatalf ("ServeBlob returned unexpected error: %#+v" , err )
338
+ }
339
+
340
+ if w .Code != http .StatusOK {
341
+ t .Fatalf (`unexpected StatusCode: %d (expected %d)` , w .Code , http .StatusOK )
342
+ }
343
+
344
+ clstr := w .Header ().Get ("Content-Length" )
345
+ if cl , err := strconv .ParseInt (clstr , 10 , 64 ); err != nil {
346
+ t .Fatalf (`unexpected Content-Length: %q (expected "%d")` , clstr , int64 (len (blob1Content )))
347
+ } else {
348
+ if cl != int64 (len (blob1Content )) {
349
+ t .Fatalf ("Content-Length does not match expected size: %d != %d" , cl , int64 (len (blob1Content )))
350
+ }
351
+ }
352
+
353
+ body := w .Body .Bytes ()
354
+ if int64 (len (body )) != int64 (len (blob1Content )) {
355
+ t .Errorf ("unexpected size of body: %d != %d" , len (body ), int64 (len (blob1Content )))
356
+ }
357
+
358
+ if localBlobStore .bytesServed != 0 {
359
+ t .Fatalf ("remote blob served locally" )
360
+ }
361
+
362
+ expectedLocalCalls := map [string ]int {
363
+ "Stat" : 1 ,
364
+ "ServeBlob" : 1 ,
365
+ }
366
+
367
+ for name , expCount := range expectedLocalCalls {
368
+ count := localBlobStore .calls [name ]
369
+ if count != expCount {
370
+ t .Errorf ("expected %d calls to method %s of local blob store, not %d" , expCount , name , count )
371
+ }
372
+ }
373
+
374
+ for name , count := range localBlobStore .calls {
375
+ if _ , exists := expectedLocalCalls [name ]; ! exists {
376
+ t .Errorf ("expected no calls to method %s of local blob store, got %d" , name , count )
377
+ }
378
+ }
379
+ }
380
+
217
381
func TestPullthroughServeBlobInsecure (t * testing.T ) {
218
382
namespace := "user"
219
383
repo1 := "app1"
0 commit comments