Browse code

Merge pull request #9784 from dmcgowan/v2-registry

Client Support for Docker Registry HTTP API V2

Jessie Frazelle authored on 2015/01/20 03:46:38
Showing 39 changed files
... ...
@@ -148,6 +148,17 @@ RUN set -x \
148 148
 	&& git clone -b v1.2 https://github.com/russross/blackfriday.git /go/src/github.com/russross/blackfriday \
149 149
 	&& go install -v github.com/cpuguy83/go-md2man
150 150
 
151
+# Install registry
152
+COPY pkg/tarsum /go/src/github.com/docker/docker/pkg/tarsum
153
+# REGISTRY_COMMIT gives us the repeatability guarantees we need
154
+# (so that we're all testing the same version of the registry)
155
+ENV REGISTRY_COMMIT 21a69f53b5c7986b831f33849d551cd59ec8cbd1
156
+RUN set -x \
157
+	&& git clone https://github.com/docker/distribution.git /go/src/github.com/docker/distribution \
158
+	&& (cd /go/src/github.com/docker/distribution && git checkout -q $REGISTRY_COMMIT) \
159
+	&& go get -d github.com/docker/distribution/cmd/registry \
160
+	&& go build -o /go/bin/registry-v2 github.com/docker/distribution/cmd/registry
161
+
151 162
 # Wrap all commands in the "docker-in-docker" script to allow nested containers
152 163
 ENTRYPOINT ["hack/dind"]
153 164
 
... ...
@@ -43,6 +43,7 @@ import (
43 43
 	"github.com/docker/docker/registry"
44 44
 	"github.com/docker/docker/runconfig"
45 45
 	"github.com/docker/docker/utils"
46
+	"github.com/docker/libtrust"
46 47
 )
47 48
 
48 49
 const (
... ...
@@ -1215,6 +1216,26 @@ func (cli *DockerCli) CmdPush(args ...string) error {
1215 1215
 
1216 1216
 	v := url.Values{}
1217 1217
 	v.Set("tag", tag)
1218
+
1219
+	body, _, err := readBody(cli.call("GET", "/images/"+remote+"/manifest?"+v.Encode(), nil, false))
1220
+	if err != nil {
1221
+		return err
1222
+	}
1223
+
1224
+	js, err := libtrust.NewJSONSignature(body)
1225
+	if err != nil {
1226
+		return err
1227
+	}
1228
+	err = js.Sign(cli.key)
1229
+	if err != nil {
1230
+		return err
1231
+	}
1232
+
1233
+	signedBody, err := js.PrettySignature("signatures")
1234
+	if err != nil {
1235
+		return err
1236
+	}
1237
+
1218 1238
 	push := func(authConfig registry.AuthConfig) error {
1219 1239
 		buf, err := json.Marshal(authConfig)
1220 1240
 		if err != nil {
... ...
@@ -1224,7 +1245,7 @@ func (cli *DockerCli) CmdPush(args ...string) error {
1224 1224
 			base64.URLEncoding.EncodeToString(buf),
1225 1225
 		}
1226 1226
 
1227
-		return cli.stream("POST", "/images/"+remote+"/push?"+v.Encode(), nil, cli.out, map[string][]string{
1227
+		return cli.stream("POST", "/images/"+remote+"/push?"+v.Encode(), bytes.NewReader(signedBody), cli.out, map[string][]string{
1228 1228
 			"X-Registry-Auth": registryAuthHeader,
1229 1229
 		})
1230 1230
 	}
... ...
@@ -608,6 +608,18 @@ func getImagesSearch(eng *engine.Engine, version version.Version, w http.Respons
608 608
 	return job.Run()
609 609
 }
610 610
 
611
+func getImageManifest(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
612
+	if err := parseForm(r); err != nil {
613
+		return err
614
+	}
615
+
616
+	job := eng.Job("image_manifest", vars["name"])
617
+	job.Setenv("tag", r.Form.Get("tag"))
618
+	job.Stdout.Add(utils.NewWriteFlusher(w))
619
+
620
+	return job.Run()
621
+}
622
+
611 623
 func postImagesPush(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
612 624
 	if vars == nil {
613 625
 		return fmt.Errorf("Missing parameter")
... ...
@@ -639,9 +651,15 @@ func postImagesPush(eng *engine.Engine, version version.Version, w http.Response
639 639
 		}
640 640
 	}
641 641
 
642
+	manifest, err := ioutil.ReadAll(r.Body)
643
+	if err != nil {
644
+		return err
645
+	}
646
+
642 647
 	job := eng.Job("push", vars["name"])
643 648
 	job.SetenvJson("metaHeaders", metaHeaders)
644 649
 	job.SetenvJson("authConfig", authConfig)
650
+	job.Setenv("manifest", string(manifest))
645 651
 	job.Setenv("tag", r.Form.Get("tag"))
646 652
 	if version.GreaterThan("1.0") {
647 653
 		job.SetenvBool("json", true)
... ...
@@ -1294,6 +1312,7 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st
1294 1294
 			"/images/viz":                     getImagesViz,
1295 1295
 			"/images/search":                  getImagesSearch,
1296 1296
 			"/images/get":                     getImagesGet,
1297
+			"/images/{name:.*}/manifest":      getImageManifest,
1297 1298
 			"/images/{name:.*}/get":           getImagesGet,
1298 1299
 			"/images/{name:.*}/history":       getImagesHistory,
1299 1300
 			"/images/{name:.*}/json":          getImagesByName,
... ...
@@ -895,8 +895,13 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error)
895 895
 		return nil, err
896 896
 	}
897 897
 
898
+	trustKey, err := api.LoadOrCreateTrustKey(config.TrustKeyPath)
899
+	if err != nil {
900
+		return nil, err
901
+	}
902
+
898 903
 	log.Debugf("Creating repository list")
899
-	repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g)
904
+	repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g, trustKey)
900 905
 	if err != nil {
901 906
 		return nil, fmt.Errorf("Couldn't create Tag store: %s", err)
902 907
 	}
... ...
@@ -961,11 +966,6 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error)
961 961
 		return nil, err
962 962
 	}
963 963
 
964
-	trustKey, err := api.LoadOrCreateTrustKey(config.TrustKeyPath)
965
-	if err != nil {
966
-		return nil, err
967
-	}
968
-
969 964
 	daemon := &Daemon{
970 965
 		ID:             trustKey.PublicKey().KeyID(),
971 966
 		repository:     daemonRepo,
... ...
@@ -77,6 +77,11 @@ func main() {
77 77
 	}
78 78
 	protoAddrParts := strings.SplitN(flHosts[0], "://", 2)
79 79
 
80
+	trustKey, err := api.LoadOrCreateTrustKey(*flTrustKey)
81
+	if err != nil {
82
+		log.Fatal(err)
83
+	}
84
+
80 85
 	var (
81 86
 		cli       *client.DockerCli
82 87
 		tlsConfig tls.Config
... ...
@@ -118,9 +123,9 @@ func main() {
118 118
 	}
119 119
 
120 120
 	if *flTls || *flTlsVerify {
121
-		cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, nil, protoAddrParts[0], protoAddrParts[1], &tlsConfig)
121
+		cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, trustKey, protoAddrParts[0], protoAddrParts[1], &tlsConfig)
122 122
 	} else {
123
-		cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, nil, protoAddrParts[0], protoAddrParts[1], nil)
123
+		cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, trustKey, protoAddrParts[0], protoAddrParts[1], nil)
124 124
 	}
125 125
 
126 126
 	if err := cli.Cmd(flag.Args()...); err != nil {
127 127
new file mode 100644
... ...
@@ -0,0 +1,196 @@
0
+package graph
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"errors"
6
+	"fmt"
7
+	"io"
8
+	"io/ioutil"
9
+	"path"
10
+
11
+	log "github.com/Sirupsen/logrus"
12
+	"github.com/docker/docker/engine"
13
+	"github.com/docker/docker/pkg/tarsum"
14
+	"github.com/docker/docker/registry"
15
+	"github.com/docker/docker/runconfig"
16
+	"github.com/docker/libtrust"
17
+)
18
+
19
+func (s *TagStore) CmdManifest(job *engine.Job) engine.Status {
20
+	if len(job.Args) != 1 {
21
+		return job.Errorf("usage: %s NAME", job.Name)
22
+	}
23
+	name := job.Args[0]
24
+	tag := job.Getenv("tag")
25
+	if tag == "" {
26
+		tag = "latest"
27
+	}
28
+
29
+	// Resolve the Repository name from fqn to endpoint + name
30
+	repoInfo, err := registry.ParseRepositoryInfo(name)
31
+	if err != nil {
32
+		return job.Error(err)
33
+	}
34
+
35
+	manifestBytes, err := s.newManifest(name, repoInfo.RemoteName, tag)
36
+	if err != nil {
37
+		return job.Error(err)
38
+	}
39
+
40
+	_, err = job.Stdout.Write(manifestBytes)
41
+	if err != nil {
42
+		return job.Error(err)
43
+	}
44
+
45
+	return engine.StatusOK
46
+}
47
+
48
+func (s *TagStore) newManifest(localName, remoteName, tag string) ([]byte, error) {
49
+	manifest := &registry.ManifestData{
50
+		Name:          remoteName,
51
+		Tag:           tag,
52
+		SchemaVersion: 1,
53
+	}
54
+	localRepo, err := s.Get(localName)
55
+	if err != nil {
56
+		return nil, err
57
+	}
58
+	if localRepo == nil {
59
+		return nil, fmt.Errorf("Repo does not exist: %s", localName)
60
+	}
61
+
62
+	// Get the top-most layer id which the tag points to
63
+	layerId, exists := localRepo[tag]
64
+	if !exists {
65
+		return nil, fmt.Errorf("Tag does not exist for %s: %s", localName, tag)
66
+	}
67
+	layersSeen := make(map[string]bool)
68
+
69
+	layer, err := s.graph.Get(layerId)
70
+	if err != nil {
71
+		return nil, err
72
+	}
73
+	if layer.Config == nil {
74
+		return nil, errors.New("Missing layer configuration")
75
+	}
76
+	manifest.Architecture = layer.Architecture
77
+	manifest.FSLayers = make([]*registry.FSLayer, 0, 4)
78
+	manifest.History = make([]*registry.ManifestHistory, 0, 4)
79
+	var metadata runconfig.Config
80
+	metadata = *layer.Config
81
+
82
+	for ; layer != nil; layer, err = layer.GetParent() {
83
+		if err != nil {
84
+			return nil, err
85
+		}
86
+
87
+		if layersSeen[layer.ID] {
88
+			break
89
+		}
90
+		if layer.Config != nil && metadata.Image != layer.ID {
91
+			err = runconfig.Merge(&metadata, layer.Config)
92
+			if err != nil {
93
+				return nil, err
94
+			}
95
+		}
96
+
97
+		archive, err := layer.TarLayer()
98
+		if err != nil {
99
+			return nil, err
100
+		}
101
+
102
+		tarSum, err := tarsum.NewTarSum(archive, true, tarsum.Version1)
103
+		if err != nil {
104
+			return nil, err
105
+		}
106
+		if _, err := io.Copy(ioutil.Discard, tarSum); err != nil {
107
+			return nil, err
108
+		}
109
+
110
+		tarId := tarSum.Sum(nil)
111
+
112
+		manifest.FSLayers = append(manifest.FSLayers, &registry.FSLayer{BlobSum: tarId})
113
+
114
+		layersSeen[layer.ID] = true
115
+		jsonData, err := ioutil.ReadFile(path.Join(s.graph.Root, layer.ID, "json"))
116
+		if err != nil {
117
+			return nil, fmt.Errorf("Cannot retrieve the path for {%s}: %s", layer.ID, err)
118
+		}
119
+		manifest.History = append(manifest.History, &registry.ManifestHistory{V1Compatibility: string(jsonData)})
120
+	}
121
+
122
+	manifestBytes, err := json.MarshalIndent(manifest, "", "   ")
123
+	if err != nil {
124
+		return nil, err
125
+	}
126
+
127
+	return manifestBytes, nil
128
+}
129
+
130
+func (s *TagStore) verifyManifest(eng *engine.Engine, manifestBytes []byte) (*registry.ManifestData, bool, error) {
131
+	sig, err := libtrust.ParsePrettySignature(manifestBytes, "signatures")
132
+	if err != nil {
133
+		return nil, false, fmt.Errorf("error parsing payload: %s", err)
134
+	}
135
+
136
+	keys, err := sig.Verify()
137
+	if err != nil {
138
+		return nil, false, fmt.Errorf("error verifying payload: %s", err)
139
+	}
140
+
141
+	payload, err := sig.Payload()
142
+	if err != nil {
143
+		return nil, false, fmt.Errorf("error retrieving payload: %s", err)
144
+	}
145
+
146
+	var manifest registry.ManifestData
147
+	if err := json.Unmarshal(payload, &manifest); err != nil {
148
+		return nil, false, fmt.Errorf("error unmarshalling manifest: %s", err)
149
+	}
150
+	if manifest.SchemaVersion != 1 {
151
+		return nil, false, fmt.Errorf("unsupported schema version: %d", manifest.SchemaVersion)
152
+	}
153
+
154
+	var verified bool
155
+	for _, key := range keys {
156
+		job := eng.Job("trust_key_check")
157
+		b, err := key.MarshalJSON()
158
+		if err != nil {
159
+			return nil, false, fmt.Errorf("error marshalling public key: %s", err)
160
+		}
161
+		namespace := manifest.Name
162
+		if namespace[0] != '/' {
163
+			namespace = "/" + namespace
164
+		}
165
+		stdoutBuffer := bytes.NewBuffer(nil)
166
+
167
+		job.Args = append(job.Args, namespace)
168
+		job.Setenv("PublicKey", string(b))
169
+		// Check key has read/write permission (0x03)
170
+		job.SetenvInt("Permission", 0x03)
171
+		job.Stdout.Add(stdoutBuffer)
172
+		if err = job.Run(); err != nil {
173
+			return nil, false, fmt.Errorf("error running key check: %s", err)
174
+		}
175
+		result := engine.Tail(stdoutBuffer, 1)
176
+		log.Debugf("Key check result: %q", result)
177
+		if result == "verified" {
178
+			verified = true
179
+		}
180
+	}
181
+
182
+	return &manifest, verified, nil
183
+}
184
+
185
+func checkValidManifest(manifest *registry.ManifestData) error {
186
+	if len(manifest.FSLayers) != len(manifest.History) {
187
+		return fmt.Errorf("length of history not equal to number of layers")
188
+	}
189
+
190
+	if len(manifest.FSLayers) == 0 {
191
+		return fmt.Errorf("no FSLayers in manifest")
192
+	}
193
+
194
+	return nil
195
+}
... ...
@@ -1,8 +1,6 @@
1 1
 package graph
2 2
 
3 3
 import (
4
-	"bytes"
5
-	"encoding/json"
6 4
 	"fmt"
7 5
 	"io"
8 6
 	"io/ioutil"
... ...
@@ -15,65 +13,11 @@ import (
15 15
 	log "github.com/Sirupsen/logrus"
16 16
 	"github.com/docker/docker/engine"
17 17
 	"github.com/docker/docker/image"
18
+	"github.com/docker/docker/pkg/tarsum"
18 19
 	"github.com/docker/docker/registry"
19 20
 	"github.com/docker/docker/utils"
20
-	"github.com/docker/libtrust"
21 21
 )
22 22
 
23
-func (s *TagStore) verifyManifest(eng *engine.Engine, manifestBytes []byte) (*registry.ManifestData, bool, error) {
24
-	sig, err := libtrust.ParsePrettySignature(manifestBytes, "signatures")
25
-	if err != nil {
26
-		return nil, false, fmt.Errorf("error parsing payload: %s", err)
27
-	}
28
-	keys, err := sig.Verify()
29
-	if err != nil {
30
-		return nil, false, fmt.Errorf("error verifying payload: %s", err)
31
-	}
32
-
33
-	payload, err := sig.Payload()
34
-	if err != nil {
35
-		return nil, false, fmt.Errorf("error retrieving payload: %s", err)
36
-	}
37
-
38
-	var manifest registry.ManifestData
39
-	if err := json.Unmarshal(payload, &manifest); err != nil {
40
-		return nil, false, fmt.Errorf("error unmarshalling manifest: %s", err)
41
-	}
42
-	if manifest.SchemaVersion != 1 {
43
-		return nil, false, fmt.Errorf("unsupported schema version: %d", manifest.SchemaVersion)
44
-	}
45
-
46
-	var verified bool
47
-	for _, key := range keys {
48
-		job := eng.Job("trust_key_check")
49
-		b, err := key.MarshalJSON()
50
-		if err != nil {
51
-			return nil, false, fmt.Errorf("error marshalling public key: %s", err)
52
-		}
53
-		namespace := manifest.Name
54
-		if namespace[0] != '/' {
55
-			namespace = "/" + namespace
56
-		}
57
-		stdoutBuffer := bytes.NewBuffer(nil)
58
-
59
-		job.Args = append(job.Args, namespace)
60
-		job.Setenv("PublicKey", string(b))
61
-		// Check key has read/write permission (0x03)
62
-		job.SetenvInt("Permission", 0x03)
63
-		job.Stdout.Add(stdoutBuffer)
64
-		if err = job.Run(); err != nil {
65
-			return nil, false, fmt.Errorf("error running key check: %s", err)
66
-		}
67
-		result := engine.Tail(stdoutBuffer, 1)
68
-		log.Debugf("Key check result: %q", result)
69
-		if result == "verified" {
70
-			verified = true
71
-		}
72
-	}
73
-
74
-	return &manifest, verified, nil
75
-}
76
-
77 23
 func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
78 24
 	if n := len(job.Args); n != 1 && n != 2 {
79 25
 		return job.Errorf("Usage: %s IMAGE [TAG]", job.Name)
... ...
@@ -112,6 +56,7 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
112 112
 	}
113 113
 	defer s.poolRemove("pull", repoInfo.LocalName+":"+tag)
114 114
 
115
+	log.Debugf("pulling image from host %q with remote name %q", repoInfo.Index.Name, repoInfo.RemoteName)
115 116
 	endpoint, err := repoInfo.GetEndpoint()
116 117
 	if err != nil {
117 118
 		return job.Error(err)
... ...
@@ -127,12 +72,13 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
127 127
 		logName += ":" + tag
128 128
 	}
129 129
 
130
-	if len(repoInfo.Index.Mirrors) == 0 && (repoInfo.Official || endpoint.Version == registry.APIVersion2) {
130
+	if len(repoInfo.Index.Mirrors) == 0 && (repoInfo.Index.Official || endpoint.Version == registry.APIVersion2) {
131 131
 		j := job.Eng.Job("trust_update_base")
132 132
 		if err = j.Run(); err != nil {
133
-			return job.Errorf("error updating trust base graph: %s", err)
133
+			log.Errorf("error updating trust base graph: %s", err)
134 134
 		}
135 135
 
136
+		log.Debugf("pulling v2 repository with local name %q", repoInfo.LocalName)
136 137
 		if err := s.pullV2Repository(job.Eng, r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel")); err == nil {
137 138
 			if err = job.Eng.Job("log", "pull", logName, "").Run(); err != nil {
138 139
 				log.Errorf("Error logging event 'pull' for %s: %s", logName, err)
... ...
@@ -141,8 +87,11 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
141 141
 		} else if err != registry.ErrDoesNotExist {
142 142
 			log.Errorf("Error from V2 registry: %s", err)
143 143
 		}
144
+
145
+		log.Debug("image does not exist on v2 registry, falling back to v1")
144 146
 	}
145 147
 
148
+	log.Debugf("pulling v1 repository with local name %q", repoInfo.LocalName)
146 149
 	if err = s.pullRepository(r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel")); err != nil {
147 150
 		return job.Error(err)
148 151
 	}
... ...
@@ -169,7 +118,7 @@ func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, repoInfo *
169 169
 	log.Debugf("Retrieving the tag list")
170 170
 	tagsList, err := r.GetRemoteTags(repoData.Endpoints, repoInfo.RemoteName, repoData.Tokens)
171 171
 	if err != nil {
172
-		log.Errorf("%v", err)
172
+		log.Errorf("unable to get remote tags: %s", err)
173 173
 		return err
174 174
 	}
175 175
 
... ...
@@ -424,22 +373,30 @@ type downloadInfo struct {
424 424
 }
425 425
 
426 426
 func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool) error {
427
+	endpoint, err := r.V2RegistryEndpoint(repoInfo.Index)
428
+	if err != nil {
429
+		return fmt.Errorf("error getting registry endpoint: %s", err)
430
+	}
431
+	auth, err := r.GetV2Authorization(endpoint, repoInfo.RemoteName, true)
432
+	if err != nil {
433
+		return fmt.Errorf("error getting authorization: %s", err)
434
+	}
427 435
 	var layersDownloaded bool
428 436
 	if tag == "" {
429 437
 		log.Debugf("Pulling tag list from V2 registry for %s", repoInfo.CanonicalName)
430
-		tags, err := r.GetV2RemoteTags(repoInfo.RemoteName, nil)
438
+		tags, err := r.GetV2RemoteTags(endpoint, repoInfo.RemoteName, auth)
431 439
 		if err != nil {
432 440
 			return err
433 441
 		}
434 442
 		for _, t := range tags {
435
-			if downloaded, err := s.pullV2Tag(eng, r, out, repoInfo, t, sf, parallel); err != nil {
443
+			if downloaded, err := s.pullV2Tag(eng, r, out, endpoint, repoInfo, t, sf, parallel, auth); err != nil {
436 444
 				return err
437 445
 			} else if downloaded {
438 446
 				layersDownloaded = true
439 447
 			}
440 448
 		}
441 449
 	} else {
442
-		if downloaded, err := s.pullV2Tag(eng, r, out, repoInfo, tag, sf, parallel); err != nil {
450
+		if downloaded, err := s.pullV2Tag(eng, r, out, endpoint, repoInfo, tag, sf, parallel, auth); err != nil {
443 451
 			return err
444 452
 		} else if downloaded {
445 453
 			layersDownloaded = true
... ...
@@ -454,9 +411,9 @@ func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out
454 454
 	return nil
455 455
 }
456 456
 
457
-func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool) (bool, error) {
457
+func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Writer, endpoint *registry.Endpoint, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool, auth *registry.RequestAuthorization) (bool, error) {
458 458
 	log.Debugf("Pulling tag from V2 registry: %q", tag)
459
-	manifestBytes, err := r.GetV2ImageManifest(repoInfo.RemoteName, tag, nil)
459
+	manifestBytes, err := r.GetV2ImageManifest(endpoint, repoInfo.RemoteName, tag, auth)
460 460
 	if err != nil {
461 461
 		return false, err
462 462
 	}
... ...
@@ -466,8 +423,8 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri
466 466
 		return false, fmt.Errorf("error verifying manifest: %s", err)
467 467
 	}
468 468
 
469
-	if len(manifest.FSLayers) != len(manifest.History) {
470
-		return false, fmt.Errorf("length of history not equal to number of layers")
469
+	if err := checkValidManifest(manifest); err != nil {
470
+		return false, err
471 471
 	}
472 472
 
473 473
 	if verified {
... ...
@@ -475,11 +432,6 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri
475 475
 	} else {
476 476
 		out.Write(sf.FormatStatus(tag, "Pulling from %s", repoInfo.CanonicalName))
477 477
 	}
478
-
479
-	if len(manifest.FSLayers) == 0 {
480
-		return false, fmt.Errorf("no blobSums in manifest")
481
-	}
482
-
483 478
 	downloads := make([]downloadInfo, len(manifest.FSLayers))
484 479
 
485 480
 	for i := len(manifest.FSLayers) - 1; i >= 0; i-- {
... ...
@@ -525,12 +477,25 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri
525 525
 					return err
526 526
 				}
527 527
 
528
-				r, l, err := r.GetV2ImageBlobReader(repoInfo.RemoteName, sumType, checksum, nil)
528
+				r, l, err := r.GetV2ImageBlobReader(endpoint, repoInfo.RemoteName, sumType, checksum, auth)
529 529
 				if err != nil {
530 530
 					return err
531 531
 				}
532 532
 				defer r.Close()
533
-				io.Copy(tmpFile, utils.ProgressReader(r, int(l), out, sf, false, utils.TruncateID(img.ID), "Downloading"))
533
+
534
+				// Wrap the reader with the appropriate TarSum reader.
535
+				tarSumReader, err := tarsum.NewTarSumForLabel(r, true, sumType)
536
+				if err != nil {
537
+					return fmt.Errorf("unable to wrap image blob reader with TarSum: %s", err)
538
+				}
539
+
540
+				io.Copy(tmpFile, utils.ProgressReader(ioutil.NopCloser(tarSumReader), int(l), out, sf, false, utils.TruncateID(img.ID), "Downloading"))
541
+
542
+				out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Verifying Checksum", nil))
543
+
544
+				if finalChecksum := tarSumReader.Sum(nil); !strings.EqualFold(finalChecksum, sumStr) {
545
+					return fmt.Errorf("image verification failed: checksum mismatch - expected %q but got %q", sumStr, finalChecksum)
546
+				}
534 547
 
535 548
 				out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Download complete", nil))
536 549
 
... ...
@@ -1,18 +1,22 @@
1 1
 package graph
2 2
 
3 3
 import (
4
+	"bytes"
4 5
 	"fmt"
5 6
 	"io"
6 7
 	"io/ioutil"
7 8
 	"os"
8 9
 	"path"
10
+	"strings"
9 11
 	"sync"
10 12
 
11 13
 	log "github.com/Sirupsen/logrus"
12 14
 	"github.com/docker/docker/engine"
15
+	"github.com/docker/docker/image"
13 16
 	"github.com/docker/docker/pkg/archive"
14 17
 	"github.com/docker/docker/registry"
15 18
 	"github.com/docker/docker/utils"
19
+	"github.com/docker/libtrust"
16 20
 )
17 21
 
18 22
 // Retrieve the all the images to be uploaded in the correct order
... ...
@@ -248,6 +252,109 @@ func (s *TagStore) pushImage(r *registry.Session, out io.Writer, imgID, ep strin
248 248
 	return imgData.Checksum, nil
249 249
 }
250 250
 
251
+func (s *TagStore) pushV2Repository(r *registry.Session, eng *engine.Engine, out io.Writer, repoInfo *registry.RepositoryInfo, manifestBytes, tag string, sf *utils.StreamFormatter) error {
252
+	if repoInfo.Official {
253
+		j := eng.Job("trust_update_base")
254
+		if err := j.Run(); err != nil {
255
+			log.Errorf("error updating trust base graph: %s", err)
256
+		}
257
+	}
258
+
259
+	endpoint, err := r.V2RegistryEndpoint(repoInfo.Index)
260
+	if err != nil {
261
+		return fmt.Errorf("error getting registry endpoint: %s", err)
262
+	}
263
+	auth, err := r.GetV2Authorization(endpoint, repoInfo.RemoteName, false)
264
+	if err != nil {
265
+		return fmt.Errorf("error getting authorization: %s", err)
266
+	}
267
+
268
+	// if no manifest is given, generate and sign with the key associated with the local tag store
269
+	if len(manifestBytes) == 0 {
270
+		mBytes, err := s.newManifest(repoInfo.LocalName, repoInfo.RemoteName, tag)
271
+		if err != nil {
272
+			return err
273
+		}
274
+		js, err := libtrust.NewJSONSignature(mBytes)
275
+		if err != nil {
276
+			return err
277
+		}
278
+
279
+		if err = js.Sign(s.trustKey); err != nil {
280
+			return err
281
+		}
282
+
283
+		signedBody, err := js.PrettySignature("signatures")
284
+		if err != nil {
285
+			return err
286
+		}
287
+		log.Infof("Signed manifest using daemon's key: %s", s.trustKey.KeyID())
288
+
289
+		manifestBytes = string(signedBody)
290
+	}
291
+
292
+	manifest, verified, err := s.verifyManifest(eng, []byte(manifestBytes))
293
+	if err != nil {
294
+		return fmt.Errorf("error verifying manifest: %s", err)
295
+	}
296
+
297
+	if err := checkValidManifest(manifest); err != nil {
298
+		return fmt.Errorf("invalid manifest: %s", err)
299
+	}
300
+
301
+	if !verified {
302
+		log.Debugf("Pushing unverified image")
303
+	}
304
+
305
+	for i := len(manifest.FSLayers) - 1; i >= 0; i-- {
306
+		var (
307
+			sumStr  = manifest.FSLayers[i].BlobSum
308
+			imgJSON = []byte(manifest.History[i].V1Compatibility)
309
+		)
310
+
311
+		sumParts := strings.SplitN(sumStr, ":", 2)
312
+		if len(sumParts) < 2 {
313
+			return fmt.Errorf("Invalid checksum: %s", sumStr)
314
+		}
315
+		manifestSum := sumParts[1]
316
+
317
+		img, err := image.NewImgJSON(imgJSON)
318
+		if err != nil {
319
+			return fmt.Errorf("Failed to parse json: %s", err)
320
+		}
321
+
322
+		img, err = s.graph.Get(img.ID)
323
+		if err != nil {
324
+			return err
325
+		}
326
+
327
+		arch, err := img.TarLayer()
328
+		if err != nil {
329
+			return fmt.Errorf("Could not get tar layer: %s", err)
330
+		}
331
+
332
+		// Call mount blob
333
+		exists, err := r.HeadV2ImageBlob(endpoint, repoInfo.RemoteName, sumParts[0], manifestSum, auth)
334
+		if err != nil {
335
+			out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil))
336
+			return err
337
+		}
338
+		if !exists {
339
+			err = r.PutV2ImageBlob(endpoint, repoInfo.RemoteName, sumParts[0], manifestSum, utils.ProgressReader(arch, int(img.Size), out, sf, false, utils.TruncateID(img.ID), "Pushing"), auth)
340
+			if err != nil {
341
+				out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil))
342
+				return err
343
+			}
344
+			out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image successfully pushed", nil))
345
+		} else {
346
+			out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image already exists", nil))
347
+		}
348
+	}
349
+
350
+	// push the manifest
351
+	return r.PutV2ImageManifest(endpoint, repoInfo.RemoteName, tag, bytes.NewReader([]byte(manifestBytes)), auth)
352
+}
353
+
251 354
 // FIXME: Allow to interrupt current push when new push of same image is done.
252 355
 func (s *TagStore) CmdPush(job *engine.Job) engine.Status {
253 356
 	if n := len(job.Args); n != 1 {
... ...
@@ -267,6 +374,7 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status {
267 267
 	}
268 268
 
269 269
 	tag := job.Getenv("tag")
270
+	manifestBytes := job.Getenv("manifest")
270 271
 	job.GetenvJson("authConfig", authConfig)
271 272
 	job.GetenvJson("metaHeaders", &metaHeaders)
272 273
 
... ...
@@ -286,6 +394,20 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status {
286 286
 		return job.Error(err2)
287 287
 	}
288 288
 
289
+	if len(tag) == 0 {
290
+		tag = DEFAULTTAG
291
+	}
292
+
293
+	if repoInfo.Index.Official || endpoint.Version == registry.APIVersion2 {
294
+		err := s.pushV2Repository(r, job.Eng, job.Stdout, repoInfo, manifestBytes, tag, sf)
295
+		if err == nil {
296
+			return engine.StatusOK
297
+		}
298
+
299
+		// error out, no fallback to V1
300
+		return job.Error(err)
301
+	}
302
+
289 303
 	if err != nil {
290 304
 		reposLen := 1
291 305
 		if tag == "" {
... ...
@@ -25,6 +25,7 @@ func (s *TagStore) Install(eng *engine.Engine) error {
25 25
 		"import":         s.CmdImport,
26 26
 		"pull":           s.CmdPull,
27 27
 		"push":           s.CmdPush,
28
+		"image_manifest": s.CmdManifest,
28 29
 	} {
29 30
 		if err := eng.Register(name, handler); err != nil {
30 31
 			return fmt.Errorf("Could not register %q: %v", name, err)
... ...
@@ -15,6 +15,7 @@ import (
15 15
 	"github.com/docker/docker/pkg/parsers"
16 16
 	"github.com/docker/docker/registry"
17 17
 	"github.com/docker/docker/utils"
18
+	"github.com/docker/libtrust"
18 19
 )
19 20
 
20 21
 const DEFAULTTAG = "latest"
... ...
@@ -27,6 +28,7 @@ type TagStore struct {
27 27
 	path         string
28 28
 	graph        *Graph
29 29
 	Repositories map[string]Repository
30
+	trustKey     libtrust.PrivateKey
30 31
 	sync.Mutex
31 32
 	// FIXME: move push/pull-related fields
32 33
 	// to a helper type
... ...
@@ -54,7 +56,7 @@ func (r Repository) Contains(u Repository) bool {
54 54
 	return true
55 55
 }
56 56
 
57
-func NewTagStore(path string, graph *Graph) (*TagStore, error) {
57
+func NewTagStore(path string, graph *Graph, key libtrust.PrivateKey) (*TagStore, error) {
58 58
 	abspath, err := filepath.Abs(path)
59 59
 	if err != nil {
60 60
 		return nil, err
... ...
@@ -63,6 +65,7 @@ func NewTagStore(path string, graph *Graph) (*TagStore, error) {
63 63
 	store := &TagStore{
64 64
 		path:         abspath,
65 65
 		graph:        graph,
66
+		trustKey:     key,
66 67
 		Repositories: make(map[string]Repository),
67 68
 		pullingPool:  make(map[string]chan struct{}),
68 69
 		pushingPool:  make(map[string]chan struct{}),
... ...
@@ -57,7 +57,7 @@ func mkTestTagStore(root string, t *testing.T) *TagStore {
57 57
 	if err != nil {
58 58
 		t.Fatal(err)
59 59
 	}
60
-	store, err := NewTagStore(path.Join(root, "tags"), graph)
60
+	store, err := NewTagStore(path.Join(root, "tags"), graph, nil)
61 61
 	if err != nil {
62 62
 		t.Fatal(err)
63 63
 	}
... ...
@@ -94,28 +94,29 @@ func StoreImage(img *Image, layerData archive.ArchiveReader, root string) error
94 94
 
95 95
 	// If layerData is not nil, unpack it into the new layer
96 96
 	if layerData != nil {
97
-		layerDataDecompressed, err := archive.DecompressStream(layerData)
98
-		if err != nil {
99
-			return err
100
-		}
97
+		// If the image doesn't have a checksum, we should add it. The layer
98
+		// checksums are verified when they are pulled from a remote, but when
99
+		// a container is committed it should be added here.
100
+		if img.Checksum == "" {
101
+			layerDataDecompressed, err := archive.DecompressStream(layerData)
102
+			if err != nil {
103
+				return err
104
+			}
105
+			defer layerDataDecompressed.Close()
101 106
 
102
-		defer layerDataDecompressed.Close()
107
+			if layerTarSum, err = tarsum.NewTarSum(layerDataDecompressed, true, tarsum.VersionDev); err != nil {
108
+				return err
109
+			}
103 110
 
104
-		if layerTarSum, err = tarsum.NewTarSum(layerDataDecompressed, true, tarsum.VersionDev); err != nil {
105
-			return err
106
-		}
111
+			if size, err = driver.ApplyDiff(img.ID, img.Parent, layerTarSum); err != nil {
112
+				return err
113
+			}
107 114
 
108
-		if size, err = driver.ApplyDiff(img.ID, img.Parent, layerTarSum); err != nil {
115
+			img.Checksum = layerTarSum.Sum(nil)
116
+		} else if size, err = driver.ApplyDiff(img.ID, img.Parent, layerData); err != nil {
109 117
 			return err
110 118
 		}
111 119
 
112
-		checksum := layerTarSum.Sum(nil)
113
-
114
-		if img.Checksum != "" && img.Checksum != checksum {
115
-			log.Warnf("image layer checksum mismatch: computed %q, expected %q", checksum, img.Checksum)
116
-		}
117
-
118
-		img.Checksum = checksum
119 120
 	}
120 121
 
121 122
 	img.Size = size
... ...
@@ -1,12 +1,57 @@
1 1
 package main
2 2
 
3 3
 import (
4
+	"fmt"
4 5
 	"os/exec"
5 6
 	"strings"
6 7
 	"testing"
7 8
 )
8 9
 
9
-// FIXME: we need a test for pulling all aliases for an image (issue #8141)
10
+// See issue docker/docker#8141
11
+func TestPullImageWithAliases(t *testing.T) {
12
+	defer setupRegistry(t)()
13
+
14
+	repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
15
+	defer deleteImages(repoName)
16
+
17
+	repos := []string{}
18
+	for _, tag := range []string{"recent", "fresh"} {
19
+		repos = append(repos, fmt.Sprintf("%v:%v", repoName, tag))
20
+	}
21
+
22
+	// Tag and push the same image multiple times.
23
+	for _, repo := range repos {
24
+		if out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "tag", "busybox", repo)); err != nil {
25
+			t.Fatalf("Failed to tag image %v: error %v, output %q", repos, err, out)
26
+		}
27
+		if out, err := exec.Command(dockerBinary, "push", repo).CombinedOutput(); err != nil {
28
+			t.Fatalf("Failed to push image %v: error %v, output %q", err, string(out))
29
+		}
30
+	}
31
+
32
+	// Clear local images store.
33
+	args := append([]string{"rmi"}, repos...)
34
+	if out, err := exec.Command(dockerBinary, args...).CombinedOutput(); err != nil {
35
+		t.Fatalf("Failed to clean images: error %v, output %q", err, string(out))
36
+	}
37
+
38
+	// Pull a single tag and verify it doesn't bring down all aliases.
39
+	pullCmd := exec.Command(dockerBinary, "pull", repos[0])
40
+	if out, _, err := runCommandWithOutput(pullCmd); err != nil {
41
+		t.Fatalf("Failed to pull %v: error %v, output %q", repoName, err, out)
42
+	}
43
+	defer deleteImages(repos[0])
44
+	if err := exec.Command(dockerBinary, "inspect", repos[0]).Run(); err != nil {
45
+		t.Fatalf("Image %v was not pulled down", repos[0])
46
+	}
47
+	for _, repo := range repos[1:] {
48
+		if err := exec.Command(dockerBinary, "inspect", repo).Run(); err == nil {
49
+			t.Fatalf("Image %v shouldn't have been pulled down", repo)
50
+		}
51
+	}
52
+
53
+	logDone("pull - image with aliases")
54
+}
10 55
 
11 56
 // pulling an image from the central registry should work
12 57
 func TestPullImageFromCentralRegistry(t *testing.T) {
... ...
@@ -3,39 +3,80 @@ package main
3 3
 import (
4 4
 	"fmt"
5 5
 	"os/exec"
6
+	"strings"
6 7
 	"testing"
8
+	"time"
7 9
 )
8 10
 
9
-// these tests need a freshly started empty private docker registry
10
-
11 11
 // pulling an image from the central registry should work
12 12
 func TestPushBusyboxImage(t *testing.T) {
13
-	// skip this test until we're able to use a registry
14
-	t.Skip()
13
+	defer setupRegistry(t)()
14
+
15
+	repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
15 16
 	// tag the image to upload it tot he private registry
16
-	repoName := fmt.Sprintf("%v/busybox", privateRegistryURL)
17 17
 	tagCmd := exec.Command(dockerBinary, "tag", "busybox", repoName)
18 18
 	if out, _, err := runCommandWithOutput(tagCmd); err != nil {
19 19
 		t.Fatalf("image tagging failed: %s, %v", out, err)
20 20
 	}
21
+	defer deleteImages(repoName)
21 22
 
22 23
 	pushCmd := exec.Command(dockerBinary, "push", repoName)
23 24
 	if out, _, err := runCommandWithOutput(pushCmd); err != nil {
24 25
 		t.Fatalf("pushing the image to the private registry has failed: %s, %v", out, err)
25 26
 	}
26
-
27
-	deleteImages(repoName)
28
-
29
-	logDone("push - push busybox to private registry")
27
+	logDone("push - busybox to private registry")
30 28
 }
31 29
 
32 30
 // pushing an image without a prefix should throw an error
33 31
 func TestPushUnprefixedRepo(t *testing.T) {
34
-	// skip this test until we're able to use a registry
35
-	t.Skip()
36 32
 	pushCmd := exec.Command(dockerBinary, "push", "busybox")
37 33
 	if out, _, err := runCommandWithOutput(pushCmd); err == nil {
38 34
 		t.Fatalf("pushing an unprefixed repo didn't result in a non-zero exit status: %s", out)
39 35
 	}
40
-	logDone("push - push unprefixed busybox repo --> must fail")
36
+	logDone("push - unprefixed busybox repo must fail")
37
+}
38
+
39
+func TestPushUntagged(t *testing.T) {
40
+	defer setupRegistry(t)()
41
+
42
+	repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
43
+
44
+	expected := "does not exist"
45
+	pushCmd := exec.Command(dockerBinary, "push", repoName)
46
+	if out, _, err := runCommandWithOutput(pushCmd); err == nil {
47
+		t.Fatalf("pushing the image to the private registry should have failed: outuput %q", out)
48
+	} else if !strings.Contains(out, expected) {
49
+		t.Fatalf("pushing the image failed with an unexpected message: expected %q, got %q", expected, out)
50
+	}
51
+	logDone("push - untagged image")
52
+}
53
+
54
+func TestPushInterrupt(t *testing.T) {
55
+	defer setupRegistry(t)()
56
+
57
+	repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
58
+	// tag the image to upload it tot he private registry
59
+	tagCmd := exec.Command(dockerBinary, "tag", "busybox", repoName)
60
+	if out, _, err := runCommandWithOutput(tagCmd); err != nil {
61
+		t.Fatalf("image tagging failed: %s, %v", out, err)
62
+	}
63
+	defer deleteImages(repoName)
64
+
65
+	pushCmd := exec.Command(dockerBinary, "push", repoName)
66
+	if err := pushCmd.Start(); err != nil {
67
+		t.Fatalf("Failed to start pushing to private registry: %v", err)
68
+	}
69
+
70
+	// Interrupt push (yes, we have no idea at what point it will get killed).
71
+	time.Sleep(200 * time.Millisecond)
72
+	if err := pushCmd.Process.Kill(); err != nil {
73
+		t.Fatalf("Failed to kill push process: %v", err)
74
+	}
75
+	// Try agin
76
+	pushCmd = exec.Command(dockerBinary, "push", repoName)
77
+	if err := pushCmd.Start(); err != nil {
78
+		t.Fatalf("Failed to start pushing to private registry: %v", err)
79
+	}
80
+
81
+	logDone("push - interrupted")
41 82
 }
... ...
@@ -864,3 +864,11 @@ func readContainerFile(containerId, filename string) ([]byte, error) {
864 864
 
865 865
 	return content, nil
866 866
 }
867
+
868
+func setupRegistry(t *testing.T) func() {
869
+	reg, err := newTestRegistryV2(t)
870
+	if err != nil {
871
+		t.Fatal(err)
872
+	}
873
+	return func() { reg.Close() }
874
+}
867 875
new file mode 100644
... ...
@@ -0,0 +1,58 @@
0
+package main
1
+
2
+import (
3
+	"fmt"
4
+	"io/ioutil"
5
+	"os"
6
+	"os/exec"
7
+	"path/filepath"
8
+	"testing"
9
+)
10
+
11
+const v2binary = "registry-v2"
12
+
13
+type testRegistryV2 struct {
14
+	cmd *exec.Cmd
15
+	dir string
16
+}
17
+
18
+func newTestRegistryV2(t *testing.T) (*testRegistryV2, error) {
19
+	template := `version: 0.1
20
+loglevel: debug
21
+storage:
22
+    filesystem:
23
+        rootdirectory: %s
24
+http:
25
+    addr: %s`
26
+	tmp, err := ioutil.TempDir("", "registry-test-")
27
+	if err != nil {
28
+		return nil, err
29
+	}
30
+	confPath := filepath.Join(tmp, "config.yaml")
31
+	config, err := os.Create(confPath)
32
+	if err != nil {
33
+		return nil, err
34
+	}
35
+	if _, err := fmt.Fprintf(config, template, tmp, privateRegistryURL); err != nil {
36
+		os.RemoveAll(tmp)
37
+		return nil, err
38
+	}
39
+
40
+	cmd := exec.Command(v2binary, confPath)
41
+	if err := cmd.Start(); err != nil {
42
+		os.RemoveAll(tmp)
43
+		if os.IsNotExist(err) {
44
+			t.Skip()
45
+		}
46
+		return nil, err
47
+	}
48
+	return &testRegistryV2{
49
+		cmd: cmd,
50
+		dir: tmp,
51
+	}, nil
52
+}
53
+
54
+func (r *testRegistryV2) Close() {
55
+	r.cmd.Process.Kill()
56
+	os.RemoveAll(r.dir)
57
+}
... ...
@@ -3,8 +3,11 @@ package tarsum
3 3
 import (
4 4
 	"bytes"
5 5
 	"compress/gzip"
6
+	"crypto"
6 7
 	"crypto/sha256"
7 8
 	"encoding/hex"
9
+	"errors"
10
+	"fmt"
8 11
 	"hash"
9 12
 	"io"
10 13
 	"strings"
... ...
@@ -39,6 +42,30 @@ func NewTarSumHash(r io.Reader, dc bool, v Version, tHash THash) (TarSum, error)
39 39
 	return ts, err
40 40
 }
41 41
 
42
+// Create a new TarSum using the provided TarSum version+hash label.
43
+func NewTarSumForLabel(r io.Reader, disableCompression bool, label string) (TarSum, error) {
44
+	parts := strings.SplitN(label, "+", 2)
45
+	if len(parts) != 2 {
46
+		return nil, errors.New("tarsum label string should be of the form: {tarsum_version}+{hash_name}")
47
+	}
48
+
49
+	versionName, hashName := parts[0], parts[1]
50
+
51
+	version, ok := tarSumVersionsByName[versionName]
52
+	if !ok {
53
+		return nil, fmt.Errorf("unknown TarSum version name: %q", versionName)
54
+	}
55
+
56
+	hashConfig, ok := standardHashConfigs[hashName]
57
+	if !ok {
58
+		return nil, fmt.Errorf("unknown TarSum hash name: %q", hashName)
59
+	}
60
+
61
+	tHash := NewTHash(hashConfig.name, hashConfig.hash.New)
62
+
63
+	return NewTarSumHash(r, disableCompression, version, tHash)
64
+}
65
+
42 66
 // TarSum is the generic interface for calculating fixed time
43 67
 // checksums of a tar archive
44 68
 type TarSum interface {
... ...
@@ -89,6 +116,18 @@ func NewTHash(name string, h func() hash.Hash) THash {
89 89
 	return simpleTHash{n: name, h: h}
90 90
 }
91 91
 
92
+type tHashConfig struct {
93
+	name string
94
+	hash crypto.Hash
95
+}
96
+
97
+var (
98
+	standardHashConfigs = map[string]tHashConfig{
99
+		"sha256": {name: "sha256", hash: crypto.SHA256},
100
+		"sha512": {name: "sha512", hash: crypto.SHA512},
101
+	}
102
+)
103
+
92 104
 // TarSum default is "sha256"
93 105
 var DefaultTHash = NewTHash("sha256", sha256.New)
94 106
 
... ...
@@ -31,11 +31,18 @@ func GetVersions() []Version {
31 31
 	return v
32 32
 }
33 33
 
34
-var tarSumVersions = map[Version]string{
35
-	Version0:   "tarsum",
36
-	Version1:   "tarsum.v1",
37
-	VersionDev: "tarsum.dev",
38
-}
34
+var (
35
+	tarSumVersions = map[Version]string{
36
+		Version0:   "tarsum",
37
+		Version1:   "tarsum.v1",
38
+		VersionDev: "tarsum.dev",
39
+	}
40
+	tarSumVersionsByName = map[string]Version{
41
+		"tarsum":     Version0,
42
+		"tarsum.v1":  Version1,
43
+		"tarsum.dev": VersionDev,
44
+	}
45
+)
39 46
 
40 47
 func (tsv Version) String() string {
41 48
 	return tarSumVersions[tsv]
... ...
@@ -10,7 +10,10 @@ import (
10 10
 	"os"
11 11
 	"path"
12 12
 	"strings"
13
+	"sync"
14
+	"time"
13 15
 
16
+	log "github.com/Sirupsen/logrus"
14 17
 	"github.com/docker/docker/utils"
15 18
 )
16 19
 
... ...
@@ -36,6 +39,88 @@ type ConfigFile struct {
36 36
 	rootPath string
37 37
 }
38 38
 
39
+type RequestAuthorization struct {
40
+	authConfig       *AuthConfig
41
+	registryEndpoint *Endpoint
42
+	resource         string
43
+	scope            string
44
+	actions          []string
45
+
46
+	tokenLock       sync.Mutex
47
+	tokenCache      string
48
+	tokenExpiration time.Time
49
+}
50
+
51
+func NewRequestAuthorization(authConfig *AuthConfig, registryEndpoint *Endpoint, resource, scope string, actions []string) *RequestAuthorization {
52
+	return &RequestAuthorization{
53
+		authConfig:       authConfig,
54
+		registryEndpoint: registryEndpoint,
55
+		resource:         resource,
56
+		scope:            scope,
57
+		actions:          actions,
58
+	}
59
+}
60
+
61
+func (auth *RequestAuthorization) getToken() (string, error) {
62
+	auth.tokenLock.Lock()
63
+	defer auth.tokenLock.Unlock()
64
+	now := time.Now()
65
+	if now.Before(auth.tokenExpiration) {
66
+		log.Debugf("Using cached token for %s", auth.authConfig.Username)
67
+		return auth.tokenCache, nil
68
+	}
69
+
70
+	client := &http.Client{
71
+		Transport: &http.Transport{
72
+			DisableKeepAlives: true,
73
+			Proxy:             http.ProxyFromEnvironment},
74
+		CheckRedirect: AddRequiredHeadersToRedirectedRequests,
75
+	}
76
+	factory := HTTPRequestFactory(nil)
77
+
78
+	for _, challenge := range auth.registryEndpoint.AuthChallenges {
79
+		switch strings.ToLower(challenge.Scheme) {
80
+		case "basic":
81
+			// no token necessary
82
+		case "bearer":
83
+			log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, auth.authConfig.Username)
84
+			params := map[string]string{}
85
+			for k, v := range challenge.Parameters {
86
+				params[k] = v
87
+			}
88
+			params["scope"] = fmt.Sprintf("%s:%s:%s", auth.resource, auth.scope, strings.Join(auth.actions, ","))
89
+			token, err := getToken(auth.authConfig.Username, auth.authConfig.Password, params, auth.registryEndpoint, client, factory)
90
+			if err != nil {
91
+				return "", err
92
+			}
93
+			auth.tokenCache = token
94
+			auth.tokenExpiration = now.Add(time.Minute)
95
+
96
+			return token, nil
97
+		default:
98
+			log.Infof("Unsupported auth scheme: %q", challenge.Scheme)
99
+		}
100
+	}
101
+
102
+	// Do not expire cache since there are no challenges which use a token
103
+	auth.tokenExpiration = time.Now().Add(time.Hour * 24)
104
+
105
+	return "", nil
106
+}
107
+
108
+func (auth *RequestAuthorization) Authorize(req *http.Request) error {
109
+	token, err := auth.getToken()
110
+	if err != nil {
111
+		return err
112
+	}
113
+	if token != "" {
114
+		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
115
+	} else if auth.authConfig.Username != "" && auth.authConfig.Password != "" {
116
+		req.SetBasicAuth(auth.authConfig.Username, auth.authConfig.Password)
117
+	}
118
+	return nil
119
+}
120
+
39 121
 // create a base64 encoded auth string to store in config
40 122
 func encodeAuth(authConfig *AuthConfig) string {
41 123
 	authStr := authConfig.Username + ":" + authConfig.Password
... ...
@@ -144,8 +229,18 @@ func SaveConfig(configFile *ConfigFile) error {
144 144
 	return nil
145 145
 }
146 146
 
147
-// try to register/login to the registry server
148
-func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, error) {
147
+// Login tries to register/login to the registry server.
148
+func Login(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) {
149
+	// Separates the v2 registry login logic from the v1 logic.
150
+	if registryEndpoint.Version == APIVersion2 {
151
+		return loginV2(authConfig, registryEndpoint, factory)
152
+	}
153
+
154
+	return loginV1(authConfig, registryEndpoint, factory)
155
+}
156
+
157
+// loginV1 tries to register/login to the v1 registry server.
158
+func loginV1(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) {
149 159
 	var (
150 160
 		status  string
151 161
 		reqBody []byte
... ...
@@ -161,6 +256,8 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e
161 161
 		serverAddress = authConfig.ServerAddress
162 162
 	)
163 163
 
164
+	log.Debugf("attempting v1 login to registry endpoint %s", registryEndpoint)
165
+
164 166
 	if serverAddress == "" {
165 167
 		return "", fmt.Errorf("Server Error: Server Address not set.")
166 168
 	}
... ...
@@ -253,6 +350,103 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e
253 253
 	return status, nil
254 254
 }
255 255
 
256
+// loginV2 tries to login to the v2 registry server. The given registry endpoint has been
257
+// pinged or setup with a list of authorization challenges. Each of these challenges are
258
+// tried until one of them succeeds. Currently supported challenge schemes are:
259
+// 		HTTP Basic Authorization
260
+// 		Token Authorization with a separate token issuing server
261
+// NOTE: the v2 logic does not attempt to create a user account if one doesn't exist. For
262
+// now, users should create their account through other means like directly from a web page
263
+// served by the v2 registry service provider. Whether this will be supported in the future
264
+// is to be determined.
265
+func loginV2(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) {
266
+	log.Debugf("attempting v2 login to registry endpoint %s", registryEndpoint)
267
+
268
+	client := &http.Client{
269
+		Transport: &http.Transport{
270
+			DisableKeepAlives: true,
271
+			Proxy:             http.ProxyFromEnvironment,
272
+		},
273
+		CheckRedirect: AddRequiredHeadersToRedirectedRequests,
274
+	}
275
+
276
+	var (
277
+		err       error
278
+		allErrors []error
279
+	)
280
+
281
+	for _, challenge := range registryEndpoint.AuthChallenges {
282
+		log.Debugf("trying %q auth challenge with params %s", challenge.Scheme, challenge.Parameters)
283
+
284
+		switch strings.ToLower(challenge.Scheme) {
285
+		case "basic":
286
+			err = tryV2BasicAuthLogin(authConfig, challenge.Parameters, registryEndpoint, client, factory)
287
+		case "bearer":
288
+			err = tryV2TokenAuthLogin(authConfig, challenge.Parameters, registryEndpoint, client, factory)
289
+		default:
290
+			// Unsupported challenge types are explicitly skipped.
291
+			err = fmt.Errorf("unsupported auth scheme: %q", challenge.Scheme)
292
+		}
293
+
294
+		if err == nil {
295
+			return "Login Succeeded", nil
296
+		}
297
+
298
+		log.Debugf("error trying auth challenge %q: %s", challenge.Scheme, err)
299
+
300
+		allErrors = append(allErrors, err)
301
+	}
302
+
303
+	return "", fmt.Errorf("no successful auth challenge for %s - errors: %s", registryEndpoint, allErrors)
304
+}
305
+
306
+func tryV2BasicAuthLogin(authConfig *AuthConfig, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *utils.HTTPRequestFactory) error {
307
+	req, err := factory.NewRequest("GET", registryEndpoint.Path(""), nil)
308
+	if err != nil {
309
+		return err
310
+	}
311
+
312
+	req.SetBasicAuth(authConfig.Username, authConfig.Password)
313
+
314
+	resp, err := client.Do(req)
315
+	if err != nil {
316
+		return err
317
+	}
318
+	defer resp.Body.Close()
319
+
320
+	if resp.StatusCode != http.StatusOK {
321
+		return fmt.Errorf("basic auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode))
322
+	}
323
+
324
+	return nil
325
+}
326
+
327
+func tryV2TokenAuthLogin(authConfig *AuthConfig, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *utils.HTTPRequestFactory) error {
328
+	token, err := getToken(authConfig.Username, authConfig.Password, params, registryEndpoint, client, factory)
329
+	if err != nil {
330
+		return err
331
+	}
332
+
333
+	req, err := factory.NewRequest("GET", registryEndpoint.Path(""), nil)
334
+	if err != nil {
335
+		return err
336
+	}
337
+
338
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
339
+
340
+	resp, err := client.Do(req)
341
+	if err != nil {
342
+		return err
343
+	}
344
+	defer resp.Body.Close()
345
+
346
+	if resp.StatusCode != http.StatusOK {
347
+		return fmt.Errorf("token auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode))
348
+	}
349
+
350
+	return nil
351
+}
352
+
256 353
 // this method matches a auth configuration to a server address or a url
257 354
 func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig {
258 355
 	configKey := index.GetAuthConfigKey()
259 356
new file mode 100644
... ...
@@ -0,0 +1,150 @@
0
+package registry
1
+
2
+import (
3
+	"net/http"
4
+	"strings"
5
+)
6
+
7
+// Octet types from RFC 2616.
8
+type octetType byte
9
+
10
+// AuthorizationChallenge carries information
11
+// from a WWW-Authenticate response header.
12
+type AuthorizationChallenge struct {
13
+	Scheme     string
14
+	Parameters map[string]string
15
+}
16
+
17
+var octetTypes [256]octetType
18
+
19
+const (
20
+	isToken octetType = 1 << iota
21
+	isSpace
22
+)
23
+
24
+func init() {
25
+	// OCTET      = <any 8-bit sequence of data>
26
+	// CHAR       = <any US-ASCII character (octets 0 - 127)>
27
+	// CTL        = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
28
+	// CR         = <US-ASCII CR, carriage return (13)>
29
+	// LF         = <US-ASCII LF, linefeed (10)>
30
+	// SP         = <US-ASCII SP, space (32)>
31
+	// HT         = <US-ASCII HT, horizontal-tab (9)>
32
+	// <">        = <US-ASCII double-quote mark (34)>
33
+	// CRLF       = CR LF
34
+	// LWS        = [CRLF] 1*( SP | HT )
35
+	// TEXT       = <any OCTET except CTLs, but including LWS>
36
+	// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
37
+	//              | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
38
+	// token      = 1*<any CHAR except CTLs or separators>
39
+	// qdtext     = <any TEXT except <">>
40
+
41
+	for c := 0; c < 256; c++ {
42
+		var t octetType
43
+		isCtl := c <= 31 || c == 127
44
+		isChar := 0 <= c && c <= 127
45
+		isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
46
+		if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
47
+			t |= isSpace
48
+		}
49
+		if isChar && !isCtl && !isSeparator {
50
+			t |= isToken
51
+		}
52
+		octetTypes[c] = t
53
+	}
54
+}
55
+
56
+func parseAuthHeader(header http.Header) []*AuthorizationChallenge {
57
+	var challenges []*AuthorizationChallenge
58
+	for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] {
59
+		v, p := parseValueAndParams(h)
60
+		if v != "" {
61
+			challenges = append(challenges, &AuthorizationChallenge{Scheme: v, Parameters: p})
62
+		}
63
+	}
64
+	return challenges
65
+}
66
+
67
+func parseValueAndParams(header string) (value string, params map[string]string) {
68
+	params = make(map[string]string)
69
+	value, s := expectToken(header)
70
+	if value == "" {
71
+		return
72
+	}
73
+	value = strings.ToLower(value)
74
+	s = "," + skipSpace(s)
75
+	for strings.HasPrefix(s, ",") {
76
+		var pkey string
77
+		pkey, s = expectToken(skipSpace(s[1:]))
78
+		if pkey == "" {
79
+			return
80
+		}
81
+		if !strings.HasPrefix(s, "=") {
82
+			return
83
+		}
84
+		var pvalue string
85
+		pvalue, s = expectTokenOrQuoted(s[1:])
86
+		if pvalue == "" {
87
+			return
88
+		}
89
+		pkey = strings.ToLower(pkey)
90
+		params[pkey] = pvalue
91
+		s = skipSpace(s)
92
+	}
93
+	return
94
+}
95
+
96
+func skipSpace(s string) (rest string) {
97
+	i := 0
98
+	for ; i < len(s); i++ {
99
+		if octetTypes[s[i]]&isSpace == 0 {
100
+			break
101
+		}
102
+	}
103
+	return s[i:]
104
+}
105
+
106
+func expectToken(s string) (token, rest string) {
107
+	i := 0
108
+	for ; i < len(s); i++ {
109
+		if octetTypes[s[i]]&isToken == 0 {
110
+			break
111
+		}
112
+	}
113
+	return s[:i], s[i:]
114
+}
115
+
116
+func expectTokenOrQuoted(s string) (value string, rest string) {
117
+	if !strings.HasPrefix(s, "\"") {
118
+		return expectToken(s)
119
+	}
120
+	s = s[1:]
121
+	for i := 0; i < len(s); i++ {
122
+		switch s[i] {
123
+		case '"':
124
+			return s[:i], s[i+1:]
125
+		case '\\':
126
+			p := make([]byte, len(s)-1)
127
+			j := copy(p, s[:i])
128
+			escape := true
129
+			for i = i + i; i < len(s); i++ {
130
+				b := s[i]
131
+				switch {
132
+				case escape:
133
+					escape = false
134
+					p[j] = b
135
+					j++
136
+				case b == '\\':
137
+					escape = true
138
+				case b == '"':
139
+					return string(p[:j]), s[i+1:]
140
+				default:
141
+					p[j] = b
142
+					j++
143
+				}
144
+			}
145
+			return "", ""
146
+		}
147
+	}
148
+	return "", ""
149
+}
... ...
@@ -23,7 +23,7 @@ type Options struct {
23 23
 const (
24 24
 	// Only used for user auth + account creation
25 25
 	INDEXSERVER    = "https://index.docker.io/v1/"
26
-	REGISTRYSERVER = "https://registry-1.docker.io/v1/"
26
+	REGISTRYSERVER = "https://registry-1.docker.io/v2/"
27 27
 	INDEXNAME      = "docker.io"
28 28
 
29 29
 	// INDEXSERVER = "https://registry-stage.hub.docker.com/v1/"
... ...
@@ -10,115 +10,170 @@ import (
10 10
 	"strings"
11 11
 
12 12
 	log "github.com/Sirupsen/logrus"
13
+	"github.com/docker/docker/registry/v2"
13 14
 )
14 15
 
15 16
 // for mocking in unit tests
16 17
 var lookupIP = net.LookupIP
17 18
 
18
-// scans string for api version in the URL path. returns the trimmed hostname, if version found, string and API version.
19
-func scanForAPIVersion(hostname string) (string, APIVersion) {
19
+// scans string for api version in the URL path. returns the trimmed address, if version found, string and API version.
20
+func scanForAPIVersion(address string) (string, APIVersion) {
20 21
 	var (
21 22
 		chunks        []string
22 23
 		apiVersionStr string
23 24
 	)
24
-	if strings.HasSuffix(hostname, "/") {
25
-		chunks = strings.Split(hostname[:len(hostname)-1], "/")
26
-		apiVersionStr = chunks[len(chunks)-1]
27
-	} else {
28
-		chunks = strings.Split(hostname, "/")
29
-		apiVersionStr = chunks[len(chunks)-1]
25
+
26
+	if strings.HasSuffix(address, "/") {
27
+		address = address[:len(address)-1]
30 28
 	}
29
+
30
+	chunks = strings.Split(address, "/")
31
+	apiVersionStr = chunks[len(chunks)-1]
32
+
31 33
 	for k, v := range apiVersions {
32 34
 		if apiVersionStr == v {
33
-			hostname = strings.Join(chunks[:len(chunks)-1], "/")
34
-			return hostname, k
35
+			address = strings.Join(chunks[:len(chunks)-1], "/")
36
+			return address, k
35 37
 		}
36 38
 	}
37
-	return hostname, DefaultAPIVersion
39
+
40
+	return address, APIVersionUnknown
38 41
 }
39 42
 
43
+// NewEndpoint parses the given address to return a registry endpoint.
40 44
 func NewEndpoint(index *IndexInfo) (*Endpoint, error) {
41 45
 	// *TODO: Allow per-registry configuration of endpoints.
42 46
 	endpoint, err := newEndpoint(index.GetAuthConfigKey(), index.Secure)
43 47
 	if err != nil {
44 48
 		return nil, err
45 49
 	}
50
+	if err := validateEndpoint(endpoint); err != nil {
51
+		return nil, err
52
+	}
53
+
54
+	return endpoint, nil
55
+}
56
+
57
+func validateEndpoint(endpoint *Endpoint) error {
58
+	log.Debugf("pinging registry endpoint %s", endpoint)
46 59
 
47 60
 	// Try HTTPS ping to registry
48 61
 	endpoint.URL.Scheme = "https"
49 62
 	if _, err := endpoint.Ping(); err != nil {
50
-
51
-		//TODO: triggering highland build can be done there without "failing"
52
-
53
-		if index.Secure {
63
+		if endpoint.IsSecure {
54 64
 			// If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry`
55 65
 			// in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP.
56
-			return nil, fmt.Errorf("Invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host)
66
+			return fmt.Errorf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host)
57 67
 		}
58 68
 
59 69
 		// If registry is insecure and HTTPS failed, fallback to HTTP.
60 70
 		log.Debugf("Error from registry %q marked as insecure: %v. Insecurely falling back to HTTP", endpoint, err)
61 71
 		endpoint.URL.Scheme = "http"
62
-		_, err2 := endpoint.Ping()
63
-		if err2 == nil {
64
-			return endpoint, nil
72
+
73
+		var err2 error
74
+		if _, err2 = endpoint.Ping(); err2 == nil {
75
+			return nil
65 76
 		}
66 77
 
67
-		return nil, fmt.Errorf("Invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
78
+		return fmt.Errorf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
68 79
 	}
69 80
 
70
-	return endpoint, nil
81
+	return nil
71 82
 }
72
-func newEndpoint(hostname string, secure bool) (*Endpoint, error) {
83
+
84
+func newEndpoint(address string, secure bool) (*Endpoint, error) {
73 85
 	var (
74
-		endpoint        = Endpoint{}
75
-		trimmedHostname string
76
-		err             error
86
+		endpoint       = new(Endpoint)
87
+		trimmedAddress string
88
+		err            error
77 89
 	)
78
-	if !strings.HasPrefix(hostname, "http") {
79
-		hostname = "https://" + hostname
90
+
91
+	if !strings.HasPrefix(address, "http") {
92
+		address = "https://" + address
80 93
 	}
81
-	trimmedHostname, endpoint.Version = scanForAPIVersion(hostname)
82
-	endpoint.URL, err = url.Parse(trimmedHostname)
83
-	if err != nil {
94
+
95
+	trimmedAddress, endpoint.Version = scanForAPIVersion(address)
96
+
97
+	if endpoint.URL, err = url.Parse(trimmedAddress); err != nil {
84 98
 		return nil, err
85 99
 	}
86
-	endpoint.secure = secure
87
-	return &endpoint, nil
100
+	endpoint.IsSecure = secure
101
+	return endpoint, nil
88 102
 }
89 103
 
90 104
 func (repoInfo *RepositoryInfo) GetEndpoint() (*Endpoint, error) {
91 105
 	return NewEndpoint(repoInfo.Index)
92 106
 }
93 107
 
108
+// Endpoint stores basic information about a registry endpoint.
94 109
 type Endpoint struct {
95
-	URL     *url.URL
96
-	Version APIVersion
97
-	secure  bool
110
+	URL            *url.URL
111
+	Version        APIVersion
112
+	IsSecure       bool
113
+	AuthChallenges []*AuthorizationChallenge
114
+	URLBuilder     *v2.URLBuilder
98 115
 }
99 116
 
100 117
 // Get the formated URL for the root of this registry Endpoint
101
-func (e Endpoint) String() string {
102
-	return fmt.Sprintf("%s/v%d/", e.URL.String(), e.Version)
118
+func (e *Endpoint) String() string {
119
+	return fmt.Sprintf("%s/v%d/", e.URL, e.Version)
103 120
 }
104 121
 
105
-func (e Endpoint) VersionString(version APIVersion) string {
106
-	return fmt.Sprintf("%s/v%d/", e.URL.String(), version)
122
+// VersionString returns a formatted string of this
123
+// endpoint address using the given API Version.
124
+func (e *Endpoint) VersionString(version APIVersion) string {
125
+	return fmt.Sprintf("%s/v%d/", e.URL, version)
107 126
 }
108 127
 
109
-func (e Endpoint) Ping() (RegistryInfo, error) {
128
+// Path returns a formatted string for the URL
129
+// of this endpoint with the given path appended.
130
+func (e *Endpoint) Path(path string) string {
131
+	return fmt.Sprintf("%s/v%d/%s", e.URL, e.Version, path)
132
+}
133
+
134
+func (e *Endpoint) Ping() (RegistryInfo, error) {
135
+	// The ping logic to use is determined by the registry endpoint version.
136
+	switch e.Version {
137
+	case APIVersion1:
138
+		return e.pingV1()
139
+	case APIVersion2:
140
+		return e.pingV2()
141
+	}
142
+
143
+	// APIVersionUnknown
144
+	// We should try v2 first...
145
+	e.Version = APIVersion2
146
+	regInfo, errV2 := e.pingV2()
147
+	if errV2 == nil {
148
+		return regInfo, nil
149
+	}
150
+
151
+	// ... then fallback to v1.
152
+	e.Version = APIVersion1
153
+	regInfo, errV1 := e.pingV1()
154
+	if errV1 == nil {
155
+		return regInfo, nil
156
+	}
157
+
158
+	e.Version = APIVersionUnknown
159
+	return RegistryInfo{}, fmt.Errorf("unable to ping registry endpoint %s\nv2 ping attempt failed with error: %s\n v1 ping attempt failed with error: %s", e, errV2, errV1)
160
+}
161
+
162
+func (e *Endpoint) pingV1() (RegistryInfo, error) {
163
+	log.Debugf("attempting v1 ping for registry endpoint %s", e)
164
+
110 165
 	if e.String() == IndexServerAddress() {
111
-		// Skip the check, we now this one is valid
166
+		// Skip the check, we know this one is valid
112 167
 		// (and we never want to fallback to http in case of error)
113 168
 		return RegistryInfo{Standalone: false}, nil
114 169
 	}
115 170
 
116
-	req, err := http.NewRequest("GET", e.String()+"_ping", nil)
171
+	req, err := http.NewRequest("GET", e.Path("_ping"), nil)
117 172
 	if err != nil {
118 173
 		return RegistryInfo{Standalone: false}, err
119 174
 	}
120 175
 
121
-	resp, _, err := doRequest(req, nil, ConnectTimeout, e.secure)
176
+	resp, _, err := doRequest(req, nil, ConnectTimeout, e.IsSecure)
122 177
 	if err != nil {
123 178
 		return RegistryInfo{Standalone: false}, err
124 179
 	}
... ...
@@ -127,7 +182,7 @@ func (e Endpoint) Ping() (RegistryInfo, error) {
127 127
 
128 128
 	jsonString, err := ioutil.ReadAll(resp.Body)
129 129
 	if err != nil {
130
-		return RegistryInfo{Standalone: false}, fmt.Errorf("Error while reading the http response: %s", err)
130
+		return RegistryInfo{Standalone: false}, fmt.Errorf("error while reading the http response: %s", err)
131 131
 	}
132 132
 
133 133
 	// If the header is absent, we assume true for compatibility with earlier
... ...
@@ -157,3 +212,33 @@ func (e Endpoint) Ping() (RegistryInfo, error) {
157 157
 	log.Debugf("RegistryInfo.Standalone: %t", info.Standalone)
158 158
 	return info, nil
159 159
 }
160
+
161
+func (e *Endpoint) pingV2() (RegistryInfo, error) {
162
+	log.Debugf("attempting v2 ping for registry endpoint %s", e)
163
+
164
+	req, err := http.NewRequest("GET", e.Path(""), nil)
165
+	if err != nil {
166
+		return RegistryInfo{}, err
167
+	}
168
+
169
+	resp, _, err := doRequest(req, nil, ConnectTimeout, e.IsSecure)
170
+	if err != nil {
171
+		return RegistryInfo{}, err
172
+	}
173
+	defer resp.Body.Close()
174
+
175
+	if resp.StatusCode == http.StatusOK {
176
+		// It would seem that no authentication/authorization is required.
177
+		// So we don't need to parse/add any authorization schemes.
178
+		return RegistryInfo{Standalone: true}, nil
179
+	}
180
+
181
+	if resp.StatusCode == http.StatusUnauthorized {
182
+		// Parse the WWW-Authenticate Header and store the challenges
183
+		// on this endpoint object.
184
+		e.AuthChallenges = parseAuthHeader(resp.Header)
185
+		return RegistryInfo{}, nil
186
+	}
187
+
188
+	return RegistryInfo{}, fmt.Errorf("v2 registry endpoint returned status %d: %q", resp.StatusCode, http.StatusText(resp.StatusCode))
189
+}
... ...
@@ -8,8 +8,10 @@ func TestEndpointParse(t *testing.T) {
8 8
 		expected string
9 9
 	}{
10 10
 		{IndexServerAddress(), IndexServerAddress()},
11
-		{"http://0.0.0.0:5000", "http://0.0.0.0:5000/v1/"},
12
-		{"0.0.0.0:5000", "https://0.0.0.0:5000/v1/"},
11
+		{"http://0.0.0.0:5000/v1/", "http://0.0.0.0:5000/v1/"},
12
+		{"http://0.0.0.0:5000/v2/", "http://0.0.0.0:5000/v2/"},
13
+		{"http://0.0.0.0:5000", "http://0.0.0.0:5000/v0/"},
14
+		{"0.0.0.0:5000", "https://0.0.0.0:5000/v0/"},
13 15
 	}
14 16
 	for _, td := range testData {
15 17
 		e, err := newEndpoint(td.str, false)
... ...
@@ -1,6 +1,7 @@
1 1
 package registry
2 2
 
3 3
 import (
4
+	log "github.com/Sirupsen/logrus"
4 5
 	"github.com/docker/docker/engine"
5 6
 )
6 7
 
... ...
@@ -38,28 +39,39 @@ func (s *Service) Install(eng *engine.Engine) error {
38 38
 // and returns OK if authentication was sucessful.
39 39
 // It can be used to verify the validity of a client's credentials.
40 40
 func (s *Service) Auth(job *engine.Job) engine.Status {
41
-	var authConfig = new(AuthConfig)
41
+	var (
42
+		authConfig = new(AuthConfig)
43
+		endpoint   *Endpoint
44
+		index      *IndexInfo
45
+		status     string
46
+		err        error
47
+	)
42 48
 
43 49
 	job.GetenvJson("authConfig", authConfig)
44 50
 
45
-	if authConfig.ServerAddress != "" {
46
-		index, err := ResolveIndexInfo(job, authConfig.ServerAddress)
47
-		if err != nil {
48
-			return job.Error(err)
49
-		}
50
-		if !index.Official {
51
-			endpoint, err := NewEndpoint(index)
52
-			if err != nil {
53
-				return job.Error(err)
54
-			}
55
-			authConfig.ServerAddress = endpoint.String()
56
-		}
57
-	}
58
-
59
-	status, err := Login(authConfig, HTTPRequestFactory(nil))
60
-	if err != nil {
51
+	addr := authConfig.ServerAddress
52
+	if addr == "" {
53
+		// Use the official registry address if not specified.
54
+		addr = IndexServerAddress()
55
+	}
56
+
57
+	if index, err = ResolveIndexInfo(job, addr); err != nil {
61 58
 		return job.Error(err)
62 59
 	}
60
+
61
+	if endpoint, err = NewEndpoint(index); err != nil {
62
+		log.Errorf("unable to get new registry endpoint: %s", err)
63
+		return job.Error(err)
64
+	}
65
+
66
+	authConfig.ServerAddress = endpoint.String()
67
+
68
+	if status, err = Login(authConfig, endpoint, HTTPRequestFactory(nil)); err != nil {
69
+		log.Errorf("unable to login against registry endpoint %s: %s", endpoint, err)
70
+		return job.Error(err)
71
+	}
72
+
73
+	log.Infof("successful registry login for endpoint %s: %s", endpoint, status)
63 74
 	job.Printf("%s\n", status)
64 75
 
65 76
 	return engine.StatusOK
... ...
@@ -65,7 +65,7 @@ func NewSession(authConfig *AuthConfig, factory *utils.HTTPRequestFactory, endpo
65 65
 }
66 66
 
67 67
 func (r *Session) doRequest(req *http.Request) (*http.Response, *http.Client, error) {
68
-	return doRequest(req, r.jar, r.timeout, r.indexEndpoint.secure)
68
+	return doRequest(req, r.jar, r.timeout, r.indexEndpoint.IsSecure)
69 69
 }
70 70
 
71 71
 // Retrieve the history of a given image from the Registry.
... ...
@@ -5,104 +5,55 @@ import (
5 5
 	"fmt"
6 6
 	"io"
7 7
 	"io/ioutil"
8
-	"net/url"
9 8
 	"strconv"
10 9
 
11 10
 	log "github.com/Sirupsen/logrus"
11
+	"github.com/docker/docker/registry/v2"
12 12
 	"github.com/docker/docker/utils"
13
-	"github.com/gorilla/mux"
14 13
 )
15 14
 
16
-func newV2RegistryRouter() *mux.Router {
17
-	router := mux.NewRouter()
18
-
19
-	v2Router := router.PathPrefix("/v2/").Subrouter()
20
-
21
-	// Version Info
22
-	v2Router.Path("/version").Name("version")
23
-
24
-	// Image Manifests
25
-	v2Router.Path("/manifest/{imagename:[a-z0-9-._/]+}/{tagname:[a-zA-Z0-9-._]+}").Name("manifests")
26
-
27
-	// List Image Tags
28
-	v2Router.Path("/tags/{imagename:[a-z0-9-._/]+}").Name("tags")
29
-
30
-	// Download a blob
31
-	v2Router.Path("/blob/{imagename:[a-z0-9-._/]+}/{sumtype:[a-z0-9._+-]+}/{sum:[a-fA-F0-9]{4,}}").Name("downloadBlob")
32
-
33
-	// Upload a blob
34
-	v2Router.Path("/blob/{imagename:[a-z0-9-._/]+}/{sumtype:[a-z0-9._+-]+}").Name("uploadBlob")
35
-
36
-	// Mounting a blob in an image
37
-	v2Router.Path("/mountblob/{imagename:[a-z0-9-._/]+}/{sumtype:[a-z0-9._+-]+}/{sum:[a-fA-F0-9]{4,}}").Name("mountBlob")
38
-
39
-	return router
40
-}
41
-
42
-// APIVersion2 /v2/
43
-var v2HTTPRoutes = newV2RegistryRouter()
44
-
45
-func getV2URL(e *Endpoint, routeName string, vars map[string]string) (*url.URL, error) {
46
-	route := v2HTTPRoutes.Get(routeName)
47
-	if route == nil {
48
-		return nil, fmt.Errorf("unknown regisry v2 route name: %q", routeName)
49
-	}
50
-
51
-	varReplace := make([]string, 0, len(vars)*2)
52
-	for key, val := range vars {
53
-		varReplace = append(varReplace, key, val)
54
-	}
55
-
56
-	routePath, err := route.URLPath(varReplace...)
57
-	if err != nil {
58
-		return nil, fmt.Errorf("unable to make registry route %q with vars %v: %s", routeName, vars, err)
59
-	}
60
-	u, err := url.Parse(REGISTRYSERVER)
61
-	if err != nil {
62
-		return nil, fmt.Errorf("invalid registry url: %s", err)
15
+func getV2Builder(e *Endpoint) *v2.URLBuilder {
16
+	if e.URLBuilder == nil {
17
+		e.URLBuilder = v2.NewURLBuilder(e.URL)
63 18
 	}
64
-
65
-	return &url.URL{
66
-		Scheme: u.Scheme,
67
-		Host:   u.Host,
68
-		Path:   routePath.Path,
69
-	}, nil
19
+	return e.URLBuilder
70 20
 }
71 21
 
72
-// V2 Provenance POC
73
-
74
-func (r *Session) GetV2Version(token []string) (*RegistryInfo, error) {
75
-	routeURL, err := getV2URL(r.indexEndpoint, "version", nil)
76
-	if err != nil {
77
-		return nil, err
78
-	}
79
-
80
-	method := "GET"
81
-	log.Debugf("[registry] Calling %q %s", method, routeURL.String())
82
-
83
-	req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil)
84
-	if err != nil {
85
-		return nil, err
86
-	}
87
-	setTokenAuth(req, token)
88
-	res, _, err := r.doRequest(req)
89
-	if err != nil {
90
-		return nil, err
91
-	}
92
-	defer res.Body.Close()
93
-	if res.StatusCode != 200 {
94
-		return nil, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d fetching Version", res.StatusCode), res)
22
+func (r *Session) V2RegistryEndpoint(index *IndexInfo) (ep *Endpoint, err error) {
23
+	// TODO check if should use Mirror
24
+	if index.Official {
25
+		ep, err = newEndpoint(REGISTRYSERVER, true)
26
+		if err != nil {
27
+			return
28
+		}
29
+		err = validateEndpoint(ep)
30
+		if err != nil {
31
+			return
32
+		}
33
+	} else if r.indexEndpoint.String() == index.GetAuthConfigKey() {
34
+		ep = r.indexEndpoint
35
+	} else {
36
+		ep, err = NewEndpoint(index)
37
+		if err != nil {
38
+			return
39
+		}
95 40
 	}
96 41
 
97
-	decoder := json.NewDecoder(res.Body)
98
-	versionInfo := new(RegistryInfo)
42
+	ep.URLBuilder = v2.NewURLBuilder(ep.URL)
43
+	return
44
+}
99 45
 
100
-	err = decoder.Decode(versionInfo)
101
-	if err != nil {
102
-		return nil, fmt.Errorf("unable to decode GetV2Version JSON response: %s", err)
46
+// GetV2Authorization gets the authorization needed to the given image
47
+// If readonly access is requested, then only the authorization may
48
+// only be used for Get operations.
49
+func (r *Session) GetV2Authorization(ep *Endpoint, imageName string, readOnly bool) (auth *RequestAuthorization, err error) {
50
+	scopes := []string{"pull"}
51
+	if !readOnly {
52
+		scopes = append(scopes, "push")
103 53
 	}
104 54
 
105
-	return versionInfo, nil
55
+	log.Debugf("Getting authorization for %s %s", imageName, scopes)
56
+	return NewRequestAuthorization(r.GetAuthConfig(true), ep, "repository", imageName, scopes), nil
106 57
 }
107 58
 
108 59
 //
... ...
@@ -112,25 +63,22 @@ func (r *Session) GetV2Version(token []string) (*RegistryInfo, error) {
112 112
 //  1.c) if anything else, err
113 113
 // 2) PUT the created/signed manifest
114 114
 //
115
-func (r *Session) GetV2ImageManifest(imageName, tagName string, token []string) ([]byte, error) {
116
-	vars := map[string]string{
117
-		"imagename": imageName,
118
-		"tagname":   tagName,
119
-	}
120
-
121
-	routeURL, err := getV2URL(r.indexEndpoint, "manifests", vars)
115
+func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, auth *RequestAuthorization) ([]byte, error) {
116
+	routeURL, err := getV2Builder(ep).BuildManifestURL(imageName, tagName)
122 117
 	if err != nil {
123 118
 		return nil, err
124 119
 	}
125 120
 
126 121
 	method := "GET"
127
-	log.Debugf("[registry] Calling %q %s", method, routeURL.String())
122
+	log.Debugf("[registry] Calling %q %s", method, routeURL)
128 123
 
129
-	req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil)
124
+	req, err := r.reqFactory.NewRequest(method, routeURL, nil)
130 125
 	if err != nil {
131 126
 		return nil, err
132 127
 	}
133
-	setTokenAuth(req, token)
128
+	if err := auth.Authorize(req); err != nil {
129
+		return nil, err
130
+	}
134 131
 	res, _, err := r.doRequest(req)
135 132
 	if err != nil {
136 133
 		return nil, err
... ...
@@ -152,29 +100,25 @@ func (r *Session) GetV2ImageManifest(imageName, tagName string, token []string)
152 152
 	return buf, nil
153 153
 }
154 154
 
155
-// - Succeeded to mount for this image scope
156
-// - Failed with no error (So continue to Push the Blob)
155
+// - Succeeded to head image blob (already exists)
156
+// - Failed with no error (continue to Push the Blob)
157 157
 // - Failed with error
158
-func (r *Session) PostV2ImageMountBlob(imageName, sumType, sum string, token []string) (bool, error) {
159
-	vars := map[string]string{
160
-		"imagename": imageName,
161
-		"sumtype":   sumType,
162
-		"sum":       sum,
163
-	}
164
-
165
-	routeURL, err := getV2URL(r.indexEndpoint, "mountBlob", vars)
158
+func (r *Session) HeadV2ImageBlob(ep *Endpoint, imageName, sumType, sum string, auth *RequestAuthorization) (bool, error) {
159
+	routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, sumType+":"+sum)
166 160
 	if err != nil {
167 161
 		return false, err
168 162
 	}
169 163
 
170
-	method := "POST"
171
-	log.Debugf("[registry] Calling %q %s", method, routeURL.String())
164
+	method := "HEAD"
165
+	log.Debugf("[registry] Calling %q %s", method, routeURL)
172 166
 
173
-	req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil)
167
+	req, err := r.reqFactory.NewRequest(method, routeURL, nil)
174 168
 	if err != nil {
175 169
 		return false, err
176 170
 	}
177
-	setTokenAuth(req, token)
171
+	if err := auth.Authorize(req); err != nil {
172
+		return false, err
173
+	}
178 174
 	res, _, err := r.doRequest(req)
179 175
 	if err != nil {
180 176
 		return false, err
... ...
@@ -184,32 +128,28 @@ func (r *Session) PostV2ImageMountBlob(imageName, sumType, sum string, token []s
184 184
 	case 200:
185 185
 		// return something indicating no push needed
186 186
 		return true, nil
187
-	case 300:
187
+	case 404:
188 188
 		// return something indicating blob push needed
189 189
 		return false, nil
190 190
 	}
191 191
 	return false, fmt.Errorf("Failed to mount %q - %s:%s : %d", imageName, sumType, sum, res.StatusCode)
192 192
 }
193 193
 
194
-func (r *Session) GetV2ImageBlob(imageName, sumType, sum string, blobWrtr io.Writer, token []string) error {
195
-	vars := map[string]string{
196
-		"imagename": imageName,
197
-		"sumtype":   sumType,
198
-		"sum":       sum,
199
-	}
200
-
201
-	routeURL, err := getV2URL(r.indexEndpoint, "downloadBlob", vars)
194
+func (r *Session) GetV2ImageBlob(ep *Endpoint, imageName, sumType, sum string, blobWrtr io.Writer, auth *RequestAuthorization) error {
195
+	routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, sumType+":"+sum)
202 196
 	if err != nil {
203 197
 		return err
204 198
 	}
205 199
 
206 200
 	method := "GET"
207
-	log.Debugf("[registry] Calling %q %s", method, routeURL.String())
208
-	req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil)
201
+	log.Debugf("[registry] Calling %q %s", method, routeURL)
202
+	req, err := r.reqFactory.NewRequest(method, routeURL, nil)
209 203
 	if err != nil {
210 204
 		return err
211 205
 	}
212
-	setTokenAuth(req, token)
206
+	if err := auth.Authorize(req); err != nil {
207
+		return err
208
+	}
213 209
 	res, _, err := r.doRequest(req)
214 210
 	if err != nil {
215 211
 		return err
... ...
@@ -226,25 +166,21 @@ func (r *Session) GetV2ImageBlob(imageName, sumType, sum string, blobWrtr io.Wri
226 226
 	return err
227 227
 }
228 228
 
229
-func (r *Session) GetV2ImageBlobReader(imageName, sumType, sum string, token []string) (io.ReadCloser, int64, error) {
230
-	vars := map[string]string{
231
-		"imagename": imageName,
232
-		"sumtype":   sumType,
233
-		"sum":       sum,
234
-	}
235
-
236
-	routeURL, err := getV2URL(r.indexEndpoint, "downloadBlob", vars)
229
+func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName, sumType, sum string, auth *RequestAuthorization) (io.ReadCloser, int64, error) {
230
+	routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, sumType+":"+sum)
237 231
 	if err != nil {
238 232
 		return nil, 0, err
239 233
 	}
240 234
 
241 235
 	method := "GET"
242
-	log.Debugf("[registry] Calling %q %s", method, routeURL.String())
243
-	req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil)
236
+	log.Debugf("[registry] Calling %q %s", method, routeURL)
237
+	req, err := r.reqFactory.NewRequest(method, routeURL, nil)
244 238
 	if err != nil {
245 239
 		return nil, 0, err
246 240
 	}
247
-	setTokenAuth(req, token)
241
+	if err := auth.Authorize(req); err != nil {
242
+		return nil, 0, err
243
+	}
248 244
 	res, _, err := r.doRequest(req)
249 245
 	if err != nil {
250 246
 		return nil, 0, err
... ...
@@ -267,105 +203,110 @@ func (r *Session) GetV2ImageBlobReader(imageName, sumType, sum string, token []s
267 267
 // Push the image to the server for storage.
268 268
 // 'layer' is an uncompressed reader of the blob to be pushed.
269 269
 // The server will generate it's own checksum calculation.
270
-func (r *Session) PutV2ImageBlob(imageName, sumType string, blobRdr io.Reader, token []string) (serverChecksum string, err error) {
271
-	vars := map[string]string{
272
-		"imagename": imageName,
273
-		"sumtype":   sumType,
270
+func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName, sumType, sumStr string, blobRdr io.Reader, auth *RequestAuthorization) error {
271
+	routeURL, err := getV2Builder(ep).BuildBlobUploadURL(imageName)
272
+	if err != nil {
273
+		return err
274 274
 	}
275 275
 
276
-	routeURL, err := getV2URL(r.indexEndpoint, "uploadBlob", vars)
276
+	log.Debugf("[registry] Calling %q %s", "POST", routeURL)
277
+	req, err := r.reqFactory.NewRequest("POST", routeURL, nil)
277 278
 	if err != nil {
278
-		return "", err
279
+		return err
280
+	}
281
+
282
+	if err := auth.Authorize(req); err != nil {
283
+		return err
284
+	}
285
+	res, _, err := r.doRequest(req)
286
+	if err != nil {
287
+		return err
279 288
 	}
289
+	location := res.Header.Get("Location")
280 290
 
281 291
 	method := "PUT"
282
-	log.Debugf("[registry] Calling %q %s", method, routeURL.String())
283
-	req, err := r.reqFactory.NewRequest(method, routeURL.String(), blobRdr)
292
+	log.Debugf("[registry] Calling %q %s", method, location)
293
+	req, err = r.reqFactory.NewRequest(method, location, blobRdr)
284 294
 	if err != nil {
285
-		return "", err
295
+		return err
286 296
 	}
287
-	setTokenAuth(req, token)
288
-	res, _, err := r.doRequest(req)
297
+	queryParams := req.URL.Query()
298
+	queryParams.Add("digest", sumType+":"+sumStr)
299
+	req.URL.RawQuery = queryParams.Encode()
300
+	if err := auth.Authorize(req); err != nil {
301
+		return err
302
+	}
303
+	res, _, err = r.doRequest(req)
289 304
 	if err != nil {
290
-		return "", err
305
+		return err
291 306
 	}
292 307
 	defer res.Body.Close()
308
+
293 309
 	if res.StatusCode != 201 {
294 310
 		if res.StatusCode == 401 {
295
-			return "", errLoginRequired
311
+			return errLoginRequired
296 312
 		}
297
-		return "", utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s blob", res.StatusCode, imageName), res)
298
-	}
299
-
300
-	type sumReturn struct {
301
-		Checksum string `json:"checksum"`
302
-	}
303
-
304
-	decoder := json.NewDecoder(res.Body)
305
-	var sumInfo sumReturn
306
-
307
-	err = decoder.Decode(&sumInfo)
308
-	if err != nil {
309
-		return "", fmt.Errorf("unable to decode PutV2ImageBlob JSON response: %s", err)
313
+		return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s blob", res.StatusCode, imageName), res)
310 314
 	}
311 315
 
312
-	// XXX this is a json struct from the registry, with its checksum
313
-	return sumInfo.Checksum, nil
316
+	return nil
314 317
 }
315 318
 
316 319
 // Finally Push the (signed) manifest of the blobs we've just pushed
317
-func (r *Session) PutV2ImageManifest(imageName, tagName string, manifestRdr io.Reader, token []string) error {
318
-	vars := map[string]string{
319
-		"imagename": imageName,
320
-		"tagname":   tagName,
321
-	}
322
-
323
-	routeURL, err := getV2URL(r.indexEndpoint, "manifests", vars)
320
+func (r *Session) PutV2ImageManifest(ep *Endpoint, imageName, tagName string, manifestRdr io.Reader, auth *RequestAuthorization) error {
321
+	routeURL, err := getV2Builder(ep).BuildManifestURL(imageName, tagName)
324 322
 	if err != nil {
325 323
 		return err
326 324
 	}
327 325
 
328 326
 	method := "PUT"
329
-	log.Debugf("[registry] Calling %q %s", method, routeURL.String())
330
-	req, err := r.reqFactory.NewRequest(method, routeURL.String(), manifestRdr)
327
+	log.Debugf("[registry] Calling %q %s", method, routeURL)
328
+	req, err := r.reqFactory.NewRequest(method, routeURL, manifestRdr)
331 329
 	if err != nil {
332 330
 		return err
333 331
 	}
334
-	setTokenAuth(req, token)
332
+	if err := auth.Authorize(req); err != nil {
333
+		return err
334
+	}
335 335
 	res, _, err := r.doRequest(req)
336 336
 	if err != nil {
337 337
 		return err
338 338
 	}
339
+	b, _ := ioutil.ReadAll(res.Body)
339 340
 	res.Body.Close()
340
-	if res.StatusCode != 201 {
341
+	if res.StatusCode != 200 {
341 342
 		if res.StatusCode == 401 {
342 343
 			return errLoginRequired
343 344
 		}
345
+		log.Debugf("Unexpected response from server: %q %#v", b, res.Header)
344 346
 		return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s:%s manifest", res.StatusCode, imageName, tagName), res)
345 347
 	}
346 348
 
347 349
 	return nil
348 350
 }
349 351
 
350
-// Given a repository name, returns a json array of string tags
351
-func (r *Session) GetV2RemoteTags(imageName string, token []string) ([]string, error) {
352
-	vars := map[string]string{
353
-		"imagename": imageName,
354
-	}
352
+type remoteTags struct {
353
+	name string
354
+	tags []string
355
+}
355 356
 
356
-	routeURL, err := getV2URL(r.indexEndpoint, "tags", vars)
357
+// Given a repository name, returns a json array of string tags
358
+func (r *Session) GetV2RemoteTags(ep *Endpoint, imageName string, auth *RequestAuthorization) ([]string, error) {
359
+	routeURL, err := getV2Builder(ep).BuildTagsURL(imageName)
357 360
 	if err != nil {
358 361
 		return nil, err
359 362
 	}
360 363
 
361 364
 	method := "GET"
362
-	log.Debugf("[registry] Calling %q %s", method, routeURL.String())
365
+	log.Debugf("[registry] Calling %q %s", method, routeURL)
363 366
 
364
-	req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil)
367
+	req, err := r.reqFactory.NewRequest(method, routeURL, nil)
365 368
 	if err != nil {
366 369
 		return nil, err
367 370
 	}
368
-	setTokenAuth(req, token)
371
+	if err := auth.Authorize(req); err != nil {
372
+		return nil, err
373
+	}
369 374
 	res, _, err := r.doRequest(req)
370 375
 	if err != nil {
371 376
 		return nil, err
... ...
@@ -381,10 +322,10 @@ func (r *Session) GetV2RemoteTags(imageName string, token []string) ([]string, e
381 381
 	}
382 382
 
383 383
 	decoder := json.NewDecoder(res.Body)
384
-	var tags []string
385
-	err = decoder.Decode(&tags)
384
+	var remote remoteTags
385
+	err = decoder.Decode(&remote)
386 386
 	if err != nil {
387 387
 		return nil, fmt.Errorf("Error while decoding the http response: %s", err)
388 388
 	}
389
-	return tags, nil
389
+	return remote.tags, nil
390 390
 }
391 391
new file mode 100644
... ...
@@ -0,0 +1,81 @@
0
+package registry
1
+
2
+import (
3
+	"encoding/json"
4
+	"errors"
5
+	"fmt"
6
+	"net/http"
7
+	"net/url"
8
+	"strings"
9
+
10
+	"github.com/docker/docker/utils"
11
+)
12
+
13
+type tokenResponse struct {
14
+	Token string `json:"token"`
15
+}
16
+
17
+func getToken(username, password string, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *utils.HTTPRequestFactory) (token string, err error) {
18
+	realm, ok := params["realm"]
19
+	if !ok {
20
+		return "", errors.New("no realm specified for token auth challenge")
21
+	}
22
+
23
+	realmURL, err := url.Parse(realm)
24
+	if err != nil {
25
+		return "", fmt.Errorf("invalid token auth challenge realm: %s", err)
26
+	}
27
+
28
+	if realmURL.Scheme == "" {
29
+		if registryEndpoint.IsSecure {
30
+			realmURL.Scheme = "https"
31
+		} else {
32
+			realmURL.Scheme = "http"
33
+		}
34
+	}
35
+
36
+	req, err := factory.NewRequest("GET", realmURL.String(), nil)
37
+	if err != nil {
38
+		return "", err
39
+	}
40
+
41
+	reqParams := req.URL.Query()
42
+	service := params["service"]
43
+	scope := params["scope"]
44
+
45
+	if service != "" {
46
+		reqParams.Add("service", service)
47
+	}
48
+
49
+	for _, scopeField := range strings.Fields(scope) {
50
+		reqParams.Add("scope", scopeField)
51
+	}
52
+
53
+	reqParams.Add("account", username)
54
+
55
+	req.URL.RawQuery = reqParams.Encode()
56
+	req.SetBasicAuth(username, password)
57
+
58
+	resp, err := client.Do(req)
59
+	if err != nil {
60
+		return "", err
61
+	}
62
+	defer resp.Body.Close()
63
+
64
+	if resp.StatusCode != http.StatusOK {
65
+		return "", fmt.Errorf("token auth attempt for registry %s: %s request failed with status: %d %s", registryEndpoint, req.URL, resp.StatusCode, http.StatusText(resp.StatusCode))
66
+	}
67
+
68
+	decoder := json.NewDecoder(resp.Body)
69
+
70
+	tr := new(tokenResponse)
71
+	if err = decoder.Decode(tr); err != nil {
72
+		return "", fmt.Errorf("unable to decode token response: %s", err)
73
+	}
74
+
75
+	if tr.Token == "" {
76
+		return "", errors.New("authorization server did not include a token in the response")
77
+	}
78
+
79
+	return tr.Token, nil
80
+}
... ...
@@ -55,14 +55,15 @@ func (av APIVersion) String() string {
55 55
 	return apiVersions[av]
56 56
 }
57 57
 
58
-var DefaultAPIVersion APIVersion = APIVersion1
59 58
 var apiVersions = map[APIVersion]string{
60 59
 	1: "v1",
61 60
 	2: "v2",
62 61
 }
63 62
 
63
+// API Version identifiers.
64 64
 const (
65
-	APIVersion1 = iota + 1
65
+	APIVersionUnknown = iota
66
+	APIVersion1
66 67
 	APIVersion2
67 68
 )
68 69
 
69 70
new file mode 100644
... ...
@@ -0,0 +1,144 @@
0
+package v2
1
+
2
+import "net/http"
3
+
4
+// TODO(stevvooe): Add route descriptors for each named route, along with
5
+// accepted methods, parameters, returned status codes and error codes.
6
+
7
+// ErrorDescriptor provides relevant information about a given error code.
8
+type ErrorDescriptor struct {
9
+	// Code is the error code that this descriptor describes.
10
+	Code ErrorCode
11
+
12
+	// Value provides a unique, string key, often captilized with
13
+	// underscores, to identify the error code. This value is used as the
14
+	// keyed value when serializing api errors.
15
+	Value string
16
+
17
+	// Message is a short, human readable decription of the error condition
18
+	// included in API responses.
19
+	Message string
20
+
21
+	// Description provides a complete account of the errors purpose, suitable
22
+	// for use in documentation.
23
+	Description string
24
+
25
+	// HTTPStatusCodes provides a list of status under which this error
26
+	// condition may arise. If it is empty, the error condition may be seen
27
+	// for any status code.
28
+	HTTPStatusCodes []int
29
+}
30
+
31
+// ErrorDescriptors provides a list of HTTP API Error codes that may be
32
+// encountered when interacting with the registry API.
33
+var ErrorDescriptors = []ErrorDescriptor{
34
+	{
35
+		Code:    ErrorCodeUnknown,
36
+		Value:   "UNKNOWN",
37
+		Message: "unknown error",
38
+		Description: `Generic error returned when the error does not have an
39
+		API classification.`,
40
+	},
41
+	{
42
+		Code:    ErrorCodeDigestInvalid,
43
+		Value:   "DIGEST_INVALID",
44
+		Message: "provided digest did not match uploaded content",
45
+		Description: `When a blob is uploaded, the registry will check that
46
+		the content matches the digest provided by the client. The error may
47
+		include a detail structure with the key "digest", including the
48
+		invalid digest string. This error may also be returned when a manifest
49
+		includes an invalid layer digest.`,
50
+		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
51
+	},
52
+	{
53
+		Code:    ErrorCodeSizeInvalid,
54
+		Value:   "SIZE_INVALID",
55
+		Message: "provided length did not match content length",
56
+		Description: `When a layer is uploaded, the provided size will be
57
+		checked against the uploaded content. If they do not match, this error
58
+		will be returned.`,
59
+		HTTPStatusCodes: []int{http.StatusBadRequest},
60
+	},
61
+	{
62
+		Code:    ErrorCodeNameInvalid,
63
+		Value:   "NAME_INVALID",
64
+		Message: "manifest name did not match URI",
65
+		Description: `During a manifest upload, if the name in the manifest
66
+		does not match the uri name, this error will be returned.`,
67
+		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
68
+	},
69
+	{
70
+		Code:    ErrorCodeTagInvalid,
71
+		Value:   "TAG_INVALID",
72
+		Message: "manifest tag did not match URI",
73
+		Description: `During a manifest upload, if the tag in the manifest
74
+		does not match the uri tag, this error will be returned.`,
75
+		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
76
+	},
77
+	{
78
+		Code:    ErrorCodeNameUnknown,
79
+		Value:   "NAME_UNKNOWN",
80
+		Message: "repository name not known to registry",
81
+		Description: `This is returned if the name used during an operation is
82
+		unknown to the registry.`,
83
+		HTTPStatusCodes: []int{http.StatusNotFound},
84
+	},
85
+	{
86
+		Code:    ErrorCodeManifestUnknown,
87
+		Value:   "MANIFEST_UNKNOWN",
88
+		Message: "manifest unknown",
89
+		Description: `This error is returned when the manifest, identified by
90
+		name and tag is unknown to the repository.`,
91
+		HTTPStatusCodes: []int{http.StatusNotFound},
92
+	},
93
+	{
94
+		Code:    ErrorCodeManifestInvalid,
95
+		Value:   "MANIFEST_INVALID",
96
+		Message: "manifest invalid",
97
+		Description: `During upload, manifests undergo several checks ensuring
98
+		validity. If those checks fail, this error may be returned, unless a
99
+		more specific error is included. The detail will contain information
100
+		the failed validation.`,
101
+		HTTPStatusCodes: []int{http.StatusBadRequest},
102
+	},
103
+	{
104
+		Code:    ErrorCodeManifestUnverified,
105
+		Value:   "MANIFEST_UNVERIFIED",
106
+		Message: "manifest failed signature verification",
107
+		Description: `During manifest upload, if the manifest fails signature
108
+		verification, this error will be returned.`,
109
+		HTTPStatusCodes: []int{http.StatusBadRequest},
110
+	},
111
+	{
112
+		Code:    ErrorCodeBlobUnknown,
113
+		Value:   "BLOB_UNKNOWN",
114
+		Message: "blob unknown to registry",
115
+		Description: `This error may be returned when a blob is unknown to the
116
+		registry in a specified repository. This can be returned with a
117
+		standard get or if a manifest references an unknown layer during
118
+		upload.`,
119
+		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
120
+	},
121
+
122
+	{
123
+		Code:    ErrorCodeBlobUploadUnknown,
124
+		Value:   "BLOB_UPLOAD_UNKNOWN",
125
+		Message: "blob upload unknown to registry",
126
+		Description: `If a blob upload has been cancelled or was never
127
+		started, this error code may be returned.`,
128
+		HTTPStatusCodes: []int{http.StatusNotFound},
129
+	},
130
+}
131
+
132
+var errorCodeToDescriptors map[ErrorCode]ErrorDescriptor
133
+var idToDescriptors map[string]ErrorDescriptor
134
+
135
+func init() {
136
+	errorCodeToDescriptors = make(map[ErrorCode]ErrorDescriptor, len(ErrorDescriptors))
137
+	idToDescriptors = make(map[string]ErrorDescriptor, len(ErrorDescriptors))
138
+
139
+	for _, descriptor := range ErrorDescriptors {
140
+		errorCodeToDescriptors[descriptor.Code] = descriptor
141
+		idToDescriptors[descriptor.Value] = descriptor
142
+	}
143
+}
0 144
new file mode 100644
... ...
@@ -0,0 +1,13 @@
0
+// Package v2 describes routes, urls and the error codes used in the Docker
1
+// Registry JSON HTTP API V2. In addition to declarations, descriptors are
2
+// provided for routes and error codes that can be used for implementation and
3
+// automatically generating documentation.
4
+//
5
+// Definitions here are considered to be locked down for the V2 registry api.
6
+// Any changes must be considered carefully and should not proceed without a
7
+// change proposal.
8
+//
9
+// Currently, while the HTTP API definitions are considered stable, the Go API
10
+// exports are considered unstable. Go API consumers should take care when
11
+// relying on these definitions until this message is deleted.
12
+package v2
0 13
new file mode 100644
... ...
@@ -0,0 +1,185 @@
0
+package v2
1
+
2
+import (
3
+	"fmt"
4
+	"strings"
5
+)
6
+
7
+// ErrorCode represents the error type. The errors are serialized via strings
8
+// and the integer format may change and should *never* be exported.
9
+type ErrorCode int
10
+
11
+const (
12
+	// ErrorCodeUnknown is a catch-all for errors not defined below.
13
+	ErrorCodeUnknown ErrorCode = iota
14
+
15
+	// ErrorCodeDigestInvalid is returned when uploading a blob if the
16
+	// provided digest does not match the blob contents.
17
+	ErrorCodeDigestInvalid
18
+
19
+	// ErrorCodeSizeInvalid is returned when uploading a blob if the provided
20
+	// size does not match the content length.
21
+	ErrorCodeSizeInvalid
22
+
23
+	// ErrorCodeNameInvalid is returned when the name in the manifest does not
24
+	// match the provided name.
25
+	ErrorCodeNameInvalid
26
+
27
+	// ErrorCodeTagInvalid is returned when the tag in the manifest does not
28
+	// match the provided tag.
29
+	ErrorCodeTagInvalid
30
+
31
+	// ErrorCodeNameUnknown when the repository name is not known.
32
+	ErrorCodeNameUnknown
33
+
34
+	// ErrorCodeManifestUnknown returned when image manifest is unknown.
35
+	ErrorCodeManifestUnknown
36
+
37
+	// ErrorCodeManifestInvalid returned when an image manifest is invalid,
38
+	// typically during a PUT operation. This error encompasses all errors
39
+	// encountered during manifest validation that aren't signature errors.
40
+	ErrorCodeManifestInvalid
41
+
42
+	// ErrorCodeManifestUnverified is returned when the manifest fails
43
+	// signature verfication.
44
+	ErrorCodeManifestUnverified
45
+
46
+	// ErrorCodeBlobUnknown is returned when a blob is unknown to the
47
+	// registry. This can happen when the manifest references a nonexistent
48
+	// layer or the result is not found by a blob fetch.
49
+	ErrorCodeBlobUnknown
50
+
51
+	// ErrorCodeBlobUploadUnknown is returned when an upload is unknown.
52
+	ErrorCodeBlobUploadUnknown
53
+)
54
+
55
+// ParseErrorCode attempts to parse the error code string, returning
56
+// ErrorCodeUnknown if the error is not known.
57
+func ParseErrorCode(s string) ErrorCode {
58
+	desc, ok := idToDescriptors[s]
59
+
60
+	if !ok {
61
+		return ErrorCodeUnknown
62
+	}
63
+
64
+	return desc.Code
65
+}
66
+
67
+// Descriptor returns the descriptor for the error code.
68
+func (ec ErrorCode) Descriptor() ErrorDescriptor {
69
+	d, ok := errorCodeToDescriptors[ec]
70
+
71
+	if !ok {
72
+		return ErrorCodeUnknown.Descriptor()
73
+	}
74
+
75
+	return d
76
+}
77
+
78
+// String returns the canonical identifier for this error code.
79
+func (ec ErrorCode) String() string {
80
+	return ec.Descriptor().Value
81
+}
82
+
83
+// Message returned the human-readable error message for this error code.
84
+func (ec ErrorCode) Message() string {
85
+	return ec.Descriptor().Message
86
+}
87
+
88
+// MarshalText encodes the receiver into UTF-8-encoded text and returns the
89
+// result.
90
+func (ec ErrorCode) MarshalText() (text []byte, err error) {
91
+	return []byte(ec.String()), nil
92
+}
93
+
94
+// UnmarshalText decodes the form generated by MarshalText.
95
+func (ec *ErrorCode) UnmarshalText(text []byte) error {
96
+	desc, ok := idToDescriptors[string(text)]
97
+
98
+	if !ok {
99
+		desc = ErrorCodeUnknown.Descriptor()
100
+	}
101
+
102
+	*ec = desc.Code
103
+
104
+	return nil
105
+}
106
+
107
+// Error provides a wrapper around ErrorCode with extra Details provided.
108
+type Error struct {
109
+	Code    ErrorCode   `json:"code"`
110
+	Message string      `json:"message,omitempty"`
111
+	Detail  interface{} `json:"detail,omitempty"`
112
+}
113
+
114
+// Error returns a human readable representation of the error.
115
+func (e Error) Error() string {
116
+	return fmt.Sprintf("%s: %s",
117
+		strings.ToLower(strings.Replace(e.Code.String(), "_", " ", -1)),
118
+		e.Message)
119
+}
120
+
121
+// Errors provides the envelope for multiple errors and a few sugar methods
122
+// for use within the application.
123
+type Errors struct {
124
+	Errors []Error `json:"errors,omitempty"`
125
+}
126
+
127
+// Push pushes an error on to the error stack, with the optional detail
128
+// argument. It is a programming error (ie panic) to push more than one
129
+// detail at a time.
130
+func (errs *Errors) Push(code ErrorCode, details ...interface{}) {
131
+	if len(details) > 1 {
132
+		panic("please specify zero or one detail items for this error")
133
+	}
134
+
135
+	var detail interface{}
136
+	if len(details) > 0 {
137
+		detail = details[0]
138
+	}
139
+
140
+	if err, ok := detail.(error); ok {
141
+		detail = err.Error()
142
+	}
143
+
144
+	errs.PushErr(Error{
145
+		Code:    code,
146
+		Message: code.Message(),
147
+		Detail:  detail,
148
+	})
149
+}
150
+
151
+// PushErr pushes an error interface onto the error stack.
152
+func (errs *Errors) PushErr(err error) {
153
+	switch err.(type) {
154
+	case Error:
155
+		errs.Errors = append(errs.Errors, err.(Error))
156
+	default:
157
+		errs.Errors = append(errs.Errors, Error{Message: err.Error()})
158
+	}
159
+}
160
+
161
+func (errs *Errors) Error() string {
162
+	switch errs.Len() {
163
+	case 0:
164
+		return "<nil>"
165
+	case 1:
166
+		return errs.Errors[0].Error()
167
+	default:
168
+		msg := "errors:\n"
169
+		for _, err := range errs.Errors {
170
+			msg += err.Error() + "\n"
171
+		}
172
+		return msg
173
+	}
174
+}
175
+
176
+// Clear clears the errors.
177
+func (errs *Errors) Clear() {
178
+	errs.Errors = errs.Errors[:0]
179
+}
180
+
181
+// Len returns the current number of errors.
182
+func (errs *Errors) Len() int {
183
+	return len(errs.Errors)
184
+}
0 185
new file mode 100644
... ...
@@ -0,0 +1,163 @@
0
+package v2
1
+
2
+import (
3
+	"encoding/json"
4
+	"reflect"
5
+	"testing"
6
+)
7
+
8
+// TestErrorCodes ensures that error code format, mappings and
9
+// marshaling/unmarshaling. round trips are stable.
10
+func TestErrorCodes(t *testing.T) {
11
+	for _, desc := range ErrorDescriptors {
12
+		if desc.Code.String() != desc.Value {
13
+			t.Fatalf("error code string incorrect: %q != %q", desc.Code.String(), desc.Value)
14
+		}
15
+
16
+		if desc.Code.Message() != desc.Message {
17
+			t.Fatalf("incorrect message for error code %v: %q != %q", desc.Code, desc.Code.Message(), desc.Message)
18
+		}
19
+
20
+		// Serialize the error code using the json library to ensure that we
21
+		// get a string and it works round trip.
22
+		p, err := json.Marshal(desc.Code)
23
+
24
+		if err != nil {
25
+			t.Fatalf("error marshaling error code %v: %v", desc.Code, err)
26
+		}
27
+
28
+		if len(p) <= 0 {
29
+			t.Fatalf("expected content in marshaled before for error code %v", desc.Code)
30
+		}
31
+
32
+		// First, unmarshal to interface and ensure we have a string.
33
+		var ecUnspecified interface{}
34
+		if err := json.Unmarshal(p, &ecUnspecified); err != nil {
35
+			t.Fatalf("error unmarshaling error code %v: %v", desc.Code, err)
36
+		}
37
+
38
+		if _, ok := ecUnspecified.(string); !ok {
39
+			t.Fatalf("expected a string for error code %v on unmarshal got a %T", desc.Code, ecUnspecified)
40
+		}
41
+
42
+		// Now, unmarshal with the error code type and ensure they are equal
43
+		var ecUnmarshaled ErrorCode
44
+		if err := json.Unmarshal(p, &ecUnmarshaled); err != nil {
45
+			t.Fatalf("error unmarshaling error code %v: %v", desc.Code, err)
46
+		}
47
+
48
+		if ecUnmarshaled != desc.Code {
49
+			t.Fatalf("unexpected error code during error code marshal/unmarshal: %v != %v", ecUnmarshaled, desc.Code)
50
+		}
51
+	}
52
+}
53
+
54
+// TestErrorsManagement does a quick check of the Errors type to ensure that
55
+// members are properly pushed and marshaled.
56
+func TestErrorsManagement(t *testing.T) {
57
+	var errs Errors
58
+
59
+	errs.Push(ErrorCodeDigestInvalid)
60
+	errs.Push(ErrorCodeBlobUnknown,
61
+		map[string]string{"digest": "sometestblobsumdoesntmatter"})
62
+
63
+	p, err := json.Marshal(errs)
64
+
65
+	if err != nil {
66
+		t.Fatalf("error marashaling errors: %v", err)
67
+	}
68
+
69
+	expectedJSON := "{\"errors\":[{\"code\":\"DIGEST_INVALID\",\"message\":\"provided digest did not match uploaded content\"},{\"code\":\"BLOB_UNKNOWN\",\"message\":\"blob unknown to registry\",\"detail\":{\"digest\":\"sometestblobsumdoesntmatter\"}}]}"
70
+
71
+	if string(p) != expectedJSON {
72
+		t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)
73
+	}
74
+
75
+	errs.Clear()
76
+	errs.Push(ErrorCodeUnknown)
77
+	expectedJSON = "{\"errors\":[{\"code\":\"UNKNOWN\",\"message\":\"unknown error\"}]}"
78
+	p, err = json.Marshal(errs)
79
+
80
+	if err != nil {
81
+		t.Fatalf("error marashaling errors: %v", err)
82
+	}
83
+
84
+	if string(p) != expectedJSON {
85
+		t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)
86
+	}
87
+}
88
+
89
+// TestMarshalUnmarshal ensures that api errors can round trip through json
90
+// without losing information.
91
+func TestMarshalUnmarshal(t *testing.T) {
92
+
93
+	var errors Errors
94
+
95
+	for _, testcase := range []struct {
96
+		description string
97
+		err         Error
98
+	}{
99
+		{
100
+			description: "unknown error",
101
+			err: Error{
102
+
103
+				Code:    ErrorCodeUnknown,
104
+				Message: ErrorCodeUnknown.Descriptor().Message,
105
+			},
106
+		},
107
+		{
108
+			description: "unknown manifest",
109
+			err: Error{
110
+				Code:    ErrorCodeManifestUnknown,
111
+				Message: ErrorCodeManifestUnknown.Descriptor().Message,
112
+			},
113
+		},
114
+		{
115
+			description: "unknown manifest",
116
+			err: Error{
117
+				Code:    ErrorCodeBlobUnknown,
118
+				Message: ErrorCodeBlobUnknown.Descriptor().Message,
119
+				Detail:  map[string]interface{}{"digest": "asdfqwerqwerqwerqwer"},
120
+			},
121
+		},
122
+	} {
123
+		fatalf := func(format string, args ...interface{}) {
124
+			t.Fatalf(testcase.description+": "+format, args...)
125
+		}
126
+
127
+		unexpectedErr := func(err error) {
128
+			fatalf("unexpected error: %v", err)
129
+		}
130
+
131
+		p, err := json.Marshal(testcase.err)
132
+		if err != nil {
133
+			unexpectedErr(err)
134
+		}
135
+
136
+		var unmarshaled Error
137
+		if err := json.Unmarshal(p, &unmarshaled); err != nil {
138
+			unexpectedErr(err)
139
+		}
140
+
141
+		if !reflect.DeepEqual(unmarshaled, testcase.err) {
142
+			fatalf("errors not equal after round trip: %#v != %#v", unmarshaled, testcase.err)
143
+		}
144
+
145
+		// Roll everything up into an error response envelope.
146
+		errors.PushErr(testcase.err)
147
+	}
148
+
149
+	p, err := json.Marshal(errors)
150
+	if err != nil {
151
+		t.Fatalf("unexpected error marshaling error envelope: %v", err)
152
+	}
153
+
154
+	var unmarshaled Errors
155
+	if err := json.Unmarshal(p, &unmarshaled); err != nil {
156
+		t.Fatalf("unexpected error unmarshaling error envelope: %v", err)
157
+	}
158
+
159
+	if !reflect.DeepEqual(unmarshaled, errors) {
160
+		t.Fatalf("errors not equal after round trip: %#v != %#v", unmarshaled, errors)
161
+	}
162
+}
0 163
new file mode 100644
... ...
@@ -0,0 +1,19 @@
0
+package v2
1
+
2
+import "regexp"
3
+
4
+// This file defines regular expressions for use in route definition. These
5
+// are also defined in the registry code base. Until they are in a common,
6
+// shared location, and exported, they must be repeated here.
7
+
8
+// RepositoryNameComponentRegexp restricts registtry path components names to
9
+// start with at least two letters or numbers, with following parts able to
10
+// separated by one period, dash or underscore.
11
+var RepositoryNameComponentRegexp = regexp.MustCompile(`[a-z0-9]+(?:[._-][a-z0-9]+)*`)
12
+
13
+// RepositoryNameRegexp builds on RepositoryNameComponentRegexp to allow 2 to
14
+// 5 path components, separated by a forward slash.
15
+var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameComponentRegexp.String() + `/){1,4}` + RepositoryNameComponentRegexp.String())
16
+
17
+// TagNameRegexp matches valid tag names. From docker/docker:graph/tags.go.
18
+var TagNameRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`)
0 19
new file mode 100644
... ...
@@ -0,0 +1,66 @@
0
+package v2
1
+
2
+import "github.com/gorilla/mux"
3
+
4
+// The following are definitions of the name under which all V2 routes are
5
+// registered. These symbols can be used to look up a route based on the name.
6
+const (
7
+	RouteNameBase            = "base"
8
+	RouteNameManifest        = "manifest"
9
+	RouteNameTags            = "tags"
10
+	RouteNameBlob            = "blob"
11
+	RouteNameBlobUpload      = "blob-upload"
12
+	RouteNameBlobUploadChunk = "blob-upload-chunk"
13
+)
14
+
15
+var allEndpoints = []string{
16
+	RouteNameManifest,
17
+	RouteNameTags,
18
+	RouteNameBlob,
19
+	RouteNameBlobUpload,
20
+	RouteNameBlobUploadChunk,
21
+}
22
+
23
+// Router builds a gorilla router with named routes for the various API
24
+// methods. This can be used directly by both server implementations and
25
+// clients.
26
+func Router() *mux.Router {
27
+	router := mux.NewRouter().
28
+		StrictSlash(true)
29
+
30
+	// GET /v2/	Check	Check that the registry implements API version 2(.1)
31
+	router.
32
+		Path("/v2/").
33
+		Name(RouteNameBase)
34
+
35
+	// GET      /v2/<name>/manifest/<tag>	Image Manifest	Fetch the image manifest identified by name and tag.
36
+	// PUT      /v2/<name>/manifest/<tag>	Image Manifest	Upload the image manifest identified by name and tag.
37
+	// DELETE   /v2/<name>/manifest/<tag>	Image Manifest	Delete the image identified by name and tag.
38
+	router.
39
+		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/manifests/{tag:" + TagNameRegexp.String() + "}").
40
+		Name(RouteNameManifest)
41
+
42
+	// GET	/v2/<name>/tags/list	Tags	Fetch the tags under the repository identified by name.
43
+	router.
44
+		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/tags/list").
45
+		Name(RouteNameTags)
46
+
47
+	// GET	/v2/<name>/blob/<digest>	Layer	Fetch the blob identified by digest.
48
+	router.
49
+		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/{digest:[a-zA-Z0-9-_+.]+:[a-zA-Z0-9-_+.=]+}").
50
+		Name(RouteNameBlob)
51
+
52
+	// POST	/v2/<name>/blob/upload/	Layer Upload	Initiate an upload of the layer identified by tarsum.
53
+	router.
54
+		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/").
55
+		Name(RouteNameBlobUpload)
56
+
57
+	// GET	/v2/<name>/blob/upload/<uuid>	Layer Upload	Get the status of the upload identified by tarsum and uuid.
58
+	// PUT	/v2/<name>/blob/upload/<uuid>	Layer Upload	Upload all or a chunk of the upload identified by tarsum and uuid.
59
+	// DELETE	/v2/<name>/blob/upload/<uuid>	Layer Upload	Cancel the upload identified by layer and uuid
60
+	router.
61
+		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid}").
62
+		Name(RouteNameBlobUploadChunk)
63
+
64
+	return router
65
+}
0 66
new file mode 100644
... ...
@@ -0,0 +1,184 @@
0
+package v2
1
+
2
+import (
3
+	"encoding/json"
4
+	"net/http"
5
+	"net/http/httptest"
6
+	"reflect"
7
+	"testing"
8
+
9
+	"github.com/gorilla/mux"
10
+)
11
+
12
+type routeTestCase struct {
13
+	RequestURI string
14
+	Vars       map[string]string
15
+	RouteName  string
16
+	StatusCode int
17
+}
18
+
19
+// TestRouter registers a test handler with all the routes and ensures that
20
+// each route returns the expected path variables. Not method verification is
21
+// present. This not meant to be exhaustive but as check to ensure that the
22
+// expected variables are extracted.
23
+//
24
+// This may go away as the application structure comes together.
25
+func TestRouter(t *testing.T) {
26
+
27
+	router := Router()
28
+
29
+	testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
30
+		testCase := routeTestCase{
31
+			RequestURI: r.RequestURI,
32
+			Vars:       mux.Vars(r),
33
+			RouteName:  mux.CurrentRoute(r).GetName(),
34
+		}
35
+
36
+		enc := json.NewEncoder(w)
37
+
38
+		if err := enc.Encode(testCase); err != nil {
39
+			http.Error(w, err.Error(), http.StatusInternalServerError)
40
+			return
41
+		}
42
+	})
43
+
44
+	// Startup test server
45
+	server := httptest.NewServer(router)
46
+
47
+	for _, testcase := range []routeTestCase{
48
+		{
49
+			RouteName:  RouteNameBase,
50
+			RequestURI: "/v2/",
51
+			Vars:       map[string]string{},
52
+		},
53
+		{
54
+			RouteName:  RouteNameManifest,
55
+			RequestURI: "/v2/foo/bar/manifests/tag",
56
+			Vars: map[string]string{
57
+				"name": "foo/bar",
58
+				"tag":  "tag",
59
+			},
60
+		},
61
+		{
62
+			RouteName:  RouteNameTags,
63
+			RequestURI: "/v2/foo/bar/tags/list",
64
+			Vars: map[string]string{
65
+				"name": "foo/bar",
66
+			},
67
+		},
68
+		{
69
+			RouteName:  RouteNameBlob,
70
+			RequestURI: "/v2/foo/bar/blobs/tarsum.dev+foo:abcdef0919234",
71
+			Vars: map[string]string{
72
+				"name":   "foo/bar",
73
+				"digest": "tarsum.dev+foo:abcdef0919234",
74
+			},
75
+		},
76
+		{
77
+			RouteName:  RouteNameBlob,
78
+			RequestURI: "/v2/foo/bar/blobs/sha256:abcdef0919234",
79
+			Vars: map[string]string{
80
+				"name":   "foo/bar",
81
+				"digest": "sha256:abcdef0919234",
82
+			},
83
+		},
84
+		{
85
+			RouteName:  RouteNameBlobUpload,
86
+			RequestURI: "/v2/foo/bar/blobs/uploads/",
87
+			Vars: map[string]string{
88
+				"name": "foo/bar",
89
+			},
90
+		},
91
+		{
92
+			RouteName:  RouteNameBlobUploadChunk,
93
+			RequestURI: "/v2/foo/bar/blobs/uploads/uuid",
94
+			Vars: map[string]string{
95
+				"name": "foo/bar",
96
+				"uuid": "uuid",
97
+			},
98
+		},
99
+		{
100
+			RouteName:  RouteNameBlobUploadChunk,
101
+			RequestURI: "/v2/foo/bar/blobs/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
102
+			Vars: map[string]string{
103
+				"name": "foo/bar",
104
+				"uuid": "D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
105
+			},
106
+		},
107
+		{
108
+			RouteName:  RouteNameBlobUploadChunk,
109
+			RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==",
110
+			Vars: map[string]string{
111
+				"name": "foo/bar",
112
+				"uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==",
113
+			},
114
+		},
115
+		{
116
+			// Check ambiguity: ensure we can distinguish between tags for
117
+			// "foo/bar/image/image" and image for "foo/bar/image" with tag
118
+			// "tags"
119
+			RouteName:  RouteNameManifest,
120
+			RequestURI: "/v2/foo/bar/manifests/manifests/tags",
121
+			Vars: map[string]string{
122
+				"name": "foo/bar/manifests",
123
+				"tag":  "tags",
124
+			},
125
+		},
126
+		{
127
+			// This case presents an ambiguity between foo/bar with tag="tags"
128
+			// and list tags for "foo/bar/manifest"
129
+			RouteName:  RouteNameTags,
130
+			RequestURI: "/v2/foo/bar/manifests/tags/list",
131
+			Vars: map[string]string{
132
+				"name": "foo/bar/manifests",
133
+			},
134
+		},
135
+		{
136
+			RouteName:  RouteNameBlobUploadChunk,
137
+			RequestURI: "/v2/foo/../../blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
138
+			StatusCode: http.StatusNotFound,
139
+		},
140
+	} {
141
+		// Register the endpoint
142
+		router.GetRoute(testcase.RouteName).Handler(testHandler)
143
+		u := server.URL + testcase.RequestURI
144
+
145
+		resp, err := http.Get(u)
146
+
147
+		if err != nil {
148
+			t.Fatalf("error issuing get request: %v", err)
149
+		}
150
+
151
+		if testcase.StatusCode == 0 {
152
+			// Override default, zero-value
153
+			testcase.StatusCode = http.StatusOK
154
+		}
155
+
156
+		if resp.StatusCode != testcase.StatusCode {
157
+			t.Fatalf("unexpected status for %s: %v %v", u, resp.Status, resp.StatusCode)
158
+		}
159
+
160
+		if testcase.StatusCode != http.StatusOK {
161
+			// We don't care about json response.
162
+			continue
163
+		}
164
+
165
+		dec := json.NewDecoder(resp.Body)
166
+
167
+		var actualRouteInfo routeTestCase
168
+		if err := dec.Decode(&actualRouteInfo); err != nil {
169
+			t.Fatalf("error reading json response: %v", err)
170
+		}
171
+		// Needs to be set out of band
172
+		actualRouteInfo.StatusCode = resp.StatusCode
173
+
174
+		if actualRouteInfo.RouteName != testcase.RouteName {
175
+			t.Fatalf("incorrect route %q matched, expected %q", actualRouteInfo.RouteName, testcase.RouteName)
176
+		}
177
+
178
+		if !reflect.DeepEqual(actualRouteInfo, testcase) {
179
+			t.Fatalf("actual does not equal expected: %#v != %#v", actualRouteInfo, testcase)
180
+		}
181
+	}
182
+
183
+}
0 184
new file mode 100644
... ...
@@ -0,0 +1,164 @@
0
+package v2
1
+
2
+import (
3
+	"net/http"
4
+	"net/url"
5
+
6
+	"github.com/gorilla/mux"
7
+)
8
+
9
+// URLBuilder creates registry API urls from a single base endpoint. It can be
10
+// used to create urls for use in a registry client or server.
11
+//
12
+// All urls will be created from the given base, including the api version.
13
+// For example, if a root of "/foo/" is provided, urls generated will be fall
14
+// under "/foo/v2/...". Most application will only provide a schema, host and
15
+// port, such as "https://localhost:5000/".
16
+type URLBuilder struct {
17
+	root   *url.URL // url root (ie http://localhost/)
18
+	router *mux.Router
19
+}
20
+
21
+// NewURLBuilder creates a URLBuilder with provided root url object.
22
+func NewURLBuilder(root *url.URL) *URLBuilder {
23
+	return &URLBuilder{
24
+		root:   root,
25
+		router: Router(),
26
+	}
27
+}
28
+
29
+// NewURLBuilderFromString workes identically to NewURLBuilder except it takes
30
+// a string argument for the root, returning an error if it is not a valid
31
+// url.
32
+func NewURLBuilderFromString(root string) (*URLBuilder, error) {
33
+	u, err := url.Parse(root)
34
+	if err != nil {
35
+		return nil, err
36
+	}
37
+
38
+	return NewURLBuilder(u), nil
39
+}
40
+
41
+// NewURLBuilderFromRequest uses information from an *http.Request to
42
+// construct the root url.
43
+func NewURLBuilderFromRequest(r *http.Request) *URLBuilder {
44
+	u := &url.URL{
45
+		Scheme: r.URL.Scheme,
46
+		Host:   r.Host,
47
+	}
48
+
49
+	return NewURLBuilder(u)
50
+}
51
+
52
+// BuildBaseURL constructs a base url for the API, typically just "/v2/".
53
+func (ub *URLBuilder) BuildBaseURL() (string, error) {
54
+	route := ub.cloneRoute(RouteNameBase)
55
+
56
+	baseURL, err := route.URL()
57
+	if err != nil {
58
+		return "", err
59
+	}
60
+
61
+	return baseURL.String(), nil
62
+}
63
+
64
+// BuildTagsURL constructs a url to list the tags in the named repository.
65
+func (ub *URLBuilder) BuildTagsURL(name string) (string, error) {
66
+	route := ub.cloneRoute(RouteNameTags)
67
+
68
+	tagsURL, err := route.URL("name", name)
69
+	if err != nil {
70
+		return "", err
71
+	}
72
+
73
+	return tagsURL.String(), nil
74
+}
75
+
76
+// BuildManifestURL constructs a url for the manifest identified by name and tag.
77
+func (ub *URLBuilder) BuildManifestURL(name, tag string) (string, error) {
78
+	route := ub.cloneRoute(RouteNameManifest)
79
+
80
+	manifestURL, err := route.URL("name", name, "tag", tag)
81
+	if err != nil {
82
+		return "", err
83
+	}
84
+
85
+	return manifestURL.String(), nil
86
+}
87
+
88
+// BuildBlobURL constructs the url for the blob identified by name and dgst.
89
+func (ub *URLBuilder) BuildBlobURL(name string, dgst string) (string, error) {
90
+	route := ub.cloneRoute(RouteNameBlob)
91
+
92
+	layerURL, err := route.URL("name", name, "digest", dgst)
93
+	if err != nil {
94
+		return "", err
95
+	}
96
+
97
+	return layerURL.String(), nil
98
+}
99
+
100
+// BuildBlobUploadURL constructs a url to begin a blob upload in the
101
+// repository identified by name.
102
+func (ub *URLBuilder) BuildBlobUploadURL(name string, values ...url.Values) (string, error) {
103
+	route := ub.cloneRoute(RouteNameBlobUpload)
104
+
105
+	uploadURL, err := route.URL("name", name)
106
+	if err != nil {
107
+		return "", err
108
+	}
109
+
110
+	return appendValuesURL(uploadURL, values...).String(), nil
111
+}
112
+
113
+// BuildBlobUploadChunkURL constructs a url for the upload identified by uuid,
114
+// including any url values. This should generally not be used by clients, as
115
+// this url is provided by server implementations during the blob upload
116
+// process.
117
+func (ub *URLBuilder) BuildBlobUploadChunkURL(name, uuid string, values ...url.Values) (string, error) {
118
+	route := ub.cloneRoute(RouteNameBlobUploadChunk)
119
+
120
+	uploadURL, err := route.URL("name", name, "uuid", uuid)
121
+	if err != nil {
122
+		return "", err
123
+	}
124
+
125
+	return appendValuesURL(uploadURL, values...).String(), nil
126
+}
127
+
128
+// clondedRoute returns a clone of the named route from the router. Routes
129
+// must be cloned to avoid modifying them during url generation.
130
+func (ub *URLBuilder) cloneRoute(name string) *mux.Route {
131
+	route := new(mux.Route)
132
+	*route = *ub.router.GetRoute(name) // clone the route
133
+
134
+	return route.
135
+		Schemes(ub.root.Scheme).
136
+		Host(ub.root.Host)
137
+}
138
+
139
+// appendValuesURL appends the parameters to the url.
140
+func appendValuesURL(u *url.URL, values ...url.Values) *url.URL {
141
+	merged := u.Query()
142
+
143
+	for _, v := range values {
144
+		for k, vv := range v {
145
+			merged[k] = append(merged[k], vv...)
146
+		}
147
+	}
148
+
149
+	u.RawQuery = merged.Encode()
150
+	return u
151
+}
152
+
153
+// appendValues appends the parameters to the url. Panics if the string is not
154
+// a url.
155
+func appendValues(u string, values ...url.Values) string {
156
+	up, err := url.Parse(u)
157
+
158
+	if err != nil {
159
+		panic(err) // should never happen
160
+	}
161
+
162
+	return appendValuesURL(up, values...).String()
163
+}
0 164
new file mode 100644
... ...
@@ -0,0 +1,100 @@
0
+package v2
1
+
2
+import (
3
+	"net/url"
4
+	"testing"
5
+)
6
+
7
+type urlBuilderTestCase struct {
8
+	description string
9
+	expected    string
10
+	build       func() (string, error)
11
+}
12
+
13
+// TestURLBuilder tests the various url building functions, ensuring they are
14
+// returning the expected values.
15
+func TestURLBuilder(t *testing.T) {
16
+
17
+	root := "http://localhost:5000/"
18
+	urlBuilder, err := NewURLBuilderFromString(root)
19
+	if err != nil {
20
+		t.Fatalf("unexpected error creating urlbuilder: %v", err)
21
+	}
22
+
23
+	for _, testcase := range []struct {
24
+		description string
25
+		expected    string
26
+		build       func() (string, error)
27
+	}{
28
+		{
29
+			description: "test base url",
30
+			expected:    "http://localhost:5000/v2/",
31
+			build:       urlBuilder.BuildBaseURL,
32
+		},
33
+		{
34
+			description: "test tags url",
35
+			expected:    "http://localhost:5000/v2/foo/bar/tags/list",
36
+			build: func() (string, error) {
37
+				return urlBuilder.BuildTagsURL("foo/bar")
38
+			},
39
+		},
40
+		{
41
+			description: "test manifest url",
42
+			expected:    "http://localhost:5000/v2/foo/bar/manifests/tag",
43
+			build: func() (string, error) {
44
+				return urlBuilder.BuildManifestURL("foo/bar", "tag")
45
+			},
46
+		},
47
+		{
48
+			description: "build blob url",
49
+			expected:    "http://localhost:5000/v2/foo/bar/blobs/tarsum.v1+sha256:abcdef0123456789",
50
+			build: func() (string, error) {
51
+				return urlBuilder.BuildBlobURL("foo/bar", "tarsum.v1+sha256:abcdef0123456789")
52
+			},
53
+		},
54
+		{
55
+			description: "build blob upload url",
56
+			expected:    "http://localhost:5000/v2/foo/bar/blobs/uploads/",
57
+			build: func() (string, error) {
58
+				return urlBuilder.BuildBlobUploadURL("foo/bar")
59
+			},
60
+		},
61
+		{
62
+			description: "build blob upload url with digest and size",
63
+			expected:    "http://localhost:5000/v2/foo/bar/blobs/uploads/?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000",
64
+			build: func() (string, error) {
65
+				return urlBuilder.BuildBlobUploadURL("foo/bar", url.Values{
66
+					"size":   []string{"10000"},
67
+					"digest": []string{"tarsum.v1+sha256:abcdef0123456789"},
68
+				})
69
+			},
70
+		},
71
+		{
72
+			description: "build blob upload chunk url",
73
+			expected:    "http://localhost:5000/v2/foo/bar/blobs/uploads/uuid-part",
74
+			build: func() (string, error) {
75
+				return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part")
76
+			},
77
+		},
78
+		{
79
+			description: "build blob upload chunk url with digest and size",
80
+			expected:    "http://localhost:5000/v2/foo/bar/blobs/uploads/uuid-part?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000",
81
+			build: func() (string, error) {
82
+				return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part", url.Values{
83
+					"size":   []string{"10000"},
84
+					"digest": []string{"tarsum.v1+sha256:abcdef0123456789"},
85
+				})
86
+			},
87
+		},
88
+	} {
89
+		u, err := testcase.build()
90
+		if err != nil {
91
+			t.Fatalf("%s: error building url: %v", testcase.description, err)
92
+		}
93
+
94
+		if u != testcase.expected {
95
+			t.Fatalf("%s: %q != %q", testcase.description, u, testcase.expected)
96
+		}
97
+	}
98
+
99
+}
... ...
@@ -50,6 +50,9 @@ func (p *JSONProgress) String() string {
50 50
 	}
51 51
 	total := units.HumanSize(float64(p.Total))
52 52
 	percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
53
+	if percentage > 50 {
54
+		percentage = 50
55
+	}
53 56
 	if width > 110 {
54 57
 		// this number can't be negetive gh#7136
55 58
 		numSpaces := 0
... ...
@@ -30,7 +30,7 @@ func TestProgress(t *testing.T) {
30 30
 	}
31 31
 
32 32
 	// this number can't be negetive gh#7136
33
-	expected = "[==============================================================>]     50 B/40 B"
33
+	expected = "[==================================================>]     50 B/40 B"
34 34
 	jp4 := JSONProgress{Current: 50, Total: 40}
35 35
 	if jp4.String() != expected {
36 36
 		t.Fatalf("Expected %q, got %q", expected, jp4.String())