cli/command/container: add create/run --umask

Signed-off-by: Youfu Zhang <zhangyoufu@gmail.com>
This commit is contained in:
Youfu Zhang
2026-09-04 03:23:44 +08:00
parent 51780c0691
commit 09d30a34bf
9 changed files with 197 additions and 0 deletions
+5
View File
@@ -139,6 +139,7 @@ type containerOptions struct {
runtime string
autoRemove bool
init bool
umask opts.UmaskOpt
annotations *opts.MapOpts
Image string
@@ -313,6 +314,9 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
flags.Var(&copts.shmSize, "shm-size", "Size of /dev/shm")
flags.StringVar(&copts.utsMode, "uts", "", "UTS namespace to use")
flags.StringVar(&copts.runtime, "runtime", "", "Runtime to use for this container")
flags.Var(&copts.umask, "umask", "Set umask for the container")
flags.SetAnnotation("umask", "version", []string{"1.56"})
flags.SetAnnotation("umask", "ostype", []string{"linux"})
flags.BoolVar(&copts.init, "init", false, "Run an init inside the container that forwards signals and reaps processes")
flags.SetAnnotation("init", "version", []string{"1.25"})
@@ -710,6 +714,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
MaskedPaths: maskedPaths,
ReadonlyPaths: readonlyPaths,
Annotations: copts.annotations.GetAll(),
Umask: copts.umask.Value(),
}
if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() {
+13
View File
@@ -154,6 +154,19 @@ func TestParseRunLinks(t *testing.T) {
}
}
func TestParseRunWithoutUmask(t *testing.T) {
_, hostConfig, _, err := parseRun([]string{"ubuntu", "bash"})
assert.NilError(t, err)
assert.Assert(t, hostConfig.Umask == nil)
}
func TestParseRunUmask(t *testing.T) {
_, hostConfig, _, err := parseRun([]string{"--umask", "0022", "ubuntu", "bash"})
assert.NilError(t, err)
assert.Assert(t, hostConfig.Umask != nil)
assert.Equal(t, uint32(0o22), *hostConfig.Umask)
}
func TestParseRunAttach(t *testing.T) {
tests := []struct {
input string
@@ -102,6 +102,7 @@ Create a new container
| `--tmpfs` | `list` | | Mount a tmpfs directory |
| `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY |
| `--ulimit` | `ulimit` | | Ulimit options |
| `--umask` | `umask` | `<nil>` | Set umask for the container |
| `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
| `--userns` | `string` | | User namespace to use |
@@ -105,6 +105,7 @@ Create and run a new container from an image
| [`--tmpfs`](#tmpfs) | `list` | | Mount a tmpfs directory |
| [`-t`](#tty), [`--tty`](#tty) | `bool` | | Allocate a pseudo-TTY |
| [`--ulimit`](#ulimit) | `ulimit` | | Ulimit options |
| [`--umask`](#umask) | `umask` | `<nil>` | Set umask for the container |
| `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
| [`--userns`](#userns) | `string` | | User namespace to use |
@@ -1392,6 +1393,56 @@ The 4th container fails and reports a "[8] System error: resource temporarily un
This fails because the caller set `nproc=3` resulting in the first three containers using up
the three processes quota set for the `daemon` user.
### <a name="umask"></a> Set umask for the container (--umask)
The `--umask` flag sets the umask for the container's processes, which
controls the default permissions for files and directories created inside
the container. The value must be specified in octal notation. Leading zeros
are optional. For example, a umask of `022` creates new files with `644`
permissions (`-rw-r--r--`) and new directories with `755` permissions
(`drwxr-xr-x`).
If you don't set the `--umask` flag, the OCI runtime applies its own
default umask. The default OCI runtime (runc) uses a default umask of
`0022`, but this value is implementation-defined and may vary between OCI
runtimes:
```console
$ docker run --rm busybox sh -c umask
0022
```
When the `--umask` flag is set, the value is included in the OCI process
configuration used for the container's entrypoint, for processes started
with `docker exec`, and for healthchecks. Whether an OCI runtime honors the
value depends on the runtime; runc honors the configured umask for
`docker exec` and for healthchecks.
To set a custom umask, use the `--umask` flag with an octal value:
```console
$ docker run --rm --umask 077 busybox sh -c umask
0077
```
The umask is also applied to processes started later with `docker exec`:
```console
$ docker run --rm -d --name umask-test --umask 077 busybox sleep 60
$ docker exec umask-test sh -c umask
0077
```
Setting the umask to `0` masks no permission bits, so files and directories
keep their full permissions. The following example creates a file and a
directory inside the container:
```console
$ docker run --rm --umask 0 busybox sh -c 'touch file && mkdir dir && stat -c "%a %n" file dir'
666 file
777 dir
```
### <a name="stop-signal"></a> Stop container with signal (--stop-signal)
The `--stop-signal` flag sends the system call signal to the
+1
View File
@@ -102,6 +102,7 @@ Create a new container
| `--tmpfs` | `list` | | Mount a tmpfs directory |
| `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY |
| `--ulimit` | `ulimit` | | Ulimit options |
| `--umask` | `umask` | `<nil>` | Set umask for the container |
| `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
| `--userns` | `string` | | User namespace to use |
+1
View File
@@ -105,6 +105,7 @@ Create and run a new container from an image
| `--tmpfs` | `list` | | Mount a tmpfs directory |
| `-t`, `--tty` | `bool` | | Allocate a pseudo-TTY |
| `--ulimit` | `ulimit` | | Ulimit options |
| `--umask` | `umask` | `<nil>` | Set umask for the container |
| `--use-api-socket` | `bool` | | Bind mount Docker API socket and required auth |
| `-u`, `--user` | `string` | | Username or UID (format: <name\|uid>[:<group\|gid>]) |
| `--userns` | `string` | | User namespace to use |
+25
View File
@@ -110,6 +110,31 @@ func TestRunWithCgroupNamespace(t *testing.T) {
result.Assert(t, icmd.Success)
}
func TestRunUmask(t *testing.T) {
environment.SkipIfDaemonNotLinux(t)
testCases := []struct {
name string
args []string
expected string
}{
{name: "unset", expected: "0022\n"},
{name: "zero", args: []string{"--umask", "0"}, expected: "0000\n"},
{name: "octal-022", args: []string{"--umask", "022"}, expected: "0022\n"},
{name: "octal-777", args: []string{"--umask", "777"}, expected: "0777\n"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
args := []string{"run", "--rm", fixtures.AlpineImage, "sh", "-c", "umask"}
args = append(args[:2], append(tc.args, args[2:]...)...)
result := icmd.RunCommand("docker", args...)
result.Assert(t, icmd.Success)
assert.Equal(t, result.Stdout(), tc.expected)
})
}
}
func TestMountSubvolume(t *testing.T) {
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.45"))
volName := "test-volume-" + t.Name()
+37
View File
@@ -11,6 +11,7 @@ import (
"net"
"path"
"slices"
"strconv"
"strings"
"github.com/docker/cli/internal/lazyregexp"
@@ -477,3 +478,39 @@ func (m *MemSwapBytes) UnmarshalJSON(s []byte) error {
b := MemBytes(*m)
return b.UnmarshalJSON(s)
}
// UmaskOpt is a type for umask values in octal format
type UmaskOpt struct {
ptr *uint32
}
// Set sets the value of the UmaskOpt by passing a string in octal format
func (u *UmaskOpt) Set(s string) error {
v, err := strconv.ParseUint(s, 8, 32)
if err != nil {
return err
}
if u.ptr == nil {
u.ptr = new(uint32)
}
*u.ptr = uint32(v)
return nil
}
// Type returns the type
func (*UmaskOpt) Type() string {
return "umask"
}
// Value returns the uint32 ptr
func (u *UmaskOpt) Value() *uint32 {
return u.ptr
}
// String returns the umask value in octal format, or "<nil>" if the pointer is nil.
func (u *UmaskOpt) String() string {
if u.ptr == nil {
return "<nil>"
}
return fmt.Sprintf("%#04o", uint64(*u.ptr))
}
+63
View File
@@ -428,3 +428,66 @@ func TestParseCPUsReturnZeroOnInvalidValues(t *testing.T) {
resValue, _ = ParseCPUs("1e-32")
assert.Equal(t, z1, resValue)
}
func TestUmaskOpt(t *testing.T) {
t.Run("type", func(t *testing.T) {
var opt UmaskOpt
assert.Equal(t, opt.Type(), "umask")
})
t.Run("nil by default", func(t *testing.T) {
var opt UmaskOpt
assert.Assert(t, opt.Value() == nil)
assert.Equal(t, opt.String(), "<nil>")
})
t.Run("rejects empty string", func(t *testing.T) {
var opt UmaskOpt
err := opt.Set("")
assert.Assert(t, err != nil)
assert.Assert(t, opt.Value() == nil)
assert.Equal(t, opt.String(), "<nil>")
})
t.Run("parses zero", func(t *testing.T) {
var opt UmaskOpt
err := opt.Set("0")
assert.NilError(t, err)
assert.Equal(t, *opt.Value(), uint32(0))
assert.Equal(t, opt.String(), "0000")
})
t.Run("parses valid octal values", func(t *testing.T) {
var opt UmaskOpt
err := opt.Set("022")
assert.NilError(t, err)
assert.Assert(t, opt.Value() != nil)
assert.Equal(t, *opt.Value(), uint32(0o22))
assert.Equal(t, opt.String(), "0022")
})
t.Run("rejects octal values with 0o prefix", func(t *testing.T) {
var opt UmaskOpt
err := opt.Set("0o22")
assert.Assert(t, err != nil)
assert.Assert(t, opt.Value() == nil)
assert.Equal(t, opt.String(), "<nil>")
})
t.Run("parses large octal values", func(t *testing.T) {
var opt UmaskOpt
err := opt.Set("077777")
assert.NilError(t, err)
assert.Assert(t, opt.Value() != nil)
assert.Equal(t, *opt.Value(), uint32(0o77777))
assert.Equal(t, opt.String(), "077777")
})
t.Run("rejects invalid octal values", func(t *testing.T) {
var opt UmaskOpt
err := opt.Set("9")
assert.Assert(t, err != nil)
assert.Assert(t, opt.Value() == nil)
assert.Equal(t, opt.String(), "<nil>")
})
}