diff --git a/bake/bake.go b/bake/bake.go index dbe839b2f83..a2a5499c684 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -1044,6 +1044,9 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { if t.Dockerfile != nil { dockerfilePath = *t.Dockerfile } + if !strings.HasPrefix(dockerfilePath, "cwd://") { + dockerfilePath = path.Clean(dockerfilePath) + } bi := build.Inputs{ ContextPath: contextPath, @@ -1054,6 +1057,38 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { bi.DockerfileInline = *t.DockerfileInline } updateContext(&bi, inp) + if strings.HasPrefix(bi.DockerfilePath, "cwd://") { + // If Dockerfile is local for a remote invocation, we first check if + // it's not outside the working directory and then resolve it to an + // absolute path. + bi.DockerfilePath = path.Clean(strings.TrimPrefix(bi.DockerfilePath, "cwd://")) + if err := checkPath(bi.DockerfilePath); err != nil { + return nil, err + } + var err error + bi.DockerfilePath, err = filepath.Abs(bi.DockerfilePath) + if err != nil { + return nil, err + } + } else if !build.IsRemoteURL(bi.DockerfilePath) && strings.HasPrefix(bi.ContextPath, "cwd://") && (inp != nil && build.IsRemoteURL(inp.URL)) { + // We don't currently support reading a remote Dockerfile with a local + // context when doing a remote invocation because we automatically + // derive the dockerfile from the context atm: + // + // target "default" { + // context = BAKE_CMD_CONTEXT + // dockerfile = "Dockerfile.app" + // } + // + // > docker buildx bake https://github.com/foo/bar.git + // failed to solve: failed to read dockerfile: open /var/lib/docker/tmp/buildkit-mount3004544897/Dockerfile.app: no such file or directory + // + // To avoid mistakenly reading a local Dockerfile, we check if the + // Dockerfile exists locally and if so, we error out. + if _, err := os.Stat(filepath.Join(path.Clean(strings.TrimPrefix(bi.ContextPath, "cwd://")), bi.DockerfilePath)); err == nil { + return nil, errors.Errorf("reading a dockerfile for a remote build invocation is currently not supported") + } + } if strings.HasPrefix(bi.ContextPath, "cwd://") { bi.ContextPath = path.Clean(strings.TrimPrefix(bi.ContextPath, "cwd://")) } diff --git a/bake/bake_test.go b/bake/bake_test.go index a54e66c9230..37789c9467d 100644 --- a/bake/bake_test.go +++ b/bake/bake_test.go @@ -3,10 +3,12 @@ package bake import ( "context" "os" + "path/filepath" "sort" "strings" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -379,7 +381,7 @@ services: require.Equal(t, []string{"web_app"}, g["default"].Targets) } -func TestHCLCwdPrefix(t *testing.T) { +func TestHCLContextCwdPrefix(t *testing.T) { fp := File{ Name: "docker-bake.hcl", Data: []byte( @@ -400,11 +402,41 @@ func TestHCLCwdPrefix(t *testing.T) { require.Equal(t, 1, len(m)) require.Contains(t, m, "app") - require.Equal(t, "test", *m["app"].Dockerfile) - require.Equal(t, "foo", *m["app"].Context) + assert.Equal(t, "test", *m["app"].Dockerfile) + assert.Equal(t, "foo", *m["app"].Context) + assert.Equal(t, "foo/test", bo["app"].Inputs.DockerfilePath) + assert.Equal(t, "foo", bo["app"].Inputs.ContextPath) +} + +func TestHCLDockerfileCwdPrefix(t *testing.T) { + fp := File{ + Name: "docker-bake.hcl", + Data: []byte( + `target "app" { + context = "." + dockerfile = "cwd://Dockerfile.app" + }`), + } + ctx := context.TODO() + + cwd, err := os.Getwd() + require.NoError(t, err) - require.Equal(t, "foo/test", bo["app"].Inputs.DockerfilePath) - require.Equal(t, "foo", bo["app"].Inputs.ContextPath) + m, g, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil) + require.NoError(t, err) + + bo, err := TargetsToBuildOpt(m, &Input{}) + require.NoError(t, err) + + require.Equal(t, 1, len(g)) + require.Equal(t, []string{"app"}, g["default"].Targets) + + require.Equal(t, 1, len(m)) + require.Contains(t, m, "app") + assert.Equal(t, "cwd://Dockerfile.app", *m["app"].Dockerfile) + assert.Equal(t, ".", *m["app"].Context) + assert.Equal(t, filepath.Join(cwd, "Dockerfile.app"), bo["app"].Inputs.DockerfilePath) + assert.Equal(t, ".", bo["app"].Inputs.ContextPath) } func TestOverrideMerge(t *testing.T) { diff --git a/build/build.go b/build/build.go index 083b1da7b7e..93950c0e70a 100644 --- a/build/build.go +++ b/build/build.go @@ -1332,6 +1332,10 @@ func LoadInputs(ctx context.Context, d *driver.DriverHandle, inp Inputs, pw prog case IsRemoteURL(inp.ContextPath): if inp.DockerfilePath == "-" { dockerfileReader = inp.InStream + } else if filepath.IsAbs(inp.DockerfilePath) { + dockerfileDir = filepath.Dir(inp.DockerfilePath) + dockerfileName = filepath.Base(inp.DockerfilePath) + target.FrontendAttrs["dockerfilekey"] = "dockerfile" } target.FrontendAttrs["context"] = inp.ContextPath default: diff --git a/tests/bake.go b/tests/bake.go index b76a8bb8a2f..33d534618be 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -1,6 +1,7 @@ package tests import ( + "os" "path/filepath" "testing" @@ -25,6 +26,8 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){ testBakeRemoteContextSubdir, testBakeRemoteCmdContextEscapeRoot, testBakeRemoteCmdContextEscapeRelative, + testBakeRemoteDockerfileCwd, + testBakeRemoteLocalContextRemoteDockerfile, } func testBakeLocal(t *testing.T, sb integration.Sandbox) { @@ -287,3 +290,101 @@ EOT require.NoError(t, err, out) require.FileExists(t, filepath.Join(dirDest, "foo")) } + +func testBakeRemoteDockerfileCwd(t *testing.T, sb integration.Sandbox) { + bakefile := []byte(` +target "default" { + context = "." + dockerfile = "cwd://Dockerfile.app" +} +`) + dockerfile := []byte(` +FROM scratch +COPY bar /bar + `) + dockerfileApp := []byte(` +FROM scratch +COPY foo /foo + `) + + dirSpec := tmpdir( + t, + fstest.CreateFile("docker-bake.hcl", bakefile, 0600), + fstest.CreateFile("Dockerfile", dockerfile, 0600), + fstest.CreateFile("foo", []byte("foo"), 0600), + fstest.CreateFile("bar", []byte("bar"), 0600), + ) + dirSrc := tmpdir( + t, + fstest.CreateFile("Dockerfile.app", dockerfileApp, 0600), + ) + dirDest := t.TempDir() + + git, err := gitutil.New(gitutil.WithWorkingDir(dirSpec)) + require.NoError(t, err) + + gitutil.GitInit(git, t) + gitutil.GitAdd(git, t, "docker-bake.hcl") + gitutil.GitAdd(git, t, "Dockerfile") + gitutil.GitAdd(git, t, "foo") + gitutil.GitAdd(git, t, "bar") + gitutil.GitCommit(git, t, "initial commit") + addr := gitutil.GitServeHTTP(git, t) + + out, err := bakeCmd( + sb, + withDir(dirSrc), + withArgs(addr, "--set", "*.output=type=local,dest="+dirDest), + ) + require.NoError(t, err, out) + require.FileExists(t, filepath.Join(dirDest, "foo")) + + err = os.Remove(filepath.Join(dirSrc, "Dockerfile.app")) + require.NoError(t, err) + + out, err = bakeCmd( + sb, + withDir(dirSrc), + withArgs(addr, "--set", "*.output=type=cacheonly"), + ) + require.Error(t, err, out) +} + +func testBakeRemoteLocalContextRemoteDockerfile(t *testing.T, sb integration.Sandbox) { + bakefile := []byte(` +target "default" { + context = BAKE_CMD_CONTEXT + dockerfile = "Dockerfile.app" +} +`) + dockerfileApp := []byte(` +FROM scratch +COPY foo /foo + `) + + dirSpec := tmpdir( + t, + fstest.CreateFile("docker-bake.hcl", bakefile, 0600), + ) + dirSrc := tmpdir( + t, + fstest.CreateFile("Dockerfile.app", dockerfileApp, 0600), + fstest.CreateFile("foo", []byte("foo"), 0600), + ) + + git, err := gitutil.New(gitutil.WithWorkingDir(dirSpec)) + require.NoError(t, err) + + gitutil.GitInit(git, t) + gitutil.GitAdd(git, t, "docker-bake.hcl") + gitutil.GitCommit(git, t, "initial commit") + addr := gitutil.GitServeHTTP(git, t) + + out, err := bakeCmd( + sb, + withDir(dirSrc), + withArgs(addr, "--set", "*.output=type=cacheonly"), + ) + require.Error(t, err, out) + require.Contains(t, out, "reading a dockerfile for a remote build invocation is currently not supported") +}