Browse code

Merge pull request #3254 from shykes/onbuild

New build instruction: ONBUILD defines a trigger to execute when extending an image with a new build

Guillaume J. Charmes authored on 2014/02/05 04:38:27
Showing 4 changed files
... ...
@@ -108,9 +108,26 @@ func (b *buildFile) CmdFrom(name string) error {
108 108
 	if b.config.Env == nil || len(b.config.Env) == 0 {
109 109
 		b.config.Env = append(b.config.Env, "HOME=/", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")
110 110
 	}
111
+	// Process ONBUILD triggers if they exist
112
+	if nTriggers := len(b.config.OnBuild); nTriggers != 0 {
113
+		fmt.Fprintf(b.errStream, "# Executing %d build triggers\n", nTriggers)
114
+	}
115
+	for n, step := range b.config.OnBuild {
116
+		if err := b.BuildStep(fmt.Sprintf("onbuild-%d", n), step); err != nil {
117
+			return err
118
+		}
119
+	}
120
+	b.config.OnBuild = []string{}
111 121
 	return nil
112 122
 }
113 123
 
124
+// The ONBUILD command declares a build instruction to be executed in any future build
125
+// using the current image as a base.
126
+func (b *buildFile) CmdOnbuild(trigger string) error {
127
+	b.config.OnBuild = append(b.config.OnBuild, trigger)
128
+	return b.commit("", b.config.Cmd, fmt.Sprintf("ONBUILD %s", trigger))
129
+}
130
+
114 131
 func (b *buildFile) CmdMaintainer(name string) error {
115 132
 	b.maintainer = name
116 133
 	return b.commit("", b.config.Cmd, fmt.Sprintf("MAINTAINER %s", name))
... ...
@@ -680,28 +697,11 @@ func (b *buildFile) Build(context io.Reader) (string, error) {
680 680
 		if len(line) == 0 || line[0] == '#' {
681 681
 			continue
682 682
 		}
683
-		tmp := strings.SplitN(line, " ", 2)
684
-		if len(tmp) != 2 {
685
-			return "", fmt.Errorf("Invalid Dockerfile format")
686
-		}
687
-		instruction := strings.ToLower(strings.Trim(tmp[0], " "))
688
-		arguments := strings.Trim(tmp[1], " ")
689
-
690
-		method, exists := reflect.TypeOf(b).MethodByName("Cmd" + strings.ToUpper(instruction[:1]) + strings.ToLower(instruction[1:]))
691
-		if !exists {
692
-			fmt.Fprintf(b.errStream, "# Skipping unknown instruction %s\n", strings.ToUpper(instruction))
693
-			continue
683
+		if err := b.BuildStep(fmt.Sprintf("%d", stepN), line); err != nil {
684
+			return "", err
694 685
 		}
695
-
696 686
 		stepN += 1
697
-		fmt.Fprintf(b.outStream, "Step %d : %s %s\n", stepN, strings.ToUpper(instruction), arguments)
698
-
699
-		ret := method.Func.Call([]reflect.Value{reflect.ValueOf(b), reflect.ValueOf(arguments)})[0].Interface()
700
-		if ret != nil {
701
-			return "", ret.(error)
702
-		}
703 687
 
704
-		fmt.Fprintf(b.outStream, " ---> %s\n", utils.TruncateID(b.image))
705 688
 	}
706 689
 	if b.image != "" {
707 690
 		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) {
713 713
 	return "", fmt.Errorf("No image was generated. This may be because the Dockerfile does not, like, do anything.\n")
714 714
 }
715 715
 
716
+// BuildStep parses a single build step from `instruction` and executes it in the current context.
717
+func (b *buildFile) BuildStep(name, expression string) error {
718
+	fmt.Fprintf(b.outStream, "Step %s : %s\n", name, expression)
719
+	tmp := strings.SplitN(expression, " ", 2)
720
+	if len(tmp) != 2 {
721
+		return fmt.Errorf("Invalid Dockerfile format")
722
+	}
723
+	instruction := strings.ToLower(strings.Trim(tmp[0], " "))
724
+	arguments := strings.Trim(tmp[1], " ")
725
+
726
+	method, exists := reflect.TypeOf(b).MethodByName("Cmd" + strings.ToUpper(instruction[:1]) + strings.ToLower(instruction[1:]))
727
+	if !exists {
728
+		fmt.Fprintf(b.errStream, "# Skipping unknown instruction %s\n", strings.ToUpper(instruction))
729
+		return nil
730
+	}
731
+
732
+	ret := method.Func.Call([]reflect.Value{reflect.ValueOf(b), reflect.ValueOf(arguments)})[0].Interface()
733
+	if ret != nil {
734
+		return ret.(error)
735
+	}
736
+
737
+	fmt.Fprintf(b.outStream, " ---> %s\n", utils.TruncateID(b.image))
738
+	return nil
739
+}
740
+
716 741
 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 {
717 742
 	return &buildFile{
718 743
 		runtime:       srv.runtime,
... ...
@@ -99,6 +99,7 @@ type Config struct {
99 99
 	WorkingDir      string
100 100
 	Entrypoint      []string
101 101
 	NetworkDisabled bool
102
+	OnBuild         []string
102 103
 }
103 104
 
104 105
 func ContainerConfigFromJob(job *engine.Job) *Config {
... ...
@@ -402,6 +402,64 @@ the image.
402 402
 The ``WORKDIR`` instruction sets the working directory in which
403 403
 the command given by ``CMD`` is executed.
404 404
 
405
+3.11 ONBUILD
406
+------------
407
+
408
+    ``ONBUILD [INSTRUCTION]``
409
+
410
+The ``ONBUILD`` instruction adds to the image a "trigger" instruction to be
411
+executed at a later time, when the image is used as the base for another build.
412
+The trigger will be executed in the context of the downstream build, as if it
413
+had been inserted immediately after the *FROM* instruction in the downstream
414
+Dockerfile.
415
+
416
+Any build instruction can be registered as a trigger.
417
+
418
+This is useful if you are building an image which will be used as a base to build
419
+other images, for example an application build environment or a daemon which may be
420
+customized with user-specific configuration.
421
+
422
+For example, if your image is a reusable python application builder, it will require
423
+application source code to be added in a particular directory, and it might require
424
+a build script to be called *after* that. You can't just call *ADD* and *RUN* now,
425
+because you don't yet have access to the application source code, and it will be
426
+different for each application build. You could simply provide application developers
427
+with a boilerplate Dockerfile to copy-paste into their application, but that is
428
+inefficient, error-prone and difficult to update because it mixes with
429
+application-specific code.
430
+
431
+The solution is to use *ONBUILD* to register in advance instructions to run later,
432
+during the next build stage.
433
+
434
+Here's how it works:
435
+
436
+1. When it encounters an *ONBUILD* instruction, the builder adds a trigger to
437
+   the metadata of the image being built.
438
+   The instruction does not otherwise affect the current build.
439
+
440
+2. At the end of the build, a list of all triggers is stored in the image manifest,
441
+   under the key *OnBuild*. They can be inspected with *docker inspect*.
442
+
443
+3. Later the image may be used as a base for a new build, using the *FROM* instruction.
444
+   As part of processing the *FROM* instruction, the downstream builder looks for *ONBUILD*
445
+   triggers, and executes them in the same order they were registered. If any of the
446
+   triggers fail, the *FROM* instruction is aborted which in turn causes the build
447
+   to fail. If all triggers succeed, the FROM instruction completes and the build
448
+   continues as usual.
449
+
450
+4. Triggers are cleared from the final image after being executed. In other words
451
+   they are not inherited by "grand-children" builds.
452
+
453
+For example you might add something like this:
454
+
455
+.. code-block:: bash
456
+
457
+    [...]
458
+    ONBUILD ADD . /app/src
459
+    ONBUILD RUN /usr/local/bin/python-build --dir /app/src
460
+    [...]
461
+
462
+
405 463
 .. _dockerfile_examples:
406 464
 
407 465
 4. Dockerfile Examples
... ...
@@ -847,3 +847,19 @@ func TestBuildFailsDockerfileEmpty(t *testing.T) {
847 847
 		t.Fatal("Expected: %v, got: %v", docker.ErrDockerfileEmpty, err)
848 848
 	}
849 849
 }
850
+
851
+func TestBuildOnBuildTrigger(t *testing.T) {
852
+	_, err := buildImage(testContextTemplate{`
853
+	from {IMAGE}
854
+	onbuild run echo here is the trigger
855
+	onbuild run touch foobar
856
+	`,
857
+		nil, nil,
858
+	},
859
+		t, nil, true,
860
+	)
861
+	if err != nil {
862
+		t.Fatal(err)
863
+	}
864
+	// FIXME: test that the 'foobar' file was created in the final build.
865
+}