From cb51681a6db4b7c62d91998ba3b1d3b98c09c61c Mon Sep 17 00:00:00 2001 From: Erik Hollensbe Date: Wed, 13 Aug 2014 03:07:41 -0700 Subject: [PATCH] builder: Fix handling of ENV references that reference themselves, plus tests. Docker-DCO-1.1-Signed-off-by: Erik Hollensbe (github: erikh) --- builder/builder.go | 19 +------ builder/evaluator/dispatchers.go | 65 ++++++++++++------------ builder/evaluator/evaluator.go | 22 ++++---- builder/evaluator/internals.go | 22 ++++---- builder/evaluator/support.go | 28 ++++++++-- builder/parser/line_parsers.go | 47 +++++++++-------- builder/parser/parser.go | 14 ++--- builder/parser/utils.go | 10 ++-- integration-cli/docker_cli_build_test.go | 9 +++- 9 files changed, 127 insertions(+), 109 deletions(-) diff --git a/builder/builder.go b/builder/builder.go index 1720b7b99f..d99d1ad9b6 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -2,7 +2,6 @@ package builder import ( "github.com/docker/docker/builder/evaluator" - "github.com/docker/docker/nat" "github.com/docker/docker/runconfig" ) @@ -10,25 +9,9 @@ import ( func NewBuilder(opts *evaluator.BuildOpts) *evaluator.BuildFile { return &evaluator.BuildFile{ Dockerfile: nil, - Env: evaluator.EnvMap{}, - Config: initRunConfig(), + Config: &runconfig.Config{}, Options: opts, TmpContainers: evaluator.UniqueMap{}, TmpImages: evaluator.UniqueMap{}, } } - -func initRunConfig() *runconfig.Config { - return &runconfig.Config{ - PortSpecs: []string{}, - // FIXME(erikh) this should be a type that lives in runconfig - ExposedPorts: map[nat.Port]struct{}{}, - Env: []string{}, - Cmd: []string{}, - - // FIXME(erikh) this should also be a type in runconfig - Volumes: map[string]struct{}{}, - Entrypoint: []string{"/bin/sh", "-c"}, - OnBuild: []string{}, - } -} diff --git a/builder/evaluator/dispatchers.go b/builder/evaluator/dispatchers.go index d05777981e..23e16b000e 100644 --- a/builder/evaluator/dispatchers.go +++ b/builder/evaluator/dispatchers.go @@ -13,12 +13,12 @@ import ( "strings" "github.com/docker/docker/nat" + "github.com/docker/docker/pkg/log" "github.com/docker/docker/runconfig" - "github.com/docker/docker/utils" ) // dispatch with no layer / parsing. This is effectively not a command. -func nullDispatch(b *BuildFile, args []string) error { +func nullDispatch(b *BuildFile, args []string, attributes map[string]bool) error { return nil } @@ -27,24 +27,28 @@ func nullDispatch(b *BuildFile, args []string) error { // Sets the environment variable foo to bar, also makes interpolation // in the dockerfile available from the next statement on via ${foo}. // -func env(b *BuildFile, args []string) error { +func env(b *BuildFile, args []string, attributes map[string]bool) error { if len(args) != 2 { return fmt.Errorf("ENV accepts two arguments") } - // the duplication here is intended to ease the replaceEnv() call's env - // handling. This routine gets much shorter with the denormalization here. - key := args[0] - b.Env[key] = args[1] - b.Config.Env = append(b.Config.Env, strings.Join([]string{key, b.Env[key]}, "=")) + fullEnv := fmt.Sprintf("%s=%s", args[0], args[1]) - return b.commit("", b.Config.Cmd, fmt.Sprintf("ENV %s=%s", key, b.Env[key])) + for i, envVar := range b.Config.Env { + envParts := strings.SplitN(envVar, "=", 2) + if args[0] == envParts[0] { + b.Config.Env[i] = fullEnv + return b.commit("", b.Config.Cmd, fmt.Sprintf("ENV %s", fullEnv)) + } + } + b.Config.Env = append(b.Config.Env, fullEnv) + return b.commit("", b.Config.Cmd, fmt.Sprintf("ENV %s", fullEnv)) } // MAINTAINER some text // // Sets the maintainer metadata. -func maintainer(b *BuildFile, args []string) error { +func maintainer(b *BuildFile, args []string, attributes map[string]bool) error { if len(args) != 1 { return fmt.Errorf("MAINTAINER requires only one argument") } @@ -58,7 +62,7 @@ func maintainer(b *BuildFile, args []string) error { // Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling // exist here. If you do not wish to have this automatic handling, use COPY. // -func add(b *BuildFile, args []string) error { +func add(b *BuildFile, args []string, attributes map[string]bool) error { if len(args) != 2 { return fmt.Errorf("ADD requires two arguments") } @@ -70,7 +74,7 @@ func add(b *BuildFile, args []string) error { // // Same as 'ADD' but without the tar and remote url handling. // -func dispatchCopy(b *BuildFile, args []string) error { +func dispatchCopy(b *BuildFile, args []string, attributes map[string]bool) error { if len(args) != 2 { return fmt.Errorf("COPY requires two arguments") } @@ -82,7 +86,7 @@ func dispatchCopy(b *BuildFile, args []string) error { // // This sets the image the dockerfile will build on top of. // -func from(b *BuildFile, args []string) error { +func from(b *BuildFile, args []string, attributes map[string]bool) error { if len(args) != 1 { return fmt.Errorf("FROM requires one argument") } @@ -114,7 +118,7 @@ func from(b *BuildFile, args []string) error { // special cases. search for 'OnBuild' in internals.go for additional special // cases. // -func onbuild(b *BuildFile, args []string) error { +func onbuild(b *BuildFile, args []string, attributes map[string]bool) error { triggerInstruction := strings.ToUpper(strings.TrimSpace(args[0])) switch triggerInstruction { case "ONBUILD": @@ -133,7 +137,7 @@ func onbuild(b *BuildFile, args []string) error { // // Set the working directory for future RUN/CMD/etc statements. // -func workdir(b *BuildFile, args []string) error { +func workdir(b *BuildFile, args []string, attributes map[string]bool) error { if len(args) != 1 { return fmt.Errorf("WORKDIR requires exactly one argument") } @@ -161,10 +165,8 @@ func workdir(b *BuildFile, args []string) error { // RUN echo hi # sh -c echo hi // RUN [ "echo", "hi" ] # echo hi // -func run(b *BuildFile, args []string) error { - if len(args) == 1 { // literal string command, not an exec array - args = append([]string{"/bin/sh", "-c"}, args[0]) - } +func run(b *BuildFile, args []string, attributes map[string]bool) error { + args = handleJsonArgs(args, attributes) if b.image == "" { return fmt.Errorf("Please provide a source image with `from` prior to run") @@ -182,7 +184,7 @@ func run(b *BuildFile, args []string) error { defer func(cmd []string) { b.Config.Cmd = cmd }(cmd) - utils.Debugf("Command to be executed: %v", b.Config.Cmd) + log.Debugf("Command to be executed: %v", b.Config.Cmd) hit, err := b.probeCache() if err != nil { @@ -196,6 +198,7 @@ func run(b *BuildFile, args []string) error { if err != nil { return err } + // Ensure that we keep the container mounted until the commit // to avoid unmounting and then mounting directly again c.Mount() @@ -217,12 +220,9 @@ func run(b *BuildFile, args []string) error { // Set the default command to run in the container (which may be empty). // Argument handling is the same as RUN. // -func cmd(b *BuildFile, args []string) error { - if len(args) < 2 { - args = append([]string{"/bin/sh", "-c"}, args...) - } +func cmd(b *BuildFile, args []string, attributes map[string]bool) error { + b.Config.Cmd = handleJsonArgs(args, attributes) - b.Config.Cmd = args if err := b.commit("", b.Config.Cmd, fmt.Sprintf("CMD %v", cmd)); err != nil { return err } @@ -239,14 +239,15 @@ func cmd(b *BuildFile, args []string) error { // Handles command processing similar to CMD and RUN, only b.Config.Entrypoint // is initialized at NewBuilder time instead of through argument parsing. // -func entrypoint(b *BuildFile, args []string) error { - b.Config.Entrypoint = args +func entrypoint(b *BuildFile, args []string, attributes map[string]bool) error { + b.Config.Entrypoint = handleJsonArgs(args, attributes) // if there is no cmd in current Dockerfile - cleanup cmd if !b.cmdSet { b.Config.Cmd = nil } - if err := b.commit("", b.Config.Cmd, fmt.Sprintf("ENTRYPOINT %v", entrypoint)); err != nil { + + if err := b.commit("", b.Config.Cmd, fmt.Sprintf("ENTRYPOINT %v", b.Config.Entrypoint)); err != nil { return err } return nil @@ -257,7 +258,7 @@ func entrypoint(b *BuildFile, args []string) error { // Expose ports for links and port mappings. This all ends up in // b.Config.ExposedPorts for runconfig. // -func expose(b *BuildFile, args []string) error { +func expose(b *BuildFile, args []string, attributes map[string]bool) error { portsTab := args if b.Config.ExposedPorts == nil { @@ -284,7 +285,7 @@ func expose(b *BuildFile, args []string) error { // Set the user to 'foo' for future commands and when running the // ENTRYPOINT/CMD at container run time. // -func user(b *BuildFile, args []string) error { +func user(b *BuildFile, args []string, attributes map[string]bool) error { if len(args) != 1 { return fmt.Errorf("USER requires exactly one argument") } @@ -298,7 +299,7 @@ func user(b *BuildFile, args []string) error { // Expose the volume /foo for use. Will also accept the JSON form, but either // way requires exactly one argument. // -func volume(b *BuildFile, args []string) error { +func volume(b *BuildFile, args []string, attributes map[string]bool) error { if len(args) != 1 { return fmt.Errorf("Volume cannot be empty") } @@ -318,6 +319,6 @@ func volume(b *BuildFile, args []string) error { } // INSERT is no longer accepted, but we still parse it. -func insert(b *BuildFile, args []string) error { +func insert(b *BuildFile, args []string, attributes map[string]bool) error { return fmt.Errorf("INSERT has been deprecated. Please use ADD instead") } diff --git a/builder/evaluator/evaluator.go b/builder/evaluator/evaluator.go index 2eb2ba8b36..dbf4e30839 100644 --- a/builder/evaluator/evaluator.go +++ b/builder/evaluator/evaluator.go @@ -38,17 +38,16 @@ import ( "github.com/docker/docker/utils" ) -type EnvMap map[string]string type UniqueMap map[string]struct{} var ( ErrDockerfileEmpty = errors.New("Dockerfile cannot be empty") ) -var evaluateTable map[string]func(*BuildFile, []string) error +var evaluateTable map[string]func(*BuildFile, []string, map[string]bool) error func init() { - evaluateTable = map[string]func(*BuildFile, []string) error{ + evaluateTable = map[string]func(*BuildFile, []string, map[string]bool) error{ "env": env, "maintainer": maintainer, "add": add, @@ -71,7 +70,6 @@ func init() { // processing as it evaluates the parsing result. type BuildFile struct { Dockerfile *parser.Node // the syntax tree of the dockerfile - Env EnvMap // map of environment variables Config *runconfig.Config // runconfig for cmd, run, entrypoint etc. Options *BuildOpts // see below @@ -152,7 +150,9 @@ func (b *BuildFile) Run(context io.Reader) (string, error) { b.clearTmp(b.TmpContainers) } return "", err - } else if b.Options.Remove { + } + fmt.Fprintf(b.Options.OutStream, " ---> %s\n", utils.TruncateID(b.image)) + if b.Options.Remove { b.clearTmp(b.TmpContainers) } } @@ -181,25 +181,29 @@ func (b *BuildFile) Run(context io.Reader) (string, error) { // features. func (b *BuildFile) dispatch(stepN int, ast *parser.Node) error { cmd := ast.Value + attrs := ast.Attributes strs := []string{} + msg := fmt.Sprintf("Step %d : %s", stepN, strings.ToUpper(cmd)) if cmd == "onbuild" { fmt.Fprintf(b.Options.OutStream, "%#v\n", ast.Next.Children[0].Value) ast = ast.Next.Children[0] - strs = append(strs, ast.Value) + strs = append(strs, b.replaceEnv(ast.Value)) + msg += " " + ast.Value } for ast.Next != nil { ast = ast.Next - strs = append(strs, replaceEnv(b, ast.Value)) + strs = append(strs, b.replaceEnv(ast.Value)) + msg += " " + ast.Value } - fmt.Fprintf(b.Options.OutStream, "Step %d : %s %s\n", stepN, strings.ToUpper(cmd), strings.Join(strs, " ")) + fmt.Fprintf(b.Options.OutStream, "%s\n", msg) // XXX yes, we skip any cmds that are not valid; the parser should have // picked these out already. if f, ok := evaluateTable[cmd]; ok { - return f(b, strs) + return f(b, strs, attrs) } return nil diff --git a/builder/evaluator/internals.go b/builder/evaluator/internals.go index 5ceb2f88a2..9519c683b3 100644 --- a/builder/evaluator/internals.go +++ b/builder/evaluator/internals.go @@ -21,12 +21,12 @@ import ( "github.com/docker/docker/archive" "github.com/docker/docker/daemon" imagepkg "github.com/docker/docker/image" + "github.com/docker/docker/pkg/log" "github.com/docker/docker/pkg/parsers" "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/pkg/system" "github.com/docker/docker/pkg/tarsum" "github.com/docker/docker/registry" - "github.com/docker/docker/runconfig" "github.com/docker/docker/utils" ) @@ -299,13 +299,15 @@ func (b *BuildFile) pullImage(name string) (*imagepkg.Image, error) { func (b *BuildFile) processImageFrom(img *imagepkg.Image) error { b.image = img.ID - b.Config = &runconfig.Config{} + if img.Config != nil { b.Config = img.Config } + if b.Config.Env == nil || len(b.Config.Env) == 0 { b.Config.Env = append(b.Config.Env, "PATH="+daemon.DefaultPathEnv) } + // Process ONBUILD triggers if they exist if nTriggers := len(b.Config.OnBuild); nTriggers != 0 { fmt.Fprintf(b.Options.ErrStream, "# Executing %d build triggers\n", nTriggers) @@ -332,7 +334,7 @@ func (b *BuildFile) processImageFrom(img *imagepkg.Image) error { // in this function. if f, ok := evaluateTable[strings.ToLower(stepInstruction)]; ok { - if err := f(b, splitStep[1:]); err != nil { + if err := f(b, splitStep[1:], nil); err != nil { return err } } else { @@ -354,11 +356,11 @@ func (b *BuildFile) probeCache() (bool, error) { return false, err } else if cache != nil { fmt.Fprintf(b.Options.OutStream, " ---> Using cache\n") - utils.Debugf("[BUILDER] Use cached version") + log.Debugf("[BUILDER] Use cached version") b.image = cache.ID return true, nil } else { - utils.Debugf("[BUILDER] Cache miss") + log.Debugf("[BUILDER] Cache miss") } } return false, nil @@ -423,19 +425,17 @@ func (b *BuildFile) run(c *daemon.Container) error { func (b *BuildFile) checkPathForAddition(orig string) error { origPath := path.Join(b.contextPath, orig) - if p, err := filepath.EvalSymlinks(origPath); err != nil { + origPath, err := filepath.EvalSymlinks(origPath) + if err != nil { if os.IsNotExist(err) { return fmt.Errorf("%s: no such file or directory", orig) } return err - } else { - origPath = p } if !strings.HasPrefix(origPath, b.contextPath) { return fmt.Errorf("Forbidden path outside the build context: %s (%s)", orig, origPath) } - _, err := os.Stat(origPath) - if err != nil { + if _, err := os.Stat(origPath); err != nil { if os.IsNotExist(err) { return fmt.Errorf("%s: no such file or directory", orig) } @@ -499,7 +499,7 @@ func (b *BuildFile) addContext(container *daemon.Container, orig, dest string, d if err := archive.UntarPath(origPath, tarDest); err == nil { return nil } else if err != io.EOF { - utils.Debugf("Couldn't untar %s to %s: %s", origPath, tarDest, err) + log.Debugf("Couldn't untar %s to %s: %s", origPath, tarDest, err) } } diff --git a/builder/evaluator/support.go b/builder/evaluator/support.go index 766fd0208a..b543676ecf 100644 --- a/builder/evaluator/support.go +++ b/builder/evaluator/support.go @@ -10,17 +10,37 @@ var ( ) // handle environment replacement. Used in dispatcher. -func replaceEnv(b *BuildFile, str string) string { +func (b *BuildFile) replaceEnv(str string) string { for _, match := range TOKEN_ENV_INTERPOLATION.FindAllString(str, -1) { match = match[strings.Index(match, "$"):] matchKey := strings.Trim(match, "${}") - for envKey, envValue := range b.Env { - if matchKey == envKey { - str = strings.Replace(str, match, envValue, -1) + for _, keyval := range b.Config.Env { + tmp := strings.SplitN(keyval, "=", 2) + if tmp[0] == matchKey { + str = strings.Replace(str, match, tmp[1], -1) } } } return str } + +func (b *BuildFile) FindEnvKey(key string) int { + for k, envVar := range b.Config.Env { + envParts := strings.SplitN(envVar, "=", 2) + if key == envParts[0] { + return k + } + } + return -1 +} + +func handleJsonArgs(args []string, attributes map[string]bool) []string { + if attributes != nil && attributes["json"] { + return args + } + + // literal string command, not an exec array + return append([]string{"/bin/sh", "-c", strings.Join(args, " ")}) +} diff --git a/builder/parser/line_parsers.go b/builder/parser/line_parsers.go index ff1f3483e9..999d97603d 100644 --- a/builder/parser/line_parsers.go +++ b/builder/parser/line_parsers.go @@ -14,13 +14,13 @@ import ( ) var ( - dockerFileErrJSONNesting = errors.New("You may not nest arrays in Dockerfile statements.") + errDockerfileJSONNesting = errors.New("You may not nest arrays in Dockerfile statements.") ) // ignore the current argument. This will still leave a command parsed, but // will not incorporate the arguments into the ast. -func parseIgnore(rest string) (*Node, error) { - return &Node{}, nil +func parseIgnore(rest string) (*Node, map[string]bool, error) { + return &Node{}, nil, nil } // used for onbuild. Could potentially be used for anything that represents a @@ -28,18 +28,18 @@ func parseIgnore(rest string) (*Node, error) { // // ONBUILD RUN foo bar -> (onbuild (run foo bar)) // -func parseSubCommand(rest string) (*Node, error) { +func parseSubCommand(rest string) (*Node, map[string]bool, error) { _, child, err := parseLine(rest) if err != nil { - return nil, err + return nil, nil, err } - return &Node{Children: []*Node{child}}, nil + return &Node{Children: []*Node{child}}, nil, nil } // parse environment like statements. Note that this does *not* handle // variable interpolation, which will be handled in the evaluator. -func parseEnv(rest string) (*Node, error) { +func parseEnv(rest string) (*Node, map[string]bool, error) { node := &Node{} rootnode := node strs := TOKEN_WHITESPACE.Split(rest, 2) @@ -47,12 +47,12 @@ func parseEnv(rest string) (*Node, error) { node.Next = &Node{} node.Next.Value = strs[1] - return rootnode, nil + return rootnode, nil, nil } // parses a whitespace-delimited set of arguments. The result is effectively a // linked list of string arguments. -func parseStringsWhitespaceDelimited(rest string) (*Node, error) { +func parseStringsWhitespaceDelimited(rest string) (*Node, map[string]bool, error) { node := &Node{} rootnode := node prevnode := node @@ -68,16 +68,18 @@ func parseStringsWhitespaceDelimited(rest string) (*Node, error) { // chain. prevnode.Next = nil - return rootnode, nil + return rootnode, nil, nil } // parsestring just wraps the string in quotes and returns a working node. -func parseString(rest string) (*Node, error) { - return &Node{rest, nil, nil}, nil +func parseString(rest string) (*Node, map[string]bool, error) { + n := &Node{} + n.Value = rest + return n, nil, nil } // parseJSON converts JSON arrays to an AST. -func parseJSON(rest string) (*Node, error) { +func parseJSON(rest string) (*Node, map[string]bool, error) { var ( myJson []interface{} next = &Node{} @@ -86,7 +88,7 @@ func parseJSON(rest string) (*Node, error) { ) if err := json.Unmarshal([]byte(rest), &myJson); err != nil { - return nil, err + return nil, nil, err } for _, str := range myJson { @@ -95,7 +97,7 @@ func parseJSON(rest string) (*Node, error) { case float64: str = strconv.FormatFloat(str.(float64), 'G', -1, 64) default: - return nil, dockerFileErrJSONNesting + return nil, nil, errDockerfileJSONNesting } next.Value = str.(string) next.Next = &Node{} @@ -105,26 +107,27 @@ func parseJSON(rest string) (*Node, error) { prevnode.Next = nil - return orignext, nil + return orignext, map[string]bool{"json": true}, nil } // parseMaybeJSON determines if the argument appears to be a JSON array. If // so, passes to parseJSON; if not, quotes the result and returns a single // node. -func parseMaybeJSON(rest string) (*Node, error) { +func parseMaybeJSON(rest string) (*Node, map[string]bool, error) { rest = strings.TrimSpace(rest) if strings.HasPrefix(rest, "[") { - node, err := parseJSON(rest) + node, attrs, err := parseJSON(rest) if err == nil { - return node, nil - } else if err == dockerFileErrJSONNesting { - return nil, err + return node, attrs, nil + } + if err == errDockerfileJSONNesting { + return nil, nil, err } } node := &Node{} node.Value = rest - return node, nil + return node, nil, nil } diff --git a/builder/parser/parser.go b/builder/parser/parser.go index cb9d28206d..47ffc9a678 100644 --- a/builder/parser/parser.go +++ b/builder/parser/parser.go @@ -21,13 +21,14 @@ import ( // works a little more effectively than a "proper" parse tree for our needs. // type Node struct { - Value string // actual content - Next *Node // the next item in the current sexp - Children []*Node // the children of this sexp + Value string // actual content + Next *Node // the next item in the current sexp + Children []*Node // the children of this sexp + Attributes map[string]bool // special attributes for this node } var ( - dispatch map[string]func(string) (*Node, error) + dispatch map[string]func(string) (*Node, map[string]bool, error) TOKEN_WHITESPACE = regexp.MustCompile(`[\t\v\f\r ]+`) TOKEN_LINE_CONTINUATION = regexp.MustCompile(`\\$`) TOKEN_COMMENT = regexp.MustCompile(`^#.*$`) @@ -40,7 +41,7 @@ func init() { // reformulating the arguments according to the rules in the parser // functions. Errors are propogated up by Parse() and the resulting AST can // be incorporated directly into the existing AST as a next. - dispatch = map[string]func(string) (*Node, error){ + dispatch = map[string]func(string) (*Node, map[string]bool, error){ "user": parseString, "onbuild": parseSubCommand, "workdir": parseString, @@ -75,12 +76,13 @@ func parseLine(line string) (string, *Node, error) { node := &Node{} node.Value = cmd - sexp, err := fullDispatch(cmd, args) + sexp, attrs, err := fullDispatch(cmd, args) if err != nil { return "", nil, err } node.Next = sexp + node.Attributes = attrs return "", node, nil } diff --git a/builder/parser/utils.go b/builder/parser/utils.go index 08d3e454dd..53cda5808b 100644 --- a/builder/parser/utils.go +++ b/builder/parser/utils.go @@ -51,17 +51,17 @@ func (node *Node) Dump() string { // performs the dispatch based on the two primal strings, cmd and args. Please // look at the dispatch table in parser.go to see how these dispatchers work. -func fullDispatch(cmd, args string) (*Node, error) { +func fullDispatch(cmd, args string) (*Node, map[string]bool, error) { if _, ok := dispatch[cmd]; !ok { - return nil, fmt.Errorf("'%s' is not a valid dockerfile command", cmd) + return nil, nil, fmt.Errorf("'%s' is not a valid dockerfile command", cmd) } - sexp, err := dispatch[cmd](args) + sexp, attrs, err := dispatch[cmd](args) if err != nil { - return nil, err + return nil, nil, err } - return sexp, nil + return sexp, attrs, nil } // splitCommand takes a single line of text and parses out the cmd and args, diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index bcff199c85..e6572a1bf4 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -685,10 +685,11 @@ func TestBuildRelativeWorkdir(t *testing.T) { func TestBuildEnv(t *testing.T) { name := "testbuildenv" - expected := "[PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PORT=2375]" + expected := "[PATH=/test:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PORT=2375]" defer deleteImages(name) _, err := buildImage(name, `FROM busybox + ENV PATH /test:$PATH ENV PORT 2375 RUN [ $(env | grep PORT) = 'PORT=2375' ]`, true) @@ -1708,6 +1709,9 @@ func TestBuildEnvUsage(t *testing.T) { name := "testbuildenvusage" defer deleteImages(name) dockerfile := `FROM busybox +ENV PATH $HOME/bin:$PATH +ENV PATH /tmp:$PATH +RUN [ "$PATH" = "/tmp:$HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ] ENV FOO /foo/baz ENV BAR /bar ENV BAZ $BAR @@ -1717,7 +1721,8 @@ RUN [ "$FOOPATH" = "$PATH:/foo/baz" ] ENV FROM hello/docker/world ENV TO /docker/world/hello ADD $FROM $TO -RUN [ "$(cat $TO)" = "hello" ]` +RUN [ "$(cat $TO)" = "hello" ] +` ctx, err := fakeContext(dockerfile, map[string]string{ "hello/docker/world": "hello", })