diff --git a/registry/blob.go b/registry/blob.go index bd59abec..60e0b22f 100644 --- a/registry/blob.go +++ b/registry/blob.go @@ -1,6 +1,7 @@ package registry import ( + "fmt" "io" "net/http" "net/url" @@ -21,6 +22,58 @@ func (registry *Registry) DownloadBlob(repository string, digest digest.Digest) return resp.Body, nil } +// Sending Monolithic chunked upload - following docker API specification for Chunked uploads : https://docs.docker.com/registry/spec/api/#listing-repositories +// See UploadBlob for more info about getBody +func (registry *Registry) UploadBlobToArtifactory(repository string, digest digest.Digest, content io.Reader, getBody func() (io.ReadCloser, error)) error { + uploadUrl, err := registry.initiateUpload(repository) + if err != nil { + return err + } + q := uploadUrl.Query() + q.Set("digest", digest.String()) + uploadUrl.RawQuery = q.Encode() + + registry.Logf("registry.blob.uploadToArtifactory url=%s repository=%s digest=%s", uploadUrl, repository, digest) + + uploadStep1, err := http.NewRequest("PATCH", uploadUrl.String(), content) + if err != nil { + return err + } + uploadStep1.Header.Set("Content-Type", "application/octet-stream") + if getBody != nil { + uploadStep1.GetBody = getBody + } + resp1, err := registry.Client.Do(uploadStep1) + if resp1 != nil { + defer resp1.Body.Close() + } + // TODO: retry upload more than 0 bytes were successfully transferred + // (HEAD upload UUID, adn check the Range header) + if err != nil { + if resp1 == nil { + return fmt.Errorf("error while uploading blob to %s, digest: %s: %s", repository, digest, err) + + } else { + return fmt.Errorf("error while uploading blob to %s: %v %v: digest: %s: %s", repository, resp1.StatusCode, resp1.Status, digest, err) + } + } + if resp1.StatusCode != 202 { + return fmt.Errorf("unexpected PATCH response while uploading blob to %s: %v %v: digest: %s", repository, resp1.StatusCode, resp1.Status, digest) + } + + uploadStep2, err := http.NewRequest("PUT", uploadUrl.String(), nil) + if err != nil { + return err + } + uploadStep2.Header.Set("Content-Type", "application/octet-stream") + if getBody != nil { + uploadStep2.GetBody = getBody + } + + _, err = registry.Client.Do(uploadStep2) + return err +} + // 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 diff --git a/registry/blob_test.go b/registry/blob_test.go index 6d5f27ec..702aa773 100644 --- a/registry/blob_test.go +++ b/registry/blob_test.go @@ -16,7 +16,13 @@ func TestRegistry_UploadBlob(t *testing.T) { foreachWritableTestcase(t, func(t *testing.T, tc *TestCase) { content := bytes.NewBuffer(blobData) - err := tc.Registry(t).UploadBlob(tc.Repository, digest, content, nil) + var err error + if !tc.Artifactory { + err = tc.Registry(t).UploadBlob(tc.Repository, digest, content, nil) + } else { + err = tc.Registry(t).UploadBlobToArtifactory(tc.Repository, digest, content, nil) + } + if err != nil { t.Error("UploadBlob() failed:", err) } @@ -44,7 +50,12 @@ func TestRegistry_UploadBlobFromFile(t *testing.T) { t.Fatal(err) } - err = tc.Registry(t).UploadBlob(tc.Repository, digest, blobReader, body) + if !tc.Artifactory { + err = tc.Registry(t).UploadBlob(tc.Repository, digest, blobReader, body) + } else { + err = tc.Registry(t).UploadBlobToArtifactory(tc.Repository, digest, blobReader, body) + } + if err != nil { t.Error("UploadBlob() failed:", err) } diff --git a/registry/testing_test.go b/registry/testing_test.go index 315f90f3..12f9c273 100644 --- a/registry/testing_test.go +++ b/registry/testing_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/docker/distribution" + "github.com/nokia/docker-registry-client/registry" "github.com/opencontainers/go-digest" ) @@ -25,10 +26,11 @@ type Expected struct { // TestCase represents a test case (normally read from a test data file). type TestCase struct { - Url string `json:"url"` - Repository string `json:"repository"` - Reference string `json:"reference"` - Writeable bool `json:"writeable,omitempty"` + Url string `json:"url"` + Repository string `json:"repository"` + Reference string `json:"reference"` + Writeable bool `json:"writeable,omitempty"` + Artifactory bool registry.Options registry *registry.Registry @@ -102,6 +104,27 @@ func testCases(t *testing.T) []*TestCase { } _testCases = append(_testCases, tc) } + + //add testcase for Artifactory if credentials are given in environment variables + artifactoryUsername := os.Getenv("DRC_TEST_ARTIFACTORY_USERNAME") + artifactoryPassword := os.Getenv("DRC_TEST_ARTIFACTORY_PASSWORD") + artifactoryUrl := os.Getenv("DRC_TEST_ARTIFACTORY_REGISTRY") + artifactoryRepo := os.Getenv("DRC_TEST_ARTIFACTORY_REPO") + if artifactoryUsername != "" && artifactoryPassword != "" && artifactoryUrl != "" && artifactoryRepo != "" { + tc := &TestCase{ + Url: artifactoryUrl, + Repository: artifactoryRepo, + Reference: "latest", + Writeable: true, + Artifactory: true, + Options: registry.Options{ + Username: username, + Password: password, + DoInitialPing: false, + }, + } + _testCases = append(_testCases, tc) + } return _testCases }