diff --git a/buildfile.go b/buildfile.go index 3b45057f2b..2a4b163bec 100644 --- a/buildfile.go +++ b/buildfile.go @@ -108,9 +108,26 @@ func (b *buildFile) CmdFrom(name string) error { if b.config.Env == nil || len(b.config.Env) == 0 { b.config.Env = append(b.config.Env, "HOME=/", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin") } + // Process ONBUILD triggers if they exist + if nTriggers := len(b.config.OnBuild); nTriggers != 0 { + fmt.Fprintf(b.errStream, "# Executing %d build triggers\n", nTriggers) + } + for n, step := range b.config.OnBuild { + if err := b.BuildStep(fmt.Sprintf("onbuild-%d", n), step); err != nil { + return err + } + } + b.config.OnBuild = []string{} return nil } +// The ONBUILD command declares a build instruction to be executed in any future build +// using the current image as a base. +func (b *buildFile) CmdOnbuild(trigger string) error { + b.config.OnBuild = append(b.config.OnBuild, trigger) + return b.commit("", b.config.Cmd, fmt.Sprintf("ONBUILD %s", trigger)) +} + func (b *buildFile) CmdMaintainer(name string) error { b.maintainer = name return b.commit("", b.config.Cmd, fmt.Sprintf("MAINTAINER %s", name)) @@ -680,28 +697,11 @@ func (b *buildFile) Build(context io.Reader) (string, error) { if len(line) == 0 || line[0] == '#' { continue } - tmp := strings.SplitN(line, " ", 2) - if len(tmp) != 2 { - return "", fmt.Errorf("Invalid Dockerfile format") + if err := b.BuildStep(fmt.Sprintf("%d", stepN), line); err != nil { + return "", err } - instruction := strings.ToLower(strings.Trim(tmp[0], " ")) - arguments := strings.Trim(tmp[1], " ") - - method, exists := reflect.TypeOf(b).MethodByName("Cmd" + strings.ToUpper(instruction[:1]) + strings.ToLower(instruction[1:])) - if !exists { - fmt.Fprintf(b.errStream, "# Skipping unknown instruction %s\n", strings.ToUpper(instruction)) - continue - } - stepN += 1 - fmt.Fprintf(b.outStream, "Step %d : %s %s\n", stepN, strings.ToUpper(instruction), arguments) - ret := method.Func.Call([]reflect.Value{reflect.ValueOf(b), reflect.ValueOf(arguments)})[0].Interface() - if ret != nil { - return "", ret.(error) - } - - fmt.Fprintf(b.outStream, " ---> %s\n", utils.TruncateID(b.image)) } if b.image != "" { fmt.Fprintf(b.outStream, "Successfully built %s\n", utils.TruncateID(b.image)) @@ -713,6 +713,31 @@ func (b *buildFile) Build(context io.Reader) (string, error) { return "", fmt.Errorf("No image was generated. This may be because the Dockerfile does not, like, do anything.\n") } +// BuildStep parses a single build step from `instruction` and executes it in the current context. +func (b *buildFile) BuildStep(name, expression string) error { + fmt.Fprintf(b.outStream, "Step %s : %s\n", name, expression) + tmp := strings.SplitN(expression, " ", 2) + if len(tmp) != 2 { + return fmt.Errorf("Invalid Dockerfile format") + } + instruction := strings.ToLower(strings.Trim(tmp[0], " ")) + arguments := strings.Trim(tmp[1], " ") + + method, exists := reflect.TypeOf(b).MethodByName("Cmd" + strings.ToUpper(instruction[:1]) + strings.ToLower(instruction[1:])) + if !exists { + fmt.Fprintf(b.errStream, "# Skipping unknown instruction %s\n", strings.ToUpper(instruction)) + return nil + } + + ret := method.Func.Call([]reflect.Value{reflect.ValueOf(b), reflect.ValueOf(arguments)})[0].Interface() + if ret != nil { + return ret.(error) + } + + fmt.Fprintf(b.outStream, " ---> %s\n", utils.TruncateID(b.image)) + return nil +} + func NewBuildFile(srv *Server, outStream, errStream io.Writer, verbose, utilizeCache, rm bool, outOld io.Writer, sf *utils.StreamFormatter, auth *auth.AuthConfig, authConfigFile *auth.ConfigFile) BuildFile { return &buildFile{ runtime: srv.runtime, diff --git a/container.go b/container.go index a283b8d0bc..81e8749d2a 100644 --- a/container.go +++ b/container.go @@ -99,6 +99,7 @@ type Config struct { WorkingDir string Entrypoint []string NetworkDisabled bool + OnBuild []string } func ContainerConfigFromJob(job *engine.Job) *Config { diff --git a/docs/sources/reference/builder.rst b/docs/sources/reference/builder.rst index 9889660913..2f71b87a93 100644 --- a/docs/sources/reference/builder.rst +++ b/docs/sources/reference/builder.rst @@ -402,6 +402,64 @@ the image. The ``WORKDIR`` instruction sets the working directory in which the command given by ``CMD`` is executed. +3.11 ONBUILD +------------ + + ``ONBUILD [INSTRUCTION]`` + +The ``ONBUILD`` instruction adds to the image a "trigger" instruction to be +executed at a later time, when the image is used as the base for another build. +The trigger will be executed in the context of the downstream build, as if it +had been inserted immediately after the *FROM* instruction in the downstream +Dockerfile. + +Any build instruction can be registered as a trigger. + +This is useful if you are building an image which will be used as a base to build +other images, for example an application build environment or a daemon which may be +customized with user-specific configuration. + +For example, if your image is a reusable python application builder, it will require +application source code to be added in a particular directory, and it might require +a build script to be called *after* that. You can't just call *ADD* and *RUN* now, +because you don't yet have access to the application source code, and it will be +different for each application build. You could simply provide application developers +with a boilerplate Dockerfile to copy-paste into their application, but that is +inefficient, error-prone and difficult to update because it mixes with +application-specific code. + +The solution is to use *ONBUILD* to register in advance instructions to run later, +during the next build stage. + +Here's how it works: + +1. When it encounters an *ONBUILD* instruction, the builder adds a trigger to + the metadata of the image being built. + The instruction does not otherwise affect the current build. + +2. At the end of the build, a list of all triggers is stored in the image manifest, + under the key *OnBuild*. They can be inspected with *docker inspect*. + +3. Later the image may be used as a base for a new build, using the *FROM* instruction. + As part of processing the *FROM* instruction, the downstream builder looks for *ONBUILD* + triggers, and executes them in the same order they were registered. If any of the + triggers fail, the *FROM* instruction is aborted which in turn causes the build + to fail. If all triggers succeed, the FROM instruction completes and the build + continues as usual. + +4. Triggers are cleared from the final image after being executed. In other words + they are not inherited by "grand-children" builds. + +For example you might add something like this: + +.. code-block:: bash + + [...] + ONBUILD ADD . /app/src + ONBUILD RUN /usr/local/bin/python-build --dir /app/src + [...] + + .. _dockerfile_examples: 4. Dockerfile Examples diff --git a/integration/buildfile_test.go b/integration/buildfile_test.go index 16a51e575d..6a7da70558 100644 --- a/integration/buildfile_test.go +++ b/integration/buildfile_test.go @@ -847,3 +847,19 @@ func TestBuildFailsDockerfileEmpty(t *testing.T) { t.Fatal("Expected: %v, got: %v", docker.ErrDockerfileEmpty, err) } } + +func TestBuildOnBuildTrigger(t *testing.T) { + _, err := buildImage(testContextTemplate{` + from {IMAGE} + onbuild run echo here is the trigger + onbuild run touch foobar + `, + nil, nil, + }, + t, nil, true, + ) + if err != nil { + t.Fatal(err) + } + // FIXME: test that the 'foobar' file was created in the final build. +}