├── .dockerignore ├── .github └── FUNDING.yml ├── .gitignore ├── CODEOWNERS ├── Dockerfile ├── LICENSE ├── NOTICE.md ├── README.md ├── assets ├── common.sh ├── in └── out ├── ci ├── build-smoke-test-image.yml └── smoke-test.yml ├── cmd ├── check │ ├── main.go │ └── models.go └── print-metadata │ ├── main.go │ ├── main_test.go │ ├── passwd │ ├── passwd.go │ ├── passwd_suite_test.go │ └── passwd_test.go │ └── suite_test.go ├── go.mod ├── go.sum ├── scripts └── tests └── tests ├── check_test.go ├── docker_image_resource_suite_test.go ├── fixtures ├── bin │ ├── docker │ └── dockerd ├── build │ └── Dockerfile ├── build_args ├── build_args_malformed ├── ecr │ ├── Dockerfile │ ├── Dockerfile.multi │ └── Dockerfile.multi-ecr ├── tag └── tags └── out_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !go.mod 3 | !go.sum 4 | !/assets 5 | !/cmd 6 | !/scripts 7 | !/tests 8 | !/LICENSE 9 | !/NOTICE.md 10 | !/README.md 11 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [taylorsilva] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | assets/check 2 | assets/print-metadata 3 | tar-src/ 4 | *.test 5 | .idea 6 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @xtremerui 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG base_image=cgr.dev/chainguard/wolfi-base 2 | ARG builder_image=concourse/golang-builder 3 | 4 | ARG BUILDPLATFORM 5 | FROM --platform=${BUILDPLATFORM} ${builder_image} AS builder 6 | 7 | ARG TARGETOS 8 | ARG TARGETARCH 9 | ENV GOOS=$TARGETOS 10 | ENV GOARCH=$TARGETARCH 11 | 12 | WORKDIR /src 13 | COPY . /src 14 | ENV CGO_ENABLED=0 15 | RUN go mod download 16 | COPY assets/ /assets 17 | RUN go build -o /assets/check ./cmd/check 18 | RUN go build -o /assets/print-metadata ./cmd/print-metadata 19 | RUN go build -o /assets/ecr-login github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login 20 | RUN set -e; \ 21 | for pkg in $(go list ./...); do \ 22 | go test -o "/tests/$(basename $pkg).test" -c $pkg; \ 23 | done 24 | 25 | FROM ${base_image} AS resource 26 | RUN apk --no-cache add \ 27 | docker \ 28 | docker-cli-buildx \ 29 | jq \ 30 | ca-certificates \ 31 | xz \ 32 | iproute2 \ 33 | mount \ 34 | umount \ 35 | cmd:tar \ 36 | sed 37 | 38 | COPY --from=builder /assets /opt/resource 39 | RUN mkdir /usr/local/bin && ln -s /opt/resource/ecr-login /usr/local/bin/docker-credential-ecr-login 40 | 41 | FROM resource AS tests 42 | COPY --from=builder /tests /tests 43 | ADD . /docker-image-resource 44 | RUN set -e; \ 45 | for test in /tests/*.test; do \ 46 | $test -ginkgo.v; \ 47 | done 48 | 49 | FROM resource 50 | -------------------------------------------------------------------------------- /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 | 203 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | Copyright 2014-2016 Alex Suraci, Chris Brown, and Pivotal Software, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | this file except in compliance with the License. You may obtain a copy of the 5 | 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 distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Image Resource 2 | 3 | Tracks and builds [Docker](https://docker.io) images. 4 | 5 | 6 | Build Status 7 | 8 | 9 | Note: docker registry must be [v2](https://docs.docker.com/registry/spec/api/). 10 | 11 | ## Source Configuration 12 | 13 | * `repository`: *Required.* The name of the repository, e.g. 14 | `concourse/docker-image-resource`. 15 | 16 | Note: When configuring a private registry which requires a login, the 17 | registry's address must contain at least one '.' e.g. `registry.local` 18 | or contain the port (e.g. `registry:443` or `registry:5000`). 19 | Otherwise docker hub will be used. 20 | 21 | Note: When configuring a private registry **using a non-root CA**, 22 | you must include the port (e.g. :443 or :5000) even though the docker CLI 23 | does not require it. 24 | 25 | * `tag`: *Optional.* The tag to track. Defaults to `latest`. 26 | 27 | * `username`: *Optional.* The username to authenticate with when pushing. 28 | 29 | * `password`: *Optional.* The password to use when authenticating. 30 | 31 | * `additional_private_registries`: *Optional.* An array of objects with the 32 | following format: 33 | 34 | ```yaml 35 | additional_private_registries: 36 | - registry: example.com/my-private-docker-registry 37 | username: my-username 38 | password: ((my-secret:my-secret)) 39 | - registry: example.com/another-private-docker-registry 40 | username: another-username 41 | password: ((another-secret:another-secret)) 42 | ``` 43 | 44 | Each entry specifies a private docker registry and credentials to be passed 45 | to `docker login`. This is used when a Dockerfile contains a FROM instruction 46 | referring to an image hosted in a docker registry that requires a login. 47 | 48 | * `aws_access_key_id`: *Optional.* AWS access key to use for acquiring ECR 49 | credentials. 50 | 51 | * `aws_secret_access_key`: *Optional.* AWS secret key to use for acquiring ECR 52 | credentials. 53 | 54 | * `aws_session_token`: *Optional.* AWS session token (assumed role) to use for acquiring ECR 55 | credentials. 56 | 57 | * `insecure_registries`: *Optional.* An array of CIDRs or `host:port` addresses 58 | to whitelist for insecure access (either `http` or unverified `https`). 59 | This option overrides any entries in `ca_certs` with the same address. 60 | 61 | * `registry_mirror`: *Optional.* A URL pointing to a docker registry mirror service. 62 | 63 | Note: `registry_mirror` is ignored if `repository` contains an explicitly-declared 64 | registry-hostname-prefixed value, such as `my-registry.com/foo/bar`, in which case 65 | the registry cited in the `repository` value is used instead of the `registry_mirror`. 66 | 67 | * `ca_certs`: *Optional.* An array of objects with the following format: 68 | 69 | ```yaml 70 | ca_certs: 71 | - domain: example.com:443 72 | cert: | 73 | -----BEGIN CERTIFICATE----- 74 | ... 75 | -----END CERTIFICATE----- 76 | - domain: 10.244.6.2:443 77 | cert: | 78 | -----BEGIN CERTIFICATE----- 79 | ... 80 | -----END CERTIFICATE----- 81 | ``` 82 | 83 | Each entry specifies the x509 CA certificate for the trusted docker registry 84 | residing at the specified domain. This is used to validate the certificate of 85 | the docker registry when the registry's certificate is signed by a custom 86 | authority (or itself). 87 | 88 | The domain should match the first component of `repository`, including the 89 | port. If the registry specified in `repository` does not use a custom cert, 90 | adding `ca_certs` will break the check script. This option is overridden by 91 | entries in `insecure_registries` with the same address or a matching CIDR. 92 | 93 | * `client_certs`: *Optional.* An array of objects with the following format: 94 | 95 | ```yaml 96 | client_certs: 97 | - domain: example.com 98 | cert: | 99 | -----BEGIN CERTIFICATE----- 100 | ... 101 | -----END CERTIFICATE----- 102 | key: | 103 | -----BEGIN RSA PRIVATE KEY----- 104 | ... 105 | -----END RSA PRIVATE KEY----- 106 | - domain: 10.244.6.2 107 | cert: | 108 | -----BEGIN CERTIFICATE----- 109 | ... 110 | -----END CERTIFICATE----- 111 | key: | 112 | -----BEGIN RSA PRIVATE KEY----- 113 | ... 114 | -----END RSA PRIVATE KEY----- 115 | ``` 116 | 117 | Each entry specifies the x509 certificate and key to use for authenticating 118 | against the docker registry residing at the specified domain. The domain 119 | should match the first component of `repository`. 120 | 121 | * `max_concurrent_downloads`: *Optional.* Maximum concurrent downloads. 122 | 123 | Limits the number of concurrent download threads. 124 | 125 | * `max_concurrent_uploads`: *Optional.* Maximum concurrent uploads. 126 | 127 | Limits the number of concurrent upload threads. 128 | 129 | ## Behavior 130 | 131 | ### `check`: Check for new images. 132 | 133 | The current image digest is fetched from the registry for the given tag of the 134 | repository. 135 | 136 | 137 | ### `in`: Fetch the image from the registry. 138 | 139 | Pulls down the repository image by the requested digest. 140 | 141 | The following files will be placed in the destination: 142 | 143 | * `/image`: If `save` is `true`, the `docker save`d image will be provided 144 | here. 145 | * `/repository`: The name of the repository that was fetched. 146 | * `/tag`: The tag of the repository that was fetched. 147 | * `/image-id`: The fetched image ID. 148 | * `/digest`: The fetched image digest. 149 | * `/rootfs.tar`: If `rootfs` is `true`, the contents of the image will be 150 | provided here. 151 | * `/metadata.json`: Collects custom metadata. Contains the container `env` variables and running `user`. 152 | * `/docker_inspect.json`: Output of the `docker inspect` on `image_id`. Useful if collecting `LABEL` [metadata](https://docs.docker.com/engine/userguide/labels-custom-metadata/) from your image. 153 | 154 | #### Parameters 155 | 156 | * `save`: *Optional.* Place a `docker save`d image in the destination. 157 | * `rootfs`: *Optional.* Place a `.tar` file of the image in the destination. 158 | * `skip_download`: *Optional.* Skip `docker pull` of image. Artifacts based 159 | on the image will not be present. 160 | 161 | As with all concourse resources, to modify params of the implicit `get` step after each `put` step you may also set these parameters under a `put` `get_params`. For example: 162 | 163 | ```yaml 164 | put: foo 165 | params: {...} 166 | get_params: {skip_download: true} 167 | ``` 168 | 169 | ### `out`: Push an image, or build and push a `Dockerfile`. 170 | 171 | Push a Docker image to the source's repository and tag. The resulting 172 | version is the image's digest. 173 | 174 | #### Parameters 175 | 176 | * `additional_tags`: *Optional.* Path to a file containing a 177 | whitespace-separated list of tags. The Docker build will additionally be 178 | pushed with those tags. 179 | 180 | * `build`: *Optional.* The path of a directory containing a `Dockerfile` to 181 | build. 182 | 183 | * `build_args`: *Optional.* A map of Docker build-time variables. These will be 184 | available as environment variables during the Docker build. 185 | 186 | While not stored in the image layers, they are stored in image metadata and 187 | so it is recommend to avoid using these to pass secrets into the build 188 | context. In multi-stage builds `ARG`s in earlier stages will not be copied 189 | to the later stages, or in the metadata of the final stage. 190 | 191 | The 192 | [build metadata](https://concourse-ci.org/implementing-resource-types.html#resource-metadata) 193 | environment variables provided by Concourse will be expanded in the values 194 | (the syntax is `$SOME_ENVVAR` or `${SOME_ENVVAR}`). 195 | 196 | Example: 197 | 198 | ```yaml 199 | build_args: 200 | DO_THING: true 201 | HOW_MANY_THINGS: 2 202 | EMAIL: me@yopmail.com 203 | CI_BUILD_ID: concourse-$BUILD_ID 204 | ``` 205 | 206 | * `build_args_file`: *Optional.* Path to a JSON file containing Docker 207 | build-time variables. 208 | 209 | Example file contents: 210 | 211 | ```yaml 212 | { "EMAIL": "me@yopmail.com", "HOW_MANY_THINGS": 1, "DO_THING": false } 213 | ``` 214 | 215 | * `secrets`: *Optional.* A map of Docker build-time secrets. These will be 216 | available as mounted paths only during the docker build phase. 217 | 218 | Secrets are not stored in any metadata or layers, so they are safe to use for 219 | access tokens and the like during the build. 220 | 221 | Example: 222 | 223 | ```yaml 224 | secrets: 225 | secret1: 226 | env: BUILD_ID 227 | secret2: 228 | source: /a/secret/file.txt 229 | ``` 230 | 231 | * `cache`: *Optional.* Default `false`. When the `build` parameter is set, 232 | first pull `image:tag` from the Docker registry (so as to use cached 233 | intermediate images when building). This will cause the resource to fail 234 | if it is set to `true` and the image does not exist yet. 235 | 236 | * `cache_from`: *Optional.* An array of images to consider as cache, in order to 237 | reuse build steps from a previous build. The array elements are paths to 238 | directories generated by a `get` step with `save: true`. This has a similar 239 | aim of `cache`, but it loads the images from disk instead of pulling them 240 | from the network, so that Concourse resource caching can be used. It also 241 | allows more than one image to be specified, which is useful for multi-stage 242 | Dockerfiles. If you want to cache an image used in a `FROM` step, you should 243 | put it in `load_bases` instead. 244 | 245 | * `cache_tag`: *Optional.* Default `tag`. The specific tag to pull before 246 | building when `cache` parameter is set. Instead of pulling the same tag 247 | that's going to be built, this allows picking a different tag like 248 | `latest` or the previous version. This will cause the resource to fail 249 | if it is set to a tag that does not exist yet. 250 | 251 | * `dockerfile`: *Optional.* The path of the `Dockerfile` in the directory if 252 | it's not at the root of the directory. 253 | 254 | * `docker_buildkit`: *Optional.* This enables a Docker BuildKit build. The value 255 | should be set to 1 if applicable. 256 | 257 | * `import_file`: *Optional.* A path to a file to `docker import` and then push. 258 | 259 | * `labels`: *Optional.* A map of labels that will be added to the image. 260 | 261 | Example: 262 | 263 | ```yaml 264 | labels: 265 | commit: b4d4823 266 | version: 1.0.3 267 | ``` 268 | 269 | * `labels_file`: *Optional.* Path to a JSON file containing the image labels. 270 | 271 | Example file contents: 272 | 273 | ```json 274 | { "commit": "b4d4823", "version": "1.0.3" } 275 | ``` 276 | 277 | * `load`: *Optional.* The path of a directory containing an image that was 278 | fetched using this same resource type with `save: true`. 279 | 280 | * `load_base`: *Optional.* A path to a directory containing an image to `docker 281 | load` before running `docker build`. The directory must have `image`, 282 | `image-id`, `repository`, and `tag` present, i.e. the tree produced by `/in`. 283 | 284 | * `load_bases`: *Optional.* Same as `load_base`, but takes an array to load 285 | multiple images. 286 | 287 | * `load_file`: *Optional.* A path to a file to `docker load` and then push. 288 | Requires `load_repository`. 289 | 290 | * `load_repository`: *Optional.* The repository of the image loaded from `load_file`. 291 | 292 | * `load_tag`: *Optional.* Default `latest`. The tag of image loaded from `load_file` 293 | 294 | * `pull_repository`: *Optional.* **DEPRECATED. Use `get` and `load` instead.** A 295 | path to a repository to pull down, and then push to this resource. 296 | 297 | * `pull_tag`: *Optional.* **DEPRECATED. Use `get` and `load` instead.** Default 298 | `latest`. The tag of the repository to pull down via `pull_repository`. 299 | 300 | * `tag`: **DEPRECATED - Use `tag_file` instead** 301 | * `tag_file`: *Optional.* The value should be a path to a file containing the name 302 | of the tag. When not set, the Docker build will be pushed with tag value set by 303 | `tag` in source configuration. 304 | 305 | * `tag_as_latest`: *Optional.* Default `false`. If true, the pushed image will 306 | be tagged as `latest` in addition to whatever other tag was specified. 307 | 308 | * `tag_prefix`: *Optional.* If specified, the tag read from the file will be 309 | prepended with this string. This is useful for adding `v` in front of version 310 | numbers. 311 | 312 | * `target_name`: *Optional.* Specify the name of the target build stage. 313 | Only supported for multi-stage Docker builds 314 | 315 | 316 | ## Example 317 | 318 | ``` yaml 319 | resources: 320 | - name: git-resource 321 | type: git 322 | source: # ... 323 | 324 | - name: git-resource-image 325 | type: docker-image 326 | source: 327 | repository: concourse/git-resource 328 | username: username 329 | password: password 330 | 331 | - name: git-resource-rootfs 332 | type: s3 333 | source: # ... 334 | 335 | jobs: 336 | - name: build-rootfs 337 | plan: 338 | - get: git-resource 339 | - put: git-resource-image 340 | params: {build: git-resource} 341 | get_params: {rootfs: true} 342 | - put: git-resource-rootfs 343 | params: {file: git-resource-image/rootfs.tar} 344 | ``` 345 | 346 | ## Development 347 | 348 | ### Prerequisites 349 | 350 | * golang is *required* - version 1.9.x is tested; earlier versions may also 351 | work. 352 | * docker is *required* - version 17.06.x is tested; earlier versions may also 353 | work. 354 | 355 | ### Running the tests 356 | 357 | The tests have been embedded with the `Dockerfile`; ensuring that the testing 358 | environment is consistent across any `docker` enabled platform. When the docker 359 | image builds, the test are run inside the docker container, on failure they 360 | will stop the build. 361 | 362 | Run the tests with the following command: 363 | 364 | ```sh 365 | docker build -t docker-image-resource . 366 | ``` 367 | 368 | To use the newly built image, push it to a docker registry that's accessible to 369 | Concourse and configure your pipeline to use it: 370 | 371 | ```yaml 372 | resource_types: 373 | - name: docker-image-resource 374 | type: docker-image 375 | privileged: true 376 | source: 377 | repository: example.com:5000/docker-image-resource 378 | tag: latest 379 | 380 | resources: 381 | - name: some-image 382 | type: docker-image-resource 383 | ... 384 | ``` 385 | 386 | ### Contributing 387 | 388 | Please make all pull requests to the `master` branch and ensure tests pass 389 | locally. 390 | -------------------------------------------------------------------------------- /assets/common.sh: -------------------------------------------------------------------------------- 1 | LOG_FILE=${LOG_FILE:-/tmp/docker.log} 2 | SKIP_PRIVILEGED=${SKIP_PRIVILEGED:-false} 3 | STARTUP_TIMEOUT=${STARTUP_TIMEOUT:-120} 4 | 5 | sanitize_cgroups() { 6 | if [ -e /sys/fs/cgroup/cgroup.controllers ]; then 7 | return 8 | fi 9 | 10 | mkdir -p /sys/fs/cgroup 11 | mountpoint -q /sys/fs/cgroup || \ 12 | mount -t tmpfs -o uid=0,gid=0,mode=0755 cgroup /sys/fs/cgroup 13 | 14 | mount -o remount,rw /sys/fs/cgroup 15 | 16 | sed -e 1d /proc/cgroups | while read sys hierarchy num enabled; do 17 | if [ "$enabled" != "1" ]; then 18 | # subsystem disabled; skip 19 | continue 20 | fi 21 | 22 | grouping="$(cat /proc/self/cgroup | cut -d: -f2 | grep "\\<$sys\\>")" || true 23 | if [ -z "$grouping" ]; then 24 | # subsystem not mounted anywhere; mount it on its own 25 | grouping="$sys" 26 | fi 27 | 28 | mountpoint="/sys/fs/cgroup/$grouping" 29 | 30 | mkdir -p "$mountpoint" 31 | 32 | # clear out existing mount to make sure new one is read-write 33 | if mountpoint -q "$mountpoint"; then 34 | umount "$mountpoint" 35 | fi 36 | 37 | mount -n -t cgroup -o "$grouping" cgroup "$mountpoint" 38 | 39 | if [ "$grouping" != "$sys" ]; then 40 | if [ -L "/sys/fs/cgroup/$sys" ]; then 41 | rm "/sys/fs/cgroup/$sys" 42 | fi 43 | 44 | ln -s "$mountpoint" "/sys/fs/cgroup/$sys" 45 | fi 46 | done 47 | 48 | if [ ! -e /sys/fs/cgroup/systemd ] && [ $(cat /proc/self/cgroup | grep '^1:name=openrc:' | wc -l) -eq 0 ]; then 49 | mkdir /sys/fs/cgroup/systemd 50 | mount -t cgroup -o none,name=systemd none /sys/fs/cgroup/systemd 51 | fi 52 | } 53 | 54 | start_docker() { 55 | mkdir -p /var/log 56 | mkdir -p /var/run 57 | 58 | if [ "$SKIP_PRIVILEGED" = "false" ]; then 59 | sanitize_cgroups 60 | 61 | # check for /proc/sys being mounted readonly, as systemd does 62 | if grep '/proc/sys\s\+\w\+\s\+ro,' /proc/mounts >/dev/null; then 63 | mount -o remount,rw /proc/sys 64 | fi 65 | fi 66 | 67 | local mtu=$(cat /sys/class/net/$(ip route get 8.8.8.8|awk '{ print $5 }')/mtu) 68 | local server_args="--mtu ${mtu}" 69 | local registry="" 70 | 71 | server_args="${server_args} --max-concurrent-downloads=$1 --max-concurrent-uploads=$2" 72 | 73 | for registry in $3; do 74 | server_args="${server_args} --insecure-registry ${registry}" 75 | done 76 | 77 | if [ -n "$4" ]; then 78 | server_args="${server_args} --registry-mirror $4" 79 | fi 80 | 81 | try_start() { 82 | dockerd --data-root /scratch/docker ${server_args} >$LOG_FILE 2>&1 & 83 | echo $! > /tmp/docker.pid 84 | 85 | sleep 1 86 | 87 | echo waiting for docker to come up... 88 | until docker info >/dev/null 2>&1; do 89 | sleep 1 90 | if ! kill -0 "$(cat /tmp/docker.pid)" 2>/dev/null; then 91 | return 1 92 | fi 93 | done 94 | } 95 | 96 | export server_args LOG_FILE 97 | declare -fx try_start 98 | trap stop_docker EXIT 99 | 100 | if ! timeout ${STARTUP_TIMEOUT} bash -ce 'while true; do try_start && break; done'; then 101 | [ -f "$LOG_FILE" ] && cat "${LOG_FILE}" 102 | echo Docker failed to start within ${STARTUP_TIMEOUT} seconds. 103 | return 1 104 | fi 105 | } 106 | 107 | stop_docker() { 108 | local pid=$(cat /tmp/docker.pid) 109 | if [ -z "$pid" ]; then 110 | return 0 111 | fi 112 | 113 | kill -TERM $pid 114 | } 115 | 116 | log_in() { 117 | local username="$1" 118 | local password="$2" 119 | local registry="$3" 120 | 121 | if [ -n "${username}" ] && [ -n "${password}" ]; then 122 | echo "${password}" | docker login -u "${username}" --password-stdin ${registry} 123 | else 124 | mkdir -p ~/.docker 125 | touch ~/.docker/config.json 126 | # This ensures the resulting JSON object remains syntactically valid 127 | echo "$(cat ~/.docker/config.json){\"credsStore\":\"ecr-login\"}" | jq -s add > ~/.docker/config.json 128 | fi 129 | } 130 | 131 | private_registry() { 132 | local repository="${1}" 133 | 134 | local registry="$(extract_registry "${repository}")" 135 | if echo "${registry}" | grep -q -x '.*[.:].*' ; then 136 | return 0 137 | fi 138 | 139 | return 1 140 | } 141 | 142 | extract_registry() { 143 | local repository="${1}" 144 | 145 | echo "${repository}" | cut -d/ -f1 146 | } 147 | 148 | extract_repository() { 149 | local long_repository="${1}" 150 | 151 | echo "${long_repository}" | cut -d/ -f2- 152 | } 153 | 154 | image_from_tag() { 155 | docker images --no-trunc "$1" | awk "{if (\$2 == \"$2\") print \$3}" 156 | } 157 | 158 | image_from_digest() { 159 | docker images --no-trunc --digests "$1" | awk "{if (\$3 == \"$2\") print \$4}" 160 | } 161 | 162 | certs_to_file() { 163 | local raw_ca_certs="${1}" 164 | local cert_count="$(echo $raw_ca_certs | jq -r '. | length')" 165 | 166 | for i in $(seq 0 $(expr "$cert_count" - 1)); 167 | do 168 | local cert_dir="/etc/docker/certs.d/$(echo $raw_ca_certs | jq -r .[$i].domain)" 169 | mkdir -p "$cert_dir" 170 | echo $raw_ca_certs | jq -r .[$i].cert >> "${cert_dir}/ca.crt" 171 | done 172 | } 173 | 174 | set_client_certs() { 175 | local raw_client_certs="${1}" 176 | local cert_count="$(echo $raw_client_certs | jq -r '. | length')" 177 | 178 | for i in $(seq 0 $(expr "$cert_count" - 1)); 179 | do 180 | local cert_dir="/etc/docker/certs.d/$(echo $raw_client_certs | jq -r .[$i].domain)" 181 | [ -d "$cert_dir" ] || mkdir -p "$cert_dir" 182 | echo $raw_client_certs | jq -r .[$i].cert >> "${cert_dir}/client.cert" 183 | echo $raw_client_certs | jq -r .[$i].key >> "${cert_dir}/client.key" 184 | done 185 | } 186 | 187 | docker_pull() { 188 | GREEN='\033[0;32m' 189 | RED='\033[0;31m' 190 | NC='\033[0m' # No Color 191 | 192 | pull_attempt=1 193 | max_attempts=3 194 | while [ "$pull_attempt" -le "$max_attempts" ]; do 195 | printf "Pulling ${GREEN}%s${NC}" "$1" 196 | 197 | if [ "$pull_attempt" != "1" ]; then 198 | printf " (attempt %s of %s)" "$pull_attempt" "$max_attempts" 199 | fi 200 | 201 | printf "...\n" 202 | 203 | if docker pull "$1"; then 204 | printf "\nSuccessfully pulled ${GREEN}%s${NC}.\n\n" "$1" 205 | return 0 206 | fi 207 | 208 | echo 209 | 210 | pull_attempt=$(expr "$pull_attempt" + 1) 211 | done 212 | 213 | printf "\n${RED}Failed to pull image %s.${NC}" "$1" 214 | return 1 215 | } 216 | -------------------------------------------------------------------------------- /assets/in: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: set ft=sh 3 | 4 | set -e 5 | 6 | exec 3>&1 # make stdout available as fd 3 for the result 7 | exec 1>&2 # redirect all output to stderr for logging 8 | 9 | source $(dirname $0)/common.sh 10 | 11 | destination=$1 12 | 13 | if [ -z "$destination" ]; then 14 | echo "usage: $0 " >&2 15 | exit 1 16 | fi 17 | 18 | # for jq 19 | PATH=/usr/local/bin:$PATH 20 | 21 | payload=$(mktemp /tmp/resource-in.XXXXXX) 22 | 23 | cat > $payload <&0 24 | 25 | insecure_registries=$(jq -r '.source.insecure_registries // [] | join(" ")' < $payload) 26 | 27 | registry_mirror=$(jq -r '.source.registry_mirror // ""' < $payload) 28 | 29 | username=$(jq -r '.source.username // ""' < $payload) 30 | password=$(jq -r '.source.password // ""' < $payload) 31 | repository="$(jq -r '.source.repository // ""' < $payload)" 32 | tag="$(jq -r '.source.tag // "latest"' < $payload)" 33 | ca_certs=$(jq -r '.source.ca_certs // []' < $payload) 34 | client_certs=$(jq -r '.source.client_certs // []' < $payload) 35 | max_concurrent_downloads=$(jq -r '.source.max_concurrent_downloads // 3' < $payload) 36 | max_concurrent_uploads=$(jq -r '.source.max_concurrent_uploads // 3' < $payload) 37 | 38 | export AWS_ACCESS_KEY_ID=$(jq -r '.source.aws_access_key_id // ""' < $payload) 39 | export AWS_SECRET_ACCESS_KEY=$(jq -r '.source.aws_secret_access_key // ""' < $payload) 40 | export AWS_SESSION_TOKEN=$(jq -r '.source.aws_session_token // ""' < $payload) 41 | 42 | if private_registry "${repository}" ; then 43 | registry="$(extract_registry "${repository}")" 44 | else 45 | registry= 46 | fi 47 | 48 | digest="$(jq -r '.version.digest' < $payload)" 49 | 50 | rootfs="$(jq -r '.params.rootfs // false' < $payload)" 51 | skip_download="$(jq -r '.params.skip_download // false' < $payload)" 52 | save="$(jq -r '.params.save // false' < $payload)" 53 | 54 | mkdir -p $destination 55 | 56 | image_name="${repository}@${digest}" 57 | 58 | if [ "$skip_download" = "false" ]; then 59 | certs_to_file "$ca_certs" 60 | set_client_certs "$client_certs" 61 | start_docker \ 62 | "${max_concurrent_downloads}" \ 63 | "${max_concurrent_uploads}" \ 64 | "$insecure_registries" \ 65 | "$registry_mirror" 66 | 67 | log_in "$username" "$password" "$registry" 68 | 69 | docker_pull "$image_name" 70 | 71 | if [ "$save" = "true" ]; then 72 | docker save -o ${destination}/image "$image_name" 73 | fi 74 | 75 | image_id="$(image_from_digest "$repository" "$digest")" 76 | 77 | echo "$image_id" > ${destination}/image-id 78 | docker inspect $image_id > ${destination}/docker_inspect.json 79 | 80 | docker run \ 81 | --cidfile=/tmp/container.cid \ 82 | -v /opt/resource/print-metadata:/tmp/print-metadata \ 83 | --entrypoint /tmp/print-metadata \ 84 | "$image_name" > ${destination}/metadata.json 85 | 86 | mkdir -p ${destination}/rootfs/ 87 | docker export $(cat /tmp/container.cid) | tar --anchored --exclude="dev" -xf - -C ${destination}/rootfs/ 88 | 89 | if [ "$rootfs" = "true" ]; then 90 | docker export $(cat /tmp/container.cid) > ${destination}/rootfs.tar 91 | fi 92 | fi 93 | 94 | echo "$repository" > ${destination}/repository 95 | echo "$tag" > ${destination}/tag 96 | echo "$digest" > ${destination}/digest 97 | 98 | jq -n "{ 99 | version: { 100 | digest: $(echo $digest | jq -R .) 101 | }, 102 | metadata: [ 103 | { name: \"repository\", value: $(echo $repository | jq -R .) }, 104 | { name: \"tag\", value: $(echo $tag | jq -R .) }, 105 | { name: \"image\", value: $(echo $image_id | head -c 12 | jq -R .) } 106 | ] 107 | }" | jq '{version: .version} + {metadata: [.metadata[] | select(.value != "")]}' >&3 108 | -------------------------------------------------------------------------------- /assets/out: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # vim: set ft=sh 3 | 4 | set -e -u 5 | 6 | exec 3>&1 # make stdout available as fd 3 for the result 7 | exec 1>&2 # redirect all output to stderr for logging 8 | 9 | source $(dirname $0)/common.sh 10 | 11 | source=$1 12 | 13 | if [ -z "$source" ]; then 14 | echo "usage: $0 " 15 | exit 1 16 | fi 17 | 18 | # for jq 19 | PATH=/usr/local/bin:$PATH 20 | 21 | payload=$(mktemp /tmp/resource-in.XXXXXX) 22 | 23 | cat > $payload <&0 24 | 25 | cd $source 26 | 27 | insecure_registries=$(jq -r '.source.insecure_registries // [] | join(" ")' < $payload) 28 | registry_mirror=$(jq -r '.source.registry_mirror // ""' < $payload) 29 | 30 | username=$(jq -r '.source.username // ""' < $payload) 31 | password=$(jq -r '.source.password // ""' < $payload) 32 | repository=$(jq -r '.source.repository // ""' < $payload) 33 | ca_certs=$(jq -r '.source.ca_certs // []' < $payload) 34 | client_certs=$(jq -r '.source.client_certs // []' < $payload) 35 | max_concurrent_downloads=$(jq -r '.source.max_concurrent_downloads // 3' < $payload) 36 | max_concurrent_uploads=$(jq -r '.source.max_concurrent_uploads // 3' < $payload) 37 | 38 | export AWS_ACCESS_KEY_ID=$(jq -r '.source.aws_access_key_id // ""' < $payload) 39 | export AWS_SECRET_ACCESS_KEY=$(jq -r '.source.aws_secret_access_key // ""' < $payload) 40 | export AWS_SESSION_TOKEN=$(jq -r '.source.aws_session_token // ""' < $payload) 41 | 42 | if private_registry "${repository}" ; then 43 | registry="$(extract_registry "${repository}")" 44 | else 45 | registry= 46 | fi 47 | 48 | certs_to_file "$ca_certs" 49 | set_client_certs "$client_certs" 50 | start_docker \ 51 | "${max_concurrent_downloads}" \ 52 | "${max_concurrent_uploads}" \ 53 | "$insecure_registries" \ 54 | "$registry_mirror" 55 | 56 | # idea to use base64 to iterate over an array of json objects 57 | # borrowed from https://www.starkandwayne.com/blog/bash-for-loop-over-json-array-using-jq/ 58 | additional_private_registries_base64=$(jq -r '.source.additional_private_registries // []' < $payload | jq -r '.[] | @base64') 59 | 60 | # authenticate to additional registries (if any) 61 | for base64_line in ${additional_private_registries_base64}; do 62 | additional_registry=$(echo $base64_line | base64 -d | jq -r '.registry') 63 | additional_username=$(echo $base64_line | base64 -d | jq -r '.username') 64 | additional_password=$(echo $base64_line | base64 -d | jq -r '.password') 65 | log_in "$additional_username" "$additional_password" "$additional_registry" 66 | done 67 | 68 | # authenticate to primary registry last 69 | log_in "$username" "$password" "$registry" 70 | 71 | tag_source=$(jq -r '.source.tag // "latest"' < $payload) 72 | tag_params=$(jq -r '.params.tag_file // ""' < $payload) 73 | # for backwards compatibility, check `tag` if `tag_file` is empty 74 | if [ -z "$tag_params" ]; then 75 | tag_params=$(jq -r '.params.tag // ""' < $payload) 76 | fi 77 | tag_prefix=$(jq -r '.params.tag_prefix // ""' < $payload) 78 | additional_tags=$(jq -r '.params.additional_tags // ""' < $payload) 79 | need_tag_as_latest=$(jq -r '.params.tag_as_latest // "false"' < $payload) 80 | build_args=$(jq -r '.params.build_args // {}' < $payload) 81 | secrets=$(jq -r '.params.secrets // {}' < $payload) 82 | build_args_file=$(jq -r '.params.build_args_file // ""' < $payload) 83 | labels=$(jq -r '.params.labels // {}' < $payload) 84 | labels_file=$(jq -r '.params.labels_file // ""' < $payload) 85 | 86 | 87 | tag_name="" 88 | if [ -n "$tag_params" ]; then 89 | if [ ! -f "$tag_params" ]; then 90 | echo "tag file '$tag_params' does not exist" 91 | exit 1 92 | fi 93 | tag_name="${tag_prefix}$(cat $tag_params)" 94 | else 95 | tag_name="$tag_source" 96 | fi 97 | 98 | additional_tag_names="" 99 | if [ -n "$additional_tags" ]; then 100 | if [ ! -f "$additional_tags" ]; then 101 | echo "additional tags file '$additional_tags' does not exist" 102 | exit 1 103 | fi 104 | additional_tag_names="$(cat $additional_tags)" 105 | fi 106 | 107 | if [ -z "$repository" ]; then 108 | echo "must specify repository" 109 | exit 1 110 | fi 111 | 112 | load=$(jq -r '.params.load // ""' < $payload) 113 | 114 | load_base=$(jq -r '.params.load_base // ""' < $payload) 115 | load_bases=$(jq -r '.params.load_bases // empty' < $payload) 116 | build=$(jq -r '.params.build // ""' < $payload) 117 | cache=$(jq -r '.params.cache' < $payload) 118 | cache_tag=$(jq -r ".params.cache_tag // \"${tag_name}\"" < $payload) 119 | cache_from=$(jq -r '.params.cache_from // empty' < $payload) 120 | dockerfile=$(jq -r ".params.dockerfile // \"${build}/Dockerfile\"" < $payload) 121 | export DOCKER_BUILDKIT=$(jq -r '.params.docker_buildkit // 0' < $payload) 122 | load_file=$(jq -r '.params.load_file // ""' < $payload) 123 | load_repository=$(jq -r '.params.load_repository // ""' < $payload) 124 | load_tag=$(jq -r '.params.load_tag // "latest"' < $payload) 125 | 126 | import_file=$(jq -r '.params.import_file // ""' < $payload) 127 | 128 | pull_repository=$(jq -r '.params.pull_repository // ""' < $payload) 129 | pull_tag=$(jq -r '.params.pull_tag // "latest"' < $payload) 130 | target_name=$(jq -r '.params.target_name // ""' < $payload) 131 | 132 | if [ -n "$load" ]; then 133 | docker load -i "${load}/image" 134 | docker tag $(cat "${load}/image-id") "${repository}:${tag_name}" 135 | elif [ -n "$build" ]; then 136 | if [ ! -f "$dockerfile" ]; then 137 | echo "It doesn't appear that given Dockerfile: \"$dockerfile\" is a file" 138 | exit 1 139 | fi 140 | 141 | load_images=() 142 | 143 | if [ -n "$load_base" ]; then 144 | load_images+=("$load_base") 145 | fi 146 | 147 | for load_image in $(echo $load_bases | jq -r '.[]'); do 148 | load_images+=("$load_image") 149 | done 150 | 151 | for load_image in $(echo $cache_from | jq -r '.[]'); do 152 | load_images+=("$load_image") 153 | done 154 | 155 | for load_image in "${load_images[@]}"; do 156 | docker load -i "${load_image}/image" 157 | docker tag \ 158 | "$(cat "${load_image}/image-id")" \ 159 | "$(cat "${load_image}/repository"):$(cat "${load_image}/tag")" 160 | done 161 | 162 | cache_from_args=() 163 | 164 | if [ "$cache" = "true" ]; then 165 | if docker_pull "${repository}:${cache_tag}"; then 166 | cache_from_args+=("--cache-from ${repository}:${cache_tag}") 167 | fi 168 | fi 169 | 170 | if [ -n "$cache_from" ]; then 171 | for cache_from_dir in $(echo $cache_from | jq -r '.[]'); do 172 | cache_image="$(cat "${cache_from_dir}/repository")" 173 | cache_tag="$(cat "${cache_from_dir}/tag")" 174 | cache_from_args+=("--cache-from ${cache_image}:${cache_tag}") 175 | done 176 | fi 177 | 178 | cache_from="${cache_from_args[@]}" 179 | 180 | expanded_build_args=() 181 | 182 | # propagate proxy settings to image builder 183 | for proxy_var in http_proxy https_proxy no_proxy ; do 184 | if [ -n "${!proxy_var:-}" ]; then 185 | expanded_build_args+=("--build-arg") 186 | expanded_build_args+=("${proxy_var}=${!proxy_var}") 187 | fi 188 | done 189 | 190 | build_arg_keys=($(echo "$build_args" | jq -r 'keys | join(" ")')) 191 | if [ "${#build_arg_keys[@]}" -gt 0 ]; then 192 | for key in "${build_arg_keys[@]}"; do 193 | value=$(echo "$build_args" | jq -r --arg "k" "$key" '.[$k]') 194 | for var in BUILD_ID BUILD_NAME BUILD_JOB_NAME BUILD_PIPELINE_NAME BUILD_TEAM_NAME ATC_EXTERNAL_URL; do 195 | value="${value//\$$var/${!var:-}}" 196 | value="${value//\$\{$var\}/${!var:-}}" 197 | done 198 | expanded_build_args+=("--build-arg") 199 | expanded_build_args+=("${key}=${value}") 200 | done 201 | fi 202 | 203 | if [ -n "$build_args_file" ]; then 204 | if jq . "$build_args_file" >/dev/null 2>&1; then 205 | build_arg_keys=($(jq -r 'keys | join(" ")' "$build_args_file")) 206 | if [ "${#build_arg_keys[@]}" -gt 0 ]; then 207 | for key in "${build_arg_keys[@]}"; do 208 | value=$(jq -r --arg "k" "$key" '.[$k]' "$build_args_file") 209 | expanded_build_args+=("--build-arg") 210 | expanded_build_args+=("${key}=${value}") 211 | done 212 | fi 213 | else 214 | echo "Failed to parse build_args_file ($build_args_file)" 215 | exit 1 216 | fi 217 | fi 218 | 219 | expanded_secrets=() 220 | 221 | secret_keys=($(echo "$secrets" | jq -r 'keys | join(" ")')) 222 | if [ "${#secret_keys[@]}" -gt 0 ]; then 223 | # Force buildkit on 224 | export DOCKER_BUILDKIT=1 225 | for key in "${secret_keys[@]}"; do 226 | value=$(echo "$secrets" | jq -r --arg "k" "$key" '.[$k]') 227 | for var in BUILD_ID BUILD_NAME BUILD_JOB_NAME BUILD_PIPELINE_NAME BUILD_TEAM_NAME ATC_EXTERNAL_URL; do 228 | value="${value//\$$var/${!var:-}}" 229 | value="${value//\$\{$var\}/${!var:-}}" 230 | done 231 | secret="id=${key}" 232 | sub=$(jq -r ".params.secrets.${key} // {}" < $payload) 233 | sub_keys=($(echo "$sub" | jq -r 'keys | join(" ")')) 234 | if [ "${#sub_keys[@]}" -gt 0 ]; then 235 | expanded_secrets+=("--secret") 236 | for key in "${sub_keys[@]}"; do 237 | value=$(echo "$sub" | jq -r --arg "k" "$key" '.[$k]') 238 | secret="${secret},${key}=${value}" 239 | done 240 | expanded_secrets+=("${secret}") 241 | fi 242 | done 243 | fi 244 | 245 | expanded_labels=() 246 | 247 | label_keys=($(echo "$labels" | jq -r 'keys | join(" ")')) 248 | if [ "${#label_keys[@]}" -gt 0 ]; then 249 | for key in "${label_keys[@]}"; do 250 | value=$(echo "$labels" | jq -r --arg "k" "$key" '.[$k]') 251 | expanded_labels+=("--label") 252 | expanded_labels+=("${key}=${value}") 253 | done 254 | fi 255 | 256 | if [ -n "$labels_file" ]; then 257 | labels_keys=($(jq -r 'keys | join(" ")' "$labels_file")) 258 | if [ "${#labels_keys[@]}" -gt 0 ]; then 259 | for key in "${labels_keys[@]}"; do 260 | value=$(jq -r --arg "k" "$key" '.[$k]' "$labels_file") 261 | expanded_labels+=("--label") 262 | expanded_labels+=("${key}=${value}") 263 | done 264 | fi 265 | fi 266 | 267 | target=() 268 | if [ -n "${target_name}" ]; then 269 | target+=("--target") 270 | target+=("${target_name}") 271 | fi 272 | 273 | ECR_REGISTRY_PATTERN='/[a-zA-Z0-9][a-zA-Z0-9_-]*\.dkr\.ecr\.[a-zA-Z0-9][a-zA-Z0-9_-]*\.amazonaws\.com(\.cn)?[^ ]*/' 274 | ecr_images=$(egrep '^\s*FROM|^\s*ARG' ${dockerfile} | \ 275 | awk "match(\$0,${ECR_REGISTRY_PATTERN}){print substr(\$0, RSTART, RLENGTH)}" ) 276 | if [ -n "$ecr_images" ]; then 277 | for ecr_image in $ecr_images 278 | do 279 | # pull will perform an authentication process needed for ECR 280 | # there is an experimental endpoint to support long running sessions 281 | # docker cli does not support it yet though 282 | # see https://github.com/moby/moby/pull/32677 283 | # and https://github.com/awslabs/amazon-ecr-credential-helper/issues/9 284 | docker pull "${ecr_image}" 285 | done 286 | fi 287 | 288 | # NOTE: deactivate amazon-ecr-credential-helper so that builds go through with the DOCKER_BUILDKIT set 289 | cp ~/.docker/config.json ~/.docker/config.json.bak 290 | cat <<< "$(jq 'del(.credsStore)' ~/.docker/config.json)" > ~/.docker/config.json 291 | docker build -t "${repository}:${tag_name}" "${target[@]}" "${expanded_build_args[@]}" "${expanded_secrets[@]}" "${expanded_labels[@]}" "${ssh_args[@]}" -f "$dockerfile" $cache_from "$build" 292 | mv ~/.docker/config.json.bak ~/.docker/config.json # This restores the credsStore: ecr-login to config.json if needed 293 | 294 | elif [ -n "$load_file" ]; then 295 | if [ -n "$load_repository" ]; then 296 | docker load -i "$load_file" 297 | docker tag "${load_repository}:${load_tag}" "${repository}:${tag_name}" 298 | else 299 | echo "must specify load_repository param" 300 | exit 1 301 | fi 302 | elif [ -n "$import_file" ]; then 303 | cat "$import_file" | docker import - "${repository}:${tag_name}" 304 | elif [ -n "$pull_repository" ]; then 305 | docker pull "${pull_repository}:${pull_tag}" 306 | docker tag "${pull_repository}:${pull_tag}" "${repository}:${tag_name}" 307 | else 308 | echo "must specify build, load, load_file, import_file, or pull_repository params" 309 | exit 1 310 | fi 311 | 312 | image_id="$(image_from_tag "$repository" "$tag_name")" 313 | 314 | # afaict there's no clean way to get the digest after a push. docker prints 315 | # this line at the end at least: 316 | # 317 | # (tagname): digest: (digest) size: (size) 318 | # 319 | # so just parse it out 320 | 321 | # careful to not let 'tee' mask exit status 322 | 323 | { 324 | if ! docker push "${repository}:${tag_name}"; then 325 | touch /tmp/push-failed 326 | fi 327 | } | tee push-output 328 | 329 | if [ -e /tmp/push-failed ]; then 330 | exit 1 331 | fi 332 | 333 | digest="$(grep 'digest' push-output | awk '{print $3}')" 334 | 335 | if [ "$need_tag_as_latest" = "true" ] && [ "${tag_name}" != "latest" ]; then 336 | docker tag "${repository}:${tag_name}" "${repository}:latest" 337 | docker push "${repository}:latest" 338 | echo "${repository}:${tag_name} tagged as latest" 339 | fi 340 | 341 | if [ -n "$additional_tag_names" ] ; then 342 | for additional_tag in $additional_tag_names; do 343 | docker tag "${repository}:${tag_name}" "${repository}:${additional_tag}" 344 | docker push "${repository}:${additional_tag}" 345 | echo "${repository}:${tag_name} tagged as ${additional_tag}" 346 | done 347 | fi 348 | 349 | jq -n "{ 350 | version: { 351 | digest: $(echo $digest | jq -R .) 352 | }, 353 | metadata: [ 354 | { name: \"image\", value: $(echo $image_id | head -c 12 | jq -R .) } 355 | ] 356 | }" >&3 357 | -------------------------------------------------------------------------------- /ci/build-smoke-test-image.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | 4 | image_resource: 5 | type: registry-image 6 | source: 7 | repository: vito/oci-build-task 8 | 9 | inputs: 10 | - name: docker-image-resource 11 | path: . 12 | 13 | outputs: 14 | - name: image 15 | 16 | params: 17 | DOCKERFILE: ~ 18 | UNPACK_ROOTFS: 'true' 19 | 20 | run: 21 | path: build -------------------------------------------------------------------------------- /ci/smoke-test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | 4 | image_resource: 5 | type: registry-image 6 | source: 7 | repository: concourse/docker-image-resource 8 | 9 | params: 10 | REPOSITORY: ~ 11 | 12 | outputs: 13 | - name: fetched-image 14 | 15 | run: 16 | path: /bin/bash 17 | args: 18 | - -exc 19 | - | 20 | pushd fetched-image 21 | jq -n '{source: {repository: $repo}}' --arg repo $REPOSITORY | \ 22 | /opt/resource/check | \ 23 | jq '{source: {repository: $repo}, version: last}' --arg repo $REPOSITORY | \ 24 | /opt/resource/in . 25 | popd -------------------------------------------------------------------------------- /cmd/check/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "regexp" 14 | "strings" 15 | "time" 16 | 17 | "code.cloudfoundry.org/lager/v3" 18 | ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login" 19 | ecrapi "github.com/awslabs/amazon-ecr-credential-helper/ecr-login/api" 20 | "github.com/cihub/seelog" 21 | "github.com/concourse/retryhttp" 22 | "github.com/distribution/reference" 23 | "github.com/docker/distribution" 24 | _ "github.com/docker/distribution/manifest/schema1" 25 | _ "github.com/docker/distribution/manifest/schema2" 26 | v2 "github.com/docker/distribution/registry/api/v2" 27 | "github.com/docker/distribution/registry/client/auth" 28 | "github.com/docker/distribution/registry/client/auth/challenge" 29 | "github.com/docker/distribution/registry/client/transport" 30 | "github.com/hashicorp/go-multierror" 31 | digest "github.com/opencontainers/go-digest" 32 | ) 33 | 34 | func main() { 35 | logger := lager.NewLogger("http") 36 | rECRRepo, err := regexp.Compile(`[a-zA-Z0-9][a-zA-Z0-9_-]*\.dkr\.ecr\.[a-zA-Z0-9][a-zA-Z0-9_-]*\.amazonaws\.com(\.cn)?[^ ]*`) 37 | fatalIf("failed to compile ECR regex", err) 38 | 39 | var request CheckRequest 40 | err = json.NewDecoder(os.Stdin).Decode(&request) 41 | fatalIf("failed to read request", err) 42 | 43 | os.Setenv("AWS_ACCESS_KEY_ID", request.Source.AWSAccessKeyID) 44 | os.Setenv("AWS_SECRET_ACCESS_KEY", request.Source.AWSSecretAccessKey) 45 | os.Setenv("AWS_SESSION_TOKEN", request.Source.AWSSessionToken) 46 | 47 | // silence benign ecr-login errors/warnings 48 | seelog.UseLogger(seelog.Disabled) 49 | 50 | if rECRRepo.MatchString(request.Source.Repository) == true { 51 | ecrUser, ecrPass, err := ecr.NewECRHelper( 52 | ecr.WithClientFactory(ecrapi.DefaultClientFactory{}), 53 | ).Get(request.Source.Repository) 54 | fatalIf("failed to get ECR credentials", err) 55 | request.Source.Username = ecrUser 56 | request.Source.Password = ecrPass 57 | } 58 | 59 | registryHost, repo := parseRepository(request.Source.Repository) 60 | 61 | explicitlyDeclaredRegistryHost := hasExplicitlyDeclaredRegistryHost(registryHost) 62 | if len(request.Source.RegistryMirror) > 0 && !explicitlyDeclaredRegistryHost { 63 | registryMirrorURL, err := url.Parse(request.Source.RegistryMirror) 64 | fatalIf("failed to parse registry mirror URL", err) 65 | registryHost = registryMirrorURL.Host 66 | } 67 | 68 | tag := string(request.Source.Tag) 69 | if tag == "" { 70 | tag = "latest" 71 | } 72 | 73 | transport, registryURL := makeTransport(logger, request, registryHost, repo) 74 | 75 | client := &http.Client{ 76 | Transport: retryRoundTripper(logger, transport), 77 | } 78 | 79 | ub, err := v2.NewURLBuilderFromString(registryURL, false) 80 | fatalIf("failed to construct registry URL builder", err) 81 | 82 | namedRef, err := reference.WithName(repo) 83 | fatalIf("failed to construct named reference", err) 84 | 85 | var response CheckResponse 86 | 87 | taggedRef, err := reference.WithTag(namedRef, tag) 88 | fatalIf("failed to construct tagged reference", err) 89 | 90 | latestManifestURL, err := ub.BuildManifestURL(taggedRef) 91 | fatalIf("failed to build latest manifest URL", err) 92 | 93 | latestDigest, foundLatest := headDigest(client, latestManifestURL, request.Source.Repository, tag) 94 | 95 | if request.Version.Digest != "" { 96 | digestRef, err := reference.WithDigest(namedRef, digest.Digest(request.Version.Digest)) 97 | fatalIf("failed to build cursor manifest URL", err) 98 | 99 | cursorManifestURL, err := ub.BuildManifestURL(digestRef) 100 | fatalIf("failed to build manifest URL", err) 101 | 102 | cursorDigest, foundCursor := headDigest(client, cursorManifestURL, request.Source.Repository, tag) 103 | 104 | if foundCursor && cursorDigest != latestDigest { 105 | response = append(response, Version{cursorDigest}) 106 | } 107 | } 108 | 109 | if foundLatest { 110 | response = append(response, Version{latestDigest}) 111 | } 112 | 113 | json.NewEncoder(os.Stdout).Encode(response) 114 | } 115 | 116 | func headDigest(client *http.Client, manifestURL, repository, tag string) (string, bool) { 117 | manifestRequest, err := http.NewRequest("HEAD", manifestURL, nil) 118 | fatalIf("failed to build manifest request", err) 119 | manifestRequest.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json") 120 | manifestRequest.Header.Add("Accept", "application/vnd.oci.image.index.v1+json") 121 | manifestRequest.Header.Add("Accept", "application/json") 122 | 123 | manifestResponse, err := client.Do(manifestRequest) 124 | fatalIf("failed to fetch manifest", err) 125 | 126 | defer manifestResponse.Body.Close() 127 | 128 | if manifestResponse.StatusCode == http.StatusNotFound { 129 | return "", false 130 | } 131 | 132 | if manifestResponse.StatusCode != http.StatusOK { 133 | fatal(fmt.Sprintf("failed to fetch digest for image '%s:%s': %s\ndoes the image exist?", repository, tag, manifestResponse.Status)) 134 | } 135 | 136 | digest := manifestResponse.Header.Get("Docker-Content-Digest") 137 | if digest == "" { 138 | return fetchDigest(client, manifestURL, repository, tag) 139 | } 140 | 141 | return digest, true 142 | } 143 | 144 | func fetchDigest(client *http.Client, manifestURL, repository, tag string) (string, bool) { 145 | manifestRequest, err := http.NewRequest("GET", manifestURL, nil) 146 | fatalIf("failed to build manifest request", err) 147 | manifestRequest.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json") 148 | manifestRequest.Header.Add("Accept", "application/vnd.oci.image.index.v1+json") 149 | manifestRequest.Header.Add("Accept", "application/json") 150 | 151 | manifestResponse, err := client.Do(manifestRequest) 152 | fatalIf("failed to fetch manifest", err) 153 | 154 | defer manifestResponse.Body.Close() 155 | 156 | if manifestResponse.StatusCode == http.StatusNotFound { 157 | return "", false 158 | } 159 | 160 | if manifestResponse.StatusCode != http.StatusOK { 161 | fatal(fmt.Sprintf("failed to fetch digest for image '%s:%s': %s\ndoes the image exist?", repository, tag, manifestResponse.Status)) 162 | } 163 | 164 | ctHeader := manifestResponse.Header.Get("Content-Type") 165 | 166 | bytes, err := io.ReadAll(manifestResponse.Body) 167 | fatalIf("failed to read response body", err) 168 | 169 | _, desc, err := distribution.UnmarshalManifest(ctHeader, bytes) 170 | fatalIf("failed to unmarshal manifest", err) 171 | 172 | return string(desc.Digest), true 173 | } 174 | 175 | func makeTransport(logger lager.Logger, request CheckRequest, registryHost string, repository string) (http.RoundTripper, string) { 176 | // for non self-signed registries, caCertPool must be nil in order to use the system certs 177 | var caCertPool *x509.CertPool 178 | if len(request.Source.DomainCerts) > 0 { 179 | caCertPool = x509.NewCertPool() 180 | for _, domainCert := range request.Source.DomainCerts { 181 | ok := caCertPool.AppendCertsFromPEM([]byte(domainCert.Cert)) 182 | if !ok { 183 | fatal(fmt.Sprintf("failed to parse CA certificate for \"%s\"", domainCert.Domain)) 184 | } 185 | } 186 | } 187 | 188 | baseTransport := &http.Transport{ 189 | Proxy: http.ProxyFromEnvironment, 190 | Dial: (&net.Dialer{ 191 | Timeout: 30 * time.Second, 192 | KeepAlive: 30 * time.Second, 193 | DualStack: true, 194 | }).Dial, 195 | DisableKeepAlives: true, 196 | TLSClientConfig: &tls.Config{RootCAs: caCertPool}, 197 | } 198 | 199 | var insecure bool 200 | for _, hostOrCIDR := range request.Source.InsecureRegistries { 201 | if isInsecure(hostOrCIDR, registryHost) { 202 | insecure = true 203 | } 204 | } 205 | 206 | if insecure { 207 | baseTransport.TLSClientConfig = &tls.Config{ 208 | InsecureSkipVerify: true, 209 | } 210 | } 211 | 212 | if len(request.Source.ClientCerts) > 0 { 213 | baseTransport.TLSClientConfig = &tls.Config{ 214 | RootCAs: caCertPool, 215 | Certificates: setClientCert(registryHost, request.Source.ClientCerts), 216 | } 217 | } 218 | 219 | authTransport := transport.NewTransport(baseTransport) 220 | 221 | pingClient := &http.Client{ 222 | Transport: retryRoundTripper(logger, authTransport), 223 | Timeout: 1 * time.Minute, 224 | } 225 | 226 | challengeManager := challenge.NewSimpleManager() 227 | 228 | var registryURL string 229 | 230 | var pingResp *http.Response 231 | var pingErr error 232 | var pingErrs error 233 | for _, scheme := range []string{"https", "http"} { 234 | registryURL = scheme + "://" + registryHost 235 | 236 | req, err := http.NewRequest("GET", registryURL+"/v2/", nil) 237 | fatalIf("failed to create ping request", err) 238 | 239 | pingResp, pingErr = pingClient.Do(req) 240 | if pingErr == nil { 241 | // clear out previous attempts' failures 242 | pingErrs = nil 243 | break 244 | } 245 | 246 | pingErrs = multierror.Append( 247 | pingErrs, 248 | fmt.Errorf("ping %s: %s", scheme, pingErr), 249 | ) 250 | } 251 | fatalIf("failed to ping registry", pingErrs) 252 | 253 | defer pingResp.Body.Close() 254 | 255 | err := challengeManager.AddResponse(pingResp) 256 | fatalIf("failed to add response to challenge manager", err) 257 | 258 | credentialStore := dumbCredentialStore{request.Source.Username, request.Source.Password} 259 | tokenHandler := auth.NewTokenHandler(authTransport, credentialStore, repository, "pull") 260 | basicHandler := auth.NewBasicHandler(credentialStore) 261 | authorizer := auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler) 262 | 263 | return transport.NewTransport(baseTransport, authorizer), registryURL 264 | } 265 | 266 | type dumbCredentialStore struct { 267 | username string 268 | password string 269 | } 270 | 271 | func (dcs dumbCredentialStore) Basic(*url.URL) (string, string) { 272 | return dcs.username, dcs.password 273 | } 274 | 275 | func (dumbCredentialStore) RefreshToken(u *url.URL, service string) string { 276 | return "" 277 | } 278 | 279 | func (dumbCredentialStore) SetRefreshToken(u *url.URL, service, token string) { 280 | } 281 | 282 | func fatalIf(doing string, err error) { 283 | if err != nil { 284 | fatal(doing + ": " + err.Error()) 285 | } 286 | } 287 | 288 | func fatal(message string) { 289 | println(message) 290 | os.Exit(1) 291 | } 292 | 293 | const officialRegistry = "registry-1.docker.io" 294 | 295 | func parseRepository(repository string) (string, string) { 296 | segs := strings.Split(repository, "/") 297 | 298 | if len(segs) > 1 && (strings.Contains(segs[0], ":") || strings.Contains(segs[0], ".")) { 299 | // In a private registry pretty much anything is valid. 300 | return segs[0], strings.Join(segs[1:], "/") 301 | } 302 | switch len(segs) { 303 | case 3: 304 | return segs[0], segs[1] + "/" + segs[2] 305 | case 2: 306 | return officialRegistry, segs[0] + "/" + segs[1] 307 | case 1: 308 | return officialRegistry, "library/" + segs[0] 309 | } 310 | 311 | fatal("malformed repository url") 312 | panic("unreachable") 313 | } 314 | 315 | // Does the repository include an explicitly declared registry host, such as 'foo.com/baz/bar' 316 | // that differs from the officialRegistry? 317 | func hasExplicitlyDeclaredRegistryHost(registryHost string) bool { 318 | return strings.Contains(registryHost, ".") && registryHost != officialRegistry 319 | } 320 | 321 | func isInsecure(hostOrCIDR string, hostPort string) bool { 322 | host, _, err := net.SplitHostPort(hostPort) 323 | if err != nil { 324 | return hostOrCIDR == hostPort 325 | } 326 | 327 | _, cidr, err := net.ParseCIDR(hostOrCIDR) 328 | if err == nil { 329 | ip := net.ParseIP(host) 330 | if ip != nil { 331 | return cidr.Contains(ip) 332 | } 333 | } 334 | 335 | return hostOrCIDR == hostPort 336 | } 337 | 338 | func retryRoundTripper(logger lager.Logger, rt http.RoundTripper) http.RoundTripper { 339 | return &retryhttp.RetryRoundTripper{ 340 | Logger: logger, 341 | BackOffFactory: retryhttp.NewExponentialBackOffFactory(5 * time.Minute), 342 | RoundTripper: rt, 343 | } 344 | } 345 | 346 | func setClientCert(registry string, list []ClientCertKey) []tls.Certificate { 347 | var clientCert []tls.Certificate 348 | for _, r := range list { 349 | if r.Domain == registry { 350 | certKey, err := tls.X509KeyPair([]byte(r.Cert), []byte(r.Key)) 351 | if err != nil { 352 | fatal(fmt.Sprintf("failed to parse client certificate and/or key for \"%s\"", r.Domain)) 353 | } 354 | clientCert = append(clientCert, certKey) 355 | } 356 | } 357 | return clientCert 358 | } 359 | -------------------------------------------------------------------------------- /cmd/check/models.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "encoding/json" 4 | 5 | type Source struct { 6 | Repository string `json:"repository"` 7 | Tag Tag `json:"tag"` 8 | Username string `json:"username"` 9 | Password string `json:"password"` 10 | InsecureRegistries []string `json:"insecure_registries"` 11 | RegistryMirror string `json:"registry_mirror"` 12 | DomainCerts []DomainCert `json:"ca_certs"` 13 | ClientCerts []ClientCertKey `json:"client_certs"` 14 | 15 | AWSAccessKeyID string `json:"aws_access_key_id"` 16 | AWSSecretAccessKey string `json:"aws_secret_access_key"` 17 | AWSSessionToken string `json:"aws_session_token"` 18 | } 19 | 20 | type Version struct { 21 | Digest string `json:"digest"` 22 | } 23 | 24 | type CheckRequest struct { 25 | Source Source `json:"source"` 26 | Version Version `json:"version"` 27 | } 28 | 29 | type CheckResponse []Version 30 | 31 | type DomainCert struct { 32 | Domain string `json:"domain"` 33 | Cert string `json:"cert"` 34 | } 35 | 36 | type ClientCertKey struct { 37 | Domain string `json:"domain"` 38 | Cert string `json:"cert"` 39 | Key string `json:"key"` 40 | } 41 | 42 | // Tag refers to a tag for an image in the registry. 43 | type Tag string 44 | 45 | // UnmarshalJSON accepts numeric and string values. 46 | func (tag *Tag) UnmarshalJSON(b []byte) (err error) { 47 | var s string 48 | if err = json.Unmarshal(b, &s); err == nil { 49 | *tag = Tag(s) 50 | } else { 51 | var n json.RawMessage 52 | if err = json.Unmarshal(b, &n); err == nil { 53 | *tag = Tag(n) 54 | } 55 | } 56 | return err 57 | } 58 | -------------------------------------------------------------------------------- /cmd/print-metadata/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "strings" 9 | "syscall" 10 | 11 | "github.com/concourse/docker-image-resource/cmd/print-metadata/passwd" 12 | ) 13 | 14 | type imageMetadata struct { 15 | User string `json:"user,omitempty"` 16 | Env []string `json:"env"` 17 | } 18 | 19 | var blacklistedEnv = map[string]bool{ 20 | "HOSTNAME": true, 21 | } 22 | 23 | var userFile = flag.String("userFile", "/etc/passwd", "") 24 | 25 | func main() { 26 | flag.Parse() 27 | 28 | username, err := getUsername(*userFile) 29 | if err != nil { 30 | fmt.Fprintf(os.Stderr, "Unable to determine username, will not be included in metadata") 31 | } 32 | 33 | err = json.NewEncoder(os.Stdout).Encode(imageMetadata{ 34 | User: username, 35 | Env: env(), 36 | }) 37 | if err != nil { 38 | panic(err) 39 | } 40 | } 41 | 42 | func getUsername(userFile string) (string, error) { 43 | users, err := passwd.ReadUsers(userFile) 44 | if err != nil { 45 | return "", err 46 | } 47 | 48 | name, found := users.NameForID(syscall.Getuid()) 49 | if !found { 50 | return "", fmt.Errorf("could not find user in %s", userFile) 51 | } 52 | 53 | return name, nil 54 | } 55 | 56 | func env() []string { 57 | var envVars []string 58 | for _, e := range os.Environ() { 59 | parts := strings.SplitN(e, "=", 2) 60 | name := parts[0] 61 | 62 | if !blacklistedEnv[name] { 63 | envVars = append(envVars, e) 64 | } 65 | } 66 | 67 | return envVars 68 | } 69 | -------------------------------------------------------------------------------- /cmd/print-metadata/main_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "runtime" 9 | "syscall" 10 | 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | 14 | "github.com/onsi/gomega/gexec" 15 | ) 16 | 17 | type imageMetadata struct { 18 | User string `json:"user"` 19 | Env []string `json:"env"` 20 | } 21 | 22 | var _ = Describe("print-metadata", func() { 23 | var ( 24 | cmd *exec.Cmd 25 | userFile *os.File 26 | 27 | metadata imageMetadata 28 | ) 29 | 30 | JustBeforeEach(func() { 31 | session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) 32 | Expect(err).NotTo(HaveOccurred()) 33 | Eventually(session).Should(gexec.Exit(0)) 34 | 35 | metadata = imageMetadata{} 36 | err = json.Unmarshal(session.Out.Contents(), &metadata) 37 | Expect(err).NotTo(HaveOccurred()) 38 | }) 39 | 40 | Context("when user file exists", func() { 41 | BeforeEach(func() { 42 | var err error 43 | userFile, err = os.CreateTemp("", "print-metadata-test") 44 | Expect(err).NotTo(HaveOccurred()) 45 | 46 | cmd = exec.Command(printMetadataPath, "-userFile", userFile.Name()) 47 | }) 48 | 49 | AfterEach(func() { 50 | userFile.Close() 51 | os.Remove(userFile.Name()) 52 | }) 53 | 54 | It("writes metadata with no user", func() { 55 | Expect(metadata.User).To(BeEmpty()) 56 | }) 57 | 58 | Context("when password file contains current user", func() { 59 | BeforeEach(func() { 60 | currentUsedID := syscall.Getuid() 61 | 62 | _, err := userFile.WriteString(fmt.Sprintf( 63 | "%s:*:%d:%d:System Administrator:/var/%s:/bin/sh\n", 64 | "some-user", 65 | currentUsedID, 66 | currentUsedID, 67 | "some-user", 68 | )) 69 | Expect(err).NotTo(HaveOccurred()) 70 | userFile.Sync() 71 | }) 72 | 73 | It("sets current user in metadata", func() { 74 | Expect(metadata.User).To(Equal("some-user")) 75 | }) 76 | }) 77 | }) 78 | 79 | Context("when password file does not exist", func() { 80 | BeforeEach(func() { 81 | cmd = exec.Command(printMetadataPath, "-userFile", "non-existent-file") 82 | }) 83 | 84 | It("writes metadata with no user", func() { 85 | Expect(metadata.User).To(BeEmpty()) 86 | }) 87 | }) 88 | 89 | Describe("environment variables", func() { 90 | BeforeEach(func() { 91 | if runtime.GOOS == "darwin" && syscall.Getuid() != 0 { 92 | Skip("OS X doesn't use /etc/passwd for multi-user mode (you need to run the tests as root)") 93 | } 94 | cmd = exec.Command(printMetadataPath) 95 | }) 96 | 97 | Context("when it is running in an environment with environment variables", func() { 98 | BeforeEach(func() { 99 | cmd.Env = []string{ 100 | "SOME=foo", 101 | "AMAZING=bar", 102 | "ENV=baz", 103 | } 104 | }) 105 | 106 | It("outputs them on stdout", func() { 107 | Expect(metadata.Env).To(ConsistOf([]string{ 108 | "SOME=foo", 109 | "AMAZING=bar", 110 | "ENV=baz", 111 | })) 112 | }) 113 | }) 114 | 115 | Context("when it is running in an environment with environment variables in the blacklist", func() { 116 | BeforeEach(func() { 117 | cmd.Env = []string{ 118 | "SOME=foo", 119 | "HOSTNAME=bar", 120 | "ENV=baz", 121 | } 122 | }) 123 | 124 | It("outputs everything but them on stdout", func() { 125 | Expect(metadata.Env).To(ConsistOf([]string{ 126 | "SOME=foo", 127 | "ENV=baz", 128 | })) 129 | }) 130 | }) 131 | }) 132 | }) 133 | -------------------------------------------------------------------------------- /cmd/print-metadata/passwd/passwd.go: -------------------------------------------------------------------------------- 1 | package passwd 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type User struct { 12 | ID int 13 | Username string 14 | } 15 | 16 | type Users []User 17 | 18 | func (users Users) NameForID(id int) (string, bool) { 19 | for _, user := range users { 20 | if id == user.ID { 21 | return user.Username, true 22 | } 23 | } 24 | return "", false 25 | } 26 | 27 | func ReadUsers(path string) (Users, error) { 28 | file, err := os.Open(path) 29 | if err != nil { 30 | return nil, err 31 | } 32 | defer file.Close() 33 | 34 | userScanner := bufio.NewScanner(file) 35 | 36 | users := []User{} 37 | lineCount := 0 38 | for userScanner.Scan() { 39 | lineCount++ 40 | userLine := strings.TrimSpace(userScanner.Text()) 41 | if userLine == "" || strings.HasPrefix(userLine, "#") { 42 | continue 43 | } 44 | userLineColumns := strings.Split(userLine, ":") 45 | if len(userLineColumns) != 7 { 46 | return nil, fmt.Errorf("malformed user on line %d", lineCount) 47 | } 48 | userName := userLineColumns[0] 49 | userIDStr := userLineColumns[2] 50 | userID, err := strconv.Atoi(userIDStr) 51 | if err != nil { 52 | return nil, fmt.Errorf("malformed user ID on line %d: %s", lineCount, userIDStr) 53 | } 54 | users = append(users, User{ 55 | Username: userName, 56 | ID: userID, 57 | }) 58 | } 59 | return users, nil 60 | } 61 | -------------------------------------------------------------------------------- /cmd/print-metadata/passwd/passwd_suite_test.go: -------------------------------------------------------------------------------- 1 | package passwd_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestPasswd(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Passwd Suite") 13 | } 14 | -------------------------------------------------------------------------------- /cmd/print-metadata/passwd/passwd_test.go: -------------------------------------------------------------------------------- 1 | package passwd_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | 11 | "github.com/concourse/docker-image-resource/cmd/print-metadata/passwd" 12 | ) 13 | 14 | var _ = Describe("Passwd", func() { 15 | var ( 16 | etcPasswdDir string 17 | etcPasswdPath string 18 | etcPasswdContents string 19 | etcPasswdUsers []passwd.User 20 | ) 21 | 22 | BeforeEach(func() { 23 | etcPasswdContents = "" 24 | etcPasswdUsers = []passwd.User{} 25 | }) 26 | 27 | JustBeforeEach(func() { 28 | path, err := os.MkdirTemp("", "passwd") 29 | Expect(err).ToNot(HaveOccurred()) 30 | 31 | etcPasswdDir = path 32 | etcPasswdPath = filepath.Join(etcPasswdDir, "passwd") 33 | 34 | for _, user := range etcPasswdUsers { 35 | etcPasswdContents += fmt.Sprintf("%s:*:%d:1:User Name:/dev/null:/usr/bin/false\n", user.Username, user.ID) 36 | } 37 | 38 | err = os.WriteFile(etcPasswdPath, []byte(etcPasswdContents), 0600) 39 | Expect(err).ToNot(HaveOccurred()) 40 | }) 41 | 42 | AfterEach(func() { 43 | err := os.RemoveAll(etcPasswdDir) 44 | Expect(err).ToNot(HaveOccurred()) 45 | }) 46 | 47 | Describe("getting a list of users", func() { 48 | Context("when there is a single user in the passwd file", func() { 49 | BeforeEach(func() { 50 | etcPasswdUsers = []passwd.User{ 51 | { 52 | ID: 1, 53 | Username: "username", 54 | }, 55 | } 56 | }) 57 | 58 | It("can read the specified passwd file", func() { 59 | users, err := passwd.ReadUsers(etcPasswdPath) 60 | Expect(err).ToNot(HaveOccurred()) 61 | Expect(users).ToNot(BeNil()) 62 | }) 63 | 64 | It("finds one user", func() { 65 | users, err := passwd.ReadUsers(etcPasswdPath) 66 | Expect(err).ToNot(HaveOccurred()) 67 | Expect(users).To(ConsistOf(etcPasswdUsers)) 68 | }) 69 | }) 70 | 71 | Context("when there is a different single user in the passwd file", func() { 72 | BeforeEach(func() { 73 | etcPasswdUsers = []passwd.User{ 74 | { 75 | ID: 2, 76 | Username: "username2", 77 | }, 78 | } 79 | }) 80 | 81 | It("finds the different user", func() { 82 | users, err := passwd.ReadUsers(etcPasswdPath) 83 | Expect(err).ToNot(HaveOccurred()) 84 | Expect(users).To(ConsistOf(etcPasswdUsers)) 85 | }) 86 | }) 87 | 88 | Context("when there are multiple users in the passwd file", func() { 89 | BeforeEach(func() { 90 | etcPasswdUsers = []passwd.User{ 91 | { 92 | ID: 1, 93 | Username: "username", 94 | }, 95 | { 96 | ID: 2, 97 | Username: "username2", 98 | }, 99 | } 100 | }) 101 | 102 | It("finds two users", func() { 103 | users, err := passwd.ReadUsers(etcPasswdPath) 104 | Expect(err).ToNot(HaveOccurred()) 105 | Expect(users).To(ConsistOf(etcPasswdUsers)) 106 | }) 107 | }) 108 | 109 | Context("when the file contains comments", func() { 110 | BeforeEach(func() { 111 | etcPasswdContents = `# this is a comment 112 | commentuser:*:1:1:User Name:/dev/null:/usr/bin/false\n 113 | ` 114 | }) 115 | 116 | It("finds the user", func() { 117 | users, err := passwd.ReadUsers(etcPasswdPath) 118 | Expect(err).ToNot(HaveOccurred()) 119 | Expect(users).To(ConsistOf(passwd.User{ 120 | ID: 1, 121 | Username: "commentuser", 122 | })) 123 | }) 124 | }) 125 | 126 | Context("when the file contains comments that have whitespace before them", func() { 127 | BeforeEach(func() { 128 | etcPasswdContents = ` # this is a comment 129 | commentuser:*:1:1:User Name:/dev/null:/usr/bin/false\n 130 | ` 131 | }) 132 | 133 | It("finds the user", func() { 134 | users, err := passwd.ReadUsers(etcPasswdPath) 135 | Expect(err).ToNot(HaveOccurred()) 136 | Expect(users).To(ConsistOf(passwd.User{ 137 | ID: 1, 138 | Username: "commentuser", 139 | })) 140 | }) 141 | }) 142 | 143 | Context("when the file contains malformed user lines", func() { 144 | BeforeEach(func() { 145 | etcPasswdContents = ` 146 | 147 | commentuser:*:1:::::1:User Name:/dev/null:/usr/bin/false\n 148 | ` 149 | }) 150 | 151 | It("returns an error", func() { 152 | _, err := passwd.ReadUsers(etcPasswdPath) 153 | Expect(err).To(MatchError("malformed user on line 3")) 154 | }) 155 | }) 156 | 157 | Context("when the file does not exist", func() { 158 | It("returns an error", func() { 159 | _, err := passwd.ReadUsers("/this/does/not/exist") 160 | Expect(err).To(HaveOccurred()) 161 | }) 162 | }) 163 | 164 | Context("when the file contains a malformed user ID", func() { 165 | BeforeEach(func() { 166 | etcPasswdContents = ` 167 | 168 | commentuser:*:hello:1:User Name:/dev/null:/usr/bin/false\n 169 | ` 170 | }) 171 | 172 | It("returns an error", func() { 173 | _, err := passwd.ReadUsers(etcPasswdPath) 174 | Expect(err).To(MatchError("malformed user ID on line 3: hello")) 175 | }) 176 | }) 177 | 178 | Context("when the file does not exist", func() { 179 | It("returns an error", func() { 180 | _, err := passwd.ReadUsers("/this/does/not/exist") 181 | Expect(err).To(HaveOccurred()) 182 | }) 183 | }) 184 | }) 185 | 186 | Describe("getting a username from a user id", func() { 187 | Context("when the user exists", func() { 188 | BeforeEach(func() { 189 | etcPasswdUsers = []passwd.User{ 190 | { 191 | ID: 1, 192 | Username: "username", 193 | }, 194 | { 195 | ID: 2, 196 | Username: "username2", 197 | }, 198 | } 199 | }) 200 | 201 | It("lets someone find the username of a particular ID", func() { 202 | users, err := passwd.ReadUsers(etcPasswdPath) 203 | Expect(err).ToNot(HaveOccurred()) 204 | 205 | name, found := users.NameForID(2) 206 | Expect(found).To(BeTrue()) 207 | Expect(name).To(Equal("username2")) 208 | }) 209 | }) 210 | 211 | Context("when a different user exists", func() { 212 | BeforeEach(func() { 213 | etcPasswdUsers = []passwd.User{ 214 | { 215 | ID: 1, 216 | Username: "username", 217 | }, 218 | { 219 | ID: 2, 220 | Username: "username2", 221 | }, 222 | } 223 | }) 224 | 225 | It("lets someone find the username of a particular ID", func() { 226 | users, err := passwd.ReadUsers(etcPasswdPath) 227 | Expect(err).ToNot(HaveOccurred()) 228 | 229 | name, found := users.NameForID(1) 230 | Expect(found).To(BeTrue()) 231 | Expect(name).To(Equal("username")) 232 | }) 233 | }) 234 | 235 | Context("when the user doesn't exist", func() { 236 | BeforeEach(func() { 237 | etcPasswdUsers = []passwd.User{ 238 | { 239 | ID: 1, 240 | Username: "username", 241 | }, 242 | { 243 | ID: 2, 244 | Username: "username2", 245 | }, 246 | } 247 | }) 248 | 249 | It("lets someone find the username of a particular ID", func() { 250 | users, err := passwd.ReadUsers(etcPasswdPath) 251 | Expect(err).ToNot(HaveOccurred()) 252 | 253 | _, found := users.NameForID(3) 254 | Expect(found).To(BeFalse()) 255 | }) 256 | }) 257 | }) 258 | }) 259 | -------------------------------------------------------------------------------- /cmd/print-metadata/suite_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | 10 | "github.com/onsi/gomega/gexec" 11 | ) 12 | 13 | var printMetadataPath string 14 | 15 | func TestSuite(t *testing.T) { 16 | RegisterFailHandler(Fail) 17 | RunSpecs(t, "cmd/print-metadata") 18 | } 19 | 20 | var _ = SynchronizedBeforeSuite(func() []byte { 21 | var path string 22 | if _, err := os.Stat("/opt/resource/print-metadata"); err == nil { 23 | path = "/opt/resource/print-metadata" 24 | } else { 25 | path, err = gexec.Build("github.com/concourse/docker-image-resource/cmd/print-metadata") 26 | Expect(err).NotTo(HaveOccurred()) 27 | } 28 | 29 | return []byte(path) 30 | }, func(data []byte) { 31 | printMetadataPath = string(data) 32 | }) 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/concourse/docker-image-resource 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | code.cloudfoundry.org/lager/v3 v3.37.0 9 | github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.9.1 10 | github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 11 | github.com/concourse/retryhttp v1.2.4 12 | github.com/distribution/reference v0.6.0 13 | github.com/docker/distribution v2.8.3+incompatible 14 | github.com/hashicorp/go-multierror v1.1.1 15 | github.com/onsi/ginkgo/v2 v2.23.4 16 | github.com/onsi/gomega v1.37.0 17 | github.com/opencontainers/go-digest v1.0.0 18 | ) 19 | 20 | require ( 21 | github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect 22 | github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect 23 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect 24 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect 25 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 26 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 27 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/ecr v1.44.0 // indirect 29 | github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.33.0 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect 31 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect 32 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect 33 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect 34 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect 35 | github.com/aws/smithy-go v1.22.3 // indirect 36 | github.com/beorn7/perks v1.0.1 // indirect 37 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 38 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 39 | github.com/docker/docker-credential-helpers v0.9.3 // indirect 40 | github.com/docker/go-metrics v0.0.2-0.20221207153146-523432a393ef // indirect 41 | github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect 42 | github.com/go-logr/logr v1.4.2 // indirect 43 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 44 | github.com/google/go-cmp v0.7.0 // indirect 45 | github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect 46 | github.com/gorilla/mux v1.8.1 // indirect 47 | github.com/hashicorp/errwrap v1.1.0 // indirect 48 | github.com/mitchellh/go-homedir v1.1.0 // indirect 49 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 50 | github.com/opencontainers/image-spec v1.1.1 // indirect 51 | github.com/openzipkin/zipkin-go v0.4.3 // indirect 52 | github.com/prometheus/client_golang v1.22.0 // indirect 53 | github.com/prometheus/client_model v0.6.2 // indirect 54 | github.com/prometheus/common v0.64.0 // indirect 55 | github.com/prometheus/procfs v0.16.1 // indirect 56 | github.com/sirupsen/logrus v1.9.3 // indirect 57 | go.uber.org/automaxprocs v1.6.0 // indirect 58 | golang.org/x/net v0.40.0 // indirect 59 | golang.org/x/sys v0.33.0 // indirect 60 | golang.org/x/text v0.25.0 // indirect 61 | golang.org/x/tools v0.33.0 // indirect 62 | google.golang.org/protobuf v1.36.6 // indirect 63 | gopkg.in/yaml.v3 v3.0.1 // indirect 64 | ) 65 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 17 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 18 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 19 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 20 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 21 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 22 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 23 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 24 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 25 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 26 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 27 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 28 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 29 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 30 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 31 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 32 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 33 | code.cloudfoundry.org/lager/v3 v3.37.0 h1:cAOUYz2am0remjeKBorIwIapzW6QoRGAFa3ztQb1ywg= 34 | code.cloudfoundry.org/lager/v3 v3.37.0/go.mod h1:k6m2qvgAaiS6fE1TpzUoCo3p3aED72dcGgSZCiQznf4= 35 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 36 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 37 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 38 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 39 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 40 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 41 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 42 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 43 | github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= 44 | github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= 45 | github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= 46 | github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= 47 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= 48 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= 49 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= 50 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= 51 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= 52 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= 53 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= 54 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= 55 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= 56 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= 57 | github.com/aws/aws-sdk-go-v2/service/ecr v1.44.0 h1:E+UTVTDH6XTSjqxHWRuY8nB6s+05UllneWxnycplHFk= 58 | github.com/aws/aws-sdk-go-v2/service/ecr v1.44.0/go.mod h1:iQ1skgw1XRK+6Lgkb0I9ODatAP72WoTILh0zXQ5DtbU= 59 | github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.33.0 h1:wA2O6pZ2r5smqJunFP4hp7qptMW4EQxs8O6RVHPulOE= 60 | github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.33.0/go.mod h1:RZL7ov7c72wSmoM8bIiVxRHgcVdzhNkVW2J36C8RF4s= 61 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= 62 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= 63 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= 64 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= 65 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= 66 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= 67 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= 68 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= 69 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= 70 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= 71 | github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= 72 | github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= 73 | github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.9.1 h1:50sS0RWhGpW/yZx2KcDNEb1u1MANv5BMEkJgcieEDTA= 74 | github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.9.1/go.mod h1:ErZOtbzuHabipRTDTor0inoRlYwbsV1ovwSxjGs/uJo= 75 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 76 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 77 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 78 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 79 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 80 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 81 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 82 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 83 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 84 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 85 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 86 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 87 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 88 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 89 | github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs= 90 | github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= 91 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 92 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 93 | github.com/concourse/retryhttp v1.2.4 h1:eo1DhK3ZaW7lWm846Y9R8/91LpETTWA6/YtBhxabclY= 94 | github.com/concourse/retryhttp v1.2.4/go.mod h1:OdmoHwj4SdbCWGnoHMvar5lcsLVQNpUkOI3uLgLBS8Q= 95 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 96 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 97 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 98 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 99 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 100 | github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= 101 | github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 102 | github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= 103 | github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= 104 | github.com/docker/go-metrics v0.0.2-0.20221207153146-523432a393ef h1:kah2DGwIVpbW2k1G6ru/ySIL3rdypw8amRhmaSAZYns= 105 | github.com/docker/go-metrics v0.0.2-0.20221207153146-523432a393ef/go.mod h1:knoTFU8fpXrUP2PSJT+OOB4d26o0s3l3JvW4pPp+GcQ= 106 | github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= 107 | github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= 108 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 109 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 110 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 111 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 112 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 113 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 114 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 115 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 116 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 117 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 118 | github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= 119 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 120 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 121 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 122 | github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 123 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 124 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 125 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 126 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 127 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 128 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 129 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 130 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 131 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 132 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 133 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 134 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 135 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 136 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 137 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 138 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 139 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 140 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 141 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 142 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 143 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 144 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 145 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 146 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 147 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 148 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 149 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 150 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 151 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 152 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 153 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 154 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 155 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 156 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 157 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 158 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 159 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 160 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 161 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 162 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 163 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 164 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 165 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 166 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 167 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 168 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 169 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 170 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 171 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 172 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 173 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 174 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 175 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 176 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 177 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 178 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 179 | github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4= 180 | github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= 181 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 182 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 183 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 184 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 185 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 186 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 187 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 188 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 189 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 190 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 191 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 192 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 193 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 194 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 195 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 196 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 197 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 198 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 199 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 200 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 201 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 202 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 203 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 204 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 205 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 206 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 207 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 208 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 209 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 210 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 211 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 212 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 213 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 214 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 215 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 216 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 217 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 218 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 219 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 220 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 221 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 222 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 223 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 224 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 225 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 226 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 227 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 228 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 229 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 230 | github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= 231 | github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= 232 | github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= 233 | github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 234 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 235 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 236 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 237 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 238 | github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= 239 | github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= 240 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 241 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 242 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 243 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 244 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 245 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 246 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 247 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 248 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 249 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 250 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 251 | github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= 252 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 253 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 254 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 255 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 256 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 257 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 258 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 259 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 260 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 261 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 262 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 263 | github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= 264 | github.com/prometheus/common v0.33.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE= 265 | github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= 266 | github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= 267 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 268 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 269 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 270 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 271 | github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 272 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 273 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 274 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 275 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 276 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 277 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 278 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 279 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 280 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 281 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 282 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 283 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 284 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 285 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 286 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 287 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 288 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 289 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 290 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 291 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 292 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 293 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 294 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 295 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 296 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 297 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 298 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 299 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 300 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 301 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 302 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 303 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 304 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 305 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 306 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 307 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 308 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 309 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 310 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 311 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 312 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 313 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 314 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 315 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 316 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 317 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 318 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 319 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 320 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 321 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 322 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 323 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 324 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 325 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 326 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 327 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 328 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 329 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 330 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 331 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 332 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 333 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 334 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 335 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 336 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 337 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 338 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 339 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 340 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 341 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 342 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 343 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 344 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 345 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 346 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 347 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 348 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 349 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 350 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 351 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 352 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 353 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 354 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 355 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 356 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 357 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 358 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 359 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 360 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 361 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 362 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 363 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 364 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 365 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 366 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 367 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 368 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 369 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 370 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 371 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 372 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 373 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 374 | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 375 | golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= 376 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 377 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 378 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 379 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 380 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 381 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 382 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 383 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 384 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 385 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 386 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 387 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 388 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 389 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 390 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 391 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 392 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 393 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 394 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 395 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 396 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 397 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 398 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 399 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 400 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 401 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 402 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 403 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 404 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 405 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 406 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 407 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 408 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 409 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 410 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 411 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 412 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 413 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 414 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 415 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 416 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 417 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 418 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 419 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 420 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 421 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 422 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 423 | golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 424 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 425 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 426 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 427 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 428 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 429 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 430 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 431 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 432 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 433 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 434 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 435 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 436 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 437 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 438 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 439 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 440 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 441 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 442 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 443 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 444 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 445 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 446 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 447 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 448 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 449 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 450 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 451 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 452 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 453 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 454 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 455 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 456 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 457 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 458 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 459 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 460 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 461 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 462 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 463 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 464 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 465 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 466 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 467 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 468 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 469 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 470 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 471 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 472 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 473 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 474 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 475 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 476 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 477 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 478 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 479 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 480 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 481 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= 482 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 483 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 484 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 485 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 486 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 487 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 488 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 489 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 490 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 491 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 492 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 493 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 494 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 495 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 496 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 497 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 498 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 499 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 500 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 501 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 502 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 503 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 504 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 505 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 506 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 507 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 508 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 509 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 510 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 511 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 512 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 513 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 514 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 515 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 516 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 517 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 518 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 519 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 520 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 521 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 522 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 523 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 524 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 525 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 526 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 527 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 528 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 529 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 530 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 531 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 532 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 533 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 534 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 535 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 536 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 537 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 538 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 539 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 540 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 541 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 542 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 543 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 544 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 545 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 546 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 547 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 548 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 549 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 550 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 551 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 552 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 553 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 554 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 555 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 556 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 557 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 558 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 559 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 560 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 561 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 562 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 563 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 564 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 565 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 566 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 567 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 568 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 569 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 570 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 571 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 572 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 573 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 574 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 575 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 576 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 577 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 578 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 579 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 580 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 581 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 582 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 583 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 584 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 585 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 586 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 587 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 588 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 589 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 590 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 591 | -------------------------------------------------------------------------------- /scripts/tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | ginkgo -race -r -skip "DockerImageResource" 6 | GOOS=linux ginkgo build -r tests/ 7 | docker run -it --privileged -v `pwd`:/docker-image-resource concourse/buildroot:iptables /docker-image-resource/tests/tests.test -------------------------------------------------------------------------------- /tests/check_test.go: -------------------------------------------------------------------------------- 1 | package docker_image_resource_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "os/exec" 8 | 9 | "encoding/json" 10 | "os" 11 | 12 | . "github.com/onsi/ginkgo/v2" 13 | . "github.com/onsi/gomega" 14 | "github.com/onsi/gomega/gbytes" 15 | "github.com/onsi/gomega/gexec" 16 | "github.com/onsi/gomega/ghttp" 17 | ) 18 | 19 | var _ = Describe("Check", func() { 20 | BeforeEach(func() { 21 | os.Setenv("PATH", "/docker-image-resource/tests/fixtures/bin:"+os.Getenv("PATH")) 22 | os.Setenv("SKIP_PRIVILEGED", "true") 23 | os.Setenv("LOG_FILE", "/dev/stderr") 24 | }) 25 | 26 | check := func(params map[string]any) *gexec.Session { 27 | command := exec.Command("/opt/resource/check", "/tmp") 28 | 29 | resourceInput, err := json.Marshal(params) 30 | Expect(err).ToNot(HaveOccurred()) 31 | 32 | command.Stdin = bytes.NewBuffer(resourceInput) 33 | 34 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 35 | Expect(err).ToNot(HaveOccurred()) 36 | <-session.Exited 37 | return session 38 | } 39 | 40 | It("errors when image is unknown", func() { 41 | repository := "kjlasdfaklklj12" 42 | tag := "latest" 43 | session := check(map[string]any{ 44 | "source": map[string]any{ 45 | "repository": repository, 46 | }, 47 | }) 48 | 49 | expectedStringInError := fmt.Sprintf("%s:%s", repository, tag) 50 | Expect(session.Err).To(gbytes.Say(expectedStringInError)) 51 | }) 52 | 53 | It("errors when image:tag is unknown", func() { 54 | repository := "kjlasdfaklklj12" 55 | tag := "aklsdf123" 56 | session := check(map[string]any{ 57 | "source": map[string]any{ 58 | "repository": repository, 59 | "tag": tag, 60 | }, 61 | }) 62 | 63 | expectedStringInError := fmt.Sprintf("%s:%s", repository, tag) 64 | Expect(session.Err).To(gbytes.Say(expectedStringInError)) 65 | }) 66 | 67 | It("prints out the digest for a existent image", func() { 68 | session := check(map[string]any{ 69 | "source": map[string]any{ 70 | "repository": "alpine", 71 | }, 72 | }) 73 | 74 | Expect(session.Out).To(gbytes.Say(`{"digest":`)) 75 | }) 76 | 77 | It("prints out the digest for a existent image and quoted numeric tag", func() { 78 | session := check(map[string]any{ 79 | "source": map[string]any{ 80 | "repository": "alpine", 81 | "tag": "3.7", 82 | }, 83 | }) 84 | 85 | Expect(session.Out).To(gbytes.Say(`{"digest":`)) 86 | }) 87 | 88 | It("prints out the digest for a existent image and numeric tag", func() { 89 | session := check(map[string]any{ 90 | "source": map[string]any{ 91 | "repository": "alpine", 92 | "tag": 3.7, 93 | }, 94 | }) 95 | 96 | Expect(session.Out).To(gbytes.Say(`{"digest":`)) 97 | }) 98 | 99 | Context("when a registry mirror is configured", func() { 100 | var ( 101 | registry *ghttp.Server 102 | session *gexec.Session 103 | latestFakeDigest string = "sha256:c4c25c2cd70e3071f08cf124c4b5c656c061dd38247d166d97098d58eeea8aa6" 104 | ) 105 | 106 | BeforeEach(func() { 107 | registry = ghttp.NewServer() 108 | 109 | BeforeEach(func() { 110 | registry.AppendHandlers( 111 | ghttp.CombineHandlers( 112 | ghttp.VerifyRequest("GET", "/v2/"), 113 | ghttp.RespondWith(http.StatusOK, "fake mirror"), 114 | ), 115 | ghttp.CombineHandlers( 116 | ghttp.VerifyRequest("HEAD", "/v2/some/fake-image/manifests/latest"), 117 | ghttp.VerifyHeaderKV("Accept", 118 | "application/vnd.docker.distribution.manifest.v2+json", 119 | "application/vnd.oci.image.index.v1+json", 120 | "application/json", 121 | ), 122 | ghttp.RespondWith(http.StatusOK, `{"fake":"manifest"}`, http.Header{ 123 | "Docker-Content-Digest": {latestFakeDigest}, 124 | }), 125 | ), 126 | ) 127 | }) 128 | 129 | AfterEach(func() { 130 | registry.Close() 131 | }) 132 | 133 | Context("when the repository contains no registry hostname", func() { 134 | BeforeEach(func() { 135 | session = check(map[string]any{ 136 | "source": map[string]any{ 137 | "repository": "some/fake-image", 138 | "registry_mirror": registry.URL(), 139 | }, 140 | }) 141 | }) 142 | 143 | It("fetches the image data from the mirror and prints out the digest and tag", func() { 144 | Expect(session.Out).To(gbytes.Say(fmt.Sprintf(`[{"digest":"%s"}]`, latestFakeDigest))) 145 | }) 146 | 147 | It("does not error", func() { 148 | Expect(session.Err).To(Equal("")) 149 | }) 150 | }) 151 | 152 | Context("when the repository contains a registry hostname different from the mirror", func() { 153 | BeforeEach(func() { 154 | session = check(map[string]any{ 155 | "source": map[string]any{ 156 | "repository": registry.URL() + "/some/fake-image", 157 | "registry_mirror": "https://thisregistrydoesnotexist.nothing", 158 | }, 159 | }) 160 | }) 161 | 162 | It("fetches the image data from the registry cited in the repository rather than the mirror and prints out the digest and tag", func() { 163 | Expect(session.Out).To(gbytes.Say(fmt.Sprintf(`[{"digest":"%s"}]`, latestFakeDigest))) 164 | }) 165 | 166 | It("does not error", func() { 167 | Expect(session.Err).To(Equal("")) 168 | }) 169 | }) 170 | }) 171 | }) 172 | }) 173 | -------------------------------------------------------------------------------- /tests/docker_image_resource_suite_test.go: -------------------------------------------------------------------------------- 1 | package docker_image_resource_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestDockerImageResource(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "DockerImageResource Suite") 13 | } 14 | -------------------------------------------------------------------------------- /tests/fixtures/bin/docker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "DOCKER:" "$@" 3 | 4 | for var in "$@"; do 5 | echo "DOCKER ARG:" "$var" 6 | done 7 | 8 | pidfile=/tmp/docker.pid 9 | if [ -f "$pidfile" ] && ! kill -0 $(<"$pidfile") &>/dev/null; then 10 | echo "Docker daemon is not runnning!" >&2 11 | exit 1 12 | fi 13 | 14 | if [ "$1" == "info" ] && [ -f /tmp/docker_failing ]; then 15 | exit 1 16 | fi 17 | 18 | if [ "$1" == "pull" ] && [ "$2" == "broken-repo:latest" ]; then 19 | exit 1 20 | fi 21 | -------------------------------------------------------------------------------- /tests/fixtures/bin/dockerd: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "DOCKERD:" "$@" 4 | 5 | if [ -f /tmp/docker_failing ]; then 6 | rm /tmp/docker_failing 7 | elif [ "$FAIL_ONCE" == "true" ]; then 8 | touch /tmp/docker_failing 9 | exec sleep 1 10 | fi 11 | 12 | exec sleep 999 13 | -------------------------------------------------------------------------------- /tests/fixtures/build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | -------------------------------------------------------------------------------- /tests/fixtures/build_args: -------------------------------------------------------------------------------- 1 | { 2 | "arg1": "arg with space", 3 | "arg2": "arg with\nnewline", 4 | "arg3": "normal" 5 | } -------------------------------------------------------------------------------- /tests/fixtures/build_args_malformed: -------------------------------------------------------------------------------- 1 | {foo} -------------------------------------------------------------------------------- /tests/fixtures/ecr/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM 123123.dkr.ecr.us-west-2.amazonaws.com:443/testing 2 | -------------------------------------------------------------------------------- /tests/fixtures/ecr/Dockerfile.multi: -------------------------------------------------------------------------------- 1 | FROM ubuntu as u 2 | 3 | FROM 123123.dkr.ecr.us-west-2.amazonaws.com:443/testing as test 4 | 5 | FROM ubuntu 6 | 7 | -------------------------------------------------------------------------------- /tests/fixtures/ecr/Dockerfile.multi-ecr: -------------------------------------------------------------------------------- 1 | FROM ubuntu as u 2 | 3 | FROM 123123.dkr.ecr.us-west-2.amazonaws.com:443/testing as test 4 | 5 | FROM 123123.dkr.ecr.us-west-2.amazonaws.com:443/testing2 as test 6 | 7 | -------------------------------------------------------------------------------- /tests/fixtures/tag: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /tests/fixtures/tags: -------------------------------------------------------------------------------- 1 | a b c 2 | -------------------------------------------------------------------------------- /tests/out_test.go: -------------------------------------------------------------------------------- 1 | package docker_image_resource_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os/exec" 7 | 8 | "encoding/json" 9 | "os" 10 | 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | "github.com/onsi/gomega/gbytes" 14 | "github.com/onsi/gomega/gexec" 15 | ) 16 | 17 | var _ = Describe("Out", func() { 18 | BeforeEach(func() { 19 | os.Setenv("PATH", "/docker-image-resource/tests/fixtures/bin:"+os.Getenv("PATH")) 20 | os.Setenv("SKIP_PRIVILEGED", "true") 21 | os.Setenv("LOG_FILE", "/dev/stderr") 22 | }) 23 | 24 | putWithEnv := func(params map[string]any, extraEnv map[string]string) *gexec.Session { 25 | command := exec.Command("/opt/resource/out", "/tmp") 26 | 27 | // Get current process environment variables 28 | newEnv := os.Environ() 29 | if extraEnv != nil { 30 | // Append each extra environment variable to new process environment 31 | // variable list 32 | for name, value := range extraEnv { 33 | newEnv = append(newEnv, fmt.Sprintf("%s=%s", name, value)) 34 | } 35 | } 36 | 37 | command.Env = newEnv 38 | 39 | resourceInput, err := json.Marshal(params) 40 | Expect(err).ToNot(HaveOccurred()) 41 | 42 | command.Stdin = bytes.NewBuffer(resourceInput) 43 | 44 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 45 | Expect(err).ToNot(HaveOccurred()) 46 | <-session.Exited 47 | return session 48 | } 49 | 50 | put := func(params map[string]any) *gexec.Session { 51 | return putWithEnv(params, nil) 52 | } 53 | 54 | dockerarg := func(cmd string) string { 55 | return "DOCKER ARG: " + cmd 56 | } 57 | 58 | docker := func(cmd string) string { 59 | return "DOCKER: " + cmd 60 | } 61 | 62 | dockerd := func(cmd string) string { 63 | return "DOCKERD: " + cmd 64 | } 65 | 66 | It("retries starting dockerd if it fails", func() { 67 | session := putWithEnv(map[string]any{ 68 | "source": map[string]any{ 69 | "repository": "test", 70 | }, 71 | "params": map[string]any{ 72 | "build": "/docker-image-resource/tests/fixtures/build", 73 | }, 74 | }, map[string]string{ 75 | "STARTUP_TIMEOUT": "5", 76 | "FAIL_ONCE": "true", 77 | }) 78 | 79 | Expect(session.Err).To(gbytes.Say("(?s:DOCKERD.*DOCKERD.*)")) 80 | }) 81 | 82 | It("times out retrying dockerd", func() { 83 | session := putWithEnv(map[string]any{ 84 | "source": map[string]any{ 85 | "repository": "test", 86 | }, 87 | "params": map[string]any{ 88 | "build": "/docker-image-resource/tests/fixtures/build", 89 | }, 90 | }, map[string]string{ 91 | "STARTUP_TIMEOUT": "1", 92 | "FAIL_ONCE": "true", 93 | }) 94 | 95 | Expect(session.Err).To(gbytes.Say(".*Docker failed to start.*")) 96 | }) 97 | 98 | It("starts dockerd with --data-root under /scratch", func() { 99 | session := put(map[string]any{ 100 | "source": map[string]any{ 101 | "repository": "test", 102 | }, 103 | "params": map[string]any{ 104 | "build": "/docker-image-resource/tests/fixtures/build", 105 | }, 106 | }) 107 | 108 | Expect(session.Err).To(gbytes.Say(dockerd(`.*--data-root /scratch/docker.*`))) 109 | }) 110 | 111 | Context("when build arguments are provided", func() { 112 | It("passes the arguments correctly to the docker daemon", func() { 113 | session := put(map[string]any{ 114 | "source": map[string]any{ 115 | "repository": "test", 116 | }, 117 | "params": map[string]any{ 118 | "build": "/docker-image-resource/tests/fixtures/build", 119 | "build_args": map[string]string{ 120 | "arg1": "arg with space", 121 | "arg2": "arg with\nnewline", 122 | "arg3": "normal", 123 | }, 124 | }, 125 | }) 126 | 127 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 128 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg1=arg with space`))) 129 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 130 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg2=arg with\nnewline`))) 131 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 132 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg3=normal`))) 133 | }) 134 | }) 135 | 136 | Context("when secrets are provided", func() { 137 | It("passes the arguments correctly to the docker daemon", func() { 138 | session := put(map[string]any{ 139 | "source": map[string]any{ 140 | "repository": "test", 141 | }, 142 | "params": map[string]any{ 143 | "build": "/docker-image-resource/tests/fixtures/build", 144 | "secrets": map[string]any{ 145 | "secret1": map[string]any{ 146 | "env": "GITHUB_TOKEN", 147 | }, 148 | "secret2": map[string]any{ 149 | "source": "/a/file/path.txt", 150 | }, 151 | "secret3": map[string]any{ 152 | "source": "/a/file/path with a space in it.txt", 153 | }, 154 | }, 155 | }, 156 | }) 157 | 158 | Expect(session.Err).To(gbytes.Say(dockerarg(`--secret`))) 159 | Expect(session.Err).To(gbytes.Say(dockerarg(`id=secret1,env=GITHUB_TOKEN`))) 160 | Expect(session.Err).To(gbytes.Say(dockerarg(`--secret`))) 161 | Expect(session.Err).To(gbytes.Say(dockerarg(`id=secret2,source=/a/file/path.txt`))) 162 | Expect(session.Err).To(gbytes.Say(dockerarg(`--secret`))) 163 | Expect(session.Err).To(gbytes.Say(dockerarg(`id=secret3,source=/a/file/path with a space in it.txt`))) 164 | }) 165 | }) 166 | 167 | Context("when labels are provided", func() { 168 | It("passes the labels correctly to the docker daemon", func() { 169 | session := put(map[string]any{ 170 | "source": map[string]any{ 171 | "repository": "test", 172 | }, 173 | "params": map[string]any{ 174 | "build": "/docker-image-resource/tests/fixtures/build", 175 | "labels": map[string]string{ 176 | "label1": "foo", 177 | "label2": "bar\nspam", 178 | "label3": "eggs eggs eggs", 179 | }, 180 | }, 181 | }) 182 | 183 | Expect(session.Err).To(gbytes.Say(dockerarg(`--label`))) 184 | Expect(session.Err).To(gbytes.Say(dockerarg(`label1=foo`))) 185 | Expect(session.Err).To(gbytes.Say(dockerarg(`--label`))) 186 | Expect(session.Err).To(gbytes.Say(dockerarg(`label2=bar\nspam`))) 187 | Expect(session.Err).To(gbytes.Say(dockerarg(`--label`))) 188 | Expect(session.Err).To(gbytes.Say(dockerarg(`label3=eggs eggs eggs`))) 189 | }) 190 | }) 191 | 192 | Context("when build arguments file is provided", func() { 193 | It("passes the arguments correctly to the docker daemon", func() { 194 | session := put(map[string]any{ 195 | "source": map[string]any{ 196 | "repository": "test", 197 | }, 198 | "params": map[string]any{ 199 | "build": "/docker-image-resource/tests/fixtures/build", 200 | "build_args_file": "/docker-image-resource/tests/fixtures/build_args", 201 | }, 202 | }) 203 | 204 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 205 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg1=arg with space`))) 206 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 207 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg2=arg with\nnewline`))) 208 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 209 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg3=normal`))) 210 | }) 211 | }) 212 | 213 | // this is close, but this test is broken. Not sure look for output 214 | Context("when build arguments file is malformed", func() { 215 | It("provides a useful error message", func() { 216 | session := put(map[string]any{ 217 | "source": map[string]any{ 218 | "repository": "test", 219 | }, 220 | "params": map[string]any{ 221 | "build": "/docker-image-resource/tests/fixtures/build", 222 | "build_args_file": "/docker-image-resource/tests/fixtures/build_args_malformed", 223 | }, 224 | }) 225 | 226 | Expect(session.Err).To(gbytes.Say(`Failed to parse build_args_file \(/docker-image-resource/tests/fixtures/build_args_malformed\)`)) 227 | }) 228 | }) 229 | 230 | Context("when build arguments contain envvars", func() { 231 | It("expands envvars and passes the arguments correctly to the docker daemon", func() { 232 | session := putWithEnv(map[string]any{ 233 | "source": map[string]any{ 234 | "repository": "test", 235 | }, 236 | "params": map[string]any{ 237 | "build": "/docker-image-resource/tests/fixtures/build", 238 | "build_args": map[string]string{ 239 | "arg01": "no envvars", 240 | "arg02": "this is the:\n$BUILD_ID", 241 | "arg03": "this is the:\n$BUILD_NAME", 242 | "arg04": "this is the:\n$BUILD_JOB_NAME", 243 | "arg05": "this is the:\n$BUILD_PIPELINE_NAME", 244 | "arg06": "this is the:\n$BUILD_TEAM_NAME", 245 | "arg07": "this is the:\n$ATC_EXTERNAL_URL", 246 | "arg08": "this syntax also works:\n${ATC_EXTERNAL_URL}", 247 | "arg09": "multiple envvars in one arg also works:\n$BUILD_ID\n$BUILD_NAME", 248 | "arg10": "$BUILD_ID at the beginning of the arg", 249 | "arg11": "at the end of the arg is the $BUILD_ID", 250 | }, 251 | }, 252 | }, map[string]string{ 253 | "BUILD_ID": "value of the:\nBUILD_ID envvar", 254 | "BUILD_NAME": "value of the:\nBUILD_NAME envvar", 255 | "BUILD_JOB_NAME": "value of the:\nBUILD_JOB_NAME envvar", 256 | "BUILD_PIPELINE_NAME": "value of the:\nBUILD_PIPELINE_NAME envvar", 257 | "BUILD_TEAM_NAME": "value of the:\nBUILD_TEAM_NAME envvar", 258 | "ATC_EXTERNAL_URL": "value of the:\nATC_EXTERNAL_URL envvar", 259 | }) 260 | 261 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 262 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg01=no envvars`))) 263 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 264 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg02=this is the:\nvalue of the:\nBUILD_ID envvar`))) 265 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 266 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg03=this is the:\nvalue of the:\nBUILD_NAME envvar`))) 267 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 268 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg04=this is the:\nvalue of the:\nBUILD_JOB_NAME envvar`))) 269 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 270 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg05=this is the:\nvalue of the:\nBUILD_PIPELINE_NAME envvar`))) 271 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 272 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg06=this is the:\nvalue of the:\nBUILD_TEAM_NAME envvar`))) 273 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 274 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg07=this is the:\nvalue of the:\nATC_EXTERNAL_URL envvar`))) 275 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 276 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg08=this syntax also works:\nvalue of the:\nATC_EXTERNAL_URL envvar`))) 277 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 278 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg09=multiple envvars in one arg also works:\nvalue of the:\nBUILD_ID envvar\nvalue of the:\nBUILD_NAME envvar`))) 279 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 280 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg10=value of the:\nBUILD_ID envvar at the beginning of the arg`))) 281 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 282 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg11=at the end of the arg is the value of the:\nBUILD_ID envvar`))) 283 | }) 284 | }) 285 | 286 | Context("when configured with limited up and download", func() { 287 | It("passes them to dockerd", func() { 288 | session := put(map[string]any{ 289 | "source": map[string]any{ 290 | "repository": "test", 291 | "max_concurrent_downloads": 7, 292 | "max_concurrent_uploads": 1, 293 | }, 294 | "params": map[string]any{ 295 | "build": "/docker-image-resource/tests/fixtures/build", 296 | }, 297 | }) 298 | 299 | Expect(session.Err).To(gbytes.Say(dockerd(`.* --max-concurrent-downloads=7 --max-concurrent-uploads=1.*`))) 300 | }) 301 | }) 302 | 303 | Context("when configured with additional private registries", func() { 304 | It("passes them to docker login", func() { 305 | session := put(map[string]any{ 306 | "source": map[string]any{ 307 | "repository": "test", 308 | "additional_private_registries": []any{ 309 | map[string]string{ 310 | "registry": "example.com/my-private-docker-registry", 311 | "username": "my-username", 312 | "password": "my-secret", 313 | }, 314 | map[string]string{ 315 | "registry": "example.com/another-private-docker-registry", 316 | "username": "another-username", 317 | "password": "another-secret", 318 | }, 319 | }, 320 | }, 321 | "params": map[string]any{ 322 | "build": "/docker-image-resource/tests/fixtures/build", 323 | }, 324 | }) 325 | 326 | Expect(session.Err).To(gbytes.Say(dockerarg(`login`))) 327 | Expect(session.Err).To(gbytes.Say(dockerarg(`-u`))) 328 | Expect(session.Err).To(gbytes.Say(dockerarg(`my-username`))) 329 | Expect(session.Err).To(gbytes.Say(dockerarg(`--password-stdin`))) 330 | Expect(session.Err).To(gbytes.Say(dockerarg(`example.com/my-private-docker-registry`))) 331 | Expect(session.Err).To(gbytes.Say(dockerarg(`login`))) 332 | Expect(session.Err).To(gbytes.Say(dockerarg(`-u`))) 333 | Expect(session.Err).To(gbytes.Say(dockerarg(`another-username`))) 334 | Expect(session.Err).To(gbytes.Say(dockerarg(`--password-stdin`))) 335 | Expect(session.Err).To(gbytes.Say(dockerarg(`example.com/another-private-docker-registry`))) 336 | }) 337 | }) 338 | 339 | Context("when configured with a insecure registries", func() { 340 | It("passes them to dockerd", func() { 341 | session := put(map[string]any{ 342 | "source": map[string]any{ 343 | "repository": "test", 344 | "insecure_registries": []string{"my-registry.gov", "other-registry.biz"}, 345 | }, 346 | "params": map[string]any{ 347 | "build": "/docker-image-resource/tests/fixtures/build", 348 | }, 349 | }) 350 | 351 | Expect(session.Err).To(gbytes.Say(dockerd(`.*--insecure-registry my-registry\.gov --insecure-registry other-registry\.biz.*`))) 352 | }) 353 | }) 354 | 355 | Context("when configured with a registry mirror", func() { 356 | It("passes it to dockerd", func() { 357 | session := put(map[string]any{ 358 | "source": map[string]any{ 359 | "repository": "test", 360 | "registry_mirror": "some-mirror", 361 | }, 362 | "params": map[string]any{ 363 | "build": "/docker-image-resource/tests/fixtures/build", 364 | }, 365 | }) 366 | 367 | Expect(session.Err).To(gbytes.Say(dockerd(`.*--registry-mirror some-mirror.*`))) 368 | }) 369 | }) 370 | 371 | Context("when configured with a target build stage", func() { 372 | It("passes it to dockerd", func() { 373 | session := put(map[string]any{ 374 | "source": map[string]any{ 375 | "repository": "test", 376 | }, 377 | "params": map[string]any{ 378 | "target_name": "test", 379 | "build": "/docker-image-resource/tests/fixtures/build", 380 | }, 381 | }) 382 | 383 | Expect(session.Err).To(gbytes.Say(dockerarg(`--target`))) 384 | Expect(session.Err).To(gbytes.Say(dockerarg(`test`))) 385 | }) 386 | }) 387 | 388 | Context("When using ECR", func() { 389 | It("calls docker pull with the ECR registry", func() { 390 | session := put(map[string]any{ 391 | "source": map[string]any{ 392 | "repository": "test", 393 | }, 394 | "params": map[string]any{ 395 | "build": "/docker-image-resource/tests/fixtures/ecr", 396 | "dockerfile": "/docker-image-resource/tests/fixtures/ecr/Dockerfile", 397 | }, 398 | }) 399 | 400 | Expect(session.Err).To(gbytes.Say(docker("pull 123123.dkr.ecr.us-west-2.amazonaws.com:443/testing"))) 401 | }) 402 | 403 | It("calls docker pull for an ECR image in a multi build docker file", func() { 404 | session := put(map[string]any{ 405 | "source": map[string]any{ 406 | "repository": "test", 407 | }, 408 | "params": map[string]any{ 409 | "build": "/docker-image-resource/tests/fixtures/ecr", 410 | "dockerfile": "/docker-image-resource/tests/fixtures/ecr/Dockerfile.multi", 411 | }, 412 | }) 413 | 414 | Expect(session.Err).To(gbytes.Say(docker("pull 123123.dkr.ecr.us-west-2.amazonaws.com:443/testing"))) 415 | }) 416 | 417 | It("calls docker pull for all ECR images in a multi build docker file", func() { 418 | session := put(map[string]any{ 419 | "source": map[string]any{ 420 | "repository": "test", 421 | }, 422 | "params": map[string]any{ 423 | "build": "/docker-image-resource/tests/fixtures/ecr", 424 | "dockerfile": "/docker-image-resource/tests/fixtures/ecr/Dockerfile.multi-ecr", 425 | }, 426 | }) 427 | 428 | Expect(session.Err).To(gbytes.Say(docker("pull 123123.dkr.ecr.us-west-2.amazonaws.com:443/testing"))) 429 | Expect(session.Err).To(gbytes.Say(docker("pull 123123.dkr.ecr.us-west-2.amazonaws.com:443/testing2"))) 430 | }) 431 | }) 432 | 433 | Context("When all proxy settings are provided with build args", func() { 434 | It("passes the arguments correctly to the docker daemon", func() { 435 | session := putWithEnv(map[string]any{ 436 | "source": map[string]any{ 437 | "repository": "test", 438 | }, 439 | "params": map[string]any{ 440 | "build": "/docker-image-resource/tests/fixtures/build", 441 | "build_args": map[string]string{ 442 | "arg1": "arg with space", 443 | "arg2": "arg with\nnewline", 444 | "arg3": "normal", 445 | }, 446 | }, 447 | }, map[string]string{ 448 | "no_proxy": "10.1.1.1", 449 | "http_proxy": "http://admin:verysecret@my-proxy.com:8080", 450 | "https_proxy": "http://another.proxy.net", 451 | }) 452 | 453 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 454 | Expect(session.Err).To(gbytes.Say(dockerarg(`http_proxy=http://admin:verysecret@my-proxy.com:8080`))) 455 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 456 | Expect(session.Err).To(gbytes.Say(dockerarg(`https_proxy=http://another.proxy.net`))) 457 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 458 | Expect(session.Err).To(gbytes.Say(dockerarg(`no_proxy=10.1.1.1`))) 459 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 460 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg1=arg with space`))) 461 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 462 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg2=arg with\nnewline`))) 463 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 464 | Expect(session.Err).To(gbytes.Say(dockerarg(`arg3=normal`))) 465 | }) 466 | }) 467 | 468 | Context("When passing tag ", func() { 469 | It("should pull tag from file", func() { 470 | session := put(map[string]any{ 471 | "source": map[string]any{ 472 | "repository": "test", 473 | }, 474 | "params": map[string]any{ 475 | "build": "/docker-image-resource/tests/fixtures/build", 476 | "tag": "/docker-image-resource/tests/fixtures/tag", 477 | }, 478 | }, 479 | ) 480 | Expect(session.Err).To(gbytes.Say(docker(`push test:foo`))) 481 | }) 482 | }) 483 | 484 | Context("When passing tag_file", func() { 485 | It("should pull tag from file", func() { 486 | session := put(map[string]any{ 487 | "source": map[string]any{ 488 | "repository": "test", 489 | }, 490 | "params": map[string]any{ 491 | "build": "/docker-image-resource/tests/fixtures/build", 492 | "tag_file": "/docker-image-resource/tests/fixtures/tag", 493 | }, 494 | }, 495 | ) 496 | Expect(session.Err).To(gbytes.Say(docker(`push test:foo`))) 497 | }) 498 | }) 499 | 500 | Context("When passing tag and tag_file", func() { 501 | It("should pull tag from file (prefer tag_file param)", func() { 502 | session := put(map[string]any{ 503 | "source": map[string]any{ 504 | "repository": "test", 505 | }, 506 | "params": map[string]any{ 507 | "build": "/docker-image-resource/tests/fixtures/build", 508 | "tag": "/docker-image-resource/tests/fixtures/doesnotexist", 509 | "tag_file": "/docker-image-resource/tests/fixtures/tag", 510 | }, 511 | }, 512 | ) 513 | Expect(session.Err).To(gbytes.Say(docker(`push test:foo`))) 514 | }) 515 | }) 516 | 517 | Context("When passing additional_tags ", func() { 518 | It("should push add the additional_tags", func() { 519 | session := put(map[string]any{ 520 | "source": map[string]any{ 521 | "repository": "test", 522 | }, 523 | "params": map[string]any{ 524 | "build": "/docker-image-resource/tests/fixtures/build", 525 | "additional_tags": "/docker-image-resource/tests/fixtures/tags", 526 | }, 527 | }, 528 | ) 529 | Expect(session.Err).To(gbytes.Say(docker(`push test:latest`))) 530 | Expect(session.Err).To(gbytes.Say(docker(`tag test:latest test:a`))) 531 | Expect(session.Err).To(gbytes.Say(docker(`push test:a`))) 532 | Expect(session.Err).To(gbytes.Say(docker(`tag test:latest test:b`))) 533 | Expect(session.Err).To(gbytes.Say(docker(`push test:b`))) 534 | Expect(session.Err).To(gbytes.Say(docker(`tag test:latest test:c`))) 535 | Expect(session.Err).To(gbytes.Say(docker(`push test:c`))) 536 | }) 537 | }) 538 | 539 | Context("When only http_proxy setting is provided, with no build arguments", func() { 540 | It("passes the arguments correctly to the docker daemon", func() { 541 | session := putWithEnv(map[string]any{ 542 | "source": map[string]any{ 543 | "repository": "test", 544 | }, 545 | "params": map[string]any{ 546 | "build": "/docker-image-resource/tests/fixtures/build", 547 | }, 548 | }, map[string]string{ 549 | "http_proxy": "http://admin:verysecret@my-proxy.com:8080", 550 | }) 551 | 552 | Expect(session.Err).To(gbytes.Say(dockerarg(`--build-arg`))) 553 | Expect(session.Err).To(gbytes.Say(dockerarg(`http_proxy=http://admin:verysecret@my-proxy.com:8080`))) 554 | }) 555 | }) 556 | 557 | Context("when load_bases are specified", func() { 558 | BeforeEach(func() { 559 | os.Mkdir("/tmp/expected_base_1", os.ModeDir) 560 | // this image should really be an actual tarball, but the test passes with text. :shrug: 561 | os.WriteFile("/tmp/expected_base_1/image", []byte("some-image-1"), os.ModePerm) 562 | os.WriteFile("/tmp/expected_base_1/repository", []byte("some-repository-1"), os.ModePerm) 563 | os.WriteFile("/tmp/expected_base_1/image-id", []byte("some-image-id-1"), os.ModePerm) 564 | os.WriteFile("/tmp/expected_base_1/tag", []byte("some-tag-1"), os.ModePerm) 565 | 566 | os.Mkdir("/tmp/expected_base_2", os.ModeDir) 567 | os.WriteFile("/tmp/expected_base_2/image", []byte("some-image-2"), os.ModePerm) 568 | os.WriteFile("/tmp/expected_base_2/repository", []byte("some-repository-2"), os.ModePerm) 569 | os.WriteFile("/tmp/expected_base_2/image-id", []byte("some-image-id-2"), os.ModePerm) 570 | os.WriteFile("/tmp/expected_base_2/tag", []byte("some-tag-2"), os.ModePerm) 571 | 572 | os.Mkdir("/tmp/unexpected_base", os.ModeDir) 573 | os.WriteFile("/tmp/unexpected_base/image", []byte("some-image-3"), os.ModePerm) 574 | os.WriteFile("/tmp/unexpected_base/repository", []byte("some-repository-3"), os.ModePerm) 575 | os.WriteFile("/tmp/unexpected_base/image-id", []byte("some-image-id-3"), os.ModePerm) 576 | os.WriteFile("/tmp/unexpected_base/tag", []byte("some-tag-3"), os.ModePerm) 577 | }) 578 | 579 | AfterEach(func() { 580 | os.RemoveAll("/tmp/expected_base_1") 581 | os.RemoveAll("/tmp/expected_base_2") 582 | os.RemoveAll("/tmp/unexpected_base") 583 | }) 584 | 585 | It("passes the arguments correctly to the docker daemon", func() { 586 | session := put(map[string]any{ 587 | "source": map[string]any{ 588 | "repository": "test", 589 | }, 590 | "params": map[string]any{ 591 | "build": "/docker-image-resource/tests/fixtures/build", 592 | "load_bases": []string{"expected_base_1", "expected_base_2"}, 593 | }, 594 | }) 595 | 596 | Expect(session.Err).To(gbytes.Say(docker(`load -i expected_base_1/image`))) 597 | Expect(session.Err).To(gbytes.Say(docker(`tag some-image-id-1 some-repository-1:some-tag-1`))) 598 | 599 | Expect(session.Err).To(gbytes.Say(docker(`load -i expected_base_2/image`))) 600 | Expect(session.Err).To(gbytes.Say(docker(`tag some-image-id-2 some-repository-2:some-tag-2`))) 601 | 602 | Expect(session.Err).NotTo(gbytes.Say(docker(`load -i unexpected_base/image`))) 603 | Expect(session.Err).NotTo(gbytes.Say(docker(`tag some-image-id-3 some-repository-3:some-tag-3`))) 604 | }) 605 | }) 606 | 607 | Context("when cache is specified are specified", func() { 608 | It("adds argument to cache_from", func() { 609 | session := put(map[string]any{ 610 | "source": map[string]any{ 611 | "repository": "test", 612 | }, 613 | "params": map[string]any{ 614 | "build": "/docker-image-resource/tests/fixtures/build", 615 | "cache": "true", 616 | }, 617 | }) 618 | Expect(session.Err).To(gbytes.Say(dockerarg(`--cache-from`))) 619 | Expect(session.Err).To(gbytes.Say(dockerarg(`test:latest`))) 620 | }) 621 | 622 | It("does not add cache_from if pull fails", func() { 623 | session := put(map[string]any{ 624 | "source": map[string]any{ 625 | "repository": "broken-repo", 626 | }, 627 | "params": map[string]any{ 628 | "build": "/docker-image-resource/tests/fixtures/build", 629 | "cache": "true", 630 | }, 631 | }) 632 | 633 | Expect(session.Err).ToNot(gbytes.Say(dockerarg(`--cache-from`))) 634 | Expect(session.Err).To(gbytes.Say(dockerarg(`broken-repo:latest`))) 635 | Expect(session.Err).To(gbytes.Say(dockerarg(`build`))) 636 | 637 | }) 638 | }) 639 | 640 | Context("when cache_from images are specified", func() { 641 | BeforeEach(func() { 642 | os.Mkdir("/tmp/cache_from_1", os.ModeDir) 643 | // this image should really be an actual tarball, but the test passes with text. :shrug: 644 | os.WriteFile("/tmp/cache_from_1/image", []byte("some-image-1"), os.ModePerm) 645 | os.WriteFile("/tmp/cache_from_1/repository", []byte("some-repository-1"), os.ModePerm) 646 | os.WriteFile("/tmp/cache_from_1/image-id", []byte("some-image-id-1"), os.ModePerm) 647 | os.WriteFile("/tmp/cache_from_1/tag", []byte("some-tag-1"), os.ModePerm) 648 | 649 | os.Mkdir("/tmp/cache_from_2", os.ModeDir) 650 | os.WriteFile("/tmp/cache_from_2/image", []byte("some-image-2"), os.ModePerm) 651 | os.WriteFile("/tmp/cache_from_2/repository", []byte("some-repository-2"), os.ModePerm) 652 | os.WriteFile("/tmp/cache_from_2/image-id", []byte("some-image-id-2"), os.ModePerm) 653 | os.WriteFile("/tmp/cache_from_2/tag", []byte("some-tag-2"), os.ModePerm) 654 | }) 655 | 656 | AfterEach(func() { 657 | os.RemoveAll("/tmp/cache_from_1") 658 | os.RemoveAll("/tmp/cache_from_2") 659 | }) 660 | 661 | It("calls docker load to load the cache_from images", func() { 662 | session := put(map[string]any{ 663 | "source": map[string]any{ 664 | "repository": "test", 665 | }, 666 | "params": map[string]any{ 667 | "build": "/docker-image-resource/tests/fixtures/build", 668 | "cache_from": []string{"cache_from_1", "cache_from_2"}, 669 | }, 670 | }) 671 | Expect(session.Err).To(gbytes.Say(docker(`load -i cache_from_1/image`))) 672 | Expect(session.Err).To(gbytes.Say(docker(`tag some-image-id-1 some-repository-1:some-tag-1`))) 673 | 674 | Expect(session.Err).To(gbytes.Say(docker(`load -i cache_from_2/image`))) 675 | Expect(session.Err).To(gbytes.Say(docker(`tag some-image-id-2 some-repository-2:some-tag-2`))) 676 | }) 677 | 678 | It("loads both cache_from and load_bases images", func() { 679 | session := put(map[string]any{ 680 | "source": map[string]any{ 681 | "repository": "test", 682 | }, 683 | "params": map[string]any{ 684 | "build": "/docker-image-resource/tests/fixtures/build", 685 | "load_bases": []string{"cache_from_1"}, 686 | "cache_from": []string{"cache_from_2"}, 687 | }, 688 | }) 689 | Expect(session.Err).To(gbytes.Say(docker(`load -i cache_from_1/image`))) 690 | Expect(session.Err).To(gbytes.Say(docker(`tag some-image-id-1 some-repository-1:some-tag-1`))) 691 | 692 | Expect(session.Err).To(gbytes.Say(docker(`load -i cache_from_2/image`))) 693 | Expect(session.Err).To(gbytes.Say(docker(`tag some-image-id-2 some-repository-2:some-tag-2`))) 694 | }) 695 | 696 | It("passes the arguments correctly to the docker build command", func() { 697 | session := put(map[string]any{ 698 | "source": map[string]any{ 699 | "repository": "test", 700 | }, 701 | "params": map[string]any{ 702 | "build": "/docker-image-resource/tests/fixtures/build", 703 | "cache_from": []string{"cache_from_1", "cache_from_2"}, 704 | }, 705 | }) 706 | 707 | Expect(session.Err).To(gbytes.Say(dockerarg(`--cache-from`))) 708 | Expect(session.Err).To(gbytes.Say(dockerarg(`some-repository-1:some-tag-1`))) 709 | Expect(session.Err).To(gbytes.Say(dockerarg(`--cache-from`))) 710 | Expect(session.Err).To(gbytes.Say(dockerarg(`some-repository-2:some-tag-2`))) 711 | }) 712 | 713 | It("does not remove the arguments generated by cache:true", func() { 714 | session := put(map[string]any{ 715 | "source": map[string]any{ 716 | "repository": "test", 717 | }, 718 | "params": map[string]any{ 719 | "build": "/docker-image-resource/tests/fixtures/build", 720 | "cache": "true", 721 | "cache_from": []string{"cache_from_1", "cache_from_2"}, 722 | }, 723 | }) 724 | 725 | Expect(session.Err).To(gbytes.Say(dockerarg(`--cache-from`))) 726 | Expect(session.Err).To(gbytes.Say(dockerarg(`test:latest`))) 727 | Expect(session.Err).To(gbytes.Say(dockerarg(`--cache-from`))) 728 | Expect(session.Err).To(gbytes.Say(dockerarg(`some-repository-1:some-tag-1`))) 729 | Expect(session.Err).To(gbytes.Say(dockerarg(`--cache-from`))) 730 | Expect(session.Err).To(gbytes.Say(dockerarg(`some-repository-2:some-tag-2`))) 731 | }) 732 | }) 733 | }) 734 | --------------------------------------------------------------------------------