diff --git a/README.md b/README.md index 1c45765..20c5ca4 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ import ( func main() { // construct client pointing to your registry client, err := reggie.NewClient("http://localhost:5000", - reggie.WithDefaultName("my/repo"), + reggie.WithDefaultName("myorg/myrepo"), reggie.WithDebug(true)) if err != nil { panic(err) @@ -152,8 +152,8 @@ func main() { blobDigest := godigest.FromBytes(blob).String() // upload the first chunk - req = client.NewRequest(reggie.PATCH, resp.GetRelativeLocation()) - req.SetHeader("Content-Type", "application/octet-stream"). + req = client.NewRequest(reggie.PATCH, resp.GetRelativeLocation()). + SetHeader("Content-Type", "application/octet-stream"). SetHeader("Content-Length", fmt.Sprintf("%d", len(blobChunk1))). SetHeader("Content-Range", blobChunk1Range). SetBody(blobChunk1) @@ -163,8 +163,8 @@ func main() { } // upload the final chunk and close the session - req = client.NewRequest(reggie.PUT, resp.GetRelativeLocation()) - req.SetHeader("Content-Length", fmt.Sprintf("%d", len(blobChunk2))). + req = client.NewRequest(reggie.PUT, resp.GetRelativeLocation()). + SetHeader("Content-Length", fmt.Sprintf("%d", len(blobChunk2))). SetHeader("Content-Range", blobChunk2Range). SetHeader("Content-Type", "application/octet-stream"). SetQueryParam("digest", blobDigest). @@ -184,21 +184,15 @@ func main() { fmt.Printf("Blob content:\n%s\n", resp.String()) // upload the manifest (referencing the uploaded blob) - ref := "test" + ref := "mytag" manifest := []byte(fmt.Sprintf( - `{ - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "config": { - "digest": "%s", - "mediaType": "application/vnd.oci.image.config.v1+json", - "size": %d - }, - "layers": [], - "schemaVersion": 2 -}`, blobDigest, len(blob))) + "{ \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\", \"config\": { \"digest\": \"%s\", "+ + "\"mediaType\": \"application/vnd.oci.image.config.v1+json\","+" \"size\": %d }, \"layers\": [], "+ + "\"schemaVersion\": 2 }", + blobDigest, len(blob))) req = client.NewRequest(reggie.PUT, "/v2//manifests/", - reggie.WithReference(ref)) - req.SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json"). + reggie.WithReference(ref)). + SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json"). SetBody(manifest) resp, err = client.Do(req) if err != nil { @@ -207,13 +201,12 @@ func main() { // validate the uploaded manifest content req = client.NewRequest(reggie.GET, "/v2//manifests/", - reggie.WithReference(ref)) - req.SetHeader("Accept", "application/vnd.oci.image.manifest.v1+json") + reggie.WithReference(ref)). + SetHeader("Accept", "application/vnd.oci.image.manifest.v1+json") resp, err = client.Do(req) if err != nil { panic(err) } fmt.Printf("Manifest content:\n%s\n", resp.String()) } - ``` diff --git a/client.go b/client.go index ef6ac13..f8d9759 100644 --- a/client.go +++ b/client.go @@ -144,5 +144,6 @@ func createTransport() *http.Transport { TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, + DisableCompression: true, } } diff --git a/client_test.go b/client_test.go index 726a552..cbffad3 100644 --- a/client_test.go +++ b/client_test.go @@ -1,6 +1,7 @@ package reggie import ( + "bytes" "encoding/base64" "fmt" "net/http" @@ -9,6 +10,11 @@ import ( "testing" ) +var ( + lastCapturedRequest *http.Request + lastCapturedRequestBodyStr string +) + func TestClient(t *testing.T) { authTestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { expectedAuthHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte("testuser:testpass")) @@ -23,6 +29,11 @@ func TestClient(t *testing.T) { defer authTestServer.Close() registryTestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + lastCapturedRequest = r + buf := new(bytes.Buffer) + buf.ReadFrom(lastCapturedRequest.Body) + lastCapturedRequestBodyStr = buf.String() + h := r.Header.Get("Authorization") if h == "Bearer abc123" { w.Header().Set("Location", "http://abc123location.io/v2/blobs/uploads/e361aeb8-3181-11ea-850d-2e728ce88125") @@ -185,4 +196,40 @@ func TestClient(t *testing.T) { t.Fatalf("Expected error with bad address") } + // Make sure headers and body match after going through auth + req = client.NewRequest(PUT, "/a/b/c"). + SetHeader("Content-Length", "3"). + SetHeader("Content-Range", "0-2"). + SetHeader("Content-Type", "application/octet-stream"). + SetQueryParam("digest", "xyz"). + SetBody([]byte("abc")) + _, err = client.Do(req) + if err != nil { + t.Fatalf("Errors executing request: %s", err) + } + + // 5 headers expected: the ones we set plus "Authorization" and "User-Agent" + numHeaders := len(lastCapturedRequest.Header) + if numHeaders != 5 { + fmt.Println(lastCapturedRequest.Header) + t.Fatalf("Expected 5 headers total, instead got %d", numHeaders) + } + + // Just to be safe, lets check each of the 5 headers are ones we expect + for _, h := range []string{ + "Content-Length", + "Content-Range", + "Content-Type", + "Authorization", + "User-Agent", + } { + if lastCapturedRequest.Header.Get(h) == "" { + t.Fatalf("Missing header: %s", h) + } + } + + // Check that the body did not get lost somewhere in the multi-layer transport + if lastCapturedRequestBodyStr != "abc" { + t.Fatalf("Expected body to be \"abc\" but instead got %s", lastCapturedRequestBodyStr) + } } diff --git a/request.go b/request.go index b6558cb..bba74c5 100644 --- a/request.go +++ b/request.go @@ -1,9 +1,7 @@ package reggie import ( - "bytes" "fmt" - "io/ioutil" "regexp" "gopkg.in/resty.v1" @@ -53,11 +51,19 @@ func WithSessionID(id string) requestOption { } } +// SetBody wraps the resty SetBody and returns the request, allowing method chaining +func (req *Request) SetBody(body []byte) *Request { + req.Request.SetBody(body) + return req +} + +// SetHeader wraps the resty SetHeader and returns the request, allowing method chaining func (req *Request) SetHeader(header, content string) *Request { req.Request.SetHeader(header, content) return req } +// SetQueryParam wraps the resty SetQueryParam and returns the request, allowing method chaining func (req *Request) SetQueryParam(param, content string) *Request { req.Request.SetQueryParam(param, content) return req @@ -79,11 +85,6 @@ func (req *Request) Execute(method, url string) (*Response, error) { return resp, err } -func (req *Request) SetBody(body []byte) *Request { - req.Request.SetBody(ioutil.NopCloser(bytes.NewReader(body))) - return req -} - func validateRequest(req *Request) error { re := regexp.MustCompile("||||//{2,}") matches := re.FindAllString(req.URL, -1)