├── .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 |
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 |
--------------------------------------------------------------------------------