github.com/anuvu/zot@v1.3.4/pkg/compliance/v1_0_0/check.go (about)

     1  // nolint: dupl
     2  package v1_0_0 // nolint:stylecheck,golint
     3  
     4  import (
     5  	"bytes"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/anuvu/zot/pkg/api"
    15  	"github.com/anuvu/zot/pkg/compliance"
    16  	. "github.com/anuvu/zot/test" // nolint:golint,stylecheck
    17  	godigest "github.com/opencontainers/go-digest"
    18  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    19  	. "github.com/smartystreets/goconvey/convey" // nolint:golint,stylecheck
    20  	"github.com/smartystreets/goconvey/convey/reporting"
    21  	"gopkg.in/resty.v1"
    22  )
    23  
    24  func CheckWorkflows(t *testing.T, config *compliance.Config) {
    25  	if config == nil || config.Address == "" || config.Port == "" {
    26  		panic("insufficient config")
    27  	}
    28  
    29  	if config.OutputJSON {
    30  		outputJSONEnter()
    31  
    32  		defer outputJSONExit()
    33  	}
    34  
    35  	baseURL := fmt.Sprintf("http://%s:%s", config.Address, config.Port)
    36  
    37  	storageInfo := config.StorageInfo
    38  
    39  	fmt.Println("------------------------------")
    40  	fmt.Println("Checking for v1.0.0 compliance")
    41  	fmt.Println("------------------------------")
    42  
    43  	Convey("Make API calls to the controller", t, func(c C) {
    44  		Convey("Check version", func() {
    45  			Print("\nCheck version")
    46  			resp, err := resty.R().Get(baseURL + "/v2/")
    47  			So(err, ShouldBeNil)
    48  			So(resp.StatusCode(), ShouldEqual, 200)
    49  		})
    50  
    51  		Convey("Get repository catalog", func() {
    52  			Print("\nGet repository catalog")
    53  			resp, err := resty.R().Get(baseURL + "/v2/_catalog")
    54  			So(err, ShouldBeNil)
    55  			So(resp.StatusCode(), ShouldEqual, 200)
    56  			So(resp.String(), ShouldNotBeEmpty)
    57  			So(resp.Header().Get("Content-Type"), ShouldEqual, api.DefaultMediaType)
    58  			var repoList api.RepositoryList
    59  			err = json.Unmarshal(resp.Body(), &repoList)
    60  			So(err, ShouldBeNil)
    61  			So(len(repoList.Repositories), ShouldEqual, 0)
    62  
    63  			// after newly created upload should succeed
    64  			resp, err = resty.R().Post(baseURL + "/v2/z/blobs/uploads/")
    65  			So(err, ShouldBeNil)
    66  			So(resp.StatusCode(), ShouldEqual, 202)
    67  
    68  			// after newly created upload should succeed
    69  			resp, err = resty.R().Post(baseURL + "/v2/a/b/c/d/blobs/uploads/")
    70  			So(err, ShouldBeNil)
    71  			So(resp.StatusCode(), ShouldEqual, 202)
    72  
    73  			resp, err = resty.R().SetResult(&api.RepositoryList{}).Get(baseURL + "/v2/_catalog")
    74  			So(err, ShouldBeNil)
    75  			So(resp.StatusCode(), ShouldEqual, 200)
    76  			So(resp.String(), ShouldNotBeEmpty)
    77  			r := resp.Result().(*api.RepositoryList)
    78  			if !config.Compliance {
    79  				// stricter check for zot ci/cd
    80  				So(len(r.Repositories), ShouldBeGreaterThan, 0)
    81  				So(r.Repositories[0], ShouldEqual, "a/b/c/d")
    82  				So(r.Repositories[1], ShouldEqual, "z")
    83  			}
    84  		})
    85  
    86  		Convey("Get images in a repository", func() {
    87  			Print("\nGet images in a repository")
    88  			// non-existent repository should fail
    89  			resp, err := resty.R().Get(baseURL + "/v2/repo1/tags/list")
    90  			So(err, ShouldBeNil)
    91  			So(resp.StatusCode(), ShouldEqual, 404)
    92  			So(resp.String(), ShouldNotBeEmpty)
    93  
    94  			// after newly created upload should succeed
    95  			resp, err = resty.R().Post(baseURL + "/v2/repo1/blobs/uploads/")
    96  			So(err, ShouldBeNil)
    97  			So(resp.StatusCode(), ShouldEqual, 202)
    98  
    99  			resp, err = resty.R().Get(baseURL + "/v2/repo1/tags/list")
   100  			So(err, ShouldBeNil)
   101  			if !config.Compliance {
   102  				// stricter check for zot ci/cd
   103  				So(resp.StatusCode(), ShouldEqual, 200)
   104  				So(resp.String(), ShouldNotBeEmpty)
   105  			}
   106  		})
   107  
   108  		Convey("Monolithic blob upload", func() {
   109  			Print("\nMonolithic blob upload")
   110  			resp, err := resty.R().Post(baseURL + "/v2/repo2/blobs/uploads/")
   111  			So(err, ShouldBeNil)
   112  			So(resp.StatusCode(), ShouldEqual, 202)
   113  			loc := Location(baseURL, resp)
   114  			So(loc, ShouldNotBeEmpty)
   115  
   116  			resp, err = resty.R().Get(loc)
   117  			So(err, ShouldBeNil)
   118  			So(resp.StatusCode(), ShouldEqual, 204)
   119  
   120  			resp, err = resty.R().Get(baseURL + "/v2/repo2/tags/list")
   121  			So(err, ShouldBeNil)
   122  			if !config.Compliance {
   123  				// stricter check for zot ci/cd
   124  				So(resp.StatusCode(), ShouldEqual, 200)
   125  				So(resp.String(), ShouldNotBeEmpty)
   126  			}
   127  
   128  			// without a "?digest=<>" should fail
   129  			content := []byte("this is a blob1")
   130  			digest := godigest.FromBytes(content)
   131  			So(digest, ShouldNotBeNil)
   132  			resp, err = resty.R().Put(loc)
   133  			So(err, ShouldBeNil)
   134  			So(resp.StatusCode(), ShouldEqual, 400)
   135  			// without the Content-Length should fail
   136  			resp, err = resty.R().SetQueryParam("digest", digest.String()).Put(loc)
   137  			So(err, ShouldBeNil)
   138  			So(resp.StatusCode(), ShouldEqual, 400)
   139  			// without any data to send, should fail
   140  			resp, err = resty.R().SetQueryParam("digest", digest.String()).
   141  				SetHeader("Content-Type", "application/octet-stream").Put(loc)
   142  			So(err, ShouldBeNil)
   143  			So(resp.StatusCode(), ShouldEqual, 400)
   144  			// monolithic blob upload: success
   145  			resp, err = resty.R().SetQueryParam("digest", digest.String()).
   146  				SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc)
   147  			So(err, ShouldBeNil)
   148  			So(resp.StatusCode(), ShouldEqual, 201)
   149  			blobLoc := Location(baseURL, resp)
   150  			So(blobLoc, ShouldNotBeEmpty)
   151  			So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
   152  			So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty)
   153  			// upload reference should now be removed
   154  			resp, err = resty.R().Get(loc)
   155  			So(err, ShouldBeNil)
   156  			So(resp.StatusCode(), ShouldEqual, 404)
   157  			// blob reference should be accessible
   158  			resp, err = resty.R().Get(blobLoc)
   159  			So(err, ShouldBeNil)
   160  			So(resp.StatusCode(), ShouldEqual, 200)
   161  		})
   162  
   163  		Convey("Monolithic blob upload with body", func() {
   164  			Print("\nMonolithic blob upload")
   165  			// create content
   166  			content := []byte("this is a blob2")
   167  			digest := godigest.FromBytes(content)
   168  			So(digest, ShouldNotBeNil)
   169  			// setting invalid URL params should fail
   170  			resp, err := resty.R().
   171  				SetQueryParam("digest", digest.String()).
   172  				SetQueryParam("from", digest.String()).
   173  				SetHeader("Content-Type", "application/octet-stream").
   174  				SetBody(content).
   175  				Post(baseURL + "/v2/repo2/blobs/uploads/")
   176  			So(err, ShouldBeNil)
   177  			So(resp.StatusCode(), ShouldEqual, 405)
   178  			// setting a "?digest=<>" but without body should fail
   179  			resp, err = resty.R().
   180  				SetQueryParam("digest", digest.String()).
   181  				SetHeader("Content-Type", "application/octet-stream").
   182  				Post(baseURL + "/v2/repo2/blobs/uploads/")
   183  			So(err, ShouldBeNil)
   184  			So(resp.StatusCode(), ShouldEqual, 400)
   185  			// set a "?digest=<>"
   186  			resp, err = resty.R().
   187  				SetQueryParam("digest", digest.String()).
   188  				SetHeader("Content-Type", "application/octet-stream").
   189  				SetBody(content).
   190  				Post(baseURL + "/v2/repo2/blobs/uploads/")
   191  			So(err, ShouldBeNil)
   192  			So(resp.StatusCode(), ShouldEqual, 201)
   193  			loc := Location(baseURL, resp)
   194  			So(loc, ShouldNotBeEmpty)
   195  			// blob reference should be accessible
   196  			resp, err = resty.R().Get(loc)
   197  			So(err, ShouldBeNil)
   198  			So(resp.StatusCode(), ShouldEqual, 200)
   199  		})
   200  
   201  		Convey("Monolithic blob upload with multiple name components", func() {
   202  			Print("\nMonolithic blob upload with multiple name components")
   203  			resp, err := resty.R().Post(baseURL + "/v2/repo10/repo20/repo30/blobs/uploads/")
   204  			So(err, ShouldBeNil)
   205  			So(resp.StatusCode(), ShouldEqual, 202)
   206  			loc := Location(baseURL, resp)
   207  			So(loc, ShouldNotBeEmpty)
   208  
   209  			resp, err = resty.R().Get(loc)
   210  			So(err, ShouldBeNil)
   211  			So(resp.StatusCode(), ShouldEqual, 204)
   212  
   213  			resp, err = resty.R().Get(baseURL + "/v2/repo10/repo20/repo30/tags/list")
   214  			So(err, ShouldBeNil)
   215  			if !config.Compliance {
   216  				// stricter check for zot ci/cd
   217  				So(resp.StatusCode(), ShouldEqual, 200)
   218  				So(resp.String(), ShouldNotBeEmpty)
   219  			}
   220  
   221  			// without a "?digest=<>" should fail
   222  			content := []byte("this is a blob3")
   223  			digest := godigest.FromBytes(content)
   224  			So(digest, ShouldNotBeNil)
   225  			resp, err = resty.R().Put(loc)
   226  			So(err, ShouldBeNil)
   227  			So(resp.StatusCode(), ShouldEqual, 400)
   228  			// without the Content-Length should fail
   229  			resp, err = resty.R().SetQueryParam("digest", digest.String()).Put(loc)
   230  			So(err, ShouldBeNil)
   231  			So(resp.StatusCode(), ShouldEqual, 400)
   232  			// without any data to send, should fail
   233  			resp, err = resty.R().SetQueryParam("digest", digest.String()).
   234  				SetHeader("Content-Type", "application/octet-stream").Put(loc)
   235  			So(err, ShouldBeNil)
   236  			So(resp.StatusCode(), ShouldEqual, 400)
   237  			// monolithic blob upload: success
   238  			resp, err = resty.R().SetQueryParam("digest", digest.String()).
   239  				SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc)
   240  			So(err, ShouldBeNil)
   241  			So(resp.StatusCode(), ShouldEqual, 201)
   242  			blobLoc := Location(baseURL, resp)
   243  			So(blobLoc, ShouldNotBeEmpty)
   244  			So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
   245  			So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty)
   246  			// upload reference should now be removed
   247  			resp, err = resty.R().Get(loc)
   248  			So(err, ShouldBeNil)
   249  			So(resp.StatusCode(), ShouldEqual, 404)
   250  			// blob reference should be accessible
   251  			resp, err = resty.R().Get(blobLoc)
   252  			So(err, ShouldBeNil)
   253  			So(resp.StatusCode(), ShouldEqual, 200)
   254  		})
   255  
   256  		Convey("Chunked blob upload", func() {
   257  			Print("\nChunked blob upload")
   258  			resp, err := resty.R().Post(baseURL + "/v2/repo3/blobs/uploads/")
   259  			So(err, ShouldBeNil)
   260  			So(resp.StatusCode(), ShouldEqual, 202)
   261  			loc := Location(baseURL, resp)
   262  			So(loc, ShouldNotBeEmpty)
   263  
   264  			var buf bytes.Buffer
   265  			chunk1 := []byte("this is the first chunk1")
   266  			n, err := buf.Write(chunk1)
   267  			So(n, ShouldEqual, len(chunk1))
   268  			So(err, ShouldBeNil)
   269  
   270  			// write first chunk
   271  			contentRange := fmt.Sprintf("%d-%d", 0, len(chunk1)-1)
   272  			resp, err = resty.R().SetHeader("Content-Type", "application/octet-stream").
   273  				SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(loc)
   274  			So(err, ShouldBeNil)
   275  			So(resp.StatusCode(), ShouldEqual, 202)
   276  
   277  			// check progress
   278  			resp, err = resty.R().Get(loc)
   279  			So(err, ShouldBeNil)
   280  			So(resp.StatusCode(), ShouldEqual, 204)
   281  			r := resp.Header().Get("Range")
   282  			So(r, ShouldNotBeEmpty)
   283  			So(r, ShouldEqual, "bytes="+contentRange)
   284  
   285  			// write same chunk should fail
   286  			contentRange = fmt.Sprintf("%d-%d", 0, len(chunk1)-1)
   287  			resp, err = resty.R().SetHeader("Content-Type", "application/octet-stream").
   288  				SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(loc)
   289  			So(err, ShouldBeNil)
   290  			So(resp.StatusCode(), ShouldEqual, 416)
   291  			So(resp.String(), ShouldNotBeEmpty)
   292  
   293  			chunk2 := []byte("this is the second chunk1")
   294  			n, err = buf.Write(chunk2)
   295  			So(n, ShouldEqual, len(chunk2))
   296  			So(err, ShouldBeNil)
   297  
   298  			digest := godigest.FromBytes(buf.Bytes())
   299  			So(digest, ShouldNotBeNil)
   300  
   301  			// write final chunk
   302  			contentRange = fmt.Sprintf("%d-%d", len(chunk1), len(buf.Bytes())-1)
   303  			resp, err = resty.R().SetQueryParam("digest", digest.String()).
   304  				SetHeader("Content-Range", contentRange).
   305  				SetHeader("Content-Type", "application/octet-stream").SetBody(chunk2).Put(loc)
   306  			So(err, ShouldBeNil)
   307  			So(resp.StatusCode(), ShouldEqual, 201)
   308  			blobLoc := Location(baseURL, resp)
   309  			So(err, ShouldBeNil)
   310  			So(resp.StatusCode(), ShouldEqual, 201)
   311  			So(blobLoc, ShouldNotBeEmpty)
   312  			So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
   313  			So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty)
   314  			// upload reference should now be removed
   315  			resp, err = resty.R().Get(loc)
   316  			So(err, ShouldBeNil)
   317  			So(resp.StatusCode(), ShouldEqual, 404)
   318  			// blob reference should be accessible
   319  			resp, err = resty.R().Get(blobLoc)
   320  			So(err, ShouldBeNil)
   321  			So(resp.StatusCode(), ShouldEqual, 200)
   322  		})
   323  
   324  		Convey("Chunked blob upload with multiple name components", func() {
   325  			Print("\nChunked blob upload with multiple name components")
   326  			resp, err := resty.R().Post(baseURL + "/v2/repo40/repo50/repo60/blobs/uploads/")
   327  			So(err, ShouldBeNil)
   328  			So(resp.StatusCode(), ShouldEqual, 202)
   329  			loc := Location(baseURL, resp)
   330  			So(loc, ShouldNotBeEmpty)
   331  
   332  			var buf bytes.Buffer
   333  			chunk1 := []byte("this is the first chunk2")
   334  			n, err := buf.Write(chunk1)
   335  			So(n, ShouldEqual, len(chunk1))
   336  			So(err, ShouldBeNil)
   337  
   338  			// write first chunk
   339  			contentRange := fmt.Sprintf("%d-%d", 0, len(chunk1)-1)
   340  			resp, err = resty.R().SetHeader("Content-Type", "application/octet-stream").
   341  				SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(loc)
   342  			So(err, ShouldBeNil)
   343  			So(resp.StatusCode(), ShouldEqual, 202)
   344  
   345  			// check progress
   346  			resp, err = resty.R().Get(loc)
   347  			So(err, ShouldBeNil)
   348  			So(resp.StatusCode(), ShouldEqual, 204)
   349  			r := resp.Header().Get("Range")
   350  			So(r, ShouldNotBeEmpty)
   351  			So(r, ShouldEqual, "bytes="+contentRange)
   352  
   353  			// write same chunk should fail
   354  			contentRange = fmt.Sprintf("%d-%d", 0, len(chunk1)-1)
   355  			resp, err = resty.R().SetHeader("Content-Type", "application/octet-stream").
   356  				SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(loc)
   357  			So(err, ShouldBeNil)
   358  			So(resp.StatusCode(), ShouldEqual, 416)
   359  			So(resp.String(), ShouldNotBeEmpty)
   360  
   361  			chunk2 := []byte("this is the second chunk2")
   362  			n, err = buf.Write(chunk2)
   363  			So(n, ShouldEqual, len(chunk2))
   364  			So(err, ShouldBeNil)
   365  
   366  			digest := godigest.FromBytes(buf.Bytes())
   367  			So(digest, ShouldNotBeNil)
   368  
   369  			// write final chunk
   370  			contentRange = fmt.Sprintf("%d-%d", len(chunk1), len(buf.Bytes())-1)
   371  			resp, err = resty.R().SetQueryParam("digest", digest.String()).
   372  				SetHeader("Content-Range", contentRange).
   373  				SetHeader("Content-Type", "application/octet-stream").SetBody(chunk2).Put(loc)
   374  			So(err, ShouldBeNil)
   375  			So(resp.StatusCode(), ShouldEqual, 201)
   376  			blobLoc := Location(baseURL, resp)
   377  			So(err, ShouldBeNil)
   378  			So(resp.StatusCode(), ShouldEqual, 201)
   379  			So(blobLoc, ShouldNotBeEmpty)
   380  			So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
   381  			So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty)
   382  			// upload reference should now be removed
   383  			resp, err = resty.R().Get(loc)
   384  			So(err, ShouldBeNil)
   385  			So(resp.StatusCode(), ShouldEqual, 404)
   386  			// blob reference should be accessible
   387  			resp, err = resty.R().Get(blobLoc)
   388  			So(err, ShouldBeNil)
   389  			So(resp.StatusCode(), ShouldEqual, 200)
   390  		})
   391  
   392  		Convey("Create and delete uploads", func() {
   393  			Print("\nCreate and delete uploads")
   394  			// create a upload
   395  			resp, err := resty.R().Post(baseURL + "/v2/repo4/blobs/uploads/")
   396  			So(err, ShouldBeNil)
   397  			So(resp.StatusCode(), ShouldEqual, 202)
   398  			loc := Location(baseURL, resp)
   399  			So(loc, ShouldNotBeEmpty)
   400  
   401  			// delete this upload
   402  			resp, err = resty.R().Delete(loc)
   403  			So(err, ShouldBeNil)
   404  			So(resp.StatusCode(), ShouldEqual, 204)
   405  		})
   406  
   407  		Convey("Create and delete blobs", func() {
   408  			Print("\nCreate and delete blobs")
   409  			// create a upload
   410  			resp, err := resty.R().Post(baseURL + "/v2/repo5/blobs/uploads/")
   411  			So(err, ShouldBeNil)
   412  			So(resp.StatusCode(), ShouldEqual, 202)
   413  			loc := Location(baseURL, resp)
   414  			So(loc, ShouldNotBeEmpty)
   415  
   416  			content := []byte("this is a blob4")
   417  			digest := godigest.FromBytes(content)
   418  			So(digest, ShouldNotBeNil)
   419  			// monolithic blob upload
   420  			resp, err = resty.R().SetQueryParam("digest", digest.String()).
   421  				SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc)
   422  			So(err, ShouldBeNil)
   423  			So(resp.StatusCode(), ShouldEqual, 201)
   424  			blobLoc := Location(baseURL, resp)
   425  			So(blobLoc, ShouldNotBeEmpty)
   426  			So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty)
   427  
   428  			// delete this blob
   429  			resp, err = resty.R().Delete(blobLoc)
   430  			So(err, ShouldBeNil)
   431  			So(resp.StatusCode(), ShouldEqual, 202)
   432  			So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
   433  		})
   434  
   435  		Convey("Mount blobs", func() {
   436  			Print("\nMount blobs from another repository")
   437  			// create a upload
   438  			resp, err := resty.R().Post(baseURL + "/v2/repo6/blobs/uploads/?digest=\"abc\"&&from=\"xyz\"")
   439  			So(err, ShouldBeNil)
   440  			So(resp.StatusCode(), ShouldBeIn, []int{201, 202, 405})
   441  		})
   442  
   443  		Convey("Manifests", func() {
   444  			Print("\nManifests")
   445  			// create a blob/layer
   446  			resp, err := resty.R().Post(baseURL + "/v2/repo7/blobs/uploads/")
   447  			So(err, ShouldBeNil)
   448  			So(resp.StatusCode(), ShouldEqual, 202)
   449  			loc := Location(baseURL, resp)
   450  			So(loc, ShouldNotBeEmpty)
   451  
   452  			// since we are not specifying any prefix i.e provided in config while starting server,
   453  			// so it should store repo7 to global root dir
   454  			_, err = os.Stat(path.Join(storageInfo[0], "repo7"))
   455  			So(err, ShouldBeNil)
   456  
   457  			resp, err = resty.R().Get(loc)
   458  			So(err, ShouldBeNil)
   459  			So(resp.StatusCode(), ShouldEqual, 204)
   460  			content := []byte("this is a blob5")
   461  			digest := godigest.FromBytes(content)
   462  			So(digest, ShouldNotBeNil)
   463  			// monolithic blob upload: success
   464  			resp, err = resty.R().SetQueryParam("digest", digest.String()).
   465  				SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc)
   466  			So(err, ShouldBeNil)
   467  			So(resp.StatusCode(), ShouldEqual, 201)
   468  			blobLoc := resp.Header().Get("Location")
   469  			So(blobLoc, ShouldNotBeEmpty)
   470  			So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
   471  			So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty)
   472  
   473  			// check a non-existent manifest
   474  			resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   475  				SetBody(content).Head(baseURL + "/v2/unknown/manifests/test:1.0")
   476  			So(err, ShouldBeNil)
   477  			So(resp.StatusCode(), ShouldEqual, 404)
   478  
   479  			// create a manifest
   480  			m := ispec.Manifest{
   481  				Config: ispec.Descriptor{
   482  					Digest: digest,
   483  					Size:   int64(len(content)),
   484  				},
   485  				Layers: []ispec.Descriptor{
   486  					{
   487  						MediaType: "application/vnd.oci.image.layer.v1.tar",
   488  						Digest:    digest,
   489  						Size:      int64(len(content)),
   490  					},
   491  				},
   492  			}
   493  			m.SchemaVersion = 2
   494  			content, err = json.Marshal(m)
   495  			So(err, ShouldBeNil)
   496  			digest = godigest.FromBytes(content)
   497  			So(digest, ShouldNotBeNil)
   498  			resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   499  				SetBody(content).Put(baseURL + "/v2/repo7/manifests/test:1.0")
   500  			So(err, ShouldBeNil)
   501  			So(resp.StatusCode(), ShouldEqual, 201)
   502  			d := resp.Header().Get(api.DistContentDigestKey)
   503  			So(d, ShouldNotBeEmpty)
   504  			So(d, ShouldEqual, digest.String())
   505  
   506  			resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   507  				SetBody(content).Put(baseURL + "/v2/repo7/manifests/test:1.0.1")
   508  			So(err, ShouldBeNil)
   509  			So(resp.StatusCode(), ShouldEqual, 201)
   510  			d = resp.Header().Get(api.DistContentDigestKey)
   511  			So(d, ShouldNotBeEmpty)
   512  			So(d, ShouldEqual, digest.String())
   513  
   514  			content = []byte("this is a blob5")
   515  			digest = godigest.FromBytes(content)
   516  			So(digest, ShouldNotBeNil)
   517  			// create a manifest with same blob but a different tag
   518  			m = ispec.Manifest{
   519  				Config: ispec.Descriptor{
   520  					Digest: digest,
   521  					Size:   int64(len(content)),
   522  				},
   523  				Layers: []ispec.Descriptor{
   524  					{
   525  						MediaType: "application/vnd.oci.image.layer.v1.tar",
   526  						Digest:    digest,
   527  						Size:      int64(len(content)),
   528  					},
   529  				},
   530  			}
   531  			m.SchemaVersion = 2
   532  			content, err = json.Marshal(m)
   533  			So(err, ShouldBeNil)
   534  			digest = godigest.FromBytes(content)
   535  			So(digest, ShouldNotBeNil)
   536  			resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   537  				SetBody(content).Put(baseURL + "/v2/repo7/manifests/test:2.0")
   538  			So(err, ShouldBeNil)
   539  			So(resp.StatusCode(), ShouldEqual, 201)
   540  			d = resp.Header().Get(api.DistContentDigestKey)
   541  			So(d, ShouldNotBeEmpty)
   542  			So(d, ShouldEqual, digest.String())
   543  
   544  			// check/get by tag
   545  			resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:1.0")
   546  			So(err, ShouldBeNil)
   547  			So(resp.StatusCode(), ShouldEqual, 200)
   548  			So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
   549  			resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:1.0")
   550  			So(err, ShouldBeNil)
   551  			So(resp.StatusCode(), ShouldEqual, 200)
   552  			So(resp.Body(), ShouldNotBeEmpty)
   553  			// check/get by reference
   554  			resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/" + digest.String())
   555  			So(err, ShouldBeNil)
   556  			So(resp.StatusCode(), ShouldEqual, 200)
   557  			So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
   558  			resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/" + digest.String())
   559  			So(err, ShouldBeNil)
   560  			So(resp.StatusCode(), ShouldEqual, 200)
   561  			So(resp.Body(), ShouldNotBeEmpty)
   562  
   563  			// delete manifest by tag should pass
   564  			resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/test:1.0")
   565  			So(err, ShouldBeNil)
   566  			So(resp.StatusCode(), ShouldEqual, 202)
   567  			// delete manifest by digest (1.0 deleted but 1.0.1 has same reference)
   568  			resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String())
   569  			So(err, ShouldBeNil)
   570  			So(resp.StatusCode(), ShouldEqual, 202)
   571  			// delete manifest by digest
   572  			resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String())
   573  			So(err, ShouldBeNil)
   574  			So(resp.StatusCode(), ShouldEqual, 404)
   575  			// delete again should fail
   576  			resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String())
   577  			So(err, ShouldBeNil)
   578  			So(resp.StatusCode(), ShouldEqual, 404)
   579  
   580  			// check/get by tag
   581  			resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:1.0")
   582  			So(err, ShouldBeNil)
   583  			So(resp.StatusCode(), ShouldEqual, 404)
   584  			resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:1.0")
   585  			So(err, ShouldBeNil)
   586  			So(resp.StatusCode(), ShouldEqual, 404)
   587  			So(resp.Body(), ShouldNotBeEmpty)
   588  			resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:2.0")
   589  			So(err, ShouldBeNil)
   590  			So(resp.StatusCode(), ShouldEqual, 404)
   591  			resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:2.0")
   592  			So(err, ShouldBeNil)
   593  			So(resp.StatusCode(), ShouldEqual, 404)
   594  			So(resp.Body(), ShouldNotBeEmpty)
   595  			// check/get by reference
   596  			resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/" + digest.String())
   597  			So(err, ShouldBeNil)
   598  			So(resp.StatusCode(), ShouldEqual, 404)
   599  			resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/" + digest.String())
   600  			So(err, ShouldBeNil)
   601  			So(resp.StatusCode(), ShouldEqual, 404)
   602  			So(resp.Body(), ShouldNotBeEmpty)
   603  		})
   604  
   605  		// pagination
   606  		Convey("Pagination", func() {
   607  			Print("\nPagination")
   608  
   609  			for i := 0; i <= 4; i++ {
   610  				// create a blob/layer
   611  				resp, err := resty.R().Post(baseURL + "/v2/page0/blobs/uploads/")
   612  				So(err, ShouldBeNil)
   613  				So(resp.StatusCode(), ShouldEqual, 202)
   614  				loc := Location(baseURL, resp)
   615  				So(loc, ShouldNotBeEmpty)
   616  
   617  				resp, err = resty.R().Get(loc)
   618  				So(err, ShouldBeNil)
   619  				So(resp.StatusCode(), ShouldEqual, 204)
   620  				content := []byte("this is a blob7")
   621  				digest := godigest.FromBytes(content)
   622  				So(digest, ShouldNotBeNil)
   623  				// monolithic blob upload: success
   624  				resp, err = resty.R().SetQueryParam("digest", digest.String()).
   625  					SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc)
   626  				So(err, ShouldBeNil)
   627  				So(resp.StatusCode(), ShouldEqual, 201)
   628  				blobLoc := resp.Header().Get("Location")
   629  				So(blobLoc, ShouldNotBeEmpty)
   630  				So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
   631  				So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty)
   632  
   633  				// create a manifest
   634  				m := ispec.Manifest{
   635  					Config: ispec.Descriptor{
   636  						Digest: digest,
   637  						Size:   int64(len(content)),
   638  					},
   639  					Layers: []ispec.Descriptor{
   640  						{
   641  							MediaType: "application/vnd.oci.image.layer.v1.tar",
   642  							Digest:    digest,
   643  							Size:      int64(len(content)),
   644  						},
   645  					},
   646  				}
   647  				m.SchemaVersion = 2
   648  				content, err = json.Marshal(m)
   649  				So(err, ShouldBeNil)
   650  				digest = godigest.FromBytes(content)
   651  				So(digest, ShouldNotBeNil)
   652  				resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   653  					SetBody(content).Put(baseURL + fmt.Sprintf("/v2/page0/manifests/test:%d.0", i))
   654  				So(err, ShouldBeNil)
   655  				So(resp.StatusCode(), ShouldEqual, 201)
   656  				d := resp.Header().Get(api.DistContentDigestKey)
   657  				So(d, ShouldNotBeEmpty)
   658  				So(d, ShouldEqual, digest.String())
   659  			}
   660  
   661  			resp, err := resty.R().Get(baseURL + "/v2/page0/tags/list")
   662  			So(err, ShouldBeNil)
   663  			So(resp.StatusCode(), ShouldEqual, 200)
   664  
   665  			resp, err = resty.R().Get(baseURL + "/v2/page0/tags/list?n=0")
   666  			So(err, ShouldBeNil)
   667  			So(resp.StatusCode(), ShouldEqual, 200)
   668  
   669  			resp, err = resty.R().Get(baseURL + "/v2/page0/tags/list?n=3")
   670  			So(err, ShouldBeNil)
   671  			So(resp.StatusCode(), ShouldEqual, 200)
   672  			next := resp.Header().Get("Link")
   673  			So(next, ShouldNotBeEmpty)
   674  
   675  			u := baseURL + strings.Split(next, ";")[0]
   676  			resp, err = resty.R().Get(u)
   677  			So(err, ShouldBeNil)
   678  			So(resp.StatusCode(), ShouldEqual, 200)
   679  			next = resp.Header().Get("Link")
   680  			So(next, ShouldBeEmpty)
   681  		})
   682  
   683  		// this is an additional test for repository names (alphanumeric)
   684  		Convey("Repository names", func() {
   685  			Print("\nRepository names")
   686  			// create a blob/layer
   687  			resp, err := resty.R().Post(baseURL + "/v2/repotest/blobs/uploads/")
   688  			So(err, ShouldBeNil)
   689  			So(resp.StatusCode(), ShouldEqual, 202)
   690  			resp, err = resty.R().Post(baseURL + "/v2/repotest123/blobs/uploads/")
   691  			So(err, ShouldBeNil)
   692  			So(resp.StatusCode(), ShouldEqual, 202)
   693  		})
   694  
   695  		Convey("Multiple Storage", func() {
   696  			// test APIS on subpath routes, default storage already tested above
   697  			// subpath route firsttest
   698  			resp, err := resty.R().Post(baseURL + "/v2/firsttest/first/blobs/uploads/")
   699  			So(err, ShouldBeNil)
   700  			So(resp.StatusCode(), ShouldEqual, 202)
   701  			firstloc := Location(baseURL, resp)
   702  			So(firstloc, ShouldNotBeEmpty)
   703  
   704  			resp, err = resty.R().Get(firstloc)
   705  			So(err, ShouldBeNil)
   706  			So(resp.StatusCode(), ShouldEqual, 204)
   707  
   708  			// if firsttest route is used as prefix in url that means repo should be stored in subpaths["firsttest"] rootdir
   709  			_, err = os.Stat(path.Join(storageInfo[1], "firsttest/first"))
   710  			So(err, ShouldBeNil)
   711  
   712  			// subpath route secondtest
   713  			resp, err = resty.R().Post(baseURL + "/v2/secondtest/second/blobs/uploads/")
   714  			So(err, ShouldBeNil)
   715  			So(resp.StatusCode(), ShouldEqual, 202)
   716  			secondloc := Location(baseURL, resp)
   717  			So(secondloc, ShouldNotBeEmpty)
   718  
   719  			resp, err = resty.R().Get(secondloc)
   720  			So(err, ShouldBeNil)
   721  			So(resp.StatusCode(), ShouldEqual, 204)
   722  
   723  			// if secondtest route is used as prefix in url that means repo should be stored in subpaths["secondtest"] rootdir
   724  			_, err = os.Stat(path.Join(storageInfo[2], "secondtest/second"))
   725  			So(err, ShouldBeNil)
   726  
   727  			content := []byte("this is a blob5")
   728  			digest := godigest.FromBytes(content)
   729  			So(digest, ShouldNotBeNil)
   730  			// monolithic blob upload: success
   731  			// first test
   732  			resp, err = resty.R().SetQueryParam("digest", digest.String()).
   733  				SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(firstloc)
   734  			So(err, ShouldBeNil)
   735  			So(resp.StatusCode(), ShouldEqual, 201)
   736  			firstblobLoc := resp.Header().Get("Location")
   737  			So(firstblobLoc, ShouldNotBeEmpty)
   738  			So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
   739  			So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty)
   740  
   741  			// second test
   742  			resp, err = resty.R().SetQueryParam("digest", digest.String()).
   743  				SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(secondloc)
   744  			So(err, ShouldBeNil)
   745  			So(resp.StatusCode(), ShouldEqual, 201)
   746  			secondblobLoc := resp.Header().Get("Location")
   747  			So(secondblobLoc, ShouldNotBeEmpty)
   748  			So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
   749  			So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty)
   750  
   751  			// check a non-existent manifest
   752  			resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   753  				SetBody(content).Head(baseURL + "/v2/unknown/manifests/test:1.0")
   754  			So(err, ShouldBeNil)
   755  			So(resp.StatusCode(), ShouldEqual, 404)
   756  
   757  			resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   758  				SetBody(content).Head(baseURL + "/v2/firsttest/unknown/manifests/test:1.0")
   759  			So(err, ShouldBeNil)
   760  			So(resp.StatusCode(), ShouldEqual, 404)
   761  
   762  			resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   763  				SetBody(content).Head(baseURL + "/v2/secondtest/unknown/manifests/test:1.0")
   764  			So(err, ShouldBeNil)
   765  			So(resp.StatusCode(), ShouldEqual, 404)
   766  
   767  			// create a manifest
   768  			m := ispec.Manifest{
   769  				Config: ispec.Descriptor{
   770  					Digest: digest,
   771  					Size:   int64(len(content)),
   772  				},
   773  				Layers: []ispec.Descriptor{
   774  					{
   775  						MediaType: "application/vnd.oci.image.layer.v1.tar",
   776  						Digest:    digest,
   777  						Size:      int64(len(content)),
   778  					},
   779  				},
   780  			}
   781  			m.SchemaVersion = 2
   782  			content, err = json.Marshal(m)
   783  			So(err, ShouldBeNil)
   784  			digest = godigest.FromBytes(content)
   785  			So(digest, ShouldNotBeNil)
   786  			// subpath firsttest
   787  			resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   788  				SetBody(content).Put(baseURL + "/v2/firsttest/first/manifests/test:1.0")
   789  			So(err, ShouldBeNil)
   790  			So(resp.StatusCode(), ShouldEqual, 201)
   791  			d := resp.Header().Get(api.DistContentDigestKey)
   792  			So(d, ShouldNotBeEmpty)
   793  			So(d, ShouldEqual, digest.String())
   794  
   795  			// subpath secondtest
   796  			resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   797  				SetBody(content).Put(baseURL + "/v2/secondtest/second/manifests/test:1.0")
   798  			So(err, ShouldBeNil)
   799  			So(resp.StatusCode(), ShouldEqual, 201)
   800  			d = resp.Header().Get(api.DistContentDigestKey)
   801  			So(d, ShouldNotBeEmpty)
   802  			So(d, ShouldEqual, digest.String())
   803  
   804  			content = []byte("this is a blob5")
   805  			digest = godigest.FromBytes(content)
   806  			So(digest, ShouldNotBeNil)
   807  			// create a manifest with same blob but a different tag
   808  			m = ispec.Manifest{
   809  				Config: ispec.Descriptor{
   810  					Digest: digest,
   811  					Size:   int64(len(content)),
   812  				},
   813  				Layers: []ispec.Descriptor{
   814  					{
   815  						MediaType: "application/vnd.oci.image.layer.v1.tar",
   816  						Digest:    digest,
   817  						Size:      int64(len(content)),
   818  					},
   819  				},
   820  			}
   821  			m.SchemaVersion = 2
   822  			content, err = json.Marshal(m)
   823  			So(err, ShouldBeNil)
   824  			digest = godigest.FromBytes(content)
   825  			So(digest, ShouldNotBeNil)
   826  
   827  			// subpath firsttest
   828  			resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   829  				SetBody(content).Put(baseURL + "/v2/firsttest/first/manifests/test:2.0")
   830  			So(err, ShouldBeNil)
   831  			So(resp.StatusCode(), ShouldEqual, 201)
   832  			d = resp.Header().Get(api.DistContentDigestKey)
   833  			So(d, ShouldNotBeEmpty)
   834  			So(d, ShouldEqual, digest.String())
   835  
   836  			// subpath secondtest
   837  			resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   838  				SetBody(content).Put(baseURL + "/v2/secondtest/second/manifests/test:2.0")
   839  			So(err, ShouldBeNil)
   840  			So(resp.StatusCode(), ShouldEqual, 201)
   841  			d = resp.Header().Get(api.DistContentDigestKey)
   842  			So(d, ShouldNotBeEmpty)
   843  			So(d, ShouldEqual, digest.String())
   844  
   845  			// check/get by tag
   846  			resp, err = resty.R().Head(baseURL + "/v2/firsttest/first/manifests/test:1.0")
   847  			So(err, ShouldBeNil)
   848  			So(resp.StatusCode(), ShouldEqual, 200)
   849  			resp, err = resty.R().Get(baseURL + "/v2/firsttest/first/manifests/test:1.0")
   850  			So(err, ShouldBeNil)
   851  			So(resp.StatusCode(), ShouldEqual, 200)
   852  			So(resp.Body(), ShouldNotBeEmpty)
   853  			resp, err = resty.R().Head(baseURL + "/v2/secondtest/second/manifests/test:1.0")
   854  			So(err, ShouldBeNil)
   855  			So(resp.StatusCode(), ShouldEqual, 200)
   856  			resp, err = resty.R().Get(baseURL + "/v2/secondtest/second/manifests/test:1.0")
   857  			So(err, ShouldBeNil)
   858  			So(resp.StatusCode(), ShouldEqual, 200)
   859  			So(resp.Body(), ShouldNotBeEmpty)
   860  
   861  			// check/get by reference
   862  			resp, err = resty.R().Head(baseURL + "/v2/firsttest/first/manifests/" + digest.String())
   863  			So(err, ShouldBeNil)
   864  			So(resp.StatusCode(), ShouldEqual, 200)
   865  			resp, err = resty.R().Get(baseURL + "/v2/firsttest/first/manifests/" + digest.String())
   866  			So(err, ShouldBeNil)
   867  			So(resp.StatusCode(), ShouldEqual, 200)
   868  			So(resp.Body(), ShouldNotBeEmpty)
   869  
   870  			resp, err = resty.R().Head(baseURL + "/v2/secondtest/second/manifests/" + digest.String())
   871  			So(err, ShouldBeNil)
   872  			So(resp.StatusCode(), ShouldEqual, 200)
   873  			resp, err = resty.R().Get(baseURL + "/v2/secondtest/second/manifests/" + digest.String())
   874  			So(err, ShouldBeNil)
   875  			So(resp.StatusCode(), ShouldEqual, 200)
   876  			So(resp.Body(), ShouldNotBeEmpty)
   877  
   878  			// delete manifest by digest
   879  			resp, err = resty.R().Delete(baseURL + "/v2/firsttest/first/manifests/" + digest.String())
   880  			So(err, ShouldBeNil)
   881  			So(resp.StatusCode(), ShouldEqual, 202)
   882  
   883  			resp, err = resty.R().Delete(baseURL + "/v2/secondtest/second/manifests/" + digest.String())
   884  			So(err, ShouldBeNil)
   885  			So(resp.StatusCode(), ShouldEqual, 202)
   886  
   887  			// delete manifest by digest
   888  			resp, err = resty.R().Delete(baseURL + "/v2/firsttest/first/manifests/" + digest.String())
   889  			So(err, ShouldBeNil)
   890  			So(resp.StatusCode(), ShouldEqual, 404)
   891  
   892  			resp, err = resty.R().Delete(baseURL + "/v2/secondtest/second/manifests/" + digest.String())
   893  			So(err, ShouldBeNil)
   894  			So(resp.StatusCode(), ShouldEqual, 404)
   895  
   896  			// delete again should fail
   897  			resp, err = resty.R().Delete(baseURL + "/v2/firsttest/first/manifests/" + digest.String())
   898  			So(err, ShouldBeNil)
   899  			So(resp.StatusCode(), ShouldEqual, 404)
   900  
   901  			resp, err = resty.R().Delete(baseURL + "/v2/secondtest/second/manifests/" + digest.String())
   902  			So(err, ShouldBeNil)
   903  			So(resp.StatusCode(), ShouldEqual, 404)
   904  
   905  			// check/get by tag
   906  			resp, err = resty.R().Head(baseURL + "/v2/firsttest/first/manifests/test:1.0")
   907  			So(err, ShouldBeNil)
   908  			So(resp.StatusCode(), ShouldEqual, 404)
   909  			resp, err = resty.R().Get(baseURL + "/v2/firsttest/first/manifests/test:1.0")
   910  			So(err, ShouldBeNil)
   911  			So(resp.StatusCode(), ShouldEqual, 404)
   912  			So(resp.Body(), ShouldNotBeEmpty)
   913  
   914  			resp, err = resty.R().Head(baseURL + "/v2/secondtest/second/manifests/test:1.0")
   915  			So(err, ShouldBeNil)
   916  			So(resp.StatusCode(), ShouldEqual, 404)
   917  			resp, err = resty.R().Get(baseURL + "/v2/secondtest/second/manifests/test:1.0")
   918  			So(err, ShouldBeNil)
   919  			So(resp.StatusCode(), ShouldEqual, 404)
   920  			So(resp.Body(), ShouldNotBeEmpty)
   921  
   922  			resp, err = resty.R().Head(baseURL + "/v2/firsttest/first/repo7/manifests/test:2.0")
   923  			So(err, ShouldBeNil)
   924  			So(resp.StatusCode(), ShouldEqual, 404)
   925  			resp, err = resty.R().Get(baseURL + "/v2/firsttest/first/manifests/test:2.0")
   926  			So(err, ShouldBeNil)
   927  			So(resp.StatusCode(), ShouldEqual, 404)
   928  			So(resp.Body(), ShouldNotBeEmpty)
   929  
   930  			resp, err = resty.R().Head(baseURL + "/v2/secondtest/second/manifests/test:2.0")
   931  			So(err, ShouldBeNil)
   932  			So(resp.StatusCode(), ShouldEqual, 404)
   933  			resp, err = resty.R().Get(baseURL + "/v2/secondtest/second/manifests/test:2.0")
   934  			So(err, ShouldBeNil)
   935  			So(resp.StatusCode(), ShouldEqual, 404)
   936  			So(resp.Body(), ShouldNotBeEmpty)
   937  
   938  			// check/get by reference
   939  			resp, err = resty.R().Head(baseURL + "/v2/firsttest/first/manifests/" + digest.String())
   940  			So(err, ShouldBeNil)
   941  			So(resp.StatusCode(), ShouldEqual, 404)
   942  			resp, err = resty.R().Get(baseURL + "/v2/firsttest/first/manifests/" + digest.String())
   943  			So(err, ShouldBeNil)
   944  			So(resp.StatusCode(), ShouldEqual, 404)
   945  			So(resp.Body(), ShouldNotBeEmpty)
   946  
   947  			resp, err = resty.R().Head(baseURL + "/v2/secondtest/second/manifests/" + digest.String())
   948  			So(err, ShouldBeNil)
   949  			So(resp.StatusCode(), ShouldEqual, 404)
   950  			resp, err = resty.R().Get(baseURL + "/v2/secondtest/second/manifests/" + digest.String())
   951  			So(err, ShouldBeNil)
   952  			So(resp.StatusCode(), ShouldEqual, 404)
   953  			So(resp.Body(), ShouldNotBeEmpty)
   954  		})
   955  	})
   956  }
   957  
   958  // nolint: gochecknoglobals
   959  var (
   960  	old  *os.File
   961  	r    *os.File
   962  	w    *os.File
   963  	outC chan string
   964  )
   965  
   966  func outputJSONEnter() {
   967  	// this env var instructs goconvey to output results to JSON (stdout)
   968  	os.Setenv("GOCONVEY_REPORTER", "json")
   969  
   970  	// stdout capture copied from: https://stackoverflow.com/a/29339052
   971  	old = os.Stdout
   972  	// keep backup of the real stdout
   973  	r, w, _ = os.Pipe()
   974  	outC = make(chan string)
   975  	os.Stdout = w
   976  
   977  	// copy the output in a separate goroutine so printing can't block indefinitely
   978  	go func() {
   979  		var buf bytes.Buffer
   980  
   981  		_, err := io.Copy(&buf, r)
   982  		if err != nil {
   983  			panic(err)
   984  		}
   985  
   986  		outC <- buf.String()
   987  	}()
   988  }
   989  
   990  func outputJSONExit() {
   991  	// back to normal state
   992  	w.Close()
   993  
   994  	os.Stdout = old // restoring the real stdout
   995  
   996  	out := <-outC
   997  
   998  	// The output of JSON is combined with regular output, so we look for the
   999  	// first occurrence of the "{" character and take everything after that
  1000  	rawJSON := "[{" + strings.Join(strings.Split(out, "{")[1:], "{")
  1001  	rawJSON = strings.Replace(rawJSON, reporting.OpenJson, "", 1)
  1002  	rawJSON = strings.Replace(rawJSON, reporting.CloseJson, "", 1)
  1003  	tmp := strings.Split(rawJSON, ",")
  1004  	rawJSON = strings.Join(tmp[0:len(tmp)-1], ",") + "]"
  1005  
  1006  	rawJSONMinified := validateMinifyRawJSON(rawJSON)
  1007  	fmt.Println(rawJSONMinified)
  1008  }
  1009  
  1010  func validateMinifyRawJSON(rawJSON string) string {
  1011  	var j interface{}
  1012  
  1013  	err := json.Unmarshal([]byte(rawJSON), &j)
  1014  	if err != nil {
  1015  		panic(err)
  1016  	}
  1017  
  1018  	rawJSONBytesMinified, err := json.Marshal(j)
  1019  	if err != nil {
  1020  		panic(err)
  1021  	}
  1022  
  1023  	return string(rawJSONBytesMinified)
  1024  }