Some refactoring of dispatch()

Remove runConfig from Builder and dispatchRequest. It is not only on
dispatchState.

Move dispatch state fields from Builder to dispatchState

Move stageName tracking to dispatchRequest.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
Daniel Nephin 2017-04-26 17:45:16 -04:00
parent a74833aa70
commit 2f0ebba0e7
8 changed files with 353 additions and 309 deletions

View File

@ -1,5 +1,10 @@
package dockerfile package dockerfile
import (
"fmt"
"github.com/docker/docker/runconfig/opts"
)
// builtinAllowedBuildArgs is list of built-in allowed build args // builtinAllowedBuildArgs is list of built-in allowed build args
// these args are considered transparent and are excluded from the image history. // these args are considered transparent and are excluded from the image history.
// Filtering from history is implemented in dispatchers.go // Filtering from history is implemented in dispatchers.go
@ -96,6 +101,19 @@ func (b *buildArgs) getAllFromMapping(source map[string]*string) map[string]stri
return m return m
} }
// FilterAllowed returns all allowed args without the filtered args
func (b *buildArgs) FilterAllowed(filter []string) []string {
envs := []string{}
configEnv := opts.ConvertKVStringsToMap(filter)
for key, val := range b.GetAllAllowed() {
if _, ok := configEnv[key]; !ok {
envs = append(envs, fmt.Sprintf("%s=%s", key, val))
}
}
return envs
}
func (b *buildArgs) getBuildArg(key string, mapping map[string]*string) (string, bool) { func (b *buildArgs) getBuildArg(key string, mapping map[string]*string) (string, bool) {
defaultValue, exists := mapping[key] defaultValue, exists := mapping[key]
// Return override from options if one is defined // Return override from options if one is defined

View File

@ -95,20 +95,12 @@ type Builder struct {
source builder.Source source builder.Source
clientCtx context.Context clientCtx context.Context
runConfig *container.Config // runconfig for cmd, run, entrypoint etc.
tmpContainers map[string]struct{} tmpContainers map[string]struct{}
imageContexts *imageContexts // helper for storing contexts from builds imageContexts *imageContexts // helper for storing contexts from builds
disableCommit bool disableCommit bool
cacheBusted bool cacheBusted bool
buildArgs *buildArgs buildArgs *buildArgs
imageCache builder.ImageCache imageCache builder.ImageCache
// TODO: these move to DispatchState
maintainer string
cmdSet bool
noBaseImage bool // A flag to track the use of `scratch` as the base image
image string // imageID
from builder.Image
} }
// newBuilder creates a new Dockerfile builder from an optional dockerfile and a Options. // newBuilder creates a new Dockerfile builder from an optional dockerfile and a Options.
@ -124,7 +116,6 @@ func newBuilder(clientCtx context.Context, options builderOptions) *Builder {
Stderr: options.ProgressWriter.StderrFormatter, Stderr: options.ProgressWriter.StderrFormatter,
Output: options.ProgressWriter.Output, Output: options.ProgressWriter.Output,
docker: options.Backend, docker: options.Backend,
runConfig: new(container.Config),
tmpContainers: map[string]struct{}{}, tmpContainers: map[string]struct{}{},
buildArgs: newBuildArgs(config.BuildArgs), buildArgs: newBuildArgs(config.BuildArgs),
} }
@ -136,7 +127,6 @@ func (b *Builder) resetImageCache() {
if icb, ok := b.docker.(builder.ImageCacheBuilder); ok { if icb, ok := b.docker.(builder.ImageCacheBuilder); ok {
b.imageCache = icb.MakeImageCache(b.options.CacheFrom) b.imageCache = icb.MakeImageCache(b.options.CacheFrom)
} }
b.noBaseImage = false
b.cacheBusted = false b.cacheBusted = false
} }
@ -154,59 +144,61 @@ func (b *Builder) build(source builder.Source, dockerfile *parser.Result) (*buil
return nil, err return nil, err
} }
imageID, err := b.dispatchDockerfileWithCancellation(dockerfile) dispatchState, err := b.dispatchDockerfileWithCancellation(dockerfile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if b.options.Target != "" && !dispatchState.isCurrentStage(b.options.Target) {
return nil, errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target)
}
b.warnOnUnusedBuildArgs() b.warnOnUnusedBuildArgs()
if imageID == "" { if dispatchState.imageID == "" {
return nil, errors.New("No image was generated. Is your Dockerfile empty?") return nil, errors.New("No image was generated. Is your Dockerfile empty?")
} }
return &builder.Result{ImageID: imageID, FromImage: b.from}, nil return &builder.Result{ImageID: dispatchState.imageID, FromImage: dispatchState.baseImage}, nil
} }
func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result) (string, error) { func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result) (*dispatchState, error) {
shlex := NewShellLex(dockerfile.EscapeToken) shlex := NewShellLex(dockerfile.EscapeToken)
state := newDispatchState()
total := len(dockerfile.AST.Children) total := len(dockerfile.AST.Children)
var imageID string var err error
for i, n := range dockerfile.AST.Children { for i, n := range dockerfile.AST.Children {
select { select {
case <-b.clientCtx.Done(): case <-b.clientCtx.Done():
logrus.Debug("Builder: build cancelled!") logrus.Debug("Builder: build cancelled!")
fmt.Fprint(b.Stdout, "Build cancelled") fmt.Fprint(b.Stdout, "Build cancelled")
return "", errors.New("Build cancelled") return nil, errors.New("Build cancelled")
default: default:
// Not cancelled yet, keep going... // Not cancelled yet, keep going...
} }
if command.From == n.Value && b.imageContexts.isCurrentTarget(b.options.Target) { if n.Value == command.From && state.isCurrentStage(b.options.Target) {
break break
} }
if err := b.dispatch(i, total, n, shlex); err != nil { opts := dispatchOptions{
state: state,
stepMsg: formatStep(i, total),
node: n,
shlex: shlex,
}
if state, err = b.dispatch(opts); err != nil {
if b.options.ForceRemove { if b.options.ForceRemove {
b.clearTmp() b.clearTmp()
} }
return "", err return nil, err
} }
// TODO: get this from dispatch fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(state.imageID))
imageID = b.image
fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(imageID))
if b.options.Remove { if b.options.Remove {
b.clearTmp() b.clearTmp()
} }
} }
return state, nil
if b.options.Target != "" && !b.imageContexts.isCurrentTarget(b.options.Target) {
return "", errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target)
}
return imageID, nil
} }
func addNodesForLabelOption(dockerfile *parser.Node, labels map[string]string) { func addNodesForLabelOption(dockerfile *parser.Node, labels map[string]string) {
@ -227,12 +219,6 @@ func (b *Builder) warnOnUnusedBuildArgs() {
} }
} }
// hasFromImage returns true if the builder has processed a `FROM <image>` line
// TODO: move to DispatchState
func (b *Builder) hasFromImage() bool {
return b.image != "" || b.noBaseImage
}
// BuildFromConfig builds directly from `changes`, treating it as if it were the contents of a Dockerfile // BuildFromConfig builds directly from `changes`, treating it as if it were the contents of a Dockerfile
// It will: // It will:
// - Call parse.Parse() to get an AST root for the concatenated Dockerfile entries. // - Call parse.Parse() to get an AST root for the concatenated Dockerfile entries.
@ -249,31 +235,28 @@ func BuildFromConfig(config *container.Config, changes []string) (*container.Con
b := newBuilder(context.Background(), builderOptions{}) b := newBuilder(context.Background(), builderOptions{})
result, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n"))) dockerfile, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
if err != nil { if err != nil {
return nil, err return nil, err
} }
// ensure that the commands are valid // ensure that the commands are valid
for _, n := range result.AST.Children { for _, n := range dockerfile.AST.Children {
if !validCommitCommands[n.Value] { if !validCommitCommands[n.Value] {
return nil, fmt.Errorf("%s is not a valid change command", n.Value) return nil, fmt.Errorf("%s is not a valid change command", n.Value)
} }
} }
b.runConfig = config
b.Stdout = ioutil.Discard b.Stdout = ioutil.Discard
b.Stderr = ioutil.Discard b.Stderr = ioutil.Discard
b.disableCommit = true b.disableCommit = true
if err := checkDispatchDockerfile(result.AST); err != nil { if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
return nil, err return nil, err
} }
dispatchState := newDispatchState()
if err := dispatchFromDockerfile(b, result); err != nil { dispatchState.runConfig = config
return nil, err return dispatchFromDockerfile(b, dockerfile, dispatchState)
}
return b.runConfig, nil
} }
func checkDispatchDockerfile(dockerfile *parser.Node) error { func checkDispatchDockerfile(dockerfile *parser.Node) error {
@ -285,15 +268,21 @@ func checkDispatchDockerfile(dockerfile *parser.Node) error {
return nil return nil
} }
func dispatchFromDockerfile(b *Builder, result *parser.Result) error { func dispatchFromDockerfile(b *Builder, result *parser.Result, dispatchState *dispatchState) (*container.Config, error) {
shlex := NewShellLex(result.EscapeToken) shlex := NewShellLex(result.EscapeToken)
ast := result.AST ast := result.AST
total := len(ast.Children) total := len(ast.Children)
for i, n := range ast.Children { for i, n := range ast.Children {
if err := b.dispatch(i, total, n, shlex); err != nil { opts := dispatchOptions{
return err state: dispatchState,
stepMsg: formatStep(i, total),
node: n,
shlex: shlex,
}
if _, err := b.dispatch(opts); err != nil {
return nil, err
} }
} }
return nil return dispatchState.runConfig, nil
} }

View File

@ -47,6 +47,7 @@ func env(req dispatchRequest) error {
return err return err
} }
runConfig := req.state.runConfig
commitMessage := bytes.NewBufferString("ENV") commitMessage := bytes.NewBufferString("ENV")
for j := 0; j < len(req.args); j += 2 { for j := 0; j < len(req.args); j += 2 {
@ -59,21 +60,21 @@ func env(req dispatchRequest) error {
commitMessage.WriteString(" " + newVar) commitMessage.WriteString(" " + newVar)
gotOne := false gotOne := false
for i, envVar := range req.runConfig.Env { for i, envVar := range runConfig.Env {
envParts := strings.SplitN(envVar, "=", 2) envParts := strings.SplitN(envVar, "=", 2)
compareFrom := envParts[0] compareFrom := envParts[0]
if equalEnvKeys(compareFrom, name) { if equalEnvKeys(compareFrom, name) {
req.runConfig.Env[i] = newVar runConfig.Env[i] = newVar
gotOne = true gotOne = true
break break
} }
} }
if !gotOne { if !gotOne {
req.runConfig.Env = append(req.runConfig.Env, newVar) runConfig.Env = append(runConfig.Env, newVar)
} }
} }
return req.builder.commit(commitMessage.String()) return req.builder.commit(req.state, commitMessage.String())
} }
// MAINTAINER some text <maybe@an.email.address> // MAINTAINER some text <maybe@an.email.address>
@ -89,8 +90,8 @@ func maintainer(req dispatchRequest) error {
} }
maintainer := req.args[0] maintainer := req.args[0]
req.builder.maintainer = maintainer req.state.maintainer = maintainer
return req.builder.commit("MAINTAINER " + maintainer) return req.builder.commit(req.state, "MAINTAINER "+maintainer)
} }
// LABEL some json data describing the image // LABEL some json data describing the image
@ -111,26 +112,25 @@ func label(req dispatchRequest) error {
} }
commitStr := "LABEL" commitStr := "LABEL"
runConfig := req.state.runConfig
if req.runConfig.Labels == nil { if runConfig.Labels == nil {
req.runConfig.Labels = map[string]string{} runConfig.Labels = map[string]string{}
} }
for j := 0; j < len(req.args); j++ { for j := 0; j < len(req.args); j++ {
// name ==> req.args[j] name := req.args[j]
// value ==> req.args[j+1] if name == "" {
if len(req.args[j]) == 0 {
return errBlankCommandNames("LABEL") return errBlankCommandNames("LABEL")
} }
newVar := req.args[j] + "=" + req.args[j+1] + "" value := req.args[j+1]
commitStr += " " + newVar commitStr += " " + name + "=" + value
req.runConfig.Labels[req.args[j]] = req.args[j+1] runConfig.Labels[name] = value
j++ j++
} }
return req.builder.commit(commitStr) return req.builder.commit(req.state, commitStr)
} }
// ADD foo /path // ADD foo /path
@ -147,7 +147,7 @@ func add(req dispatchRequest) error {
return err return err
} }
return req.builder.runContextCommand(req.args, true, true, "ADD", nil) return req.builder.runContextCommand(req, true, true, "ADD", nil)
} }
// COPY foo /path // COPY foo /path
@ -174,13 +174,13 @@ func dispatchCopy(req dispatchRequest) error {
} }
} }
return req.builder.runContextCommand(req.args, false, false, "COPY", im) return req.builder.runContextCommand(req, false, false, "COPY", im)
} }
// FROM imagename[:tag | @digest] [AS build-stage-name] // FROM imagename[:tag | @digest] [AS build-stage-name]
// //
func from(req dispatchRequest) error { func from(req dispatchRequest) error {
ctxName, err := parseBuildStageName(req.args) stageName, err := parseBuildStageName(req.args)
if err != nil { if err != nil {
return err return err
} }
@ -190,21 +190,23 @@ func from(req dispatchRequest) error {
} }
req.builder.resetImageCache() req.builder.resetImageCache()
if _, err := req.builder.imageContexts.add(ctxName); err != nil { req.state.noBaseImage = false
req.state.stageName = stageName
if _, err := req.builder.imageContexts.add(stageName); err != nil {
return err return err
} }
image, err := req.builder.getFromImage(req.shlex, req.args[0]) image, err := req.builder.getFromImage(req.state, req.shlex, req.args[0])
if err != nil { if err != nil {
return err return err
} }
if image != nil { if image != nil {
req.builder.imageContexts.update(image.ImageID(), image.RunConfig()) req.builder.imageContexts.update(image.ImageID(), image.RunConfig())
} }
req.builder.from = image req.state.baseImage = image
req.builder.buildArgs.ResetAllowed() req.builder.buildArgs.ResetAllowed()
return req.builder.processImageFrom(image) return req.builder.processImageFrom(req.state, image)
} }
func parseBuildStageName(args []string) (string, error) { func parseBuildStageName(args []string) (string, error) {
@ -222,7 +224,7 @@ func parseBuildStageName(args []string) (string, error) {
return stageName, nil return stageName, nil
} }
func (b *Builder) getFromImage(shlex *ShellLex, name string) (builder.Image, error) { func (b *Builder) getFromImage(dispatchState *dispatchState, shlex *ShellLex, name string) (builder.Image, error) {
substitutionArgs := []string{} substitutionArgs := []string{}
for key, value := range b.buildArgs.GetAllMeta() { for key, value := range b.buildArgs.GetAllMeta() {
substitutionArgs = append(substitutionArgs, key+"="+value) substitutionArgs = append(substitutionArgs, key+"="+value)
@ -246,8 +248,8 @@ func (b *Builder) getFromImage(shlex *ShellLex, name string) (builder.Image, err
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return nil, errors.New("Windows does not support FROM scratch") return nil, errors.New("Windows does not support FROM scratch")
} }
b.image = "" dispatchState.imageID = ""
b.noBaseImage = true dispatchState.noBaseImage = true
return nil, nil return nil, nil
} }
return pullOrGetImage(b, name) return pullOrGetImage(b, name)
@ -279,9 +281,10 @@ func onbuild(req dispatchRequest) error {
return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction) return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction)
} }
runConfig := req.state.runConfig
original := regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(req.original, "") original := regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(req.original, "")
req.runConfig.OnBuild = append(req.runConfig.OnBuild, original) runConfig.OnBuild = append(runConfig.OnBuild, original)
return req.builder.commit("ONBUILD " + original) return req.builder.commit(req.state, "ONBUILD "+original)
} }
// WORKDIR /tmp // WORKDIR /tmp
@ -298,9 +301,10 @@ func workdir(req dispatchRequest) error {
return err return err
} }
runConfig := req.state.runConfig
// This is from the Dockerfile and will not necessarily be in platform // This is from the Dockerfile and will not necessarily be in platform
// specific semantics, hence ensure it is converted. // specific semantics, hence ensure it is converted.
req.runConfig.WorkingDir, err = normaliseWorkdir(req.runConfig.WorkingDir, req.args[0]) runConfig.WorkingDir, err = normaliseWorkdir(runConfig.WorkingDir, req.args[0])
if err != nil { if err != nil {
return err return err
} }
@ -315,9 +319,9 @@ func workdir(req dispatchRequest) error {
return nil return nil
} }
comment := "WORKDIR " + req.runConfig.WorkingDir comment := "WORKDIR " + runConfig.WorkingDir
runConfigWithCommentCmd := copyRunConfig(req.runConfig, withCmdCommentString(comment)) runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment))
if hit, err := req.builder.probeCache(req.builder.image, runConfigWithCommentCmd); err != nil || hit { if hit, err := req.builder.probeCache(req.state, runConfigWithCommentCmd); err != nil || hit {
return err return err
} }
@ -334,7 +338,7 @@ func workdir(req dispatchRequest) error {
return err return err
} }
return req.builder.commitContainer(container.ID, runConfigWithCommentCmd) return req.builder.commitContainer(req.state, container.ID, runConfigWithCommentCmd)
} }
// RUN some command yo // RUN some command yo
@ -348,7 +352,7 @@ func workdir(req dispatchRequest) error {
// RUN [ "echo", "hi" ] # echo hi // RUN [ "echo", "hi" ] # echo hi
// //
func run(req dispatchRequest) error { func run(req dispatchRequest) error {
if !req.builder.hasFromImage() { if !req.state.hasFromImage() {
return errors.New("Please provide a source image with `from` prior to run") return errors.New("Please provide a source image with `from` prior to run")
} }
@ -356,29 +360,30 @@ func run(req dispatchRequest) error {
return err return err
} }
stateRunConfig := req.state.runConfig
args := handleJSONArgs(req.args, req.attributes) args := handleJSONArgs(req.args, req.attributes)
if !req.attributes["json"] { if !req.attributes["json"] {
args = append(getShell(req.runConfig), args...) args = append(getShell(stateRunConfig), args...)
} }
cmdFromArgs := strslice.StrSlice(args) cmdFromArgs := strslice.StrSlice(args)
buildArgs := req.builder.buildArgsWithoutConfigEnv() buildArgs := req.builder.buildArgs.FilterAllowed(stateRunConfig.Env)
saveCmd := cmdFromArgs saveCmd := cmdFromArgs
if len(buildArgs) > 0 { if len(buildArgs) > 0 {
saveCmd = prependEnvOnCmd(req.builder.buildArgs, buildArgs, cmdFromArgs) saveCmd = prependEnvOnCmd(req.builder.buildArgs, buildArgs, cmdFromArgs)
} }
runConfigForCacheProbe := copyRunConfig(req.runConfig, runConfigForCacheProbe := copyRunConfig(stateRunConfig,
withCmd(saveCmd), withCmd(saveCmd),
withEntrypointOverride(saveCmd, nil)) withEntrypointOverride(saveCmd, nil))
hit, err := req.builder.probeCache(req.builder.image, runConfigForCacheProbe) hit, err := req.builder.probeCache(req.state, runConfigForCacheProbe)
if err != nil || hit { if err != nil || hit {
return err return err
} }
runConfig := copyRunConfig(req.runConfig, runConfig := copyRunConfig(stateRunConfig,
withCmd(cmdFromArgs), withCmd(cmdFromArgs),
withEnv(append(req.runConfig.Env, buildArgs...)), withEnv(append(stateRunConfig.Env, buildArgs...)),
withEntrypointOverride(saveCmd, strslice.StrSlice{""})) withEntrypointOverride(saveCmd, strslice.StrSlice{""}))
// set config as already being escaped, this prevents double escaping on windows // set config as already being escaped, this prevents double escaping on windows
@ -393,7 +398,7 @@ func run(req dispatchRequest) error {
return err return err
} }
return req.builder.commitContainer(cID, runConfigForCacheProbe) return req.builder.commitContainer(req.state, cID, runConfigForCacheProbe)
} }
// Derive the command to use for probeCache() and to commit in this container. // Derive the command to use for probeCache() and to commit in this container.
@ -431,22 +436,22 @@ func cmd(req dispatchRequest) error {
return err return err
} }
runConfig := req.state.runConfig
cmdSlice := handleJSONArgs(req.args, req.attributes) cmdSlice := handleJSONArgs(req.args, req.attributes)
if !req.attributes["json"] { if !req.attributes["json"] {
cmdSlice = append(getShell(req.runConfig), cmdSlice...) cmdSlice = append(getShell(runConfig), cmdSlice...)
} }
req.runConfig.Cmd = strslice.StrSlice(cmdSlice) runConfig.Cmd = strslice.StrSlice(cmdSlice)
// set config as already being escaped, this prevents double escaping on windows // set config as already being escaped, this prevents double escaping on windows
req.runConfig.ArgsEscaped = true runConfig.ArgsEscaped = true
if err := req.builder.commit(fmt.Sprintf("CMD %q", cmdSlice)); err != nil { if err := req.builder.commit(req.state, fmt.Sprintf("CMD %q", cmdSlice)); err != nil {
return err return err
} }
if len(req.args) != 0 { if len(req.args) != 0 {
req.builder.cmdSet = true req.state.cmdSet = true
} }
return nil return nil
@ -478,6 +483,7 @@ func healthcheck(req dispatchRequest) error {
if len(req.args) == 0 { if len(req.args) == 0 {
return errAtLeastOneArgument("HEALTHCHECK") return errAtLeastOneArgument("HEALTHCHECK")
} }
runConfig := req.state.runConfig
typ := strings.ToUpper(req.args[0]) typ := strings.ToUpper(req.args[0])
args := req.args[1:] args := req.args[1:]
if typ == "NONE" { if typ == "NONE" {
@ -485,12 +491,12 @@ func healthcheck(req dispatchRequest) error {
return errors.New("HEALTHCHECK NONE takes no arguments") return errors.New("HEALTHCHECK NONE takes no arguments")
} }
test := strslice.StrSlice{typ} test := strslice.StrSlice{typ}
req.runConfig.Healthcheck = &container.HealthConfig{ runConfig.Healthcheck = &container.HealthConfig{
Test: test, Test: test,
} }
} else { } else {
if req.runConfig.Healthcheck != nil { if runConfig.Healthcheck != nil {
oldCmd := req.runConfig.Healthcheck.Test oldCmd := runConfig.Healthcheck.Test
if len(oldCmd) > 0 && oldCmd[0] != "NONE" { if len(oldCmd) > 0 && oldCmd[0] != "NONE" {
fmt.Fprintf(req.builder.Stdout, "Note: overriding previous HEALTHCHECK: %v\n", oldCmd) fmt.Fprintf(req.builder.Stdout, "Note: overriding previous HEALTHCHECK: %v\n", oldCmd)
} }
@ -554,10 +560,10 @@ func healthcheck(req dispatchRequest) error {
healthcheck.Retries = 0 healthcheck.Retries = 0
} }
req.runConfig.Healthcheck = &healthcheck runConfig.Healthcheck = &healthcheck
} }
return req.builder.commit(fmt.Sprintf("HEALTHCHECK %q", req.runConfig.Healthcheck)) return req.builder.commit(req.state, fmt.Sprintf("HEALTHCHECK %q", runConfig.Healthcheck))
} }
// ENTRYPOINT /usr/sbin/nginx // ENTRYPOINT /usr/sbin/nginx
@ -573,27 +579,28 @@ func entrypoint(req dispatchRequest) error {
return err return err
} }
runConfig := req.state.runConfig
parsed := handleJSONArgs(req.args, req.attributes) parsed := handleJSONArgs(req.args, req.attributes)
switch { switch {
case req.attributes["json"]: case req.attributes["json"]:
// ENTRYPOINT ["echo", "hi"] // ENTRYPOINT ["echo", "hi"]
req.runConfig.Entrypoint = strslice.StrSlice(parsed) runConfig.Entrypoint = strslice.StrSlice(parsed)
case len(parsed) == 0: case len(parsed) == 0:
// ENTRYPOINT [] // ENTRYPOINT []
req.runConfig.Entrypoint = nil runConfig.Entrypoint = nil
default: default:
// ENTRYPOINT echo hi // ENTRYPOINT echo hi
req.runConfig.Entrypoint = strslice.StrSlice(append(getShell(req.runConfig), parsed[0])) runConfig.Entrypoint = strslice.StrSlice(append(getShell(runConfig), parsed[0]))
} }
// when setting the entrypoint if a CMD was not explicitly set then // when setting the entrypoint if a CMD was not explicitly set then
// set the command to nil // set the command to nil
if !req.builder.cmdSet { if !req.state.cmdSet {
req.runConfig.Cmd = nil runConfig.Cmd = nil
} }
return req.builder.commit(fmt.Sprintf("ENTRYPOINT %q", req.runConfig.Entrypoint)) return req.builder.commit(req.state, fmt.Sprintf("ENTRYPOINT %q", runConfig.Entrypoint))
} }
// EXPOSE 6667/tcp 7000/tcp // EXPOSE 6667/tcp 7000/tcp
@ -612,8 +619,9 @@ func expose(req dispatchRequest) error {
return err return err
} }
if req.runConfig.ExposedPorts == nil { runConfig := req.state.runConfig
req.runConfig.ExposedPorts = make(nat.PortSet) if runConfig.ExposedPorts == nil {
runConfig.ExposedPorts = make(nat.PortSet)
} }
ports, _, err := nat.ParsePortSpecs(portsTab) ports, _, err := nat.ParsePortSpecs(portsTab)
@ -627,14 +635,14 @@ func expose(req dispatchRequest) error {
portList := make([]string, len(ports)) portList := make([]string, len(ports))
var i int var i int
for port := range ports { for port := range ports {
if _, exists := req.runConfig.ExposedPorts[port]; !exists { if _, exists := runConfig.ExposedPorts[port]; !exists {
req.runConfig.ExposedPorts[port] = struct{}{} runConfig.ExposedPorts[port] = struct{}{}
} }
portList[i] = string(port) portList[i] = string(port)
i++ i++
} }
sort.Strings(portList) sort.Strings(portList)
return req.builder.commit("EXPOSE " + strings.Join(portList, " ")) return req.builder.commit(req.state, "EXPOSE "+strings.Join(portList, " "))
} }
// USER foo // USER foo
@ -651,8 +659,8 @@ func user(req dispatchRequest) error {
return err return err
} }
req.runConfig.User = req.args[0] req.state.runConfig.User = req.args[0]
return req.builder.commit(fmt.Sprintf("USER %v", req.args)) return req.builder.commit(req.state, fmt.Sprintf("USER %v", req.args))
} }
// VOLUME /foo // VOLUME /foo
@ -668,17 +676,18 @@ func volume(req dispatchRequest) error {
return err return err
} }
if req.runConfig.Volumes == nil { runConfig := req.state.runConfig
req.runConfig.Volumes = map[string]struct{}{} if runConfig.Volumes == nil {
runConfig.Volumes = map[string]struct{}{}
} }
for _, v := range req.args { for _, v := range req.args {
v = strings.TrimSpace(v) v = strings.TrimSpace(v)
if v == "" { if v == "" {
return errors.New("VOLUME specified can not be an empty string") return errors.New("VOLUME specified can not be an empty string")
} }
req.runConfig.Volumes[v] = struct{}{} runConfig.Volumes[v] = struct{}{}
} }
return req.builder.commit(fmt.Sprintf("VOLUME %v", req.args)) return req.builder.commit(req.state, fmt.Sprintf("VOLUME %v", req.args))
} }
// STOPSIGNAL signal // STOPSIGNAL signal
@ -695,8 +704,8 @@ func stopSignal(req dispatchRequest) error {
return err return err
} }
req.runConfig.StopSignal = sig req.state.runConfig.StopSignal = sig
return req.builder.commit(fmt.Sprintf("STOPSIGNAL %v", req.args)) return req.builder.commit(req.state, fmt.Sprintf("STOPSIGNAL %v", req.args))
} }
// ARG name[=value] // ARG name[=value]
@ -742,11 +751,11 @@ func arg(req dispatchRequest) error {
req.builder.buildArgs.AddArg(name, value) req.builder.buildArgs.AddArg(name, value)
// Arg before FROM doesn't add a layer // Arg before FROM doesn't add a layer
if !req.builder.hasFromImage() { if !req.state.hasFromImage() {
req.builder.buildArgs.AddMetaArg(name, value) req.builder.buildArgs.AddMetaArg(name, value)
return nil return nil
} }
return req.builder.commit("ARG " + arg) return req.builder.commit(req.state, "ARG "+arg)
} }
// SHELL powershell -command // SHELL powershell -command
@ -763,12 +772,12 @@ func shell(req dispatchRequest) error {
return errAtLeastOneArgument("SHELL") return errAtLeastOneArgument("SHELL")
case req.attributes["json"]: case req.attributes["json"]:
// SHELL ["powershell", "-command"] // SHELL ["powershell", "-command"]
req.runConfig.Shell = strslice.StrSlice(shellSlice) req.state.runConfig.Shell = strslice.StrSlice(shellSlice)
default: default:
// SHELL powershell -command - not JSON // SHELL powershell -command - not JSON
return errNotJSON("SHELL", req.original) return errNotJSON("SHELL", req.original)
} }
return req.builder.commit(fmt.Sprintf("SHELL %v", shellSlice)) return req.builder.commit(req.state, fmt.Sprintf("SHELL %v", shellSlice))
} }
func errAtLeastOneArgument(command string) error { func errAtLeastOneArgument(command string) error {

View File

@ -26,7 +26,7 @@ type commandWithFunction struct {
func withArgs(f dispatcher) func([]string) error { func withArgs(f dispatcher) func([]string) error {
return func(args []string) error { return func(args []string) error {
return f(dispatchRequest{args: args, runConfig: &container.Config{}}) return f(dispatchRequest{args: args})
} }
} }
@ -38,17 +38,16 @@ func withBuilderAndArgs(builder *Builder, f dispatcher) func([]string) error {
func defaultDispatchReq(builder *Builder, args ...string) dispatchRequest { func defaultDispatchReq(builder *Builder, args ...string) dispatchRequest {
return dispatchRequest{ return dispatchRequest{
builder: builder, builder: builder,
args: args, args: args,
flags: NewBFlags(), flags: NewBFlags(),
runConfig: &container.Config{}, shlex: NewShellLex(parser.DefaultEscapeToken),
shlex: NewShellLex(parser.DefaultEscapeToken), state: &dispatchState{runConfig: &container.Config{}},
} }
} }
func newBuilderWithMockBackend() *Builder { func newBuilderWithMockBackend() *Builder {
b := &Builder{ b := &Builder{
runConfig: &container.Config{},
options: &types.ImageBuildOptions{}, options: &types.ImageBuildOptions{},
docker: &MockBackend{}, docker: &MockBackend{},
buildArgs: newBuildArgs(make(map[string]*string)), buildArgs: newBuildArgs(make(map[string]*string)),
@ -138,7 +137,7 @@ func TestEnv2Variables(t *testing.T) {
fmt.Sprintf("%s=%s", args[0], args[1]), fmt.Sprintf("%s=%s", args[0], args[1]),
fmt.Sprintf("%s=%s", args[2], args[3]), fmt.Sprintf("%s=%s", args[2], args[3]),
} }
assert.Equal(t, expected, req.runConfig.Env) assert.Equal(t, expected, req.state.runConfig.Env)
} }
func TestEnvValueWithExistingRunConfigEnv(t *testing.T) { func TestEnvValueWithExistingRunConfigEnv(t *testing.T) {
@ -146,7 +145,7 @@ func TestEnvValueWithExistingRunConfigEnv(t *testing.T) {
args := []string{"var1", "val1"} args := []string{"var1", "val1"}
req := defaultDispatchReq(b, args...) req := defaultDispatchReq(b, args...)
req.runConfig.Env = []string{"var1=old", "var2=fromenv"} req.state.runConfig.Env = []string{"var1=old", "var2=fromenv"}
err := env(req) err := env(req)
require.NoError(t, err) require.NoError(t, err)
@ -154,16 +153,17 @@ func TestEnvValueWithExistingRunConfigEnv(t *testing.T) {
fmt.Sprintf("%s=%s", args[0], args[1]), fmt.Sprintf("%s=%s", args[0], args[1]),
"var2=fromenv", "var2=fromenv",
} }
assert.Equal(t, expected, req.runConfig.Env) assert.Equal(t, expected, req.state.runConfig.Env)
} }
func TestMaintainer(t *testing.T) { func TestMaintainer(t *testing.T) {
maintainerEntry := "Some Maintainer <maintainer@example.com>" maintainerEntry := "Some Maintainer <maintainer@example.com>"
b := newBuilderWithMockBackend() b := newBuilderWithMockBackend()
err := maintainer(defaultDispatchReq(b, maintainerEntry)) req := defaultDispatchReq(b, maintainerEntry)
err := maintainer(req)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, maintainerEntry, b.maintainer) assert.Equal(t, maintainerEntry, req.state.maintainer)
} }
func TestLabel(t *testing.T) { func TestLabel(t *testing.T) {
@ -176,13 +176,14 @@ func TestLabel(t *testing.T) {
err := label(req) err := label(req)
require.NoError(t, err) require.NoError(t, err)
require.Contains(t, req.runConfig.Labels, labelName) require.Contains(t, req.state.runConfig.Labels, labelName)
assert.Equal(t, req.runConfig.Labels[labelName], labelValue) assert.Equal(t, req.state.runConfig.Labels[labelName], labelValue)
} }
func TestFromScratch(t *testing.T) { func TestFromScratch(t *testing.T) {
b := newBuilderWithMockBackend() b := newBuilderWithMockBackend()
err := from(defaultDispatchReq(b, "scratch")) req := defaultDispatchReq(b, "scratch")
err := from(req)
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
assert.EqualError(t, err, "Windows does not support FROM scratch") assert.EqualError(t, err, "Windows does not support FROM scratch")
@ -190,8 +191,8 @@ func TestFromScratch(t *testing.T) {
} }
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "", b.image) assert.Equal(t, "", req.state.imageID)
assert.Equal(t, true, b.noBaseImage) assert.Equal(t, true, req.state.noBaseImage)
} }
func TestFromWithArg(t *testing.T) { func TestFromWithArg(t *testing.T) {
@ -205,11 +206,12 @@ func TestFromWithArg(t *testing.T) {
b.docker.(*MockBackend).getImageOnBuildFunc = getImage b.docker.(*MockBackend).getImageOnBuildFunc = getImage
require.NoError(t, arg(defaultDispatchReq(b, "THETAG="+tag))) require.NoError(t, arg(defaultDispatchReq(b, "THETAG="+tag)))
err := from(defaultDispatchReq(b, "alpine${THETAG}")) req := defaultDispatchReq(b, "alpine${THETAG}")
err := from(req)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, expected, b.image) assert.Equal(t, expected, req.state.imageID)
assert.Equal(t, expected, b.from.ImageID()) assert.Equal(t, expected, req.state.baseImage.ImageID())
assert.Len(t, b.buildArgs.GetAllAllowed(), 0) assert.Len(t, b.buildArgs.GetAllAllowed(), 0)
assert.Len(t, b.buildArgs.GetAllMeta(), 1) assert.Len(t, b.buildArgs.GetAllMeta(), 1)
} }
@ -225,9 +227,10 @@ func TestFromWithUndefinedArg(t *testing.T) {
b.docker.(*MockBackend).getImageOnBuildFunc = getImage b.docker.(*MockBackend).getImageOnBuildFunc = getImage
b.options.BuildArgs = map[string]*string{"THETAG": &tag} b.options.BuildArgs = map[string]*string{"THETAG": &tag}
err := from(defaultDispatchReq(b, "alpine${THETAG}")) req := defaultDispatchReq(b, "alpine${THETAG}")
err := from(req)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, expected, b.image) assert.Equal(t, expected, req.state.imageID)
} }
func TestOnbuildIllegalTriggers(t *testing.T) { func TestOnbuildIllegalTriggers(t *testing.T) {
@ -249,11 +252,11 @@ func TestOnbuild(t *testing.T) {
req := defaultDispatchReq(b, "ADD", ".", "/app/src") req := defaultDispatchReq(b, "ADD", ".", "/app/src")
req.original = "ONBUILD ADD . /app/src" req.original = "ONBUILD ADD . /app/src"
req.runConfig = &container.Config{} req.state.runConfig = &container.Config{}
err := onbuild(req) err := onbuild(req)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "ADD . /app/src", req.runConfig.OnBuild[0]) assert.Equal(t, "ADD . /app/src", req.state.runConfig.OnBuild[0])
} }
func TestWorkdir(t *testing.T) { func TestWorkdir(t *testing.T) {
@ -266,7 +269,7 @@ func TestWorkdir(t *testing.T) {
req := defaultDispatchReq(b, workingDir) req := defaultDispatchReq(b, workingDir)
err := workdir(req) err := workdir(req)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, workingDir, req.runConfig.WorkingDir) assert.Equal(t, workingDir, req.state.runConfig.WorkingDir)
} }
func TestCmd(t *testing.T) { func TestCmd(t *testing.T) {
@ -284,8 +287,8 @@ func TestCmd(t *testing.T) {
expectedCommand = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", command)) expectedCommand = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", command))
} }
assert.Equal(t, expectedCommand, req.runConfig.Cmd) assert.Equal(t, expectedCommand, req.state.runConfig.Cmd)
assert.True(t, b.cmdSet) assert.True(t, req.state.cmdSet)
} }
func TestHealthcheckNone(t *testing.T) { func TestHealthcheckNone(t *testing.T) {
@ -295,8 +298,8 @@ func TestHealthcheckNone(t *testing.T) {
err := healthcheck(req) err := healthcheck(req)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, req.runConfig.Healthcheck) require.NotNil(t, req.state.runConfig.Healthcheck)
assert.Equal(t, []string{"NONE"}, req.runConfig.Healthcheck.Test) assert.Equal(t, []string{"NONE"}, req.state.runConfig.Healthcheck.Test)
} }
func TestHealthcheckCmd(t *testing.T) { func TestHealthcheckCmd(t *testing.T) {
@ -307,9 +310,9 @@ func TestHealthcheckCmd(t *testing.T) {
err := healthcheck(req) err := healthcheck(req)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, req.runConfig.Healthcheck) require.NotNil(t, req.state.runConfig.Healthcheck)
expectedTest := []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"} expectedTest := []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"}
assert.Equal(t, expectedTest, req.runConfig.Healthcheck.Test) assert.Equal(t, expectedTest, req.state.runConfig.Healthcheck.Test)
} }
func TestEntrypoint(t *testing.T) { func TestEntrypoint(t *testing.T) {
@ -319,7 +322,7 @@ func TestEntrypoint(t *testing.T) {
req := defaultDispatchReq(b, entrypointCmd) req := defaultDispatchReq(b, entrypointCmd)
err := entrypoint(req) err := entrypoint(req)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, req.runConfig.Entrypoint) require.NotNil(t, req.state.runConfig.Entrypoint)
var expectedEntrypoint strslice.StrSlice var expectedEntrypoint strslice.StrSlice
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
@ -327,7 +330,7 @@ func TestEntrypoint(t *testing.T) {
} else { } else {
expectedEntrypoint = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", entrypointCmd)) expectedEntrypoint = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", entrypointCmd))
} }
assert.Equal(t, expectedEntrypoint, req.runConfig.Entrypoint) assert.Equal(t, expectedEntrypoint, req.state.runConfig.Entrypoint)
} }
func TestExpose(t *testing.T) { func TestExpose(t *testing.T) {
@ -338,12 +341,12 @@ func TestExpose(t *testing.T) {
err := expose(req) err := expose(req)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, req.runConfig.ExposedPorts) require.NotNil(t, req.state.runConfig.ExposedPorts)
require.Len(t, req.runConfig.ExposedPorts, 1) require.Len(t, req.state.runConfig.ExposedPorts, 1)
portsMapping, err := nat.ParsePortSpec(exposedPort) portsMapping, err := nat.ParsePortSpec(exposedPort)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, req.runConfig.ExposedPorts, portsMapping[0].Port) assert.Contains(t, req.state.runConfig.ExposedPorts, portsMapping[0].Port)
} }
func TestUser(t *testing.T) { func TestUser(t *testing.T) {
@ -353,7 +356,7 @@ func TestUser(t *testing.T) {
req := defaultDispatchReq(b, userCommand) req := defaultDispatchReq(b, userCommand)
err := user(req) err := user(req)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, userCommand, req.runConfig.User) assert.Equal(t, userCommand, req.state.runConfig.User)
} }
func TestVolume(t *testing.T) { func TestVolume(t *testing.T) {
@ -365,9 +368,9 @@ func TestVolume(t *testing.T) {
err := volume(req) err := volume(req)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, req.runConfig.Volumes) require.NotNil(t, req.state.runConfig.Volumes)
assert.Len(t, req.runConfig.Volumes, 1) assert.Len(t, req.state.runConfig.Volumes, 1)
assert.Contains(t, req.runConfig.Volumes, exposedVolume) assert.Contains(t, req.state.runConfig.Volumes, exposedVolume)
} }
func TestStopSignal(t *testing.T) { func TestStopSignal(t *testing.T) {
@ -377,7 +380,7 @@ func TestStopSignal(t *testing.T) {
req := defaultDispatchReq(b, signal) req := defaultDispatchReq(b, signal)
err := stopSignal(req) err := stopSignal(req)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, signal, req.runConfig.StopSignal) assert.Equal(t, signal, req.state.runConfig.StopSignal)
} }
func TestArg(t *testing.T) { func TestArg(t *testing.T) {
@ -405,7 +408,7 @@ func TestShell(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
expectedShell := strslice.StrSlice([]string{shellCmd}) expectedShell := strslice.StrSlice([]string{shellCmd})
assert.Equal(t, expectedShell, req.runConfig.Shell) assert.Equal(t, expectedShell, req.state.runConfig.Shell)
} }
func TestParseOptInterval(t *testing.T) { func TestParseOptInterval(t *testing.T) {
@ -439,8 +442,9 @@ func TestRunWithBuildArgs(t *testing.T) {
b.buildArgs.argsFromOptions["HTTP_PROXY"] = strPtr("FOO") b.buildArgs.argsFromOptions["HTTP_PROXY"] = strPtr("FOO")
b.disableCommit = false b.disableCommit = false
runConfig := &container.Config{}
origCmd := strslice.StrSlice([]string{"cmd", "in", "from", "image"}) origCmd := strslice.StrSlice([]string{"cmd", "in", "from", "image"})
cmdWithShell := strslice.StrSlice(append(getShell(b.runConfig), "echo foo")) cmdWithShell := strslice.StrSlice(append(getShell(runConfig), "echo foo"))
envVars := []string{"|1", "one=two"} envVars := []string{"|1", "one=two"}
cachedCmd := strslice.StrSlice(append(envVars, cmdWithShell...)) cachedCmd := strslice.StrSlice(append(envVars, cmdWithShell...))
@ -477,12 +481,10 @@ func TestRunWithBuildArgs(t *testing.T) {
req := defaultDispatchReq(b, "abcdef") req := defaultDispatchReq(b, "abcdef")
require.NoError(t, from(req)) require.NoError(t, from(req))
b.buildArgs.AddArg("one", strPtr("two")) b.buildArgs.AddArg("one", strPtr("two"))
// TODO: this can be removed with b.runConfig
req.runConfig.Cmd = origCmd
req.args = []string{"echo foo"} req.args = []string{"echo foo"}
require.NoError(t, run(req)) require.NoError(t, run(req))
// Check that runConfig.Cmd has not been modified by run // Check that runConfig.Cmd has not been modified by run
assert.Equal(t, origCmd, b.runConfig.Cmd) assert.Equal(t, origCmd, req.state.runConfig.Cmd)
} }

View File

@ -25,6 +25,7 @@ import (
"strings" "strings"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/builder"
"github.com/docker/docker/builder/dockerfile/command" "github.com/docker/docker/builder/dockerfile/command"
"github.com/docker/docker/builder/dockerfile/parser" "github.com/docker/docker/builder/dockerfile/parser"
"github.com/docker/docker/runconfig/opts" "github.com/docker/docker/runconfig/opts"
@ -64,19 +65,19 @@ type dispatchRequest struct {
attributes map[string]bool attributes map[string]bool
flags *BFlags flags *BFlags
original string original string
runConfig *container.Config
shlex *ShellLex shlex *ShellLex
state *dispatchState
} }
func newDispatchRequestFromNode(node *parser.Node, builder *Builder, args []string, shlex *ShellLex) dispatchRequest { func newDispatchRequestFromOptions(options dispatchOptions, builder *Builder, args []string) dispatchRequest {
return dispatchRequest{ return dispatchRequest{
builder: builder, builder: builder,
args: args, args: args,
attributes: node.Attributes, attributes: options.node.Attributes,
original: node.Original, original: options.node.Original,
flags: NewBFlagsWithArgs(node.Flags), flags: NewBFlagsWithArgs(options.node.Flags),
runConfig: builder.runConfig, shlex: options.shlex,
shlex: shlex, state: options.state,
} }
} }
@ -107,6 +108,10 @@ func init() {
} }
} }
func formatStep(stepN int, stepTotal int) string {
return fmt.Sprintf("%d/%d", stepN+1, stepTotal)
}
// This method is the entrypoint to all statement handling routines. // This method is the entrypoint to all statement handling routines.
// //
// Almost all nodes will have this structure: // Almost all nodes will have this structure:
@ -121,117 +126,144 @@ func init() {
// such as `RUN` in ONBUILD RUN foo. There is special case logic in here to // such as `RUN` in ONBUILD RUN foo. There is special case logic in here to
// deal with that, at least until it becomes more of a general concern with new // deal with that, at least until it becomes more of a general concern with new
// features. // features.
func (b *Builder) dispatch(stepN int, stepTotal int, node *parser.Node, shlex *ShellLex) error { func (b *Builder) dispatch(options dispatchOptions) (*dispatchState, error) {
node := options.node
cmd := node.Value cmd := node.Value
upperCasedCmd := strings.ToUpper(cmd) upperCasedCmd := strings.ToUpper(cmd)
// To ensure the user is given a decent error message if the platform // To ensure the user is given a decent error message if the platform
// on which the daemon is running does not support a builder command. // on which the daemon is running does not support a builder command.
if err := platformSupports(strings.ToLower(cmd)); err != nil { if err := platformSupports(strings.ToLower(cmd)); err != nil {
return err return nil, err
} }
strList := []string{} msg := bytes.NewBufferString(fmt.Sprintf("Step %s : %s%s",
msg := bytes.NewBufferString(fmt.Sprintf("Step %d/%d : %s", stepN+1, stepTotal, upperCasedCmd)) options.stepMsg, upperCasedCmd, formatFlags(node.Flags)))
if len(node.Flags) > 0 {
msg.WriteString(strings.Join(node.Flags, " "))
}
args := []string{}
ast := node ast := node
if cmd == "onbuild" { if cmd == command.Onbuild {
if ast.Next == nil { var err error
return errors.New("ONBUILD requires at least one argument") ast, args, err = handleOnBuildNode(node, msg)
}
ast = ast.Next.Children[0]
strList = append(strList, ast.Value)
msg.WriteString(" " + ast.Value)
if len(ast.Flags) > 0 {
msg.WriteString(" " + strings.Join(ast.Flags, " "))
}
}
msgList := initMsgList(ast)
// Append build args to runConfig environment variables
envs := append(b.runConfig.Env, b.buildArgsWithoutConfigEnv()...)
processFunc := getProcessFunc(shlex, cmd)
for i := 0; ast.Next != nil; i++ {
ast = ast.Next
words, err := processFunc(ast.Value, envs)
if err != nil { if err != nil {
return err return nil, err
} }
strList = append(strList, words...)
msgList[i] = ast.Value
} }
msg.WriteString(" " + strings.Join(msgList, " ")) runConfigEnv := options.state.runConfig.Env
envs := append(runConfigEnv, b.buildArgs.FilterAllowed(runConfigEnv)...)
processFunc := createProcessWordFunc(options.shlex, cmd, envs)
words, err := getDispatchArgsFromNode(ast, processFunc, msg)
if err != nil {
return nil, err
}
args = append(args, words...)
fmt.Fprintln(b.Stdout, msg.String()) fmt.Fprintln(b.Stdout, msg.String())
// XXX yes, we skip any cmds that are not valid; the parser should have f, ok := evaluateTable[cmd]
// picked these out already. if !ok {
if f, ok := evaluateTable[cmd]; ok { return nil, fmt.Errorf("unknown instruction: %s", upperCasedCmd)
if err := f(newDispatchRequestFromNode(node, b, strList, shlex)); err != nil { }
return err if err := f(newDispatchRequestFromOptions(options, b, args)); err != nil {
return nil, err
}
options.state.updateRunConfig()
return options.state, nil
}
type dispatchOptions struct {
state *dispatchState
stepMsg string
node *parser.Node
shlex *ShellLex
}
// dispatchState is a data object which is modified by dispatchers
type dispatchState struct {
runConfig *container.Config
maintainer string
cmdSet bool
noBaseImage bool
imageID string
baseImage builder.Image
stageName string
}
func newDispatchState() *dispatchState {
return &dispatchState{runConfig: &container.Config{}}
}
func (r *dispatchState) updateRunConfig() {
r.runConfig.Image = r.imageID
}
// hasFromImage returns true if the builder has processed a `FROM <image>` line
func (r *dispatchState) hasFromImage() bool {
return r.imageID != "" || r.noBaseImage
}
func (r *dispatchState) runConfigEnvMapping() map[string]string {
return opts.ConvertKVStringsToMap(r.runConfig.Env)
}
func (r *dispatchState) isCurrentStage(target string) bool {
if target == "" {
return false
}
return strings.EqualFold(r.stageName, target)
}
func handleOnBuildNode(ast *parser.Node, msg *bytes.Buffer) (*parser.Node, []string, error) {
if ast.Next == nil {
return nil, nil, errors.New("ONBUILD requires at least one argument")
}
ast = ast.Next.Children[0]
msg.WriteString(" " + ast.Value + formatFlags(ast.Flags))
return ast, []string{ast.Value}, nil
}
func formatFlags(flags []string) string {
if len(flags) > 0 {
return " " + strings.Join(flags, " ")
}
return ""
}
func getDispatchArgsFromNode(ast *parser.Node, processFunc processWordFunc, msg *bytes.Buffer) ([]string, error) {
args := []string{}
for i := 0; ast.Next != nil; i++ {
ast = ast.Next
words, err := processFunc(ast.Value)
if err != nil {
return nil, err
} }
// TODO: return an object instead of setting things on builder args = append(args, words...)
// If the step created a new image set it as the imageID for the msg.WriteString(" " + ast.Value)
// current runConfig
b.runConfig.Image = b.image
return nil
} }
return args, nil
return fmt.Errorf("Unknown instruction: %s", upperCasedCmd)
} }
// count the number of nodes that we are going to traverse first type processWordFunc func(string) ([]string, error)
// allocation of those list a lot when they have a lot of arguments
func initMsgList(cursor *parser.Node) []string {
var n int
for ; cursor.Next != nil; n++ {
cursor = cursor.Next
}
return make([]string, n)
}
type processFunc func(string, []string) ([]string, error) func createProcessWordFunc(shlex *ShellLex, cmd string, envs []string) processWordFunc {
func getProcessFunc(shlex *ShellLex, cmd string) processFunc {
switch { switch {
case !replaceEnvAllowed[cmd]: case !replaceEnvAllowed[cmd]:
return func(word string, _ []string) ([]string, error) { return func(word string) ([]string, error) {
return []string{word}, nil return []string{word}, nil
} }
case allowWordExpansion[cmd]: case allowWordExpansion[cmd]:
return shlex.ProcessWords return func(word string) ([]string, error) {
return shlex.ProcessWords(word, envs)
}
default: default:
return func(word string, envs []string) ([]string, error) { return func(word string) ([]string, error) {
word, err := shlex.ProcessWord(word, envs) word, err := shlex.ProcessWord(word, envs)
return []string{word}, err return []string{word}, err
} }
} }
} }
// buildArgsWithoutConfigEnv returns a list of key=value pairs for all the build
// args that are not overriden by runConfig environment variables.
func (b *Builder) buildArgsWithoutConfigEnv() []string {
envs := []string{}
configEnv := b.runConfigEnvMapping()
for key, val := range b.buildArgs.GetAllAllowed() {
if _, ok := configEnv[key]; !ok {
envs = append(envs, fmt.Sprintf("%s=%s", key, val))
}
}
return envs
}
func (b *Builder) runConfigEnvMapping() map[string]string {
return opts.ConvertKVStringsToMap(b.runConfig.Env)
}
// checkDispatch does a simple check for syntax errors of the Dockerfile. // checkDispatch does a simple check for syntax errors of the Dockerfile.
// Because some of the instructions can only be validated through runtime, // Because some of the instructions can only be validated through runtime,
// arg, env, etc., this syntax check will not be complete and could not replace // arg, env, etc., this syntax check will not be complete and could not replace

View File

@ -123,7 +123,7 @@ func initDispatchTestCases() []dispatchTestCase {
{ {
name: "Invalid instruction", name: "Invalid instruction",
dockerfile: `foo bar`, dockerfile: `foo bar`,
expectedError: "Unknown instruction: FOO", expectedError: "unknown instruction: FOO",
files: nil, files: nil,
}} }}
@ -177,13 +177,11 @@ func executeTestCase(t *testing.T, testCase dispatchTestCase) {
t.Fatalf("Error when parsing Dockerfile: %s", err) t.Fatalf("Error when parsing Dockerfile: %s", err)
} }
config := &container.Config{}
options := &types.ImageBuildOptions{ options := &types.ImageBuildOptions{
BuildArgs: make(map[string]*string), BuildArgs: make(map[string]*string),
} }
b := &Builder{ b := &Builder{
runConfig: config,
options: options, options: options,
Stdout: ioutil.Discard, Stdout: ioutil.Discard,
source: context, source: context,
@ -192,7 +190,14 @@ func executeTestCase(t *testing.T, testCase dispatchTestCase) {
shlex := NewShellLex(parser.DefaultEscapeToken) shlex := NewShellLex(parser.DefaultEscapeToken)
n := result.AST n := result.AST
err = b.dispatch(0, len(n.Children), n.Children[0], shlex) state := &dispatchState{runConfig: &container.Config{}}
opts := dispatchOptions{
state: state,
stepMsg: formatStep(0, len(n.Children)),
node: n.Children[0],
shlex: shlex,
}
state, err = b.dispatch(opts)
if err == nil { if err == nil {
t.Fatalf("No error when executing test %s", testCase.name) t.Fatalf("No error when executing test %s", testCase.name)

View File

@ -19,11 +19,10 @@ type pathCache interface {
// imageContexts is a helper for stacking up built image rootfs and reusing // imageContexts is a helper for stacking up built image rootfs and reusing
// them as contexts // them as contexts
type imageContexts struct { type imageContexts struct {
b *Builder b *Builder
list []*imageMount list []*imageMount
byName map[string]*imageMount byName map[string]*imageMount
cache pathCache cache pathCache
currentName string
} }
func (ic *imageContexts) newImageMount(id string) *imageMount { func (ic *imageContexts) newImageMount(id string) *imageMount {
@ -41,7 +40,6 @@ func (ic *imageContexts) add(name string) (*imageMount, error) {
} }
ic.byName[name] = im ic.byName[name] = im
} }
ic.currentName = name
ic.list = append(ic.list, im) ic.list = append(ic.list, im)
return im, nil return im, nil
} }
@ -96,13 +94,6 @@ func (ic *imageContexts) unmount() (retErr error) {
return return
} }
func (ic *imageContexts) isCurrentTarget(target string) bool {
if target == "" {
return false
}
return strings.EqualFold(ic.currentName, target)
}
func (ic *imageContexts) getCache(id, path string) (interface{}, bool) { func (ic *imageContexts) getCache(id, path string) (interface{}, bool) {
if ic.cache != nil { if ic.cache != nil {
if id == "" { if id == "" {

View File

@ -35,16 +35,16 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func (b *Builder) commit(comment string) error { func (b *Builder) commit(dispatchState *dispatchState, comment string) error {
if b.disableCommit { if b.disableCommit {
return nil return nil
} }
if !b.hasFromImage() { if !dispatchState.hasFromImage() {
return errors.New("Please provide a source image with `from` prior to commit") return errors.New("Please provide a source image with `from` prior to commit")
} }
runConfigWithCommentCmd := copyRunConfig(b.runConfig, withCmdComment(comment)) runConfigWithCommentCmd := copyRunConfig(dispatchState.runConfig, withCmdComment(comment))
hit, err := b.probeCache(b.image, runConfigWithCommentCmd) hit, err := b.probeCache(dispatchState, runConfigWithCommentCmd)
if err != nil || hit { if err != nil || hit {
return err return err
} }
@ -53,20 +53,21 @@ func (b *Builder) commit(comment string) error {
return err return err
} }
return b.commitContainer(id, runConfigWithCommentCmd) return b.commitContainer(dispatchState, id, runConfigWithCommentCmd)
} }
func (b *Builder) commitContainer(id string, containerConfig *container.Config) error { // TODO: see if any args can be dropped
func (b *Builder) commitContainer(dispatchState *dispatchState, id string, containerConfig *container.Config) error {
if b.disableCommit { if b.disableCommit {
return nil return nil
} }
commitCfg := &backend.ContainerCommitConfig{ commitCfg := &backend.ContainerCommitConfig{
ContainerCommitConfig: types.ContainerCommitConfig{ ContainerCommitConfig: types.ContainerCommitConfig{
Author: b.maintainer, Author: dispatchState.maintainer,
Pause: true, Pause: true,
// TODO: this should be done by Commit() // TODO: this should be done by Commit()
Config: copyRunConfig(b.runConfig), Config: copyRunConfig(dispatchState.runConfig),
}, },
ContainerConfig: containerConfig, ContainerConfig: containerConfig,
} }
@ -77,10 +78,8 @@ func (b *Builder) commitContainer(id string, containerConfig *container.Config)
return err return err
} }
// TODO: this function should return imageID and runConfig instead of setting dispatchState.imageID = imageID
// then on the builder b.imageContexts.update(imageID, dispatchState.runConfig)
b.image = imageID
b.imageContexts.update(imageID, b.runConfig)
return nil return nil
} }
@ -91,7 +90,9 @@ type copyInfo struct {
decompress bool decompress bool
} }
func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalDecompression bool, cmdName string, imageSource *imageMount) error { // TODO: this needs to be split so that a Builder method doesn't accept req
func (b *Builder) runContextCommand(req dispatchRequest, allowRemote bool, allowLocalDecompression bool, cmdName string, imageSource *imageMount) error {
args := req.args
if len(args) < 2 { if len(args) < 2 {
return fmt.Errorf("Invalid %s format - at least two arguments required", cmdName) return fmt.Errorf("Invalid %s format - at least two arguments required", cmdName)
} }
@ -163,9 +164,9 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalD
// TODO: should this have been using origPaths instead of srcHash in the comment? // TODO: should this have been using origPaths instead of srcHash in the comment?
runConfigWithCommentCmd := copyRunConfig( runConfigWithCommentCmd := copyRunConfig(
b.runConfig, req.state.runConfig,
withCmdCommentString(fmt.Sprintf("%s %s in %s ", cmdName, srcHash, dest))) withCmdCommentString(fmt.Sprintf("%s %s in %s ", cmdName, srcHash, dest)))
if hit, err := b.probeCache(b.image, runConfigWithCommentCmd); err != nil || hit { if hit, err := b.probeCache(req.state, runConfigWithCommentCmd); err != nil || hit {
return err return err
} }
@ -181,7 +182,7 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalD
// Twiddle the destination when it's a relative path - meaning, make it // Twiddle the destination when it's a relative path - meaning, make it
// relative to the WORKINGDIR // relative to the WORKINGDIR
if dest, err = normaliseDest(cmdName, b.runConfig.WorkingDir, dest); err != nil { if dest, err = normaliseDest(cmdName, req.state.runConfig.WorkingDir, dest); err != nil {
return err return err
} }
@ -191,7 +192,7 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalD
} }
} }
return b.commitContainer(container.ID, runConfigWithCommentCmd) return b.commitContainer(req.state, container.ID, runConfigWithCommentCmd)
} }
type runConfigModifier func(*container.Config) type runConfigModifier func(*container.Config)
@ -479,20 +480,20 @@ func (b *Builder) calcCopyInfo(cmdName, origPath string, allowLocalDecompression
return copyInfos, nil return copyInfos, nil
} }
func (b *Builder) processImageFrom(img builder.Image) error { func (b *Builder) processImageFrom(dispatchState *dispatchState, img builder.Image) error {
if img != nil { if img != nil {
b.image = img.ImageID() dispatchState.imageID = img.ImageID()
if img.RunConfig() != nil { if img.RunConfig() != nil {
b.runConfig = img.RunConfig() dispatchState.runConfig = img.RunConfig()
} }
} }
// Check to see if we have a default PATH, note that windows won't // Check to see if we have a default PATH, note that windows won't
// have one as it's set by HCS // have one as it's set by HCS
if system.DefaultPathEnv != "" { if system.DefaultPathEnv != "" {
if _, ok := b.runConfigEnvMapping()["PATH"]; !ok { if _, ok := dispatchState.runConfigEnvMapping()["PATH"]; !ok {
b.runConfig.Env = append(b.runConfig.Env, dispatchState.runConfig.Env = append(dispatchState.runConfig.Env,
"PATH="+system.DefaultPathEnv) "PATH="+system.DefaultPathEnv)
} }
} }
@ -503,7 +504,7 @@ func (b *Builder) processImageFrom(img builder.Image) error {
} }
// Process ONBUILD triggers if they exist // Process ONBUILD triggers if they exist
if nTriggers := len(b.runConfig.OnBuild); nTriggers != 0 { if nTriggers := len(dispatchState.runConfig.OnBuild); nTriggers != 0 {
word := "trigger" word := "trigger"
if nTriggers > 1 { if nTriggers > 1 {
word = "triggers" word = "triggers"
@ -512,21 +513,21 @@ func (b *Builder) processImageFrom(img builder.Image) error {
} }
// Copy the ONBUILD triggers, and remove them from the config, since the config will be committed. // Copy the ONBUILD triggers, and remove them from the config, since the config will be committed.
onBuildTriggers := b.runConfig.OnBuild onBuildTriggers := dispatchState.runConfig.OnBuild
b.runConfig.OnBuild = []string{} dispatchState.runConfig.OnBuild = []string{}
// Reset stdin settings as all build actions run without stdin // Reset stdin settings as all build actions run without stdin
b.runConfig.OpenStdin = false dispatchState.runConfig.OpenStdin = false
b.runConfig.StdinOnce = false dispatchState.runConfig.StdinOnce = false
// parse the ONBUILD triggers by invoking the parser // parse the ONBUILD triggers by invoking the parser
for _, step := range onBuildTriggers { for _, step := range onBuildTriggers {
result, err := parser.Parse(strings.NewReader(step)) dockerfile, err := parser.Parse(strings.NewReader(step))
if err != nil { if err != nil {
return err return err
} }
for _, n := range result.AST.Children { for _, n := range dockerfile.AST.Children {
if err := checkDispatch(n); err != nil { if err := checkDispatch(n); err != nil {
return err return err
} }
@ -540,7 +541,7 @@ func (b *Builder) processImageFrom(img builder.Image) error {
} }
} }
if err := dispatchFromDockerfile(b, result); err != nil { if _, err := dispatchFromDockerfile(b, dockerfile, dispatchState); err != nil {
return err return err
} }
} }
@ -551,12 +552,12 @@ func (b *Builder) processImageFrom(img builder.Image) error {
// If an image is found, probeCache returns `(true, nil)`. // If an image is found, probeCache returns `(true, nil)`.
// If no image is found, it returns `(false, nil)`. // If no image is found, it returns `(false, nil)`.
// If there is any error, it returns `(false, err)`. // If there is any error, it returns `(false, err)`.
func (b *Builder) probeCache(parentID string, runConfig *container.Config) (bool, error) { func (b *Builder) probeCache(dispatchState *dispatchState, runConfig *container.Config) (bool, error) {
c := b.imageCache c := b.imageCache
if c == nil || b.options.NoCache || b.cacheBusted { if c == nil || b.options.NoCache || b.cacheBusted {
return false, nil return false, nil
} }
cache, err := c.GetCache(parentID, runConfig) cache, err := c.GetCache(dispatchState.imageID, runConfig)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -568,16 +569,13 @@ func (b *Builder) probeCache(parentID string, runConfig *container.Config) (bool
fmt.Fprint(b.Stdout, " ---> Using cache\n") fmt.Fprint(b.Stdout, " ---> Using cache\n")
logrus.Debugf("[BUILDER] Use cached version: %s", runConfig.Cmd) logrus.Debugf("[BUILDER] Use cached version: %s", runConfig.Cmd)
b.image = string(cache) dispatchState.imageID = string(cache)
b.imageContexts.update(b.image, runConfig) b.imageContexts.update(dispatchState.imageID, runConfig)
return true, nil return true, nil
} }
func (b *Builder) create(runConfig *container.Config) (string, error) { func (b *Builder) create(runConfig *container.Config) (string, error) {
if !b.hasFromImage() {
return "", errors.New("Please provide a source image with `from` prior to run")
}
resources := container.Resources{ resources := container.Resources{
CgroupParent: b.options.CgroupParent, CgroupParent: b.options.CgroupParent,
CPUShares: b.options.CPUShares, CPUShares: b.options.CPUShares,