Client Support for Docker Registry HTTP API V2
| ... | ... |
@@ -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 := ®istry.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, ®istry.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, ®istry.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())
|