├── LICENSE ├── README.md ├── go.mod ├── go.sum └── mkctr.go /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020 Tailscale & AUTHORS. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `mkctr`: cross platform container builder for go 2 | 3 | `mkctr` is a small go binary which uses `GOOS= GOARCH= go build` directly to compile go binaries and then uses [go-containerregistry](https://github.com/google/go-containerregistry) to create and publish the new containers based on the desired platforms. 4 | 5 | This is inspired by [ko](https://github.com/google/ko) which is awesome but doesn't support multiple binaries in a single container. 6 | 7 | ## Usage 8 | 9 | ```bash 10 | mkctr \ 11 | --base="alpine:latest" \ 12 | --gopaths="\ 13 | tailscale.com/cmd/tailscale:/usr/local/bin/tailscale, \ 14 | tailscale.com/cmd/tailscaled:/usr/local/bin/tailscaled" \ 15 | --tags="latest" \ 16 | --repos="tailscale/tailscale" \ 17 | [--files=foo.txt:/var/lib/foo.txt,bar.txt:/var/lib/bar.txt] \ 18 | [--target=] \ # e.g. flyio, local 19 | [--push] \ 20 | [--] [...] 21 | ``` 22 | 23 | `mkctr` auto discovers `GOOS`/`GOARCH` from the specified base image. If the base image supports multiple platforms, binaries are compiled for each platform as long as it's one of `linux/amd64`, `linux/386`, `linux/arm`, `linux/arm64`. Multi-arch base image must be either an [OCI image index](https://github.com/opencontainers/image-spec/blob/main/image-index.md) or [Docker manifest list](https://github.com/openshift/docker-distribution/blob/master/docs/spec/manifest-v2-2.md#manifest-list). 24 | `mkctr` produces image of the same media type as the base image and uses the media type of the base image, or of the individual image references in case of a multi-arch image, to determine the media type of the layer it builds. 25 | 26 | 27 | ## Maturity 28 | This is under active development. While Tailscale uses it, backwards compatability is not guaranteed, and some functionality is missing. 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tailscale/mkctr 2 | 3 | // Use point versions of Go, see https://github.com/tailscale/tailscale/pull/13485 4 | go 1.23.1 5 | 6 | require github.com/google/go-containerregistry v0.20.2 7 | 8 | require ( 9 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 10 | github.com/Microsoft/go-winio v0.6.2 // indirect 11 | github.com/containerd/log v0.1.0 // indirect 12 | github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect 13 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 14 | github.com/distribution/reference v0.6.0 // indirect 15 | github.com/docker/cli v27.4.1+incompatible // indirect 16 | github.com/docker/distribution v2.8.3+incompatible // indirect 17 | github.com/docker/docker v27.4.1+incompatible // indirect 18 | github.com/docker/docker-credential-helpers v0.8.2 // indirect 19 | github.com/docker/go-connections v0.5.0 // indirect 20 | github.com/docker/go-units v0.5.0 // indirect 21 | github.com/felixge/httpsnoop v1.0.4 // indirect 22 | github.com/go-logr/logr v1.4.2 // indirect 23 | github.com/go-logr/stdr v1.2.2 // indirect 24 | github.com/gogo/protobuf v1.3.2 // indirect 25 | github.com/klauspost/compress v1.17.11 // indirect 26 | github.com/mitchellh/go-homedir v1.1.0 // indirect 27 | github.com/moby/docker-image-spec v1.3.1 // indirect 28 | github.com/opencontainers/go-digest v1.0.0 // indirect 29 | github.com/opencontainers/image-spec v1.1.0 // indirect 30 | github.com/pkg/errors v0.9.1 // indirect 31 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 32 | github.com/sirupsen/logrus v1.9.3 // indirect 33 | github.com/vbatts/tar-split v0.11.6 // indirect 34 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 35 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect 36 | go.opentelemetry.io/otel v1.33.0 // indirect 37 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect 38 | go.opentelemetry.io/otel/metric v1.33.0 // indirect 39 | go.opentelemetry.io/otel/sdk v1.28.0 // indirect 40 | go.opentelemetry.io/otel/trace v1.33.0 // indirect 41 | golang.org/x/net v0.27.0 // indirect 42 | golang.org/x/sync v0.10.0 // indirect 43 | golang.org/x/sys v0.29.0 // indirect 44 | golang.org/x/time v0.5.0 // indirect 45 | gotest.tools/v3 v3.4.0 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 2 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 3 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 4 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 5 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 6 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 7 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 8 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 9 | github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= 10 | github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 14 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 16 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 17 | github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI= 18 | github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 19 | github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= 20 | github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 21 | github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4= 22 | github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 23 | github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= 24 | github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= 25 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 26 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 27 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 28 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 29 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 30 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 31 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 32 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 33 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 34 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 35 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 36 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 37 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 38 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 39 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 40 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 41 | github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo= 42 | github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8= 43 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 44 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 45 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= 46 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= 47 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 48 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 49 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 50 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 51 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 52 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 53 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 54 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 55 | github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= 56 | github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 57 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 58 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 59 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 60 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 61 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 62 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 63 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 64 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 65 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 66 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 67 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 68 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 69 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 70 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 71 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 72 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 73 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 74 | github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= 75 | github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= 76 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 77 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 78 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 79 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 80 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= 81 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= 82 | go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= 83 | go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= 84 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= 85 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= 86 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= 87 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= 88 | go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= 89 | go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= 90 | go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= 91 | go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= 92 | go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= 93 | go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= 94 | go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= 95 | go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= 96 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 97 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 98 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 99 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 100 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 101 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 102 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 103 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 104 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 105 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 106 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 107 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 108 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 109 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 110 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 111 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 112 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 113 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 117 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 118 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 119 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 120 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 121 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 122 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 123 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 124 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 125 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 126 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 127 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 128 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 129 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 130 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 131 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 132 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 133 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 134 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 135 | google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= 136 | google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= 137 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= 138 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 139 | google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= 140 | google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= 141 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 142 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 143 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 144 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 145 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 146 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 147 | gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= 148 | gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= 149 | -------------------------------------------------------------------------------- /mkctr.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // mkctr builds the Tailscale OCI containers. 6 | package main 7 | 8 | import ( 9 | "archive/tar" 10 | "bytes" 11 | "context" 12 | "errors" 13 | "flag" 14 | "fmt" 15 | "io" 16 | "io/fs" 17 | "log" 18 | "os" 19 | "os/exec" 20 | "path/filepath" 21 | "runtime" 22 | "strings" 23 | "time" 24 | 25 | "github.com/google/go-containerregistry/pkg/authn" 26 | "github.com/google/go-containerregistry/pkg/name" 27 | v1 "github.com/google/go-containerregistry/pkg/v1" 28 | "github.com/google/go-containerregistry/pkg/v1/daemon" 29 | "github.com/google/go-containerregistry/pkg/v1/empty" 30 | "github.com/google/go-containerregistry/pkg/v1/mutate" 31 | "github.com/google/go-containerregistry/pkg/v1/remote" 32 | "github.com/google/go-containerregistry/pkg/v1/tarball" 33 | "github.com/google/go-containerregistry/pkg/v1/types" 34 | ) 35 | 36 | type logf func(msg string, args ...interface{}) 37 | 38 | func withPrefix(f logf, prefix string) logf { 39 | return func(format string, args ...interface{}) { 40 | f(prefix+format, args...) 41 | } 42 | } 43 | 44 | // parseFiles parses a comma-separated list of colon-separated pairs 45 | // into a map of filePathOnDisk -> filePathInContainer. 46 | func parseFiles(s string) (map[string]string, error) { 47 | ret := map[string]string{} 48 | if len(s) == 0 { 49 | return ret, nil 50 | } 51 | for _, f := range strings.Split(s, ",") { 52 | f = strings.TrimSpace(f) 53 | fs := strings.Split(f, ":") 54 | if len(fs) != 2 { 55 | return nil, fmt.Errorf("unparseable file field %q", f) 56 | } 57 | ret[fs[0]] = fs[1] 58 | } 59 | return ret, nil 60 | } 61 | 62 | func parseRepos(reg, tags []string) ([]name.Tag, error) { 63 | var refs []name.Tag 64 | for _, rs := range reg { 65 | r, err := name.NewRepository(rs) 66 | if err != nil { 67 | return nil, err 68 | } 69 | for _, t := range tags { 70 | refs = append(refs, r.Tag(t)) 71 | } 72 | } 73 | return refs, nil 74 | } 75 | 76 | type buildParams struct { 77 | baseImage string 78 | goPaths map[string]string 79 | staticFiles map[string]string 80 | imageRefs []name.Tag 81 | publish bool 82 | ldflags string 83 | gotags string 84 | target string 85 | verbose bool 86 | annotations map[string]string // OCI image annotations 87 | } 88 | 89 | func main() { 90 | var ( 91 | baseImage = flag.String("base", "", "base image for container") 92 | gopaths = flag.String("gopaths", "", "comma-separated list of go paths in src:dst form") 93 | files = flag.String("files", "", "comma-separated list of static files in src:dst form") 94 | repos = flag.String("repos", "", "comma-separated list of image registries") 95 | tagArg = flag.String("tags", "", "comma-separated tags") 96 | ldflagsArg = flag.String("ldflags", "", "the --ldflags value to pass to go") 97 | gotags = flag.String("gotags", "", "the --tags value to pass to go") 98 | push = flag.Bool("push", false, "publish the image") 99 | target = flag.String("target", "", "build for a specific env (options: flyio, local)") 100 | verbose = flag.Bool("v", false, "verbose build output") 101 | annotations = flag.String("annotations", "", `OCI image annotations https://github.com/opencontainers/image-spec/blob/main/annotations.md. 102 | Annotations must be comma separated key=value pairs, i.e key1=val1,key2=val2. For a single image manifest annotations will get added to the image manifest. 103 | For an image index (a multi-platform manifest list) annotations will get added to each image manifest as well as the image index. 104 | Annotations with empty values are not supported.`) 105 | ) 106 | flag.Parse() 107 | if *tagArg == "" { 108 | log.Fatal("tags must be set") 109 | } 110 | if *repos == "" { 111 | log.Fatal("registries must be set") 112 | } 113 | if *baseImage == "" { 114 | log.Fatal("baseImage must be set") 115 | } 116 | switch *target { 117 | case "", "flyio", "local": 118 | default: 119 | log.Fatalf("unsupported target %q", *target) 120 | } 121 | refs, err := parseRepos(strings.Split(*repos, ","), strings.Split(*tagArg, ",")) 122 | if err != nil { 123 | log.Fatal(err) 124 | } 125 | paths, err := parseFiles(*gopaths) 126 | if err != nil { 127 | log.Fatal(err) 128 | } 129 | staticFiles, err := parseFiles(*files) 130 | if err != nil { 131 | log.Fatal(err) 132 | } 133 | if len(paths) == 0 && len(staticFiles) == 0 { 134 | log.Fatal("at least one of --files or --gopaths must be set") 135 | } 136 | 137 | bp := &buildParams{ 138 | baseImage: *baseImage, 139 | goPaths: paths, 140 | staticFiles: staticFiles, 141 | imageRefs: refs, 142 | publish: *push, 143 | ldflags: *ldflagsArg, 144 | gotags: *gotags, 145 | target: *target, 146 | verbose: *verbose, 147 | annotations: parseAnnotations(*annotations), 148 | } 149 | 150 | if err := fetchAndBuild(bp); err != nil { 151 | log.Fatal(err) 152 | } 153 | } 154 | 155 | func fetchBaseImage(baseImage string, opts ...remote.Option) (*remote.Descriptor, error) { 156 | baseRef, err := name.ParseReference(baseImage) 157 | if err != nil { 158 | return nil, err 159 | } 160 | desc, err := remote.Get(baseRef, opts...) 161 | if err != nil { 162 | return nil, err 163 | } 164 | return desc, nil 165 | } 166 | 167 | // canRunLocal reports whether the platform can run the binary locally, to be 168 | // used by the local target. 169 | func canRunLocal(p v1.Platform) bool { 170 | if p.OS != "linux" { 171 | return false 172 | } 173 | if runtime.GOOS == "linux" { 174 | return p.Architecture == runtime.GOARCH 175 | } 176 | if runtime.GOOS == "darwin" { 177 | // macOS can run amd64 linux binaries in docker. 178 | return p.Architecture == "amd64" 179 | } 180 | return false 181 | } 182 | 183 | func verifyPlatform(p v1.Platform, target string) error { 184 | if p.OS != "linux" { 185 | return fmt.Errorf("unsupported OS: %v", p.OS) 186 | } 187 | if target == "local" && !canRunLocal(p) { 188 | return fmt.Errorf("not required for target %q", target) 189 | } 190 | if target == "flyio" && p.Architecture != "amd64" { 191 | return fmt.Errorf("not required for target %q", target) 192 | } 193 | switch p.Architecture { 194 | case "arm", "arm64", "amd64", "386": 195 | default: 196 | return fmt.Errorf("unsupported arch: %v", p.Architecture) 197 | } 198 | return nil 199 | } 200 | 201 | func fetchAndBuild(bp *buildParams) error { 202 | ctx := context.Background() 203 | logf := log.Printf 204 | remoteOpts := []remote.Option{ 205 | remote.WithAuthFromKeychain(authn.DefaultKeychain), 206 | remote.WithContext(ctx), 207 | } 208 | baseDesc, err := fetchBaseImage(bp.baseImage, remoteOpts...) 209 | if err != nil { 210 | return err 211 | } 212 | 213 | switch baseDesc.MediaType { 214 | case types.OCIManifestSchema1, types.DockerManifestSchema2: 215 | // baseRef is an image. 216 | // Special case to make it only build for that one platform. 217 | baseImage, err := baseDesc.Image() 218 | if err != nil { 219 | return err 220 | } 221 | 222 | config, err := baseImage.ConfigFile() 223 | if err != nil { 224 | return fmt.Errorf("error getting config: %w", err) 225 | } 226 | if config.Architecture == "" || config.OS == "" { 227 | return fmt.Errorf("unknown platform for image: %v", bp.baseImage) 228 | } 229 | 230 | p := v1.Platform{ 231 | OS: config.OS, 232 | Architecture: config.Architecture, 233 | } 234 | if config.Variant != "" { 235 | p.Variant = config.Variant 236 | } 237 | 238 | if err := verifyPlatform(p, bp.target); err != nil { 239 | return err 240 | } 241 | logf := withPrefix(logf, fmt.Sprintf("%v/%v: ", p.OS, p.Architecture)) 242 | img, err := createImageForBase(bp, logf, baseImage, p) 243 | if err != nil { 244 | return err 245 | } 246 | if !bp.publish { 247 | logf("not pushing") 248 | return nil 249 | } 250 | 251 | img = mutate.Annotations(img, bp.annotations).(v1.Image) // OCI annotations 252 | 253 | for _, r := range bp.imageRefs { 254 | if bp.target == "local" { 255 | if err := loadLocalImage(logf, r, img); err != nil { 256 | return err 257 | } 258 | continue 259 | } 260 | logf("pushing to %v", r) 261 | if err := remote.Write(r, img, remoteOpts...); err != nil { 262 | return err 263 | } 264 | } 265 | return nil 266 | case types.OCIImageIndex, types.DockerManifestList: 267 | // baseRef is a multi-platform index, rest of the method handles this. 268 | default: 269 | return fmt.Errorf("failed to interpret base as index or image: %v", baseDesc.MediaType) 270 | } 271 | baseIndex, err := baseDesc.ImageIndex() 272 | if err != nil { 273 | return err 274 | } 275 | 276 | im, err := baseIndex.IndexManifest() 277 | if err != nil { 278 | return fmt.Errorf("failed to interpret base as index: %w", err) 279 | } 280 | var adds []mutate.IndexAddendum 281 | // Try to build images for all supported platforms. 282 | for _, id := range im.Manifests { 283 | logf := withPrefix(logf, fmt.Sprintf("%v/%v: ", id.Platform.OS, id.Platform.Architecture)) 284 | if id.Platform == nil { 285 | return fmt.Errorf("unknown platform for image: %v", bp.baseImage) 286 | } 287 | if err := verifyPlatform(*id.Platform, bp.target); err != nil { 288 | logf("skipping: %v", err) 289 | continue 290 | } 291 | logf("base digest: %v", id.Digest) 292 | bi, err := baseIndex.Image(id.Digest) 293 | if err != nil { 294 | return err 295 | } 296 | logf("building") 297 | img, err := createImageForBase(bp, logf, bi, *id.Platform) 298 | if err != nil { 299 | return err 300 | } 301 | 302 | // Ensure that any provided OCI annotations are added to each OCI image manifest. 303 | img = mutate.Annotations(img, bp.annotations).(v1.Image) 304 | 305 | if args := flag.Args(); len(args) > 0 { 306 | img, err = mutate.Config(img, v1.Config{ 307 | Cmd: args, 308 | }) 309 | if err != nil { 310 | return err 311 | } 312 | } 313 | d, err := img.Digest() 314 | if err != nil { 315 | return err 316 | } 317 | logf("new digest: %v", d) 318 | adds = append(adds, mutate.IndexAddendum{ 319 | Add: img, 320 | Descriptor: v1.Descriptor{ 321 | MediaType: id.MediaType, 322 | URLs: id.URLs, 323 | Platform: id.Platform, 324 | }, 325 | }) 326 | } 327 | switch len(adds) { 328 | case 0: 329 | logf("no images") 330 | return nil 331 | case 1: 332 | // Don't use a manifest for a single image. 333 | img := adds[0].Add.(v1.Image) 334 | d, err := img.Digest() 335 | if err != nil { 336 | return err 337 | } 338 | logf("image digest: %v", d) 339 | if !bp.publish { 340 | logf("not pushing") 341 | return nil 342 | } 343 | 344 | for _, r := range bp.imageRefs { 345 | if bp.target == "local" { 346 | if err := loadLocalImage(logf, r, img); err != nil { 347 | return err 348 | } 349 | continue 350 | } 351 | logf("pushing to %v", r) 352 | if err := remote.Write(r, img, remoteOpts...); err != nil { 353 | return err 354 | } 355 | } 356 | return nil 357 | } 358 | if bp.target == "local" { 359 | return fmt.Errorf("cannot build multi-platform images for local target") 360 | } 361 | // Generate a new 'fat manifest' with all the platform images. If we are 362 | // at this point the base was either a Dokcer manifest list or an OCI 363 | // image index- make sure the new manifest of that type. 364 | idx := mutate.AppendManifests(mutate.IndexMediaType(empty.Index, baseDesc.MediaType), adds...) 365 | d, err := idx.Digest() 366 | if err != nil { 367 | return err 368 | } 369 | 370 | // Add any provided OCI annotations to the image index. 371 | idx = mutate.Annotations(idx, bp.annotations).(v1.ImageIndex) 372 | 373 | logf("index digest: %v", d) 374 | if !bp.publish { 375 | logf("not pushing") 376 | return nil 377 | } 378 | 379 | for _, r := range bp.imageRefs { 380 | logf("pushing to %v", r) 381 | if err := remote.WriteIndex(r, idx, remoteOpts...); err != nil { 382 | return err 383 | } 384 | } 385 | 386 | return nil 387 | } 388 | 389 | func goarm(platform v1.Platform) (string, error) { 390 | if platform.Architecture != "arm" { 391 | return "", fmt.Errorf("not arm: %v", platform.Architecture) 392 | } 393 | v := platform.Variant 394 | if len(v) != 2 { 395 | return "", fmt.Errorf("unexpected varient: %v", v) 396 | } 397 | if v[0] != 'v' || !('0' <= v[1] && v[1] <= '9') { 398 | return "", fmt.Errorf("unexpected varient: %v", v) 399 | } 400 | return string(v[1]), nil 401 | } 402 | 403 | func createImageForBase(bp *buildParams, logf logf, base v1.Image, platform v1.Platform) (v1.Image, error) { 404 | tmpDir, err := os.MkdirTemp("", "mkctr") 405 | if err != nil { 406 | return nil, err 407 | } 408 | defer os.RemoveAll(tmpDir) 409 | 410 | env := append(os.Environ(), 411 | "CGO_ENABLED=0", 412 | "GOOS="+platform.OS, 413 | "GOARCH="+platform.Architecture, 414 | ) 415 | if platform.Architecture == "arm" { 416 | v, err := goarm(platform) 417 | if err != nil { 418 | return nil, err 419 | } 420 | env = append(env, "GOARM="+v) 421 | } 422 | 423 | files := map[string]string{} 424 | for src, dst := range bp.staticFiles { 425 | files[src] = dst 426 | } 427 | 428 | // Compile all the goPaths 429 | for gp, dst := range bp.goPaths { 430 | logf("compiling %v", gp) 431 | n, err := compileGoBinary(gp, tmpDir, env, bp.ldflags, bp.gotags, bp.verbose) 432 | if err != nil { 433 | return nil, err 434 | } 435 | logf("output %v -> %v", gp, n) 436 | files[n] = dst 437 | } 438 | // Determine media type of the base image. 439 | var layerMediaType types.MediaType 440 | mt, err := base.MediaType() 441 | if err != nil { 442 | return nil, fmt.Errorf("error determining base image media type: %w", err) 443 | } 444 | switch mt { 445 | case types.OCIManifestSchema1: 446 | layerMediaType = types.OCILayer 447 | case types.DockerManifestSchema2: 448 | layerMediaType = types.DockerLayer 449 | default: 450 | return nil, fmt.Errorf("unknown base image media type %v, accepted types are OCI image manifest v1 (%s) and Docker image manifest v2 (%s)", mt, types.OCIManifestSchema1, types.DockerManifestSchema2) 451 | } 452 | layer, err := layerFromFiles(logf, files, layerMediaType) 453 | if err != nil { 454 | return nil, err 455 | } 456 | return mutate.AppendLayers(base, layer) 457 | } 458 | 459 | func compileGoBinary(what, where string, env []string, ldflags, gotags string, verbose bool) (string, error) { 460 | f, err := os.CreateTemp(where, "out") 461 | if err != nil { 462 | return "", err 463 | } 464 | out := f.Name() 465 | if err := f.Close(); err != nil { 466 | return "", err 467 | } 468 | args := []string{ 469 | "build", 470 | "-trimpath", 471 | } 472 | if verbose { 473 | args = append(args, "-v") 474 | } 475 | if len(gotags) > 0 { 476 | args = append(args, "--tags="+gotags) 477 | } 478 | if len(ldflags) > 0 { 479 | args = append(args, "--ldflags="+ldflags) 480 | } 481 | args = append(args, 482 | "-o="+out, 483 | what, 484 | ) 485 | cmd := exec.Command("go", args...) 486 | cmd.Env = env 487 | cmd.Stdout = os.Stdout 488 | cmd.Stderr = os.Stderr 489 | if err := cmd.Run(); err != nil { 490 | return "", err 491 | } 492 | return out, nil 493 | } 494 | 495 | func layerFromFiles(logf logf, files map[string]string, layerMediaType types.MediaType) (v1.Layer, error) { 496 | buf := bytes.NewBuffer(nil) 497 | tw := tar.NewWriter(buf) 498 | defer tw.Close() 499 | 500 | dirs := make(map[string]bool) 501 | writeDir := func(dir string) error { 502 | if dirs[dir] { 503 | return nil 504 | } 505 | logf("creating dir %v", dir) 506 | if err := tw.WriteHeader(&tar.Header{ 507 | Name: dir, 508 | Typeflag: tar.TypeDir, 509 | Mode: 0555, 510 | // Set time to 0 to make the images reproducible. 511 | ModTime: time.Time{}, 512 | }); err != nil { 513 | return err 514 | } 515 | dirs[dir] = true 516 | return nil 517 | } 518 | for src, dst := range files { 519 | err := filepath.WalkDir(src, func(srcWalk string, d fs.DirEntry, err error) error { 520 | path := strings.TrimPrefix(srcWalk, src) 521 | dstWalk := filepath.Join(dst, path) 522 | writeDir(filepath.Dir(dstWalk)) 523 | if d.IsDir() { 524 | return writeDir(dstWalk) 525 | } 526 | logf("copying %v -> %v", srcWalk, dstWalk) 527 | return tarFile(tw, srcWalk, dstWalk) 528 | }) 529 | if err != nil { 530 | return nil, err 531 | } 532 | } 533 | if err := tw.Close(); err != nil { 534 | return nil, err 535 | } 536 | 537 | binaryLayerBytes := buf.Bytes() 538 | // An alternative to using tarball.LayerFromOpener would be to use 539 | // stream.NewLayer 540 | // https://pkg.go.dev/github.com/google/go-containerregistry@v0.17.0/pkg/v1/stream#NewLayer. 541 | // This would, however, require us to restructure the code to write each 542 | // layer to the upstream repository immediately after producing it. At 543 | // this point we (irbekrm) are not sure if there would be any benefits 544 | // to switching to stream.NewLayer. 545 | // https://github.com/google/go-containerregistry/tree/main/pkg/v1/stream#caveats 546 | return tarball.LayerFromOpener(func() (io.ReadCloser, error) { 547 | return io.NopCloser(bytes.NewBuffer(binaryLayerBytes)), nil 548 | }, tarball.WithCompressedCaching, tarball.WithMediaType(layerMediaType)) 549 | } 550 | 551 | func tarFile(tw *tar.Writer, src, dst string) error { 552 | file, err := os.Open(src) 553 | if err != nil { 554 | return err 555 | } 556 | defer file.Close() 557 | stat, err := file.Stat() 558 | if err != nil { 559 | return err 560 | } 561 | if err := tw.WriteHeader(&tar.Header{ 562 | Name: dst, 563 | Size: stat.Size(), 564 | Typeflag: tar.TypeReg, 565 | Mode: 0555, 566 | // Set time to 0 to make the images reproducible. 567 | ModTime: time.Time{}, 568 | }); err != nil { 569 | return err 570 | } 571 | if _, err := io.Copy(tw, file); err != nil { 572 | return err 573 | } 574 | return nil 575 | } 576 | 577 | func loadLocalImage(logf logf, tag name.Tag, img v1.Image) error { 578 | if _, err := daemon.Write(tag, img); err == nil { 579 | return nil 580 | } 581 | 582 | // Assume we failed because the docker daemon API is not available, try a 583 | // CLI option instead. 584 | var bin string 585 | if p, err := exec.LookPath("docker"); err == nil { 586 | bin = p 587 | } else if p, err = exec.LookPath("podman"); err == nil { 588 | bin = p 589 | } else if p, err = exec.LookPath("nerdctl"); err == nil { 590 | bin = p 591 | } else { 592 | return errors.New("no suitable docker CLI-compatible binary found") 593 | } 594 | 595 | cmd := exec.Command(bin, "image", "load") 596 | imgReader, imgWriter := io.Pipe() 597 | defer imgReader.Close() 598 | go func() { 599 | defer imgWriter.Close() 600 | tarball.Write(tag, img, imgWriter) 601 | }() 602 | cmd.Stdin = imgReader 603 | logf("running command: %s", cmd.String()) 604 | out, err := cmd.CombinedOutput() 605 | logf("output: %s", string(out)) 606 | if err != nil { 607 | return err 608 | } 609 | 610 | return nil 611 | } 612 | 613 | // parseAnnotations accepts a string with comma separated key=value pairs of annotations i.e key1=val1,key2=val2 and 614 | // returns them as a parsed map. 615 | func parseAnnotations(s string) map[string]string { 616 | ss := strings.Split(s, ",") 617 | annotations := make(map[string]string) 618 | for _, annot := range ss { 619 | kv := strings.SplitN(annot, "=", 2) 620 | if len(kv) != 2 { 621 | continue 622 | } 623 | annotations[kv[0]] = kv[1] 624 | } 625 | return annotations 626 | } 627 | --------------------------------------------------------------------------------