Merge pull request #3254 from shykes/onbuild
New build instruction: ONBUILD defines a trigger to execute when extending an image with a new build
This commit is contained in:
commit
81b2940c89
4 changed files with 119 additions and 19 deletions
63
buildfile.go
63
buildfile.go
|
@ -108,9 +108,26 @@ func (b *buildFile) CmdFrom(name string) error {
|
||||||
if b.config.Env == nil || len(b.config.Env) == 0 {
|
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")
|
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
|
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 {
|
func (b *buildFile) CmdMaintainer(name string) error {
|
||||||
b.maintainer = name
|
b.maintainer = name
|
||||||
return b.commit("", b.config.Cmd, fmt.Sprintf("MAINTAINER %s", 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] == '#' {
|
if len(line) == 0 || line[0] == '#' {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tmp := strings.SplitN(line, " ", 2)
|
if err := b.BuildStep(fmt.Sprintf("%d", stepN), line); err != nil {
|
||||||
if len(tmp) != 2 {
|
return "", err
|
||||||
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))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
stepN += 1
|
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 != "" {
|
if b.image != "" {
|
||||||
fmt.Fprintf(b.outStream, "Successfully built %s\n", utils.TruncateID(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")
|
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 {
|
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{
|
return &buildFile{
|
||||||
runtime: srv.runtime,
|
runtime: srv.runtime,
|
||||||
|
|
|
@ -99,6 +99,7 @@ type Config struct {
|
||||||
WorkingDir string
|
WorkingDir string
|
||||||
Entrypoint []string
|
Entrypoint []string
|
||||||
NetworkDisabled bool
|
NetworkDisabled bool
|
||||||
|
OnBuild []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ContainerConfigFromJob(job *engine.Job) *Config {
|
func ContainerConfigFromJob(job *engine.Job) *Config {
|
||||||
|
|
|
@ -402,6 +402,64 @@ the image.
|
||||||
The ``WORKDIR`` instruction sets the working directory in which
|
The ``WORKDIR`` instruction sets the working directory in which
|
||||||
the command given by ``CMD`` is executed.
|
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:
|
.. _dockerfile_examples:
|
||||||
|
|
||||||
4. Dockerfile Examples
|
4. Dockerfile Examples
|
||||||
|
|
|
@ -847,3 +847,19 @@ func TestBuildFailsDockerfileEmpty(t *testing.T) {
|
||||||
t.Fatal("Expected: %v, got: %v", docker.ErrDockerfileEmpty, err)
|
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.
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue