From 3f39050637d454e9ee8075153a917c8bfccb5bae Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Wed, 11 Feb 2015 14:21:38 -0500 Subject: [PATCH] Allow setting ulimits for containers Signed-off-by: Brian Goff --- daemon/config.go | 4 + daemon/container.go | 24 ++++ daemon/execdriver/driver.go | 17 +-- daemon/execdriver/native/create.go | 12 ++ daemon/start.go | 1 + .../reference/api/docker_remote_api.md | 7 +- .../reference/api/docker_remote_api_v1.18.md | 9 +- docs/sources/reference/commandline/cli.md | 26 +++++ integration-cli/docker_cli_daemon_test.go | 53 +++++++++ integration-cli/docker_cli_run_unix_test.go | 15 +++ opts/opts.go | 5 + opts/ulimit.go | 44 ++++++++ pkg/ulimit/ulimit.go | 106 ++++++++++++++++++ pkg/ulimit/ulimit_test.go | 41 +++++++ runconfig/hostconfig.go | 5 + runconfig/parse.go | 6 + 16 files changed, 365 insertions(+), 10 deletions(-) create mode 100644 opts/ulimit.go create mode 100644 pkg/ulimit/ulimit.go create mode 100644 pkg/ulimit/ulimit_test.go diff --git a/daemon/config.go b/daemon/config.go index 67806fce72..68db93ccfd 100644 --- a/daemon/config.go +++ b/daemon/config.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/daemon/networkdriver" "github.com/docker/docker/opts" flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/ulimit" ) const ( @@ -44,6 +45,7 @@ type Config struct { Context map[string][]string TrustKeyPath string Labels []string + Ulimits map[string]*ulimit.Ulimit } // InstallFlags adds command-line options to the top-level flag parser for @@ -75,6 +77,8 @@ func (config *Config) InstallFlags() { opts.IPListVar(&config.Dns, []string{"#dns", "-dns"}, "DNS server to use") opts.DnsSearchListVar(&config.DnsSearch, []string{"-dns-search"}, "DNS search domains to use") opts.LabelListVar(&config.Labels, []string{"-label"}, "Set key=value labels to the daemon") + config.Ulimits = make(map[string]*ulimit.Ulimit) + opts.UlimitMapVar(config.Ulimits, []string{"-default-ulimit"}, "Set default ulimits for containers") } func getDefaultNetworkMtu() int { diff --git a/daemon/container.go b/daemon/container.go index 676674bc2d..82b9fdb0b1 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -31,6 +31,7 @@ import ( "github.com/docker/docker/pkg/networkfs/resolvconf" "github.com/docker/docker/pkg/promise" "github.com/docker/docker/pkg/symlink" + "github.com/docker/docker/pkg/ulimit" "github.com/docker/docker/runconfig" "github.com/docker/docker/utils" ) @@ -276,11 +277,34 @@ func populateCommand(c *Container, env []string) error { return err } + var rlimits []*ulimit.Rlimit + ulimits := c.hostConfig.Ulimits + + // Merge ulimits with daemon defaults + ulIdx := make(map[string]*ulimit.Ulimit) + for _, ul := range ulimits { + ulIdx[ul.Name] = ul + } + for name, ul := range c.daemon.config.Ulimits { + if _, exists := ulIdx[name]; !exists { + ulimits = append(ulimits, ul) + } + } + + for _, limit := range ulimits { + rl, err := limit.GetRlimit() + if err != nil { + return err + } + rlimits = append(rlimits, rl) + } + resources := &execdriver.Resources{ Memory: c.Config.Memory, MemorySwap: c.Config.MemorySwap, CpuShares: c.Config.CpuShares, Cpuset: c.Config.Cpuset, + Rlimits: rlimits, } processConfig := execdriver.ProcessConfig{ diff --git a/daemon/execdriver/driver.go b/daemon/execdriver/driver.go index f1d7ca70ae..24bbc62416 100644 --- a/daemon/execdriver/driver.go +++ b/daemon/execdriver/driver.go @@ -2,14 +2,16 @@ package execdriver import ( "errors" - "github.com/docker/docker/daemon/execdriver/native/template" - "github.com/docker/libcontainer" - "github.com/docker/libcontainer/devices" "io" "os" "os/exec" "strings" "time" + + "github.com/docker/docker/daemon/execdriver/native/template" + "github.com/docker/docker/pkg/ulimit" + "github.com/docker/libcontainer" + "github.com/docker/libcontainer/devices" ) // Context is a generic key value pair that allows @@ -99,10 +101,11 @@ type NetworkInterface struct { } type Resources struct { - Memory int64 `json:"memory"` - MemorySwap int64 `json:"memory_swap"` - CpuShares int64 `json:"cpu_shares"` - Cpuset string `json:"cpuset"` + Memory int64 `json:"memory"` + MemorySwap int64 `json:"memory_swap"` + CpuShares int64 `json:"cpu_shares"` + Cpuset string `json:"cpuset"` + Rlimits []*ulimit.Rlimit `json:"rlimits"` } type ResourceStats struct { diff --git a/daemon/execdriver/native/create.go b/daemon/execdriver/native/create.go index e45074be3f..3442f66a00 100644 --- a/daemon/execdriver/native/create.go +++ b/daemon/execdriver/native/create.go @@ -58,6 +58,8 @@ func (d *driver) createContainer(c *execdriver.Command) (*libcontainer.Config, e return nil, err } + d.setupRlimits(container, c) + cmds := make(map[string]*exec.Cmd) d.Lock() for k, v := range d.activeContainers { @@ -172,6 +174,16 @@ func (d *driver) setCapabilities(container *libcontainer.Config, c *execdriver.C return err } +func (d *driver) setupRlimits(container *libcontainer.Config, c *execdriver.Command) { + if c.Resources == nil { + return + } + + for _, rlimit := range c.Resources.Rlimits { + container.Rlimits = append(container.Rlimits, libcontainer.Rlimit((*rlimit))) + } +} + func (d *driver) setupMounts(container *libcontainer.Config, c *execdriver.Command) error { for _, m := range c.Mounts { container.MountConfig.Mounts = append(container.MountConfig.Mounts, &mount.Mount{ diff --git a/daemon/start.go b/daemon/start.go index 4a35555dca..e5076d9128 100644 --- a/daemon/start.go +++ b/daemon/start.go @@ -74,6 +74,7 @@ func (daemon *Daemon) setHostConfig(container *Container, hostConfig *runconfig. if err := daemon.RegisterLinks(container, hostConfig); err != nil { return err } + container.hostConfig = hostConfig container.toDisk() diff --git a/docs/sources/reference/api/docker_remote_api.md b/docs/sources/reference/api/docker_remote_api.md index 4a8c699f00..4f844f4549 100644 --- a/docs/sources/reference/api/docker_remote_api.md +++ b/docs/sources/reference/api/docker_remote_api.md @@ -51,6 +51,12 @@ You can still call an old version of the API using **New!** This endpoint now returns `Os`, `Arch` and `KernelVersion`. +`POST /containers/create` +`POST /containers/(id)/start` + +**New!** +You can set ulimit settings to be used within the container. + ## v1.17 ### Full Documentation @@ -86,7 +92,6 @@ root filesystem as read only. **New!** This endpoint returns a live stream of a container's resource usage statistics. - ## v1.16 ### Full Documentation diff --git a/docs/sources/reference/api/docker_remote_api_v1.18.md b/docs/sources/reference/api/docker_remote_api_v1.18.md index 72d184115b..e43ad431e5 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.18.md +++ b/docs/sources/reference/api/docker_remote_api_v1.18.md @@ -155,7 +155,8 @@ Create a container "CapDrop": ["MKNOD"], "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, "NetworkMode": "bridge", - "Devices": [] + "Devices": [], + "Ulimits": [{}] } } @@ -244,6 +245,9 @@ Json Parameters: - **Devices** - A list of devices to add to the container specified in the form `{ "PathOnHost": "/dev/deviceName", "PathInContainer": "/dev/deviceName", "CgroupPermissions": "mrw"}` + - **Ulimits** - A list of ulimits to be set in the container, specified as + `{ "Name": , "Soft": , "Hard": }`, for example: + `Ulimits: { "Name": "nofile", "Soft": 1024, "Hard", 2048 }}` Query Parameters: @@ -337,7 +341,8 @@ Return low-level information on the container `id` "Name": "on-failure" }, "SecurityOpt": null, - "VolumesFrom": null + "VolumesFrom": null, + "Ulimits": [{}] }, "HostnamePath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hostname", "HostsPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hosts", diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 2f34c32926..7b611c8883 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -109,6 +109,7 @@ expect an integer, and they can only be specified once. --tlskey="~/.docker/key.pem" Path to TLS key file --tlsverify=false Use TLS and verify the remote -v, --version=false Print version information and quit + --default-ulimit=[] Set default ulimit settings for containers. Options with [] may be specified multiple times. @@ -404,6 +405,14 @@ This will only add the proxy and authentication to the Docker daemon's requests your `docker build`s and running containers will need extra configuration to use the proxy +### Default Ulimits + +`--default-ulimit` allows you to set the default `ulimit` options to use for all +containers. It takes the same options as `--ulimit` for `docker run`. If these +defaults are not set, `ulimit` settings will be inheritted, if not set on +`docker run`, from the Docker daemon. Any `--ulimit` options passed to +`docker run` will overwrite these defaults. + ### Miscellaneous options IP masquerading uses address translation to allow containers without a public IP to talk @@ -1974,6 +1983,23 @@ You can add other hosts into a container's `/etc/hosts` file by using one or mor > $ alias hostip="ip route show 0.0.0.0/0 | grep -Eo 'via \S+' | awk '{ print \$2 }'" > $ docker run --add-host=docker:$(hostip) --rm -it debian +### Setting ulimits in a container + +Since setting `ulimit` settings in a container requires extra privileges not +available in the default container, you can set these using the `--ulimit` flag. +`--ulimit` is specified with a soft and hard limit as such: +`=[:]`, for example: + +``` + $ docker run --ulimit nofile=1024:1024 --rm debian ulimit -n + 1024 +``` + +>**Note:** +> If you do not provide a `hard limit`, the `soft limit` will be used for both +values. If no `ulimits` are set, they will be inherited from the default `ulimits` +set on the daemon. + ## save Usage: docker save [OPTIONS] IMAGE [IMAGE...] diff --git a/integration-cli/docker_cli_daemon_test.go b/integration-cli/docker_cli_daemon_test.go index 0e56a2d438..302aade33e 100644 --- a/integration-cli/docker_cli_daemon_test.go +++ b/integration-cli/docker_cli_daemon_test.go @@ -480,3 +480,56 @@ func TestDaemonUpgradeWithVolumes(t *testing.T) { logDone("daemon - volumes from old(pre 1.3) daemon work") } + +func TestDaemonUlimitDefaults(t *testing.T) { + d := NewDaemon(t) + + if err := d.StartWithBusybox("--default-ulimit", "nofile=42:42", "--default-ulimit", "nproc=1024:1024"); err != nil { + t.Fatal(err) + } + + out, err := d.Cmd("run", "--ulimit", "nproc=2048", "--name=test", "busybox", "/bin/sh", "-c", "echo $(ulimit -n); echo $(ulimit -p)") + if err != nil { + t.Fatal(out, err) + } + + outArr := strings.Split(out, "\n") + if len(outArr) < 2 { + t.Fatal("got unexpected output: %s", out) + } + nofile := strings.TrimSpace(outArr[0]) + nproc := strings.TrimSpace(outArr[1]) + + if nofile != "42" { + t.Fatalf("expected `ulimit -n` to be `42`, got: %s", nofile) + } + if nproc != "2048" { + t.Fatalf("exepcted `ulimit -p` to be 2048, got: %s", nproc) + } + + // Now restart daemon with a new default + if err := d.Restart("--default-ulimit", "nofile=43"); err != nil { + t.Fatal(err) + } + + out, err = d.Cmd("start", "-a", "test") + if err != nil { + t.Fatal(err) + } + + outArr = strings.Split(out, "\n") + if len(outArr) < 2 { + t.Fatal("got unexpected output: %s", out) + } + nofile = strings.TrimSpace(outArr[0]) + nproc = strings.TrimSpace(outArr[1]) + + if nofile != "43" { + t.Fatalf("expected `ulimit -n` to be `43`, got: %s", nofile) + } + if nproc != "2048" { + t.Fatalf("exepcted `ulimit -p` to be 2048, got: %s", nproc) + } + + logDone("daemon - default ulimits are applied") +} diff --git a/integration-cli/docker_cli_run_unix_test.go b/integration-cli/docker_cli_run_unix_test.go index c539554ecb..98abce7d0c 100644 --- a/integration-cli/docker_cli_run_unix_test.go +++ b/integration-cli/docker_cli_run_unix_test.go @@ -91,3 +91,18 @@ func TestRunWithVolumesIsRecursive(t *testing.T) { logDone("run - volumes are bind mounted recursively") } + +func TestRunWithUlimits(t *testing.T) { + defer deleteAllContainers() + out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "--name=testulimits", "--ulimit", "nofile=42", "busybox", "/bin/sh", "-c", "ulimit -n")) + if err != nil { + t.Fatal(err, out) + } + + ul := strings.TrimSpace(out) + if ul != "42" { + t.Fatalf("expected `ulimit -n` to be 42, got %s", ul) + } + + logDone("run - ulimits are set") +} diff --git a/opts/opts.go b/opts/opts.go index e2596983cf..6b42fa6871 100644 --- a/opts/opts.go +++ b/opts/opts.go @@ -11,6 +11,7 @@ import ( "github.com/docker/docker/api" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/pkg/ulimit" "github.com/docker/docker/utils" ) @@ -43,6 +44,10 @@ func LabelListVar(values *[]string, names []string, usage string) { flag.Var(newListOptsRef(values, ValidateLabel), names, usage) } +func UlimitMapVar(values map[string]*ulimit.Ulimit, names []string, usage string) { + flag.Var(NewUlimitOpt(values), names, usage) +} + // ListOpts type type ListOpts struct { values *[]string diff --git a/opts/ulimit.go b/opts/ulimit.go new file mode 100644 index 0000000000..361eadf220 --- /dev/null +++ b/opts/ulimit.go @@ -0,0 +1,44 @@ +package opts + +import ( + "fmt" + + "github.com/docker/docker/pkg/ulimit" +) + +type UlimitOpt struct { + values map[string]*ulimit.Ulimit +} + +func NewUlimitOpt(ref map[string]*ulimit.Ulimit) *UlimitOpt { + return &UlimitOpt{ref} +} + +func (o *UlimitOpt) Set(val string) error { + l, err := ulimit.Parse(val) + if err != nil { + return err + } + + o.values[l.Name] = l + + return nil +} + +func (o *UlimitOpt) String() string { + var out []string + for _, v := range o.values { + out = append(out, v.String()) + } + + return fmt.Sprintf("%v", out) +} + +func (o *UlimitOpt) GetList() []*ulimit.Ulimit { + var ulimits []*ulimit.Ulimit + for _, v := range o.values { + ulimits = append(ulimits, v) + } + + return ulimits +} diff --git a/pkg/ulimit/ulimit.go b/pkg/ulimit/ulimit.go new file mode 100644 index 0000000000..2375315e3e --- /dev/null +++ b/pkg/ulimit/ulimit.go @@ -0,0 +1,106 @@ +package ulimit + +import ( + "fmt" + "strconv" + "strings" +) + +// Human friendly version of Rlimit +type Ulimit struct { + Name string + Hard int64 + Soft int64 +} + +type Rlimit struct { + Type int `json:"type,omitempty"` + Hard uint64 `json:"hard,omitempty"` + Soft uint64 `json:"soft,omitempty"` +} + +const ( + // magic numbers for making the syscall + // some of these are defined in the syscall package, but not all. + // Also since Windows client doesn't get access to the syscall package, need to + // define these here + RLIMIT_AS = 9 + RLIMIT_CORE = 4 + RLIMIT_CPU = 0 + RLIMIT_DATA = 2 + RLIMIT_FSIZE = 1 + RLIMIT_LOCKS = 10 + RLIMIT_MEMLOCK = 8 + RLIMIT_MSGQUEUE = 12 + RLIMIT_NICE = 13 + RLIMIT_NOFILE = 7 + RLIMIT_NPROC = 6 + RLIMIT_RSS = 5 + RLIMIT_RTPRIO = 14 + RLIMIT_RTTIME = 15 + RLIMIT_SIGPENDING = 11 + RLIMIT_STACK = 3 +) + +var ulimitNameMapping = map[string]int{ + //"as": RLIMIT_AS, // Disbaled since this doesn't seem usable with the way Docker inits a container. + "core": RLIMIT_CORE, + "cpu": RLIMIT_CPU, + "data": RLIMIT_DATA, + "fsize": RLIMIT_FSIZE, + "locks": RLIMIT_LOCKS, + "memlock": RLIMIT_MEMLOCK, + "msgqueue": RLIMIT_MSGQUEUE, + "nice": RLIMIT_NICE, + "nofile": RLIMIT_NOFILE, + "nproc": RLIMIT_NPROC, + "rss": RLIMIT_RSS, + "rtprio": RLIMIT_RTPRIO, + "rttime": RLIMIT_RTTIME, + "sigpending": RLIMIT_SIGPENDING, + "stack": RLIMIT_STACK, +} + +func Parse(val string) (*Ulimit, error) { + parts := strings.SplitN(val, "=", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid ulimit argument: %s", val) + } + + if _, exists := ulimitNameMapping[parts[0]]; !exists { + return nil, fmt.Errorf("invalid ulimit type: %s", parts[0]) + } + + limitVals := strings.SplitN(parts[1], ":", 2) + if len(limitVals) > 2 { + return nil, fmt.Errorf("too many limit value arguments - %s, can only have up to two, `soft[:hard]`", parts[1]) + } + + soft, err := strconv.ParseInt(limitVals[0], 10, 64) + if err != nil { + return nil, err + } + + hard := soft // in case no hard was set + if len(limitVals) == 2 { + hard, err = strconv.ParseInt(limitVals[1], 10, 64) + } + if soft > hard { + return nil, fmt.Errorf("ulimit soft limit must be less than or equal to hard limit: %d > %d", soft, hard) + } + + return &Ulimit{Name: parts[0], Soft: soft, Hard: hard}, nil +} + +func (u *Ulimit) GetRlimit() (*Rlimit, error) { + t, exists := ulimitNameMapping[u.Name] + if !exists { + return nil, fmt.Errorf("invalid ulimit name %s", u.Name) + } + + return &Rlimit{Type: t, Soft: uint64(u.Soft), Hard: uint64(u.Hard)}, nil +} + +func (u *Ulimit) String() string { + return fmt.Sprintf("%s=%s:%s", u.Name, u.Soft, u.Hard) +} diff --git a/pkg/ulimit/ulimit_test.go b/pkg/ulimit/ulimit_test.go new file mode 100644 index 0000000000..419b5e0407 --- /dev/null +++ b/pkg/ulimit/ulimit_test.go @@ -0,0 +1,41 @@ +package ulimit + +import "testing" + +func TestParseInvalidLimitType(t *testing.T) { + if _, err := Parse("notarealtype=1024:1024"); err == nil { + t.Fatalf("expected error on invalid ulimit type") + } +} + +func TestParseBadFormat(t *testing.T) { + if _, err := Parse("nofile:1024:1024"); err == nil { + t.Fatal("expected error on bad syntax") + } + + if _, err := Parse("nofile"); err == nil { + t.Fatal("expected error on bad syntax") + } + + if _, err := Parse("nofile="); err == nil { + t.Fatal("expected error on bad syntax") + } + if _, err := Parse("nofile=:"); err == nil { + t.Fatal("expected error on bad syntax") + } + if _, err := Parse("nofile=:1024"); err == nil { + t.Fatal("expected error on bad syntax") + } +} + +func TestParseHardLessThanSoft(t *testing.T) { + if _, err := Parse("nofile:1024:1"); err == nil { + t.Fatal("expected error on hard limit less than soft limit") + } +} + +func TestParseInvalidValueType(t *testing.T) { + if _, err := Parse("nofile:asdf"); err == nil { + t.Fatal("expected error on bad value type") + } +} diff --git a/runconfig/hostconfig.go b/runconfig/hostconfig.go index 3aff582f92..85db438b7d 100644 --- a/runconfig/hostconfig.go +++ b/runconfig/hostconfig.go @@ -5,6 +5,7 @@ import ( "github.com/docker/docker/engine" "github.com/docker/docker/nat" + "github.com/docker/docker/pkg/ulimit" "github.com/docker/docker/utils" ) @@ -119,6 +120,7 @@ type HostConfig struct { RestartPolicy RestartPolicy SecurityOpt []string ReadonlyRootfs bool + Ulimits []*ulimit.Ulimit } // This is used by the create command when you want to set both the @@ -156,6 +158,9 @@ func ContainerHostConfigFromJob(job *engine.Job) *HostConfig { job.GetenvJson("PortBindings", &hostConfig.PortBindings) job.GetenvJson("Devices", &hostConfig.Devices) job.GetenvJson("RestartPolicy", &hostConfig.RestartPolicy) + + job.GetenvJson("Ulimits", &hostConfig.Ulimits) + hostConfig.SecurityOpt = job.GetenvList("SecurityOpt") if Binds := job.GetenvList("Binds"); Binds != nil { hostConfig.Binds = Binds diff --git a/runconfig/parse.go b/runconfig/parse.go index 9e64f54436..76b3bb6265 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -10,6 +10,7 @@ import ( "github.com/docker/docker/opts" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/pkg/ulimit" "github.com/docker/docker/pkg/units" "github.com/docker/docker/utils" ) @@ -32,6 +33,9 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe flEnv = opts.NewListOpts(opts.ValidateEnv) flDevices = opts.NewListOpts(opts.ValidatePath) + ulimits = make(map[string]*ulimit.Ulimit) + flUlimits = opts.NewUlimitOpt(ulimits) + flPublish = opts.NewListOpts(nil) flExpose = opts.NewListOpts(nil) flDns = opts.NewListOpts(opts.ValidateIPAddress) @@ -82,6 +86,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe cmd.Var(&flCapAdd, []string{"-cap-add"}, "Add Linux capabilities") cmd.Var(&flCapDrop, []string{"-cap-drop"}, "Drop Linux capabilities") cmd.Var(&flSecurityOpt, []string{"-security-opt"}, "Security Options") + cmd.Var(flUlimits, []string{"-ulimit"}, "Ulimit options") cmd.Require(flag.Min, 1) @@ -309,6 +314,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe RestartPolicy: restartPolicy, SecurityOpt: flSecurityOpt.GetAll(), ReadonlyRootfs: *flReadonlyRootfs, + Ulimits: flUlimits.GetList(), } // When allocating stdin in attached mode, close stdin at client disconnect