diff --git a/Dockerfile b/Dockerfile
index 5329cee7..bf4e0d7f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,4 +16,4 @@ RUN make PREFIX=/go clean binaries
VOLUME ["/var/lib/registry"]
EXPOSE 5000
ENTRYPOINT ["registry"]
-CMD ["/etc/docker/registry/config.yml"]
+CMD ["serve", "/etc/docker/registry/config.yml"]
diff --git a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/LICENSE.txt b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/LICENSE.txt
new file mode 100644
index 00000000..d6456956
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/NOTICE.txt b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/NOTICE.txt
new file mode 100644
index 00000000..5f14d116
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/NOTICE.txt
@@ -0,0 +1,3 @@
+AWS SDK for Go
+Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+Copyright 2014-2015 Stripe, Inc.
diff --git a/Godeps/_workspace/src/github.com/docker/goamz/LICENSE b/Godeps/_workspace/src/github.com/docker/goamz/LICENSE
new file mode 100644
index 00000000..53320c35
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/docker/goamz/LICENSE
@@ -0,0 +1,185 @@
+This software is licensed under the LGPLv3, included below.
+
+As a special exception to the GNU Lesser General Public License version 3
+("LGPL3"), the copyright holders of this Library give you permission to
+convey to a third party a Combined Work that links statically or dynamically
+to this Library without providing any Minimal Corresponding Source or
+Minimal Application Code as set out in 4d or providing the installation
+information set out in section 4e, provided that you comply with the other
+provisions of LGPL3 and provided that you meet, for the Application the
+terms and conditions of the license(s) which apply to the Application.
+
+Except as stated in this special exception, the provisions of LGPL3 will
+continue to comply in full to this Library. If you modify this Library, you
+may apply this exception to your version of this Library, but you are not
+obliged to do so. If you do not wish to do so, delete this exception
+statement from your version. This exception does not (and cannot) modify any
+license terms which apply to the Application, with which you must still
+comply.
+
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/Godeps/_workspace/src/github.com/gorilla/handlers/LICENSE b/Godeps/_workspace/src/github.com/gorilla/handlers/LICENSE
new file mode 100644
index 00000000..66ea3c8a
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/gorilla/handlers/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2013 The Gorilla Handlers Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Godeps/_workspace/src/github.com/gorilla/handlers/canonical.go b/Godeps/_workspace/src/github.com/gorilla/handlers/canonical.go
new file mode 100644
index 00000000..3961695c
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/gorilla/handlers/canonical.go
@@ -0,0 +1,71 @@
+package handlers
+
+import (
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+type canonical struct {
+ h http.Handler
+ domain string
+ code int
+}
+
+// CanonicalHost is HTTP middleware that re-directs requests to the canonical
+// domain. It accepts a domain and a status code (e.g. 301 or 302) and
+// re-directs clients to this domain. The existing request path is maintained.
+//
+// Note: If the provided domain is considered invalid by url.Parse or otherwise
+// returns an empty scheme or host, clients are not re-directed.
+// not re-directed.
+//
+// Example:
+//
+// r := mux.NewRouter()
+// canonical := handlers.CanonicalHost("http://www.gorillatoolkit.org", 302)
+// r.HandleFunc("/route", YourHandler)
+//
+// log.Fatal(http.ListenAndServe(":7000", canonical(r)))
+//
+func CanonicalHost(domain string, code int) func(h http.Handler) http.Handler {
+ fn := func(h http.Handler) http.Handler {
+ return canonical{h, domain, code}
+ }
+
+ return fn
+}
+
+func (c canonical) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ dest, err := url.Parse(c.domain)
+ if err != nil {
+ // Call the next handler if the provided domain fails to parse.
+ c.h.ServeHTTP(w, r)
+ return
+ }
+
+ if dest.Scheme == "" || dest.Host == "" {
+ // Call the next handler if the scheme or host are empty.
+ // Note that url.Parse won't fail on in this case.
+ c.h.ServeHTTP(w, r)
+ return
+ }
+
+ if !strings.EqualFold(cleanHost(r.Host), dest.Host) {
+ // Re-build the destination URL
+ dest := dest.Scheme + "://" + dest.Host + r.URL.Path
+ http.Redirect(w, r, dest, c.code)
+ }
+
+ c.h.ServeHTTP(w, r)
+}
+
+// cleanHost cleans invalid Host headers by stripping anything after '/' or ' '.
+// This is backported from Go 1.5 (in response to issue #11206) and attempts to
+// mitigate malformed Host headers that do not match the format in RFC7230.
+func cleanHost(in string) string {
+ if i := strings.IndexAny(in, " /"); i != -1 {
+ return in[:i]
+ }
+ return in
+}
diff --git a/Godeps/_workspace/src/github.com/gorilla/handlers/doc.go b/Godeps/_workspace/src/github.com/gorilla/handlers/doc.go
new file mode 100644
index 00000000..944e5a8a
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/gorilla/handlers/doc.go
@@ -0,0 +1,9 @@
+/*
+Package handlers is a collection of handlers (aka "HTTP middleware") for use
+with Go's net/http package (or any framework supporting http.Handler).
+
+The package includes handlers for logging in standardised formats, compressing
+HTTP responses, validating content types and other useful tools for manipulating
+requests and responses.
+*/
+package handlers
diff --git a/Godeps/_workspace/src/github.com/gorilla/handlers/proxy_headers.go b/Godeps/_workspace/src/github.com/gorilla/handlers/proxy_headers.go
new file mode 100644
index 00000000..268de9c6
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/gorilla/handlers/proxy_headers.go
@@ -0,0 +1,113 @@
+package handlers
+
+import (
+ "net/http"
+ "regexp"
+ "strings"
+)
+
+var (
+ // De-facto standard header keys.
+ xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
+ xRealIP = http.CanonicalHeaderKey("X-Real-IP")
+ xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Scheme")
+)
+
+var (
+ // RFC7239 defines a new "Forwarded: " header designed to replace the
+ // existing use of X-Forwarded-* headers.
+ // e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43
+ forwarded = http.CanonicalHeaderKey("Forwarded")
+ // Allows for a sub-match of the first value after 'for=' to the next
+ // comma, semi-colon or space. The match is case-insensitive.
+ forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|,| )]+)`)
+ // Allows for a sub-match for the first instance of scheme (http|https)
+ // prefixed by 'proto='. The match is case-insensitive.
+ protoRegex = regexp.MustCompile(`(?i)(?:proto=)(https|http)`)
+)
+
+// ProxyHeaders inspects common reverse proxy headers and sets the corresponding
+// fields in the HTTP request struct. These are X-Forwarded-For and X-Real-IP
+// for the remote (client) IP address, X-Forwarded-Proto for the scheme
+// (http|https) and the RFC7239 Forwarded header, which may include both client
+// IPs and schemes.
+//
+// NOTE: This middleware should only be used when behind a reverse
+// proxy like nginx, HAProxy or Apache. Reverse proxies that don't (or are
+// configured not to) strip these headers from client requests, or where these
+// headers are accepted "as is" from a remote client (e.g. when Go is not behind
+// a proxy), can manifest as a vulnerability if your application uses these
+// headers for validating the 'trustworthiness' of a request.
+func ProxyHeaders(h http.Handler) http.Handler {
+ fn := func(w http.ResponseWriter, r *http.Request) {
+ // Set the remote IP with the value passed from the proxy.
+ if fwd := getIP(r); fwd != "" {
+ r.RemoteAddr = fwd
+ }
+
+ // Set the scheme (proto) with the value passed from the proxy.
+ if scheme := getScheme(r); scheme != "" {
+ r.URL.Scheme = scheme
+ }
+
+ // Call the next handler in the chain.
+ h.ServeHTTP(w, r)
+ }
+
+ return http.HandlerFunc(fn)
+}
+
+// getIP retrieves the IP from the X-Forwarded-For, X-Real-IP and RFC7239
+// Forwarded headers (in that order).
+func getIP(r *http.Request) string {
+ var addr string
+
+ if fwd := r.Header.Get(xForwardedFor); fwd != "" {
+ // Only grab the first (client) address. Note that '192.168.0.1,
+ // 10.1.1.1' is a valid key for X-Forwarded-For where addresses after
+ // the first may represent forwarding proxies earlier in the chain.
+ s := strings.Index(fwd, ", ")
+ if s == -1 {
+ s = len(fwd)
+ }
+ addr = fwd[:s]
+ } else if fwd := r.Header.Get(xRealIP); fwd != "" {
+ // X-Real-IP should only contain one IP address (the client making the
+ // request).
+ addr = fwd
+ } else if fwd := r.Header.Get(forwarded); fwd != "" {
+ // match should contain at least two elements if the protocol was
+ // specified in the Forwarded header. The first element will always be
+ // the 'for=' capture, which we ignore. In the case of multiple IP
+ // addresses (for=8.8.8.8, 8.8.4.4,172.16.1.20 is valid) we only
+ // extract the first, which should be the client IP.
+ if match := forRegex.FindStringSubmatch(fwd); len(match) > 1 {
+ // IPv6 addresses in Forwarded headers are quoted-strings. We strip
+ // these quotes.
+ addr = strings.Trim(match[1], `"`)
+ }
+ }
+
+ return addr
+}
+
+// getScheme retrieves the scheme from the X-Forwarded-Proto and RFC7239
+// Forwarded headers (in that order).
+func getScheme(r *http.Request) string {
+ var scheme string
+
+ // Retrieve the scheme from X-Forwarded-Proto.
+ if proto := r.Header.Get(xForwardedProto); proto != "" {
+ scheme = strings.ToLower(proto)
+ } else if proto := r.Header.Get(forwarded); proto != "" {
+ // match should contain at least two elements if the protocol was
+ // specified in the Forwarded header. The first element will always be
+ // the 'proto=' capture, which we ignore. In the case of multiple proto
+ // parameters (invalid) we only extract the first.
+ if match := protoRegex.FindStringSubmatch(proto); len(match) > 1 {
+ scheme = strings.ToLower(match[1])
+ }
+ }
+
+ return scheme
+}
diff --git a/Godeps/_workspace/src/github.com/noahdesu/go-ceph/LICENSE b/Godeps/_workspace/src/github.com/noahdesu/go-ceph/LICENSE
new file mode 100644
index 00000000..08d70bfc
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/noahdesu/go-ceph/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Noah Watkins
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Godeps/_workspace/src/golang.org/x/crypto/LICENSE b/Godeps/_workspace/src/golang.org/x/crypto/LICENSE
new file mode 100644
index 00000000..6a66aea5
--- /dev/null
+++ b/Godeps/_workspace/src/golang.org/x/crypto/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Godeps/_workspace/src/golang.org/x/crypto/PATENTS b/Godeps/_workspace/src/golang.org/x/crypto/PATENTS
new file mode 100644
index 00000000..73309904
--- /dev/null
+++ b/Godeps/_workspace/src/golang.org/x/crypto/PATENTS
@@ -0,0 +1,22 @@
+Additional IP Rights Grant (Patents)
+
+"This implementation" means the copyrightable works distributed by
+Google as part of the Go project.
+
+Google hereby grants to You a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable (except as stated in this section)
+patent license to make, have made, use, offer to sell, sell, import,
+transfer and otherwise run, modify and propagate the contents of this
+implementation of Go, where such license applies only to those patent
+claims, both currently owned or controlled by Google and acquired in
+the future, licensable by Google that are necessarily infringed by this
+implementation of Go. This grant does not include claims that would be
+infringed only as a consequence of further modification of this
+implementation. If you or your agent or exclusive licensee institute or
+order or agree to the institution of patent litigation against any
+entity (including a cross-claim or counterclaim in a lawsuit) alleging
+that this implementation of Go or any code incorporated within this
+implementation of Go constitutes direct or contributory patent
+infringement, or inducement of patent infringement, then any patent
+rights granted to you under this License for this implementation of Go
+shall terminate as of the date such litigation is filed.
diff --git a/Godeps/_workspace/src/golang.org/x/net/LICENSE b/Godeps/_workspace/src/golang.org/x/net/LICENSE
new file mode 100644
index 00000000..6a66aea5
--- /dev/null
+++ b/Godeps/_workspace/src/golang.org/x/net/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Godeps/_workspace/src/golang.org/x/net/PATENTS b/Godeps/_workspace/src/golang.org/x/net/PATENTS
new file mode 100644
index 00000000..73309904
--- /dev/null
+++ b/Godeps/_workspace/src/golang.org/x/net/PATENTS
@@ -0,0 +1,22 @@
+Additional IP Rights Grant (Patents)
+
+"This implementation" means the copyrightable works distributed by
+Google as part of the Go project.
+
+Google hereby grants to You a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable (except as stated in this section)
+patent license to make, have made, use, offer to sell, sell, import,
+transfer and otherwise run, modify and propagate the contents of this
+implementation of Go, where such license applies only to those patent
+claims, both currently owned or controlled by Google and acquired in
+the future, licensable by Google that are necessarily infringed by this
+implementation of Go. This grant does not include claims that would be
+infringed only as a consequence of further modification of this
+implementation. If you or your agent or exclusive licensee institute or
+order or agree to the institution of patent litigation against any
+entity (including a cross-claim or counterclaim in a lawsuit) alleging
+that this implementation of Go or any code incorporated within this
+implementation of Go constitutes direct or contributory patent
+infringement, or inducement of patent infringement, then any patent
+rights granted to you under this License for this implementation of Go
+shall terminate as of the date such litigation is filed.
diff --git a/blobs.go b/blobs.go
index ce43ea2e..e80800f8 100644
--- a/blobs.go
+++ b/blobs.go
@@ -97,6 +97,11 @@ type BlobDeleter interface {
Delete(ctx context.Context, dgst digest.Digest) error
}
+// BlobEnumerator enables iterating over blobs from storage
+type BlobEnumerator interface {
+ Enumerate(ctx context.Context, ingester func(dgst digest.Digest) error) error
+}
+
// BlobDescriptorService manages metadata about a blob by digest. Most
// implementations will not expose such an interface explicitly. Such mappings
// should be maintained by interacting with the BlobIngester. Hence, this is
diff --git a/cmd/registry/main.go b/cmd/registry/main.go
index 6e667c07..686af12b 100644
--- a/cmd/registry/main.go
+++ b/cmd/registry/main.go
@@ -20,5 +20,5 @@ import (
)
func main() {
- registry.Cmd.Execute()
+ registry.RootCmd.Execute()
}
diff --git a/docs/gc.md b/docs/gc.md
new file mode 100644
index 00000000..34565369
--- /dev/null
+++ b/docs/gc.md
@@ -0,0 +1,28 @@
+
+
+# What Garbage Collection Does
+
+Garbage collection is a process that delete blobs to which no manifests refer.
+It runs in two phases. First, in the 'mark' phase, the process scans all the
+manifests in the registry. From these manifests, it constructs a set of content
+address digests. This set is the 'mark set' and denotes the set of blobs to *not*
+delete. Secondly, in the 'sweep' phase, the process scans all the blobs and if
+a blob's content address digest is not in the mark set, the process will delete
+it.
+
+
+# How to Run
+
+You can run garbage collection by running
+
+ docker run --rm registry-image-name garbage-collect /etc/docker/registry/config.yml
+
+NOTE: You should ensure that the registry itself is in read-only mode or not running at
+all. If you were to upload an image while garbage collection is running, there is the
+risk that the image's layers will be mistakenly deleted, leading to a corrupted image.
diff --git a/manifests.go b/manifests.go
index 40c5622f..80fea173 100644
--- a/manifests.go
+++ b/manifests.go
@@ -53,12 +53,18 @@ type ManifestService interface {
// Delete removes the manifest specified by the given digest. Deleting
// a manifest that doesn't exist will return ErrManifestNotFound
Delete(ctx context.Context, dgst digest.Digest) error
+}
- // Enumerate fills 'manifests' with the manifests in this service up
- // to the size of 'manifests' and returns 'n' for the number of entries
- // which were filled. 'last' contains an offset in the manifest set
- // and can be used to resume iteration.
- //Enumerate(ctx context.Context, manifests []Manifest, last Manifest) (n int, err error)
+// ManifestEnumerator enables iterating over manifests
+type ManifestEnumerator interface {
+ // Enumerate calls ingester for each manifest.
+ Enumerate(ctx context.Context, ingester func(digest.Digest) error) error
+}
+
+// SignaturesGetter provides an interface for getting the signatures of a schema1 manifest. If the digest
+// refered to is not a schema1 manifest, an error should be returned.
+type SignaturesGetter interface {
+ GetSignatures(ctx context.Context, manifestDigest digest.Digest) ([]digest.Digest, error)
}
// Describable is an interface for descriptors
diff --git a/registry.go b/registry.go
index 37dbb49b..fbe605e0 100644
--- a/registry.go
+++ b/registry.go
@@ -40,6 +40,17 @@ type Namespace interface {
// which were filled. 'last' contains an offset in the catalog, and 'err' will be
// set to io.EOF if there are no more entries to obtain.
Repositories(ctx context.Context, repos []string, last string) (n int, err error)
+
+ // Blobs returns a blob enumerator to access all blobs
+ Blobs() BlobEnumerator
+
+ // BlobStatter returns a BlobStatter to control
+ BlobStatter() BlobStatter
+}
+
+// RepositoryEnumerator describes an operation to enumerate repositories
+type RepositoryEnumerator interface {
+ Enumerate(ctx context.Context, ingester func(string) error) error
}
// ManifestServiceOption is a function argument for Manifest Service methods
diff --git a/registry/garbagecollect.go b/registry/garbagecollect.go
new file mode 100644
index 00000000..5e165aea
--- /dev/null
+++ b/registry/garbagecollect.go
@@ -0,0 +1,150 @@
+package registry
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/docker/distribution"
+ "github.com/docker/distribution/context"
+ "github.com/docker/distribution/digest"
+ "github.com/docker/distribution/manifest/schema1"
+ "github.com/docker/distribution/manifest/schema2"
+ "github.com/docker/distribution/reference"
+ "github.com/docker/distribution/registry/storage"
+ "github.com/docker/distribution/registry/storage/driver"
+ "github.com/docker/distribution/registry/storage/driver/factory"
+
+ "github.com/spf13/cobra"
+)
+
+func markAndSweep(storageDriver driver.StorageDriver) error {
+ ctx := context.Background()
+
+ // Construct a registry
+ registry, err := storage.NewRegistry(ctx, storageDriver)
+ if err != nil {
+ return fmt.Errorf("failed to construct registry: %v", err)
+ }
+
+ repositoryEnumerator, ok := registry.(distribution.RepositoryEnumerator)
+ if !ok {
+ return fmt.Errorf("coercion error: unable to convert Namespace to RepositoryEnumerator")
+ }
+
+ // mark
+ markSet := make(map[digest.Digest]struct{})
+ err = repositoryEnumerator.Enumerate(ctx, func(repoName string) error {
+ var err error
+ named, err := reference.ParseNamed(repoName)
+ if err != nil {
+ return fmt.Errorf("failed to parse repo name %s: %v", repoName, err)
+ }
+ repository, err := registry.Repository(ctx, named)
+ if err != nil {
+ return fmt.Errorf("failed to construct repository: %v", err)
+ }
+
+ manifestService, err := repository.Manifests(ctx)
+ if err != nil {
+ return fmt.Errorf("failed to construct manifest service: %v", err)
+ }
+
+ manifestEnumerator, ok := manifestService.(distribution.ManifestEnumerator)
+ if !ok {
+ return fmt.Errorf("coercion error: unable to convert ManifestService into ManifestEnumerator")
+ }
+
+ err = manifestEnumerator.Enumerate(ctx, func(dgst digest.Digest) error {
+ // Mark the manifest's blob
+ markSet[dgst] = struct{}{}
+
+ manifest, err := manifestService.Get(ctx, dgst)
+ if err != nil {
+ return fmt.Errorf("failed to retrieve manifest for digest %v: %v", dgst, err)
+ }
+
+ descriptors := manifest.References()
+ for _, descriptor := range descriptors {
+ markSet[descriptor.Digest] = struct{}{}
+ }
+
+ switch manifest.(type) {
+ case *schema1.SignedManifest:
+ signaturesGetter, ok := manifestService.(distribution.SignaturesGetter)
+ if !ok {
+ return fmt.Errorf("coercion error: unable to convert ManifestSErvice into SignaturesGetter")
+ }
+ signatures, err := signaturesGetter.GetSignatures(ctx, dgst)
+ if err != nil {
+ return fmt.Errorf("failed to get signatures for signed manifest: %v", err)
+ }
+ for _, signatureDigest := range signatures {
+ markSet[signatureDigest] = struct{}{}
+ }
+ break
+ case *schema2.DeserializedManifest:
+ config := manifest.(*schema2.DeserializedManifest).Config
+ markSet[config.Digest] = struct{}{}
+ break
+ }
+
+ return nil
+ })
+
+ return err
+ })
+
+ if err != nil {
+ return fmt.Errorf("failed to mark: %v\n", err)
+ }
+
+ // sweep
+ blobService := registry.Blobs()
+ deleteSet := make(map[digest.Digest]struct{})
+ err = blobService.Enumerate(ctx, func(dgst digest.Digest) error {
+ // check if digest is in markSet. If not, delete it!
+ if _, ok := markSet[dgst]; !ok {
+ deleteSet[dgst] = struct{}{}
+ }
+ return nil
+ })
+
+ // Construct vacuum
+ vacuum := storage.NewVacuum(ctx, storageDriver)
+ for dgst := range deleteSet {
+ err = vacuum.RemoveBlob(string(dgst))
+ if err != nil {
+ return fmt.Errorf("failed to delete blob %s: %v\n", dgst, err)
+ }
+ }
+
+ return err
+}
+
+// GCCmd is the cobra command that corresponds to the garbage-collect subcommand
+var GCCmd = &cobra.Command{
+ Use: "garbage-collect ",
+ Short: "`garbage-collects` deletes layers not referenced by any manifests",
+ Long: "`garbage-collects` deletes layers not referenced by any manifests",
+ Run: func(cmd *cobra.Command, args []string) {
+
+ config, err := resolveConfiguration(args)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "configuration error: %v\n", err)
+ cmd.Usage()
+ os.Exit(1)
+ }
+
+ driver, err := factory.Create(config.Storage.Type(), config.Storage.Parameters())
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to construct %s driver: %v", config.Storage.Type(), err)
+ os.Exit(1)
+ }
+
+ err = markAndSweep(driver)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to garbage collect: %v", err)
+ os.Exit(1)
+ }
+ },
+}
diff --git a/registry/garbagecollect_test.go b/registry/garbagecollect_test.go
new file mode 100644
index 00000000..951a9e81
--- /dev/null
+++ b/registry/garbagecollect_test.go
@@ -0,0 +1,343 @@
+package registry
+
+import (
+ "io"
+ "testing"
+
+ "github.com/docker/distribution"
+ "github.com/docker/distribution/context"
+ "github.com/docker/distribution/digest"
+ "github.com/docker/distribution/reference"
+ "github.com/docker/distribution/registry/storage"
+ "github.com/docker/distribution/registry/storage/driver"
+ "github.com/docker/distribution/registry/storage/driver/inmemory"
+ "github.com/docker/distribution/testutil"
+)
+
+type image struct {
+ manifest distribution.Manifest
+ manifestDigest digest.Digest
+ layers map[digest.Digest]io.ReadSeeker
+}
+
+func createRegistry(t *testing.T, driver driver.StorageDriver) distribution.Namespace {
+ ctx := context.Background()
+ registry, err := storage.NewRegistry(ctx, driver, storage.EnableDelete)
+ if err != nil {
+ t.Fatalf("Failed to construct namespace")
+ }
+ return registry
+}
+
+func makeRepository(t *testing.T, registry distribution.Namespace, name string) distribution.Repository {
+ ctx := context.Background()
+
+ // Initialize a dummy repository
+ named, err := reference.ParseNamed(name)
+ if err != nil {
+ t.Fatalf("Failed to parse name %s: %v", name, err)
+ }
+
+ repo, err := registry.Repository(ctx, named)
+ if err != nil {
+ t.Fatalf("Failed to construct repository: %v", err)
+ }
+ return repo
+}
+
+func makeManifestService(t *testing.T, repository distribution.Repository) distribution.ManifestService {
+ ctx := context.Background()
+
+ manifestService, err := repository.Manifests(ctx)
+ if err != nil {
+ t.Fatalf("Failed to construct manifest store: %v", err)
+ }
+ return manifestService
+}
+
+func allBlobs(t *testing.T, registry distribution.Namespace) map[digest.Digest]struct{} {
+ ctx := context.Background()
+ blobService := registry.Blobs()
+ allBlobsMap := make(map[digest.Digest]struct{})
+ err := blobService.Enumerate(ctx, func(dgst digest.Digest) error {
+ allBlobsMap[dgst] = struct{}{}
+ return nil
+ })
+ if err != nil {
+ t.Fatalf("Error getting all blobs: %v", err)
+ }
+ return allBlobsMap
+}
+
+func uploadImage(t *testing.T, repository distribution.Repository, im image) digest.Digest {
+ // upload layers
+ err := testutil.UploadBlobs(repository, im.layers)
+ if err != nil {
+ t.Fatalf("layer upload failed: %v", err)
+ }
+
+ // upload manifest
+ ctx := context.Background()
+ manifestService := makeManifestService(t, repository)
+ manifestDigest, err := manifestService.Put(ctx, im.manifest)
+ if err != nil {
+ t.Fatalf("manifest upload failed: %v", err)
+ }
+
+ return manifestDigest
+}
+
+func uploadRandomSchema1Image(t *testing.T, repository distribution.Repository) image {
+ randomLayers, err := testutil.CreateRandomLayers(2)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+
+ digests := []digest.Digest{}
+ for digest := range randomLayers {
+ digests = append(digests, digest)
+ }
+
+ manifest, err := testutil.MakeSchema1Manifest(digests)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+
+ manifestDigest := uploadImage(t, repository, image{manifest: manifest, layers: randomLayers})
+ return image{
+ manifest: manifest,
+ manifestDigest: manifestDigest,
+ layers: randomLayers,
+ }
+}
+
+func uploadRandomSchema2Image(t *testing.T, repository distribution.Repository) image {
+ randomLayers, err := testutil.CreateRandomLayers(2)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+
+ digests := []digest.Digest{}
+ for digest := range randomLayers {
+ digests = append(digests, digest)
+ }
+
+ manifest, err := testutil.MakeSchema2Manifest(repository, digests)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+
+ manifestDigest := uploadImage(t, repository, image{manifest: manifest, layers: randomLayers})
+ return image{
+ manifest: manifest,
+ manifestDigest: manifestDigest,
+ layers: randomLayers,
+ }
+}
+
+func TestNoDeletionNoEffect(t *testing.T) {
+ ctx := context.Background()
+ inmemoryDriver := inmemory.New()
+
+ registry := createRegistry(t, inmemoryDriver)
+ repo := makeRepository(t, registry, "palailogos")
+ manifestService, err := repo.Manifests(ctx)
+
+ image1 := uploadRandomSchema1Image(t, repo)
+ image2 := uploadRandomSchema1Image(t, repo)
+ image3 := uploadRandomSchema2Image(t, repo)
+
+ // construct manifestlist for fun.
+ blobstatter := registry.BlobStatter()
+ manifestList, err := testutil.MakeManifestList(blobstatter, []digest.Digest{
+ image1.manifestDigest, image2.manifestDigest})
+ if err != nil {
+ t.Fatalf("Failed to make manifest list: %v", err)
+ }
+
+ _, err = manifestService.Put(ctx, manifestList)
+ if err != nil {
+ t.Fatalf("Failed to add manifest list: %v", err)
+ }
+
+ // Run GC
+ err = markAndSweep(inmemoryDriver)
+ if err != nil {
+ t.Fatalf("Failed mark and sweep: %v", err)
+ }
+
+ blobs := allBlobs(t, registry)
+
+ // the +1 at the end is for the manifestList
+ // the first +3 at the end for each manifest's blob
+ // the second +3 at the end for each manifest's signature/config layer
+ totalBlobCount := len(image1.layers) + len(image2.layers) + len(image3.layers) + 1 + 3 + 3
+ if len(blobs) != totalBlobCount {
+ t.Fatalf("Garbage collection affected storage")
+ }
+}
+
+func TestDeletionHasEffect(t *testing.T) {
+ ctx := context.Background()
+ inmemoryDriver := inmemory.New()
+
+ registry := createRegistry(t, inmemoryDriver)
+ repo := makeRepository(t, registry, "komnenos")
+ manifests, err := repo.Manifests(ctx)
+
+ image1 := uploadRandomSchema1Image(t, repo)
+ image2 := uploadRandomSchema1Image(t, repo)
+ image3 := uploadRandomSchema2Image(t, repo)
+
+ manifests.Delete(ctx, image2.manifestDigest)
+ manifests.Delete(ctx, image3.manifestDigest)
+
+ // Run GC
+ err = markAndSweep(inmemoryDriver)
+ if err != nil {
+ t.Fatalf("Failed mark and sweep: %v", err)
+ }
+
+ blobs := allBlobs(t, registry)
+
+ // check that the image1 manifest and all the layers are still in blobs
+ if _, ok := blobs[image1.manifestDigest]; !ok {
+ t.Fatalf("First manifest is missing")
+ }
+
+ for layer := range image1.layers {
+ if _, ok := blobs[layer]; !ok {
+ t.Fatalf("manifest 1 layer is missing: %v", layer)
+ }
+ }
+
+ // check that image2 and image3 layers are not still around
+ for layer := range image2.layers {
+ if _, ok := blobs[layer]; ok {
+ t.Fatalf("manifest 2 layer is present: %v", layer)
+ }
+ }
+
+ for layer := range image3.layers {
+ if _, ok := blobs[layer]; ok {
+ t.Fatalf("manifest 3 layer is present: %v", layer)
+ }
+ }
+}
+
+func getAnyKey(digests map[digest.Digest]io.ReadSeeker) (d digest.Digest) {
+ for d = range digests {
+ break
+ }
+ return
+}
+
+func getKeys(digests map[digest.Digest]io.ReadSeeker) (ds []digest.Digest) {
+ for d := range digests {
+ ds = append(ds, d)
+ }
+ return
+}
+
+func TestDeletionWithSharedLayer(t *testing.T) {
+ ctx := context.Background()
+ inmemoryDriver := inmemory.New()
+
+ registry := createRegistry(t, inmemoryDriver)
+ repo := makeRepository(t, registry, "tzimiskes")
+
+ // Create random layers
+ randomLayers1, err := testutil.CreateRandomLayers(3)
+ if err != nil {
+ t.Fatalf("failed to make layers: %v", err)
+ }
+
+ randomLayers2, err := testutil.CreateRandomLayers(3)
+ if err != nil {
+ t.Fatalf("failed to make layers: %v", err)
+ }
+
+ // Upload all layers
+ err = testutil.UploadBlobs(repo, randomLayers1)
+ if err != nil {
+ t.Fatalf("failed to upload layers: %v", err)
+ }
+
+ err = testutil.UploadBlobs(repo, randomLayers2)
+ if err != nil {
+ t.Fatalf("failed to upload layers: %v", err)
+ }
+
+ // Construct manifests
+ manifest1, err := testutil.MakeSchema1Manifest(getKeys(randomLayers1))
+ if err != nil {
+ t.Fatalf("failed to make manifest: %v", err)
+ }
+
+ sharedKey := getAnyKey(randomLayers1)
+ manifest2, err := testutil.MakeSchema2Manifest(repo, append(getKeys(randomLayers2), sharedKey))
+ if err != nil {
+ t.Fatalf("failed to make manifest: %v", err)
+ }
+
+ manifestService := makeManifestService(t, repo)
+
+ // Upload manifests
+ _, err = manifestService.Put(ctx, manifest1)
+ if err != nil {
+ t.Fatalf("manifest upload failed: %v", err)
+ }
+
+ manifestDigest2, err := manifestService.Put(ctx, manifest2)
+ if err != nil {
+ t.Fatalf("manifest upload failed: %v", err)
+ }
+
+ // delete
+ err = manifestService.Delete(ctx, manifestDigest2)
+ if err != nil {
+ t.Fatalf("manifest deletion failed: %v", err)
+ }
+
+ // check that all of the layers in layer 1 are still there
+ blobs := allBlobs(t, registry)
+ for dgst := range randomLayers1 {
+ if _, ok := blobs[dgst]; !ok {
+ t.Fatalf("random layer 1 blob missing: %v", dgst)
+ }
+ }
+}
+
+func TestOrphanBlobDeleted(t *testing.T) {
+ inmemoryDriver := inmemory.New()
+
+ registry := createRegistry(t, inmemoryDriver)
+ repo := makeRepository(t, registry, "michael_z_doukas")
+
+ digests, err := testutil.CreateRandomLayers(1)
+ if err != nil {
+ t.Fatalf("Failed to create random digest: %v", err)
+ }
+
+ if err = testutil.UploadBlobs(repo, digests); err != nil {
+ t.Fatalf("Failed to upload blob: %v", err)
+ }
+
+ // formality to create the necessary directories
+ uploadRandomSchema2Image(t, repo)
+
+ // Run GC
+ err = markAndSweep(inmemoryDriver)
+ if err != nil {
+ t.Fatalf("Failed mark and sweep: %v", err)
+ }
+
+ blobs := allBlobs(t, registry)
+
+ // check that orphan blob layers are not still around
+ for dgst := range digests {
+ if _, ok := blobs[dgst]; ok {
+ t.Fatalf("Orphan layer is present: %v", dgst)
+ }
+ }
+}
diff --git a/registry/proxy/proxymanifeststore.go b/registry/proxy/proxymanifeststore.go
index b8109667..f08e285d 100644
--- a/registry/proxy/proxymanifeststore.go
+++ b/registry/proxy/proxymanifeststore.go
@@ -93,8 +93,3 @@ func (pms proxyManifestStore) Put(ctx context.Context, manifest distribution.Man
func (pms proxyManifestStore) Delete(ctx context.Context, dgst digest.Digest) error {
return distribution.ErrUnsupported
}
-
-/*func (pms proxyManifestStore) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) {
- return 0, distribution.ErrUnsupported
-}
-*/
diff --git a/registry/proxy/proxyregistry.go b/registry/proxy/proxyregistry.go
index e25fe783..1663ab69 100644
--- a/registry/proxy/proxyregistry.go
+++ b/registry/proxy/proxyregistry.go
@@ -166,6 +166,14 @@ func (pr *proxyingRegistry) Repository(ctx context.Context, name reference.Named
}, nil
}
+func (pr *proxyingRegistry) Blobs() distribution.BlobEnumerator {
+ return pr.embedded.Blobs()
+}
+
+func (pr *proxyingRegistry) BlobStatter() distribution.BlobStatter {
+ return pr.embedded.BlobStatter()
+}
+
// authChallenger encapsulates a request to the upstream to establish credential challenges
type authChallenger interface {
tryEstablishChallenges(context.Context) error
diff --git a/registry/registry.go b/registry/registry.go
index 86cb6a17..a1ba3b1a 100644
--- a/registry/registry.go
+++ b/registry/registry.go
@@ -24,16 +24,12 @@ import (
"github.com/yvasiyarov/gorelic"
)
-// Cmd is a cobra command for running the registry.
-var Cmd = &cobra.Command{
- Use: "registry ",
- Short: "registry stores and distributes Docker images",
- Long: "registry stores and distributes Docker images.",
+// ServeCmd is a cobra command for running the registry.
+var ServeCmd = &cobra.Command{
+ Use: "serve ",
+ Short: "`serve` stores and distributes Docker images",
+ Long: "`serve` stores and distributes Docker images.",
Run: func(cmd *cobra.Command, args []string) {
- if showVersion {
- version.PrintVersion()
- return
- }
// setup context
ctx := context.WithVersion(context.Background(), version.Version)
@@ -65,12 +61,6 @@ var Cmd = &cobra.Command{
},
}
-var showVersion bool
-
-func init() {
- Cmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "show the version and exit")
-}
-
// A Registry represents a complete instance of the registry.
// TODO(aaronl): It might make sense for Registry to become an interface.
type Registry struct {
diff --git a/registry/root.go b/registry/root.go
new file mode 100644
index 00000000..46338b46
--- /dev/null
+++ b/registry/root.go
@@ -0,0 +1,28 @@
+package registry
+
+import (
+ "github.com/docker/distribution/version"
+ "github.com/spf13/cobra"
+)
+
+var showVersion bool
+
+func init() {
+ RootCmd.AddCommand(ServeCmd)
+ RootCmd.AddCommand(GCCmd)
+ RootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "show the version and exit")
+}
+
+// RootCmd is the main command for the 'registry' binary.
+var RootCmd = &cobra.Command{
+ Use: "registry",
+ Short: "`registry`",
+ Long: "`registry`",
+ Run: func(cmd *cobra.Command, args []string) {
+ if showVersion {
+ version.PrintVersion()
+ return
+ }
+ cmd.Usage()
+ },
+}
diff --git a/registry/storage/blobstore.go b/registry/storage/blobstore.go
index f8fe23fe..9034cb68 100644
--- a/registry/storage/blobstore.go
+++ b/registry/storage/blobstore.go
@@ -1,6 +1,8 @@
package storage
import (
+ "path"
+
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
@@ -85,6 +87,36 @@ func (bs *blobStore) Put(ctx context.Context, mediaType string, p []byte) (distr
}, bs.driver.PutContent(ctx, bp, p)
}
+func (bs *blobStore) Enumerate(ctx context.Context, ingester func(dgst digest.Digest) error) error {
+
+ specPath, err := pathFor(blobsPathSpec{})
+ if err != nil {
+ return err
+ }
+
+ err = Walk(ctx, bs.driver, specPath, func(fileInfo driver.FileInfo) error {
+ // skip directories
+ if fileInfo.IsDir() {
+ return nil
+ }
+
+ currentPath := fileInfo.Path()
+ // we only want to parse paths that end with /data
+ _, fileName := path.Split(currentPath)
+ if fileName != "data" {
+ return nil
+ }
+
+ digest, err := digestFromPath(currentPath)
+ if err != nil {
+ return err
+ }
+
+ return ingester(digest)
+ })
+ return err
+}
+
// path returns the canonical path for the blob identified by digest. The blob
// may or may not exist.
func (bs *blobStore) path(dgst digest.Digest) (string, error) {
diff --git a/registry/storage/catalog.go b/registry/storage/catalog.go
index 481489f2..3b13b7ad 100644
--- a/registry/storage/catalog.go
+++ b/registry/storage/catalog.go
@@ -64,3 +64,34 @@ func (reg *registry) Repositories(ctx context.Context, repos []string, last stri
return n, errVal
}
+
+// Enumerate applies ingester to each repository
+func (reg *registry) Enumerate(ctx context.Context, ingester func(string) error) error {
+ repoNameBuffer := make([]string, 100)
+ var last string
+ for {
+ n, err := reg.Repositories(ctx, repoNameBuffer, last)
+ if err != nil && err != io.EOF {
+ return err
+ }
+
+ if n == 0 {
+ break
+ }
+
+ last = repoNameBuffer[n-1]
+ for i := 0; i < n; i++ {
+ repoName := repoNameBuffer[i]
+ err = ingester(repoName)
+ if err != nil {
+ return err
+ }
+ }
+
+ if err == io.EOF {
+ break
+ }
+ }
+ return nil
+
+}
diff --git a/registry/storage/linkedblobstore.go b/registry/storage/linkedblobstore.go
index 3e6f9c2d..76a1c29d 100644
--- a/registry/storage/linkedblobstore.go
+++ b/registry/storage/linkedblobstore.go
@@ -3,6 +3,7 @@ package storage
import (
"fmt"
"net/http"
+ "path"
"time"
"github.com/docker/distribution"
@@ -37,6 +38,9 @@ type linkedBlobStore struct {
// removed an the blob links folder should be merged. The first entry is
// treated as the "canonical" link location and will be used for writes.
linkPathFns []linkPathFunc
+
+ // linkDirectoryPathSpec locates the root directories in which one might find links
+ linkDirectoryPathSpec pathSpec
}
var _ distribution.BlobStore = &linkedBlobStore{}
@@ -236,6 +240,55 @@ func (lbs *linkedBlobStore) Delete(ctx context.Context, dgst digest.Digest) erro
return nil
}
+func (lbs *linkedBlobStore) Enumerate(ctx context.Context, ingestor func(digest.Digest) error) error {
+ rootPath, err := pathFor(lbs.linkDirectoryPathSpec)
+ if err != nil {
+ return err
+ }
+ err = Walk(ctx, lbs.blobStore.driver, rootPath, func(fileInfo driver.FileInfo) error {
+ // exit early if directory...
+ if fileInfo.IsDir() {
+ return nil
+ }
+ filePath := fileInfo.Path()
+
+ // check if it's a link
+ _, fileName := path.Split(filePath)
+ if fileName != "link" {
+ return nil
+ }
+
+ // read the digest found in link
+ digest, err := lbs.blobStore.readlink(ctx, filePath)
+ if err != nil {
+ return err
+ }
+
+ // ensure this conforms to the linkPathFns
+ _, err = lbs.Stat(ctx, digest)
+ if err != nil {
+ // we expect this error to occur so we move on
+ if err == distribution.ErrBlobUnknown {
+ return nil
+ }
+ return err
+ }
+
+ err = ingestor(digest)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
func (lbs *linkedBlobStore) mount(ctx context.Context, sourceRepo reference.Named, dgst digest.Digest) (distribution.Descriptor, error) {
repo, err := lbs.registry.Repository(ctx, sourceRepo)
if err != nil {
diff --git a/registry/storage/manifeststore.go b/registry/storage/manifeststore.go
index e259af48..f3660c98 100644
--- a/registry/storage/manifeststore.go
+++ b/registry/storage/manifeststore.go
@@ -2,6 +2,7 @@ package storage
import (
"fmt"
+ "path"
"encoding/json"
"github.com/docker/distribution"
@@ -129,6 +130,52 @@ func (ms *manifestStore) Delete(ctx context.Context, dgst digest.Digest) error {
return ms.blobStore.Delete(ctx, dgst)
}
-func (ms *manifestStore) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) {
- return 0, distribution.ErrUnsupported
+func (ms *manifestStore) Enumerate(ctx context.Context, ingester func(digest.Digest) error) error {
+ err := ms.blobStore.Enumerate(ctx, func(dgst digest.Digest) error {
+ err := ingester(dgst)
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+ return err
+}
+
+// Only valid for schema1 signed manifests
+func (ms *manifestStore) GetSignatures(ctx context.Context, manifestDigest digest.Digest) ([]digest.Digest, error) {
+ // sanity check that digest refers to a schema1 digest
+ manifest, err := ms.Get(ctx, manifestDigest)
+ if err != nil {
+ return nil, err
+ }
+
+ if _, ok := manifest.(*schema1.SignedManifest); !ok {
+ return nil, fmt.Errorf("digest %v is not for schema1 manifest", manifestDigest)
+ }
+
+ signaturesPath, err := pathFor(manifestSignaturesPathSpec{
+ name: ms.repository.Named().Name(),
+ revision: manifestDigest,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ signaturesPath = path.Join(signaturesPath, "sha256")
+
+ signaturePaths, err := ms.blobStore.driver.List(ctx, signaturesPath)
+ if err != nil {
+ return nil, err
+ }
+
+ var digests []digest.Digest
+ for _, sigPath := range signaturePaths {
+ sigdigest, err := digest.ParseDigest("sha256:" + path.Base(sigPath))
+ if err != nil {
+ // merely found not a digest
+ continue
+ }
+ digests = append(digests, sigdigest)
+ }
+ return digests, nil
}
diff --git a/registry/storage/paths.go b/registry/storage/paths.go
index 6ee54127..8985f043 100644
--- a/registry/storage/paths.go
+++ b/registry/storage/paths.go
@@ -74,6 +74,7 @@ const (
//
// Manifests:
//
+// manifestRevisionsPathSpec: /v2/repositories//_manifests/revisions/
// manifestRevisionPathSpec: /v2/repositories//_manifests/revisions///
// manifestRevisionLinkPathSpec: /v2/repositories//_manifests/revisions///link
// manifestSignaturesPathSpec: /v2/repositories//_manifests/revisions///signatures/
@@ -100,6 +101,7 @@ const (
//
// Blob Store:
//
+// blobsPathSpec: /v2/blobs/
// blobPathSpec: /v2/blobs///
// blobDataPathSpec: /v2/blobs////data
// blobMediaTypePathSpec: /v2/blobs////data
@@ -125,6 +127,9 @@ func pathFor(spec pathSpec) (string, error) {
switch v := spec.(type) {
+ case manifestRevisionsPathSpec:
+ return path.Join(append(repoPrefix, v.name, "_manifests", "revisions")...), nil
+
case manifestRevisionPathSpec:
components, err := digestPathComponents(v.revision, false)
if err != nil {
@@ -246,6 +251,17 @@ func pathFor(spec pathSpec) (string, error) {
blobLinkPathComponents := append(repoPrefix, v.name, "_layers")
return path.Join(path.Join(append(blobLinkPathComponents, components...)...), "link"), nil
+ case blobsPathSpec:
+ blobsPathPrefix := append(rootPrefix, "blobs")
+ return path.Join(blobsPathPrefix...), nil
+ case blobPathSpec:
+ components, err := digestPathComponents(v.digest, true)
+ if err != nil {
+ return "", err
+ }
+
+ blobPathPrefix := append(rootPrefix, "blobs")
+ return path.Join(append(blobPathPrefix, components...)...), nil
case blobDataPathSpec:
components, err := digestPathComponents(v.digest, true)
if err != nil {
@@ -281,6 +297,14 @@ type pathSpec interface {
pathSpec()
}
+// manifestRevisionsPathSpec describes the directory path for
+// a manifest revision.
+type manifestRevisionsPathSpec struct {
+ name string
+}
+
+func (manifestRevisionsPathSpec) pathSpec() {}
+
// manifestRevisionPathSpec describes the components of the directory path for
// a manifest revision.
type manifestRevisionPathSpec struct {
@@ -404,12 +428,17 @@ var blobAlgorithmReplacer = strings.NewReplacer(
";", "/",
)
-// // blobPathSpec contains the path for the registry global blob store.
-// type blobPathSpec struct {
-// digest digest.Digest
-// }
+// blobsPathSpec contains the path for the blobs directory
+type blobsPathSpec struct{}
-// func (blobPathSpec) pathSpec() {}
+func (blobsPathSpec) pathSpec() {}
+
+// blobPathSpec contains the path for the registry global blob store.
+type blobPathSpec struct {
+ digest digest.Digest
+}
+
+func (blobPathSpec) pathSpec() {}
// blobDataPathSpec contains the path for the registry global blob store. For
// now, this contains layer data, exclusively.
@@ -491,3 +520,23 @@ func digestPathComponents(dgst digest.Digest, multilevel bool) ([]string, error)
return append(prefix, suffix...), nil
}
+
+// Reconstructs a digest from a path
+func digestFromPath(digestPath string) (digest.Digest, error) {
+
+ digestPath = strings.TrimSuffix(digestPath, "/data")
+ dir, hex := path.Split(digestPath)
+ dir = path.Dir(dir)
+ dir, next := path.Split(dir)
+
+ // next is either the algorithm OR the first two characters in the hex string
+ var algo string
+ if next == hex[:2] {
+ algo = path.Base(dir)
+ } else {
+ algo = next
+ }
+
+ dgst := digest.NewDigestFromHex(algo, hex)
+ return dgst, dgst.Validate()
+}
diff --git a/registry/storage/paths_test.go b/registry/storage/paths_test.go
index 2ad78e9d..91004bd4 100644
--- a/registry/storage/paths_test.go
+++ b/registry/storage/paths_test.go
@@ -2,6 +2,8 @@ package storage
import (
"testing"
+
+ "github.com/docker/distribution/digest"
)
func TestPathMapper(t *testing.T) {
@@ -120,3 +122,29 @@ func TestPathMapper(t *testing.T) {
}
}
+
+func TestDigestFromPath(t *testing.T) {
+ for _, testcase := range []struct {
+ path string
+ expected digest.Digest
+ multilevel bool
+ err error
+ }{
+ {
+ path: "/docker/registry/v2/blobs/sha256/99/9943fffae777400c0344c58869c4c2619c329ca3ad4df540feda74d291dd7c86/data",
+ multilevel: true,
+ expected: "sha256:9943fffae777400c0344c58869c4c2619c329ca3ad4df540feda74d291dd7c86",
+ err: nil,
+ },
+ } {
+ result, err := digestFromPath(testcase.path)
+ if err != testcase.err {
+ t.Fatalf("Unexpected error value %v when we wanted %v", err, testcase.err)
+ }
+
+ if result != testcase.expected {
+ t.Fatalf("Unexpected result value %v when we wanted %v", result, testcase.expected)
+
+ }
+ }
+}
diff --git a/registry/storage/registry.go b/registry/storage/registry.go
index 9c74ebbc..a1128b4a 100644
--- a/registry/storage/registry.go
+++ b/registry/storage/registry.go
@@ -147,6 +147,14 @@ func (reg *registry) Repository(ctx context.Context, canonicalName reference.Nam
}, nil
}
+func (reg *registry) Blobs() distribution.BlobEnumerator {
+ return reg.blobStore
+}
+
+func (reg *registry) BlobStatter() distribution.BlobStatter {
+ return reg.statter
+}
+
// repository provides name-scoped access to various services.
type repository struct {
*registry
@@ -180,6 +188,8 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M
blobLinkPath,
}
+ manifestDirectoryPathSpec := manifestRevisionsPathSpec{name: repo.name.Name()}
+
blobStore := &linkedBlobStore{
ctx: ctx,
blobStore: repo.blobStore,
@@ -193,7 +203,8 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M
// TODO(stevvooe): linkPath limits this blob store to only
// manifests. This instance cannot be used for blob checks.
- linkPathFns: manifestLinkPathFns,
+ linkPathFns: manifestLinkPathFns,
+ linkDirectoryPathSpec: manifestDirectoryPathSpec,
}
ms := &manifestStore{
diff --git a/registry/storage/vacuum.go b/registry/storage/vacuum.go
index 60d5a2fa..3bdfebf2 100644
--- a/registry/storage/vacuum.go
+++ b/registry/storage/vacuum.go
@@ -34,11 +34,13 @@ func (v Vacuum) RemoveBlob(dgst string) error {
return err
}
- blobPath, err := pathFor(blobDataPathSpec{digest: d})
+ blobPath, err := pathFor(blobPathSpec{digest: d})
if err != nil {
return err
}
+
context.GetLogger(v.ctx).Infof("Deleting blob: %s", blobPath)
+
err = v.driver.Delete(v.ctx, blobPath)
if err != nil {
return err
diff --git a/testutil/manifests.go b/testutil/manifests.go
new file mode 100644
index 00000000..c4f9fef5
--- /dev/null
+++ b/testutil/manifests.go
@@ -0,0 +1,87 @@
+package testutil
+
+import (
+ "fmt"
+
+ "github.com/docker/distribution"
+ "github.com/docker/distribution/context"
+ "github.com/docker/distribution/digest"
+ "github.com/docker/distribution/manifest"
+ "github.com/docker/distribution/manifest/manifestlist"
+ "github.com/docker/distribution/manifest/schema1"
+ "github.com/docker/distribution/manifest/schema2"
+ "github.com/docker/libtrust"
+)
+
+// MakeManifestList constructs a manifest list out of a list of manifest digests
+func MakeManifestList(blobstatter distribution.BlobStatter, manifestDigests []digest.Digest) (*manifestlist.DeserializedManifestList, error) {
+ ctx := context.Background()
+
+ var manifestDescriptors []manifestlist.ManifestDescriptor
+ for _, manifestDigest := range manifestDigests {
+ descriptor, err := blobstatter.Stat(ctx, manifestDigest)
+ if err != nil {
+ return nil, err
+ }
+ platformSpec := manifestlist.PlatformSpec{
+ Architecture: "atari2600",
+ OS: "CP/M",
+ Variant: "ternary",
+ Features: []string{"VLIW", "superscalaroutoforderdevnull"},
+ }
+ manifestDescriptor := manifestlist.ManifestDescriptor{
+ Descriptor: descriptor,
+ Platform: platformSpec,
+ }
+ manifestDescriptors = append(manifestDescriptors, manifestDescriptor)
+ }
+
+ return manifestlist.FromDescriptors(manifestDescriptors)
+}
+
+// MakeSchema1Manifest constructs a schema 1 manifest from a given list of digests and returns
+// the digest of the manifest
+func MakeSchema1Manifest(digests []digest.Digest) (distribution.Manifest, error) {
+ manifest := schema1.Manifest{
+ Versioned: manifest.Versioned{
+ SchemaVersion: 1,
+ },
+ Name: "who",
+ Tag: "cares",
+ }
+
+ for _, digest := range digests {
+ manifest.FSLayers = append(manifest.FSLayers, schema1.FSLayer{BlobSum: digest})
+ manifest.History = append(manifest.History, schema1.History{V1Compatibility: ""})
+ }
+
+ pk, err := libtrust.GenerateECP256PrivateKey()
+ if err != nil {
+ return nil, fmt.Errorf("unexpected error generating private key: %v", err)
+ }
+
+ signedManifest, err := schema1.Sign(&manifest, pk)
+ if err != nil {
+ return nil, fmt.Errorf("error signing manifest: %v", err)
+ }
+
+ return signedManifest, nil
+}
+
+// MakeSchema2Manifest constructs a schema 2 manifest from a given list of digests and returns
+// the digest of the manifest
+func MakeSchema2Manifest(repository distribution.Repository, digests []digest.Digest) (distribution.Manifest, error) {
+ ctx := context.Background()
+ blobStore := repository.Blobs(ctx)
+ builder := schema2.NewManifestBuilder(blobStore, []byte{})
+ for _, digest := range digests {
+ builder.AppendReference(distribution.Descriptor{Digest: digest})
+ }
+
+ manifest, err := builder.Build(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("unexpected error generating manifest: %v", err)
+ }
+
+ return manifest, nil
+}
diff --git a/testutil/tarfile.go b/testutil/tarfile.go
index 2c1d2d82..baa5ac5a 100644
--- a/testutil/tarfile.go
+++ b/testutil/tarfile.go
@@ -9,6 +9,8 @@ import (
mrand "math/rand"
"time"
+ "github.com/docker/distribution"
+ "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
)
@@ -76,3 +78,39 @@ func CreateRandomTarFile() (rs io.ReadSeeker, dgst digest.Digest, err error) {
return bytes.NewReader(target.Bytes()), dgst, nil
}
+
+// CreateRandomLayers returns a map of n digests. We don't particularly care
+// about the order of said digests (since they're all random anyway).
+func CreateRandomLayers(n int) (map[digest.Digest]io.ReadSeeker, error) {
+ digestMap := map[digest.Digest]io.ReadSeeker{}
+ for i := 0; i < n; i++ {
+ rs, ds, err := CreateRandomTarFile()
+ if err != nil {
+ return nil, fmt.Errorf("unexpected error generating test layer file: %v", err)
+ }
+
+ dgst := digest.Digest(ds)
+ digestMap[dgst] = rs
+ }
+ return digestMap, nil
+}
+
+// UploadBlobs lets you upload blobs to a repository
+func UploadBlobs(repository distribution.Repository, layers map[digest.Digest]io.ReadSeeker) error {
+ ctx := context.Background()
+ for digest, rs := range layers {
+ wr, err := repository.Blobs(ctx).Create(ctx)
+ if err != nil {
+ return fmt.Errorf("unexpected error creating upload: %v", err)
+ }
+
+ if _, err := io.Copy(wr, rs); err != nil {
+ return fmt.Errorf("unexpected error copying to upload: %v", err)
+ }
+
+ if _, err := wr.Commit(ctx, distribution.Descriptor{Digest: digest}); err != nil {
+ return fmt.Errorf("unexpected error committinng upload: %v", err)
+ }
+ }
+ return nil
+}