Skip to content

Commit cbf6120

Browse files
committed
cli/command/container: add create/run --umask
Signed-off-by: Youfu Zhang <zhangyoufu@gmail.com>
1 parent 717d7ee commit cbf6120

9 files changed

Lines changed: 197 additions & 0 deletions

File tree

‎cli/command/container/opts.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ type containerOptions struct {
139139
runtime string
140140
autoRemove bool
141141
init bool
142+
umask opts.UmaskOpt
142143
annotations *opts.MapOpts
143144

144145
Image string
@@ -313,6 +314,9 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
313314
flags.Var(&copts.shmSize, "shm-size", "Size of /dev/shm")
314315
flags.StringVar(&copts.utsMode, "uts", "", "UTS namespace to use")
315316
flags.StringVar(&copts.runtime, "runtime", "", "Runtime to use for this container")
317+
flags.Var(&copts.umask, "umask", "Set umask for the container")
318+
flags.SetAnnotation("umask", "version", []string{"1.56"})
319+
flags.SetAnnotation("umask", "ostype", []string{"linux"})
316320

317321
flags.BoolVar(&copts.init, "init", false, "Run an init inside the container that forwards signals and reaps processes")
318322
flags.SetAnnotation("init", "version", []string{"1.25"})
@@ -710,6 +714,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
710714
MaskedPaths: maskedPaths,
711715
ReadonlyPaths: readonlyPaths,
712716
Annotations: copts.annotations.GetAll(),
717+
Umask: copts.umask.Value(),
713718
}
714719

715720
if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() {

‎cli/command/container/opts_test.go‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,19 @@ func TestParseRunLinks(t *testing.T) {
154154
}
155155
}
156156

157+
func TestParseRunWithoutUmask(t *testing.T) {
158+
_, hostConfig, _, err := parseRun([]string{"ubuntu", "bash"})
159+
assert.NilError(t, err)
160+
assert.Assert(t, hostConfig.Umask == nil)
161+
}
162+
163+
func TestParseRunUmask(t *testing.T) {
164+
_, hostConfig, _, err := parseRun([]string{"--umask", "0022", "ubuntu", "bash"})
165+
assert.NilError(t, err)
166+
assert.Assert(t, hostConfig.Umask != nil)
167+
assert.Equal(t, uint32(0o22), *hostConfig.Umask)
168+
}
169+
157170
func TestParseRunAttach(t *testing.T) {
158171
tests := []struct {
159172
input string

‎docs/reference/commandline/container_create.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Create a new container
102102
| `--tmpfs` | `list` | | Mount a tmpfs directory |
103103
| `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY |
104104
| `--ulimit` | `ulimit` | | Ulimit options |
105+
| `--umask` | `umask` | `<nil>` | Set umask for the container |
105106
| `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
106107
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
107108
| `--userns` | `string` | | User namespace to use |

‎docs/reference/commandline/container_run.md‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ Create and run a new container from an image
105105
| [`--tmpfs`](#tmpfs) | `list` | | Mount a tmpfs directory |
106106
| [`-t`](#tty), [`--tty`](#tty) | `bool` | | Allocate a pseudo-TTY |
107107
| [`--ulimit`](#ulimit) | `ulimit` | | Ulimit options |
108+
| [`--umask`](#umask) | `umask` | `<nil>` | Set umask for the container |
108109
| `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
109110
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
110111
| [`--userns`](#userns) | `string` | | User namespace to use |
@@ -1392,6 +1393,56 @@ The 4th container fails and reports a "[8] System error: resource temporarily un
13921393
This fails because the caller set `nproc=3` resulting in the first three containers using up
13931394
the three processes quota set for the `daemon` user.
13941395

1396+
### <a name="umask"></a> Set umask for the container (--umask)
1397+
1398+
The `--umask` flag sets the umask for the container's processes, which
1399+
controls the default permissions for files and directories created inside
1400+
the container. The value must be specified in octal notation. Leading zeros
1401+
are optional. For example, a umask of `022` creates new files with `644`
1402+
permissions (`-rw-r--r--`) and new directories with `755` permissions
1403+
(`drwxr-xr-x`).
1404+
1405+
If you don't set the `--umask` flag, the OCI runtime applies its own
1406+
default umask. The default OCI runtime (runc) uses a default umask of
1407+
`0022`, but this value is implementation-defined and may vary between OCI
1408+
runtimes:
1409+
1410+
```console
1411+
$ docker run --rm busybox sh -c umask
1412+
0022
1413+
```
1414+
1415+
When the `--umask` flag is set, the value is included in the OCI process
1416+
configuration used for the container's entrypoint, for processes started
1417+
with `docker exec`, and for healthchecks. Whether an OCI runtime honors the
1418+
value depends on the runtime; runc honors the configured umask for
1419+
`docker exec` and for healthchecks.
1420+
1421+
To set a custom umask, use the `--umask` flag with an octal value:
1422+
1423+
```console
1424+
$ docker run --rm --umask 077 busybox sh -c umask
1425+
0077
1426+
```
1427+
1428+
The umask is also applied to processes started later with `docker exec`:
1429+
1430+
```console
1431+
$ docker run --rm -d --name umask-test --umask 077 busybox sleep 60
1432+
$ docker exec umask-test sh -c umask
1433+
0077
1434+
```
1435+
1436+
Setting the umask to `0` masks no permission bits, so files and directories
1437+
keep their full permissions. The following example creates a file and a
1438+
directory inside the container:
1439+
1440+
```console
1441+
$ docker run --rm --umask 0 busybox sh -c 'touch file && mkdir dir && stat -c "%a %n" file dir'
1442+
666 file
1443+
777 dir
1444+
```
1445+
13951446
### <a name="stop-signal"></a> Stop container with signal (--stop-signal)
13961447

13971448
The `--stop-signal` flag sends the system call signal to the

‎docs/reference/commandline/create.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Create a new container
102102
| `--tmpfs` | `list` | | Mount a tmpfs directory |
103103
| `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY |
104104
| `--ulimit` | `ulimit` | | Ulimit options |
105+
| `--umask` | `umask` | `<nil>` | Set umask for the container |
105106
| `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
106107
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
107108
| `--userns` | `string` | | User namespace to use |

‎docs/reference/commandline/run.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ Create and run a new container from an image
105105
| `--tmpfs` | `list` | | Mount a tmpfs directory |
106106
| `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY |
107107
| `--ulimit` | `ulimit` | | Ulimit options |
108+
| `--umask` | `umask` | `<nil>` | Set umask for the container |
108109
| `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
109110
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
110111
| `--userns` | `string` | | User namespace to use |

‎e2e/container/run_test.go‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,31 @@ func TestRunWithCgroupNamespace(t *testing.T) {
110110
result.Assert(t, icmd.Success)
111111
}
112112

113+
func TestRunUmask(t *testing.T) {
114+
environment.SkipIfDaemonNotLinux(t)
115+
116+
testCases := []struct {
117+
name string
118+
args []string
119+
expected string
120+
}{
121+
{name: "unset", expected: "0022\n"},
122+
{name: "zero", args: []string{"--umask", "0"}, expected: "0000\n"},
123+
{name: "octal-022", args: []string{"--umask", "022"}, expected: "0022\n"},
124+
{name: "octal-777", args: []string{"--umask", "777"}, expected: "0777\n"},
125+
}
126+
127+
for _, tc := range testCases {
128+
t.Run(tc.name, func(t *testing.T) {
129+
args := []string{"run", "--rm", fixtures.AlpineImage, "sh", "-c", "umask"}
130+
args = append(args[:2], append(tc.args, args[2:]...)...)
131+
result := icmd.RunCommand("docker", args...)
132+
result.Assert(t, icmd.Success)
133+
assert.Equal(t, result.Stdout(), tc.expected)
134+
})
135+
}
136+
}
137+
113138
func TestMountSubvolume(t *testing.T) {
114139
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.45"))
115140
volName := "test-volume-" + t.Name()

‎opts/opts.go‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net"
1212
"path"
1313
"slices"
14+
"strconv"
1415
"strings"
1516

1617
"github.com/docker/cli/internal/lazyregexp"
@@ -477,3 +478,39 @@ func (m *MemSwapBytes) UnmarshalJSON(s []byte) error {
477478
b := MemBytes(*m)
478479
return b.UnmarshalJSON(s)
479480
}
481+
482+
// UmaskOpt is a type for umask values in octal format
483+
type UmaskOpt struct {
484+
ptr *uint32
485+
}
486+
487+
// Set sets the value of the UmaskOpt by passing a string in octal format
488+
func (u *UmaskOpt) Set(s string) error {
489+
v, err := strconv.ParseUint(s, 8, 32)
490+
if err != nil {
491+
return err
492+
}
493+
if u.ptr == nil {
494+
u.ptr = new(uint32)
495+
}
496+
*u.ptr = uint32(v)
497+
return nil
498+
}
499+
500+
// Type returns the type
501+
func (*UmaskOpt) Type() string {
502+
return "umask"
503+
}
504+
505+
// Value returns the uint32 ptr
506+
func (u *UmaskOpt) Value() *uint32 {
507+
return u.ptr
508+
}
509+
510+
// String returns the umask value in octal format, or "<nil>" if the pointer is nil.
511+
func (u *UmaskOpt) String() string {
512+
if u.ptr == nil {
513+
return "<nil>"
514+
}
515+
return fmt.Sprintf("%#04o", uint64(*u.ptr))
516+
}

‎opts/opts_test.go‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,3 +428,66 @@ func TestParseCPUsReturnZeroOnInvalidValues(t *testing.T) {
428428
resValue, _ = ParseCPUs("1e-32")
429429
assert.Equal(t, z1, resValue)
430430
}
431+
432+
func TestUmaskOpt(t *testing.T) {
433+
t.Run("type", func(t *testing.T) {
434+
var opt UmaskOpt
435+
assert.Equal(t, opt.Type(), "umask")
436+
})
437+
438+
t.Run("nil by default", func(t *testing.T) {
439+
var opt UmaskOpt
440+
assert.Assert(t, opt.Value() == nil)
441+
assert.Equal(t, opt.String(), "<nil>")
442+
})
443+
444+
t.Run("rejects empty string", func(t *testing.T) {
445+
var opt UmaskOpt
446+
err := opt.Set("")
447+
assert.Assert(t, err != nil)vvvv
448+
assert.Assert(t, opt.Value() == nil)
449+
assert.Equal(t, opt.String(), "<nil>")
450+
})
451+
452+
t.Run("parses zero", func(t *testing.T) {
453+
var opt UmaskOpt
454+
err := opt.Set("0")
455+
assert.NilError(t, err)
456+
assert.Equal(t, *opt.Value(), uint32(0))
457+
assert.Equal(t, opt.String(), "0000")
458+
})
459+
460+
t.Run("parses valid octal values", func(t *testing.T) {
461+
var opt UmaskOpt
462+
err := opt.Set("022")
463+
assert.NilError(t, err)
464+
assert.Assert(t, opt.Value() != nil)
465+
assert.Equal(t, *opt.Value(), uint32(0o22))
466+
assert.Equal(t, opt.String(), "0022")
467+
})
468+
469+
t.Run("rejects octal values with 0o prefix", func(t *testing.T) {
470+
var opt UmaskOpt
471+
err := opt.Set("0o22")
472+
assert.Assert(t, err != nil)
473+
assert.Assert(t, opt.Value() == nil)
474+
assert.Equal(t, opt.String(), "<nil>")
475+
})
476+
477+
t.Run("parses large octal values", func(t *testing.T) {
478+
var opt UmaskOpt
479+
err := opt.Set("077777")
480+
assert.NilError(t, err)
481+
assert.Assert(t, opt.Value() != nil)
482+
assert.Equal(t, *opt.Value(), uint32(0o77777))
483+
assert.Equal(t, opt.String(), "077777")
484+
})
485+
486+
t.Run("rejects invalid octal values", func(t *testing.T) {
487+
var opt UmaskOpt
488+
err := opt.Set("9")
489+
assert.Assert(t, err != nil)
490+
assert.Assert(t, opt.Value() == nil)
491+
assert.Equal(t, opt.String(), "<nil>")
492+
})
493+
}

0 commit comments

Comments
 (0)