zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/compliance/v1_0_0/check.go (about)

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