Skip to content

Commit

Permalink
Add support for build-time environment variables to the 'build' API
Browse files Browse the repository at this point in the history
A build-time environment variable gets used only while processing
the 'RUN' primitive of a DockerFile. Such a variable is only
accessible during 'RUN' and is 'not' persisted with the intermediate
and final docker images, thereby addressing the portability concerns
of the images generated with 'build'

Signed-off-by: Madhav Puri <madhav.puri@gmail.com>
  • Loading branch information
mapuri committed Dec 4, 2014
1 parent e15ffa4 commit d5fb15c
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 0 deletions.
22 changes: 22 additions & 0 deletions api/client/commands.go
Expand Up @@ -83,6 +83,11 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
rm := cmd.Bool([]string{"#rm", "-rm"}, true, "Remove intermediate containers after a successful build")
forceRm := cmd.Bool([]string{"-force-rm"}, false, "Always remove intermediate containers, even after unsuccessful builds")
pull := cmd.Bool([]string{"-pull"}, false, "Always attempt to pull a newer version of the image")
flEnv := opts.NewListOpts(opts.ValidateEnv)
flEnvFile := opts.NewListOpts(nil)
cmd.Var(&flEnv, []string{"e", "-env"}, "Set build-time environment variables")
cmd.Var(&flEnvFile, []string{"-env-file"}, "Read in a line delimited file of build-time environment variables")

if err := cmd.Parse(args); err != nil {
return nil
}
Expand Down Expand Up @@ -231,6 +236,23 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
}
headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf))

// collect all the environment variables for the container
envVariables := []string{}
for _, ef := range flEnvFile.GetAll() {
parsedVars, err := opts.ParseEnvFile(ef)
if err != nil {
return err
}
envVariables = append(envVariables, parsedVars...)
}
// parse the '-e' and '--env' after, to allow override the env-file
envVariables = append(envVariables, flEnv.GetAll()...)
buf, err = json.Marshal(envVariables)
if err != nil {
return err
}
headers.Add("X-BuildEnv", base64.URLEncoding.EncodeToString(buf))

if context != nil {
headers.Set("Content-Type", "application/tar")
}
Expand Down
10 changes: 10 additions & 0 deletions api/server/server.go
Expand Up @@ -987,6 +987,8 @@ func postBuild(eng *engine.Engine, version version.Version, w http.ResponseWrite
authConfig = &registry.AuthConfig{}
configFileEncoded = r.Header.Get("X-Registry-Config")
configFile = &registry.ConfigFile{}
buildEnvEncoded = r.Header.Get("X-BuildEnv")
buildEnv = []string{}
job = eng.Job("build")
)

Expand All @@ -1012,6 +1014,13 @@ func postBuild(eng *engine.Engine, version version.Version, w http.ResponseWrite
}
}

if buildEnvEncoded != "" {
buildEnvJson := base64.NewDecoder(base64.URLEncoding, strings.NewReader(buildEnvEncoded))
if err := json.NewDecoder(buildEnvJson).Decode(&buildEnv); err != nil {
return err
}
}

if version.GreaterThanOrEqualTo("1.8") {
job.SetenvBool("json", true)
streamJSON(job, w, true)
Expand All @@ -1037,6 +1046,7 @@ 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.SetenvList("buildEnv", buildEnv)

if err := job.Run(); err != nil {
if !job.Stdout.Used() {
Expand Down
8 changes: 8 additions & 0 deletions builder/dispatchers.go
Expand Up @@ -216,10 +216,16 @@ func run(b *Builder, args []string, attributes map[string]bool, original string)
// set Cmd manually, this is special case only for Dockerfiles
b.Config.Cmd = config.Cmd
runconfig.Merge(b.Config, config)
// set build-time environment for 'run'. We let dockerfile
// environment override build-time environment.
env := b.Config.Env
b.Config.Env = append(b.BuildEnv, b.Config.Env...)

defer func(cmd []string) { b.Config.Cmd = cmd }(cmd)
defer func(env []string) { b.Config.Env = env }(env)

log.Debugf("[BUILDER] Command to be executed: %v", b.Config.Cmd)
log.Debugf("[BUILDER] Environment (applied in order): %v", b.Config.Env)

hit, err := b.probeCache()
if err != nil {
Expand All @@ -243,6 +249,8 @@ func run(b *Builder, args []string, attributes map[string]bool, original string)
if err != nil {
return err
}
// revert to original config, we don't persist build time environment
b.Config.Env = env
if err := b.commit(c.ID, cmd, "run"); err != nil {
return err
}
Expand Down
3 changes: 3 additions & 0 deletions builder/evaluator.go
Expand Up @@ -101,6 +101,9 @@ type Builder struct {

Config *runconfig.Config // runconfig for cmd, run, entrypoint etc.

// build-time environment variables for 'run'. These are not persisted with final or intermediate build images.
BuildEnv []string

// both of these are controlled by the Remove and ForceRemove options in BuildOpts
TmpContainers map[string]struct{} // a map of containers used for removes

Expand Down
3 changes: 3 additions & 0 deletions builder/job.go
Expand Up @@ -39,11 +39,13 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) engine.Status {
pull = job.GetenvBool("pull")
authConfig = &registry.AuthConfig{}
configFile = &registry.ConfigFile{}
buildEnv = []string{}
tag string
context io.ReadCloser
)
job.GetenvJson("authConfig", authConfig)
job.GetenvJson("configFile", configFile)
buildEnv = job.GetenvList("buildEnv")

repoName, tag = parsers.ParseRepositoryTag(repoName)
if repoName != "" {
Expand Down Expand Up @@ -118,6 +120,7 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) engine.Status {
StreamFormatter: sf,
AuthConfig: authConfig,
AuthConfigFile: configFile,
BuildEnv: buildEnv,
}

id, err := builder.Run(context)
Expand Down
15 changes: 15 additions & 0 deletions docs/man/docker-build.1.md
Expand Up @@ -6,6 +6,8 @@ docker-build - Build a new image from the source code at PATH

# SYNOPSIS
**docker build**
[**-e**|**--env**[=*[]*]]
[**--env-file**[=*[]*]]
[**--force-rm**[=*false*]]
[**--no-cache**[=*false*]]
[**-q**|**--quiet**[=*false*]]
Expand All @@ -30,6 +32,19 @@ When a Git repository is set as the **URL**, the repository is used
as context.

# OPTIONS
**-e**, **--env**=*environment*
Set build-time environment variables. This option allows you to specify
arbitrary environment variables that are available for the command(s) that will
be executed as part of 'RUN' primitive of Dockerfile. Such a variable is only
accessible during 'RUN' and is not persisted with the intermediate
and final docker images, keeping the generated image portable across the
actual deployment environments. This gives the flexibility to build
an image by passing host specific environment variables (like http_proxy) without
changing the Dockerfile.

**--env-file**=[]
Read in a line delimited file of build-time environment variables.

**--force-rm**=*true*|*false*
Always remove intermediate containers, even after unsuccessful builds. The default is *false*.

Expand Down
57 changes: 57 additions & 0 deletions integration-cli/docker_cli_build_test.go
Expand Up @@ -3522,3 +3522,60 @@ func TestBuildWithTabs(t *testing.T) {
}
logDone("build - with tabs")
}

func TestBuildBuildTimeEnv(t *testing.T) {
envKey := "foo"
envVal := "bar"
buildCmd := exec.Command(dockerBinary, "build", "-t", "bldenvtest", "-e",
fmt.Sprintf("%s=%s", envKey, envVal), "-")
buildCmd.Stdin = strings.NewReader(fmt.Sprintf("FROM busybox\n"+
"RUN echo $%s\n"+
"CMD echo $%s\n",
envKey, envKey))

if out, _, err := runCommandWithOutput(buildCmd); err != nil || !strings.Contains(out, envVal) {
if err != nil {
t.Fatalf("build failed to complete: %v %v", out, err)
}
defer func() { deleteImages("bldenvtest") }()
t.Fatalf("failed to access environment variable in output: '%v' "+
"expected: '%v'", out, envVal)
}

runCmd := exec.Command(dockerBinary, "run", "bldenvtest")
if out, _, err := runCommandWithOutput(runCmd); out != "\n" || err != nil {
t.Fatalf("run produced invalid output: '%q', expected '%q'", out, "")
}

logDone("build - build an image with build time environment variables")
}

func TestBuildBuildTimeEnvOverride(t *testing.T) {
envKey := "foo"
envVal := "bar"
envValOveride := "bar-override"
buildCmd := exec.Command(dockerBinary, "build", "-t", "bldenvtest", "-e",
fmt.Sprintf("%s=%s", envKey, envVal), "-")
buildCmd.Stdin = strings.NewReader(fmt.Sprintf("FROM busybox\n"+
"ENV %s %s \n"+
"RUN echo $%s\n"+
"CMD echo $%s\n",
envKey, envValOveride,
envKey, envKey))

if out, _, err := runCommandWithOutput(buildCmd); err != nil || !strings.Contains(out, envValOveride) {
if err != nil {
t.Fatalf("build failed to complete: %v %v", out, err)
}
defer func() { deleteImages("bldenvtest") }()
t.Fatalf("failed to access environment variable in output: '%v' "+
"expected: '%v'", out, envValOveride)
}

runCmd := exec.Command(dockerBinary, "run", "bldenvtest")
if out, _, err := runCommandWithOutput(runCmd); !strings.Contains(out, envValOveride) || err != nil {
t.Fatalf("run produced invalid output: '%q', expected '%q'", out, envValOveride)
}

logDone("build - build an image with build time environment variables override")
}

0 comments on commit d5fb15c

Please sign in to comment.