Skip to content
This repository has been archived by the owner on Mar 16, 2024. It is now read-only.

Fix UploadBlob() and PutManifest() in case of token authentication #60

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
language: go
go: 1.7.1
go: 1.8
sudo: false
script: make travis
notifications:
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ API](http://docs.docker.com/registry/spec/api/), for Go applications.
```go
import (
"github.com/heroku/docker-registry-client/registry"
"github.com/docker/distribution/digest"
digest "github.com/opencontainers/go-digest"
"github.com/docker/distribution/manifest"
"github.com/docker/libtrust"
)
Expand Down Expand Up @@ -120,8 +120,8 @@ if err != nil {
// …
}
if !exists {
stream :=
hub.UploadBlob("example/repo", digest, stream)
stream := bytes.NewBuffer(...)
hub.UploadBlob("example/repo", digest, stream, nil)
}
```

Expand Down
21 changes: 18 additions & 3 deletions registry/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@ func (registry *Registry) DownloadBlob(repository string, digest digest.Digest)
return resp.Body, nil
}

func (registry *Registry) UploadBlob(repository string, digest digest.Digest, content io.Reader) error {
// UploadBlob can be used to upload an FS layer or an image config file into the given repository.
// It uploads the bytes read from content. Digest must match with the hash of those bytes.
// In case of token authentication the HTTP request must be retried after a 401 Unauthorized response
// (see https://docs.docker.com/registry/spec/auth/token/). In this case the getBody function is called
// in order to retrieve a fresh instance of the content reader. This behaviour matches exactly of the
// GetBody parameter of http.Client. This also means that if content is of type *bytes.Buffer,
// *bytes.Reader or *strings.Reader, then GetBody is populated automatically (as explained in the
// documentation of http.NewRequest()), so nil can be passed as the getBody parameter.
func (registry *Registry) UploadBlob(repository string, digest digest.Digest, content io.Reader, getBody func() (io.ReadCloser, error)) error {
uploadUrl, err := registry.initiateUpload(repository)
if err != nil {
return err
Expand All @@ -37,9 +45,16 @@ func (registry *Registry) UploadBlob(repository string, digest digest.Digest, co
return err
}
upload.Header.Set("Content-Type", "application/octet-stream")
if getBody != nil {
upload.GetBody = getBody
}

_, err = registry.Client.Do(upload)
return err
resp, err := registry.Client.Do(upload)
if err != nil {
return err
}
_ = resp.Body.Close()
return nil
}

func (registry *Registry) HasBlob(repository string, digest digest.Digest) (bool, error) {
Expand Down
82 changes: 82 additions & 0 deletions registry/blob_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package registry

import (
"bytes"
"io"
"io/ioutil"
"os"
"testing"

digest "github.com/opencontainers/go-digest"
)

const dockerhubUrl = "https://registry-1.docker.io"

func dockerhubTestCredentials(t *testing.T) (username, password string) {
const usernameEnv, passwordEnv = "DRC_TEST_DOCKERHUB_USERNAME", "DRC_TEST_DOCKERHUB_PASSWORD"
username = os.Getenv(usernameEnv)
password = os.Getenv(passwordEnv)
if username == "" || password == "" {
t.Skipf("DockerHub test credentials aren't specified in environment variables %s and %s", usernameEnv, passwordEnv)
}
return
}

func TestRegistry_UploadBlob(t *testing.T) {
username, password := dockerhubTestCredentials(t)
repository := username + "/docker-registry-client-test"
registry, err := New(dockerhubUrl, username, password)
if err != nil {
t.Fatal("couldn't connect to registry:", err)
}

blobData := []byte("This is a test blob.")
digest := digest.FromBytes(blobData)
content := bytes.NewBuffer(blobData)
err = registry.UploadBlob(repository, digest, content, nil)
if err != nil {
t.Error("couldn't upload blob:", err)
}

}

func TestRegistry_UploadBlobFromFile(t *testing.T) {
username, password := dockerhubTestCredentials(t)
repository := username + "/docker-registry-client-test"
registry, err := New(dockerhubUrl, username, password)
if err != nil {
t.Fatal("couldn't create registry client:", err)
}

// create blob file
blobData := []byte("This is a test blob.")
tmpfile, err := ioutil.TempFile("", "testblob")
if err != nil {
t.Fatal(err)
}
filename := tmpfile.Name()
defer os.Remove(filename) // error deliberately ignored
if _, err := tmpfile.Write(blobData); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
t.Fatal(err)
}

// prepare UploadBlob() parameters
digest := digest.FromBytes(blobData)
body := func() (io.ReadCloser, error) {
// NOTE: the file will be closed by UploadBlob() (actually the http.Client)
return os.Open(filename)
}
blobReader, err := body()
if err != nil {
t.Fatal(err)
}

// call UploadBlob()
err = registry.UploadBlob(repository, digest, blobReader, body)
if err != nil {
t.Error("UploadBlob() failed:", err)
}
}
27 changes: 27 additions & 0 deletions registry/tokentransport.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ type TokenTransport struct {
Password string
}

// CannotReplayRequestBody describes the HTTP error, when the server responded with a WWW-Authenticate header,
// but the body of the request (POST, PUT, PATCH) couldn't be replayed
type CannotReplayRequestBody struct {
Err error
}

func (e CannotReplayRequestBody) Error() string {
msg := "HTTP server responded with a WWW-Authenticate header, but the body of the request cannot be replayed"
if e.Err != nil {
msg += ": " + e.Err.Error()
}
return msg
}

var _ error = CannotReplayRequestBody{}

func (t *TokenTransport) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := t.Transport.RoundTrip(req)
if err != nil {
Expand Down Expand Up @@ -70,6 +86,17 @@ func (t *TokenTransport) auth(authService *authService) (string, *http.Response,

func (t *TokenTransport) retry(req *http.Request, token string) (*http.Response, error) {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
if req.Body != nil {
// reinitialize Body reader if necessary
if req.GetBody == nil {
return nil, &CannotReplayRequestBody{fmt.Errorf("missing GetBody parameter")}
}
var err error
req.Body, err = req.GetBody()
if err != nil {
return nil, &CannotReplayRequestBody{Err: err}
}
}
resp, err := t.Transport.RoundTrip(req)
return resp, err
}
Expand Down