Merge pull request #32952 from dnephin/refactor-builder-dispatch-state
[Builder] Move dispatch state to a new struct
This commit is contained in:
commit
b106dd8b57
8 changed files with 353 additions and 309 deletions
|
@ -1,5 +1,10 @@
|
|||
package dockerfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/docker/docker/runconfig/opts"
|
||||
)
|
||||
|
||||
// builtinAllowedBuildArgs is list of built-in allowed build args
|
||||
// these args are considered transparent and are excluded from the image history.
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
defaultValue, exists := mapping[key]
|
||||
// Return override from options if one is defined
|
||||
|
|
|
@ -95,20 +95,12 @@ type Builder struct {
|
|||
source builder.Source
|
||||
clientCtx context.Context
|
||||
|
||||
runConfig *container.Config // runconfig for cmd, run, entrypoint etc.
|
||||
tmpContainers map[string]struct{}
|
||||
imageContexts *imageContexts // helper for storing contexts from builds
|
||||
disableCommit bool
|
||||
cacheBusted bool
|
||||
buildArgs *buildArgs
|
||||
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.
|
||||
|
@ -124,7 +116,6 @@ func newBuilder(clientCtx context.Context, options builderOptions) *Builder {
|
|||
Stderr: options.ProgressWriter.StderrFormatter,
|
||||
Output: options.ProgressWriter.Output,
|
||||
docker: options.Backend,
|
||||
runConfig: new(container.Config),
|
||||
tmpContainers: map[string]struct{}{},
|
||||
buildArgs: newBuildArgs(config.BuildArgs),
|
||||
}
|
||||
|
@ -136,7 +127,6 @@ func (b *Builder) resetImageCache() {
|
|||
if icb, ok := b.docker.(builder.ImageCacheBuilder); ok {
|
||||
b.imageCache = icb.MakeImageCache(b.options.CacheFrom)
|
||||
}
|
||||
b.noBaseImage = false
|
||||
b.cacheBusted = false
|
||||
}
|
||||
|
||||
|
@ -154,59 +144,61 @@ func (b *Builder) build(source builder.Source, dockerfile *parser.Result) (*buil
|
|||
return nil, err
|
||||
}
|
||||
|
||||
imageID, err := b.dispatchDockerfileWithCancellation(dockerfile)
|
||||
dispatchState, err := b.dispatchDockerfileWithCancellation(dockerfile)
|
||||
if err != nil {
|
||||
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()
|
||||
|
||||
if imageID == "" {
|
||||
if dispatchState.imageID == "" {
|
||||
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)
|
||||
|
||||
state := newDispatchState()
|
||||
total := len(dockerfile.AST.Children)
|
||||
var imageID string
|
||||
var err error
|
||||
for i, n := range dockerfile.AST.Children {
|
||||
select {
|
||||
case <-b.clientCtx.Done():
|
||||
logrus.Debug("Builder: build cancelled!")
|
||||
fmt.Fprint(b.Stdout, "Build cancelled")
|
||||
return "", errors.New("Build cancelled")
|
||||
return nil, errors.New("Build cancelled")
|
||||
default:
|
||||
// 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
|
||||
}
|
||||
|
||||
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 {
|
||||
b.clearTmp()
|
||||
}
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: get this from dispatch
|
||||
imageID = b.image
|
||||
|
||||
fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(imageID))
|
||||
fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(state.imageID))
|
||||
if b.options.Remove {
|
||||
b.clearTmp()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
return state, nil
|
||||
}
|
||||
|
||||
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
|
||||
// It will:
|
||||
// - 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{})
|
||||
|
||||
result, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
|
||||
dockerfile, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure that the commands are valid
|
||||
for _, n := range result.AST.Children {
|
||||
for _, n := range dockerfile.AST.Children {
|
||||
if !validCommitCommands[n.Value] {
|
||||
return nil, fmt.Errorf("%s is not a valid change command", n.Value)
|
||||
}
|
||||
}
|
||||
|
||||
b.runConfig = config
|
||||
b.Stdout = ioutil.Discard
|
||||
b.Stderr = ioutil.Discard
|
||||
b.disableCommit = true
|
||||
|
||||
if err := checkDispatchDockerfile(result.AST); err != nil {
|
||||
if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := dispatchFromDockerfile(b, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.runConfig, nil
|
||||
dispatchState := newDispatchState()
|
||||
dispatchState.runConfig = config
|
||||
return dispatchFromDockerfile(b, dockerfile, dispatchState)
|
||||
}
|
||||
|
||||
func checkDispatchDockerfile(dockerfile *parser.Node) error {
|
||||
|
@ -285,15 +268,21 @@ func checkDispatchDockerfile(dockerfile *parser.Node) error {
|
|||
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)
|
||||
ast := result.AST
|
||||
total := len(ast.Children)
|
||||
|
||||
for i, n := range ast.Children {
|
||||
if err := b.dispatch(i, total, n, shlex); err != nil {
|
||||
return err
|
||||
opts := dispatchOptions{
|
||||
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
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ func env(req dispatchRequest) error {
|
|||
return err
|
||||
}
|
||||
|
||||
runConfig := req.state.runConfig
|
||||
commitMessage := bytes.NewBufferString("ENV")
|
||||
|
||||
for j := 0; j < len(req.args); j += 2 {
|
||||
|
@ -59,21 +60,21 @@ func env(req dispatchRequest) error {
|
|||
commitMessage.WriteString(" " + newVar)
|
||||
|
||||
gotOne := false
|
||||
for i, envVar := range req.runConfig.Env {
|
||||
for i, envVar := range runConfig.Env {
|
||||
envParts := strings.SplitN(envVar, "=", 2)
|
||||
compareFrom := envParts[0]
|
||||
if equalEnvKeys(compareFrom, name) {
|
||||
req.runConfig.Env[i] = newVar
|
||||
runConfig.Env[i] = newVar
|
||||
gotOne = true
|
||||
break
|
||||
}
|
||||
}
|
||||
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>
|
||||
|
@ -89,8 +90,8 @@ func maintainer(req dispatchRequest) error {
|
|||
}
|
||||
|
||||
maintainer := req.args[0]
|
||||
req.builder.maintainer = maintainer
|
||||
return req.builder.commit("MAINTAINER " + maintainer)
|
||||
req.state.maintainer = maintainer
|
||||
return req.builder.commit(req.state, "MAINTAINER "+maintainer)
|
||||
}
|
||||
|
||||
// LABEL some json data describing the image
|
||||
|
@ -111,26 +112,25 @@ func label(req dispatchRequest) error {
|
|||
}
|
||||
|
||||
commitStr := "LABEL"
|
||||
runConfig := req.state.runConfig
|
||||
|
||||
if req.runConfig.Labels == nil {
|
||||
req.runConfig.Labels = map[string]string{}
|
||||
if runConfig.Labels == nil {
|
||||
runConfig.Labels = map[string]string{}
|
||||
}
|
||||
|
||||
for j := 0; j < len(req.args); j++ {
|
||||
// name ==> req.args[j]
|
||||
// value ==> req.args[j+1]
|
||||
|
||||
if len(req.args[j]) == 0 {
|
||||
name := req.args[j]
|
||||
if name == "" {
|
||||
return errBlankCommandNames("LABEL")
|
||||
}
|
||||
|
||||
newVar := req.args[j] + "=" + req.args[j+1] + ""
|
||||
commitStr += " " + newVar
|
||||
value := req.args[j+1]
|
||||
commitStr += " " + name + "=" + value
|
||||
|
||||
req.runConfig.Labels[req.args[j]] = req.args[j+1]
|
||||
runConfig.Labels[name] = value
|
||||
j++
|
||||
}
|
||||
return req.builder.commit(commitStr)
|
||||
return req.builder.commit(req.state, commitStr)
|
||||
}
|
||||
|
||||
// ADD foo /path
|
||||
|
@ -147,7 +147,7 @@ func add(req dispatchRequest) error {
|
|||
return err
|
||||
}
|
||||
|
||||
return req.builder.runContextCommand(req.args, true, true, "ADD", nil)
|
||||
return req.builder.runContextCommand(req, true, true, "ADD", nil)
|
||||
}
|
||||
|
||||
// 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]
|
||||
//
|
||||
func from(req dispatchRequest) error {
|
||||
ctxName, err := parseBuildStageName(req.args)
|
||||
stageName, err := parseBuildStageName(req.args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -190,21 +190,23 @@ func from(req dispatchRequest) error {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if image != nil {
|
||||
req.builder.imageContexts.update(image.ImageID(), image.RunConfig())
|
||||
}
|
||||
req.builder.from = image
|
||||
req.state.baseImage = image
|
||||
|
||||
req.builder.buildArgs.ResetAllowed()
|
||||
return req.builder.processImageFrom(image)
|
||||
return req.builder.processImageFrom(req.state, image)
|
||||
}
|
||||
|
||||
func parseBuildStageName(args []string) (string, error) {
|
||||
|
@ -222,7 +224,7 @@ func parseBuildStageName(args []string) (string, error) {
|
|||
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{}
|
||||
for key, value := range b.buildArgs.GetAllMeta() {
|
||||
substitutionArgs = append(substitutionArgs, key+"="+value)
|
||||
|
@ -246,8 +248,8 @@ func (b *Builder) getFromImage(shlex *ShellLex, name string) (builder.Image, err
|
|||
if runtime.GOOS == "windows" {
|
||||
return nil, errors.New("Windows does not support FROM scratch")
|
||||
}
|
||||
b.image = ""
|
||||
b.noBaseImage = true
|
||||
dispatchState.imageID = ""
|
||||
dispatchState.noBaseImage = true
|
||||
return nil, nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
runConfig := req.state.runConfig
|
||||
original := regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(req.original, "")
|
||||
req.runConfig.OnBuild = append(req.runConfig.OnBuild, original)
|
||||
return req.builder.commit("ONBUILD " + original)
|
||||
runConfig.OnBuild = append(runConfig.OnBuild, original)
|
||||
return req.builder.commit(req.state, "ONBUILD "+original)
|
||||
}
|
||||
|
||||
// WORKDIR /tmp
|
||||
|
@ -298,9 +301,10 @@ func workdir(req dispatchRequest) error {
|
|||
return err
|
||||
}
|
||||
|
||||
runConfig := req.state.runConfig
|
||||
// This is from the Dockerfile and will not necessarily be in platform
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
|
@ -315,9 +319,9 @@ func workdir(req dispatchRequest) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
comment := "WORKDIR " + req.runConfig.WorkingDir
|
||||
runConfigWithCommentCmd := copyRunConfig(req.runConfig, withCmdCommentString(comment))
|
||||
if hit, err := req.builder.probeCache(req.builder.image, runConfigWithCommentCmd); err != nil || hit {
|
||||
comment := "WORKDIR " + runConfig.WorkingDir
|
||||
runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment))
|
||||
if hit, err := req.builder.probeCache(req.state, runConfigWithCommentCmd); err != nil || hit {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -334,7 +338,7 @@ func workdir(req dispatchRequest) error {
|
|||
return err
|
||||
}
|
||||
|
||||
return req.builder.commitContainer(container.ID, runConfigWithCommentCmd)
|
||||
return req.builder.commitContainer(req.state, container.ID, runConfigWithCommentCmd)
|
||||
}
|
||||
|
||||
// RUN some command yo
|
||||
|
@ -348,7 +352,7 @@ func workdir(req dispatchRequest) error {
|
|||
// RUN [ "echo", "hi" ] # echo hi
|
||||
//
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -356,29 +360,30 @@ func run(req dispatchRequest) error {
|
|||
return err
|
||||
}
|
||||
|
||||
stateRunConfig := req.state.runConfig
|
||||
args := handleJSONArgs(req.args, req.attributes)
|
||||
if !req.attributes["json"] {
|
||||
args = append(getShell(req.runConfig), args...)
|
||||
args = append(getShell(stateRunConfig), args...)
|
||||
}
|
||||
cmdFromArgs := strslice.StrSlice(args)
|
||||
buildArgs := req.builder.buildArgsWithoutConfigEnv()
|
||||
buildArgs := req.builder.buildArgs.FilterAllowed(stateRunConfig.Env)
|
||||
|
||||
saveCmd := cmdFromArgs
|
||||
if len(buildArgs) > 0 {
|
||||
saveCmd = prependEnvOnCmd(req.builder.buildArgs, buildArgs, cmdFromArgs)
|
||||
}
|
||||
|
||||
runConfigForCacheProbe := copyRunConfig(req.runConfig,
|
||||
runConfigForCacheProbe := copyRunConfig(stateRunConfig,
|
||||
withCmd(saveCmd),
|
||||
withEntrypointOverride(saveCmd, nil))
|
||||
hit, err := req.builder.probeCache(req.builder.image, runConfigForCacheProbe)
|
||||
hit, err := req.builder.probeCache(req.state, runConfigForCacheProbe)
|
||||
if err != nil || hit {
|
||||
return err
|
||||
}
|
||||
|
||||
runConfig := copyRunConfig(req.runConfig,
|
||||
runConfig := copyRunConfig(stateRunConfig,
|
||||
withCmd(cmdFromArgs),
|
||||
withEnv(append(req.runConfig.Env, buildArgs...)),
|
||||
withEnv(append(stateRunConfig.Env, buildArgs...)),
|
||||
withEntrypointOverride(saveCmd, strslice.StrSlice{""}))
|
||||
|
||||
// set config as already being escaped, this prevents double escaping on windows
|
||||
|
@ -393,7 +398,7 @@ func run(req dispatchRequest) error {
|
|||
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.
|
||||
|
@ -431,22 +436,22 @@ func cmd(req dispatchRequest) error {
|
|||
return err
|
||||
}
|
||||
|
||||
runConfig := req.state.runConfig
|
||||
cmdSlice := handleJSONArgs(req.args, req.attributes)
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
if len(req.args) != 0 {
|
||||
req.builder.cmdSet = true
|
||||
req.state.cmdSet = true
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -478,6 +483,7 @@ func healthcheck(req dispatchRequest) error {
|
|||
if len(req.args) == 0 {
|
||||
return errAtLeastOneArgument("HEALTHCHECK")
|
||||
}
|
||||
runConfig := req.state.runConfig
|
||||
typ := strings.ToUpper(req.args[0])
|
||||
args := req.args[1:]
|
||||
if typ == "NONE" {
|
||||
|
@ -485,12 +491,12 @@ func healthcheck(req dispatchRequest) error {
|
|||
return errors.New("HEALTHCHECK NONE takes no arguments")
|
||||
}
|
||||
test := strslice.StrSlice{typ}
|
||||
req.runConfig.Healthcheck = &container.HealthConfig{
|
||||
runConfig.Healthcheck = &container.HealthConfig{
|
||||
Test: test,
|
||||
}
|
||||
} else {
|
||||
if req.runConfig.Healthcheck != nil {
|
||||
oldCmd := req.runConfig.Healthcheck.Test
|
||||
if runConfig.Healthcheck != nil {
|
||||
oldCmd := runConfig.Healthcheck.Test
|
||||
if len(oldCmd) > 0 && oldCmd[0] != "NONE" {
|
||||
fmt.Fprintf(req.builder.Stdout, "Note: overriding previous HEALTHCHECK: %v\n", oldCmd)
|
||||
}
|
||||
|
@ -554,10 +560,10 @@ func healthcheck(req dispatchRequest) error {
|
|||
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
|
||||
|
@ -573,27 +579,28 @@ func entrypoint(req dispatchRequest) error {
|
|||
return err
|
||||
}
|
||||
|
||||
runConfig := req.state.runConfig
|
||||
parsed := handleJSONArgs(req.args, req.attributes)
|
||||
|
||||
switch {
|
||||
case req.attributes["json"]:
|
||||
// ENTRYPOINT ["echo", "hi"]
|
||||
req.runConfig.Entrypoint = strslice.StrSlice(parsed)
|
||||
runConfig.Entrypoint = strslice.StrSlice(parsed)
|
||||
case len(parsed) == 0:
|
||||
// ENTRYPOINT []
|
||||
req.runConfig.Entrypoint = nil
|
||||
runConfig.Entrypoint = nil
|
||||
default:
|
||||
// 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
|
||||
// set the command to nil
|
||||
if !req.builder.cmdSet {
|
||||
req.runConfig.Cmd = nil
|
||||
if !req.state.cmdSet {
|
||||
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
|
||||
|
@ -612,8 +619,9 @@ func expose(req dispatchRequest) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if req.runConfig.ExposedPorts == nil {
|
||||
req.runConfig.ExposedPorts = make(nat.PortSet)
|
||||
runConfig := req.state.runConfig
|
||||
if runConfig.ExposedPorts == nil {
|
||||
runConfig.ExposedPorts = make(nat.PortSet)
|
||||
}
|
||||
|
||||
ports, _, err := nat.ParsePortSpecs(portsTab)
|
||||
|
@ -627,14 +635,14 @@ func expose(req dispatchRequest) error {
|
|||
portList := make([]string, len(ports))
|
||||
var i int
|
||||
for port := range ports {
|
||||
if _, exists := req.runConfig.ExposedPorts[port]; !exists {
|
||||
req.runConfig.ExposedPorts[port] = struct{}{}
|
||||
if _, exists := runConfig.ExposedPorts[port]; !exists {
|
||||
runConfig.ExposedPorts[port] = struct{}{}
|
||||
}
|
||||
portList[i] = string(port)
|
||||
i++
|
||||
}
|
||||
sort.Strings(portList)
|
||||
return req.builder.commit("EXPOSE " + strings.Join(portList, " "))
|
||||
return req.builder.commit(req.state, "EXPOSE "+strings.Join(portList, " "))
|
||||
}
|
||||
|
||||
// USER foo
|
||||
|
@ -651,8 +659,8 @@ func user(req dispatchRequest) error {
|
|||
return err
|
||||
}
|
||||
|
||||
req.runConfig.User = req.args[0]
|
||||
return req.builder.commit(fmt.Sprintf("USER %v", req.args))
|
||||
req.state.runConfig.User = req.args[0]
|
||||
return req.builder.commit(req.state, fmt.Sprintf("USER %v", req.args))
|
||||
}
|
||||
|
||||
// VOLUME /foo
|
||||
|
@ -668,17 +676,18 @@ func volume(req dispatchRequest) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if req.runConfig.Volumes == nil {
|
||||
req.runConfig.Volumes = map[string]struct{}{}
|
||||
runConfig := req.state.runConfig
|
||||
if runConfig.Volumes == nil {
|
||||
runConfig.Volumes = map[string]struct{}{}
|
||||
}
|
||||
for _, v := range req.args {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
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
|
||||
|
@ -695,8 +704,8 @@ func stopSignal(req dispatchRequest) error {
|
|||
return err
|
||||
}
|
||||
|
||||
req.runConfig.StopSignal = sig
|
||||
return req.builder.commit(fmt.Sprintf("STOPSIGNAL %v", req.args))
|
||||
req.state.runConfig.StopSignal = sig
|
||||
return req.builder.commit(req.state, fmt.Sprintf("STOPSIGNAL %v", req.args))
|
||||
}
|
||||
|
||||
// ARG name[=value]
|
||||
|
@ -742,11 +751,11 @@ func arg(req dispatchRequest) error {
|
|||
req.builder.buildArgs.AddArg(name, value)
|
||||
|
||||
// Arg before FROM doesn't add a layer
|
||||
if !req.builder.hasFromImage() {
|
||||
if !req.state.hasFromImage() {
|
||||
req.builder.buildArgs.AddMetaArg(name, value)
|
||||
return nil
|
||||
}
|
||||
return req.builder.commit("ARG " + arg)
|
||||
return req.builder.commit(req.state, "ARG "+arg)
|
||||
}
|
||||
|
||||
// SHELL powershell -command
|
||||
|
@ -763,12 +772,12 @@ func shell(req dispatchRequest) error {
|
|||
return errAtLeastOneArgument("SHELL")
|
||||
case req.attributes["json"]:
|
||||
// SHELL ["powershell", "-command"]
|
||||
req.runConfig.Shell = strslice.StrSlice(shellSlice)
|
||||
req.state.runConfig.Shell = strslice.StrSlice(shellSlice)
|
||||
default:
|
||||
// SHELL powershell -command - not JSON
|
||||
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 {
|
||||
|
|
|
@ -26,7 +26,7 @@ type commandWithFunction struct {
|
|||
|
||||
func withArgs(f dispatcher) func([]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 {
|
||||
return dispatchRequest{
|
||||
builder: builder,
|
||||
args: args,
|
||||
flags: NewBFlags(),
|
||||
runConfig: &container.Config{},
|
||||
shlex: NewShellLex(parser.DefaultEscapeToken),
|
||||
builder: builder,
|
||||
args: args,
|
||||
flags: NewBFlags(),
|
||||
shlex: NewShellLex(parser.DefaultEscapeToken),
|
||||
state: &dispatchState{runConfig: &container.Config{}},
|
||||
}
|
||||
}
|
||||
|
||||
func newBuilderWithMockBackend() *Builder {
|
||||
b := &Builder{
|
||||
runConfig: &container.Config{},
|
||||
options: &types.ImageBuildOptions{},
|
||||
docker: &MockBackend{},
|
||||
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[2], args[3]),
|
||||
}
|
||||
assert.Equal(t, expected, req.runConfig.Env)
|
||||
assert.Equal(t, expected, req.state.runConfig.Env)
|
||||
}
|
||||
|
||||
func TestEnvValueWithExistingRunConfigEnv(t *testing.T) {
|
||||
|
@ -146,7 +145,7 @@ func TestEnvValueWithExistingRunConfigEnv(t *testing.T) {
|
|||
|
||||
args := []string{"var1", "val1"}
|
||||
req := defaultDispatchReq(b, args...)
|
||||
req.runConfig.Env = []string{"var1=old", "var2=fromenv"}
|
||||
req.state.runConfig.Env = []string{"var1=old", "var2=fromenv"}
|
||||
err := env(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -154,16 +153,17 @@ func TestEnvValueWithExistingRunConfigEnv(t *testing.T) {
|
|||
fmt.Sprintf("%s=%s", args[0], args[1]),
|
||||
"var2=fromenv",
|
||||
}
|
||||
assert.Equal(t, expected, req.runConfig.Env)
|
||||
assert.Equal(t, expected, req.state.runConfig.Env)
|
||||
}
|
||||
|
||||
func TestMaintainer(t *testing.T) {
|
||||
maintainerEntry := "Some Maintainer <maintainer@example.com>"
|
||||
|
||||
b := newBuilderWithMockBackend()
|
||||
err := maintainer(defaultDispatchReq(b, maintainerEntry))
|
||||
req := defaultDispatchReq(b, maintainerEntry)
|
||||
err := maintainer(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, maintainerEntry, b.maintainer)
|
||||
assert.Equal(t, maintainerEntry, req.state.maintainer)
|
||||
}
|
||||
|
||||
func TestLabel(t *testing.T) {
|
||||
|
@ -176,13 +176,14 @@ func TestLabel(t *testing.T) {
|
|||
err := label(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Contains(t, req.runConfig.Labels, labelName)
|
||||
assert.Equal(t, req.runConfig.Labels[labelName], labelValue)
|
||||
require.Contains(t, req.state.runConfig.Labels, labelName)
|
||||
assert.Equal(t, req.state.runConfig.Labels[labelName], labelValue)
|
||||
}
|
||||
|
||||
func TestFromScratch(t *testing.T) {
|
||||
b := newBuilderWithMockBackend()
|
||||
err := from(defaultDispatchReq(b, "scratch"))
|
||||
req := defaultDispatchReq(b, "scratch")
|
||||
err := from(req)
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
assert.EqualError(t, err, "Windows does not support FROM scratch")
|
||||
|
@ -190,8 +191,8 @@ func TestFromScratch(t *testing.T) {
|
|||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", b.image)
|
||||
assert.Equal(t, true, b.noBaseImage)
|
||||
assert.Equal(t, "", req.state.imageID)
|
||||
assert.Equal(t, true, req.state.noBaseImage)
|
||||
}
|
||||
|
||||
func TestFromWithArg(t *testing.T) {
|
||||
|
@ -205,11 +206,12 @@ func TestFromWithArg(t *testing.T) {
|
|||
b.docker.(*MockBackend).getImageOnBuildFunc = getImage
|
||||
|
||||
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)
|
||||
assert.Equal(t, expected, b.image)
|
||||
assert.Equal(t, expected, b.from.ImageID())
|
||||
assert.Equal(t, expected, req.state.imageID)
|
||||
assert.Equal(t, expected, req.state.baseImage.ImageID())
|
||||
assert.Len(t, b.buildArgs.GetAllAllowed(), 0)
|
||||
assert.Len(t, b.buildArgs.GetAllMeta(), 1)
|
||||
}
|
||||
|
@ -225,9 +227,10 @@ func TestFromWithUndefinedArg(t *testing.T) {
|
|||
b.docker.(*MockBackend).getImageOnBuildFunc = getImage
|
||||
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)
|
||||
assert.Equal(t, expected, b.image)
|
||||
assert.Equal(t, expected, req.state.imageID)
|
||||
}
|
||||
|
||||
func TestOnbuildIllegalTriggers(t *testing.T) {
|
||||
|
@ -249,11 +252,11 @@ func TestOnbuild(t *testing.T) {
|
|||
|
||||
req := defaultDispatchReq(b, "ADD", ".", "/app/src")
|
||||
req.original = "ONBUILD ADD . /app/src"
|
||||
req.runConfig = &container.Config{}
|
||||
req.state.runConfig = &container.Config{}
|
||||
|
||||
err := onbuild(req)
|
||||
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) {
|
||||
|
@ -266,7 +269,7 @@ func TestWorkdir(t *testing.T) {
|
|||
req := defaultDispatchReq(b, workingDir)
|
||||
err := workdir(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, workingDir, req.runConfig.WorkingDir)
|
||||
assert.Equal(t, workingDir, req.state.runConfig.WorkingDir)
|
||||
}
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
|
@ -284,8 +287,8 @@ func TestCmd(t *testing.T) {
|
|||
expectedCommand = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", command))
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedCommand, req.runConfig.Cmd)
|
||||
assert.True(t, b.cmdSet)
|
||||
assert.Equal(t, expectedCommand, req.state.runConfig.Cmd)
|
||||
assert.True(t, req.state.cmdSet)
|
||||
}
|
||||
|
||||
func TestHealthcheckNone(t *testing.T) {
|
||||
|
@ -295,8 +298,8 @@ func TestHealthcheckNone(t *testing.T) {
|
|||
err := healthcheck(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, req.runConfig.Healthcheck)
|
||||
assert.Equal(t, []string{"NONE"}, req.runConfig.Healthcheck.Test)
|
||||
require.NotNil(t, req.state.runConfig.Healthcheck)
|
||||
assert.Equal(t, []string{"NONE"}, req.state.runConfig.Healthcheck.Test)
|
||||
}
|
||||
|
||||
func TestHealthcheckCmd(t *testing.T) {
|
||||
|
@ -307,9 +310,9 @@ func TestHealthcheckCmd(t *testing.T) {
|
|||
err := healthcheck(req)
|
||||
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"}
|
||||
assert.Equal(t, expectedTest, req.runConfig.Healthcheck.Test)
|
||||
assert.Equal(t, expectedTest, req.state.runConfig.Healthcheck.Test)
|
||||
}
|
||||
|
||||
func TestEntrypoint(t *testing.T) {
|
||||
|
@ -319,7 +322,7 @@ func TestEntrypoint(t *testing.T) {
|
|||
req := defaultDispatchReq(b, entrypointCmd)
|
||||
err := entrypoint(req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, req.runConfig.Entrypoint)
|
||||
require.NotNil(t, req.state.runConfig.Entrypoint)
|
||||
|
||||
var expectedEntrypoint strslice.StrSlice
|
||||
if runtime.GOOS == "windows" {
|
||||
|
@ -327,7 +330,7 @@ func TestEntrypoint(t *testing.T) {
|
|||
} else {
|
||||
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) {
|
||||
|
@ -338,12 +341,12 @@ func TestExpose(t *testing.T) {
|
|||
err := expose(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, req.runConfig.ExposedPorts)
|
||||
require.Len(t, req.runConfig.ExposedPorts, 1)
|
||||
require.NotNil(t, req.state.runConfig.ExposedPorts)
|
||||
require.Len(t, req.state.runConfig.ExposedPorts, 1)
|
||||
|
||||
portsMapping, err := nat.ParsePortSpec(exposedPort)
|
||||
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) {
|
||||
|
@ -353,7 +356,7 @@ func TestUser(t *testing.T) {
|
|||
req := defaultDispatchReq(b, userCommand)
|
||||
err := user(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, userCommand, req.runConfig.User)
|
||||
assert.Equal(t, userCommand, req.state.runConfig.User)
|
||||
}
|
||||
|
||||
func TestVolume(t *testing.T) {
|
||||
|
@ -365,9 +368,9 @@ func TestVolume(t *testing.T) {
|
|||
err := volume(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, req.runConfig.Volumes)
|
||||
assert.Len(t, req.runConfig.Volumes, 1)
|
||||
assert.Contains(t, req.runConfig.Volumes, exposedVolume)
|
||||
require.NotNil(t, req.state.runConfig.Volumes)
|
||||
assert.Len(t, req.state.runConfig.Volumes, 1)
|
||||
assert.Contains(t, req.state.runConfig.Volumes, exposedVolume)
|
||||
}
|
||||
|
||||
func TestStopSignal(t *testing.T) {
|
||||
|
@ -377,7 +380,7 @@ func TestStopSignal(t *testing.T) {
|
|||
req := defaultDispatchReq(b, signal)
|
||||
err := stopSignal(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, signal, req.runConfig.StopSignal)
|
||||
assert.Equal(t, signal, req.state.runConfig.StopSignal)
|
||||
}
|
||||
|
||||
func TestArg(t *testing.T) {
|
||||
|
@ -405,7 +408,7 @@ func TestShell(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
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) {
|
||||
|
@ -439,8 +442,9 @@ func TestRunWithBuildArgs(t *testing.T) {
|
|||
b.buildArgs.argsFromOptions["HTTP_PROXY"] = strPtr("FOO")
|
||||
b.disableCommit = false
|
||||
|
||||
runConfig := &container.Config{}
|
||||
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"}
|
||||
cachedCmd := strslice.StrSlice(append(envVars, cmdWithShell...))
|
||||
|
||||
|
@ -477,12 +481,10 @@ func TestRunWithBuildArgs(t *testing.T) {
|
|||
req := defaultDispatchReq(b, "abcdef")
|
||||
require.NoError(t, from(req))
|
||||
b.buildArgs.AddArg("one", strPtr("two"))
|
||||
// TODO: this can be removed with b.runConfig
|
||||
req.runConfig.Cmd = origCmd
|
||||
|
||||
req.args = []string{"echo foo"}
|
||||
require.NoError(t, run(req))
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"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/parser"
|
||||
"github.com/docker/docker/runconfig/opts"
|
||||
|
@ -64,19 +65,19 @@ type dispatchRequest struct {
|
|||
attributes map[string]bool
|
||||
flags *BFlags
|
||||
original string
|
||||
runConfig *container.Config
|
||||
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{
|
||||
builder: builder,
|
||||
args: args,
|
||||
attributes: node.Attributes,
|
||||
original: node.Original,
|
||||
flags: NewBFlagsWithArgs(node.Flags),
|
||||
runConfig: builder.runConfig,
|
||||
shlex: shlex,
|
||||
attributes: options.node.Attributes,
|
||||
original: options.node.Original,
|
||||
flags: NewBFlagsWithArgs(options.node.Flags),
|
||||
shlex: options.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.
|
||||
//
|
||||
// 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
|
||||
// deal with that, at least until it becomes more of a general concern with new
|
||||
// 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
|
||||
upperCasedCmd := strings.ToUpper(cmd)
|
||||
|
||||
// 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.
|
||||
if err := platformSupports(strings.ToLower(cmd)); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
strList := []string{}
|
||||
msg := bytes.NewBufferString(fmt.Sprintf("Step %d/%d : %s", stepN+1, stepTotal, upperCasedCmd))
|
||||
|
||||
if len(node.Flags) > 0 {
|
||||
msg.WriteString(strings.Join(node.Flags, " "))
|
||||
}
|
||||
msg := bytes.NewBufferString(fmt.Sprintf("Step %s : %s%s",
|
||||
options.stepMsg, upperCasedCmd, formatFlags(node.Flags)))
|
||||
|
||||
args := []string{}
|
||||
ast := node
|
||||
if cmd == "onbuild" {
|
||||
if ast.Next == nil {
|
||||
return errors.New("ONBUILD requires at least one argument")
|
||||
}
|
||||
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 cmd == command.Onbuild {
|
||||
var err error
|
||||
ast, args, err = handleOnBuildNode(node, msg)
|
||||
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())
|
||||
|
||||
// XXX yes, we skip any cmds that are not valid; the parser should have
|
||||
// picked these out already.
|
||||
if f, ok := evaluateTable[cmd]; ok {
|
||||
if err := f(newDispatchRequestFromNode(node, b, strList, shlex)); err != nil {
|
||||
return err
|
||||
f, ok := evaluateTable[cmd]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown instruction: %s", upperCasedCmd)
|
||||
}
|
||||
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
|
||||
// If the step created a new image set it as the imageID for the
|
||||
// current runConfig
|
||||
b.runConfig.Image = b.image
|
||||
return nil
|
||||
args = append(args, words...)
|
||||
msg.WriteString(" " + ast.Value)
|
||||
}
|
||||
|
||||
return fmt.Errorf("Unknown instruction: %s", upperCasedCmd)
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// count the number of nodes that we are going to traverse first
|
||||
// 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 processWordFunc func(string) ([]string, error)
|
||||
|
||||
type processFunc func(string, []string) ([]string, error)
|
||||
|
||||
func getProcessFunc(shlex *ShellLex, cmd string) processFunc {
|
||||
func createProcessWordFunc(shlex *ShellLex, cmd string, envs []string) processWordFunc {
|
||||
switch {
|
||||
case !replaceEnvAllowed[cmd]:
|
||||
return func(word string, _ []string) ([]string, error) {
|
||||
return func(word string) ([]string, error) {
|
||||
return []string{word}, nil
|
||||
}
|
||||
case allowWordExpansion[cmd]:
|
||||
return shlex.ProcessWords
|
||||
return func(word string) ([]string, error) {
|
||||
return shlex.ProcessWords(word, envs)
|
||||
}
|
||||
default:
|
||||
return func(word string, envs []string) ([]string, error) {
|
||||
return func(word string) ([]string, error) {
|
||||
word, err := shlex.ProcessWord(word, envs)
|
||||
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.
|
||||
// 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
|
||||
|
|
|
@ -123,7 +123,7 @@ func initDispatchTestCases() []dispatchTestCase {
|
|||
{
|
||||
name: "Invalid instruction",
|
||||
dockerfile: `foo bar`,
|
||||
expectedError: "Unknown instruction: FOO",
|
||||
expectedError: "unknown instruction: FOO",
|
||||
files: nil,
|
||||
}}
|
||||
|
||||
|
@ -177,13 +177,11 @@ func executeTestCase(t *testing.T, testCase dispatchTestCase) {
|
|||
t.Fatalf("Error when parsing Dockerfile: %s", err)
|
||||
}
|
||||
|
||||
config := &container.Config{}
|
||||
options := &types.ImageBuildOptions{
|
||||
BuildArgs: make(map[string]*string),
|
||||
}
|
||||
|
||||
b := &Builder{
|
||||
runConfig: config,
|
||||
options: options,
|
||||
Stdout: ioutil.Discard,
|
||||
source: context,
|
||||
|
@ -192,7 +190,14 @@ func executeTestCase(t *testing.T, testCase dispatchTestCase) {
|
|||
|
||||
shlex := NewShellLex(parser.DefaultEscapeToken)
|
||||
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 {
|
||||
t.Fatalf("No error when executing test %s", testCase.name)
|
||||
|
|
|
@ -19,11 +19,10 @@ type pathCache interface {
|
|||
// imageContexts is a helper for stacking up built image rootfs and reusing
|
||||
// them as contexts
|
||||
type imageContexts struct {
|
||||
b *Builder
|
||||
list []*imageMount
|
||||
byName map[string]*imageMount
|
||||
cache pathCache
|
||||
currentName string
|
||||
b *Builder
|
||||
list []*imageMount
|
||||
byName map[string]*imageMount
|
||||
cache pathCache
|
||||
}
|
||||
|
||||
func (ic *imageContexts) newImageMount(id string) *imageMount {
|
||||
|
@ -41,7 +40,6 @@ func (ic *imageContexts) add(name string) (*imageMount, error) {
|
|||
}
|
||||
ic.byName[name] = im
|
||||
}
|
||||
ic.currentName = name
|
||||
ic.list = append(ic.list, im)
|
||||
return im, nil
|
||||
}
|
||||
|
@ -96,13 +94,6 @@ func (ic *imageContexts) unmount() (retErr error) {
|
|||
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) {
|
||||
if ic.cache != nil {
|
||||
if id == "" {
|
||||
|
|
|
@ -35,16 +35,16 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (b *Builder) commit(comment string) error {
|
||||
func (b *Builder) commit(dispatchState *dispatchState, comment string) error {
|
||||
if b.disableCommit {
|
||||
return nil
|
||||
}
|
||||
if !b.hasFromImage() {
|
||||
if !dispatchState.hasFromImage() {
|
||||
return errors.New("Please provide a source image with `from` prior to commit")
|
||||
}
|
||||
|
||||
runConfigWithCommentCmd := copyRunConfig(b.runConfig, withCmdComment(comment))
|
||||
hit, err := b.probeCache(b.image, runConfigWithCommentCmd)
|
||||
runConfigWithCommentCmd := copyRunConfig(dispatchState.runConfig, withCmdComment(comment))
|
||||
hit, err := b.probeCache(dispatchState, runConfigWithCommentCmd)
|
||||
if err != nil || hit {
|
||||
return err
|
||||
}
|
||||
|
@ -53,20 +53,21 @@ func (b *Builder) commit(comment string) error {
|
|||
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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
commitCfg := &backend.ContainerCommitConfig{
|
||||
ContainerCommitConfig: types.ContainerCommitConfig{
|
||||
Author: b.maintainer,
|
||||
Author: dispatchState.maintainer,
|
||||
Pause: true,
|
||||
// TODO: this should be done by Commit()
|
||||
Config: copyRunConfig(b.runConfig),
|
||||
Config: copyRunConfig(dispatchState.runConfig),
|
||||
},
|
||||
ContainerConfig: containerConfig,
|
||||
}
|
||||
|
@ -77,10 +78,8 @@ func (b *Builder) commitContainer(id string, containerConfig *container.Config)
|
|||
return err
|
||||
}
|
||||
|
||||
// TODO: this function should return imageID and runConfig instead of setting
|
||||
// then on the builder
|
||||
b.image = imageID
|
||||
b.imageContexts.update(imageID, b.runConfig)
|
||||
dispatchState.imageID = imageID
|
||||
b.imageContexts.update(imageID, dispatchState.runConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -91,7 +90,9 @@ type copyInfo struct {
|
|||
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 {
|
||||
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?
|
||||
runConfigWithCommentCmd := copyRunConfig(
|
||||
b.runConfig,
|
||||
req.state.runConfig,
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -479,20 +480,20 @@ func (b *Builder) calcCopyInfo(cmdName, origPath string, allowLocalDecompression
|
|||
return copyInfos, nil
|
||||
}
|
||||
|
||||
func (b *Builder) processImageFrom(img builder.Image) error {
|
||||
func (b *Builder) processImageFrom(dispatchState *dispatchState, img builder.Image) error {
|
||||
if img != nil {
|
||||
b.image = img.ImageID()
|
||||
dispatchState.imageID = img.ImageID()
|
||||
|
||||
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
|
||||
// have one as it's set by HCS
|
||||
if system.DefaultPathEnv != "" {
|
||||
if _, ok := b.runConfigEnvMapping()["PATH"]; !ok {
|
||||
b.runConfig.Env = append(b.runConfig.Env,
|
||||
if _, ok := dispatchState.runConfigEnvMapping()["PATH"]; !ok {
|
||||
dispatchState.runConfig.Env = append(dispatchState.runConfig.Env,
|
||||
"PATH="+system.DefaultPathEnv)
|
||||
}
|
||||
}
|
||||
|
@ -503,7 +504,7 @@ func (b *Builder) processImageFrom(img builder.Image) error {
|
|||
}
|
||||
|
||||
// Process ONBUILD triggers if they exist
|
||||
if nTriggers := len(b.runConfig.OnBuild); nTriggers != 0 {
|
||||
if nTriggers := len(dispatchState.runConfig.OnBuild); nTriggers != 0 {
|
||||
word := "trigger"
|
||||
if nTriggers > 1 {
|
||||
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.
|
||||
onBuildTriggers := b.runConfig.OnBuild
|
||||
b.runConfig.OnBuild = []string{}
|
||||
onBuildTriggers := dispatchState.runConfig.OnBuild
|
||||
dispatchState.runConfig.OnBuild = []string{}
|
||||
|
||||
// Reset stdin settings as all build actions run without stdin
|
||||
b.runConfig.OpenStdin = false
|
||||
b.runConfig.StdinOnce = false
|
||||
dispatchState.runConfig.OpenStdin = false
|
||||
dispatchState.runConfig.StdinOnce = false
|
||||
|
||||
// parse the ONBUILD triggers by invoking the parser
|
||||
for _, step := range onBuildTriggers {
|
||||
result, err := parser.Parse(strings.NewReader(step))
|
||||
dockerfile, err := parser.Parse(strings.NewReader(step))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, n := range result.AST.Children {
|
||||
for _, n := range dockerfile.AST.Children {
|
||||
if err := checkDispatch(n); err != nil {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -551,12 +552,12 @@ func (b *Builder) processImageFrom(img builder.Image) error {
|
|||
// If an image is found, probeCache returns `(true, nil)`.
|
||||
// If no image is found, it returns `(false, nil)`.
|
||||
// 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
|
||||
if c == nil || b.options.NoCache || b.cacheBusted {
|
||||
return false, nil
|
||||
}
|
||||
cache, err := c.GetCache(parentID, runConfig)
|
||||
cache, err := c.GetCache(dispatchState.imageID, runConfig)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -568,16 +569,13 @@ func (b *Builder) probeCache(parentID string, runConfig *container.Config) (bool
|
|||
|
||||
fmt.Fprint(b.Stdout, " ---> Using cache\n")
|
||||
logrus.Debugf("[BUILDER] Use cached version: %s", runConfig.Cmd)
|
||||
b.image = string(cache)
|
||||
b.imageContexts.update(b.image, runConfig)
|
||||
dispatchState.imageID = string(cache)
|
||||
b.imageContexts.update(dispatchState.imageID, runConfig)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
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{
|
||||
CgroupParent: b.options.CgroupParent,
|
||||
CPUShares: b.options.CPUShares,
|
||||
|
|
Loading…
Reference in a new issue