Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 121 additions & 4 deletions bake/bake.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ type File struct {
Data []byte
}

type ParseOpt struct {
FileRelativePaths bool
}

type Override struct {
Value string
ArrValue []string
Expand Down Expand Up @@ -197,8 +201,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
}
Expand Down Expand Up @@ -337,11 +341,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
Expand Down Expand Up @@ -369,7 +375,7 @@ func ParseFiles(files []File, defaults, vars map[string]string) (_ *Config, _ *h
}

if len(composeFiles) > 0 {
cfg, cmperr := ParseComposeFiles(composeFiles, vars)
cfg, cmperr := parseComposeFilesWithBase(composeFiles, vars, frel)
if cmperr != nil {
return nil, nil, errors.Wrap(cmperr, "failed to parse compose file")
}
Expand Down Expand Up @@ -413,9 +419,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 filepath.ToSlash(filepath.Clean(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))
Expand Down Expand Up @@ -781,6 +842,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) {
Expand Down Expand Up @@ -829,10 +896,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()
Expand Down Expand Up @@ -863,8 +966,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
Expand All @@ -886,6 +995,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 {
Expand Down
163 changes: 163 additions & 0 deletions bake/bake_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,169 @@ func TestHCLDockerfileCwdPrefix(t *testing.T) {
assert.Equal(t, ".", bo["app"].Inputs.ContextPath)
}

func TestFileRelativePaths(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 TestFileRelativePathsDefaultContext(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 TestFileRelativePathsUseDefinitionFile(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 TestFileRelativePathsDoesNotRebaseOverrides(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 TestComposeFileRelativePaths(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",
Expand Down
Loading
Loading