├── .dockerignore ├── .github └── workflows │ └── test.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── contrib ├── buildinfo_to_kusto.json ├── kusto.create.kql ├── lookup.json ├── mod-builtin.json └── mod.sh ├── docker.go ├── docker_test.go ├── generate.go ├── go.mod ├── go.sum ├── main.go ├── path.go ├── resolver.go └── test ├── docker_test.go ├── main_test.go ├── mod_test.go └── util_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .gitignore 3 | .git 4 | Dockerfile 5 | gnarly 6 | docker 7 | contrib/ 8 | bin/ 9 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "main" ] 7 | pull_request: 8 | branches: [ "main" ] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.18 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Create builder 25 | run: docker buildx create --use && docker buildx inspect --bootstrap 26 | 27 | - name: Test 28 | run: TEST_ALLOW_ALT_META=1 go test -v ./... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docker 2 | gnarly 3 | bin/ 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.18 2 | FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION} AS build 3 | WORKDIR /go/src/github.com/deislabs/gnarly 4 | COPY go.mod . 5 | COPY go.sum . 6 | RUN \ 7 | --mount=type=cache,target=/go/pkg/mod \ 8 | --mount=type=cache,target=/root/.cache/go-build,id=go-build-cache-${TARGETPLATFORM} \ 9 | go mod download 10 | COPY . . 11 | ARG TARGETPLATFORM TARGETOS TARGETARCH TARGETVARIANT 12 | ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH} 13 | SHELL ["/bin/bash", "-xec"] 14 | RUN \ 15 | --mount=type=cache,target=/go/pkg/mod \ 16 | --mount=type=cache,target=/root/.cache/go-build,id=go-build-cache-${TARGETPLATFORM} \ 17 | GOARM=${TARGETVARIANT#v} CGO_ENABLED=0 go build . 18 | 19 | FROM scratch 20 | COPY --from=build /go/src/github.com/deislabs/gnarly/gnarly / -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TEST_COUNT ?= 1 2 | TEST ?= go test -count=$(TEST_COUNT) $(TEST_FLAGS) $(if $(V),-v,) 3 | 4 | .PHONY: gnarly 5 | gnarly: 6 | CGO_ENABLED=0 go build $(if $(OUTPUT),-o $(OUTPUT)/$(@),) . 7 | 8 | clean: 9 | rm gnarly 10 | 11 | .PHONY: e2e 12 | e2e: gnarly 13 | $(TEST) ./test 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a tool to get a list of image sources out of a Dockerfile. 2 | 3 | It can also optionally run another program to add a "replace" directive to test out a feature I'm working on to replace images with a different source reference. 4 | 5 | This tool only generates metadata that can be fed into other tools. 6 | It is based on a lot of in progress work and is only here as a placeholder. 7 | Some things are being linked to as a library instead of used as intended (buildkit API calls) because the functionaility is not available in any released buildkit/dockerfile parser yet. 8 | 9 | There are absolutely no stability guarentees between releases at this time. 10 | A command that worked one way before may behave differently or even not even exist in the next release. 11 | 12 | ### Example Usage 13 | 14 | This tool can output data in two formats: 15 | 16 | - `--format=build-flags` - Used to output flags directly to `docker buildx build` 17 | - `--format=modfile` - Experimental file which requires a custom syntax parser (`--build-arg BUILDKIT_SYNTAX="mcr.microsoft.com/oss/moby/dockerfile:modfile1"`). The file is passed along with the build context and repalcements are by the parser during build. 18 | 19 | The default format is `build-flags`. 20 | 21 | ```console 22 | $ ./gnarly --mod-prog=contrib/mod.sh --mod-config=contrib/lookup.json 23 | --build-context docker.io/library/golang:1.18=docker-image://mcr.microsoft.com/oss/go/microsoft/golang:1.18 $ 24 | ``` 25 | 26 | Only sources that have a replacement will be output. 27 | Notice it does not print a newline character so it can be passed directly to `docker buildx build`. 28 | 29 | When using this format, whatever `--build-args` you pass to this tool will also be part of the output so you don't have to specify build-args to both this tool and to `docker buildx build. 30 | 31 | With `docker buildx build`: 32 | ```console 33 | $ docker buildx build $(./gnarly --mod-prog=contrib/mod.sh --mod-config=contrib/lookup.json) . 34 | [+] Building 32.0s (12/12) FINISHED 35 | => [internal] load build definition from Dockerfile 0.0s 36 | => => transferring dockerfile: 496B 0.0s 37 | => [internal] load .dockerignore 0.0s 38 | => => transferring context: 2B 0.0s 39 | => [context golang:1.18] load metadata for mcr.microsoft.com/oss/go/microsoft/golang:1.18 0.0s 40 | => [context golang:1.18] mcr.microsoft.com/oss/go/microsoft/golang:1.18 0.0s 41 | => => resolve mcr.microsoft.com/oss/go/microsoft/golang:1.18 0.0s 42 | => [internal] load build context 0.0s 43 | => => transferring context: 7.03kB 0.0s 44 | => CACHED [build 1/7] WORKDIR /go/src/github.com/deislabs/gnarly 0.0s 45 | => CACHED [build 2/7] COPY go.mod . 0.0s 46 | => CACHED [build 3/7] COPY go.sum . 0.0s 47 | => CACHED [build 4/7] RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build go mod download 0.0s 48 | => [build 5/7] COPY . . 0.2s 49 | => [build 6/7] RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build . 31.5s 50 | => [stage-1 1/1] COPY --from=build /go/src/github.com/deislabs/gnarly/gnarly / 51 | ``` 52 | 53 | 54 | 55 | For `--format=modfile`: 56 | 57 | ```console 58 | $ ./gnarly --format=modfile --mod-prog=contrib/mod.sh --mod-config=contrib/lookup.json | tee Dockerfile.mod 59 | { 60 | "sources": [ 61 | { 62 | "type": "docker-image", 63 | "ref": "docker.io/library/golang:1.18", 64 | "replace": "mcr.microsoft.com/oss/go/microsoft/golang:1.18" 65 | } 66 | ] 67 | } 68 | $ docker buildx build --build-arg BUILDKIT_SYNTAX=mcr.microsoft.com/oss/moby/dockerfile:modfile1 . 69 | ``` 70 | 71 | Here we've told `gnarly` to use `contrib/mod.sh` as a tool to handle replacements. 72 | The `contrib/mod.sh` script uses `contrib/lookup.json` as a lookup table for replacements. 73 | For each ref that is found in the Dockerfile by `gnarly`, the `contrib/mod.sh` is called with the found ref as the first argument. The `contrib/mod.sh` script can return an empty string or a replacement ref. 74 | You can specify a path to a config file to use, which will be passed along to the mod-prog as an environment variable `MOD_CONFIG`. 75 | 76 | The output of this is saved to `Dockerfile.mod` which is a special file that the syntax parser shown above will parse to handle replacements. 77 | 78 | This also supports a built-in replacement generator. 79 | This takes a config file (passed via `--mod-config`) with a list of match/replace rules. 80 | The first match is used for each ref. 81 | 82 | ```json 83 | [ 84 | {"match": "", "replace": ""} 85 | ] 86 | ``` 87 | 88 | The `match` field can be a regex, and the `replace` value can make use of capture groups from the regex. 89 | See [regexp.ReplaceAllString](https://pkg.go.dev/regexp#Regexp.ReplaceAllString) for more details. 90 | As an example, see `contrib/mod-builtin.json`. 91 | 92 | In some cases you may not want to modify the main build context with a Dockerfile.mod, which could dirty the git tree or potentially interfere with the actual build. For this case you can use a special "named" context with the mod file in it. 93 | 94 | 95 | ```console 96 | $ dir="$(mktemp -d)" # Make a temp dir where we'll store the Dockerfile.mod 97 | $ ./gnarly --format=modfile --mod-prog=contrib/mod.sh --mod-config=contrib/lookup.json | tee "${dir}/Dockerfile.mod" # Generate the Dockerfile.mod and store it in the temp dir created above. 98 | { 99 | "sources": [ 100 | { 101 | "type": "docker-image", 102 | "ref": "docker.io/library/golang:1.18", 103 | "replace": "mcr.microsoft.com/oss/go/microsoft/golang:1.18" 104 | } 105 | ] 106 | } 107 | $ docker buildx build --build-arg BUILDKIT_SYNTAX=mcr.microsoft.com/oss/moby/dockerfile:modfile1 --build-context "dockerfile-mod=${dir}" . 108 | [+] Building 2.2s (17/17) FINISHED 109 | => [internal] load .dockerignore 0.0s 110 | => => transferring context: 2B 0.0s 111 | => [internal] load build definition from Dockerfile 0.0s 112 | => => transferring dockerfile: 496B 0.0s 113 | => resolve image config for mcr.microsoft.com/oss/moby/dockerfile:modfile1 0.3s 114 | => CACHED docker-image://mcr.microsoft.com/oss/moby/dockerfile:modfile1@sha256:ddccaae065a61196876e89b99eb88ac66cfbc4e21daea9e90f0588dab02420ae 0.0s 115 | => => resolve mcr.microsoft.com/oss/moby/dockerfile:modfile1@sha256:ddccaae065a61196876e89b99eb88ac66cfbc4e21daea9e90f0588dab02420ae 0.0s 116 | => [internal] load build definition from Dockerfile 0.0s 117 | => => transferring dockerfile: 496B 0.0s 118 | => [context dockerfile-mod] load .dockerignore 0.0s 119 | => => transferring dockerfile-mod: 2B 0.0s 120 | => [context dockerfile-mod] load from client 0.0s 121 | => => transferring dockerfile-mod: 36B 0.0s 122 | => [internal] load metadata for mcr.microsoft.com/oss/go/microsoft/golang:1.18 0.1s 123 | => [build 1/7] FROM mcr.microsoft.com/oss/go/microsoft/golang:1.18@sha256:fba12e22cb828665f844f123c5bfd5143f8e9c00c960d6abd4653b1b0e35df6c 0.0s 124 | => => resolve mcr.microsoft.com/oss/go/microsoft/golang:1.18@sha256:fba12e22cb828665f844f123c5bfd5143f8e9c00c960d6abd4653b1b0e35df6c 0.0s 125 | => [internal] load build context 0.0s 126 | => => transferring context: 19.10kB 0.0s 127 | => CACHED [build 2/7] WORKDIR /go/src/github.com/deislabs/gnarly 0.0s 128 | => CACHED [build 3/7] COPY go.mod . 0.0s 129 | => CACHED [build 4/7] COPY go.sum . 0.0s 130 | => CACHED [build 5/7] RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build go mod download 0.0s 131 | => [build 6/7] COPY . . 0.2s 132 | => [build 7/7] RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build . 1.2s 133 | => [stage-1 1/1] COPY --from=build /go/src/github.com/deislabs/gnarly/gnarly / 134 | $ rm -rf "${dir}" 135 | ``` 136 | 137 | Here we have passed a `--build-context` with a value of `"dockerfile-mod=${dir}"`. 138 | This is an extra context that buildx will send called `dockerfile-mod`. 139 | In the specified `BUILDKIT_SYNTAX`, the `dockerfile-mod` named context is a special context it will use to look for a `Dockerfile.mod`. 140 | This can be a directory, an image, or even a URL. See https://www.docker.com/blog/dockerfiles-now-support-multiple-build-contexts/ for more details on named contexts. 141 | You can also specify a custom name for the named context, but you'll need to set a `--build-arg` to tell the builder about that name. 142 | 143 | ```console 144 | $ docker buildx build --build-arg BUILDKIT_SYNTAX=mcr.microsoft.com/oss/moby/dockerfile:modfile1 --build-context "my-custom-name=${dir}" --build-arg BUILDKIT_MOD_CONTEXT=my-custom-name . 145 | ``` 146 | 147 | ## One more thing 148 | 149 | This tool can also be used to wrap the `docker` cli. 150 | When in this mode the tool looks at the passed in arguments to see if it is an invocation of `docker build` or `docker buildx build`. 151 | If so it will auto-generate the replacements and inject the neccessary arguments and execute the real docker. 152 | 153 | This can be done in one of two ways: 154 | 155 | 1. You can create a symlink (or just call the binary `docker`...) to the `gnarly` binary called `docker`. Any invocation where against this symlink will make it act as a docker wrapper., e.g. `ln -s ln -s ./gnarly docker` 156 | 2. In lieu of symlinking or other such methods, you can set the environment variable `DOCKERFILE_MOD_INVOKE_DOCKER=1`, this has the same affect as 1. 157 | 158 | This can completely wrap docker (even `docker run`, `docker exec`, etc). 159 | This should work with 100% of use cases **except** since it is are trying to generate mod data for builds, remote build contexts (e.g. `docker buildx build `) are not currently supported. 160 | The workaround for this is to pre-generate your mod files and pass the path as an environment variable `DOCKERFILE_MOD_PATH=`. 161 | This workaround is going to be the best way to make sure no builds fail because of some missing functionality in `gnarly`. 162 | 163 | This will also inject `buildx` into a build invocation if `docker build` does not support the `--build-context` flag. 164 | Example: `docker build .` will be changed to `docker buildx build .`. 165 | The `buildx` subcommand is injected immediately before the `build` argument, so it should account for any flags before it. 166 | 167 | This also supports passing through some environment variables that will be converted to flags passed to `docker buildx build`. 168 | 169 | #### Supported env vars 170 | 171 | | var name | description | flag output | 172 | |----------|-------------|-------------| 173 | | BUILDKIT_METADATA_FILE | Path to output build metadata to | `--metadata-file=` | 174 | | BUILDKIT_SYNTAX | Dockerfile parser image | `--build-arg BUILDKIT_SYNTAX` | 175 | | BUILDKIT_OUTPUT | Spec for outputing build results | `--output=` | 176 | | BUILDKIT_CACHE_TO | Remote cache spec to forward the build cache to | `--cache-to=` | 177 | | BUILDKIT_CACHE_FROM | Remote cache spec to populate the build cache with | `--cache-from=` | 178 | | BUILDKIT_PLATFORM | Platform spec to build, e.g. `linux/amd64` | `--platform=` | 179 | | BUILDX_LOAD | Bool-like value to tell buildx to load the image into Docker | `--load` | 180 | | BUILDKIT_TAG | CSV list of image tags to override values passed to the docker CLI | `-t=` | 181 | | BUILDKIT_METADATA_DIR | Directory to store buildkit metadata file with randomly generated name | `--metadata-file=/metadata-.json` | 182 | 183 | In general this mode is only recommended when you do not have control over the build invocation and as such cannot inject your own build arguments. 184 | 185 | There may be some failures with the command line parsing, particularly around how boolean flags are handled. There are some special case handlers for these, but there may be more. 186 | -------------------------------------------------------------------------------- /contrib/buildinfo_to_kusto.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "column": "containerimage.buildinfo_frontend", 4 | "Properties": { 5 | "Path": "$['containerimage.buildinfo']['frontend']" 6 | } 7 | }, 8 | { 9 | "column": "containerimage.buildinfo_attrs", 10 | "Properties": { 11 | "Path": "$['containerimage.buildinfo']['attrs']" 12 | } 13 | }, 14 | { 15 | "column": "containerimage.buildinfo_sources", 16 | "Properties": { 17 | "Path": "$['containerimage.buildinfo']['sources']" 18 | } 19 | }, 20 | { 21 | "column": "containerimage.config.digest", 22 | "Properties": { 23 | "Path": "$['containerimage.config.digest']" 24 | } 25 | }, 26 | { 27 | "column": "containerimage.descriptor_mediaType", 28 | "Properties": { 29 | "Path": "$['containerimage.descriptor']['mediaType']" 30 | } 31 | }, 32 | { 33 | "column": "containerimage.descriptor_digest", 34 | "Properties": { 35 | "Path": "$['containerimage.descriptor']['digest']" 36 | } 37 | }, 38 | { 39 | "column": "containerimage.descriptor_size", 40 | "Properties": { 41 | "Path": "$['containerimage.descriptor']['size']" 42 | } 43 | }, 44 | { 45 | "column": "containerimage.descriptor_annotations", 46 | "Properties": { 47 | "Path": "$['containerimage.descriptor']['annotations']" 48 | } 49 | }, 50 | { 51 | "column": "containerimage.digest", 52 | "Properties": { 53 | "Path": "$['containerimage.digest']" 54 | } 55 | }, 56 | { 57 | "column": "image.name", 58 | "Properties": { 59 | "Path": "$['image.name']" 60 | } 61 | } 62 | ] -------------------------------------------------------------------------------- /contrib/kusto.create.kql: -------------------------------------------------------------------------------- 1 | // Create table command 2 | //////////////////////////////////////////////////////////// 3 | .create table ['BuildkitMeta'] (['containerimage.buildinfo_frontend']:string, ['containerimage.buildinfo_attrs']:dynamic, ['containerimage.buildinfo_sources']:dynamic, ['containerimage.config.digest']:string, ['containerimage.descriptor_mediaType']:string, ['containerimage.descriptor_digest']:string, ['containerimage.descriptor_size']:long, ['containerimage.descriptor_annotations']:dynamic, ['containerimage.digest']:string, ['image.name']:string) 4 | 5 | // Create mapping command 6 | //////////////////////////////////////////////////////////// 7 | .create table ['BuildkitMeta'] ingestion json mapping 'BuildkitMeta_mapping' '[{"column":"containerimage.buildinfo_frontend", "Properties":{"Path":"$[\'containerimage.buildinfo\'][\'frontend\']"}},{"column":"containerimage.buildinfo_attrs", "Properties":{"Path":"$[\'containerimage.buildinfo\'][\'attrs\']"}},{"column":"containerimage.buildinfo_sources", "Properties":{"Path":"$[\'containerimage.buildinfo\'][\'sources\']"}},{"column":"containerimage.config.digest", "Properties":{"Path":"$[\'containerimage.config.digest\']"}},{"column":"containerimage.descriptor_mediaType", "Properties":{"Path":"$[\'containerimage.descriptor\'][\'mediaType\']"}},{"column":"containerimage.descriptor_digest", "Properties":{"Path":"$[\'containerimage.descriptor\'][\'digest\']"}},{"column":"containerimage.descriptor_size", "Properties":{"Path":"$[\'containerimage.descriptor\'][\'size\']"}},{"column":"containerimage.descriptor_annotations", "Properties":{"Path":"$[\'containerimage.descriptor\'][\'annotations\']"}},{"column":"containerimage.digest", "Properties":{"Path":"$[\'containerimage.digest\']"}},{"column":"image.name", "Properties":{"Path":"$[\'image.name\']"}}]' 8 | 9 | // Ingest data into table command 10 | /////////////////////////////////////////////////////////// 11 | .ingest async into table ['BuildkitMeta'] (h'') with (format='multijson',ingestionMappingReference='BuildkitMeta_mapping',ingestionMappingType='json',tags="['47710107-3d06-489a-95b3-f597af0273af']") -------------------------------------------------------------------------------- /contrib/lookup.json: -------------------------------------------------------------------------------- 1 | { 2 | "docker.io/library/golang:1.18": "mcr.microsoft.com/oss/go/microsoft/golang:1.18" 3 | } -------------------------------------------------------------------------------- /contrib/mod-builtin.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "match": "docker.io/library/golang:1\\.17(\\.[1-8])(-.*)", 4 | "replace": "mcr.microsoft.com/oss/go/microsoft/golang:1.17${2}" 5 | }, 6 | { 7 | "match": "docker.io/library/golang:1\\.17(\\.\\d*)(.*)", 8 | "replace": "mcr.microsoft.com/oss/go/microsoft/golang:1.17${1}${2}" 9 | }, 10 | { 11 | "match": "docker.io/library/golang:(1\\.17|1\\.18|1\\.19)(.*)", 12 | "replace": "mcr.microsoft.com/oss/go/microsoft/golang:${1}${2}" 13 | } 14 | ] -------------------------------------------------------------------------------- /contrib/mod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "${MOD_CONFIG}" ]; then 4 | MOD_CONFIG=lookup.json 5 | fi 6 | 7 | if [ ! -f "${MOD_CONFIG}" ]; then 8 | >&2 echo config file missing: ${MOD_CONFIG} 9 | exit 1 10 | fi 11 | 12 | jq -re ".[\"$1\"] | select(.!=null)" "$MOD_CONFIG" || true -------------------------------------------------------------------------------- /docker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "bufio" 6 | "bytes" 7 | "compress/bzip2" 8 | "compress/gzip" 9 | "context" 10 | "crypto/rand" 11 | "encoding/hex" 12 | "encoding/json" 13 | "fmt" 14 | "io" 15 | "io/ioutil" 16 | "net/url" 17 | "os" 18 | "os/exec" 19 | "os/signal" 20 | "path/filepath" 21 | "strconv" 22 | "strings" 23 | "syscall" 24 | ) 25 | 26 | const ( 27 | dockerBin = "docker" 28 | pathEnv = "PATH" 29 | ) 30 | 31 | // Env vars which are used when gnarly is invoked as a wrapper for the docker binary. 32 | var ( 33 | // Pass through a custom syntax parser to the docker build command. 34 | parser = os.Getenv("BUILDKIT_SYNTAX") 35 | 36 | // Set a path to a pre-existing modfile instead of calculating one on each invocation. 37 | modPath = os.Getenv("DOCKERFILE_MOD_PATH") 38 | 39 | // Set the path to store the metadata output from docker build 40 | metaPath = os.Getenv("BUILDKIT_METADATA_FILE") 41 | 42 | // Enable debug logging 43 | dockerDebug = os.Getenv("DEBUG") 44 | 45 | // Bool-like value to add the `--load` flag to `docker buildx build` 46 | buildxLoad = os.Getenv("BUILDX_LOAD") 47 | 48 | // Set cache from/to for `docker buildx build` 49 | cacheTo = os.Getenv("BUILDKIT_CACHE_TO") 50 | cacheFrom = os.Getenv("BUILDKIT_CACHE_FROM") 51 | 52 | // Set the platform to build 53 | buildkitPlatform = os.Getenv("BUILDKIT_PLATFORM") 54 | 55 | // Replace existing tags with the ones specified in the environment 56 | buildkitTag = os.Getenv("BUILDKIT_TAG") 57 | 58 | // Replace `--output`with the one specified in this env var 59 | // Multiple entries can be split with the `:` character 60 | // "Real" `:` charactters that should be part of the output spec can be escaped with a `\` character. 61 | // We'll also escape this on new lines. 62 | // 63 | // Note, buildx accepts an array of values for this, but doesn't currently support this: https://github.com/docker/buildx/blob/a8bb25d1b5bd758e293b78d7ef2934a16341b77c/build/build.go#L447 64 | buildkitOutput = os.Getenv("BUILDKIT_OUTPUT") 65 | 66 | // Directory to store randomly named metadata files for each build 67 | // Use this instead of `BUILDKIT_METADATA_FILE` to avoid potentially overwriting files from a previous build invocation 68 | buildkitMetadataDir = os.Getenv("BUILDKIT_METADATA_DIR") 69 | ) 70 | 71 | var ( 72 | // When IsDocker is true because of the special env var instead of based on argv[0], we don't want to filter the path. 73 | noFilterPath bool 74 | 75 | knownBoolFlags = map[string]bool{ 76 | "--load": true, 77 | "--no-cache": true, 78 | "--pull": true, 79 | "--push": true, 80 | "-q": true, 81 | "--quiet": true, 82 | "--tls": true, 83 | "--tlsverify": true, 84 | } 85 | ) 86 | 87 | func IsDocker() bool { 88 | if filepath.Base(os.Args[0]) == dockerBin { 89 | return true 90 | } 91 | if os.Getenv("DOCKERFILE_MOD_INVOKE_DOCKER") == "1" { 92 | noFilterPath = true 93 | return true 94 | } 95 | return false 96 | } 97 | 98 | func InvokeDocker() { 99 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) 100 | defer cancel() 101 | if err := invokeDocker(ctx); err != nil { 102 | fmt.Fprintln(os.Stderr, "[gnarly]: Error while wrapping docker cli:", err) 103 | os.Exit(1) 104 | } 105 | } 106 | 107 | func debug(args ...interface{}) { 108 | if dockerDebug != "" { 109 | fmt.Fprintln(os.Stderr, append([]interface{}{"[gnarly]:"}, args...)...) 110 | } 111 | } 112 | 113 | type dockerArgs struct { 114 | BuildArgs map[string]string 115 | DockerfileName string 116 | Build bool 117 | BuildPos int 118 | Buildx bool 119 | Context string 120 | MetaData string 121 | FilterFlags []int 122 | Tags []string 123 | Output []string 124 | } 125 | 126 | func newDockerArgs() dockerArgs { 127 | var tags []string 128 | if len(buildkitTag) > 0 { 129 | tags = strings.Split(buildkitTag, ",") 130 | } 131 | 132 | var output []string 133 | if len(buildkitOutput) > 0 { 134 | // The format for a normal buildkit output is `key1=value1,key2=value2,...` 135 | // In order to support more than one output, we need to split the string. 136 | // To do this we'll split on the `separator` character below. 137 | // 1. The string may be escaped with a `\`+`separator`, so we'll first replace those escaped values with a null byte. 138 | // 2. Split the string on the separator. 139 | // 3. Replace the null byte with the separator character (unescaped) in all of the split strings. 140 | // 4. Also split on newline characters for convenience. 141 | const ( 142 | separator = ":" 143 | escapeChar = "\\" 144 | ) 145 | escaped := strings.ReplaceAll(buildkitOutput, escapeChar+separator, "\x00") 146 | split := strings.Split(escaped, separator) 147 | for _, s := range split { 148 | ss := strings.ReplaceAll(s, "\x00", escapeChar) 149 | output = append(output, strings.Split(ss, "\n")...) 150 | } 151 | } 152 | 153 | return dockerArgs{ 154 | BuildArgs: make(map[string]string), 155 | DockerfileName: "Dockerfile", 156 | Tags: tags, 157 | Output: output, 158 | } 159 | } 160 | 161 | // Returns true if next is consumed here and should be skipped when itterating args 162 | func handleDockerFlag(arg, next string, dArgs *dockerArgs) (handledNext bool, omit bool) { 163 | splitArg := strings.SplitN(arg, "=", 2) 164 | hasValue := len(splitArg) == 2 165 | var value string 166 | if hasValue { 167 | value = splitArg[1] 168 | } else { 169 | value = next 170 | } 171 | 172 | debug(splitArg[0], value) 173 | if dArgs.Build { 174 | switch fl := splitArg[0]; fl { 175 | case "--build-arg": 176 | split := strings.SplitN(value, "=", 2) 177 | var v string 178 | if len(split) == 2 { 179 | v = split[1] 180 | } 181 | debug("setting build arg", split[0], v) 182 | dArgs.BuildArgs[split[0]] = v 183 | case "-f", "--file": 184 | dArgs.DockerfileName = value 185 | case "--metadata-file": 186 | debug("setting metadata file", arg, value) 187 | dArgs.MetaData = value 188 | case "-t", "--tag": 189 | if len(dArgs.Tags) > 0 { 190 | debug("filterting flag", fl, value) 191 | omit = true 192 | } 193 | case "-o", "--output": 194 | if len(dArgs.Tags) > 0 && strings.Contains(value, "type=registry") { 195 | debug("filtering flag", fl, value) 196 | omit = true 197 | } 198 | if len(dArgs.Output) > 0 { 199 | debug("filterting flag", fl, value) 200 | omit = true 201 | } 202 | } 203 | } 204 | 205 | if !hasValue && value != "" { 206 | if knownBoolFlags[splitArg[0]] { 207 | // We have a known bool flag, check if the next arg is a value passed to the bool flag 208 | if isBoolV, _ := strconv.ParseBool(value); isBoolV { 209 | return true, omit 210 | } 211 | } 212 | if len(value) > 0 && value[0] != '-' { 213 | // This is a value for the current option, which we've already captured, so skip it. 214 | return true, omit 215 | } 216 | } 217 | 218 | return false, omit 219 | } 220 | 221 | // Expects all args that would be passed to dodcker except argv[0] itself. 222 | // e.g. if argv is "docker build -t foo -f bar", the args would be "build -t foo -f bar" 223 | func parseDockerArgs(args []string, dArgs *dockerArgs) { 224 | var ( 225 | skipNext bool 226 | subCommand string 227 | ) 228 | 229 | const ( 230 | buildx = "buildx" 231 | build = "build" 232 | ) 233 | 234 | for i, arg := range args { 235 | if skipNext { 236 | skipNext = false 237 | continue 238 | } 239 | 240 | switch arg { 241 | case build: 242 | if subCommand == "" { 243 | subCommand = build 244 | } 245 | if subCommand == buildx || subCommand == build { 246 | dArgs.Build = true 247 | dArgs.BuildPos = i 248 | } 249 | continue 250 | case buildx: 251 | // `buildx` must come before `build` to be considered 252 | if subCommand == "" { 253 | if !dArgs.Build { 254 | dArgs.Buildx = true 255 | } 256 | subCommand = buildx 257 | } 258 | continue 259 | } 260 | 261 | if arg[0] == '-' { 262 | if arg == "-" { 263 | // for builds, this means read the context from stdin 264 | dArgs.Context = "-" 265 | continue 266 | } 267 | var next string 268 | if i < len(args)-1 { 269 | next = args[i+1] 270 | } 271 | var omit bool 272 | skipNext, omit = handleDockerFlag(arg, next, dArgs) 273 | if omit { 274 | dArgs.FilterFlags = append(dArgs.FilterFlags, i) 275 | if skipNext { 276 | dArgs.FilterFlags = append(dArgs.FilterFlags, i+1) 277 | } 278 | } 279 | continue 280 | } 281 | 282 | subCommand = arg 283 | 284 | if dArgs.Build { 285 | if dArgs.Context != "" { 286 | panic("[gnarly]: found multiple contexts -- this is a bug in the argument parser") 287 | } 288 | dArgs.Context = arg 289 | } 290 | } 291 | } 292 | 293 | func invokeDocker(ctx context.Context) error { 294 | d := lookPath(dockerBin) 295 | if d == "" { 296 | return &exec.Error{Name: dockerBin, Err: exec.ErrNotFound} 297 | } 298 | 299 | var ( 300 | args []string 301 | ) 302 | if len(os.Args) > 1 { 303 | args = os.Args[1:] 304 | } 305 | 306 | dArgs := newDockerArgs() 307 | parseDockerArgs(args, &dArgs) 308 | 309 | var metaCopy bool 310 | if dArgs.Build { 311 | if dArgs.Context == "" { 312 | return fmt.Errorf("could not find context for build in command line arguments") 313 | } 314 | 315 | for n, i := range dArgs.FilterFlags { 316 | args = append(args[:i-n], args[i-n+1:]...) 317 | } 318 | 319 | if !dArgs.Buildx { 320 | out, err := exec.CommandContext(ctx, d, "build", "--help").CombinedOutput() 321 | if err != nil { 322 | debug("error while checking if `docker build` supports --build-context:", err, ":", string(out)) 323 | } 324 | 325 | // Newer versions of docker *may* support --build-context, but that depends on a number of factors... so just check if `docker build --help` says it supports it. 326 | // If not then inject buildx into the args. 327 | if !strings.Contains(string(out), "--build-context") { 328 | debug("injecting buildx into args") 329 | args = append(args[:dArgs.BuildPos], append([]string{"buildx"}, args[dArgs.BuildPos:]...)...) 330 | } 331 | 332 | // unless otherwise specified, assume we want to use `--load` when upgrading from `docker build` to `docker buildx build` 333 | if buildxLoad == "" { 334 | buildxLoad = "true" 335 | } 336 | } 337 | 338 | if buildkitMetadataDir != "" { 339 | if metaPath != "" { 340 | return fmt.Errorf("conflicting options: both BUILDKIT_METADATA_DIR and BUILDKIT_METADATA_FILE are set but are mutually exclsuive") 341 | } 342 | if err := os.MkdirAll(buildkitMetadataDir, 0750); err != nil { 343 | return fmt.Errorf("failed to create buildkit metadata dir: %v", err) 344 | } 345 | metaPath = getRandomFilename(buildkitMetadataDir, "metadata-") + ".json" 346 | if metaPath == "" { 347 | return fmt.Errorf("could not get random filename for buildkit metadata") 348 | } 349 | } 350 | if metaPath != "" { 351 | debug("injecting metadata file into args") 352 | if dArgs.MetaData != "" && metaPath != dArgs.MetaData { 353 | debug("build arguments already specified a metadata file, creating helper to copy it") 354 | metaCopy = true 355 | } 356 | args = append(args, "--metadata-file", metaPath) 357 | } 358 | 359 | if parser != "" { 360 | args = append(args, "--build-arg=BUILDKIT_SYNTAX="+parser) 361 | } 362 | 363 | var result Result 364 | switch { 365 | case modPath != "": 366 | debug("Reading source replacements from", modPath) 367 | data, err := os.ReadFile(modPath) 368 | if err != nil { 369 | return fmt.Errorf("error reading specified modfile path: %w", err) 370 | } 371 | 372 | if err := json.Unmarshal(data, &result); err != nil { 373 | return fmt.Errorf("error parsing specified modfile: %w", err) 374 | } 375 | case modConfig != "": 376 | debug("Generating source replacements from config", modConfig, "using prog", modProg) 377 | dt, err := getDockerfile(dArgs.Context, dArgs.DockerfileName) 378 | if err != nil { 379 | return err 380 | } 381 | 382 | result, err = Generate(ctx, dt, dArgs.BuildArgs) 383 | if err != nil { 384 | return err 385 | } 386 | default: 387 | debug("no modfile or modconfig, skipping source analysis") 388 | } 389 | 390 | for _, o := range dArgs.Output { 391 | args = append(args, "--output="+o) 392 | } 393 | 394 | if buildxLoad != "" { 395 | load, err := strconv.ParseBool(buildxLoad) 396 | if err != nil { 397 | debug("error parsing BUILDX_LOAD:", err) 398 | } 399 | if load { 400 | args = append(args, "--load") 401 | } 402 | } 403 | 404 | if cacheFrom != "" { 405 | args = append(args, "--cache-from="+cacheFrom) 406 | } 407 | if cacheTo != "" { 408 | args = append(args, "--cache-to="+cacheTo) 409 | } 410 | 411 | if buildkitPlatform != "" { 412 | args = append(args, "--platform="+buildkitPlatform) 413 | } 414 | 415 | for _, t := range dArgs.Tags { 416 | debug("adding tag:", t) 417 | args = append(args, "-t="+t) 418 | } 419 | 420 | for _, s := range result.Sources { 421 | if s.Replace != "" { 422 | args = append(args, fmt.Sprintf("--build-context=%s=%s://%s", s.Ref, s.Type, s.Replace)) 423 | } 424 | } 425 | } 426 | 427 | select { 428 | case <-ctx.Done(): 429 | default: 430 | } 431 | 432 | debug(d, strings.Join(args, " ")) 433 | if !metaCopy { 434 | if err := syscall.Exec(d, append([]string{filepath.Base(d)}, args...), os.Environ()); err != nil { 435 | return fmt.Errorf("error executing actual docker bin: %w", err) 436 | } 437 | // Nothing happens in our code after this. 438 | // `syscall.Exec` takes over the whole process 439 | } 440 | 441 | cmd := exec.CommandContext(ctx, d, args...) 442 | cmd.Stdin = os.Stdin 443 | cmd.Stdout = os.Stdout 444 | cmd.Stderr = os.Stderr 445 | cmd.Env = os.Environ() 446 | cmd.SysProcAttr = &syscall.SysProcAttr{ 447 | Pdeathsig: syscall.SIGTERM, 448 | } 449 | 450 | if err := cmd.Run(); err != nil { 451 | return fmt.Errorf("error executing actual docker bin: %w", err) 452 | } 453 | 454 | f1, err := os.Open(metaPath) 455 | if err != nil { 456 | return fmt.Errorf("error opening metadata file: %w", err) 457 | } 458 | defer f1.Close() 459 | 460 | f2, err := os.Create(dArgs.MetaData) 461 | if err != nil { 462 | return fmt.Errorf("error creating metadata file: %w", err) 463 | } 464 | defer f2.Close() 465 | 466 | _, err = io.Copy(f2, f1) 467 | return err 468 | } 469 | 470 | const ( 471 | Uncompressed = iota 472 | Bzip2 473 | Gzip 474 | Xz 475 | ) 476 | 477 | func detectCompression(magic []byte) int { 478 | for compression, m := range map[int][]byte{ 479 | Bzip2: {0x42, 0x5A, 0x68}, 480 | Gzip: {0x1F, 0x8B, 0x08}, 481 | Xz: {0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, 482 | } { 483 | if len(magic) < len(m) { 484 | continue 485 | } 486 | if bytes.Equal(m, magic[:len(m)]) { 487 | return compression 488 | } 489 | } 490 | 491 | return Uncompressed 492 | } 493 | 494 | type unsupportedURLContext struct { 495 | scheme string 496 | } 497 | 498 | func (e unsupportedURLContext) Error() string { 499 | return "unsupported context scheme: " + e.scheme 500 | } 501 | 502 | func getDockerfile(context, p string) ([]byte, error) { 503 | if context == "-" { 504 | f, err := os.CreateTemp("", "dockermod-"+context) 505 | if err != nil { 506 | return nil, fmt.Errorf("error creating temp file to pipe from stdin: %w", err) 507 | } 508 | defer f.Close() 509 | 510 | rdr := io.TeeReader(os.Stdin, f) 511 | 512 | dt, err := dockerfileFromReader(rdr, p) 513 | if err != nil { 514 | return nil, err 515 | } 516 | 517 | if _, err := f.Seek(0, io.SeekStart); err != nil { 518 | return nil, fmt.Errorf("error seeking to start of temp context file: %w", err) 519 | } 520 | 521 | if err := syscall.Dup3(int(f.Fd()), int(os.Stdin.Fd()), 0); err != nil { 522 | return nil, fmt.Errorf("error duping temp context file to stdin: %w", err) 523 | } 524 | 525 | return dt, nil 526 | } 527 | 528 | u, err := url.Parse(context) 529 | if err == nil { 530 | switch u.Scheme { 531 | case "http", "https": 532 | return nil, unsupportedURLContext{u.Scheme} 533 | case "git": 534 | return nil, unsupportedURLContext{u.Scheme} 535 | } 536 | } 537 | 538 | if p[0] == '/' { 539 | return os.ReadFile(p) 540 | } 541 | if _, err := os.Stat(context); err == nil { 542 | return os.ReadFile(filepath.Join(context, p)) 543 | } 544 | 545 | return nil, fmt.Errorf("unable to locate %s in context %s", p, context) 546 | } 547 | 548 | func xzStream(in io.Reader) (io.Reader, error) { 549 | cmd := exec.Command("xz", "-d", "-c", "-q") 550 | cmd.Stdin = in 551 | 552 | pr, pw := io.Pipe() 553 | cmd.Stdout = pw 554 | 555 | errBuf := bytes.NewBuffer(nil) 556 | cmd.Stderr = errBuf 557 | 558 | if err := cmd.Start(); err != nil { 559 | return nil, err 560 | } 561 | 562 | go func() { 563 | if err := cmd.Wait(); err != nil { 564 | pr.CloseWithError(fmt.Errorf("%s: %w", errBuf, err)) 565 | } 566 | }() 567 | 568 | return pr, nil 569 | } 570 | 571 | func dockerfileFromReader(f io.Reader, p string) ([]byte, error) { 572 | bufReader := bufio.NewReader(f) 573 | var rdr io.Reader = bufReader 574 | 575 | magic, err := bufReader.Peek(1024) 576 | if err != nil && err != io.EOF { 577 | return nil, err 578 | } 579 | 580 | switch detectCompression(magic) { 581 | case Bzip2: 582 | rdr = bzip2.NewReader(rdr) 583 | case Gzip: 584 | rdr, err = gzip.NewReader(rdr) 585 | if err != nil { 586 | return nil, err 587 | } 588 | case Xz: 589 | rdr, err = xzStream(rdr) 590 | if err != nil { 591 | return nil, err 592 | } 593 | } 594 | 595 | tr := tar.NewReader(bytes.NewBuffer(magic)) 596 | if _, err := tr.Next(); err != nil { 597 | // Not an archive 598 | return ioutil.ReadAll(rdr) 599 | } 600 | 601 | tr = tar.NewReader(rdr) 602 | for { 603 | th, err := tr.Next() 604 | if err != nil { 605 | return nil, err 606 | } 607 | 608 | if th.Name != p { 609 | continue 610 | } 611 | 612 | return ioutil.ReadAll(tr) 613 | } 614 | } 615 | 616 | func randomID() string { 617 | b := make([]byte, 4) 618 | if _, err := rand.Read(b); err != nil { 619 | panic(err) 620 | } 621 | return hex.EncodeToString(b) 622 | } 623 | 624 | func getRandomFilename(dir, prefix string) string { 625 | for i := 0; i < 100; i++ { 626 | p := filepath.Join(dir, prefix+randomID()) 627 | if _, err := os.Stat(p); os.IsNotExist(err) { 628 | return p 629 | } 630 | } 631 | return "" 632 | } 633 | -------------------------------------------------------------------------------- /docker_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestDockerfileFromReader(t *testing.T) { 10 | t.Run("raw dockerfile", func(t *testing.T) { 11 | dockerfile := ` 12 | FROM busybox 13 | RUN echo hello 14 | ` 15 | rdr := strings.NewReader(dockerfile) 16 | 17 | dt, err := dockerfileFromReader(rdr, "") 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | if string(dt) != dockerfile { 23 | t.Fatalf("Expected %s, got %s", dockerfile, string(dt)) 24 | } 25 | }) 26 | } 27 | 28 | func TestParseDockerArgs(t *testing.T) { 29 | dArgs := newDockerArgs() 30 | parseDockerArgs([]string{"run", "-it", "--rm", "busybox", "sh"}, &dArgs) 31 | if !reflect.DeepEqual(dArgs, newDockerArgs()) { 32 | t.Errorf("Got unexpected docker args, should be empty: %+v", dArgs) 33 | } 34 | 35 | dArgs = newDockerArgs() 36 | parseDockerArgs([]string{"run", "-it", "--rm", "--name", "build", "busybox", "sh"}, &dArgs) 37 | if dArgs.Build { 38 | t.Errorf("Got unexpected docker args, should not be build: %+v", dArgs) 39 | } 40 | dArgs = newDockerArgs() 41 | parseDockerArgs([]string{"run", "-it", "--rm", "build", "sh"}, &dArgs) 42 | if dArgs.Build { 43 | t.Errorf("Got unexpected docker args, should not be build: %+v", dArgs) 44 | } 45 | dArgs = newDockerArgs() 46 | parseDockerArgs([]string{"--tls", "run", "-it", "--rm", "build", "sh"}, &dArgs) 47 | if dArgs.Build { 48 | t.Errorf("Got unexpected docker args, should not be build: %+v", dArgs) 49 | } 50 | 51 | dArgs = newDockerArgs() 52 | parseDockerArgs([]string{"run", "-it", "--rm", "buildx", "sh"}, &dArgs) 53 | if dArgs.Buildx { 54 | t.Errorf("Got unexpected docker args, should not be build: %+v", dArgs) 55 | } 56 | 57 | dArgs = newDockerArgs() 58 | parseDockerArgs([]string{"build", ".", "-t", "test", "-f", "Dockerfile.test"}, &dArgs) 59 | if dArgs.Context != "." { 60 | t.Errorf("Got unexpected context path, expected ., got: %s", dArgs.Context) 61 | } 62 | if dArgs.DockerfileName != "Dockerfile.test" { 63 | t.Errorf("Got unexpected dockerfile name, expected Dockerfile.test, got: %s", dArgs.DockerfileName) 64 | } 65 | 66 | dArgs = newDockerArgs() 67 | parseDockerArgs([]string{"build", "-"}, &dArgs) 68 | if dArgs.Context != "-" { 69 | t.Errorf("Got unexpected context path, expected -, got: %s", dArgs.Context) 70 | } 71 | 72 | osArgs := []string{"build", "--build-arg", "foo=bar", "--bool-flag", "-t", "foo", "--output=type=registry,dest=bar", "--other-flag", "some value", "--build-arg=baz=quux", "--file", t.Name(), "."} 73 | dArgs = newDockerArgs() 74 | parseDockerArgs(osArgs, &dArgs) 75 | 76 | if !dArgs.Build { 77 | t.Error("Expected `build` to be true") 78 | } 79 | if dArgs.Buildx { 80 | t.Error("Expected `buildx` to be false") 81 | } 82 | 83 | dArgs = newDockerArgs() 84 | dArgs.Tags = []string{"asdf"} 85 | parseDockerArgs(append([]string{"buildx"}, osArgs...), &dArgs) 86 | 87 | if !dArgs.Build { 88 | t.Error("Expected `build` to be true") 89 | } 90 | if !dArgs.Buildx { 91 | t.Error("Expected `buildx` to be true") 92 | } 93 | 94 | if dArgs.DockerfileName != t.Name() { 95 | t.Fatalf("Expected dockerfile name %s, got %s", t.Name(), dArgs.DockerfileName) 96 | } 97 | 98 | if len(dArgs.BuildArgs) != 2 { 99 | t.Errorf("Expected 2 build args: %v", dArgs.BuildArgs) 100 | } 101 | 102 | if v, ok := dArgs.BuildArgs["foo"]; v != "bar" { 103 | t.Errorf("Expected 'bar', got '%s', isSet: %v", dArgs.BuildArgs["foo"], ok) 104 | } 105 | 106 | if v, ok := dArgs.BuildArgs["baz"]; v != "quux" { 107 | t.Errorf("Expected 'quux', got '%s', isSet: %v", v, ok) 108 | } 109 | 110 | if len(dArgs.FilterFlags) != 3 { 111 | t.Fatalf("Expected 3 filter flags, got: %v", dArgs.FilterFlags) 112 | } 113 | if dArgs.FilterFlags[0] != 5 { 114 | t.Errorf("Expected 4, got %d", dArgs.FilterFlags[0]) 115 | } 116 | if dArgs.FilterFlags[1] != 6 { 117 | t.Errorf("Expected 5, got %d", dArgs.FilterFlags[1]) 118 | } 119 | if dArgs.FilterFlags[2] != 7 { 120 | t.Errorf("Expected 6, got %d", dArgs.FilterFlags[2]) 121 | } 122 | 123 | dArgs = newDockerArgs() 124 | parseDockerArgs([]string{"run", "-it", "--rm", "golang:1.18", "go", "build"}, &dArgs) 125 | if dArgs.Build { 126 | t.Error("Expected `build` to be false since it is a not a docker build command") 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /generate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "os" 10 | "os/exec" 11 | "regexp" 12 | "sort" 13 | "strings" 14 | 15 | "github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb" 16 | ) 17 | 18 | type Source struct { 19 | Type string `json:"type"` 20 | Ref string `json:"ref"` 21 | Replace string `json:"replace,omitempty"` 22 | } 23 | 24 | type Result struct { 25 | Sources []Source `json:"sources"` 26 | } 27 | 28 | func Generate(ctx context.Context, dt []byte, buildArgs map[string]string) (Result, error) { 29 | targets, err := dockerfile2llb.ListTargets(context.TODO(), dt) 30 | if err != nil { 31 | return Result{}, fmt.Errorf("error listing dockerfile targets: %w", err) 32 | } 33 | 34 | r := newResolver() 35 | for _, target := range targets.Targets { 36 | _, err = dockerfile2llb.Dockefile2Outline(ctx, dt, dockerfile2llb.ConvertOpt{ 37 | BuildArgs: func() map[string]string { 38 | if len(buildArgs) > 0 { 39 | return buildArgs 40 | } 41 | return nil 42 | }(), 43 | Target: target.Name, 44 | MetaResolver: r, 45 | }) 46 | if err != nil { 47 | return Result{}, fmt.Errorf("error parsing dockerfile: %w", err) 48 | } 49 | } 50 | 51 | var result Result 52 | 53 | buf := bytes.NewBuffer(nil) 54 | stderr := bytes.NewBuffer(nil) 55 | 56 | type matchRule struct { 57 | Match string `json:"match"` 58 | Replace string `json:"replace"` 59 | regex *regexp.Regexp 60 | } 61 | 62 | var matchers []matchRule 63 | if modProg == "" && modConfig != "" { 64 | data, err := os.ReadFile(modConfig) 65 | if err != nil { 66 | return Result{}, fmt.Errorf("error reading mod config: %w", err) 67 | } 68 | if len(data) > 0 { 69 | if err := json.Unmarshal(data, &matchers); err != nil { 70 | return Result{}, fmt.Errorf("error parsing mod config for builtin matcher: %w", err) 71 | } 72 | for i, v := range matchers { 73 | matchers[i].regex, err = regexp.Compile(v.Match) 74 | if err != nil { 75 | return Result{}, fmt.Errorf("error compiling matcher regex from mod config: %w", err) 76 | } 77 | } 78 | } 79 | } 80 | 81 | replace := func(ref string) string { 82 | if modProg == "" { 83 | for _, rule := range matchers { 84 | if !rule.regex.MatchString(ref) { 85 | continue 86 | } 87 | return rule.regex.ReplaceAllString(ref, rule.Replace) 88 | } 89 | return "" 90 | } 91 | 92 | buf.Reset() 93 | stderr.Reset() 94 | cmdWithArgs := strings.Fields(modProg) 95 | cmdWithArgs = append(cmdWithArgs, ref) 96 | cmd := exec.CommandContext(ctx, cmdWithArgs[0], cmdWithArgs[1:]...) 97 | cmd.Stdout = buf 98 | cmd.Stderr = stderr 99 | if modConfig != "" { 100 | cmd.Env = append(os.Environ(), "MOD_CONFIG="+modConfig) 101 | } 102 | if err := cmd.Run(); err != nil { 103 | if stderr.Len() == 0 { 104 | stderr.WriteString("") 105 | } 106 | panic(fmt.Sprintf("%s: %v", stderr, err)) 107 | } 108 | 109 | if stderr.Len() > 0 { 110 | io.Copy(os.Stderr, stderr) 111 | } 112 | 113 | return strings.TrimSpace(buf.String()) 114 | } 115 | 116 | for _, resolved := range r.refs { 117 | s := Source{Type: "docker-image", Ref: resolved, Replace: replace(resolved)} 118 | debug("resolved", s.Ref, "with replacement:", s.Replace) 119 | result.Sources = append(result.Sources, s) 120 | } 121 | 122 | // Sort for stable output for testing 123 | sort.Slice(result.Sources, func(i, j int) bool { 124 | return result.Sources[i].Ref < result.Sources[j].Ref 125 | }) 126 | 127 | return result, nil 128 | } 129 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/deislabs/gnarly 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/moby/buildkit v0.10.1-0.20220402051847-3e38a2d34830 7 | github.com/opencontainers/go-digest v1.0.0 8 | ) 9 | 10 | require ( 11 | github.com/agext/levenshtein v1.2.3 // indirect 12 | github.com/containerd/containerd v1.6.3 // indirect 13 | github.com/containerd/ttrpc v1.1.0 // indirect 14 | github.com/containerd/typeurl v1.0.2 // indirect 15 | github.com/docker/distribution v2.8.1+incompatible // indirect 16 | github.com/docker/docker v20.10.14+incompatible // indirect 17 | github.com/docker/go-connections v0.4.0 // indirect 18 | github.com/docker/go-units v0.4.0 // indirect 19 | github.com/gogo/googleapis v1.4.1 // indirect 20 | github.com/gogo/protobuf v1.3.2 // indirect 21 | github.com/golang/protobuf v1.5.2 // indirect 22 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 23 | github.com/klauspost/compress v1.15.1 // indirect 24 | github.com/moby/locker v1.0.1 // indirect 25 | github.com/moby/sys/signal v0.6.0 // indirect 26 | github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect 27 | github.com/pkg/errors v0.9.1 // indirect 28 | github.com/sirupsen/logrus v1.8.1 // indirect 29 | github.com/tonistiigi/fsutil v0.0.0-20220413024721-3c5c7e848994 // indirect 30 | golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect 31 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect 32 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 33 | golang.org/x/sys v0.0.0-20220405210540-1e041c57c461 // indirect 34 | golang.org/x/text v0.3.7 // indirect 35 | google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 // indirect 36 | google.golang.org/grpc v1.45.0 // indirect 37 | google.golang.org/protobuf v1.27.1 // indirect 38 | ) 39 | 40 | replace github.com/moby/buildkit => github.com/tonistiigi/buildkit v0.10.0-rc2.0.20220506204119-72529114ffc4 41 | 42 | replace github.com/docker/docker => github.com/docker/docker v20.10.3-0.20220224222438-c78f6963a1c0+incompatible 43 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= 2 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 6 | github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= 7 | github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 8 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 9 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 10 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 11 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 12 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 13 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 14 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 15 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 16 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 17 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 18 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 19 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 20 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= 21 | github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 22 | github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 23 | github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 24 | github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 25 | github.com/containerd/containerd v1.6.3 h1:JfgUEIAH07xDWk6kqz0P3ArZt+KJ9YeihSC9uyFtSKg= 26 | github.com/containerd/containerd v1.6.3/go.mod h1:gCVGrYRYFm2E8GmuUIbj/NGD7DLZQLzSJQazjVKDOig= 27 | github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= 28 | github.com/containerd/ttrpc v1.1.0 h1:GbtyLRxb0gOLR0TYQWt3O6B0NvT8tMdorEHqIQo/lWI= 29 | github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= 30 | github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY7aY= 31 | github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= 32 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 33 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 34 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 35 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 36 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 37 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 38 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 39 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 40 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 41 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 42 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 43 | github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= 44 | github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 45 | github.com/docker/docker v20.10.3-0.20220224222438-c78f6963a1c0+incompatible h1:Ptj2To+ezU/mCBUKdYXBQ2r3/2EJojAlOZrsgprF+is= 46 | github.com/docker/docker v20.10.3-0.20220224222438-c78f6963a1c0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 47 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 48 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 49 | github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= 50 | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 51 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 52 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 53 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 54 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 55 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 56 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= 57 | github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= 58 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 59 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 60 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 61 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 62 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 63 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 64 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 65 | github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= 66 | github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= 67 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 68 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 69 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 70 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 71 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 72 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 73 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 74 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 75 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 76 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 77 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 78 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 79 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 80 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 81 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 82 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 83 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 84 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 85 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 86 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 87 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 88 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 89 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 90 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 91 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 92 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 93 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 94 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 95 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 96 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 97 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 98 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 99 | github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= 100 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 101 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 102 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 103 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 104 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 105 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 106 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 107 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 108 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 109 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 110 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 111 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 112 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 113 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 114 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 115 | github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A= 116 | github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 117 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 118 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 119 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 120 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 121 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 122 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 123 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 124 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 125 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 126 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 127 | github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= 128 | github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= 129 | github.com/moby/sys/signal v0.6.0 h1:aDpY94H8VlhTGa9sNYUFCFsMZIUh5wm0B6XkIoJj/iY= 130 | github.com/moby/sys/signal v0.6.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= 131 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 132 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 133 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 134 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 135 | github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= 136 | github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 137 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 138 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 139 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 140 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 141 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 142 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 143 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 144 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 145 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 146 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 147 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 148 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 149 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 150 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 151 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 152 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 153 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 154 | github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= 155 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 156 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 157 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 158 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 159 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 160 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 161 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 162 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 163 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 164 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 165 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 166 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 167 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 168 | github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 169 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 170 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 171 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 172 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 173 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 174 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 175 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 176 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 177 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 178 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 179 | github.com/tonistiigi/buildkit v0.10.0-rc2.0.20220506204119-72529114ffc4 h1:t1Fh9w+bRFiuQiMtccgqS7AjDuF6/uBd2KYEJ92CL38= 180 | github.com/tonistiigi/buildkit v0.10.0-rc2.0.20220506204119-72529114ffc4/go.mod h1:zDYeklEGKb37dWR9td8KDcY4JtgVrsKfSSzhJOSdO5g= 181 | github.com/tonistiigi/fsutil v0.0.0-20220413024721-3c5c7e848994 h1:kui3WK5NgTQUD0NdxsCohHLXaN43AzLBI0BES0uoaYQ= 182 | github.com/tonistiigi/fsutil v0.0.0-20220413024721-3c5c7e848994/go.mod h1:oPAfvw32vlUJSjyDcQ3Bu0nb2ON2B+G0dtVN/SZNJiA= 183 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 184 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 185 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 186 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 187 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 188 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 189 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 190 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 191 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 192 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 193 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 194 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 195 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 196 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 197 | golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= 198 | golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 199 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 200 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 201 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 202 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 203 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 204 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 205 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 206 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 207 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 208 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 209 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 210 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 211 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 212 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 213 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 214 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 215 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 216 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 217 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 218 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 219 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= 220 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 221 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 222 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 223 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 224 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 225 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 226 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 227 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 228 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 229 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 230 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 231 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 232 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 233 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 234 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 235 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 236 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 237 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 238 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 239 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 240 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 241 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 242 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 243 | golang.org/x/sys v0.0.0-20210313202042-bd2e13477e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 244 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 245 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 246 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 247 | golang.org/x/sys v0.0.0-20220405210540-1e041c57c461 h1:kHVeDEnfKn3T238CvrUcz6KeEsFHVaKh4kMTt6Wsysg= 248 | golang.org/x/sys v0.0.0-20220405210540-1e041c57c461/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 249 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 250 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= 251 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 252 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 253 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 254 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 255 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 256 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 257 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 258 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 259 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 260 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 261 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 262 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 263 | golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 264 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 265 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 266 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 267 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 268 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 269 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 270 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 271 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 272 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 273 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 274 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 275 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 276 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 277 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 278 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 279 | google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 h1:Et6SkiuvnBn+SgrSYXs/BrUpGB4mbdwt4R3vaPIlicA= 280 | google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= 281 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 282 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 283 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 284 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 285 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 286 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 287 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 288 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 289 | google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= 290 | google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= 291 | google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= 292 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 293 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 294 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 295 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 296 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 297 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 298 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 299 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 300 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 301 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 302 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 303 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 304 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 305 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 306 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 307 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 308 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 309 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 310 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 311 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 312 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 313 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 314 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 315 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 316 | gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= 317 | gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= 318 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 319 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 320 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "os/signal" 11 | "strings" 12 | "syscall" 13 | ) 14 | 15 | const ( 16 | formatModfile = "modfile" 17 | formatBuildFlags = "build-flags" 18 | ) 19 | 20 | var ( 21 | modProg = os.Getenv("DOCKERFILE_MOD_PROG") 22 | modConfig = os.Getenv("DOCKERFILE_MOD_CONFIG") 23 | ) 24 | 25 | func main() { 26 | if IsDocker() { 27 | InvokeDocker() 28 | return 29 | } 30 | 31 | buildArgs := argFlag{} 32 | format := os.Getenv("DOCKERFILE_MOD_FORMAT") 33 | if format == "" { 34 | format = formatBuildFlags 35 | } 36 | 37 | flag.Var(&buildArgs, "build-arg", "set build args to pass through -- these are required if the dockerfie uses args to determine an image source") 38 | flag.StringVar(&modProg, "mod-prog", modProg, "Set program to execute to modify a reference as a replace rule") 39 | flag.StringVar(&modConfig, "mod-config", modConfig, "Set the config file to pass to mod prog") 40 | flag.StringVar(&format, "format", format, "Set the output format. Formats: modfile, build-flags") 41 | 42 | flag.Parse() 43 | 44 | var ( 45 | err error 46 | dt []byte 47 | ) 48 | if flag.NArg() == 0 || flag.Arg(0) == "-" { 49 | stat, e := os.Stdin.Stat() 50 | if e != nil { 51 | panic(err) 52 | } 53 | if stat.Mode()&os.ModeCharDevice == 0 { 54 | dt, err = ioutil.ReadAll(os.Stdin) 55 | } else { 56 | dt, err = ioutil.ReadFile("Dockerfile") 57 | } 58 | } else { 59 | dt, err = ioutil.ReadFile(flag.Arg(0)) 60 | } 61 | if err != nil { 62 | fmt.Fprintln(os.Stderr, "error reading dockerfile:", err) 63 | os.Exit(1) 64 | } 65 | 66 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) 67 | defer cancel() 68 | 69 | result, err := Generate(ctx, dt, buildArgs) 70 | if err != nil { 71 | fmt.Fprintln(os.Stderr, "error generating mods:", err) 72 | os.Exit(2) 73 | } 74 | 75 | switch format { 76 | case formatModfile: 77 | data, err := json.MarshalIndent(result, "", "\t") 78 | if err != nil { 79 | panic(err) 80 | } 81 | fmt.Println(string(data)) 82 | return 83 | case formatBuildFlags: 84 | sb := &strings.Builder{} 85 | 86 | for k, v := range buildArgs { 87 | sb.WriteString(fmt.Sprintf("--build-arg %s=%s ", k, v)) 88 | } 89 | 90 | for _, resolved := range result.Sources { 91 | if resolved.Replace != "" { 92 | sb.WriteString(fmt.Sprintf("--build-context %s=docker-image://%s ", resolved.Ref, resolved.Replace)) 93 | } 94 | } 95 | fmt.Print(sb.String()) 96 | return 97 | default: 98 | fmt.Fprintln(os.Stderr, "unknown format:", format) 99 | os.Exit(1) 100 | } 101 | } 102 | 103 | type argFlag map[string]string 104 | 105 | func (f *argFlag) Set(val string) error { 106 | v := strings.SplitN(val, "=", 2) 107 | if len(v) != 2 { 108 | return fmt.Errorf("expected format =") 109 | } 110 | (*f)[v[0]] = v[1] 111 | return nil 112 | } 113 | 114 | func (f *argFlag) String() string { 115 | fv := *f 116 | vals := make([]string, 0, len(fv)) 117 | 118 | for k, v := range fv { 119 | vals = append(vals, k+"="+v) 120 | } 121 | return strings.Join(vals, " ") 122 | } 123 | -------------------------------------------------------------------------------- /path.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | ) 9 | 10 | func lookPath(name string) string { 11 | paths := filepath.SplitList(os.Getenv(pathEnv)) 12 | 13 | execPath, err := filepath.EvalSymlinks("/proc/self/exe") 14 | if err != nil { 15 | debug("error evaluating exec path:", err) 16 | return "" 17 | } 18 | 19 | if !filepath.IsAbs(execPath) { 20 | if _, err := os.Stat(execPath); err != nil { 21 | if !os.IsNotExist(err) { 22 | debug("lookPath: stat error %s: %v", execPath, err) 23 | return "" 24 | } 25 | 26 | execPath, err = exec.LookPath(execPath) 27 | if err != nil { 28 | debug("exec.LookPath error: %v", err) 29 | return "" 30 | } 31 | } 32 | } 33 | 34 | dir := filepath.Dir(execPath) 35 | debug("current bin:", execPath) 36 | for _, p := range paths { 37 | if !noFilterPath && p == dir { 38 | // Skip the directory where our binary is located 39 | debug("Skipping docker bin lookup for:", p) 40 | continue 41 | } 42 | 43 | f := filepath.Join(p, name) 44 | 45 | f, err = filepath.EvalSymlinks(f) 46 | if err != nil { 47 | if !os.IsNotExist(err) { 48 | debug("error evaluating symlink:", err) 49 | } 50 | continue 51 | } 52 | if f == execPath { 53 | continue 54 | } 55 | if err := findExecutable(f); err == nil { 56 | debug("found", name+":", f) 57 | return f 58 | } 59 | } 60 | 61 | debug(name, "not found in $PATH:", os.Getenv(pathEnv)) 62 | return "" 63 | } 64 | 65 | func findExecutable(file string) error { 66 | d, err := os.Stat(file) 67 | if err != nil { 68 | return err 69 | } 70 | if m := d.Mode(); !m.IsDir() && m&0111 != 0 { 71 | return nil 72 | } 73 | return fs.ErrPermission 74 | } 75 | -------------------------------------------------------------------------------- /resolver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/moby/buildkit/client/llb" 8 | "github.com/opencontainers/go-digest" 9 | ) 10 | 11 | func newResolver() *metaResolver { 12 | return &metaResolver{ 13 | refs: make(map[string]string), 14 | } 15 | } 16 | 17 | type metaResolver struct { 18 | mu sync.Mutex 19 | refs map[string]string 20 | } 21 | 22 | const ( 23 | emptyDigest = digest.Digest("sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a") 24 | emptyConfig = "{}" 25 | ) 26 | 27 | func (r *metaResolver) ResolveImageConfig(ctx context.Context, ref string, opt llb.ResolveImageConfigOpt) (digest.Digest, []byte, error) { 28 | r.mu.Lock() 29 | r.refs[ref] = ref 30 | r.mu.Unlock() 31 | return emptyDigest, []byte(emptyConfig), nil 32 | } 33 | -------------------------------------------------------------------------------- /test/docker_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | func TestDocker(t *testing.T) { 10 | if testing.Short() { 11 | t.Skip("skipping docker test in short mode") 12 | } 13 | 14 | testDockerfile := []byte(` 15 | FROM busybox AS busy 16 | FROM alpine AS alp 17 | 18 | # Just some dummy things to make sure both the above targets are built 19 | FROM scratch 20 | COPY --from=busy / /tmp /tmp-busy 21 | COPY --from=alp / /tmp /tmp-alpine 22 | `) 23 | 24 | // This are pinning sha's because the buildkit metadata adds the sha for contexts provided with `--build-context` 25 | expected := marshalResult(t, map[string]interface{}{ 26 | bInfoKey: Result{ 27 | Sources: []Source{ 28 | {Type: "docker-image", Ref: "docker.io/library/alpine:latest"}, 29 | {Type: "docker-image", Ref: "docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c"}, 30 | }, 31 | }, 32 | imageNameKey: "docker.io/library/snowflake:latest,docker.io/library/flurry:latest", 33 | }) 34 | 35 | // Work around for non-determinism of metadata file 36 | // In order to use the test process must be executed with TEST_ALLOW_ALT_META=1 37 | expectedAlt := marshalResult(t, map[string]interface{}{ 38 | bInfoKey: Result{ 39 | Sources: []Source{ 40 | {Type: "docker-image", Ref: "docker.io/library/alpine:latest"}, 41 | {Type: "docker-image", Ref: "docker.io/library/alpine:latest@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c"}, 42 | }, 43 | }, 44 | imageNameKey: "docker.io/library/snowflake:latest,docker.io/library/flurry:latest", 45 | }) 46 | 47 | extModConfig := []byte(` 48 | { 49 | "docker.io/library/busybox:latest": "docker.io/library/alpine:latest@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c" 50 | } 51 | `) 52 | 53 | builtinModCfg := []byte(` 54 | [ 55 | {"match": "docker.io/library/busybox:latest", "replace": "docker.io/library/alpine:latest@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c"} 56 | ] 57 | `) 58 | 59 | t.Run("with invoke docker env", func(t *testing.T) { 60 | t.Run("non-build commands", func(t *testing.T) { 61 | t.Run("docker run", testCmd([]byte("hello"), withDockerArgs("run", "--rm", "--tmpfs=/tmp", "--pids-limit", "100", "busybox", "echo", "hello"))) 62 | }) 63 | t.Run("build commands", func(t *testing.T) { 64 | createBuildx(t) 65 | buildOpts := func(buildx bool) cmdOpt { 66 | return func(t *testing.T, cfg *cmdConfig) { 67 | withDockerfile(bytes.NewReader(testDockerfile))(t, cfg) 68 | args := []string{"build", "-t", "filtered"} 69 | if buildx { 70 | args = append([]string{"buildx"}, args...) 71 | } 72 | withDockerArgs(args...)(t, cfg) 73 | withAlt(expectedAlt)(t, cfg) 74 | withTags("snowflake", "flurry")(t, cfg) // validates that the buildkit metadata has the tags specified here 75 | withOutput("type=docker,dest="+filepath.Join(t.TempDir(), "img.tar"))(t, cfg) // validates that buildkit writes our tar file to this location. 76 | } 77 | } 78 | t.Run("pre-generate", func(t *testing.T) { 79 | modfile := Result{ 80 | Sources: []Source{ 81 | {Type: "docker-image", Ref: "docker.io/library/busybox:latest", Replace: "docker.io/library/alpine:latest@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c"}, 82 | }, 83 | } 84 | // pre-generate the mods instead of doing it on the fly when invoking docker. 85 | t.Run("context dir", func(t *testing.T) { 86 | t.Run("without buildx", testCmd(expected, buildOpts(false), withModfile(modfile))) 87 | t.Run("with buildx", testCmd(expected, buildOpts(true), withModfile(modfile))) 88 | }) 89 | t.Run("context stdin", func(t *testing.T) { 90 | t.Run("without buildx", testCmd(expected, withStdin, buildOpts(false), withModfile(modfile))) 91 | t.Run("with buildx", testCmd(expected, withStdin, buildOpts(true), withModfile(modfile))) 92 | }) 93 | }) 94 | t.Run("generate", func(t *testing.T) { 95 | t.Run("context stdin", func(t *testing.T) { 96 | t.Run("external mod", func(t *testing.T) { 97 | t.Run("without buildx", testCmd(expected, withStdin, withModProg, withModConfig(extModConfig), buildOpts(false))) 98 | t.Run("with buildx", testCmd(expected, withStdin, withModProg, withModConfig(extModConfig), buildOpts(false))) 99 | }) 100 | t.Run("builtin mod", func(t *testing.T) { 101 | t.Run("without buildx", testCmd(expected, withStdin, withModConfig(builtinModCfg), buildOpts(false))) 102 | t.Run("with buildx", testCmd(expected, withStdin, withModConfig(builtinModCfg), buildOpts(true))) 103 | }) 104 | }) 105 | }) 106 | }) 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /test/main_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "testing" 11 | ) 12 | 13 | var ( 14 | gnarlyPath string 15 | ) 16 | 17 | func TestMain(m *testing.M) { 18 | if IsModProg() { 19 | DoMod() 20 | return 21 | } 22 | 23 | dir, err := ioutil.TempDir("", "gnarly-test") 24 | if err != nil { 25 | panic(err) 26 | } 27 | cleanup := func() { 28 | if err := os.RemoveAll(dir); err != nil { 29 | fmt.Fprintln(os.Stderr, "error cleaning up test bin:", err) 30 | } 31 | } 32 | 33 | cmd := exec.Command("make", gnarly) 34 | cmd.Dir = filepath.Dir(getwd()) 35 | cmd.Env = append(os.Environ(), "OUTPUT="+dir) 36 | out, err := cmd.CombinedOutput() 37 | if err != nil { 38 | fmt.Fprintln(os.Stderr, "error compiling gnarly bin:", string(out)) 39 | os.Exit(1) 40 | } 41 | 42 | gnarlyPath = filepath.Join(dir, gnarly) 43 | os.Setenv("PATH", dir+":"+os.Getenv("PATH")) 44 | 45 | code := m.Run() 46 | cleanup() 47 | os.Exit(code) 48 | } 49 | 50 | func getwd() string { 51 | dir, err := os.Getwd() 52 | if err != nil { 53 | panic(err) 54 | } 55 | return dir 56 | } 57 | 58 | // AsModProg allows the current binary to be used as a mod program by gnarly. 59 | func AsModProg(t *testing.T) string { 60 | dir := t.TempDir() 61 | 62 | src, err := os.Readlink("/proc/self/exe") 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | target := filepath.Join(dir, modProg) 67 | if err := os.Symlink(src, target); err != nil { 68 | t.Fatal(err) 69 | } 70 | return target 71 | } 72 | 73 | // IsModProg is used to determine if the current execution should be done as a mod-prog 74 | // This is used from TestMain. 75 | func IsModProg() bool { 76 | return filepath.Base(os.Args[0]) == modProg 77 | } 78 | 79 | // DoMod executes as a mod-prog 80 | func DoMod() { 81 | ref := os.Args[1] 82 | configPath := os.Getenv("MOD_CONFIG") 83 | if configPath == "" { 84 | fmt.Fprintln(os.Stderr, "MOD_CONFIG not set") 85 | os.Exit(1) 86 | } 87 | 88 | configData, err := os.ReadFile(configPath) 89 | if err != nil { 90 | fmt.Fprintln(os.Stderr, "error reading config", err) 91 | os.Exit(1) 92 | } 93 | 94 | config := map[string]string{} 95 | if err := json.Unmarshal(configData, &config); err != nil { 96 | fmt.Fprintln(os.Stderr, "error parsing config", err) 97 | os.Exit(1) 98 | } 99 | 100 | fmt.Printf(config[ref]) 101 | } 102 | -------------------------------------------------------------------------------- /test/mod_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestModOutput(t *testing.T) { 12 | var ( 13 | testDockerfile = []byte(` 14 | FROM foo:1.0 AS foo1 15 | FROM foo as foo2 16 | FROM docker.io/library/foo:1.0 AS foo3 17 | FROM unnamed 18 | FROM foo:unhandled AS foo4 19 | `) 20 | extExpectedModfileOutput = Result{ 21 | Sources: []Source{ 22 | {Type: "docker-image", Ref: "docker.io/library/foo:1.0", Replace: "docker.io/library/bar:1.0"}, 23 | {Type: "docker-image", Ref: "docker.io/library/foo:latest", Replace: "docker.io/library/bar:latest"}, 24 | {Type: "docker-image", Ref: "docker.io/library/foo:unhandled"}, 25 | }, 26 | } 27 | extModConfig = []byte(` 28 | { 29 | "docker.io/library/foo:1.0": "docker.io/library/bar:1.0", 30 | "docker.io/library/foo:latest": "docker.io/library/bar:latest" 31 | } 32 | `) 33 | 34 | // This just gives the same output as the 'external' mod config 35 | // This is not going to test that the actual replacement engine works as expected, for that we can use unit tests. 36 | builtinModConfig = []byte(` 37 | [ 38 | { 39 | "match": "docker.io/library/foo:1.0", "replace": "docker.io/library/bar:1.0" 40 | }, 41 | { 42 | "match": "docker.io/library/foo:latest", "replace": "docker.io/library/bar:latest" 43 | } 44 | ] 45 | `) 46 | ) 47 | 48 | modFileOutput := marshalResult(t, extExpectedModfileOutput) 49 | modFileOutput = bytes.TrimSpace(modFileOutput) 50 | flagsOutput := []byte(strings.TrimSpace(extExpectedModfileOutput.AsFlags())) 51 | 52 | t.Run("stdin", func(t *testing.T) { 53 | t.Run("external mod", func(t *testing.T) { 54 | t.Run("modfile", testCmd(modFileOutput, withStdin, withDockerfile(bytes.NewReader(testDockerfile)), withFormat("modfile"), withModProg, withModConfig(extModConfig))) 55 | t.Run("build-flags", testCmd(flagsOutput, withStdin, withDockerfile(bytes.NewReader(testDockerfile)), withFormat("build-flags"), withModProg, withModConfig(extModConfig))) 56 | }) 57 | t.Run("builtin mod", func(t *testing.T) { 58 | t.Run("modfile", testCmd(modFileOutput, withStdin, withDockerfile(bytes.NewReader(testDockerfile)), withFormat("modfile"), withModConfig(builtinModConfig))) 59 | t.Run("build-flags", testCmd(flagsOutput, withStdin, withDockerfile(bytes.NewReader(testDockerfile)), withFormat("build-flags"), withModConfig(builtinModConfig))) 60 | }) 61 | }) 62 | t.Run("file", func(t *testing.T) { 63 | dir := t.TempDir() 64 | dockerfilePath := filepath.Join(dir, "Dockerfile") 65 | if err := os.WriteFile(dockerfilePath, testDockerfile, 0644); err != nil { 66 | t.Fatal(err) 67 | } 68 | t.Run("external mod", func(t *testing.T) { 69 | t.Run("modfile", testCmd(modFileOutput, withFormat("modfile"), withModProg, withModConfig(extModConfig), withDockerfile(bytes.NewReader(testDockerfile)))) 70 | t.Run("build-flags", testCmd(flagsOutput, withFormat("build-flags"), withModProg, withModConfig(extModConfig), withDockerfile(bytes.NewReader(testDockerfile)))) 71 | }) 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /test/util_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "sort" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | "testing" 17 | ) 18 | 19 | const ( 20 | modProg = "modprog" 21 | docker = "docker" 22 | gnarly = "gnarly" 23 | 24 | bInfoKey = "containerimage.buildinfo" 25 | imageNameKey = "image.name" 26 | ) 27 | 28 | type Result struct { 29 | Sources []Source `json:"sources"` 30 | Image []string `json:"-"` 31 | } 32 | 33 | func unmarshalResult(t *testing.T, b []byte) Result { 34 | r := map[string]json.RawMessage{} 35 | if err := json.Unmarshal(b, &r); err != nil { 36 | t.Fatal(err) 37 | } 38 | var ret Result 39 | if v, ok := r[bInfoKey]; ok { 40 | if err := json.Unmarshal(v, &ret); err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | var s string 45 | if err := json.Unmarshal(r[imageNameKey], &s); err != nil { 46 | t.Fatal(err) 47 | } 48 | ret.Image = strings.Split(s, ",") 49 | return ret 50 | } 51 | 52 | if err := json.Unmarshal(b, &ret); err != nil { 53 | t.Fatal(err) 54 | } 55 | return ret 56 | } 57 | 58 | func marshalResult(t *testing.T, val interface{}) []byte { 59 | t.Helper() 60 | 61 | switch v := val.(type) { 62 | case Result: 63 | sort.Slice(v.Sources, func(i, j int) bool { 64 | return v.Sources[i].Ref < v.Sources[j].Ref 65 | }) 66 | val = v 67 | if len(v.Image) > 0 { 68 | val = map[string]interface{}{ 69 | bInfoKey: v, 70 | imageNameKey: strings.Join(v.Image, ","), 71 | } 72 | } 73 | case map[string]interface{}: 74 | vv, ok := v[bInfoKey] 75 | if ok { 76 | vr := vv.(Result) 77 | sort.Slice(vr.Sources, func(i, j int) bool { 78 | return vr.Sources[i].Ref < vr.Sources[j].Ref 79 | }) 80 | v[bInfoKey] = vr 81 | } 82 | val = v 83 | } 84 | 85 | b, err := json.MarshalIndent(val, "", "\t") 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | return b 90 | } 91 | 92 | type Source struct { 93 | Type string `json:"type"` 94 | Ref string `json:"ref"` 95 | Replace string `json:"replace,omitempty"` 96 | } 97 | 98 | func (s Source) AsFlag() string { 99 | if s.Replace == "" { 100 | return "" 101 | } 102 | return fmt.Sprintf("--build-context %s=%s://%s", s.Ref, s.Type, s.Replace) 103 | } 104 | 105 | func (r Result) AsFlags() string { 106 | sb := &strings.Builder{} 107 | for _, s := range r.Sources { 108 | sb.WriteString(s.AsFlag()) 109 | sb.WriteString(" ") 110 | } 111 | return sb.String() 112 | } 113 | 114 | type cmdOpt func(t *testing.T, cfg *cmdConfig) 115 | 116 | func withStdin(t *testing.T, cfg *cmdConfig) { 117 | cfg.Stdin = true 118 | } 119 | 120 | func withDockerfile(dockerfile io.Reader) cmdOpt { 121 | return func(t *testing.T, cfg *cmdConfig) { 122 | cfg.Dockerfile = dockerfile 123 | } 124 | } 125 | 126 | func withFormat(format string) cmdOpt { 127 | return func(t *testing.T, cfg *cmdConfig) { 128 | cfg.Format = format 129 | } 130 | } 131 | 132 | func withModProg(t *testing.T, cfg *cmdConfig) { 133 | cfg.ModProg = AsModProg(t) 134 | } 135 | 136 | func withModConfig(config []byte) cmdOpt { 137 | return func(t *testing.T, cfg *cmdConfig) { 138 | dir := t.TempDir() 139 | configPath := filepath.Join(dir, "config") 140 | if err := ioutil.WriteFile(configPath, config, 0644); err != nil { 141 | t.Fatal(err) 142 | } 143 | cfg.ModConfig = configPath 144 | } 145 | } 146 | 147 | func withDockerArgs(args ...string) cmdOpt { 148 | return func(t *testing.T, cfg *cmdConfig) { 149 | cfg.DockerArgs = args 150 | } 151 | } 152 | 153 | func withTags(tags ...string) cmdOpt { 154 | return func(t *testing.T, cfg *cmdConfig) { 155 | cfg.Tags = append(cfg.Tags, tags...) 156 | } 157 | } 158 | 159 | func withOutput(output ...string) cmdOpt { 160 | return func(t *testing.T, cfg *cmdConfig) { 161 | cfg.Output = append(cfg.Output, output...) 162 | } 163 | } 164 | 165 | func withModfile(r Result) cmdOpt { 166 | return func(t *testing.T, cfg *cmdConfig) { 167 | p := filepath.Join(t.TempDir(), "modfile") 168 | if err := os.WriteFile(p, marshalResult(t, r), 0600); err != nil { 169 | t.Fatal(err) 170 | } 171 | cfg.Modfile = p 172 | } 173 | } 174 | 175 | func withAlt(alt []byte) cmdOpt { 176 | return func(t *testing.T, cfg *cmdConfig) { 177 | cfg.expectedAlt = alt 178 | } 179 | } 180 | 181 | type cmdConfig struct { 182 | Format string 183 | AsDocker bool 184 | DockerArgs []string 185 | Dockerfile io.Reader 186 | Stdin bool 187 | ModProg string 188 | ModConfig string 189 | Modfile string 190 | expectedAlt []byte 191 | Tags []string 192 | Output []string 193 | } 194 | 195 | var openOnce sync.Once 196 | 197 | // This is a hack to bust the go test cache automatically 198 | func bustCmdCache(t *testing.T) { 199 | openOnce.Do(func() { 200 | // Opening the root dir busts the cache any time anything changes 201 | // We could probably scope this down to just non-test .go files, but we'd need to list them using `ls` or something, because `os.ReadDir` will open the dir anyway. 202 | f, err := os.Open(filepath.Dir(getwd())) 203 | if err != nil { 204 | t.Fatal(err) 205 | } 206 | f.Close() 207 | }) 208 | } 209 | 210 | func testCmd(expected []byte, opts ...cmdOpt) func(t *testing.T) { 211 | return func(t *testing.T) { 212 | bustCmdCache(t) 213 | 214 | var cfg cmdConfig 215 | for _, o := range opts { 216 | o(t, &cfg) 217 | } 218 | 219 | prog := gnarly 220 | if cfg.AsDocker { 221 | prog = docker 222 | } 223 | cmd := exec.Command(prog, cfg.DockerArgs...) 224 | cmd.Env = os.Environ() 225 | cmd.Env = append(cmd.Env, "DEBUG=1") 226 | 227 | if len(cfg.Tags) > 0 { 228 | tags := "BUILDKIT_TAG=" + strings.Join(cfg.Tags, ",") 229 | cmd.Env = append(cmd.Env, tags) 230 | } 231 | 232 | if len(cfg.DockerArgs) > 0 { 233 | if !cfg.AsDocker { 234 | cmd.Env = append(cmd.Env, "DOCKERFILE_MOD_INVOKE_DOCKER=1") 235 | } else { 236 | dir := t.TempDir() 237 | p := filepath.Join(dir, docker) 238 | if err := os.Symlink(gnarlyPath, p); err != nil { 239 | t.Fatal(err) 240 | } 241 | t.Setenv("PATH", p+":"+os.Getenv("PATH")) 242 | } 243 | } 244 | 245 | metdataFilePath := filepath.Join(t.TempDir(), "metadata.json") 246 | if len(cfg.DockerArgs) > 0 || cfg.AsDocker { 247 | cmd.Env = append(cmd.Env, "BUILDKIT_METADATA_FILE="+metdataFilePath) 248 | } 249 | 250 | if cfg.ModProg != "" { 251 | cmd.Env = append(cmd.Env, "DOCKERFILE_MOD_PROG="+cfg.ModProg) 252 | } 253 | if cfg.ModConfig != "" { 254 | cmd.Env = append(cmd.Env, "DOCKERFILE_MOD_CONFIG="+cfg.ModConfig) 255 | } 256 | if cfg.Format != "" { 257 | cmd.Env = append(cmd.Env, "DOCKERFILE_MOD_FORMAT="+cfg.Format) 258 | } 259 | if cfg.Modfile != "" { 260 | cmd.Env = append(cmd.Env, "DOCKERFILE_MOD_PATH="+cfg.Modfile) 261 | } 262 | if len(cfg.Output) > 0 { 263 | cmd.Env = append(cmd.Env, "BUILDKIT_OUTPUT="+strings.Join(cfg.Output, "\n")) 264 | } 265 | 266 | if cfg.Dockerfile != nil { 267 | if len(cfg.DockerArgs) > 0 { 268 | cmd.Args = append(cmd.Args, "--output=type=docker,dest="+filepath.Join(t.TempDir(), "img.tar")) 269 | } 270 | if cfg.Stdin { 271 | cmd.Stdin = cfg.Dockerfile 272 | if cfg.AsDocker || len(cfg.DockerArgs) > 0 { 273 | cmd.Args = append(cmd.Args, "-") 274 | } 275 | } else { 276 | data, err := ioutil.ReadAll(cfg.Dockerfile) 277 | if err != nil { 278 | t.Fatal(err) 279 | } 280 | 281 | p := filepath.Join(t.TempDir(), "Dockerfile") 282 | if err := os.WriteFile(p, data, 0644); err != nil { 283 | t.Fatal(err) 284 | } 285 | 286 | if cfg.AsDocker || len(cfg.DockerArgs) > 0 { 287 | p = filepath.Dir(p) 288 | } 289 | cmd.Args = append(cmd.Args, p) 290 | } 291 | } 292 | 293 | stdout := bytes.NewBuffer(nil) 294 | stderr := bytes.NewBuffer(nil) 295 | 296 | defer func() { 297 | if t.Failed() { 298 | t.Log(cmd.Args) 299 | if stdout.Len() > 0 { 300 | t.Log(stdout) 301 | } 302 | if stderr.Len() > 0 { 303 | t.Log(stderr) 304 | } 305 | } 306 | }() 307 | 308 | cmd.Stdout = stdout 309 | cmd.Stderr = stderr 310 | if err := cmd.Run(); err != nil { 311 | t.Fatal(err) 312 | } 313 | 314 | out := stdout.Bytes() 315 | if cfg.AsDocker || len(cfg.DockerArgs) > 0 { 316 | if cfg.DockerArgs[0] == "build" || (cfg.DockerArgs[0] == "buildx" && cfg.DockerArgs[1] == "build") { 317 | meta, err := os.ReadFile(metdataFilePath) 318 | if err != nil { 319 | t.Fatalf("error reading docker build metadata file: %v", err) 320 | } 321 | 322 | actualR := unmarshalResult(t, meta) 323 | out = marshalResult(t, actualR) 324 | 325 | exepctedR := unmarshalResult(t, expected) 326 | expected = marshalResult(t, exepctedR) 327 | } 328 | } 329 | 330 | for _, o := range cfg.Output { 331 | split := strings.SplitN(o, "dest=", 2) 332 | if len(split) > 1 { 333 | _, err := os.Stat(strings.SplitN(split[1], ",", 2)[0]) 334 | if err != nil { 335 | t.Errorf("could not stat output file: %v", err) 336 | } 337 | } 338 | } 339 | 340 | out = bytes.TrimSpace(out) 341 | if !bytes.Equal(out, expected) { 342 | var allowAlt bool 343 | if cfg.expectedAlt != nil { 344 | var err error 345 | if allowAltEnv := os.Getenv("TEST_ALLOW_ALT_META"); allowAltEnv != "" { 346 | allowAlt, err = strconv.ParseBool(allowAltEnv) 347 | if err != nil { 348 | t.Log(err) 349 | } 350 | } 351 | } 352 | if !allowAlt { 353 | t.Fatalf("expected %s, got %s", string(expected), string(out)) 354 | } 355 | if !bytes.Equal(out, cfg.expectedAlt) { 356 | t.Errorf("expected %s, got %s", string(expected), string(out)) 357 | t.Fatalf("expected %s, got %s", string(cfg.expectedAlt), string(out)) 358 | } 359 | } 360 | } 361 | } 362 | 363 | func createBuildx(t *testing.T) { 364 | builderName := strings.ToLower(strings.Replace(t.Name(), "/", "_", -1)) 365 | if _, err := exec.Command(docker, "buildx", "create", "--name", builderName).CombinedOutput(); err != nil { 366 | exec.Command(docker, "buildx", "create", "--name", builderName).Run() 367 | out, err := exec.Command(docker, "buildx", "create", "--name", builderName).CombinedOutput() 368 | if err != nil { 369 | t.Fatal(string(out)) 370 | } 371 | } 372 | t.Cleanup(func() { 373 | out, err := exec.Command("docker", "buildx", "rm", builderName).CombinedOutput() 374 | if err != nil { 375 | t.Log(string(out)) 376 | } 377 | }) 378 | 379 | out, err := exec.Command(docker, "buildx", "inspect", "--bootstrap", builderName).CombinedOutput() 380 | if err != nil { 381 | t.Fatal(string(out)) 382 | } 383 | t.Setenv("BUILDX_BUILDER", builderName) 384 | } 385 | --------------------------------------------------------------------------------