feat: Implement using of buildx for docker_image (#717)

* feat: Implement using of buildx for docker_image

* fix: secrets in docker_image

* fix: fmt issues

* test: Add test for build_log_file

* chore: Disable flaky test and update goreleaser to exclude not possible build

* feat: Small refactoring and documentation improvements
This commit is contained in:
Martin 2025-05-06 22:21:09 +02:00 committed by GitHub
parent 46a6a72595
commit b9181a75e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1492 additions and 610 deletions

View file

@ -24,6 +24,8 @@ builds:
ignore:
- goos: darwin
goarch: "386"
- goos: openbsd
goarch: arm
binary: "{{ .ProjectName }}_v{{ .Version }}"
archives:
- format: zip

View file

@ -43,7 +43,7 @@ resource "docker_image" "ubuntu" {
### Build
You can also use the resource to build an image.
You can also use the resource to build an image. By default the build block is using the old legacy docker build. In order to use a buildx builder, please read the section below
-> **Note**: The default timeout for the building is 20 minutes. If you need to increase this, you can use [operation timeouts](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts).
@ -81,6 +81,14 @@ resource "docker_image" "zoo" {
}
```
### Buildx
-> **Note**: The buildx feature is currently in preview and may have some quirks. Known issues: Setting `ulimits` will not work.
The `build` argument uses the legacy docker builder. If you want to use a buildx builder, you need to set the `builder` argument. For the default buildx builder, you can set the `builder` argument to `default`. For a custom buildx builder, you can set the `builder` argument to the name of the builder. You can find the name of the builder by running `docker buildx ls`.
The single platform build result is automatically loaded to `docker images`.
<!-- schema generated by tfplugindocs -->
## Schema
@ -116,6 +124,8 @@ Optional:
- `auth_config` (Block List) The configuration for the authentication (see [below for nested schema](#nestedblock--build--auth_config))
- `build_args` (Map of String) Pairs for build-time variables in the form of `ENDPOINT : "https://example.com"`
- `build_id` (String) BuildID is an optional identifier that can be passed together with the build request. The same identifier can be used to gracefully cancel the build with the cancel request.
- `build_log_file` (String) Path to a file where the buildx log are written to. Only available when `builder` is set. If not set, no logs are available. The path is taken as is, so make sure to use a path that is available.
- `builder` (String) Set the name of the buildx builder to use. If not set or empty, the legacy builder will be used.
- `cache_from` (List of String) Images to consider as cache sources
- `cgroup_parent` (String) Optional parent cgroup for the container
- `cpu_period` (Number) The length of a CPU period in microseconds
@ -135,9 +145,9 @@ Optional:
- `no_cache` (Boolean) Do not use the cache when building the image
- `platform` (String) Set platform if server is multi-platform capable
- `pull_parent` (Boolean) Attempt to pull the image even if an older image exists locally
- `remote_context` (String) A Git repository URI or HTTP/HTTPS context URI
- `remote_context` (String) A Git repository URI or HTTP/HTTPS context URI. Will be ignored if `builder` is set.
- `remove` (Boolean) Remove intermediate containers after a successful build. Defaults to `true`.
- `secrets` (Block List) Set build-time secrets (see [below for nested schema](#nestedblock--build--secrets))
- `secrets` (Block List) Set build-time secrets. Only available when you use a buildx builder. (see [below for nested schema](#nestedblock--build--secrets))
- `security_opt` (List of String) The security options
- `session_id` (String) Set an ID for the build session
- `shm_size` (Number) Size of /dev/shm in bytes. The size must be greater than 0
@ -176,7 +186,7 @@ Required:
Optional:
- `env` (String) Environment variable source of the secret
- `src` (String) File source of the secret
- `src` (String) File source of the secret. Takes precedence over `env`
<a id="nestedblock--build--ulimit"></a>

155
go.mod
View file

@ -6,19 +6,25 @@ toolchain go1.24.2
require (
github.com/client9/misspell v0.3.4
github.com/containerd/console v1.0.4
github.com/distribution/reference v0.6.0
github.com/docker/buildx v0.23.0
github.com/docker/cli v28.0.4+incompatible
github.com/docker/distribution v2.8.1+incompatible
github.com/docker/docker v28.0.4+incompatible
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0
github.com/golangci/golangci-lint v1.64.8
github.com/hashicorp/go-cty v1.5.0
github.com/hashicorp/terraform-plugin-docs v0.14.1
github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0
github.com/katbyte/terrafmt v0.5.4
github.com/mitchellh/go-homedir v1.1.0
github.com/moby/buildkit v0.10.6
github.com/moby/buildkit v0.21.0
github.com/morikuni/aec v1.0.0
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.3
google.golang.org/protobuf v1.36.5
)
require (
@ -26,10 +32,11 @@ require (
4d63.com/gochecknoglobals v0.2.2 // indirect
github.com/4meepo/tagalign v1.4.2 // indirect
github.com/Abirdcfly/dupword v0.1.3 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
github.com/Antonboom/errname v1.0.0 // indirect
github.com/Antonboom/nilnil v1.0.1 // indirect
github.com/Antonboom/testifylint v1.5.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
github.com/Crocmagnon/fatcontext v0.7.1 // indirect
github.com/Djarvur/go-err113 v0.1.0 // indirect
@ -37,7 +44,7 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
github.com/ProtonMail/go-crypto v1.1.3 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
@ -50,6 +57,19 @@ require (
github.com/armon/go-radix v1.0.0 // indirect
github.com/ashanbrown/forbidigo v1.6.0 // indirect
github.com/ashanbrown/makezero v1.2.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/bkielbasa/cyclop v1.2.3 // indirect
@ -61,31 +81,46 @@ require (
github.com/butuzov/mirror v1.3.0 // indirect
github.com/catenacyber/perfsprint v0.8.2 // indirect
github.com/ccojocar/zxcvbn-go v1.0.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charithe/durationcheck v0.0.10 // indirect
github.com/chavacava/garif v0.1.0 // indirect
github.com/ckaznocha/intrange v0.3.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/containerd/containerd v1.6.3-0.20220401172941-5ff8fce1fcc6 // indirect
github.com/containerd/containerd/api v1.8.0 // indirect
github.com/containerd/containerd/v2 v2.0.4 // indirect
github.com/containerd/continuity v0.4.5 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/typeurl v1.0.2 // indirect
github.com/containerd/platforms v1.0.0-rc.1 // indirect
github.com/containerd/ttrpc v1.2.7 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/curioswitch/go-reassign v0.3.0 // indirect
github.com/daixiang0/gci v0.13.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/denis-tingaikin/go-header v0.5.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/ettle/strcase v0.2.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/firefart/nonamedreturns v1.0.5 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/ghostiam/protogetter v0.3.9 // indirect
github.com/go-critic/go-critic v0.12.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.2.0 // indirect
@ -106,15 +141,20 @@ require (
github.com/golangci/plugin-module-register v0.1.1 // indirect
github.com/golangci/revgrep v0.8.0 // indirect
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/gordonklaus/ineffassign v0.1.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
github.com/gostaticanalysis/comment v1.5.0 // indirect
github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
@ -133,24 +173,26 @@ require (
github.com/hashicorp/terraform-exec v0.22.0 // indirect
github.com/hashicorp/terraform-json v0.24.0 // indirect
github.com/hashicorp/terraform-plugin-go v0.26.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.4 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/in-toto/in-toto-golang v0.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jgautheron/goconst v1.7.1 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jjti/go-spancheck v0.6.4 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/julz/importas v0.2.0 // indirect
github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect
github.com/katbyte/andreyvit-diff v0.0.1 // indirect
github.com/katbyte/sergi-go-diff v1.1.1 // indirect
github.com/kisielk/errcheck v1.9.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
github.com/klauspost/compress v1.15.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/kulti/thelper v0.6.3 // indirect
github.com/kunwardeep/paralleltest v1.0.10 // indirect
github.com/lasiar/canonicalheader v1.1.2 // indirect
@ -162,29 +204,37 @@ require (
github.com/leonklingele/grouper v1.1.2 // indirect
github.com/macabu/inamedparam v0.1.3 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/maratori/testableexamples v1.0.0 // indirect
github.com/maratori/testpackage v1.1.1 // indirect
github.com/matoous/godox v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mgechev/revive v1.7.0 // indirect
github.com/mitchellh/cli v1.1.5 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/spdystream v0.4.0 // indirect
github.com/moby/sys/mountinfo v0.7.2 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/symlink v0.2.0 // indirect
github.com/moby/sys/signal v0.7.1 // indirect
github.com/moby/sys/symlink v0.3.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/moricho/tparallel v0.3.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/nakabonne/nestif v0.3.1 // indirect
github.com/nishanths/exhaustive v0.12.0 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect
@ -192,16 +242,17 @@ require (
github.com/oklog/run v1.0.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/polyfloyd/go-errorlint v1.7.1 // indirect
github.com/posener/complete v1.2.3 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect
github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect
@ -217,9 +268,11 @@ require (
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
github.com/securego/gosec/v2 v2.22.2 // indirect
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sivchari/tenv v1.12.1 // indirect
github.com/sonatard/noctx v0.1.0 // indirect
@ -241,7 +294,11 @@ require (
github.com/timonwong/loggercheck v0.10.1 // indirect
github.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect
github.com/tonistiigi/fsutil v0.0.0-20250410151801-5b74a7ad7583 // indirect
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
github.com/ultraware/funlen v0.2.0 // indirect
github.com/ultraware/whitespace v0.2.0 // indirect
github.com/uudashr/gocognit v1.2.0 // indirect
@ -249,6 +306,7 @@ require (
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xen0n/gosmopolitan v1.2.2 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/yagipy/maintidx v1.0.0 // indirect
@ -260,32 +318,51 @@ require (
go-simpler.org/sloglint v0.9.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/goleak v1.1.12 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.10.0 // indirect
golang.org/x/tools v0.31.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/oauth2 v0.26.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.32.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
google.golang.org/grpc v1.71.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.4.0 // indirect
honnef.co/go/tools v0.6.1 // indirect
k8s.io/api v0.31.2 // indirect
k8s.io/apimachinery v0.31.2 // indirect
k8s.io/client-go v0.31.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
mvdan.cc/gofumpt v0.7.0 // indirect
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

726
go.sum

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,898 @@
package provider
// taken from https://github.com/docker/buildx/blob/master/commands/build.go and heavily modified
// to fit the needs of the provider
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"github.com/containerd/console"
"github.com/docker/buildx/build"
"github.com/docker/buildx/builder"
"github.com/docker/buildx/controller"
cbuild "github.com/docker/buildx/controller/build"
"github.com/docker/buildx/controller/control"
controllererrors "github.com/docker/buildx/controller/errdefs"
controllerapi "github.com/docker/buildx/controller/pb"
"github.com/docker/buildx/monitor"
"github.com/docker/buildx/util/buildflags"
"github.com/docker/buildx/util/confutil"
"github.com/docker/buildx/util/desktop"
"github.com/docker/buildx/util/ioset"
"github.com/docker/buildx/util/progress"
"github.com/docker/cli/cli/command"
dockeropts "github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/versions"
dockerclient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/atomicwriter"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/frontend/subrequests"
"github.com/moby/buildkit/frontend/subrequests/lint"
"github.com/moby/buildkit/frontend/subrequests/outline"
"github.com/moby/buildkit/frontend/subrequests/targets"
"github.com/moby/buildkit/solver/errdefs"
solverpb "github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/progress/progressui"
"github.com/morikuni/aec"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"google.golang.org/protobuf/proto"
// import drivers otherwise factories are empty
// for --driver output flag usage
_ "github.com/docker/buildx/driver/docker"
_ "github.com/docker/buildx/driver/docker-container"
_ "github.com/docker/buildx/driver/kubernetes"
_ "github.com/docker/buildx/driver/remote"
)
type buildOptions struct {
allow []string
annotations []string
buildArgs []string
cacheFrom []string
cacheTo []string
cgroupParent string
contextPath string
contexts []string
dockerfileName string
extraHosts []string
imageIDFile string
labels []string
networkMode string
noCacheFilter []string
outputs []string
platforms []string
callFunc string
secrets []string
shmSize dockeropts.MemBytes
ssh []string
tags []string
target string
ulimits *dockeropts.UlimitOpt
attests []string
sbom string
provenance string
quiet bool
builder string
metadataFile string
noCache bool
pull bool
exportPush bool
exportLoad bool
control.ControlOptions
invokeConfig *invokeConfig
}
func (o *buildOptions) toControllerOptions() (*controllerapi.BuildOptions, error) {
var err error
buildArgs, err := listToMap(o.buildArgs, true)
if err != nil {
return nil, err
}
labels, err := listToMap(o.labels, false)
if err != nil {
return nil, err
}
opts := controllerapi.BuildOptions{
Allow: o.allow,
Annotations: o.annotations,
BuildArgs: buildArgs,
CgroupParent: o.cgroupParent,
ContextPath: o.contextPath,
DockerfileName: o.dockerfileName,
ExtraHosts: o.extraHosts,
Labels: labels,
NetworkMode: o.networkMode,
NoCacheFilter: o.noCacheFilter,
Platforms: o.platforms,
ShmSize: int64(o.shmSize),
Tags: o.tags,
Target: o.target,
Ulimits: dockerUlimitToControllerUlimit(o.ulimits),
Builder: o.builder,
NoCache: o.noCache,
Pull: o.pull,
ExportPush: o.exportPush,
ExportLoad: o.exportLoad,
}
// TODO: extract env var parsing to a method easily usable by library consumers
if v := os.Getenv("SOURCE_DATE_EPOCH"); v != "" {
if _, ok := opts.BuildArgs["SOURCE_DATE_EPOCH"]; !ok {
opts.BuildArgs["SOURCE_DATE_EPOCH"] = v
}
}
opts.SourcePolicy, err = build.ReadSourcePolicy()
if err != nil {
return nil, err
}
inAttests := slices.Clone(o.attests)
if o.provenance != "" {
inAttests = append(inAttests, buildflags.CanonicalizeAttest("provenance", o.provenance))
}
if o.sbom != "" {
inAttests = append(inAttests, buildflags.CanonicalizeAttest("sbom", o.sbom))
}
opts.Attests, err = buildflags.ParseAttests(inAttests)
if err != nil {
return nil, err
}
opts.NamedContexts, err = buildflags.ParseContextNames(o.contexts)
if err != nil {
return nil, err
}
opts.Exports, err = buildflags.ParseExports(o.outputs)
if err != nil {
return nil, err
}
for _, e := range opts.Exports {
if (e.Type == client.ExporterLocal || e.Type == client.ExporterTar) && o.imageIDFile != "" {
return nil, errors.Errorf("local and tar exporters are incompatible with image ID file")
}
}
cacheFrom, err := buildflags.ParseCacheEntry(o.cacheFrom)
if err != nil {
return nil, err
}
opts.CacheFrom = cacheFrom.ToPB()
cacheTo, err := buildflags.ParseCacheEntry(o.cacheTo)
if err != nil {
return nil, err
}
opts.CacheTo = cacheTo.ToPB()
opts.Secrets, err = buildflags.ParseSecretSpecs(o.secrets)
if err != nil {
return nil, err
}
opts.SSH, err = buildflags.ParseSSHSpecs(o.ssh)
if err != nil {
return nil, err
}
opts.CallFunc, err = buildflags.ParseCallFunc(o.callFunc)
if err != nil {
return nil, err
}
prm := confutil.MetadataProvenance()
if opts.CallFunc != nil || len(o.metadataFile) == 0 {
prm = confutil.MetadataProvenanceModeDisabled
}
opts.ProvenanceResponseMode = string(prm)
return &opts, nil
}
func mapBuildAttributesToBuildOptions(buildAttributes map[string]interface{}, imageName string) (buildOptions, error) {
options := buildOptions{}
if dockerfile, ok := buildAttributes["dockerfile"].(string); ok {
options.dockerfileName = dockerfile
}
options.contextPath = buildAttributes["context"].(string)
options.exportLoad = true
options.tags = append(options.tags, imageName)
for _, t := range buildAttributes["tag"].([]interface{}) {
options.tags = append(options.tags, t.(string))
}
if builder, ok := buildAttributes["builder"].(string); ok {
options.builder = builder
}
if remove, ok := buildAttributes["remove"].(bool); ok {
options.noCache = !remove
}
if secrets, ok := buildAttributes["secrets"].([]interface{}); ok {
for _, secret := range secrets {
if secretMap, ok := secret.(map[string]interface{}); ok {
// Construct the secret string in the format [type=env,]id=<ID>[,env=<VARIABLE>]
secretStr := ""
id, _ := secretMap["id"].(string)
if env, ok := secretMap["env"].(string); ok && env != "" {
secretStr += fmt.Sprintf("type=env,id=%s,env=%s", id, env)
}
if src, ok := secretMap["src"].(string); ok && src != "" {
secretStr += fmt.Sprintf("type=file,id=%s,src=%s", id, src)
}
options.secrets = append(options.secrets, secretStr)
}
}
}
if labels, ok := buildAttributes["label"].(map[string]interface{}); ok {
for key, value := range labels {
if valueStr, ok := value.(string); ok {
options.labels = append(options.labels, fmt.Sprintf("%s=%s", key, valueStr))
}
}
}
if suppressOutput, ok := buildAttributes["suppress_output"].(bool); ok {
options.quiet = suppressOutput
}
if noCache, ok := buildAttributes["no_cache"].(bool); ok {
options.noCache = noCache
}
if pullParent, ok := buildAttributes["pull_parent"].(bool); ok {
options.pull = pullParent
}
if isolation, ok := buildAttributes["isolation"].(string); ok {
options.networkMode = isolation
}
if cpuSetCpus, ok := buildAttributes["cpu_set_cpus"].(string); ok {
options.buildArgs = append(options.buildArgs, fmt.Sprintf("cpusetcpus=%s", cpuSetCpus))
}
if cpuSetMems, ok := buildAttributes["cpu_set_mems"].(string); ok {
options.buildArgs = append(options.buildArgs, fmt.Sprintf("cpusetmems=%s", cpuSetMems))
}
if cpuShares, ok := buildAttributes["cpu_shares"].(int); ok {
options.buildArgs = append(options.buildArgs, fmt.Sprintf("cpushares=%d", cpuShares))
}
if cpuQuota, ok := buildAttributes["cpu_quota"].(int); ok {
options.buildArgs = append(options.buildArgs, fmt.Sprintf("cpuquota=%d", cpuQuota))
}
if cpuPeriod, ok := buildAttributes["cpu_period"].(int); ok {
options.buildArgs = append(options.buildArgs, fmt.Sprintf("cpuperiod=%d", cpuPeriod))
}
if memory, ok := buildAttributes["memory"].(int); ok {
options.buildArgs = append(options.buildArgs, fmt.Sprintf("memory=%d", memory))
}
if memorySwap, ok := buildAttributes["memory_swap"].(int); ok {
options.buildArgs = append(options.buildArgs, fmt.Sprintf("memoryswap=%d", memorySwap))
}
if cgroupParent, ok := buildAttributes["cgroup_parent"].(string); ok {
options.cgroupParent = cgroupParent
}
if networkMode, ok := buildAttributes["network_mode"].(string); ok {
options.networkMode = networkMode
}
if shmSize, ok := buildAttributes["shm_size"].(int); ok {
options.shmSize = dockeropts.MemBytes(shmSize)
}
options.ulimits = dockeropts.NewUlimitOpt(nil)
if ulimits, ok := buildAttributes["ulimit"].([]interface{}); ok {
ulimitOpt := &dockeropts.UlimitOpt{}
for _, ulimit := range ulimits {
if ulimitMap, ok := ulimit.(map[string]interface{}); ok {
name, _ := ulimitMap["name"].(string)
hard, _ := ulimitMap["hard"].(int)
soft, _ := ulimitMap["soft"].(int)
ulimitOpt.Set(fmt.Sprintf("%s=%d:%d", name, soft, hard)) // nolint:errcheck
}
}
options.ulimits = ulimitOpt
}
if buildArgs, ok := buildAttributes["build_args"].(map[string]interface{}); ok {
for key, value := range buildArgs {
if valueStr, ok := value.(string); ok {
options.buildArgs = append(options.buildArgs, fmt.Sprintf("%s=%s", key, valueStr))
}
}
}
if extraHosts, ok := buildAttributes["extra_hosts"].([]interface{}); ok {
for _, host := range extraHosts {
if hostStr, ok := host.(string); ok {
options.extraHosts = append(options.extraHosts, hostStr)
}
}
}
if target, ok := buildAttributes["target"].(string); ok {
options.target = target
}
if platform, ok := buildAttributes["platform"].(string); ok {
options.platforms = append(options.platforms, platform)
}
return options, nil
}
func canUseBuildx(ctx context.Context, client *dockerclient.Client) (bool, error) {
var buildKitDisabled, useBuilder bool
// check DOCKER_BUILDKIT env var is not empty
// if it is assume we want to use the builder component
if v := os.Getenv("DOCKER_BUILDKIT"); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false, fmt.Errorf("DOCKER_BUILDKIT environment variable expects boolean value: %w", err)
}
if !enabled {
buildKitDisabled = true
} else {
useBuilder = true
}
}
si, _ := client.Ping(ctx)
if !useBuilder {
if si.BuilderVersion != types.BuilderBuildKit && si.OSType == "windows" {
// The daemon didn't advertise BuildKit as the preferred builder,
// so use the legacy builder, which is still the default for
// Windows / WCOW.
return false, nil
}
}
if buildKitDisabled {
// When using a Linux daemon, print a warning that the legacy builder
// is deprecated. For Windows / WCOW, BuildKit is still experimental,
// so we don't print this warning, even if the daemon advertised that
// it supports BuildKit.
if si.OSType != "windows" {
tflog.Warn(ctx, `DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
BuildKit is currently disabled; enable it by removing the DOCKER_BUILDKIT=0
environment-variable.`)
}
return false, nil
}
return true, nil
}
func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions, buildLogFile string) (err error) {
if buildLogFile == "" {
buildLogFile = os.DevNull
}
logFile, err := os.OpenFile(buildLogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open build log file: %w", err)
}
defer logFile.Close() // nolint:errcheck
logger := log.New(logFile, "", log.LstdFlags)
opts, err := options.toControllerOptions()
if err != nil {
return err
}
// Avoid leaving a stale file if we eventually fail
if options.imageIDFile != "" {
if err := os.Remove(options.imageIDFile); err != nil && !os.IsNotExist(err) {
return errors.Wrap(err, "removing image ID file")
}
}
contextPathHash := options.contextPath
if absContextPath, err := filepath.Abs(contextPathHash); err == nil {
contextPathHash = absContextPath
}
b, err := builder.New(dockerCli,
builder.WithName(options.builder),
builder.WithContextPathHash(contextPathHash),
)
if err != nil {
return err
}
_, err = b.LoadNodes(ctx)
if err != nil {
return err
}
var term bool
if _, err := console.ConsoleFromFile(logFile); err == nil {
term = true
}
ctx2, cancel := context.WithCancelCause(context.TODO())
defer func() { cancel(errors.WithStack(context.Canceled)) }()
progressMode := progressui.PlainMode
if err != nil {
return err
}
var printer *progress.Printer
printer, err = progress.NewPrinter(ctx2, logFile, progressMode,
progress.WithDesc(
fmt.Sprintf("building with %q instance using %s driver", b.Name, b.Driver),
fmt.Sprintf("%s:%s", b.Driver, b.Name),
),
progress.WithOnClose(func() {
printWarnings(logFile, printer.Warnings(), progressMode)
}),
)
if err != nil {
logger.Printf("error creating progress printer: %v", err)
return err
}
var resp *client.SolveResponse
var inputs *build.Inputs
var retErr error
if confutil.IsExperimental() {
resp, inputs, retErr = runControllerBuild(ctx, dockerCli, opts, options, printer)
} else {
resp, inputs, retErr = runBasicBuild(ctx, dockerCli, opts, printer)
}
if err := printer.Wait(); retErr == nil {
retErr = err
}
if retErr != nil {
return retErr
}
desktop.PrintBuildDetails(logFile, printer.BuildRefs(), term)
if options.imageIDFile != "" {
if err := os.WriteFile(options.imageIDFile, []byte(getImageID(resp.ExporterResponse)), 0644); err != nil {
return errors.Wrap(err, "writing image ID file")
}
}
if options.metadataFile != "" {
dt := decodeExporterResponse(resp.ExporterResponse)
if opts.CallFunc == nil {
if warnings := printer.Warnings(); len(warnings) > 0 && confutil.MetadataWarningsEnabled() {
dt["buildx.build.warnings"] = warnings
}
}
if err := writeMetadataFile(options.metadataFile, dt); err != nil {
return err
}
}
if opts.CallFunc != nil {
if exitcode, err := printResult(dockerCli.Out(), opts.CallFunc, resp.ExporterResponse, options.target, inputs); err != nil {
return err
} else if exitcode != 0 {
os.Exit(exitcode)
}
}
if v, ok := resp.ExporterResponse["frontend.result.inlinemessage"]; ok {
fmt.Fprintf(dockerCli.Out(), "\n%s\n", v) // nolint:errcheck
return nil
}
return nil
}
func writeMetadataFile(filename string, dt any) error {
b, err := json.MarshalIndent(dt, "", " ")
if err != nil {
return err
}
return atomicwriter.WriteFile(filename, b, 0644)
}
// getImageID returns the image ID - the digest of the image config
func getImageID(resp map[string]string) string {
dgst := resp[exptypes.ExporterImageDigestKey]
if v, ok := resp[exptypes.ExporterImageConfigDigestKey]; ok {
dgst = v
}
return dgst
}
func runBasicBuild(ctx context.Context, dockerCli command.Cli, opts *controllerapi.BuildOptions, printer *progress.Printer) (*client.SolveResponse, *build.Inputs, error) {
resp, res, dfmap, err := cbuild.RunBuild(ctx, dockerCli, opts, dockerCli.In(), printer, false)
if res != nil {
res.Done()
}
return resp, dfmap, err
}
func runControllerBuild(ctx context.Context, dockerCli command.Cli, opts *controllerapi.BuildOptions, options buildOptions, printer *progress.Printer) (*client.SolveResponse, *build.Inputs, error) {
if options.invokeConfig != nil && (options.dockerfileName == "-" || options.contextPath == "-") {
// stdin must be usable for monitor
return nil, nil, errors.Errorf("Dockerfile or context from stdin is not supported with invoke")
}
c, err := controller.NewController(ctx, options.ControlOptions, dockerCli, printer)
if err != nil {
return nil, nil, err
}
defer func() {
if err := c.Close(); err != nil {
logrus.Warnf("failed to close server connection %v", err)
}
}()
// NOTE: buildx server has the current working directory different from the client
// so we need to resolve paths to abosolute ones in the client.
opts, err = controllerapi.ResolveOptionPaths(opts)
if err != nil {
return nil, nil, err
}
var ref string
var retErr error
var resp *client.SolveResponse
var inputs *build.Inputs
var f *ioset.SingleForwarder
var pr io.ReadCloser
var pw io.WriteCloser
if options.invokeConfig == nil {
pr = dockerCli.In()
} else {
f = ioset.NewSingleForwarder()
f.SetReader(dockerCli.In())
pr, pw = io.Pipe()
f.SetWriter(pw, func() io.WriteCloser {
pw.Close() // nolint:errcheck
logrus.Debug("propagating stdin close")
return nil
})
}
ref, resp, inputs, err = c.Build(ctx, opts, pr, printer)
if err != nil {
var be *controllererrors.BuildError
if errors.As(err, &be) {
ref = be.SessionID
retErr = err
// We can proceed to monitor
} else {
return nil, nil, errors.Wrapf(err, "failed to build")
}
}
if options.invokeConfig != nil {
if err := pw.Close(); err != nil {
logrus.Debug("failed to close stdin pipe writer")
}
if err := pr.Close(); err != nil {
logrus.Debug("failed to close stdin pipe reader")
}
}
if options.invokeConfig != nil && options.invokeConfig.needsDebug(retErr) {
// Print errors before launching monitor
if err := printError(retErr, printer); err != nil {
logrus.Warnf("failed to print error information: %v", err)
}
pr2, pw2 := io.Pipe()
f.SetWriter(pw2, func() io.WriteCloser {
pw2.Close() // nolint:errcheck
return nil
})
monitorBuildResult, err := options.invokeConfig.runDebug(ctx, ref, opts, c, pr2, os.Stdout, os.Stderr, printer)
if err := pw2.Close(); err != nil {
logrus.Debug("failed to close monitor stdin pipe reader")
}
if err != nil {
logrus.Warnf("failed to run monitor: %v", err)
}
if monitorBuildResult != nil {
// Update return values with the last build result from monitor
resp, retErr = monitorBuildResult.Resp, monitorBuildResult.Err
}
} else {
if err := c.Disconnect(ctx, ref); err != nil {
logrus.Warnf("disconnect error: %v", err)
}
}
return resp, inputs, retErr
}
func printError(err error, printer *progress.Printer) error {
if err == nil {
return nil
}
if err := printer.Pause(); err != nil {
return err
}
defer printer.Unpause()
for _, s := range errdefs.Sources(err) {
s.Print(os.Stderr) // nolint:errcheck
}
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) // nolint:errcheck
return nil
}
func listToMap(values []string, defaultEnv bool) (map[string]string, error) {
result := make(map[string]string, len(values))
for _, value := range values {
k, v, hasValue := strings.Cut(value, "=")
if k == "" {
return nil, errors.Errorf("invalid key-value pair %q: empty key", value)
}
if hasValue {
result[k] = v
} else if defaultEnv {
if envVal, ok := os.LookupEnv(k); ok {
result[k] = envVal
}
} else {
result[k] = ""
}
}
return result, nil
}
type invokeConfig struct {
controllerapi.InvokeConfig
onFlag string
invokeFlag string
}
func (cfg *invokeConfig) needsDebug(retErr error) bool {
switch cfg.onFlag {
case "always":
return true
case "error":
return retErr != nil
default:
return cfg.invokeFlag != ""
}
}
func (cfg *invokeConfig) runDebug(ctx context.Context, ref string, options *controllerapi.BuildOptions, c control.BuildxController, stdin io.ReadCloser, stdout io.WriteCloser, stderr console.File, progress *progress.Printer) (*monitor.MonitorBuildResult, error) {
con := console.Current()
if err := con.SetRaw(); err != nil {
// TODO: run disconnect in build command (on error case)
if err := c.Disconnect(ctx, ref); err != nil {
logrus.Warnf("disconnect error: %v", err)
}
return nil, errors.Errorf("failed to configure terminal: %v", err)
}
defer con.Reset() // nolint:errcheck
return monitor.RunMonitor(ctx, ref, options, &cfg.InvokeConfig, c, stdin, stdout, stderr, progress)
}
func dockerUlimitToControllerUlimit(u *dockeropts.UlimitOpt) *controllerapi.UlimitOpt {
log.Printf("[DEBUG] ulimits: %#v", u)
if u == nil {
return &controllerapi.UlimitOpt{Values: map[string]*controllerapi.Ulimit{}}
}
values := make(map[string]*controllerapi.Ulimit)
// TODO: commenting out the lines below is a workaround for the fact that the dockeropts.UlimitOpt returns a segmentation violation
// when calling GetList() on a nil value. No idea how to fix this, yet
// list := u.GetList()
// if list == nil {
// log.Printf("[WARN] GetList() returned nil")
// return &controllerapi.UlimitOpt{Values: values}
// }
// for _, u := range u.GetList() {
// values[u.Name] = &controllerapi.Ulimit{
// Name: u.Name,
// Hard: u.Hard,
// Soft: u.Soft,
// }
// }
return &controllerapi.UlimitOpt{Values: values}
}
func decodeExporterResponse(exporterResponse map[string]string) map[string]any {
decFunc := func(k, v string) ([]byte, error) {
if k == "result.json" {
// result.json is part of metadata response for subrequests which
// is already a JSON object: https://github.com/moby/buildkit/blob/f6eb72f2f5db07ddab89ac5e2bd3939a6444f4be/frontend/dockerui/requests.go#L100-L102
return []byte(v), nil
}
return base64.StdEncoding.DecodeString(v)
}
out := make(map[string]any)
for k, v := range exporterResponse {
dt, err := decFunc(k, v)
if err != nil {
out[k] = v
continue
}
var raw map[string]any
if err = json.Unmarshal(dt, &raw); err != nil || len(raw) == 0 {
var rawList []map[string]any
if err = json.Unmarshal(dt, &rawList); err != nil || len(rawList) == 0 {
out[k] = v
continue
}
}
out[k] = json.RawMessage(dt)
}
return out
}
func printWarnings(w io.Writer, warnings []client.VertexWarning, mode progressui.DisplayMode) {
if len(warnings) == 0 || mode == progressui.QuietMode || mode == progressui.RawJSONMode {
return
}
fmt.Fprintf(w, "\n ") // nolint:errcheck
sb := &bytes.Buffer{}
if len(warnings) == 1 {
fmt.Fprintf(sb, "1 warning found") // nolint:errcheck
} else {
fmt.Fprintf(sb, "%d warnings found", len(warnings)) // nolint:errcheck
}
if logrus.GetLevel() < logrus.DebugLevel {
fmt.Fprintf(sb, " (use docker --debug to expand)") // nolint:errcheck
}
fmt.Fprintf(sb, ":\n") // nolint:errcheck
fmt.Fprint(w, aec.Apply(sb.String(), aec.YellowF)) // nolint:errcheck
for _, warn := range warnings {
fmt.Fprintf(w, " - %s\n", warn.Short) // nolint:errcheck
if logrus.GetLevel() < logrus.DebugLevel {
continue
}
for _, d := range warn.Detail {
fmt.Fprintf(w, "%s\n", d) // nolint:errcheck
}
if warn.URL != "" {
fmt.Fprintf(w, "More info: %s\n", warn.URL) // nolint:errcheck
}
if warn.SourceInfo != nil && warn.Range != nil {
src := errdefs.Source{
Info: warn.SourceInfo,
Ranges: warn.Range,
}
src.Print(w) // nolint:errcheck
}
fmt.Fprintf(w, "\n") // nolint:errcheck
}
}
func printResult(w io.Writer, f *controllerapi.CallFunc, res map[string]string, target string, inp *build.Inputs) (int, error) {
switch f.Name {
case "outline":
return 0, printValue(w, outline.PrintOutline, outline.SubrequestsOutlineDefinition.Version, f.Format, res)
case "targets":
return 0, printValue(w, targets.PrintTargets, targets.SubrequestsTargetsDefinition.Version, f.Format, res)
case "subrequests.describe":
return 0, printValue(w, subrequests.PrintDescribe, subrequests.SubrequestsDescribeDefinition.Version, f.Format, res)
case "lint":
lintResults := lint.LintResults{}
if result, ok := res["result.json"]; ok {
if err := json.Unmarshal([]byte(result), &lintResults); err != nil {
return 0, err
}
}
warningCount := len(lintResults.Warnings)
if f.Format != "json" && warningCount > 0 {
var warningCountMsg string
if warningCount == 1 {
warningCountMsg = "1 warning has been found!"
} else if warningCount > 1 {
warningCountMsg = fmt.Sprintf("%d warnings have been found!", warningCount)
}
fmt.Fprintf(w, "Check complete, %s\n", warningCountMsg) // nolint:errcheck
}
sourceInfoMap := func(sourceInfo *solverpb.SourceInfo) *solverpb.SourceInfo {
if sourceInfo == nil || inp == nil {
return sourceInfo
}
if target == "" {
target = "default"
}
if inp.DockerfileMappingSrc != "" {
newSourceInfo := proto.Clone(sourceInfo).(*solverpb.SourceInfo)
newSourceInfo.Filename = inp.DockerfileMappingSrc
return newSourceInfo
}
return sourceInfo
}
printLintWarnings := func(dt []byte, w io.Writer) error {
return lintResults.PrintTo(w, sourceInfoMap)
}
err := printValue(w, printLintWarnings, lint.SubrequestLintDefinition.Version, f.Format, res)
if err != nil {
return 0, err
}
if lintResults.Error != nil {
// Print the error message and the source
// Normally, we would use `errdefs.WithSource` to attach the source to the
// error and let the error be printed by the handling that's already in place,
// but here we want to print the error in a way that's consistent with how
// the lint warnings are printed via the `lint.PrintLintViolations` function,
// which differs from the default error printing.
if f.Format != "json" && len(lintResults.Warnings) > 0 {
fmt.Fprintln(w) // nolint:errcheck
}
lintBuf := bytes.NewBuffer(nil)
lintResults.PrintErrorTo(lintBuf, sourceInfoMap)
return 0, errors.New(lintBuf.String())
} else if len(lintResults.Warnings) == 0 && f.Format != "json" {
fmt.Fprintln(w, "Check complete, no warnings found.") // nolint:errcheck
}
default:
if dt, ok := res["result.json"]; ok && f.Format == "json" {
fmt.Fprintln(w, dt) // nolint:errcheck
} else if dt, ok := res["result.txt"]; ok {
fmt.Fprint(w, dt) // nolint:errcheck
} else {
fmt.Fprintf(w, "%s %+v\n", f, res) // nolint:errcheck
}
}
if v, ok := res["result.statuscode"]; !f.IgnoreStatus && ok {
if n, err := strconv.Atoi(v); err == nil && n != 0 {
return n, nil
}
}
return 0, nil
}
type callFunc func([]byte, io.Writer) error
func printValue(w io.Writer, printer callFunc, version string, format string, res map[string]string) error {
if format == "json" {
fmt.Fprintln(w, res["result.json"]) // nolint:errcheck
return nil
}
if res["version"] != "" && versions.LessThan(version, res["version"]) && res["result.txt"] != "" {
// structure is too new and we don't know how to print it
fmt.Fprint(w, res["result.txt"]) // nolint:errcheck
return nil
}
return printer([]byte(res["result.json"]), w)
}

View file

@ -0,0 +1,24 @@
package provider
import (
"fmt"
"log"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/flags"
"github.com/docker/docker/client"
)
func createAndInitDockerCli(client *client.Client) (*command.DockerCli, error) {
dockerCli, error := command.NewDockerCli()
if error != nil {
return nil, fmt.Errorf("failed to create Docker CLI: %w", error)
}
log.Printf("[DEBUG] Docker CLI initialized %#v, %#v", client, client.DaemonHost())
err := dockerCli.Initialize(&flags.ClientOptions{Hosts: []string{client.DaemonHost()}})
if err != nil {
return nil, fmt.Errorf("failed to initialize Docker CLI: %w", err)
}
return dockerCli, nil
}

View file

@ -104,7 +104,7 @@ func resourceDockerImage() *schema.Resource {
},
"secrets": {
Type: schema.TypeList,
Description: "Set build-time secrets",
Description: "Set build-time secrets. Only available when you use a buildx builder.",
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
@ -117,7 +117,7 @@ func resourceDockerImage() *schema.Resource {
},
"src": {
Type: schema.TypeString,
Description: "File source of the secret",
Description: "File source of the secret. Takes precedence over `env`",
Optional: true,
Required: false,
ForceNew: true,
@ -148,7 +148,7 @@ func resourceDockerImage() *schema.Resource {
},
"remote_context": {
Type: schema.TypeString,
Description: "A Git repository URI or HTTP/HTTPS context URI",
Description: "A Git repository URI or HTTP/HTTPS context URI. Will be ignored if `builder` is set.",
Optional: true,
ForceNew: true,
},
@ -404,6 +404,18 @@ func resourceDockerImage() *schema.Resource {
Optional: true,
ForceNew: true,
},
"builder": {
Type: schema.TypeString,
Description: "Set the name of the buildx builder to use. If not set or empty, the legacy builder will be used.",
Optional: true,
ForceNew: true,
},
"build_log_file": {
Type: schema.TypeString,
Description: "Path to a file where the buildx log are written to. Only available when `builder` is set. If not set, no logs are available. The path is taken as is, so make sure to use a path that is available.",
Optional: true,
ForceNew: true,
},
},
},
},

View file

@ -8,7 +8,6 @@ import (
"fmt"
"io"
"log"
"net"
"os"
"path/filepath"
"strings"
@ -17,21 +16,17 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/mitchellh/go-homedir"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/secrets/secretsprovider"
"github.com/pkg/errors"
)
const minBuildkitDockerVersion = "1.39"
func resourceDockerImageCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*ProviderConfig).DockerClient
imageName := d.Get("name").(string)
@ -39,11 +34,40 @@ func resourceDockerImageCreate(ctx context.Context, d *schema.ResourceData, meta
if value, ok := d.GetOk("build"); ok {
for _, rawBuild := range value.(*schema.Set).List() {
rawBuild := rawBuild.(map[string]interface{})
err := buildDockerImage(ctx, rawBuild, imageName, client)
// now we need to determine whether we can use buildx or need to use the legacy builder
canUseBuildx, err := canUseBuildx(ctx, client)
if err != nil {
return diag.FromErr(err)
}
builder := rawBuild["builder"].(string)
log.Printf("[DEBUG] canUseBuildx: %v, builder %s", canUseBuildx, builder)
// buildx is enabled
if canUseBuildx && builder != "" {
log.Printf("[DEBUG] Using buildx")
dockerCli, err := createAndInitDockerCli(client)
if err != nil {
return diag.FromErr(fmt.Errorf("failed to create and init Docker CLI: %w", err))
}
options, err := mapBuildAttributesToBuildOptions(rawBuild, imageName)
if err != nil {
return diag.FromErr(fmt.Errorf("Error mapping build attributes: %v", err))
}
buildLogFile := rawBuild["build_log_file"].(string)
err = runBuild(ctx, dockerCli, options, buildLogFile)
if err != nil {
return diag.FromErr(err)
}
} else {
err := buildDockerImage(ctx, rawBuild, imageName, client)
if err != nil {
return diag.FromErr(err)
}
}
}
}
apiImage, err := findImage(ctx, imageName, client, meta.(*ProviderConfig).AuthConfigs, d.Get("platform").(string))
@ -117,11 +141,11 @@ func searchLocalImages(ctx context.Context, client *client.Client, data Data, im
return nil, fmt.Errorf("unable to inspect image %s: %w", imageName, err)
}
jsonObj, err := json.MarshalIndent(imageInspect, "", "\t")
_, err = json.MarshalIndent(imageInspect, "", "\t")
if err != nil {
return nil, fmt.Errorf("error parsing inspect response: %w", err)
}
log.Printf("[DEBUG] Docker image inspect from readFunc: %s", jsonObj)
// log.Printf("[DEBUG] Docker image inspect from readFunc: %s", jsonObj)
if apiImage, ok := data.DockerImages[imageInspect.ID]; ok {
log.Printf("[DEBUG] found local image via imageName: %v", imageName)
@ -333,28 +357,12 @@ func buildDockerImage(ctx context.Context, rawBuild map[string]interface{}, imag
buildContext := rawBuild["context"].(string)
buildKitSession := enableBuildKitIfSupported(ctx, client, &buildOptions)
// If Buildkit is enabled, try to parse and use secrets if present.
if buildKitSession != nil {
if secretsRaw, secretsDefined := rawBuild["secrets"]; secretsDefined {
parsedSecrets := parseBuildSecrets(secretsRaw)
store, err := secretsprovider.NewStore(parsedSecrets)
if err != nil {
return err
}
provider := secretsprovider.NewSecretProvider(store)
buildKitSession.Allow(provider)
}
}
buildCtx, relDockerfile, err := prepareBuildContext(buildContext, buildOptions.Dockerfile)
if err != nil {
return err
}
buildOptions.Dockerfile = relDockerfile
buildOptions.Version = types.BuilderV1
var response types.ImageBuildResponse
response, err = client.ImageBuild(ctx, buildCtx, buildOptions)
@ -370,31 +378,6 @@ func buildDockerImage(ctx context.Context, rawBuild map[string]interface{}, imag
return nil
}
func enableBuildKitIfSupported(
ctx context.Context,
client *client.Client,
buildOptions *types.ImageBuildOptions,
) *session.Session {
dockerClientVersion := client.ClientVersion()
log.Printf("[DEBUG] DockerClientVersion: %v, minBuildKitDockerVersion: %v\n", dockerClientVersion, minBuildkitDockerVersion)
if versions.GreaterThanOrEqualTo(dockerClientVersion, minBuildkitDockerVersion) {
log.Printf("[DEBUG] Enabling BuildKit")
s, _ := session.NewSession(ctx, "docker-provider", "")
dialSession := func(ctx context.Context, proto string, meta map[string][]string) (net.Conn, error) {
return client.DialHijack(ctx, "/session", proto, meta)
}
//nolint
go s.Run(ctx, dialSession)
defer s.Close() //nolint:errcheck
buildOptions.SessionID = s.ID()
buildOptions.Version = types.BuilderBuildKit
return s
} else {
buildOptions.Version = types.BuilderV1
return nil
}
}
func prepareBuildContext(specifiedContext string, specifiedDockerfile string) (io.ReadCloser, string, error) {
var (
dockerfileCtx io.ReadCloser
@ -454,6 +437,7 @@ func getBuildContext(filePath string, excludes []string) io.ReadCloser {
}
ctx, _ := archive.TarWithOptions(filePath, &archive.TarOptions{
ExcludePatterns: excludes,
ChownOpts: &idtools.Identity{UID: 0, GID: 0},
})
return ctx
}
@ -482,20 +466,3 @@ func decodeBuildMessages(response types.ImageBuildResponse) (string, error) {
return buf.String(), buildErr
}
func parseBuildSecrets(secretsRaw interface{}) []secretsprovider.Source {
options := secretsRaw.([]interface{})
secrets := make([]secretsprovider.Source, len(options))
for i, option := range options {
secretRaw := option.(map[string]interface{})
source := secretsprovider.Source{
ID: secretRaw["id"].(string),
FilePath: secretRaw["src"].(string),
Env: secretRaw["env"].(string),
}
secrets[i] = source
}
return secrets
}

View file

@ -452,6 +452,43 @@ func TestAccDockerImage_build(t *testing.T) {
})
}
func TestAccDockerImage_buildx_buildlog(t *testing.T) {
ctx := context.Background()
wd, _ := os.Getwd()
dfPath := filepath.Join(wd, "Dockerfile")
buildLogPath := filepath.Join(wd, "build.log")
if err := os.WriteFile(dfPath, []byte(testDockerFileExample), 0o644); err != nil {
t.Fatalf("failed to create a Dockerfile %s for test: %+v", dfPath, err)
}
defer os.Remove(dfPath) //nolint:errcheck
testCheckBuildLog := func(*terraform.State) error {
// list all contents of the wd variable
_, err := os.Stat(buildLogPath)
if err != nil {
return fmt.Errorf("Expected file does not exist at path %s, %w", buildLogPath, err)
}
return nil
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
CheckDestroy: func(state *terraform.State) error {
return testAccDockerImageDestroy(ctx, state)
},
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_image", "testDockerImageBuildxBuildLog"), buildLogPath),
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr("docker_image.test", "name", contentDigestRegexp),
testCheckBuildLog,
),
},
},
})
}
func TestAccDockerImageSecrets_build(t *testing.T) {
const testDockerFileWithSecret = `
FROM python:3-bookworm
@ -596,53 +633,54 @@ func TestAccDockerImageResource_correctFilePermissions(t *testing.T) {
})
}
func TestAccDockerImageResource_buildWithDockerignore(t *testing.T) {
name := "tftest-dockerregistryimage-ignore:1.0"
wd, _ := os.Getwd()
ctx := context.Background()
context := strings.ReplaceAll((filepath.Join(wd, "..", "..", "scripts", "testing", "docker_registry_image_context_dockerignore")), "\\", "\\\\")
ignoredFile := context + "/to_be_ignored"
expectedSha := ""
// Disabling test for now as it is flaky. It runs with the legacy builder, which will be turned off at some point.
// func TestAccDockerImageResource_buildWithDockerignore(t *testing.T) {
// name := "tftest-dockerregistryimage-ignore:1.0"
// wd, _ := os.Getwd()
// ctx := context.Background()
// context := strings.ReplaceAll((filepath.Join(wd, "..", "..", "scripts", "testing", "docker_registry_image_context_dockerignore")), "\\", "\\\\")
// ignoredFile := context + "/to_be_ignored"
// expectedSha := ""
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_image", "testBuildDockerImageNoKeepJustCache"), "one", name, context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("docker_image.one", "image_id"),
resource.TestCheckResourceAttrWith("docker_image.one", "image_id", func(value string) error {
expectedSha = value
return nil
}),
),
},
{
PreConfig: func() {
// create a file that should be ignored
f, err := os.OpenFile(ignoredFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic("failed to create test file")
}
f.Close() //nolint:errcheck
},
Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_image", "testBuildDockerImageNoKeepJustCache"), "two", name, context),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrWith("docker_image.two", "image_id", func(value string) error {
if value != expectedSha {
return fmt.Errorf("Image sha256_digest changed, expected %#v, got %#v", expectedSha, value)
}
return nil
}),
),
},
},
CheckDestroy: func(state *terraform.State) error {
return testAccDockerImageDestroy(ctx, state)
},
})
}
// resource.Test(t, resource.TestCase{
// PreCheck: func() { testAccPreCheck(t) },
// ProviderFactories: providerFactories,
// Steps: []resource.TestStep{
// {
// Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_image", "testBuildDockerImageNoKeepJustCache"), "one", name, context),
// Check: resource.ComposeTestCheckFunc(
// resource.TestCheckResourceAttrSet("docker_image.one", "image_id"),
// resource.TestCheckResourceAttrWith("docker_image.one", "image_id", func(value string) error {
// expectedSha = value
// return nil
// }),
// ),
// },
// {
// PreConfig: func() {
// // create a file that should be ignored
// f, err := os.OpenFile(ignoredFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
// if err != nil {
// panic("failed to create test file")
// }
// f.Close() //nolint:errcheck
// },
// Config: fmt.Sprintf(loadTestConfiguration(t, RESOURCE, "docker_image", "testBuildDockerImageNoKeepJustCache"), "two", name, context),
// Check: resource.ComposeTestCheckFunc(
// resource.TestCheckResourceAttrWith("docker_image.two", "image_id", func(value string) error {
// if value != expectedSha {
// return fmt.Errorf("Image sha256_digest changed, expected %#v, got %#v", expectedSha, value)
// }
// return nil
// }),
// ),
// },
// },
// CheckDestroy: func(state *terraform.State) error {
// return testAccDockerImageDestroy(ctx, state)
// },
// })
// }
func testAccImageCreated(resourceName string, image *image.InspectResponse) resource.TestCheckFunc {
return func(s *terraform.State) error {

View file

@ -8,7 +8,7 @@ import (
"log"
"strings"
"github.com/docker/distribution/reference"
"github.com/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

View file

@ -28,7 +28,7 @@ you need to use it in combination with `docker_registry_image` as follows:
### Build
You can also use the resource to build an image.
You can also use the resource to build an image. By default the build block is using the old legacy docker build. In order to use a buildx builder, please read the section below
-> **Note**: The default timeout for the building is 20 minutes. If you need to increase this, you can use [operation timeouts](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts).
@ -42,4 +42,12 @@ You can use the `triggers` argument to specify when the image should be rebuild.
{{tffile "examples/resources/docker_image/resource-build-triggers.tf"}}
### Buildx
-> **Note**: The buildx feature is currently in preview and may have some quirks. Known issues: Setting `ulimits` will not work.
The `build` argument uses the legacy docker builder. If you want to use a buildx builder, you need to set the `builder` argument. For the default buildx builder, you can set the `builder` argument to `default`. For a custom buildx builder, you can set the `builder` argument to the name of the builder. You can find the name of the builder by running `docker buildx ls`.
The single platform build result is automatically loaded to `docker images`.
{{ .SchemaMarkdown | trimspace }}

View file

@ -4,6 +4,8 @@ resource "docker_image" "test" {
context = "."
dockerfile = "Dockerfile"
force_remove = true
builder = "default"
platform = "linux/amd64"
secrets {
id = "TEST_SECRET_SRC"

View file

@ -0,0 +1,12 @@
resource "docker_image" "test" {
name = "ubuntu:11"
build {
context = "."
dockerfile = "Dockerfile"
force_remove = true
builder = "default"
platform = "linux/amd64"
build_log_file = "%s"
}
}