From e6ae89a45a699bd44f03517396777e34ec76018b Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Fri, 6 Feb 2015 09:33:01 -0500 Subject: [PATCH] Allow setting resource constrains for build Closes #10191 Allow `docker build` to set --cpu-shares, --cpuset, --memory, --memory-swap for all containers created by the build. Signed-off-by: Brian Goff --- api/client/commands.go | 31 ++++++++ api/server/server.go | 4 + builder/evaluator.go | 7 ++ builder/internals.go | 10 ++- builder/job.go | 8 ++ docs/man/docker-build.1.md | 5 ++ docs/man/docker-run.1.md | 2 +- .../reference/api/docker_remote_api.md | 7 +- .../reference/api/docker_remote_api_v1.18.md | 4 + docs/sources/reference/commandline/cli.md | 4 + integration-cli/docker_cli_build_test.go | 73 ++++++++++++++++++- 11 files changed, 151 insertions(+), 4 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index f4b3efb4fa..8b4ac3ba79 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -90,6 +90,10 @@ func (cli *DockerCli) CmdBuild(args ...string) error { forceRm := cmd.Bool([]string{"-force-rm"}, false, "Always remove intermediate containers") pull := cmd.Bool([]string{"-pull"}, false, "Always attempt to pull a newer version of the image") dockerfileName := cmd.String([]string{"f", "-file"}, "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") + flMemoryString := cmd.String([]string{"m", "-memory"}, "", "Memory limit") + flMemorySwap := cmd.String([]string{"-memory-swap"}, "", "Total memory (memory + swap), '-1' to disable swap") + flCpuShares := cmd.Int64([]string{"c", "-cpu-shares"}, 0, "CPU shares (relative weight)") + flCpuSetCpus := cmd.String([]string{"-cpuset-cpus"}, "", "CPUs in which to allow execution (0-3, 0,1)") cmd.Require(flag.Exact, 1) @@ -242,6 +246,28 @@ func (cli *DockerCli) CmdBuild(args ...string) error { Action: "Sending build context to Docker daemon", }) } + + var memory int64 + if *flMemoryString != "" { + parsedMemory, err := units.RAMInBytes(*flMemoryString) + if err != nil { + return err + } + memory = parsedMemory + } + + var memorySwap int64 + if *flMemorySwap != "" { + if *flMemorySwap == "-1" { + memorySwap = -1 + } else { + parsedMemorySwap, err := units.RAMInBytes(*flMemorySwap) + if err != nil { + return err + } + memorySwap = parsedMemorySwap + } + } // Send the build context v := &url.Values{} @@ -283,6 +309,11 @@ func (cli *DockerCli) CmdBuild(args ...string) error { v.Set("pull", "1") } + v.Set("cpusetcpus", *flCpuSetCpus) + v.Set("cpushares", strconv.FormatInt(*flCpuShares, 10)) + v.Set("memory", strconv.FormatInt(memory, 10)) + v.Set("memswap", strconv.FormatInt(memorySwap, 10)) + v.Set("dockerfile", *dockerfileName) cli.LoadConfigFile() diff --git a/api/server/server.go b/api/server/server.go index 45ff1a2cc7..d244d2a0ce 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -1082,6 +1082,10 @@ func postBuild(eng *engine.Engine, version version.Version, w http.ResponseWrite job.Setenv("forcerm", r.FormValue("forcerm")) job.SetenvJson("authConfig", authConfig) job.SetenvJson("configFile", configFile) + job.Setenv("memswap", r.FormValue("memswap")) + job.Setenv("memory", r.FormValue("memory")) + job.Setenv("cpusetcpus", r.FormValue("cpusetcpus")) + job.Setenv("cpushares", r.FormValue("cpushares")) if err := job.Run(); err != nil { if !job.Stdout.Used() { diff --git a/builder/evaluator.go b/builder/evaluator.go index 9808a3c42d..985656f16a 100644 --- a/builder/evaluator.go +++ b/builder/evaluator.go @@ -125,6 +125,12 @@ type Builder struct { context tarsum.TarSum // the context is a tarball that is uploaded by the client contextPath string // the path of the temporary directory the local context is unpacked to (server side) noBaseImage bool // indicates that this build does not start from any base image, but is being built from an empty file system. + + // Set resource restrictions for build containers + cpuSetCpus string + cpuShares int64 + memory int64 + memorySwap int64 } // Run the builder with the context. This is the lynchpin of this package. This @@ -156,6 +162,7 @@ func (b *Builder) Run(context io.Reader) (string, error) { // some initializations that would not have been supplied by the caller. b.Config = &runconfig.Config{} + b.TmpContainers = map[string]struct{}{} for i, n := range b.dockerfile.Children { diff --git a/builder/internals.go b/builder/internals.go index 765284652a..67650f75bc 100644 --- a/builder/internals.go +++ b/builder/internals.go @@ -34,6 +34,7 @@ import ( "github.com/docker/docker/pkg/tarsum" "github.com/docker/docker/pkg/urlutil" "github.com/docker/docker/registry" + "github.com/docker/docker/runconfig" "github.com/docker/docker/utils" ) @@ -537,10 +538,17 @@ func (b *Builder) create() (*daemon.Container, error) { } b.Config.Image = b.image + hostConfig := &runconfig.HostConfig{ + CpuShares: b.cpuShares, + CpusetCpus: b.cpuSetCpus, + Memory: b.memory, + MemorySwap: b.memorySwap, + } + config := *b.Config // Create the container - c, warnings, err := b.Daemon.Create(b.Config, nil, "") + c, warnings, err := b.Daemon.Create(b.Config, hostConfig, "") if err != nil { return nil, err } diff --git a/builder/job.go b/builder/job.go index fb629e1c20..27591129cd 100644 --- a/builder/job.go +++ b/builder/job.go @@ -57,6 +57,10 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) engine.Status { rm = job.GetenvBool("rm") forceRm = job.GetenvBool("forcerm") pull = job.GetenvBool("pull") + memory = job.GetenvInt64("memory") + memorySwap = job.GetenvInt64("memswap") + cpuShares = job.GetenvInt64("cpushares") + cpuSetCpus = job.Getenv("cpusetcpus") authConfig = ®istry.AuthConfig{} configFile = ®istry.ConfigFile{} tag string @@ -145,6 +149,10 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) engine.Status { AuthConfig: authConfig, AuthConfigFile: configFile, dockerfileName: dockerfileName, + cpuShares: cpuShares, + cpuSetCpus: cpuSetCpus, + memory: memory, + memorySwap: memorySwap, } id, err := builder.Run(context) diff --git a/docs/man/docker-build.1.md b/docs/man/docker-build.1.md index 3a9472b236..fe6250fc19 100644 --- a/docs/man/docker-build.1.md +++ b/docs/man/docker-build.1.md @@ -14,6 +14,11 @@ docker-build - Build a new image from the source code at PATH [**-q**|**--quiet**[=*false*]] [**--rm**[=*true*]] [**-t**|**--tag**[=*TAG*]] +[**-m**|**--memory**[=*MEMORY*]] +[**--memory-swap**[=*MEMORY-SWAP*]] +[**-c**|**--cpu-shares**[=*0*]] +[**--cpuset-cpus**[=*CPUSET-CPUS*]] + PATH | URL | - # DESCRIPTION diff --git a/docs/man/docker-run.1.md b/docs/man/docker-run.1.md index 234d8dc522..7e8187744f 100644 --- a/docs/man/docker-run.1.md +++ b/docs/man/docker-run.1.md @@ -31,7 +31,7 @@ docker-run - Run a command in a new container [**--lxc-conf**[=*[]*]] [**--log-driver**[=*[]*]] [**-m**|**--memory**[=*MEMORY*]] -[**--memory-swap**[=*MEMORY-SWAP]] +[**--memory-swap**[=*MEMORY-SWAP*]] [**--mac-address**[=*MAC-ADDRESS*]] [**--name**[=*NAME*]] [**--net**[=*"bridge"*]] diff --git a/docs/sources/reference/api/docker_remote_api.md b/docs/sources/reference/api/docker_remote_api.md index 9ae67783ed..b4286d1fa2 100644 --- a/docs/sources/reference/api/docker_remote_api.md +++ b/docs/sources/reference/api/docker_remote_api.md @@ -60,13 +60,18 @@ You can set ulimit settings to be used within the container. `GET /info` **New!** -This endpoint now returns `SystemTime`, `HttpProxy`,`HttpsProxy` and `NoProxy`. +This endpoint now returns `SystemTime`, `HttpProxy`,`HttpsProxy` and `NoProxy`. `GET /images/json` **New!** Added a `RepoDigests` field to include image digest information. +`POST /build` + +**New!** +Builds can now set resource constraints for all containers created for the build. + ## v1.17 ### 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 ac22563b7f..6518247c7e 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.18.md +++ b/docs/sources/reference/api/docker_remote_api_v1.18.md @@ -1156,6 +1156,10 @@ Query Parameters: - **pull** - attempt to pull the image even if an older image exists locally - **rm** - remove intermediate containers after a successful build (default behavior) - **forcerm** - always remove intermediate containers (includes rm) +- **memory** - set memory limit for build +- **memswap** - Total memory (memory + swap), `-1` to disable swap +- **cpushares** - CPU shares (relative weight) +- **cpusetcpus** - CPUs in which to allow exection, e.g., `0-3`, `0,1` Request Headers: diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 7a89426a98..4afb95197f 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -515,6 +515,10 @@ is returned by the `docker attach` command to its caller too: -q, --quiet=false Suppress the verbose output generated by the containers --rm=true Remove intermediate containers after a successful build -t, --tag="" Repository name (and optionally a tag) for the image + -m, --memory="" Memory limit for all build containers + --memory-swap="" Total memory (memory + swap), `-1` to disable swap + -c, --cpu-shares CPU Shares (relative weight) + --cpuset-cpus="" CPUs in which to allow exection, e.g. `0-3`, `0,1` Builds Docker images from a Dockerfile and a "context". A build's context is the files located in the specified `PATH` or `URL`. The build process can diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 1c3c070cd4..3647616726 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -4455,7 +4455,7 @@ func TestBuildExoticShellInterpolation(t *testing.T) { _, err := buildImage(name, ` FROM busybox - + ENV SOME_VAR a.b.c RUN [ "$SOME_VAR" = 'a.b.c' ] @@ -5276,3 +5276,74 @@ RUN [ "/hello" ]`, map[string]string{}) logDone("build - RUN with one JSON arg") } + +func TestBuildResourceConstraintsAreUsed(t *testing.T) { + name := "testbuildresourceconstraints" + defer deleteAllContainers() + defer deleteImages(name) + + ctx, err := fakeContext(` + FROM hello-world:frozen + RUN ["/hello"] + `, map[string]string{}) + if err != nil { + t.Fatal(err) + } + + cmd := exec.Command(dockerBinary, "build", "--rm=false", "--memory=64m", "--memory-swap=-1", "--cpuset-cpus=1", "--cpu-shares=100", "-t", name, ".") + cmd.Dir = ctx.Dir + + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatal(err, out) + } + out, _, err = dockerCmd(t, "ps", "-lq") + if err != nil { + t.Fatal(err, out) + } + + cID := stripTrailingCharacters(out) + + type hostConfig struct { + Memory float64 // Use float64 here since the json decoder sees it that way + MemorySwap int + CpusetCpus string + CpuShares int + } + + cfg, err := inspectFieldJSON(cID, "HostConfig") + if err != nil { + t.Fatal(err) + } + + var c1 hostConfig + if err := json.Unmarshal([]byte(cfg), &c1); err != nil { + t.Fatal(err, cfg) + } + mem := int64(c1.Memory) + if mem != 67108864 || c1.MemorySwap != -1 || c1.CpusetCpus != "1" || c1.CpuShares != 100 { + t.Fatalf("resource constraints not set properly:\nMemory: %d, MemSwap: %d, CpusetCpus: %s, CpuShares: %d", + mem, c1.MemorySwap, c1.CpusetCpus, c1.CpuShares) + } + + // Make sure constraints aren't saved to image + _, _, err = dockerCmd(t, "run", "--name=test", name) + if err != nil { + t.Fatal(err) + } + cfg, err = inspectFieldJSON("test", "HostConfig") + if err != nil { + t.Fatal(err) + } + var c2 hostConfig + if err := json.Unmarshal([]byte(cfg), &c2); err != nil { + t.Fatal(err, cfg) + } + mem = int64(c2.Memory) + if mem == 67108864 || c2.MemorySwap == -1 || c2.CpusetCpus == "1" || c2.CpuShares == 100 { + t.Fatalf("resource constraints leaked from build:\nMemory: %d, MemSwap: %d, CpusetCpus: %s, CpuShares: %d", + mem, c2.MemorySwap, c2.CpusetCpus, c2.CpuShares) + } + + logDone("build - resource constraints applied") +}