bake: add file-relative path opt-in
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
+53
-3
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/docker/buildx/bake/hclparser"
|
"github.com/docker/buildx/bake/hclparser"
|
||||||
"github.com/docker/buildx/build"
|
"github.com/docker/buildx/build"
|
||||||
"github.com/docker/buildx/util/buildflags"
|
"github.com/docker/buildx/util/buildflags"
|
||||||
|
"github.com/docker/buildx/util/osutil"
|
||||||
"github.com/docker/buildx/util/platformutil"
|
"github.com/docker/buildx/util/platformutil"
|
||||||
"github.com/docker/buildx/util/progress"
|
"github.com/docker/buildx/util/progress"
|
||||||
"github.com/docker/buildx/util/urlutil"
|
"github.com/docker/buildx/util/urlutil"
|
||||||
@@ -46,6 +47,10 @@ type File struct {
|
|||||||
Data []byte
|
Data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ParseOpt struct {
|
||||||
|
FileRelativePaths bool
|
||||||
|
}
|
||||||
|
|
||||||
type Override struct {
|
type Override struct {
|
||||||
Value string
|
Value string
|
||||||
ArrValue []string
|
ArrValue []string
|
||||||
@@ -197,8 +202,8 @@ func ListTargets(files []File) ([]string, error) {
|
|||||||
return dedupSlice(targets), nil
|
return dedupSlice(targets), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadTargets(ctx context.Context, files []File, targets, overrides []string, defaults, vars map[string]string, ent *EntitlementConf) (map[string]*Target, map[string]*Group, error) {
|
func ReadTargets(ctx context.Context, files []File, targets, overrides []string, defaults, vars map[string]string, ent *EntitlementConf, opts ...ParseOpt) (map[string]*Target, map[string]*Group, error) {
|
||||||
c, _, err := ParseFiles(files, defaults, vars)
|
c, _, err := ParseFiles(files, defaults, vars, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -337,7 +342,7 @@ func (c Config) matchNames(pattern string) ([]string, error) {
|
|||||||
return names, nil
|
return names, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseFiles(files []File, defaults, vars map[string]string) (_ *Config, _ *hclparser.ParseMeta, err error) {
|
func ParseFiles(files []File, defaults, vars map[string]string, opts ...ParseOpt) (_ *Config, _ *hclparser.ParseMeta, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
err = formatHCLError(err, files)
|
err = formatHCLError(err, files)
|
||||||
}()
|
}()
|
||||||
@@ -413,9 +418,54 @@ func ParseFiles(files []File, defaults, vars map[string]string) (_ *Config, _ *h
|
|||||||
pm = *res
|
pm = *res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
if opt.FileRelativePaths {
|
||||||
|
rebaseContextPaths(&c, files)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &c, &pm, nil
|
return &c, &pm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rebaseContextPaths(c *Config, files []File) {
|
||||||
|
base, ok := firstLocalFileDir(files)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, t := range c.Targets {
|
||||||
|
if t.Context != nil {
|
||||||
|
contextPath := rebaseContextPath(base, *t.Context)
|
||||||
|
t.Context = &contextPath
|
||||||
|
}
|
||||||
|
for k, v := range t.Contexts {
|
||||||
|
t.Contexts[k] = rebaseContextPath(base, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstLocalFileDir(files []File) (string, bool) {
|
||||||
|
if len(files) == 0 || files[0].Name == "-" || urlutil.IsRemoteURL(files[0].Name) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return filepath.Dir(files[0].Name), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func rebaseContextPath(base, p string) string {
|
||||||
|
if p == "" || isSpecialContextPath(p) || filepath.IsAbs(p) {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
return osutil.SanitizePath(filepath.Join(base, filepath.FromSlash(p)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSpecialContextPath(p string) bool {
|
||||||
|
return strings.HasPrefix(p, "cwd://") ||
|
||||||
|
strings.HasPrefix(p, "target:") ||
|
||||||
|
strings.HasPrefix(p, "docker-image:") ||
|
||||||
|
strings.HasPrefix(p, "oci-layout://") ||
|
||||||
|
urlutil.IsRemoteURL(p)
|
||||||
|
}
|
||||||
|
|
||||||
func dedupeConfig(c Config) Config {
|
func dedupeConfig(c Config) Config {
|
||||||
c2 := c
|
c2 := c
|
||||||
c2.Groups = make([]*Group, 0, len(c2.Groups))
|
c2.Groups = make([]*Group, 0, len(c2.Groups))
|
||||||
|
|||||||
@@ -763,6 +763,99 @@ func TestHCLDockerfileCwdPrefix(t *testing.T) {
|
|||||||
assert.Equal(t, ".", bo["app"].Inputs.ContextPath)
|
assert.Equal(t, ".", bo["app"].Inputs.ContextPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextPathRebase(t *testing.T) {
|
||||||
|
fp := File{
|
||||||
|
Name: filepath.Join("subdir", "docker-bake.hcl"),
|
||||||
|
Data: []byte(`
|
||||||
|
target "base" {
|
||||||
|
context = "base"
|
||||||
|
}
|
||||||
|
|
||||||
|
target "app" {
|
||||||
|
context = "."
|
||||||
|
dockerfile = "Dockerfile.app"
|
||||||
|
contexts = {
|
||||||
|
shared = "../shared"
|
||||||
|
cwd = "cwd://local"
|
||||||
|
linked = "target:base"
|
||||||
|
image = "docker-image://alpine:latest"
|
||||||
|
layout = "oci-layout://layout"
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
}
|
||||||
|
|
||||||
|
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, nil, nil, nil, &EntitlementConf{}, ParseOpt{
|
||||||
|
FileRelativePaths: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, filepath.ToSlash(filepath.Clean("subdir")), *m["app"].Context)
|
||||||
|
require.Equal(t, "Dockerfile.app", *m["app"].Dockerfile)
|
||||||
|
require.Equal(t, filepath.ToSlash(filepath.Clean("shared")), m["app"].Contexts["shared"])
|
||||||
|
require.Equal(t, "cwd://local", m["app"].Contexts["cwd"])
|
||||||
|
require.Equal(t, "target:base", m["app"].Contexts["linked"])
|
||||||
|
require.Equal(t, "docker-image://alpine:latest", m["app"].Contexts["image"])
|
||||||
|
require.Equal(t, "oci-layout://layout", m["app"].Contexts["layout"])
|
||||||
|
require.Equal(t, filepath.ToSlash(filepath.Clean("subdir/base")), *m["base"].Context)
|
||||||
|
|
||||||
|
bo, err := TargetsToBuildOpt(m, &Input{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, filepath.ToSlash(filepath.Clean("subdir")), bo["app"].Inputs.ContextPath)
|
||||||
|
require.Equal(t, filepath.Join("subdir", "Dockerfile.app"), bo["app"].Inputs.DockerfilePath)
|
||||||
|
require.Equal(t, filepath.ToSlash(filepath.Clean("shared")), bo["app"].Inputs.NamedContexts["shared"].Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOverridesNotRebased(t *testing.T) {
|
||||||
|
fp := File{
|
||||||
|
Name: filepath.Join("subdir", "docker-bake.hcl"),
|
||||||
|
Data: []byte(`
|
||||||
|
target "app" {
|
||||||
|
context = "."
|
||||||
|
contexts = {
|
||||||
|
shared = "../shared"
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
}
|
||||||
|
|
||||||
|
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"app"}, []string{
|
||||||
|
"app.context=override",
|
||||||
|
"app.contexts.shared=override-shared",
|
||||||
|
}, nil, nil, &EntitlementConf{}, ParseOpt{
|
||||||
|
FileRelativePaths: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, "override", *m["app"].Context)
|
||||||
|
require.Equal(t, "override-shared", m["app"].Contexts["shared"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComposePathRebase(t *testing.T) {
|
||||||
|
fp := File{
|
||||||
|
Name: filepath.Join("tests", "docker-compose.yml"),
|
||||||
|
Data: []byte(`
|
||||||
|
services:
|
||||||
|
debian:
|
||||||
|
build:
|
||||||
|
context: ./dockerfiles/debian
|
||||||
|
additional_contexts:
|
||||||
|
shared: ../shared
|
||||||
|
`),
|
||||||
|
}
|
||||||
|
|
||||||
|
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"debian"}, nil, nil, nil, &EntitlementConf{}, ParseOpt{
|
||||||
|
FileRelativePaths: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, filepath.ToSlash(filepath.Clean("tests/dockerfiles/debian")), *m["debian"].Context)
|
||||||
|
require.Equal(t, filepath.ToSlash(filepath.Clean("shared")), m["debian"].Contexts["shared"])
|
||||||
|
|
||||||
|
bo, err := TargetsToBuildOpt(m, &Input{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, filepath.ToSlash(filepath.Clean("tests/dockerfiles/debian")), bo["debian"].Inputs.ContextPath)
|
||||||
|
require.Equal(t, filepath.Join("tests", "dockerfiles", "debian", "Dockerfile"), bo["debian"].Inputs.DockerfilePath)
|
||||||
|
}
|
||||||
|
|
||||||
func TestOverrideMerge(t *testing.T) {
|
func TestOverrideMerge(t *testing.T) {
|
||||||
fp := File{
|
fp := File{
|
||||||
Name: "docker-bake.hcl",
|
Name: "docker-bake.hcl",
|
||||||
|
|||||||
+23
-2
@@ -12,6 +12,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
@@ -48,6 +49,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
bakeEnvFileSeparator = "BUILDX_BAKE_PATH_SEPARATOR"
|
bakeEnvFileSeparator = "BUILDX_BAKE_PATH_SEPARATOR"
|
||||||
bakeEnvFilePath = "BUILDX_BAKE_FILE"
|
bakeEnvFilePath = "BUILDX_BAKE_FILE"
|
||||||
|
bakeEnvFileRelative = "BUILDX_BAKE_FILE_RELATIVE_PATHS"
|
||||||
)
|
)
|
||||||
|
|
||||||
type bakeOptions struct {
|
type bakeOptions struct {
|
||||||
@@ -225,9 +227,16 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
fileRelativePaths, err := bakeFileRelativePaths()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parseOpt := bake.ParseOpt{
|
||||||
|
FileRelativePaths: fileRelativePaths,
|
||||||
|
}
|
||||||
|
|
||||||
if in.list != "" {
|
if in.list != "" {
|
||||||
cfg, pm, err := bake.ParseFiles(files, defaults, vars)
|
cfg, pm, err := bake.ParseFiles(files, defaults, vars, parseOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -246,7 +255,7 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tgts, grps, err := bake.ReadTargets(ctx, files, targets, overrides, defaults, vars, &ent)
|
tgts, grps, err := bake.ReadTargets(ctx, files, targets, overrides, defaults, vars, &ent, parseOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -674,6 +683,18 @@ func bakeArgs(args []string) (url, cmdContext string, targets []string) {
|
|||||||
return url, cmdContext, targets
|
return url, cmdContext, targets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func bakeFileRelativePaths() (bool, error) {
|
||||||
|
v := strings.TrimSpace(os.Getenv(bakeEnvFileRelative))
|
||||||
|
if v == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
enabled, err := strconv.ParseBool(v)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrapf(err, "failed to parse %s value %q", bakeEnvFileRelative, v)
|
||||||
|
}
|
||||||
|
return enabled, nil
|
||||||
|
}
|
||||||
|
|
||||||
func readBakeFiles(ctx context.Context, nodes []builder.Node, url string, names []string, stdin io.Reader, pw progress.Writer, filesFromEnv bool) (files []bake.File, inp *bake.Input, err error) {
|
func readBakeFiles(ctx context.Context, nodes []builder.Node, url string, names []string, stdin io.Reader, pw progress.Writer, filesFromEnv bool) (files []bake.File, inp *bake.Input, err error) {
|
||||||
var lnames []string // local
|
var lnames []string // local
|
||||||
var rnames []string // remote
|
var rnames []string // remote
|
||||||
|
|||||||
@@ -416,6 +416,10 @@ target "app" {
|
|||||||
```
|
```
|
||||||
|
|
||||||
This resolves to the current working directory (`"."`) by default.
|
This resolves to the current working directory (`"."`) by default.
|
||||||
|
Set `BUILDX_BAKE_FILE_RELATIVE_PATHS=1` to resolve local directory paths in
|
||||||
|
`target.context` and `target.contexts` relative to the directory of the first
|
||||||
|
Bake file. Use `cwd://` for paths that should remain relative to the current
|
||||||
|
working directory when this opt-in is enabled.
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ docker buildx bake --print -f - <<< 'target "default" {}'
|
$ docker buildx bake --print -f - <<< 'target "default" {}'
|
||||||
|
|||||||
@@ -153,6 +153,12 @@ This is mutually exclusive with `-f` / `--file`; if both are specified, the envi
|
|||||||
Multiple definitions can be specified by separating them with the system's path separator
|
Multiple definitions can be specified by separating them with the system's path separator
|
||||||
(typically `;` on Windows and `:` elsewhere), but can be changed with `BUILDX_BAKE_PATH_SEPARATOR`.
|
(typically `;` on Windows and `:` elsewhere), but can be changed with `BUILDX_BAKE_PATH_SEPARATOR`.
|
||||||
|
|
||||||
|
By default, local directory build contexts in Bake files are resolved from the
|
||||||
|
current working directory. To opt in to resolving local directory build contexts
|
||||||
|
from the directory of the first Bake file, set
|
||||||
|
`BUILDX_BAKE_FILE_RELATIVE_PATHS=1`. Use the `cwd://` prefix for paths that
|
||||||
|
should remain relative to the current working directory.
|
||||||
|
|
||||||
You can pass the names of the targets to build, to build only specific target(s).
|
You can pass the names of the targets to build, to build only specific target(s).
|
||||||
The following example builds the `db` and `webapp-release` targets that are
|
The following example builds the `db` and `webapp-release` targets that are
|
||||||
defined in the `docker-bake.dev.hcl` file:
|
defined in the `docker-bake.dev.hcl` file:
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){
|
|||||||
testBakePrintRemoteContextSubdir,
|
testBakePrintRemoteContextSubdir,
|
||||||
testBakeLocal,
|
testBakeLocal,
|
||||||
testBakeLocalMulti,
|
testBakeLocalMulti,
|
||||||
|
testBakeFileRelativePaths,
|
||||||
testBakeLocalExportDeleteMode,
|
testBakeLocalExportDeleteMode,
|
||||||
testBakeRemote,
|
testBakeRemote,
|
||||||
testBakeRemoteAuth,
|
testBakeRemoteAuth,
|
||||||
@@ -674,6 +675,76 @@ services:
|
|||||||
require.FileExists(t, filepath.Join(dirDest2, "foo"))
|
require.FileExists(t, filepath.Join(dirDest2, "foo"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testBakeFileRelativePaths(t *testing.T, sb integration.Sandbox) {
|
||||||
|
t.Run("compose context", func(t *testing.T) {
|
||||||
|
dockerfile := []byte(`
|
||||||
|
FROM scratch
|
||||||
|
COPY marker /marker
|
||||||
|
COPY --from=shared shared-marker /shared-marker
|
||||||
|
`)
|
||||||
|
composefile := []byte(`
|
||||||
|
services:
|
||||||
|
debian:
|
||||||
|
build:
|
||||||
|
context: ./dockerfiles/debian
|
||||||
|
additional_contexts:
|
||||||
|
shared: ../shared
|
||||||
|
`)
|
||||||
|
|
||||||
|
dir := tmpdir(
|
||||||
|
t,
|
||||||
|
fstest.CreateDir("tests", 0700),
|
||||||
|
fstest.CreateDir("tests/dockerfiles", 0700),
|
||||||
|
fstest.CreateDir("tests/dockerfiles/debian", 0700),
|
||||||
|
fstest.CreateDir("shared", 0700),
|
||||||
|
fstest.CreateFile("tests/docker-compose.yml", composefile, 0600),
|
||||||
|
fstest.CreateFile("tests/dockerfiles/debian/Dockerfile", dockerfile, 0600),
|
||||||
|
fstest.CreateFile("tests/dockerfiles/debian/marker", []byte("marker"), 0600),
|
||||||
|
fstest.CreateFile("shared/shared-marker", []byte("shared"), 0600),
|
||||||
|
)
|
||||||
|
dirDest := t.TempDir()
|
||||||
|
|
||||||
|
out, err := bakeCmd(
|
||||||
|
sb,
|
||||||
|
withDir(dir),
|
||||||
|
withArgs("--file", "tests/docker-compose.yml", "--set", "*.output=type=local,dest="+dirDest),
|
||||||
|
withEnv("BUILDX_BAKE_FILE_RELATIVE_PATHS=1"),
|
||||||
|
)
|
||||||
|
require.NoError(t, err, out)
|
||||||
|
require.FileExists(t, filepath.Join(dirDest, "marker"))
|
||||||
|
require.FileExists(t, filepath.Join(dirDest, "shared-marker"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cwd prefix", func(t *testing.T) {
|
||||||
|
bakefile := []byte(`
|
||||||
|
target "default" {
|
||||||
|
context = "cwd://."
|
||||||
|
dockerfile-inline = <<EOT
|
||||||
|
FROM scratch
|
||||||
|
COPY root-marker /root-marker
|
||||||
|
EOT
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
dir := tmpdir(
|
||||||
|
t,
|
||||||
|
fstest.CreateDir("definitions", 0700),
|
||||||
|
fstest.CreateFile("definitions/docker-bake.hcl", bakefile, 0600),
|
||||||
|
fstest.CreateFile("root-marker", []byte("root"), 0600),
|
||||||
|
)
|
||||||
|
dirDest := t.TempDir()
|
||||||
|
|
||||||
|
out, err := bakeCmd(
|
||||||
|
sb,
|
||||||
|
withDir(dir),
|
||||||
|
withArgs("--file", "definitions/docker-bake.hcl", "--set", "*.output=type=local,dest="+dirDest),
|
||||||
|
withEnv("BUILDX_BAKE_FILE_RELATIVE_PATHS=1"),
|
||||||
|
)
|
||||||
|
require.NoError(t, err, out)
|
||||||
|
require.FileExists(t, filepath.Join(dirDest, "root-marker"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func testBakeLocalExportDeleteMode(t *testing.T, sb integration.Sandbox) {
|
func testBakeLocalExportDeleteMode(t *testing.T, sb integration.Sandbox) {
|
||||||
dockerfile := []byte(`
|
dockerfile := []byte(`
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
|||||||
Reference in New Issue
Block a user