Implements garbage collection subcommand
- Includes a change in the command to run the registry. The registry server itself is now started up as a subcommand. - Includes changes to the high level interfaces to support enumeration of various registry objects. Signed-off-by: Andrew T Nguyen <andrew.nguyen@docker.com>master
							parent
							
								
									e430d77342
								
							
						
					
					
						commit
						feab4aafbc
					
				|  | @ -16,4 +16,4 @@ RUN make PREFIX=/go clean binaries | ||||||
| VOLUME ["/var/lib/registry"] | VOLUME ["/var/lib/registry"] | ||||||
| EXPOSE 5000 | EXPOSE 5000 | ||||||
| ENTRYPOINT ["registry"] | ENTRYPOINT ["registry"] | ||||||
| CMD ["/etc/docker/registry/config.yml"] | CMD ["serve", "/etc/docker/registry/config.yml"] | ||||||
|  |  | ||||||
|  | @ -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. | ||||||
|  | @ -0,0 +1,3 @@ | ||||||
|  | AWS SDK for Go | ||||||
|  | Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.  | ||||||
|  | Copyright 2014-2015 Stripe, Inc. | ||||||
|  | @ -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. <http://fsf.org/> | ||||||
|  |  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. | ||||||
|  | @ -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. | ||||||
|  | @ -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 | ||||||
|  | } | ||||||
|  | @ -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 | ||||||
							
								
								
									
										113
									
								
								Godeps/_workspace/src/github.com/gorilla/handlers/proxy_headers.go
								
								
									generated
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										113
									
								
								Godeps/_workspace/src/github.com/gorilla/handlers/proxy_headers.go
								
								
									generated
								
								
									vendored
								
								
									Normal file
								
							|  | @ -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 | ||||||
|  | } | ||||||
|  | @ -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. | ||||||
|  | @ -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. | ||||||
|  | @ -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. | ||||||
|  | @ -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. | ||||||
|  | @ -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. | ||||||
							
								
								
									
										5
									
								
								blobs.go
								
								
								
								
							
							
						
						
									
										5
									
								
								blobs.go
								
								
								
								
							|  | @ -97,6 +97,11 @@ type BlobDeleter interface { | ||||||
| 	Delete(ctx context.Context, dgst digest.Digest) error | 	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
 | // BlobDescriptorService manages metadata about a blob by digest. Most
 | ||||||
| // implementations will not expose such an interface explicitly. Such mappings
 | // implementations will not expose such an interface explicitly. Such mappings
 | ||||||
| // should be maintained by interacting with the BlobIngester. Hence, this is
 | // should be maintained by interacting with the BlobIngester. Hence, this is
 | ||||||
|  |  | ||||||
|  | @ -20,5 +20,5 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func main() { | func main() { | ||||||
| 	registry.Cmd.Execute() | 	registry.RootCmd.Execute() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,28 @@ | ||||||
|  | <!--[metadata]> | ||||||
|  | +++ | ||||||
|  | title = "Garbage Collection" | ||||||
|  | description = "High level discussion of garabage collection" | ||||||
|  | keywords = ["registry, garbage, images, tags, repository, distribution"] | ||||||
|  | +++ | ||||||
|  | <![end-metadata]--> | ||||||
|  | 
 | ||||||
|  | # 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. | ||||||
							
								
								
									
										16
									
								
								manifests.go
								
								
								
								
							
							
						
						
									
										16
									
								
								manifests.go
								
								
								
								
							|  | @ -53,12 +53,18 @@ type ManifestService interface { | ||||||
| 	// Delete removes the manifest specified by the given digest. Deleting
 | 	// Delete removes the manifest specified by the given digest. Deleting
 | ||||||
| 	// a manifest that doesn't exist will return ErrManifestNotFound
 | 	// a manifest that doesn't exist will return ErrManifestNotFound
 | ||||||
| 	Delete(ctx context.Context, dgst digest.Digest) error | 	Delete(ctx context.Context, dgst digest.Digest) error | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| 	// Enumerate fills 'manifests' with the manifests in this service up
 | // ManifestEnumerator enables iterating over manifests
 | ||||||
| 	// to the size of 'manifests' and returns 'n' for the number of entries
 | type ManifestEnumerator interface { | ||||||
| 	// which were filled.  'last' contains an offset in the manifest set
 | 	// Enumerate calls ingester for each manifest.
 | ||||||
| 	// and can be used to resume iteration.
 | 	Enumerate(ctx context.Context, ingester func(digest.Digest) error) error | ||||||
| 	//Enumerate(ctx context.Context, manifests []Manifest, last Manifest) (n int, err 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
 | // Describable is an interface for descriptors
 | ||||||
|  |  | ||||||
							
								
								
									
										11
									
								
								registry.go
								
								
								
								
							
							
						
						
									
										11
									
								
								registry.go
								
								
								
								
							|  | @ -40,6 +40,17 @@ type Namespace interface { | ||||||
| 	// which were filled.  'last' contains an offset in the catalog, and 'err' will be
 | 	// 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.
 | 	// set to io.EOF if there are no more entries to obtain.
 | ||||||
| 	Repositories(ctx context.Context, repos []string, last string) (n int, err error) | 	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
 | // ManifestServiceOption is a function argument for Manifest Service methods
 | ||||||
|  |  | ||||||
|  | @ -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 <config>", | ||||||
|  | 	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) | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | } | ||||||
|  | @ -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) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -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 { | func (pms proxyManifestStore) Delete(ctx context.Context, dgst digest.Digest) error { | ||||||
| 	return distribution.ErrUnsupported | 	return distribution.ErrUnsupported | ||||||
| } | } | ||||||
| 
 |  | ||||||
| /*func (pms proxyManifestStore) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) { |  | ||||||
| 	return 0, distribution.ErrUnsupported |  | ||||||
| } |  | ||||||
| */ |  | ||||||
|  |  | ||||||
|  | @ -166,6 +166,14 @@ func (pr *proxyingRegistry) Repository(ctx context.Context, name reference.Named | ||||||
| 	}, nil | 	}, 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
 | // authChallenger encapsulates a request to the upstream to establish credential challenges
 | ||||||
| type authChallenger interface { | type authChallenger interface { | ||||||
| 	tryEstablishChallenges(context.Context) error | 	tryEstablishChallenges(context.Context) error | ||||||
|  |  | ||||||
|  | @ -24,16 +24,12 @@ import ( | ||||||
| 	"github.com/yvasiyarov/gorelic" | 	"github.com/yvasiyarov/gorelic" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Cmd is a cobra command for running the registry.
 | // ServeCmd is a cobra command for running the registry.
 | ||||||
| var Cmd = &cobra.Command{ | var ServeCmd = &cobra.Command{ | ||||||
| 	Use:   "registry <config>", | 	Use:   "serve <config>", | ||||||
| 	Short: "registry stores and distributes Docker images", | 	Short: "`serve` stores and distributes Docker images", | ||||||
| 	Long:  "registry stores and distributes Docker images.", | 	Long:  "`serve` stores and distributes Docker images.", | ||||||
| 	Run: func(cmd *cobra.Command, args []string) { | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 		if showVersion { |  | ||||||
| 			version.PrintVersion() |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
| 		// setup context
 | 		// setup context
 | ||||||
| 		ctx := context.WithVersion(context.Background(), version.Version) | 		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.
 | // A Registry represents a complete instance of the registry.
 | ||||||
| // TODO(aaronl): It might make sense for Registry to become an interface.
 | // TODO(aaronl): It might make sense for Registry to become an interface.
 | ||||||
| type Registry struct { | type Registry struct { | ||||||
|  |  | ||||||
|  | @ -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() | ||||||
|  | 	}, | ||||||
|  | } | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| package storage | package storage | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"path" | ||||||
|  | 
 | ||||||
| 	"github.com/docker/distribution" | 	"github.com/docker/distribution" | ||||||
| 	"github.com/docker/distribution/context" | 	"github.com/docker/distribution/context" | ||||||
| 	"github.com/docker/distribution/digest" | 	"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) | 	}, 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
 | // path returns the canonical path for the blob identified by digest. The blob
 | ||||||
| // may or may not exist.
 | // may or may not exist.
 | ||||||
| func (bs *blobStore) path(dgst digest.Digest) (string, error) { | func (bs *blobStore) path(dgst digest.Digest) (string, error) { | ||||||
|  |  | ||||||
|  | @ -64,3 +64,34 @@ func (reg *registry) Repositories(ctx context.Context, repos []string, last stri | ||||||
| 
 | 
 | ||||||
| 	return n, errVal | 	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 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ package storage | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"path" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/docker/distribution" | 	"github.com/docker/distribution" | ||||||
|  | @ -37,6 +38,9 @@ type linkedBlobStore struct { | ||||||
| 	// removed an the blob links folder should be merged. The first entry is
 | 	// 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.
 | 	// treated as the "canonical" link location and will be used for writes.
 | ||||||
| 	linkPathFns []linkPathFunc | 	linkPathFns []linkPathFunc | ||||||
|  | 
 | ||||||
|  | 	// linkDirectoryPathSpec locates the root directories in which one might find links
 | ||||||
|  | 	linkDirectoryPathSpec pathSpec | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var _ distribution.BlobStore = &linkedBlobStore{} | var _ distribution.BlobStore = &linkedBlobStore{} | ||||||
|  | @ -236,6 +240,55 @@ func (lbs *linkedBlobStore) Delete(ctx context.Context, dgst digest.Digest) erro | ||||||
| 	return nil | 	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) { | func (lbs *linkedBlobStore) mount(ctx context.Context, sourceRepo reference.Named, dgst digest.Digest) (distribution.Descriptor, error) { | ||||||
| 	repo, err := lbs.registry.Repository(ctx, sourceRepo) | 	repo, err := lbs.registry.Repository(ctx, sourceRepo) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ package storage | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"path" | ||||||
| 
 | 
 | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"github.com/docker/distribution" | 	"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) | 	return ms.blobStore.Delete(ctx, dgst) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ms *manifestStore) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) { | func (ms *manifestStore) Enumerate(ctx context.Context, ingester func(digest.Digest) error) error { | ||||||
| 	return 0, distribution.ErrUnsupported | 	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 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -74,6 +74,7 @@ const ( | ||||||
| //
 | //
 | ||||||
| //	Manifests:
 | //	Manifests:
 | ||||||
| //
 | //
 | ||||||
|  | // 	manifestRevisionsPathSpec:      <root>/v2/repositories/<name>/_manifests/revisions/
 | ||||||
| // 	manifestRevisionPathSpec:      <root>/v2/repositories/<name>/_manifests/revisions/<algorithm>/<hex digest>/
 | // 	manifestRevisionPathSpec:      <root>/v2/repositories/<name>/_manifests/revisions/<algorithm>/<hex digest>/
 | ||||||
| // 	manifestRevisionLinkPathSpec:  <root>/v2/repositories/<name>/_manifests/revisions/<algorithm>/<hex digest>/link
 | // 	manifestRevisionLinkPathSpec:  <root>/v2/repositories/<name>/_manifests/revisions/<algorithm>/<hex digest>/link
 | ||||||
| // 	manifestSignaturesPathSpec:    <root>/v2/repositories/<name>/_manifests/revisions/<algorithm>/<hex digest>/signatures/
 | // 	manifestSignaturesPathSpec:    <root>/v2/repositories/<name>/_manifests/revisions/<algorithm>/<hex digest>/signatures/
 | ||||||
|  | @ -100,6 +101,7 @@ const ( | ||||||
| //
 | //
 | ||||||
| //	Blob Store:
 | //	Blob Store:
 | ||||||
| //
 | //
 | ||||||
|  | //	blobsPathSpec:                  <root>/v2/blobs/
 | ||||||
| // 	blobPathSpec:                   <root>/v2/blobs/<algorithm>/<first two hex bytes of digest>/<hex digest>
 | // 	blobPathSpec:                   <root>/v2/blobs/<algorithm>/<first two hex bytes of digest>/<hex digest>
 | ||||||
| // 	blobDataPathSpec:               <root>/v2/blobs/<algorithm>/<first two hex bytes of digest>/<hex digest>/data
 | // 	blobDataPathSpec:               <root>/v2/blobs/<algorithm>/<first two hex bytes of digest>/<hex digest>/data
 | ||||||
| // 	blobMediaTypePathSpec:               <root>/v2/blobs/<algorithm>/<first two hex bytes of digest>/<hex digest>/data
 | // 	blobMediaTypePathSpec:               <root>/v2/blobs/<algorithm>/<first two hex bytes of digest>/<hex digest>/data
 | ||||||
|  | @ -125,6 +127,9 @@ func pathFor(spec pathSpec) (string, error) { | ||||||
| 
 | 
 | ||||||
| 	switch v := spec.(type) { | 	switch v := spec.(type) { | ||||||
| 
 | 
 | ||||||
|  | 	case manifestRevisionsPathSpec: | ||||||
|  | 		return path.Join(append(repoPrefix, v.name, "_manifests", "revisions")...), nil | ||||||
|  | 
 | ||||||
| 	case manifestRevisionPathSpec: | 	case manifestRevisionPathSpec: | ||||||
| 		components, err := digestPathComponents(v.revision, false) | 		components, err := digestPathComponents(v.revision, false) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | @ -246,6 +251,17 @@ func pathFor(spec pathSpec) (string, error) { | ||||||
| 		blobLinkPathComponents := append(repoPrefix, v.name, "_layers") | 		blobLinkPathComponents := append(repoPrefix, v.name, "_layers") | ||||||
| 
 | 
 | ||||||
| 		return path.Join(path.Join(append(blobLinkPathComponents, components...)...), "link"), nil | 		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: | 	case blobDataPathSpec: | ||||||
| 		components, err := digestPathComponents(v.digest, true) | 		components, err := digestPathComponents(v.digest, true) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | @ -281,6 +297,14 @@ type pathSpec interface { | ||||||
| 	pathSpec() | 	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
 | // manifestRevisionPathSpec describes the components of the directory path for
 | ||||||
| // a manifest revision.
 | // a manifest revision.
 | ||||||
| type manifestRevisionPathSpec struct { | type manifestRevisionPathSpec struct { | ||||||
|  | @ -404,12 +428,17 @@ var blobAlgorithmReplacer = strings.NewReplacer( | ||||||
| 	";", "/", | 	";", "/", | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // // blobPathSpec contains the path for the registry global blob store.
 | // blobsPathSpec contains the path for the blobs directory
 | ||||||
| // type blobPathSpec struct {
 | type blobsPathSpec struct{} | ||||||
| // 	digest digest.Digest
 |  | ||||||
| // }
 |  | ||||||
| 
 | 
 | ||||||
| // 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
 | // blobDataPathSpec contains the path for the registry global blob store. For
 | ||||||
| // now, this contains layer data, exclusively.
 | // now, this contains layer data, exclusively.
 | ||||||
|  | @ -491,3 +520,23 @@ func digestPathComponents(dgst digest.Digest, multilevel bool) ([]string, error) | ||||||
| 
 | 
 | ||||||
| 	return append(prefix, suffix...), nil | 	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() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -2,6 +2,8 @@ package storage | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/docker/distribution/digest" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestPathMapper(t *testing.T) { | 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) | ||||||
|  | 
 | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -147,6 +147,14 @@ func (reg *registry) Repository(ctx context.Context, canonicalName reference.Nam | ||||||
| 	}, nil | 	}, 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.
 | // repository provides name-scoped access to various services.
 | ||||||
| type repository struct { | type repository struct { | ||||||
| 	*registry | 	*registry | ||||||
|  | @ -180,6 +188,8 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M | ||||||
| 		blobLinkPath, | 		blobLinkPath, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	manifestDirectoryPathSpec := manifestRevisionsPathSpec{name: repo.name.Name()} | ||||||
|  | 
 | ||||||
| 	blobStore := &linkedBlobStore{ | 	blobStore := &linkedBlobStore{ | ||||||
| 		ctx:           ctx, | 		ctx:           ctx, | ||||||
| 		blobStore:     repo.blobStore, | 		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
 | 		// TODO(stevvooe): linkPath limits this blob store to only
 | ||||||
| 		// manifests. This instance cannot be used for blob checks.
 | 		// manifests. This instance cannot be used for blob checks.
 | ||||||
| 		linkPathFns: manifestLinkPathFns, | 		linkPathFns:           manifestLinkPathFns, | ||||||
|  | 		linkDirectoryPathSpec: manifestDirectoryPathSpec, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	ms := &manifestStore{ | 	ms := &manifestStore{ | ||||||
|  |  | ||||||
|  | @ -34,11 +34,13 @@ func (v Vacuum) RemoveBlob(dgst string) error { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	blobPath, err := pathFor(blobDataPathSpec{digest: d}) | 	blobPath, err := pathFor(blobPathSpec{digest: d}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	context.GetLogger(v.ctx).Infof("Deleting blob: %s", blobPath) | 	context.GetLogger(v.ctx).Infof("Deleting blob: %s", blobPath) | ||||||
|  | 
 | ||||||
| 	err = v.driver.Delete(v.ctx, blobPath) | 	err = v.driver.Delete(v.ctx, blobPath) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | } | ||||||
|  | @ -9,6 +9,8 @@ import ( | ||||||
| 	mrand "math/rand" | 	mrand "math/rand" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/docker/distribution" | ||||||
|  | 	"github.com/docker/distribution/context" | ||||||
| 	"github.com/docker/distribution/digest" | 	"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 | 	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 | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue