Skip to content

Commit 1ae9449

Browse files
author
OpenShift Bot
authored
Merge pull request #13283 from legionus/dockerregistry-pullthrough-broken-registry
Merged by openshift-bot
2 parents c9eff77 + f298117 commit 1ae9449

File tree

2 files changed

+204
-4
lines changed

2 files changed

+204
-4
lines changed

pkg/dockerregistry/server/pullthroughblobstore.go

+40-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package server
22

33
import (
4+
"fmt"
45
"io"
56
"net/http"
7+
"os"
68
"sync"
79
"time"
810

@@ -26,6 +28,7 @@ var _ distribution.BlobStore = &pullthroughBlobStore{}
2628
// the image stream and looks for those that have the layer.
2729
func (pbs *pullthroughBlobStore) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
2830
context.GetLogger(ctx).Debugf("(*pullthroughBlobStore).Stat: starting with dgst=%s", dgst.String())
31+
2932
// check the local store for the blob
3033
desc, err := pbs.BlobStore.Stat(ctx, dgst)
3134
switch {
@@ -108,6 +111,35 @@ func setResponseHeaders(w http.ResponseWriter, length int64, mediaType string, d
108111
w.Header().Set("Etag", digest.String())
109112
}
110113

114+
// serveRemoteContent tries to use http.ServeContent for remote content.
115+
func serveRemoteContent(rw http.ResponseWriter, req *http.Request, desc distribution.Descriptor, remoteReader io.ReadSeeker) (bool, error) {
116+
// Set the appropriate content serving headers.
117+
setResponseHeaders(rw, desc.Size, desc.MediaType, desc.Digest)
118+
119+
// Fallback to Copy if request wasn't given.
120+
if req == nil {
121+
return false, nil
122+
}
123+
124+
// Check whether remoteReader is seekable. The remoteReader' Seek method must work: ServeContent uses
125+
// a seek to the end of the content to determine its size.
126+
if _, err := remoteReader.Seek(0, os.SEEK_END); err != nil {
127+
// The remoteReader isn't seekable. It means that the remote response under the hood of remoteReader
128+
// doesn't contain any Content-Range or Content-Length headers. In this case we need to rollback to
129+
// simple Copy.
130+
return false, nil
131+
}
132+
133+
// Move pointer back to begin.
134+
if _, err := remoteReader.Seek(0, os.SEEK_SET); err != nil {
135+
return false, err
136+
}
137+
138+
http.ServeContent(rw, req, desc.Digest.String(), time.Time{}, remoteReader)
139+
140+
return true, nil
141+
}
142+
111143
// inflight tracks currently downloading blobs
112144
var inflight = make(map[digest.Digest]struct{})
113145

@@ -129,12 +161,16 @@ func (pbs *pullthroughBlobStore) copyContent(store BlobGetterService, ctx contex
129161

130162
rw, ok := writer.(http.ResponseWriter)
131163
if ok {
132-
setResponseHeaders(rw, desc.Size, desc.MediaType, dgst)
133-
// serve range requests
134-
if req != nil {
135-
http.ServeContent(rw, req, desc.Digest.String(), time.Time{}, remoteReader)
164+
contentHandled, err := serveRemoteContent(rw, req, desc, remoteReader)
165+
if err != nil {
166+
return distribution.Descriptor{}, err
167+
}
168+
169+
if contentHandled {
136170
return desc, nil
137171
}
172+
173+
rw.Header().Set("Content-Length", fmt.Sprintf("%d", desc.Size))
138174
}
139175

140176
if _, err = io.CopyN(writer, remoteReader, desc.Size); err != nil {

pkg/dockerregistry/server/pullthroughblobstore_test.go

+164
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import (
44
"bytes"
55
"crypto/sha256"
66
"fmt"
7+
"io/ioutil"
78
"net/http"
89
"net/http/httptest"
910
"net/url"
1011
"os"
1112
"strconv"
13+
"strings"
1214
"testing"
1315
"time"
1416

@@ -214,6 +216,168 @@ func TestPullthroughServeBlob(t *testing.T) {
214216
}
215217
}
216218

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+
217381
func TestPullthroughServeBlobInsecure(t *testing.T) {
218382
namespace := "user"
219383
repo1 := "app1"

0 commit comments

Comments
 (0)