diff --git a/bake/bake.go b/bake/bake.go index 66ef0a1d55b4..e50fda4dcc5b 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -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,64 @@ 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) { + for _, t := range c.Targets { + 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 { + contextPath := rebaseContextPath(t.defaultContextBase, ".") + t.Context = &contextPath + } + 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)) @@ -781,6 +846,12 @@ type Target struct { // linked is a private field to mark a target used as a linked one linked bool + + defaultContextBase string + hasDefaultContextBase bool + contextBase string + hasContextBase bool + contextsBase map[string]string } func (t *Target) MarshalJSON() ([]byte, error) { @@ -829,10 +900,46 @@ 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 _, ok := content.Attributes["context"]; ok { + t.contextBase = base + t.hasContextBase = true + } + 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 (t *Target) normalize() { t.Annotations = removeDupesStr(t.Annotations) t.Attest = t.Attest.Normalize() @@ -863,8 +970,14 @@ func (t *Target) normalize() { } func (t *Target) Merge(t2 *Target) { + if t2.hasDefaultContextBase { + t.defaultContextBase = t2.defaultContextBase + t.hasDefaultContextBase = true + } if t2.Context != nil { t.Context = t2.Context + t.contextBase = t2.contextBase + t.hasContextBase = t2.hasContextBase } if t2.Dockerfile != nil { t.Dockerfile = t2.Dockerfile @@ -886,6 +999,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 { diff --git a/bake/bake_test.go b/bake/bake_test.go index de3fb9636e06..3e3b516b11ee 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -763,6 +763,169 @@ 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 = <