├── .github └── workflows │ └── main.yaml ├── .gitignore ├── .grenrc.yml ├── .headache-run ├── AUTHORS ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── VERSION ├── azure-pipelines.yml ├── cmd └── irel │ └── main.go ├── codecov.yml ├── go.mod ├── go.sum ├── headache.json ├── license-header.txt └── pkg ├── image ├── digest.go ├── digest_test.go ├── doc.go ├── image_suite_test.go ├── name.go └── name_test.go ├── images ├── images_suite_test.go ├── set.go └── set_test.go ├── irel ├── copy.go ├── digest.go ├── env.go ├── env_test.go ├── irel_suite_test.go ├── layout.go ├── layout_add.go ├── layout_find.go ├── layout_push.go ├── map.go └── root.go ├── pathmapping ├── doc.go ├── ginkgo_suite_test.go ├── path_mapping.go ├── path_mapping_test.go ├── tag_digest_mapping.go └── tag_digest_mapping_test.go ├── registry ├── client.go ├── doc.go ├── ggcr │ ├── client.go │ ├── client_test.go │ ├── ggcr_suite_test.go │ ├── image.go │ ├── image_test.go │ ├── layout.go │ ├── layout_test.go │ ├── path │ │ ├── layoutpath.go │ │ └── pathfakes │ │ │ └── fake_layout_path.go │ ├── registryclientfakes │ │ └── fake_registry_client.go │ ├── remote.go │ └── remote_test.go ├── ggcrfakes │ ├── fake_image.go │ └── fake_image_index.go ├── image.go ├── layout.go └── registryfakes │ └── fake_image.go └── transport ├── http.go ├── http_test.go ├── testdata ├── ca.crt └── empty.crt └── transport_suite_test.go /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: release workflow 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | build: 8 | name: release action 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - uses: actions/setup-go@v1 13 | with: 14 | go-version: '1.12' 15 | - name: build binaries 16 | run: | 17 | make release 18 | - name: populate release 19 | uses: softprops/action-gh-release@v1 20 | with: 21 | files: irel-*-amd64* 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.txt 2 | /irel 3 | /irel-darwin-amd64.tgz 4 | /irel-linux-amd64.tgz 5 | /irel-windows-amd64.zip 6 | .idea 7 | -------------------------------------------------------------------------------- /.grenrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dataSource: "prs" 3 | onlyMilestones: false 4 | groupBy: false 5 | -------------------------------------------------------------------------------- /.headache-run: -------------------------------------------------------------------------------- 1 | # Generated by headache | 1578071397 -- commit me! 2 | encoded_configuration:ewogICJoZWFkZXJGaWxlIjogIi4vbGljZW5zZS1oZWFkZXIudHh0IiwKICAic3R5bGUiOiAiU2xhc2hTdGFyIiwKICAiaW5jbHVkZXMiOiBbIioqLyouZ28iXSwKICAiZXhjbHVkZXMiOiBbIioqL2Zha2VfKi5nbyJdLAogICJkYXRhIjogewogICAgIk93bmVyIjogIlBpdm90YWwgU29mdHdhcmUsIEluYy4iCiAgfQp9 3 | encoded_header:Q29weXJpZ2h0IChjKSB7ey5TdGFydFllYXJ9fS1QcmVzZW50IHt7Lk93bmVyfX0gQWxsIHJpZ2h0cyByZXNlcnZlZC4KCkxpY2Vuc2VkIHVuZGVyIHRoZSBBcGFjaGUgTGljZW5zZSwgVmVyc2lvbiAyLjAgKHRoZSAiTGljZW5zZSIpOwp5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlIHdpdGggdGhlIExpY2Vuc2UuCllvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgIGh0dHA6Ly93d3cuYXBhY2hlLm9yZy9saWNlbnNlcy9MSUNFTlNFLTIuMAoKVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQpkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLApXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IgaW1wbGllZC4KU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZApsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4= 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of pivotal/image-relocation authors for copyright purposes. 2 | # 3 | # This does not necessarily list everyone who has contributed code, since in 4 | # some cases, their employer may be the copyright holder. To see the full list 5 | # of contributors, see the revision history in source control. 6 | Pivotal Software, Inc. 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Pivotal Projects 2 | 3 | We’d love to accept your patches and contributions to this project. Please review the following guidelines you'll need to follow in order to make a contribution. 4 | 5 | ## Contributor License Agreement 6 | 7 | All contributors to this project must have a signed Contributor License Agreement (**"CLA"**) on file with us. The CLA grants us the permissions we need to use and redistribute your contributions as part of the project; you or your employer retain the copyright to your contribution. Head over to https://cla.pivotal.io/ to see your current agreement(s) on file or to sign a new one. 8 | 9 | We generally only need you (or your employer) to sign our CLA once and once signed, you should be able to submit contributions to any Pivotal project. 10 | 11 | Note: if you would like to submit an "_obvious fix_" for something like a typo, formatting issue or spelling mistake, you may not need to sign the CLA. Please see our information on [obvious fixes](https://cla.pivotal.io/about#obvious-fix) for more details. 12 | 13 | ## Code reviews 14 | 15 | All submissions, including submissions by project members, require review and we use GitHub's pull requests for this purpose. Please consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) if you need more information about using pull requests. 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test all check-counterfeiter gen-mocks release 2 | 3 | all: test 4 | 5 | OUTPUT = ./irel 6 | GO_SOURCES = $(shell find . -type f -name '*.go') 7 | VERSION ?= $(shell cat VERSION) 8 | GITSHA = $(shell git rev-parse HEAD) 9 | GITDIRTY = $(shell git diff --quiet HEAD || echo "dirty") 10 | LDFLAGS_VERSION = -X github.com/pivotal/image-relocation/pkg/irel.cli_version=$(VERSION) \ 11 | -X github.com/pivotal/image-relocation/pkg/irel.cli_gitsha=$(GITSHA) \ 12 | -X github.com/pivotal/image-relocation/pkg/irel.cli_gitdirty=$(GITDIRTY) 13 | 14 | test: 15 | GO111MODULE=on go test ./... -coverprofile=coverage.txt -covermode=atomic 16 | 17 | check-counterfeiter: 18 | # Use go get in GOPATH mode to install/update counterfeiter. This avoids polluting go.mod/go.sum. 19 | @which counterfeiter > /dev/null || (echo counterfeiter not found: issue "GO111MODULE=off go get -u github.com/maxbrunsfeld/counterfeiter" && false) 20 | 21 | gen-mocks: check-counterfeiter 22 | counterfeiter -o pkg/registry/ggcrfakes/fake_image.go github.com/google/go-containerregistry/pkg/v1.Image 23 | counterfeiter -o pkg/registry/ggcrfakes/fake_image_index.go github.com/google/go-containerregistry/pkg/v1.ImageIndex 24 | counterfeiter pkg/registry/ggcr/path LayoutPath 25 | counterfeiter pkg/registry Image 26 | counterfeiter -o pkg/registry/ggcr/registryclientfakes/fake_registry_client.go ./pkg/registry/ggcr RegistryClient 27 | 28 | irel: $(GO_SOURCES) 29 | GO111MODULE=on go build -ldflags "$(LDFLAGS_VERSION)" -o $(OUTPUT) cmd/irel/main.go 30 | 31 | release: $(GO_SOURCES) test 32 | GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS_VERSION)" -o $(OUTPUT) cmd/irel/main.go && tar -czf irel-darwin-amd64.tgz $(OUTPUT) && rm -f $(OUTPUT) 33 | GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS_VERSION)" -o $(OUTPUT) cmd/irel/main.go && tar -czf irel-linux-amd64.tgz $(OUTPUT) && rm -f $(OUTPUT) 34 | GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS_VERSION)" -o $(OUTPUT).exe cmd/irel/main.go && zip -mq irel-windows-amd64.zip $(OUTPUT).exe && rm -f $(OUTPUT).exe 35 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | image relocation utilities 2 | 3 | Copyright (c) 2018 - Present Pivotal Software, Inc. All Rights Reserved. 4 | 5 | This product is licensed to you under the Apache License, Version 2.0 (the "License"). You may not use this product 6 | except in compliance with the License. 7 | 8 | This product may include a number of subcomponents with separate copyright notices and license terms. Your use of these 9 | subcomponents is subject to the terms and conditions of the subcomponent's license, as noted in the LICENSE file. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker/OCI image relocation 2 | 3 | [![GoDoc](https://godoc.org/github.com/pivotal/image-relocation?status.svg)](https://godoc.org/github.com/pivotal/image-relocation) 4 | [![Go Report Card](https://goreportcard.com/badge/pivotal/image-relocation)](https://goreportcard.com/report/pivotal/image-relocation) 5 | [![Build Status](https://dev.azure.com/projectriff/pivotal-image-relocation/_apis/build/status/pivotal.image-relocation?branchName=master)](https://dev.azure.com/projectriff/pivotal-image-relocation/_build/latest?definitionId=11&branchName=master) 6 | [![codecov](https://codecov.io/gh/pivotal/image-relocation/branch/master/graph/badge.svg)](https://codecov.io/gh/pivotal/image-relocation) 7 | 8 | This repository contains a Go module for relocating Docker and OCI images. 9 | 10 | ## What is image relocation? 11 | _Relocating_ an image means copying it to another repository, possibly in a private registry. 12 | 13 | Using a separate registry has some advantages: 14 | * It provides complete control over when the image is updated or deleted: 15 | * This provides isolation from unwanted updates or deletion of the original image. 16 | * If the image becomes stale, for instance when it has known vulnerabilities, it can be deleted. 17 | * The registry can be hosted on a private network for security or other reasons. 18 | 19 | A highly desirable property of image relocation is that the image digest of the relocated image is the same as that of the original images. 20 | This gives the user confidence that the relocated image consists of the same bits as the original image. 21 | 22 | ## Relocating image names 23 | An image name consists of a domain name (with optional port) and a path. The image name may also contain a tag and/or a digest. 24 | The domain name determines the network location of a registry. 25 | The path consists of one or more components separated by forward slashes. 26 | The first component is sometimes, by convention for certain registries, a user name providing access control to the image. 27 | 28 | Let’s look at some examples: 29 | * The image name `docker.io/istio/proxyv2` refers to an image with user name `istio` residing in the docker hub registry at `docker.io`. 30 | * The image name `projectriff/builder:v1` is short-hand for `docker.io/projectriff/builder:v1` which refers to an image with user name `projectriff` also residing at `docker.io`. 31 | * The image name `gcr.io/cf-elafros/knative-releases/github.com/knative/serving/cmd/autoscaler@sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef` refers to an image with user name `cf-elafros` residing at `gcr.io`. 32 | 33 | When an image is relocated to a registry, the domain name is set to that of the registry. 34 | Relocation takes a _repository prefix_ which is used to prefix the relocated image names. 35 | 36 | The path of a relocated image may: 37 | * Include the original user name for readability 38 | * Be “flattened” to accommodate registries which do not support hierarchical paths with more than two components 39 | * End with a hash of the image name (to avoid collisions) 40 | * Preserve any tag in the original image name 41 | * Preserve any digest in the original image name. 42 | 43 | For instance, when relocated to a repository prefix `example.com/user`, the above image names might become something like this: 44 | * `example.com/user/istio-proxyv2-f93a2cacc6cafa0474a2d6990a4dd1a0` 45 | * `example.com/user/projectriff-builder-a4a25a99d48adad8310050be267a10ce:v1` 46 | * `example.com/user/cf-elafros-knative-releases-github.com-knative-serving-cmd-autoscaler-c74d62dc488234d6d1aaa38808898140@sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef` 47 | 48 | The hash added to the end of the relocated image path should not depend on any tag and/or digest in 49 | the original image name. This ensures a one-to-one mapping between repositories. In other words, if: 50 | 51 | x maps to y 52 | 53 | where `x` and `y` are image names without tags or digests, then 54 | 55 | x:t maps to y:t (for all tags t) 56 | 57 | and 58 | 59 | x@d maps to y@d (for all digests d). 60 | 61 | ## Bundles and relocation mappings 62 | 63 | From an image relocation point of view, a _bundle_ is a software package which declares the images it uses, 64 | typically using image references. 65 | 66 | A _thin_ bundle's image references refer to repositories on the internet. 67 | When the thin bundle is installed and run, the images are pulled from their repositories. 68 | 69 | A _thick_ bundle's image references refer to binary images packaged with the bundle, typically in an archive file. 70 | The images must somehow be loaded from the bundle before they can be used. 71 | 72 | The images of a bundle can be relocated to a registry in which case a _relocation mapping_ maps 73 | each original image reference to its relocated counterpart. The keys of the relocation mapping are the images 74 | declared by the bundle. The relocation mapping needs to be applied to the bundle so that when the bundle 75 | is installed and run, it will pull its images from the registry. 76 | 77 | A thick bundle needs to be relocated before its images can be pulled. 78 | A thin bundle _may_ be relocated, although this is not usually necessary. 79 | 80 | Note: the terminology in this section originated in the [CNAB standard](https://cnab.io/). 81 | 82 | ## Example scenarios 83 | 84 | The following scenarios, adapted from the 85 | [relocation guide](https://github.com/deislabs/duffle/blob/master/docs/guides/relocation-guide.md) of the CNAB reference 86 | implementation, describe relocation of thin and thick bundles. 87 | 88 | ### Thin bundle relocation 89 | 90 | The [Acme Corporation](https://en.wikipedia.org/wiki/Acme_Corporation) needs to install some "forge" software packaged as a thin bundle (`forge.json`). 91 | Acme is used to things going wrong, so they have evolved sophisticated processes to protect their systems. 92 | In particular, all their production software must be loaded from Acme-internal repositories. 93 | This protects them from outages when an external repository goes down. 94 | It also gives them complete control over what software they run in production. 95 | 96 | So Acme needs to pull the images referenced by `forge.json` from external repositories and store them in an Acme-internal registry. 97 | This will be done in a DMZ with access to the internet and write access to the internal registry. 98 | 99 | Suppose their internal registry is hosted at `registry.internal.acme.com` and they have created a user `smith` to manage the forge software. 100 | They can relocate the images to their registry using a repository prefix `registry.internal.acme.com/smith`. 101 | They can now install the bundle and use the relocation mapping to reconfigure the bundle to use 102 | the relocated image names instead of the original image names. 103 | 104 | When the bundle runs, the images are pulled from the internal registry. 105 | 106 | ### Thick bundle relocation 107 | 108 | Gringotts Wizarding Bank (GWB) needs to install some software into a new coin sorting machine. 109 | For GWB, security is paramount. Like Acme, all their production software must be loaded from internal repositories. 110 | However, GWB regard a networked DMZ as too insecure. Their data center has no connection to the external internet. 111 | 112 | Software is delivered to GWB encoded in Base64 and etched on large stones which are then rolled by hand into the 113 | GWB data center, scanned, and decoded. The stones are stored for future security audits. 114 | 115 | GWB obtains the new software as a thick bundle (`sort.tgz`) and relocates it to their private registry 116 | using a repository prefix of `registry.gold.gwb.dia/griphook`. 117 | This loads the images from `sort.tgz` into the private registry. Relocating from a thick bundle does not need 118 | access to the original image repositories (which would prevent it from running inside the GWB data center). 119 | 120 | They can now install the bundle and use the relocation mapping to reconfigure the bundle to use 121 | the relocated image names instead of the original image names. 122 | 123 | Again when the bundle runs, the images are pulled from the internal registry. 124 | Since relocation need not modify the original bundle or produce a new bundle, GWB can use the original stones 125 | in security audits. 126 | 127 | ## Packages provided 128 | 129 | The Go packages provided by this repository include: 130 | * some rich types representing image names and digests 131 | * a "path mapping" utility for relocating image names 132 | * a registry package for: 133 | * obtaining the digest of an image 134 | * copying images between repositories 135 | * copying images between repositories and an [OCI image layout](https://github.com/opencontainers/image-spec/blob/master/image-layout.md) on disk, e.g. to implement thick bundles. 136 | 137 | For details, please refer to the [package documentation](https://godoc.org/github.com/pivotal/image-relocation). 138 | 139 | ### Docker daemon 140 | 141 | This repository reads images directly from their repositories and does not attempt to read images 142 | from the Docker daemon. This is primarily because the daemon doesn't guarantee to provide the 143 | same digest of an image as when the image has been pushed to a repository. 144 | 145 | ## Command line interface 146 | 147 | A CLI, `irel`, is provided for manual use and experimentation. Issue `make irel` to build it. 148 | 149 | ## Where is this repository used? 150 | 151 | This repository was originally factored out of the [Pivotal Function Service](https://pivotal.io/platform/pivotal-function-service) 152 | which provided a command line interface for relocating the images in its distributions. 153 | 154 | [duffle](https://github.com/deislabs/duffle), the [CNAB](https://cnab.io/) reference implementation, uses this repository to relocate bundles. 155 | 156 | The riff project also [experimented](https://github.com/projectriff/cnab-k8s-installer-base) with using this 157 | repository to create CNAB bundles which could relocate themselves (before duffle could do relocation). 158 | 159 | [sheaf](https://github.com/bryanl/sheaf) uses this repository to create OCI image layouts and to relocate images. 160 | 161 | ## Alternatives 162 | 163 | If this repository isn't quite what you're looking for, try: 164 | * the underlying library: [ggcr](https://github.com/google/go-containerregistry) 165 | * [kbld](https://github.com/k14s/kbld) (also based on ggcr) 166 | * [k8s container image promoter](https://github.com/kubernetes-sigs/k8s-container-image-promoter) (also based on ggcr) 167 | 168 | ## Development 169 | 170 | To create a release on github, merge a commit which removes "-snapshot" from [VERSION](VERSION) (and, optionally, 171 | bumps the major version if there has been an incompatible change), then push a tag beginning with "v". 172 | To continue development of the next release, merge a commit which bumps the minor version in [VERSION](VERSION) and adds 173 | "-snapshot" back in. -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.9.0-snapshot -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Go 2 | # Build your Go project. 3 | # Add steps that test, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/go 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'Ubuntu-16.04' 11 | 12 | variables: 13 | GOBIN: '$(GOPATH)/bin' # Go binaries path 14 | GOROOT: '/usr/local/go1.12' # Go installation path 15 | GOPATH: '$(system.defaultWorkingDirectory)/gopath' # Go workspace path 16 | modulePath: '$(GOPATH)/src/github.com/$(build.repository.name)' # Path to the module's code 17 | 18 | steps: 19 | - script: | 20 | mkdir -p '$(GOBIN)' 21 | mkdir -p '$(GOPATH)/pkg' 22 | mkdir -p '$(modulePath)' 23 | shopt -s extglob 24 | shopt -s dotglob 25 | mv !(gopath) '$(modulePath)' 26 | echo '##vso[task.prependpath]$(GOBIN)' 27 | echo '##vso[task.prependpath]$(GOROOT)/bin' 28 | displayName: 'Set up the Go workspace' 29 | 30 | - script: | 31 | go version 32 | make 33 | workingDirectory: '$(modulePath)' 34 | displayName: 'make' 35 | 36 | - script: | 37 | bash <(curl https://codecov.io/bash) -f 'coverage.txt' 38 | workingDirectory: '$(modulePath)' 39 | displayName: 'codecov' -------------------------------------------------------------------------------- /cmd/irel/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "github.com/pivotal/image-relocation/pkg/irel" 22 | "os" 23 | ) 24 | 25 | func main() { 26 | cmd := irel.Root 27 | 28 | cmd.Version = irel.CliVersion() 29 | cmd.Flags().Bool("version", false, "display CLI version") 30 | 31 | if err := cmd.Execute(); err != nil { 32 | fmt.Println(err) 33 | os.Exit(1) 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | ci: 3 | - dev.azure.com # Azure pipelines server 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pivotal/image-relocation 2 | 3 | require ( 4 | github.com/docker/distribution v2.7.0+incompatible 5 | github.com/google/go-cmp v0.3.0 // indirect 6 | github.com/google/go-containerregistry v0.0.0-20191015185424-71da34e4d9b3 7 | github.com/onsi/ginkgo v1.10.1 8 | github.com/onsi/gomega v1.7.0 9 | github.com/opencontainers/go-digest v1.0.0-rc1 10 | github.com/pkg/errors v0.8.1 11 | github.com/spf13/cobra v0.0.5 12 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f // indirect 13 | ) 14 | 15 | go 1.12 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.25.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/Azure/azure-sdk-for-go v19.1.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= 3 | github.com/Azure/go-autorest v10.15.5+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= 6 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 7 | github.com/aws/aws-sdk-go v1.15.90/go.mod h1:es1KtYUFs7le0xQ3rOihkuoVD90z7D0fR2Qm4S00/gU= 8 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 9 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 10 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 11 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 16 | github.com/docker/cli v0.0.0-20190925022749-754388324470 h1:KrSeY2qJPl1blFLllwCMBIgwilomqEte/nb8dPhqY2o= 17 | github.com/docker/cli v0.0.0-20190925022749-754388324470/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 18 | github.com/docker/distribution v2.6.0-rc.1.0.20180327202408-83389a148052+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 19 | github.com/docker/distribution v2.7.0+incompatible h1:neUDAlf3wX6Ml4HdqTrbcOHXtfRN0TFIwt6YFL7N9RU= 20 | github.com/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 21 | github.com/docker/docker v1.4.2-0.20180531152204-71cd53e4a197/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 22 | github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ= 23 | github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= 24 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 25 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 26 | github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 27 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 28 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 29 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 30 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 31 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 32 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 33 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 34 | github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 35 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 36 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 37 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 38 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 39 | github.com/google/go-containerregistry v0.0.0-20191015185424-71da34e4d9b3 h1:Fvv300WkFzqeCIgU3eJ5jBXrykXvNZmD8bxTHtfwhvQ= 40 | github.com/google/go-containerregistry v0.0.0-20191015185424-71da34e4d9b3/go.mod h1:ZXFeSndFcK4vB1NR4voH1Zm38K7ViUNiYtfIBDxrwf0= 41 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 42 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 43 | github.com/googleapis/gnostic v0.2.2/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 44 | github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= 45 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 46 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 47 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 48 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 49 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 50 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 51 | github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 52 | github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 53 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 54 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 55 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 56 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 57 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 58 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 59 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 60 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 61 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 62 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 63 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 64 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 65 | github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= 66 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 67 | github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= 68 | github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 69 | github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= 70 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 71 | github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= 72 | github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= 73 | github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= 74 | github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 75 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 76 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 77 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 78 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 79 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 80 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 81 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 82 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 83 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 84 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 85 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 86 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 87 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 88 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 89 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 90 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 91 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 92 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 93 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 94 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 95 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 96 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 97 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 98 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 99 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 100 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 101 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 102 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 103 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 104 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 105 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 106 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 107 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 108 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= 109 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 110 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 111 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 112 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 113 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 114 | golang.org/x/oauth2 v0.0.0-20180724155351-3d292e4d0cdc/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 115 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= 116 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 118 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 119 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 120 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= 121 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 122 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 123 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 124 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v9p/3ea4Rz+nnM5K/i4= 127 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 128 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 129 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 130 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 131 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 132 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 133 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 134 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 135 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 136 | golang.org/x/tools v0.0.0-20191014205221-18e3458ac98b/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 137 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 138 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 139 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 140 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 141 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 142 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 143 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 144 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 145 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 146 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 147 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 148 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 149 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 150 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 151 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 152 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 153 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 154 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 155 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 156 | k8s.io/api v0.0.0-20180904230853-4e7be11eab3f/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= 157 | k8s.io/apimachinery v0.0.0-20180904193909-def12e63c512/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= 158 | k8s.io/client-go v0.0.0-20180910083459-2cefa64ff137/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= 159 | k8s.io/kube-openapi v0.0.0-20180731170545-e3762e86a74c/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= 160 | k8s.io/kubernetes v1.11.10/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= 161 | -------------------------------------------------------------------------------- /headache.json: -------------------------------------------------------------------------------- 1 | { 2 | "headerFile": "./license-header.txt", 3 | "style": "SlashStar", 4 | "includes": ["**/*.go"], 5 | "excludes": ["**/fake_*.go"], 6 | "data": { 7 | "Owner": "Pivotal Software, Inc." 8 | } 9 | } -------------------------------------------------------------------------------- /license-header.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) {{.StartYear}}-Present {{.Owner}} All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /pkg/image/digest.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package image 18 | 19 | import "github.com/opencontainers/go-digest" 20 | 21 | // Digest provides a CAS address of an image (either an image manifest or a manifest list). 22 | type Digest struct { 23 | dig digest.Digest 24 | } 25 | 26 | // NewDigest returns the Digest for a given digest string or an error if the digest string is invalid. 27 | // The digest string must be of the form "alg:hash" where alg is an algorithm, such as sha256, and hash 28 | // is a string output by that algorithm. 29 | func NewDigest(dig string) (Digest, error) { 30 | d, err := digest.Parse(dig) 31 | if err != nil { 32 | return EmptyDigest, err 33 | } 34 | return Digest{d}, nil 35 | } 36 | 37 | // EmptyDigest is an invalid, zero value for Digest. 38 | var EmptyDigest Digest 39 | 40 | func init() { 41 | EmptyDigest = Digest{""} 42 | } 43 | 44 | // String returns the string form of the Digest. 45 | func (d Digest) String() string { 46 | return string(d.dig) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/image/digest_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package image_test 18 | 19 | import ( 20 | . "github.com/onsi/ginkgo" 21 | . "github.com/onsi/gomega" 22 | "github.com/pivotal/image-relocation/pkg/image" 23 | ) 24 | 25 | var _ = Describe("Digest", func() { 26 | Describe("EmptyDigest", func() { 27 | It("should produce an empty string form", func() { 28 | Expect(image.EmptyDigest.String()).To(BeEmpty()) 29 | }) 30 | }) 31 | 32 | Describe("NewDigest", func() { 33 | var ( 34 | str string 35 | digest image.Digest 36 | err error 37 | ) 38 | 39 | JustBeforeEach(func() { 40 | digest, err = image.NewDigest(str) 41 | }) 42 | 43 | Context("when the input string is valid", func() { 44 | BeforeEach(func() { 45 | str = "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" 46 | }) 47 | 48 | It("should produce a digest with the correct string form", func() { 49 | Expect(err).NotTo(HaveOccurred()) 50 | Expect(digest.String()).To(Equal(str)) 51 | }) 52 | }) 53 | 54 | Context("when the input string is invalid", func() { 55 | BeforeEach(func() { 56 | str = "sha256:adeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" 57 | }) 58 | 59 | It("should return a suitable error", func() { 60 | Expect(err).To(MatchError("invalid checksum digest length")) 61 | }) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /pkg/image/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Package image is concerned with docker image names and digests 18 | package image 19 | -------------------------------------------------------------------------------- /pkg/image/image_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package image_test 18 | 19 | import ( 20 | "testing" 21 | 22 | . "github.com/onsi/ginkgo" 23 | . "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestImage(t *testing.T) { 27 | RegisterFailHandler(Fail) 28 | RunSpecs(t, "Image Suite") 29 | } 30 | -------------------------------------------------------------------------------- /pkg/image/name.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package image 18 | 19 | import ( 20 | "fmt" 21 | "path" 22 | "strings" 23 | 24 | "github.com/docker/distribution/reference" 25 | ) 26 | 27 | const ( 28 | dockerHubHost = "docker.io" 29 | fullDockerHubHost = "index.docker.io" 30 | ) 31 | 32 | // Name is a named image reference. It can refer to an image manifest or a manifest list (e.g. a multi-arch image). 33 | type Name struct { 34 | ref reference.Named 35 | } 36 | 37 | // EmptyName is an invalid, zero value for Name. 38 | var EmptyName Name 39 | 40 | func init() { 41 | EmptyName = Name{nil} 42 | } 43 | 44 | // NewName returns the Name for the given image reference or an error if the image reference is invalid. 45 | func NewName(i string) (Name, error) { 46 | ref, err := reference.ParseNormalizedNamed(i) 47 | if err != nil { 48 | return Name{}, fmt.Errorf("invalid image reference: %q", i) 49 | } 50 | return Name{ref}, nil 51 | } 52 | 53 | // Normalize returns a fully-qualified equivalent to the Name. Useful on synonyms. 54 | func (img Name) Normalize() Name { 55 | if img.ref == nil { 56 | return EmptyName 57 | } 58 | ref, err := NewName(img.String()) 59 | if err != nil { 60 | panic(err) // should never happen 61 | } 62 | return ref 63 | } 64 | 65 | // Name returns the string form of the Name without any tag or digest. 66 | func (img Name) Name() string { 67 | return img.ref.Name() 68 | } 69 | 70 | // String returns a string representation of the Name. 71 | func (img Name) String() string { 72 | if img.ref == nil { 73 | return "" 74 | } 75 | return img.ref.String() 76 | } 77 | 78 | // Host returns the host of the Name. See also Path. 79 | func (img Name) Host() string { 80 | h, _ := img.parseHostPath() 81 | return h 82 | } 83 | 84 | // Path returns the path of the name. See also Host. 85 | func (img Name) Path() string { 86 | _, p := img.parseHostPath() 87 | return p 88 | } 89 | 90 | // Tag returns the tag of the Name or an empty string if the Name is not tagged. 91 | func (img Name) Tag() string { 92 | if taggedRef, ok := img.ref.(reference.Tagged); ok { 93 | return taggedRef.Tag() 94 | } 95 | return "" 96 | } 97 | 98 | // WithTag returns a new Name with the same value as the Name, but with the given tag. It returns an error if and only 99 | // if the tag is invalid. 100 | func (img Name) WithTag(tag string) (Name, error) { 101 | namedTagged, err := reference.WithTag(img.ref, tag) 102 | if err != nil { 103 | return EmptyName, fmt.Errorf("Cannot apply tag %s to image.Name %v: %v", tag, img, err) 104 | } 105 | return Name{namedTagged}, nil 106 | } 107 | 108 | // Digest returns the digest of the Name or EmptyDigest if the Name does not have a digest. 109 | func (img Name) Digest() Digest { 110 | if digestedRef, ok := img.ref.(reference.Digested); ok { 111 | d, err := NewDigest(string(digestedRef.Digest())) 112 | if err != nil { 113 | panic(err) // should never happen 114 | } 115 | return d 116 | } 117 | return EmptyDigest 118 | } 119 | 120 | // WithoutTagOrDigest returns a new Name with the same value as the Name, but with any tag or digest removed. 121 | func (img Name) WithoutTagOrDigest() Name { 122 | return Name{reference.TrimNamed(img)} 123 | } 124 | 125 | // WithDigest returns a new Name with the same value as the Name, but with the given digest. It returns an error if and only 126 | // if the digest is invalid. 127 | func (img Name) WithDigest(digest Digest) (Name, error) { 128 | digested, err := reference.WithDigest(img.ref, digest.dig) 129 | if err != nil { 130 | return EmptyName, fmt.Errorf("Cannot apply digest %s to image.Name %v: %v", digest, img, err) 131 | } 132 | 133 | return Name{digested}, nil 134 | } 135 | 136 | // WithoutDigest returns a new Name with the same value as the Name, but with any digest removed. It preserves any tag. 137 | func (img Name) WithoutDigest() Name { 138 | n := img.WithoutTagOrDigest() 139 | tag := img.Tag() 140 | if tag == "" { 141 | return n 142 | } 143 | n, _ = n.WithTag(tag) 144 | return n 145 | } 146 | 147 | // Synonyms returns the image names equivalent to a given image name. A synonym is not necessarily 148 | // normalized: in particular it may not have a host name. 149 | func (img Name) Synonyms() []Name { 150 | if img.ref == nil { 151 | return []Name{EmptyName} 152 | } 153 | imgHost, imgRepoPath := img.parseHostPath() 154 | nameMap := map[Name]struct{}{img: {}} 155 | 156 | if imgHost == dockerHubHost { 157 | elidedImg := imgRepoPath 158 | name, err := synonym(img, elidedImg) 159 | if err == nil { 160 | nameMap[name] = struct{}{} 161 | } 162 | 163 | elidedImgElements := strings.Split(elidedImg, "/") 164 | if len(elidedImgElements) == 2 && elidedImgElements[0] == "library" { 165 | name, err := synonym(img, elidedImgElements[1]) 166 | if err == nil { 167 | nameMap[name] = struct{}{} 168 | } 169 | } 170 | 171 | fullImg := path.Join(fullDockerHubHost, imgRepoPath) 172 | name, err = synonym(img, fullImg) 173 | if err == nil { 174 | nameMap[name] = struct{}{} 175 | } 176 | 177 | dockerImg := path.Join(dockerHubHost, imgRepoPath) 178 | name, err = synonym(img, dockerImg) 179 | if err == nil { 180 | nameMap[name] = struct{}{} 181 | } 182 | } 183 | 184 | names := []Name{} 185 | for n := range nameMap { 186 | names = append(names, n) 187 | } 188 | 189 | return names 190 | } 191 | 192 | func synonym(original Name, newName string) (Name, error) { 193 | named, err := reference.WithName(newName) 194 | if err != nil { 195 | return EmptyName, err 196 | } 197 | 198 | if taggedRef, ok := original.ref.(reference.Tagged); ok { 199 | named, err = reference.WithTag(named, taggedRef.Tag()) 200 | if err != nil { 201 | return EmptyName, err 202 | } 203 | } 204 | 205 | if digestedRef, ok := original.ref.(reference.Digested); ok { 206 | named, err = reference.WithDigest(named, digestedRef.Digest()) 207 | if err != nil { 208 | return EmptyName, err 209 | } 210 | } 211 | 212 | return Name{named}, nil 213 | } 214 | 215 | func (img Name) parseHostPath() (host string, repoPath string) { 216 | s := strings.SplitN(img.ref.Name(), "/", 2) 217 | if len(s) == 1 { 218 | return img.Normalize().parseHostPath() 219 | } 220 | return s[0], s[1] 221 | } 222 | -------------------------------------------------------------------------------- /pkg/images/images_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package images_test 18 | 19 | import ( 20 | "testing" 21 | 22 | . "github.com/onsi/ginkgo" 23 | . "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestImages(t *testing.T) { 27 | RegisterFailHandler(Fail) 28 | RunSpecs(t, "Images Suite") 29 | } 30 | -------------------------------------------------------------------------------- /pkg/images/set.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package images 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "sort" 23 | "strings" 24 | 25 | "github.com/pivotal/image-relocation/pkg/image" 26 | ) 27 | 28 | // Set is an immutable collection of image references without duplicates. 29 | type Set struct { 30 | m map[image.Name]struct{} // wrap map in a struct to avoid exposing map operations 31 | } 32 | 33 | // Empty is a Set with no image references. 34 | var Empty Set 35 | 36 | func init() { 37 | Empty = empty() 38 | } 39 | 40 | func empty() Set { 41 | return Set{ 42 | m: make(map[image.Name]struct{}), 43 | } 44 | } 45 | 46 | // New constructs a Set from some image references. 47 | func New(ss ...string) (Set, error) { 48 | set := empty() 49 | for _, s := range ss { 50 | name, err := image.NewName(s) 51 | if err != nil { 52 | return Set{}, err 53 | } 54 | set.m[name] = struct{}{} 55 | } 56 | return set, nil 57 | } 58 | 59 | func (s Set) clone() Set { 60 | c := empty() 61 | for i := range s.m { 62 | c.m[i] = struct{}{} 63 | } 64 | return c 65 | } 66 | 67 | // Union returns the mathematical union of this Set with another Set. 68 | func (s Set) Union(t Set) Set { 69 | u := s.clone() 70 | for i := range t.m { 71 | u.m[i] = struct{}{} 72 | } 73 | return u 74 | } 75 | 76 | // Slice returns this set as an unsorted slice of image references. 77 | func (s Set) Slice() []image.Name { 78 | var result []image.Name 79 | for i := range s.m { 80 | result = append(result, i) 81 | } 82 | return result 83 | } 84 | 85 | // Strings returns the image references in the set as a sorted slice of strings. 86 | func (s Set) Strings() []string { 87 | var result []string 88 | for i := range s.m { 89 | result = append(result, i.String()) 90 | } 91 | sort.Strings(result) 92 | return result 93 | } 94 | 95 | // String returns a sorted, string representation of the set. 96 | func (s Set) String() string { 97 | return fmt.Sprintf("[%s]", strings.Join(s.Strings(), ", ")) 98 | } 99 | 100 | // MarshalJSON encodes this Set as a JSON array of image references. 101 | func (s Set) MarshalJSON() ([]byte, error) { 102 | return json.Marshal(s.Strings()) 103 | } 104 | 105 | // UnmarshalJSON decodes a JSON array of image references into a Set. 106 | func (s *Set) UnmarshalJSON(data []byte) error { 107 | var v interface{} 108 | if err := json.Unmarshal(data, &v); err != nil { 109 | return err 110 | } 111 | 112 | if v == nil { 113 | *s = Empty 114 | return nil 115 | } 116 | 117 | refs, ok := v.([]interface{}) 118 | if !ok { 119 | return fmt.Errorf("unmarshalled data not a slice: %v", v) 120 | } 121 | 122 | var strs []string 123 | for _, ref := range refs { 124 | x, ok := ref.(string) 125 | if !ok { 126 | return fmt.Errorf("unmarshalled slice contains a value which is not a string: %v", x) 127 | } 128 | strs = append(strs, x) 129 | } 130 | var err error 131 | *s, err = New(strs...) 132 | return err 133 | } 134 | -------------------------------------------------------------------------------- /pkg/images/set_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package images_test 18 | 19 | import ( 20 | "fmt" 21 | 22 | . "github.com/onsi/ginkgo" 23 | . "github.com/onsi/gomega" 24 | 25 | "github.com/pivotal/image-relocation/pkg/image" 26 | "github.com/pivotal/image-relocation/pkg/images" 27 | ) 28 | 29 | var _ = Describe("Image set", func() { 30 | const ( 31 | refA = "example.com/u/a" 32 | refB = "example.com/u/b" 33 | ) 34 | 35 | Describe("New", func() { 36 | var ( 37 | ii []string 38 | s images.Set 39 | err error 40 | ) 41 | 42 | JustBeforeEach(func() { 43 | s, err = images.New(ii...) 44 | }) 45 | 46 | Context("when the input slice is empty", func() { 47 | BeforeEach(func() { 48 | ii = []string{} 49 | }) 50 | 51 | It("should construct an empty set", func() { 52 | Expect(err).NotTo(HaveOccurred()) 53 | Expect(s).To(Equal(images.Empty)) 54 | }) 55 | }) 56 | 57 | Context("when the input slice is non-empty", func() { 58 | BeforeEach(func() { 59 | ii = []string{refA, refB} 60 | }) 61 | 62 | It("should construct the correct set", func() { 63 | Expect(err).NotTo(HaveOccurred()) 64 | 65 | v, err := images.New(refB, refA) 66 | Expect(err).NotTo(HaveOccurred()) 67 | 68 | Expect(s).To(Equal(v)) 69 | }) 70 | }) 71 | 72 | Context("when the input slice has an invalid image reference", func() { 73 | BeforeEach(func() { 74 | ii = []string{"::"} 75 | }) 76 | 77 | It("should return a suitable error", func() { 78 | Expect(err).To(MatchError(`invalid image reference: "::"`)) 79 | }) 80 | }) 81 | }) 82 | 83 | Describe("Union", func() { 84 | It("should produce the correct result", func() { 85 | s, err := images.New(refA) 86 | Expect(err).NotTo(HaveOccurred()) 87 | 88 | u, err := images.New(refB) 89 | Expect(err).NotTo(HaveOccurred()) 90 | 91 | v, err := images.New(refA, refB) 92 | Expect(err).NotTo(HaveOccurred()) 93 | 94 | Expect(s.Union(u)).To(Equal(v)) 95 | }) 96 | }) 97 | 98 | Describe("Slice", func() { 99 | It("should produce the correct result", func() { 100 | s, err := images.New(refA, refB) 101 | Expect(err).NotTo(HaveOccurred()) 102 | 103 | a, err := image.NewName(refA) 104 | Expect(err).NotTo(HaveOccurred()) 105 | 106 | b, err := image.NewName(refB) 107 | Expect(err).NotTo(HaveOccurred()) 108 | 109 | Expect(s.Slice()).To(ConsistOf(a, b)) 110 | }) 111 | }) 112 | 113 | Describe("Strings", func() { 114 | It("should produce the correct result", func() { 115 | s, err := images.New(refB, refA) 116 | Expect(err).NotTo(HaveOccurred()) 117 | 118 | Expect(s.Strings()).To(Equal([]string{refA, refB})) 119 | }) 120 | }) 121 | 122 | Describe("String", func() { 123 | It("should produce the correct result", func() { 124 | s, err := images.New(refB, refA) 125 | Expect(err).NotTo(HaveOccurred()) 126 | 127 | Expect(s.String()).To(Equal(fmt.Sprintf("[%s, %s]", refA, refB))) 128 | }) 129 | }) 130 | 131 | Describe("Marshalling", func() { 132 | It("should marshall to the correct string and back again", func() { 133 | s, err := images.New(refA, refB) 134 | Expect(err).NotTo(HaveOccurred()) 135 | 136 | sb, err := s.MarshalJSON() 137 | Expect(err).NotTo(HaveOccurred()) 138 | 139 | Expect(string(sb)).To(Equal(fmt.Sprintf("[%q,%q]", refA, refB))) 140 | 141 | var u images.Set 142 | err = (&u).UnmarshalJSON(sb) 143 | Expect(err).NotTo(HaveOccurred()) 144 | 145 | Expect(u).To(Equal(s)) 146 | }) 147 | 148 | It("should unmarshall a string containing 'null' correctly", func() { 149 | var u images.Set 150 | err := (&u).UnmarshalJSON([]byte("null")) 151 | Expect(err).NotTo(HaveOccurred()) 152 | 153 | Expect(u).To(Equal(images.Empty)) 154 | }) 155 | }) 156 | }) 157 | -------------------------------------------------------------------------------- /pkg/irel/copy.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package irel 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | 23 | "github.com/pivotal/image-relocation/pkg/image" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | func init() { Root.AddCommand(newCmdCopy()) } 28 | 29 | func newCmdCopy() *cobra.Command { 30 | return &cobra.Command{ 31 | Use: "copy SRC_REF DST_REF", 32 | Aliases: []string{"cp"}, 33 | Short: "Efficiently copy a remote image from one repository to another", 34 | Args: cobra.ExactArgs(2), 35 | Run: copy, 36 | } 37 | } 38 | 39 | func copy(cmd *cobra.Command, args []string) { 40 | srcStr, dstStr := args[0], args[1] 41 | src, err := image.NewName(srcStr) 42 | if err != nil { 43 | log.Fatalf("invalid reference %q: %v", srcStr, err) 44 | } 45 | dst, err := image.NewName(dstStr) 46 | if err != nil { 47 | log.Fatalf("invalid reference %q: %v", dstStr, err) 48 | } 49 | 50 | regClient := mustGetRegistryClient() 51 | dig, _, err := regClient.Copy(src, dst) 52 | if err != nil { 53 | log.Fatalf("copy failed: %v", err) 54 | } 55 | fmt.Printf("copied %s to %s with content digest %s\n", src, dst, dig) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/irel/digest.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package irel 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | 23 | "github.com/pivotal/image-relocation/pkg/image" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | func init() { Root.AddCommand(newCmdDigest()) } 28 | 29 | func newCmdDigest() *cobra.Command { 30 | return &cobra.Command{ 31 | Use: "digest REF", 32 | Aliases: []string{"dig"}, 33 | Short: "Print content digest of an image", 34 | Args: cobra.ExactArgs(1), 35 | Run: digest, 36 | } 37 | } 38 | 39 | func digest(cmd *cobra.Command, args []string) { 40 | refStr := args[0] 41 | ref, err := image.NewName(refStr) 42 | if err != nil { 43 | log.Fatalf("invalid reference %q: %v", refStr, err) 44 | } 45 | 46 | regClient := mustGetRegistryClient() 47 | dig, err := regClient.Digest(ref) 48 | if err != nil { 49 | log.Fatalf("digest failed: %v", err) 50 | } 51 | fmt.Printf("%s\n", dig) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/irel/env.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package irel 18 | 19 | import "fmt" 20 | 21 | var ( 22 | cli_version = "unknown" 23 | cli_gitsha = "unknown sha" 24 | cli_gitdirty = "" 25 | ) 26 | 27 | // CliVersion returns a version string based on values supplied at build time 28 | func CliVersion() string { 29 | var version string 30 | if cli_gitdirty == "" { 31 | version = fmt.Sprintf("%s (%s)", cli_version, cli_gitsha) 32 | } else { 33 | version = fmt.Sprintf("%s (%s, with local modifications)", cli_version, cli_gitsha) 34 | } 35 | return version 36 | } 37 | -------------------------------------------------------------------------------- /pkg/irel/env_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package irel 18 | 19 | import ( 20 | . "github.com/onsi/ginkgo" 21 | . "github.com/onsi/gomega" 22 | ) 23 | 24 | var _ = Describe("CliVersion", func() { 25 | var version string 26 | 27 | JustBeforeEach(func() { 28 | version = CliVersion() 29 | }) 30 | 31 | It("should default correctly", func() { 32 | Expect(version).To(Equal("unknown (unknown sha)")) 33 | }) 34 | 35 | Context("when version is known", func() { 36 | BeforeEach(func() { 37 | cli_version = "0.0.0" 38 | }) 39 | 40 | It("should include the version", func() { 41 | Expect(version).To(Equal("0.0.0 (unknown sha)")) 42 | }) 43 | 44 | Context("when SHA is known", func() { 45 | BeforeEach(func() { 46 | cli_gitsha = "abc" 47 | }) 48 | 49 | It("should include the SHA", func() { 50 | Expect(version).To(Equal("0.0.0 (abc)")) 51 | }) 52 | 53 | Context("when repo is dirty", func() { 54 | BeforeEach(func() { 55 | cli_gitdirty = "dirty" 56 | }) 57 | 58 | It("should note the repo is dirty", func() { 59 | Expect(version).To(Equal("0.0.0 (abc, with local modifications)")) 60 | }) 61 | }) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /pkg/irel/irel_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package irel_test 18 | 19 | import ( 20 | "testing" 21 | 22 | . "github.com/onsi/ginkgo" 23 | . "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestIrel(t *testing.T) { 27 | RegisterFailHandler(Fail) 28 | RunSpecs(t, "Irel Suite") 29 | } 30 | -------------------------------------------------------------------------------- /pkg/irel/layout.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package irel 18 | 19 | import "github.com/spf13/cobra" 20 | 21 | func init() { Root.AddCommand(newCmdLayout()) } 22 | 23 | func newCmdLayout() *cobra.Command { 24 | cmd := &cobra.Command{ 25 | Use: "layout", 26 | Short: "OCI image layout commands", 27 | } 28 | cmd.AddCommand( 29 | newCmdLayoutAdd(), 30 | newCmdLayoutFind(), 31 | newCmdLayoutPush(), 32 | ) 33 | return cmd 34 | } 35 | -------------------------------------------------------------------------------- /pkg/irel/layout_add.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package irel 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | 23 | "github.com/pivotal/image-relocation/pkg/image" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | func newCmdLayoutAdd() *cobra.Command { 28 | return &cobra.Command{ 29 | Use: "add LAYOUT_PATH REF", 30 | Short: "copy an image from a remote repository to an OCI image layout", 31 | Args: cobra.ExactArgs(2), 32 | Run: layoutAdd, 33 | } 34 | } 35 | 36 | func layoutAdd(cmd *cobra.Command, args []string) { 37 | layoutPath, refStr := args[0], args[1] 38 | ref, err := image.NewName(refStr) 39 | if err != nil { 40 | log.Fatalf("invalid reference %q: %v", refStr, err) 41 | } 42 | 43 | regClient := mustGetRegistryClient() 44 | layout, err := regClient.ReadLayout(layoutPath) 45 | if err != nil { 46 | layout, err = regClient.NewLayout(layoutPath) 47 | if err != nil { 48 | log.Fatalf("failed to create OCI image layout: %v", err) 49 | } 50 | } 51 | 52 | dig, err := layout.Add(ref) 53 | if err != nil { 54 | log.Fatalf("add failed: %v", err) 55 | } 56 | fmt.Printf("added %s with digest %s to OCI image layout at %s\n", ref, dig, layoutPath) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/irel/layout_find.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package irel 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | 23 | "github.com/pivotal/image-relocation/pkg/image" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | func newCmdLayoutFind() *cobra.Command { 28 | return &cobra.Command{ 29 | Use: "find LAYOUT_PATH REF", 30 | Short: "find an image in an OCI image layout", 31 | Args: cobra.ExactArgs(2), 32 | Run: layoutFind, 33 | } 34 | } 35 | 36 | func layoutFind(cmd *cobra.Command, args []string) { 37 | layoutPath, refStr := args[0], args[1] 38 | ref, err := image.NewName(refStr) 39 | if err != nil { 40 | log.Fatalf("invalid reference %q: %v", refStr, err) 41 | } 42 | 43 | regClient := mustGetRegistryClient() 44 | layout, err := regClient.ReadLayout(layoutPath) 45 | if err != nil { 46 | log.Fatalf("failed to create OCI image layout: %v", err) 47 | } 48 | 49 | dig, err := layout.Find(ref) 50 | if err != nil { 51 | log.Fatalf("find failed: %v", err) 52 | } 53 | fmt.Printf("found %s with digest %s in OCI image layout at %s\n", ref, dig, layoutPath) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/irel/layout_push.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package irel 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | 23 | "github.com/pivotal/image-relocation/pkg/image" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | func newCmdLayoutPush() *cobra.Command { 28 | return &cobra.Command{ 29 | Use: "push LAYOUT_PATH CONTENT_DIGEST REF", 30 | Short: "copy an image with a given content digest from an OCI image layout to a remote repository", 31 | Args: cobra.ExactArgs(3), 32 | Run: layoutPush, 33 | } 34 | } 35 | 36 | func layoutPush(cmd *cobra.Command, args []string) { 37 | layoutPath, digStr, refStr := args[0], args[1], args[2] 38 | ref, err := image.NewName(refStr) 39 | if err != nil { 40 | log.Fatalf("invalid reference %q: %v", refStr, err) 41 | } 42 | 43 | dig, err := image.NewDigest(digStr) 44 | if err != nil { 45 | log.Fatalf("invalid digest %q: %v", digStr, err) 46 | } 47 | 48 | regClient := mustGetRegistryClient() 49 | layout, err := regClient.ReadLayout(layoutPath) 50 | if err != nil { 51 | log.Fatalf("failed to access OCI image layout: %v", err) 52 | } 53 | 54 | err = layout.Push(dig, ref) 55 | if err != nil { 56 | log.Fatalf("push failed: %v", err) 57 | } 58 | fmt.Printf("wrote image with digest %s from OCI image layout at %s to %s\n", dig, layoutPath, ref) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/irel/map.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package irel 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | 23 | "github.com/pivotal/image-relocation/pkg/image" 24 | "github.com/pivotal/image-relocation/pkg/pathmapping" 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | func init() { Root.AddCommand(newCmdMap()) } 29 | 30 | func newCmdMap() *cobra.Command { 31 | var repoPrefix string 32 | cmd := &cobra.Command{ 33 | Use: "map REF", 34 | Short: "Map an image reference to a relocated reference", 35 | Args: cobra.ExactArgs(1), 36 | Run: func(_ *cobra.Command, args []string) { 37 | pathMapping(repoPrefix, args) 38 | }, 39 | } 40 | cmd.Flags().StringVarP(&repoPrefix, "repository-prefix", "r", "", "base value to which an image name is appended to create the full repository") 41 | cmd.MarkFlagRequired("repository-prefix") 42 | 43 | return cmd 44 | } 45 | 46 | func pathMapping(repoPrefix string, args []string) { 47 | refStr := args[0] 48 | ref, err := image.NewName(refStr) 49 | if err != nil { 50 | log.Fatalf("invalid reference %q: %v", refStr, err) 51 | } 52 | 53 | mapped, err := pathmapping.FlattenRepoPathPreserveTagDigest(repoPrefix, ref) 54 | if err != nil { 55 | log.Fatalf("path flattening failed: %v", err) 56 | } 57 | fmt.Printf("%s\n", mapped) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/irel/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package irel 18 | 19 | import ( 20 | "log" 21 | 22 | "github.com/pivotal/image-relocation/pkg/registry" 23 | "github.com/pivotal/image-relocation/pkg/registry/ggcr" 24 | "github.com/pivotal/image-relocation/pkg/transport" 25 | 26 | "github.com/spf13/cobra" 27 | ) 28 | 29 | var ( 30 | caCertPaths []string 31 | skipTLSVerify bool 32 | 33 | // Root is the root of the tree of irel commands 34 | Root = &cobra.Command{ 35 | Use: "irel", 36 | Short: "irel is a tool for relocating container images", 37 | Run: func(cmd *cobra.Command, _ []string) { cmd.Usage() }, 38 | DisableAutoGenTag: true, 39 | } 40 | ) 41 | 42 | func init() { 43 | Root.PersistentFlags().StringSliceVarP(&caCertPaths, "ca-cert-path", "", nil, "Path to CA certificate for verifying registry TLS certificates (can be repeated for multiple certificates)") 44 | Root.PersistentFlags().BoolVarP(&skipTLSVerify, "skip-tls-verify", "", false, "Skip TLS certificate verification for registries") 45 | } 46 | 47 | func mustGetRegistryClient() registry.Client { 48 | tport, err := transport.NewHttpTransport(caCertPaths, skipTLSVerify) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | return ggcr.NewRegistryClient(ggcr.WithTransport(tport)) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/pathmapping/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Package pathmapping maps image names to repositories with new prefixes 18 | package pathmapping 19 | -------------------------------------------------------------------------------- /pkg/pathmapping/ginkgo_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pathmapping_test 18 | 19 | import ( 20 | . "github.com/onsi/ginkgo" 21 | . "github.com/onsi/gomega" 22 | 23 | "testing" 24 | ) 25 | 26 | func TestCommands(t *testing.T) { 27 | RegisterFailHandler(Fail) 28 | RunSpecs(t, "Path Mapping Suite") 29 | } 30 | -------------------------------------------------------------------------------- /pkg/pathmapping/path_mapping.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pathmapping 18 | 19 | import ( 20 | "crypto/md5" 21 | "encoding/hex" 22 | "fmt" 23 | "strings" 24 | 25 | "github.com/docker/distribution/reference" 26 | 27 | "github.com/pivotal/image-relocation/pkg/image" 28 | ) 29 | 30 | // PathMapping is a type of function which maps a given Name to a new Name by apply a repository prefix. 31 | type PathMapping func(repoPrefix string, originalImage image.Name) (image.Name, error) 32 | 33 | // FlattenRepoPath maps the given Name to a new Name with a given repository prefix. 34 | // It aims to avoid collisions between repositories and to include enough of the original name 35 | // to make it recognizable by a human being. 36 | func FlattenRepoPath(repoPrefix string, originalImage image.Name) (image.Name, error) { 37 | hasher := md5.New() 38 | hasher.Write([]byte(originalImage.Name())) 39 | hash := hex.EncodeToString(hasher.Sum(nil)) 40 | available := reference.NameTotalLengthMax - len(mappedPath(repoPrefix, "", hash)) 41 | fp := flatPath(originalImage.Path(), available) 42 | var mp string 43 | if fp == "" { 44 | mp = fmt.Sprintf("%s/%s", repoPrefix, hash) 45 | } else { 46 | mp = mappedPath(repoPrefix, fp, hash) 47 | } 48 | mn, err := image.NewName(mp) 49 | if err != nil { 50 | return image.EmptyName, err // should never occur 51 | } 52 | return mn, nil 53 | } 54 | 55 | func mappedPath(repoPrefix string, repoPath string, hash string) string { 56 | return fmt.Sprintf("%s/%s-%s", repoPrefix, repoPath, hash) 57 | } 58 | 59 | func flatPath(repoPath string, size int) string { 60 | return strings.Join(crunch(strings.Split(repoPath, "/"), size), "-") 61 | } 62 | 63 | func crunch(components []string, size int) []string { 64 | for n := len(components); n > 0; n-- { 65 | comp := reduce(components, n) 66 | if len(strings.Join(comp, "-")) <= size { 67 | return comp 68 | } 69 | 70 | } 71 | if len(components) > 0 && len(components[0]) <= size { 72 | return []string{components[0]} 73 | } 74 | return []string{} 75 | } 76 | 77 | func reduce(components []string, n int) []string { 78 | if len(components) < 2 || len(components) <= n { 79 | return components 80 | } 81 | 82 | tmp := make([]string, len(components)) 83 | copy(tmp, components) 84 | 85 | last := components[len(tmp)-1] 86 | if n < 2 { 87 | return []string{last} 88 | } 89 | 90 | front := tmp[0 : n-1] 91 | return append(front, "-", last) 92 | } 93 | -------------------------------------------------------------------------------- /pkg/pathmapping/path_mapping_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pathmapping_test 18 | 19 | import ( 20 | . "github.com/onsi/ginkgo" 21 | . "github.com/onsi/gomega" 22 | "github.com/pivotal/image-relocation/pkg/image" 23 | "github.com/pivotal/image-relocation/pkg/pathmapping" 24 | ) 25 | 26 | var _ = Describe("FlattenRepoPath", func() { 27 | var ( 28 | name image.Name 29 | mapped string 30 | ) 31 | 32 | JustBeforeEach(func() { 33 | mappedImage, err := pathmapping.FlattenRepoPath("test.host/testuser", name) 34 | Expect(err).NotTo(HaveOccurred()) 35 | mapped = mappedImage.String() 36 | 37 | // check that the mapped path is valid 38 | _, err = image.NewName(mapped) 39 | Expect(err).NotTo(HaveOccurred()) 40 | }) 41 | 42 | Context("when the image path has a single element", func() { 43 | BeforeEach(func() { 44 | var err error 45 | name, err = image.NewName("some.registry.com/some-user") 46 | Expect(err).NotTo(HaveOccurred()) 47 | }) 48 | 49 | It("should flatten the path correctly", func() { 50 | Expect(mapped).To(Equal("test.host/testuser/some-user-9482d6a53a1789fb7304a4fe88362903")) 51 | }) 52 | }) 53 | 54 | Context("when the image path has more than a single element", func() { 55 | BeforeEach(func() { 56 | var err error 57 | name, err = image.NewName("some.registry.com/some-user/some/path") 58 | Expect(err).NotTo(HaveOccurred()) 59 | }) 60 | 61 | It("should flatten the path correctly", func() { 62 | Expect(mapped).To(Equal("test.host/testuser/some-user-some-path-3236c106420c1d0898246e1d2b6ba8b6")) 63 | }) 64 | }) 65 | 66 | Context("when the image has a tag", func() { 67 | BeforeEach(func() { 68 | var err error 69 | name, err = image.NewName("some.registry.com/some-user/some/path:v1") 70 | Expect(err).NotTo(HaveOccurred()) 71 | }) 72 | 73 | It("should flatten the path correctly", func() { 74 | Expect(mapped).To(Equal("test.host/testuser/some-user-some-path-3236c106420c1d0898246e1d2b6ba8b6")) 75 | }) 76 | }) 77 | 78 | Context("when the image has a digest", func() { 79 | BeforeEach(func() { 80 | var err error 81 | name, err = image.NewName("some.registry.com/some-user/some/path@sha256:1e725169f37aec55908694840bc808fb13ebf02cb1765df225437c56a796f870") 82 | Expect(err).NotTo(HaveOccurred()) 83 | }) 84 | 85 | It("should flatten the path correctly", func() { 86 | Expect(mapped).To(Equal("test.host/testuser/some-user-some-path-3236c106420c1d0898246e1d2b6ba8b6")) 87 | }) 88 | }) 89 | 90 | Context("when the image has a digest and a tag", func() { 91 | BeforeEach(func() { 92 | var err error 93 | name, err = image.NewName("some.registry.com/some-user/some/path:v1@sha256:1e725169f37aec55908694840bc808fb13ebf02cb1765df225437c56a796f870") 94 | Expect(err).NotTo(HaveOccurred()) 95 | }) 96 | 97 | It("should flatten the path correctly", func() { 98 | Expect(mapped).To(Equal("test.host/testuser/some-user-some-path-3236c106420c1d0898246e1d2b6ba8b6")) 99 | }) 100 | }) 101 | 102 | Context("when the image path is long and has many elements", func() { 103 | BeforeEach(func() { 104 | var err error 105 | name, err = image.NewName("some.registry.com/some-user/axxxxxxxxx/bxxxxxxxxx/cxxxxxxxxx/dxxxxxxxxx/" + 106 | "exxxxxxxxx/fxxxxxxxxx/gxxxxxxxxx/hxxxxxxxxx/ixxxxxxxxx/jxxxxxxxxx/kxxxxxxxxx/lxxxxxxxxx/" + 107 | "mxxxxxxxxx/nxxxxxxxxx/oxxxxxxxxx/pxxxxxxxxx/qxxxxxxxxx/rxxxxxxxxx/sxxxxxxxxx/txxxxxxxxx") 108 | Expect(err).NotTo(HaveOccurred()) 109 | }) 110 | 111 | It("should omit some portions of the path", func() { 112 | Expect(mapped).To(Equal("test.host/testuser/some-user-axxxxxxxxx-bxxxxxxxxx-cxxxxxxxxx-dxxxxxxxxx-exxxxxxxxx-fxxxxxxxxx-gxxxxxxxxx-hxxxxxxxxx-ixxxxxxxxx-jxxxxxxxxx-kxxxxxxxxx-lxxxxxxxxx-mxxxxxxxxx-nxxxxxxxxx-oxxxxxxxxx-pxxxxxxxxx---txxxxxxxxx-b0d16e8b4d43f2ec842cfcc61989a966")) 113 | }) 114 | }) 115 | 116 | Context("when the image path is long and has few elements", func() { 117 | BeforeEach(func() { 118 | var err error 119 | name, err = image.NewName("some.registry.com/some-user/axxxxxxxxxabxxxxxxxxxbcxxxxxxxxxcdxxxxxxxxxd" + 120 | "exxxxxxxxxefxxxxxxxxx/gxxxxxxxxxghxxxxxxxxxhixxxxxxxxxijxxxxxxxxxjkxxxxxxxxxklxxxxxxxxxl" + 121 | "mxxxxxxxxxmnxxxxxxxxxnoxxxxxxxxxopxxxxxxxxxpqxxxxxxxxxqrxxxxxxxxxrsxxxxxxxxx/txxxxxxxxx") 122 | Expect(err).NotTo(HaveOccurred()) 123 | }) 124 | 125 | It("should omit some portions of the path", func() { 126 | Expect(mapped).To(Equal("test.host/testuser/some-user-axxxxxxxxxabxxxxxxxxxbcxxxxxxxxxcdxxxxxxxxxdexxxxxxxxxefxxxxxxxxx---txxxxxxxxx-4817a2fce97ff7aae687d14e66328781")) 127 | }) 128 | }) 129 | 130 | Context("when the image path is long and has two elements", func() { 131 | BeforeEach(func() { 132 | var err error 133 | name, err = image.NewName("some.registry.com/some-user/axxxxxxxxxabxxxxxxxxxbcxxxxxxxxxcdxxxxxxxxxd" + 134 | "exxxxxxxxxefxxxxxxxxxfgxxxxxxxxxghxxxxxxxxxhixxxxxxxxxijxxxxxxxxxjkxxxxxxxxxklxxxxxxxxxl" + 135 | "mxxxxxxxxxmnxxxxxxxxxnoxxxxxxxxxopxxxxxxxxxpqxxxxxxxxxqrxxxxxxxxxrsxxxxxxxxxstxxxxxxxxx") 136 | Expect(err).NotTo(HaveOccurred()) 137 | }) 138 | 139 | It("should omit some portions of the path", func() { 140 | Expect(mapped).To(Equal("test.host/testuser/some-user-a363dc80420c33618202ba2828aec456")) 141 | }) 142 | }) 143 | 144 | Context("when the image path is long and has three elements", func() { 145 | BeforeEach(func() { 146 | var err error 147 | name, err = image.NewName("some.registry.com/some-user/axxxxxxxxxabxxxxxxxxxbcxxxxxxxxxcdxxxxxxxxxd" + 148 | "exxxxxxxxxefxxxxxxxxxfgxxxxxxxxxghxxxxxxxxxhixxxxxxxxxijxxxxxxxxxjkxxxxxxxxxklxxxxxxxxxl" + 149 | "mxxxxxxxxxmnxxxxxxxxxnoxxxxxxxxxopxxxxxxxxxpqxxxxxxxxxqrxxxxxxxxxrsxxxxxxxxx/txxxxxxxxx") 150 | Expect(err).NotTo(HaveOccurred()) 151 | }) 152 | 153 | It("should omit some portions of the path", func() { 154 | Expect(mapped).To(Equal("test.host/testuser/some-user---txxxxxxxxx-5134b4594954926468fe0bdc23f640ef")) 155 | }) 156 | }) 157 | 158 | Context("when the first element of the image path is long", func() { 159 | BeforeEach(func() { 160 | var err error 161 | name, err = image.NewName("some.registry.com/axxxxxxxxxabxxxxxxxxxbcxxxxxxxxxcdxxxxxxxxxd" + 162 | "exxxxxxxxxefxxxxxxxxxfgxxxxxxxxxghxxxxxxxxxhixxxxxxxxxijxxxxxxxxxjkxxxxxxxxxklxxxxxxxxxl" + 163 | "mxxxxxxxxxmnxxxxxxxxxnoxxxxxxxxxopxxxxxxxxxpqxxxxxxxxxqrxxxxxxxxxrsxxxxxxxxxstxxxxxxxxx/suffix") 164 | Expect(err).NotTo(HaveOccurred()) 165 | }) 166 | 167 | It("should omit some portions of the path", func() { 168 | Expect(mapped).To(Equal("test.host/testuser/suffix-3fa7b8289050d7d4fe5d56f3098397a0")) 169 | }) 170 | }) 171 | }) 172 | -------------------------------------------------------------------------------- /pkg/pathmapping/tag_digest_mapping.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pathmapping 18 | 19 | import ( 20 | "github.com/pivotal/image-relocation/pkg/image" 21 | ) 22 | 23 | // FlattenRepoPathPreserveTagDigest maps the given Name to a new Name with a given repository prefix. 24 | // It aims to avoid collisions between repositories and to include enough of the original name 25 | // to make it recognizable by a human being. It preserves any tag and/or digest. 26 | func FlattenRepoPathPreserveTagDigest(repoPrefix string, originalImage image.Name) (image.Name, error) { 27 | rn, err := FlattenRepoPath(repoPrefix, originalImage) 28 | if err != nil { 29 | return image.EmptyName, err 30 | } 31 | 32 | // Preserve any tag 33 | if tag := originalImage.Tag(); tag != "" { 34 | var err error 35 | rn, err = rn.WithTag(tag) 36 | if err != nil { 37 | return image.EmptyName, err // should never occur 38 | } 39 | } 40 | 41 | // Preserve any digest 42 | if dig := originalImage.Digest(); dig != image.EmptyDigest { 43 | var err error 44 | rn, err = rn.WithDigest(dig) 45 | if err != nil { 46 | return image.EmptyName, err // should never occur 47 | } 48 | } 49 | 50 | return rn, nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/pathmapping/tag_digest_mapping_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package pathmapping_test 18 | 19 | import ( 20 | . "github.com/onsi/ginkgo" 21 | . "github.com/onsi/gomega" 22 | "github.com/pivotal/image-relocation/pkg/image" 23 | "github.com/pivotal/image-relocation/pkg/pathmapping" 24 | ) 25 | 26 | var _ = Describe("FlattenRepoPathPreserveTagDigest", func() { 27 | 28 | const expectedMappedPath = "test.host/testuser/some-user-some-path-f4cdc2223f0c472921033d606fa74a89" 29 | 30 | var ( 31 | name image.Name 32 | mapped string 33 | mappedWithoutTagOrDigest string 34 | tag string 35 | digest string 36 | ) 37 | 38 | JustBeforeEach(func() { 39 | result, err := pathmapping.FlattenRepoPathPreserveTagDigest("test.host/testuser", name) 40 | Expect(err).NotTo(HaveOccurred()) 41 | mapped = result.String() 42 | tag = result.Tag() 43 | digest = result.Digest().String() 44 | mappedWithoutTagOrDigest = result.WithoutTagOrDigest().String() 45 | 46 | // check that the mapped path is valid 47 | _, err = image.NewName(mapped) 48 | Expect(err).NotTo(HaveOccurred()) 49 | }) 50 | 51 | Context("when the image has neither a tag nor a digest", func() { 52 | BeforeEach(func() { 53 | var err error 54 | name, err = image.NewName("some.registry.com/some-user/some-path") 55 | Expect(err).NotTo(HaveOccurred()) 56 | }) 57 | 58 | It("should map the image correctly", func() { 59 | Expect(mapped).To(Equal(expectedMappedPath)) 60 | }) 61 | 62 | It("should not introduce a tag", func() { 63 | Expect(tag).To(BeEmpty()) 64 | }) 65 | 66 | It("should not introduce a digest", func() { 67 | Expect(digest).To(BeEmpty()) 68 | }) 69 | }) 70 | 71 | Context("when the image has a tag", func() { 72 | BeforeEach(func() { 73 | var err error 74 | name, err = image.NewName("some.registry.com/some-user/some-path:v1") 75 | Expect(err).NotTo(HaveOccurred()) 76 | }) 77 | 78 | It("should preserve the tag", func() { 79 | Expect(tag).To(Equal("v1")) 80 | }) 81 | 82 | It("should not introduce a digest", func() { 83 | Expect(digest).To(BeEmpty()) 84 | }) 85 | 86 | It("should map the path without regard to the tag", func() { 87 | Expect(mappedWithoutTagOrDigest).To(Equal(expectedMappedPath)) 88 | }) 89 | }) 90 | 91 | Context("when the image has a digest", func() { 92 | BeforeEach(func() { 93 | var err error 94 | name, err = image.NewName("some.registry.com/some-user/some-path@sha256:1e725169f37aec55908694840bc808fb13ebf02cb1765df225437c56a796f870") 95 | Expect(err).NotTo(HaveOccurred()) 96 | }) 97 | 98 | It("should not introduce a tag", func() { 99 | Expect(tag).To(BeEmpty()) 100 | }) 101 | 102 | It("should preserve the digest", func() { 103 | Expect(digest).To(Equal("sha256:1e725169f37aec55908694840bc808fb13ebf02cb1765df225437c56a796f870")) 104 | }) 105 | 106 | It("should map the path without regard to the digest", func() { 107 | Expect(mappedWithoutTagOrDigest).To(Equal(expectedMappedPath)) 108 | }) 109 | }) 110 | 111 | Context("when the image has a digest and a tag", func() { 112 | BeforeEach(func() { 113 | var err error 114 | name, err = image.NewName("some.registry.com/some-user/some-path:v1@sha256:1e725169f37aec55908694840bc808fb13ebf02cb1765df225437c56a796f870") 115 | Expect(err).NotTo(HaveOccurred()) 116 | }) 117 | 118 | It("should preserve the tag", func() { 119 | Expect(tag).To(Equal("v1")) 120 | }) 121 | 122 | It("should preserve the digest", func() { 123 | Expect(digest).To(Equal("sha256:1e725169f37aec55908694840bc808fb13ebf02cb1765df225437c56a796f870")) 124 | }) 125 | 126 | It("should map the path without regard to the tag or the digest", func() { 127 | Expect(mappedWithoutTagOrDigest).To(Equal(expectedMappedPath)) 128 | }) 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /pkg/registry/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package registry 18 | 19 | import ( 20 | "github.com/pivotal/image-relocation/pkg/image" 21 | ) 22 | 23 | // Client provides a way of interacting with image registries. 24 | type Client interface { 25 | // Digest returns the digest of the given image or an error if the image does not exist or the digest is unavailable. 26 | Digest(image.Name) (image.Digest, error) 27 | 28 | // Copy copies the given source image to the given target and returns the image's digest (which is preserved) and 29 | // the size in bytes of the raw image manifest. 30 | Copy(source image.Name, target image.Name) (image.Digest, int64, error) 31 | 32 | // NewLayout creates a Layout for the Client and creates a corresponding directory containing a new OCI image layout at 33 | // the given file system path. 34 | NewLayout(path string) (Layout, error) 35 | 36 | // ReadLayout creates a Layout for the Client from the given file system path of a directory containing an existing 37 | // OCI image layout. 38 | ReadLayout(path string) (Layout, error) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/registry/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Package registry provides operations on image registries 18 | package registry 19 | -------------------------------------------------------------------------------- /pkg/registry/ggcr/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ggcr 18 | 19 | import ( 20 | "fmt" 21 | "net/http" 22 | "os" 23 | 24 | v1 "github.com/google/go-containerregistry/pkg/v1" 25 | "github.com/google/go-containerregistry/pkg/v1/empty" 26 | "github.com/google/go-containerregistry/pkg/v1/layout" 27 | 28 | "github.com/pivotal/image-relocation/pkg/image" 29 | "github.com/pivotal/image-relocation/pkg/registry" 30 | ) 31 | 32 | const outputDirPermissions = 0755 33 | 34 | // RegistryClient provides methods for building abstract images. 35 | // This interface is not intended for external consumption. 36 | type RegistryClient interface { 37 | // ReadRemoteImage builds an abstract image from a repository. 38 | ReadRemoteImage(n image.Name) (registry.Image, error) 39 | 40 | // NewImageFromManifest builds an abstract image from an image manifest. 41 | NewImageFromManifest(img v1.Image) registry.Image 42 | 43 | // NewImageFromIndex builds an abstract image from an image index. 44 | NewImageFromIndex(img v1.ImageIndex) registry.Image 45 | } 46 | 47 | type manifestWriter func(v1.Image, image.Name) error 48 | type indexWriter func(v1.ImageIndex, image.Name) error 49 | 50 | type client struct { 51 | readRemoteImage func(image.Name) (registry.Image, error) 52 | writeRemoteImage manifestWriter 53 | writeRemoteIndex indexWriter 54 | } 55 | 56 | var ( 57 | // Ensure client conforms to the relevant interfaces. 58 | _ RegistryClient = &client{} 59 | _ registry.Client = &client{} 60 | ) 61 | 62 | // Option represents a functional option for NewRegistryClient. 63 | type Option func(*client) 64 | 65 | // WithTransport overrides the default transport used for remote operations, default is http.DefaultTransport. 66 | func WithTransport(transport http.RoundTripper) Option { 67 | return func(c *client) { 68 | writeRemoteImageFunc := writeRemoteImage(transport) 69 | writeRemoteIndexFunc := writeRemoteIndex(transport) 70 | 71 | c.readRemoteImage = readRemoteImage(writeRemoteImageFunc, writeRemoteIndexFunc, transport) 72 | c.writeRemoteImage = writeRemoteImageFunc 73 | c.writeRemoteIndex = writeRemoteIndexFunc 74 | } 75 | } 76 | 77 | // NewRegistryClient returns a new Client. 78 | func NewRegistryClient(options ...Option) *client { 79 | client := &client{} 80 | 81 | // default transport 82 | WithTransport(http.DefaultTransport)(client) 83 | 84 | // apply functional options 85 | for _, opt := range options { 86 | opt(client) 87 | } 88 | 89 | return client 90 | } 91 | 92 | func (r *client) Digest(n image.Name) (image.Digest, error) { 93 | img, err := r.ReadRemoteImage(n) 94 | if err != nil { 95 | return image.EmptyDigest, err 96 | } 97 | 98 | hash, err := img.Digest() 99 | if err != nil { 100 | return image.EmptyDigest, err 101 | } 102 | 103 | return image.NewDigest(hash.String()) 104 | } 105 | 106 | func (r *client) Copy(source image.Name, target image.Name) (image.Digest, int64, error) { 107 | img, err := r.ReadRemoteImage(source) 108 | if err != nil { 109 | return image.EmptyDigest, 0, fmt.Errorf("failed to read image %v: %v", source, err) 110 | } 111 | 112 | sourceDigest, err := img.Digest() 113 | if err != nil { 114 | return image.EmptyDigest, 0, fmt.Errorf("failed to read digest of image %v: %v", source, err) 115 | } 116 | 117 | targetDigest, s, err := img.Write(target) 118 | if err != nil { 119 | return image.EmptyDigest, 0, fmt.Errorf("failed to write image %v to %v: %v", source, target, err) 120 | } 121 | if sourceDigest != targetDigest { 122 | return image.EmptyDigest, 0, fmt.Errorf("failed to preserve digest of image %v: source digest %v, target digest %v", source, sourceDigest, targetDigest) 123 | } 124 | return targetDigest, s, err 125 | } 126 | 127 | func (r *client) NewLayout(path string) (registry.Layout, error) { 128 | if _, err := os.Stat(path); err != nil { 129 | if !os.IsNotExist(err) { 130 | return nil, err 131 | } 132 | if err := os.MkdirAll(path, outputDirPermissions); err != nil { 133 | return nil, err 134 | } 135 | } 136 | 137 | lp, err := layout.Write(path, empty.Index) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | return NewImageLayout(r, lp), nil 143 | } 144 | 145 | func (r *client) ReadLayout(path string) (registry.Layout, error) { 146 | lp, err := layout.FromPath(path) 147 | if err != nil { 148 | return nil, err 149 | } 150 | return NewImageLayout(r, lp), nil 151 | } 152 | 153 | func (r *client) ReadRemoteImage(n image.Name) (registry.Image, error) { 154 | return r.readRemoteImage(n) 155 | } 156 | 157 | func (r *client) NewImageFromManifest(img v1.Image) registry.Image { 158 | return newImageFromManifest(img, r.writeRemoteImage) 159 | } 160 | 161 | func (r *client) NewImageFromIndex(idx v1.ImageIndex) registry.Image { 162 | return newImageFromIndex(idx, r.writeRemoteIndex) 163 | } 164 | -------------------------------------------------------------------------------- /pkg/registry/ggcr/client_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ggcr 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | . "github.com/onsi/ginkgo" 23 | . "github.com/onsi/gomega" 24 | "github.com/pivotal/image-relocation/pkg/image" 25 | "github.com/pivotal/image-relocation/pkg/registry" 26 | "github.com/pivotal/image-relocation/pkg/registry/registryfakes" 27 | ) 28 | 29 | var _ = Describe("Client", func() { 30 | var ( 31 | cl *client 32 | outputDig image.Digest 33 | rawManifestSize int64 34 | err error 35 | testError error 36 | readArg image.Name 37 | readResultImage registry.Image 38 | readResultErr error 39 | writeArgName image.Name 40 | dig image.Digest 41 | dig2 image.Digest 42 | fakeImage *registryfakes.FakeImage 43 | ) 44 | 45 | BeforeEach(func() { 46 | cl = &client{} 47 | fakeImage = ®istryfakes.FakeImage{} 48 | dig = createDigest("sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") 49 | dig2 = createDigest("sha256:afabcafeafabcafeafabcafeafabcafeafabcafeafabcafeafabcafeafabcafe") 50 | fakeImage.DigestReturns(dig, nil) 51 | fakeImage.WriteStub = func(name image.Name) (digest image.Digest, i int64, e error) { 52 | writeArgName = name 53 | return dig, 3, nil 54 | } 55 | readResultImage = fakeImage 56 | readResultErr = nil 57 | cl.readRemoteImage = func(n image.Name) (registry.Image, error) { 58 | readArg = n 59 | return readResultImage, readResultErr 60 | } 61 | testError = errors.New("something bad happened") 62 | }) 63 | 64 | Describe("Copy", func() { 65 | JustBeforeEach(func() { 66 | outputDig, rawManifestSize, err = cl.Copy(createName("source"), createName("target")) 67 | }) 68 | 69 | Context("when no errors occur", func() { 70 | It("should succeed", func() { 71 | Expect(err).NotTo(HaveOccurred()) 72 | _ = rawManifestSize 73 | }) 74 | 75 | It("should copy the source repository to the target repository", func() { 76 | Expect(readArg.String()).To(Equal("docker.io/library/source")) 77 | Expect(writeArgName.String()).To(Equal("docker.io/library/target")) 78 | }) 79 | 80 | It("should return the correct digest", func() { 81 | Expect(outputDig.String()).To(Equal(dig.String())) 82 | }) 83 | 84 | It("should return the correct raw manifest size", func() { 85 | Expect(rawManifestSize).To(Equal(int64(3))) 86 | }) 87 | }) 88 | 89 | Context("when reading the source image fails", func() { 90 | BeforeEach(func() { 91 | readResultImage = nil 92 | readResultErr = testError 93 | }) 94 | 95 | It("should return a corresponding error", func() { 96 | Expect(err).To(MatchError("failed to read image docker.io/library/source: something bad happened")) 97 | }) 98 | }) 99 | 100 | Context("when reading the source image digest fails", func() { 101 | BeforeEach(func() { 102 | fakeImage.DigestReturns(dig, testError) 103 | }) 104 | 105 | It("should return a corresponding error", func() { 106 | Expect(err).To(MatchError("failed to read digest of image docker.io/library/source: something bad happened")) 107 | }) 108 | }) 109 | 110 | Context("when writing the target image image fails", func() { 111 | BeforeEach(func() { 112 | fakeImage.WriteStub = func(name image.Name) (digest image.Digest, i int64, e error) { 113 | return image.EmptyDigest, 0, testError 114 | } 115 | }) 116 | 117 | It("should return a corresponding error", func() { 118 | Expect(err).To(MatchError("failed to write image docker.io/library/source to docker.io/library/target: something bad happened")) 119 | }) 120 | }) 121 | 122 | Context("when writing the target image produces a distinct digest", func() { 123 | BeforeEach(func() { 124 | fakeImage.WriteStub = func(name image.Name) (digest image.Digest, i int64, e error) { 125 | return dig2, 0, nil 126 | } 127 | }) 128 | 129 | It("should return a corresponding error", func() { 130 | Expect(err).To(MatchError(fmt.Sprintf("failed to preserve digest of image docker.io/library/source: source digest %v, target digest %v", dig, dig2))) 131 | }) 132 | }) 133 | }) 134 | }) 135 | 136 | func createName(n string) image.Name { 137 | nm, err := image.NewName(n) 138 | Expect(err).NotTo(HaveOccurred()) 139 | return nm 140 | } 141 | 142 | func createDigest(h string) image.Digest { 143 | dig, err := image.NewDigest(h) 144 | Expect(err).NotTo(HaveOccurred()) 145 | return dig 146 | } 147 | -------------------------------------------------------------------------------- /pkg/registry/ggcr/ggcr_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ggcr_test 18 | 19 | import ( 20 | "testing" 21 | 22 | . "github.com/onsi/ginkgo" 23 | . "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestGgcr(t *testing.T) { 27 | RegisterFailHandler(Fail) 28 | RunSpecs(t, "ggcr Suite") 29 | } 30 | -------------------------------------------------------------------------------- /pkg/registry/ggcr/image.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ggcr 18 | 19 | import ( 20 | "fmt" 21 | 22 | v1 "github.com/google/go-containerregistry/pkg/v1" 23 | "github.com/google/go-containerregistry/pkg/v1/layout" 24 | 25 | "github.com/pivotal/image-relocation/pkg/image" 26 | "github.com/pivotal/image-relocation/pkg/registry/ggcr/path" 27 | ) 28 | 29 | func newImageFromManifest(img v1.Image, mfstWriter manifestWriter) *imageManifest { 30 | return &imageManifest{manifest: img, mfstWriter: mfstWriter} 31 | } 32 | 33 | type imageManifest struct { 34 | manifest v1.Image 35 | mfstWriter manifestWriter 36 | } 37 | 38 | func (m *imageManifest) Digest() (image.Digest, error) { 39 | hash, err := m.manifest.Digest() 40 | if err != nil { 41 | return image.EmptyDigest, err 42 | } 43 | return image.NewDigest(hash.String()) 44 | } 45 | 46 | func (m *imageManifest) Write(target image.Name) (image.Digest, int64, error) { 47 | dig, err := m.Digest() 48 | if err != nil { 49 | return image.EmptyDigest, 0, fmt.Errorf("failed to read digest of image: %v", err) 50 | } 51 | 52 | err = m.mfstWriter(m.manifest, target) 53 | if err != nil { 54 | return image.EmptyDigest, 0, fmt.Errorf("failed to write image %v: %v", target, err) 55 | } 56 | 57 | rawManifest, err := m.manifest.RawManifest() 58 | if err != nil { 59 | return image.EmptyDigest, 0, fmt.Errorf("failed to get raw manifest of image: %v", err) 60 | } 61 | 62 | return dig, int64(len(rawManifest)), nil 63 | } 64 | 65 | func (m *imageManifest) appendToLayout(layoutPath path.LayoutPath, options ...layout.Option) error { 66 | return layoutPath.AppendImage(m.manifest, options...) 67 | } 68 | 69 | type imageIndex struct { 70 | index v1.ImageIndex 71 | idxWriter indexWriter 72 | } 73 | 74 | func (i *imageIndex) Digest() (image.Digest, error) { 75 | hash, err := i.index.Digest() 76 | if err != nil { 77 | return image.EmptyDigest, err 78 | } 79 | return image.NewDigest(hash.String()) 80 | } 81 | 82 | func (i *imageIndex) Write(target image.Name) (image.Digest, int64, error) { 83 | dig, err := i.Digest() 84 | if err != nil { 85 | return image.EmptyDigest, 0, fmt.Errorf("failed to read digest of image index: %v", err) 86 | } 87 | 88 | err = i.idxWriter(i.index, target) 89 | if err != nil { 90 | return image.EmptyDigest, 0, fmt.Errorf("failed to write image index %v: %v", target, err) 91 | } 92 | 93 | rawManifest, err := i.index.RawManifest() 94 | if err != nil { 95 | return image.EmptyDigest, 0, fmt.Errorf("failed to get raw manifest of image index: %v", err) 96 | } 97 | 98 | return dig, int64(len(rawManifest)), nil 99 | } 100 | 101 | func (i *imageIndex) appendToLayout(layoutPath path.LayoutPath, options ...layout.Option) error { 102 | return layoutPath.AppendIndex(i.index, options...) 103 | } 104 | 105 | func newImageFromIndex(idx v1.ImageIndex, idxWriter indexWriter) *imageIndex { 106 | return &imageIndex{index: idx, idxWriter: idxWriter} 107 | } 108 | -------------------------------------------------------------------------------- /pkg/registry/ggcr/image_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ggcr 18 | 19 | import ( 20 | v1 "github.com/google/go-containerregistry/pkg/v1" 21 | . "github.com/onsi/ginkgo" 22 | . "github.com/onsi/gomega" 23 | "github.com/pkg/errors" 24 | 25 | "github.com/pivotal/image-relocation/pkg/image" 26 | "github.com/pivotal/image-relocation/pkg/registry/ggcr/path/pathfakes" 27 | "github.com/pivotal/image-relocation/pkg/registry/ggcrfakes" 28 | ) 29 | 30 | var _ = Describe("Image", func() { 31 | var ( 32 | nm image.Name 33 | testDigest image.Digest 34 | testHash v1.Hash 35 | testErr error 36 | err error 37 | ) 38 | 39 | BeforeEach(func() { 40 | nm, err = image.NewName("ubuntu") 41 | Expect(err).NotTo(HaveOccurred()) 42 | 43 | const sha = "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" 44 | testDigest, err = image.NewDigest(sha) 45 | Expect(err).NotTo(HaveOccurred()) 46 | testHash, err = v1.NewHash(sha) 47 | Expect(err).NotTo(HaveOccurred()) 48 | 49 | testErr = errors.New("wat") 50 | }) 51 | 52 | Describe("imageManifest", func() { 53 | var ( 54 | im *imageManifest 55 | mockManifest *ggcrfakes.FakeImage 56 | manifestWriterStub manifestWriter 57 | ) 58 | 59 | BeforeEach(func() { 60 | mockManifest = &ggcrfakes.FakeImage{} 61 | }) 62 | 63 | JustBeforeEach(func() { 64 | im = newImageFromManifest(mockManifest, manifestWriterStub) 65 | }) 66 | 67 | Describe("Digest", func() { 68 | Context("when the digest is available", func() { 69 | BeforeEach(func() { 70 | mockManifest.DigestReturns(testHash, nil) 71 | }) 72 | 73 | It("should return the digest", func() { 74 | d, err := im.Digest() 75 | Expect(err).NotTo(HaveOccurred()) 76 | Expect(d).To(Equal(testDigest)) 77 | }) 78 | }) 79 | 80 | Context("when the digest is not available", func() { 81 | BeforeEach(func() { 82 | mockManifest.DigestReturns(testHash, testErr) 83 | }) 84 | 85 | It("should return a suitable error", func() { 86 | _, err := im.Digest() 87 | Expect(err).To(MatchError(testErr)) 88 | }) 89 | }) 90 | }) 91 | 92 | Describe("Write", func() { 93 | var ( 94 | dig image.Digest 95 | sz int64 96 | writtenImage v1.Image 97 | writtenName image.Name 98 | writeErr error 99 | ) 100 | 101 | BeforeEach(func() { 102 | writeErr = nil 103 | manifestWriterStub = func(i v1.Image, n image.Name) error { 104 | writtenImage = i 105 | writtenName = n 106 | return writeErr 107 | } 108 | }) 109 | 110 | JustBeforeEach(func() { 111 | dig, sz, err = im.Write(nm) 112 | }) 113 | 114 | Context("when the digest is available", func() { 115 | var testRawManifest []byte 116 | 117 | BeforeEach(func() { 118 | mockManifest.DigestReturns(testHash, nil) 119 | testRawManifest = []byte{0, 1} 120 | mockManifest.RawManifestReturns(testRawManifest, nil) 121 | }) 122 | 123 | It("should return the digest", func() { 124 | Expect(err).NotTo(HaveOccurred()) 125 | Expect(dig).To(Equal(testDigest)) 126 | Expect(sz).To(Equal(int64(len(testRawManifest)))) 127 | }) 128 | 129 | It("should write the image", func() { 130 | Expect(writtenImage).To(Equal(mockManifest)) 131 | Expect(writtenName).To(Equal(nm)) 132 | }) 133 | 134 | Context("when writing fails", func() { 135 | BeforeEach(func() { 136 | writeErr = testErr 137 | }) 138 | 139 | It("should return a suitable error", func() { 140 | Expect(err).To(MatchError("failed to write image docker.io/library/ubuntu: wat")) 141 | }) 142 | }) 143 | 144 | Context("when the raw manifest is not available", func() { 145 | BeforeEach(func() { 146 | mockManifest.RawManifestReturns(nil, testErr) 147 | }) 148 | 149 | It("should return a suitable error", func() { 150 | Expect(err).To(MatchError("failed to get raw manifest of image: wat")) 151 | }) 152 | }) 153 | }) 154 | 155 | Context("when the digest is not available", func() { 156 | BeforeEach(func() { 157 | mockManifest.DigestReturns(testHash, testErr) 158 | }) 159 | 160 | It("should return a suitable error", func() { 161 | Expect(err).To(MatchError("failed to read digest of image: wat")) 162 | }) 163 | }) 164 | }) 165 | 166 | Describe("appendToLayout", func() { 167 | var mockLayoutPath *pathfakes.FakeLayoutPath 168 | 169 | BeforeEach(func() { 170 | mockLayoutPath = &pathfakes.FakeLayoutPath{} 171 | }) 172 | 173 | JustBeforeEach(func() { 174 | err = im.appendToLayout(mockLayoutPath) 175 | }) 176 | 177 | Context("when appending the image succeeds", func() { 178 | It("should pass the correct image", func() { 179 | Expect(err).NotTo(HaveOccurred()) 180 | mfst, opts := mockLayoutPath.AppendImageArgsForCall(0) 181 | Expect(mfst).To(Equal(mockManifest)) 182 | Expect(opts).To(BeEmpty()) 183 | }) 184 | }) 185 | 186 | Context("when appending the image fails", func() { 187 | BeforeEach(func() { 188 | mockLayoutPath.AppendImageReturns(testErr) 189 | }) 190 | 191 | It("should return the error", func() { 192 | Expect(err).To(MatchError(testErr)) 193 | }) 194 | }) 195 | }) 196 | }) 197 | 198 | Describe("imageIndex", func() { 199 | var ( 200 | im *imageIndex 201 | mockIndex *ggcrfakes.FakeImageIndex 202 | indexWriterStub indexWriter 203 | ) 204 | 205 | BeforeEach(func() { 206 | mockIndex = &ggcrfakes.FakeImageIndex{} 207 | }) 208 | 209 | JustBeforeEach(func() { 210 | im = newImageFromIndex(mockIndex, indexWriterStub) 211 | }) 212 | 213 | Describe("Digest", func() { 214 | Context("when the digest is available", func() { 215 | BeforeEach(func() { 216 | mockIndex.DigestReturns(testHash, nil) 217 | }) 218 | 219 | It("should return the digest", func() { 220 | d, err := im.Digest() 221 | Expect(err).NotTo(HaveOccurred()) 222 | Expect(d).To(Equal(testDigest)) 223 | }) 224 | }) 225 | 226 | Context("when the digest is not available", func() { 227 | BeforeEach(func() { 228 | mockIndex.DigestReturns(testHash, testErr) 229 | }) 230 | 231 | It("should return the error", func() { 232 | _, err := im.Digest() 233 | Expect(err).To(MatchError(testErr)) 234 | }) 235 | }) 236 | }) 237 | 238 | Describe("Write", func() { 239 | var ( 240 | dig image.Digest 241 | sz int64 242 | writtenImage v1.ImageIndex 243 | writtenName image.Name 244 | writeErr error 245 | ) 246 | 247 | BeforeEach(func() { 248 | writeErr = nil 249 | indexWriterStub = func(i v1.ImageIndex, n image.Name) error { 250 | writtenImage = i 251 | writtenName = n 252 | return writeErr 253 | } 254 | }) 255 | 256 | JustBeforeEach(func() { 257 | dig, sz, err = im.Write(nm) 258 | }) 259 | 260 | Context("when the digest is available", func() { 261 | var testRawManifest []byte 262 | 263 | BeforeEach(func() { 264 | mockIndex.DigestReturns(testHash, nil) 265 | testRawManifest = []byte{0, 1} 266 | mockIndex.RawManifestReturns(testRawManifest, nil) 267 | }) 268 | 269 | It("should return the digest", func() { 270 | Expect(err).NotTo(HaveOccurred()) 271 | Expect(dig).To(Equal(testDigest)) 272 | Expect(sz).To(Equal(int64(len(testRawManifest)))) 273 | }) 274 | 275 | It("should write the image", func() { 276 | Expect(writtenImage).To(Equal(mockIndex)) 277 | Expect(writtenName).To(Equal(nm)) 278 | }) 279 | 280 | Context("when writing fails", func() { 281 | BeforeEach(func() { 282 | writeErr = testErr 283 | }) 284 | 285 | It("should return a suitable error", func() { 286 | Expect(err).To(MatchError("failed to write image index docker.io/library/ubuntu: wat")) 287 | }) 288 | }) 289 | 290 | Context("when the raw manifest is not available", func() { 291 | BeforeEach(func() { 292 | mockIndex.RawManifestReturns(nil, testErr) 293 | }) 294 | 295 | It("should return a suitable error", func() { 296 | Expect(err).To(MatchError("failed to get raw manifest of image index: wat")) 297 | }) 298 | }) 299 | }) 300 | 301 | Context("when the digest is not available", func() { 302 | BeforeEach(func() { 303 | mockIndex.DigestReturns(testHash, testErr) 304 | }) 305 | 306 | It("should return a suitable error", func() { 307 | Expect(err).To(MatchError("failed to read digest of image index: wat")) 308 | }) 309 | }) 310 | }) 311 | 312 | Describe("appendToLayout", func() { 313 | var mockLayoutPath *pathfakes.FakeLayoutPath 314 | 315 | BeforeEach(func() { 316 | mockLayoutPath = &pathfakes.FakeLayoutPath{} 317 | }) 318 | 319 | JustBeforeEach(func() { 320 | err = im.appendToLayout(mockLayoutPath) 321 | }) 322 | 323 | Context("when appending the image succeeds", func() { 324 | It("should pass the correct image", func() { 325 | Expect(err).NotTo(HaveOccurred()) 326 | mfst, opts := mockLayoutPath.AppendIndexArgsForCall(0) 327 | Expect(mfst).To(Equal(mockIndex)) 328 | Expect(opts).To(BeEmpty()) 329 | }) 330 | }) 331 | 332 | Context("when appending the image fails", func() { 333 | BeforeEach(func() { 334 | mockLayoutPath.AppendIndexReturns(testErr) 335 | }) 336 | 337 | It("should return the error", func() { 338 | Expect(err).To(MatchError(testErr)) 339 | }) 340 | }) 341 | }) 342 | }) 343 | }) 344 | -------------------------------------------------------------------------------- /pkg/registry/ggcr/layout.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ggcr 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/pivotal/image-relocation/pkg/registry" 23 | "github.com/pivotal/image-relocation/pkg/registry/ggcr/path" 24 | 25 | "github.com/google/go-containerregistry/pkg/v1" 26 | "github.com/google/go-containerregistry/pkg/v1/layout" 27 | 28 | "github.com/pivotal/image-relocation/pkg/image" 29 | ) 30 | 31 | const refNameAnnotation = "org.opencontainers.image.ref.name" 32 | 33 | type imageLayout struct { 34 | registryClient RegistryClient 35 | layoutPath path.LayoutPath 36 | } 37 | 38 | func NewImageLayout(registryClient RegistryClient, layoutPath path.LayoutPath) registry.Layout { 39 | return &imageLayout{ 40 | registryClient: registryClient, 41 | layoutPath: layoutPath, 42 | } 43 | } 44 | 45 | // appendable is an interface implemented by types which can be appended to an OCI image layout. 46 | type appendable interface { 47 | // appendToLayout appends the image to a given OCI image layout using the given layout options. 48 | appendToLayout(layoutPath path.LayoutPath, options ...layout.Option) error 49 | } 50 | 51 | func (l *imageLayout) Add(n image.Name) (image.Digest, error) { 52 | img, err := l.registryClient.ReadRemoteImage(n) 53 | if err != nil { 54 | return image.EmptyDigest, err 55 | } 56 | 57 | annotations := map[string]string{ 58 | refNameAnnotation: n.String(), 59 | } 60 | if img, ok := img.(appendable); ok { 61 | if err := img.appendToLayout(l.layoutPath, layout.WithAnnotations(annotations)); err != nil { 62 | return image.EmptyDigest, err 63 | } 64 | } 65 | 66 | hash, err := img.Digest() 67 | if err != nil { 68 | return image.EmptyDigest, err 69 | } 70 | 71 | return image.NewDigest(hash.String()) 72 | } 73 | 74 | func (l *imageLayout) Push(digest image.Digest, n image.Name) error { 75 | img, err := l.findByDigest(digest) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | _, _, err = img.Write(n) 81 | if err != nil { 82 | return fmt.Errorf("failed to write image %v to %v: %v", digest, n, err) 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func (l *imageLayout) Find(n image.Name) (image.Digest, error) { 89 | imageIndex, err := l.layoutPath.ImageIndex() 90 | if err != nil { 91 | return image.EmptyDigest, err 92 | } 93 | indexMan, err := imageIndex.IndexManifest() 94 | if err != nil { 95 | return image.EmptyDigest, err 96 | } 97 | 98 | for _, imageMan := range indexMan.Manifests { 99 | if ref, ok := imageMan.Annotations[refNameAnnotation]; ok { 100 | r, err := image.NewName(ref) 101 | if err != nil { 102 | return image.EmptyDigest, err 103 | } 104 | if r == n { 105 | return image.NewDigest(imageMan.Digest.String()) 106 | } 107 | } 108 | } 109 | 110 | return image.EmptyDigest, fmt.Errorf("image %v not found in layout", n) 111 | } 112 | 113 | func (l *imageLayout) findByDigest(digest image.Digest) (registry.Image, error) { 114 | hash, err := v1.NewHash(digest.String()) 115 | if err != nil { 116 | return nil, err 117 | } 118 | imageIndex, err := l.layoutPath.ImageIndex() 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | im, err := imageIndex.Image(hash) 124 | if err == nil { 125 | return l.registryClient.NewImageFromManifest(im), nil 126 | } 127 | 128 | idx, err := imageIndex.ImageIndex(hash) 129 | if err == nil { 130 | return l.registryClient.NewImageFromIndex(idx), nil 131 | } 132 | 133 | return nil, err 134 | } 135 | -------------------------------------------------------------------------------- /pkg/registry/ggcr/layout_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ggcr_test 18 | 19 | import ( 20 | "errors" 21 | 22 | "github.com/google/go-containerregistry/pkg/v1" 23 | . "github.com/onsi/ginkgo" 24 | . "github.com/onsi/gomega" 25 | 26 | "github.com/pivotal/image-relocation/pkg/image" 27 | "github.com/pivotal/image-relocation/pkg/registry" 28 | "github.com/pivotal/image-relocation/pkg/registry/ggcr" 29 | "github.com/pivotal/image-relocation/pkg/registry/ggcr/path/pathfakes" 30 | "github.com/pivotal/image-relocation/pkg/registry/ggcr/registryclientfakes" 31 | "github.com/pivotal/image-relocation/pkg/registry/ggcrfakes" 32 | "github.com/pivotal/image-relocation/pkg/registry/registryfakes" 33 | ) 34 | 35 | var _ = Describe("Layout", func() { 36 | var ( 37 | layout registry.Layout 38 | mockLayoutPath *pathfakes.FakeLayoutPath 39 | mockImageIndex *ggcrfakes.FakeImageIndex 40 | mockRegistryClient *registryclientfakes.FakeRegistryClient 41 | testError error 42 | ) 43 | 44 | BeforeEach(func() { 45 | mockLayoutPath = &pathfakes.FakeLayoutPath{} 46 | mockImageIndex = &ggcrfakes.FakeImageIndex{} 47 | mockRegistryClient = ®istryclientfakes.FakeRegistryClient{} 48 | 49 | layout = ggcr.NewImageLayout(mockRegistryClient, mockLayoutPath) 50 | 51 | testError = errors.New("failed") 52 | }) 53 | 54 | Describe("Find", func() { 55 | var ( 56 | indexManifest *v1.IndexManifest 57 | im image.Name 58 | dig image.Digest 59 | err error 60 | testHash v1.Hash 61 | ) 62 | 63 | BeforeEach(func() { 64 | indexManifest = &v1.IndexManifest{} 65 | mockImageIndex.IndexManifestReturns(indexManifest, nil) 66 | mockLayoutPath.ImageIndexReturns(mockImageIndex, nil) 67 | var err error 68 | im, err = image.NewName("testimage") 69 | Expect(err).NotTo(HaveOccurred()) 70 | testHash, err = v1.NewHash("sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") 71 | Expect(err).NotTo(HaveOccurred()) 72 | }) 73 | 74 | JustBeforeEach(func() { 75 | dig, err = layout.Find(im) 76 | }) 77 | 78 | Context("when the image is present", func() { 79 | BeforeEach(func() { 80 | indexManifest.Manifests = []v1.Descriptor{ 81 | { 82 | Annotations: map[string]string{"org.opencontainers.image.ref.name": "testimage"}, 83 | Digest: testHash, 84 | }, 85 | } 86 | }) 87 | 88 | It("should find the image", func() { 89 | Expect(err).NotTo(HaveOccurred()) 90 | Expect(dig.String()).To(Equal(testHash.String())) 91 | }) 92 | }) 93 | 94 | Context("when the image is absent", func() { 95 | It("should return a suitable error", func() { 96 | Expect(err).To(MatchError("image docker.io/library/testimage not found in layout")) 97 | }) 98 | }) 99 | 100 | Context("when accessing the image index returns an error", func() { 101 | BeforeEach(func() { 102 | mockLayoutPath.ImageIndexReturns(nil, testError) 103 | }) 104 | 105 | It("should propagate the error", func() { 106 | Expect(err).To(MatchError(testError)) 107 | }) 108 | }) 109 | 110 | Context("when accessing the index manifest returns an error", func() { 111 | BeforeEach(func() { 112 | mockImageIndex.IndexManifestReturns(nil, testError) 113 | }) 114 | 115 | It("should propagate the error", func() { 116 | Expect(err).To(MatchError(testError)) 117 | }) 118 | }) 119 | 120 | Context("when an image in the layout has an invalid name", func() { 121 | BeforeEach(func() { 122 | indexManifest.Manifests = []v1.Descriptor{ 123 | { 124 | Annotations: map[string]string{"org.opencontainers.image.ref.name": ":"}, 125 | Digest: testHash, 126 | }, 127 | } 128 | }) 129 | 130 | It("should return a suitable error", func() { 131 | Expect(err).To(MatchError("invalid image reference: \":\"")) 132 | }) 133 | }) 134 | }) 135 | 136 | Describe("Push", func() { 137 | const testDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000" 138 | var ( 139 | hash v1.Hash 140 | digest image.Digest 141 | targetRef image.Name 142 | err error 143 | mockAbstractImage *registryfakes.FakeImage 144 | ) 145 | 146 | BeforeEach(func() { 147 | hash, err = v1.NewHash(testDigest) 148 | Expect(err).NotTo(HaveOccurred()) 149 | 150 | digest, err = image.NewDigest(testDigest) 151 | Expect(err).NotTo(HaveOccurred()) 152 | 153 | targetRef, err = image.NewName("someref") // actual value is irrelevant 154 | Expect(err).NotTo(HaveOccurred()) 155 | 156 | mockAbstractImage = ®istryfakes.FakeImage{} 157 | }) 158 | 159 | JustBeforeEach(func() { 160 | err = layout.Push(digest, targetRef) 161 | }) 162 | 163 | Context("when the digest refers to a manifest", func() { 164 | var mockImage *ggcrfakes.FakeImage 165 | 166 | BeforeEach(func() { 167 | mockImage = &ggcrfakes.FakeImage{} 168 | mockLayoutPath.ImageIndexReturns(mockImageIndex, nil) 169 | mockImageIndex.ImageReturns(mockImage, nil) 170 | mockRegistryClient.NewImageFromManifestReturns(mockAbstractImage) 171 | mockAbstractImage.WriteReturns(digest, 99, nil) 172 | }) 173 | 174 | It("should write the manifest", func() { 175 | Expect(mockImageIndex.ImageArgsForCall(0)).To(Equal(hash)) 176 | Expect(mockRegistryClient.NewImageFromManifestArgsForCall(0)).To(Equal(mockImage)) 177 | Expect(mockAbstractImage.WriteArgsForCall(0)).To(Equal(targetRef)) 178 | }) 179 | }) 180 | 181 | Context("when the digest refers to an image index", func() { 182 | var mockImageIndex2 *ggcrfakes.FakeImageIndex 183 | 184 | BeforeEach(func() { 185 | mockImageIndex2 = &ggcrfakes.FakeImageIndex{} 186 | mockLayoutPath.ImageIndexReturns(mockImageIndex, nil) 187 | mockImageIndex.ImageReturns(nil, errors.New("some error")) 188 | mockImageIndex.ImageIndexReturns(mockImageIndex2, nil) 189 | mockRegistryClient.NewImageFromIndexReturns(mockAbstractImage) 190 | mockAbstractImage.WriteReturns(digest, 99, nil) 191 | }) 192 | 193 | It("should write the manifest", func() { 194 | Expect(mockImageIndex.ImageArgsForCall(0)).To(Equal(hash)) 195 | Expect(mockImageIndex.ImageIndexArgsForCall(0)).To(Equal(hash)) 196 | Expect(mockRegistryClient.NewImageFromIndexArgsForCall(0)).To(Equal(mockImageIndex2)) 197 | Expect(mockAbstractImage.WriteArgsForCall(0)).To(Equal(targetRef)) 198 | }) 199 | }) 200 | 201 | Context("when the digest refers neither to a manifest nor an image index", func() { 202 | BeforeEach(func() { 203 | mockLayoutPath.ImageIndexReturns(mockImageIndex, nil) 204 | mockImageIndex.ImageReturns(nil, errors.New("image error")) 205 | mockImageIndex.ImageIndexReturns(nil, errors.New("index error")) 206 | }) 207 | 208 | It("should return either the image lookup error or the index lookup error", func() { 209 | // Note: in practice the errors are identical, e.g. "could not find descriptor in index: sha256:..." 210 | Expect(err).To(Or(MatchError("image error"), MatchError("index error"))) 211 | }) 212 | }) 213 | }) 214 | }) 215 | -------------------------------------------------------------------------------- /pkg/registry/ggcr/path/layoutpath.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package path 18 | 19 | import ( 20 | "github.com/google/go-containerregistry/pkg/v1" 21 | "github.com/google/go-containerregistry/pkg/v1/layout" 22 | ) 23 | 24 | type LayoutPath interface { 25 | AppendImage(img v1.Image, options ...layout.Option) error 26 | AppendIndex(ii v1.ImageIndex, options ...layout.Option) error 27 | ImageIndex() (v1.ImageIndex, error) 28 | } 29 | 30 | -------------------------------------------------------------------------------- /pkg/registry/ggcr/path/pathfakes/fake_layout_path.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package pathfakes 3 | 4 | import ( 5 | sync "sync" 6 | 7 | v1 "github.com/google/go-containerregistry/pkg/v1" 8 | layout "github.com/google/go-containerregistry/pkg/v1/layout" 9 | path "github.com/pivotal/image-relocation/pkg/registry/ggcr/path" 10 | ) 11 | 12 | type FakeLayoutPath struct { 13 | AppendImageStub func(v1.Image, ...layout.Option) error 14 | appendImageMutex sync.RWMutex 15 | appendImageArgsForCall []struct { 16 | arg1 v1.Image 17 | arg2 []layout.Option 18 | } 19 | appendImageReturns struct { 20 | result1 error 21 | } 22 | appendImageReturnsOnCall map[int]struct { 23 | result1 error 24 | } 25 | AppendIndexStub func(v1.ImageIndex, ...layout.Option) error 26 | appendIndexMutex sync.RWMutex 27 | appendIndexArgsForCall []struct { 28 | arg1 v1.ImageIndex 29 | arg2 []layout.Option 30 | } 31 | appendIndexReturns struct { 32 | result1 error 33 | } 34 | appendIndexReturnsOnCall map[int]struct { 35 | result1 error 36 | } 37 | ImageIndexStub func() (v1.ImageIndex, error) 38 | imageIndexMutex sync.RWMutex 39 | imageIndexArgsForCall []struct { 40 | } 41 | imageIndexReturns struct { 42 | result1 v1.ImageIndex 43 | result2 error 44 | } 45 | imageIndexReturnsOnCall map[int]struct { 46 | result1 v1.ImageIndex 47 | result2 error 48 | } 49 | invocations map[string][][]interface{} 50 | invocationsMutex sync.RWMutex 51 | } 52 | 53 | func (fake *FakeLayoutPath) AppendImage(arg1 v1.Image, arg2 ...layout.Option) error { 54 | fake.appendImageMutex.Lock() 55 | ret, specificReturn := fake.appendImageReturnsOnCall[len(fake.appendImageArgsForCall)] 56 | fake.appendImageArgsForCall = append(fake.appendImageArgsForCall, struct { 57 | arg1 v1.Image 58 | arg2 []layout.Option 59 | }{arg1, arg2}) 60 | fake.recordInvocation("AppendImage", []interface{}{arg1, arg2}) 61 | fake.appendImageMutex.Unlock() 62 | if fake.AppendImageStub != nil { 63 | return fake.AppendImageStub(arg1, arg2...) 64 | } 65 | if specificReturn { 66 | return ret.result1 67 | } 68 | fakeReturns := fake.appendImageReturns 69 | return fakeReturns.result1 70 | } 71 | 72 | func (fake *FakeLayoutPath) AppendImageCallCount() int { 73 | fake.appendImageMutex.RLock() 74 | defer fake.appendImageMutex.RUnlock() 75 | return len(fake.appendImageArgsForCall) 76 | } 77 | 78 | func (fake *FakeLayoutPath) AppendImageCalls(stub func(v1.Image, ...layout.Option) error) { 79 | fake.appendImageMutex.Lock() 80 | defer fake.appendImageMutex.Unlock() 81 | fake.AppendImageStub = stub 82 | } 83 | 84 | func (fake *FakeLayoutPath) AppendImageArgsForCall(i int) (v1.Image, []layout.Option) { 85 | fake.appendImageMutex.RLock() 86 | defer fake.appendImageMutex.RUnlock() 87 | argsForCall := fake.appendImageArgsForCall[i] 88 | return argsForCall.arg1, argsForCall.arg2 89 | } 90 | 91 | func (fake *FakeLayoutPath) AppendImageReturns(result1 error) { 92 | fake.appendImageMutex.Lock() 93 | defer fake.appendImageMutex.Unlock() 94 | fake.AppendImageStub = nil 95 | fake.appendImageReturns = struct { 96 | result1 error 97 | }{result1} 98 | } 99 | 100 | func (fake *FakeLayoutPath) AppendImageReturnsOnCall(i int, result1 error) { 101 | fake.appendImageMutex.Lock() 102 | defer fake.appendImageMutex.Unlock() 103 | fake.AppendImageStub = nil 104 | if fake.appendImageReturnsOnCall == nil { 105 | fake.appendImageReturnsOnCall = make(map[int]struct { 106 | result1 error 107 | }) 108 | } 109 | fake.appendImageReturnsOnCall[i] = struct { 110 | result1 error 111 | }{result1} 112 | } 113 | 114 | func (fake *FakeLayoutPath) AppendIndex(arg1 v1.ImageIndex, arg2 ...layout.Option) error { 115 | fake.appendIndexMutex.Lock() 116 | ret, specificReturn := fake.appendIndexReturnsOnCall[len(fake.appendIndexArgsForCall)] 117 | fake.appendIndexArgsForCall = append(fake.appendIndexArgsForCall, struct { 118 | arg1 v1.ImageIndex 119 | arg2 []layout.Option 120 | }{arg1, arg2}) 121 | fake.recordInvocation("AppendIndex", []interface{}{arg1, arg2}) 122 | fake.appendIndexMutex.Unlock() 123 | if fake.AppendIndexStub != nil { 124 | return fake.AppendIndexStub(arg1, arg2...) 125 | } 126 | if specificReturn { 127 | return ret.result1 128 | } 129 | fakeReturns := fake.appendIndexReturns 130 | return fakeReturns.result1 131 | } 132 | 133 | func (fake *FakeLayoutPath) AppendIndexCallCount() int { 134 | fake.appendIndexMutex.RLock() 135 | defer fake.appendIndexMutex.RUnlock() 136 | return len(fake.appendIndexArgsForCall) 137 | } 138 | 139 | func (fake *FakeLayoutPath) AppendIndexCalls(stub func(v1.ImageIndex, ...layout.Option) error) { 140 | fake.appendIndexMutex.Lock() 141 | defer fake.appendIndexMutex.Unlock() 142 | fake.AppendIndexStub = stub 143 | } 144 | 145 | func (fake *FakeLayoutPath) AppendIndexArgsForCall(i int) (v1.ImageIndex, []layout.Option) { 146 | fake.appendIndexMutex.RLock() 147 | defer fake.appendIndexMutex.RUnlock() 148 | argsForCall := fake.appendIndexArgsForCall[i] 149 | return argsForCall.arg1, argsForCall.arg2 150 | } 151 | 152 | func (fake *FakeLayoutPath) AppendIndexReturns(result1 error) { 153 | fake.appendIndexMutex.Lock() 154 | defer fake.appendIndexMutex.Unlock() 155 | fake.AppendIndexStub = nil 156 | fake.appendIndexReturns = struct { 157 | result1 error 158 | }{result1} 159 | } 160 | 161 | func (fake *FakeLayoutPath) AppendIndexReturnsOnCall(i int, result1 error) { 162 | fake.appendIndexMutex.Lock() 163 | defer fake.appendIndexMutex.Unlock() 164 | fake.AppendIndexStub = nil 165 | if fake.appendIndexReturnsOnCall == nil { 166 | fake.appendIndexReturnsOnCall = make(map[int]struct { 167 | result1 error 168 | }) 169 | } 170 | fake.appendIndexReturnsOnCall[i] = struct { 171 | result1 error 172 | }{result1} 173 | } 174 | 175 | func (fake *FakeLayoutPath) ImageIndex() (v1.ImageIndex, error) { 176 | fake.imageIndexMutex.Lock() 177 | ret, specificReturn := fake.imageIndexReturnsOnCall[len(fake.imageIndexArgsForCall)] 178 | fake.imageIndexArgsForCall = append(fake.imageIndexArgsForCall, struct { 179 | }{}) 180 | fake.recordInvocation("ImageIndex", []interface{}{}) 181 | fake.imageIndexMutex.Unlock() 182 | if fake.ImageIndexStub != nil { 183 | return fake.ImageIndexStub() 184 | } 185 | if specificReturn { 186 | return ret.result1, ret.result2 187 | } 188 | fakeReturns := fake.imageIndexReturns 189 | return fakeReturns.result1, fakeReturns.result2 190 | } 191 | 192 | func (fake *FakeLayoutPath) ImageIndexCallCount() int { 193 | fake.imageIndexMutex.RLock() 194 | defer fake.imageIndexMutex.RUnlock() 195 | return len(fake.imageIndexArgsForCall) 196 | } 197 | 198 | func (fake *FakeLayoutPath) ImageIndexCalls(stub func() (v1.ImageIndex, error)) { 199 | fake.imageIndexMutex.Lock() 200 | defer fake.imageIndexMutex.Unlock() 201 | fake.ImageIndexStub = stub 202 | } 203 | 204 | func (fake *FakeLayoutPath) ImageIndexReturns(result1 v1.ImageIndex, result2 error) { 205 | fake.imageIndexMutex.Lock() 206 | defer fake.imageIndexMutex.Unlock() 207 | fake.ImageIndexStub = nil 208 | fake.imageIndexReturns = struct { 209 | result1 v1.ImageIndex 210 | result2 error 211 | }{result1, result2} 212 | } 213 | 214 | func (fake *FakeLayoutPath) ImageIndexReturnsOnCall(i int, result1 v1.ImageIndex, result2 error) { 215 | fake.imageIndexMutex.Lock() 216 | defer fake.imageIndexMutex.Unlock() 217 | fake.ImageIndexStub = nil 218 | if fake.imageIndexReturnsOnCall == nil { 219 | fake.imageIndexReturnsOnCall = make(map[int]struct { 220 | result1 v1.ImageIndex 221 | result2 error 222 | }) 223 | } 224 | fake.imageIndexReturnsOnCall[i] = struct { 225 | result1 v1.ImageIndex 226 | result2 error 227 | }{result1, result2} 228 | } 229 | 230 | func (fake *FakeLayoutPath) Invocations() map[string][][]interface{} { 231 | fake.invocationsMutex.RLock() 232 | defer fake.invocationsMutex.RUnlock() 233 | fake.appendImageMutex.RLock() 234 | defer fake.appendImageMutex.RUnlock() 235 | fake.appendIndexMutex.RLock() 236 | defer fake.appendIndexMutex.RUnlock() 237 | fake.imageIndexMutex.RLock() 238 | defer fake.imageIndexMutex.RUnlock() 239 | copiedInvocations := map[string][][]interface{}{} 240 | for key, value := range fake.invocations { 241 | copiedInvocations[key] = value 242 | } 243 | return copiedInvocations 244 | } 245 | 246 | func (fake *FakeLayoutPath) recordInvocation(key string, args []interface{}) { 247 | fake.invocationsMutex.Lock() 248 | defer fake.invocationsMutex.Unlock() 249 | if fake.invocations == nil { 250 | fake.invocations = map[string][][]interface{}{} 251 | } 252 | if fake.invocations[key] == nil { 253 | fake.invocations[key] = [][]interface{}{} 254 | } 255 | fake.invocations[key] = append(fake.invocations[key], args) 256 | } 257 | 258 | var _ path.LayoutPath = new(FakeLayoutPath) 259 | -------------------------------------------------------------------------------- /pkg/registry/ggcr/registryclientfakes/fake_registry_client.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package registryclientfakes 3 | 4 | import ( 5 | sync "sync" 6 | 7 | v1 "github.com/google/go-containerregistry/pkg/v1" 8 | image "github.com/pivotal/image-relocation/pkg/image" 9 | registry "github.com/pivotal/image-relocation/pkg/registry" 10 | ggcr "github.com/pivotal/image-relocation/pkg/registry/ggcr" 11 | ) 12 | 13 | type FakeRegistryClient struct { 14 | NewImageFromIndexStub func(v1.ImageIndex) registry.Image 15 | newImageFromIndexMutex sync.RWMutex 16 | newImageFromIndexArgsForCall []struct { 17 | arg1 v1.ImageIndex 18 | } 19 | newImageFromIndexReturns struct { 20 | result1 registry.Image 21 | } 22 | newImageFromIndexReturnsOnCall map[int]struct { 23 | result1 registry.Image 24 | } 25 | NewImageFromManifestStub func(v1.Image) registry.Image 26 | newImageFromManifestMutex sync.RWMutex 27 | newImageFromManifestArgsForCall []struct { 28 | arg1 v1.Image 29 | } 30 | newImageFromManifestReturns struct { 31 | result1 registry.Image 32 | } 33 | newImageFromManifestReturnsOnCall map[int]struct { 34 | result1 registry.Image 35 | } 36 | ReadRemoteImageStub func(image.Name) (registry.Image, error) 37 | readRemoteImageMutex sync.RWMutex 38 | readRemoteImageArgsForCall []struct { 39 | arg1 image.Name 40 | } 41 | readRemoteImageReturns struct { 42 | result1 registry.Image 43 | result2 error 44 | } 45 | readRemoteImageReturnsOnCall map[int]struct { 46 | result1 registry.Image 47 | result2 error 48 | } 49 | invocations map[string][][]interface{} 50 | invocationsMutex sync.RWMutex 51 | } 52 | 53 | func (fake *FakeRegistryClient) NewImageFromIndex(arg1 v1.ImageIndex) registry.Image { 54 | fake.newImageFromIndexMutex.Lock() 55 | ret, specificReturn := fake.newImageFromIndexReturnsOnCall[len(fake.newImageFromIndexArgsForCall)] 56 | fake.newImageFromIndexArgsForCall = append(fake.newImageFromIndexArgsForCall, struct { 57 | arg1 v1.ImageIndex 58 | }{arg1}) 59 | fake.recordInvocation("NewImageFromIndex", []interface{}{arg1}) 60 | fake.newImageFromIndexMutex.Unlock() 61 | if fake.NewImageFromIndexStub != nil { 62 | return fake.NewImageFromIndexStub(arg1) 63 | } 64 | if specificReturn { 65 | return ret.result1 66 | } 67 | fakeReturns := fake.newImageFromIndexReturns 68 | return fakeReturns.result1 69 | } 70 | 71 | func (fake *FakeRegistryClient) NewImageFromIndexCallCount() int { 72 | fake.newImageFromIndexMutex.RLock() 73 | defer fake.newImageFromIndexMutex.RUnlock() 74 | return len(fake.newImageFromIndexArgsForCall) 75 | } 76 | 77 | func (fake *FakeRegistryClient) NewImageFromIndexCalls(stub func(v1.ImageIndex) registry.Image) { 78 | fake.newImageFromIndexMutex.Lock() 79 | defer fake.newImageFromIndexMutex.Unlock() 80 | fake.NewImageFromIndexStub = stub 81 | } 82 | 83 | func (fake *FakeRegistryClient) NewImageFromIndexArgsForCall(i int) v1.ImageIndex { 84 | fake.newImageFromIndexMutex.RLock() 85 | defer fake.newImageFromIndexMutex.RUnlock() 86 | argsForCall := fake.newImageFromIndexArgsForCall[i] 87 | return argsForCall.arg1 88 | } 89 | 90 | func (fake *FakeRegistryClient) NewImageFromIndexReturns(result1 registry.Image) { 91 | fake.newImageFromIndexMutex.Lock() 92 | defer fake.newImageFromIndexMutex.Unlock() 93 | fake.NewImageFromIndexStub = nil 94 | fake.newImageFromIndexReturns = struct { 95 | result1 registry.Image 96 | }{result1} 97 | } 98 | 99 | func (fake *FakeRegistryClient) NewImageFromIndexReturnsOnCall(i int, result1 registry.Image) { 100 | fake.newImageFromIndexMutex.Lock() 101 | defer fake.newImageFromIndexMutex.Unlock() 102 | fake.NewImageFromIndexStub = nil 103 | if fake.newImageFromIndexReturnsOnCall == nil { 104 | fake.newImageFromIndexReturnsOnCall = make(map[int]struct { 105 | result1 registry.Image 106 | }) 107 | } 108 | fake.newImageFromIndexReturnsOnCall[i] = struct { 109 | result1 registry.Image 110 | }{result1} 111 | } 112 | 113 | func (fake *FakeRegistryClient) NewImageFromManifest(arg1 v1.Image) registry.Image { 114 | fake.newImageFromManifestMutex.Lock() 115 | ret, specificReturn := fake.newImageFromManifestReturnsOnCall[len(fake.newImageFromManifestArgsForCall)] 116 | fake.newImageFromManifestArgsForCall = append(fake.newImageFromManifestArgsForCall, struct { 117 | arg1 v1.Image 118 | }{arg1}) 119 | fake.recordInvocation("NewImageFromManifest", []interface{}{arg1}) 120 | fake.newImageFromManifestMutex.Unlock() 121 | if fake.NewImageFromManifestStub != nil { 122 | return fake.NewImageFromManifestStub(arg1) 123 | } 124 | if specificReturn { 125 | return ret.result1 126 | } 127 | fakeReturns := fake.newImageFromManifestReturns 128 | return fakeReturns.result1 129 | } 130 | 131 | func (fake *FakeRegistryClient) NewImageFromManifestCallCount() int { 132 | fake.newImageFromManifestMutex.RLock() 133 | defer fake.newImageFromManifestMutex.RUnlock() 134 | return len(fake.newImageFromManifestArgsForCall) 135 | } 136 | 137 | func (fake *FakeRegistryClient) NewImageFromManifestCalls(stub func(v1.Image) registry.Image) { 138 | fake.newImageFromManifestMutex.Lock() 139 | defer fake.newImageFromManifestMutex.Unlock() 140 | fake.NewImageFromManifestStub = stub 141 | } 142 | 143 | func (fake *FakeRegistryClient) NewImageFromManifestArgsForCall(i int) v1.Image { 144 | fake.newImageFromManifestMutex.RLock() 145 | defer fake.newImageFromManifestMutex.RUnlock() 146 | argsForCall := fake.newImageFromManifestArgsForCall[i] 147 | return argsForCall.arg1 148 | } 149 | 150 | func (fake *FakeRegistryClient) NewImageFromManifestReturns(result1 registry.Image) { 151 | fake.newImageFromManifestMutex.Lock() 152 | defer fake.newImageFromManifestMutex.Unlock() 153 | fake.NewImageFromManifestStub = nil 154 | fake.newImageFromManifestReturns = struct { 155 | result1 registry.Image 156 | }{result1} 157 | } 158 | 159 | func (fake *FakeRegistryClient) NewImageFromManifestReturnsOnCall(i int, result1 registry.Image) { 160 | fake.newImageFromManifestMutex.Lock() 161 | defer fake.newImageFromManifestMutex.Unlock() 162 | fake.NewImageFromManifestStub = nil 163 | if fake.newImageFromManifestReturnsOnCall == nil { 164 | fake.newImageFromManifestReturnsOnCall = make(map[int]struct { 165 | result1 registry.Image 166 | }) 167 | } 168 | fake.newImageFromManifestReturnsOnCall[i] = struct { 169 | result1 registry.Image 170 | }{result1} 171 | } 172 | 173 | func (fake *FakeRegistryClient) ReadRemoteImage(arg1 image.Name) (registry.Image, error) { 174 | fake.readRemoteImageMutex.Lock() 175 | ret, specificReturn := fake.readRemoteImageReturnsOnCall[len(fake.readRemoteImageArgsForCall)] 176 | fake.readRemoteImageArgsForCall = append(fake.readRemoteImageArgsForCall, struct { 177 | arg1 image.Name 178 | }{arg1}) 179 | fake.recordInvocation("ReadRemoteImage", []interface{}{arg1}) 180 | fake.readRemoteImageMutex.Unlock() 181 | if fake.ReadRemoteImageStub != nil { 182 | return fake.ReadRemoteImageStub(arg1) 183 | } 184 | if specificReturn { 185 | return ret.result1, ret.result2 186 | } 187 | fakeReturns := fake.readRemoteImageReturns 188 | return fakeReturns.result1, fakeReturns.result2 189 | } 190 | 191 | func (fake *FakeRegistryClient) ReadRemoteImageCallCount() int { 192 | fake.readRemoteImageMutex.RLock() 193 | defer fake.readRemoteImageMutex.RUnlock() 194 | return len(fake.readRemoteImageArgsForCall) 195 | } 196 | 197 | func (fake *FakeRegistryClient) ReadRemoteImageCalls(stub func(image.Name) (registry.Image, error)) { 198 | fake.readRemoteImageMutex.Lock() 199 | defer fake.readRemoteImageMutex.Unlock() 200 | fake.ReadRemoteImageStub = stub 201 | } 202 | 203 | func (fake *FakeRegistryClient) ReadRemoteImageArgsForCall(i int) image.Name { 204 | fake.readRemoteImageMutex.RLock() 205 | defer fake.readRemoteImageMutex.RUnlock() 206 | argsForCall := fake.readRemoteImageArgsForCall[i] 207 | return argsForCall.arg1 208 | } 209 | 210 | func (fake *FakeRegistryClient) ReadRemoteImageReturns(result1 registry.Image, result2 error) { 211 | fake.readRemoteImageMutex.Lock() 212 | defer fake.readRemoteImageMutex.Unlock() 213 | fake.ReadRemoteImageStub = nil 214 | fake.readRemoteImageReturns = struct { 215 | result1 registry.Image 216 | result2 error 217 | }{result1, result2} 218 | } 219 | 220 | func (fake *FakeRegistryClient) ReadRemoteImageReturnsOnCall(i int, result1 registry.Image, result2 error) { 221 | fake.readRemoteImageMutex.Lock() 222 | defer fake.readRemoteImageMutex.Unlock() 223 | fake.ReadRemoteImageStub = nil 224 | if fake.readRemoteImageReturnsOnCall == nil { 225 | fake.readRemoteImageReturnsOnCall = make(map[int]struct { 226 | result1 registry.Image 227 | result2 error 228 | }) 229 | } 230 | fake.readRemoteImageReturnsOnCall[i] = struct { 231 | result1 registry.Image 232 | result2 error 233 | }{result1, result2} 234 | } 235 | 236 | func (fake *FakeRegistryClient) Invocations() map[string][][]interface{} { 237 | fake.invocationsMutex.RLock() 238 | defer fake.invocationsMutex.RUnlock() 239 | fake.newImageFromIndexMutex.RLock() 240 | defer fake.newImageFromIndexMutex.RUnlock() 241 | fake.newImageFromManifestMutex.RLock() 242 | defer fake.newImageFromManifestMutex.RUnlock() 243 | fake.readRemoteImageMutex.RLock() 244 | defer fake.readRemoteImageMutex.RUnlock() 245 | copiedInvocations := map[string][][]interface{}{} 246 | for key, value := range fake.invocations { 247 | copiedInvocations[key] = value 248 | } 249 | return copiedInvocations 250 | } 251 | 252 | func (fake *FakeRegistryClient) recordInvocation(key string, args []interface{}) { 253 | fake.invocationsMutex.Lock() 254 | defer fake.invocationsMutex.Unlock() 255 | if fake.invocations == nil { 256 | fake.invocations = map[string][][]interface{}{} 257 | } 258 | if fake.invocations[key] == nil { 259 | fake.invocations[key] = [][]interface{}{} 260 | } 261 | fake.invocations[key] = append(fake.invocations[key], args) 262 | } 263 | 264 | var _ ggcr.RegistryClient = new(FakeRegistryClient) 265 | -------------------------------------------------------------------------------- /pkg/registry/ggcr/remote.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ggcr 18 | 19 | import ( 20 | "errors" 21 | "net/http" 22 | 23 | "github.com/google/go-containerregistry/pkg/v1/types" 24 | "github.com/pivotal/image-relocation/pkg/registry" 25 | 26 | "github.com/google/go-containerregistry/pkg/authn" 27 | "github.com/google/go-containerregistry/pkg/name" 28 | v1 "github.com/google/go-containerregistry/pkg/v1" 29 | "github.com/google/go-containerregistry/pkg/v1/remote" 30 | "github.com/pivotal/image-relocation/pkg/image" 31 | ) 32 | 33 | var ( 34 | resolveFunc = authn.DefaultKeychain.Resolve 35 | repoGetFunc = remote.Get 36 | repoWriteFunc = remote.Write 37 | repoIndexWriteFunc = remote.WriteIndex 38 | ) 39 | 40 | func readRemoteImage(mfstWriter manifestWriter, idxWriter indexWriter, transport http.RoundTripper) func(image.Name) (registry.Image, error) { 41 | return func(n image.Name) (registry.Image, error) { 42 | auth, err := resolve(n) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | if n.Tag() == "" && n.Digest() == image.EmptyDigest { 48 | // use default tag 49 | n, err = n.WithTag("latest") 50 | if err != nil { 51 | return nil, err 52 | } 53 | } 54 | ref, err := name.ParseReference(n.String(), name.StrictValidation) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | desc, err := repoGetFunc(ref, remote.WithAuth(auth), remote.WithTransport(transport)) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | switch desc.MediaType { 65 | case types.OCIImageIndex, types.DockerManifestList: 66 | idx, err := desc.ImageIndex() 67 | if err != nil { 68 | return nil, err 69 | } 70 | return newImageFromIndex(idx, idxWriter), nil 71 | default: 72 | // assume all other media types are images since some images don't set the media type 73 | } 74 | img, err := desc.Image() 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | return newImageFromManifest(img, mfstWriter), nil 80 | } 81 | } 82 | 83 | func writeRemoteImage(transport http.RoundTripper) func(v1.Image, image.Name) error { 84 | return func(i v1.Image, n image.Name) error { 85 | auth, err := resolve(n) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | ref, err := getWriteReference(n) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | return repoWriteFunc(ref, i, remote.WithAuth(auth), remote.WithTransport(transport)) 96 | } 97 | } 98 | 99 | func writeRemoteIndex(transport http.RoundTripper) func(v1.ImageIndex, image.Name) error { 100 | return func(i v1.ImageIndex, n image.Name) error { 101 | auth, err := resolve(n) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | ref, err := getWriteReference(n) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | return repoIndexWriteFunc(ref, i, remote.WithAuth(auth), remote.WithTransport(transport)) 112 | } 113 | } 114 | 115 | func resolve(n image.Name) (authn.Authenticator, error) { 116 | if n == image.EmptyName { 117 | return nil, errors.New("empty image name invalid") 118 | } 119 | repo, err := name.NewRepository(n.WithoutTagOrDigest().String(), name.WeakValidation) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | return resolveFunc(repo.Registry) 125 | } 126 | 127 | func getWriteReference(n image.Name) (name.Reference, error) { 128 | // if target image reference is both tagged and digested, ignore the digest so the tag is preserved 129 | // (the digest will be preserved by go-containerregistry) 130 | if n.Tag() != "" { 131 | n = n.WithoutDigest() 132 | } 133 | 134 | return name.ParseReference(n.String(), name.WeakValidation) 135 | } 136 | -------------------------------------------------------------------------------- /pkg/registry/ggcr/remote_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package ggcr 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | 23 | "github.com/google/go-containerregistry/pkg/authn" 24 | "github.com/google/go-containerregistry/pkg/name" 25 | v1 "github.com/google/go-containerregistry/pkg/v1" 26 | "github.com/google/go-containerregistry/pkg/v1/remote" 27 | . "github.com/onsi/ginkgo" 28 | . "github.com/onsi/gomega" 29 | "github.com/pivotal/image-relocation/pkg/image" 30 | "github.com/pivotal/image-relocation/pkg/registry/ggcrfakes" 31 | ) 32 | 33 | var _ = Describe("remote utilities", func() { 34 | var ( 35 | imageName image.Name 36 | mockImage *ggcrfakes.FakeImage 37 | testDigest string 38 | testError error 39 | err error 40 | ) 41 | 42 | BeforeEach(func() { 43 | var err error 44 | imageName, err = image.NewName("imagename") 45 | Expect(err).NotTo(HaveOccurred()) 46 | 47 | testDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000" 48 | mockImage = &ggcrfakes.FakeImage{} 49 | h1, err := v1.NewHash(testDigest) 50 | Expect(err).NotTo(HaveOccurred()) 51 | mockImage.DigestReturns(h1, nil) 52 | 53 | testError = errors.New("hard cheese") 54 | }) 55 | 56 | // FIXME: get coverage back up 57 | 58 | Describe("readRemoteImage", func() { 59 | JustBeforeEach(func() { 60 | _, err = readRemoteImage(nil, nil, nil)(imageName) 61 | }) 62 | 63 | BeforeEach(func() { 64 | var err error 65 | imageName, err = image.NewName("imagename") 66 | Expect(err).NotTo(HaveOccurred()) 67 | 68 | // In most tests, keychain resolution succeeds 69 | resolveFunc = func(authn.Resource) (authn.Authenticator, error) { 70 | return nil, nil 71 | } 72 | }) 73 | 74 | Context("when keychain resolution fails", func() { 75 | BeforeEach(func() { 76 | resolveFunc = func(authn.Resource) (authn.Authenticator, error) { 77 | return nil, testError 78 | } 79 | }) 80 | 81 | It("should return the error", func() { 82 | Expect(err).To(Equal(testError)) 83 | }) 84 | }) 85 | 86 | Context("when the image name is empty", func() { 87 | BeforeEach(func() { 88 | imageName = image.EmptyName 89 | }) 90 | 91 | It("should return an error", func() { 92 | Expect(err).To(MatchError("empty image name invalid")) 93 | }) 94 | }) 95 | }) 96 | 97 | Describe("writeRemoteImage", func() { 98 | JustBeforeEach(func() { 99 | err = writeRemoteImage(nil)(mockImage, imageName) 100 | }) 101 | 102 | Context("when writing to the repository succeeds", func() { 103 | BeforeEach(func() { 104 | repoWriteFunc = func(ref name.Reference, img v1.Image, options ...remote.Option) error { 105 | return nil 106 | } 107 | }) 108 | 109 | It("should succeed", func() { 110 | Expect(err).NotTo(HaveOccurred()) 111 | }) 112 | }) 113 | 114 | Context("when writing to the repository return an error", func() { 115 | BeforeEach(func() { 116 | repoWriteFunc = func(ref name.Reference, img v1.Image, options ...remote.Option) error { 117 | return testError 118 | } 119 | }) 120 | 121 | It("should return the error", func() { 122 | Expect(err).To(Equal(testError)) 123 | }) 124 | }) 125 | 126 | Context("when the image name is empty", func() { 127 | BeforeEach(func() { 128 | imageName = image.EmptyName 129 | }) 130 | 131 | It("should return an error", func() { 132 | Expect(err).To(MatchError("empty image name invalid")) 133 | }) 134 | }) 135 | 136 | Context("when the image name is both tagged and digested", func() { 137 | var writeRef name.Reference 138 | BeforeEach(func() { 139 | imageName, err = image.NewName(fmt.Sprintf("example.com/eg:1@%s", testDigest)) 140 | Expect(err).NotTo(HaveOccurred()) 141 | repoWriteFunc = func(ref name.Reference, img v1.Image, options ...remote.Option) error { 142 | writeRef = ref 143 | return nil 144 | } 145 | }) 146 | 147 | It("should discard the digest from the written reference", func() { 148 | Expect(writeRef.String()).To(Equal("example.com/eg:1")) 149 | }) 150 | }) 151 | }) 152 | 153 | Describe("writeRemoteIndex", func() { 154 | var mockIndex *ggcrfakes.FakeImageIndex 155 | 156 | BeforeEach(func() { 157 | mockIndex = &ggcrfakes.FakeImageIndex{} 158 | }) 159 | 160 | JustBeforeEach(func() { 161 | err = writeRemoteIndex(nil)(mockIndex, imageName) 162 | }) 163 | 164 | Context("when writing to the repository succeeds", func() { 165 | BeforeEach(func() { 166 | repoIndexWriteFunc = func(ref name.Reference, ii v1.ImageIndex, options ...remote.Option) error { 167 | return nil 168 | } 169 | }) 170 | 171 | It("should succeed", func() { 172 | Expect(err).NotTo(HaveOccurred()) 173 | }) 174 | }) 175 | 176 | Context("when writing to the repository return an error", func() { 177 | BeforeEach(func() { 178 | repoIndexWriteFunc = func(ref name.Reference, ii v1.ImageIndex, options ...remote.Option) error { 179 | return testError 180 | } 181 | }) 182 | 183 | It("should return the error", func() { 184 | Expect(err).To(Equal(testError)) 185 | }) 186 | }) 187 | 188 | Context("when the image name is empty", func() { 189 | BeforeEach(func() { 190 | imageName = image.EmptyName 191 | }) 192 | 193 | It("should return an error", func() { 194 | Expect(err).To(MatchError("empty image name invalid")) 195 | }) 196 | }) 197 | 198 | Context("when the image name is both tagged and digested", func() { 199 | var writeRef name.Reference 200 | BeforeEach(func() { 201 | imageName, err = image.NewName(fmt.Sprintf("example.com/eg:1@%s", testDigest)) 202 | Expect(err).NotTo(HaveOccurred()) 203 | repoIndexWriteFunc = func(ref name.Reference, ii v1.ImageIndex, options ...remote.Option) error { 204 | writeRef = ref 205 | return nil 206 | } 207 | }) 208 | 209 | It("should discard the digest from the written reference", func() { 210 | Expect(writeRef.String()).To(Equal("example.com/eg:1")) 211 | }) 212 | }) 213 | }) 214 | }) 215 | -------------------------------------------------------------------------------- /pkg/registry/ggcrfakes/fake_image_index.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package ggcrfakes 3 | 4 | import ( 5 | sync "sync" 6 | 7 | v1 "github.com/google/go-containerregistry/pkg/v1" 8 | types "github.com/google/go-containerregistry/pkg/v1/types" 9 | ) 10 | 11 | type FakeImageIndex struct { 12 | DigestStub func() (v1.Hash, error) 13 | digestMutex sync.RWMutex 14 | digestArgsForCall []struct { 15 | } 16 | digestReturns struct { 17 | result1 v1.Hash 18 | result2 error 19 | } 20 | digestReturnsOnCall map[int]struct { 21 | result1 v1.Hash 22 | result2 error 23 | } 24 | ImageStub func(v1.Hash) (v1.Image, error) 25 | imageMutex sync.RWMutex 26 | imageArgsForCall []struct { 27 | arg1 v1.Hash 28 | } 29 | imageReturns struct { 30 | result1 v1.Image 31 | result2 error 32 | } 33 | imageReturnsOnCall map[int]struct { 34 | result1 v1.Image 35 | result2 error 36 | } 37 | ImageIndexStub func(v1.Hash) (v1.ImageIndex, error) 38 | imageIndexMutex sync.RWMutex 39 | imageIndexArgsForCall []struct { 40 | arg1 v1.Hash 41 | } 42 | imageIndexReturns struct { 43 | result1 v1.ImageIndex 44 | result2 error 45 | } 46 | imageIndexReturnsOnCall map[int]struct { 47 | result1 v1.ImageIndex 48 | result2 error 49 | } 50 | IndexManifestStub func() (*v1.IndexManifest, error) 51 | indexManifestMutex sync.RWMutex 52 | indexManifestArgsForCall []struct { 53 | } 54 | indexManifestReturns struct { 55 | result1 *v1.IndexManifest 56 | result2 error 57 | } 58 | indexManifestReturnsOnCall map[int]struct { 59 | result1 *v1.IndexManifest 60 | result2 error 61 | } 62 | MediaTypeStub func() (types.MediaType, error) 63 | mediaTypeMutex sync.RWMutex 64 | mediaTypeArgsForCall []struct { 65 | } 66 | mediaTypeReturns struct { 67 | result1 types.MediaType 68 | result2 error 69 | } 70 | mediaTypeReturnsOnCall map[int]struct { 71 | result1 types.MediaType 72 | result2 error 73 | } 74 | RawManifestStub func() ([]byte, error) 75 | rawManifestMutex sync.RWMutex 76 | rawManifestArgsForCall []struct { 77 | } 78 | rawManifestReturns struct { 79 | result1 []byte 80 | result2 error 81 | } 82 | rawManifestReturnsOnCall map[int]struct { 83 | result1 []byte 84 | result2 error 85 | } 86 | SizeStub func() (int64, error) 87 | sizeMutex sync.RWMutex 88 | sizeArgsForCall []struct { 89 | } 90 | sizeReturns struct { 91 | result1 int64 92 | result2 error 93 | } 94 | sizeReturnsOnCall map[int]struct { 95 | result1 int64 96 | result2 error 97 | } 98 | invocations map[string][][]interface{} 99 | invocationsMutex sync.RWMutex 100 | } 101 | 102 | func (fake *FakeImageIndex) Digest() (v1.Hash, error) { 103 | fake.digestMutex.Lock() 104 | ret, specificReturn := fake.digestReturnsOnCall[len(fake.digestArgsForCall)] 105 | fake.digestArgsForCall = append(fake.digestArgsForCall, struct { 106 | }{}) 107 | fake.recordInvocation("Digest", []interface{}{}) 108 | fake.digestMutex.Unlock() 109 | if fake.DigestStub != nil { 110 | return fake.DigestStub() 111 | } 112 | if specificReturn { 113 | return ret.result1, ret.result2 114 | } 115 | fakeReturns := fake.digestReturns 116 | return fakeReturns.result1, fakeReturns.result2 117 | } 118 | 119 | func (fake *FakeImageIndex) DigestCallCount() int { 120 | fake.digestMutex.RLock() 121 | defer fake.digestMutex.RUnlock() 122 | return len(fake.digestArgsForCall) 123 | } 124 | 125 | func (fake *FakeImageIndex) DigestCalls(stub func() (v1.Hash, error)) { 126 | fake.digestMutex.Lock() 127 | defer fake.digestMutex.Unlock() 128 | fake.DigestStub = stub 129 | } 130 | 131 | func (fake *FakeImageIndex) DigestReturns(result1 v1.Hash, result2 error) { 132 | fake.digestMutex.Lock() 133 | defer fake.digestMutex.Unlock() 134 | fake.DigestStub = nil 135 | fake.digestReturns = struct { 136 | result1 v1.Hash 137 | result2 error 138 | }{result1, result2} 139 | } 140 | 141 | func (fake *FakeImageIndex) DigestReturnsOnCall(i int, result1 v1.Hash, result2 error) { 142 | fake.digestMutex.Lock() 143 | defer fake.digestMutex.Unlock() 144 | fake.DigestStub = nil 145 | if fake.digestReturnsOnCall == nil { 146 | fake.digestReturnsOnCall = make(map[int]struct { 147 | result1 v1.Hash 148 | result2 error 149 | }) 150 | } 151 | fake.digestReturnsOnCall[i] = struct { 152 | result1 v1.Hash 153 | result2 error 154 | }{result1, result2} 155 | } 156 | 157 | func (fake *FakeImageIndex) Image(arg1 v1.Hash) (v1.Image, error) { 158 | fake.imageMutex.Lock() 159 | ret, specificReturn := fake.imageReturnsOnCall[len(fake.imageArgsForCall)] 160 | fake.imageArgsForCall = append(fake.imageArgsForCall, struct { 161 | arg1 v1.Hash 162 | }{arg1}) 163 | fake.recordInvocation("Image", []interface{}{arg1}) 164 | fake.imageMutex.Unlock() 165 | if fake.ImageStub != nil { 166 | return fake.ImageStub(arg1) 167 | } 168 | if specificReturn { 169 | return ret.result1, ret.result2 170 | } 171 | fakeReturns := fake.imageReturns 172 | return fakeReturns.result1, fakeReturns.result2 173 | } 174 | 175 | func (fake *FakeImageIndex) ImageCallCount() int { 176 | fake.imageMutex.RLock() 177 | defer fake.imageMutex.RUnlock() 178 | return len(fake.imageArgsForCall) 179 | } 180 | 181 | func (fake *FakeImageIndex) ImageCalls(stub func(v1.Hash) (v1.Image, error)) { 182 | fake.imageMutex.Lock() 183 | defer fake.imageMutex.Unlock() 184 | fake.ImageStub = stub 185 | } 186 | 187 | func (fake *FakeImageIndex) ImageArgsForCall(i int) v1.Hash { 188 | fake.imageMutex.RLock() 189 | defer fake.imageMutex.RUnlock() 190 | argsForCall := fake.imageArgsForCall[i] 191 | return argsForCall.arg1 192 | } 193 | 194 | func (fake *FakeImageIndex) ImageReturns(result1 v1.Image, result2 error) { 195 | fake.imageMutex.Lock() 196 | defer fake.imageMutex.Unlock() 197 | fake.ImageStub = nil 198 | fake.imageReturns = struct { 199 | result1 v1.Image 200 | result2 error 201 | }{result1, result2} 202 | } 203 | 204 | func (fake *FakeImageIndex) ImageReturnsOnCall(i int, result1 v1.Image, result2 error) { 205 | fake.imageMutex.Lock() 206 | defer fake.imageMutex.Unlock() 207 | fake.ImageStub = nil 208 | if fake.imageReturnsOnCall == nil { 209 | fake.imageReturnsOnCall = make(map[int]struct { 210 | result1 v1.Image 211 | result2 error 212 | }) 213 | } 214 | fake.imageReturnsOnCall[i] = struct { 215 | result1 v1.Image 216 | result2 error 217 | }{result1, result2} 218 | } 219 | 220 | func (fake *FakeImageIndex) ImageIndex(arg1 v1.Hash) (v1.ImageIndex, error) { 221 | fake.imageIndexMutex.Lock() 222 | ret, specificReturn := fake.imageIndexReturnsOnCall[len(fake.imageIndexArgsForCall)] 223 | fake.imageIndexArgsForCall = append(fake.imageIndexArgsForCall, struct { 224 | arg1 v1.Hash 225 | }{arg1}) 226 | fake.recordInvocation("ImageIndex", []interface{}{arg1}) 227 | fake.imageIndexMutex.Unlock() 228 | if fake.ImageIndexStub != nil { 229 | return fake.ImageIndexStub(arg1) 230 | } 231 | if specificReturn { 232 | return ret.result1, ret.result2 233 | } 234 | fakeReturns := fake.imageIndexReturns 235 | return fakeReturns.result1, fakeReturns.result2 236 | } 237 | 238 | func (fake *FakeImageIndex) ImageIndexCallCount() int { 239 | fake.imageIndexMutex.RLock() 240 | defer fake.imageIndexMutex.RUnlock() 241 | return len(fake.imageIndexArgsForCall) 242 | } 243 | 244 | func (fake *FakeImageIndex) ImageIndexCalls(stub func(v1.Hash) (v1.ImageIndex, error)) { 245 | fake.imageIndexMutex.Lock() 246 | defer fake.imageIndexMutex.Unlock() 247 | fake.ImageIndexStub = stub 248 | } 249 | 250 | func (fake *FakeImageIndex) ImageIndexArgsForCall(i int) v1.Hash { 251 | fake.imageIndexMutex.RLock() 252 | defer fake.imageIndexMutex.RUnlock() 253 | argsForCall := fake.imageIndexArgsForCall[i] 254 | return argsForCall.arg1 255 | } 256 | 257 | func (fake *FakeImageIndex) ImageIndexReturns(result1 v1.ImageIndex, result2 error) { 258 | fake.imageIndexMutex.Lock() 259 | defer fake.imageIndexMutex.Unlock() 260 | fake.ImageIndexStub = nil 261 | fake.imageIndexReturns = struct { 262 | result1 v1.ImageIndex 263 | result2 error 264 | }{result1, result2} 265 | } 266 | 267 | func (fake *FakeImageIndex) ImageIndexReturnsOnCall(i int, result1 v1.ImageIndex, result2 error) { 268 | fake.imageIndexMutex.Lock() 269 | defer fake.imageIndexMutex.Unlock() 270 | fake.ImageIndexStub = nil 271 | if fake.imageIndexReturnsOnCall == nil { 272 | fake.imageIndexReturnsOnCall = make(map[int]struct { 273 | result1 v1.ImageIndex 274 | result2 error 275 | }) 276 | } 277 | fake.imageIndexReturnsOnCall[i] = struct { 278 | result1 v1.ImageIndex 279 | result2 error 280 | }{result1, result2} 281 | } 282 | 283 | func (fake *FakeImageIndex) IndexManifest() (*v1.IndexManifest, error) { 284 | fake.indexManifestMutex.Lock() 285 | ret, specificReturn := fake.indexManifestReturnsOnCall[len(fake.indexManifestArgsForCall)] 286 | fake.indexManifestArgsForCall = append(fake.indexManifestArgsForCall, struct { 287 | }{}) 288 | fake.recordInvocation("IndexManifest", []interface{}{}) 289 | fake.indexManifestMutex.Unlock() 290 | if fake.IndexManifestStub != nil { 291 | return fake.IndexManifestStub() 292 | } 293 | if specificReturn { 294 | return ret.result1, ret.result2 295 | } 296 | fakeReturns := fake.indexManifestReturns 297 | return fakeReturns.result1, fakeReturns.result2 298 | } 299 | 300 | func (fake *FakeImageIndex) IndexManifestCallCount() int { 301 | fake.indexManifestMutex.RLock() 302 | defer fake.indexManifestMutex.RUnlock() 303 | return len(fake.indexManifestArgsForCall) 304 | } 305 | 306 | func (fake *FakeImageIndex) IndexManifestCalls(stub func() (*v1.IndexManifest, error)) { 307 | fake.indexManifestMutex.Lock() 308 | defer fake.indexManifestMutex.Unlock() 309 | fake.IndexManifestStub = stub 310 | } 311 | 312 | func (fake *FakeImageIndex) IndexManifestReturns(result1 *v1.IndexManifest, result2 error) { 313 | fake.indexManifestMutex.Lock() 314 | defer fake.indexManifestMutex.Unlock() 315 | fake.IndexManifestStub = nil 316 | fake.indexManifestReturns = struct { 317 | result1 *v1.IndexManifest 318 | result2 error 319 | }{result1, result2} 320 | } 321 | 322 | func (fake *FakeImageIndex) IndexManifestReturnsOnCall(i int, result1 *v1.IndexManifest, result2 error) { 323 | fake.indexManifestMutex.Lock() 324 | defer fake.indexManifestMutex.Unlock() 325 | fake.IndexManifestStub = nil 326 | if fake.indexManifestReturnsOnCall == nil { 327 | fake.indexManifestReturnsOnCall = make(map[int]struct { 328 | result1 *v1.IndexManifest 329 | result2 error 330 | }) 331 | } 332 | fake.indexManifestReturnsOnCall[i] = struct { 333 | result1 *v1.IndexManifest 334 | result2 error 335 | }{result1, result2} 336 | } 337 | 338 | func (fake *FakeImageIndex) MediaType() (types.MediaType, error) { 339 | fake.mediaTypeMutex.Lock() 340 | ret, specificReturn := fake.mediaTypeReturnsOnCall[len(fake.mediaTypeArgsForCall)] 341 | fake.mediaTypeArgsForCall = append(fake.mediaTypeArgsForCall, struct { 342 | }{}) 343 | fake.recordInvocation("MediaType", []interface{}{}) 344 | fake.mediaTypeMutex.Unlock() 345 | if fake.MediaTypeStub != nil { 346 | return fake.MediaTypeStub() 347 | } 348 | if specificReturn { 349 | return ret.result1, ret.result2 350 | } 351 | fakeReturns := fake.mediaTypeReturns 352 | return fakeReturns.result1, fakeReturns.result2 353 | } 354 | 355 | func (fake *FakeImageIndex) MediaTypeCallCount() int { 356 | fake.mediaTypeMutex.RLock() 357 | defer fake.mediaTypeMutex.RUnlock() 358 | return len(fake.mediaTypeArgsForCall) 359 | } 360 | 361 | func (fake *FakeImageIndex) MediaTypeCalls(stub func() (types.MediaType, error)) { 362 | fake.mediaTypeMutex.Lock() 363 | defer fake.mediaTypeMutex.Unlock() 364 | fake.MediaTypeStub = stub 365 | } 366 | 367 | func (fake *FakeImageIndex) MediaTypeReturns(result1 types.MediaType, result2 error) { 368 | fake.mediaTypeMutex.Lock() 369 | defer fake.mediaTypeMutex.Unlock() 370 | fake.MediaTypeStub = nil 371 | fake.mediaTypeReturns = struct { 372 | result1 types.MediaType 373 | result2 error 374 | }{result1, result2} 375 | } 376 | 377 | func (fake *FakeImageIndex) MediaTypeReturnsOnCall(i int, result1 types.MediaType, result2 error) { 378 | fake.mediaTypeMutex.Lock() 379 | defer fake.mediaTypeMutex.Unlock() 380 | fake.MediaTypeStub = nil 381 | if fake.mediaTypeReturnsOnCall == nil { 382 | fake.mediaTypeReturnsOnCall = make(map[int]struct { 383 | result1 types.MediaType 384 | result2 error 385 | }) 386 | } 387 | fake.mediaTypeReturnsOnCall[i] = struct { 388 | result1 types.MediaType 389 | result2 error 390 | }{result1, result2} 391 | } 392 | 393 | func (fake *FakeImageIndex) RawManifest() ([]byte, error) { 394 | fake.rawManifestMutex.Lock() 395 | ret, specificReturn := fake.rawManifestReturnsOnCall[len(fake.rawManifestArgsForCall)] 396 | fake.rawManifestArgsForCall = append(fake.rawManifestArgsForCall, struct { 397 | }{}) 398 | fake.recordInvocation("RawManifest", []interface{}{}) 399 | fake.rawManifestMutex.Unlock() 400 | if fake.RawManifestStub != nil { 401 | return fake.RawManifestStub() 402 | } 403 | if specificReturn { 404 | return ret.result1, ret.result2 405 | } 406 | fakeReturns := fake.rawManifestReturns 407 | return fakeReturns.result1, fakeReturns.result2 408 | } 409 | 410 | func (fake *FakeImageIndex) RawManifestCallCount() int { 411 | fake.rawManifestMutex.RLock() 412 | defer fake.rawManifestMutex.RUnlock() 413 | return len(fake.rawManifestArgsForCall) 414 | } 415 | 416 | func (fake *FakeImageIndex) RawManifestCalls(stub func() ([]byte, error)) { 417 | fake.rawManifestMutex.Lock() 418 | defer fake.rawManifestMutex.Unlock() 419 | fake.RawManifestStub = stub 420 | } 421 | 422 | func (fake *FakeImageIndex) RawManifestReturns(result1 []byte, result2 error) { 423 | fake.rawManifestMutex.Lock() 424 | defer fake.rawManifestMutex.Unlock() 425 | fake.RawManifestStub = nil 426 | fake.rawManifestReturns = struct { 427 | result1 []byte 428 | result2 error 429 | }{result1, result2} 430 | } 431 | 432 | func (fake *FakeImageIndex) RawManifestReturnsOnCall(i int, result1 []byte, result2 error) { 433 | fake.rawManifestMutex.Lock() 434 | defer fake.rawManifestMutex.Unlock() 435 | fake.RawManifestStub = nil 436 | if fake.rawManifestReturnsOnCall == nil { 437 | fake.rawManifestReturnsOnCall = make(map[int]struct { 438 | result1 []byte 439 | result2 error 440 | }) 441 | } 442 | fake.rawManifestReturnsOnCall[i] = struct { 443 | result1 []byte 444 | result2 error 445 | }{result1, result2} 446 | } 447 | 448 | func (fake *FakeImageIndex) Size() (int64, error) { 449 | fake.sizeMutex.Lock() 450 | ret, specificReturn := fake.sizeReturnsOnCall[len(fake.sizeArgsForCall)] 451 | fake.sizeArgsForCall = append(fake.sizeArgsForCall, struct { 452 | }{}) 453 | fake.recordInvocation("Size", []interface{}{}) 454 | fake.sizeMutex.Unlock() 455 | if fake.SizeStub != nil { 456 | return fake.SizeStub() 457 | } 458 | if specificReturn { 459 | return ret.result1, ret.result2 460 | } 461 | fakeReturns := fake.sizeReturns 462 | return fakeReturns.result1, fakeReturns.result2 463 | } 464 | 465 | func (fake *FakeImageIndex) SizeCallCount() int { 466 | fake.sizeMutex.RLock() 467 | defer fake.sizeMutex.RUnlock() 468 | return len(fake.sizeArgsForCall) 469 | } 470 | 471 | func (fake *FakeImageIndex) SizeCalls(stub func() (int64, error)) { 472 | fake.sizeMutex.Lock() 473 | defer fake.sizeMutex.Unlock() 474 | fake.SizeStub = stub 475 | } 476 | 477 | func (fake *FakeImageIndex) SizeReturns(result1 int64, result2 error) { 478 | fake.sizeMutex.Lock() 479 | defer fake.sizeMutex.Unlock() 480 | fake.SizeStub = nil 481 | fake.sizeReturns = struct { 482 | result1 int64 483 | result2 error 484 | }{result1, result2} 485 | } 486 | 487 | func (fake *FakeImageIndex) SizeReturnsOnCall(i int, result1 int64, result2 error) { 488 | fake.sizeMutex.Lock() 489 | defer fake.sizeMutex.Unlock() 490 | fake.SizeStub = nil 491 | if fake.sizeReturnsOnCall == nil { 492 | fake.sizeReturnsOnCall = make(map[int]struct { 493 | result1 int64 494 | result2 error 495 | }) 496 | } 497 | fake.sizeReturnsOnCall[i] = struct { 498 | result1 int64 499 | result2 error 500 | }{result1, result2} 501 | } 502 | 503 | func (fake *FakeImageIndex) Invocations() map[string][][]interface{} { 504 | fake.invocationsMutex.RLock() 505 | defer fake.invocationsMutex.RUnlock() 506 | fake.digestMutex.RLock() 507 | defer fake.digestMutex.RUnlock() 508 | fake.imageMutex.RLock() 509 | defer fake.imageMutex.RUnlock() 510 | fake.imageIndexMutex.RLock() 511 | defer fake.imageIndexMutex.RUnlock() 512 | fake.indexManifestMutex.RLock() 513 | defer fake.indexManifestMutex.RUnlock() 514 | fake.mediaTypeMutex.RLock() 515 | defer fake.mediaTypeMutex.RUnlock() 516 | fake.rawManifestMutex.RLock() 517 | defer fake.rawManifestMutex.RUnlock() 518 | fake.sizeMutex.RLock() 519 | defer fake.sizeMutex.RUnlock() 520 | copiedInvocations := map[string][][]interface{}{} 521 | for key, value := range fake.invocations { 522 | copiedInvocations[key] = value 523 | } 524 | return copiedInvocations 525 | } 526 | 527 | func (fake *FakeImageIndex) recordInvocation(key string, args []interface{}) { 528 | fake.invocationsMutex.Lock() 529 | defer fake.invocationsMutex.Unlock() 530 | if fake.invocations == nil { 531 | fake.invocations = map[string][][]interface{}{} 532 | } 533 | if fake.invocations[key] == nil { 534 | fake.invocations[key] = [][]interface{}{} 535 | } 536 | fake.invocations[key] = append(fake.invocations[key], args) 537 | } 538 | 539 | var _ v1.ImageIndex = new(FakeImageIndex) 540 | -------------------------------------------------------------------------------- /pkg/registry/image.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package registry 18 | 19 | import ( 20 | "github.com/pivotal/image-relocation/pkg/image" 21 | ) 22 | 23 | // Image represents an abstract image which could be an image manifest or an image index (e.g. a multi-arch image). 24 | type Image interface { 25 | // Digest returns the repository digest of the image. 26 | Digest() (image.Digest, error) 27 | 28 | // Write writes the image to a given reference and returns the image's digest and size. 29 | Write(target image.Name) (image.Digest, int64, error) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/registry/layout.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package registry 18 | 19 | import ( 20 | "github.com/pivotal/image-relocation/pkg/image" 21 | ) 22 | 23 | // A Layout abstracts an OCI image layout on disk. 24 | type Layout interface { 25 | // Add adds the image at the given image reference to the layout and returns the image's digest. 26 | Add(n image.Name) (image.Digest, error) 27 | 28 | // Push pushes the image with the given digest from the layout to the given image reference. 29 | Push(digest image.Digest, name image.Name) error 30 | 31 | // Find returns the digest of an image in the layout with the given image reference. 32 | Find(n image.Name) (image.Digest, error) 33 | } 34 | 35 | -------------------------------------------------------------------------------- /pkg/registry/registryfakes/fake_image.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package registryfakes 3 | 4 | import ( 5 | sync "sync" 6 | 7 | image "github.com/pivotal/image-relocation/pkg/image" 8 | registry "github.com/pivotal/image-relocation/pkg/registry" 9 | ) 10 | 11 | type FakeImage struct { 12 | DigestStub func() (image.Digest, error) 13 | digestMutex sync.RWMutex 14 | digestArgsForCall []struct { 15 | } 16 | digestReturns struct { 17 | result1 image.Digest 18 | result2 error 19 | } 20 | digestReturnsOnCall map[int]struct { 21 | result1 image.Digest 22 | result2 error 23 | } 24 | WriteStub func(image.Name) (image.Digest, int64, error) 25 | writeMutex sync.RWMutex 26 | writeArgsForCall []struct { 27 | arg1 image.Name 28 | } 29 | writeReturns struct { 30 | result1 image.Digest 31 | result2 int64 32 | result3 error 33 | } 34 | writeReturnsOnCall map[int]struct { 35 | result1 image.Digest 36 | result2 int64 37 | result3 error 38 | } 39 | invocations map[string][][]interface{} 40 | invocationsMutex sync.RWMutex 41 | } 42 | 43 | func (fake *FakeImage) Digest() (image.Digest, error) { 44 | fake.digestMutex.Lock() 45 | ret, specificReturn := fake.digestReturnsOnCall[len(fake.digestArgsForCall)] 46 | fake.digestArgsForCall = append(fake.digestArgsForCall, struct { 47 | }{}) 48 | fake.recordInvocation("Digest", []interface{}{}) 49 | fake.digestMutex.Unlock() 50 | if fake.DigestStub != nil { 51 | return fake.DigestStub() 52 | } 53 | if specificReturn { 54 | return ret.result1, ret.result2 55 | } 56 | fakeReturns := fake.digestReturns 57 | return fakeReturns.result1, fakeReturns.result2 58 | } 59 | 60 | func (fake *FakeImage) DigestCallCount() int { 61 | fake.digestMutex.RLock() 62 | defer fake.digestMutex.RUnlock() 63 | return len(fake.digestArgsForCall) 64 | } 65 | 66 | func (fake *FakeImage) DigestCalls(stub func() (image.Digest, error)) { 67 | fake.digestMutex.Lock() 68 | defer fake.digestMutex.Unlock() 69 | fake.DigestStub = stub 70 | } 71 | 72 | func (fake *FakeImage) DigestReturns(result1 image.Digest, result2 error) { 73 | fake.digestMutex.Lock() 74 | defer fake.digestMutex.Unlock() 75 | fake.DigestStub = nil 76 | fake.digestReturns = struct { 77 | result1 image.Digest 78 | result2 error 79 | }{result1, result2} 80 | } 81 | 82 | func (fake *FakeImage) DigestReturnsOnCall(i int, result1 image.Digest, result2 error) { 83 | fake.digestMutex.Lock() 84 | defer fake.digestMutex.Unlock() 85 | fake.DigestStub = nil 86 | if fake.digestReturnsOnCall == nil { 87 | fake.digestReturnsOnCall = make(map[int]struct { 88 | result1 image.Digest 89 | result2 error 90 | }) 91 | } 92 | fake.digestReturnsOnCall[i] = struct { 93 | result1 image.Digest 94 | result2 error 95 | }{result1, result2} 96 | } 97 | 98 | func (fake *FakeImage) Write(arg1 image.Name) (image.Digest, int64, error) { 99 | fake.writeMutex.Lock() 100 | ret, specificReturn := fake.writeReturnsOnCall[len(fake.writeArgsForCall)] 101 | fake.writeArgsForCall = append(fake.writeArgsForCall, struct { 102 | arg1 image.Name 103 | }{arg1}) 104 | fake.recordInvocation("Write", []interface{}{arg1}) 105 | fake.writeMutex.Unlock() 106 | if fake.WriteStub != nil { 107 | return fake.WriteStub(arg1) 108 | } 109 | if specificReturn { 110 | return ret.result1, ret.result2, ret.result3 111 | } 112 | fakeReturns := fake.writeReturns 113 | return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 114 | } 115 | 116 | func (fake *FakeImage) WriteCallCount() int { 117 | fake.writeMutex.RLock() 118 | defer fake.writeMutex.RUnlock() 119 | return len(fake.writeArgsForCall) 120 | } 121 | 122 | func (fake *FakeImage) WriteCalls(stub func(image.Name) (image.Digest, int64, error)) { 123 | fake.writeMutex.Lock() 124 | defer fake.writeMutex.Unlock() 125 | fake.WriteStub = stub 126 | } 127 | 128 | func (fake *FakeImage) WriteArgsForCall(i int) image.Name { 129 | fake.writeMutex.RLock() 130 | defer fake.writeMutex.RUnlock() 131 | argsForCall := fake.writeArgsForCall[i] 132 | return argsForCall.arg1 133 | } 134 | 135 | func (fake *FakeImage) WriteReturns(result1 image.Digest, result2 int64, result3 error) { 136 | fake.writeMutex.Lock() 137 | defer fake.writeMutex.Unlock() 138 | fake.WriteStub = nil 139 | fake.writeReturns = struct { 140 | result1 image.Digest 141 | result2 int64 142 | result3 error 143 | }{result1, result2, result3} 144 | } 145 | 146 | func (fake *FakeImage) WriteReturnsOnCall(i int, result1 image.Digest, result2 int64, result3 error) { 147 | fake.writeMutex.Lock() 148 | defer fake.writeMutex.Unlock() 149 | fake.WriteStub = nil 150 | if fake.writeReturnsOnCall == nil { 151 | fake.writeReturnsOnCall = make(map[int]struct { 152 | result1 image.Digest 153 | result2 int64 154 | result3 error 155 | }) 156 | } 157 | fake.writeReturnsOnCall[i] = struct { 158 | result1 image.Digest 159 | result2 int64 160 | result3 error 161 | }{result1, result2, result3} 162 | } 163 | 164 | func (fake *FakeImage) Invocations() map[string][][]interface{} { 165 | fake.invocationsMutex.RLock() 166 | defer fake.invocationsMutex.RUnlock() 167 | fake.digestMutex.RLock() 168 | defer fake.digestMutex.RUnlock() 169 | fake.writeMutex.RLock() 170 | defer fake.writeMutex.RUnlock() 171 | copiedInvocations := map[string][][]interface{}{} 172 | for key, value := range fake.invocations { 173 | copiedInvocations[key] = value 174 | } 175 | return copiedInvocations 176 | } 177 | 178 | func (fake *FakeImage) recordInvocation(key string, args []interface{}) { 179 | fake.invocationsMutex.Lock() 180 | defer fake.invocationsMutex.Unlock() 181 | if fake.invocations == nil { 182 | fake.invocations = map[string][][]interface{}{} 183 | } 184 | if fake.invocations[key] == nil { 185 | fake.invocations[key] = [][]interface{}{} 186 | } 187 | fake.invocations[key] = append(fake.invocations[key], args) 188 | } 189 | 190 | var _ registry.Image = new(FakeImage) 191 | -------------------------------------------------------------------------------- /pkg/transport/http.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package transport 18 | 19 | import ( 20 | "crypto/tls" 21 | "crypto/x509" 22 | "fmt" 23 | "io/ioutil" 24 | "net/http" 25 | ) 26 | 27 | func NewHttpTransport(certs []string, insecureSkipVerify bool) (*http.Transport, error) { 28 | transport := http.DefaultTransport.(*http.Transport) 29 | 30 | if len(certs) > 0 || insecureSkipVerify { 31 | transport.TLSClientConfig = &tls.Config{ 32 | InsecureSkipVerify: insecureSkipVerify, 33 | } 34 | 35 | pool, err := x509.SystemCertPool() 36 | if err != nil { 37 | pool = x509.NewCertPool() 38 | } 39 | 40 | for _, path := range certs { 41 | pem, err := ioutil.ReadFile(path) 42 | if err != nil { 43 | return nil, fmt.Errorf("could not read certificates from %q: %v", path, err) 44 | } 45 | 46 | if ok := pool.AppendCertsFromPEM(pem); !ok { 47 | return nil, fmt.Errorf("could not append %q to certificate pool", path) 48 | } 49 | } 50 | 51 | transport.TLSClientConfig.RootCAs = pool 52 | } 53 | 54 | return transport, nil 55 | } 56 | -------------------------------------------------------------------------------- /pkg/transport/http_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package transport_test 18 | 19 | import ( 20 | "net/http" 21 | "strings" 22 | 23 | . "github.com/onsi/ginkgo" 24 | . "github.com/onsi/gomega" 25 | 26 | "github.com/pivotal/image-relocation/pkg/transport" 27 | ) 28 | 29 | var _ = Describe("NewHttpTransport", func() { 30 | var ( 31 | certs []string 32 | insecureSkipVerify bool 33 | httpTransport *http.Transport 34 | err error 35 | ) 36 | 37 | BeforeEach(func() { 38 | certs = []string{} 39 | insecureSkipVerify = false 40 | }) 41 | 42 | JustBeforeEach(func() { 43 | httpTransport, err = transport.NewHttpTransport(certs, insecureSkipVerify) 44 | }) 45 | 46 | Context("when skipping TLS certificate verification is set to false", func() { 47 | BeforeEach(func() { 48 | insecureSkipVerify = false 49 | }) 50 | 51 | It("should not skip TLS verification", func() { 52 | Expect(err).NotTo(HaveOccurred()) 53 | if httpTransport.TLSClientConfig != nil { 54 | Expect(httpTransport.TLSClientConfig.InsecureSkipVerify).To(BeFalse()) 55 | } 56 | }) 57 | 58 | Context("when CA certs are provided", func() { 59 | BeforeEach(func() { 60 | certs = []string{"testdata/ca.crt"} 61 | }) 62 | 63 | It("should not skip TLS verification", func() { 64 | Expect(err).NotTo(HaveOccurred()) 65 | Expect(httpTransport.TLSClientConfig.InsecureSkipVerify).To(BeFalse()) 66 | }) 67 | 68 | It("should use the provided CA certs", func() { 69 | // Check there is a subject with organization ACME 70 | Expect(findSubject(httpTransport.TLSClientConfig.RootCAs.Subjects(), "ACME")).To(BeTrue()) 71 | }) 72 | }) 73 | 74 | Context("when an empty CA cert is provided", func() { 75 | BeforeEach(func() { 76 | certs = []string{"testdata/empty.crt"} 77 | }) 78 | 79 | It("should return a suitable error", func() { 80 | Expect(err).To(MatchError(`could not append "testdata/empty.crt" to certificate pool`)) 81 | }) 82 | }) 83 | 84 | Context("when a non-existent CA cert is provided", func() { 85 | BeforeEach(func() { 86 | certs = []string{"testdata/nosuch.crt"} 87 | }) 88 | 89 | It("should return a suitable error", func() { 90 | Expect(err).To(MatchError(HavePrefix(`could not read certificates from "testdata/nosuch.crt":`))) 91 | }) 92 | }) 93 | }) 94 | 95 | Context("when skipping TLS certificate verification is set to true", func() { 96 | BeforeEach(func() { 97 | insecureSkipVerify = true 98 | }) 99 | 100 | It("should skip TLS verification", func() { 101 | Expect(err).NotTo(HaveOccurred()) 102 | Expect(httpTransport.TLSClientConfig.InsecureSkipVerify).To(BeTrue()) 103 | }) 104 | 105 | Context("when CA certs are also provided", func() { 106 | BeforeEach(func() { 107 | certs = []string{"testdata/ca.crt"} 108 | }) 109 | 110 | It("should still skip TLS verification", func() { 111 | Expect(err).NotTo(HaveOccurred()) 112 | Expect(httpTransport.TLSClientConfig.InsecureSkipVerify).To(BeTrue()) 113 | }) 114 | }) 115 | }) 116 | }) 117 | 118 | func findSubject(subjects [][]byte, org string) bool { 119 | found := false 120 | for _, subject := range subjects { 121 | if strings.Contains(string(subject), org) { 122 | found = true 123 | break 124 | } 125 | } 126 | return found 127 | } 128 | -------------------------------------------------------------------------------- /pkg/transport/testdata/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC0jCCAboCCQDRwW9gBuYHmDANBgkqhkiG9w0BAQsFADAqMQwwCgYDVQQDDANj 3 | YTExDTALBgNVBAoMBEFDTUUxCzAJBgNVBAYTAlhZMCAXDTE5MTEwNTEzNDM0OFoY 4 | DzQ3NTcxMDAyMTM0MzQ4WjAqMQwwCgYDVQQDDANjYTExDTALBgNVBAoMBEFDTUUx 5 | CzAJBgNVBAYTAlhZMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzCMw 6 | 15RQ+ucRkCz4LL7WEz9IPGv/SNm1eTKK4js1g11ACKBisVZpwb64H1SxiSJwXJjT 7 | 5q5qJzIo1NYU8nOixo+kz8XHCPBhWqK/K+kuEi+T421Q75/i+DCtybnLn8+X258a 8 | dA/ig3c8hDrsaK7A58cOs+ss2uvdPzrXedDf2wpo09pWB3CLpxONVu3nUJDT7W7P 9 | YUZ0+WCKCe6eyFVy22mo+I4dk8CnDKmavETM/pERdieT9kHrR1fgru35SGQIj6/J 10 | vcx1nN4HV62xxjPezpmqIleOZbeCIstiz6rOwjWqvqjUR2JBa9EleDv8oQhamJiF 11 | wW/S6yiCRkZkwxmQJwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAwt+0wVjFnF38i 12 | n4vKOn1SaGDKVhwsHoIY7INQqTXXULE2pQgmGB5LmCUVSguyxfwMoKyuVo/jcden 13 | ZKFYlJC8UZcqPzMwG8CqAvDA8upDSbVGQt3/qji7UfPb5oQn3zt7K/BG6P2YxFo4 14 | o2aGgzxPv4z4sYb2+83/KVPaDR+hcPpQbJZ8p1H8bfhk6sCBlDwXrMazr0DzQ8+8 15 | G6K6XWIcqTd3Ebgmw3mg7Zzce4Csf//XGcBgiiCsw960XeTzSlRDD1yaJjVY/+Ln 16 | te6r0bJsaI6LvZ0ORUSCq77wqOF4P5fXIAVH3VS3uKM+hwkkXYM+FTJZx6Wx++3H 17 | BfPgvLmn 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /pkg/transport/testdata/empty.crt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/image-relocation/4cc801b4f8e789031afc5549d78ceb7c443c39e3/pkg/transport/testdata/empty.crt -------------------------------------------------------------------------------- /pkg/transport/transport_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019-Present Pivotal Software, Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package transport_test 18 | 19 | import ( 20 | "testing" 21 | 22 | . "github.com/onsi/ginkgo" 23 | . "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestTransport(t *testing.T) { 27 | RegisterFailHandler(Fail) 28 | RunSpecs(t, "Transport Suite") 29 | } 30 | --------------------------------------------------------------------------------