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/build"
|
||||
"github.com/docker/buildx/util/buildflags"
|
||||
"github.com/docker/buildx/util/osutil"
|
||||
"github.com/docker/buildx/util/platformutil"
|
||||
"github.com/docker/buildx/util/progress"
|
||||
"github.com/docker/buildx/util/urlutil"
|
||||
@@ -46,6 +47,10 @@ type File struct {
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type ParseOpt struct {
|
||||
FileRelativePaths bool
|
||||
}
|
||||
|
||||
type Override struct {
|
||||
Value string
|
||||
ArrValue []string
|
||||
@@ -197,8 +202,8 @@ func ListTargets(files []File) ([]string, error) {
|
||||
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) {
|
||||
c, _, err := ParseFiles(files, defaults, vars)
|
||||
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, opts...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -337,7 +342,7 @@ func (c Config) matchNames(pattern string) ([]string, error) {
|
||||
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() {
|
||||
err = formatHCLError(err, files)
|
||||
}()
|
||||
@@ -413,9 +418,54 @@ func ParseFiles(files []File, defaults, vars map[string]string) (_ *Config, _ *h
|
||||
pm = *res
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
if opt.FileRelativePaths {
|
||||
rebaseContextPaths(&c, files)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
c2 := c
|
||||
c2.Groups = make([]*Group, 0, len(c2.Groups))
|
||||
|
||||
@@ -763,6 +763,99 @@ func TestHCLDockerfileCwdPrefix(t *testing.T) {
|
||||
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) {
|
||||
fp := File{
|
||||
Name: "docker-bake.hcl",
|
||||
|
||||
+23
-2
@@ -12,6 +12,7 @@ import (
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/tabwriter"
|
||||
@@ -48,6 +49,7 @@ import (
|
||||
const (
|
||||
bakeEnvFileSeparator = "BUILDX_BAKE_PATH_SEPARATOR"
|
||||
bakeEnvFilePath = "BUILDX_BAKE_FILE"
|
||||
bakeEnvFileRelative = "BUILDX_BAKE_FILE_RELATIVE_PATHS"
|
||||
)
|
||||
|
||||
type bakeOptions struct {
|
||||
@@ -225,9 +227,16 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileRelativePaths, err := bakeFileRelativePaths()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parseOpt := bake.ParseOpt{
|
||||
FileRelativePaths: fileRelativePaths,
|
||||
}
|
||||
|
||||
if in.list != "" {
|
||||
cfg, pm, err := bake.ParseFiles(files, defaults, vars)
|
||||
cfg, pm, err := bake.ParseFiles(files, defaults, vars, parseOpt)
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -674,6 +683,18 @@ func bakeArgs(args []string) (url, cmdContext string, targets []string) {
|
||||
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) {
|
||||
var lnames []string // local
|
||||
var rnames []string // remote
|
||||
|
||||
@@ -416,6 +416,10 @@ target "app" {
|
||||
```
|
||||
|
||||
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
|
||||
$ 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
|
||||
(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).
|
||||
The following example builds the `db` and `webapp-release` targets that are
|
||||
defined in the `docker-bake.dev.hcl` file:
|
||||
|
||||
@@ -45,6 +45,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){
|
||||
testBakePrintRemoteContextSubdir,
|
||||
testBakeLocal,
|
||||
testBakeLocalMulti,
|
||||
testBakeFileRelativePaths,
|
||||
testBakeLocalExportDeleteMode,
|
||||
testBakeRemote,
|
||||
testBakeRemoteAuth,
|
||||
@@ -674,6 +675,76 @@ services:
|
||||
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) {
|
||||
dockerfile := []byte(`
|
||||
FROM scratch
|
||||
|
||||
Reference in New Issue
Block a user