├── .envrc ├── .github └── FUNDING.yml ├── .gitignore ├── .gitmodules ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── NOTICE.md ├── README.md ├── bin └── setup-cgroups ├── buildkitd.go ├── buildkitd_config.go ├── buildkitd_test.go ├── cmd ├── build │ └── main.go └── task │ └── main.go ├── example.yml ├── go.mod ├── go.sum ├── registry.go ├── scripts ├── build ├── build-image ├── push-image ├── setup-buildkit.sh └── test ├── task.go ├── task_test.go ├── testdata ├── add-hosts │ └── Dockerfile ├── basic │ └── Dockerfile ├── build-args │ ├── Dockerfile │ ├── build_arg_file │ ├── build_args_file │ └── build_args_file.yaml ├── buildkit-secret │ ├── Dockerfile │ └── secret ├── buildkit-ssh │ ├── Dockerfile │ ├── id_rsa_test │ └── id_rsa_test.pub ├── buildkitd-config │ └── mirrors.toml ├── dockerfile-path │ ├── Dockerfile │ └── hello.Dockerfile ├── image-args │ ├── Dockerfile │ └── Dockerfile.uppercase ├── labels │ ├── Dockerfile │ ├── label_file │ ├── label_layer.dockerfile │ └── labels_file ├── mirror │ └── Dockerfile ├── multi-arch │ └── Dockerfile ├── multi-target │ └── Dockerfile ├── target │ ├── Dockerfile │ └── target_file └── unpack-rootfs │ └── Dockerfile ├── types.go └── unpack.go /.envrc: -------------------------------------------------------------------------------- 1 | export PATH=$PWD/bin:$PATH 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [taylorsilva] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/buildctl 2 | bin/buildkitd 3 | bin/buildkit-* 4 | bin/rootless* 5 | bin/build 6 | bin/task 7 | image/ 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "rootlesskit"] 2 | path = rootlesskit 3 | url = https://github.com/rootless-containers/rootlesskit 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # contributing 2 | 3 | This project runs `buildkit` which is most easily run natively on Linux. 4 | 5 | The repository contains submodules; they must be initialized first like so: 6 | 7 | ```sh 8 | git submodule update --init --recursive 9 | ``` 10 | 11 | ## building 12 | 13 | The `Dockerfile` makes use of the experimental `RUN --mount` flag, enabled by 14 | the following: 15 | 16 | ```sh 17 | export DOCKER_BUILDKIT=1 18 | ``` 19 | 20 | Building can be done with `docker build` as normal, though if you're planning 21 | to test this as a Concourse task you'll need to tag and push your own image: 22 | 23 | ```sh 24 | docker build -t myuser/oci-build-task . 25 | docker push myuser/oci-build-task 26 | ``` 27 | 28 | ...and then reference `myuser/oci-build-task` in your task. 29 | 30 | 31 | ## running tests 32 | 33 | The tests only run on Linux. If your on a non-linux machine, you can use Docker 34 | to quickly build yourself a dev environment by running the following commands: 35 | 36 | ```sh 37 | $ docker run -it -v ".:/src" --privileged cgr.dev/chainguard/wolfi-base 38 | > cd /src 39 | > apk add bash curl go 40 | > ./scripts/setup-buildkit.sh 41 | ``` 42 | 43 | The tests can be run rootless, though doing so requires `newuidmap` and 44 | `newgidmap` to be installed: 45 | 46 | ```sh 47 | apt install uidmap 48 | ``` 49 | 50 | Once this is all done, the tests can be run like so: 51 | 52 | ```sh 53 | ./scripts/test 54 | ``` 55 | 56 | > side note: it would be *super cool* to leverage rootless mode to be able to 57 | > run the tests as part of the `Dockerfile` - unfortunately image building 58 | > involves bind-mounting, which `docker build` does not permit. 59 | 60 | ## pushing to `concourse/oci-build-task` 61 | 62 | The pipeline for managing this task is in the [concourse/ci 63 | repo](https://github.com/concourse/ci/blob/master/pipelines/oci-build-task.yml). 64 | The pipeline itself is running in our CI here: 65 | [https://ci.concourse-ci.org/teams/main/pipelines/oci-build-task](https://ci.concourse-ci.org/teams/main/pipelines/oci-build-task) 66 | 67 | You can use the `publish-*` jobs to release a new version of the resource. 68 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | ARG base_image=cgr.dev/chainguard/wolfi-base 3 | ARG builder_image=concourse/golang-builder 4 | 5 | ARG BUILDPLATFORM 6 | FROM --platform=${BUILDPLATFORM} ${builder_image} AS builder 7 | 8 | ARG TARGETOS 9 | ARG TARGETARCH 10 | ENV GOOS=$TARGETOS 11 | ENV GOARCH=$TARGETARCH 12 | 13 | COPY . /src 14 | WORKDIR /src 15 | RUN --mount=type=cache,target=/root/.cache/go-build go mod download 16 | ENV CGO_ENABLED=0 17 | RUN go build -o /assets/task ./cmd/task 18 | RUN go build -o /assets/build ./cmd/build 19 | 20 | ARG BUILDKIT_VERSION=0.22.0 21 | WORKDIR /buildkit 22 | RUN apk --no-cache add curl 23 | RUN curl -L "https://github.com/moby/buildkit/releases/download/v${BUILDKIT_VERSION}/buildkit-v${BUILDKIT_VERSION}.linux-${TARGETARCH}.tar.gz" -o buildkit.tar.gz && \ 24 | tar xf buildkit.tar.gz 25 | 26 | FROM ${base_image} AS task 27 | RUN apk --no-cache add \ 28 | cmd:umount \ 29 | cmd:mount \ 30 | cmd:mountpoint 31 | COPY --from=builder /assets/task /usr/bin/ 32 | COPY --from=builder /assets/build /usr/bin/ 33 | COPY --from=builder /buildkit/bin/ /usr/bin/ 34 | COPY bin/setup-cgroups /usr/bin/ 35 | RUN for cmd in task build buildkitd buildctl mount umount mountpoint setup-cgroups; do \ 36 | which $cmd >/dev/null || { echo "$cmd binary not found!"; exit 1; }; \ 37 | done 38 | ENTRYPOINT ["task"] 39 | 40 | FROM task 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018 Pivotal Software, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | this file except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `oci-build` task 2 | 3 | A Concourse task for building [OCI 4 | images](https://github.com/opencontainers/image-spec). Currently uses 5 | [`buildkit`](http://github.com/moby/buildkit) for building. 6 | 7 | A stretch goal of this is to support running without `privileged: true`, though 8 | it currently still requires it. 9 | 10 | 11 | 12 | - [usage](#usage) 13 | * [`image_resource`](#image_resource) 14 | * [`params`](#params) 15 | * [`inputs`](#inputs) 16 | * [`outputs`](#outputs) 17 | * [`caches`](#caches) 18 | * [`run`](#run) 19 | - [migrating from the `docker-image` resource](#migrating-from-the-docker-image-resource) 20 | - [differences from `builder` task](#differences-from-builder-task) 21 | - [example](#example) 22 | 23 | 24 | 25 | ## usage 26 | 27 | The task implementation is available as an image on Docker Hub at 28 | [`concourse/oci-build-task`](http://hub.docker.com/r/concourse/oci-build-task). 29 | (This image is built from [`Dockerfile`](Dockerfile) using the `oci-build` task 30 | itself.) 31 | 32 | This task implementation started as a spike to explore patterns around 33 | [reusable tasks](https://github.com/concourse/rfcs/issues/7) to hopefully lead 34 | to a proper RFC. Until that RFC is written and implemented, configuration is 35 | still done by way of providing your own task config as follows: 36 | 37 | ### `image_resource` 38 | 39 | First, your task needs to point to the `oci-build-task` image: 40 | 41 | ```yaml 42 | image_resource: 43 | type: registry-image 44 | source: 45 | repository: concourse/oci-build-task 46 | ``` 47 | 48 | ### `params` 49 | 50 | Any of the following optional parameters may be specified. These are all exposed 51 | as _environment variables_ to the task, therefore only string values are 52 | allowed. This is a pain point with re-usable tasks that will ideally be resolved 53 | by [prototypes](https://github.com/concourse/rfcs/blob/master/037-prototypes/proposal.md). 54 | 55 | * `CONTEXT` (default `.`): the path to the directory to provide as the context 56 | for the build. 57 | 58 | * `DOCKERFILE` (default `$CONTEXT/Dockerfile`): the path to the `Dockerfile` 59 | to build. 60 | 61 | * `BUILDKIT_SSH` your ssh key location that is mounted in your `Dockerfile`. This is 62 | generally used for pulling dependencies from private repositories. 63 | 64 | For Example. In your `Dockerfile`, you can mount a key as 65 | ``` 66 | RUN --mount=type=ssh,id=github_ssh_key pip install -U -r ./hats/requirements-test.txt 67 | ``` 68 | 69 | Then in your Concourse YAML configuration: 70 | ``` 71 | params: 72 | BUILDKIT_SSH: github_ssh_key= 73 | ``` 74 | 75 | Read more about ssh mount [here](https://docs.docker.com/develop/develop-images/build_enhancements/). 76 | 77 | * `BUILD_ARG_*`: params prefixed with `BUILD_ARG_` will be provided as build 78 | args. For example `BUILD_ARG_foo=bar`, will set the `foo` build arg as `bar`. 79 | 80 | * `BUILD_ARGS_FILE` (default empty): path to a file containing build args. By 81 | default the task will assume each line is in the form `foo=bar`, one per 82 | line. Empty lines are skipped. If the file ends in `yml` or `yaml` it will 83 | be parsed as a YAML file. The YAML file can only contain string keys and 84 | values. 85 | 86 | Example simple file contents: 87 | 88 | ``` 89 | EMAIL=me@example.com 90 | HOW_MANY_THINGS=1 91 | DO_THING=false 92 | ``` 93 | Example YAML file contents: 94 | 95 | ```yaml 96 | EMAIL: me@example.com 97 | HOW_MANY_THINGS: "1" 98 | DO_THING: "false" 99 | MULTI_LINE_ARG: | 100 | thing1 101 | thing2 102 | ``` 103 | 104 | * `BUILDKIT_SECRET_*`: files with extra secrets which are made available via 105 | `--mount=type=secret,id=...`. See [New Docker Build secret information](https://docs.docker.com/develop/develop-images/build_enhancements/#new-docker-build-secret-information) for more information on build secrets. 106 | 107 | For example, running with `BUILDKIT_SECRET_config=my-repo/config` will allow 108 | you to do the following... 109 | 110 | ``` 111 | RUN --mount=type=secret,id=config cat /run/secrets/config 112 | ``` 113 | 114 | * `BUILDKIT_SECRETTEXT_*`: literal text of extra secrets to be made available 115 | via the same mechanism described for `$BUILDKIT_SECRET_*` above. The 116 | difference is that this is easier to use with credential managers: 117 | 118 | `BUILDKIT_SECRETTEXT_mysecret=(( mysecret ))` puts the content that 119 | `(( mysecret ))` expands to in `/run/secrets/mysecret`. 120 | 121 | * `IMAGE_ARG_*`: params prefixed with `IMAGE_ARG_*` point to image tarballs 122 | (i.e. `docker save` format) or path to images in OCI layout format, to preload 123 | so that they do not have to be fetched during the build. An image reference 124 | will be provided as the given build arg name. For example, 125 | `IMAGE_ARG_base_image=ubuntu/image.tar` will set `base_image` to a local image 126 | reference for using `ubuntu/image.tar`. 127 | 128 | This must be accepted as an argument for use; for example: 129 | 130 | ``` 131 | ARG base_image 132 | FROM ${base_image} 133 | ``` 134 | 135 | * `IMAGE_PLATFORM`: Specify the target platform(s) to build the image for. For 136 | example `IMAGE_PLATFORM=linux/arm64,linux/amd64` will build the image for the 137 | Linux OS and architectures `arm64` and `amd64`. By default, images will be 138 | built for the current worker's platform that the task is running on. 139 | 140 | * `LABEL_*`: params prefixed with `LABEL_` will be set as image labels. 141 | For example `LABEL_foo=bar`, will set the `foo` label to `bar`. 142 | 143 | * `LABELS_FILE` (default empty): path to a file containing labels in 144 | the form `foo=bar`, one per line. Empty lines are skipped. 145 | 146 | * `TARGET` (default empty): a target build stage to build, as named with the 147 | `FROM … AS ` syntax in your `Dockerfile`. 148 | 149 | * `TARGET_FILE` (default empty): path to a file containing the name of the 150 | target build stage to build. 151 | 152 | * `ADDITIONAL_TARGETS` (default empty): a comma-separated (`,`) list of 153 | additional target build stages to build. 154 | 155 | * `REGISTRY_MIRRORS` (default empty): a comma-separated (`,`) list of registry 156 | mirrors to use for `docker.io`. If you need to specify authentication details 157 | then consider using `BUILDKIT_EXTRA_CONFIG` instead. 158 | 159 | * `UNPACK_ROOTFS` (default `false`): unpack the image as Concourse's image 160 | format (`rootfs/`, `metadata.json`) for use with the [`image` task step 161 | option](https://concourse-ci.org/jobs.html#schema.step.task-step.image). 162 | 163 | * `OUTPUT_OCI` (default `false`): outputs an OCI compliant image, allowing 164 | for multi-arch image builds when setting IMAGE_PLATFORM to 165 | [multiple platforms](https://docs.docker.com/desktop/extensions-sdk/extensions/multi-arch/). 166 | The image output format will be a directory when this flag is set to true. 167 | 168 | * `BUILDKIT_ADD_HOSTS` (default empty): extra host definitions for `buildkit` 169 | to properly resolve custom hostnames. The value is as comma-separated 170 | (`,`) list of key-value pairs (using syntax `hostname=ip-address`), each 171 | defining an IP address for resolving some custom hostname. 172 | 173 | * `BUILDKIT_EXTRA_CONFIG` (default empty): a string written verbatim to builkit's 174 | TOML config file. See [buildkitd.toml](https://docs.docker.com/build/buildkit/toml-configuration/). 175 | 176 | ### `inputs` 177 | 178 | There are no required inputs - your task should just list each artifact it 179 | needs as an input. Typically this is in close correlation with `$CONTEXT`: 180 | 181 | ```yaml 182 | params: 183 | CONTEXT: my-image 184 | 185 | inputs: 186 | - name: my-image 187 | ``` 188 | 189 | Should your build be dependent on multiple inputs, you may want to leave 190 | `CONTEXT` as its default (`.`) and set an explicit path to the `DOCKERFILE`: 191 | 192 | ```yaml 193 | params: 194 | DOCKERFILE: my-repo/Dockerfile 195 | 196 | inputs: 197 | - name: my-repo 198 | - name: some-dependency 199 | ``` 200 | 201 | It might also make sense to place one input under another, like so: 202 | 203 | ```yaml 204 | params: 205 | CONTEXT: my-repo 206 | 207 | inputs: 208 | - name: my-repo 209 | - name: some-dependency 210 | path: my-repo/some-dependency 211 | ``` 212 | 213 | Or, to fully rely on the default behavior and use `path` to wire up the context 214 | accordingly, you could set your primary context as `path: .` and set up any 215 | additional inputs underneath: 216 | 217 | ```yaml 218 | inputs: 219 | - name: my-repo 220 | path: . 221 | - name: some-dependency 222 | ``` 223 | 224 | ### `outputs` 225 | 226 | A single output named `image` may be configured: 227 | 228 | ```yaml 229 | outputs: 230 | - name: image 231 | ``` 232 | 233 | Use [`output_mapping`] to map this output to a different name in your build plan. 234 | This approach should be used if you're building multiple images in parallel so that 235 | they can have distinct names. 236 | 237 | [`output_mapping`]: https://concourse-ci.org/jobs.html#schema.step.task-step.output_mapping 238 | 239 | The output will contain the following files: 240 | 241 | * `image.tar`: the OCI image tarball. This tarball can be uploaded to a 242 | registry using the [Registry Image 243 | resource](https://github.com/concourse/registry-image-resource#out-push-an-image-up-to-the-registry-under-the-given-tags). 244 | 245 | * `digest`: the digest of the OCI config. This file can be used to tag the 246 | image after it has been loaded with `docker load`, like so: 247 | 248 | ```sh 249 | docker load -i image/image.tar 250 | docker tag $(cat image/digest) my-name 251 | ``` 252 | 253 | If `$UNPACK_ROOTFS` is configured, the following additional entries will be 254 | created: 255 | 256 | * `rootfs/*`: the unpacked contents of the image's filesystem. 257 | 258 | * `metadata.json`: a JSON file containing the image's env and user 259 | configuration. 260 | 261 | This is a Concourse-specific format to support using the newly built image for 262 | a subsequent task by pointing the task step's [`image` 263 | option](https://concourse-ci.org/task-step.html#task-step-image) to the output, 264 | like so: 265 | 266 | ```yaml 267 | plan: 268 | - task: build-image 269 | params: 270 | UNPACK_ROOTFS: true 271 | output_mapping: {image: my-built-image} 272 | - task: use-image 273 | image: my-built-image 274 | ``` 275 | 276 | (The `output_mapping` here is just for clarity; alternatively you could just 277 | set `image: image`.) 278 | 279 | > Note: at some point Concourse will likely standardize on OCI instead. 280 | 281 | ### `caches` 282 | 283 | Caching can be enabled by caching the `cache` path on the task: 284 | 285 | ```yaml 286 | caches: 287 | - path: cache 288 | ``` 289 | 290 | This only caches the build layers that Buildkit makes and will only be hit if 291 | the same worker is used between one build and the next. 292 | 293 | NOTE: the contents of `--mount=type=cache` directories are not cached, see https://github.com/concourse/oci-build-task/issues/87 294 | 295 | ### `run` 296 | 297 | Your task should run the `build` executable: 298 | 299 | ```yaml 300 | run: 301 | path: build 302 | ``` 303 | 304 | 305 | ## migrating from the `docker-image` resource 306 | 307 | The `docker-image` resource was previously used for building and pushing a 308 | Docker image to a registry in one fell swoop. 309 | 310 | The `oci-build` task, in contrast, only supports building images - it does not 311 | support pushing or even tagging the image. It can be used to build an image and 312 | use it for a subsequent task image without pushing it to a registry, by 313 | configuring `$UNPACK_ROOTFS`. 314 | 315 | In order to push the newly built image, you can use a resource like the 316 | [`registry-image` 317 | resource](https://github.com/concourse/registry-image-resource) like so: 318 | 319 | ```yaml 320 | resources: 321 | - name: my-image-src 322 | type: git 323 | source: 324 | uri: https://github.com/... 325 | 326 | - name: my-image 327 | type: registry-image 328 | source: 329 | repository: my-user/my-repo 330 | 331 | jobs: 332 | - name: build-and-push 333 | plan: 334 | # fetch repository source (containing Dockerfile) 335 | - get: my-image-src 336 | 337 | # build using `oci-build` task 338 | # 339 | # note: this task config could be pushed into `my-image-src` and loaded using 340 | # `file:` instead 341 | - task: build 342 | privileged: true 343 | config: 344 | platform: linux 345 | 346 | image_resource: 347 | type: registry-image 348 | source: 349 | repository: concourse/oci-build-task 350 | 351 | inputs: 352 | - name: my-image-src 353 | path: . 354 | 355 | outputs: 356 | - name: image 357 | 358 | run: 359 | path: build 360 | 361 | # push using `registry-image` resource 362 | - put: my-image 363 | params: {image: image/image.tar} 364 | ``` 365 | 366 | 367 | ## differences from `builder` task 368 | 369 | The [`builder` task](https://github.com/concourse/builder-task) was a stepping 370 | stone that led to the `oci-build` task. It is now deprecated. The transition 371 | should be relatively smooth, with the following differences: 372 | 373 | * The `oci-build` task does not support configuring `$REPOSITORY` or `$TAG`. 374 | * for running the image with `docker`, a `digest` file is provided which can 375 | be tagged with `docker tag` 376 | * for pushing the image, the repository and tag are configured in the 377 | [`registry-image` 378 | resource](https://github.com/concourse/registry-image-resource) 379 | * The `oci-build` task has a more efficient caching implementation. By using 380 | `buildkit` directly we can make use of its `local` cache exporter/importer, 381 | which doesn't require a separate translation step for saving into the task 382 | cache. 383 | * This task is written in Go instead of Bash, and has tests! 384 | 385 | 386 | ## example 387 | 388 | This repo contains an `example.yml`, which builds the image for the task 389 | itself: 390 | 391 | ```sh 392 | fly -t dev execute -c example.yml -i context=. -o image=. -p 393 | docker load -i image.tar 394 | ``` 395 | 396 | That `-p` at the end is not a typo; it runs the task with elevated privileges. 397 | -------------------------------------------------------------------------------- /bin/setup-cgroups: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e -u 4 | 5 | if mountpoint -q /sys/fs/cgroup; then 6 | # already mounted; skip 7 | exit 0 8 | fi 9 | 10 | mkdir -p /sys/fs/cgroup 11 | mount -t tmpfs -o uid=0,gid=0,mode=0755 cgroup /sys/fs/cgroup 12 | 13 | sed -e 1d /proc/cgroups | while read -r sys _ _ enabled; do 14 | if [ "$enabled" != "1" ]; then 15 | # subsystem disabled; skip 16 | continue 17 | fi 18 | 19 | grouping="$(grep "\\<$sys\\>" /proc/self/cgroup | cut -d: -f2)" || true 20 | if [ -z "$grouping" ]; then 21 | # subsystem not mounted anywhere; mount it on its own 22 | grouping="$sys" 23 | fi 24 | 25 | mountpoint="/sys/fs/cgroup/$grouping" 26 | 27 | mkdir -p "$mountpoint" 28 | 29 | # clear out existing mount to make sure new one is read-write 30 | if mountpoint -q "$mountpoint"; then 31 | umount "$mountpoint" 32 | fi 33 | 34 | mount -n -t cgroup -o "$grouping" cgroup "$mountpoint" 35 | 36 | if [ "$grouping" != "$sys" ]; then 37 | if [ -L "/sys/fs/cgroup/$sys" ]; then 38 | rm "/sys/fs/cgroup/$sys" 39 | fi 40 | 41 | ln -s "$mountpoint" "/sys/fs/cgroup/$sys" 42 | fi 43 | done 44 | 45 | if [ ! -e /sys/fs/cgroup/systemd ] && [ "$(grep -c '^1:name=openrc:' /proc/self/cgroup)" -eq 0 ]; then 46 | mkdir /sys/fs/cgroup/systemd 47 | mount -t cgroup -o none,name=systemd none /sys/fs/cgroup/systemd 48 | fi 49 | -------------------------------------------------------------------------------- /buildkitd.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/url" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/BurntSushi/toml" 14 | "github.com/pkg/errors" 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | type Buildkitd struct { 19 | Addr string 20 | 21 | rootDir string 22 | proc *os.Process 23 | } 24 | 25 | // BuildkitdOpts to provide to Buildkitd 26 | type BuildkitdOpts struct { 27 | RootDir string 28 | ConfigPath string 29 | } 30 | 31 | func SpawnBuildkitd(req Request, opts *BuildkitdOpts) (*Buildkitd, error) { 32 | err := run(os.Stdout, "setup-cgroups") 33 | if err != nil { 34 | return nil, errors.Wrap(err, "setup cgroups") 35 | } 36 | 37 | rootDir := filepath.Join(os.TempDir(), "buildkitd") 38 | if opts != nil && opts.RootDir != "" { 39 | rootDir = opts.RootDir 40 | } 41 | 42 | err = os.MkdirAll(rootDir, 0755) 43 | if err != nil { 44 | return nil, errors.Wrap(err, "create root dir") 45 | } 46 | 47 | sockPath := filepath.Join(rootDir, "buildkitd.sock") 48 | logPath := filepath.Join(rootDir, "buildkitd.log") 49 | 50 | configPath := filepath.Join(rootDir, "builtkitd.toml") 51 | if opts != nil && opts.ConfigPath != "" { 52 | configPath = opts.ConfigPath 53 | } 54 | 55 | err = generateConfig(req, configPath) 56 | if err != nil { 57 | return nil, errors.Wrap(err, "generate config") 58 | } 59 | 60 | addr := (&url.URL{Scheme: "unix", Path: sockPath}).String() 61 | 62 | buildkitdFlags := []string{ 63 | "--root", rootDir, 64 | "--addr", addr, 65 | "--config", configPath, 66 | } 67 | 68 | if req.Config.Debug { 69 | buildkitdFlags = append(buildkitdFlags, "--debug") 70 | } 71 | 72 | var cmd *exec.Cmd 73 | if os.Getuid() == 0 { 74 | cmd = exec.Command("buildkitd", buildkitdFlags...) 75 | } else { 76 | cmd = exec.Command("rootlesskit", append([]string{"buildkitd"}, buildkitdFlags...)...) 77 | } 78 | 79 | // kill buildkitd on exit 80 | cmd.SysProcAttr = &syscall.SysProcAttr{ 81 | Pdeathsig: syscall.SIGKILL, 82 | } 83 | 84 | logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND, 0600) 85 | if err != nil { 86 | return nil, errors.Wrap(err, "open log file") 87 | } 88 | 89 | cmd.Stdout = logFile 90 | cmd.Stderr = logFile 91 | 92 | err = cmd.Start() 93 | if err != nil { 94 | return nil, errors.Wrap(err, "start buildkitd") 95 | } 96 | 97 | err = logFile.Close() 98 | if err != nil { 99 | return nil, errors.Wrap(err, "close log file") 100 | } 101 | 102 | for { 103 | err := buildctl(addr, io.Discard, "debug", "workers") 104 | if err == nil { 105 | break 106 | } 107 | 108 | err = cmd.Process.Signal(syscall.Signal(0)) 109 | if err != nil { 110 | logrus.Warn("builtkitd process probe failed:", err) 111 | 112 | logrus.Warn("dumping buildkit logs due to probe failure") 113 | fmt.Fprintln(os.Stderr) 114 | dumpLogFile(logPath) 115 | 116 | os.Exit(1) 117 | } 118 | 119 | logrus.Debugf("waiting for buildkitd...") 120 | time.Sleep(100 * time.Millisecond) 121 | } 122 | 123 | logrus.Debug("buildkitd started") 124 | 125 | return &Buildkitd{ 126 | Addr: addr, 127 | 128 | rootDir: rootDir, 129 | proc: cmd.Process, 130 | }, nil 131 | } 132 | 133 | func (buildkitd *Buildkitd) Cleanup() error { 134 | err := buildkitd.proc.Signal(syscall.SIGTERM) 135 | if err != nil { 136 | return errors.Wrap(err, "terminate buildkitd") 137 | } 138 | 139 | _, err = buildkitd.proc.Wait() 140 | if err != nil { 141 | return errors.Wrap(err, "wait buildkitd") 142 | } 143 | 144 | return nil 145 | } 146 | 147 | func generateConfig(req Request, configPath string) error { 148 | var config BuildkitdConfig 149 | 150 | if len(req.Config.RegistryMirrors) > 0 { 151 | var registryConfigs = make(map[string]RegistryConfig) 152 | registryConfigs["docker.io"] = RegistryConfig{ 153 | Mirrors: req.Config.RegistryMirrors, 154 | } 155 | 156 | config.Registries = registryConfigs 157 | } 158 | 159 | err := os.MkdirAll(filepath.Dir(configPath), 0700) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | f, err := os.Create(configPath) 165 | if err != nil { 166 | return err 167 | } 168 | 169 | err = toml.NewEncoder(f).Encode(config) 170 | if err != nil { 171 | return err 172 | } 173 | 174 | if len(req.Config.BuildkitExtraConfig) > 0 { 175 | var tmp interface{} 176 | _, err = toml.Decode(req.Config.BuildkitExtraConfig, &tmp) 177 | if err != nil { 178 | return errors.Wrap(err, "Extra buildkit config must be valid TOML") 179 | } 180 | 181 | _, err = f.WriteString(req.Config.BuildkitExtraConfig) 182 | if err != nil { 183 | return err 184 | } 185 | } 186 | 187 | return f.Close() 188 | } 189 | 190 | func dumpLogFile(logPath string) { 191 | logFile, err := os.Open(logPath) 192 | if err != nil { 193 | logrus.Warn("error opening log file:", err) 194 | return 195 | } 196 | 197 | _, err = io.Copy(os.Stderr, logFile) 198 | if err != nil { 199 | logrus.Warn("error streaming log file:", err) 200 | return 201 | } 202 | 203 | err = logFile.Close() 204 | if err != nil { 205 | logrus.Warn("error closing log file:", err) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /buildkitd_config.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | type BuildkitdConfig struct { 4 | Registries map[string]RegistryConfig `toml:"registry"` 5 | } 6 | 7 | type RegistryConfig struct { 8 | Mirrors []string `toml:"mirrors"` 9 | PlainHTTP *bool `toml:"http"` 10 | Insecure *bool `toml:"insecure"` 11 | RootCAs []string `toml:"ca"` 12 | KeyPairs []TLSKeyPair `toml:"keypair"` 13 | TLSConfigDir []string `toml:"tlsconfigdir"` 14 | } 15 | 16 | type TLSKeyPair struct { 17 | Key string `toml:"key"` 18 | Certificate string `toml:"cert"` 19 | } 20 | 21 | type TLSConfig struct { 22 | Cert string `toml:"cert"` 23 | Key string `toml:"key"` 24 | CA string `toml:"ca"` 25 | } 26 | -------------------------------------------------------------------------------- /buildkitd_test.go: -------------------------------------------------------------------------------- 1 | package task_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "github.com/stretchr/testify/suite" 10 | 11 | task "github.com/concourse/oci-build-task" 12 | ) 13 | 14 | type BuildkitdSuite struct { 15 | suite.Suite 16 | *require.Assertions 17 | 18 | buildkitd *task.Buildkitd 19 | outputsDir string 20 | req task.Request 21 | } 22 | 23 | func (s *BuildkitdSuite) TearDownSuite() { 24 | if s.buildkitd != nil { 25 | err := s.buildkitd.Cleanup() 26 | s.NoError(err) 27 | } 28 | } 29 | 30 | func (s *BuildkitdSuite) SetupTest() { 31 | var err error 32 | s.outputsDir, err = os.MkdirTemp("", "oci-build-task-test") 33 | s.NoError(err) 34 | 35 | s.req = task.Request{ 36 | ResponsePath: filepath.Join(s.outputsDir, "response.json"), 37 | Config: task.Config{}, 38 | } 39 | } 40 | 41 | func (s *BuildkitdSuite) TearDownTest() { 42 | err := os.RemoveAll(s.outputsDir) 43 | s.NoError(err) 44 | } 45 | 46 | func (s *BuildkitdSuite) TestNoConfig() { 47 | var pathExists bool 48 | 49 | if _, err := os.Stat(s.configPath()); err == nil { 50 | pathExists = true 51 | } 52 | 53 | s.Assert().False(pathExists) 54 | } 55 | 56 | func (s *BuildkitdSuite) TestGenerateConfig() { 57 | var err error 58 | 59 | s.req.Config.RegistryMirrors = []string{"hub.docker.io"} 60 | 61 | s.buildkitd, err = task.SpawnBuildkitd(s.req, &task.BuildkitdOpts{ 62 | ConfigPath: s.configPath("mirrors.toml"), 63 | }) 64 | s.NoError(err) 65 | 66 | configContent, err := os.ReadFile(s.configPath("mirrors.toml")) 67 | s.NoError(err) 68 | 69 | expectedContent, err := os.ReadFile("testdata/buildkitd-config/mirrors.toml") 70 | s.NoError(err) 71 | 72 | s.Equal(expectedContent, configContent) 73 | } 74 | 75 | func (s *BuildkitdSuite) configPath(path ...string) string { 76 | return filepath.Join(append([]string{s.outputsDir, "config"}, path...)...) 77 | } 78 | 79 | func TestBuildkitd(t *testing.T) { 80 | suite.Run(t, &BuildkitdSuite{ 81 | Assertions: require.New(t), 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /cmd/build/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | 10 | "github.com/sirupsen/logrus" 11 | task "github.com/concourse/oci-build-task" 12 | "github.com/vrischmann/envconfig" 13 | ) 14 | 15 | const buildArgPrefix = "BUILD_ARG_" 16 | const imageArgPrefix = "IMAGE_ARG_" 17 | const labelPrefix = "LABEL_" 18 | 19 | const buildkitSecretPrefix = "BUILDKIT_SECRET_" 20 | const buildkitSecretTextPrefix = "BUILDKIT_SECRETTEXT_" 21 | 22 | func main() { 23 | req := task.Request{ 24 | ResponsePath: "/dev/null", 25 | } 26 | 27 | err := envconfig.Init(&req.Config) 28 | failIf("parse config from env", err) 29 | 30 | // envconfig does not support maps, so we initialize it here 31 | req.Config.BuildkitSecrets = make(map[string]string) 32 | 33 | // carry over BUILD_ARG_* and LABEL_* vars manually 34 | for _, env := range os.Environ() { 35 | if strings.HasPrefix(env, buildArgPrefix) { 36 | req.Config.BuildArgs = append( 37 | req.Config.BuildArgs, 38 | strings.TrimPrefix(env, buildArgPrefix), 39 | ) 40 | } 41 | 42 | if strings.HasPrefix(env, imageArgPrefix) { 43 | req.Config.ImageArgs = append( 44 | req.Config.ImageArgs, 45 | strings.TrimPrefix(env, imageArgPrefix), 46 | ) 47 | } 48 | 49 | if strings.HasPrefix(env, labelPrefix) { 50 | req.Config.Labels = append( 51 | req.Config.Labels, 52 | strings.TrimPrefix(env, labelPrefix), 53 | ) 54 | } 55 | 56 | if strings.HasPrefix(env, buildkitSecretPrefix) { 57 | seg := strings.SplitN( 58 | strings.TrimPrefix(env, buildkitSecretPrefix), "=", 2) 59 | 60 | req.Config.BuildkitSecrets[seg[0]] = seg[1] 61 | } 62 | 63 | if strings.HasPrefix(env, buildkitSecretTextPrefix) { 64 | seg := strings.SplitN( 65 | strings.TrimPrefix(env, buildkitSecretTextPrefix), "=", 2) 66 | 67 | err := task.StoreSecret(&req, seg[0], seg[1]) 68 | failIf("store secret provided as text", err) 69 | } 70 | } 71 | 72 | logrus.Debugf("read config from env: %#v\n", req.Config) 73 | 74 | reqPayload, err := json.Marshal(req) 75 | failIf("marshal request", err) 76 | 77 | task := exec.Command("task") 78 | task.Stdin = bytes.NewBuffer(reqPayload) 79 | task.Stdout = os.Stdout 80 | task.Stderr = os.Stderr 81 | 82 | err = task.Run() 83 | failIf("run task", err) 84 | } 85 | 86 | func failIf(msg string, err error) { 87 | if err != nil { 88 | logrus.Fatalln("failed to", msg+":", err) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cmd/task/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "github.com/sirupsen/logrus" 8 | "github.com/u-root/u-root/pkg/termios" 9 | task "github.com/concourse/oci-build-task" 10 | ) 11 | 12 | func main() { 13 | var req task.Request 14 | err := json.NewDecoder(os.Stdin).Decode(&req) 15 | failIf("read request", err) 16 | 17 | wd, err := os.Getwd() 18 | failIf("get root path", err) 19 | 20 | // limit max columns; Concourse sets a super high value and buildctl happily 21 | // fills the whole screen with whitespace 22 | ws, err := termios.GetWinSize(os.Stdout.Fd()) 23 | if err == nil { 24 | ws.Col = 100 25 | 26 | err = termios.SetWinSize(os.Stdout.Fd(), ws) 27 | if err != nil { 28 | logrus.Warn("failed to set window size:", err) 29 | } 30 | } 31 | 32 | var opts task.BuildkitdOpts 33 | if _, err := os.Stat("/scratch"); err == nil { 34 | opts.RootDir = "/scratch/buildkitd" 35 | } 36 | 37 | buildkitd, err := task.SpawnBuildkitd(req, &opts) 38 | failIf("start buildkitd", err) 39 | 40 | res, err := task.Build(buildkitd, wd, req) 41 | if err != nil { 42 | buildkitd.Cleanup() 43 | } 44 | failIf("build", err) 45 | 46 | err = buildkitd.Cleanup() 47 | failIf("cleanup buildkitd", err) 48 | 49 | responseFile, err := os.Create(req.ResponsePath) 50 | failIf("open response path", err) 51 | 52 | err = json.NewEncoder(responseFile).Encode(res) 53 | failIf("write response", err) 54 | 55 | err = responseFile.Close() 56 | failIf("close response file", err) 57 | } 58 | 59 | func failIf(msg string, err error) { 60 | if err != nil { 61 | logrus.Fatalln("failed to", msg+":", err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /example.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | 4 | image_resource: 5 | type: registry-image 6 | source: 7 | repository: concourse/oci-build-task 8 | tag: master 9 | 10 | inputs: 11 | - name: context 12 | path: . 13 | 14 | outputs: 15 | - name: image 16 | 17 | caches: 18 | - path: cache 19 | 20 | run: {path: build} 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/concourse/oci-build-task 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/BurntSushi/toml v1.5.0 9 | github.com/concourse/go-archive v1.0.1 10 | github.com/fatih/color v1.18.0 11 | github.com/google/go-containerregistry v0.20.3 12 | github.com/julienschmidt/httprouter v1.3.0 13 | github.com/pkg/errors v0.9.1 14 | github.com/sirupsen/logrus v1.9.3 15 | github.com/stretchr/testify v1.10.0 16 | github.com/u-root/u-root v7.0.0+incompatible 17 | github.com/vbauerster/mpb v3.4.0+incompatible 18 | github.com/vrischmann/envconfig v1.4.1 19 | ) 20 | 21 | require ( 22 | github.com/VividCortex/ewma v1.2.0 // indirect 23 | github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/docker/cli v27.5.0+incompatible // indirect 26 | github.com/docker/distribution v2.8.3+incompatible // indirect 27 | github.com/docker/docker-credential-helpers v0.8.2 // indirect 28 | github.com/klauspost/compress v1.18.0 // indirect 29 | github.com/mattn/go-colorable v0.1.14 // indirect 30 | github.com/mattn/go-isatty v0.0.20 // indirect 31 | github.com/mitchellh/go-homedir v1.1.0 // indirect 32 | github.com/onsi/ginkgo v1.16.5 // indirect 33 | github.com/onsi/gomega v1.27.8 // indirect 34 | github.com/opencontainers/go-digest v1.0.0 // indirect 35 | github.com/opencontainers/image-spec v1.1.1 // indirect 36 | github.com/pmezard/go-difflib v1.0.0 // indirect 37 | github.com/vbatts/tar-split v0.12.1 // indirect 38 | golang.org/x/crypto v0.37.0 // indirect 39 | golang.org/x/sync v0.13.0 // indirect 40 | golang.org/x/sys v0.32.0 // indirect 41 | golang.org/x/term v0.31.0 // indirect 42 | gopkg.in/yaml.v3 v3.0.1 43 | ) 44 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= 2 | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= 4 | github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= 5 | github.com/concourse/go-archive v1.0.1 h1:6jQk0VDiE4G6lNJQ0mLZ7XmxbqI3spO4x0wgVwk4pfo= 6 | github.com/concourse/go-archive v1.0.1/go.mod h1:Xfo080IPQBmVz3I5ehjCddW3phA2mwv0NFwlpjf5CO8= 7 | github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= 8 | github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= 9 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM= 14 | github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 15 | github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= 16 | github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 17 | github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= 18 | github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= 19 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 20 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 21 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 22 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 23 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 24 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 25 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 26 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 27 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 28 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 29 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 30 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 31 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 32 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 33 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 34 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 35 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 36 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 37 | github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= 38 | github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= 39 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 40 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 41 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 42 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 43 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 44 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 45 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 46 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 47 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 48 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 49 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 50 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 51 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 52 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 53 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 54 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 55 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 56 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 57 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 58 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 59 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 60 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 61 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 62 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 63 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 64 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 65 | github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= 66 | github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= 67 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 68 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 69 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 70 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 71 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 72 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 73 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 74 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 75 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 76 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 77 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 78 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 79 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 80 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 81 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 82 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 83 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 84 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 85 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 86 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 87 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 88 | github.com/u-root/u-root v7.0.0+incompatible h1:u+KSS04pSxJGI5E7WE4Bs9+Zd75QjFv+REkjy/aoAc8= 89 | github.com/u-root/u-root v7.0.0+incompatible/go.mod h1:RYkpo8pTHrNjW08opNd/U6p/RJE7K0D8fXO0d47+3YY= 90 | github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= 91 | github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= 92 | github.com/vbauerster/mpb v3.4.0+incompatible h1:mfiiYw87ARaeRW6x5gWwYRUawxaW1tLAD8IceomUCNw= 93 | github.com/vbauerster/mpb v3.4.0+incompatible/go.mod h1:zAHG26FUhVKETRu+MWqYXcI70POlC6N8up9p1dID7SU= 94 | github.com/vrischmann/envconfig v1.4.1 h1:fucz2HsoAkJCLgIngWdWqLNxNjdWD14zfrLF6EQPdY4= 95 | github.com/vrischmann/envconfig v1.4.1/go.mod h1:cX3p+/PEssil6fWwzIS7kf8iFpli3giuxXGHxckucYc= 96 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 97 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 98 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 99 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 100 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 101 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 102 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 103 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 104 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 105 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 106 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 107 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 108 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 109 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 110 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 111 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 112 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 113 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 114 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 115 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 116 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 117 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 118 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 119 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 127 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 129 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 130 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 131 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 132 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 133 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 134 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 135 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 136 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 137 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 138 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 139 | golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= 140 | golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= 141 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 142 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 143 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 144 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 145 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 146 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 147 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 148 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 149 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 150 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 151 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 152 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 153 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 154 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 155 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 156 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 157 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 158 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 159 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 160 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 161 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 162 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 163 | gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= 164 | gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= 165 | -------------------------------------------------------------------------------- /registry.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "net/http" 8 | "os" 9 | "strings" 10 | 11 | v1 "github.com/google/go-containerregistry/pkg/v1" 12 | "github.com/google/go-containerregistry/pkg/v1/layout" 13 | "github.com/google/go-containerregistry/pkg/v1/tarball" 14 | "github.com/google/go-containerregistry/pkg/v1/types" 15 | "github.com/julienschmidt/httprouter" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | type ImageArg struct { 20 | Index v1.ImageIndex 21 | Image v1.Image 22 | BuildArgName string 23 | } 24 | type LocalRegistry map[string]ImageArg 25 | 26 | func LoadRegistry(imagePaths map[string]string) (LocalRegistry, error) { 27 | images := LocalRegistry{} 28 | for name, path := range imagePaths { 29 | stat, err := os.Stat(path) 30 | if err != nil { 31 | return nil, fmt.Errorf("error inspecting path: %w", err) 32 | } 33 | 34 | var index v1.ImageIndex 35 | var image v1.Image 36 | if stat.IsDir() { 37 | index, err = layout.ImageIndexFromPath(path) 38 | if err != nil { 39 | return nil, fmt.Errorf("image from path: %w", err) 40 | } 41 | } else { 42 | image, err = tarball.ImageFromPath(path, nil) 43 | if err != nil { 44 | return nil, fmt.Errorf("image from tarball: %w", err) 45 | } 46 | } 47 | 48 | images[strings.ToLower(name)] = ImageArg{Index: index, Image: image, BuildArgName: name} 49 | } 50 | 51 | return images, nil 52 | } 53 | 54 | func ServeRegistry(reg LocalRegistry) (string, error) { 55 | router := httprouter.New() 56 | router.GET("/v2/:name/manifests/:ref", reg.GetManifest) 57 | router.GET("/v2/:name/blobs/:digest", reg.GetBlob) 58 | 59 | router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 60 | logrus.WithFields(logrus.Fields{ 61 | "method": r.Method, 62 | "path": r.URL.Path, 63 | }).Warnf("unknown request") 64 | }) 65 | 66 | listener, err := net.Listen("tcp", ":0") 67 | if err != nil { 68 | return "", fmt.Errorf("listen: %w", err) 69 | } 70 | 71 | go http.Serve(listener, router) 72 | 73 | _, port, err := net.SplitHostPort(listener.Addr().String()) 74 | if err != nil { 75 | return "", fmt.Errorf("split registry host/port: %w", err) 76 | } 77 | 78 | return port, nil 79 | } 80 | 81 | func (registry LocalRegistry) BuildArgs(port string) []string { 82 | var buildArgs []string 83 | for name, image := range registry { 84 | buildArgs = append(buildArgs, fmt.Sprintf("%s=localhost:%s/%s", image.BuildArgName, port, name)) 85 | } 86 | 87 | return buildArgs 88 | } 89 | 90 | func (registry LocalRegistry) GetManifest(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 91 | name := p.ByName("name") 92 | ref := p.ByName("ref") 93 | 94 | logrus.WithFields(logrus.Fields{ 95 | "accept": r.Header["Accept"], 96 | }).Debugf("get manifest for %s at %s", name, ref) 97 | 98 | img, found := registry[name] 99 | if !found { 100 | w.WriteHeader(http.StatusNotFound) 101 | return 102 | } 103 | 104 | var mediaType types.MediaType 105 | var blob []byte 106 | var digest v1.Hash 107 | var err error 108 | 109 | if img.Image != nil { 110 | mediaType, err = img.Image.MediaType() 111 | if err != nil { 112 | logrus.Errorf("failed to get media type: %s", err) 113 | w.WriteHeader(http.StatusInternalServerError) 114 | return 115 | } 116 | 117 | blob, err = img.Image.RawManifest() 118 | if err != nil { 119 | logrus.Errorf("failed to get manifest: %s", err) 120 | w.WriteHeader(http.StatusInternalServerError) 121 | return 122 | } 123 | 124 | digest, err = img.Image.Digest() 125 | if err != nil { 126 | logrus.Errorf("failed to get digest: %s", err) 127 | w.WriteHeader(http.StatusInternalServerError) 128 | return 129 | } 130 | 131 | } 132 | 133 | if img.Index != nil { 134 | digest, err = img.Index.Digest() 135 | if err != nil { 136 | logrus.Errorf("error getting ImageIndex's digest: %s", err) 137 | w.WriteHeader(http.StatusInternalServerError) 138 | return 139 | } 140 | 141 | // Check if we were given a Hash. An err means we were NOT given a Hash 142 | // and got a string like "latest" or a semver. In that case we return 143 | // the ImageIndex itself 144 | refHash, err := v1.NewHash(ref) 145 | if digest.String() == ref || err != nil { 146 | mediaType, err = img.Index.MediaType() 147 | if err != nil { 148 | logrus.Errorf("error getting MediaType: %s", err) 149 | w.WriteHeader(http.StatusInternalServerError) 150 | return 151 | } 152 | 153 | blob, err = img.Index.RawManifest() 154 | if err != nil { 155 | logrus.Errorf("error getting RawManifest: %s", err) 156 | w.WriteHeader(http.StatusInternalServerError) 157 | return 158 | } 159 | } else { 160 | // TODO: technically there could be nested ImageIndex's, but they're 161 | // not common so not bothering to handle those right now 162 | 163 | //try and find ref inside ImageIndex 164 | digest = refHash 165 | 166 | image, err := img.Index.Image(digest) 167 | if err != nil { 168 | logrus.Errorf("error getting Image from ImageIndex: %s", err) 169 | w.WriteHeader(http.StatusInternalServerError) 170 | return 171 | } 172 | 173 | mediaType, err = image.MediaType() 174 | if err != nil { 175 | logrus.Errorf("error getting MediaType from Image: %s", err) 176 | w.WriteHeader(http.StatusInternalServerError) 177 | return 178 | } 179 | 180 | blob, err = image.RawManifest() 181 | if err != nil { 182 | logrus.Errorf("error getting RawManifest from Image: %s", err) 183 | w.WriteHeader(http.StatusInternalServerError) 184 | return 185 | } 186 | } 187 | 188 | } 189 | 190 | w.Header().Set("Content-Type", string(mediaType)) 191 | w.Header().Set("Content-Length", fmt.Sprintf("%d", len(blob))) 192 | w.Header().Set("Docker-Content-Digest", digest.String()) 193 | 194 | if r.Method == "HEAD" { 195 | return 196 | } 197 | 198 | _, err = w.Write(blob) 199 | if err != nil { 200 | logrus.Errorf("write manifest blob: %s", err) 201 | return 202 | } 203 | } 204 | 205 | func (registry LocalRegistry) GetBlob(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 206 | name := p.ByName("name") 207 | dig := p.ByName("digest") 208 | 209 | logrus.WithFields(logrus.Fields{ 210 | "accept": r.Header["Accept"], 211 | }).Debugf("get blob %s", dig) 212 | 213 | img, found := registry[name] 214 | if !found { 215 | w.WriteHeader(http.StatusNotFound) 216 | return 217 | } 218 | 219 | hash, err := v1.NewHash(dig) 220 | if err != nil { 221 | logrus.Errorf("failed to parse digest: %s", err) 222 | w.WriteHeader(http.StatusInternalServerError) 223 | return 224 | } 225 | 226 | var layer v1.Layer 227 | 228 | if img.Image != nil { 229 | image := img.Image 230 | 231 | cfgHash, err := image.ConfigName() 232 | if err != nil { 233 | logrus.Errorf("failed to get config hash: %s", err) 234 | w.WriteHeader(http.StatusInternalServerError) 235 | return 236 | } 237 | 238 | if hash == cfgHash { 239 | manifest, err := image.Manifest() 240 | if err != nil { 241 | logrus.Errorf("get image manifest: %s", err) 242 | w.WriteHeader(http.StatusInternalServerError) 243 | return 244 | } 245 | 246 | cfgBlob, err := image.RawConfigFile() 247 | if err != nil { 248 | logrus.Errorf("failed to get config file: %s", err) 249 | w.WriteHeader(http.StatusInternalServerError) 250 | return 251 | } 252 | 253 | w.Header().Set("Content-Type", string(manifest.Config.MediaType)) 254 | w.Header().Set("Content-Length", fmt.Sprintf("%d", len(cfgBlob))) 255 | 256 | if r.Method == "HEAD" { 257 | return 258 | } 259 | 260 | _, err = w.Write(cfgBlob) 261 | if err != nil { 262 | logrus.Errorf("write config blob: %s", err) 263 | return 264 | } 265 | 266 | return 267 | } 268 | 269 | layer, err = image.LayerByDigest(hash) 270 | if err != nil { 271 | logrus.Errorf("failed to get layer: %s", err) 272 | w.WriteHeader(http.StatusInternalServerError) 273 | return 274 | } 275 | } 276 | 277 | if img.Index != nil { 278 | index, err := img.Index.IndexManifest() 279 | if err != nil { 280 | logrus.Errorf("error getting Manifest from ImageIndex: %s", err) 281 | w.WriteHeader(http.StatusInternalServerError) 282 | return 283 | } 284 | 285 | // Search all images in the ImageIndex for the requested layer 286 | for _, desc := range index.Manifests { 287 | if desc.MediaType.IsImage() { 288 | img, err := img.Index.Image(desc.Digest) 289 | if err != nil { 290 | logrus.Errorf("error getting image from ImageIndex: %s", err) 291 | w.WriteHeader(http.StatusInternalServerError) 292 | return 293 | } 294 | 295 | // ignore errors related to not finding the layer and just keep searching 296 | l, err := img.LayerByDigest(hash) 297 | if err == nil { 298 | layer = l 299 | break 300 | } 301 | } 302 | } 303 | 304 | if layer == nil { 305 | logrus.Errorf("layer not found in ImageIndex: %s", err) 306 | w.WriteHeader(http.StatusNotFound) 307 | return 308 | } 309 | } 310 | 311 | size, err := layer.Size() 312 | if err != nil { 313 | logrus.Errorf("failed to get layer size: %s", err) 314 | w.WriteHeader(http.StatusInternalServerError) 315 | return 316 | } 317 | 318 | mt, err := layer.MediaType() 319 | if err != nil { 320 | logrus.Errorf("failed to get layer media type: %s", err) 321 | w.WriteHeader(http.StatusInternalServerError) 322 | return 323 | } 324 | 325 | w.Header().Set("Content-Type", string(mt)) 326 | w.Header().Set("Content-Length", fmt.Sprintf("%d", size)) 327 | 328 | if r.Method == "HEAD" { 329 | return 330 | } 331 | 332 | blob, err := layer.Compressed() 333 | if err != nil { 334 | logrus.Errorf("failed to read layer: %s", err) 335 | w.WriteHeader(http.StatusInternalServerError) 336 | return 337 | } 338 | 339 | _, err = io.Copy(w, blob) 340 | if err != nil { 341 | logrus.Errorf("write blob: %s", err) 342 | return 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u 4 | 5 | export CGO_ENABLED=0 6 | go build -o bin/task ./cmd/task 7 | go build -o bin/build ./cmd/build 8 | -------------------------------------------------------------------------------- /scripts/build-image: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -x 4 | 5 | cd $(dirname $0)/.. 6 | 7 | export PATH=$PWD/bin:$PATH 8 | 9 | . ./scripts/setup-buildkit.sh 10 | 11 | mkdir -p image 12 | 13 | build 14 | -------------------------------------------------------------------------------- /scripts/push-image: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u 4 | 5 | tag="" 6 | version="" 7 | 8 | case $GITHUB_REF in 9 | refs/heads/*) 10 | tag=$(echo $GITHUB_REF | sed 's|refs/heads/||') 11 | ;; 12 | 13 | refs/tags/v[0-9]*) 14 | version=$(echo $GITHUB_REF | sed 's|refs/tags/v||') 15 | ;; 16 | 17 | refs/tags/*) 18 | tag=$(echo $GITHUB_REF | sed 's|refs/tags/||') 19 | ;; 20 | 21 | refs/pull/[0-9]*/merge) 22 | tag=pr$(echo $GITHUB_REF | sed 's|refs/pull/\([0-9]\+\)/merge|\1|') 23 | ;; 24 | 25 | *) 26 | tag=$GITHUB_SHA 27 | ;; 28 | esac 29 | 30 | if [[ -n "$tag" ]]; then 31 | exec /opt/resource/out . </dev/null || ! which buildkitd >/dev/null; then 6 | arch="$(uname -m)" 7 | case "$arch" in 8 | "aarch64") 9 | arch="arm64" 10 | ;; 11 | 12 | "x86_64") 13 | arch="amd64" 14 | ;; 15 | 16 | *) 17 | ;; 18 | esac 19 | 20 | BUILDKIT_VERSION="0.22.0" 21 | BUILDKIT_URL="https://github.com/moby/buildkit/releases/download/v${BUILDKIT_VERSION}/buildkit-v${BUILDKIT_VERSION}.linux-${arch}.tar.gz" 22 | 23 | curl -fL "$BUILDKIT_URL" | tar zxf - 24 | fi 25 | 26 | if [ "$(id -u)" != "0" ]; then 27 | if ! which newuidmap >/dev/null || ! which newgidmap >/dev/null; then 28 | echo "newuidmap and newgidmap must be installed" 29 | exit 1 30 | fi 31 | 32 | if ! which rootlesskit >/dev/null; then 33 | pushd rootlesskit 34 | make 35 | popd 36 | 37 | cp rootlesskit/bin/* bin/ 38 | fi 39 | fi 40 | 41 | # prevents failure to create /run/runc 42 | export XDG_RUNTIME_DIR=/tmp/buildkitd 43 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u 4 | 5 | cd $(dirname $0)/.. 6 | 7 | export PATH=$PWD/bin:$PATH 8 | 9 | . ./scripts/setup-buildkit.sh 10 | 11 | go test "$@" 12 | -------------------------------------------------------------------------------- /task.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "gopkg.in/yaml.v3" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | 13 | v1 "github.com/google/go-containerregistry/pkg/v1" 14 | "github.com/google/go-containerregistry/pkg/v1/layout" 15 | "github.com/google/go-containerregistry/pkg/v1/tarball" 16 | "github.com/pkg/errors" 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | // Q: Audit name to not include "/"? 21 | func StoreSecret(req *Request, name, value string) error { 22 | secretDir := filepath.Join(os.TempDir(), "buildkit-secrets") 23 | secretFile := filepath.Join(secretDir, name) 24 | err := os.MkdirAll(secretDir, 0700) 25 | if err != nil { 26 | return fmt.Errorf("unable to create secret directory: %w", err) 27 | } 28 | err = os.WriteFile(secretFile, []byte(value), 0600) 29 | if err != nil { 30 | return fmt.Errorf("unable to write secret to file: %w", err) 31 | } 32 | if req.Config.BuildkitSecrets == nil { 33 | req.Config.BuildkitSecrets = make(map[string]string, 1) 34 | } 35 | req.Config.BuildkitSecrets[name] = secretFile 36 | return nil 37 | } 38 | 39 | func Build(buildkitd *Buildkitd, outputsDir string, req Request) (Response, error) { 40 | if req.Config.Debug { 41 | logrus.SetLevel(logrus.DebugLevel) 42 | } 43 | 44 | cfg := req.Config 45 | err := sanitize(&cfg) 46 | if err != nil { 47 | return Response{}, errors.Wrap(err, "config") 48 | } 49 | 50 | cacheDir := filepath.Join(outputsDir, "cache") 51 | 52 | res := Response{ 53 | Outputs: []string{"image", "cache"}, 54 | } 55 | 56 | dockerfileDir := filepath.Dir(cfg.DockerfilePath) 57 | dockerfileName := filepath.Base(cfg.DockerfilePath) 58 | 59 | buildctlArgs := []string{ 60 | "build", 61 | "--progress", "plain", 62 | "--frontend", "dockerfile.v0", 63 | "--local", "context=" + cfg.ContextDir, 64 | "--local", "dockerfile=" + dockerfileDir, 65 | "--opt", "filename=" + dockerfileName, 66 | } 67 | 68 | for _, arg := range cfg.Labels { 69 | buildctlArgs = append(buildctlArgs, 70 | "--opt", "label:"+arg, 71 | ) 72 | } 73 | 74 | for _, arg := range cfg.BuildArgs { 75 | buildctlArgs = append(buildctlArgs, 76 | "--opt", "build-arg:"+arg, 77 | ) 78 | } 79 | 80 | if len(req.Config.ImageArgs) > 0 { 81 | imagePaths := map[string]string{} 82 | for _, arg := range req.Config.ImageArgs { 83 | segs := strings.SplitN(arg, "=", 2) 84 | imagePaths[segs[0]] = segs[1] 85 | } 86 | 87 | registry, err := LoadRegistry(imagePaths) 88 | if err != nil { 89 | return Response{}, fmt.Errorf("create local image registry: %w", err) 90 | } 91 | 92 | port, err := ServeRegistry(registry) 93 | if err != nil { 94 | return Response{}, fmt.Errorf("create local image registry: %w", err) 95 | } 96 | 97 | for _, arg := range registry.BuildArgs(port) { 98 | buildctlArgs = append(buildctlArgs, 99 | "--opt", "build-arg:"+arg, 100 | ) 101 | } 102 | } 103 | 104 | if _, err := os.Stat(cacheDir); err == nil { 105 | buildctlArgs = append(buildctlArgs, 106 | "--export-cache", "type=local,mode=max,dest="+cacheDir, 107 | ) 108 | } 109 | 110 | for id, src := range cfg.BuildkitSecrets { 111 | buildctlArgs = append(buildctlArgs, 112 | "--secret", "id="+id+",src="+src, 113 | ) 114 | } 115 | 116 | var builds [][]string 117 | var targets []string 118 | var imagePaths []string 119 | 120 | outputType := "docker" 121 | if cfg.OutputOCI { 122 | outputType = "oci" 123 | } 124 | 125 | for _, t := range cfg.AdditionalTargets { 126 | // prevent re-use of the buildctlArgs slice as it is appended to later on, 127 | // and that would clobber args for all targets if the slice was re-used 128 | targetArgs := make([]string, len(buildctlArgs)) 129 | copy(targetArgs, buildctlArgs) 130 | 131 | targetArgs = append(targetArgs, "--opt", "target="+t) 132 | 133 | targetDir := filepath.Join(outputsDir, t) 134 | 135 | if _, err := os.Stat(targetDir); err == nil { 136 | imagePath := filepath.Join(targetDir, "image.tar") 137 | imagePaths = append(imagePaths, imagePath) 138 | 139 | targetArgs = append(targetArgs, 140 | "--output", "type="+outputType+",dest="+imagePath, 141 | ) 142 | } 143 | 144 | builds = append(builds, targetArgs) 145 | targets = append(targets, t) 146 | } 147 | 148 | finalTargetDir := filepath.Join(outputsDir, "image") 149 | if _, err := os.Stat(finalTargetDir); err == nil { 150 | imagePath := filepath.Join(finalTargetDir, "image.tar") 151 | imagePaths = append(imagePaths, imagePath) 152 | 153 | buildctlArgs = append(buildctlArgs, 154 | "--output", "type="+outputType+",dest="+imagePath, 155 | ) 156 | } 157 | 158 | if cfg.Target != "" { 159 | buildctlArgs = append(buildctlArgs, 160 | "--opt", "target="+cfg.Target, 161 | ) 162 | } 163 | 164 | if cfg.AddHosts != "" { 165 | buildctlArgs = append(buildctlArgs, 166 | "--opt", "add-hosts="+cfg.AddHosts, 167 | ) 168 | } 169 | 170 | if cfg.BuildkitSSH != "" { 171 | buildctlArgs = append(buildctlArgs, 172 | "--ssh", cfg.BuildkitSSH, 173 | ) 174 | } 175 | 176 | if req.Config.ImagePlatform != "" { 177 | buildctlArgs = append(buildctlArgs, 178 | "--opt", "platform="+req.Config.ImagePlatform, 179 | ) 180 | } 181 | 182 | builds = append(builds, buildctlArgs) 183 | targets = append(targets, "") 184 | 185 | for i, args := range builds { 186 | if i > 0 { 187 | fmt.Fprintln(os.Stderr) 188 | } 189 | 190 | targetName := targets[i] 191 | if targetName == "" { 192 | logrus.Info("building image") 193 | } else { 194 | logrus.Infof("building target '%s'", targetName) 195 | } 196 | 197 | if _, err := os.Stat(filepath.Join(cacheDir, "index.json")); err == nil { 198 | args = append(args, 199 | "--import-cache", "type=local,src="+cacheDir, 200 | ) 201 | } 202 | 203 | logrus.Debugf("running buildctl %s", strings.Join(args, " ")) 204 | 205 | err = buildctl(buildkitd.Addr, os.Stdout, args...) 206 | if err != nil { 207 | return Response{}, errors.Wrap(err, "build") 208 | } 209 | } 210 | 211 | if req.Config.OutputOCI { 212 | err = loadOciImages(imagePaths, req) 213 | if err != nil { 214 | return Response{}, err 215 | } 216 | } else { 217 | err = loadImages(imagePaths, req) 218 | if err != nil { 219 | return Response{}, err 220 | } 221 | } 222 | 223 | return res, nil 224 | } 225 | 226 | func loadImages(imagePaths []string, req Request) error { 227 | for _, imagePath := range imagePaths { 228 | image, err := tarball.ImageFromPath(imagePath, nil) 229 | if err != nil { 230 | return errors.Wrap(err, "open oci image") 231 | } 232 | 233 | outputDir := filepath.Dir(imagePath) 234 | 235 | m, err := image.Manifest() 236 | if err != nil { 237 | return errors.Wrap(err, "get image manifest") 238 | } 239 | 240 | err = writeDigest(outputDir, m.Config.Digest) 241 | if err != nil { 242 | return err 243 | } 244 | 245 | if req.Config.UnpackRootfs { 246 | err = unpackRootfs(outputDir, image, req.Config) 247 | if err != nil { 248 | return errors.Wrap(err, "unpack rootfs") 249 | } 250 | } 251 | } 252 | return nil 253 | } 254 | 255 | func loadOciImages(imagePaths []string, req Request) error { 256 | for _, imagePath := range imagePaths { 257 | _, err := os.Stat(imagePath) 258 | if err != nil { 259 | return errors.Wrapf(err, "image path %s not valid", imagePath) 260 | } 261 | 262 | // go-containerregistry does not currently have support for loading a OCI formated 263 | // image from a tarball, so we decompress it before doing anything. 264 | targetDir := filepath.Dir(imagePath) 265 | imageDir := filepath.Join(targetDir, "image") 266 | logrus.Infof("decompressing OCI image tar to: %s", imageDir) 267 | err = os.MkdirAll(imageDir, 0700) 268 | if err != nil { 269 | return errors.Wrapf(err, "unable to create image dir %s", imageDir) 270 | } 271 | run(os.Stdout, "tar", "-xvf", imagePath, "-C", imageDir) 272 | 273 | l, err := layout.ImageIndexFromPath(imageDir) 274 | if err != nil { 275 | return errors.Wrapf(err, "failed to load %s as OCI layout", imagePath) 276 | } 277 | 278 | m, err := l.IndexManifest() 279 | if err != nil { 280 | return errors.Wrap(err, "error getting index manifest") 281 | } 282 | 283 | manifest := m.Manifests[0] 284 | 285 | outputDir := filepath.Dir(imagePath) 286 | 287 | err = writeDigest(outputDir, manifest.Digest) 288 | if err != nil { 289 | return err 290 | } 291 | } 292 | 293 | return nil 294 | } 295 | 296 | func writeDigest(dest string, digest v1.Hash) error { 297 | digestPath := filepath.Join(dest, "digest") 298 | 299 | err := os.WriteFile(digestPath, []byte(digest.String()), 0644) 300 | if err != nil { 301 | return errors.Wrap(err, "write digest file") 302 | } 303 | 304 | return nil 305 | } 306 | 307 | func unpackRootfs(dest string, image v1.Image, cfg Config) error { 308 | rootfsDir := filepath.Join(dest, "rootfs") 309 | metadataPath := filepath.Join(dest, "metadata.json") 310 | 311 | logrus.Info("unpacking image") 312 | 313 | err := unpackImage(rootfsDir, image, cfg.Debug) 314 | if err != nil { 315 | return errors.Wrap(err, "unpack image") 316 | } 317 | 318 | err = writeImageMetadata(metadataPath, image) 319 | if err != nil { 320 | return errors.Wrap(err, "write image metadata") 321 | } 322 | 323 | return nil 324 | } 325 | 326 | func writeImageMetadata(metadataPath string, image v1.Image) error { 327 | cfg, err := image.ConfigFile() 328 | if err != nil { 329 | return errors.Wrap(err, "load image config") 330 | } 331 | 332 | meta, err := os.Create(metadataPath) 333 | if err != nil { 334 | return errors.Wrap(err, "create metadata file") 335 | } 336 | 337 | err = json.NewEncoder(meta).Encode(ImageMetadata{ 338 | Env: cfg.Config.Env, 339 | User: cfg.Config.User, 340 | }) 341 | if err != nil { 342 | return errors.Wrap(err, "encode metadata") 343 | } 344 | 345 | err = meta.Close() 346 | if err != nil { 347 | return errors.Wrap(err, "close meta") 348 | } 349 | 350 | return nil 351 | } 352 | 353 | func sanitize(cfg *Config) error { 354 | if cfg.ContextDir == "" { 355 | cfg.ContextDir = "." 356 | } 357 | 358 | if cfg.DockerfilePath == "" { 359 | cfg.DockerfilePath = filepath.Join(cfg.ContextDir, "Dockerfile") 360 | } 361 | 362 | if cfg.TargetFile != "" { 363 | target, err := os.ReadFile(cfg.TargetFile) 364 | if err != nil { 365 | return errors.Wrap(err, "read target file") 366 | } 367 | 368 | cfg.Target = strings.TrimSpace(string(target)) 369 | } 370 | 371 | if cfg.BuildArgsFile != "" { 372 | buildArgs, err := os.ReadFile(cfg.BuildArgsFile) 373 | if err != nil { 374 | return errors.Wrap(err, "read build args file") 375 | } 376 | 377 | if strings.HasSuffix(cfg.BuildArgsFile, ".yml") || strings.HasSuffix(cfg.BuildArgsFile, ".yaml") { 378 | var buildArgsData map[string]string 379 | err = yaml.Unmarshal(buildArgs, &buildArgsData) 380 | if err != nil { 381 | return errors.Wrap(err, "read build args yaml file") 382 | } 383 | for key, arg := range buildArgsData { 384 | cfg.BuildArgs = append(cfg.BuildArgs, key + "=" + arg) 385 | } 386 | } else { 387 | for _, arg := range strings.Split(string(buildArgs), "\n") { 388 | if len(arg) == 0 { 389 | // skip blank lines 390 | continue 391 | } 392 | 393 | cfg.BuildArgs = append(cfg.BuildArgs, arg) 394 | } 395 | } 396 | } 397 | 398 | if cfg.LabelsFile != "" { 399 | Labels, err := os.ReadFile(cfg.LabelsFile) 400 | if err != nil { 401 | return errors.Wrap(err, "read labels file") 402 | } 403 | 404 | for _, arg := range strings.Split(string(Labels), "\n") { 405 | if len(arg) == 0 { 406 | // skip blank lines 407 | continue 408 | } 409 | 410 | cfg.Labels = append(cfg.Labels, arg) 411 | } 412 | } 413 | 414 | return nil 415 | } 416 | 417 | func buildctl(addr string, out io.Writer, args ...string) error { 418 | return run(out, "buildctl", append([]string{"--addr=" + addr}, args...)...) 419 | } 420 | 421 | func run(out io.Writer, path string, args ...string) error { 422 | cmd := exec.Command(path, args...) 423 | cmd.Stdout = out 424 | cmd.Stderr = out 425 | cmd.Stdin = os.Stdin 426 | return cmd.Run() 427 | } 428 | -------------------------------------------------------------------------------- /task_test.go: -------------------------------------------------------------------------------- 1 | package task_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http/httptest" 7 | "net/url" 8 | "os" 9 | "path/filepath" 10 | "reflect" 11 | "runtime" 12 | "testing" 13 | 14 | task "github.com/concourse/oci-build-task" 15 | "github.com/google/go-containerregistry/pkg/name" 16 | "github.com/google/go-containerregistry/pkg/registry" 17 | v1 "github.com/google/go-containerregistry/pkg/v1" 18 | "github.com/google/go-containerregistry/pkg/v1/layout" 19 | "github.com/google/go-containerregistry/pkg/v1/match" 20 | "github.com/google/go-containerregistry/pkg/v1/mutate" 21 | "github.com/google/go-containerregistry/pkg/v1/random" 22 | "github.com/google/go-containerregistry/pkg/v1/remote" 23 | "github.com/google/go-containerregistry/pkg/v1/tarball" 24 | "github.com/google/go-containerregistry/pkg/v1/types" 25 | "github.com/stretchr/testify/require" 26 | "github.com/stretchr/testify/suite" 27 | ) 28 | 29 | type TaskSuite struct { 30 | suite.Suite 31 | *require.Assertions 32 | 33 | buildkitd *task.Buildkitd 34 | outputsDir string 35 | req task.Request 36 | } 37 | 38 | func (s *TaskSuite) SetupSuite() { 39 | var err error 40 | s.buildkitd, err = task.SpawnBuildkitd(task.Request{}, nil) 41 | s.NoError(err) 42 | } 43 | 44 | func (s *TaskSuite) TearDownSuite() { 45 | err := s.buildkitd.Cleanup() 46 | s.NoError(err) 47 | } 48 | 49 | func (s *TaskSuite) SetupTest() { 50 | var err error 51 | s.outputsDir, err = os.MkdirTemp("", "oci-build-task-test") 52 | s.NoError(err) 53 | 54 | err = os.Mkdir(s.imagePath(), 0755) 55 | s.NoError(err) 56 | 57 | s.req = task.Request{ 58 | ResponsePath: filepath.Join(s.outputsDir, "response.json"), 59 | Config: task.Config{ 60 | Debug: true, 61 | }, 62 | } 63 | } 64 | 65 | func (s *TaskSuite) TearDownTest() { 66 | err := os.RemoveAll(s.outputsDir) 67 | s.NoError(err) 68 | } 69 | 70 | func (s *TaskSuite) TestBasicBuild() { 71 | s.req.Config.ContextDir = "testdata/basic" 72 | 73 | _, err := s.build() 74 | s.NoError(err) 75 | } 76 | 77 | func (s *TaskSuite) TestNoOutputBuild() { 78 | s.req.Config.ContextDir = "testdata/basic" 79 | 80 | err := os.RemoveAll(s.imagePath()) 81 | s.NoError(err) 82 | 83 | _, err = s.build() 84 | s.NoError(err) 85 | } 86 | 87 | func (s *TaskSuite) TestDigestFile() { 88 | s.req.Config.ContextDir = "testdata/basic" 89 | 90 | _, err := s.build() 91 | s.NoError(err) 92 | 93 | digest, err := os.ReadFile(s.imagePath("digest")) 94 | s.NoError(err) 95 | 96 | image, err := tarball.ImageFromPath(s.imagePath("image.tar"), nil) 97 | s.NoError(err) 98 | 99 | manifest, err := image.Manifest() 100 | s.NoError(err) 101 | 102 | s.Equal(string(digest), manifest.Config.Digest.String()) 103 | } 104 | 105 | func (s *TaskSuite) TestDockerfilePath() { 106 | s.req.Config.ContextDir = "testdata/dockerfile-path" 107 | s.req.Config.DockerfilePath = "testdata/dockerfile-path/hello.Dockerfile" 108 | 109 | _, err := s.build() 110 | s.NoError(err) 111 | } 112 | 113 | func (s *TaskSuite) TestTarget() { 114 | s.req.Config.ContextDir = "testdata/target" 115 | s.req.Config.Target = "working-target" 116 | 117 | _, err := s.build() 118 | s.NoError(err) 119 | } 120 | 121 | func (s *TaskSuite) TestBuildkitSSH() { 122 | s.req.Config.ContextDir = "testdata/buildkit-ssh" 123 | s.req.Config.BuildkitSSH = "my_ssh_key=testdata/buildkit-ssh/id_rsa_test" 124 | 125 | _, err := s.build() 126 | s.NoError(err) 127 | } 128 | 129 | func (s *TaskSuite) TestTargetFile() { 130 | s.req.Config.ContextDir = "testdata/target" 131 | s.req.Config.TargetFile = "testdata/target/target_file" 132 | 133 | _, err := s.build() 134 | s.NoError(err) 135 | } 136 | 137 | func (s *TaskSuite) TestBuildArgs() { 138 | s.req.Config.ContextDir = "testdata/build-args" 139 | s.req.Config.BuildArgs = []string{ 140 | "some_arg=some_value", 141 | "some_other_arg=some_other_value", 142 | } 143 | 144 | // the Dockerfile itself asserts that the arg has been received 145 | _, err := s.build() 146 | s.NoError(err) 147 | } 148 | 149 | func (s *TaskSuite) TestBuildArgsFile() { 150 | s.req.Config.ContextDir = "testdata/build-args" 151 | s.req.Config.BuildArgsFile = "testdata/build-args/build_args_file" 152 | 153 | // the Dockerfile itself asserts that the arg has been received 154 | _, err := s.build() 155 | s.NoError(err) 156 | } 157 | 158 | func (s *TaskSuite) TestBuildArgsYamlFile() { 159 | s.req.Config.ContextDir = "testdata/build-args" 160 | s.req.Config.BuildArgsFile = "testdata/build-args/build_args_file.yaml" 161 | 162 | // the Dockerfile itself asserts that the arg has been received 163 | _, err := s.build() 164 | s.NoError(err) 165 | } 166 | 167 | func (s *TaskSuite) TestBuildArgsStaticAndFile() { 168 | s.req.Config.ContextDir = "testdata/build-args" 169 | s.req.Config.BuildArgs = []string{"some_arg=some_value"} 170 | s.req.Config.BuildArgsFile = "testdata/build-args/build_arg_file" 171 | 172 | // the Dockerfile itself asserts that the arg has been received 173 | _, err := s.build() 174 | s.NoError(err) 175 | } 176 | 177 | func (s *TaskSuite) TestLabels() { 178 | s.req.Config.ContextDir = "testdata/labels" 179 | expectedLabels := map[string]string{ 180 | "some_label": "some_value", 181 | "some_other_label": "some_other_value", 182 | } 183 | s.req.Config.Labels = make([]string, 0, len(expectedLabels)) 184 | 185 | for k, v := range expectedLabels { 186 | s.req.Config.Labels = append(s.req.Config.Labels, fmt.Sprintf("%s=%s", k, v)) 187 | } 188 | 189 | _, err := s.build() 190 | s.NoError(err) 191 | 192 | image, err := tarball.ImageFromPath(s.imagePath("image.tar"), nil) 193 | s.NoError(err) 194 | 195 | configFile, err := image.ConfigFile() 196 | s.NoError(err) 197 | 198 | s.True(reflect.DeepEqual(expectedLabels, configFile.Config.Labels)) 199 | } 200 | 201 | func (s *TaskSuite) TestLabelsFile() { 202 | s.req.Config.ContextDir = "testdata/labels" 203 | expectedLabels := map[string]string{ 204 | "some_label": "some_value", 205 | "some_other_label": "some_other_value", 206 | } 207 | s.req.Config.LabelsFile = "testdata/labels/labels_file" 208 | 209 | _, err := s.build() 210 | s.NoError(err) 211 | 212 | image, err := tarball.ImageFromPath(s.imagePath("image.tar"), nil) 213 | s.NoError(err) 214 | 215 | configFile, err := image.ConfigFile() 216 | s.NoError(err) 217 | 218 | s.True(reflect.DeepEqual(expectedLabels, configFile.Config.Labels)) 219 | } 220 | 221 | func (s *TaskSuite) TestLabelsStaticAndFileAndLayer() { 222 | s.req.Config.ContextDir = "testdata/labels" 223 | s.req.Config.DockerfilePath = "testdata/labels/label_layer.dockerfile" 224 | expectedLabels := map[string]string{ 225 | "some_label": "some_value", 226 | "some_other_label": "some_other_value", 227 | "label_layer": "some_label_layer_value", 228 | } 229 | s.req.Config.Labels = []string{"some_label=some_value"} 230 | s.req.Config.LabelsFile = "testdata/labels/label_file" 231 | 232 | _, err := s.build() 233 | s.NoError(err) 234 | 235 | image, err := tarball.ImageFromPath(s.imagePath("image.tar"), nil) 236 | s.NoError(err) 237 | 238 | configFile, err := image.ConfigFile() 239 | s.NoError(err) 240 | 241 | s.True(reflect.DeepEqual(expectedLabels, configFile.Config.Labels)) 242 | } 243 | 244 | func (s *TaskSuite) TestUnpackRootfs() { 245 | s.req.Config.ContextDir = "testdata/unpack-rootfs" 246 | s.req.Config.UnpackRootfs = true 247 | 248 | _, err := s.build() 249 | s.NoError(err) 250 | 251 | meta, err := s.imageMetadata("image") 252 | s.NoError(err) 253 | 254 | rootfsContent, err := os.ReadFile(s.imagePath("rootfs", "Dockerfile")) 255 | s.NoError(err) 256 | 257 | expectedContent, err := os.ReadFile("testdata/unpack-rootfs/Dockerfile") 258 | s.NoError(err) 259 | 260 | s.Equal(rootfsContent, expectedContent) 261 | 262 | s.Equal(meta.User, "banana") 263 | s.Equal(meta.Env, []string{"PATH=/darkness", "BA=nana"}) 264 | } 265 | 266 | func (s *TaskSuite) TestBuildkitTextualSecrets() { 267 | s.req.Config.ContextDir = "testdata/buildkit-secret" 268 | err := task.StoreSecret(&s.req, "secret", "hello-world") 269 | s.NoError(err) 270 | 271 | _, err = s.build() 272 | s.NoError(err) 273 | } 274 | 275 | func (s *TaskSuite) TestBuildkitSecrets() { 276 | s.req.Config.ContextDir = "testdata/buildkit-secret" 277 | s.req.Config.BuildkitSecrets = map[string]string{"secret": "testdata/buildkit-secret/secret"} 278 | 279 | _, err := s.build() 280 | s.NoError(err) 281 | } 282 | 283 | func (s *TaskSuite) TestRegistryMirrors() { 284 | mirror := httptest.NewServer(registry.New()) 285 | defer mirror.Close() 286 | 287 | image := s.randomImage(1024, 2, "linux", "amd64") 288 | 289 | mirrorURL, err := url.Parse(mirror.URL) 290 | s.NoError(err) 291 | 292 | mirrorRef, err := name.NewTag(fmt.Sprintf("%s/library/mirrored-image:some-tag", mirrorURL.Host)) 293 | s.NoError(err) 294 | 295 | err = remote.Write(mirrorRef, image) 296 | s.NoError(err) 297 | 298 | s.req.Config.ContextDir = "testdata/mirror" 299 | s.req.Config.RegistryMirrors = []string{mirrorURL.Host} 300 | 301 | rootDir, err := os.MkdirTemp("", "mirrored-buildkitd") 302 | s.NoError(err) 303 | 304 | defer os.RemoveAll(rootDir) 305 | 306 | mirroredBuildkitd, err := task.SpawnBuildkitd(s.req, &task.BuildkitdOpts{ 307 | RootDir: rootDir, 308 | }) 309 | s.NoError(err) 310 | 311 | defer mirroredBuildkitd.Cleanup() 312 | 313 | _, err = task.Build(mirroredBuildkitd, s.outputsDir, s.req) 314 | s.NoError(err) 315 | 316 | builtImage, err := tarball.ImageFromPath(s.imagePath("image.tar"), nil) 317 | s.NoError(err) 318 | 319 | layers, err := image.Layers() 320 | s.NoError(err) 321 | 322 | builtLayers, err := builtImage.Layers() 323 | s.NoError(err) 324 | s.Len(builtLayers, len(layers)) 325 | 326 | for i := 0; i < len(layers); i++ { 327 | digest, err := layers[i].Digest() 328 | s.NoError(err) 329 | 330 | builtDigest, err := builtLayers[i].Digest() 331 | s.NoError(err) 332 | 333 | s.Equal(digest, builtDigest) 334 | } 335 | } 336 | 337 | func (s *TaskSuite) TestImageArgs() { 338 | imagesDir, err := os.MkdirTemp("", "preload-images") 339 | s.NoError(err) 340 | 341 | defer os.RemoveAll(imagesDir) 342 | 343 | firstImage := s.randomImage(1024, 2, "linux", "amd64") 344 | firstPath := filepath.Join(imagesDir, "first.tar") 345 | err = tarball.WriteToFile(firstPath, nil, firstImage) 346 | s.NoError(err) 347 | 348 | secondImage := s.randomImage(1024, 2, "linux", "amd64") 349 | secondPath := filepath.Join(imagesDir, "second.tar") 350 | err = tarball.WriteToFile(secondPath, nil, secondImage) 351 | s.NoError(err) 352 | 353 | s.req.Config.ContextDir = "testdata/image-args" 354 | s.req.Config.AdditionalTargets = []string{"first"} 355 | s.req.Config.ImageArgs = []string{ 356 | "first_image=" + firstPath, 357 | "second_image=" + secondPath, 358 | } 359 | 360 | err = os.Mkdir(s.outputPath("first"), 0755) 361 | s.NoError(err) 362 | 363 | _, err = s.build() 364 | s.NoError(err) 365 | 366 | firstBuiltImage, err := tarball.ImageFromPath(s.outputPath("first", "image.tar"), nil) 367 | s.NoError(err) 368 | 369 | secondBuiltImage, err := tarball.ImageFromPath(s.outputPath("image", "image.tar"), nil) 370 | s.NoError(err) 371 | 372 | for image, builtImage := range map[v1.Image]v1.Image{ 373 | firstImage: firstBuiltImage, 374 | secondImage: secondBuiltImage, 375 | } { 376 | layers, err := image.Layers() 377 | s.NoError(err) 378 | 379 | builtLayers, err := builtImage.Layers() 380 | s.NoError(err) 381 | s.Len(builtLayers, len(layers)+1) 382 | 383 | for i := 0; i < len(layers); i++ { 384 | digest, err := layers[i].Digest() 385 | s.NoError(err) 386 | 387 | builtDigest, err := builtLayers[i].Digest() 388 | s.NoError(err) 389 | 390 | s.Equal(digest, builtDigest) 391 | } 392 | } 393 | } 394 | 395 | func (s *TaskSuite) TestImageArgsWithOCIImages() { 396 | imagesDir, err := os.MkdirTemp("", "preload-images") 397 | s.NoError(err) 398 | 399 | defer os.RemoveAll(imagesDir) 400 | 401 | firstImage := s.randomImageIndex(1024, 2, "linux", runtime.GOARCH) 402 | firstPath := filepath.Join(imagesDir, "first") 403 | _, err = layout.Write(firstPath, firstImage) 404 | s.NoError(err) 405 | 406 | secondImage := s.randomImageIndex(1024, 2, "linux", runtime.GOARCH) 407 | secondPath := filepath.Join(imagesDir, "second") 408 | _, err = layout.Write(secondPath, secondImage) 409 | s.NoError(err) 410 | 411 | s.req.Config.ContextDir = "testdata/image-args" 412 | s.req.Config.AdditionalTargets = []string{"first"} 413 | s.req.Config.ImageArgs = []string{ 414 | "first_image=" + firstPath, 415 | "second_image=" + secondPath, 416 | } 417 | 418 | err = os.Mkdir(s.outputPath("first"), 0755) 419 | s.NoError(err) 420 | 421 | _, err = s.build() 422 | s.NoError(err) 423 | 424 | _, err = tarball.ImageFromPath(s.outputPath("first", "image.tar"), nil) 425 | s.NoError(err) 426 | 427 | _, err = tarball.ImageFromPath(s.outputPath("image", "image.tar"), nil) 428 | s.NoError(err) 429 | } 430 | 431 | func (s *TaskSuite) TestImageArgsWithUppercaseName() { 432 | imagesDir, err := os.MkdirTemp("", "preload-images") 433 | s.NoError(err) 434 | 435 | defer os.RemoveAll(imagesDir) 436 | 437 | image := s.randomImage(1024, 2, "linux", "amd64") 438 | imagePath := filepath.Join(imagesDir, "first.tar") 439 | err = tarball.WriteToFile(imagePath, nil, image) 440 | s.NoError(err) 441 | 442 | s.req.Config.ContextDir = "testdata/image-args" 443 | s.req.Config.DockerfilePath = "testdata/image-args/Dockerfile.uppercase" 444 | s.req.Config.ImageArgs = []string{ 445 | "FIRST_IMAGE=" + imagePath, 446 | } 447 | s.req.Config.UnpackRootfs = true 448 | 449 | _, err = s.build() 450 | s.NoError(err) 451 | 452 | meta, err := s.imageMetadata("image") 453 | s.NoError(err) 454 | 455 | rootfsContent, err := os.ReadFile(s.imagePath("rootfs", "Dockerfile.second")) 456 | s.NoError(err) 457 | 458 | expectedContent, err := os.ReadFile("testdata/image-args/Dockerfile.uppercase") 459 | s.NoError(err) 460 | 461 | s.Equal(rootfsContent, expectedContent) 462 | 463 | s.Equal(meta.User, "banana") 464 | s.Equal(meta.Env, []string{"PATH=/darkness", "BA=nana"}) 465 | } 466 | 467 | func (s *TaskSuite) TestImageArgsUnpack() { 468 | imagesDir, err := os.MkdirTemp("", "preload-images") 469 | s.NoError(err) 470 | 471 | defer os.RemoveAll(imagesDir) 472 | 473 | image := s.randomImage(1024, 2, "linux", "amd64") 474 | imagePath := filepath.Join(imagesDir, "first.tar") 475 | err = tarball.WriteToFile(imagePath, nil, image) 476 | s.NoError(err) 477 | 478 | s.req.Config.ContextDir = "testdata/image-args" 479 | s.req.Config.AdditionalTargets = []string{"first"} 480 | s.req.Config.ImageArgs = []string{ 481 | "first_image=" + imagePath, 482 | "second_image=" + imagePath, 483 | } 484 | s.req.Config.UnpackRootfs = true 485 | 486 | _, err = s.build() 487 | s.NoError(err) 488 | 489 | meta, err := s.imageMetadata("image") 490 | s.NoError(err) 491 | 492 | rootfsContent, err := os.ReadFile(s.imagePath("rootfs", "Dockerfile.second")) 493 | s.NoError(err) 494 | 495 | expectedContent, err := os.ReadFile("testdata/image-args/Dockerfile") 496 | s.NoError(err) 497 | 498 | s.Equal(rootfsContent, expectedContent) 499 | 500 | s.Equal(meta.User, "banana") 501 | s.Equal(meta.Env, []string{"PATH=/darkness", "BA=nana"}) 502 | } 503 | 504 | func (s *TaskSuite) TestMultiTarget() { 505 | s.req.Config.ContextDir = "testdata/multi-target" 506 | s.req.Config.AdditionalTargets = []string{"additional-target"} 507 | 508 | err := os.Mkdir(s.outputPath("additional-target"), 0755) 509 | s.NoError(err) 510 | 511 | _, err = s.build() 512 | s.NoError(err) 513 | 514 | finalImage, err := tarball.ImageFromPath(s.imagePath("image.tar"), nil) 515 | s.NoError(err) 516 | 517 | finalCfg, err := finalImage.ConfigFile() 518 | s.NoError(err) 519 | s.Equal("final-target", finalCfg.Config.Labels["target"]) 520 | 521 | additionalImage, err := tarball.ImageFromPath(s.outputPath("additional-target", "image.tar"), nil) 522 | s.NoError(err) 523 | 524 | additionalCfg, err := additionalImage.ConfigFile() 525 | s.NoError(err) 526 | s.Equal("additional-target", additionalCfg.Config.Labels["target"]) 527 | } 528 | 529 | func (s *TaskSuite) TestMultiTargetExplicitTarget() { 530 | s.req.Config.ContextDir = "testdata/multi-target" 531 | s.req.Config.AdditionalTargets = []string{"additional-target"} 532 | s.req.Config.Target = "final-target" 533 | 534 | err := os.Mkdir(s.outputPath("additional-target"), 0755) 535 | s.NoError(err) 536 | 537 | _, err = s.build() 538 | s.NoError(err) 539 | 540 | finalImage, err := tarball.ImageFromPath(s.imagePath("image.tar"), nil) 541 | s.NoError(err) 542 | 543 | finalCfg, err := finalImage.ConfigFile() 544 | s.NoError(err) 545 | s.Equal("final-target", finalCfg.Config.Labels["target"]) 546 | 547 | additionalImage, err := tarball.ImageFromPath(s.outputPath("additional-target", "image.tar"), nil) 548 | s.NoError(err) 549 | 550 | additionalCfg, err := additionalImage.ConfigFile() 551 | s.NoError(err) 552 | s.Equal("additional-target", additionalCfg.Config.Labels["target"]) 553 | } 554 | 555 | func (s *TaskSuite) TestMultiTargetDigest() { 556 | s.req.Config.ContextDir = "testdata/multi-target" 557 | s.req.Config.AdditionalTargets = []string{"additional-target"} 558 | 559 | err := os.Mkdir(s.outputPath("additional-target"), 0755) 560 | s.NoError(err) 561 | 562 | _, err = s.build() 563 | s.NoError(err) 564 | 565 | additionalImage, err := tarball.ImageFromPath(s.outputPath("additional-target", "image.tar"), nil) 566 | s.NoError(err) 567 | digest, err := os.ReadFile(s.outputPath("additional-target", "digest")) 568 | s.NoError(err) 569 | additionalManifest, err := additionalImage.Manifest() 570 | s.NoError(err) 571 | s.Equal(string(digest), additionalManifest.Config.Digest.String()) 572 | 573 | finalImage, err := tarball.ImageFromPath(s.imagePath("image.tar"), nil) 574 | s.NoError(err) 575 | digest, err = os.ReadFile(s.outputPath("image", "digest")) 576 | s.NoError(err) 577 | finalManifest, err := finalImage.Manifest() 578 | s.NoError(err) 579 | s.Equal(string(digest), finalManifest.Config.Digest.String()) 580 | } 581 | 582 | func (s *TaskSuite) TestMultiTargetUnpack() { 583 | s.req.Config.ContextDir = "testdata/multi-target" 584 | s.req.Config.AdditionalTargets = []string{"additional-target"} 585 | s.req.Config.UnpackRootfs = true 586 | 587 | err := os.Mkdir(s.outputPath("additional-target"), 0755) 588 | s.NoError(err) 589 | 590 | _, err = s.build() 591 | s.NoError(err) 592 | 593 | rootfsContent, err := os.ReadFile(s.outputPath("additional-target", "rootfs", "Dockerfile.banana")) 594 | s.NoError(err) 595 | expectedContent, err := os.ReadFile("testdata/multi-target/Dockerfile") 596 | s.NoError(err) 597 | s.Equal(rootfsContent, expectedContent) 598 | 599 | meta, err := s.imageMetadata("additional-target") 600 | s.NoError(err) 601 | s.Equal(meta.User, "banana") 602 | s.Equal(meta.Env, []string{"PATH=/darkness", "BA=nana"}) 603 | 604 | rootfsContent, err = os.ReadFile(s.outputPath("image", "rootfs", "Dockerfile.orange")) 605 | s.NoError(err) 606 | expectedContent, err = os.ReadFile("testdata/multi-target/Dockerfile") 607 | s.NoError(err) 608 | s.Equal(rootfsContent, expectedContent) 609 | 610 | meta, err = s.imageMetadata("image") 611 | s.NoError(err) 612 | s.Equal(meta.User, "orange") 613 | s.Equal(meta.Env, []string{"PATH=/lightness", "OR=ange"}) 614 | } 615 | 616 | func (s *TaskSuite) TestAddHosts() { 617 | s.req.Config.ContextDir = "testdata/add-hosts" 618 | s.req.Config.AddHosts = "test-host=1.2.3.4" 619 | 620 | _, err := s.build() 621 | s.NoError(err) 622 | } 623 | 624 | func (s *TaskSuite) TestImagePlatform() { 625 | s.req.Config.ContextDir = "testdata/basic" 626 | s.req.Config.ImagePlatform = "linux/arm64" 627 | 628 | _, err := s.build() 629 | s.NoError(err) 630 | 631 | image, err := tarball.ImageFromPath(s.imagePath("image.tar"), nil) 632 | s.NoError(err) 633 | 634 | configFile, err := image.ConfigFile() 635 | s.NoError(err) 636 | 637 | s.Equal("linux", configFile.OS) 638 | s.Equal("arm64", configFile.Architecture) 639 | } 640 | 641 | func (s *TaskSuite) TestOciImage() { 642 | s.req.Config.ContextDir = "testdata/multi-arch" 643 | s.req.Config.ImagePlatform = "linux/arm64,linux/amd64" 644 | s.req.Config.OutputOCI = true 645 | 646 | _, err := s.build() 647 | s.NoError(err) 648 | 649 | l, err := layout.ImageIndexFromPath(s.imagePath("image")) 650 | s.NoError(err) 651 | 652 | im, err := l.IndexManifest() 653 | s.NoError(err) 654 | 655 | desc := im.Manifests[0] 656 | ii, err := l.ImageIndex(desc.Digest) 657 | s.NoError(err) 658 | 659 | images, err := ii.IndexManifest() 660 | s.NoError(err) 661 | 662 | expectedArch := []string{"arm64", "amd64"} 663 | var actualArch []string 664 | for _, manifest := range images.Manifests { 665 | actualArch = append(actualArch, string(manifest.Platform.Architecture)) 666 | } 667 | 668 | s.True(reflect.DeepEqual(expectedArch, actualArch)) 669 | } 670 | 671 | func (s *TaskSuite) build() (task.Response, error) { 672 | return task.Build(s.buildkitd, s.outputsDir, s.req) 673 | } 674 | 675 | func (s *TaskSuite) imagePath(path ...string) string { 676 | return s.outputPath(append([]string{"image"}, path...)...) 677 | } 678 | 679 | func (s *TaskSuite) outputPath(path ...string) string { 680 | return filepath.Join(append([]string{s.outputsDir}, path...)...) 681 | } 682 | 683 | func (s *TaskSuite) imageMetadata(output string) (task.ImageMetadata, error) { 684 | metadataPayload, err := os.ReadFile(s.outputPath(output, "metadata.json")) 685 | if err != nil { 686 | return task.ImageMetadata{}, err 687 | } 688 | 689 | var meta task.ImageMetadata 690 | err = json.Unmarshal(metadataPayload, &meta) 691 | if err != nil { 692 | return task.ImageMetadata{}, err 693 | } 694 | 695 | return meta, nil 696 | } 697 | 698 | func (s *TaskSuite) randomImage(byteSize, layers int64, os, arch string) v1.Image { 699 | image, err := random.Image(byteSize, layers) 700 | s.NoError(err) 701 | 702 | cf, err := image.ConfigFile() 703 | s.NoError(err) 704 | 705 | cf = cf.DeepCopy() 706 | cf.OS = os 707 | cf.Architecture = arch 708 | 709 | image, err = mutate.ConfigFile(image, cf) 710 | s.NoError(err) 711 | 712 | return image 713 | } 714 | 715 | func (s *TaskSuite) randomImageIndex(byteSize, layers int64, os, arch string) v1.ImageIndex { 716 | index, err := random.Index(byteSize, layers, 1) 717 | s.NoError(err) 718 | manifest, err := index.IndexManifest() 719 | s.NoError(err) 720 | 721 | var image v1.Image 722 | for _, m := range manifest.Manifests { 723 | if m.MediaType.IsImage() { 724 | image, err = index.Image(m.Digest) 725 | s.NoError(err) 726 | break 727 | } 728 | } 729 | 730 | index = mutate.RemoveManifests(index, match.MediaTypes(string(types.OCIManifestSchema1), string(types.DockerManifestSchema2))) 731 | 732 | cf, err := image.ConfigFile() 733 | s.NoError(err) 734 | 735 | cf = cf.DeepCopy() 736 | cf.OS = os 737 | cf.Architecture = arch 738 | 739 | image, err = mutate.ConfigFile(image, cf) 740 | s.NoError(err) 741 | 742 | index = mutate.AppendManifests(index, mutate.IndexAddendum{Add: image}) 743 | 744 | return index 745 | } 746 | 747 | func TestSuite(t *testing.T) { 748 | suite.Run(t, &TaskSuite{ 749 | Assertions: require.New(t), 750 | }) 751 | } 752 | -------------------------------------------------------------------------------- /testdata/add-hosts/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | 3 | RUN grep '1.2.3.4.*test-host' /etc/hosts 4 | -------------------------------------------------------------------------------- /testdata/basic/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY Dockerfile / 3 | -------------------------------------------------------------------------------- /testdata/build-args/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | ARG some_arg 3 | ARG some_other_arg 4 | RUN test "$some_arg" = "some_value" 5 | RUN test "$some_other_arg" = "some_other_value" 6 | -------------------------------------------------------------------------------- /testdata/build-args/build_arg_file: -------------------------------------------------------------------------------- 1 | some_other_arg=some_other_value 2 | -------------------------------------------------------------------------------- /testdata/build-args/build_args_file: -------------------------------------------------------------------------------- 1 | some_arg=some_value 2 | some_other_arg=some_other_value 3 | -------------------------------------------------------------------------------- /testdata/build-args/build_args_file.yaml: -------------------------------------------------------------------------------- 1 | some_arg: some_value 2 | some_other_arg: some_other_value 3 | -------------------------------------------------------------------------------- /testdata/buildkit-secret/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1.11 2 | FROM busybox 3 | RUN --mount=type=secret,id=secret test "$(cat /run/secrets/secret)" = "hello-world" 4 | -------------------------------------------------------------------------------- /testdata/buildkit-secret/secret: -------------------------------------------------------------------------------- 1 | hello-world -------------------------------------------------------------------------------- /testdata/buildkit-ssh/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1.11 2 | FROM alpine 3 | 4 | RUN apk add --no-cache openssh-client 5 | 6 | # shows private key available in ssh agent 7 | RUN --mount=type=ssh,id=my_ssh_key ssh-add -l | grep "SHA256:DFxHFuit9VQtxkBrZWzJhf5OTL5/RwzCJuZjTAPC1DI" 8 | -------------------------------------------------------------------------------- /testdata/buildkit-ssh/id_rsa_test: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn 3 | NhAAAAAwEAAQAAAgEArNG0wYPOc/emyQKvfX6aPIyTqB8xTA7qaHrEQI5ljLrE7FdF702z 4 | sQijG+0i4R9X5TGf/kLLmmQBXHKnfgfIGzh1/zKHRc+s9fM+fcgrTQD5Ybsj0U+rRMKtoQ 5 | ypJSqGYfTAbhXYxCEOpjUTphtheuxhuDtoSpt2mcE75U78EyzsBM2oEorvLZWv8G70FiOA 6 | 9rw/RMGzxtsp9pBDPhgbMPnQXB9euILq7VjtuS8kqnlqgYpsgISGt+W90okIcbEf5Cr+c/ 7 | J0WUoGnMqy8b4WXU0dJsxsSY2rVitXuykIBm6WJfKcKeF6fdZv9Yh7qr5yzKhzuPtgvCS1 8 | UFD752ZUsTa+XIFF+XD3ZpnatLDlDdDqYHIQjMkYeI9DWXmIzI4GmqY1PMAylZAcoFc2el 9 | R0084QigStcUQbHaFXXg7yUJuz/A/WKxlpkcMZ+3sjbScEV3Y26HdS1LyqOI8hgQL11jm0 10 | rApxQTln4NkS5UdrfZ7tlXi7lTeLZJUjiuOs63UsUd++z/a1k8o8rYuC+2cmRjlco+qUlD 11 | ShA1IvzGDSLLVr9Im+ZRDtrbL+6PaXbzqX1wwsow/XwgsHDIO8O57TvCM0akEzHsRUw+5B 12 | lJhXURQOko8i2Acs+yc1DgGWugXwFokE6+i66v6X0iFGrei0C3yKqVIBisav9TYlveYi6B 13 | EAAAdYY2LAYGNiwGAAAAAHc3NoLXJzYQAAAgEArNG0wYPOc/emyQKvfX6aPIyTqB8xTA7q 14 | aHrEQI5ljLrE7FdF702zsQijG+0i4R9X5TGf/kLLmmQBXHKnfgfIGzh1/zKHRc+s9fM+fc 15 | grTQD5Ybsj0U+rRMKtoQypJSqGYfTAbhXYxCEOpjUTphtheuxhuDtoSpt2mcE75U78Eyzs 16 | BM2oEorvLZWv8G70FiOA9rw/RMGzxtsp9pBDPhgbMPnQXB9euILq7VjtuS8kqnlqgYpsgI 17 | SGt+W90okIcbEf5Cr+c/J0WUoGnMqy8b4WXU0dJsxsSY2rVitXuykIBm6WJfKcKeF6fdZv 18 | 9Yh7qr5yzKhzuPtgvCS1UFD752ZUsTa+XIFF+XD3ZpnatLDlDdDqYHIQjMkYeI9DWXmIzI 19 | 4GmqY1PMAylZAcoFc2elR0084QigStcUQbHaFXXg7yUJuz/A/WKxlpkcMZ+3sjbScEV3Y2 20 | 6HdS1LyqOI8hgQL11jm0rApxQTln4NkS5UdrfZ7tlXi7lTeLZJUjiuOs63UsUd++z/a1k8 21 | o8rYuC+2cmRjlco+qUlDShA1IvzGDSLLVr9Im+ZRDtrbL+6PaXbzqX1wwsow/XwgsHDIO8 22 | O57TvCM0akEzHsRUw+5BlJhXURQOko8i2Acs+yc1DgGWugXwFokE6+i66v6X0iFGrei0C3 23 | yKqVIBisav9TYlveYi6BEAAAADAQABAAACAC0iZ67Smay31RtSVDrWQbnmjPmvi9RtYNMA 24 | nRivF951ONDwyhcBmBh4RQEaIa3h3bbCCyMAluQvkhtu5keICL7zM/3+WE3nOyjQ6lU3Xf 25 | eydW8MzQHxaK2AmpAhGlvWi1ox7b/SfNZcO9M0sXkEVaUg7zSKb2Zsy8DEMrxksHjhlpJ6 26 | k5akKmshWN3WXHEbvKaz62ItpKIewwreCnHBIfjchYpJtxdBOdHbE9r1cQIEy8ghOs0lz3 27 | lfyv+dQlNPCyEwCVyGydQ3/Rod5jY0iuq7JQ3o/a8IDONoVnVO9gWYBCbNAfP+K1uKEZty 28 | lbm2G41PwUlifZUlkST0wZVhqGRiGtnBTw9JbQRcc0nUthDHn8mVevY2y2Dy1mSNvpAXQM 29 | THOZ5jAc1lfhZ5WuMEhESS5QeTS95Sz+co+EB0AIgNH0nmuryu2/WGECNCLrCDqIOgA/gn 30 | 8uVHYxjAihXok1ARyzCu6pmVzpKXFyNwsnnj0fSC8ttZxdwtKrw7WhKoP96PozNPG6zIa/ 31 | yQ8eHCDxO1KHZUL0CiMrhA8jS0bondo7hpPxTC8L0x+ib/zLNmvxRyGB0h27BCPm0U+tMO 32 | R0+vT86LFp7wJrQG2/Doo3rpeBkPepHWWQ+gxn+sFCLjx9GwDxNFM4mggJi8cD2oxQYYet 33 | nkh/wPIaN+2su/Zj8BAAABACd5dBvovh1vvOZuwTtaCmbTpc3PFwG1yOtqxQb1w/TAD4Ss 34 | HOG3LiM2C2vFySokGeKGgt35GjIXgR2L0m2Lj+SgBteNKy1GvAX6MZT3MCw0aaH9FBzTTw 35 | bBG0mGRNvw8d9dvtfkaJdfOaojnCi2c7mAX3bTiK1gmDPeX8izQ9ktYqVusyRgtacZOv9P 36 | Y0giYnIWYsakPDEnLSD85nDcjnZ1FeP1bokwOqTyc6on8S786p/JeHEjn8Mh8y6aG2cRW3 37 | 8/eP2eqkCX/R3pd0hIjKNMilYDMKu+xGcDiSPqKBA5GYRJ4loWAE2xcZhlMM4GhgxXYdZy 38 | kEqcF0JWnOC68CQAAAEBANhEZ3QZ7umloo6TDq5F2PaKiX9ZrrJ0hAySCUyvk7Z9tdWcCb 39 | f4wA6KbuwG5hOIv2JJmFzV6q/Xn17Ct8TGKY0i6a1e61WCAuKUxhHPbs0EVTSbBLLrVcAx 40 | h45YTLhqeAqUAgSCylnWGBfOB6+bKUYhMBLROB9K7Mm0xckbj073VrN1pGyJwpjwdnd+TV 41 | zG9H9Lskf3dJT/s7p0+MGgiRsQBIiIrxDVx/gJAb2NFhbnCRWL0Nktnde4aoI+pvBhHguA 42 | qPqnC32+ZFu5q0p72bPC3TK7q9gCufrB8vBW4zi4vK0MtaY/UHK27ukZbYCqa9SS6A5ZNo 43 | Ssl4kD0W3XTIkAAAEBAMyR1JAfDLwkY+FJWW593EigjfCsCxyovsCu7eCVDTNqj58GmVO5 44 | jwpX7AcTJ9sbck1cajPJJJonOa9qNf2UQDgoB7SJ+9Zw+aNaLFfFBsCHOouuUFrDjTDf1M 45 | j8FJpV33Qt2VCbPMZwkwFgtQhhMSJbrUv1wZhDfW7WMKRBV1QOeGX+GlW30GnSobxlFKUS 46 | mDMYAtsRHa/4+w/itOQgQT6e2nPYlwofMbrZxReaAUMAvt6RwZyoUJ9+gStuQ7gLYvG4Nt 47 | ou1ZzGDYavhTRsywAMfkQ9CN9fSqieafDTcqcBi2XcCx7IrbDCLYJZA3zB6HFzgGvF/plG 48 | gy40fbJ6LUkAAAAcem9lbGlAWm9lcy1NYWNCb29rLVByby5sb2NhbAECAwQFBgc= 49 | -----END OPENSSH PRIVATE KEY----- 50 | -------------------------------------------------------------------------------- /testdata/buildkit-ssh/id_rsa_test.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCs0bTBg85z96bJAq99fpo8jJOoHzFMDupoesRAjmWMusTsV0XvTbOxCKMb7SLhH1flMZ/+QsuaZAFccqd+B8gbOHX/ModFz6z18z59yCtNAPlhuyPRT6tEwq2hDKklKoZh9MBuFdjEIQ6mNROmG2F67GG4O2hKm3aZwTvlTvwTLOwEzagSiu8tla/wbvQWI4D2vD9EwbPG2yn2kEM+GBsw+dBcH164gurtWO25LySqeWqBimyAhIa35b3SiQhxsR/kKv5z8nRZSgacyrLxvhZdTR0mzGxJjatWK1e7KQgGbpYl8pwp4Xp91m/1iHuqvnLMqHO4+2C8JLVQUPvnZlSxNr5cgUX5cPdmmdq0sOUN0OpgchCMyRh4j0NZeYjMjgaapjU8wDKVkBygVzZ6VHTTzhCKBK1xRBsdoVdeDvJQm7P8D9YrGWmRwxn7eyNtJwRXdjbod1LUvKo4jyGBAvXWObSsCnFBOWfg2RLlR2t9nu2VeLuVN4tklSOK46zrdSxR377P9rWTyjyti4L7ZyZGOVyj6pSUNKEDUi/MYNIstWv0ib5lEO2tsv7o9pdvOpfXDCyjD9fCCwcMg7w7ntO8IzRqQTMexFTD7kGUmFdRFA6SjyLYByz7JzUOAZa6BfAWiQTr6Lrq/pfSIUat6LQLfIqpUgGKxq/1NiW95iLoEQ== zoeli@Zoes-MacBook-Pro.local 2 | -------------------------------------------------------------------------------- /testdata/buildkitd-config/mirrors.toml: -------------------------------------------------------------------------------- 1 | [registry] 2 | [registry."docker.io"] 3 | mirrors = ["hub.docker.io"] 4 | -------------------------------------------------------------------------------- /testdata/dockerfile-path/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | 3 | # this Dockerfile should never be used; it's just here to test that the 4 | # configured dockerfile path is respected, so just make it fail in case it 5 | # does get used 6 | RUN false 7 | -------------------------------------------------------------------------------- /testdata/dockerfile-path/hello.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | RUN true 3 | -------------------------------------------------------------------------------- /testdata/image-args/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG first_image 2 | ARG second_image 3 | 4 | FROM ${first_image} AS first 5 | COPY Dockerfile /Dockerfile.first 6 | 7 | FROM ${second_image} 8 | USER banana 9 | ENV PATH=/darkness 10 | ENV BA=nana 11 | COPY Dockerfile /Dockerfile.second 12 | -------------------------------------------------------------------------------- /testdata/image-args/Dockerfile.uppercase: -------------------------------------------------------------------------------- 1 | ARG FIRST_IMAGE 2 | 3 | FROM ${FIRST_IMAGE} 4 | USER banana 5 | ENV PATH=/darkness 6 | ENV BA=nana 7 | COPY Dockerfile.uppercase /Dockerfile.second 8 | -------------------------------------------------------------------------------- /testdata/labels/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY Dockerfile / -------------------------------------------------------------------------------- /testdata/labels/label_file: -------------------------------------------------------------------------------- 1 | some_other_label=some_other_value 2 | -------------------------------------------------------------------------------- /testdata/labels/label_layer.dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY Dockerfile / 3 | LABEL label_layer=some_label_layer_value -------------------------------------------------------------------------------- /testdata/labels/labels_file: -------------------------------------------------------------------------------- 1 | some_label=some_value 2 | some_other_label=some_other_value 3 | -------------------------------------------------------------------------------- /testdata/mirror/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mirrored-image:some-tag 2 | -------------------------------------------------------------------------------- /testdata/multi-arch/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1.11 2 | FROM alpine 3 | 4 | RUN apk add --no-cache vim 5 | -------------------------------------------------------------------------------- /testdata/multi-target/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch AS additional-target 2 | LABEL target=additional-target 3 | USER banana 4 | ADD Dockerfile /Dockerfile.banana 5 | ENV PATH=/darkness 6 | ENV BA=nana 7 | 8 | FROM scratch AS final-target 9 | LABEL target=final-target 10 | USER orange 11 | ADD Dockerfile /Dockerfile.orange 12 | ENV PATH=/lightness 13 | ENV OR=ange 14 | -------------------------------------------------------------------------------- /testdata/target/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | RUN false 3 | 4 | FROM busybox AS working-target 5 | RUN true 6 | 7 | FROM busybox AS broken-target 8 | RUN false 9 | -------------------------------------------------------------------------------- /testdata/target/target_file: -------------------------------------------------------------------------------- 1 | working-target 2 | -------------------------------------------------------------------------------- /testdata/unpack-rootfs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | USER banana 3 | ADD Dockerfile /Dockerfile 4 | ENV PATH=/darkness 5 | ENV BA=nana 6 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | // Request is the request payload sent from Concourse to execute the task. 4 | // 5 | // This is currently not really exercised by Concourse; it's a mock-up of what 6 | // a future 'reusable tasks' design may look like. 7 | type Request struct { 8 | ResponsePath string `json:"response_path"` 9 | Config Config `json:"config"` 10 | } 11 | 12 | // Response is sent back to Concourse by writing this structure to the 13 | // `response_path` specified in the request. 14 | // 15 | // This is also a mock-up. Right now it communicates the available outputs, 16 | // which may be useful to assist pipeline authors in knowing what artifacts are 17 | // available after a task excutes. 18 | // 19 | // In the future, pipeline authors may list which outputs they would like to 20 | // propagate to the rest of the build plan, by specifying `outputs` or 21 | // `output_mapping` like so: 22 | // 23 | // task: build 24 | // outputs: [image] 25 | // 26 | // task: build 27 | // output_mapping: {image: my-image} 28 | // 29 | // Outputs may also be 'cached', meaning their previous value will be present 30 | // for subsequent runs of the task: 31 | // 32 | // task: build 33 | // outputs: [image] 34 | // caches: [cache] 35 | type Response struct { 36 | Outputs []string `json:"outputs"` 37 | } 38 | 39 | // Config contains the configuration for the task. 40 | // 41 | // In the future, when Concourse supports a 'reusable task' interface, this 42 | // will be provided as a JSON request on `stdin`. 43 | // 44 | // For now, and for backwards-compatibility, we will also support taking values 45 | // from task params (i.e. env), hence the use of `envconfig:`. 46 | type Config struct { 47 | Debug bool `json:"debug" envconfig:"optional"` 48 | 49 | ContextDir string `json:"context" envconfig:"CONTEXT,optional"` 50 | DockerfilePath string `json:"dockerfile,omitempty" envconfig:"DOCKERFILE,optional"` 51 | BuildkitSSH string `json:"buildkit_ssh" envconfig:"optional"` 52 | 53 | Target string `json:"target" envconfig:"optional"` 54 | TargetFile string `json:"target_file" envconfig:"optional"` 55 | AdditionalTargets []string `json:"additional_targets" envconfig:"ADDITIONAL_TARGETS,optional"` 56 | 57 | BuildArgs []string `json:"build_args" envconfig:"optional"` 58 | BuildArgsFile string `json:"build_args_file" envconfig:"optional"` 59 | 60 | RegistryMirrors []string `json:"registry_mirrors" envconfig:"REGISTRY_MIRRORS,optional"` 61 | 62 | Labels []string `json:"labels" envconfig:"optional"` 63 | LabelsFile string `json:"labels_file" envconfig:"optional"` 64 | 65 | BuildkitSecrets map[string]string `json:"buildkit_secrets" envconfig:"optional"` 66 | 67 | BuildkitExtraConfig string `json:"buildkit_extra_config" envconfig:"BUILDKIT_EXTRA_CONFIG,optional"` 68 | 69 | // Unpack the OCI image into Concourse's rootfs/ + metadata.json image scheme. 70 | // 71 | // Theoretically this would go away if/when we standardize on OCI. 72 | UnpackRootfs bool `json:"unpack_rootfs" envconfig:"optional"` 73 | 74 | OutputOCI bool `json:"output_oci" envconfig:"optional"` 75 | 76 | // Images to pre-load in order to avoid fetching at build time. Mapping from 77 | // build arg name to OCI image tarball path. 78 | // 79 | // Each image will be pre-loaded and a build arg will be set to a value 80 | // appropriate for setting in 'FROM ...'. 81 | ImageArgs []string `json:"image_args" envconfig:"optional"` 82 | 83 | AddHosts string `json:"add_hosts" envconfig:"BUILDKIT_ADD_HOSTS,optional"` 84 | 85 | ImagePlatform string `json:"image_platform" envconfig:"optional"` 86 | } 87 | 88 | // ImageMetadata is the schema written to manifest.json when producing the 89 | // legacy Concourse image format (rootfs/..., metadata.json). 90 | type ImageMetadata struct { 91 | Env []string `json:"env"` 92 | User string `json:"user"` 93 | } 94 | -------------------------------------------------------------------------------- /unpack.go: -------------------------------------------------------------------------------- 1 | // taken verbatim from registry-image resource; extract common lib? 2 | 3 | package task 4 | 5 | import ( 6 | "archive/tar" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/concourse/go-archive/tarfs" 14 | "github.com/fatih/color" 15 | v1 "github.com/google/go-containerregistry/pkg/v1" 16 | "github.com/sirupsen/logrus" 17 | "github.com/vbauerster/mpb" 18 | "github.com/vbauerster/mpb/decor" 19 | ) 20 | 21 | const whiteoutPrefix = ".wh." 22 | 23 | func unpackImage(dest string, img v1.Image, debug bool) error { 24 | layers, err := img.Layers() 25 | if err != nil { 26 | return err 27 | } 28 | 29 | chown := os.Getuid() == 0 30 | 31 | var out io.Writer 32 | if debug { 33 | out = io.Discard 34 | } else { 35 | out = os.Stderr 36 | } 37 | 38 | progress := mpb.New(mpb.WithOutput(out)) 39 | 40 | bars := make([]*mpb.Bar, len(layers)) 41 | 42 | for i, layer := range layers { 43 | size, err := layer.Size() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | digest, err := layer.Digest() 49 | if err != nil { 50 | return err 51 | } 52 | 53 | bars[i] = progress.AddBar( 54 | size, 55 | mpb.PrependDecorators(decor.Name(color.HiBlackString(digest.Hex[0:12]))), 56 | mpb.AppendDecorators(decor.CountersKibiByte("%.1f/%.1f")), 57 | ) 58 | } 59 | 60 | // iterate over layers in reverse order; no need to write things files that 61 | // are modified by later layers anyway 62 | for i, layer := range layers { 63 | logrus.Debugf("extracting layer %d of %d", i+1, len(layers)) 64 | 65 | err = extractLayer(dest, layer, bars[i], chown) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | bars[i].SetTotal(bars[i].Current(), true) 71 | } 72 | 73 | progress.Wait() 74 | 75 | return nil 76 | } 77 | 78 | func extractLayer(dest string, layer v1.Layer, bar *mpb.Bar, chown bool) error { 79 | r, err := layer.Uncompressed() 80 | if err != nil { 81 | return fmt.Errorf("compressed: %w", err) 82 | } 83 | 84 | defer r.Close() 85 | 86 | tr := tar.NewReader(bar.ProxyReader(r)) 87 | 88 | for { 89 | hdr, err := tr.Next() 90 | if err == io.EOF { 91 | break 92 | } 93 | 94 | if err != nil { 95 | return err 96 | } 97 | 98 | path := filepath.Join(dest, filepath.Clean(hdr.Name)) 99 | base := filepath.Base(path) 100 | dir := filepath.Dir(path) 101 | 102 | log := logrus.WithFields(logrus.Fields{ 103 | "Name": hdr.Name, 104 | }) 105 | 106 | log.Debug("unpacking") 107 | 108 | if strings.HasPrefix(base, whiteoutPrefix) { 109 | // layer has marked a file as deleted 110 | name := strings.TrimPrefix(base, whiteoutPrefix) 111 | removedPath := filepath.Join(dir, name) 112 | 113 | log.Debugf("removing %s", removedPath) 114 | 115 | err := os.RemoveAll(removedPath) 116 | if err != nil { 117 | return nil 118 | } 119 | 120 | continue 121 | } 122 | 123 | if hdr.Typeflag == tar.TypeBlock || hdr.Typeflag == tar.TypeChar { 124 | // devices can't be created in a user namespace 125 | log.Debugf("skipping device %s", hdr.Name) 126 | continue 127 | } 128 | 129 | if hdr.Typeflag == tar.TypeSymlink { 130 | log.Debugf("symlinking to %s", hdr.Linkname) 131 | } 132 | 133 | if hdr.Typeflag == tar.TypeLink { 134 | log.Debugf("hardlinking to %s", hdr.Linkname) 135 | } 136 | 137 | if fi, err := os.Lstat(path); err == nil { 138 | if fi.IsDir() && hdr.Name == "." { 139 | continue 140 | } 141 | 142 | if !(fi.IsDir() && hdr.Typeflag == tar.TypeDir) { 143 | log.Debugf("removing existing path") 144 | if err := os.RemoveAll(path); err != nil { 145 | return fmt.Errorf("remove: %w", err) 146 | } 147 | } 148 | } 149 | 150 | if err := tarfs.ExtractEntry(hdr, dest, tr, chown); err != nil { 151 | log.Debugf("extracting") 152 | return fmt.Errorf("extract entry: %w", err) 153 | } 154 | } 155 | 156 | return nil 157 | } 158 | --------------------------------------------------------------------------------