Repository: regclient/regclient
Pull request #394 (2023-03-25 14:51:03)

Allow registry to adjust blob chunk size
<!--

Commits must be signed indicating your agreement to the [DCO](https://developercertificate.org/).
See [DCO missing](https://github.com/src-d/guide/blob/master/developer-community/fix-DCO.md) for steps to fix a missing signoff.

-->

### Fixes issue

<!-- If this is a bug fix, include "fixes #xxxx", or "closes #xxxx" -->

### Describe the change

This adds support for the `OCI-Chunk-Min-Length` header defined in https://github.com/opencontainers/distribution-spec/pull/391

<!-- Include the type of change: bug fix, new feature, breaking change, documentation update -->
<!-- Describe what was changed, why the change was made, and how it was implemented -->

### How to verify it

Performing a chunked upload to a registry that uses this header, with a small chunk size, will automatically adjust as long as the chunk size doesn't exceed the limit.
<!-- Include steps that can be taken to verify the change -->

### Changelog text

- Support `OCI-Chunk-Min-Length` header
<!-- If the release changelog should have an entry for this, include it here -->

### Please verify and check that the pull request fulfills the following requirements

<!-- Mark the following with an [X] to verify they are included -->

- [X] Tests have been added or not applicable
- [X] Documentation has been added, updated, or not applicable
- [X] Changes have been rebased to main
- [X] Multiple commits to the same code have been squashed



Patch:
diff --git a/cmd/regbot/config.go b/cmd/regbot/config.go
index 1524c0e..24f8d3e 100644
--- a/cmd/regbot/config.go
+++ b/cmd/regbot/config.go
@@ -21,12 +21,14 @@ type Config struct {
 
 // ConfigDefaults is uses for general options and defaults for ConfigScript entries
 type ConfigDefaults struct {
-	Interval       time.Duration `yaml:"interval" json:"interval"`
-	Schedule       string        `yaml:"schedule" json:"schedule"`
-	Parallel       int           `yaml:"parallel" json:"parallel"`
-	SkipDockerConf bool          `yaml:"skipDockerConfig" json:"skipDockerConfig"`
-	Timeout        time.Duration `yaml:"timeout" json:"timeout"`
-	UserAgent      string        `yaml:"userAgent" json:"userAgent"`
+	Interval time.Duration `yaml:"interval" json:"interval"`
+	Schedule string        `yaml:"schedule" json:"schedule"`
+	Parallel int           `yaml:"parallel" json:"parallel"`
+	Timeout  time.Duration `yaml:"timeout" json:"timeout"`
+	// general options
+	BlobLimit      int64  `yaml:"blobLimit" json:"blobLimit"`
+	SkipDockerConf bool   `yaml:"skipDockerConfig" json:"skipDockerConfig"`
+	UserAgent      string `yaml:"userAgent" json:"userAgent"`
 }
 
 // ConfigScript defines a source/target repository to sync
@@ -105,13 +107,6 @@ func configExpandTemplates(c *Config) error {
 		}
 		c.Creds[i].RegCert = val
 	}
-	// for i := range c.Scripts {
-	// 	val, err := template.String(c.Scripts[i].Script, nil)
-	// 	if err != nil {
-	// 		return err
-	// 	}
-	// 	c.Scripts[i].Script = val
-	// }
 	return nil
 }
 
diff --git a/cmd/regbot/root.go b/cmd/regbot/root.go
index ce2f88d..8d0295b 100644
--- a/cmd/regbot/root.go
+++ b/cmd/regbot/root.go
@@ -247,6 +247,12 @@ func loadConf() error {
 	rcOpts := []regclient.Opt{
 		regclient.WithLog(log),
 	}
+	if conf.Defaults.BlobLimit != 0 {
+		rcOpts = append(rcOpts, regclient.WithBlobLimit(conf.Defaults.BlobLimit))
+	}
+	if !conf.Defaults.SkipDockerConf {
+		rcOpts = append(rcOpts, regclient.WithDockerCreds(), regclient.WithDockerCerts())
+	}
 	if conf.Defaults.UserAgent != "" {
 		rcOpts = append(rcOpts, regclient.WithUserAgent(conf.Defaults.UserAgent))
 	} else {
@@ -257,9 +263,6 @@ func loadConf() error {
 			rcOpts = append(rcOpts, regclient.WithUserAgent(UserAgent+" ("+info.VCSRef+")"))
 		}
 	}
-	if !conf.Defaults.SkipDockerConf {
-		rcOpts = append(rcOpts, regclient.WithDockerCreds(), regclient.WithDockerCerts())
-	}
 	rcHosts := []config.Host{}
 	for _, host := range conf.Creds {
 		if host.Scheme != "" {
diff --git a/cmd/regctl/config.go b/cmd/regctl/config.go
index 75bf8bd..f249470 100644
--- a/cmd/regctl/config.go
+++ b/cmd/regctl/config.go
@@ -10,6 +10,8 @@ import (
 
 	"github.com/regclient/regclient/config"
 	"github.com/regclient/regclient/internal/conffile"
+	"github.com/regclient/regclient/pkg/template"
+	"github.com/spf13/cobra"
 )
 
 var (
@@ -26,8 +28,90 @@ type Config struct {
 	Filename      string                  `json:"-"`                 // filename that was loaded
 	Version       int                     `json:"version,omitempty"` // version the file in case the config file syntax changes in the future
 	Hosts         map[string]*config.Host `json:"hosts"`
-	IncDockerCred *bool                   `json:"incDockerCred,omitempty"`
+	BlobLimit     int64                   `json:"blobLimit,omitempty"`
 	IncDockerCert *bool                   `json:"incDockerCert,omitempty"`
+	IncDockerCred *bool                   `json:"incDockerCred,omitempty"`
+}
+
+var configOpts struct {
+	blobLimit  int64
+	dockerCert bool
+	dockerCred bool
+	format     string
+}
+var configCmd = &cobra.Command{
+	Use:   "config <cmd>",
+	Short: "read/set configuration options",
+}
+var configGetCmd = &cobra.Command{
+	Use:   "get",
+	Short: "show the config",
+	Long:  `Displays the configuration. Passwords are not included in the output.`,
+	Args:  cobra.ExactArgs(0),
+	RunE:  runConfigGet,
+}
+var configSetCmd = &cobra.Command{
+	Use:   "set",
+	Short: "set a configuration option",
+	Long:  `Modifies an option used in future executions.`,
+	Args:  cobra.ExactArgs(0),
+	RunE:  runConfigSet,
+}
+
+func init() {
+	configGetCmd.Flags().StringVar(&configOpts.format, "format", "{{ printPretty . }}", "format the output with Go template syntax")
+
+	configSetCmd.Flags().Int64Var(&configOpts.blobLimit, "blob-limit", 0, "limit for blob chunks, this is stored in memory")
+	configSetCmd.Flags().BoolVar(&configOpts.dockerCert, "docker-cert", false, "load certificates from docker")
+	configSetCmd.Flags().BoolVar(&configOpts.dockerCred, "docker-cred", false, "load credentials from docker")
+
+	configCmd.AddCommand(configGetCmd)
+	configCmd.AddCommand(configSetCmd)
+	rootCmd.AddCommand(configCmd)
+}
+
+func runConfigGet(cmd *cobra.Command, args []string) error {
+	c, err := ConfigLoadDefault()
+	if err != nil {
+		return err
+	}
+	for i := range c.Hosts {
+		c.Hosts[i].Pass = ""
+		c.Hosts[i].Token = ""
+	}
+
+	return template.Writer(cmd.OutOrStdout(), configOpts.format, c)
+}
+
+func runConfigSet(cmd *cobra.Command, args []string) error {
+	c, err := ConfigLoadDefault()
+	if err != nil {
+		return err
+	}
+
+	if flagChanged(cmd, "blob-limit") {
+		c.BlobLimit = configOpts.blobLimit
+	}
+	if flagChanged(cmd, "docker-cert") {
+		if !configOpts.dockerCert {
+			c.IncDockerCert = &configOpts.dockerCert
+		} else {
+			c.IncDockerCert = nil
+		}
+	}
+	if flagChanged(cmd, "docker-cred") {
+		if !configOpts.dockerCred {
+			c.IncDockerCred = &configOpts.dockerCred
+		} else {
+			c.IncDockerCred = nil
+		}
+	}
+
+	err = c.ConfigSave()
+	if err != nil {
+		return err
+	}
+	return nil
 }
 
 // ConfigNew creates an empty configuration
diff --git a/cmd/regctl/root.go b/cmd/regctl/root.go
index 770ec26..16bc122 100644
--- a/cmd/regctl/root.go
+++ b/cmd/regctl/root.go
@@ -109,6 +109,9 @@ func newRegClient() *regclient.RegClient {
 			rcOpts = append(rcOpts, regclient.WithUserAgent(UserAgent+" ("+info.VCSRef+")"))
 		}
 	}
+	if conf.BlobLimit != 0 {
+		rcOpts = append(rcOpts, regclient.WithBlobLimit(conf.BlobLimit))
+	}
 	if conf.IncDockerCred == nil || *conf.IncDockerCred {
 		rcOpts = append(rcOpts, regclient.WithDockerCreds())
 	}
diff --git a/cmd/regsync/config.go b/cmd/regsync/config.go
index 9111eaa..ea77bee 100644
--- a/cmd/regsync/config.go
+++ b/cmd/regsync/config.go
@@ -46,9 +46,11 @@ type ConfigDefaults struct {
 	ForceRecursive  *bool                  `yaml:"forceRecursive" json:"forceRecursive"`
 	IncludeExternal *bool                  `yaml:"includeExternal" json:"includeExternal"`
 	MediaTypes      []string               `yaml:"mediaTypes" json:"mediaTypes"`
-	SkipDockerConf  bool                   `yaml:"skipDockerConfig" json:"skipDockerConfig"`
 	Hooks           ConfigHooks            `yaml:"hooks" json:"hooks"`
-	UserAgent       string                 `yaml:"userAgent" json:"userAgent"`
+	// general options
+	BlobLimit      int64  `yaml:"blobLimit" json:"blobLimit"`
+	SkipDockerConf bool   `yaml:"skipDockerConfig" json:"skipDockerConfig"`
+	UserAgent      string `yaml:"userAgent" json:"userAgent"`
 }
 
 // ConfigRateLimit is for rate limit settings
diff --git a/cmd/regsync/root.go b/cmd/regsync/root.go
index 0a8c8b0..76bb3bf 100644
--- a/cmd/regsync/root.go
+++ b/cmd/regsync/root.go
@@ -343,6 +343,12 @@ func loadConf() error {
 	rcOpts := []regclient.Opt{
 		regclient.WithLog(log),
 	}
+	if conf.Defaults.BlobLimit != 0 {
+		rcOpts = append(rcOpts, regclient.WithBlobLimit(conf.Defaults.BlobLimit))
+	}
+	if !conf.Defaults.SkipDockerConf {
+		rcOpts = append(rcOpts, regclient.WithDockerCreds(), regclient.WithDockerCerts())
+	}
 	if conf.Defaults.UserAgent != "" {
 		rcOpts = append(rcOpts, regclient.WithUserAgent(conf.Defaults.UserAgent))
 	} else {
@@ -353,9 +359,6 @@ func loadConf() error {
 			rcOpts = append(rcOpts, regclient.WithUserAgent(UserAgent+" ("+info.VCSRef+")"))
 		}
 	}
-	if !conf.Defaults.SkipDockerConf {
-		rcOpts = append(rcOpts, regclient.WithDockerCreds(), regclient.WithDockerCerts())
-	}
 	rcHosts := []config.Host{}
 	for _, host := range conf.Creds {
 		if host.Scheme != "" {
diff --git a/regclient.go b/regclient.go
index b4d5323..b31c99e 100644
--- a/regclient.go
+++ b/regclient.go
@@ -95,6 +95,13 @@ func New(opts ...Opt) *RegClient {
 	return &rc
 }
 
+// WithBlobLimit sets the max size for chunked blob uploads which get stored in memory
+func WithBlobLimit(limit int64) Opt {
+	return func(rc *RegClient) {
+		rc.regOpts = append(rc.regOpts, reg.WithBlobLimit(limit))
+	}
+}
+
 // WithCertDir adds a path of certificates to trust similar to Docker's /etc/docker/certs.d
 func WithCertDir(path ...string) Opt {
 	return func(rc *RegClient) {
diff --git a/regclient/regclient.go b/regclient/regclient.go
index 1e98b38..261d277 100644
--- a/regclient/regclient.go
+++ b/regclient/regclient.go
@@ -9,14 +9,13 @@ import (
 	"github.com/regclient/regclient/config"
 	"github.com/regclient/regclient/internal/reghttp"
 	"github.com/regclient/regclient/scheme"
-	"github.com/regclient/regclient/scheme/reg"
 	"github.com/regclient/regclient/types/repo"
 	"github.com/regclient/regclient/types/tag"
 )
 
 const (
-	DefaultBlobChunk   = reg.DefaultBlobChunk
-	DefaultBlobMax     = reg.DefaultBlobMax
+	DefaultBlobChunk   = 1024 * 1024
+	DefaultBlobMax     = -1
 	DefaultRetryLimit  = reghttp.DefaultRetryLimit
 	DefaultUserAgent   = rcTop.DefaultUserAgent
 	DockerCertDir      = rcTop.DockerCertDir
diff --git a/scheme/reg/blob.go b/scheme/reg/blob.go
index b119e2c..558ab59 100644
--- a/scheme/reg/blob.go
+++ b/scheme/reg/blob.go
@@ -253,6 +253,30 @@ func (reg *Reg) blobGetUploadURL(ctx context.Context, r ref.Ref) (*url.URL, erro
 		return nil, fmt.Errorf("failed to send blob post, ref %s: %w", r.CommonName(), reghttp.HTTPError(resp.HTTPResponse().StatusCode))
 	}
 
+	// if min size header received, check/adjust host settings
+	minSizeStr := resp.HTTPResponse().Header.Get(blobChunkMinHeader)
+	if minSizeStr != "" {
+		minSize, err := strconv.ParseInt(minSizeStr, 10, 64)
+		if err != nil {
+			reg.log.WithFields(logrus.Fields{
+				"size": minSizeStr,
+				"err":  err,
+			}).Warn("Failed to parse chunk size header")
+		} else {
+			host := reg.hostGet(r.Registry)
+			if (host.BlobChunk > 0 && minSize > host.BlobChunk) || (host.BlobChunk <= 0 && minSize > reg.blobChunkSize) {
+				if minSize > reg.blobChunkLimit {
+					host.BlobChunk = reg.blobChunkLimit
+				} else {
+					host.BlobChunk = minSize
+				}
+				reg.log.WithFields(logrus.Fields{
+					"size": host.BlobChunk,
+					"host": host.Name,
+				}).Debug("Registry requested min chunk size")
+			}
+		}
+	}
 	// Extract the location into a new putURL based on whether it's relative, fqdn with a scheme, or without a scheme.
 	location := resp.HTTPResponse().Header.Get("Location")
 	if location == "" {
@@ -299,6 +323,31 @@ func (reg *Reg) blobMount(ctx context.Context, rTgt ref.Ref, d types.Descriptor,
 		return nil, "", fmt.Errorf("failed to mount blob, digest %s, ref %s: %w", d.Digest.String(), rTgt.CommonName(), err)
 	}
 	defer resp.Close()
+
+	// if min size header received, check/adjust host settings
+	minSizeStr := resp.HTTPResponse().Header.Get(blobChunkMinHeader)
+	if minSizeStr != "" {
+		minSize, err := strconv.ParseInt(minSizeStr, 10, 64)
+		if err != nil {
+			reg.log.WithFields(logrus.Fields{
+				"size": minSizeStr,
+				"err":  err,
+			}).Warn("Failed to parse chunk size header")
+		} else {
+			host := reg.hostGet(rTgt.Registry)
+			if (host.BlobChunk > 0 && minSize > host.BlobChunk) || (host.BlobChunk <= 0 && minSize > reg.blobChunkSize) {
+				if minSize > reg.blobChunkLimit {
+					host.BlobChunk = reg.blobChunkLimit
+				} else {
+					host.BlobChunk = minSize
+				}
+				reg.log.WithFields(logrus.Fields{
+					"size": host.BlobChunk,
+					"host": host.Name,
+				}).Debug("Registry requested min chunk size")
+			}
+		}
+	}
 	// 201 indicates the blob mount succeeded
 	if resp.HTTPResponse().StatusCode == 201 {
 		return nil, "", nil
diff --git a/scheme/reg/reg.go b/scheme/reg/reg.go
index 5ccf228..b7019f4 100644
--- a/scheme/reg/reg.go
+++ b/scheme/reg/reg.go
@@ -13,21 +13,26 @@ import (
 )
 
 const (
-	// DefaultBlobChunk 1M chunks, this is allocated in a memory buffer
-	DefaultBlobChunk = 1024 * 1024
-	// DefaultBlobMax is disabled to support registries without chunked upload support
-	DefaultBlobMax = -1
+	// blobChunkMinHeader is returned by registries requesting a minimum chunk size
+	blobChunkMinHeader = "OCI-Chunk-Min-Length"
+	// defaultBlobChunk 1M chunks, this is allocated in a memory buffer
+	defaultBlobChunk = 1024 * 1024
+	// defaultBlobChunkLimit 1G chunks, prevents a memory exhaustion attack
+	defaultBlobChunkLimit = 1024 * 1024 * 1024
+	// defaultBlobMax is disabled to support registries without chunked upload support
+	defaultBlobMax = -1
 )
 
 // Reg is used for interacting with remote registry servers
 type Reg struct {
-	reghttp       *reghttp.Client
-	reghttpOpts   []reghttp.Opts
-	log           *logrus.Logger
-	hosts         map[string]*config.Host
-	blobChunkSize int64
-	blobMaxPut    int64
-	mu            sync.Mutex
+	reghttp        *reghttp.Client
+	reghttpOpts    []reghttp.Opts
+	log            *logrus.Logger
+	hosts          map[string]*config.Host
+	blobChunkSize  int64
+	blobChunkLimit int64
+	blobMaxPut     int64
+	mu             sync.Mutex
 }
 
 // Opts provides options to access registries
@@ -36,10 +41,11 @@ type Opts func(*Reg)
 // New returns a Reg pointer with any provided options
 func New(opts ...Opts) *Reg {
 	r := Reg{
-		reghttpOpts:   []reghttp.Opts{},
-		blobChunkSize: DefaultBlobChunk,
-		blobMaxPut:    DefaultBlobMax,
-		hosts:         map[string]*config.Host{},
+		reghttpOpts:    []reghttp.Opts{},
+		blobChunkSize:  defaultBlobChunk,
+		blobChunkLimit: defaultBlobChunkLimit,
+		blobMaxPut:     defaultBlobMax,
+		hosts:          map[string]*config.Host{},
 	}
 	for _, opt := range opts {
 		opt(&r)
@@ -63,10 +69,10 @@ func (reg *Reg) hostGet(hostname string) *config.Host {
 }
 
 // WithBlobSize overrides default blob sizes
-func WithBlobSize(chunk, max int64) Opts {
+func WithBlobSize(size, max int64) Opts {
 	return func(r *Reg) {
-		if chunk > 0 {
-			r.blobChunkSize = chunk
+		if size > 0 {
+			r.blobChunkSize = size
 		}
 		if max != 0 {
 			r.blobMaxPut = max
@@ -74,6 +80,18 @@ func WithBlobSize(chunk, max int64) Opts {
 	}
 }
 
+// WithBlobLimit overrides default blob limit
+func WithBlobLimit(limit int64) Opts {
+	return func(r *Reg) {
+		if limit > 0 {
+			r.blobChunkLimit = limit
+		}
+		if r.blobMaxPut > 0 && r.blobMaxPut < limit {
+			r.blobMaxPut = limit
+		}
+	}
+}
+
 // WithCerts adds certificates
 func WithCerts(certs [][]byte) Opts {
 	return func(r *Reg) {


Test patch:
diff --git a/cmd/regctl/config_test.go b/cmd/regctl/config_test.go
new file mode 100644
index 0000000..35b4185
--- /dev/null
+++ b/cmd/regctl/config_test.go
@@ -0,0 +1,106 @@
+package main
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+func TestConfig(t *testing.T) {
+	// set a temp dir for storing configs
+	tempDir := t.TempDir()
+	origEnv, set := os.LookupEnv(ConfigEnv)
+	if set {
+		defer os.Setenv(ConfigEnv, origEnv)
+	}
+	os.Setenv(ConfigEnv, filepath.Join(tempDir, "config.json"))
+
+	// test empty config
+	out, err := cobraTest(t, "config", "get", "--format", "{{ json . }}")
+	if err != nil {
+		t.Errorf("failed to run config get: %v", err)
+	}
+	if out != `{"hosts":{}}` {
+		t.Errorf("unexpected output from empty config, expected: %s, received: %s", `{"hosts":{}}`, out)
+	}
+
+	// set options
+	testLimit := "420000000"
+	out, err = cobraTest(t, "config", "set", "--blob-limit", testLimit)
+	if err != nil {
+		t.Errorf("failed to set blob-limit: %v", err)
+	}
+	if out != "" {
+		t.Errorf("unexpected output from set: %s", out)
+	}
+	out, err = cobraTest(t, "config", "set", "--docker-cert=false")
+	if err != nil {
+		t.Errorf("failed to set docker-cert: %v", err)
+	}
+	if out != "" {
+		t.Errorf("unexpected output from set: %s", out)
+	}
+	out, err = cobraTest(t, "config", "set", "--docker-cred=false")
+	if err != nil {
+		t.Errorf("failed to set docker-cred: %v", err)
+	}
+	if out != "" {
+		t.Errorf("unexpected output from set: %s", out)
+	}
+
+	// get changes
+	out, err = cobraTest(t, "config", "get", "--format", "{{ .BlobLimit }}")
+	if err != nil {
+		t.Errorf("failed to run config get on blob-limit: %v", err)
+	}
+	if out != testLimit {
+		t.Errorf("unexpected output for blob-limit, expected: %s, received: %s", testLimit, out)
+	}
+	out, err = cobraTest(t, "config", "get", "--format", "{{ .IncDockerCert }}")
+	if err != nil {
+		t.Errorf("failed to run config get on docker-cert: %v", err)
+	}
+	if out != "false" {
+		t.Errorf("unexpected output for docker-cert, expected: false, received: %s", out)
+	}
+	out, err = cobraTest(t, "config", "get", "--format", "{{ .IncDockerCred }}")
+	if err != nil {
+		t.Errorf("failed to run config get on docker-cred: %v", err)
+	}
+	if out != "false" {
+		t.Errorf("unexpected output for docker-cred, expected: false, received: %s", out)
+	}
+
+	// reset back to zero values
+	out, err = cobraTest(t, "config", "set", "--blob-limit", "0")
+	if err != nil {
+		t.Errorf("failed to set blob-limit: %v", err)
+	}
+	if out != "" {
+		t.Errorf("unexpected output from set: %s", out)
+	}
+	out, err = cobraTest(t, "config", "set", "--docker-cert")
+	if err != nil {
+		t.Errorf("failed to set docker-cert: %v", err)
+	}
+	if out != "" {
+		t.Errorf("unexpected output from set: %s", out)
+	}
+	out, err = cobraTest(t, "config", "set", "--docker-cred")
+	if err != nil {
+		t.Errorf("failed to set docker-cred: %v", err)
+	}
+	if out != "" {
+		t.Errorf("unexpected output from set: %s", out)
+	}
+
+	// verify config is empty
+	out, err = cobraTest(t, "config", "get", "--format", "{{ json . }}")
+	if err != nil {
+		t.Errorf("failed to run config get: %v", err)
+	}
+	if out != `{"hosts":{}}` {
+		t.Errorf("unexpected output from empty config, expected: %s, received: %s", `{"hosts":{}}`, out)
+	}
+
+}
diff --git a/scheme/reg/blob_test.go b/scheme/reg/blob_test.go
index d49957c..536c28b 100644
--- a/scheme/reg/blob_test.go
+++ b/scheme/reg/blob_test.go
@@ -371,12 +371,15 @@ func TestBlobPut(t *testing.T) {
 	blobChunk := 512
 	blobLen := 1024  // must be blobChunk < blobLen <= blobChunk * 2
 	blobLen3 := 1000 // blob without a full final chunk
+	blobLen4 := 2048 // must be blobChunk < blobLen <= blobChunk * 2
 	d1, blob1 := reqresp.NewRandomBlob(blobLen, seed)
 	uuid1 := uuid.New()
 	d2, blob2 := reqresp.NewRandomBlob(blobLen, seed+1)
 	uuid2 := uuid.New()
 	d3, blob3 := reqresp.NewRandomBlob(blobLen3, seed+2)
 	uuid3 := uuid.New()
+	d4, blob4 := reqresp.NewRandomBlob(blobLen4, seed+3)
+	uuid4 := uuid.New()
 	// dMissing := digest.FromBytes([]byte("missing"))
 	user := "testing"
 	pass := "password"
@@ -517,30 +520,6 @@ func TestBlobPut(t *testing.T) {
 				},
 			},
 		},
-		// upload patch 2 fail for d2
-		// {
-		// 	ReqEntry: reqresp.ReqEntry{
-		// 		DelOnUse: true,
-		// 		Name:     "PATCH 2 fail for d2",
-		// 		Method:   "PATCH",
-		// 		Path:     "/v2" + blobRepo + "/blobs/uploads/" + uuid2.String(),
-		// 		Query: map[string][]string{
-		// 			"chunk": {"2"},
-		// 		},
-		// 		Headers: http.Header{
-		// 			"Content-Length": {fmt.Sprintf("%d", blobLen-blobChunk)},
-		// 			"Content-Range":  {fmt.Sprintf("%d-%d", blobChunk, blobLen)},
-		// 			"Content-Type":   {"application/octet-stream"},
-		// 		},
-		// 		Body: blob2[blobChunk:],
-		// 	},
-		// 	RespEntry: reqresp.RespEntry{
-		// 		Status: http.StatusGatewayTimeout,
-		// 		Headers: http.Header{
-		// 			"Content-Length": {fmt.Sprintf("%d", 0)},
-		// 		},
-		// 	},
-		// },
 		// upload patch 2b for d2
 		{
 			ReqEntry: reqresp.ReqEntry{
@@ -684,30 +663,6 @@ func TestBlobPut(t *testing.T) {
 				},
 			},
 		},
-		// upload patch 2 fail for d3
-		// {
-		// 	ReqEntry: reqresp.ReqEntry{
-		// 		DelOnUse: true,
-		// 		Name:     "PATCH 2 fail for d3",
-		// 		Method:   "PATCH",
-		// 		Path:     "/v2" + blobRepo + "/blobs/uploads/" + uuid3.String(),
-		// 		Query: map[string][]string{
-		// 			"chunk": {"2"},
-		// 		},
-		// 		Headers: http.Header{
-		// 			"Content-Length": {fmt.Sprintf("%d", blobLen3-blobChunk)},
-		// 			"Content-Range":  {fmt.Sprintf("%d-%d", blobChunk, blobLen3)},
-		// 			"Content-Type":   {"application/octet-stream"},
-		// 		},
-		// 		Body: blob2[blobChunk:],
-		// 	},
-		// 	RespEntry: reqresp.RespEntry{
-		// 		Status: http.StatusGatewayTimeout,
-		// 		Headers: http.Header{
-		// 			"Content-Length": {fmt.Sprintf("%d", 0)},
-		// 		},
-		// 	},
-		// },
 		// upload patch 2 for d3
 		{
 			ReqEntry: reqresp.ReqEntry{
@@ -822,6 +777,101 @@ func TestBlobPut(t *testing.T) {
 				},
 			},
 		},
+
+		// get upload4 location
+		{
+			ReqEntry: reqresp.ReqEntry{
+				Name:   "POST for d4",
+				Method: "POST",
+				Path:   "/v2" + blobRepo + "/blobs/uploads/",
+				Query: map[string][]string{
+					"mount": {d4.String()},
+				},
+			},
+			RespEntry: reqresp.RespEntry{
+				Status: http.StatusAccepted,
+				Headers: http.Header{
+					"Content-Length":   {"0"},
+					"Location":         {uuid4.String()},
+					blobChunkMinHeader: {fmt.Sprintf("%d", blobLen4/2)},
+				},
+			},
+		},
+		// upload put for d4
+		{
+			ReqEntry: reqresp.ReqEntry{
+				DelOnUse: false,
+				Name:     "PUT for patched d4",
+				Method:   "PUT",
+				Path:     "/v2" + blobRepo + "/blobs/uploads/" + uuid4.String(),
+				Query: map[string][]string{
+					"digest": {d4.String()},
+					"chunk":  {"3"},
+				},
+				Headers: http.Header{
+					"Content-Length": {"0"},
+					"Content-Type":   {"application/octet-stream"},
+				},
+			},
+			RespEntry: reqresp.RespEntry{
+				Status: http.StatusCreated,
+				Headers: http.Header{
+					"Content-Length":        {"0"},
+					"Location":              {"/v2" + blobRepo + "/blobs/" + d4.String()},
+					"Docker-Content-Digest": {d4.String()},
+				},
+			},
+		},
+		// upload patch 2 for d4
+		{
+			ReqEntry: reqresp.ReqEntry{
+				DelOnUse: false,
+				Name:     "PATCH 2 for d4",
+				Method:   "PATCH",
+				Path:     "/v2" + blobRepo + "/blobs/uploads/" + uuid4.String(),
+				Query: map[string][]string{
+					"chunk": {"2"},
+				},
+				Headers: http.Header{
+					"Content-Length": {fmt.Sprintf("%d", blobLen4/2)},
+					"Content-Range":  {fmt.Sprintf("%d-%d", blobLen4/2, blobLen4-1)},
+					"Content-Type":   {"application/octet-stream"},
+				},
+				Body: blob4[blobLen4/2:],
+			},
+			RespEntry: reqresp.RespEntry{
+				Status: http.StatusAccepted,
+				Headers: http.Header{
+					"Content-Length": {fmt.Sprintf("%d", 0)},
+					"Range":          {fmt.Sprintf("bytes=0-%d", blobLen4-1)},
+					"Location":       {uuid4.String() + "?chunk=3"},
+				},
+			},
+		},
+		// upload patch 1 for d4
+		{
+			ReqEntry: reqresp.ReqEntry{
+				DelOnUse: false,
+				Name:     "PATCH 1 for d4",
+				Method:   "PATCH",
+				Path:     "/v2" + blobRepo + "/blobs/uploads/" + uuid4.String(),
+				Query:    map[string][]string{},
+				Headers: http.Header{
+					"Content-Length": {fmt.Sprintf("%d", blobLen4/2)},
+					"Content-Range":  {fmt.Sprintf("0-%d", blobLen4/2-1)},
+					"Content-Type":   {"application/octet-stream"},
+				},
+				Body: blob4[0 : blobLen4/2],
+			},
+			RespEntry: reqresp.RespEntry{
+				Status: http.StatusAccepted,
+				Headers: http.Header{
+					"Content-Length": {fmt.Sprintf("%d", 0)},
+					"Range":          {fmt.Sprintf("bytes=0-%d", blobLen4/2-1)},
+					"Location":       {uuid4.String() + "?chunk=2"},
+				},
+			},
+		},
 	}
 	rrs = append(rrs, reqresp.BaseEntries...)
 	// create a server
@@ -840,6 +890,15 @@ func TestBlobPut(t *testing.T) {
 			User:      user,
 			Pass:      pass,
 		},
+		{
+			Name:      "chunked." + tsHost,
+			Hostname:  tsHost,
+			TLS:       config.TLSDisabled,
+			BlobChunk: int64(blobChunk),
+			BlobMax:   int64(blobChunk * 3),
+			User:      user,
+			Pass:      pass,
+		},
 		{
 			Name:      "retry." + tsHost,
 			Hostname:  tsHost,
@@ -923,4 +982,24 @@ func TestBlobPut(t *testing.T) {
 
 	})
 
+	t.Run("Chunk resized", func(t *testing.T) {
+		r, err := ref.New("chunked." + tsURL.Host + blobRepo)
+		if err != nil {
+			t.Errorf("Failed creating ref: %v", err)
+		}
+		br := bytes.NewReader(blob4)
+		dp, err := reg.BlobPut(ctx, r, types.Descriptor{Digest: d4, Size: int64(len(blob4))}, br)
+		if err != nil {
+			t.Errorf("Failed running BlobPut: %v", err)
+			return
+		}
+		if dp.Digest.String() != d4.String() {
+			t.Errorf("Digest mismatch, expected %s, received %s", d4.String(), dp.Digest.String())
+		}
+		if dp.Size != int64(len(blob4)) {
+			t.Errorf("Content length mismatch, expected %d, received %d", len(blob4), dp.Size)
+		}
+	})
+
+	// TODO: test failed mount (blobGetUploadURL)
 }