1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

builder: Fix handling of ENV references that reference themselves, plus tests.

Docker-DCO-1.1-Signed-off-by: Erik Hollensbe <github@hollensbe.org> (github: erikh)
This commit is contained in:
Erik Hollensbe 2014-08-13 03:07:41 -07:00
parent 1ae4c00a19
commit cb51681a6d
9 changed files with 127 additions and 109 deletions

View file

@ -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{},
}
}

View file

@ -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 <maybe@an.email.address>
//
// 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")
}

View file

@ -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

View file

@ -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)
}
}

View file

@ -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, " ")})
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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,

View file

@ -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",
})