diff --git a/frontend/dockerfile/dockerfile2llb/convert.go b/frontend/dockerfile/dockerfile2llb/convert.go index 552d09e6f7b0..28c7efcaec20 100644 --- a/frontend/dockerfile/dockerfile2llb/convert.go +++ b/frontend/dockerfile/dockerfile2llb/convert.go @@ -7,6 +7,7 @@ import ( "fmt" "math" "net/url" + "os" "path" "path/filepath" "sort" @@ -493,7 +494,7 @@ func dispatch(d *dispatchState, cmd command, opt dispatchOpt) error { case *instructions.WorkdirCommand: err = dispatchWorkdir(d, c, true, &opt) case *instructions.AddCommand: - err = dispatchCopy(d, c.SourcesAndDest, opt.buildContext, true, c, c.Chown, c.Location(), opt) + err = dispatchCopy(d, c.SourcesAndDest, opt.buildContext, true, c, c.Chown, c.Chmod, c.Location(), opt) if err == nil { for _, src := range c.Sources() { if !strings.HasPrefix(src, "http://") && !strings.HasPrefix(src, "https://") { @@ -528,7 +529,7 @@ func dispatch(d *dispatchState, cmd command, opt dispatchOpt) error { if len(cmd.sources) != 0 { l = cmd.sources[0].state } - err = dispatchCopy(d, c.SourcesAndDest, l, false, c, c.Chown, c.Location(), opt) + err = dispatchCopy(d, c.SourcesAndDest, l, false, c, c.Chown, c.Chmod, c.Location(), opt) if err == nil && len(cmd.sources) == 0 { for _, src := range c.Sources() { d.ctxPaths[path.Join("/", filepath.ToSlash(src))] = struct{}{} @@ -722,7 +723,7 @@ func dispatchWorkdir(d *dispatchState, c *instructions.WorkdirCommand, commit bo return nil } -func dispatchCopyFileOp(d *dispatchState, c instructions.SourcesAndDest, sourceState llb.State, isAddCommand bool, cmdToPrint fmt.Stringer, chown string, loc []parser.Range, opt dispatchOpt) error { +func dispatchCopyFileOp(d *dispatchState, c instructions.SourcesAndDest, sourceState llb.State, isAddCommand bool, cmdToPrint fmt.Stringer, chown string, chmod string, loc []parser.Range, opt dispatchOpt) error { pp, err := pathRelativeToWorkingDir(d.state, c.Dest()) if err != nil { return err @@ -738,6 +739,15 @@ func dispatchCopyFileOp(d *dispatchState, c instructions.SourcesAndDest, sourceS copyOpt = append(copyOpt, llb.WithUser(chown)) } + var mode *os.FileMode + if chmod != "" { + p, err := strconv.ParseUint(chmod, 8, 32) + if err == nil { + perm := os.FileMode(p) + mode = &perm + } + } + commitMessage := bytes.NewBufferString("") if isAddCommand { commitMessage.WriteString("ADD") @@ -780,6 +790,7 @@ func dispatchCopyFileOp(d *dispatchState, c instructions.SourcesAndDest, sourceS } } else { opts := append([]llb.CopyOption{&llb.CopyInfo{ + Mode: mode, FollowSymlinks: true, CopyDirContentsOnly: true, AttemptUnpack: isAddCommand, @@ -820,9 +831,16 @@ func dispatchCopyFileOp(d *dispatchState, c instructions.SourcesAndDest, sourceS return commitToHistory(&d.image, commitMessage.String(), true, &d.state) } -func dispatchCopy(d *dispatchState, c instructions.SourcesAndDest, sourceState llb.State, isAddCommand bool, cmdToPrint fmt.Stringer, chown string, loc []parser.Range, opt dispatchOpt) error { +func dispatchCopy(d *dispatchState, c instructions.SourcesAndDest, sourceState llb.State, isAddCommand bool, cmdToPrint fmt.Stringer, chown string, chmod string, loc []parser.Range, opt dispatchOpt) error { if useFileOp(opt.buildArgValues, opt.llbCaps) { - return dispatchCopyFileOp(d, c, sourceState, isAddCommand, cmdToPrint, chown, loc, opt) + return dispatchCopyFileOp(d, c, sourceState, isAddCommand, cmdToPrint, chown, chmod, loc, opt) + } + + if chmod != "" { + if opt.llbCaps != nil && opt.llbCaps.Supports(pb.CapFileBase) != nil { + return errors.Wrap(opt.llbCaps.Supports(pb.CapFileBase), "chmod is not supported") + } + return errors.New("chmod is not supported") } img := llb.Image(opt.copyImage, llb.MarkImageInternal, llb.Platform(opt.buildPlatforms[0]), WithInternalName("helper image for file operations")) diff --git a/frontend/dockerfile/dockerfile_test.go b/frontend/dockerfile/dockerfile_test.go index 6dd7ca243213..9e6911090027 100644 --- a/frontend/dockerfile/dockerfile_test.go +++ b/frontend/dockerfile/dockerfile_test.go @@ -113,6 +113,7 @@ var fileOpTests = []integration.Test{ testNoSnapshotLeak, testCopySymlinks, testCopyChown, + testCopyChmod, testCopyOverrideFiles, testCopyVarSubstitution, testCopyWildcards, @@ -2914,6 +2915,77 @@ COPY --from=base /out / require.Equal(t, "1000 nogroup\n", string(dt)) } +func testCopyChmod(t *testing.T, sb integration.Sandbox) { + f := getFrontend(t, sb) + isFileOp := getFileOp(t, sb) + + dockerfile := []byte(` +FROM busybox AS base + +RUN mkdir -m 0777 /out +COPY --chmod=0644 foo / +COPY --chmod=777 bar /baz +COPY --chmod=0 foo /foobis + +RUN stat -c "%04a" /foo > /out/fooperm +RUN stat -c "%04a" /baz > /out/barperm +RUN stat -c "%04a" /foobis > /out/foobisperm +FROM scratch +COPY --from=base /out / +`) + + dir, err := tmpdir( + fstest.CreateFile("Dockerfile", dockerfile, 0600), + fstest.CreateFile("foo", []byte(`foo-contents`), 0600), + fstest.CreateFile("bar", []byte(`bar-contents`), 0700), + ) + + require.NoError(t, err) + defer os.RemoveAll(dir) + + c, err := client.New(context.TODO(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + destDir, err := ioutil.TempDir("", "buildkit") + require.NoError(t, err) + defer os.RemoveAll(destDir) + + _, err = f.Solve(context.TODO(), c, client.SolveOpt{ + Exports: []client.ExportEntry{ + { + Type: client.ExporterLocal, + OutputDir: destDir, + }, + }, + FrontendAttrs: map[string]string{ + "build-arg:BUILDKIT_DISABLE_FILEOP": strconv.FormatBool(!isFileOp), + }, + LocalDirs: map[string]string{ + builder.DefaultLocalNameDockerfile: dir, + builder.DefaultLocalNameContext: dir, + }, + }, nil) + + if !isFileOp { + require.Contains(t, err.Error(), "chmod is not supported") + return + } + require.NoError(t, err) + + dt, err := ioutil.ReadFile(filepath.Join(destDir, "fooperm")) + require.NoError(t, err) + require.Equal(t, "0644\n", string(dt)) + + dt, err = ioutil.ReadFile(filepath.Join(destDir, "barperm")) + require.NoError(t, err) + require.Equal(t, "0777\n", string(dt)) + + dt, err = ioutil.ReadFile(filepath.Join(destDir, "foobisperm")) + require.NoError(t, err) + require.Equal(t, "0000\n", string(dt)) +} + func testCopyOverrideFiles(t *testing.T, sb integration.Sandbox) { f := getFrontend(t, sb) isFileOp := getFileOp(t, sb) diff --git a/frontend/dockerfile/instructions/commands.go b/frontend/dockerfile/instructions/commands.go index 2580c9602798..4a4146a43344 100644 --- a/frontend/dockerfile/instructions/commands.go +++ b/frontend/dockerfile/instructions/commands.go @@ -188,6 +188,7 @@ type AddCommand struct { withNameAndCode SourcesAndDest Chown string + Chmod string } // Expand variables @@ -209,6 +210,7 @@ type CopyCommand struct { SourcesAndDest From string Chown string + Chmod string } // Expand variables diff --git a/frontend/dockerfile/instructions/parse.go b/frontend/dockerfile/instructions/parse.go index 93c9767cbb80..1be9d7b2999d 100644 --- a/frontend/dockerfile/instructions/parse.go +++ b/frontend/dockerfile/instructions/parse.go @@ -239,6 +239,7 @@ func parseAdd(req parseRequest) (*AddCommand, error) { return nil, errNoDestinationArgument("ADD") } flChown := req.flags.AddString("chown", "") + flChmod := req.flags.AddString("chmod", "") if err := req.flags.Parse(); err != nil { return nil, err } @@ -246,6 +247,7 @@ func parseAdd(req parseRequest) (*AddCommand, error) { SourcesAndDest: SourcesAndDest(req.args), withNameAndCode: newWithNameAndCode(req), Chown: flChown.Value, + Chmod: flChmod.Value, }, nil } @@ -255,6 +257,7 @@ func parseCopy(req parseRequest) (*CopyCommand, error) { } flChown := req.flags.AddString("chown", "") flFrom := req.flags.AddString("from", "") + flChmod := req.flags.AddString("chmod", "") if err := req.flags.Parse(); err != nil { return nil, err } @@ -263,6 +266,7 @@ func parseCopy(req parseRequest) (*CopyCommand, error) { From: flFrom.Value, withNameAndCode: newWithNameAndCode(req), Chown: flChown.Value, + Chmod: flChmod.Value, }, nil } diff --git a/solver/llbsolver/file/backend.go b/solver/llbsolver/file/backend.go index e23239f00e92..a690012287dc 100644 --- a/solver/llbsolver/file/backend.go +++ b/solver/llbsolver/file/backend.go @@ -294,6 +294,7 @@ func (fb *Backend) Mkfile(ctx context.Context, m, user, group fileoptypes.Mount, return mkfile(ctx, dir, action, u, mnt.m.IdentityMapping()) } + func (fb *Backend) Rm(ctx context.Context, m fileoptypes.Mount, action pb.FileActionRm) error { mnt, ok := m.(*Mount) if !ok { @@ -309,6 +310,7 @@ func (fb *Backend) Rm(ctx context.Context, m fileoptypes.Mount, action pb.FileAc return rm(ctx, dir, action) } + func (fb *Backend) Copy(ctx context.Context, m1, m2, user, group fileoptypes.Mount, action pb.FileActionCopy) error { mnt1, ok := m1.(*Mount) if !ok {