mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-08-15 00:05:57 +08:00
Compare commits
86 Commits
v0.18.0-rc
...
v0.19.0-rc
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6a1cf78879 | ||
![]() |
ec1f712328 | ||
![]() |
5ce6597c07 | ||
![]() |
9c75071793 | ||
![]() |
d612139b19 | ||
![]() |
42f7898c53 | ||
![]() |
3148c098a2 | ||
![]() |
f95d574f94 | ||
![]() |
60822781be | ||
![]() |
4c83475703 | ||
![]() |
17eff25fe5 | ||
![]() |
9c8ffb77d6 | ||
![]() |
13a426fca6 | ||
![]() |
1a039115bc | ||
![]() |
07d58782b8 | ||
![]() |
3ccbb88e6a | ||
![]() |
a34c641bc4 | ||
![]() |
f10be074b4 | ||
![]() |
9f429965c0 | ||
![]() |
f3929447d7 | ||
![]() |
615f4f6759 | ||
![]() |
9a7b028bab | ||
![]() |
1af4f05ba4 | ||
![]() |
4b5d78db9b | ||
![]() |
d2c512a95b | ||
![]() |
5937ba0e00 | ||
![]() |
21fb026aa3 | ||
![]() |
bc45641086 | ||
![]() |
96689e5d05 | ||
![]() |
50a8f11f0f | ||
![]() |
11cf38bd97 | ||
![]() |
300d56b3ff | ||
![]() |
e04da86aca | ||
![]() |
9f1fc99018 | ||
![]() |
26bbddb5d6 | ||
![]() |
58fd190c31 | ||
![]() |
e7a53fb829 | ||
![]() |
c0fd64f4f8 | ||
![]() |
0c629335ac | ||
![]() |
f216b71ad2 | ||
![]() |
debe8c0187 | ||
![]() |
a69d857b8a | ||
![]() |
a6ef9db84d | ||
![]() |
9c27be752c | ||
![]() |
82a65d4f9b | ||
![]() |
8647f408ac | ||
![]() |
e51cdcac50 | ||
![]() |
55a544d976 | ||
![]() |
3b943bd4ba | ||
![]() |
502bb51a3b | ||
![]() |
48977780ad | ||
![]() |
e540bb03a4 | ||
![]() |
919c52395d | ||
![]() |
7f01c63be7 | ||
![]() |
076d2f19d5 | ||
![]() |
3c3150b8d3 | ||
![]() |
b03d8c52e1 | ||
![]() |
e67ccb080b | ||
![]() |
dab02c347e | ||
![]() |
6caa151e98 | ||
![]() |
be6d8326a8 | ||
![]() |
7855f8324b | ||
![]() |
850e5330ad | ||
![]() |
b7ea25eb59 | ||
![]() |
8cdeac54ab | ||
![]() |
752c70a06c | ||
![]() |
83dd969dc1 | ||
![]() |
a5bb117ff0 | ||
![]() |
735b7f68fe | ||
![]() |
bcac44f658 | ||
![]() |
d46595eed8 | ||
![]() |
62407927fa | ||
![]() |
c7b0a84c6a | ||
![]() |
1aac809c63 | ||
![]() |
9b0575b589 | ||
![]() |
9f3a578149 | ||
![]() |
14b31d8b77 | ||
![]() |
e26911f403 | ||
![]() |
cd8d61a9d7 | ||
![]() |
3a56161d03 | ||
![]() |
0fd935b0ca | ||
![]() |
704b2cc52d | ||
![]() |
6b2dc8ce56 | ||
![]() |
a585faf3d2 | ||
![]() |
ca502cc9a5 | ||
![]() |
8056a3dc7c |
85
.github/CONTRIBUTING.md
vendored
85
.github/CONTRIBUTING.md
vendored
@@ -188,6 +188,89 @@ To generate new vendored files with go modules run:
|
||||
$ make vendor
|
||||
```
|
||||
|
||||
### Generate profiling data
|
||||
|
||||
You can configure Buildx to generate [`pprof`](https://github.com/google/pprof)
|
||||
memory and CPU profiles to analyze and optimize your builds. These profiles are
|
||||
useful for identifying performance bottlenecks, detecting memory
|
||||
inefficiencies, and ensuring the program (Buildx) runs efficiently.
|
||||
|
||||
The following environment variables control whether Buildx generates profiling
|
||||
data for builds:
|
||||
|
||||
```console
|
||||
$ export BUILDX_CPU_PROFILE=buildx_cpu.prof
|
||||
$ export BUILDX_MEM_PROFILE=buildx_mem.prof
|
||||
```
|
||||
|
||||
When set, Buildx emits profiling samples for the builds to the location
|
||||
specified by the environment variable.
|
||||
|
||||
To analyze and visualize profiling samples, you need `pprof` from the Go
|
||||
toolchain, and (optionally) GraphViz for visualization in a graphical format.
|
||||
|
||||
To inspect profiling data with `pprof`:
|
||||
|
||||
1. Build a local binary of Buildx from source.
|
||||
|
||||
```console
|
||||
$ docker buildx bake
|
||||
```
|
||||
|
||||
The binary gets exported to `./bin/build/buildx`.
|
||||
|
||||
2. Run a build and with the environment variables set to generate profiling data.
|
||||
|
||||
```console
|
||||
$ export BUILDX_CPU_PROFILE=buildx_cpu.prof
|
||||
$ export BUILDX_MEM_PROFILE=buildx_mem.prof
|
||||
$ ./bin/build/buildx bake
|
||||
```
|
||||
|
||||
This creates `buildx_cpu.prof` and `buildx_mem.prof` for the build.
|
||||
|
||||
3. Start `pprof` and specify the filename of the profile that you want to
|
||||
analyze.
|
||||
|
||||
```console
|
||||
$ go tool pprof buildx_cpu.prof
|
||||
```
|
||||
|
||||
This opens the `pprof` interactive console. From here, you can inspect the
|
||||
profiling sample using various commands. For example, use `top 10` command
|
||||
to view the top 10 most time-consuming entries.
|
||||
|
||||
```plaintext
|
||||
(pprof) top 10
|
||||
Showing nodes accounting for 3.04s, 91.02% of 3.34s total
|
||||
Dropped 123 nodes (cum <= 0.02s)
|
||||
Showing top 10 nodes out of 159
|
||||
flat flat% sum% cum cum%
|
||||
1.14s 34.13% 34.13% 1.14s 34.13% syscall.syscall
|
||||
0.91s 27.25% 61.38% 0.91s 27.25% runtime.kevent
|
||||
0.35s 10.48% 71.86% 0.35s 10.48% runtime.pthread_cond_wait
|
||||
0.22s 6.59% 78.44% 0.22s 6.59% runtime.pthread_cond_signal
|
||||
0.15s 4.49% 82.93% 0.15s 4.49% runtime.usleep
|
||||
0.10s 2.99% 85.93% 0.10s 2.99% runtime.memclrNoHeapPointers
|
||||
0.10s 2.99% 88.92% 0.10s 2.99% runtime.memmove
|
||||
0.03s 0.9% 89.82% 0.03s 0.9% runtime.madvise
|
||||
0.02s 0.6% 90.42% 0.02s 0.6% runtime.(*mspan).typePointersOfUnchecked
|
||||
0.02s 0.6% 91.02% 0.02s 0.6% runtime.pcvalue
|
||||
```
|
||||
|
||||
To view the call graph in a GUI, run `go tool pprof -http=:8081 <sample>`.
|
||||
|
||||
> [!NOTE]
|
||||
> Requires [GraphViz](https://www.graphviz.org/) to be installed.
|
||||
|
||||
```console
|
||||
$ go tool pprof -http=:8081 buildx_cpu.prof
|
||||
Serving web UI on http://127.0.0.1:8081
|
||||
http://127.0.0.1:8081
|
||||
```
|
||||
|
||||
For more information about using `pprof` and how to interpret the call graph,
|
||||
refer to the [`pprof` README](https://github.com/google/pprof/blob/main/doc/README.md).
|
||||
|
||||
### Conventions
|
||||
|
||||
@@ -343,4 +426,4 @@ The rules:
|
||||
|
||||
If you are having trouble getting into the mood of idiomatic Go, we recommend
|
||||
reading through [Effective Go](https://golang.org/doc/effective_go.html). The
|
||||
[Go Blog](https://blog.golang.org) is also a great resource.
|
||||
[Go Blog](https://blog.golang.org) is also a great resource.
|
||||
|
35
.github/workflows/build.yml
vendored
35
.github/workflows/build.yml
vendored
@@ -36,7 +36,7 @@ env:
|
||||
TEST_CACHE_SCOPE: "test"
|
||||
TESTFLAGS: "-v --parallel=6 --timeout=30m"
|
||||
GOTESTSUM_FORMAT: "standard-verbose"
|
||||
GO_VERSION: "1.22"
|
||||
GO_VERSION: "1.23"
|
||||
GOTESTSUM_VERSION: "v1.9.0" # same as one in Dockerfile
|
||||
|
||||
jobs:
|
||||
@@ -54,9 +54,9 @@ jobs:
|
||||
- master
|
||||
- latest
|
||||
- buildx-stable-1
|
||||
- v0.17.2
|
||||
- v0.16.0
|
||||
- v0.15.2
|
||||
- v0.14.1
|
||||
- v0.13.2
|
||||
worker:
|
||||
- docker-container
|
||||
- remote
|
||||
@@ -76,6 +76,16 @@ jobs:
|
||||
- worker: docker+containerd # same as docker, but with containerd snapshotter
|
||||
pkg: ./tests
|
||||
mode: experimental
|
||||
- worker: "docker@26.1"
|
||||
pkg: ./tests
|
||||
- worker: "docker+containerd@26.1" # same as docker, but with containerd snapshotter
|
||||
pkg: ./tests
|
||||
- worker: "docker@26.1"
|
||||
pkg: ./tests
|
||||
mode: experimental
|
||||
- worker: "docker+containerd@26.1" # same as docker, but with containerd snapshotter
|
||||
pkg: ./tests
|
||||
mode: experimental
|
||||
steps:
|
||||
-
|
||||
name: Prepare
|
||||
@@ -86,7 +96,7 @@ jobs:
|
||||
fi
|
||||
testFlags="--run=//worker=$(echo "${{ matrix.worker }}" | sed 's/\+/\\+/g')$"
|
||||
case "${{ matrix.worker }}" in
|
||||
docker | docker+containerd)
|
||||
docker | docker+containerd | docker@* | docker+containerd@*)
|
||||
echo "TESTFLAGS=${{ env.TESTFLAGS_DOCKER }} $testFlags" >> $GITHUB_ENV
|
||||
;;
|
||||
*)
|
||||
@@ -131,7 +141,7 @@ jobs:
|
||||
-
|
||||
name: Send to Codecov
|
||||
if: always()
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
directory: ./bin/testreports
|
||||
flags: integration
|
||||
@@ -158,7 +168,7 @@ jobs:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-24.04
|
||||
- macos-12
|
||||
- macos-14
|
||||
- windows-2022
|
||||
env:
|
||||
SKIP_INTEGRATION_TESTS: 1
|
||||
@@ -203,7 +213,7 @@ jobs:
|
||||
-
|
||||
name: Send to Codecov
|
||||
if: always()
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
directory: ${{ env.TESTREPORTS_DIR }}
|
||||
env_vars: RUNNER_OS
|
||||
@@ -227,10 +237,10 @@ jobs:
|
||||
govulncheck:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
# same as global permission
|
||||
contents: read
|
||||
# required to write sarif report
|
||||
security-events: write
|
||||
# required to check out the repository
|
||||
contents: read
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
@@ -374,6 +384,8 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ github.ref == 'refs/heads/master' && github.repository == 'docker/buildx' }}
|
||||
permissions:
|
||||
# same as global permission
|
||||
contents: read
|
||||
# required to write sarif report
|
||||
security-events: write
|
||||
needs:
|
||||
@@ -404,6 +416,9 @@ jobs:
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
# required to create GitHub release
|
||||
contents: write
|
||||
needs:
|
||||
- test-integration
|
||||
- test-unit
|
||||
@@ -433,7 +448,7 @@ jobs:
|
||||
-
|
||||
name: GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8
|
||||
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
11
.github/workflows/codeql.yml
vendored
11
.github/workflows/codeql.yml
vendored
@@ -17,16 +17,15 @@ on:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.22"
|
||||
GO_VERSION: "1.23"
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
security-events: write
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
|
3
.github/workflows/docs-release.yml
vendored
3
.github/workflows/docs-release.yml
vendored
@@ -23,6 +23,9 @@ jobs:
|
||||
open-pr:
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ (github.event.release.prerelease != true || github.event.inputs.tag != '') && github.repository == 'docker/buildx' }}
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
-
|
||||
name: Checkout docs repo
|
||||
|
8
.github/workflows/labeler.yml
vendored
8
.github/workflows/labeler.yml
vendored
@@ -18,10 +18,12 @@ on:
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# same as global permission
|
||||
contents: read
|
||||
# required for writing labels
|
||||
pull-requests: write
|
||||
steps:
|
||||
-
|
||||
name: Run
|
||||
|
@@ -1,26 +1,52 @@
|
||||
run:
|
||||
timeout: 30m
|
||||
|
||||
modules-download-mode: vendor
|
||||
# default uses Go version from the go.mod file, fallback on the env var
|
||||
# `GOVERSION`, fallback on 1.17: https://golangci-lint.run/usage/configuration/#run-configuration
|
||||
go: "1.23"
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- gofmt
|
||||
- govet
|
||||
- bodyclose
|
||||
- depguard
|
||||
- forbidigo
|
||||
- gocritic
|
||||
- gofmt
|
||||
- goimports
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- makezero
|
||||
- misspell
|
||||
- unused
|
||||
- noctx
|
||||
- nolintlint
|
||||
- revive
|
||||
- staticcheck
|
||||
- testifylint
|
||||
- typecheck
|
||||
- nolintlint
|
||||
- gosec
|
||||
- forbidigo
|
||||
- unused
|
||||
- whitespace
|
||||
disable-all: true
|
||||
|
||||
linters-settings:
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- "ifElseChain"
|
||||
- "assignOp"
|
||||
- "appendAssign"
|
||||
- "singleCaseSwitch"
|
||||
- "exitAfterDefer" # FIXME
|
||||
importas:
|
||||
alias:
|
||||
# Enforce alias to prevent it accidentally being used instead of
|
||||
# buildkit errdefs package (or vice-versa).
|
||||
- pkg: "github.com/containerd/errdefs"
|
||||
alias: "cerrdefs"
|
||||
- pkg: "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
alias: "ocispecs"
|
||||
- pkg: "github.com/opencontainers/go-digest"
|
||||
alias: "digest"
|
||||
govet:
|
||||
enable:
|
||||
- nilness
|
||||
@@ -43,14 +69,27 @@ linters-settings:
|
||||
desc: The io/ioutil package has been deprecated.
|
||||
forbidigo:
|
||||
forbid:
|
||||
- '^context\.WithCancel(# use context\.WithCancelCause instead)?$'
|
||||
- '^context\.WithDeadline(# use context\.WithDeadline instead)?$'
|
||||
- '^context\.WithTimeout(# use context\.WithTimeoutCause instead)?$'
|
||||
- '^ctx\.Err(# use context\.Cause instead)?$'
|
||||
- '^fmt\.Errorf(# use errors\.Errorf instead)?$'
|
||||
- '^platforms\.DefaultString(# use platforms\.Format(platforms\.DefaultSpec()) instead\.)?$'
|
||||
gosec:
|
||||
excludes:
|
||||
- G204 # Audit use of command execution
|
||||
- G402 # TLS MinVersion too low
|
||||
- G115 # integer overflow conversion (TODO: verify these)
|
||||
config:
|
||||
G306: "0644"
|
||||
testifylint:
|
||||
disable:
|
||||
# disable rules that reduce the test condition
|
||||
- "empty"
|
||||
- "bool-compare"
|
||||
- "len"
|
||||
- "negative-positive"
|
||||
|
||||
|
||||
issues:
|
||||
exclude-files:
|
||||
|
21
Dockerfile
21
Dockerfile
@@ -1,20 +1,23 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.22
|
||||
ARG GO_VERSION=1.23
|
||||
ARG XX_VERSION=1.5.0
|
||||
|
||||
# for testing
|
||||
ARG DOCKER_VERSION=27.2.1
|
||||
ARG DOCKER_VERSION=27.4.0-rc.2
|
||||
ARG DOCKER_VERSION_ALT_26=26.1.3
|
||||
ARG DOCKER_CLI_VERSION=${DOCKER_VERSION}
|
||||
ARG GOTESTSUM_VERSION=v1.9.0
|
||||
ARG REGISTRY_VERSION=2.8.0
|
||||
ARG BUILDKIT_VERSION=v0.16.0
|
||||
ARG UNDOCK_VERSION=0.7.0
|
||||
ARG GOTESTSUM_VERSION=v1.12.0
|
||||
ARG REGISTRY_VERSION=2.8.3
|
||||
ARG BUILDKIT_VERSION=v0.17.2
|
||||
ARG UNDOCK_VERSION=0.8.0
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS golatest
|
||||
FROM moby/moby-bin:$DOCKER_VERSION AS docker-engine
|
||||
FROM dockereng/cli-bin:$DOCKER_CLI_VERSION AS docker-cli
|
||||
FROM moby/moby-bin:$DOCKER_VERSION_ALT_26 AS docker-engine-alt
|
||||
FROM dockereng/cli-bin:$DOCKER_VERSION_ALT_26 AS docker-cli-alt
|
||||
FROM registry:$REGISTRY_VERSION AS registry
|
||||
FROM moby/buildkit:$BUILDKIT_VERSION AS buildkit
|
||||
FROM crazymax/undock:$UNDOCK_VERSION AS undock
|
||||
@@ -77,6 +80,7 @@ RUN --mount=type=bind,target=. \
|
||||
set -e
|
||||
xx-go --wrap
|
||||
DESTDIR=/usr/bin VERSION=$(cat /buildx-version/version) REVISION=$(cat /buildx-version/revision) GO_EXTRA_LDFLAGS="-s -w" ./hack/build
|
||||
file /usr/bin/docker-buildx
|
||||
xx-verify --static /usr/bin/docker-buildx
|
||||
EOT
|
||||
|
||||
@@ -95,7 +99,9 @@ FROM scratch AS binaries-unix
|
||||
COPY --link --from=buildx-build /usr/bin/docker-buildx /buildx
|
||||
|
||||
FROM binaries-unix AS binaries-darwin
|
||||
FROM binaries-unix AS binaries-freebsd
|
||||
FROM binaries-unix AS binaries-linux
|
||||
FROM binaries-unix AS binaries-openbsd
|
||||
|
||||
FROM scratch AS binaries-windows
|
||||
COPY --link --from=buildx-build /usr/bin/docker-buildx /buildx.exe
|
||||
@@ -120,10 +126,13 @@ COPY --link --from=gotestsum /out /usr/bin/
|
||||
COPY --link --from=registry /bin/registry /usr/bin/
|
||||
COPY --link --from=docker-engine / /usr/bin/
|
||||
COPY --link --from=docker-cli / /usr/bin/
|
||||
COPY --link --from=docker-engine-alt / /opt/docker-alt-26/
|
||||
COPY --link --from=docker-cli-alt / /opt/docker-alt-26/
|
||||
COPY --link --from=buildkit /usr/bin/buildkitd /usr/bin/
|
||||
COPY --link --from=buildkit /usr/bin/buildctl /usr/bin/
|
||||
COPY --link --from=undock /usr/local/bin/undock /usr/bin/
|
||||
COPY --link --from=binaries /buildx /usr/bin/
|
||||
ENV TEST_DOCKER_EXTRA="docker@26.1=/opt/docker-alt-26"
|
||||
|
||||
FROM integration-test-base AS integration-test
|
||||
COPY . .
|
||||
|
406
bake/bake.go
406
bake/bake.go
@@ -2,6 +2,7 @@ package bake
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
@@ -496,7 +497,9 @@ func (c Config) loadLinks(name string, t *Target, m map[string]*Target, o map[st
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t2.Outputs = []string{"type=cacheonly"}
|
||||
t2.Outputs = []*buildflags.ExportEntry{
|
||||
{Type: "cacheonly"},
|
||||
}
|
||||
t2.linked = true
|
||||
m[target] = t2
|
||||
}
|
||||
@@ -695,59 +698,61 @@ type Target struct {
|
||||
// Inherits is the only field that cannot be overridden with --set
|
||||
Inherits []string `json:"inherits,omitempty" hcl:"inherits,optional" cty:"inherits"`
|
||||
|
||||
Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"`
|
||||
Attest []string `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"`
|
||||
Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"`
|
||||
Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"`
|
||||
Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"`
|
||||
DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"`
|
||||
Args map[string]*string `json:"args,omitempty" hcl:"args,optional" cty:"args"`
|
||||
Labels map[string]*string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"`
|
||||
Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"`
|
||||
CacheFrom []string `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"`
|
||||
CacheTo []string `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"`
|
||||
Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"`
|
||||
Secrets []string `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"`
|
||||
SSH []string `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"`
|
||||
Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"`
|
||||
Outputs []string `json:"output,omitempty" hcl:"output,optional" cty:"output"`
|
||||
Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"`
|
||||
NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"`
|
||||
NetworkMode *string `json:"network,omitempty" hcl:"network,optional" cty:"network"`
|
||||
NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"`
|
||||
ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional"`
|
||||
Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional"`
|
||||
Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"`
|
||||
Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"`
|
||||
Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"`
|
||||
Attest []string `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"`
|
||||
Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"`
|
||||
Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"`
|
||||
Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"`
|
||||
DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"`
|
||||
Args map[string]*string `json:"args,omitempty" hcl:"args,optional" cty:"args"`
|
||||
Labels map[string]*string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"`
|
||||
Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"`
|
||||
CacheFrom []*buildflags.CacheOptionsEntry `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"`
|
||||
CacheTo []*buildflags.CacheOptionsEntry `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"`
|
||||
Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"`
|
||||
Secrets []*buildflags.Secret `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"`
|
||||
SSH []*buildflags.SSH `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"`
|
||||
Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"`
|
||||
Outputs []*buildflags.ExportEntry `json:"output,omitempty" hcl:"output,optional" cty:"output"`
|
||||
Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"`
|
||||
NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"`
|
||||
NetworkMode *string `json:"network,omitempty" hcl:"network,optional" cty:"network"`
|
||||
NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"`
|
||||
ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional"`
|
||||
Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional"`
|
||||
Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"`
|
||||
Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"`
|
||||
// IMPORTANT: if you add more fields here, do not forget to update newOverrides/AddOverrides and docs/bake-reference.md.
|
||||
|
||||
// linked is a private field to mark a target used as a linked one
|
||||
linked bool
|
||||
}
|
||||
|
||||
var _ hclparser.WithEvalContexts = &Target{}
|
||||
var _ hclparser.WithGetName = &Target{}
|
||||
var _ hclparser.WithEvalContexts = &Group{}
|
||||
var _ hclparser.WithGetName = &Group{}
|
||||
var (
|
||||
_ hclparser.WithEvalContexts = &Target{}
|
||||
_ hclparser.WithGetName = &Target{}
|
||||
_ hclparser.WithEvalContexts = &Group{}
|
||||
_ hclparser.WithGetName = &Group{}
|
||||
)
|
||||
|
||||
func (t *Target) normalize() {
|
||||
t.Annotations = removeDupes(t.Annotations)
|
||||
t.Annotations = removeDupesStr(t.Annotations)
|
||||
t.Attest = removeAttestDupes(t.Attest)
|
||||
t.Tags = removeDupes(t.Tags)
|
||||
t.Tags = removeDupesStr(t.Tags)
|
||||
t.Secrets = removeDupes(t.Secrets)
|
||||
t.SSH = removeDupes(t.SSH)
|
||||
t.Platforms = removeDupes(t.Platforms)
|
||||
t.Platforms = removeDupesStr(t.Platforms)
|
||||
t.CacheFrom = removeDupes(t.CacheFrom)
|
||||
t.CacheTo = removeDupes(t.CacheTo)
|
||||
t.Outputs = removeDupes(t.Outputs)
|
||||
t.NoCacheFilter = removeDupes(t.NoCacheFilter)
|
||||
t.Ulimits = removeDupes(t.Ulimits)
|
||||
t.NoCacheFilter = removeDupesStr(t.NoCacheFilter)
|
||||
t.Ulimits = removeDupesStr(t.Ulimits)
|
||||
|
||||
if t.NetworkMode != nil && *t.NetworkMode == "host" {
|
||||
t.Entitlements = append(t.Entitlements, "network.host")
|
||||
}
|
||||
|
||||
t.Entitlements = removeDupes(t.Entitlements)
|
||||
t.Entitlements = removeDupesStr(t.Entitlements)
|
||||
|
||||
for k, v := range t.Contexts {
|
||||
if v == "" {
|
||||
@@ -865,7 +870,7 @@ func (t *Target) AddOverrides(overrides map[string]Override) error {
|
||||
t.Dockerfile = &value
|
||||
case "args":
|
||||
if len(keys) != 2 {
|
||||
return errors.Errorf("args require name")
|
||||
return errors.Errorf("invalid format for args, expecting args.<name>=<value>")
|
||||
}
|
||||
if t.Args == nil {
|
||||
t.Args = map[string]*string{}
|
||||
@@ -873,7 +878,7 @@ func (t *Target) AddOverrides(overrides map[string]Override) error {
|
||||
t.Args[keys[1]] = &value
|
||||
case "contexts":
|
||||
if len(keys) != 2 {
|
||||
return errors.Errorf("contexts require name")
|
||||
return errors.Errorf("invalid format for contexts, expecting contexts.<name>=<value>")
|
||||
}
|
||||
if t.Contexts == nil {
|
||||
t.Contexts = map[string]string{}
|
||||
@@ -881,7 +886,7 @@ func (t *Target) AddOverrides(overrides map[string]Override) error {
|
||||
t.Contexts[keys[1]] = value
|
||||
case "labels":
|
||||
if len(keys) != 2 {
|
||||
return errors.Errorf("labels require name")
|
||||
return errors.Errorf("invalid format for labels, expecting labels.<name>=<value>")
|
||||
}
|
||||
if t.Labels == nil {
|
||||
t.Labels = map[string]*string{}
|
||||
@@ -890,21 +895,41 @@ func (t *Target) AddOverrides(overrides map[string]Override) error {
|
||||
case "tags":
|
||||
t.Tags = o.ArrValue
|
||||
case "cache-from":
|
||||
t.CacheFrom = o.ArrValue
|
||||
cacheFrom, err := parseCacheArrValues(o.ArrValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.CacheFrom = cacheFrom
|
||||
case "cache-to":
|
||||
t.CacheTo = o.ArrValue
|
||||
cacheTo, err := parseCacheArrValues(o.ArrValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.CacheTo = cacheTo
|
||||
case "target":
|
||||
t.Target = &value
|
||||
case "call":
|
||||
t.Call = &value
|
||||
case "secrets":
|
||||
t.Secrets = o.ArrValue
|
||||
secrets, err := parseArrValue[buildflags.Secret](o.ArrValue)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid value for outputs")
|
||||
}
|
||||
t.Secrets = secrets
|
||||
case "ssh":
|
||||
t.SSH = o.ArrValue
|
||||
ssh, err := parseArrValue[buildflags.SSH](o.ArrValue)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid value for outputs")
|
||||
}
|
||||
t.SSH = ssh
|
||||
case "platform":
|
||||
t.Platforms = o.ArrValue
|
||||
case "output":
|
||||
t.Outputs = o.ArrValue
|
||||
outputs, err := parseArrValue[buildflags.ExportEntry](o.ArrValue)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid value for outputs")
|
||||
}
|
||||
t.Outputs = outputs
|
||||
case "entitlements":
|
||||
t.Entitlements = append(t.Entitlements, o.ArrValue...)
|
||||
case "annotations":
|
||||
@@ -1115,62 +1140,34 @@ func updateContext(t *build.Inputs, inp *Input) {
|
||||
t.ContextState = &st
|
||||
}
|
||||
|
||||
// validateContextsEntitlements is a basic check to ensure contexts do not
|
||||
// escape local directories when loaded from remote sources. This is to be
|
||||
// replaced with proper entitlements support in the future.
|
||||
func validateContextsEntitlements(t build.Inputs, inp *Input) error {
|
||||
if inp == nil || inp.State == nil {
|
||||
return nil
|
||||
}
|
||||
if v, ok := os.LookupEnv("BAKE_ALLOW_REMOTE_FS_ACCESS"); ok {
|
||||
if vv, _ := strconv.ParseBool(v); vv {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
func collectLocalPaths(t build.Inputs) []string {
|
||||
var out []string
|
||||
if t.ContextState == nil {
|
||||
if err := checkPath(t.ContextPath); err != nil {
|
||||
return err
|
||||
if v, ok := isLocalPath(t.ContextPath); ok {
|
||||
out = append(out, v)
|
||||
}
|
||||
if v, ok := isLocalPath(t.DockerfilePath); ok {
|
||||
out = append(out, v)
|
||||
}
|
||||
} else if strings.HasPrefix(t.ContextPath, "cwd://") {
|
||||
out = append(out, strings.TrimPrefix(t.ContextPath, "cwd://"))
|
||||
}
|
||||
for _, v := range t.NamedContexts {
|
||||
if v.State != nil {
|
||||
continue
|
||||
}
|
||||
if err := checkPath(v.Path); err != nil {
|
||||
return err
|
||||
if v, ok := isLocalPath(v.Path); ok {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return out
|
||||
}
|
||||
|
||||
func checkPath(p string) error {
|
||||
func isLocalPath(p string) (string, bool) {
|
||||
if build.IsRemoteURL(p) || strings.HasPrefix(p, "target:") || strings.HasPrefix(p, "docker-image:") {
|
||||
return nil
|
||||
return "", false
|
||||
}
|
||||
p, err := filepath.EvalSymlinks(p)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
p, err = filepath.Abs(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(wd, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parts := strings.Split(rel, string(os.PathSeparator))
|
||||
if parts[0] == ".." {
|
||||
return errors.Errorf("path %s is outside of the working directory, please set BAKE_ALLOW_REMOTE_FS_ACCESS=1", p)
|
||||
}
|
||||
return nil
|
||||
return strings.TrimPrefix(p, "cwd://"), true
|
||||
}
|
||||
|
||||
func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
||||
@@ -1210,9 +1207,6 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
||||
// it's not outside the working directory and then resolve it to an
|
||||
// absolute path.
|
||||
bi.DockerfilePath = path.Clean(strings.TrimPrefix(bi.DockerfilePath, "cwd://"))
|
||||
if err := checkPath(bi.DockerfilePath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var err error
|
||||
bi.DockerfilePath, err = filepath.Abs(bi.DockerfilePath)
|
||||
if err != nil {
|
||||
@@ -1249,10 +1243,6 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateContextsEntitlements(bi, inp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t.Context = &bi.ContextPath
|
||||
|
||||
args := map[string]string{}
|
||||
@@ -1309,23 +1299,29 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
||||
}
|
||||
bo.Platforms = platforms
|
||||
|
||||
secrets, err := buildflags.ParseSecretSpecs(t.Secrets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
secrets := make([]*controllerapi.Secret, len(t.Secrets))
|
||||
for i, s := range t.Secrets {
|
||||
secrets[i] = s.ToPB()
|
||||
}
|
||||
bo.SecretSpecs = secrets
|
||||
|
||||
secretAttachment, err := controllerapi.CreateSecrets(secrets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bo.Session = append(bo.Session, secretAttachment)
|
||||
|
||||
sshSpecs, err := buildflags.ParseSSHSpecs(t.SSH)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(sshSpecs) == 0 && (buildflags.IsGitSSH(bi.ContextPath) || (inp != nil && buildflags.IsGitSSH(inp.URL))) {
|
||||
sshSpecs = append(sshSpecs, &controllerapi.SSH{ID: "default"})
|
||||
var sshSpecs []*controllerapi.SSH
|
||||
if len(t.SSH) > 0 {
|
||||
sshSpecs := make([]*controllerapi.SSH, len(t.SSH))
|
||||
for i, s := range t.SSH {
|
||||
sshSpecs[i] = s.ToPB()
|
||||
}
|
||||
} else if buildflags.IsGitSSH(bi.ContextPath) || (inp != nil && buildflags.IsGitSSH(inp.URL)) {
|
||||
sshSpecs = []*controllerapi.SSH{{ID: "default"}}
|
||||
}
|
||||
bo.SSHSpecs = sshSpecs
|
||||
|
||||
sshAttachment, err := controllerapi.CreateSSH(sshSpecs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1342,22 +1338,23 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
||||
}
|
||||
}
|
||||
|
||||
cacheImports, err := buildflags.ParseCacheEntry(t.CacheFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
cacheImports := make([]*controllerapi.CacheOptionsEntry, len(t.CacheFrom))
|
||||
for i, ci := range t.CacheFrom {
|
||||
cacheImports[i] = ci.ToPB()
|
||||
}
|
||||
bo.CacheFrom = controllerapi.CreateCaches(cacheImports)
|
||||
|
||||
cacheExports, err := buildflags.ParseCacheEntry(t.CacheTo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
cacheExports := make([]*controllerapi.CacheOptionsEntry, len(t.CacheTo))
|
||||
for i, ce := range t.CacheTo {
|
||||
cacheExports[i] = ce.ToPB()
|
||||
}
|
||||
bo.CacheTo = controllerapi.CreateCaches(cacheExports)
|
||||
|
||||
outputs, err := buildflags.ParseExports(t.Outputs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
outputs := make([]*controllerapi.ExportEntry, len(t.Outputs))
|
||||
for i, output := range t.Outputs {
|
||||
outputs[i] = output.ToPB()
|
||||
}
|
||||
|
||||
bo.Exports, err = controllerapi.CreateExports(outputs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1403,7 +1400,35 @@ func defaultTarget() *Target {
|
||||
return &Target{}
|
||||
}
|
||||
|
||||
func removeDupes(s []string) []string {
|
||||
type comparable[E any] interface {
|
||||
Equal(other E) bool
|
||||
}
|
||||
|
||||
func removeDupes[E comparable[E]](s []E) []E {
|
||||
// Move backwards through the slice.
|
||||
// For each element, any elements after the current element are unique.
|
||||
// If we find our current element conflicts with an existing element,
|
||||
// then we swap the offender with the end of the slice and chop it off.
|
||||
|
||||
// Start at the second to last element.
|
||||
// The last element is always unique.
|
||||
for i := len(s) - 2; i >= 0; i-- {
|
||||
elem := s[i]
|
||||
// Check for duplicates after our current element.
|
||||
for j := i + 1; j < len(s); j++ {
|
||||
if elem.Equal(s[j]) {
|
||||
// Found a duplicate, exchange the
|
||||
// duplicate with the last element.
|
||||
s[j], s[len(s)-1] = s[len(s)-1], s[j]
|
||||
s = s[:len(s)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func removeDupesStr(s []string) []string {
|
||||
i := 0
|
||||
seen := make(map[string]struct{}, len(s))
|
||||
for _, v := range s {
|
||||
@@ -1440,86 +1465,76 @@ func removeAttestDupes(s []string) []string {
|
||||
return res
|
||||
}
|
||||
|
||||
func parseOutput(str string) map[string]string {
|
||||
fields, err := csvvalue.Fields(str, nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
res := map[string]string{}
|
||||
for _, field := range fields {
|
||||
parts := strings.SplitN(field, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
res[parts[0]] = parts[1]
|
||||
func setPushOverride(outputs []*buildflags.ExportEntry, push bool) []*buildflags.ExportEntry {
|
||||
if !push {
|
||||
// Disable push for any relevant export types
|
||||
for i := 0; i < len(outputs); {
|
||||
output := outputs[i]
|
||||
switch output.Type {
|
||||
case "registry":
|
||||
// Filter out registry output type
|
||||
outputs[i], outputs[len(outputs)-1] = outputs[len(outputs)-1], outputs[i]
|
||||
outputs = outputs[:len(outputs)-1]
|
||||
continue
|
||||
case "image":
|
||||
// Override push attribute
|
||||
output.Attrs["push"] = "false"
|
||||
}
|
||||
i++
|
||||
}
|
||||
return outputs
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func parseOutputType(str string) string {
|
||||
if out := parseOutput(str); out != nil {
|
||||
if v, ok := out["type"]; ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func setPushOverride(outputs []string, push bool) []string {
|
||||
var out []string
|
||||
// Force push to be enabled
|
||||
setPush := true
|
||||
for _, output := range outputs {
|
||||
typ := parseOutputType(output)
|
||||
if typ == "image" || typ == "registry" {
|
||||
// no need to set push if image or registry types already defined
|
||||
if output.Type != "docker" {
|
||||
// If there is an output type that is not docker, don't set "push"
|
||||
setPush = false
|
||||
if typ == "registry" {
|
||||
if !push {
|
||||
// don't set registry output if "push" is false
|
||||
continue
|
||||
}
|
||||
// no need to set "push" attribute to true for registry
|
||||
out = append(out, output)
|
||||
continue
|
||||
}
|
||||
out = append(out, output+",push="+strconv.FormatBool(push))
|
||||
} else {
|
||||
if typ != "docker" {
|
||||
// if there is any output that is not docker, don't set "push"
|
||||
setPush = false
|
||||
}
|
||||
out = append(out, output)
|
||||
}
|
||||
|
||||
// Set push attribute for image
|
||||
if output.Type == "image" {
|
||||
output.Attrs["push"] = "true"
|
||||
}
|
||||
}
|
||||
if push && setPush {
|
||||
out = append(out, "type=image,push=true")
|
||||
|
||||
if setPush {
|
||||
// No existing output that pushes so add one
|
||||
outputs = append(outputs, &buildflags.ExportEntry{
|
||||
Type: "image",
|
||||
Attrs: map[string]string{
|
||||
"push": "true",
|
||||
},
|
||||
})
|
||||
}
|
||||
return out
|
||||
return outputs
|
||||
}
|
||||
|
||||
func setLoadOverride(outputs []string, load bool) []string {
|
||||
func setLoadOverride(outputs []*buildflags.ExportEntry, load bool) []*buildflags.ExportEntry {
|
||||
if !load {
|
||||
return outputs
|
||||
}
|
||||
setLoad := true
|
||||
|
||||
for _, output := range outputs {
|
||||
if typ := parseOutputType(output); typ == "docker" {
|
||||
if v := parseOutput(output); v != nil {
|
||||
// dest set means we want to output as tar so don't set load
|
||||
if _, ok := v["dest"]; !ok {
|
||||
setLoad = false
|
||||
break
|
||||
}
|
||||
switch output.Type {
|
||||
case "docker":
|
||||
// if dest is not set, we can reuse this entry and do not need to set load
|
||||
if output.Destination == "" {
|
||||
return outputs
|
||||
}
|
||||
} else if typ != "image" && typ != "registry" && typ != "oci" {
|
||||
case "image", "registry", "oci":
|
||||
// Ignore
|
||||
default:
|
||||
// if there is any output that is not an image, registry
|
||||
// or oci, don't set "load" similar to push override
|
||||
setLoad = false
|
||||
break
|
||||
return outputs
|
||||
}
|
||||
}
|
||||
if setLoad {
|
||||
outputs = append(outputs, "type=docker")
|
||||
}
|
||||
|
||||
outputs = append(outputs, &buildflags.ExportEntry{
|
||||
Type: "docker",
|
||||
})
|
||||
return outputs
|
||||
}
|
||||
|
||||
@@ -1558,3 +1573,50 @@ func toNamedContexts(m map[string]string) map[string]build.NamedContext {
|
||||
}
|
||||
return m2
|
||||
}
|
||||
|
||||
type arrValue[B any] interface {
|
||||
encoding.TextUnmarshaler
|
||||
*B
|
||||
}
|
||||
|
||||
func parseArrValue[T any, PT arrValue[T]](s []string) ([]*T, error) {
|
||||
outputs := make([]*T, 0, len(s))
|
||||
for _, text := range s {
|
||||
output := new(T)
|
||||
if err := PT(output).UnmarshalText([]byte(text)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outputs = append(outputs, output)
|
||||
}
|
||||
return outputs, nil
|
||||
}
|
||||
|
||||
func parseCacheArrValues(s []string) ([]*buildflags.CacheOptionsEntry, error) {
|
||||
outs := make([]*buildflags.CacheOptionsEntry, 0, len(s))
|
||||
for _, in := range s {
|
||||
if !strings.Contains(in, "=") {
|
||||
// This is ref only format. Each field in the CSV is its own entry.
|
||||
fields, err := csvvalue.Fields(in, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
out := buildflags.CacheOptionsEntry{}
|
||||
if err := out.UnmarshalText([]byte(field)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outs = append(outs, &out)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Normal entry.
|
||||
out := buildflags.CacheOptionsEntry{}
|
||||
if err := out.UnmarshalText([]byte(in)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outs = append(outs, &out)
|
||||
}
|
||||
return outs, nil
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package bake
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -59,8 +60,8 @@ target "webapp" {
|
||||
t.Run("InvalidTargetOverrides", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"nosuchtarget.context=foo"}, nil)
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, err.Error(), "could not find any target matching 'nosuchtarget'")
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "could not find any target matching 'nosuchtarget'", err.Error())
|
||||
})
|
||||
|
||||
t.Run("ArgsOverrides", func(t *testing.T) {
|
||||
@@ -116,7 +117,7 @@ target "webapp" {
|
||||
t.Run("ContextOverride", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, _, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.context"}, nil)
|
||||
require.NotNil(t, err)
|
||||
require.Error(t, err)
|
||||
|
||||
m, g, err := ReadTargets(ctx, []File{fp}, []string{"webapp"}, []string{"webapp.context=foo"}, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -203,8 +204,8 @@ target "webapp" {
|
||||
// NOTE: I am unsure whether failing to match should always error out
|
||||
// instead of simply skipping that override.
|
||||
// Let's enforce the error and we can relax it later if users complain.
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, err.Error(), "could not find any target matching 'nomatch*'")
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "could not find any target matching 'nomatch*'", err.Error())
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -228,7 +229,7 @@ func TestPushOverride(t *testing.T) {
|
||||
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.push=true"}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(m["app"].Outputs))
|
||||
require.Equal(t, "type=image,push=true", m["app"].Outputs[0])
|
||||
require.Equal(t, "type=image,push=true", m["app"].Outputs[0].String())
|
||||
})
|
||||
|
||||
t.Run("type image", func(t *testing.T) {
|
||||
@@ -242,7 +243,7 @@ func TestPushOverride(t *testing.T) {
|
||||
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.push=true"}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(m["app"].Outputs))
|
||||
require.Equal(t, "type=image,compression=zstd,push=true", m["app"].Outputs[0])
|
||||
require.Equal(t, "type=image,compression=zstd,push=true", m["app"].Outputs[0].String())
|
||||
})
|
||||
|
||||
t.Run("type image push false", func(t *testing.T) {
|
||||
@@ -256,7 +257,7 @@ func TestPushOverride(t *testing.T) {
|
||||
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.push=false"}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(m["app"].Outputs))
|
||||
require.Equal(t, "type=image,compression=zstd,push=false", m["app"].Outputs[0])
|
||||
require.Equal(t, "type=image,compression=zstd,push=false", m["app"].Outputs[0].String())
|
||||
})
|
||||
|
||||
t.Run("type registry", func(t *testing.T) {
|
||||
@@ -270,7 +271,7 @@ func TestPushOverride(t *testing.T) {
|
||||
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.push=true"}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(m["app"].Outputs))
|
||||
require.Equal(t, "type=registry", m["app"].Outputs[0])
|
||||
require.Equal(t, "type=registry", m["app"].Outputs[0].String())
|
||||
})
|
||||
|
||||
t.Run("type registry push false", func(t *testing.T) {
|
||||
@@ -300,9 +301,9 @@ func TestPushOverride(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(m))
|
||||
require.Equal(t, 1, len(m["foo"].Outputs))
|
||||
require.Equal(t, []string{"type=local,dest=out"}, m["foo"].Outputs)
|
||||
require.Equal(t, []string{"type=local,dest=out"}, stringify(m["foo"].Outputs))
|
||||
require.Equal(t, 1, len(m["bar"].Outputs))
|
||||
require.Equal(t, []string{"type=image,push=true"}, m["bar"].Outputs)
|
||||
require.Equal(t, []string{"type=image,push=true"}, stringify(m["bar"].Outputs))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -317,7 +318,7 @@ func TestLoadOverride(t *testing.T) {
|
||||
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(m["app"].Outputs))
|
||||
require.Equal(t, "type=docker", m["app"].Outputs[0])
|
||||
require.Equal(t, "type=docker", m["app"].Outputs[0].String())
|
||||
})
|
||||
|
||||
t.Run("type docker", func(t *testing.T) {
|
||||
@@ -331,7 +332,7 @@ func TestLoadOverride(t *testing.T) {
|
||||
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(m["app"].Outputs))
|
||||
require.Equal(t, []string{"type=docker"}, m["app"].Outputs)
|
||||
require.Equal(t, []string{"type=docker"}, stringify(m["app"].Outputs))
|
||||
})
|
||||
|
||||
t.Run("type image", func(t *testing.T) {
|
||||
@@ -345,7 +346,7 @@ func TestLoadOverride(t *testing.T) {
|
||||
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(m["app"].Outputs))
|
||||
require.Equal(t, []string{"type=image", "type=docker"}, m["app"].Outputs)
|
||||
require.Equal(t, []string{"type=docker", "type=image"}, stringify(m["app"].Outputs))
|
||||
})
|
||||
|
||||
t.Run("type image load false", func(t *testing.T) {
|
||||
@@ -359,7 +360,7 @@ func TestLoadOverride(t *testing.T) {
|
||||
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=false"}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(m["app"].Outputs))
|
||||
require.Equal(t, []string{"type=image"}, m["app"].Outputs)
|
||||
require.Equal(t, []string{"type=image"}, stringify(m["app"].Outputs))
|
||||
})
|
||||
|
||||
t.Run("type registry", func(t *testing.T) {
|
||||
@@ -373,7 +374,7 @@ func TestLoadOverride(t *testing.T) {
|
||||
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(m["app"].Outputs))
|
||||
require.Equal(t, []string{"type=registry", "type=docker"}, m["app"].Outputs)
|
||||
require.Equal(t, []string{"type=docker", "type=registry"}, stringify(m["app"].Outputs))
|
||||
})
|
||||
|
||||
t.Run("type oci", func(t *testing.T) {
|
||||
@@ -387,7 +388,7 @@ func TestLoadOverride(t *testing.T) {
|
||||
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(m["app"].Outputs))
|
||||
require.Equal(t, []string{"type=oci,dest=out", "type=docker"}, m["app"].Outputs)
|
||||
require.Equal(t, []string{"type=docker", "type=oci,dest=out"}, stringify(m["app"].Outputs))
|
||||
})
|
||||
|
||||
t.Run("type docker with dest", func(t *testing.T) {
|
||||
@@ -401,7 +402,7 @@ func TestLoadOverride(t *testing.T) {
|
||||
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{"*.load=true"}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(m["app"].Outputs))
|
||||
require.Equal(t, []string{"type=docker,dest=out", "type=docker"}, m["app"].Outputs)
|
||||
require.Equal(t, []string{"type=docker", "type=docker,dest=out"}, stringify(m["app"].Outputs))
|
||||
})
|
||||
|
||||
t.Run("type local and empty target", func(t *testing.T) {
|
||||
@@ -418,9 +419,9 @@ func TestLoadOverride(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(m))
|
||||
require.Equal(t, 1, len(m["foo"].Outputs))
|
||||
require.Equal(t, []string{"type=local,dest=out"}, m["foo"].Outputs)
|
||||
require.Equal(t, []string{"type=local,dest=out"}, stringify(m["foo"].Outputs))
|
||||
require.Equal(t, 1, len(m["bar"].Outputs))
|
||||
require.Equal(t, []string{"type=docker"}, m["bar"].Outputs)
|
||||
require.Equal(t, []string{"type=docker"}, stringify(m["bar"].Outputs))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -440,12 +441,10 @@ func TestLoadAndPushOverride(t *testing.T) {
|
||||
require.Equal(t, 2, len(m))
|
||||
|
||||
require.Equal(t, 1, len(m["foo"].Outputs))
|
||||
sort.Strings(m["foo"].Outputs)
|
||||
require.Equal(t, []string{"type=local,dest=out"}, m["foo"].Outputs)
|
||||
require.Equal(t, []string{"type=local,dest=out"}, stringify(m["foo"].Outputs))
|
||||
|
||||
require.Equal(t, 2, len(m["bar"].Outputs))
|
||||
sort.Strings(m["bar"].Outputs)
|
||||
require.Equal(t, []string{"type=docker", "type=image,push=true"}, m["bar"].Outputs)
|
||||
require.Equal(t, []string{"type=docker", "type=image,push=true"}, stringify(m["bar"].Outputs))
|
||||
})
|
||||
|
||||
t.Run("type registry", func(t *testing.T) {
|
||||
@@ -461,8 +460,7 @@ func TestLoadAndPushOverride(t *testing.T) {
|
||||
require.Equal(t, 1, len(m))
|
||||
|
||||
require.Equal(t, 2, len(m["foo"].Outputs))
|
||||
sort.Strings(m["foo"].Outputs)
|
||||
require.Equal(t, []string{"type=docker", "type=registry"}, m["foo"].Outputs)
|
||||
require.Equal(t, []string{"type=docker", "type=registry"}, stringify(m["foo"].Outputs))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -674,7 +672,7 @@ func TestOverrideMerge(t *testing.T) {
|
||||
|
||||
require.Equal(t, []string{"linux/arm", "linux/ppc64le"}, m["app"].Platforms)
|
||||
require.Equal(t, 1, len(m["app"].Outputs))
|
||||
require.Equal(t, "type=registry", m["app"].Outputs[0])
|
||||
require.Equal(t, "type=registry", m["app"].Outputs[0].String())
|
||||
}
|
||||
|
||||
func TestReadContexts(t *testing.T) {
|
||||
@@ -840,7 +838,7 @@ func TestReadContextFromTargetChain(t *testing.T) {
|
||||
mid, ok := m["mid"]
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 1, len(mid.Outputs))
|
||||
require.Equal(t, "type=cacheonly", mid.Outputs[0])
|
||||
require.Equal(t, "type=cacheonly", mid.Outputs[0].String())
|
||||
require.Equal(t, 1, len(mid.Contexts))
|
||||
|
||||
base, ok := m["base"]
|
||||
@@ -924,7 +922,8 @@ func TestReadTargetsDefault(t *testing.T) {
|
||||
Data: []byte(`
|
||||
target "default" {
|
||||
dockerfile = "test"
|
||||
}`)}
|
||||
}`),
|
||||
}
|
||||
|
||||
m, g, err := ReadTargets(ctx, []File{f}, []string{"default"}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -942,7 +941,8 @@ func TestReadTargetsSpecified(t *testing.T) {
|
||||
Data: []byte(`
|
||||
target "image" {
|
||||
dockerfile = "test"
|
||||
}`)}
|
||||
}`),
|
||||
}
|
||||
|
||||
_, _, err := ReadTargets(ctx, []File{f}, []string{"default"}, nil, nil)
|
||||
require.Error(t, err)
|
||||
@@ -967,7 +967,8 @@ group "foo" {
|
||||
}
|
||||
target "image" {
|
||||
dockerfile = "test"
|
||||
}`)}
|
||||
}`),
|
||||
}
|
||||
|
||||
m, g, err := ReadTargets(ctx, []File{f}, []string{"foo"}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -993,7 +994,8 @@ target "foo" {
|
||||
}
|
||||
target "image" {
|
||||
dockerfile = "test"
|
||||
}`)}
|
||||
}`),
|
||||
}
|
||||
|
||||
m, g, err := ReadTargets(ctx, []File{f}, []string{"foo"}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -1036,7 +1038,8 @@ target "image-release" {
|
||||
inherits = ["image"]
|
||||
output = ["type=image,push=true"]
|
||||
tags = ["user/app:latest"]
|
||||
}`)}
|
||||
}`),
|
||||
}
|
||||
|
||||
fyml := File{
|
||||
Name: "docker-compose.yml",
|
||||
@@ -1060,7 +1063,8 @@ services:
|
||||
args:
|
||||
CT_ECR: foo
|
||||
CT_TAG: bar
|
||||
image: ct-fake-aws:bar`)}
|
||||
image: ct-fake-aws:bar`),
|
||||
}
|
||||
|
||||
fjson := File{
|
||||
Name: "docker-bake.json",
|
||||
@@ -1081,7 +1085,8 @@ services:
|
||||
]
|
||||
}
|
||||
}
|
||||
}`)}
|
||||
}`),
|
||||
}
|
||||
|
||||
m, g, err := ReadTargets(ctx, []File{fhcl}, []string{"default"}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -1089,7 +1094,7 @@ services:
|
||||
require.Equal(t, []string{"image"}, g["default"].Targets)
|
||||
require.Equal(t, 1, len(m))
|
||||
require.Equal(t, 1, len(m["image"].Outputs))
|
||||
require.Equal(t, "type=docker", m["image"].Outputs[0])
|
||||
require.Equal(t, "type=docker", m["image"].Outputs[0].String())
|
||||
|
||||
m, g, err = ReadTargets(ctx, []File{fhcl}, []string{"image-release"}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -1097,7 +1102,7 @@ services:
|
||||
require.Equal(t, []string{"image-release"}, g["default"].Targets)
|
||||
require.Equal(t, 1, len(m))
|
||||
require.Equal(t, 1, len(m["image-release"].Outputs))
|
||||
require.Equal(t, "type=image,push=true", m["image-release"].Outputs[0])
|
||||
require.Equal(t, "type=image,push=true", m["image-release"].Outputs[0].String())
|
||||
|
||||
m, g, err = ReadTargets(ctx, []File{fhcl}, []string{"image", "image-release"}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -1106,7 +1111,7 @@ services:
|
||||
require.Equal(t, 2, len(m))
|
||||
require.Equal(t, ".", *m["image"].Context)
|
||||
require.Equal(t, 1, len(m["image-release"].Outputs))
|
||||
require.Equal(t, "type=image,push=true", m["image-release"].Outputs[0])
|
||||
require.Equal(t, "type=image,push=true", m["image-release"].Outputs[0].String())
|
||||
|
||||
m, g, err = ReadTargets(ctx, []File{fyml, fhcl}, []string{"default"}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -1166,7 +1171,8 @@ target "foo" {
|
||||
}
|
||||
target "image" {
|
||||
output = ["type=docker"]
|
||||
}`)}
|
||||
}`),
|
||||
}
|
||||
|
||||
m, g, err := ReadTargets(ctx, []File{f}, []string{"foo"}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -1200,7 +1206,8 @@ target "foo" {
|
||||
}
|
||||
target "image" {
|
||||
output = ["type=docker"]
|
||||
}`)}
|
||||
}`),
|
||||
}
|
||||
|
||||
m, g, err := ReadTargets(ctx, []File{f}, []string{"foo"}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -1209,7 +1216,7 @@ target "image" {
|
||||
require.Equal(t, []string{"foo", "image"}, g["foo"].Targets)
|
||||
require.Equal(t, 2, len(m))
|
||||
require.Equal(t, "bar", *m["foo"].Dockerfile)
|
||||
require.Equal(t, "type=docker", m["image"].Outputs[0])
|
||||
require.Equal(t, "type=docker", m["image"].Outputs[0].String())
|
||||
|
||||
m, g, err = ReadTargets(ctx, []File{f}, []string{"foo", "image"}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -1218,7 +1225,7 @@ target "image" {
|
||||
require.Equal(t, []string{"foo", "image"}, g["foo"].Targets)
|
||||
require.Equal(t, 2, len(m))
|
||||
require.Equal(t, "bar", *m["foo"].Dockerfile)
|
||||
require.Equal(t, "type=docker", m["image"].Outputs[0])
|
||||
require.Equal(t, "type=docker", m["image"].Outputs[0].String())
|
||||
}
|
||||
|
||||
func TestNestedInherits(t *testing.T) {
|
||||
@@ -1247,7 +1254,8 @@ target "c" {
|
||||
}
|
||||
target "d" {
|
||||
inherits = ["b", "c"]
|
||||
}`)}
|
||||
}`),
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
@@ -1315,7 +1323,8 @@ group "default" {
|
||||
"child1",
|
||||
"child2"
|
||||
]
|
||||
}`)}
|
||||
}`),
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
@@ -1351,9 +1360,9 @@ group "default" {
|
||||
require.Equal(t, []string{"child1", "child2"}, g["default"].Targets)
|
||||
require.Equal(t, 2, len(m))
|
||||
require.Equal(t, tt.wantch1, m["child1"].Args)
|
||||
require.Equal(t, []string{"type=docker"}, m["child1"].Outputs)
|
||||
require.Equal(t, []string{"type=docker"}, stringify(m["child1"].Outputs))
|
||||
require.Equal(t, tt.wantch2, m["child2"].Args)
|
||||
require.Equal(t, []string{"type=docker"}, m["child2"].Outputs)
|
||||
require.Equal(t, []string{"type=docker"}, stringify(m["child2"].Outputs))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1442,7 +1451,8 @@ group "e" {
|
||||
|
||||
target "f" {
|
||||
context = "./foo"
|
||||
}`)}
|
||||
}`),
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
names []string
|
||||
@@ -1721,7 +1731,7 @@ func TestAnnotations(t *testing.T) {
|
||||
|
||||
require.Equal(t, 1, len(m))
|
||||
require.Contains(t, m, "app")
|
||||
require.Equal(t, "type=image,name=foo", m["app"].Outputs[0])
|
||||
require.Equal(t, "type=image,name=foo", m["app"].Outputs[0].String())
|
||||
require.Equal(t, "manifest[linux/amd64]:foo=bar", m["app"].Annotations[0])
|
||||
|
||||
require.Len(t, bo["app"].Exports, 1)
|
||||
@@ -1856,3 +1866,164 @@ func TestNetNone(t *testing.T) {
|
||||
require.Len(t, bo["app"].Allow, 0)
|
||||
require.Equal(t, "none", bo["app"].NetworkMode)
|
||||
}
|
||||
|
||||
func TestVariableValidation(t *testing.T) {
|
||||
fp := File{
|
||||
Name: "docker-bake.hcl",
|
||||
Data: []byte(`
|
||||
variable "FOO" {
|
||||
validation {
|
||||
condition = FOO != ""
|
||||
error_message = "FOO is required."
|
||||
}
|
||||
}
|
||||
target "app" {
|
||||
args = {
|
||||
FOO = FOO
|
||||
}
|
||||
}
|
||||
`),
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
t.Setenv("FOO", "bar")
|
||||
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "FOO is required.")
|
||||
})
|
||||
}
|
||||
|
||||
func TestVariableValidationMulti(t *testing.T) {
|
||||
fp := File{
|
||||
Name: "docker-bake.hcl",
|
||||
Data: []byte(`
|
||||
variable "FOO" {
|
||||
validation {
|
||||
condition = FOO != ""
|
||||
error_message = "FOO is required."
|
||||
}
|
||||
validation {
|
||||
condition = strlen(FOO) > 4
|
||||
error_message = "FOO must be longer than 4 characters."
|
||||
}
|
||||
}
|
||||
target "app" {
|
||||
args = {
|
||||
FOO = FOO
|
||||
}
|
||||
}
|
||||
`),
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
t.Setenv("FOO", "barbar")
|
||||
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("InvalidLength", func(t *testing.T) {
|
||||
t.Setenv("FOO", "bar")
|
||||
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "FOO must be longer than 4 characters.")
|
||||
})
|
||||
|
||||
t.Run("InvalidEmpty", func(t *testing.T) {
|
||||
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "FOO is required.")
|
||||
})
|
||||
}
|
||||
|
||||
func TestVariableValidationWithDeps(t *testing.T) {
|
||||
fp := File{
|
||||
Name: "docker-bake.hcl",
|
||||
Data: []byte(`
|
||||
variable "FOO" {}
|
||||
variable "BAR" {
|
||||
validation {
|
||||
condition = FOO != ""
|
||||
error_message = "BAR requires FOO to be set."
|
||||
}
|
||||
}
|
||||
target "app" {
|
||||
args = {
|
||||
BAR = BAR
|
||||
}
|
||||
}
|
||||
`),
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
t.Setenv("FOO", "bar")
|
||||
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("SetBar", func(t *testing.T) {
|
||||
t.Setenv("FOO", "bar")
|
||||
t.Setenv("BAR", "baz")
|
||||
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "BAR requires FOO to be set.")
|
||||
})
|
||||
}
|
||||
|
||||
func TestVariableValidationTyped(t *testing.T) {
|
||||
fp := File{
|
||||
Name: "docker-bake.hcl",
|
||||
Data: []byte(`
|
||||
variable "FOO" {
|
||||
default = 0
|
||||
validation {
|
||||
condition = FOO > 5
|
||||
error_message = "FOO must be greater than 5."
|
||||
}
|
||||
}
|
||||
target "app" {
|
||||
args = {
|
||||
FOO = FOO
|
||||
}
|
||||
}
|
||||
`),
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
t.Setenv("FOO", "10")
|
||||
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "FOO must be greater than 5.")
|
||||
})
|
||||
}
|
||||
|
||||
func stringify[V fmt.Stringer](values []V) []string {
|
||||
s := make([]string, len(values))
|
||||
for i, v := range values {
|
||||
s[i] = v.String()
|
||||
}
|
||||
sort.Strings(s)
|
||||
return s
|
||||
}
|
||||
|
101
bake/compose.go
101
bake/compose.go
@@ -5,13 +5,14 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/consts"
|
||||
"github.com/compose-spec/compose-go/v2/dotenv"
|
||||
"github.com/compose-spec/compose-go/v2/loader"
|
||||
composetypes "github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/buildx/util/buildflags"
|
||||
dockeropts "github.com/docker/cli/opts"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/pkg/errors"
|
||||
@@ -102,6 +103,12 @@ func ParseCompose(cfgs []composetypes.ConfigFile, envs map[string]string) (*Conf
|
||||
shmSize = &shmSizeStr
|
||||
}
|
||||
|
||||
var networkModeP *string
|
||||
if s.Build.Network != "" {
|
||||
networkMode := s.Build.Network
|
||||
networkModeP = &networkMode
|
||||
}
|
||||
|
||||
var ulimits []string
|
||||
if s.Build.Ulimits != nil {
|
||||
for n, u := range s.Build.Ulimits {
|
||||
@@ -113,14 +120,16 @@ func ParseCompose(cfgs []composetypes.ConfigFile, envs map[string]string) (*Conf
|
||||
}
|
||||
}
|
||||
|
||||
var ssh []string
|
||||
var ssh []*buildflags.SSH
|
||||
for _, bkey := range s.Build.SSH {
|
||||
sshkey := composeToBuildkitSSH(bkey)
|
||||
ssh = append(ssh, sshkey)
|
||||
}
|
||||
sort.Strings(ssh)
|
||||
slices.SortFunc(ssh, func(a, b *buildflags.SSH) int {
|
||||
return a.Less(b)
|
||||
})
|
||||
|
||||
var secrets []string
|
||||
var secrets []*buildflags.Secret
|
||||
for _, bs := range s.Build.Secrets {
|
||||
secret, err := composeToBuildkitSecret(bs, cfg.Secrets[bs.Source])
|
||||
if err != nil {
|
||||
@@ -136,6 +145,16 @@ func ParseCompose(cfgs []composetypes.ConfigFile, envs map[string]string) (*Conf
|
||||
labels[k] = &v
|
||||
}
|
||||
|
||||
cacheFrom, err := parseCacheArrValues(s.Build.CacheFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cacheTo, err := parseCacheArrValues(s.Build.CacheTo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g.Targets = append(g.Targets, targetName)
|
||||
t := &Target{
|
||||
Name: targetName,
|
||||
@@ -152,9 +171,9 @@ func ParseCompose(cfgs []composetypes.ConfigFile, envs map[string]string) (*Conf
|
||||
val, ok := cfg.Environment[val]
|
||||
return val, ok
|
||||
})),
|
||||
CacheFrom: s.Build.CacheFrom,
|
||||
CacheTo: s.Build.CacheTo,
|
||||
NetworkMode: &s.Build.Network,
|
||||
CacheFrom: cacheFrom,
|
||||
CacheTo: cacheTo,
|
||||
NetworkMode: networkModeP,
|
||||
SSH: ssh,
|
||||
Secrets: secrets,
|
||||
ShmSize: shmSize,
|
||||
@@ -173,7 +192,6 @@ func ParseCompose(cfgs []composetypes.ConfigFile, envs map[string]string) (*Conf
|
||||
c.Targets = append(c.Targets, t)
|
||||
}
|
||||
c.Groups = append(c.Groups, g)
|
||||
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
@@ -292,8 +310,10 @@ type xbake struct {
|
||||
// https://github.com/docker/docs/blob/main/content/build/bake/compose-file.md#extension-field-with-x-bake
|
||||
}
|
||||
|
||||
type stringMap map[string]string
|
||||
type stringArray []string
|
||||
type (
|
||||
stringMap map[string]string
|
||||
stringArray []string
|
||||
)
|
||||
|
||||
func (sa *stringArray) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var multi []string
|
||||
@@ -329,23 +349,45 @@ func (t *Target) composeExtTarget(exts map[string]interface{}) error {
|
||||
t.Tags = dedupSlice(append(t.Tags, xb.Tags...))
|
||||
}
|
||||
if len(xb.CacheFrom) > 0 {
|
||||
t.CacheFrom = dedupSlice(append(t.CacheFrom, xb.CacheFrom...))
|
||||
cacheFrom, err := parseCacheArrValues(xb.CacheFrom)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.CacheFrom = removeDupes(append(t.CacheFrom, cacheFrom...))
|
||||
}
|
||||
if len(xb.CacheTo) > 0 {
|
||||
t.CacheTo = dedupSlice(append(t.CacheTo, xb.CacheTo...))
|
||||
cacheTo, err := parseCacheArrValues(xb.CacheTo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.CacheTo = removeDupes(append(t.CacheTo, cacheTo...))
|
||||
}
|
||||
if len(xb.Secrets) > 0 {
|
||||
t.Secrets = dedupSlice(append(t.Secrets, xb.Secrets...))
|
||||
secrets, err := parseArrValue[buildflags.Secret](xb.Secrets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Secrets = removeDupes(append(t.Secrets, secrets...))
|
||||
}
|
||||
if len(xb.SSH) > 0 {
|
||||
t.SSH = dedupSlice(append(t.SSH, xb.SSH...))
|
||||
sort.Strings(t.SSH)
|
||||
ssh, err := parseArrValue[buildflags.SSH](xb.SSH)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.SSH = removeDupes(append(t.SSH, ssh...))
|
||||
slices.SortFunc(t.SSH, func(a, b *buildflags.SSH) int {
|
||||
return a.Less(b)
|
||||
})
|
||||
}
|
||||
if len(xb.Platforms) > 0 {
|
||||
t.Platforms = dedupSlice(append(t.Platforms, xb.Platforms...))
|
||||
}
|
||||
if len(xb.Outputs) > 0 {
|
||||
t.Outputs = dedupSlice(append(t.Outputs, xb.Outputs...))
|
||||
outputs, err := parseArrValue[buildflags.ExportEntry](xb.Outputs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Outputs = removeDupes(append(t.Outputs, outputs...))
|
||||
}
|
||||
if xb.Pull != nil {
|
||||
t.Pull = xb.Pull
|
||||
@@ -365,35 +407,30 @@ func (t *Target) composeExtTarget(exts map[string]interface{}) error {
|
||||
|
||||
// composeToBuildkitSecret converts secret from compose format to buildkit's
|
||||
// csv format.
|
||||
func composeToBuildkitSecret(inp composetypes.ServiceSecretConfig, psecret composetypes.SecretConfig) (string, error) {
|
||||
func composeToBuildkitSecret(inp composetypes.ServiceSecretConfig, psecret composetypes.SecretConfig) (*buildflags.Secret, error) {
|
||||
if psecret.External {
|
||||
return "", errors.Errorf("unsupported external secret %s", psecret.Name)
|
||||
return nil, errors.Errorf("unsupported external secret %s", psecret.Name)
|
||||
}
|
||||
|
||||
var bkattrs []string
|
||||
secret := &buildflags.Secret{}
|
||||
if inp.Source != "" {
|
||||
bkattrs = append(bkattrs, "id="+inp.Source)
|
||||
secret.ID = inp.Source
|
||||
}
|
||||
if psecret.File != "" {
|
||||
bkattrs = append(bkattrs, "src="+psecret.File)
|
||||
secret.FilePath = psecret.File
|
||||
}
|
||||
if psecret.Environment != "" {
|
||||
bkattrs = append(bkattrs, "env="+psecret.Environment)
|
||||
secret.Env = psecret.Environment
|
||||
}
|
||||
|
||||
return strings.Join(bkattrs, ","), nil
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
// composeToBuildkitSSH converts secret from compose format to buildkit's
|
||||
// csv format.
|
||||
func composeToBuildkitSSH(sshKey composetypes.SSHKey) string {
|
||||
var bkattrs []string
|
||||
|
||||
bkattrs = append(bkattrs, sshKey.ID)
|
||||
|
||||
func composeToBuildkitSSH(sshKey composetypes.SSHKey) *buildflags.SSH {
|
||||
bkssh := &buildflags.SSH{ID: sshKey.ID}
|
||||
if sshKey.Path != "" {
|
||||
bkattrs = append(bkattrs, sshKey.Path)
|
||||
bkssh.Paths = []string{sshKey.Path}
|
||||
}
|
||||
|
||||
return strings.Join(bkattrs, "=")
|
||||
return bkssh
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
func TestParseCompose(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
db:
|
||||
build: ./db
|
||||
@@ -74,14 +74,14 @@ secrets:
|
||||
require.Equal(t, "Dockerfile-alternate", *c.Targets[1].Dockerfile)
|
||||
require.Equal(t, 1, len(c.Targets[1].Args))
|
||||
require.Equal(t, ptrstr("123"), c.Targets[1].Args["buildno"])
|
||||
require.Equal(t, []string{"type=local,src=path/to/cache"}, c.Targets[1].CacheFrom)
|
||||
require.Equal(t, []string{"type=local,dest=path/to/cache"}, c.Targets[1].CacheTo)
|
||||
require.Equal(t, []string{"type=local,src=path/to/cache"}, stringify(c.Targets[1].CacheFrom))
|
||||
require.Equal(t, []string{"type=local,dest=path/to/cache"}, stringify(c.Targets[1].CacheTo))
|
||||
require.Equal(t, "none", *c.Targets[1].NetworkMode)
|
||||
require.Equal(t, []string{"default", "key=path/to/key"}, c.Targets[1].SSH)
|
||||
require.Equal(t, []string{"default", "key=path/to/key"}, stringify(c.Targets[1].SSH))
|
||||
require.Equal(t, []string{
|
||||
"id=token,env=ENV_TOKEN",
|
||||
"id=aws,src=/root/.aws/credentials",
|
||||
}, c.Targets[1].Secrets)
|
||||
"id=token,env=ENV_TOKEN",
|
||||
}, stringify(c.Targets[1].Secrets))
|
||||
|
||||
require.Equal(t, "webapp2", c.Targets[2].Name)
|
||||
require.Equal(t, "dir", *c.Targets[2].Context)
|
||||
@@ -89,7 +89,7 @@ secrets:
|
||||
}
|
||||
|
||||
func TestNoBuildOutOfTreeService(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
external:
|
||||
image: "verycooldb:1337"
|
||||
@@ -103,7 +103,7 @@ services:
|
||||
}
|
||||
|
||||
func TestParseComposeTarget(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
db:
|
||||
build:
|
||||
@@ -129,7 +129,7 @@ services:
|
||||
}
|
||||
|
||||
func TestComposeBuildWithoutContext(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
db:
|
||||
build:
|
||||
@@ -153,7 +153,7 @@ services:
|
||||
}
|
||||
|
||||
func TestBuildArgEnvCompose(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
version: "3.8"
|
||||
services:
|
||||
example:
|
||||
@@ -179,7 +179,7 @@ services:
|
||||
}
|
||||
|
||||
func TestInconsistentComposeFile(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
webapp:
|
||||
entrypoint: echo 1
|
||||
@@ -190,7 +190,7 @@ services:
|
||||
}
|
||||
|
||||
func TestAdvancedNetwork(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
db:
|
||||
networks:
|
||||
@@ -215,7 +215,7 @@ networks:
|
||||
}
|
||||
|
||||
func TestTags(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
example:
|
||||
image: example
|
||||
@@ -233,7 +233,7 @@ services:
|
||||
}
|
||||
|
||||
func TestDependsOnList(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -269,7 +269,7 @@ networks:
|
||||
}
|
||||
|
||||
func TestComposeExt(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
addon:
|
||||
image: ct-addon:bar
|
||||
@@ -336,23 +336,23 @@ services:
|
||||
require.Equal(t, map[string]*string{"CT_ECR": ptrstr("foo"), "CT_TAG": ptrstr("bar")}, c.Targets[0].Args)
|
||||
require.Equal(t, []string{"ct-addon:baz", "ct-addon:foo", "ct-addon:alp"}, c.Targets[0].Tags)
|
||||
require.Equal(t, []string{"linux/amd64", "linux/arm64"}, c.Targets[0].Platforms)
|
||||
require.Equal(t, []string{"user/app:cache", "type=local,src=path/to/cache"}, c.Targets[0].CacheFrom)
|
||||
require.Equal(t, []string{"user/app:cache", "type=local,dest=path/to/cache"}, c.Targets[0].CacheTo)
|
||||
require.Equal(t, []string{"default", "key=path/to/key", "other=path/to/otherkey"}, c.Targets[0].SSH)
|
||||
require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheFrom))
|
||||
require.Equal(t, []string{"type=local,dest=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheTo))
|
||||
require.Equal(t, []string{"default", "key=path/to/key", "other=path/to/otherkey"}, stringify(c.Targets[0].SSH))
|
||||
require.Equal(t, newBool(true), c.Targets[0].Pull)
|
||||
require.Equal(t, map[string]string{"alpine": "docker-image://alpine:3.13"}, c.Targets[0].Contexts)
|
||||
require.Equal(t, []string{"ct-fake-aws:bar"}, c.Targets[1].Tags)
|
||||
require.Equal(t, []string{"id=mysecret,src=/local/secret", "id=mysecret2,src=/local/secret2"}, c.Targets[1].Secrets)
|
||||
require.Equal(t, []string{"default"}, c.Targets[1].SSH)
|
||||
require.Equal(t, []string{"id=mysecret,src=/local/secret", "id=mysecret2,src=/local/secret2"}, stringify(c.Targets[1].Secrets))
|
||||
require.Equal(t, []string{"default"}, stringify(c.Targets[1].SSH))
|
||||
require.Equal(t, []string{"linux/arm64"}, c.Targets[1].Platforms)
|
||||
require.Equal(t, []string{"type=docker"}, c.Targets[1].Outputs)
|
||||
require.Equal(t, []string{"type=docker"}, stringify(c.Targets[1].Outputs))
|
||||
require.Equal(t, newBool(true), c.Targets[1].NoCache)
|
||||
require.Equal(t, ptrstr("128MiB"), c.Targets[1].ShmSize)
|
||||
require.Equal(t, []string{"nofile=1024:1024"}, c.Targets[1].Ulimits)
|
||||
}
|
||||
|
||||
func TestComposeExtDedup(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
webapp:
|
||||
image: app:bar
|
||||
@@ -383,9 +383,9 @@ services:
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, []string{"ct-addon:foo", "ct-addon:baz"}, c.Targets[0].Tags)
|
||||
require.Equal(t, []string{"user/app:cache", "type=local,src=path/to/cache"}, c.Targets[0].CacheFrom)
|
||||
require.Equal(t, []string{"user/app:cache", "type=local,dest=path/to/cache"}, c.Targets[0].CacheTo)
|
||||
require.Equal(t, []string{"default", "key=path/to/key"}, c.Targets[0].SSH)
|
||||
require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheFrom))
|
||||
require.Equal(t, []string{"type=local,dest=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheTo))
|
||||
require.Equal(t, []string{"default", "key=path/to/key"}, stringify(c.Targets[0].SSH))
|
||||
}
|
||||
|
||||
func TestEnv(t *testing.T) {
|
||||
@@ -396,7 +396,7 @@ func TestEnv(t *testing.T) {
|
||||
_, err = envf.WriteString("FOO=bsdf -csdf\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
scratch:
|
||||
build:
|
||||
@@ -424,7 +424,7 @@ func TestDotEnv(t *testing.T) {
|
||||
err := os.WriteFile(filepath.Join(tmpdir, ".env"), []byte("FOO=bar"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
scratch:
|
||||
build:
|
||||
@@ -443,7 +443,7 @@ services:
|
||||
}
|
||||
|
||||
func TestPorts(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
foo:
|
||||
build:
|
||||
@@ -664,7 +664,7 @@ target "default" {
|
||||
}
|
||||
|
||||
func TestComposeNullArgs(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
scratch:
|
||||
build:
|
||||
@@ -680,7 +680,7 @@ services:
|
||||
}
|
||||
|
||||
func TestDependsOn(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
foo:
|
||||
build:
|
||||
@@ -711,7 +711,7 @@ services:
|
||||
`), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
include:
|
||||
- compose-foo.yml
|
||||
|
||||
@@ -740,7 +740,7 @@ services:
|
||||
}
|
||||
|
||||
func TestDevelop(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
scratch:
|
||||
build:
|
||||
@@ -759,7 +759,7 @@ services:
|
||||
}
|
||||
|
||||
func TestCgroup(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
scratch:
|
||||
build:
|
||||
@@ -772,7 +772,7 @@ services:
|
||||
}
|
||||
|
||||
func TestProjectName(t *testing.T) {
|
||||
var dt = []byte(`
|
||||
dt := []byte(`
|
||||
services:
|
||||
scratch:
|
||||
build:
|
||||
|
@@ -2,15 +2,21 @@ package bake
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/docker/buildx/util/osutil"
|
||||
"github.com/moby/buildkit/util/entitlements"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@@ -67,10 +73,8 @@ func ParseEntitlements(in []string) (EntitlementConf, error) {
|
||||
conf.ImagePush = append(conf.ImagePush, v)
|
||||
conf.ImageLoad = append(conf.ImageLoad, v)
|
||||
default:
|
||||
return conf, errors.Errorf("uknown entitlement key %q", k)
|
||||
return conf, errors.Errorf("unknown entitlement key %q", k)
|
||||
}
|
||||
|
||||
// TODO: dedupe slices and parent paths
|
||||
}
|
||||
}
|
||||
return conf, nil
|
||||
@@ -101,10 +105,73 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rwPaths := map[string]struct{}{}
|
||||
roPaths := map[string]struct{}{}
|
||||
|
||||
for _, p := range collectLocalPaths(bo.Inputs) {
|
||||
roPaths[p] = struct{}{}
|
||||
}
|
||||
|
||||
for _, out := range bo.Exports {
|
||||
if out.Type == "local" {
|
||||
if dest, ok := out.Attrs["dest"]; ok {
|
||||
rwPaths[dest] = struct{}{}
|
||||
}
|
||||
}
|
||||
if out.Type == "tar" {
|
||||
if dest, ok := out.Attrs["dest"]; ok && dest != "-" {
|
||||
rwPaths[dest] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ce := range bo.CacheTo {
|
||||
if ce.Type == "local" {
|
||||
if dest, ok := ce.Attrs["dest"]; ok {
|
||||
rwPaths[dest] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ci := range bo.CacheFrom {
|
||||
if ci.Type == "local" {
|
||||
if src, ok := ci.Attrs["src"]; ok {
|
||||
roPaths[src] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, secret := range bo.SecretSpecs {
|
||||
if secret.FilePath != "" {
|
||||
roPaths[secret.FilePath] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ssh := range bo.SSHSpecs {
|
||||
for _, p := range ssh.Paths {
|
||||
roPaths[p] = struct{}{}
|
||||
}
|
||||
if len(ssh.Paths) == 0 {
|
||||
expected.SSH = true
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
expected.FSRead, err = findMissingPaths(c.FSRead, roPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expected.FSWrite, err = findMissingPaths(c.FSWrite, rwPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
|
||||
func (c EntitlementConf) Prompt(ctx context.Context, isRemote bool, out io.Writer) error {
|
||||
var term bool
|
||||
if _, err := console.ConsoleFromFile(os.Stdin); err == nil {
|
||||
term = true
|
||||
@@ -113,32 +180,78 @@ func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
|
||||
var msgs []string
|
||||
var flags []string
|
||||
|
||||
// these warnings are currently disabled to give users time to update
|
||||
var msgsFS []string
|
||||
var flagsFS []string
|
||||
|
||||
if c.NetworkHost {
|
||||
msgs = append(msgs, " - Running build containers that can access host network")
|
||||
flags = append(flags, "network.host")
|
||||
flags = append(flags, string(EntitlementKeyNetworkHost))
|
||||
}
|
||||
if c.SecurityInsecure {
|
||||
msgs = append(msgs, " - Running privileged containers that can make system changes")
|
||||
flags = append(flags, "security.insecure")
|
||||
flags = append(flags, string(EntitlementKeySecurityInsecure))
|
||||
}
|
||||
|
||||
if len(msgs) == 0 {
|
||||
if c.SSH {
|
||||
msgsFS = append(msgsFS, " - Forwarding default SSH agent socket")
|
||||
flagsFS = append(flagsFS, string(EntitlementKeySSH))
|
||||
}
|
||||
|
||||
roPaths, rwPaths, commonPaths := groupSamePaths(c.FSRead, c.FSWrite)
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get current working directory")
|
||||
}
|
||||
wd, err = filepath.EvalSymlinks(wd)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to evaluate working directory")
|
||||
}
|
||||
roPaths = toRelativePaths(roPaths, wd)
|
||||
rwPaths = toRelativePaths(rwPaths, wd)
|
||||
commonPaths = toRelativePaths(commonPaths, wd)
|
||||
|
||||
if len(commonPaths) > 0 {
|
||||
for _, p := range commonPaths {
|
||||
msgsFS = append(msgsFS, fmt.Sprintf(" - Read and write access to path %s", p))
|
||||
flagsFS = append(flagsFS, string(EntitlementKeyFS)+"="+p)
|
||||
}
|
||||
}
|
||||
|
||||
if len(roPaths) > 0 {
|
||||
for _, p := range roPaths {
|
||||
msgsFS = append(msgsFS, fmt.Sprintf(" - Read access to path %s", p))
|
||||
flagsFS = append(flagsFS, string(EntitlementKeyFSRead)+"="+p)
|
||||
}
|
||||
}
|
||||
|
||||
if len(rwPaths) > 0 {
|
||||
for _, p := range rwPaths {
|
||||
msgsFS = append(msgsFS, fmt.Sprintf(" - Write access to path %s", p))
|
||||
flagsFS = append(flagsFS, string(EntitlementKeyFSWrite)+"="+p)
|
||||
}
|
||||
}
|
||||
|
||||
if len(msgs) == 0 && len(msgsFS) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "Your build is requesting privileges for following possibly insecure capabilities:\n\n")
|
||||
for _, m := range msgs {
|
||||
for _, m := range slices.Concat(msgs, msgsFS) {
|
||||
fmt.Fprintf(out, "%s\n", m)
|
||||
}
|
||||
|
||||
for i, f := range flags {
|
||||
flags[i] = "--allow=" + f
|
||||
}
|
||||
for i, f := range flagsFS {
|
||||
flagsFS[i] = "--allow=" + f
|
||||
}
|
||||
|
||||
if term {
|
||||
fmt.Fprintf(out, "\nIn order to not see this message in the future pass %q to grant requested privileges.\n", strings.Join(flags, " "))
|
||||
fmt.Fprintf(out, "\nIn order to not see this message in the future pass %q to grant requested privileges.\n", strings.Join(slices.Concat(flags, flagsFS), " "))
|
||||
} else {
|
||||
fmt.Fprintf(out, "\nPass %q to grant requested privileges.\n", strings.Join(flags, " "))
|
||||
fmt.Fprintf(out, "\nPass %q to grant requested privileges.\n", strings.Join(slices.Concat(flags, flagsFS), " "))
|
||||
}
|
||||
|
||||
args := append([]string(nil), os.Args...)
|
||||
@@ -149,7 +262,35 @@ func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
|
||||
|
||||
if idx != -1 {
|
||||
fmt.Fprintf(out, "\nYour full command with requested privileges:\n\n")
|
||||
fmt.Fprintf(out, "%s %s %s\n\n", strings.Join(args[:idx+1], " "), strings.Join(flags, " "), strings.Join(args[idx+1:], " "))
|
||||
fmt.Fprintf(out, "%s %s %s\n\n", strings.Join(args[:idx+1], " "), strings.Join(slices.Concat(flags, flagsFS), " "), strings.Join(args[idx+1:], " "))
|
||||
}
|
||||
|
||||
fsEntitlementsEnabled := false
|
||||
if isRemote {
|
||||
if v, ok := os.LookupEnv("BAKE_ALLOW_REMOTE_FS_ACCESS"); ok {
|
||||
vv, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse BAKE_ALLOW_REMOTE_FS_ACCESS value %q", v)
|
||||
}
|
||||
fsEntitlementsEnabled = !vv
|
||||
} else {
|
||||
fsEntitlementsEnabled = true
|
||||
}
|
||||
}
|
||||
v, fsEntitlementsSet := os.LookupEnv("BUILDX_BAKE_ENTITLEMENTS_FS")
|
||||
if fsEntitlementsSet {
|
||||
vv, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse BUILDX_BAKE_ENTITLEMENTS_FS value %q", v)
|
||||
}
|
||||
fsEntitlementsEnabled = vv
|
||||
}
|
||||
|
||||
if !fsEntitlementsEnabled && len(msgs) == 0 {
|
||||
if !fsEntitlementsSet {
|
||||
fmt.Fprintf(out, "This warning will become an error in a future release. To enable filesystem entitlements checks at the moment, set BUILDX_BAKE_ENTITLEMENTS_FS=1 .\n\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if term {
|
||||
@@ -173,3 +314,288 @@ func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
|
||||
|
||||
return errors.Errorf("additional privileges requested")
|
||||
}
|
||||
|
||||
func isParentOrEqualPath(p, parent string) bool {
|
||||
if p == parent || parent == "/" {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(p, filepath.Clean(parent+string(filepath.Separator))) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func findMissingPaths(set []string, paths map[string]struct{}) ([]string, error) {
|
||||
set, allowAny, err := evaluatePaths(set)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if allowAny {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
paths, err = evaluateToExistingPaths(paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths, err = dedupPaths(paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(paths))
|
||||
loop0:
|
||||
for p := range paths {
|
||||
for _, c := range set {
|
||||
if isParentOrEqualPath(p, c) {
|
||||
continue loop0
|
||||
}
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
slices.Sort(out)
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func dedupPaths(in map[string]struct{}) (map[string]struct{}, error) {
|
||||
arr := make([]string, 0, len(in))
|
||||
for p := range in {
|
||||
arr = append(arr, filepath.Clean(p))
|
||||
}
|
||||
|
||||
slices.SortFunc(arr, func(a, b string) int {
|
||||
return cmp.Compare(len(a), len(b))
|
||||
})
|
||||
|
||||
m := make(map[string]struct{}, len(arr))
|
||||
loop0:
|
||||
for _, p := range arr {
|
||||
for parent := range m {
|
||||
if strings.HasPrefix(p, parent+string(filepath.Separator)) {
|
||||
continue loop0
|
||||
}
|
||||
}
|
||||
m[p] = struct{}{}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func toRelativePaths(in []string, wd string) []string {
|
||||
out := make([]string, 0, len(in))
|
||||
for _, p := range in {
|
||||
rel, err := filepath.Rel(wd, p)
|
||||
if err == nil {
|
||||
// allow up to one level of ".." in the path
|
||||
if !strings.HasPrefix(rel, ".."+string(filepath.Separator)+"..") {
|
||||
out = append(out, rel)
|
||||
continue
|
||||
}
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func groupSamePaths(in1, in2 []string) ([]string, []string, []string) {
|
||||
if in1 == nil || in2 == nil {
|
||||
return in1, in2, nil
|
||||
}
|
||||
|
||||
slices.Sort(in1)
|
||||
slices.Sort(in2)
|
||||
|
||||
common := []string{}
|
||||
i, j := 0, 0
|
||||
|
||||
for i < len(in1) && j < len(in2) {
|
||||
switch {
|
||||
case in1[i] == in2[j]:
|
||||
common = append(common, in1[i])
|
||||
i++
|
||||
j++
|
||||
case in1[i] < in2[j]:
|
||||
i++
|
||||
default:
|
||||
j++
|
||||
}
|
||||
}
|
||||
|
||||
in1 = removeCommonPaths(in1, common)
|
||||
in2 = removeCommonPaths(in2, common)
|
||||
|
||||
return in1, in2, common
|
||||
}
|
||||
|
||||
func removeCommonPaths(in, common []string) []string {
|
||||
filtered := make([]string, 0, len(in))
|
||||
commonIndex := 0
|
||||
for _, path := range in {
|
||||
if commonIndex < len(common) && path == common[commonIndex] {
|
||||
commonIndex++
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, path)
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func evaluatePaths(in []string) ([]string, bool, error) {
|
||||
out := make([]string, 0, len(in))
|
||||
allowAny := false
|
||||
for _, p := range in {
|
||||
if p == "*" {
|
||||
allowAny = true
|
||||
continue
|
||||
}
|
||||
v, err := filepath.Abs(p)
|
||||
if err != nil {
|
||||
return nil, false, errors.Wrapf(err, "failed to evaluate path %q", p)
|
||||
}
|
||||
v, err = filepath.EvalSymlinks(v)
|
||||
if err != nil {
|
||||
return nil, false, errors.Wrapf(err, "failed to evaluate path %q", p)
|
||||
}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out, allowAny, nil
|
||||
}
|
||||
|
||||
func evaluateToExistingPaths(in map[string]struct{}) (map[string]struct{}, error) {
|
||||
m := make(map[string]struct{}, len(in))
|
||||
for p := range in {
|
||||
v, err := evaluateToExistingPath(p)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to evaluate path %q", p)
|
||||
}
|
||||
v, err = osutil.GetLongPathName(v)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to evaluate path %q", p)
|
||||
}
|
||||
m[v] = struct{}{}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func evaluateToExistingPath(in string) (string, error) {
|
||||
in, err := filepath.Abs(in)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
volLen := volumeNameLen(in)
|
||||
pathSeparator := string(os.PathSeparator)
|
||||
|
||||
if volLen < len(in) && os.IsPathSeparator(in[volLen]) {
|
||||
volLen++
|
||||
}
|
||||
vol := in[:volLen]
|
||||
dest := vol
|
||||
linksWalked := 0
|
||||
var end int
|
||||
for start := volLen; start < len(in); start = end {
|
||||
for start < len(in) && os.IsPathSeparator(in[start]) {
|
||||
start++
|
||||
}
|
||||
end = start
|
||||
for end < len(in) && !os.IsPathSeparator(in[end]) {
|
||||
end++
|
||||
}
|
||||
|
||||
if end == start {
|
||||
break
|
||||
} else if in[start:end] == "." {
|
||||
continue
|
||||
} else if in[start:end] == ".." {
|
||||
var r int
|
||||
for r = len(dest) - 1; r >= volLen; r-- {
|
||||
if os.IsPathSeparator(dest[r]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if r < volLen || dest[r+1:] == ".." {
|
||||
if len(dest) > volLen {
|
||||
dest += pathSeparator
|
||||
}
|
||||
dest += ".."
|
||||
} else {
|
||||
dest = dest[:r]
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if len(dest) > volumeNameLen(dest) && !os.IsPathSeparator(dest[len(dest)-1]) {
|
||||
dest += pathSeparator
|
||||
}
|
||||
dest += in[start:end]
|
||||
|
||||
fi, err := os.Lstat(dest)
|
||||
if err != nil {
|
||||
// If the component doesn't exist, return the last valid path
|
||||
if os.IsNotExist(err) {
|
||||
for r := len(dest) - 1; r >= volLen; r-- {
|
||||
if os.IsPathSeparator(dest[r]) {
|
||||
return dest[:r], nil
|
||||
}
|
||||
}
|
||||
return vol, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
if fi.Mode()&fs.ModeSymlink == 0 {
|
||||
if !fi.Mode().IsDir() && end < len(in) {
|
||||
return "", syscall.ENOTDIR
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
linksWalked++
|
||||
if linksWalked > 255 {
|
||||
return "", errors.New("too many symlinks")
|
||||
}
|
||||
|
||||
link, err := os.Readlink(dest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
in = link + in[end:]
|
||||
|
||||
v := volumeNameLen(link)
|
||||
if v > 0 {
|
||||
if v < len(link) && os.IsPathSeparator(link[v]) {
|
||||
v++
|
||||
}
|
||||
vol = link[:v]
|
||||
dest = vol
|
||||
end = len(vol)
|
||||
} else if len(link) > 0 && os.IsPathSeparator(link[0]) {
|
||||
dest = link[:1]
|
||||
end = 1
|
||||
vol = link[:1]
|
||||
volLen = 1
|
||||
} else {
|
||||
var r int
|
||||
for r = len(dest) - 1; r >= volLen; r-- {
|
||||
if os.IsPathSeparator(dest[r]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if r < volLen {
|
||||
dest = vol
|
||||
} else {
|
||||
dest = dest[:r]
|
||||
}
|
||||
end = 0
|
||||
}
|
||||
}
|
||||
return filepath.Clean(dest), nil
|
||||
}
|
||||
|
||||
func volumeNameLen(s string) int {
|
||||
return len(filepath.VolumeName(s))
|
||||
}
|
||||
|
460
bake/entitlements_test.go
Normal file
460
bake/entitlements_test.go
Normal file
@@ -0,0 +1,460 @@
|
||||
package bake
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/docker/buildx/controller/pb"
|
||||
"github.com/docker/buildx/util/osutil"
|
||||
"github.com/moby/buildkit/client"
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/util/entitlements"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEvaluateToExistingPath(t *testing.T) {
|
||||
tempDir, err := osutil.GetLongPathName(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Setup temporary directory structure for testing
|
||||
existingFile := filepath.Join(tempDir, "existing_file")
|
||||
require.NoError(t, os.WriteFile(existingFile, []byte("test"), 0644))
|
||||
|
||||
existingDir := filepath.Join(tempDir, "existing_dir")
|
||||
require.NoError(t, os.Mkdir(existingDir, 0755))
|
||||
|
||||
symlinkToFile := filepath.Join(tempDir, "symlink_to_file")
|
||||
require.NoError(t, os.Symlink(existingFile, symlinkToFile))
|
||||
|
||||
symlinkToDir := filepath.Join(tempDir, "symlink_to_dir")
|
||||
require.NoError(t, os.Symlink(existingDir, symlinkToDir))
|
||||
|
||||
nonexistentPath := filepath.Join(tempDir, "nonexistent", "path", "file.txt")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "Existing file",
|
||||
input: existingFile,
|
||||
expected: existingFile,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Existing directory",
|
||||
input: existingDir,
|
||||
expected: existingDir,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Symlink to file",
|
||||
input: symlinkToFile,
|
||||
expected: existingFile,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Symlink to directory",
|
||||
input: symlinkToDir,
|
||||
expected: existingDir,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Non-existent path",
|
||||
input: nonexistentPath,
|
||||
expected: tempDir,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Non-existent intermediate path",
|
||||
input: filepath.Join(tempDir, "nonexistent", "file.txt"),
|
||||
expected: tempDir,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Root path",
|
||||
input: "/",
|
||||
expected: func() string {
|
||||
root, _ := filepath.Abs("/")
|
||||
return root
|
||||
}(),
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := evaluateToExistingPath(tt.input)
|
||||
|
||||
if tt.expectErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDedupePaths(t *testing.T) {
|
||||
wd := osutil.GetWd()
|
||||
tcases := []struct {
|
||||
in map[string]struct{}
|
||||
out map[string]struct{}
|
||||
}{
|
||||
{
|
||||
in: map[string]struct{}{
|
||||
"/a/b/c": {},
|
||||
"/a/b/d": {},
|
||||
"/a/b/e": {},
|
||||
},
|
||||
out: map[string]struct{}{
|
||||
"/a/b/c": {},
|
||||
"/a/b/d": {},
|
||||
"/a/b/e": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
in: map[string]struct{}{
|
||||
"/a/b/c": {},
|
||||
"/a/b/c/d": {},
|
||||
"/a/b/c/d/e": {},
|
||||
"/a/b/../b/c": {},
|
||||
},
|
||||
out: map[string]struct{}{
|
||||
"/a/b/c": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
in: map[string]struct{}{
|
||||
filepath.Join(wd, "a/b/c"): {},
|
||||
filepath.Join(wd, "../aa"): {},
|
||||
filepath.Join(wd, "a/b"): {},
|
||||
filepath.Join(wd, "a/b/d"): {},
|
||||
filepath.Join(wd, "../aa/b"): {},
|
||||
filepath.Join(wd, "../../bb"): {},
|
||||
},
|
||||
out: map[string]struct{}{
|
||||
"a/b": {},
|
||||
"../aa": {},
|
||||
filepath.Join(wd, "../../bb"): {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range tcases {
|
||||
t.Run(fmt.Sprintf("case%d", i), func(t *testing.T) {
|
||||
out, err := dedupPaths(tc.in)
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
// convert to relative paths as that is shown to user
|
||||
arr := make([]string, 0, len(out))
|
||||
for k := range out {
|
||||
arr = append(arr, k)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
arr = toRelativePaths(arr, wd)
|
||||
m := make(map[string]struct{})
|
||||
for _, v := range arr {
|
||||
m[filepath.ToSlash(v)] = struct{}{}
|
||||
}
|
||||
o := make(map[string]struct{}, len(tc.out))
|
||||
for k := range tc.out {
|
||||
o[filepath.ToSlash(k)] = struct{}{}
|
||||
}
|
||||
require.Equal(t, o, m)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateEntitlements(t *testing.T) {
|
||||
dir1 := t.TempDir()
|
||||
dir2 := t.TempDir()
|
||||
|
||||
// the paths returned by entitlements validation will have symlinks resolved
|
||||
expDir1, err := filepath.EvalSymlinks(dir1)
|
||||
require.NoError(t, err)
|
||||
expDir2, err := filepath.EvalSymlinks(dir2)
|
||||
require.NoError(t, err)
|
||||
|
||||
escapeLink := filepath.Join(dir1, "escape_link")
|
||||
require.NoError(t, os.Symlink("../../aa", escapeLink))
|
||||
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
expWd, err := filepath.EvalSymlinks(wd)
|
||||
require.NoError(t, err)
|
||||
|
||||
tcases := []struct {
|
||||
name string
|
||||
conf EntitlementConf
|
||||
opt build.Options
|
||||
expected EntitlementConf
|
||||
}{
|
||||
{
|
||||
name: "No entitlements",
|
||||
opt: build.Options{
|
||||
Inputs: build.Inputs{
|
||||
ContextState: &llb.State{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NetworkHostMissing",
|
||||
opt: build.Options{
|
||||
Allow: []entitlements.Entitlement{
|
||||
entitlements.EntitlementNetworkHost,
|
||||
},
|
||||
},
|
||||
expected: EntitlementConf{
|
||||
NetworkHost: true,
|
||||
FSRead: []string{expWd},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NetworkHostSet",
|
||||
conf: EntitlementConf{
|
||||
NetworkHost: true,
|
||||
},
|
||||
opt: build.Options{
|
||||
Allow: []entitlements.Entitlement{
|
||||
entitlements.EntitlementNetworkHost,
|
||||
},
|
||||
},
|
||||
expected: EntitlementConf{
|
||||
FSRead: []string{expWd},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SecurityAndNetworkHostMissing",
|
||||
opt: build.Options{
|
||||
Allow: []entitlements.Entitlement{
|
||||
entitlements.EntitlementNetworkHost,
|
||||
entitlements.EntitlementSecurityInsecure,
|
||||
},
|
||||
},
|
||||
expected: EntitlementConf{
|
||||
NetworkHost: true,
|
||||
SecurityInsecure: true,
|
||||
FSRead: []string{expWd},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SecurityMissingAndNetworkHostSet",
|
||||
conf: EntitlementConf{
|
||||
NetworkHost: true,
|
||||
},
|
||||
opt: build.Options{
|
||||
Allow: []entitlements.Entitlement{
|
||||
entitlements.EntitlementNetworkHost,
|
||||
entitlements.EntitlementSecurityInsecure,
|
||||
},
|
||||
},
|
||||
expected: EntitlementConf{
|
||||
SecurityInsecure: true,
|
||||
FSRead: []string{expWd},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SSHMissing",
|
||||
opt: build.Options{
|
||||
SSHSpecs: []*pb.SSH{
|
||||
{
|
||||
ID: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EntitlementConf{
|
||||
SSH: true,
|
||||
FSRead: []string{expWd},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ExportLocal",
|
||||
opt: build.Options{
|
||||
Exports: []client.ExportEntry{
|
||||
{
|
||||
Type: "local",
|
||||
Attrs: map[string]string{
|
||||
"dest": dir1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "local",
|
||||
Attrs: map[string]string{
|
||||
"dest": filepath.Join(dir1, "subdir"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "local",
|
||||
Attrs: map[string]string{
|
||||
"dest": dir2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EntitlementConf{
|
||||
FSWrite: func() []string {
|
||||
exp := []string{expDir1, expDir2}
|
||||
slices.Sort(exp)
|
||||
return exp
|
||||
}(),
|
||||
FSRead: []string{expWd},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SecretFromSubFile",
|
||||
opt: build.Options{
|
||||
SecretSpecs: []*pb.Secret{
|
||||
{
|
||||
FilePath: filepath.Join(dir1, "subfile"),
|
||||
},
|
||||
},
|
||||
},
|
||||
conf: EntitlementConf{
|
||||
FSRead: []string{wd, dir1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SecretFromEscapeLink",
|
||||
opt: build.Options{
|
||||
SecretSpecs: []*pb.Secret{
|
||||
{
|
||||
FilePath: escapeLink,
|
||||
},
|
||||
},
|
||||
},
|
||||
conf: EntitlementConf{
|
||||
FSRead: []string{wd, dir1},
|
||||
},
|
||||
expected: EntitlementConf{
|
||||
FSRead: []string{filepath.Join(expDir1, "../..")},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SecretFromEscapeLinkAllowRoot",
|
||||
opt: build.Options{
|
||||
SecretSpecs: []*pb.Secret{
|
||||
{
|
||||
FilePath: escapeLink,
|
||||
},
|
||||
},
|
||||
},
|
||||
conf: EntitlementConf{
|
||||
FSRead: []string{"/"},
|
||||
},
|
||||
expected: EntitlementConf{
|
||||
FSRead: func() []string {
|
||||
// on windows root (/) is only allowed if it is the same volume as wd
|
||||
if filepath.VolumeName(wd) == filepath.VolumeName(escapeLink) {
|
||||
return nil
|
||||
}
|
||||
// if not, then escapeLink is not allowed
|
||||
exp, err := evaluateToExistingPath(escapeLink)
|
||||
require.NoError(t, err)
|
||||
exp, err = filepath.EvalSymlinks(exp)
|
||||
require.NoError(t, err)
|
||||
return []string{exp}
|
||||
}(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SecretFromEscapeLinkAllowAny",
|
||||
opt: build.Options{
|
||||
SecretSpecs: []*pb.Secret{
|
||||
{
|
||||
FilePath: escapeLink,
|
||||
},
|
||||
},
|
||||
},
|
||||
conf: EntitlementConf{
|
||||
FSRead: []string{"*"},
|
||||
},
|
||||
expected: EntitlementConf{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
expected, err := tc.conf.Validate(map[string]build.Options{"test": tc.opt})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expected, expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupSamePaths(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in1 []string
|
||||
in2 []string
|
||||
expected1 []string
|
||||
expected2 []string
|
||||
expectedC []string
|
||||
}{
|
||||
{
|
||||
name: "All common paths",
|
||||
in1: []string{"/path/a", "/path/b", "/path/c"},
|
||||
in2: []string{"/path/a", "/path/b", "/path/c"},
|
||||
expected1: []string{},
|
||||
expected2: []string{},
|
||||
expectedC: []string{"/path/a", "/path/b", "/path/c"},
|
||||
},
|
||||
{
|
||||
name: "No common paths",
|
||||
in1: []string{"/path/a", "/path/b"},
|
||||
in2: []string{"/path/c", "/path/d"},
|
||||
expected1: []string{"/path/a", "/path/b"},
|
||||
expected2: []string{"/path/c", "/path/d"},
|
||||
expectedC: []string{},
|
||||
},
|
||||
{
|
||||
name: "Some common paths",
|
||||
in1: []string{"/path/a", "/path/b", "/path/c"},
|
||||
in2: []string{"/path/b", "/path/c", "/path/d"},
|
||||
expected1: []string{"/path/a"},
|
||||
expected2: []string{"/path/d"},
|
||||
expectedC: []string{"/path/b", "/path/c"},
|
||||
},
|
||||
{
|
||||
name: "Empty inputs",
|
||||
in1: []string{},
|
||||
in2: []string{},
|
||||
expected1: []string{},
|
||||
expected2: []string{},
|
||||
expectedC: []string{},
|
||||
},
|
||||
{
|
||||
name: "One empty input",
|
||||
in1: []string{"/path/a", "/path/b"},
|
||||
in2: []string{},
|
||||
expected1: []string{"/path/a", "/path/b"},
|
||||
expected2: []string{},
|
||||
expectedC: []string{},
|
||||
},
|
||||
{
|
||||
name: "Unsorted inputs with common paths",
|
||||
in1: []string{"/path/c", "/path/a", "/path/b"},
|
||||
in2: []string{"/path/b", "/path/c", "/path/a"},
|
||||
expected1: []string{},
|
||||
expected2: []string{},
|
||||
expectedC: []string{"/path/a", "/path/b", "/path/c"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
out1, out2, common := groupSamePaths(tt.in1, tt.in2)
|
||||
require.Equal(t, tt.expected1, out1, "in1 should match expected1")
|
||||
require.Equal(t, tt.expected2, out2, "in2 should match expected2")
|
||||
require.Equal(t, tt.expectedC, common, "common should match expectedC")
|
||||
})
|
||||
}
|
||||
}
|
209
bake/hcl_test.go
209
bake/hcl_test.go
@@ -17,6 +17,7 @@ func TestHCLBasic(t *testing.T) {
|
||||
target "db" {
|
||||
context = "./db"
|
||||
tags = ["docker.io/tonistiigi/db"]
|
||||
output = ["type=image"]
|
||||
}
|
||||
|
||||
target "webapp" {
|
||||
@@ -25,6 +26,9 @@ func TestHCLBasic(t *testing.T) {
|
||||
args = {
|
||||
buildno = "123"
|
||||
}
|
||||
output = [
|
||||
{ type = "image" }
|
||||
]
|
||||
}
|
||||
|
||||
target "cross" {
|
||||
@@ -49,18 +53,18 @@ func TestHCLBasic(t *testing.T) {
|
||||
require.Equal(t, []string{"db", "webapp"}, c.Groups[0].Targets)
|
||||
|
||||
require.Equal(t, 4, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "db")
|
||||
require.Equal(t, "db", c.Targets[0].Name)
|
||||
require.Equal(t, "./db", *c.Targets[0].Context)
|
||||
|
||||
require.Equal(t, c.Targets[1].Name, "webapp")
|
||||
require.Equal(t, "webapp", c.Targets[1].Name)
|
||||
require.Equal(t, 1, len(c.Targets[1].Args))
|
||||
require.Equal(t, ptrstr("123"), c.Targets[1].Args["buildno"])
|
||||
|
||||
require.Equal(t, c.Targets[2].Name, "cross")
|
||||
require.Equal(t, "cross", c.Targets[2].Name)
|
||||
require.Equal(t, 2, len(c.Targets[2].Platforms))
|
||||
require.Equal(t, []string{"linux/amd64", "linux/arm64"}, c.Targets[2].Platforms)
|
||||
|
||||
require.Equal(t, c.Targets[3].Name, "webapp-plus")
|
||||
require.Equal(t, "webapp-plus", c.Targets[3].Name)
|
||||
require.Equal(t, 1, len(c.Targets[3].Args))
|
||||
require.Equal(t, map[string]*string{"IAMCROSS": ptrstr("true")}, c.Targets[3].Args)
|
||||
}
|
||||
@@ -109,18 +113,18 @@ func TestHCLBasicInJSON(t *testing.T) {
|
||||
require.Equal(t, []string{"db", "webapp"}, c.Groups[0].Targets)
|
||||
|
||||
require.Equal(t, 4, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "db")
|
||||
require.Equal(t, "db", c.Targets[0].Name)
|
||||
require.Equal(t, "./db", *c.Targets[0].Context)
|
||||
|
||||
require.Equal(t, c.Targets[1].Name, "webapp")
|
||||
require.Equal(t, "webapp", c.Targets[1].Name)
|
||||
require.Equal(t, 1, len(c.Targets[1].Args))
|
||||
require.Equal(t, ptrstr("123"), c.Targets[1].Args["buildno"])
|
||||
|
||||
require.Equal(t, c.Targets[2].Name, "cross")
|
||||
require.Equal(t, "cross", c.Targets[2].Name)
|
||||
require.Equal(t, 2, len(c.Targets[2].Platforms))
|
||||
require.Equal(t, []string{"linux/amd64", "linux/arm64"}, c.Targets[2].Platforms)
|
||||
|
||||
require.Equal(t, c.Targets[3].Name, "webapp-plus")
|
||||
require.Equal(t, "webapp-plus", c.Targets[3].Name)
|
||||
require.Equal(t, 1, len(c.Targets[3].Args))
|
||||
require.Equal(t, map[string]*string{"IAMCROSS": ptrstr("true")}, c.Targets[3].Args)
|
||||
}
|
||||
@@ -146,7 +150,7 @@ func TestHCLWithFunctions(t *testing.T) {
|
||||
require.Equal(t, []string{"webapp"}, c.Groups[0].Targets)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "webapp")
|
||||
require.Equal(t, "webapp", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("124"), c.Targets[0].Args["buildno"])
|
||||
}
|
||||
|
||||
@@ -176,7 +180,7 @@ func TestHCLWithUserDefinedFunctions(t *testing.T) {
|
||||
require.Equal(t, []string{"webapp"}, c.Groups[0].Targets)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "webapp")
|
||||
require.Equal(t, "webapp", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("124"), c.Targets[0].Args["buildno"])
|
||||
}
|
||||
|
||||
@@ -205,7 +209,7 @@ func TestHCLWithVariables(t *testing.T) {
|
||||
require.Equal(t, []string{"webapp"}, c.Groups[0].Targets)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "webapp")
|
||||
require.Equal(t, "webapp", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("123"), c.Targets[0].Args["buildno"])
|
||||
|
||||
t.Setenv("BUILD_NUMBER", "456")
|
||||
@@ -218,7 +222,7 @@ func TestHCLWithVariables(t *testing.T) {
|
||||
require.Equal(t, []string{"webapp"}, c.Groups[0].Targets)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "webapp")
|
||||
require.Equal(t, "webapp", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("456"), c.Targets[0].Args["buildno"])
|
||||
}
|
||||
|
||||
@@ -241,7 +245,7 @@ func TestHCLWithVariablesInFunctions(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "webapp")
|
||||
require.Equal(t, "webapp", c.Targets[0].Name)
|
||||
require.Equal(t, []string{"user/repo:v1"}, c.Targets[0].Tags)
|
||||
|
||||
t.Setenv("REPO", "docker/buildx")
|
||||
@@ -250,7 +254,7 @@ func TestHCLWithVariablesInFunctions(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "webapp")
|
||||
require.Equal(t, "webapp", c.Targets[0].Name)
|
||||
require.Equal(t, []string{"docker/buildx:v1"}, c.Targets[0].Tags)
|
||||
}
|
||||
|
||||
@@ -279,7 +283,7 @@ func TestHCLMultiFileSharedVariables(t *testing.T) {
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("pre-abc"), c.Targets[0].Args["v1"])
|
||||
require.Equal(t, ptrstr("abc-post"), c.Targets[0].Args["v2"])
|
||||
|
||||
@@ -292,7 +296,7 @@ func TestHCLMultiFileSharedVariables(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("pre-def"), c.Targets[0].Args["v1"])
|
||||
require.Equal(t, ptrstr("def-post"), c.Targets[0].Args["v2"])
|
||||
}
|
||||
@@ -328,7 +332,7 @@ func TestHCLVarsWithVars(t *testing.T) {
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("pre--ABCDEF-"), c.Targets[0].Args["v1"])
|
||||
require.Equal(t, ptrstr("ABCDEF-post"), c.Targets[0].Args["v2"])
|
||||
|
||||
@@ -341,7 +345,7 @@ func TestHCLVarsWithVars(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("pre--NEWDEF-"), c.Targets[0].Args["v1"])
|
||||
require.Equal(t, ptrstr("NEWDEF-post"), c.Targets[0].Args["v2"])
|
||||
}
|
||||
@@ -366,7 +370,7 @@ func TestHCLTypedVariables(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("lower"), c.Targets[0].Args["v1"])
|
||||
require.Equal(t, ptrstr("yes"), c.Targets[0].Args["v2"])
|
||||
|
||||
@@ -377,7 +381,7 @@ func TestHCLTypedVariables(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("higher"), c.Targets[0].Args["v1"])
|
||||
require.Equal(t, ptrstr("no"), c.Targets[0].Args["v2"])
|
||||
|
||||
@@ -475,7 +479,7 @@ func TestHCLAttrs(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("attr-abcdef"), c.Targets[0].Args["v1"])
|
||||
|
||||
// env does not apply if no variable
|
||||
@@ -484,7 +488,7 @@ func TestHCLAttrs(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("attr-abcdef"), c.Targets[0].Args["v1"])
|
||||
// attr-multifile
|
||||
}
|
||||
@@ -592,11 +596,118 @@ func TestHCLAttrsCustomType(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, []string{"linux/arm64", "linux/amd64"}, c.Targets[0].Platforms)
|
||||
require.Equal(t, ptrstr("linux/arm64"), c.Targets[0].Args["v1"])
|
||||
}
|
||||
|
||||
func TestHCLAttrsCapsuleType(t *testing.T) {
|
||||
dt := []byte(`
|
||||
target "app" {
|
||||
cache-from = [
|
||||
{ type = "registry", ref = "user/app:cache" },
|
||||
{ type = "local", src = "path/to/cache" },
|
||||
]
|
||||
|
||||
cache-to = [
|
||||
{ type = "local", dest = "path/to/cache" },
|
||||
]
|
||||
|
||||
output = [
|
||||
{ type = "oci", dest = "../out.tar" },
|
||||
]
|
||||
|
||||
secret = [
|
||||
{ id = "mysecret", src = "/local/secret" },
|
||||
{ id = "mysecret2", env = "TOKEN" },
|
||||
]
|
||||
|
||||
ssh = [
|
||||
{ id = "default" },
|
||||
{ id = "key", paths = ["path/to/key"] },
|
||||
]
|
||||
}
|
||||
`)
|
||||
|
||||
c, err := ParseFile(dt, "docker-bake.hcl")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, []string{"type=oci,dest=../out.tar"}, stringify(c.Targets[0].Outputs))
|
||||
require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheFrom))
|
||||
require.Equal(t, []string{"type=local,dest=path/to/cache"}, stringify(c.Targets[0].CacheTo))
|
||||
require.Equal(t, []string{"id=mysecret,src=/local/secret", "id=mysecret2,env=TOKEN"}, stringify(c.Targets[0].Secrets))
|
||||
require.Equal(t, []string{"default", "key=path/to/key"}, stringify(c.Targets[0].SSH))
|
||||
}
|
||||
|
||||
func TestHCLAttrsCapsuleTypeVars(t *testing.T) {
|
||||
dt := []byte(`
|
||||
variable "foo" {
|
||||
default = "bar"
|
||||
}
|
||||
|
||||
target "app" {
|
||||
cache-from = [
|
||||
{ type = "registry", ref = "user/app:cache" },
|
||||
{ type = "local", src = "path/to/cache" },
|
||||
]
|
||||
|
||||
cache-to = [ target.app.cache-from[0] ]
|
||||
|
||||
output = [
|
||||
{ type = "oci", dest = "../out.tar" },
|
||||
]
|
||||
|
||||
secret = [
|
||||
{ id = "mysecret", src = "/local/secret" },
|
||||
]
|
||||
|
||||
ssh = [
|
||||
{ id = "default" },
|
||||
{ id = "key", paths = ["path/to/${target.app.output[0].type}"] },
|
||||
]
|
||||
}
|
||||
|
||||
target "web" {
|
||||
cache-from = target.app.cache-from
|
||||
|
||||
output = [ "type=oci,dest=../${foo}.tar" ]
|
||||
|
||||
secret = [
|
||||
{ id = target.app.output[0].type, src = "/local/secret" },
|
||||
]
|
||||
}
|
||||
`)
|
||||
|
||||
c, err := ParseFile(dt, "docker-bake.hcl")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 2, len(c.Targets))
|
||||
|
||||
findTarget := func(t *testing.T, name string) *Target {
|
||||
t.Helper()
|
||||
for _, tgt := range c.Targets {
|
||||
if tgt.Name == name {
|
||||
return tgt
|
||||
}
|
||||
}
|
||||
t.Fatalf("could not find target %q", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
app := findTarget(t, "app")
|
||||
require.Equal(t, []string{"type=oci,dest=../out.tar"}, stringify(app.Outputs))
|
||||
require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(app.CacheFrom))
|
||||
require.Equal(t, []string{"user/app:cache"}, stringify(app.CacheTo))
|
||||
require.Equal(t, []string{"id=mysecret,src=/local/secret"}, stringify(app.Secrets))
|
||||
require.Equal(t, []string{"default", "key=path/to/oci"}, stringify(app.SSH))
|
||||
|
||||
web := findTarget(t, "web")
|
||||
require.Equal(t, []string{"type=oci,dest=../bar.tar"}, stringify(web.Outputs))
|
||||
require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(web.CacheFrom))
|
||||
require.Equal(t, []string{"id=oci,src=/local/secret"}, stringify(web.Secrets))
|
||||
}
|
||||
|
||||
func TestHCLMultiFileAttrs(t *testing.T) {
|
||||
dt := []byte(`
|
||||
variable "FOO" {
|
||||
@@ -618,7 +729,7 @@ func TestHCLMultiFileAttrs(t *testing.T) {
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("pre-def"), c.Targets[0].Args["v1"])
|
||||
|
||||
t.Setenv("FOO", "ghi")
|
||||
@@ -630,7 +741,7 @@ func TestHCLMultiFileAttrs(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("pre-ghi"), c.Targets[0].Args["v1"])
|
||||
}
|
||||
|
||||
@@ -653,7 +764,7 @@ func TestHCLMultiFileGlobalAttrs(t *testing.T) {
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, "pre-def", *c.Targets[0].Args["v1"])
|
||||
}
|
||||
|
||||
@@ -839,12 +950,12 @@ func TestHCLRenameMultiFile(t *testing.T) {
|
||||
|
||||
require.Equal(t, 2, len(c.Targets))
|
||||
|
||||
require.Equal(t, c.Targets[0].Name, "bar")
|
||||
require.Equal(t, *c.Targets[0].Dockerfile, "x")
|
||||
require.Equal(t, *c.Targets[0].Target, "z")
|
||||
require.Equal(t, "bar", c.Targets[0].Name)
|
||||
require.Equal(t, "x", *c.Targets[0].Dockerfile)
|
||||
require.Equal(t, "z", *c.Targets[0].Target)
|
||||
|
||||
require.Equal(t, c.Targets[1].Name, "foo")
|
||||
require.Equal(t, *c.Targets[1].Context, "y")
|
||||
require.Equal(t, "foo", c.Targets[1].Name)
|
||||
require.Equal(t, "y", *c.Targets[1].Context)
|
||||
}
|
||||
|
||||
func TestHCLMatrixBasic(t *testing.T) {
|
||||
@@ -862,10 +973,10 @@ func TestHCLMatrixBasic(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 2, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "x")
|
||||
require.Equal(t, c.Targets[1].Name, "y")
|
||||
require.Equal(t, *c.Targets[0].Dockerfile, "x.Dockerfile")
|
||||
require.Equal(t, *c.Targets[1].Dockerfile, "y.Dockerfile")
|
||||
require.Equal(t, "x", c.Targets[0].Name)
|
||||
require.Equal(t, "y", c.Targets[1].Name)
|
||||
require.Equal(t, "x.Dockerfile", *c.Targets[0].Dockerfile)
|
||||
require.Equal(t, "y.Dockerfile", *c.Targets[1].Dockerfile)
|
||||
|
||||
require.Equal(t, 1, len(c.Groups))
|
||||
require.Equal(t, "default", c.Groups[0].Name)
|
||||
@@ -948,9 +1059,9 @@ func TestHCLMatrixMaps(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 2, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "aa")
|
||||
require.Equal(t, "aa", c.Targets[0].Name)
|
||||
require.Equal(t, c.Targets[0].Args["target"], ptrstr("valbb"))
|
||||
require.Equal(t, c.Targets[1].Name, "cc")
|
||||
require.Equal(t, "cc", c.Targets[1].Name)
|
||||
require.Equal(t, c.Targets[1].Args["target"], ptrstr("valdd"))
|
||||
}
|
||||
|
||||
@@ -1141,7 +1252,7 @@ func TestJSONAttributes(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("pre-abc-def"), c.Targets[0].Args["v1"])
|
||||
}
|
||||
|
||||
@@ -1166,7 +1277,7 @@ func TestJSONFunctions(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("pre-<FOO-abc>"), c.Targets[0].Args["v1"])
|
||||
}
|
||||
|
||||
@@ -1184,7 +1295,7 @@ func TestJSONInvalidFunctions(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr(`myfunc("foo")`), c.Targets[0].Args["v1"])
|
||||
}
|
||||
|
||||
@@ -1212,7 +1323,7 @@ func TestHCLFunctionInAttr(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("FOO <> [baz]"), c.Targets[0].Args["v1"])
|
||||
}
|
||||
|
||||
@@ -1243,7 +1354,7 @@ services:
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, ptrstr("foo"), c.Targets[0].Args["v1"])
|
||||
require.Equal(t, ptrstr("bar"), c.Targets[0].Args["v2"])
|
||||
require.Equal(t, "dir", *c.Targets[0].Context)
|
||||
@@ -1266,7 +1377,7 @@ func TestHCLBuiltinVars(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(c.Targets))
|
||||
require.Equal(t, c.Targets[0].Name, "app")
|
||||
require.Equal(t, "app", c.Targets[0].Name)
|
||||
require.Equal(t, "foo", *c.Targets[0].Context)
|
||||
require.Equal(t, "test", *c.Targets[0].Dockerfile)
|
||||
}
|
||||
@@ -1332,17 +1443,17 @@ target "b" {
|
||||
|
||||
require.Equal(t, 4, len(c.Targets))
|
||||
|
||||
require.Equal(t, c.Targets[0].Name, "metadata-a")
|
||||
require.Equal(t, "metadata-a", c.Targets[0].Name)
|
||||
require.Equal(t, []string{"app/a:1.0.0", "app/a:latest"}, c.Targets[0].Tags)
|
||||
|
||||
require.Equal(t, c.Targets[1].Name, "metadata-b")
|
||||
require.Equal(t, "metadata-b", c.Targets[1].Name)
|
||||
require.Equal(t, []string{"app/b:1.0.0", "app/b:latest"}, c.Targets[1].Tags)
|
||||
|
||||
require.Equal(t, c.Targets[2].Name, "a")
|
||||
require.Equal(t, "a", c.Targets[2].Name)
|
||||
require.Equal(t, ".", *c.Targets[2].Context)
|
||||
require.Equal(t, "a", *c.Targets[2].Target)
|
||||
|
||||
require.Equal(t, c.Targets[3].Name, "b")
|
||||
require.Equal(t, "b", c.Targets[3].Name)
|
||||
require.Equal(t, ".", *c.Targets[3].Context)
|
||||
require.Equal(t, "b", *c.Targets[3].Target)
|
||||
}
|
||||
@@ -1389,10 +1500,10 @@ target "two" {
|
||||
|
||||
require.Equal(t, 2, len(c.Targets))
|
||||
|
||||
require.Equal(t, c.Targets[0].Name, "one")
|
||||
require.Equal(t, "one", c.Targets[0].Name)
|
||||
require.Equal(t, map[string]*string{"a": ptrstr("pre-ghi-jkl")}, c.Targets[0].Args)
|
||||
|
||||
require.Equal(t, c.Targets[1].Name, "two")
|
||||
require.Equal(t, "two", c.Targets[1].Name)
|
||||
require.Equal(t, map[string]*string{"b": ptrstr("pre-jkl")}, c.Targets[1].Args)
|
||||
}
|
||||
|
||||
|
355
bake/hclparser/LICENSE
Normal file
355
bake/hclparser/LICENSE
Normal file
@@ -0,0 +1,355 @@
|
||||
Copyright (c) 2014 HashiCorp, Inc.
|
||||
|
||||
Mozilla Public License, version 2.0
|
||||
|
||||
1. Definitions
|
||||
|
||||
1.1. “Contributor”
|
||||
|
||||
means each individual or legal entity that creates, contributes to the
|
||||
creation of, or owns Covered Software.
|
||||
|
||||
1.2. “Contributor Version”
|
||||
|
||||
means the combination of the Contributions of others (if any) used by a
|
||||
Contributor and that particular Contributor’s Contribution.
|
||||
|
||||
1.3. “Contribution”
|
||||
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. “Covered Software”
|
||||
|
||||
means Source Code Form to which the initial Contributor has attached the
|
||||
notice in Exhibit A, the Executable Form of such Source Code Form, and
|
||||
Modifications of such Source Code Form, in each case including portions
|
||||
thereof.
|
||||
|
||||
1.5. “Incompatible With Secondary Licenses”
|
||||
means
|
||||
|
||||
a. that the initial Contributor has attached the notice described in
|
||||
Exhibit B to the Covered Software; or
|
||||
|
||||
b. that the Covered Software was made available under the terms of version
|
||||
1.1 or earlier of the License, but not also under the terms of a
|
||||
Secondary License.
|
||||
|
||||
1.6. “Executable Form”
|
||||
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. “Larger Work”
|
||||
|
||||
means a work that combines Covered Software with other material, in a separate
|
||||
file or files, that is not Covered Software.
|
||||
|
||||
1.8. “License”
|
||||
|
||||
means this document.
|
||||
|
||||
1.9. “Licensable”
|
||||
|
||||
means having the right to grant, to the maximum extent possible, whether at the
|
||||
time of the initial grant or subsequently, any and all of the rights conveyed by
|
||||
this License.
|
||||
|
||||
1.10. “Modifications”
|
||||
|
||||
means any of the following:
|
||||
|
||||
a. any file in Source Code Form that results from an addition to, deletion
|
||||
from, or modification of the contents of Covered Software; or
|
||||
|
||||
b. any new file in Source Code Form that contains any Covered Software.
|
||||
|
||||
1.11. “Patent Claims” of a Contributor
|
||||
|
||||
means any patent claim(s), including without limitation, method, process,
|
||||
and apparatus claims, in any patent Licensable by such Contributor that
|
||||
would be infringed, but for the grant of the License, by the making,
|
||||
using, selling, offering for sale, having made, import, or transfer of
|
||||
either its Contributions or its Contributor Version.
|
||||
|
||||
1.12. “Secondary License”
|
||||
|
||||
means either the GNU General Public License, Version 2.0, the GNU Lesser
|
||||
General Public License, Version 2.1, the GNU Affero General Public
|
||||
License, Version 3.0, or any later versions of those licenses.
|
||||
|
||||
1.13. “Source Code Form”
|
||||
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. “You” (or “Your”)
|
||||
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, “You” includes any entity that controls, is
|
||||
controlled by, or is under common control with You. For purposes of this
|
||||
definition, “control” means (a) the power, direct or indirect, to cause
|
||||
the direction or management of such entity, whether by contract or
|
||||
otherwise, or (b) ownership of more than fifty percent (50%) of the
|
||||
outstanding shares or beneficial ownership of such entity.
|
||||
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
a. under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or as
|
||||
part of a Larger Work; and
|
||||
|
||||
b. under Patent Claims of such Contributor to make, use, sell, offer for
|
||||
sale, have made, import, and otherwise transfer either its Contributions
|
||||
or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution become
|
||||
effective for each Contribution on the date the Contributor first distributes
|
||||
such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under this
|
||||
License. No additional rights or licenses will be implied from the distribution
|
||||
or licensing of Covered Software under this License. Notwithstanding Section
|
||||
2.1(b) above, no patent license is granted by a Contributor:
|
||||
|
||||
a. for any code that a Contributor has removed from Covered Software; or
|
||||
|
||||
b. for infringements caused by: (i) Your and any other third party’s
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
c. under Patent Claims infringed by Covered Software in the absence of its
|
||||
Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks, or
|
||||
logos of any Contributor (except as may be necessary to comply with the
|
||||
notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this License
|
||||
(see Section 10.2) or under the terms of a Secondary License (if permitted
|
||||
under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its Contributions
|
||||
are its original creation(s) or it has sufficient rights to grant the
|
||||
rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under applicable
|
||||
copyright doctrines of fair use, fair dealing, or other equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
||||
Section 2.1.
|
||||
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under the
|
||||
terms of this License. You must inform recipients that the Source Code Form
|
||||
of the Covered Software is governed by the terms of this License, and how
|
||||
they can obtain a copy of this License. You may not attempt to alter or
|
||||
restrict the recipients’ rights in the Source Code Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
a. such Covered Software must also be made available in Source Code Form,
|
||||
as described in Section 3.1, and You must inform recipients of the
|
||||
Executable Form how they can obtain a copy of such Source Code Form by
|
||||
reasonable means in a timely manner, at a charge no more than the cost
|
||||
of distribution to the recipient; and
|
||||
|
||||
b. You may distribute such Executable Form under the terms of this License,
|
||||
or sublicense it under different terms, provided that the license for
|
||||
the Executable Form does not attempt to limit or alter the recipients’
|
||||
rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for the
|
||||
Covered Software. If the Larger Work is a combination of Covered Software
|
||||
with a work governed by one or more Secondary Licenses, and the Covered
|
||||
Software is not Incompatible With Secondary Licenses, this License permits
|
||||
You to additionally distribute such Covered Software under the terms of
|
||||
such Secondary License(s), so that the recipient of the Larger Work may, at
|
||||
their option, further distribute the Covered Software under the terms of
|
||||
either this License or such Secondary License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices (including
|
||||
copyright notices, patent notices, disclaimers of warranty, or limitations
|
||||
of liability) contained within the Source Code Form of the Covered
|
||||
Software, except that You may alter any license notices to the extent
|
||||
required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on behalf
|
||||
of any Contributor. You must make it absolutely clear that any such
|
||||
warranty, support, indemnity, or liability obligation is offered by You
|
||||
alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this License
|
||||
with respect to some or all of the Covered Software due to statute, judicial
|
||||
order, or regulation then You must: (a) comply with the terms of this License
|
||||
to the maximum extent possible; and (b) describe the limitations and the code
|
||||
they affect. Such description must be placed in a text file included with all
|
||||
distributions of the Covered Software under this License. Except to the
|
||||
extent prohibited by statute or regulation, such description must be
|
||||
sufficiently detailed for a recipient of ordinary skill to be able to
|
||||
understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically if You
|
||||
fail to comply with any of its terms. However, if You become compliant,
|
||||
then the rights granted under this License from a particular Contributor
|
||||
are reinstated (a) provisionally, unless and until such Contributor
|
||||
explicitly and finally terminates Your grants, and (b) on an ongoing basis,
|
||||
if such Contributor fails to notify You of the non-compliance by some
|
||||
reasonable means prior to 60 days after You have come back into compliance.
|
||||
Moreover, Your grants from a particular Contributor are reinstated on an
|
||||
ongoing basis if such Contributor notifies You of the non-compliance by
|
||||
some reasonable means, this is the first time You have received notice of
|
||||
non-compliance with this License from such Contributor, and You become
|
||||
compliant prior to 30 days after Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions, counter-claims,
|
||||
and cross-claims) alleging that a Contributor Version directly or
|
||||
indirectly infringes any patent, then the rights granted to You by any and
|
||||
all Contributors for the Covered Software under Section 2.1 of this License
|
||||
shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
|
||||
license agreements (excluding distributors and resellers) which have been
|
||||
validly granted by You or Your distributors under this License prior to
|
||||
termination shall survive termination.
|
||||
|
||||
6. Disclaimer of Warranty
|
||||
|
||||
Covered Software is provided under this License on an “as is” basis, without
|
||||
warranty of any kind, either expressed, implied, or statutory, including,
|
||||
without limitation, warranties that the Covered Software is free of defects,
|
||||
merchantable, fit for a particular purpose or non-infringing. The entire
|
||||
risk as to the quality and performance of the Covered Software is with You.
|
||||
Should any Covered Software prove defective in any respect, You (not any
|
||||
Contributor) assume the cost of any necessary servicing, repair, or
|
||||
correction. This disclaimer of warranty constitutes an essential part of this
|
||||
License. No use of any Covered Software is authorized under this License
|
||||
except under this disclaimer.
|
||||
|
||||
7. Limitation of Liability
|
||||
|
||||
Under no circumstances and under no legal theory, whether tort (including
|
||||
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
||||
distributes Covered Software as permitted above, be liable to You for any
|
||||
direct, indirect, special, incidental, or consequential damages of any
|
||||
character including, without limitation, damages for lost profits, loss of
|
||||
goodwill, work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses, even if such party shall have been
|
||||
informed of the possibility of such damages. This limitation of liability
|
||||
shall not apply to liability for death or personal injury resulting from such
|
||||
party’s negligence to the extent applicable law prohibits such limitation.
|
||||
Some jurisdictions do not allow the exclusion or limitation of incidental or
|
||||
consequential damages, so this exclusion and limitation may not apply to You.
|
||||
|
||||
8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the courts of
|
||||
a jurisdiction where the defendant maintains its principal place of business
|
||||
and such litigation shall be governed by laws of that jurisdiction, without
|
||||
reference to its conflict-of-law provisions. Nothing in this Section shall
|
||||
prevent a party’s ability to bring cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject matter
|
||||
hereof. If any provision of this License is held to be unenforceable, such
|
||||
provision shall be reformed only to the extent necessary to make it
|
||||
enforceable. Any law or regulation which provides that the language of a
|
||||
contract shall be construed against the drafter shall not be used to construe
|
||||
this License against a Contributor.
|
||||
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version of
|
||||
the License under which You originally received the Covered Software, or
|
||||
under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a modified
|
||||
version of this License if you rename the license and remove any
|
||||
references to the name of the license steward (except to note that such
|
||||
modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular file, then
|
||||
You may include the notice in a location (such as a LICENSE file in a relevant
|
||||
directory) where a recipient would be likely to look for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - “Incompatible With Secondary Licenses” Notice
|
||||
|
||||
This Source Code Form is “Incompatible
|
||||
With Secondary Licenses”, as defined by
|
||||
the Mozilla Public License, v. 2.0.
|
348
bake/hclparser/gohcl/decode.go
Normal file
348
bake/hclparser/gohcl/decode.go
Normal file
@@ -0,0 +1,348 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package gohcl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
"github.com/zclconf/go-cty/cty/gocty"
|
||||
)
|
||||
|
||||
// DecodeOptions allows customizing sections of the decoding process.
|
||||
type DecodeOptions struct {
|
||||
ImpliedType func(gv interface{}) (cty.Type, error)
|
||||
Convert func(in cty.Value, want cty.Type) (cty.Value, error)
|
||||
}
|
||||
|
||||
func (o DecodeOptions) DecodeBody(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
|
||||
o = o.withDefaults()
|
||||
|
||||
rv := reflect.ValueOf(val)
|
||||
if rv.Kind() != reflect.Ptr {
|
||||
panic(fmt.Sprintf("target value must be a pointer, not %s", rv.Type().String()))
|
||||
}
|
||||
|
||||
return o.decodeBodyToValue(body, ctx, rv.Elem())
|
||||
}
|
||||
|
||||
// DecodeBody extracts the configuration within the given body into the given
|
||||
// value. This value must be a non-nil pointer to either a struct or
|
||||
// a map, where in the former case the configuration will be decoded using
|
||||
// struct tags and in the latter case only attributes are allowed and their
|
||||
// values are decoded into the map.
|
||||
//
|
||||
// The given EvalContext is used to resolve any variables or functions in
|
||||
// expressions encountered while decoding. This may be nil to require only
|
||||
// constant values, for simple applications that do not support variables or
|
||||
// functions.
|
||||
//
|
||||
// The returned diagnostics should be inspected with its HasErrors method to
|
||||
// determine if the populated value is valid and complete. If error diagnostics
|
||||
// are returned then the given value may have been partially-populated but
|
||||
// may still be accessed by a careful caller for static analysis and editor
|
||||
// integration use-cases.
|
||||
func DecodeBody(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
|
||||
return DecodeOptions{}.DecodeBody(body, ctx, val)
|
||||
}
|
||||
|
||||
func (o DecodeOptions) decodeBodyToValue(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics {
|
||||
et := val.Type()
|
||||
switch et.Kind() {
|
||||
case reflect.Struct:
|
||||
return o.decodeBodyToStruct(body, ctx, val)
|
||||
case reflect.Map:
|
||||
return o.decodeBodyToMap(body, ctx, val)
|
||||
default:
|
||||
panic(fmt.Sprintf("target value must be pointer to struct or map, not %s", et.String()))
|
||||
}
|
||||
}
|
||||
|
||||
func (o DecodeOptions) decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics {
|
||||
schema, partial := ImpliedBodySchema(val.Interface())
|
||||
|
||||
var content *hcl.BodyContent
|
||||
var leftovers hcl.Body
|
||||
var diags hcl.Diagnostics
|
||||
if partial {
|
||||
content, leftovers, diags = body.PartialContent(schema)
|
||||
} else {
|
||||
content, diags = body.Content(schema)
|
||||
}
|
||||
if content == nil {
|
||||
return diags
|
||||
}
|
||||
|
||||
tags := getFieldTags(val.Type())
|
||||
|
||||
if tags.Body != nil {
|
||||
fieldIdx := *tags.Body
|
||||
field := val.Type().Field(fieldIdx)
|
||||
fieldV := val.Field(fieldIdx)
|
||||
switch {
|
||||
case bodyType.AssignableTo(field.Type):
|
||||
fieldV.Set(reflect.ValueOf(body))
|
||||
|
||||
default:
|
||||
diags = append(diags, o.decodeBodyToValue(body, ctx, fieldV)...)
|
||||
}
|
||||
}
|
||||
|
||||
if tags.Remain != nil {
|
||||
fieldIdx := *tags.Remain
|
||||
field := val.Type().Field(fieldIdx)
|
||||
fieldV := val.Field(fieldIdx)
|
||||
switch {
|
||||
case bodyType.AssignableTo(field.Type):
|
||||
fieldV.Set(reflect.ValueOf(leftovers))
|
||||
case attrsType.AssignableTo(field.Type):
|
||||
attrs, attrsDiags := leftovers.JustAttributes()
|
||||
if len(attrsDiags) > 0 {
|
||||
diags = append(diags, attrsDiags...)
|
||||
}
|
||||
fieldV.Set(reflect.ValueOf(attrs))
|
||||
default:
|
||||
diags = append(diags, o.decodeBodyToValue(leftovers, ctx, fieldV)...)
|
||||
}
|
||||
}
|
||||
|
||||
for name, fieldIdx := range tags.Attributes {
|
||||
attr := content.Attributes[name]
|
||||
field := val.Type().Field(fieldIdx)
|
||||
fieldV := val.Field(fieldIdx)
|
||||
|
||||
if attr == nil {
|
||||
if !exprType.AssignableTo(field.Type) {
|
||||
continue
|
||||
}
|
||||
|
||||
// As a special case, if the target is of type hcl.Expression then
|
||||
// we'll assign an actual expression that evalues to a cty null,
|
||||
// so the caller can deal with it within the cty realm rather
|
||||
// than within the Go realm.
|
||||
synthExpr := hcl.StaticExpr(cty.NullVal(cty.DynamicPseudoType), body.MissingItemRange())
|
||||
fieldV.Set(reflect.ValueOf(synthExpr))
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case attrType.AssignableTo(field.Type):
|
||||
fieldV.Set(reflect.ValueOf(attr))
|
||||
case exprType.AssignableTo(field.Type):
|
||||
fieldV.Set(reflect.ValueOf(attr.Expr))
|
||||
default:
|
||||
diags = append(diags, o.DecodeExpression(
|
||||
attr.Expr, ctx, fieldV.Addr().Interface(),
|
||||
)...)
|
||||
}
|
||||
}
|
||||
|
||||
blocksByType := content.Blocks.ByType()
|
||||
|
||||
for typeName, fieldIdx := range tags.Blocks {
|
||||
blocks := blocksByType[typeName]
|
||||
field := val.Type().Field(fieldIdx)
|
||||
|
||||
ty := field.Type
|
||||
isSlice := false
|
||||
isPtr := false
|
||||
if ty.Kind() == reflect.Slice {
|
||||
isSlice = true
|
||||
ty = ty.Elem()
|
||||
}
|
||||
if ty.Kind() == reflect.Ptr {
|
||||
isPtr = true
|
||||
ty = ty.Elem()
|
||||
}
|
||||
|
||||
if len(blocks) > 1 && !isSlice {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Duplicate %s block", typeName),
|
||||
Detail: fmt.Sprintf(
|
||||
"Only one %s block is allowed. Another was defined at %s.",
|
||||
typeName, blocks[0].DefRange.String(),
|
||||
),
|
||||
Subject: &blocks[1].DefRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if len(blocks) == 0 {
|
||||
if isSlice || isPtr {
|
||||
if val.Field(fieldIdx).IsNil() {
|
||||
val.Field(fieldIdx).Set(reflect.Zero(field.Type))
|
||||
}
|
||||
} else {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Missing %s block", typeName),
|
||||
Detail: fmt.Sprintf("A %s block is required.", typeName),
|
||||
Subject: body.MissingItemRange().Ptr(),
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case isSlice:
|
||||
elemType := ty
|
||||
if isPtr {
|
||||
elemType = reflect.PointerTo(ty)
|
||||
}
|
||||
sli := val.Field(fieldIdx)
|
||||
if sli.IsNil() {
|
||||
sli = reflect.MakeSlice(reflect.SliceOf(elemType), len(blocks), len(blocks))
|
||||
}
|
||||
|
||||
for i, block := range blocks {
|
||||
if isPtr {
|
||||
if i >= sli.Len() {
|
||||
sli = reflect.Append(sli, reflect.New(ty))
|
||||
}
|
||||
v := sli.Index(i)
|
||||
if v.IsNil() {
|
||||
v = reflect.New(ty)
|
||||
}
|
||||
diags = append(diags, o.decodeBlockToValue(block, ctx, v.Elem())...)
|
||||
sli.Index(i).Set(v)
|
||||
} else {
|
||||
if i >= sli.Len() {
|
||||
sli = reflect.Append(sli, reflect.Indirect(reflect.New(ty)))
|
||||
}
|
||||
diags = append(diags, o.decodeBlockToValue(block, ctx, sli.Index(i))...)
|
||||
}
|
||||
}
|
||||
|
||||
if sli.Len() > len(blocks) {
|
||||
sli.SetLen(len(blocks))
|
||||
}
|
||||
|
||||
val.Field(fieldIdx).Set(sli)
|
||||
|
||||
default:
|
||||
block := blocks[0]
|
||||
if isPtr {
|
||||
v := val.Field(fieldIdx)
|
||||
if v.IsNil() {
|
||||
v = reflect.New(ty)
|
||||
}
|
||||
diags = append(diags, o.decodeBlockToValue(block, ctx, v.Elem())...)
|
||||
val.Field(fieldIdx).Set(v)
|
||||
} else {
|
||||
diags = append(diags, o.decodeBlockToValue(block, ctx, val.Field(fieldIdx))...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
func (o DecodeOptions) decodeBodyToMap(body hcl.Body, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics {
|
||||
attrs, diags := body.JustAttributes()
|
||||
if attrs == nil {
|
||||
return diags
|
||||
}
|
||||
|
||||
mv := reflect.MakeMap(v.Type())
|
||||
|
||||
for k, attr := range attrs {
|
||||
switch {
|
||||
case attrType.AssignableTo(v.Type().Elem()):
|
||||
mv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(attr))
|
||||
case exprType.AssignableTo(v.Type().Elem()):
|
||||
mv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(attr.Expr))
|
||||
default:
|
||||
ev := reflect.New(v.Type().Elem())
|
||||
diags = append(diags, o.DecodeExpression(attr.Expr, ctx, ev.Interface())...)
|
||||
mv.SetMapIndex(reflect.ValueOf(k), ev.Elem())
|
||||
}
|
||||
}
|
||||
|
||||
v.Set(mv)
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
func (o DecodeOptions) decodeBlockToValue(block *hcl.Block, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics {
|
||||
diags := o.decodeBodyToValue(block.Body, ctx, v)
|
||||
|
||||
if len(block.Labels) > 0 {
|
||||
blockTags := getFieldTags(v.Type())
|
||||
for li, lv := range block.Labels {
|
||||
lfieldIdx := blockTags.Labels[li].FieldIndex
|
||||
v.Field(lfieldIdx).Set(reflect.ValueOf(lv))
|
||||
}
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
func (o DecodeOptions) DecodeExpression(expr hcl.Expression, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
|
||||
o = o.withDefaults()
|
||||
|
||||
srcVal, diags := expr.Value(ctx)
|
||||
|
||||
convTy, err := o.ImpliedType(val)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("unsuitable DecodeExpression target: %s", err))
|
||||
}
|
||||
|
||||
srcVal, err = o.Convert(srcVal, convTy)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Unsuitable value type",
|
||||
Detail: fmt.Sprintf("Unsuitable value: %s", err.Error()),
|
||||
Subject: expr.StartRange().Ptr(),
|
||||
Context: expr.Range().Ptr(),
|
||||
})
|
||||
return diags
|
||||
}
|
||||
|
||||
err = gocty.FromCtyValue(srcVal, val)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Unsuitable value type",
|
||||
Detail: fmt.Sprintf("Unsuitable value: %s", err.Error()),
|
||||
Subject: expr.StartRange().Ptr(),
|
||||
Context: expr.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
// DecodeExpression extracts the value of the given expression into the given
|
||||
// value. This value must be something that gocty is able to decode into,
|
||||
// since the final decoding is delegated to that package.
|
||||
//
|
||||
// The given EvalContext is used to resolve any variables or functions in
|
||||
// expressions encountered while decoding. This may be nil to require only
|
||||
// constant values, for simple applications that do not support variables or
|
||||
// functions.
|
||||
//
|
||||
// The returned diagnostics should be inspected with its HasErrors method to
|
||||
// determine if the populated value is valid and complete. If error diagnostics
|
||||
// are returned then the given value may have been partially-populated but
|
||||
// may still be accessed by a careful caller for static analysis and editor
|
||||
// integration use-cases.
|
||||
func DecodeExpression(expr hcl.Expression, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
|
||||
return DecodeOptions{}.DecodeExpression(expr, ctx, val)
|
||||
}
|
||||
|
||||
func (o DecodeOptions) withDefaults() DecodeOptions {
|
||||
if o.ImpliedType == nil {
|
||||
o.ImpliedType = gocty.ImpliedType
|
||||
}
|
||||
|
||||
if o.Convert == nil {
|
||||
o.Convert = convert.Convert
|
||||
}
|
||||
return o
|
||||
}
|
806
bake/hclparser/gohcl/decode_test.go
Normal file
806
bake/hclparser/gohcl/decode_test.go
Normal file
@@ -0,0 +1,806 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package gohcl
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
hclJSON "github.com/hashicorp/hcl/v2/json"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestDecodeBody(t *testing.T) {
|
||||
deepEquals := func(other interface{}) func(v interface{}) bool {
|
||||
return func(v interface{}) bool {
|
||||
return reflect.DeepEqual(v, other)
|
||||
}
|
||||
}
|
||||
|
||||
type withNameExpression struct {
|
||||
Name hcl.Expression `hcl:"name"`
|
||||
}
|
||||
|
||||
type withTwoAttributes struct {
|
||||
A string `hcl:"a,optional"`
|
||||
B string `hcl:"b,optional"`
|
||||
}
|
||||
|
||||
type withNestedBlock struct {
|
||||
Plain string `hcl:"plain,optional"`
|
||||
Nested *withTwoAttributes `hcl:"nested,block"`
|
||||
}
|
||||
|
||||
type withListofNestedBlocks struct {
|
||||
Nested []*withTwoAttributes `hcl:"nested,block"`
|
||||
}
|
||||
|
||||
type withListofNestedBlocksNoPointers struct {
|
||||
Nested []withTwoAttributes `hcl:"nested,block"`
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
Body map[string]interface{}
|
||||
Target func() interface{}
|
||||
Check func(v interface{}) bool
|
||||
DiagCount int
|
||||
}{
|
||||
{
|
||||
map[string]interface{}{},
|
||||
makeInstantiateType(struct{}{}),
|
||||
deepEquals(struct{}{}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{},
|
||||
makeInstantiateType(struct {
|
||||
Name string `hcl:"name"`
|
||||
}{}),
|
||||
deepEquals(struct {
|
||||
Name string `hcl:"name"`
|
||||
}{}),
|
||||
1, // name is required
|
||||
},
|
||||
{
|
||||
map[string]interface{}{},
|
||||
makeInstantiateType(struct {
|
||||
Name *string `hcl:"name"`
|
||||
}{}),
|
||||
deepEquals(struct {
|
||||
Name *string `hcl:"name"`
|
||||
}{}),
|
||||
0,
|
||||
}, // name nil
|
||||
{
|
||||
map[string]interface{}{},
|
||||
makeInstantiateType(struct {
|
||||
Name string `hcl:"name,optional"`
|
||||
}{}),
|
||||
deepEquals(struct {
|
||||
Name string `hcl:"name,optional"`
|
||||
}{}),
|
||||
0,
|
||||
}, // name optional
|
||||
{
|
||||
map[string]interface{}{},
|
||||
makeInstantiateType(withNameExpression{}),
|
||||
func(v interface{}) bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
wne, valid := v.(withNameExpression)
|
||||
if !valid {
|
||||
return false
|
||||
}
|
||||
|
||||
if wne.Name == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
nameVal, _ := wne.Name.Value(nil)
|
||||
return nameVal.IsNull()
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"name": "Ermintrude",
|
||||
},
|
||||
makeInstantiateType(withNameExpression{}),
|
||||
func(v interface{}) bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
wne, valid := v.(withNameExpression)
|
||||
if !valid {
|
||||
return false
|
||||
}
|
||||
|
||||
if wne.Name == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
nameVal, _ := wne.Name.Value(nil)
|
||||
return nameVal.Equals(cty.StringVal("Ermintrude")).True()
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"name": "Ermintrude",
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Name string `hcl:"name"`
|
||||
}{}),
|
||||
deepEquals(struct {
|
||||
Name string `hcl:"name"`
|
||||
}{"Ermintrude"}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"name": "Ermintrude",
|
||||
"age": 23,
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Name string `hcl:"name"`
|
||||
}{}),
|
||||
deepEquals(struct {
|
||||
Name string `hcl:"name"`
|
||||
}{"Ermintrude"}),
|
||||
1, // Extraneous "age" property
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"name": "Ermintrude",
|
||||
"age": 50,
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Name string `hcl:"name"`
|
||||
Attrs hcl.Attributes `hcl:",remain"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
got := gotI.(struct {
|
||||
Name string `hcl:"name"`
|
||||
Attrs hcl.Attributes `hcl:",remain"`
|
||||
})
|
||||
return got.Name == "Ermintrude" && len(got.Attrs) == 1 && got.Attrs["age"] != nil
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"name": "Ermintrude",
|
||||
"age": 50,
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Name string `hcl:"name"`
|
||||
Remain hcl.Body `hcl:",remain"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
got := gotI.(struct {
|
||||
Name string `hcl:"name"`
|
||||
Remain hcl.Body `hcl:",remain"`
|
||||
})
|
||||
|
||||
attrs, _ := got.Remain.JustAttributes()
|
||||
|
||||
return got.Name == "Ermintrude" && len(attrs) == 1 && attrs["age"] != nil
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"name": "Ermintrude",
|
||||
"living": true,
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Name string `hcl:"name"`
|
||||
Remain map[string]cty.Value `hcl:",remain"`
|
||||
}{}),
|
||||
deepEquals(struct {
|
||||
Name string `hcl:"name"`
|
||||
Remain map[string]cty.Value `hcl:",remain"`
|
||||
}{
|
||||
Name: "Ermintrude",
|
||||
Remain: map[string]cty.Value{
|
||||
"living": cty.True,
|
||||
},
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"name": "Ermintrude",
|
||||
"age": 50,
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Name string `hcl:"name"`
|
||||
Body hcl.Body `hcl:",body"`
|
||||
Remain hcl.Body `hcl:",remain"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
got := gotI.(struct {
|
||||
Name string `hcl:"name"`
|
||||
Body hcl.Body `hcl:",body"`
|
||||
Remain hcl.Body `hcl:",remain"`
|
||||
})
|
||||
|
||||
attrs, _ := got.Body.JustAttributes()
|
||||
|
||||
return got.Name == "Ermintrude" && len(attrs) == 2 &&
|
||||
attrs["name"] != nil && attrs["age"] != nil
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"noodle": map[string]interface{}{},
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Noodle struct{} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
// Generating no diagnostics is good enough for this one.
|
||||
return true
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"noodle": []map[string]interface{}{{}},
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Noodle struct{} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
// Generating no diagnostics is good enough for this one.
|
||||
return true
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"noodle": []map[string]interface{}{{}, {}},
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Noodle struct{} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
// Generating one diagnostic is good enough for this one.
|
||||
return true
|
||||
},
|
||||
1,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{},
|
||||
makeInstantiateType(struct {
|
||||
Noodle struct{} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
// Generating one diagnostic is good enough for this one.
|
||||
return true
|
||||
},
|
||||
1,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"noodle": []map[string]interface{}{},
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Noodle struct{} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
// Generating one diagnostic is good enough for this one.
|
||||
return true
|
||||
},
|
||||
1,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"noodle": map[string]interface{}{},
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Noodle *struct{} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
return gotI.(struct {
|
||||
Noodle *struct{} `hcl:"noodle,block"`
|
||||
}).Noodle != nil
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"noodle": []map[string]interface{}{{}},
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Noodle *struct{} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
return gotI.(struct {
|
||||
Noodle *struct{} `hcl:"noodle,block"`
|
||||
}).Noodle != nil
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"noodle": []map[string]interface{}{},
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Noodle *struct{} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
return gotI.(struct {
|
||||
Noodle *struct{} `hcl:"noodle,block"`
|
||||
}).Noodle == nil
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"noodle": []map[string]interface{}{{}, {}},
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Noodle *struct{} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
// Generating one diagnostic is good enough for this one.
|
||||
return true
|
||||
},
|
||||
1,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"noodle": []map[string]interface{}{},
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Noodle []struct{} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
noodle := gotI.(struct {
|
||||
Noodle []struct{} `hcl:"noodle,block"`
|
||||
}).Noodle
|
||||
return len(noodle) == 0
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"noodle": []map[string]interface{}{{}},
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Noodle []struct{} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
noodle := gotI.(struct {
|
||||
Noodle []struct{} `hcl:"noodle,block"`
|
||||
}).Noodle
|
||||
return len(noodle) == 1
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"noodle": []map[string]interface{}{{}, {}},
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Noodle []struct{} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
noodle := gotI.(struct {
|
||||
Noodle []struct{} `hcl:"noodle,block"`
|
||||
}).Noodle
|
||||
return len(noodle) == 2
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"noodle": map[string]interface{}{},
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Noodle struct {
|
||||
Name string `hcl:"name,label"`
|
||||
} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
//nolint:misspell
|
||||
// Generating two diagnostics is good enough for this one.
|
||||
// (one for the missing noodle block and the other for
|
||||
// the JSON serialization detecting the missing level of
|
||||
// heirarchy for the label.)
|
||||
return true
|
||||
},
|
||||
2,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"noodle": map[string]interface{}{
|
||||
"foo_foo": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Noodle struct {
|
||||
Name string `hcl:"name,label"`
|
||||
} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
noodle := gotI.(struct {
|
||||
Noodle struct {
|
||||
Name string `hcl:"name,label"`
|
||||
} `hcl:"noodle,block"`
|
||||
}).Noodle
|
||||
return noodle.Name == "foo_foo"
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"noodle": map[string]interface{}{
|
||||
"foo_foo": map[string]interface{}{},
|
||||
"bar_baz": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Noodle struct {
|
||||
Name string `hcl:"name,label"`
|
||||
} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
// One diagnostic is enough for this one.
|
||||
return true
|
||||
},
|
||||
1,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"noodle": map[string]interface{}{
|
||||
"foo_foo": map[string]interface{}{},
|
||||
"bar_baz": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Noodles []struct {
|
||||
Name string `hcl:"name,label"`
|
||||
} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
noodles := gotI.(struct {
|
||||
Noodles []struct {
|
||||
Name string `hcl:"name,label"`
|
||||
} `hcl:"noodle,block"`
|
||||
}).Noodles
|
||||
return len(noodles) == 2 && (noodles[0].Name == "foo_foo" || noodles[0].Name == "bar_baz") && (noodles[1].Name == "foo_foo" || noodles[1].Name == "bar_baz") && noodles[0].Name != noodles[1].Name
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"noodle": map[string]interface{}{
|
||||
"foo_foo": map[string]interface{}{
|
||||
"type": "rice",
|
||||
},
|
||||
},
|
||||
},
|
||||
makeInstantiateType(struct {
|
||||
Noodle struct {
|
||||
Name string `hcl:"name,label"`
|
||||
Type string `hcl:"type"`
|
||||
} `hcl:"noodle,block"`
|
||||
}{}),
|
||||
func(gotI interface{}) bool {
|
||||
noodle := gotI.(struct {
|
||||
Noodle struct {
|
||||
Name string `hcl:"name,label"`
|
||||
Type string `hcl:"type"`
|
||||
} `hcl:"noodle,block"`
|
||||
}).Noodle
|
||||
return noodle.Name == "foo_foo" && noodle.Type == "rice"
|
||||
},
|
||||
0,
|
||||
},
|
||||
|
||||
{
|
||||
map[string]interface{}{
|
||||
"name": "Ermintrude",
|
||||
"age": 34,
|
||||
},
|
||||
makeInstantiateType(map[string]string(nil)),
|
||||
deepEquals(map[string]string{
|
||||
"name": "Ermintrude",
|
||||
"age": "34",
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"name": "Ermintrude",
|
||||
"age": 89,
|
||||
},
|
||||
makeInstantiateType(map[string]*hcl.Attribute(nil)),
|
||||
func(gotI interface{}) bool {
|
||||
got := gotI.(map[string]*hcl.Attribute)
|
||||
return len(got) == 2 && got["name"] != nil && got["age"] != nil
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"name": "Ermintrude",
|
||||
"age": 13,
|
||||
},
|
||||
makeInstantiateType(map[string]hcl.Expression(nil)),
|
||||
func(gotI interface{}) bool {
|
||||
got := gotI.(map[string]hcl.Expression)
|
||||
return len(got) == 2 && got["name"] != nil && got["age"] != nil
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"name": "Ermintrude",
|
||||
"living": true,
|
||||
},
|
||||
makeInstantiateType(map[string]cty.Value(nil)),
|
||||
deepEquals(map[string]cty.Value{
|
||||
"name": cty.StringVal("Ermintrude"),
|
||||
"living": cty.True,
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
// Retain "nested" block while decoding
|
||||
map[string]interface{}{
|
||||
"plain": "foo",
|
||||
},
|
||||
func() interface{} {
|
||||
return &withNestedBlock{
|
||||
Plain: "bar",
|
||||
Nested: &withTwoAttributes{
|
||||
A: "bar",
|
||||
},
|
||||
}
|
||||
},
|
||||
func(gotI interface{}) bool {
|
||||
foo := gotI.(withNestedBlock)
|
||||
return foo.Plain == "foo" && foo.Nested != nil && foo.Nested.A == "bar"
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
// Retain values in "nested" block while decoding
|
||||
map[string]interface{}{
|
||||
"nested": map[string]interface{}{
|
||||
"a": "foo",
|
||||
},
|
||||
},
|
||||
func() interface{} {
|
||||
return &withNestedBlock{
|
||||
Nested: &withTwoAttributes{
|
||||
B: "bar",
|
||||
},
|
||||
}
|
||||
},
|
||||
func(gotI interface{}) bool {
|
||||
foo := gotI.(withNestedBlock)
|
||||
return foo.Nested.A == "foo" && foo.Nested.B == "bar"
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
// Retain values in "nested" block list while decoding
|
||||
map[string]interface{}{
|
||||
"nested": []map[string]interface{}{
|
||||
{
|
||||
"a": "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
func() interface{} {
|
||||
return &withListofNestedBlocks{
|
||||
Nested: []*withTwoAttributes{
|
||||
{
|
||||
B: "bar",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
func(gotI interface{}) bool {
|
||||
n := gotI.(withListofNestedBlocks)
|
||||
return n.Nested[0].A == "foo" && n.Nested[0].B == "bar"
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
// Remove additional elements from the list while decoding nested blocks
|
||||
map[string]interface{}{
|
||||
"nested": []map[string]interface{}{
|
||||
{
|
||||
"a": "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
func() interface{} {
|
||||
return &withListofNestedBlocks{
|
||||
Nested: []*withTwoAttributes{
|
||||
{
|
||||
B: "bar",
|
||||
},
|
||||
{
|
||||
B: "bar",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
func(gotI interface{}) bool {
|
||||
n := gotI.(withListofNestedBlocks)
|
||||
return len(n.Nested) == 1
|
||||
},
|
||||
0,
|
||||
},
|
||||
{
|
||||
// Make sure decoding value slices works the same as pointer slices.
|
||||
map[string]interface{}{
|
||||
"nested": []map[string]interface{}{
|
||||
{
|
||||
"b": "bar",
|
||||
},
|
||||
{
|
||||
"b": "baz",
|
||||
},
|
||||
},
|
||||
},
|
||||
func() interface{} {
|
||||
return &withListofNestedBlocksNoPointers{
|
||||
Nested: []withTwoAttributes{
|
||||
{
|
||||
B: "foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
func(gotI interface{}) bool {
|
||||
n := gotI.(withListofNestedBlocksNoPointers)
|
||||
return n.Nested[0].B == "bar" && len(n.Nested) == 2
|
||||
},
|
||||
0,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
// For convenience here we're going to use the JSON parser
|
||||
// to process the given body.
|
||||
buf, err := json.Marshal(test.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("error JSON-encoding body for test %d: %s", i, err)
|
||||
}
|
||||
|
||||
t.Run(string(buf), func(t *testing.T) {
|
||||
file, diags := hclJSON.Parse(buf, "test.json")
|
||||
if len(diags) != 0 {
|
||||
t.Fatalf("diagnostics while parsing: %s", diags.Error())
|
||||
}
|
||||
|
||||
targetVal := reflect.ValueOf(test.Target())
|
||||
|
||||
diags = DecodeBody(file.Body, nil, targetVal.Interface())
|
||||
if len(diags) != test.DiagCount {
|
||||
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
|
||||
for _, diag := range diags {
|
||||
t.Logf(" - %s", diag.Error())
|
||||
}
|
||||
}
|
||||
got := targetVal.Elem().Interface()
|
||||
if !test.Check(got) {
|
||||
t.Errorf("wrong result\ngot: %s", spew.Sdump(got))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeExpression(t *testing.T) {
|
||||
tests := []struct {
|
||||
Value cty.Value
|
||||
Target interface{}
|
||||
Want interface{}
|
||||
DiagCount int
|
||||
}{
|
||||
{
|
||||
cty.StringVal("hello"),
|
||||
"",
|
||||
"hello",
|
||||
0,
|
||||
},
|
||||
{
|
||||
cty.StringVal("hello"),
|
||||
cty.NilVal,
|
||||
cty.StringVal("hello"),
|
||||
0,
|
||||
},
|
||||
{
|
||||
cty.NumberIntVal(2),
|
||||
"",
|
||||
"2",
|
||||
0,
|
||||
},
|
||||
{
|
||||
cty.StringVal("true"),
|
||||
false,
|
||||
true,
|
||||
0,
|
||||
},
|
||||
{
|
||||
cty.NullVal(cty.String),
|
||||
"",
|
||||
"",
|
||||
1, // null value is not allowed
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.String),
|
||||
"",
|
||||
"",
|
||||
1, // value must be known
|
||||
},
|
||||
{
|
||||
cty.ListVal([]cty.Value{cty.True}),
|
||||
false,
|
||||
false,
|
||||
1, // bool required
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||
expr := &fixedExpression{test.Value}
|
||||
|
||||
targetVal := reflect.New(reflect.TypeOf(test.Target))
|
||||
|
||||
diags := DecodeExpression(expr, nil, targetVal.Interface())
|
||||
if len(diags) != test.DiagCount {
|
||||
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
|
||||
for _, diag := range diags {
|
||||
t.Logf(" - %s", diag.Error())
|
||||
}
|
||||
}
|
||||
got := targetVal.Elem().Interface()
|
||||
if !reflect.DeepEqual(got, test.Want) {
|
||||
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fixedExpression struct {
|
||||
val cty.Value
|
||||
}
|
||||
|
||||
func (e *fixedExpression) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
||||
return e.val, nil
|
||||
}
|
||||
|
||||
func (e *fixedExpression) Range() (r hcl.Range) {
|
||||
return
|
||||
}
|
||||
|
||||
func (e *fixedExpression) StartRange() (r hcl.Range) {
|
||||
return
|
||||
}
|
||||
|
||||
func (e *fixedExpression) Variables() []hcl.Traversal {
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeInstantiateType(target interface{}) func() interface{} {
|
||||
return func() interface{} {
|
||||
return reflect.New(reflect.TypeOf(target)).Interface()
|
||||
}
|
||||
}
|
65
bake/hclparser/gohcl/doc.go
Normal file
65
bake/hclparser/gohcl/doc.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package gohcl allows decoding HCL configurations into Go data structures.
|
||||
//
|
||||
// It provides a convenient and concise way of describing the schema for
|
||||
// configuration and then accessing the resulting data via native Go
|
||||
// types.
|
||||
//
|
||||
// A struct field tag scheme is used, similar to other decoding and
|
||||
// unmarshalling libraries. The tags are formatted as in the following example:
|
||||
//
|
||||
// ThingType string `hcl:"thing_type,attr"`
|
||||
//
|
||||
// Within each tag there are two comma-separated tokens. The first is the
|
||||
// name of the corresponding construct in configuration, while the second
|
||||
// is a keyword giving the kind of construct expected. The following
|
||||
// kind keywords are supported:
|
||||
//
|
||||
// attr (the default) indicates that the value is to be populated from an attribute
|
||||
// block indicates that the value is to populated from a block
|
||||
// label indicates that the value is to populated from a block label
|
||||
// optional is the same as attr, but the field is optional
|
||||
// remain indicates that the value is to be populated from the remaining body after populating other fields
|
||||
//
|
||||
// "attr" fields may either be of type *hcl.Expression, in which case the raw
|
||||
// expression is assigned, or of any type accepted by gocty, in which case
|
||||
// gocty will be used to assign the value to a native Go type.
|
||||
//
|
||||
// "block" fields may be a struct that recursively uses the same tags, or a
|
||||
// slice of such structs, in which case multiple blocks of the corresponding
|
||||
// type are decoded into the slice.
|
||||
//
|
||||
// "body" can be placed on a single field of type hcl.Body to capture
|
||||
// the full hcl.Body that was decoded for a block. This does not allow leftover
|
||||
// values like "remain", so a decoding error will still be returned if leftover
|
||||
// fields are given. If you want to capture the decoding body PLUS leftover
|
||||
// fields, you must specify a "remain" field as well to prevent errors. The
|
||||
// body field and the remain field will both contain the leftover fields.
|
||||
//
|
||||
// "label" fields are considered only in a struct used as the type of a field
|
||||
// marked as "block", and are used sequentially to capture the labels of
|
||||
// the blocks being decoded. In this case, the name token is used only as
|
||||
// an identifier for the label in diagnostic messages.
|
||||
//
|
||||
// "optional" fields behave like "attr" fields, but they are optional
|
||||
// and will not give parsing errors if they are missing.
|
||||
//
|
||||
// "remain" can be placed on a single field that may be either of type
|
||||
// hcl.Body or hcl.Attributes, in which case any remaining body content is
|
||||
// placed into this field for delayed processing. If no "remain" field is
|
||||
// present then any attributes or blocks not matched by another valid tag
|
||||
// will cause an error diagnostic.
|
||||
//
|
||||
// Only a subset of this tagging/typing vocabulary is supported for the
|
||||
// "Encode" family of functions. See the EncodeIntoBody docs for full details
|
||||
// on the constraints there.
|
||||
//
|
||||
// Broadly-speaking this package deals with two types of error. The first is
|
||||
// errors in the configuration itself, which are returned as diagnostics
|
||||
// written with the configuration author as the target audience. The second
|
||||
// is bugs in the calling program, such as invalid struct tags, which are
|
||||
// surfaced via panics since there can be no useful runtime handling of such
|
||||
// errors and they should certainly not be returned to the user as diagnostics.
|
||||
package gohcl
|
192
bake/hclparser/gohcl/encode.go
Normal file
192
bake/hclparser/gohcl/encode.go
Normal file
@@ -0,0 +1,192 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package gohcl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/hcl/v2/hclwrite"
|
||||
"github.com/zclconf/go-cty/cty/gocty"
|
||||
)
|
||||
|
||||
// EncodeIntoBody replaces the contents of the given hclwrite Body with
|
||||
// attributes and blocks derived from the given value, which must be a
|
||||
// struct value or a pointer to a struct value with the struct tags defined
|
||||
// in this package.
|
||||
//
|
||||
// This function can work only with fully-decoded data. It will ignore any
|
||||
// fields tagged as "remain", any fields that decode attributes into either
|
||||
// hcl.Attribute or hcl.Expression values, and any fields that decode blocks
|
||||
// into hcl.Attributes values. This function does not have enough information
|
||||
// to complete the decoding of these types.
|
||||
//
|
||||
// Any fields tagged as "label" are ignored by this function. Use EncodeAsBlock
|
||||
// to produce a whole hclwrite.Block including block labels.
|
||||
//
|
||||
// As long as a suitable value is given to encode and the destination body
|
||||
// is non-nil, this function will always complete. It will panic in case of
|
||||
// any errors in the calling program, such as passing an inappropriate type
|
||||
// or a nil body.
|
||||
//
|
||||
// The layout of the resulting HCL source is derived from the ordering of
|
||||
// the struct fields, with blank lines around nested blocks of different types.
|
||||
// Fields representing attributes should usually precede those representing
|
||||
// blocks so that the attributes can group togather in the result. For more
|
||||
// control, use the hclwrite API directly.
|
||||
func EncodeIntoBody(val interface{}, dst *hclwrite.Body) {
|
||||
rv := reflect.ValueOf(val)
|
||||
ty := rv.Type()
|
||||
if ty.Kind() == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
ty = rv.Type()
|
||||
}
|
||||
if ty.Kind() != reflect.Struct {
|
||||
panic(fmt.Sprintf("value is %s, not struct", ty.Kind()))
|
||||
}
|
||||
|
||||
tags := getFieldTags(ty)
|
||||
populateBody(rv, ty, tags, dst)
|
||||
}
|
||||
|
||||
// EncodeAsBlock creates a new hclwrite.Block populated with the data from
|
||||
// the given value, which must be a struct or pointer to struct with the
|
||||
// struct tags defined in this package.
|
||||
//
|
||||
// If the given struct type has fields tagged with "label" tags then they
|
||||
// will be used in order to annotate the created block with labels.
|
||||
//
|
||||
// This function has the same constraints as EncodeIntoBody and will panic
|
||||
// if they are violated.
|
||||
func EncodeAsBlock(val interface{}, blockType string) *hclwrite.Block {
|
||||
rv := reflect.ValueOf(val)
|
||||
ty := rv.Type()
|
||||
if ty.Kind() == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
ty = rv.Type()
|
||||
}
|
||||
if ty.Kind() != reflect.Struct {
|
||||
panic(fmt.Sprintf("value is %s, not struct", ty.Kind()))
|
||||
}
|
||||
|
||||
tags := getFieldTags(ty)
|
||||
labels := make([]string, len(tags.Labels))
|
||||
for i, lf := range tags.Labels {
|
||||
lv := rv.Field(lf.FieldIndex)
|
||||
// We just stringify whatever we find. It should always be a string
|
||||
// but if not then we'll still do something reasonable.
|
||||
labels[i] = fmt.Sprintf("%s", lv.Interface())
|
||||
}
|
||||
|
||||
block := hclwrite.NewBlock(blockType, labels)
|
||||
populateBody(rv, ty, tags, block.Body())
|
||||
return block
|
||||
}
|
||||
|
||||
func populateBody(rv reflect.Value, ty reflect.Type, tags *fieldTags, dst *hclwrite.Body) {
|
||||
nameIdxs := make(map[string]int, len(tags.Attributes)+len(tags.Blocks))
|
||||
namesOrder := make([]string, 0, len(tags.Attributes)+len(tags.Blocks))
|
||||
for n, i := range tags.Attributes {
|
||||
nameIdxs[n] = i
|
||||
namesOrder = append(namesOrder, n)
|
||||
}
|
||||
for n, i := range tags.Blocks {
|
||||
nameIdxs[n] = i
|
||||
namesOrder = append(namesOrder, n)
|
||||
}
|
||||
sort.SliceStable(namesOrder, func(i, j int) bool {
|
||||
ni, nj := namesOrder[i], namesOrder[j]
|
||||
return nameIdxs[ni] < nameIdxs[nj]
|
||||
})
|
||||
|
||||
dst.Clear()
|
||||
|
||||
prevWasBlock := false
|
||||
for _, name := range namesOrder {
|
||||
fieldIdx := nameIdxs[name]
|
||||
field := ty.Field(fieldIdx)
|
||||
fieldTy := field.Type
|
||||
fieldVal := rv.Field(fieldIdx)
|
||||
|
||||
if fieldTy.Kind() == reflect.Ptr {
|
||||
fieldTy = fieldTy.Elem()
|
||||
fieldVal = fieldVal.Elem()
|
||||
}
|
||||
|
||||
if _, isAttr := tags.Attributes[name]; isAttr {
|
||||
if exprType.AssignableTo(fieldTy) || attrType.AssignableTo(fieldTy) {
|
||||
continue // ignore undecoded fields
|
||||
}
|
||||
if !fieldVal.IsValid() {
|
||||
continue // ignore (field value is nil pointer)
|
||||
}
|
||||
if fieldTy.Kind() == reflect.Ptr && fieldVal.IsNil() {
|
||||
continue // ignore
|
||||
}
|
||||
if prevWasBlock {
|
||||
dst.AppendNewline()
|
||||
prevWasBlock = false
|
||||
}
|
||||
|
||||
valTy, err := gocty.ImpliedType(fieldVal.Interface())
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("cannot encode %T as HCL expression: %s", fieldVal.Interface(), err))
|
||||
}
|
||||
|
||||
val, err := gocty.ToCtyValue(fieldVal.Interface(), valTy)
|
||||
if err != nil {
|
||||
// This should never happen, since we should always be able
|
||||
// to decode into the implied type.
|
||||
panic(fmt.Sprintf("failed to encode %T as %#v: %s", fieldVal.Interface(), valTy, err))
|
||||
}
|
||||
|
||||
dst.SetAttributeValue(name, val)
|
||||
} else { // must be a block, then
|
||||
elemTy := fieldTy
|
||||
isSeq := false
|
||||
if elemTy.Kind() == reflect.Slice || elemTy.Kind() == reflect.Array {
|
||||
isSeq = true
|
||||
elemTy = elemTy.Elem()
|
||||
}
|
||||
|
||||
if bodyType.AssignableTo(elemTy) || attrsType.AssignableTo(elemTy) {
|
||||
continue // ignore undecoded fields
|
||||
}
|
||||
prevWasBlock = false
|
||||
|
||||
if isSeq {
|
||||
l := fieldVal.Len()
|
||||
for i := 0; i < l; i++ {
|
||||
elemVal := fieldVal.Index(i)
|
||||
if !elemVal.IsValid() {
|
||||
continue // ignore (elem value is nil pointer)
|
||||
}
|
||||
if elemTy.Kind() == reflect.Ptr && elemVal.IsNil() {
|
||||
continue // ignore
|
||||
}
|
||||
block := EncodeAsBlock(elemVal.Interface(), name)
|
||||
if !prevWasBlock {
|
||||
dst.AppendNewline()
|
||||
prevWasBlock = true
|
||||
}
|
||||
dst.AppendBlock(block)
|
||||
}
|
||||
} else {
|
||||
if !fieldVal.IsValid() {
|
||||
continue // ignore (field value is nil pointer)
|
||||
}
|
||||
if elemTy.Kind() == reflect.Ptr && fieldVal.IsNil() {
|
||||
continue // ignore
|
||||
}
|
||||
block := EncodeAsBlock(fieldVal.Interface(), name)
|
||||
if !prevWasBlock {
|
||||
dst.AppendNewline()
|
||||
prevWasBlock = true
|
||||
}
|
||||
dst.AppendBlock(block)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
67
bake/hclparser/gohcl/encode_test.go
Normal file
67
bake/hclparser/gohcl/encode_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package gohcl_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
"github.com/hashicorp/hcl/v2/hclwrite"
|
||||
)
|
||||
|
||||
func ExampleEncodeIntoBody() {
|
||||
type Service struct {
|
||||
Name string `hcl:"name,label"`
|
||||
Exe []string `hcl:"executable"`
|
||||
}
|
||||
type Constraints struct {
|
||||
OS string `hcl:"os"`
|
||||
Arch string `hcl:"arch"`
|
||||
}
|
||||
type App struct {
|
||||
Name string `hcl:"name"`
|
||||
Desc string `hcl:"description"`
|
||||
Constraints *Constraints `hcl:"constraints,block"`
|
||||
Services []Service `hcl:"service,block"`
|
||||
}
|
||||
|
||||
app := App{
|
||||
Name: "awesome-app",
|
||||
Desc: "Such an awesome application",
|
||||
Constraints: &Constraints{
|
||||
OS: "linux",
|
||||
Arch: "amd64",
|
||||
},
|
||||
Services: []Service{
|
||||
{
|
||||
Name: "web",
|
||||
Exe: []string{"./web", "--listen=:8080"},
|
||||
},
|
||||
{
|
||||
Name: "worker",
|
||||
Exe: []string{"./worker"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
f := hclwrite.NewEmptyFile()
|
||||
gohcl.EncodeIntoBody(&app, f.Body())
|
||||
fmt.Printf("%s", f.Bytes())
|
||||
|
||||
// Output:
|
||||
// name = "awesome-app"
|
||||
// description = "Such an awesome application"
|
||||
//
|
||||
// constraints {
|
||||
// os = "linux"
|
||||
// arch = "amd64"
|
||||
// }
|
||||
//
|
||||
// service "web" {
|
||||
// executable = ["./web", "--listen=:8080"]
|
||||
// }
|
||||
// service "worker" {
|
||||
// executable = ["./worker"]
|
||||
// }
|
||||
}
|
185
bake/hclparser/gohcl/schema.go
Normal file
185
bake/hclparser/gohcl/schema.go
Normal file
@@ -0,0 +1,185 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package gohcl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
)
|
||||
|
||||
// ImpliedBodySchema produces a hcl.BodySchema derived from the type of the
|
||||
// given value, which must be a struct value or a pointer to one. If an
|
||||
// inappropriate value is passed, this function will panic.
|
||||
//
|
||||
// The second return argument indicates whether the given struct includes
|
||||
// a "remain" field, and thus the returned schema is non-exhaustive.
|
||||
//
|
||||
// This uses the tags on the fields of the struct to discover how each
|
||||
// field's value should be expressed within configuration. If an invalid
|
||||
// mapping is attempted, this function will panic.
|
||||
func ImpliedBodySchema(val interface{}) (schema *hcl.BodySchema, partial bool) {
|
||||
ty := reflect.TypeOf(val)
|
||||
|
||||
if ty.Kind() == reflect.Ptr {
|
||||
ty = ty.Elem()
|
||||
}
|
||||
|
||||
if ty.Kind() != reflect.Struct {
|
||||
panic(fmt.Sprintf("given value must be struct, not %T", val))
|
||||
}
|
||||
|
||||
var attrSchemas []hcl.AttributeSchema
|
||||
var blockSchemas []hcl.BlockHeaderSchema
|
||||
|
||||
tags := getFieldTags(ty)
|
||||
|
||||
attrNames := make([]string, 0, len(tags.Attributes))
|
||||
for n := range tags.Attributes {
|
||||
attrNames = append(attrNames, n)
|
||||
}
|
||||
sort.Strings(attrNames)
|
||||
for _, n := range attrNames {
|
||||
idx := tags.Attributes[n]
|
||||
optional := tags.Optional[n]
|
||||
field := ty.Field(idx)
|
||||
|
||||
var required bool
|
||||
|
||||
switch {
|
||||
case field.Type.AssignableTo(exprType):
|
||||
//nolint:misspell
|
||||
// If we're decoding to hcl.Expression then absense can be
|
||||
// indicated via a null value, so we don't specify that
|
||||
// the field is required during decoding.
|
||||
required = false
|
||||
case field.Type.Kind() != reflect.Ptr && !optional:
|
||||
required = true
|
||||
default:
|
||||
required = false
|
||||
}
|
||||
|
||||
attrSchemas = append(attrSchemas, hcl.AttributeSchema{
|
||||
Name: n,
|
||||
Required: required,
|
||||
})
|
||||
}
|
||||
|
||||
blockNames := make([]string, 0, len(tags.Blocks))
|
||||
for n := range tags.Blocks {
|
||||
blockNames = append(blockNames, n)
|
||||
}
|
||||
sort.Strings(blockNames)
|
||||
for _, n := range blockNames {
|
||||
idx := tags.Blocks[n]
|
||||
field := ty.Field(idx)
|
||||
fty := field.Type
|
||||
if fty.Kind() == reflect.Slice {
|
||||
fty = fty.Elem()
|
||||
}
|
||||
if fty.Kind() == reflect.Ptr {
|
||||
fty = fty.Elem()
|
||||
}
|
||||
if fty.Kind() != reflect.Struct {
|
||||
panic(fmt.Sprintf(
|
||||
"hcl 'block' tag kind cannot be applied to %s field %s: struct required", field.Type.String(), field.Name,
|
||||
))
|
||||
}
|
||||
ftags := getFieldTags(fty)
|
||||
var labelNames []string
|
||||
if len(ftags.Labels) > 0 {
|
||||
labelNames = make([]string, len(ftags.Labels))
|
||||
for i, l := range ftags.Labels {
|
||||
labelNames[i] = l.Name
|
||||
}
|
||||
}
|
||||
|
||||
blockSchemas = append(blockSchemas, hcl.BlockHeaderSchema{
|
||||
Type: n,
|
||||
LabelNames: labelNames,
|
||||
})
|
||||
}
|
||||
|
||||
partial = tags.Remain != nil
|
||||
schema = &hcl.BodySchema{
|
||||
Attributes: attrSchemas,
|
||||
Blocks: blockSchemas,
|
||||
}
|
||||
return schema, partial
|
||||
}
|
||||
|
||||
type fieldTags struct {
|
||||
Attributes map[string]int
|
||||
Blocks map[string]int
|
||||
Labels []labelField
|
||||
Remain *int
|
||||
Body *int
|
||||
Optional map[string]bool
|
||||
}
|
||||
|
||||
type labelField struct {
|
||||
FieldIndex int
|
||||
Name string
|
||||
}
|
||||
|
||||
func getFieldTags(ty reflect.Type) *fieldTags {
|
||||
ret := &fieldTags{
|
||||
Attributes: map[string]int{},
|
||||
Blocks: map[string]int{},
|
||||
Optional: map[string]bool{},
|
||||
}
|
||||
|
||||
ct := ty.NumField()
|
||||
for i := 0; i < ct; i++ {
|
||||
field := ty.Field(i)
|
||||
tag := field.Tag.Get("hcl")
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
comma := strings.Index(tag, ",")
|
||||
var name, kind string
|
||||
if comma != -1 {
|
||||
name = tag[:comma]
|
||||
kind = tag[comma+1:]
|
||||
} else {
|
||||
name = tag
|
||||
kind = "attr"
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case "attr":
|
||||
ret.Attributes[name] = i
|
||||
case "block":
|
||||
ret.Blocks[name] = i
|
||||
case "label":
|
||||
ret.Labels = append(ret.Labels, labelField{
|
||||
FieldIndex: i,
|
||||
Name: name,
|
||||
})
|
||||
case "remain":
|
||||
if ret.Remain != nil {
|
||||
panic("only one 'remain' tag is permitted")
|
||||
}
|
||||
idx := i // copy, because this loop will continue assigning to i
|
||||
ret.Remain = &idx
|
||||
case "body":
|
||||
if ret.Body != nil {
|
||||
panic("only one 'body' tag is permitted")
|
||||
}
|
||||
idx := i // copy, because this loop will continue assigning to i
|
||||
ret.Body = &idx
|
||||
case "optional":
|
||||
ret.Attributes[name] = i
|
||||
ret.Optional[name] = true
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid hcl field tag kind %q on %s %q", kind, field.Type.String(), field.Name))
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
233
bake/hclparser/gohcl/schema_test.go
Normal file
233
bake/hclparser/gohcl/schema_test.go
Normal file
@@ -0,0 +1,233 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package gohcl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
)
|
||||
|
||||
func TestImpliedBodySchema(t *testing.T) {
|
||||
tests := []struct {
|
||||
val interface{}
|
||||
wantSchema *hcl.BodySchema
|
||||
wantPartial bool
|
||||
}{
|
||||
{
|
||||
struct{}{},
|
||||
&hcl.BodySchema{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
struct {
|
||||
Ignored bool
|
||||
}{},
|
||||
&hcl.BodySchema{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
struct {
|
||||
Attr1 bool `hcl:"attr1"`
|
||||
Attr2 bool `hcl:"attr2"`
|
||||
}{},
|
||||
&hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "attr1",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "attr2",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
struct {
|
||||
Attr *bool `hcl:"attr,attr"`
|
||||
}{},
|
||||
&hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "attr",
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
struct {
|
||||
Thing struct{} `hcl:"thing,block"`
|
||||
}{},
|
||||
&hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "thing",
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
struct {
|
||||
Thing struct {
|
||||
Type string `hcl:"type,label"`
|
||||
Name string `hcl:"name,label"`
|
||||
} `hcl:"thing,block"`
|
||||
}{},
|
||||
&hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "thing",
|
||||
LabelNames: []string{"type", "name"},
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
struct {
|
||||
Thing []struct {
|
||||
Type string `hcl:"type,label"`
|
||||
Name string `hcl:"name,label"`
|
||||
} `hcl:"thing,block"`
|
||||
}{},
|
||||
&hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "thing",
|
||||
LabelNames: []string{"type", "name"},
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
struct {
|
||||
Thing *struct {
|
||||
Type string `hcl:"type,label"`
|
||||
Name string `hcl:"name,label"`
|
||||
} `hcl:"thing,block"`
|
||||
}{},
|
||||
&hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "thing",
|
||||
LabelNames: []string{"type", "name"},
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
struct {
|
||||
Thing struct {
|
||||
Name string `hcl:"name,label"`
|
||||
Something string `hcl:"something"`
|
||||
} `hcl:"thing,block"`
|
||||
}{},
|
||||
&hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "thing",
|
||||
LabelNames: []string{"name"},
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
struct {
|
||||
Doodad string `hcl:"doodad"`
|
||||
Thing struct {
|
||||
Name string `hcl:"name,label"`
|
||||
} `hcl:"thing,block"`
|
||||
}{},
|
||||
&hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "doodad",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "thing",
|
||||
LabelNames: []string{"name"},
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
struct {
|
||||
Doodad string `hcl:"doodad"`
|
||||
Config string `hcl:",remain"`
|
||||
}{},
|
||||
&hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "doodad",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
struct {
|
||||
Expr hcl.Expression `hcl:"expr"`
|
||||
}{},
|
||||
&hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "expr",
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
struct {
|
||||
Meh string `hcl:"meh,optional"`
|
||||
}{},
|
||||
&hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "meh",
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%#v", test.val), func(t *testing.T) {
|
||||
schema, partial := ImpliedBodySchema(test.val)
|
||||
if !reflect.DeepEqual(schema, test.wantSchema) {
|
||||
t.Errorf(
|
||||
"wrong schema\ngot: %s\nwant: %s",
|
||||
spew.Sdump(schema), spew.Sdump(test.wantSchema),
|
||||
)
|
||||
}
|
||||
|
||||
if partial != test.wantPartial {
|
||||
t.Errorf(
|
||||
"wrong partial flag\ngot: %#v\nwant: %#v",
|
||||
partial, test.wantPartial,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
19
bake/hclparser/gohcl/types.go
Normal file
19
bake/hclparser/gohcl/types.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package gohcl
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
)
|
||||
|
||||
var victimExpr hcl.Expression
|
||||
var victimBody hcl.Body
|
||||
|
||||
var exprType = reflect.TypeOf(&victimExpr).Elem()
|
||||
var bodyType = reflect.TypeOf(&victimBody).Elem()
|
||||
var blockType = reflect.TypeOf((*hcl.Block)(nil)) //nolint:unused
|
||||
var attrType = reflect.TypeOf((*hcl.Attribute)(nil))
|
||||
var attrsType = reflect.TypeOf(hcl.Attributes(nil))
|
@@ -10,12 +10,11 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/buildx/bake/hclparser/gohcl"
|
||||
"github.com/docker/buildx/util/userfunc"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/gocty"
|
||||
)
|
||||
|
||||
type Opt struct {
|
||||
@@ -25,11 +24,17 @@ type Opt struct {
|
||||
}
|
||||
|
||||
type variable struct {
|
||||
Name string `json:"-" hcl:"name,label"`
|
||||
Default *hcl.Attribute `json:"default,omitempty" hcl:"default,optional"`
|
||||
Description string `json:"description,omitempty" hcl:"description,optional"`
|
||||
Body hcl.Body `json:"-" hcl:",body"`
|
||||
Remain hcl.Body `json:"-" hcl:",remain"`
|
||||
Name string `json:"-" hcl:"name,label"`
|
||||
Default *hcl.Attribute `json:"default,omitempty" hcl:"default,optional"`
|
||||
Description string `json:"description,omitempty" hcl:"description,optional"`
|
||||
Validations []*variableValidation `json:"validation,omitempty" hcl:"validation,block"`
|
||||
Body hcl.Body `json:"-" hcl:",body"`
|
||||
Remain hcl.Body `json:"-" hcl:",remain"`
|
||||
}
|
||||
|
||||
type variableValidation struct {
|
||||
Condition hcl.Expression `json:"condition" hcl:"condition"`
|
||||
ErrorMessage hcl.Expression `json:"error_message" hcl:"error_message"`
|
||||
}
|
||||
|
||||
type functionDef struct {
|
||||
@@ -448,7 +453,7 @@ func (p *parser) resolveBlock(block *hcl.Block, target *hcl.BodySchema) (err err
|
||||
}
|
||||
|
||||
// decode!
|
||||
diag = gohcl.DecodeBody(body(), ectx, output.Interface())
|
||||
diag = decodeBody(body(), ectx, output.Interface())
|
||||
if diag.HasErrors() {
|
||||
return diag
|
||||
}
|
||||
@@ -470,11 +475,11 @@ func (p *parser) resolveBlock(block *hcl.Block, target *hcl.BodySchema) (err err
|
||||
}
|
||||
|
||||
// store the result into the evaluation context (so it can be referenced)
|
||||
outputType, err := gocty.ImpliedType(output.Interface())
|
||||
outputType, err := ImpliedType(output.Interface())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outputValue, err := gocty.ToCtyValue(output.Interface(), outputType)
|
||||
outputValue, err := ToCtyValue(output.Interface(), outputType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -486,7 +491,12 @@ func (p *parser) resolveBlock(block *hcl.Block, target *hcl.BodySchema) (err err
|
||||
m = map[string]cty.Value{}
|
||||
}
|
||||
m[name] = outputValue
|
||||
p.ectx.Variables[block.Type] = cty.MapVal(m)
|
||||
|
||||
// The logical contents of this structure is similar to a map,
|
||||
// but it's possible for some attributes to be different in a way that's
|
||||
// illegal for a map so we use an object here instead which is structurally
|
||||
// equivalent but allows disparate types for different keys.
|
||||
p.ectx.Variables[block.Type] = cty.ObjectVal(m)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -541,6 +551,33 @@ func (p *parser) resolveBlockNames(block *hcl.Block) ([]string, error) {
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func (p *parser) validateVariables(vars map[string]*variable, ectx *hcl.EvalContext) hcl.Diagnostics {
|
||||
var diags hcl.Diagnostics
|
||||
for _, v := range vars {
|
||||
for _, validation := range v.Validations {
|
||||
condition, condDiags := validation.Condition.Value(ectx)
|
||||
if condDiags.HasErrors() {
|
||||
diags = append(diags, condDiags...)
|
||||
continue
|
||||
}
|
||||
if !condition.True() {
|
||||
message, msgDiags := validation.ErrorMessage.Value(ectx)
|
||||
if msgDiags.HasErrors() {
|
||||
diags = append(diags, msgDiags...)
|
||||
continue
|
||||
}
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Validation failed",
|
||||
Detail: message.AsString(),
|
||||
Subject: validation.Condition.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return diags
|
||||
}
|
||||
|
||||
type Variable struct {
|
||||
Name string
|
||||
Description string
|
||||
@@ -686,6 +723,9 @@ func Parse(b hcl.Body, opt Opt, val interface{}) (*ParseMeta, hcl.Diagnostics) {
|
||||
}
|
||||
vars = append(vars, v)
|
||||
}
|
||||
if diags := p.validateVariables(p.vars, p.ectx); diags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
for k := range p.funcs {
|
||||
if err := p.resolveFunction(p.ectx, k); err != nil {
|
||||
@@ -947,3 +987,8 @@ func key(ks ...any) uint64 {
|
||||
}
|
||||
return hash.Sum64()
|
||||
}
|
||||
|
||||
func decodeBody(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
|
||||
dec := gohcl.DecodeOptions{ImpliedType: ImpliedType}
|
||||
return dec.DecodeBody(body, ctx, val)
|
||||
}
|
||||
|
@@ -170,7 +170,6 @@ func indexOfFunc() function.Function {
|
||||
}
|
||||
}
|
||||
return cty.NilVal, errors.New("item not found")
|
||||
|
||||
},
|
||||
})
|
||||
}
|
||||
|
160
bake/hclparser/type_implied.go
Normal file
160
bake/hclparser/type_implied.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2017-2018 Martin Atkins
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package hclparser
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// ImpliedType takes an arbitrary Go value (as an interface{}) and attempts
|
||||
// to find a suitable cty.Type instance that could be used for a conversion
|
||||
// with ToCtyValue.
|
||||
//
|
||||
// This allows -- for simple situations at least -- types to be defined just
|
||||
// once in Go and the cty types derived from the Go types, but in the process
|
||||
// it makes some assumptions that may be undesirable so applications are
|
||||
// encouraged to build their cty types directly if exacting control is
|
||||
// required.
|
||||
//
|
||||
// Not all Go types can be represented as cty types, so an error may be
|
||||
// returned which is usually considered to be a bug in the calling program.
|
||||
// In particular, ImpliedType will never use capsule types in its returned
|
||||
// type, because it cannot know the capsule types supported by the calling
|
||||
// program.
|
||||
func ImpliedType(gv interface{}) (cty.Type, error) {
|
||||
rt := reflect.TypeOf(gv)
|
||||
var path cty.Path
|
||||
return impliedType(rt, path)
|
||||
}
|
||||
|
||||
func impliedType(rt reflect.Type, path cty.Path) (cty.Type, error) {
|
||||
if ety, err := impliedTypeExt(rt, path); err == nil {
|
||||
return ety, nil
|
||||
}
|
||||
|
||||
switch rt.Kind() {
|
||||
case reflect.Ptr:
|
||||
return impliedType(rt.Elem(), path)
|
||||
|
||||
// Primitive types
|
||||
case reflect.Bool:
|
||||
return cty.Bool, nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return cty.Number, nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return cty.Number, nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return cty.Number, nil
|
||||
case reflect.String:
|
||||
return cty.String, nil
|
||||
|
||||
// Collection types
|
||||
case reflect.Slice:
|
||||
path := append(path, cty.IndexStep{Key: cty.UnknownVal(cty.Number)})
|
||||
ety, err := impliedType(rt.Elem(), path)
|
||||
if err != nil {
|
||||
return cty.NilType, err
|
||||
}
|
||||
return cty.List(ety), nil
|
||||
case reflect.Map:
|
||||
if !stringType.AssignableTo(rt.Key()) {
|
||||
return cty.NilType, path.NewErrorf("no cty.Type for %s (must have string keys)", rt)
|
||||
}
|
||||
path := append(path, cty.IndexStep{Key: cty.UnknownVal(cty.String)})
|
||||
ety, err := impliedType(rt.Elem(), path)
|
||||
if err != nil {
|
||||
return cty.NilType, err
|
||||
}
|
||||
return cty.Map(ety), nil
|
||||
|
||||
// Structural types
|
||||
case reflect.Struct:
|
||||
return impliedStructType(rt, path)
|
||||
|
||||
default:
|
||||
return cty.NilType, path.NewErrorf("no cty.Type for %s", rt)
|
||||
}
|
||||
}
|
||||
|
||||
func impliedStructType(rt reflect.Type, path cty.Path) (cty.Type, error) {
|
||||
if valueType.AssignableTo(rt) {
|
||||
// Special case: cty.Value represents cty.DynamicPseudoType, for
|
||||
// type conformance checking.
|
||||
return cty.DynamicPseudoType, nil
|
||||
}
|
||||
|
||||
fieldIdxs := structTagIndices(rt)
|
||||
if len(fieldIdxs) == 0 {
|
||||
return cty.NilType, path.NewErrorf("no cty.Type for %s (no cty field tags)", rt)
|
||||
}
|
||||
|
||||
atys := make(map[string]cty.Type, len(fieldIdxs))
|
||||
|
||||
{
|
||||
// Temporary extension of path for attributes
|
||||
path := append(path, nil)
|
||||
|
||||
for k, fi := range fieldIdxs {
|
||||
path[len(path)-1] = cty.GetAttrStep{Name: k}
|
||||
|
||||
ft := rt.Field(fi).Type
|
||||
aty, err := impliedType(ft, path)
|
||||
if err != nil {
|
||||
return cty.NilType, err
|
||||
}
|
||||
|
||||
atys[k] = aty
|
||||
}
|
||||
}
|
||||
|
||||
return cty.Object(atys), nil
|
||||
}
|
||||
|
||||
var (
|
||||
valueType = reflect.TypeOf(cty.Value{})
|
||||
stringType = reflect.TypeOf("")
|
||||
)
|
||||
|
||||
// structTagIndices interrogates the fields of the given type (which must
|
||||
// be a struct type, or we'll panic) and returns a map from the cty
|
||||
// attribute names declared via struct tags to the indices of the
|
||||
// fields holding those tags.
|
||||
//
|
||||
// This function will panic if two fields within the struct are tagged with
|
||||
// the same cty attribute name.
|
||||
func structTagIndices(st reflect.Type) map[string]int {
|
||||
ct := st.NumField()
|
||||
ret := make(map[string]int, ct)
|
||||
|
||||
for i := 0; i < ct; i++ {
|
||||
field := st.Field(i)
|
||||
attrName := field.Tag.Get("cty")
|
||||
if attrName != "" {
|
||||
ret[attrName] = i
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
141
bake/hclparser/type_implied_ext.go
Normal file
141
bake/hclparser/type_implied_ext.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package hclparser
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
"github.com/zclconf/go-cty/cty/gocty"
|
||||
)
|
||||
|
||||
type CapsuleValue interface {
|
||||
// FromCtyValue will initialize this value using a cty.Value.
|
||||
FromCtyValue(in cty.Value, path cty.Path) error
|
||||
|
||||
// ToCtyValue will convert this capsule value into a native
|
||||
// cty.Value. This should not return a capsule type.
|
||||
ToCtyValue() cty.Value
|
||||
}
|
||||
|
||||
type extensionType int
|
||||
|
||||
const (
|
||||
nativeTypeExtension extensionType = iota
|
||||
)
|
||||
|
||||
func impliedTypeExt(rt reflect.Type, _ cty.Path) (cty.Type, error) {
|
||||
if rt.AssignableTo(capsuleValueType) {
|
||||
return capsuleValueCapsuleType(rt), nil
|
||||
}
|
||||
return cty.NilType, errdefs.ErrNotImplemented
|
||||
}
|
||||
|
||||
var (
|
||||
capsuleValueType = reflect.TypeFor[CapsuleValue]()
|
||||
capsuleValueTypes sync.Map
|
||||
)
|
||||
|
||||
func capsuleValueCapsuleType(rt reflect.Type) cty.Type {
|
||||
if val, loaded := capsuleValueTypes.Load(rt); loaded {
|
||||
return val.(cty.Type)
|
||||
}
|
||||
|
||||
// First time used.
|
||||
ety := cty.CapsuleWithOps(rt.Name(), rt.Elem(), &cty.CapsuleOps{
|
||||
ConversionTo: func(_ cty.Type) func(cty.Value, cty.Path) (any, error) {
|
||||
return func(in cty.Value, p cty.Path) (any, error) {
|
||||
rv := reflect.New(rt.Elem()).Interface()
|
||||
if err := rv.(CapsuleValue).FromCtyValue(in, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rv, nil
|
||||
}
|
||||
},
|
||||
ConversionFrom: func(want cty.Type) func(any, cty.Path) (cty.Value, error) {
|
||||
return func(in any, _ cty.Path) (cty.Value, error) {
|
||||
v := in.(CapsuleValue).ToCtyValue()
|
||||
return convert.Convert(v, want)
|
||||
}
|
||||
},
|
||||
ExtensionData: func(key any) any {
|
||||
switch key {
|
||||
case nativeTypeExtension:
|
||||
zero := reflect.Zero(rt).Interface()
|
||||
return zero.(CapsuleValue).ToCtyValue().Type()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Attempt to store the new type. Use whichever was loaded first in the case of a race condition.
|
||||
val, _ := capsuleValueTypes.LoadOrStore(rt, ety)
|
||||
return val.(cty.Type)
|
||||
}
|
||||
|
||||
// ToNativeValue will convert a value to only native cty types which will
|
||||
// remove capsule types if possible.
|
||||
func ToNativeValue(in cty.Value) cty.Value {
|
||||
want := toNativeType(in.Type())
|
||||
if in.Type().Equals(want) {
|
||||
return in
|
||||
} else if out, err := convert.Convert(in, want); err == nil {
|
||||
return out
|
||||
}
|
||||
return cty.NullVal(want)
|
||||
}
|
||||
|
||||
func toNativeType(in cty.Type) cty.Type {
|
||||
if et := in.MapElementType(); et != nil {
|
||||
return cty.Map(toNativeType(*et))
|
||||
}
|
||||
|
||||
if et := in.SetElementType(); et != nil {
|
||||
return cty.Set(toNativeType(*et))
|
||||
}
|
||||
|
||||
if et := in.ListElementType(); et != nil {
|
||||
return cty.List(toNativeType(*et))
|
||||
}
|
||||
|
||||
if in.IsObjectType() {
|
||||
var optional []string
|
||||
inAttrTypes := in.AttributeTypes()
|
||||
outAttrTypes := make(map[string]cty.Type, len(inAttrTypes))
|
||||
for name, typ := range inAttrTypes {
|
||||
outAttrTypes[name] = toNativeType(typ)
|
||||
if in.AttributeOptional(name) {
|
||||
optional = append(optional, name)
|
||||
}
|
||||
}
|
||||
return cty.ObjectWithOptionalAttrs(outAttrTypes, optional)
|
||||
}
|
||||
|
||||
if in.IsTupleType() {
|
||||
inTypes := in.TupleElementTypes()
|
||||
outTypes := make([]cty.Type, len(inTypes))
|
||||
for i, typ := range inTypes {
|
||||
outTypes[i] = toNativeType(typ)
|
||||
}
|
||||
return cty.Tuple(outTypes)
|
||||
}
|
||||
|
||||
if in.IsCapsuleType() {
|
||||
if out := in.CapsuleExtensionData(nativeTypeExtension); out != nil {
|
||||
return out.(cty.Type)
|
||||
}
|
||||
return cty.DynamicPseudoType
|
||||
}
|
||||
|
||||
return in
|
||||
}
|
||||
|
||||
func ToCtyValue(val any, ty cty.Type) (cty.Value, error) {
|
||||
out, err := gocty.ToCtyValue(val, ty)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
return ToNativeValue(out), nil
|
||||
}
|
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/buildx/builder"
|
||||
controllerapi "github.com/docker/buildx/controller/pb"
|
||||
"github.com/docker/buildx/driver"
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/docker/buildx/util/desktop"
|
||||
@@ -76,6 +77,8 @@ type Options struct {
|
||||
NoCacheFilter []string
|
||||
Platforms []specs.Platform
|
||||
Pull bool
|
||||
SecretSpecs []*controllerapi.Secret
|
||||
SSHSpecs []*controllerapi.SSH
|
||||
ShmSize opts.MemBytes
|
||||
Tags []string
|
||||
Target string
|
||||
|
@@ -23,7 +23,7 @@ func setupTest(tb testing.TB) {
|
||||
gitutil.GitInit(c, tb)
|
||||
|
||||
df := []byte("FROM alpine:latest\n")
|
||||
assert.NoError(tb, os.WriteFile("Dockerfile", df, 0644))
|
||||
require.NoError(tb, os.WriteFile("Dockerfile", df, 0644))
|
||||
|
||||
gitutil.GitAdd(c, tb, "Dockerfile")
|
||||
gitutil.GitCommit(c, tb, "initial commit")
|
||||
@@ -32,7 +32,7 @@ func setupTest(tb testing.TB) {
|
||||
|
||||
func TestGetGitAttributesNotGitRepo(t *testing.T) {
|
||||
_, err := getGitAttributes(context.Background(), t.TempDir(), "Dockerfile")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGetGitAttributesBadGitRepo(t *testing.T) {
|
||||
@@ -47,7 +47,7 @@ func TestGetGitAttributesNoContext(t *testing.T) {
|
||||
setupTest(t)
|
||||
|
||||
addGitAttrs, err := getGitAttributes(context.Background(), "", "Dockerfile")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
var so client.SolveOpt
|
||||
addGitAttrs(&so)
|
||||
assert.Empty(t, so.FrontendAttrs)
|
||||
@@ -195,8 +195,8 @@ func TestLocalDirsSub(t *testing.T) {
|
||||
gitutil.GitInit(c, t)
|
||||
|
||||
df := []byte("FROM alpine:latest\n")
|
||||
assert.NoError(t, os.MkdirAll("app", 0755))
|
||||
assert.NoError(t, os.WriteFile("app/Dockerfile", df, 0644))
|
||||
require.NoError(t, os.MkdirAll("app", 0755))
|
||||
require.NoError(t, os.WriteFile("app/Dockerfile", df, 0644))
|
||||
|
||||
gitutil.GitAdd(c, t, "app/Dockerfile")
|
||||
gitutil.GitCommit(c, t, "initial commit")
|
||||
|
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
type Container struct {
|
||||
cancelOnce sync.Once
|
||||
containerCancel func()
|
||||
containerCancel func(error)
|
||||
isUnavailable atomic.Bool
|
||||
initStarted atomic.Bool
|
||||
container gateway.Container
|
||||
@@ -31,18 +31,18 @@ func NewContainer(ctx context.Context, resultCtx *ResultHandle, cfg *controllera
|
||||
errCh := make(chan error)
|
||||
go func() {
|
||||
err := resultCtx.build(func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
ctx, cancel := context.WithCancelCause(ctx)
|
||||
go func() {
|
||||
<-mainCtx.Done()
|
||||
cancel()
|
||||
cancel(errors.WithStack(context.Canceled))
|
||||
}()
|
||||
|
||||
containerCfg, err := resultCtx.getContainerConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
containerCtx, containerCancel := context.WithCancel(ctx)
|
||||
defer containerCancel()
|
||||
containerCtx, containerCancel := context.WithCancelCause(ctx)
|
||||
defer containerCancel(errors.WithStack(context.Canceled))
|
||||
bkContainer, err := c.NewContainer(containerCtx, containerCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -83,7 +83,7 @@ func (c *Container) Cancel() {
|
||||
c.markUnavailable()
|
||||
c.cancelOnce.Do(func() {
|
||||
if c.containerCancel != nil {
|
||||
c.containerCancel()
|
||||
c.containerCancel(errors.WithStack(context.Canceled))
|
||||
}
|
||||
close(c.releaseCh)
|
||||
})
|
||||
|
@@ -10,7 +10,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func generateRandomData(size int) []byte {
|
||||
@@ -57,7 +56,7 @@ func TestSyncMultiReaderParallel(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err, "Reader %d error", readerId)
|
||||
assert.NoError(t, err, "Reader %d error", readerId)
|
||||
|
||||
if mathrand.Intn(1000) == 0 { //nolint:gosec
|
||||
t.Logf("Reader %d closing", readerId)
|
||||
|
@@ -82,7 +82,7 @@ func NewResultHandle(ctx context.Context, cc *client.Client, opt client.SolveOpt
|
||||
var respHandle *ResultHandle
|
||||
|
||||
go func() {
|
||||
defer cancel(context.Canceled) // ensure no dangling processes
|
||||
defer func() { cancel(errors.WithStack(context.Canceled)) }() // ensure no dangling processes
|
||||
|
||||
var res *gateway.Result
|
||||
var err error
|
||||
@@ -181,7 +181,7 @@ func NewResultHandle(ctx context.Context, cc *client.Client, opt client.SolveOpt
|
||||
case <-respHandle.done:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
return nil, ctx.Err()
|
||||
return nil, context.Cause(ctx)
|
||||
}, nil)
|
||||
if respHandle != nil {
|
||||
return
|
||||
|
@@ -138,7 +138,7 @@ func TestToBuildkitExtraHosts(t *testing.T) {
|
||||
actualOut, actualErr := toBuildkitExtraHosts(context.TODO(), tc.input, nil)
|
||||
if tc.expectedErr == "" {
|
||||
require.Equal(t, tc.expectedOut, actualOut)
|
||||
require.Nil(t, actualErr)
|
||||
require.NoError(t, actualErr)
|
||||
} else {
|
||||
require.Zero(t, actualOut)
|
||||
require.Error(t, actualErr, tc.expectedErr)
|
||||
|
@@ -288,7 +288,15 @@ func GetBuilders(dockerCli command.Cli, txn *store.Txn) ([]*Builder, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
builders := make([]*Builder, len(storeng))
|
||||
contexts, err := dockerCli.ContextStore().List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Slice(contexts, func(i, j int) bool {
|
||||
return contexts[i].Name < contexts[j].Name
|
||||
})
|
||||
|
||||
builders := make([]*Builder, len(storeng), len(storeng)+len(contexts))
|
||||
seen := make(map[string]struct{})
|
||||
for i, ng := range storeng {
|
||||
b, err := New(dockerCli,
|
||||
@@ -303,14 +311,6 @@ func GetBuilders(dockerCli command.Cli, txn *store.Txn) ([]*Builder, error) {
|
||||
seen[b.NodeGroup.Name] = struct{}{}
|
||||
}
|
||||
|
||||
contexts, err := dockerCli.ContextStore().List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Slice(contexts, func(i, j int) bool {
|
||||
return contexts[i].Name < contexts[j].Name
|
||||
})
|
||||
|
||||
for _, c := range contexts {
|
||||
// if a context has the same name as an instance from the store, do not
|
||||
// add it to the builders list. An instance from the store takes
|
||||
@@ -522,8 +522,9 @@ func Create(ctx context.Context, txn *store.Txn, dockerCli command.Cli, opts Cre
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
||||
defer cancel()
|
||||
cancelCtx, cancel := context.WithCancelCause(ctx)
|
||||
timeoutCtx, _ := context.WithTimeoutCause(cancelCtx, 20*time.Second, errors.WithStack(context.DeadlineExceeded)) //nolint:govet,lostcancel // no need to manually cancel this context as we already rely on parent
|
||||
defer func() { cancel(errors.WithStack(context.Canceled)) }()
|
||||
|
||||
nodes, err := b.LoadNodes(timeoutCtx, WithData())
|
||||
if err != nil {
|
||||
|
@@ -19,13 +19,13 @@ func TestCsvToMap(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Contains(t, r, "tolerations")
|
||||
require.Equal(t, r["tolerations"], "key=foo,value=bar;key=foo2,value=bar2")
|
||||
require.Equal(t, "key=foo,value=bar;key=foo2,value=bar2", r["tolerations"])
|
||||
|
||||
require.Contains(t, r, "replicas")
|
||||
require.Equal(t, r["replicas"], "1")
|
||||
require.Equal(t, "1", r["replicas"])
|
||||
|
||||
require.Contains(t, r, "namespace")
|
||||
require.Equal(t, r["namespace"], "default")
|
||||
require.Equal(t, "default", r["namespace"])
|
||||
}
|
||||
|
||||
func TestParseBuildkitdFlags(t *testing.T) {
|
||||
|
@@ -107,16 +107,23 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get current working directory")
|
||||
}
|
||||
// filesystem access under the current working directory is allowed by default
|
||||
ent.FSRead = append(ent.FSRead, wd)
|
||||
ent.FSWrite = append(ent.FSWrite, wd)
|
||||
|
||||
ctx2, cancel := context.WithCancel(context.TODO())
|
||||
defer cancel()
|
||||
ctx2, cancel := context.WithCancelCause(context.TODO())
|
||||
defer cancel(errors.WithStack(context.Canceled))
|
||||
|
||||
var nodes []builder.Node
|
||||
var progressConsoleDesc, progressTextDesc string
|
||||
|
||||
// instance only needed for reading remote bake files or building
|
||||
var driverType string
|
||||
if url != "" || !in.printOnly {
|
||||
if url != "" || !(in.printOnly || in.listTargets || in.listVars) {
|
||||
b, err := builder.New(dockerCli,
|
||||
builder.WithName(in.builder),
|
||||
builder.WithContextPathHash(contextPathHash),
|
||||
@@ -250,7 +257,7 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := exp.Prompt(ctx, &syncWriter{w: dockerCli.Err(), wait: printer.Wait}); err != nil {
|
||||
if err := exp.Prompt(ctx, url != "", &syncWriter{w: dockerCli.Err(), wait: printer.Wait}); err != nil {
|
||||
return err
|
||||
}
|
||||
if printer.IsDone() {
|
||||
|
@@ -325,8 +325,8 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
|
||||
}
|
||||
attributes := buildMetricAttributes(dockerCli, driverType, &options)
|
||||
|
||||
ctx2, cancel := context.WithCancel(context.TODO())
|
||||
defer cancel()
|
||||
ctx2, cancel := context.WithCancelCause(context.TODO())
|
||||
defer func() { cancel(errors.WithStack(context.Canceled)) }()
|
||||
progressMode, err := options.toDisplayMode()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -763,8 +763,11 @@ func decodeExporterResponse(exporterResponse map[string]string) map[string]inter
|
||||
}
|
||||
var raw map[string]interface{}
|
||||
if err = json.Unmarshal(dt, &raw); err != nil || len(raw) == 0 {
|
||||
out[k] = v
|
||||
continue
|
||||
var rawList []map[string]interface{}
|
||||
if err = json.Unmarshal(dt, &rawList); err != nil || len(rawList) == 0 {
|
||||
out[k] = v
|
||||
continue
|
||||
}
|
||||
}
|
||||
out[k] = json.RawMessage(dt)
|
||||
}
|
||||
@@ -882,7 +885,6 @@ func printWarnings(w io.Writer, warnings []client.VertexWarning, mode progressui
|
||||
src.Print(w)
|
||||
}
|
||||
fmt.Fprintf(w, "\n")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -42,7 +42,7 @@ func runCreate(ctx context.Context, dockerCli command.Cli, in createOptions, arg
|
||||
return errors.Errorf("can't push with no tags specified, please set --tag or --dry-run")
|
||||
}
|
||||
|
||||
fileArgs := make([]string, len(in.files))
|
||||
fileArgs := make([]string, len(in.files), len(in.files)+len(args))
|
||||
for i, f := range in.files {
|
||||
dt, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
@@ -173,8 +173,8 @@ func runCreate(ctx context.Context, dockerCli command.Cli, in createOptions, arg
|
||||
// new resolver cause need new auth
|
||||
r = imagetools.New(imageopt)
|
||||
|
||||
ctx2, cancel := context.WithCancel(context.TODO())
|
||||
defer cancel()
|
||||
ctx2, cancel := context.WithCancelCause(context.TODO())
|
||||
defer func() { cancel(errors.WithStack(context.Canceled)) }()
|
||||
printer, err := progress.NewPrinter(ctx2, os.Stderr, progressui.DisplayMode(in.progress))
|
||||
if err != nil {
|
||||
return err
|
||||
|
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/debug"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -34,8 +35,9 @@ func runInspect(ctx context.Context, dockerCli command.Cli, in inspectOptions) e
|
||||
return err
|
||||
}
|
||||
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
||||
defer cancel()
|
||||
timeoutCtx, cancel := context.WithCancelCause(ctx)
|
||||
timeoutCtx, _ = context.WithTimeoutCause(timeoutCtx, 20*time.Second, errors.WithStack(context.DeadlineExceeded)) //nolint:govet,lostcancel // no need to manually cancel this context as we already rely on parent
|
||||
defer func() { cancel(errors.WithStack(context.Canceled)) }()
|
||||
|
||||
nodes, err := b.LoadNodes(timeoutCtx, builder.WithData())
|
||||
if in.bootstrap {
|
||||
|
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
@@ -57,8 +58,9 @@ func runLs(ctx context.Context, dockerCli command.Cli, in lsOptions) error {
|
||||
return err
|
||||
}
|
||||
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
||||
defer cancel()
|
||||
timeoutCtx, cancel := context.WithCancelCause(ctx)
|
||||
timeoutCtx, _ = context.WithTimeoutCause(timeoutCtx, 20*time.Second, errors.WithStack(context.DeadlineExceeded)) //nolint:govet,lostcancel // no need to manually cancel this context as we already rely on parent
|
||||
defer func() { cancel(errors.WithStack(context.Canceled)) }()
|
||||
|
||||
eg, _ := errgroup.WithContext(timeoutCtx)
|
||||
for _, b := range builders {
|
||||
@@ -319,7 +321,7 @@ func (tp truncatedPlatforms) String() string {
|
||||
if tpf, ok := tp.res[mpf]; ok {
|
||||
seen[mpf] = struct{}{}
|
||||
if len(tpf) == 1 {
|
||||
out = append(out, fmt.Sprintf("%s", tpf[0]))
|
||||
out = append(out, tpf[0])
|
||||
count++
|
||||
} else {
|
||||
hasPreferredPlatform := false
|
||||
@@ -347,7 +349,7 @@ func (tp truncatedPlatforms) String() string {
|
||||
continue
|
||||
}
|
||||
if len(tp.res[mpf]) == 1 {
|
||||
out = append(out, fmt.Sprintf("%s", tp.res[mpf][0]))
|
||||
out = append(out, tp.res[mpf][0])
|
||||
count++
|
||||
} else {
|
||||
hasPreferredPlatform := false
|
||||
|
@@ -16,6 +16,9 @@ import (
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/moby/buildkit/client"
|
||||
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
||||
pb "github.com/moby/buildkit/solver/pb"
|
||||
"github.com/moby/buildkit/util/apicaps"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -107,6 +110,17 @@ func runPrune(ctx context.Context, dockerCli command.Cli, opts pruneOptions) err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// check if the client supports newer prune options
|
||||
if opts.maxUsedSpace.Value() != 0 || opts.minFreeSpace.Value() != 0 {
|
||||
caps, err := loadLLBCaps(ctx, c)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to load buildkit capabilities for prune")
|
||||
}
|
||||
if caps.Supports(pb.CapGCFreeSpaceFilter) != nil {
|
||||
return errors.New("buildkit v0.17.0+ is required for max-used-space and min-free-space filters")
|
||||
}
|
||||
}
|
||||
|
||||
popts := []client.PruneOption{
|
||||
client.WithKeepOpt(pi.KeepDuration, opts.reservedSpace.Value(), opts.maxUsedSpace.Value(), opts.minFreeSpace.Value()),
|
||||
client.WithFilter(pi.Filter),
|
||||
@@ -133,6 +147,17 @@ func runPrune(ctx context.Context, dockerCli command.Cli, opts pruneOptions) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadLLBCaps(ctx context.Context, c *client.Client) (apicaps.CapSet, error) {
|
||||
var caps apicaps.CapSet
|
||||
_, err := c.Build(ctx, client.SolveOpt{
|
||||
Internal: true,
|
||||
}, "buildx", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
||||
caps = c.BuildOpts().LLBCaps
|
||||
return nil, nil
|
||||
}, nil)
|
||||
return caps, err
|
||||
}
|
||||
|
||||
func pruneCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
|
||||
options := pruneOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
|
@@ -150,8 +150,9 @@ func rmAllInactive(ctx context.Context, txn *store.Txn, dockerCli command.Cli, i
|
||||
return err
|
||||
}
|
||||
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
||||
defer cancel()
|
||||
timeoutCtx, cancel := context.WithCancelCause(ctx)
|
||||
timeoutCtx, _ = context.WithTimeoutCause(timeoutCtx, 20*time.Second, errors.WithStack(context.DeadlineExceeded)) //nolint:govet,lostcancel // no need to manually cancel this context as we already rely on parent
|
||||
defer func() { cancel(errors.WithStack(context.Canceled)) }()
|
||||
|
||||
eg, _ := errgroup.WithContext(timeoutCtx)
|
||||
for _, b := range builders {
|
||||
|
@@ -46,7 +46,6 @@ func runUse(dockerCli command.Cli, in useOptions) error {
|
||||
return errors.Errorf("run `docker context use %s` to switch to context %s", in.builder, in.builder)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return errors.Wrapf(err, "failed to find instance %q", in.builder)
|
||||
}
|
||||
|
@@ -109,7 +109,7 @@ func (b *localController) Invoke(ctx context.Context, sessionID string, pid stri
|
||||
|
||||
// Attach containerIn to this process
|
||||
ioCancelledCh := make(chan struct{})
|
||||
proc.ForwardIO(&ioset.In{Stdin: ioIn, Stdout: ioOut, Stderr: ioErr}, func() { close(ioCancelledCh) })
|
||||
proc.ForwardIO(&ioset.In{Stdin: ioIn, Stdout: ioOut, Stderr: ioErr}, func(error) { close(ioCancelledCh) })
|
||||
|
||||
select {
|
||||
case <-ioCancelledCh:
|
||||
@@ -117,7 +117,7 @@ func (b *localController) Invoke(ctx context.Context, sessionID string, pid stri
|
||||
case err := <-proc.Done():
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
return context.Cause(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -45,6 +45,7 @@ func CreateExports(entries []*ExportEntry) ([]client.ExportEntry, error) {
|
||||
supportDir = !tar
|
||||
case "registry":
|
||||
out.Type = client.ExporterImage
|
||||
out.Attrs["push"] = "true"
|
||||
}
|
||||
|
||||
if supportDir {
|
||||
|
@@ -153,7 +153,6 @@ func ResolveOptionPaths(options *BuildOptions) (_ *BuildOptions, err error) {
|
||||
}
|
||||
}
|
||||
ps = append(ps, p)
|
||||
|
||||
}
|
||||
s.Paths = ps
|
||||
ssh = append(ssh, s)
|
||||
|
@@ -22,9 +22,7 @@ func (w *writer) Write(status *client.SolveStatus) {
|
||||
w.ch <- ToControlStatus(status)
|
||||
}
|
||||
|
||||
func (w *writer) WriteBuildRef(target string, ref string) {
|
||||
return
|
||||
}
|
||||
func (w *writer) WriteBuildRef(target string, ref string) {}
|
||||
|
||||
func (w *writer) ValidateLogSource(digest.Digest, interface{}) bool {
|
||||
return true
|
||||
|
@@ -18,16 +18,16 @@ type Process struct {
|
||||
invokeConfig *pb.InvokeConfig
|
||||
errCh chan error
|
||||
processCancel func()
|
||||
serveIOCancel func()
|
||||
serveIOCancel func(error)
|
||||
}
|
||||
|
||||
// ForwardIO forwards process's io to the specified reader/writer.
|
||||
// Optionally specify ioCancelCallback which will be called when
|
||||
// the process closes the specified IO. This will be useful for additional cleanup.
|
||||
func (p *Process) ForwardIO(in *ioset.In, ioCancelCallback func()) {
|
||||
func (p *Process) ForwardIO(in *ioset.In, ioCancelCallback func(error)) {
|
||||
p.inEnd.SetIn(in)
|
||||
if f := p.serveIOCancel; f != nil {
|
||||
f()
|
||||
f(errors.WithStack(context.Canceled))
|
||||
}
|
||||
p.serveIOCancel = ioCancelCallback
|
||||
}
|
||||
@@ -124,9 +124,16 @@ func (m *Manager) StartProcess(pid string, resultCtx *build.ResultHandle, cfg *p
|
||||
f.SetOut(&out)
|
||||
|
||||
// Register process
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
ctx, cancel := context.WithCancelCause(context.TODO())
|
||||
var cancelOnce sync.Once
|
||||
processCancelFunc := func() { cancelOnce.Do(func() { cancel(); f.Close(); in.Close(); out.Close() }) }
|
||||
processCancelFunc := func() {
|
||||
cancelOnce.Do(func() {
|
||||
cancel(errors.WithStack(context.Canceled))
|
||||
f.Close()
|
||||
in.Close()
|
||||
out.Close()
|
||||
})
|
||||
}
|
||||
p := &Process{
|
||||
inEnd: f,
|
||||
invokeConfig: cfg,
|
||||
|
@@ -62,9 +62,10 @@ func NewRemoteBuildxController(ctx context.Context, dockerCli command.Cli, opts
|
||||
serverRoot := filepath.Join(rootDir, "shared")
|
||||
|
||||
// connect to buildx server if it is already running
|
||||
ctx2, cancel := context.WithTimeout(ctx, 1*time.Second)
|
||||
ctx2, cancel := context.WithCancelCause(ctx)
|
||||
ctx2, _ = context.WithTimeoutCause(ctx2, 1*time.Second, errors.WithStack(context.DeadlineExceeded)) //nolint:govet,lostcancel // no need to manually cancel this context as we already rely on parent
|
||||
c, err := newBuildxClientAndCheck(ctx2, filepath.Join(serverRoot, defaultSocketFilename))
|
||||
cancel()
|
||||
cancel(errors.WithStack(context.Canceled))
|
||||
if err != nil {
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
return nil, errors.Wrap(err, "cannot connect to the buildx server")
|
||||
@@ -90,9 +91,10 @@ func NewRemoteBuildxController(ctx context.Context, dockerCli command.Cli, opts
|
||||
go wait()
|
||||
|
||||
// wait for buildx server to be ready
|
||||
ctx2, cancel = context.WithTimeout(ctx, 10*time.Second)
|
||||
ctx2, cancel = context.WithCancelCause(ctx)
|
||||
ctx2, _ = context.WithTimeoutCause(ctx2, 10*time.Second, errors.WithStack(context.DeadlineExceeded)) //nolint:govet,lostcancel // no need to manually cancel this context as we already rely on parent
|
||||
c, err = newBuildxClientAndCheck(ctx2, filepath.Join(serverRoot, defaultSocketFilename))
|
||||
cancel()
|
||||
cancel(errors.WithStack(context.Canceled))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot connect to the buildx server")
|
||||
}
|
||||
|
@@ -302,7 +302,6 @@ func attachIO(ctx context.Context, stream msgStream, initMessage *pb.InitMessage
|
||||
out = cfg.stderr
|
||||
default:
|
||||
return errors.Errorf("unsupported fd %d", file.Fd)
|
||||
|
||||
}
|
||||
if out == nil {
|
||||
logrus.Warnf("attachIO: no writer for fd %d", file.Fd)
|
||||
@@ -345,7 +344,7 @@ func receive(ctx context.Context, stream msgStream) (*pb.Message, error) {
|
||||
case err := <-errCh:
|
||||
return nil, err
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
return nil, context.Cause(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -37,7 +37,7 @@ type Server struct {
|
||||
type session struct {
|
||||
buildOnGoing atomic.Bool
|
||||
statusChan chan *pb.StatusResponse
|
||||
cancelBuild func()
|
||||
cancelBuild func(error)
|
||||
buildOptions *pb.BuildOptions
|
||||
inputPipe *io.PipeWriter
|
||||
|
||||
@@ -109,7 +109,7 @@ func (m *Server) Disconnect(ctx context.Context, req *pb.DisconnectRequest) (res
|
||||
m.sessionMu.Lock()
|
||||
if s, ok := m.session[sessionID]; ok {
|
||||
if s.cancelBuild != nil {
|
||||
s.cancelBuild()
|
||||
s.cancelBuild(errors.WithStack(context.Canceled))
|
||||
}
|
||||
s.cancelRunningProcesses()
|
||||
if s.result != nil {
|
||||
@@ -127,7 +127,7 @@ func (m *Server) Close() error {
|
||||
for k := range m.session {
|
||||
if s, ok := m.session[k]; ok {
|
||||
if s.cancelBuild != nil {
|
||||
s.cancelBuild()
|
||||
s.cancelBuild(errors.WithStack(context.Canceled))
|
||||
}
|
||||
s.cancelRunningProcesses()
|
||||
}
|
||||
@@ -199,8 +199,8 @@ func (m *Server) Build(ctx context.Context, req *pb.BuildRequest) (*pb.BuildResp
|
||||
pw := pb.NewProgressWriter(statusChan)
|
||||
|
||||
// Build the specified request
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
ctx, cancel := context.WithCancelCause(ctx)
|
||||
defer func() { cancel(errors.WithStack(context.Canceled)) }()
|
||||
resp, res, _, buildErr := m.buildFunc(ctx, req.Options, inR, pw)
|
||||
m.sessionMu.Lock()
|
||||
if s, ok := m.session[sessionID]; ok {
|
||||
@@ -341,7 +341,7 @@ func (m *Server) Input(stream pb.Controller_InputServer) (err error) {
|
||||
select {
|
||||
case msg = <-msgCh:
|
||||
case <-ctx.Done():
|
||||
return errors.Wrap(ctx.Err(), "canceled")
|
||||
return context.Cause(ctx)
|
||||
}
|
||||
if msg == nil {
|
||||
return nil
|
||||
@@ -370,9 +370,9 @@ func (m *Server) Invoke(srv pb.Controller_InvokeServer) error {
|
||||
initDoneCh := make(chan *processes.Process)
|
||||
initErrCh := make(chan error)
|
||||
eg, egCtx := errgroup.WithContext(context.TODO())
|
||||
srvIOCtx, srvIOCancel := context.WithCancel(egCtx)
|
||||
srvIOCtx, srvIOCancel := context.WithCancelCause(egCtx)
|
||||
eg.Go(func() error {
|
||||
defer srvIOCancel()
|
||||
defer srvIOCancel(errors.WithStack(context.Canceled))
|
||||
return serveIO(srvIOCtx, srv, func(initMessage *pb.InitMessage) (retErr error) {
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
@@ -418,7 +418,7 @@ func (m *Server) Invoke(srv pb.Controller_InvokeServer) error {
|
||||
})
|
||||
})
|
||||
eg.Go(func() (rErr error) {
|
||||
defer srvIOCancel()
|
||||
defer srvIOCancel(errors.WithStack(context.Canceled))
|
||||
// Wait for init done
|
||||
var proc *processes.Process
|
||||
select {
|
||||
|
@@ -41,11 +41,15 @@ target "lint" {
|
||||
platforms = GOLANGCI_LINT_MULTIPLATFORM != "" ? [
|
||||
"darwin/amd64",
|
||||
"darwin/arm64",
|
||||
"freebsd/amd64",
|
||||
"freebsd/arm64",
|
||||
"linux/amd64",
|
||||
"linux/arm64",
|
||||
"linux/s390x",
|
||||
"linux/ppc64le",
|
||||
"linux/riscv64",
|
||||
"openbsd/amd64",
|
||||
"openbsd/arm64",
|
||||
"windows/amd64",
|
||||
"windows/arm64"
|
||||
] : []
|
||||
@@ -154,6 +158,8 @@ target "binaries-cross" {
|
||||
platforms = [
|
||||
"darwin/amd64",
|
||||
"darwin/arm64",
|
||||
"freebsd/amd64",
|
||||
"freebsd/arm64",
|
||||
"linux/amd64",
|
||||
"linux/arm/v6",
|
||||
"linux/arm/v7",
|
||||
@@ -161,6 +167,8 @@ target "binaries-cross" {
|
||||
"linux/ppc64le",
|
||||
"linux/riscv64",
|
||||
"linux/s390x",
|
||||
"openbsd/amd64",
|
||||
"openbsd/arm64",
|
||||
"windows/amd64",
|
||||
"windows/arm64"
|
||||
]
|
||||
|
@@ -359,6 +359,21 @@ target "app" {
|
||||
}
|
||||
```
|
||||
|
||||
### `target.call`
|
||||
|
||||
Specifies the frontend method to use. Frontend methods let you, for example,
|
||||
execute build checks only, instead of running a build. This is the same as the
|
||||
`--call` flag.
|
||||
|
||||
```hcl
|
||||
target "app" {
|
||||
call = "check"
|
||||
}
|
||||
```
|
||||
|
||||
For more information about frontend methods, refer to the CLI reference for
|
||||
[`docker buildx build --call`](https://docs.docker.com/reference/cli/docker/buildx/build/#call).
|
||||
|
||||
### `target.context`
|
||||
|
||||
Specifies the location of the build context to use for this target.
|
||||
|
@@ -177,7 +177,6 @@ func (d *Driver) create(ctx context.Context, l progress.SubLogger) error {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
_, err := d.DockerAPI.ContainerCreate(ctx, cfg, hc, &network.NetworkingConfig{}, nil, d.Name)
|
||||
if err != nil && !errdefs.IsConflict(err) {
|
||||
@@ -213,7 +212,7 @@ func (d *Driver) wait(ctx context.Context, l progress.SubLogger) error {
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
return context.Cause(ctx)
|
||||
case <-time.After(time.Duration(try*120) * time.Millisecond):
|
||||
try++
|
||||
continue
|
||||
|
@@ -29,7 +29,7 @@ func (d *Driver) Bootstrap(ctx context.Context, l progress.Logger) error {
|
||||
func (d *Driver) Info(ctx context.Context) (*driver.Info, error) {
|
||||
_, err := d.DockerAPI.ServerVersion(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(driver.ErrNotConnecting{}, err.Error())
|
||||
return nil, errors.Wrap(driver.ErrNotConnecting{}, err.Error())
|
||||
}
|
||||
return &driver.Info{
|
||||
Status: driver.Running,
|
||||
@@ -39,7 +39,7 @@ func (d *Driver) Info(ctx context.Context) (*driver.Info, error) {
|
||||
func (d *Driver) Version(ctx context.Context) (string, error) {
|
||||
v, err := d.DockerAPI.ServerVersion(ctx)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(driver.ErrNotConnecting{}, err.Error())
|
||||
return "", errors.Wrap(driver.ErrNotConnecting{}, err.Error())
|
||||
}
|
||||
if bkversion, _ := resolveBuildKitVersion(v.Version); bkversion != "" {
|
||||
return bkversion, nil
|
||||
|
@@ -176,11 +176,6 @@ func resolveBuildKitVersion(ver string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
//if _, errs := c.Validate(mobyVersion); len(errs) > 0 {
|
||||
// for _, err := range errs {
|
||||
// fmt.Printf("%s: %v\n", m.MobyVersionConstraint, err)
|
||||
// }
|
||||
//}
|
||||
if !c.Check(mobyVersion) {
|
||||
continue
|
||||
}
|
||||
|
@@ -191,7 +191,7 @@ func checkClientConfig(t *testing.T, ep Endpoint, server, namespace string, ca,
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, proxyURLString, proxyURL.String())
|
||||
} else {
|
||||
assert.True(t, cfg.Proxy == nil, "expected proxy to be nil, but is not nil instead")
|
||||
assert.Nil(t, cfg.Proxy, "expected proxy to be nil, but is not nil instead")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ func TestSaveLoadGKEConfig(t *testing.T) {
|
||||
persistedMetadata, err := store.GetMetadata("gke-context")
|
||||
require.NoError(t, err)
|
||||
persistedEPMeta := EndpointFromContext(persistedMetadata)
|
||||
assert.True(t, persistedEPMeta != nil)
|
||||
assert.NotNil(t, persistedEPMeta)
|
||||
persistedEP, err := persistedEPMeta.WithTLSData(store, "gke-context")
|
||||
require.NoError(t, err)
|
||||
persistedCfg := persistedEP.KubernetesConfig()
|
||||
@@ -249,7 +249,7 @@ func TestSaveLoadEKSConfig(t *testing.T) {
|
||||
persistedMetadata, err := store.GetMetadata("eks-context")
|
||||
require.NoError(t, err)
|
||||
persistedEPMeta := EndpointFromContext(persistedMetadata)
|
||||
assert.True(t, persistedEPMeta != nil)
|
||||
assert.NotNil(t, persistedEPMeta)
|
||||
persistedEP, err := persistedEPMeta.WithTLSData(store, "eks-context")
|
||||
require.NoError(t, err)
|
||||
persistedCfg := persistedEP.KubernetesConfig()
|
||||
@@ -274,14 +274,14 @@ func TestSaveLoadK3SConfig(t *testing.T) {
|
||||
persistedMetadata, err := store.GetMetadata("k3s-context")
|
||||
require.NoError(t, err)
|
||||
persistedEPMeta := EndpointFromContext(persistedMetadata)
|
||||
assert.True(t, persistedEPMeta != nil)
|
||||
assert.NotNil(t, persistedEPMeta)
|
||||
persistedEP, err := persistedEPMeta.WithTLSData(store, "k3s-context")
|
||||
require.NoError(t, err)
|
||||
persistedCfg := persistedEP.KubernetesConfig()
|
||||
actualCfg, err := persistedCfg.ClientConfig()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, len(actualCfg.Username) > 0)
|
||||
assert.True(t, len(actualCfg.Password) > 0)
|
||||
assert.Greater(t, len(actualCfg.Username), 0)
|
||||
assert.Greater(t, len(actualCfg.Password), 0)
|
||||
assert.Equal(t, expectedCfg.Username, actualCfg.Username)
|
||||
assert.Equal(t, expectedCfg.Password, actualCfg.Password)
|
||||
}
|
||||
|
@@ -112,7 +112,7 @@ func (d *Driver) wait(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
return context.Cause(ctx)
|
||||
case <-timeoutChan:
|
||||
return err
|
||||
case <-ticker.C:
|
||||
|
@@ -47,8 +47,9 @@ func (d *Driver) Bootstrap(ctx context.Context, l progress.Logger) error {
|
||||
return err
|
||||
}
|
||||
return progress.Wrap("[internal] waiting for connection", l, func(_ progress.SubLogger) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
||||
defer cancel()
|
||||
cancelCtx, cancel := context.WithCancelCause(ctx)
|
||||
ctx, _ := context.WithTimeoutCause(cancelCtx, 20*time.Second, errors.WithStack(context.DeadlineExceeded)) //nolint:govet,lostcancel // no need to manually cancel this context as we already rely on parent
|
||||
defer func() { cancel(errors.WithStack(context.Canceled)) }()
|
||||
return c.Wait(ctx)
|
||||
})
|
||||
}
|
||||
|
59
go.mod
59
go.mod
@@ -6,19 +6,20 @@ require (
|
||||
github.com/Masterminds/semver/v3 v3.2.1
|
||||
github.com/Microsoft/go-winio v0.6.2
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.6
|
||||
github.com/compose-spec/compose-go/v2 v2.2.0
|
||||
github.com/compose-spec/compose-go/v2 v2.4.4
|
||||
github.com/containerd/console v1.0.4
|
||||
github.com/containerd/containerd v1.7.22
|
||||
github.com/containerd/continuity v0.4.4
|
||||
github.com/containerd/errdefs v0.1.0
|
||||
github.com/containerd/containerd v1.7.24
|
||||
github.com/containerd/continuity v0.4.5
|
||||
github.com/containerd/errdefs v0.3.0
|
||||
github.com/containerd/log v0.1.0
|
||||
github.com/containerd/platforms v0.2.1
|
||||
github.com/containerd/typeurl/v2 v2.2.0
|
||||
github.com/containerd/typeurl/v2 v2.2.3
|
||||
github.com/creack/pty v1.1.21
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/cli v27.3.1+incompatible
|
||||
github.com/docker/cli v27.4.0-rc.2+incompatible
|
||||
github.com/docker/cli-docs-tool v0.8.0
|
||||
github.com/docker/docker v27.3.1+incompatible
|
||||
github.com/docker/docker v27.4.0-rc.2+incompatible
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/gofrs/flock v0.12.1
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
@@ -27,7 +28,7 @@ require (
|
||||
github.com/hashicorp/hcl/v2 v2.20.1
|
||||
github.com/in-toto/in-toto-golang v0.5.0
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/moby/buildkit v0.17.0-rc2
|
||||
github.com/moby/buildkit v0.18.0-rc2
|
||||
github.com/moby/sys/mountinfo v0.7.2
|
||||
github.com/moby/sys/signal v0.7.1
|
||||
github.com/morikuni/aec v1.0.0
|
||||
@@ -41,19 +42,19 @@ require (
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tonistiigi/fsutil v0.0.0-20241028165955-397af5306b5c
|
||||
github.com/tonistiigi/fsutil v0.0.0-20241121093142-31cf1f437184
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4
|
||||
github.com/zclconf/go-cty v1.14.4
|
||||
go.opentelemetry.io/otel v1.21.0
|
||||
go.opentelemetry.io/otel/metric v1.21.0
|
||||
go.opentelemetry.io/otel/sdk v1.21.0
|
||||
go.opentelemetry.io/otel/trace v1.21.0
|
||||
go.opentelemetry.io/otel v1.28.0
|
||||
go.opentelemetry.io/otel/metric v1.28.0
|
||||
go.opentelemetry.io/otel/sdk v1.28.0
|
||||
go.opentelemetry.io/otel/trace v1.28.0
|
||||
golang.org/x/mod v0.21.0
|
||||
golang.org/x/sync v0.8.0
|
||||
golang.org/x/sys v0.26.0
|
||||
golang.org/x/term v0.24.0
|
||||
golang.org/x/text v0.18.0
|
||||
google.golang.org/grpc v1.66.2
|
||||
google.golang.org/grpc v1.66.3
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1
|
||||
google.golang.org/protobuf v1.35.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -82,12 +83,11 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
|
||||
github.com/aws/smithy-go v1.19.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/containerd/api v1.7.19 // indirect
|
||||
github.com/containerd/ttrpc v1.2.5 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||
@@ -96,7 +96,7 @@ require (
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fvbommel/sortorder v1.0.1 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // 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
|
||||
@@ -109,7 +109,7 @@ require (
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
@@ -121,7 +121,6 @@ require (
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.12 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
@@ -138,9 +137,9 @@ require (
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.17.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/client_golang v1.20.2 // 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/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
@@ -155,14 +154,14 @@ require (
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
||||
golang.org/x/net v0.29.0 // indirect
|
||||
@@ -170,8 +169,8 @@ require (
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
golang.org/x/tools v0.25.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/klog/v2 v2.110.1 // indirect
|
||||
|
130
go.sum
130
go.sum
@@ -14,8 +14,8 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Microsoft/hcsshim v0.12.5 h1:bpTInLlDy/nDRWFVcefDZZ1+U8tS+rz3MxjKgu9boo0=
|
||||
github.com/Microsoft/hcsshim v0.12.5/go.mod h1:tIUGego4G1EN5Hb6KC90aDYiUI2dqLSTTOCjVNpOgZ8=
|
||||
github.com/Microsoft/hcsshim v0.12.8 h1:BtDWYlFMcWhorrvSSo2M7z0csPdw6t7no/C3FsSvqiI=
|
||||
github.com/Microsoft/hcsshim v0.12.8/go.mod h1:cibQ4BqhJ32FXDwPdQhKhwrwophnh3FuT4nwQZF907w=
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||
@@ -73,8 +73,8 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembj
|
||||
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
|
||||
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
|
||||
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
|
||||
@@ -83,21 +83,21 @@ github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnTh
|
||||
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
|
||||
github.com/compose-spec/compose-go/v2 v2.2.0 h1:VsQosGhuO+H9wh5laiIiAe4TVd73kQ5NWwmNrdm0HRA=
|
||||
github.com/compose-spec/compose-go/v2 v2.2.0/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
|
||||
github.com/compose-spec/compose-go/v2 v2.4.4 h1:cvHBl5Jf1iNBmRrZCICmHvaoskYc1etTPEMLKVwokAY=
|
||||
github.com/compose-spec/compose-go/v2 v2.4.4/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
|
||||
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
|
||||
github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0=
|
||||
github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE=
|
||||
github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0=
|
||||
github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0=
|
||||
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
|
||||
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/containerd/containerd v1.7.22 h1:nZuNnNRA6T6jB975rx2RRNqqH2k6ELYKDZfqTHqwyy0=
|
||||
github.com/containerd/containerd v1.7.22/go.mod h1:e3Jz1rYRUZ2Lt51YrH9Rz0zPyJBOlSvB3ghr2jbVD8g=
|
||||
github.com/containerd/containerd v1.7.24 h1:zxszGrGjrra1yYJW/6rhm9cJ1ZQ8rkKBR48brqsa7nA=
|
||||
github.com/containerd/containerd v1.7.24/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw=
|
||||
github.com/containerd/containerd/api v1.7.19 h1:VWbJL+8Ap4Ju2mx9c9qS1uFSB1OVYr5JJrW2yT5vFoA=
|
||||
github.com/containerd/containerd/api v1.7.19/go.mod h1:fwGavl3LNwAV5ilJ0sbrABL44AQxmNjDRcwheXDb6Ig=
|
||||
github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII=
|
||||
github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
|
||||
github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM=
|
||||
github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0=
|
||||
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
|
||||
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
|
||||
github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4=
|
||||
github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY=
|
||||
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
@@ -111,8 +111,8 @@ github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
|
||||
github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU=
|
||||
github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
|
||||
github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso=
|
||||
github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g=
|
||||
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
|
||||
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
@@ -125,15 +125,15 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ=
|
||||
github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v27.4.0-rc.2+incompatible h1:A0GZwegDlt2wdt3tpmrUzkVOZmbhvd7i05wPSf7Oo74=
|
||||
github.com/docker/cli v27.4.0-rc.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli-docs-tool v0.8.0 h1:YcDWl7rQJC3lJ7WVZRwSs3bc9nka97QLWfyJQli8yJU=
|
||||
github.com/docker/cli-docs-tool v0.8.0/go.mod h1:8TQQ3E7mOXoYUs811LiPdUnAhXrcVsBIrW21a5pUbdk=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
|
||||
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v27.4.0-rc.2+incompatible h1:9OJjVGtelk/zGC3TyKweJ29b9Axzh0s/0vtU4mneumE=
|
||||
github.com/docker/docker v27.4.0-rc.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
|
||||
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
|
||||
@@ -166,8 +166,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
@@ -192,8 +192,6 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
|
||||
github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -228,8 +226,8 @@ github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWS
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -279,6 +277,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/magiconair/properties v1.5.3 h1:C8fxWnhYyME3n0klPOhVM7PtYUB3eV1W3DeFmN3j53Y=
|
||||
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
@@ -290,8 +290,6 @@ github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebG
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
|
||||
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
@@ -303,8 +301,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/z
|
||||
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/buildkit v0.17.0-rc2 h1:+L7qAYuBEA9D7IBTEYdzFf4SsC7JBEOcAG8oRVrzveA=
|
||||
github.com/moby/buildkit v0.17.0-rc2/go.mod h1:ru8NFyDHD8HbuKaLXJIjK9nr3x6FZR+IWjtF07S+wdM=
|
||||
github.com/moby/buildkit v0.18.0-rc2 h1:Q4jxxicZM5sqdy6gJPikgILBrBUncPok0Z0rRbhtVWk=
|
||||
github.com/moby/buildkit v0.18.0-rc2/go.mod h1:vCR5CX8NGsPTthTg681+9kdmfvkvqJBXEv71GZe5msU=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
|
||||
@@ -375,18 +373,18 @@ github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg=
|
||||
github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
@@ -395,8 +393,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE=
|
||||
@@ -443,8 +441,8 @@ github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4D
|
||||
github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw=
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205 h1:eUk79E1w8yMtXeHSzjKorxuC8qJOnyXQnLaJehxpJaI=
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
|
||||
github.com/tonistiigi/fsutil v0.0.0-20241028165955-397af5306b5c h1:bQLsfX+uEPZUjyR2qmoJs5F8Z57bPVmF3IsUf22Xo9Y=
|
||||
github.com/tonistiigi/fsutil v0.0.0-20241028165955-397af5306b5c/go.mod h1:Dl/9oEjK7IqnjAm21Okx/XIxUCFJzvh+XdVHUlBwXTw=
|
||||
github.com/tonistiigi/fsutil v0.0.0-20241121093142-31cf1f437184 h1:RgyoSI38Y36zjQaszel/0RAcIehAnjA1B0RiUV9SDO4=
|
||||
github.com/tonistiigi/fsutil v0.0.0-20241121093142-31cf1f437184/go.mod h1:Dl/9oEjK7IqnjAm21Okx/XIxUCFJzvh+XdVHUlBwXTw=
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8=
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE=
|
||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
|
||||
@@ -474,30 +472,30 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
|
||||
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
|
||||
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0 h1:bflGWrfYyuulcdxf14V6n9+CoQcu5SAAdHmDPAJnlps=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0/go.mod h1:qcTO4xHAxZLaLxPd60TdE88rxtItPHgHWqOhOGRr0as=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I=
|
||||
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
|
||||
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
|
||||
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
|
||||
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q=
|
||||
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
|
||||
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
|
||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg=
|
||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -572,13 +570,13 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ=
|
||||
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 h1:+rdxYoE3E5htTEWIe15GlN6IfvbURM//Jt0mmkmm6ZU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo=
|
||||
google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
|
||||
google.golang.org/grpc v1.66.3 h1:TWlsh8Mv0QI/1sIbs1W36lqRclxrmF+eFJ4DbI0fuhA=
|
||||
google.golang.org/grpc v1.66.3/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.22
|
||||
ARG GO_VERSION=1.23
|
||||
ARG FORMATS=md,yaml
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine AS docsgen
|
||||
|
@@ -5,8 +5,8 @@
|
||||
# Copyright The Buildx Authors.
|
||||
# Licensed under the Apache License, Version 2.0
|
||||
|
||||
ARG GO_VERSION="1.22"
|
||||
ARG PROTOC_VERSION="3.11.4"
|
||||
ARG GO_VERSION=1.23
|
||||
ARG PROTOC_VERSION=3.11.4
|
||||
ARG PROTOC_GOOGLEAPIS_VERSION=2af421884dd468d565137215c946ebe4e245ae26
|
||||
|
||||
# protoc is dynamically linked to glibc so can't use alpine base
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION="1.22"
|
||||
ARG GOVULNCHECK_VERSION="v1.1.3"
|
||||
ARG GO_VERSION=1.23
|
||||
ARG GOVULNCHECK_VERSION=v1.1.3
|
||||
ARG FORMAT="text"
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine AS base
|
||||
|
@@ -1,12 +1,11 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.22
|
||||
ARG XX_VERSION=1.3.0
|
||||
ARG GOLANGCI_LINT_VERSION=1.57.2
|
||||
ARG GOPLS_VERSION=v0.20.0
|
||||
ARG GO_VERSION=1.23
|
||||
ARG XX_VERSION=1.5.0
|
||||
ARG GOLANGCI_LINT_VERSION=1.62.0
|
||||
ARG GOPLS_VERSION=v0.26.0
|
||||
# disabled: deprecated unusedvariable simplifyrange
|
||||
ARG GOPLS_ANALYZERS="embeddirective fillreturns infertypeargs nonewvars noresultvalues simplifycompositelit simplifyslice stubmethods undeclaredname unusedparams useany"
|
||||
|
||||
ARG GOPLS_ANALYZERS="embeddirective fillreturns infertypeargs nonewvars norangeoverfunc noresultvalues simplifycompositelit simplifyslice undeclaredname unusedparams useany"
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.22
|
||||
ARG GO_VERSION=1.23
|
||||
ARG MODOUTDATED_VERSION=v0.9.0
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine AS base
|
||||
|
@@ -326,7 +326,7 @@ func (m *monitor) invoke(ctx context.Context, pid string, cfg *controllerapi.Inv
|
||||
if m.AttachedSessionID() == "" {
|
||||
return nil
|
||||
}
|
||||
invokeCtx, invokeCancel := context.WithCancel(ctx)
|
||||
invokeCtx, invokeCancel := context.WithCancelCause(ctx)
|
||||
|
||||
containerIn, containerOut := ioset.Pipe()
|
||||
m.invokeIO.SetOut(&containerOut)
|
||||
@@ -336,7 +336,7 @@ func (m *monitor) invoke(ctx context.Context, pid string, cfg *controllerapi.Inv
|
||||
cancelOnce.Do(func() {
|
||||
containerIn.Close()
|
||||
m.invokeIO.SetOut(nil)
|
||||
invokeCancel()
|
||||
invokeCancel(errors.WithStack(context.Canceled))
|
||||
})
|
||||
<-waitInvokeDoneCh
|
||||
}
|
||||
|
@@ -17,13 +17,13 @@ func TestNodeGroupUpdate(t *testing.T) {
|
||||
err = ng.Update("foo1", "foo1", []string{"linux/arm64", "linux/arm/v7"}, true, true, nil, "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, len(ng.Nodes), 2)
|
||||
require.Equal(t, 2, len(ng.Nodes))
|
||||
|
||||
// update
|
||||
err = ng.Update("foo", "foo2", []string{"linux/amd64", "linux/arm"}, true, false, nil, "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, len(ng.Nodes), 2)
|
||||
require.Equal(t, 2, len(ng.Nodes))
|
||||
require.Equal(t, []string{"linux/amd64", "linux/arm/v7"}, platformutil.Format(ng.Nodes[0].Platforms))
|
||||
require.Equal(t, []string{"linux/arm64"}, platformutil.Format(ng.Nodes[1].Platforms))
|
||||
|
||||
@@ -39,6 +39,6 @@ func TestNodeGroupUpdate(t *testing.T) {
|
||||
err = ng.Leave("foo")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, len(ng.Nodes), 1)
|
||||
require.Equal(t, 1, len(ng.Nodes))
|
||||
require.Equal(t, []string{"linux/arm64"}, platformutil.Format(ng.Nodes[0].Platforms))
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -44,7 +45,7 @@ func TestNodeLocking(t *testing.T) {
|
||||
|
||||
go func() {
|
||||
_, release, err := s.Txn()
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
release()
|
||||
close(ready)
|
||||
}()
|
||||
|
@@ -63,48 +63,70 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){
|
||||
}
|
||||
|
||||
func testBakePrint(t *testing.T, sb integration.Sandbox) {
|
||||
dockerfile := []byte(`
|
||||
FROM busybox
|
||||
ARG HELLO
|
||||
RUN echo "Hello ${HELLO}"
|
||||
`)
|
||||
bakefile := []byte(`
|
||||
testCases := []struct {
|
||||
name string
|
||||
f string
|
||||
dt []byte
|
||||
}{
|
||||
{
|
||||
"HCL",
|
||||
"docker-bake.hcl",
|
||||
[]byte(`
|
||||
target "build" {
|
||||
args = {
|
||||
HELLO = "foo"
|
||||
}
|
||||
}
|
||||
`)
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
|
||||
fstest.CreateFile("Dockerfile", dockerfile, 0600),
|
||||
)
|
||||
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--print", "build"))
|
||||
stdout := bytes.Buffer{}
|
||||
stderr := bytes.Buffer{}
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
require.NoError(t, cmd.Run(), stdout.String(), stderr.String())
|
||||
|
||||
var def struct {
|
||||
Group map[string]*bake.Group `json:"group,omitempty"`
|
||||
Target map[string]*bake.Target `json:"target"`
|
||||
`)},
|
||||
{
|
||||
"Compose",
|
||||
"compose.yml",
|
||||
[]byte(`
|
||||
services:
|
||||
build:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
HELLO: foo
|
||||
`)},
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(stdout.Bytes(), &def))
|
||||
|
||||
require.Len(t, def.Group, 1)
|
||||
require.Contains(t, def.Group, "default")
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateFile(tc.f, tc.dt, 0600),
|
||||
fstest.CreateFile("Dockerfile", []byte(`
|
||||
FROM busybox
|
||||
ARG HELLO
|
||||
RUN echo "Hello ${HELLO}"
|
||||
`), 0600),
|
||||
)
|
||||
|
||||
require.Equal(t, []string{"build"}, def.Group["default"].Targets)
|
||||
require.Len(t, def.Target, 1)
|
||||
require.Contains(t, def.Target, "build")
|
||||
require.Equal(t, ".", *def.Target["build"].Context)
|
||||
require.Equal(t, "Dockerfile", *def.Target["build"].Dockerfile)
|
||||
require.Equal(t, map[string]*string{"HELLO": ptrstr("foo")}, def.Target["build"].Args)
|
||||
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--print", "build"))
|
||||
stdout := bytes.Buffer{}
|
||||
stderr := bytes.Buffer{}
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
require.NoError(t, cmd.Run(), stdout.String(), stderr.String())
|
||||
|
||||
require.Equal(t, `{
|
||||
var def struct {
|
||||
Group map[string]*bake.Group `json:"group,omitempty"`
|
||||
Target map[string]*bake.Target `json:"target"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(stdout.Bytes(), &def))
|
||||
|
||||
require.Len(t, def.Group, 1)
|
||||
require.Contains(t, def.Group, "default")
|
||||
|
||||
require.Equal(t, []string{"build"}, def.Group["default"].Targets)
|
||||
require.Len(t, def.Target, 1)
|
||||
require.Contains(t, def.Target, "build")
|
||||
require.Equal(t, ".", *def.Target["build"].Context)
|
||||
require.Equal(t, "Dockerfile", *def.Target["build"].Dockerfile)
|
||||
require.Equal(t, map[string]*string{"HELLO": ptrstr("foo")}, def.Target["build"].Args)
|
||||
|
||||
require.JSONEq(t, `{
|
||||
"group": {
|
||||
"default": {
|
||||
"targets": [
|
||||
@@ -123,6 +145,8 @@ target "build" {
|
||||
}
|
||||
}
|
||||
`, stdout.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testBakeLocal(t *testing.T, sb integration.Sandbox) {
|
||||
@@ -484,7 +508,8 @@ EOT
|
||||
withArgs(addr, "--set", "*.output=type=local,dest="+dirDest),
|
||||
)
|
||||
require.Error(t, err, out)
|
||||
require.Contains(t, out, "outside of the working directory, please set BAKE_ALLOW_REMOTE_FS_ACCESS")
|
||||
require.Contains(t, out, "Your build is requesting privileges for following possibly insecure capabilities")
|
||||
require.Contains(t, out, "Read access to path ../")
|
||||
|
||||
out, err = bakeCmd(
|
||||
sb,
|
||||
@@ -531,7 +556,8 @@ EOT
|
||||
withArgs(addr, "--set", "*.output=type=local,dest="+dirDest),
|
||||
)
|
||||
require.Error(t, err, out)
|
||||
require.Contains(t, out, "outside of the working directory, please set BAKE_ALLOW_REMOTE_FS_ACCESS")
|
||||
require.Contains(t, out, "Your build is requesting privileges for following possibly insecure capabilities")
|
||||
require.Contains(t, out, "Read access to path ..")
|
||||
|
||||
out, err = bakeCmd(
|
||||
sb,
|
||||
@@ -954,7 +980,6 @@ func testBakeMultiPlatform(t *testing.T, sb integration.Sandbox) {
|
||||
require.NotNil(t, img)
|
||||
img = imgs.Find("linux/arm64")
|
||||
require.NotNil(t, img)
|
||||
|
||||
} else {
|
||||
require.Error(t, err, string(out))
|
||||
require.Contains(t, string(out), "Multi-platform build is not supported")
|
||||
@@ -1444,7 +1469,7 @@ target "third" {
|
||||
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
|
||||
)
|
||||
|
||||
dockerfilePathFirst := filepath.Join("Dockerfile")
|
||||
dockerfilePathFirst := "Dockerfile"
|
||||
dockerfilePathSecond := filepath.Join("subdir", "Dockerfile")
|
||||
dockerfilePathThird := filepath.Join("subdir", "subsubdir", "Dockerfile")
|
||||
|
||||
|
@@ -471,7 +471,7 @@ RUN echo foo > /bar`)
|
||||
cmd := buildxCmd(sb, withArgs("build", "--output=type=cacheonly", dir))
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(out))
|
||||
require.False(t, buildDetailsPattern.MatchString(string(out)), fmt.Sprintf("build details link not expected in output, got %q", out))
|
||||
require.False(t, buildDetailsPattern.MatchString(string(out)), "build details link not expected in output, got %q", out)
|
||||
|
||||
// create desktop-build .lastaccess file
|
||||
home, err := os.UserHomeDir() // TODO: sandbox should create a temp home dir and expose it through its interface
|
||||
@@ -491,7 +491,7 @@ RUN echo foo > /bar`)
|
||||
cmd = buildxCmd(sb, withArgs("build", "--output=type=cacheonly", dir))
|
||||
out, err = cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(out))
|
||||
require.True(t, buildDetailsPattern.MatchString(string(out)), fmt.Sprintf("expected build details link in output, got %q", out))
|
||||
require.True(t, buildDetailsPattern.MatchString(string(out)), "expected build details link in output, got %q", out)
|
||||
|
||||
// build erroneous dockerfile
|
||||
dockerfile = []byte(`FROM busybox:latest
|
||||
@@ -500,12 +500,12 @@ RUN exit 1`)
|
||||
cmd = buildxCmd(sb, withArgs("build", "--output=type=cacheonly", dir))
|
||||
out, err = cmd.CombinedOutput()
|
||||
require.Error(t, err, string(out))
|
||||
require.True(t, buildDetailsPattern.MatchString(string(out)), fmt.Sprintf("expected build details link in output, got %q", out))
|
||||
require.True(t, buildDetailsPattern.MatchString(string(out)), "expected build details link in output, got %q", out)
|
||||
}
|
||||
|
||||
func testBuildProgress(t *testing.T, sb integration.Sandbox) {
|
||||
dir := createTestProject(t)
|
||||
sbDriver, _ := driverName(sb.Name())
|
||||
sbDriver, _, _ := driverName(sb.Name())
|
||||
name := sb.Address()
|
||||
|
||||
// progress=tty
|
||||
@@ -578,7 +578,7 @@ func testBuildBuildArgNoKey(t *testing.T, sb integration.Sandbox) {
|
||||
cmd := buildxCmd(sb, withArgs("build", "--build-arg", "=TEST_STRING", dir))
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.Error(t, err, string(out))
|
||||
require.Equal(t, strings.TrimSpace(string(out)), `ERROR: invalid key-value pair "=TEST_STRING": empty key`)
|
||||
require.Equal(t, `ERROR: invalid key-value pair "=TEST_STRING": empty key`, strings.TrimSpace(string(out)))
|
||||
}
|
||||
|
||||
func testBuildLabelNoKey(t *testing.T, sb integration.Sandbox) {
|
||||
@@ -586,7 +586,7 @@ func testBuildLabelNoKey(t *testing.T, sb integration.Sandbox) {
|
||||
cmd := buildxCmd(sb, withArgs("build", "--label", "=TEST_STRING", dir))
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.Error(t, err, string(out))
|
||||
require.Equal(t, strings.TrimSpace(string(out)), `ERROR: invalid key-value pair "=TEST_STRING": empty key`)
|
||||
require.Equal(t, `ERROR: invalid key-value pair "=TEST_STRING": empty key`, strings.TrimSpace(string(out)))
|
||||
}
|
||||
|
||||
func testBuildCacheExportNotSupported(t *testing.T, sb integration.Sandbox) {
|
||||
@@ -649,7 +649,6 @@ func testBuildMultiPlatform(t *testing.T, sb integration.Sandbox) {
|
||||
require.NotNil(t, img)
|
||||
img = imgs.Find("linux/arm64")
|
||||
require.NotNil(t, img)
|
||||
|
||||
} else {
|
||||
require.Error(t, err, string(out))
|
||||
require.Contains(t, string(out), "Multi-platform build is not supported")
|
||||
|
@@ -102,7 +102,7 @@ func testCreateRemoteContainer(t *testing.T, sb integration.Sandbox) {
|
||||
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
if v, ok := strings.CutPrefix(line, "Status:"); ok {
|
||||
require.Equal(t, strings.TrimSpace(v), "running")
|
||||
require.Equal(t, "running", strings.TrimSpace(v))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/moby/buildkit/util/progress/progressui"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -57,7 +56,7 @@ func testDialStdio(t *testing.T, sb integration.Sandbox) {
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Error("timeout waiting for buildx command to exit")
|
||||
case <-chErr:
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}()
|
||||
|
||||
|
@@ -45,7 +45,7 @@ func testInspect(t *testing.T, sb integration.Sandbox) {
|
||||
}
|
||||
|
||||
require.Equal(t, sb.Address(), name)
|
||||
sbDriver, _ := driverName(sb.Name())
|
||||
sbDriver, _, _ := driverName(sb.Name())
|
||||
require.Equal(t, sbDriver, driver)
|
||||
if isDockerWorker(sb) {
|
||||
require.NotEmpty(t, hostGatewayIP, "host-gateway-ip worker label should be set with docker driver")
|
||||
|
@@ -95,33 +95,37 @@ func buildxConfig(sb integration.Sandbox) string {
|
||||
}
|
||||
|
||||
func isMobyWorker(sb integration.Sandbox) bool {
|
||||
name, hasFeature := driverName(sb.Name())
|
||||
name, _, hasFeature := driverName(sb.Name())
|
||||
return name == "docker" && !hasFeature
|
||||
}
|
||||
|
||||
func isMobyContainerdSnapWorker(sb integration.Sandbox) bool {
|
||||
name, hasFeature := driverName(sb.Name())
|
||||
name, _, hasFeature := driverName(sb.Name())
|
||||
return name == "docker" && hasFeature
|
||||
}
|
||||
|
||||
func isDockerWorker(sb integration.Sandbox) bool {
|
||||
name, _ := driverName(sb.Name())
|
||||
name, _, _ := driverName(sb.Name())
|
||||
return name == "docker"
|
||||
}
|
||||
|
||||
func isDockerContainerWorker(sb integration.Sandbox) bool {
|
||||
name, _ := driverName(sb.Name())
|
||||
name, _, _ := driverName(sb.Name())
|
||||
return name == "docker-container"
|
||||
}
|
||||
|
||||
func driverName(sbName string) (string, bool) {
|
||||
func driverName(sbName string) (string, bool, bool) {
|
||||
name := sbName
|
||||
var hasFeature bool
|
||||
var hasVersion, hasFeature bool
|
||||
if b, _, ok := strings.Cut(sbName, "@"); ok {
|
||||
name = b
|
||||
hasVersion = true
|
||||
}
|
||||
if b, _, ok := strings.Cut(name, "+"); ok {
|
||||
name = b
|
||||
hasFeature = true
|
||||
}
|
||||
return name, hasFeature
|
||||
return name, hasVersion, hasFeature
|
||||
}
|
||||
|
||||
func isExperimental() bool {
|
||||
|
@@ -34,7 +34,7 @@ func testLs(t *testing.T, sb integration.Sandbox) {
|
||||
},
|
||||
}
|
||||
|
||||
sbDriver, _ := driverName(sb.Name())
|
||||
sbDriver, _, _ := driverName(sb.Name())
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@@ -2,10 +2,14 @@ package workers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/moby/buildkit/identity"
|
||||
"github.com/moby/buildkit/util/testutil/dockerd"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
bkworkers "github.com/moby/buildkit/util/testutil/workers"
|
||||
"github.com/pkg/errors"
|
||||
@@ -13,18 +17,61 @@ import (
|
||||
|
||||
func InitDockerWorker() {
|
||||
integration.Register(&dockerWorker{
|
||||
id: "docker",
|
||||
id: "docker",
|
||||
binary: dockerd.DefaultDockerdBinary,
|
||||
})
|
||||
integration.Register(&dockerWorker{
|
||||
id: "docker+containerd",
|
||||
binary: dockerd.DefaultDockerdBinary,
|
||||
containerdSnapshotter: true,
|
||||
})
|
||||
// e.g. `docker@26.0=/opt/docker-26.0,docker@25.0=/opt/docker-25.0`
|
||||
if s := os.Getenv("TEST_DOCKER_EXTRA"); s != "" {
|
||||
entries := strings.Split(s, ",")
|
||||
for _, entry := range entries {
|
||||
ver, bin, err := func(entry string) (string, string, error) {
|
||||
p1 := strings.Split(strings.TrimSpace(entry), "=")
|
||||
if len(p1) != 2 {
|
||||
return "", "", errors.Errorf("invalid entry: %q", entry)
|
||||
}
|
||||
name, bin := p1[0], p1[1]
|
||||
if _, err := os.Stat(bin); err != nil {
|
||||
return "", "", errors.Wrapf(err, "bin not found: %q", bin)
|
||||
}
|
||||
p2 := strings.Split(strings.TrimSpace(name), "@")
|
||||
if len(p2) != 2 {
|
||||
return "", "", errors.Errorf("invalid name: %q", name)
|
||||
}
|
||||
_, ver := p2[0], p2[1]
|
||||
if ver == "" {
|
||||
return "", "", errors.New("empty version")
|
||||
}
|
||||
return ver, bin, nil
|
||||
}(entry)
|
||||
if err != nil {
|
||||
panic(errors.Wrapf(err, "unexpected TEST_DOCKER_EXTRA: %q", s))
|
||||
}
|
||||
integration.Register(&dockerWorker{
|
||||
id: fmt.Sprintf("docker@%s", ver),
|
||||
binary: filepath.Join(bin, "dockerd"),
|
||||
extraEnv: []string{fmt.Sprintf("PATH=%s:%s", bin, os.Getenv("PATH"))},
|
||||
})
|
||||
integration.Register(&dockerWorker{
|
||||
id: fmt.Sprintf("docker+containerd@%s", ver),
|
||||
binary: filepath.Join(bin, "dockerd"),
|
||||
containerdSnapshotter: true,
|
||||
extraEnv: []string{fmt.Sprintf("PATH=%s:%s", bin, os.Getenv("PATH"))},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type dockerWorker struct {
|
||||
id string
|
||||
binary string
|
||||
containerdSnapshotter bool
|
||||
unsupported []string
|
||||
extraEnv []string
|
||||
}
|
||||
|
||||
func (c dockerWorker) Name() string {
|
||||
@@ -42,7 +89,9 @@ func (c *dockerWorker) NetNSDetached() bool {
|
||||
func (c dockerWorker) New(ctx context.Context, cfg *integration.BackendConfig) (b integration.Backend, cl func() error, err error) {
|
||||
moby := bkworkers.Moby{
|
||||
ID: c.id,
|
||||
Binary: c.binary,
|
||||
ContainerdSnapshotter: c.containerdSnapshotter,
|
||||
ExtraEnv: c.extraEnv,
|
||||
}
|
||||
bk, bkclose, err := moby.New(ctx, cfg)
|
||||
if err != nil {
|
||||
|
@@ -2,6 +2,8 @@ package buildflags
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"maps"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -9,66 +11,154 @@ import (
|
||||
controllerapi "github.com/docker/buildx/controller/pb"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tonistiigi/go-csvvalue"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
jsoncty "github.com/zclconf/go-cty/cty/json"
|
||||
)
|
||||
|
||||
type CacheOptionsEntry struct {
|
||||
Type string `json:"type"`
|
||||
Attrs map[string]string `json:"attrs,omitempty"`
|
||||
}
|
||||
|
||||
func (e *CacheOptionsEntry) Equal(other *CacheOptionsEntry) bool {
|
||||
if e.Type != other.Type {
|
||||
return false
|
||||
}
|
||||
return maps.Equal(e.Attrs, other.Attrs)
|
||||
}
|
||||
|
||||
func (e *CacheOptionsEntry) String() string {
|
||||
// Special registry syntax.
|
||||
if e.Type == "registry" && len(e.Attrs) == 1 {
|
||||
if ref, ok := e.Attrs["ref"]; ok {
|
||||
return ref
|
||||
}
|
||||
}
|
||||
|
||||
var b csvBuilder
|
||||
if e.Type != "" {
|
||||
b.Write("type", e.Type)
|
||||
}
|
||||
if len(e.Attrs) > 0 {
|
||||
b.WriteAttributes(e.Attrs)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (e *CacheOptionsEntry) ToPB() *controllerapi.CacheOptionsEntry {
|
||||
return &controllerapi.CacheOptionsEntry{
|
||||
Type: e.Type,
|
||||
Attrs: maps.Clone(e.Attrs),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *CacheOptionsEntry) MarshalJSON() ([]byte, error) {
|
||||
m := maps.Clone(e.Attrs)
|
||||
if m == nil {
|
||||
m = map[string]string{}
|
||||
}
|
||||
m["type"] = e.Type
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
func (e *CacheOptionsEntry) UnmarshalJSON(data []byte) error {
|
||||
var m map[string]string
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.Type = m["type"]
|
||||
delete(m, "type")
|
||||
|
||||
e.Attrs = m
|
||||
return e.validate(data)
|
||||
}
|
||||
|
||||
func (e *CacheOptionsEntry) IsActive() bool {
|
||||
// Always active if not gha.
|
||||
if e.Type != "gha" {
|
||||
return true
|
||||
}
|
||||
return e.Attrs["token"] != "" && e.Attrs["url"] != ""
|
||||
}
|
||||
|
||||
func (e *CacheOptionsEntry) UnmarshalText(text []byte) error {
|
||||
in := string(text)
|
||||
fields, err := csvvalue.Fields(in, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(fields) == 1 && !strings.Contains(fields[0], "=") {
|
||||
e.Type = "registry"
|
||||
e.Attrs = map[string]string{"ref": fields[0]}
|
||||
return nil
|
||||
}
|
||||
|
||||
e.Type = ""
|
||||
e.Attrs = map[string]string{}
|
||||
|
||||
for _, field := range fields {
|
||||
parts := strings.SplitN(field, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return errors.Errorf("invalid value %s", field)
|
||||
}
|
||||
key := strings.ToLower(parts[0])
|
||||
value := parts[1]
|
||||
switch key {
|
||||
case "type":
|
||||
e.Type = value
|
||||
default:
|
||||
e.Attrs[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if e.Type == "" {
|
||||
return errors.Errorf("type required form> %q", in)
|
||||
}
|
||||
addGithubToken(e)
|
||||
addAwsCredentials(e)
|
||||
return e.validate(text)
|
||||
}
|
||||
|
||||
func (e *CacheOptionsEntry) validate(gv interface{}) error {
|
||||
if e.Type == "" {
|
||||
var text []byte
|
||||
switch gv := gv.(type) {
|
||||
case []byte:
|
||||
text = gv
|
||||
case string:
|
||||
text = []byte(gv)
|
||||
case cty.Value:
|
||||
text, _ = jsoncty.Marshal(gv, gv.Type())
|
||||
default:
|
||||
text, _ = json.Marshal(gv)
|
||||
}
|
||||
return errors.Errorf("type required form> %q", string(text))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseCacheEntry(in []string) ([]*controllerapi.CacheOptionsEntry, error) {
|
||||
outs := make([]*controllerapi.CacheOptionsEntry, 0, len(in))
|
||||
for _, in := range in {
|
||||
fields, err := csvvalue.Fields(in, nil)
|
||||
if err != nil {
|
||||
var out CacheOptionsEntry
|
||||
if err := out.UnmarshalText([]byte(in)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isRefOnlyFormat(fields) {
|
||||
for _, field := range fields {
|
||||
outs = append(outs, &controllerapi.CacheOptionsEntry{
|
||||
Type: "registry",
|
||||
Attrs: map[string]string{"ref": field},
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
out := controllerapi.CacheOptionsEntry{
|
||||
Attrs: map[string]string{},
|
||||
}
|
||||
for _, field := range fields {
|
||||
parts := strings.SplitN(field, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, errors.Errorf("invalid value %s", field)
|
||||
}
|
||||
key := strings.ToLower(parts[0])
|
||||
value := parts[1]
|
||||
switch key {
|
||||
case "type":
|
||||
out.Type = value
|
||||
default:
|
||||
out.Attrs[key] = value
|
||||
}
|
||||
}
|
||||
if out.Type == "" {
|
||||
return nil, errors.Errorf("type required form> %q", in)
|
||||
}
|
||||
if !addGithubToken(&out) {
|
||||
if !out.IsActive() {
|
||||
// Skip inactive cache entries.
|
||||
continue
|
||||
}
|
||||
addAwsCredentials(&out)
|
||||
outs = append(outs, &out)
|
||||
outs = append(outs, out.ToPB())
|
||||
}
|
||||
return outs, nil
|
||||
}
|
||||
|
||||
func isRefOnlyFormat(in []string) bool {
|
||||
for _, v := range in {
|
||||
if strings.Contains(v, "=") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func addGithubToken(ci *controllerapi.CacheOptionsEntry) bool {
|
||||
func addGithubToken(ci *CacheOptionsEntry) {
|
||||
if ci.Type != "gha" {
|
||||
return true
|
||||
return
|
||||
}
|
||||
if _, ok := ci.Attrs["token"]; !ok {
|
||||
if v, ok := os.LookupEnv("ACTIONS_RUNTIME_TOKEN"); ok {
|
||||
@@ -80,10 +170,9 @@ func addGithubToken(ci *controllerapi.CacheOptionsEntry) bool {
|
||||
ci.Attrs["url"] = v
|
||||
}
|
||||
}
|
||||
return ci.Attrs["token"] != "" && ci.Attrs["url"] != ""
|
||||
}
|
||||
|
||||
func addAwsCredentials(ci *controllerapi.CacheOptionsEntry) {
|
||||
func addAwsCredentials(ci *CacheOptionsEntry) {
|
||||
if ci.Type != "s3" {
|
||||
return
|
||||
}
|
||||
|
183
util/buildflags/cty.go
Normal file
183
util/buildflags/cty.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package buildflags
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"sync"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
"github.com/zclconf/go-cty/cty/gocty"
|
||||
)
|
||||
|
||||
func (e *CacheOptionsEntry) FromCtyValue(in cty.Value, p cty.Path) error {
|
||||
conv, err := convert.Convert(in, cty.Map(cty.String))
|
||||
if err == nil {
|
||||
m := conv.AsValueMap()
|
||||
if err := getAndDelete(m, "type", &e.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
e.Attrs = asMap(m)
|
||||
return e.validate(in)
|
||||
}
|
||||
return unmarshalTextFallback(in, e, err)
|
||||
}
|
||||
|
||||
func (e *CacheOptionsEntry) ToCtyValue() cty.Value {
|
||||
if e == nil {
|
||||
return cty.NullVal(cty.Map(cty.String))
|
||||
}
|
||||
|
||||
vals := make(map[string]cty.Value, len(e.Attrs)+1)
|
||||
for k, v := range e.Attrs {
|
||||
vals[k] = cty.StringVal(v)
|
||||
}
|
||||
vals["type"] = cty.StringVal(e.Type)
|
||||
return cty.MapVal(vals)
|
||||
}
|
||||
|
||||
func (e *ExportEntry) FromCtyValue(in cty.Value, p cty.Path) error {
|
||||
conv, err := convert.Convert(in, cty.Map(cty.String))
|
||||
if err == nil {
|
||||
m := conv.AsValueMap()
|
||||
if err := getAndDelete(m, "type", &e.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := getAndDelete(m, "dest", &e.Destination); err != nil {
|
||||
return err
|
||||
}
|
||||
e.Attrs = asMap(m)
|
||||
return e.validate()
|
||||
}
|
||||
return unmarshalTextFallback(in, e, err)
|
||||
}
|
||||
|
||||
func (e *ExportEntry) ToCtyValue() cty.Value {
|
||||
if e == nil {
|
||||
return cty.NullVal(cty.Map(cty.String))
|
||||
}
|
||||
|
||||
vals := make(map[string]cty.Value, len(e.Attrs)+2)
|
||||
for k, v := range e.Attrs {
|
||||
vals[k] = cty.StringVal(v)
|
||||
}
|
||||
vals["type"] = cty.StringVal(e.Type)
|
||||
vals["dest"] = cty.StringVal(e.Destination)
|
||||
return cty.MapVal(vals)
|
||||
}
|
||||
|
||||
var secretType = sync.OnceValue(func() cty.Type {
|
||||
return cty.ObjectWithOptionalAttrs(
|
||||
map[string]cty.Type{
|
||||
"id": cty.String,
|
||||
"src": cty.String,
|
||||
"env": cty.String,
|
||||
},
|
||||
[]string{"id", "src", "env"},
|
||||
)
|
||||
})
|
||||
|
||||
func (e *Secret) FromCtyValue(in cty.Value, p cty.Path) (err error) {
|
||||
conv, err := convert.Convert(in, secretType())
|
||||
if err == nil {
|
||||
if id := conv.GetAttr("id"); !id.IsNull() {
|
||||
e.ID = id.AsString()
|
||||
}
|
||||
if src := conv.GetAttr("src"); !src.IsNull() {
|
||||
e.FilePath = src.AsString()
|
||||
}
|
||||
if env := conv.GetAttr("env"); !env.IsNull() {
|
||||
e.Env = env.AsString()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return unmarshalTextFallback(in, e, err)
|
||||
}
|
||||
|
||||
func (e *Secret) ToCtyValue() cty.Value {
|
||||
if e == nil {
|
||||
return cty.NullVal(secretType())
|
||||
}
|
||||
|
||||
return cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal(e.ID),
|
||||
"src": cty.StringVal(e.FilePath),
|
||||
"env": cty.StringVal(e.Env),
|
||||
})
|
||||
}
|
||||
|
||||
var sshType = sync.OnceValue(func() cty.Type {
|
||||
return cty.ObjectWithOptionalAttrs(
|
||||
map[string]cty.Type{
|
||||
"id": cty.String,
|
||||
"paths": cty.List(cty.String),
|
||||
},
|
||||
[]string{"id", "paths"},
|
||||
)
|
||||
})
|
||||
|
||||
func (e *SSH) FromCtyValue(in cty.Value, p cty.Path) (err error) {
|
||||
conv, err := convert.Convert(in, sshType())
|
||||
if err == nil {
|
||||
if id := conv.GetAttr("id"); !id.IsNull() {
|
||||
e.ID = id.AsString()
|
||||
}
|
||||
if paths := conv.GetAttr("paths"); !paths.IsNull() {
|
||||
if err := gocty.FromCtyValue(paths, &e.Paths); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return unmarshalTextFallback(in, e, err)
|
||||
}
|
||||
|
||||
func (e *SSH) ToCtyValue() cty.Value {
|
||||
if e == nil {
|
||||
return cty.NullVal(sshType())
|
||||
}
|
||||
|
||||
var ctyPaths cty.Value
|
||||
if len(e.Paths) > 0 {
|
||||
paths := make([]cty.Value, len(e.Paths))
|
||||
for i, path := range e.Paths {
|
||||
paths[i] = cty.StringVal(path)
|
||||
}
|
||||
ctyPaths = cty.ListVal(paths)
|
||||
} else {
|
||||
ctyPaths = cty.ListValEmpty(cty.String)
|
||||
}
|
||||
|
||||
return cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal(e.ID),
|
||||
"paths": ctyPaths,
|
||||
})
|
||||
}
|
||||
|
||||
func getAndDelete(m map[string]cty.Value, attr string, gv interface{}) error {
|
||||
if v, ok := m[attr]; ok {
|
||||
delete(m, attr)
|
||||
return gocty.FromCtyValue(v, gv)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func asMap(m map[string]cty.Value) map[string]string {
|
||||
out := make(map[string]string, len(m))
|
||||
for k, v := range m {
|
||||
out[k] = v.AsString()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func unmarshalTextFallback[V encoding.TextUnmarshaler](in cty.Value, v V, inErr error) (outErr error) {
|
||||
// Attempt to convert this type to a string.
|
||||
conv, err := convert.Convert(in, cty.String)
|
||||
if err != nil {
|
||||
// Cannot convert. Do not attempt to convert and return the original error.
|
||||
return inErr
|
||||
}
|
||||
|
||||
// Conversion was successful. Use UnmarshalText on the string and return any
|
||||
// errors associated with that.
|
||||
return v.UnmarshalText([]byte(conv.AsString()))
|
||||
}
|
@@ -1,7 +1,10 @@
|
||||
package buildflags
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"maps"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/platforms"
|
||||
@@ -13,67 +16,131 @@ import (
|
||||
"github.com/tonistiigi/go-csvvalue"
|
||||
)
|
||||
|
||||
type ExportEntry struct {
|
||||
Type string `json:"type"`
|
||||
Attrs map[string]string `json:"attrs,omitempty"`
|
||||
Destination string `json:"dest,omitempty"`
|
||||
}
|
||||
|
||||
func (e *ExportEntry) Equal(other *ExportEntry) bool {
|
||||
if e.Type != other.Type || e.Destination != other.Destination {
|
||||
return false
|
||||
}
|
||||
return maps.Equal(e.Attrs, other.Attrs)
|
||||
}
|
||||
|
||||
func (e *ExportEntry) String() string {
|
||||
var b csvBuilder
|
||||
if e.Type != "" {
|
||||
b.Write("type", e.Type)
|
||||
}
|
||||
if e.Destination != "" {
|
||||
b.Write("dest", e.Destination)
|
||||
}
|
||||
if len(e.Attrs) > 0 {
|
||||
b.WriteAttributes(e.Attrs)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (e *ExportEntry) ToPB() *controllerapi.ExportEntry {
|
||||
return &controllerapi.ExportEntry{
|
||||
Type: e.Type,
|
||||
Attrs: maps.Clone(e.Attrs),
|
||||
Destination: e.Destination,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ExportEntry) MarshalJSON() ([]byte, error) {
|
||||
m := maps.Clone(e.Attrs)
|
||||
if m == nil {
|
||||
m = map[string]string{}
|
||||
}
|
||||
m["type"] = e.Type
|
||||
if e.Destination != "" {
|
||||
m["dest"] = e.Destination
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
func (e *ExportEntry) UnmarshalJSON(data []byte) error {
|
||||
var m map[string]string
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.Type = m["type"]
|
||||
delete(m, "type")
|
||||
|
||||
e.Destination = m["dest"]
|
||||
delete(m, "dest")
|
||||
|
||||
e.Attrs = m
|
||||
return e.validate()
|
||||
}
|
||||
|
||||
func (e *ExportEntry) UnmarshalText(text []byte) error {
|
||||
s := string(text)
|
||||
fields, err := csvvalue.Fields(s, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear the target entry.
|
||||
e.Type = ""
|
||||
e.Attrs = map[string]string{}
|
||||
e.Destination = ""
|
||||
|
||||
if len(fields) == 1 && fields[0] == s && !strings.HasPrefix(s, "type=") {
|
||||
if s != "-" {
|
||||
e.Type = client.ExporterLocal
|
||||
e.Destination = s
|
||||
return nil
|
||||
}
|
||||
|
||||
e.Type = client.ExporterTar
|
||||
e.Destination = s
|
||||
}
|
||||
|
||||
if e.Type == "" {
|
||||
for _, field := range fields {
|
||||
parts := strings.SplitN(field, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return errors.Errorf("invalid value %s", field)
|
||||
}
|
||||
key := strings.TrimSpace(strings.ToLower(parts[0]))
|
||||
value := parts[1]
|
||||
switch key {
|
||||
case "type":
|
||||
e.Type = value
|
||||
case "dest":
|
||||
e.Destination = value
|
||||
default:
|
||||
e.Attrs[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return e.validate()
|
||||
}
|
||||
|
||||
func (e *ExportEntry) validate() error {
|
||||
if e.Type == "" {
|
||||
return errors.Errorf("type is required for output")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseExports(inp []string) ([]*controllerapi.ExportEntry, error) {
|
||||
var outs []*controllerapi.ExportEntry
|
||||
if len(inp) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
for _, s := range inp {
|
||||
fields, err := csvvalue.Fields(s, nil)
|
||||
if err != nil {
|
||||
var out ExportEntry
|
||||
if err := out.UnmarshalText([]byte(s)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := controllerapi.ExportEntry{
|
||||
Attrs: map[string]string{},
|
||||
}
|
||||
if len(fields) == 1 && fields[0] == s && !strings.HasPrefix(s, "type=") {
|
||||
if s != "-" {
|
||||
outs = append(outs, &controllerapi.ExportEntry{
|
||||
Type: client.ExporterLocal,
|
||||
Destination: s,
|
||||
})
|
||||
continue
|
||||
}
|
||||
out = controllerapi.ExportEntry{
|
||||
Type: client.ExporterTar,
|
||||
Destination: s,
|
||||
}
|
||||
}
|
||||
|
||||
if out.Type == "" {
|
||||
for _, field := range fields {
|
||||
parts := strings.SplitN(field, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, errors.Errorf("invalid value %s", field)
|
||||
}
|
||||
key := strings.TrimSpace(strings.ToLower(parts[0]))
|
||||
value := parts[1]
|
||||
switch key {
|
||||
case "type":
|
||||
out.Type = value
|
||||
default:
|
||||
out.Attrs[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
if out.Type == "" {
|
||||
return nil, errors.Errorf("type is required for output")
|
||||
}
|
||||
|
||||
if out.Type == "registry" {
|
||||
out.Type = client.ExporterImage
|
||||
if _, ok := out.Attrs["push"]; !ok {
|
||||
out.Attrs["push"] = "true"
|
||||
}
|
||||
}
|
||||
|
||||
if dest, ok := out.Attrs["dest"]; ok {
|
||||
out.Destination = dest
|
||||
delete(out.Attrs, "dest")
|
||||
}
|
||||
|
||||
outs = append(outs, &out)
|
||||
outs = append(outs, out.ToPB())
|
||||
}
|
||||
return outs, nil
|
||||
}
|
||||
@@ -138,7 +205,35 @@ func ParseAnnotations(inp []string) (map[exptypes.AnnotationKey]string, error) {
|
||||
}
|
||||
annotations[ak] = v
|
||||
}
|
||||
|
||||
}
|
||||
return annotations, nil
|
||||
}
|
||||
|
||||
type csvBuilder struct {
|
||||
sb strings.Builder
|
||||
}
|
||||
|
||||
func (w *csvBuilder) Write(key, value string) {
|
||||
if w.sb.Len() > 0 {
|
||||
w.sb.WriteByte(',')
|
||||
}
|
||||
w.sb.WriteString(key)
|
||||
w.sb.WriteByte('=')
|
||||
w.sb.WriteString(value)
|
||||
}
|
||||
|
||||
func (w *csvBuilder) WriteAttributes(attrs map[string]string) {
|
||||
keys := make([]string, 0, len(attrs))
|
||||
for key := range attrs {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
w.Write(key, attrs[key])
|
||||
}
|
||||
}
|
||||
|
||||
func (w *csvBuilder) String() string {
|
||||
return w.sb.String()
|
||||
}
|
||||
|
@@ -8,6 +8,80 @@ import (
|
||||
"github.com/tonistiigi/go-csvvalue"
|
||||
)
|
||||
|
||||
type Secret struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
FilePath string `json:"src,omitempty"`
|
||||
Env string `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Secret) Equal(other *Secret) bool {
|
||||
return s.ID == other.ID && s.FilePath == other.FilePath && s.Env == other.Env
|
||||
}
|
||||
|
||||
func (s *Secret) String() string {
|
||||
var b csvBuilder
|
||||
if s.ID != "" {
|
||||
b.Write("id", s.ID)
|
||||
}
|
||||
if s.FilePath != "" {
|
||||
b.Write("src", s.FilePath)
|
||||
}
|
||||
if s.Env != "" {
|
||||
b.Write("env", s.Env)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (s *Secret) ToPB() *controllerapi.Secret {
|
||||
return &controllerapi.Secret{
|
||||
ID: s.ID,
|
||||
FilePath: s.FilePath,
|
||||
Env: s.Env,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Secret) UnmarshalText(text []byte) error {
|
||||
value := string(text)
|
||||
fields, err := csvvalue.Fields(value, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse csv secret")
|
||||
}
|
||||
|
||||
*s = Secret{}
|
||||
|
||||
var typ string
|
||||
for _, field := range fields {
|
||||
parts := strings.SplitN(field, "=", 2)
|
||||
key := strings.ToLower(parts[0])
|
||||
|
||||
if len(parts) != 2 {
|
||||
return errors.Errorf("invalid field '%s' must be a key=value pair", field)
|
||||
}
|
||||
|
||||
value := parts[1]
|
||||
switch key {
|
||||
case "type":
|
||||
if value != "file" && value != "env" {
|
||||
return errors.Errorf("unsupported secret type %q", value)
|
||||
}
|
||||
typ = value
|
||||
case "id":
|
||||
s.ID = value
|
||||
case "source", "src":
|
||||
s.FilePath = value
|
||||
case "env":
|
||||
s.Env = value
|
||||
default:
|
||||
return errors.Errorf("unexpected key '%s' in '%s'", key, field)
|
||||
}
|
||||
}
|
||||
if typ == "env" && s.Env == "" {
|
||||
s.Env = s.FilePath
|
||||
s.FilePath = ""
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseSecretSpecs(sl []string) ([]*controllerapi.Secret, error) {
|
||||
fs := make([]*controllerapi.Secret, 0, len(sl))
|
||||
for _, v := range sl {
|
||||
@@ -21,42 +95,9 @@ func ParseSecretSpecs(sl []string) ([]*controllerapi.Secret, error) {
|
||||
}
|
||||
|
||||
func parseSecret(value string) (*controllerapi.Secret, error) {
|
||||
fields, err := csvvalue.Fields(value, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse csv secret")
|
||||
var s Secret
|
||||
if err := s.UnmarshalText([]byte(value)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fs := controllerapi.Secret{}
|
||||
|
||||
var typ string
|
||||
for _, field := range fields {
|
||||
parts := strings.SplitN(field, "=", 2)
|
||||
key := strings.ToLower(parts[0])
|
||||
|
||||
if len(parts) != 2 {
|
||||
return nil, errors.Errorf("invalid field '%s' must be a key=value pair", field)
|
||||
}
|
||||
|
||||
value := parts[1]
|
||||
switch key {
|
||||
case "type":
|
||||
if value != "file" && value != "env" {
|
||||
return nil, errors.Errorf("unsupported secret type %q", value)
|
||||
}
|
||||
typ = value
|
||||
case "id":
|
||||
fs.ID = value
|
||||
case "source", "src":
|
||||
fs.FilePath = value
|
||||
case "env":
|
||||
fs.Env = value
|
||||
default:
|
||||
return nil, errors.Errorf("unexpected key '%s' in '%s'", key, field)
|
||||
}
|
||||
}
|
||||
if typ == "env" && fs.Env == "" {
|
||||
fs.Env = fs.FilePath
|
||||
fs.FilePath = ""
|
||||
}
|
||||
return &fs, nil
|
||||
return s.ToPB(), nil
|
||||
}
|
||||
|
@@ -1,12 +1,60 @@
|
||||
package buildflags
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
controllerapi "github.com/docker/buildx/controller/pb"
|
||||
"github.com/moby/buildkit/util/gitutil"
|
||||
)
|
||||
|
||||
type SSH struct {
|
||||
ID string `json:"id,omitempty" cty:"id"`
|
||||
Paths []string `json:"paths,omitempty" cty:"paths"`
|
||||
}
|
||||
|
||||
func (s *SSH) Equal(other *SSH) bool {
|
||||
return s.Less(other) == 0
|
||||
}
|
||||
|
||||
func (s *SSH) Less(other *SSH) int {
|
||||
if s.ID != other.ID {
|
||||
return cmp.Compare(s.ID, other.ID)
|
||||
}
|
||||
return slices.Compare(s.Paths, other.Paths)
|
||||
}
|
||||
|
||||
func (s *SSH) String() string {
|
||||
if len(s.Paths) == 0 {
|
||||
return s.ID
|
||||
}
|
||||
|
||||
var b csvBuilder
|
||||
paths := strings.Join(s.Paths, ",")
|
||||
b.Write(s.ID, paths)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (s *SSH) ToPB() *controllerapi.SSH {
|
||||
return &controllerapi.SSH{
|
||||
ID: s.ID,
|
||||
Paths: s.Paths,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSH) UnmarshalText(text []byte) error {
|
||||
parts := strings.SplitN(string(text), "=", 2)
|
||||
|
||||
s.ID = parts[0]
|
||||
if len(parts) > 1 {
|
||||
s.Paths = strings.Split(parts[1], ",")
|
||||
} else {
|
||||
s.Paths = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseSSHSpecs(sl []string) ([]*controllerapi.SSH, error) {
|
||||
var outs []*controllerapi.SSH
|
||||
if len(sl) == 0 {
|
||||
@@ -14,14 +62,11 @@ func ParseSSHSpecs(sl []string) ([]*controllerapi.SSH, error) {
|
||||
}
|
||||
|
||||
for _, s := range sl {
|
||||
parts := strings.SplitN(s, "=", 2)
|
||||
out := controllerapi.SSH{
|
||||
ID: parts[0],
|
||||
var out SSH
|
||||
if err := out.UnmarshalText([]byte(s)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
out.Paths = strings.Split(parts[1], ",")
|
||||
}
|
||||
outs = append(outs, &out)
|
||||
outs = append(outs, out.ToPB())
|
||||
}
|
||||
return outs, nil
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIsSubPath(t *testing.T) {
|
||||
@@ -51,7 +52,7 @@ func TestIsSubPath(t *testing.T) {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ok, err := isSubPath(tt.basePath, tt.subPath)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, ok)
|
||||
})
|
||||
}
|
||||
|
@@ -41,7 +41,6 @@ func (c *Client) LoadImage(ctx context.Context, name string, status progress.Wri
|
||||
pr, pw := io.Pipe()
|
||||
done := make(chan struct{})
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
var w *waitingWriter
|
||||
w = &waitingWriter{
|
||||
PipeWriter: pw,
|
||||
@@ -67,8 +66,7 @@ func (c *Client) LoadImage(ctx context.Context, name string, status progress.Wri
|
||||
handleErr(err)
|
||||
}
|
||||
},
|
||||
done: done,
|
||||
cancel: cancel,
|
||||
done: done,
|
||||
}
|
||||
return w, func() {
|
||||
pr.Close()
|
||||
@@ -101,12 +99,11 @@ func (c *Client) features(ctx context.Context, name string) map[Feature]bool {
|
||||
|
||||
type waitingWriter struct {
|
||||
*io.PipeWriter
|
||||
f func()
|
||||
once sync.Once
|
||||
mu sync.Mutex
|
||||
err error
|
||||
done chan struct{}
|
||||
cancel func()
|
||||
f func()
|
||||
once sync.Once
|
||||
mu sync.Mutex
|
||||
err error
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (w *waitingWriter) Write(dt []byte) (int, error) {
|
||||
|
@@ -55,7 +55,7 @@ func fromReader(l progress.SubLogger, rc io.ReadCloser) error {
|
||||
Started: &now,
|
||||
}
|
||||
}
|
||||
timeDelta := time.Now().Sub(st.Timestamp)
|
||||
timeDelta := time.Since(st.Timestamp)
|
||||
if timeDelta < minTimeDelta {
|
||||
continue
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -25,7 +26,7 @@ func WithAccessToken(token string) GitServeOpt {
|
||||
func GitServeHTTP(c *Git, t testing.TB, opts ...GitServeOpt) (url string) {
|
||||
t.Helper()
|
||||
gitUpdateServerInfo(c, t)
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
ctx, cancel := context.WithCancelCause(context.TODO())
|
||||
|
||||
gs := &gitServe{}
|
||||
for _, opt := range opts {
|
||||
@@ -38,7 +39,7 @@ func GitServeHTTP(c *Git, t testing.TB, opts ...GitServeOpt) (url string) {
|
||||
name := "test.git"
|
||||
dir, err := c.GitDir()
|
||||
if err != nil {
|
||||
cancel()
|
||||
cancel(err)
|
||||
}
|
||||
|
||||
var addr string
|
||||
@@ -84,7 +85,7 @@ func GitServeHTTP(c *Git, t testing.TB, opts ...GitServeOpt) (url string) {
|
||||
<-ready
|
||||
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
cancel(errors.Errorf("cleanup"))
|
||||
<-done
|
||||
})
|
||||
return fmt.Sprintf("http://%s/%s", addr, name)
|
||||
|
@@ -47,7 +47,6 @@ func (f mockFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.Rea
|
||||
reader := io.NopCloser(strings.NewReader(desc.Annotations["test_content"]))
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (r mockResolver) Resolve(ctx context.Context, ref string) (name string, desc ocispec.Descriptor, err error) {
|
||||
|
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/opencontainers/go-digest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
@@ -19,7 +20,7 @@ func TestLoad(t *testing.T) {
|
||||
r := getImageNoAttestation()
|
||||
indexDigest := reflect.ValueOf(r.indexes).MapKeys()[0].String()
|
||||
result, err := loader.Load(ctx, fmt.Sprintf("test@%s", indexDigest))
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
if err == nil {
|
||||
assert.Equal(t, 1, len(result.indexes))
|
||||
assert.Equal(t, 2, len(result.images))
|
||||
@@ -32,7 +33,7 @@ func TestLoad(t *testing.T) {
|
||||
r = getImageWithAttestation(plainSpdx)
|
||||
indexDigest = reflect.ValueOf(r.indexes).MapKeys()[0].String()
|
||||
result, err = loader.Load(ctx, fmt.Sprintf("test@%s", indexDigest))
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
if err == nil {
|
||||
assert.Equal(t, 1, len(result.indexes))
|
||||
assert.Equal(t, 2, len(result.images))
|
||||
@@ -92,7 +93,7 @@ func TestSBOM(t *testing.T) {
|
||||
r.assets["linux/amd64"] = a
|
||||
actual, err := r.SBOM()
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(actual))
|
||||
})
|
||||
}
|
||||
@@ -140,7 +141,7 @@ func TestProvenance(t *testing.T) {
|
||||
r.assets["linux/amd64"] = a
|
||||
actual, err := r.Provenance()
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(actual))
|
||||
})
|
||||
}
|
||||
@@ -167,7 +168,7 @@ func Test_isInTotoDSSE(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.mime, func(t *testing.T) {
|
||||
assert.Equal(t, isInTotoDSSE(test.mime), test.expected)
|
||||
assert.Equal(t, test.expected, isInTotoDSSE(test.mime))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -175,19 +176,19 @@ func Test_isInTotoDSSE(t *testing.T) {
|
||||
func Test_decodeDSSE(t *testing.T) {
|
||||
// Returns input when mime isn't a DSSE type
|
||||
actual, err := decodeDSSE([]byte("foobar"), "application/vnd.in-toto+json")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("foobar"), actual)
|
||||
|
||||
// Returns the base64 decoded payload if is a DSSE
|
||||
payload := base64.StdEncoding.EncodeToString([]byte("hello world"))
|
||||
envelope := fmt.Sprintf("{\"payload\":\"%s\"}", payload)
|
||||
actual, err = decodeDSSE([]byte(envelope), "application/vnd.in-toto.spdx+dsse")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "hello world", string(actual))
|
||||
|
||||
_, err = decodeDSSE([]byte("not a json"), "application/vnd.in-toto.spdx+dsse")
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = decodeDSSE([]byte("{\"payload\": \"not base64\"}"), "application/vnd.in-toto.spdx+dsse")
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
@@ -61,7 +61,7 @@ func (m *Map) Get(ctx context.Context, keys ...string) (map[string]interface{},
|
||||
m.mu.Unlock()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
return nil, context.Cause(ctx)
|
||||
case <-ch:
|
||||
m.mu.Lock()
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ package waitmap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -34,12 +33,12 @@ func TestTimeout(t *testing.T) {
|
||||
|
||||
m.Set("foo", "bar")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeoutCause(context.TODO(), 100*time.Millisecond, nil)
|
||||
defer cancel()
|
||||
|
||||
_, err := m.Get(ctx, "bar")
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, context.DeadlineExceeded))
|
||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
}
|
||||
|
||||
func TestBlocking(t *testing.T) {
|
||||
|
4
vendor/github.com/cenkalti/backoff/v4/README.md
generated
vendored
4
vendor/github.com/cenkalti/backoff/v4/README.md
generated
vendored
@@ -1,4 +1,4 @@
|
||||
# Exponential Backoff [![GoDoc][godoc image]][godoc] [![Build Status][travis image]][travis] [![Coverage Status][coveralls image]][coveralls]
|
||||
# Exponential Backoff [![GoDoc][godoc image]][godoc] [![Coverage Status][coveralls image]][coveralls]
|
||||
|
||||
This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client].
|
||||
|
||||
@@ -21,8 +21,6 @@ Use https://pkg.go.dev/github.com/cenkalti/backoff/v4 to view the documentation.
|
||||
|
||||
[godoc]: https://pkg.go.dev/github.com/cenkalti/backoff/v4
|
||||
[godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png
|
||||
[travis]: https://travis-ci.org/cenkalti/backoff
|
||||
[travis image]: https://travis-ci.org/cenkalti/backoff.png?branch=master
|
||||
[coveralls]: https://coveralls.io/github/cenkalti/backoff?branch=master
|
||||
[coveralls image]: https://coveralls.io/repos/github/cenkalti/backoff/badge.svg?branch=master
|
||||
|
||||
|
57
vendor/github.com/cenkalti/backoff/v4/exponential.go
generated
vendored
57
vendor/github.com/cenkalti/backoff/v4/exponential.go
generated
vendored
@@ -71,6 +71,9 @@ type Clock interface {
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
// ExponentialBackOffOpts is a function type used to configure ExponentialBackOff options.
|
||||
type ExponentialBackOffOpts func(*ExponentialBackOff)
|
||||
|
||||
// Default values for ExponentialBackOff.
|
||||
const (
|
||||
DefaultInitialInterval = 500 * time.Millisecond
|
||||
@@ -81,7 +84,7 @@ const (
|
||||
)
|
||||
|
||||
// NewExponentialBackOff creates an instance of ExponentialBackOff using default values.
|
||||
func NewExponentialBackOff() *ExponentialBackOff {
|
||||
func NewExponentialBackOff(opts ...ExponentialBackOffOpts) *ExponentialBackOff {
|
||||
b := &ExponentialBackOff{
|
||||
InitialInterval: DefaultInitialInterval,
|
||||
RandomizationFactor: DefaultRandomizationFactor,
|
||||
@@ -91,10 +94,62 @@ func NewExponentialBackOff() *ExponentialBackOff {
|
||||
Stop: Stop,
|
||||
Clock: SystemClock,
|
||||
}
|
||||
for _, fn := range opts {
|
||||
fn(b)
|
||||
}
|
||||
b.Reset()
|
||||
return b
|
||||
}
|
||||
|
||||
// WithInitialInterval sets the initial interval between retries.
|
||||
func WithInitialInterval(duration time.Duration) ExponentialBackOffOpts {
|
||||
return func(ebo *ExponentialBackOff) {
|
||||
ebo.InitialInterval = duration
|
||||
}
|
||||
}
|
||||
|
||||
// WithRandomizationFactor sets the randomization factor to add jitter to intervals.
|
||||
func WithRandomizationFactor(randomizationFactor float64) ExponentialBackOffOpts {
|
||||
return func(ebo *ExponentialBackOff) {
|
||||
ebo.RandomizationFactor = randomizationFactor
|
||||
}
|
||||
}
|
||||
|
||||
// WithMultiplier sets the multiplier for increasing the interval after each retry.
|
||||
func WithMultiplier(multiplier float64) ExponentialBackOffOpts {
|
||||
return func(ebo *ExponentialBackOff) {
|
||||
ebo.Multiplier = multiplier
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxInterval sets the maximum interval between retries.
|
||||
func WithMaxInterval(duration time.Duration) ExponentialBackOffOpts {
|
||||
return func(ebo *ExponentialBackOff) {
|
||||
ebo.MaxInterval = duration
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxElapsedTime sets the maximum total time for retries.
|
||||
func WithMaxElapsedTime(duration time.Duration) ExponentialBackOffOpts {
|
||||
return func(ebo *ExponentialBackOff) {
|
||||
ebo.MaxElapsedTime = duration
|
||||
}
|
||||
}
|
||||
|
||||
// WithRetryStopDuration sets the duration after which retries should stop.
|
||||
func WithRetryStopDuration(duration time.Duration) ExponentialBackOffOpts {
|
||||
return func(ebo *ExponentialBackOff) {
|
||||
ebo.Stop = duration
|
||||
}
|
||||
}
|
||||
|
||||
// WithClockProvider sets the clock used to measure time.
|
||||
func WithClockProvider(clock Clock) ExponentialBackOffOpts {
|
||||
return func(ebo *ExponentialBackOff) {
|
||||
ebo.Clock = clock
|
||||
}
|
||||
}
|
||||
|
||||
type systemClock struct{}
|
||||
|
||||
func (t systemClock) Now() time.Time {
|
||||
|
64
vendor/github.com/compose-spec/compose-go/v2/cli/options.go
generated
vendored
64
vendor/github.com/compose-spec/compose-go/v2/cli/options.go
generated
vendored
@@ -403,22 +403,24 @@ func (o *ProjectOptions) GetWorkingDir() (string, error) {
|
||||
return os.Getwd()
|
||||
}
|
||||
|
||||
func (o *ProjectOptions) GetConfigFiles() ([]types.ConfigFile, error) {
|
||||
configPaths, err := o.getConfigPaths()
|
||||
// ReadConfigFiles reads ConfigFiles and populates the content field
|
||||
func (o *ProjectOptions) ReadConfigFiles(ctx context.Context, workingDir string, options *ProjectOptions) (*types.ConfigDetails, error) {
|
||||
config, err := loader.LoadConfigFiles(ctx, options.ConfigPaths, workingDir, options.loadOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configs := make([][]byte, len(config.ConfigFiles))
|
||||
|
||||
var configs []types.ConfigFile
|
||||
for _, f := range configPaths {
|
||||
for i, c := range config.ConfigFiles {
|
||||
var err error
|
||||
var b []byte
|
||||
if f == "-" {
|
||||
if c.IsStdin() {
|
||||
b, err = io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
f, err := filepath.Abs(f)
|
||||
f, err := filepath.Abs(c.Filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -427,27 +429,31 @@ func (o *ProjectOptions) GetConfigFiles() ([]types.ConfigFile, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
configs = append(configs, types.ConfigFile{
|
||||
Filename: f,
|
||||
Content: b,
|
||||
})
|
||||
configs[i] = b
|
||||
}
|
||||
return configs, err
|
||||
for i, c := range configs {
|
||||
config.ConfigFiles[i].Content = c
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// LoadProject loads compose file according to options and bind to types.Project go structs
|
||||
func (o *ProjectOptions) LoadProject(ctx context.Context) (*types.Project, error) {
|
||||
configDetails, err := o.prepare()
|
||||
config, err := o.prepare(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
project, err := loader.LoadWithContext(ctx, configDetails, o.loadOptions...)
|
||||
project, err := loader.LoadWithContext(ctx, types.ConfigDetails{
|
||||
ConfigFiles: config.ConfigFiles,
|
||||
WorkingDir: config.WorkingDir,
|
||||
Environment: o.Environment,
|
||||
}, o.loadOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, config := range configDetails.ConfigFiles {
|
||||
for _, config := range config.ConfigFiles {
|
||||
project.ComposeFiles = append(project.ComposeFiles, config.Filename)
|
||||
}
|
||||
|
||||
@@ -456,36 +462,31 @@ func (o *ProjectOptions) LoadProject(ctx context.Context) (*types.Project, error
|
||||
|
||||
// LoadModel loads compose file according to options and returns a raw (yaml tree) model
|
||||
func (o *ProjectOptions) LoadModel(ctx context.Context) (map[string]any, error) {
|
||||
configDetails, err := o.prepare()
|
||||
configDetails, err := o.prepare(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return loader.LoadModelWithContext(ctx, configDetails, o.loadOptions...)
|
||||
return loader.LoadModelWithContext(ctx, *configDetails, o.loadOptions...)
|
||||
}
|
||||
|
||||
// prepare converts ProjectOptions into loader's types.ConfigDetails and configures default load options
|
||||
func (o *ProjectOptions) prepare() (types.ConfigDetails, error) {
|
||||
configs, err := o.GetConfigFiles()
|
||||
func (o *ProjectOptions) prepare(ctx context.Context) (*types.ConfigDetails, error) {
|
||||
defaultDir, err := o.GetWorkingDir()
|
||||
if err != nil {
|
||||
return types.ConfigDetails{}, err
|
||||
return &types.ConfigDetails{}, err
|
||||
}
|
||||
|
||||
workingDir, err := o.GetWorkingDir()
|
||||
configDetails, err := o.ReadConfigFiles(ctx, defaultDir, o)
|
||||
if err != nil {
|
||||
return types.ConfigDetails{}, err
|
||||
}
|
||||
|
||||
configDetails := types.ConfigDetails{
|
||||
ConfigFiles: configs,
|
||||
WorkingDir: workingDir,
|
||||
Environment: o.Environment,
|
||||
return configDetails, err
|
||||
}
|
||||
|
||||
o.loadOptions = append(o.loadOptions,
|
||||
withNamePrecedenceLoad(workingDir, o),
|
||||
withNamePrecedenceLoad(defaultDir, o),
|
||||
withConvertWindowsPaths(o),
|
||||
withListeners(o))
|
||||
|
||||
return configDetails, nil
|
||||
}
|
||||
|
||||
@@ -502,8 +503,13 @@ func withNamePrecedenceLoad(absWorkingDir string, options *ProjectOptions) func(
|
||||
} else if nameFromEnv, ok := options.Environment[consts.ComposeProjectName]; ok && nameFromEnv != "" {
|
||||
opts.SetProjectName(nameFromEnv, true)
|
||||
} else {
|
||||
dirname := filepath.Base(absWorkingDir)
|
||||
symlink, err := filepath.EvalSymlinks(absWorkingDir)
|
||||
if err == nil && filepath.Base(symlink) != dirname {
|
||||
logrus.Warnf("project has been loaded without an explicit name from a symlink. Using name %q", dirname)
|
||||
}
|
||||
opts.SetProjectName(
|
||||
loader.NormalizeProjectName(filepath.Base(absWorkingDir)),
|
||||
loader.NormalizeProjectName(dirname),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
38
vendor/github.com/compose-spec/compose-go/v2/dotenv/format.go
generated
vendored
Normal file
38
vendor/github.com/compose-spec/compose-go/v2/dotenv/format.go
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
Copyright 2020 The Compose Specification Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package dotenv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
var formats = map[string]Parser{}
|
||||
|
||||
type Parser func(r io.Reader, filename string, lookup func(key string) (string, bool)) (map[string]string, error)
|
||||
|
||||
func RegisterFormat(format string, p Parser) {
|
||||
formats[format] = p
|
||||
}
|
||||
|
||||
func ParseWithFormat(r io.Reader, filename string, resolve LookupFn, format string) (map[string]string, error) {
|
||||
parser, ok := formats[format]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported env_file format %q", format)
|
||||
}
|
||||
return parser(r, filename, resolve)
|
||||
}
|
6
vendor/github.com/compose-spec/compose-go/v2/dotenv/godotenv.go
generated
vendored
6
vendor/github.com/compose-spec/compose-go/v2/dotenv/godotenv.go
generated
vendored
@@ -86,7 +86,7 @@ func ReadWithLookup(lookupFn LookupFn, filenames ...string) (map[string]string,
|
||||
envMap := make(map[string]string)
|
||||
|
||||
for _, filename := range filenames {
|
||||
individualEnvMap, individualErr := readFile(filename, lookupFn)
|
||||
individualEnvMap, individualErr := ReadFile(filename, lookupFn)
|
||||
|
||||
if individualErr != nil {
|
||||
return envMap, individualErr
|
||||
@@ -129,7 +129,7 @@ func filenamesOrDefault(filenames []string) []string {
|
||||
}
|
||||
|
||||
func loadFile(filename string, overload bool) error {
|
||||
envMap, err := readFile(filename, nil)
|
||||
envMap, err := ReadFile(filename, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func loadFile(filename string, overload bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func readFile(filename string, lookupFn LookupFn) (map[string]string, error) {
|
||||
func ReadFile(filename string, lookupFn LookupFn) (map[string]string, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
6
vendor/github.com/compose-spec/compose-go/v2/dotenv/parser.go
generated
vendored
6
vendor/github.com/compose-spec/compose-go/v2/dotenv/parser.go
generated
vendored
@@ -119,7 +119,7 @@ loop:
|
||||
offset = i + 1
|
||||
inherited = rune == '\n'
|
||||
break loop
|
||||
case '_', '.', '[', ']':
|
||||
case '_', '.', '-', '[', ']':
|
||||
default:
|
||||
// variable name should match [A-Za-z0-9_.-]
|
||||
if unicode.IsLetter(rune) || unicode.IsNumber(rune) {
|
||||
@@ -136,6 +136,10 @@ loop:
|
||||
return "", "", inherited, errors.New("zero length string")
|
||||
}
|
||||
|
||||
if inherited && strings.IndexByte(key, ' ') == -1 {
|
||||
p.line++
|
||||
}
|
||||
|
||||
// trim whitespace
|
||||
key = strings.TrimRightFunc(key, unicode.IsSpace)
|
||||
cutset := strings.TrimLeftFunc(src[offset:], isSpace)
|
||||
|
2
vendor/github.com/compose-spec/compose-go/v2/format/volume.go
generated
vendored
2
vendor/github.com/compose-spec/compose-go/v2/format/volume.go
generated
vendored
@@ -95,7 +95,7 @@ func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolu
|
||||
if isBindOption(option) {
|
||||
setBindOption(volume, option)
|
||||
}
|
||||
// ignore unknown options
|
||||
// ignore unknown options FIXME why not report an error here?
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
68
vendor/github.com/compose-spec/compose-go/v2/loader/loader.go
generated
vendored
68
vendor/github.com/compose-spec/compose-go/v2/loader/loader.go
generated
vendored
@@ -30,6 +30,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/consts"
|
||||
"github.com/compose-spec/compose-go/v2/errdefs"
|
||||
interp "github.com/compose-spec/compose-go/v2/interpolation"
|
||||
"github.com/compose-spec/compose-go/v2/override"
|
||||
"github.com/compose-spec/compose-go/v2/paths"
|
||||
@@ -139,9 +140,9 @@ func (l localResourceLoader) abs(p string) string {
|
||||
return filepath.Join(l.WorkingDir, p)
|
||||
}
|
||||
|
||||
func (l localResourceLoader) Accept(p string) bool {
|
||||
_, err := os.Stat(l.abs(p))
|
||||
return err == nil
|
||||
func (l localResourceLoader) Accept(_ string) bool {
|
||||
// LocalResourceLoader is the last loader tested so it always should accept the config and try to get the content.
|
||||
return true
|
||||
}
|
||||
|
||||
func (l localResourceLoader) Load(_ context.Context, p string) (string, error) {
|
||||
@@ -300,6 +301,58 @@ func parseYAML(decoder *yaml.Decoder) (map[string]interface{}, PostProcessor, er
|
||||
return converted.(map[string]interface{}), &processor, nil
|
||||
}
|
||||
|
||||
// LoadConfigFiles ingests config files with ResourceLoader and returns config details with paths to local copies
|
||||
func LoadConfigFiles(ctx context.Context, configFiles []string, workingDir string, options ...func(*Options)) (*types.ConfigDetails, error) {
|
||||
if len(configFiles) < 1 {
|
||||
return &types.ConfigDetails{}, fmt.Errorf("no configuration file provided: %w", errdefs.ErrNotFound)
|
||||
}
|
||||
|
||||
opts := &Options{}
|
||||
config := &types.ConfigDetails{
|
||||
ConfigFiles: make([]types.ConfigFile, len(configFiles)),
|
||||
}
|
||||
|
||||
for _, op := range options {
|
||||
op(opts)
|
||||
}
|
||||
opts.ResourceLoaders = append(opts.ResourceLoaders, localResourceLoader{})
|
||||
|
||||
for i, p := range configFiles {
|
||||
if p == "-" {
|
||||
config.ConfigFiles[i] = types.ConfigFile{
|
||||
Filename: p,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for _, loader := range opts.ResourceLoaders {
|
||||
_, isLocalResourceLoader := loader.(localResourceLoader)
|
||||
if !loader.Accept(p) {
|
||||
continue
|
||||
}
|
||||
local, err := loader.Load(ctx, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.WorkingDir == "" && !isLocalResourceLoader {
|
||||
config.WorkingDir = filepath.Dir(local)
|
||||
}
|
||||
abs, err := filepath.Abs(local)
|
||||
if err != nil {
|
||||
abs = local
|
||||
}
|
||||
config.ConfigFiles[i] = types.ConfigFile{
|
||||
Filename: abs,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if config.WorkingDir == "" {
|
||||
config.WorkingDir = workingDir
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// Load reads a ConfigDetails and returns a fully loaded configuration.
|
||||
// Deprecated: use LoadWithContext.
|
||||
func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) {
|
||||
@@ -470,6 +523,8 @@ func loadYamlFile(ctx context.Context, file types.ConfigFile, opts *Options, wor
|
||||
return err
|
||||
}
|
||||
|
||||
dict = OmitEmpty(dict)
|
||||
|
||||
// Canonical transformation can reveal duplicates, typically as ports can be a range and conflict with an override
|
||||
dict, err = override.EnforceUnicity(dict)
|
||||
return err
|
||||
@@ -675,6 +730,7 @@ func NormalizeProjectName(s string) string {
|
||||
|
||||
var userDefinedKeys = []tree.Path{
|
||||
"services",
|
||||
"services.*.depends_on",
|
||||
"volumes",
|
||||
"networks",
|
||||
"secrets",
|
||||
@@ -687,7 +743,7 @@ func processExtensions(dict map[string]any, p tree.Path, extensions map[string]a
|
||||
for key, value := range dict {
|
||||
skip := false
|
||||
for _, uk := range userDefinedKeys {
|
||||
if uk.Matches(p) {
|
||||
if p.Matches(uk) {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
@@ -770,14 +826,14 @@ func secretConfigDecoderHook(from, to reflect.Type, data interface{}) (interface
|
||||
// Check if the input is a map and we're decoding into a SecretConfig
|
||||
if from.Kind() == reflect.Map && to == reflect.TypeOf(types.SecretConfig{}) {
|
||||
if v, ok := data.(map[string]interface{}); ok {
|
||||
if ext, ok := v["#extensions"].(map[string]interface{}); ok {
|
||||
if ext, ok := v[consts.Extensions].(map[string]interface{}); ok {
|
||||
if val, ok := ext[types.SecretConfigXValue].(string); ok {
|
||||
// Return a map with the Content field populated
|
||||
v["Content"] = val
|
||||
delete(ext, types.SecretConfigXValue)
|
||||
|
||||
if len(ext) == 0 {
|
||||
delete(v, "#extensions")
|
||||
delete(v, consts.Extensions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
vendor/github.com/compose-spec/compose-go/v2/loader/normalize.go
generated
vendored
14
vendor/github.com/compose-spec/compose-go/v2/loader/normalize.go
generated
vendored
@@ -18,6 +18,7 @@ package loader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -102,6 +103,17 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if v, ok := service["volumes"]; ok {
|
||||
volumes := v.([]any)
|
||||
for i, volume := range volumes {
|
||||
vol := volume.(map[string]any)
|
||||
target := vol["target"].(string)
|
||||
vol["target"] = path.Clean(target)
|
||||
volumes[i] = vol
|
||||
}
|
||||
service["volumes"] = volumes
|
||||
}
|
||||
|
||||
if n, ok := service["volumes_from"]; ok {
|
||||
volumesFrom := n.([]any)
|
||||
for _, v := range volumesFrom {
|
||||
@@ -123,9 +135,9 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) {
|
||||
}
|
||||
services[name] = service
|
||||
}
|
||||
|
||||
dict["services"] = services
|
||||
}
|
||||
|
||||
setNameFromKey(dict)
|
||||
|
||||
return dict, nil
|
||||
|
74
vendor/github.com/compose-spec/compose-go/v2/loader/omitEmpty.go
generated
vendored
Normal file
74
vendor/github.com/compose-spec/compose-go/v2/loader/omitEmpty.go
generated
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
Copyright 2020 The Compose Specification Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package loader
|
||||
|
||||
import "github.com/compose-spec/compose-go/v2/tree"
|
||||
|
||||
var omitempty = []tree.Path{
|
||||
"services.*.dns"}
|
||||
|
||||
// OmitEmpty removes empty attributes which are irrelevant when unset
|
||||
func OmitEmpty(yaml map[string]any) map[string]any {
|
||||
cleaned := omitEmpty(yaml, tree.NewPath())
|
||||
return cleaned.(map[string]any)
|
||||
}
|
||||
|
||||
func omitEmpty(data any, p tree.Path) any {
|
||||
switch v := data.(type) {
|
||||
case map[string]any:
|
||||
for k, e := range v {
|
||||
if isEmpty(e) && mustOmit(p) {
|
||||
delete(v, k)
|
||||
continue
|
||||
}
|
||||
|
||||
v[k] = omitEmpty(e, p.Next(k))
|
||||
}
|
||||
return v
|
||||
case []any:
|
||||
var c []any
|
||||
for _, e := range v {
|
||||
if isEmpty(e) && mustOmit(p) {
|
||||
continue
|
||||
}
|
||||
|
||||
c = append(c, omitEmpty(e, p.Next("[]")))
|
||||
}
|
||||
return c
|
||||
default:
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
func mustOmit(p tree.Path) bool {
|
||||
for _, pattern := range omitempty {
|
||||
if p.Matches(pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isEmpty(e any) bool {
|
||||
if e == nil {
|
||||
return true
|
||||
}
|
||||
if v, ok := e.(string); ok && v == "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user