zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/cmd/zb/helper.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"math/rand"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"path"
    14  	"sync"
    15  	"time"
    16  
    17  	"github.com/google/uuid"
    18  	godigest "github.com/opencontainers/go-digest"
    19  	imeta "github.com/opencontainers/image-spec/specs-go"
    20  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    21  	"gopkg.in/resty.v1"
    22  
    23  	zerr "zotregistry.dev/zot/errors"
    24  	"zotregistry.dev/zot/pkg/common"
    25  )
    26  
    27  func makeHTTPGetRequest(url string, resultPtr interface{}, client *resty.Client) error {
    28  	resp, err := client.R().Get(url)
    29  	if err != nil {
    30  		return err
    31  	}
    32  
    33  	if resp.StatusCode() != http.StatusOK {
    34  		log.Printf("unable to make GET request on %s, response status code: %d", url, resp.StatusCode())
    35  
    36  		return fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusOK,
    37  			resp.StatusCode(), string(resp.Body()))
    38  	}
    39  
    40  	err = json.Unmarshal(resp.Body(), resultPtr)
    41  	if err != nil {
    42  		return err
    43  	}
    44  
    45  	return nil
    46  }
    47  
    48  func makeHTTPDeleteRequest(url string, client *resty.Client) error {
    49  	resp, err := client.R().Delete(url)
    50  	if err != nil {
    51  		return err
    52  	}
    53  
    54  	if resp.StatusCode() != http.StatusAccepted {
    55  		log.Printf("unable to make DELETE request on %s, response status code: %d", url, resp.StatusCode())
    56  
    57  		return fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusAccepted,
    58  			resp.StatusCode(), string(resp.Body()))
    59  	}
    60  
    61  	return nil
    62  }
    63  
    64  func deleteTestRepo(repos []string, url string, client *resty.Client) error {
    65  	for _, repo := range repos {
    66  		var tags common.ImageTags
    67  
    68  		// get tags
    69  		err := makeHTTPGetRequest(fmt.Sprintf("%s/v2/%s/tags/list", url, repo), &tags, client)
    70  		if err != nil {
    71  			return err
    72  		}
    73  
    74  		for _, tag := range tags.Tags {
    75  			var manifest ispec.Manifest
    76  
    77  			// first get tag manifest to get containing blobs
    78  			err := makeHTTPGetRequest(fmt.Sprintf("%s/v2/%s/manifests/%s", url, repo, tag), &manifest, client)
    79  			if err != nil {
    80  				return err
    81  			}
    82  
    83  			// delete manifest so that we don't trigger BlobInUse error
    84  			err = makeHTTPDeleteRequest(fmt.Sprintf("%s/v2/%s/manifests/%s", url, repo, tag), client)
    85  			if err != nil {
    86  				return err
    87  			}
    88  
    89  			// delete blobs
    90  			for _, blob := range manifest.Layers {
    91  				err := makeHTTPDeleteRequest(fmt.Sprintf("%s/v2/%s/blobs/%s", url, repo, blob.Digest.String()), client)
    92  				if err != nil {
    93  					return err
    94  				}
    95  			}
    96  
    97  			// delete config blob
    98  			err = makeHTTPDeleteRequest(fmt.Sprintf("%s/v2/%s/blobs/%s", url, repo, manifest.Config.Digest.String()), client)
    99  			if err != nil {
   100  				return err
   101  			}
   102  		}
   103  	}
   104  
   105  	return nil
   106  }
   107  
   108  func pullAndCollect(url string, repos []string, manifestItem manifestStruct,
   109  	config testConfig, client *resty.Client, statsCh chan statsRecord,
   110  ) []string {
   111  	manifestHash := manifestItem.manifestHash
   112  	manifestBySizeHash := manifestItem.manifestBySizeHash
   113  
   114  	func() {
   115  		start := time.Now()
   116  
   117  		var isConnFail, isErr bool
   118  
   119  		var statusCode int
   120  
   121  		var latency time.Duration
   122  
   123  		defer func() {
   124  			// send a stats record
   125  			statsCh <- statsRecord{
   126  				latency:    latency,
   127  				statusCode: statusCode,
   128  				isConnFail: isConnFail,
   129  				isErr:      isErr,
   130  			}
   131  		}()
   132  
   133  		if config.mixedSize {
   134  			_, idx := getRandomSize(config.probabilityRange)
   135  
   136  			manifestHash = manifestBySizeHash[idx]
   137  		}
   138  
   139  		for repo, manifestTag := range manifestHash {
   140  			manifestLoc := fmt.Sprintf("%s/v2/%s/manifests/%s", url, repo, manifestTag)
   141  
   142  			// check manifest
   143  			resp, err := client.R().
   144  				SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   145  				Head(manifestLoc)
   146  
   147  			latency = time.Since(start)
   148  
   149  			if err != nil {
   150  				isConnFail = true
   151  
   152  				return
   153  			}
   154  
   155  			// request specific check
   156  			statusCode = resp.StatusCode()
   157  			if statusCode != http.StatusOK {
   158  				isErr = true
   159  
   160  				return
   161  			}
   162  
   163  			// send request and get the manifest
   164  			resp, err = client.R().
   165  				SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   166  				Get(manifestLoc)
   167  
   168  			latency = time.Since(start)
   169  
   170  			if err != nil {
   171  				isConnFail = true
   172  
   173  				return
   174  			}
   175  
   176  			// request specific check
   177  			statusCode = resp.StatusCode()
   178  			if statusCode != http.StatusOK {
   179  				isErr = true
   180  
   181  				return
   182  			}
   183  
   184  			manifestBody := resp.Body()
   185  
   186  			// file copy simulation
   187  			_, err = io.Copy(io.Discard, bytes.NewReader(manifestBody))
   188  
   189  			latency = time.Since(start)
   190  
   191  			if err != nil {
   192  				log.Fatal(err)
   193  			}
   194  
   195  			var pulledManifest ispec.Manifest
   196  
   197  			err = json.Unmarshal(manifestBody, &pulledManifest)
   198  			if err != nil {
   199  				log.Fatal(err)
   200  			}
   201  
   202  			// check config
   203  			configDigest := pulledManifest.Config.Digest
   204  			configLoc := fmt.Sprintf("%s/v2/%s/blobs/%s", url, repo, configDigest)
   205  			resp, err = client.R().Head(configLoc)
   206  
   207  			latency = time.Since(start)
   208  
   209  			if err != nil {
   210  				isConnFail = true
   211  
   212  				return
   213  			}
   214  
   215  			// request specific check
   216  			statusCode = resp.StatusCode()
   217  			if statusCode != http.StatusOK {
   218  				isErr = true
   219  
   220  				return
   221  			}
   222  
   223  			// send request and get the config
   224  			resp, err = client.R().Get(configLoc)
   225  
   226  			latency = time.Since(start)
   227  
   228  			if err != nil {
   229  				isConnFail = true
   230  
   231  				return
   232  			}
   233  
   234  			// request specific check
   235  			statusCode = resp.StatusCode()
   236  			if statusCode != http.StatusOK {
   237  				isErr = true
   238  
   239  				return
   240  			}
   241  
   242  			configBody := resp.Body()
   243  
   244  			// file copy simulation
   245  			_, err = io.Copy(io.Discard, bytes.NewReader(configBody))
   246  
   247  			latency = time.Since(start)
   248  
   249  			if err != nil {
   250  				log.Fatal(err)
   251  			}
   252  
   253  			// download blobs
   254  			for _, layer := range pulledManifest.Layers {
   255  				blobDigest := layer.Digest
   256  				blobLoc := fmt.Sprintf("%s/v2/%s/blobs/%s", url, repo, blobDigest)
   257  
   258  				// check blob
   259  				resp, err := client.R().Head(blobLoc)
   260  
   261  				latency = time.Since(start)
   262  
   263  				if err != nil {
   264  					isConnFail = true
   265  
   266  					return
   267  				}
   268  
   269  				// request specific check
   270  				statusCode = resp.StatusCode()
   271  				if statusCode != http.StatusOK {
   272  					isErr = true
   273  
   274  					return
   275  				}
   276  
   277  				// send request and get response the blob
   278  				resp, err = client.R().Get(blobLoc)
   279  
   280  				latency = time.Since(start)
   281  
   282  				if err != nil {
   283  					isConnFail = true
   284  
   285  					return
   286  				}
   287  
   288  				// request specific check
   289  				statusCode = resp.StatusCode()
   290  				if statusCode != http.StatusOK {
   291  					isErr = true
   292  
   293  					return
   294  				}
   295  
   296  				blobBody := resp.Body()
   297  
   298  				// file copy simulation
   299  				_, err = io.Copy(io.Discard, bytes.NewReader(blobBody))
   300  				if err != nil {
   301  					log.Fatal(err)
   302  				}
   303  			}
   304  		}
   305  	}()
   306  
   307  	return repos
   308  }
   309  
   310  func pushMonolithImage(workdir, url, trepo string, repos []string, config testConfig,
   311  	client *resty.Client,
   312  ) (map[string]string, []string, error) {
   313  	var statusCode int
   314  
   315  	// key: repository name. value: manifest name
   316  	manifestHash := make(map[string]string)
   317  
   318  	ruid, err := uuid.NewUUID()
   319  	if err != nil {
   320  		return nil, repos, err
   321  	}
   322  
   323  	var repo string
   324  
   325  	if trepo != "" {
   326  		repo = trepo + "/" + ruid.String()
   327  	} else {
   328  		repo = ruid.String()
   329  	}
   330  
   331  	repos = append(repos, repo)
   332  
   333  	// upload blob
   334  	resp, err := client.R().Post(fmt.Sprintf("%s/v2/%s/blobs/uploads/", url, repo))
   335  	if err != nil {
   336  		return nil, repos, err
   337  	}
   338  
   339  	// request specific check
   340  	statusCode = resp.StatusCode()
   341  	if statusCode != http.StatusAccepted {
   342  		return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusAccepted,
   343  			resp.StatusCode(), string(resp.Body())) //nolint: goerr113
   344  	}
   345  
   346  	loc := getLocation(url, resp)
   347  
   348  	var size int
   349  
   350  	if config.size == 0 {
   351  		size, _ = getRandomSize(config.probabilityRange)
   352  	} else {
   353  		size = config.size
   354  	}
   355  
   356  	blob := path.Join(workdir, fmt.Sprintf("%d.blob", size))
   357  
   358  	fhandle, err := os.OpenFile(blob, os.O_RDONLY, defaultFilePerms)
   359  	if err != nil {
   360  		return nil, repos, err
   361  	}
   362  
   363  	defer fhandle.Close()
   364  
   365  	// stream the entire blob
   366  	digest := blobHash[blob]
   367  
   368  	resp, err = client.R().
   369  		SetContentLength(true).
   370  		SetQueryParam("digest", digest.String()).
   371  		SetHeader("Content-Length", fmt.Sprintf("%d", size)).
   372  		SetHeader("Content-Type", "application/octet-stream").SetBody(fhandle).Put(loc)
   373  
   374  	if err != nil {
   375  		return nil, repos, err
   376  	}
   377  
   378  	// request specific check
   379  	statusCode = resp.StatusCode()
   380  	if statusCode != http.StatusCreated {
   381  		return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusCreated,
   382  			resp.StatusCode(), string(resp.Body()))
   383  	}
   384  
   385  	// upload image config blob
   386  	resp, err = client.R().
   387  		Post(fmt.Sprintf("%s/v2/%s/blobs/uploads/", url, repo))
   388  
   389  	if err != nil {
   390  		return nil, repos, err
   391  	}
   392  
   393  	// request specific check
   394  	statusCode = resp.StatusCode()
   395  	if statusCode != http.StatusAccepted {
   396  		return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusAccepted,
   397  			resp.StatusCode(), string(resp.Body()))
   398  	}
   399  
   400  	loc = getLocation(url, resp)
   401  	cblob, cdigest := getImageConfig()
   402  	resp, err = client.R().
   403  		SetContentLength(true).
   404  		SetHeader("Content-Length", fmt.Sprintf("%d", len(cblob))).
   405  		SetHeader("Content-Type", "application/octet-stream").
   406  		SetQueryParam("digest", cdigest.String()).
   407  		SetBody(cblob).
   408  		Put(loc)
   409  
   410  	if err != nil {
   411  		return nil, repos, err
   412  	}
   413  
   414  	// request specific check
   415  	statusCode = resp.StatusCode()
   416  	if statusCode != http.StatusCreated {
   417  		return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusCreated,
   418  			resp.StatusCode(), string(resp.Body()))
   419  	}
   420  
   421  	// create a manifest
   422  	manifest := ispec.Manifest{
   423  		Versioned: imeta.Versioned{
   424  			SchemaVersion: defaultSchemaVersion,
   425  		},
   426  		Config: ispec.Descriptor{
   427  			MediaType: "application/vnd.oci.image.config.v1+json",
   428  			Digest:    cdigest,
   429  			Size:      int64(len(cblob)),
   430  		},
   431  		Layers: []ispec.Descriptor{
   432  			{
   433  				MediaType: "application/vnd.oci.image.layer.v1.tar",
   434  				Digest:    digest,
   435  				Size:      int64(size),
   436  			},
   437  		},
   438  	}
   439  
   440  	content, err := json.MarshalIndent(&manifest, "", "\t")
   441  	if err != nil {
   442  		return nil, repos, err
   443  	}
   444  
   445  	manifestTag := fmt.Sprintf("tag%d", size)
   446  
   447  	// finish upload
   448  	resp, err = client.R().
   449  		SetContentLength(true).
   450  		SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   451  		SetBody(content).
   452  		Put(fmt.Sprintf("%s/v2/%s/manifests/%s", url, repo, manifestTag))
   453  
   454  	if err != nil {
   455  		return nil, repos, err
   456  	}
   457  
   458  	// request specific check
   459  	statusCode = resp.StatusCode()
   460  	if statusCode != http.StatusCreated {
   461  		return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusCreated,
   462  			resp.StatusCode(), string(resp.Body()))
   463  	}
   464  
   465  	manifestHash[repo] = manifestTag
   466  
   467  	return manifestHash, repos, nil
   468  }
   469  
   470  func pushMonolithAndCollect(workdir, url, trepo string, count int,
   471  	repos []string, config testConfig, client *resty.Client,
   472  	statsCh chan statsRecord,
   473  ) []string {
   474  	func() {
   475  		start := time.Now()
   476  
   477  		var isConnFail, isErr bool
   478  
   479  		var statusCode int
   480  
   481  		var latency time.Duration
   482  
   483  		defer func() {
   484  			// send a stats record
   485  			statsCh <- statsRecord{
   486  				latency:    latency,
   487  				statusCode: statusCode,
   488  				isConnFail: isConnFail,
   489  				isErr:      isErr,
   490  			}
   491  		}()
   492  
   493  		ruid, err := uuid.NewUUID()
   494  		if err != nil {
   495  			log.Fatal(err)
   496  		}
   497  
   498  		var repo string
   499  
   500  		if trepo != "" {
   501  			repo = trepo + "/" + ruid.String()
   502  		} else {
   503  			repo = ruid.String()
   504  		}
   505  
   506  		repos = append(repos, repo)
   507  
   508  		// create a new upload
   509  		resp, err := client.R().
   510  			Post(fmt.Sprintf("%s/v2/%s/blobs/uploads/", url, repo))
   511  
   512  		latency = time.Since(start)
   513  
   514  		if err != nil {
   515  			isConnFail = true
   516  
   517  			return
   518  		}
   519  
   520  		// request specific check
   521  		statusCode = resp.StatusCode()
   522  		if statusCode != http.StatusAccepted {
   523  			isErr = true
   524  
   525  			return
   526  		}
   527  
   528  		loc := getLocation(url, resp)
   529  
   530  		var size int
   531  
   532  		if config.mixedSize {
   533  			size, _ = getRandomSize(config.probabilityRange)
   534  		} else {
   535  			size = config.size
   536  		}
   537  
   538  		blob := path.Join(workdir, fmt.Sprintf("%d.blob", size))
   539  
   540  		fhandle, err := os.OpenFile(blob, os.O_RDONLY, defaultFilePerms)
   541  		if err != nil {
   542  			isConnFail = true
   543  
   544  			return
   545  		}
   546  
   547  		defer fhandle.Close()
   548  
   549  		// stream the entire blob
   550  		digest := blobHash[blob]
   551  
   552  		resp, err = client.R().
   553  			SetContentLength(true).
   554  			SetHeader("Content-Length", fmt.Sprintf("%d", size)).
   555  			SetHeader("Content-Type", "application/octet-stream").
   556  			SetQueryParam("digest", digest.String()).
   557  			SetBody(fhandle).
   558  			Put(loc)
   559  
   560  		latency = time.Since(start)
   561  
   562  		if err != nil {
   563  			isConnFail = true
   564  
   565  			return
   566  		}
   567  
   568  		// request specific check
   569  		statusCode = resp.StatusCode()
   570  		if statusCode != http.StatusCreated {
   571  			isErr = true
   572  
   573  			return
   574  		}
   575  
   576  		// upload image config blob
   577  		resp, err = client.R().
   578  			Post(fmt.Sprintf("%s/v2/%s/blobs/uploads/", url, repo))
   579  
   580  		latency = time.Since(start)
   581  
   582  		if err != nil {
   583  			isConnFail = true
   584  
   585  			return
   586  		}
   587  
   588  		// request specific check
   589  		statusCode = resp.StatusCode()
   590  		if statusCode != http.StatusAccepted {
   591  			isErr = true
   592  
   593  			return
   594  		}
   595  
   596  		loc = getLocation(url, resp)
   597  		cblob, cdigest := getImageConfig()
   598  		resp, err = client.R().
   599  			SetContentLength(true).
   600  			SetHeader("Content-Length", fmt.Sprintf("%d", len(cblob))).
   601  			SetHeader("Content-Type", "application/octet-stream").
   602  			SetQueryParam("digest", cdigest.String()).
   603  			SetBody(cblob).
   604  			Put(loc)
   605  
   606  		latency = time.Since(start)
   607  
   608  		if err != nil {
   609  			isConnFail = true
   610  
   611  			return
   612  		}
   613  
   614  		// request specific check
   615  		statusCode = resp.StatusCode()
   616  		if statusCode != http.StatusCreated {
   617  			isErr = true
   618  
   619  			return
   620  		}
   621  
   622  		// create a manifest
   623  		manifest := ispec.Manifest{
   624  			Versioned: imeta.Versioned{
   625  				SchemaVersion: defaultSchemaVersion,
   626  			},
   627  			Config: ispec.Descriptor{
   628  				MediaType: "application/vnd.oci.image.config.v1+json",
   629  				Digest:    cdigest,
   630  				Size:      int64(len(cblob)),
   631  			},
   632  			Layers: []ispec.Descriptor{
   633  				{
   634  					MediaType: "application/vnd.oci.image.layer.v1.tar",
   635  					Digest:    digest,
   636  					Size:      int64(size),
   637  				},
   638  			},
   639  		}
   640  
   641  		content, err := json.MarshalIndent(&manifest, "", "\t")
   642  		if err != nil {
   643  			log.Fatal(err)
   644  		}
   645  
   646  		manifestTag := fmt.Sprintf("tag%d", count)
   647  
   648  		resp, err = client.R().
   649  			SetContentLength(true).
   650  			SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   651  			SetBody(content).
   652  			Put(fmt.Sprintf("%s/v2/%s/manifests/%s", url, repo, manifestTag))
   653  
   654  		latency = time.Since(start)
   655  
   656  		if err != nil {
   657  			isConnFail = true
   658  
   659  			return
   660  		}
   661  
   662  		// request specific check
   663  		statusCode = resp.StatusCode()
   664  		if statusCode != http.StatusCreated {
   665  			isErr = true
   666  
   667  			return
   668  		}
   669  	}()
   670  
   671  	return repos
   672  }
   673  
   674  func pushChunkAndCollect(workdir, url, trepo string, count int,
   675  	repos []string, config testConfig, client *resty.Client,
   676  	statsCh chan statsRecord,
   677  ) []string {
   678  	func() {
   679  		start := time.Now()
   680  
   681  		var isConnFail, isErr bool
   682  
   683  		var statusCode int
   684  
   685  		var latency time.Duration
   686  
   687  		defer func() {
   688  			// send a stats record
   689  			statsCh <- statsRecord{
   690  				latency:    latency,
   691  				statusCode: statusCode,
   692  				isConnFail: isConnFail,
   693  				isErr:      isErr,
   694  			}
   695  		}()
   696  
   697  		ruid, err := uuid.NewUUID()
   698  		if err != nil {
   699  			log.Fatal(err)
   700  		}
   701  
   702  		var repo string
   703  
   704  		if trepo != "" {
   705  			repo = trepo + "/" + ruid.String()
   706  		} else {
   707  			repo = ruid.String()
   708  		}
   709  
   710  		repos = append(repos, repo)
   711  
   712  		// create a new upload
   713  		resp, err := client.R().
   714  			Post(fmt.Sprintf("%s/v2/%s/blobs/uploads/", url, repo))
   715  
   716  		latency = time.Since(start)
   717  
   718  		if err != nil {
   719  			isConnFail = true
   720  
   721  			return
   722  		}
   723  
   724  		// request specific check
   725  		statusCode = resp.StatusCode()
   726  		if statusCode != http.StatusAccepted {
   727  			isErr = true
   728  
   729  			return
   730  		}
   731  
   732  		loc := getLocation(url, resp)
   733  
   734  		var size int
   735  
   736  		if config.mixedSize {
   737  			size, _ = getRandomSize(config.probabilityRange)
   738  		} else {
   739  			size = config.size
   740  		}
   741  
   742  		blob := path.Join(workdir, fmt.Sprintf("%d.blob", size))
   743  
   744  		fhandle, err := os.OpenFile(blob, os.O_RDONLY, defaultFilePerms)
   745  		if err != nil {
   746  			isConnFail = true
   747  
   748  			return
   749  		}
   750  
   751  		defer fhandle.Close()
   752  
   753  		digest := blobHash[blob]
   754  
   755  		// upload blob
   756  		resp, err = client.R().
   757  			SetContentLength(true).
   758  			SetHeader("Content-Type", "application/octet-stream").
   759  			SetBody(fhandle).
   760  			Patch(loc)
   761  
   762  		latency = time.Since(start)
   763  
   764  		if err != nil {
   765  			isConnFail = true
   766  
   767  			return
   768  		}
   769  
   770  		loc = getLocation(url, resp)
   771  
   772  		// request specific check
   773  		statusCode = resp.StatusCode()
   774  		if statusCode != http.StatusAccepted {
   775  			isErr = true
   776  
   777  			return
   778  		}
   779  
   780  		// finish upload
   781  		resp, err = client.R().
   782  			SetContentLength(true).
   783  			SetHeader("Content-Length", fmt.Sprintf("%d", size)).
   784  			SetHeader("Content-Type", "application/octet-stream").
   785  			SetQueryParam("digest", digest.String()).
   786  			Put(loc)
   787  
   788  		latency = time.Since(start)
   789  
   790  		if err != nil {
   791  			isConnFail = true
   792  
   793  			return
   794  		}
   795  
   796  		// request specific check
   797  		statusCode = resp.StatusCode()
   798  		if statusCode != http.StatusCreated {
   799  			isErr = true
   800  
   801  			return
   802  		}
   803  
   804  		// upload image config blob
   805  		resp, err = client.R().
   806  			Post(fmt.Sprintf("%s/v2/%s/blobs/uploads/", url, repo))
   807  
   808  		latency = time.Since(start)
   809  
   810  		if err != nil {
   811  			isConnFail = true
   812  
   813  			return
   814  		}
   815  
   816  		// request specific check
   817  		statusCode = resp.StatusCode()
   818  		if statusCode != http.StatusAccepted {
   819  			isErr = true
   820  
   821  			return
   822  		}
   823  
   824  		loc = getLocation(url, resp)
   825  		cblob, cdigest := getImageConfig()
   826  		resp, err = client.R().
   827  			SetContentLength(true).
   828  			SetHeader("Content-Type", "application/octet-stream").
   829  			SetBody(fhandle).
   830  			Patch(loc)
   831  
   832  		if err != nil {
   833  			isConnFail = true
   834  
   835  			return
   836  		}
   837  
   838  		// request specific check
   839  		statusCode = resp.StatusCode()
   840  		if statusCode != http.StatusAccepted {
   841  			isErr = true
   842  
   843  			return
   844  		}
   845  
   846  		// upload blob
   847  		resp, err = client.R().
   848  			SetContentLength(true).
   849  			SetHeader("Content-Type", "application/octet-stream").
   850  			SetBody(cblob).
   851  			Patch(loc)
   852  
   853  		latency = time.Since(start)
   854  
   855  		if err != nil {
   856  			isConnFail = true
   857  
   858  			return
   859  		}
   860  
   861  		loc = getLocation(url, resp)
   862  
   863  		// request specific check
   864  		statusCode = resp.StatusCode()
   865  		if statusCode != http.StatusAccepted {
   866  			isErr = true
   867  
   868  			return
   869  		}
   870  
   871  		// finish upload
   872  		resp, err = client.R().
   873  			SetContentLength(true).
   874  			SetHeader("Content-Length", fmt.Sprintf("%d", len(cblob))).
   875  			SetHeader("Content-Type", "application/octet-stream").
   876  			SetQueryParam("digest", cdigest.String()).
   877  			Put(loc)
   878  
   879  		latency = time.Since(start)
   880  
   881  		if err != nil {
   882  			isConnFail = true
   883  
   884  			return
   885  		}
   886  
   887  		// request specific check
   888  		statusCode = resp.StatusCode()
   889  		if statusCode != http.StatusCreated {
   890  			isErr = true
   891  
   892  			return
   893  		}
   894  
   895  		// create a manifest
   896  		manifest := ispec.Manifest{
   897  			Versioned: imeta.Versioned{
   898  				SchemaVersion: defaultSchemaVersion,
   899  			},
   900  			Config: ispec.Descriptor{
   901  				MediaType: "application/vnd.oci.image.config.v1+json",
   902  				Digest:    cdigest,
   903  				Size:      int64(len(cblob)),
   904  			},
   905  			Layers: []ispec.Descriptor{
   906  				{
   907  					MediaType: "application/vnd.oci.image.layer.v1.tar",
   908  					Digest:    digest,
   909  					Size:      int64(size),
   910  				},
   911  			},
   912  		}
   913  
   914  		content, err := json.Marshal(manifest)
   915  		if err != nil {
   916  			log.Fatal(err)
   917  		}
   918  
   919  		manifestTag := fmt.Sprintf("tag%d", count)
   920  
   921  		// finish upload
   922  		resp, err = client.R().
   923  			SetContentLength(true).
   924  			SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
   925  			SetBody(content).
   926  			Put(fmt.Sprintf("%s/v2/%s/manifests/%s", url, repo, manifestTag))
   927  
   928  		latency = time.Since(start)
   929  
   930  		if err != nil {
   931  			isConnFail = true
   932  
   933  			return
   934  		}
   935  
   936  		// request specific check
   937  		statusCode = resp.StatusCode()
   938  		if statusCode != http.StatusCreated {
   939  			isErr = true
   940  
   941  			return
   942  		}
   943  	}()
   944  
   945  	return repos
   946  }
   947  
   948  func getRandomSize(probabilityRange []float64) (int, int) {
   949  	var size int
   950  
   951  	idx := flipFunc(probabilityRange)
   952  	smallSizeIdx := 0
   953  	mediumSizeIdx := 1
   954  	largeSizeIdx := 2
   955  
   956  	switch idx {
   957  	case smallSizeIdx:
   958  		size = smallBlob
   959  		current := loadOrStore(&statusRequests, "1MB", 0)
   960  		statusRequests.Store("1MB", current+1)
   961  	case mediumSizeIdx:
   962  		size = mediumBlob
   963  		current := loadOrStore(&statusRequests, "10MB", 0)
   964  		statusRequests.Store("10MB", current+1)
   965  	case largeSizeIdx:
   966  		size = largeBlob
   967  		current := loadOrStore(&statusRequests, "100MB", 0)
   968  		statusRequests.Store("100MB", current+1)
   969  	default:
   970  		size = 0
   971  	}
   972  
   973  	return size, idx
   974  }
   975  
   976  //nolint:gosec
   977  func flipFunc(probabilityRange []float64) int {
   978  	seed := time.Now().UTC().UnixNano()
   979  	mrand := rand.New(rand.NewSource(seed))
   980  	toss := mrand.Float64()
   981  
   982  	for idx, r := range probabilityRange {
   983  		if toss < r {
   984  			return idx
   985  		}
   986  	}
   987  
   988  	return len(probabilityRange) - 1
   989  }
   990  
   991  // pbty - probabilities.
   992  func normalizeProbabilityRange(pbty []float64) []float64 {
   993  	dim := len(pbty)
   994  
   995  	// npd - normalized probability density
   996  	npd := make([]float64, dim)
   997  
   998  	for idx := range pbty {
   999  		npd[idx] = 0.0
  1000  	}
  1001  
  1002  	// [0.2, 0.7, 0.1] -> [0.2, 0.9, 1]
  1003  	npd[0] = pbty[0]
  1004  	for i := 1; i < dim; i++ {
  1005  		npd[i] = npd[i-1] + pbty[i]
  1006  	}
  1007  
  1008  	return npd
  1009  }
  1010  
  1011  func loadOrStore(statusRequests *sync.Map, key string, value int) int { //nolint:unparam
  1012  	val, _ := statusRequests.LoadOrStore(key, value)
  1013  
  1014  	intValue, ok := val.(int)
  1015  	if !ok {
  1016  		log.Fatalf("invalid type: %#v, should be int", val)
  1017  	}
  1018  
  1019  	return intValue
  1020  }
  1021  
  1022  func getImageConfig() ([]byte, godigest.Digest) {
  1023  	createdTime := time.Date(2011, time.Month(1), 1, 1, 1, 1, 0, time.UTC)
  1024  
  1025  	config := ispec.Image{
  1026  		Created: &createdTime,
  1027  		Author:  "ZotUser",
  1028  		Platform: ispec.Platform{
  1029  			OS:           "linux",
  1030  			Architecture: "amd64",
  1031  		},
  1032  		RootFS: ispec.RootFS{
  1033  			Type:    "layers",
  1034  			DiffIDs: []godigest.Digest{},
  1035  		},
  1036  	}
  1037  
  1038  	configBlobContent, err := json.MarshalIndent(&config, "", "\t")
  1039  	if err != nil {
  1040  		log.Fatal(err)
  1041  	}
  1042  
  1043  	configBlobDigestRaw := godigest.FromBytes(configBlobContent)
  1044  
  1045  	return configBlobContent, configBlobDigestRaw
  1046  }
  1047  
  1048  func getLocation(baseURL string, resp *resty.Response) string {
  1049  	// For some API responses, the Location header is set and is supposed to
  1050  	// indicate an opaque value. However, it is not clear if this value is an
  1051  	// absolute URL (https://server:port/v2/...) or just a path (/v2/...)
  1052  	// zot implements the latter as per the spec, but some registries appear to
  1053  	// return the former - this needs to be clarified
  1054  	loc := resp.Header().Get("Location")
  1055  
  1056  	uloc, err := url.Parse(loc)
  1057  	if err != nil {
  1058  		return ""
  1059  	}
  1060  
  1061  	path := uloc.Path
  1062  
  1063  	return baseURL + path
  1064  }