Merge pull request #3935 from crazy-max/bake-file-relative-paths
bake: add file-relative path opt-in
This commit is contained in:
+181
-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,11 +342,13 @@ 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)
|
||||
}()
|
||||
|
||||
frel := fileRelativePaths(opts)
|
||||
|
||||
var c Config
|
||||
var composeFiles []File
|
||||
var hclFiles []*hcl.File
|
||||
@@ -373,6 +380,9 @@ func ParseFiles(files []File, defaults, vars map[string]string) (_ *Config, _ *h
|
||||
if cmperr != nil {
|
||||
return nil, nil, errors.Wrap(cmperr, "failed to parse compose file")
|
||||
}
|
||||
if frel {
|
||||
setComposeContextBase(cfg, composeFiles)
|
||||
}
|
||||
c = mergeConfig(c, *cfg)
|
||||
c = dedupeConfig(c)
|
||||
}
|
||||
@@ -413,9 +423,78 @@ func ParseFiles(files []File, defaults, vars map[string]string) (_ *Config, _ *h
|
||||
pm = *res
|
||||
}
|
||||
|
||||
if frel {
|
||||
rebaseContextPaths(&c)
|
||||
}
|
||||
|
||||
return &c, &pm, nil
|
||||
}
|
||||
|
||||
func fileRelativePaths(opts []ParseOpt) bool {
|
||||
return slices.ContainsFunc(opts, func(opt ParseOpt) bool {
|
||||
return opt.FileRelativePaths
|
||||
})
|
||||
}
|
||||
|
||||
func rebaseContextPaths(c *Config) {
|
||||
targets := make(map[string]*Target, len(c.Targets))
|
||||
for _, t := range c.Targets {
|
||||
targets[t.Name] = t
|
||||
}
|
||||
|
||||
for _, t := range c.Targets {
|
||||
if ref := targets[t.contextBaseRef]; ref != nil {
|
||||
switch {
|
||||
case ref.hasContextBase:
|
||||
t.contextBase = ref.contextBase
|
||||
t.hasContextBase = true
|
||||
case ref.hasDefaultContextBase:
|
||||
t.contextBase = ref.defaultContextBase
|
||||
t.hasContextBase = true
|
||||
}
|
||||
}
|
||||
t.rebaseContextPaths()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Target) rebaseContextPaths() {
|
||||
if t.Context != nil {
|
||||
if t.hasContextBase {
|
||||
contextPath := rebaseContextPath(t.contextBase, *t.Context)
|
||||
t.Context = &contextPath
|
||||
}
|
||||
} else if t.hasDefaultContextBase {
|
||||
t.useDefaultContextBase = true
|
||||
}
|
||||
for k, v := range t.Contexts {
|
||||
if base, ok := t.contextsBase[k]; ok {
|
||||
t.Contexts[k] = rebaseContextPath(base, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func localFileDir(name string) (string, bool) {
|
||||
if name == "" || name == "-" || urlutil.IsRemoteURL(name) {
|
||||
return "", false
|
||||
}
|
||||
return filepath.Dir(name), true
|
||||
}
|
||||
|
||||
func rebaseContextPath(base, p string) string {
|
||||
if base == "" || 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))
|
||||
@@ -690,6 +769,9 @@ func (c Config) ResolveTarget(name string, overrides map[string]map[string]Overr
|
||||
t.Inherits = nil
|
||||
if t.Context == nil {
|
||||
s := "."
|
||||
if t.useDefaultContextBase {
|
||||
s = rebaseContextPath(t.defaultContextBase, ".")
|
||||
}
|
||||
t.Context = &s
|
||||
}
|
||||
if t.Dockerfile == nil || (t.Dockerfile != nil && *t.Dockerfile == "") {
|
||||
@@ -781,6 +863,14 @@ type Target struct {
|
||||
|
||||
// linked is a private field to mark a target used as a linked one
|
||||
linked bool
|
||||
|
||||
defaultContextBase string
|
||||
hasDefaultContextBase bool
|
||||
useDefaultContextBase bool
|
||||
contextBase string
|
||||
hasContextBase bool
|
||||
contextBaseRef string
|
||||
contextsBase map[string]string
|
||||
}
|
||||
|
||||
func (t *Target) MarshalJSON() ([]byte, error) {
|
||||
@@ -829,10 +919,82 @@ func (t *Target) MarshalJSON() ([]byte, error) {
|
||||
var (
|
||||
_ hclparser.WithEvalContexts = &Target{}
|
||||
_ hclparser.WithGetName = &Target{}
|
||||
_ hclparser.WithBlockSource = &Target{}
|
||||
_ hclparser.WithEvalContexts = &Group{}
|
||||
_ hclparser.WithGetName = &Group{}
|
||||
)
|
||||
|
||||
func (t *Target) SetBlockSource(block *hcl.Block) {
|
||||
base, _ := localFileDir(block.DefRange.Filename)
|
||||
t.defaultContextBase = base
|
||||
t.hasDefaultContextBase = true
|
||||
|
||||
content, _, diags := block.Body.PartialContent(&hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{Name: "context"},
|
||||
{Name: "contexts"},
|
||||
},
|
||||
})
|
||||
if diags.HasErrors() {
|
||||
return
|
||||
}
|
||||
if attr, ok := content.Attributes["context"]; ok {
|
||||
t.contextBase = base
|
||||
t.hasContextBase = true
|
||||
t.contextBaseRef = targetContextRef(attr.Expr)
|
||||
}
|
||||
if _, ok := content.Attributes["contexts"]; ok {
|
||||
t.setContextsBase(base)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Target) setContextsBase(base string) {
|
||||
if len(t.Contexts) == 0 {
|
||||
return
|
||||
}
|
||||
if t.contextsBase == nil {
|
||||
t.contextsBase = map[string]string{}
|
||||
}
|
||||
for k := range t.Contexts {
|
||||
t.contextsBase[k] = base
|
||||
}
|
||||
}
|
||||
|
||||
func targetContextRef(expr hcl.Expression) string {
|
||||
traversal, diags := hcl.AbsTraversalForExpr(expr)
|
||||
if diags.HasErrors() || len(traversal) != 3 {
|
||||
return ""
|
||||
}
|
||||
root, ok := traversal[0].(hcl.TraverseRoot)
|
||||
if !ok || root.Name != "target" {
|
||||
return ""
|
||||
}
|
||||
target, ok := traversalStepName(traversal[1])
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
field, ok := traversal[2].(hcl.TraverseAttr)
|
||||
if !ok || field.Name != "context" {
|
||||
return ""
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
func traversalStepName(step hcl.Traverser) (string, bool) {
|
||||
switch step := step.(type) {
|
||||
case hcl.TraverseAttr:
|
||||
return step.Name, true
|
||||
case hcl.TraverseIndex:
|
||||
key, err := convert.Convert(step.Key, cty.String)
|
||||
if err != nil || key.IsNull() || !key.IsKnown() {
|
||||
return "", false
|
||||
}
|
||||
return key.AsString(), true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Target) normalize() {
|
||||
t.Annotations = removeDupesStr(t.Annotations)
|
||||
t.Attest = t.Attest.Normalize()
|
||||
@@ -863,8 +1025,16 @@ func (t *Target) normalize() {
|
||||
}
|
||||
|
||||
func (t *Target) Merge(t2 *Target) {
|
||||
if t2.hasDefaultContextBase {
|
||||
t.defaultContextBase = t2.defaultContextBase
|
||||
t.hasDefaultContextBase = true
|
||||
t.useDefaultContextBase = t2.useDefaultContextBase
|
||||
}
|
||||
if t2.Context != nil {
|
||||
t.Context = t2.Context
|
||||
t.contextBase = t2.contextBase
|
||||
t.hasContextBase = t2.hasContextBase
|
||||
t.contextBaseRef = t2.contextBaseRef
|
||||
}
|
||||
if t2.Dockerfile != nil {
|
||||
t.Dockerfile = t2.Dockerfile
|
||||
@@ -886,6 +1056,14 @@ func (t *Target) Merge(t2 *Target) {
|
||||
t.Contexts = map[string]string{}
|
||||
}
|
||||
t.Contexts[k] = v
|
||||
if t.contextsBase == nil {
|
||||
t.contextsBase = map[string]string{}
|
||||
}
|
||||
if base, ok := t2.contextsBase[k]; ok {
|
||||
t.contextsBase[k] = base
|
||||
} else {
|
||||
delete(t.contextsBase, k)
|
||||
}
|
||||
}
|
||||
for k, v := range t2.Labels {
|
||||
if v == nil {
|
||||
|
||||
@@ -763,6 +763,242 @@ 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 TestDefaultContextRebase(t *testing.T) {
|
||||
fp := File{
|
||||
Name: filepath.Join("definitions", "docker-bake.hcl"),
|
||||
Data: []byte(`
|
||||
target "app" {
|
||||
dockerfile-inline = <<EOT
|
||||
FROM scratch
|
||||
EOT
|
||||
}`),
|
||||
}
|
||||
|
||||
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("definitions")), *m["app"].Context)
|
||||
|
||||
bo, err := TargetsToBuildOpt(m, &Input{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, filepath.ToSlash(filepath.Clean("definitions")), bo["app"].Inputs.ContextPath)
|
||||
}
|
||||
|
||||
func TestDefinitionPathBase(t *testing.T) {
|
||||
fp1 := File{
|
||||
Name: filepath.Join("one", "docker-bake.hcl"),
|
||||
Data: []byte(`
|
||||
target "app" {
|
||||
context = "."
|
||||
contexts = {
|
||||
shared = "../shared"
|
||||
}
|
||||
}
|
||||
|
||||
target "implicit" {
|
||||
dockerfile-inline = <<EOT
|
||||
FROM scratch
|
||||
EOT
|
||||
}`),
|
||||
}
|
||||
fp2 := File{
|
||||
Name: filepath.Join("two", "docker-bake.hcl"),
|
||||
Data: []byte(`
|
||||
target "app" {
|
||||
tags = ["app:latest"]
|
||||
}
|
||||
|
||||
target "other" {
|
||||
context = "."
|
||||
}`),
|
||||
}
|
||||
|
||||
m, _, err := ReadTargets(context.TODO(), []File{fp1, fp2}, []string{"app", "implicit", "other"}, nil, nil, nil, &EntitlementConf{}, ParseOpt{
|
||||
FileRelativePaths: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, filepath.ToSlash(filepath.Clean("one")), *m["app"].Context)
|
||||
require.Equal(t, filepath.ToSlash(filepath.Clean("shared")), m["app"].Contexts["shared"])
|
||||
require.Equal(t, []string{"app:latest"}, m["app"].Tags)
|
||||
require.Equal(t, filepath.ToSlash(filepath.Clean("one")), *m["implicit"].Context)
|
||||
require.Equal(t, filepath.ToSlash(filepath.Clean("two")), *m["other"].Context)
|
||||
}
|
||||
|
||||
func TestInheritedContextRebase(t *testing.T) {
|
||||
t.Run("same file", func(t *testing.T) {
|
||||
fp := File{
|
||||
Name: filepath.Join("subdir", "docker-bake.hcl"),
|
||||
Data: []byte(`
|
||||
target "base" {
|
||||
context = "basectx"
|
||||
}
|
||||
|
||||
target "app" {
|
||||
inherits = ["base"]
|
||||
tags = ["app:latest"]
|
||||
}`),
|
||||
}
|
||||
|
||||
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/basectx")), *m["app"].Context)
|
||||
})
|
||||
|
||||
t.Run("cross file", func(t *testing.T) {
|
||||
fp1 := File{
|
||||
Name: filepath.Join("one", "docker-bake.hcl"),
|
||||
Data: []byte(`
|
||||
target "base" {
|
||||
context = "basectx"
|
||||
}`),
|
||||
}
|
||||
fp2 := File{
|
||||
Name: filepath.Join("two", "docker-bake.hcl"),
|
||||
Data: []byte(`
|
||||
target "app" {
|
||||
inherits = ["base"]
|
||||
tags = ["app:latest"]
|
||||
}`),
|
||||
}
|
||||
|
||||
m, _, err := ReadTargets(context.TODO(), []File{fp1, fp2}, []string{"app"}, nil, nil, nil, &EntitlementConf{}, ParseOpt{
|
||||
FileRelativePaths: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, filepath.ToSlash(filepath.Clean("one/basectx")), *m["app"].Context)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTargetReferenceContextRebase(t *testing.T) {
|
||||
fp1 := File{
|
||||
Name: filepath.Join("one", "docker-bake.hcl"),
|
||||
Data: []byte(`
|
||||
target "base" {
|
||||
context = "basectx"
|
||||
}`),
|
||||
}
|
||||
fp2 := File{
|
||||
Name: filepath.Join("two", "docker-bake.hcl"),
|
||||
Data: []byte(`
|
||||
target "app" {
|
||||
context = target.base.context
|
||||
}`),
|
||||
}
|
||||
|
||||
m, _, err := ReadTargets(context.TODO(), []File{fp1, fp2}, []string{"app"}, nil, nil, nil, &EntitlementConf{}, ParseOpt{
|
||||
FileRelativePaths: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, filepath.ToSlash(filepath.Clean("one/basectx")), *m["app"].Context)
|
||||
}
|
||||
|
||||
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
|
||||
implicit:
|
||||
build:
|
||||
dockerfile_inline: |
|
||||
FROM scratch
|
||||
`),
|
||||
}
|
||||
|
||||
m, _, err := ReadTargets(context.TODO(), []File{fp}, []string{"debian", "implicit"}, 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"])
|
||||
require.Equal(t, filepath.ToSlash(filepath.Clean("tests")), *m["implicit"].Context)
|
||||
|
||||
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)
|
||||
require.Equal(t, filepath.ToSlash(filepath.Clean("tests")), bo["implicit"].Inputs.ContextPath)
|
||||
}
|
||||
|
||||
func TestOverrideMerge(t *testing.T) {
|
||||
fp := File{
|
||||
Name: "docker-bake.hcl",
|
||||
|
||||
+32
-1
@@ -21,11 +21,23 @@ import (
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
func ParseComposeFiles(fs []File, envOverrides map[string]string) (*Config, error) {
|
||||
func ParseComposeFiles(fs []File, envOverrides map[string]string, opts ...ParseOpt) (*Config, error) {
|
||||
envs, err := composeEnv(envOverrides)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg, err := parseComposeFiles(fs, envs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fileRelativePaths(opts) {
|
||||
setComposeContextBase(cfg, fs)
|
||||
rebaseContextPaths(cfg)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func parseComposeFiles(fs []File, envs map[string]string) (*Config, error) {
|
||||
var cfgs []composetypes.ConfigFile
|
||||
for _, f := range fs {
|
||||
cfgs = append(cfgs, composetypes.ConfigFile{
|
||||
@@ -36,6 +48,25 @@ func ParseComposeFiles(fs []File, envOverrides map[string]string) (*Config, erro
|
||||
return ParseCompose(cfgs, envs)
|
||||
}
|
||||
|
||||
func setComposeContextBase(c *Config, files []File) {
|
||||
if len(files) == 0 {
|
||||
return
|
||||
}
|
||||
base, ok := localFileDir(files[0].Name)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for _, t := range c.Targets {
|
||||
t.defaultContextBase = base
|
||||
t.hasDefaultContextBase = true
|
||||
if t.Context != nil {
|
||||
t.contextBase = base
|
||||
t.hasContextBase = true
|
||||
}
|
||||
t.setContextsBase(base)
|
||||
}
|
||||
}
|
||||
|
||||
func ParseCompose(cfgs []composetypes.ConfigFile, envs map[string]string) (*Config, error) {
|
||||
cfg, err := loadComposeFiles(cfgs, envs)
|
||||
if err != nil {
|
||||
|
||||
@@ -174,6 +174,45 @@ services:
|
||||
require.Equal(t, "webapp", *c.Targets[1].Target)
|
||||
}
|
||||
|
||||
func TestComposeProjectBase(t *testing.T) {
|
||||
fp := File{
|
||||
Name: filepath.Join("project", "compose.yml"),
|
||||
Data: []byte(`
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: ./app
|
||||
`),
|
||||
}
|
||||
fp2 := File{
|
||||
Name: filepath.Join("overrides", "compose.yml"),
|
||||
Data: []byte(`
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
additional_contexts:
|
||||
shared: ./shared
|
||||
other:
|
||||
build:
|
||||
context: ./other
|
||||
`),
|
||||
}
|
||||
|
||||
c, err := ParseComposeFiles([]File{fp, fp2}, nil, ParseOpt{
|
||||
FileRelativePaths: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
targets := map[string]*Target{}
|
||||
for _, t := range c.Targets {
|
||||
targets[t.Name] = t
|
||||
}
|
||||
|
||||
require.Equal(t, filepath.ToSlash(filepath.Clean("project/app")), *targets["app"].Context)
|
||||
require.Equal(t, filepath.ToSlash(filepath.Clean("project/shared")), targets["app"].Contexts["shared"])
|
||||
require.Equal(t, filepath.ToSlash(filepath.Clean("project/other")), *targets["other"].Context)
|
||||
}
|
||||
|
||||
func TestBuildArgEnvCompose(t *testing.T) {
|
||||
dt := []byte(`
|
||||
version: "3.8"
|
||||
|
||||
@@ -91,6 +91,10 @@ type WithGetName interface {
|
||||
GetName(ectx *hcl.EvalContext, block *hcl.Block, loadDeps func(hcl.Expression) hcl.Diagnostics) (string, error)
|
||||
}
|
||||
|
||||
type WithBlockSource interface {
|
||||
SetBlockSource(block *hcl.Block)
|
||||
}
|
||||
|
||||
// errUndefined is returned when a variable or function is not defined.
|
||||
type errUndefined struct{}
|
||||
|
||||
@@ -944,6 +948,10 @@ func Parse(b hcl.Body, opt Opt, val any) (*ParseMeta, hcl.Diagnostics) {
|
||||
|
||||
vvs := p.blockValues[b]
|
||||
for _, vv := range vvs {
|
||||
if v, ok := vv.Interface().(WithBlockSource); ok {
|
||||
v.SetBlockSource(b)
|
||||
}
|
||||
|
||||
t := types[b.Type]
|
||||
lblIndex, lblExists := getNameIndex(vv)
|
||||
lblName, _ := getName(vv)
|
||||
|
||||
+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,11 @@ 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 Bake file that defines
|
||||
each path. Compose files use the first Compose file directory as the base, which
|
||||
matches Compose project directory semantics. 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,14 @@ 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 Bake file that defines each path, set
|
||||
`BUILDX_BAKE_FILE_RELATIVE_PATHS=1`. Compose files use the first Compose file
|
||||
directory as the base, which matches Compose project directory semantics. 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:
|
||||
|
||||
+184
@@ -45,6 +45,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){
|
||||
testBakePrintRemoteContextSubdir,
|
||||
testBakeLocal,
|
||||
testBakeLocalMulti,
|
||||
testBakeFileRelativePaths,
|
||||
testBakeLocalExportDeleteMode,
|
||||
testBakeRemote,
|
||||
testBakeRemoteAuth,
|
||||
@@ -674,6 +675,189 @@ 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("compose project", func(t *testing.T) {
|
||||
composefile := []byte(`
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: ./app
|
||||
dockerfile_inline: |
|
||||
FROM scratch
|
||||
COPY marker /marker
|
||||
COPY --from=shared shared-marker /shared-marker
|
||||
`)
|
||||
overridefile := []byte(`
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
additional_contexts:
|
||||
shared: ./shared
|
||||
`)
|
||||
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateDir("project", 0700),
|
||||
fstest.CreateDir("project/app", 0700),
|
||||
fstest.CreateDir("project/shared", 0700),
|
||||
fstest.CreateDir("overrides", 0700),
|
||||
fstest.CreateFile("project/compose.yml", composefile, 0600),
|
||||
fstest.CreateFile("project/app/marker", []byte("marker"), 0600),
|
||||
fstest.CreateFile("project/shared/shared-marker", []byte("shared"), 0600),
|
||||
fstest.CreateFile("overrides/compose.yml", overridefile, 0600),
|
||||
)
|
||||
dirDest := t.TempDir()
|
||||
|
||||
out, err := bakeCmd(
|
||||
sb,
|
||||
withDir(dir),
|
||||
withArgs("--file", "project/compose.yml", "--file", "overrides/compose.yml", "--set", "app.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("default context", func(t *testing.T) {
|
||||
bakefile := []byte(`
|
||||
target "default" {
|
||||
dockerfile-inline = <<EOT
|
||||
FROM scratch
|
||||
COPY marker /marker
|
||||
EOT
|
||||
}
|
||||
`)
|
||||
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateDir("definitions", 0700),
|
||||
fstest.CreateFile("definitions/docker-bake.hcl", bakefile, 0600),
|
||||
fstest.CreateFile("definitions/marker", []byte("marker"), 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, "marker"))
|
||||
})
|
||||
|
||||
t.Run("hcl target reference", func(t *testing.T) {
|
||||
baseBakefile := []byte(`
|
||||
target "base" {
|
||||
context = "basectx"
|
||||
}
|
||||
`)
|
||||
appBakefile := []byte(`
|
||||
target "app" {
|
||||
context = target.base.context
|
||||
dockerfile-inline = <<EOT
|
||||
FROM scratch
|
||||
COPY marker /marker
|
||||
EOT
|
||||
}
|
||||
`)
|
||||
|
||||
dir := tmpdir(
|
||||
t,
|
||||
fstest.CreateDir("one", 0700),
|
||||
fstest.CreateDir("one/basectx", 0700),
|
||||
fstest.CreateDir("two", 0700),
|
||||
fstest.CreateDir("two/basectx", 0700),
|
||||
fstest.CreateFile("one/docker-bake.hcl", baseBakefile, 0600),
|
||||
fstest.CreateFile("one/basectx/marker", []byte("source-file"), 0600),
|
||||
fstest.CreateFile("two/docker-bake.hcl", appBakefile, 0600),
|
||||
fstest.CreateFile("two/basectx/marker", []byte("consumer-file"), 0600),
|
||||
)
|
||||
dirDest := t.TempDir()
|
||||
|
||||
out, err := bakeCmd(
|
||||
sb,
|
||||
withDir(dir),
|
||||
withArgs("--file", "one/docker-bake.hcl", "--file", "two/docker-bake.hcl", "--set", "app.output=type=local,dest="+dirDest, "app"),
|
||||
withEnv("BUILDX_BAKE_FILE_RELATIVE_PATHS=1"),
|
||||
)
|
||||
require.NoError(t, err, out)
|
||||
|
||||
dt, err := os.ReadFile(filepath.Join(dirDest, "marker"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "source-file", string(dt))
|
||||
})
|
||||
|
||||
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