Browse code

Add an experimental --mount flag to oc ex dockerbuild

Allows files to be bind mounted into the running container that will not
be part of the final build.

Clayton Coleman authored on 2016/07/20 13:34:33
Showing 9 changed files
... ...
@@ -10624,6 +10624,7 @@ _oc_ex_dockerbuild()
10624 10624
     flags+=("--dockerfile=")
10625 10625
     flags_with_completion+=("--dockerfile")
10626 10626
     flags_completion+=("_filedir")
10627
+    flags+=("--mount=")
10627 10628
     flags+=("--api-version=")
10628 10629
     flags+=("--as=")
10629 10630
     flags+=("--certificate-authority=")
... ...
@@ -14957,6 +14957,7 @@ _openshift_cli_ex_dockerbuild()
14957 14957
     flags+=("--dockerfile=")
14958 14958
     flags_with_completion+=("--dockerfile")
14959 14959
     flags_completion+=("_filedir")
14960
+    flags+=("--mount=")
14960 14961
     flags+=("--api-version=")
14961 14962
     flags+=("--as=")
14962 14963
     flags+=("--certificate-authority=")
... ...
@@ -10785,6 +10785,7 @@ _oc_ex_dockerbuild()
10785 10785
     flags+=("--dockerfile=")
10786 10786
     flags_with_completion+=("--dockerfile")
10787 10787
     flags_completion+=("_filedir")
10788
+    flags+=("--mount=")
10788 10789
     flags+=("--api-version=")
10789 10790
     flags+=("--as=")
10790 10791
     flags+=("--certificate-authority=")
... ...
@@ -15118,6 +15118,7 @@ _openshift_cli_ex_dockerbuild()
15118 15118
     flags+=("--dockerfile=")
15119 15119
     flags_with_completion+=("--dockerfile")
15120 15120
     flags_completion+=("_filedir")
15121
+    flags+=("--mount=")
15121 15122
     flags+=("--api-version=")
15122 15123
     flags+=("--as=")
15123 15124
     flags+=("--certificate-authority=")
... ...
@@ -1348,6 +1348,9 @@ Perform a direct Docker build
1348 1348
 ----
1349 1349
   # Build the current directory into a single layer and tag
1350 1350
   oc ex dockerbuild . myimage:latest
1351
+
1352
+  # Mount a client secret into the build at a certain path
1353
+  oc ex dockerbuild . myimage:latest --mount ~/mysecret.pem:/etc/pki/secret/mysecret.pem
1351 1354
 ----
1352 1355
 ====
1353 1356
 
... ...
@@ -17,7 +17,12 @@ Build a Dockerfile into a single layer
17 17
 
18 18
 .PP
19 19
 Builds the provided directory with a Dockerfile into a single layered image.
20
-Requires that you have a working connection to a Docker engine.
20
+Requires that you have a working connection to a Docker engine. You may mount
21
+secrets or config into the build with the \-\-mount flag \- these files will not
22
+be included in the final image.
23
+
24
+.PP
25
+Experimental: This command is under active development and may change without notice.
21 26
 
22 27
 
23 28
 .SH OPTIONS
... ...
@@ -29,6 +34,10 @@ Requires that you have a working connection to a Docker engine.
29 29
 \fB\-\-dockerfile\fP=""
30 30
     An optional path to a Dockerfile to use.
31 31
 
32
+.PP
33
+\fB\-\-mount\fP=[]
34
+    An optional list of files and directories to mount during the build. Use SRC:DST syntax for each path.
35
+
32 36
 
33 37
 .SH OPTIONS INHERITED FROM PARENT COMMANDS
34 38
 .PP
... ...
@@ -104,6 +113,9 @@ Requires that you have a working connection to a Docker engine.
104 104
   # Build the current directory into a single layer and tag
105 105
   oc ex dockerbuild . myimage:latest
106 106
 
107
+  # Mount a client secret into the build at a certain path
108
+  oc ex dockerbuild . myimage:latest \-\-mount \~/mysecret.pem:/etc/pki/secret/mysecret.pem
109
+
107 110
 .fi
108 111
 .RE
109 112
 
... ...
@@ -17,7 +17,12 @@ Build a Dockerfile into a single layer
17 17
 
18 18
 .PP
19 19
 Builds the provided directory with a Dockerfile into a single layered image.
20
-Requires that you have a working connection to a Docker engine.
20
+Requires that you have a working connection to a Docker engine. You may mount
21
+secrets or config into the build with the \-\-mount flag \- these files will not
22
+be included in the final image.
23
+
24
+.PP
25
+Experimental: This command is under active development and may change without notice.
21 26
 
22 27
 
23 28
 .SH OPTIONS
... ...
@@ -29,6 +34,10 @@ Requires that you have a working connection to a Docker engine.
29 29
 \fB\-\-dockerfile\fP=""
30 30
     An optional path to a Dockerfile to use.
31 31
 
32
+.PP
33
+\fB\-\-mount\fP=[]
34
+    An optional list of files and directories to mount during the build. Use SRC:DST syntax for each path.
35
+
32 36
 
33 37
 .SH OPTIONS INHERITED FROM PARENT COMMANDS
34 38
 .PP
... ...
@@ -104,6 +113,9 @@ Requires that you have a working connection to a Docker engine.
104 104
   # Build the current directory into a single layer and tag
105 105
   openshift cli ex dockerbuild . myimage:latest
106 106
 
107
+  # Mount a client secret into the build at a certain path
108
+  openshift cli ex dockerbuild . myimage:latest \-\-mount \~/mysecret.pem:/etc/pki/secret/mysecret.pem
109
+
107 110
 .fi
108 111
 .RE
109 112
 
... ...
@@ -25,10 +25,17 @@ const (
25 25
 Build a Dockerfile into a single layer
26 26
 
27 27
 Builds the provided directory with a Dockerfile into a single layered image.
28
-Requires that you have a working connection to a Docker engine.`
28
+Requires that you have a working connection to a Docker engine. You may mount
29
+secrets or config into the build with the --mount flag - these files will not
30
+be included in the final image.
31
+
32
+Experimental: This command is under active development and may change without notice.`
29 33
 
30 34
 	dockerbuildExample = `  # Build the current directory into a single layer and tag
31
-  %[1]s ex dockerbuild . myimage:latest`
35
+  %[1]s ex dockerbuild . myimage:latest
36
+
37
+  # Mount a client secret into the build at a certain path
38
+  %[1]s ex dockerbuild . myimage:latest --mount ~/mysecret.pem:/etc/pki/secret/mysecret.pem`
32 39
 )
33 40
 
34 41
 type DockerbuildOptions struct {
... ...
@@ -37,6 +44,9 @@ type DockerbuildOptions struct {
37 37
 
38 38
 	Client *docker.Client
39 39
 
40
+	MountSpecs []string
41
+
42
+	Mounts         []builder.Mount
40 43
 	Directory      string
41 44
 	Tag            string
42 45
 	DockerfilePath string
... ...
@@ -68,6 +78,7 @@ func NewCmdDockerbuild(fullName string, f *clientcmd.Factory, out, errOut io.Wri
68 68
 		},
69 69
 	}
70 70
 
71
+	cmd.Flags().StringSliceVar(&options.MountSpecs, "mount", options.MountSpecs, "An optional list of files and directories to mount during the build. Use SRC:DST syntax for each path.")
71 72
 	cmd.Flags().StringVar(&options.DockerfilePath, "dockerfile", options.DockerfilePath, "An optional path to a Dockerfile to use.")
72 73
 	cmd.Flags().BoolVar(&options.AllowPull, "allow-pull", true, "Pull the images that are not present.")
73 74
 	cmd.MarkFlagFilename("dockerfile")
... ...
@@ -89,6 +100,17 @@ func (o *DockerbuildOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command,
89 89
 	if len(o.DockerfilePath) == 0 {
90 90
 		o.DockerfilePath = filepath.Join(o.Directory, "Dockerfile")
91 91
 	}
92
+
93
+	var mounts []builder.Mount
94
+	for _, s := range o.MountSpecs {
95
+		segments := strings.Split(s, ":")
96
+		if len(segments) != 2 {
97
+			return kcmdutil.UsageError(cmd, "--mount must be of the form SOURCE:DEST")
98
+		}
99
+		mounts = append(mounts, builder.Mount{SourcePath: segments[0], DestinationPath: segments[1]})
100
+	}
101
+	o.Mounts = mounts
102
+
92 103
 	client, err := docker.NewClientFromEnv()
93 104
 	if err != nil {
94 105
 		return err
... ...
@@ -114,6 +136,7 @@ func (o *DockerbuildOptions) Run() error {
114 114
 	e.Out, e.ErrOut = o.Out, o.Err
115 115
 	e.AllowPull = o.AllowPull
116 116
 	e.Directory = o.Directory
117
+	e.TransientMounts = o.Mounts
117 118
 	e.Tag = o.Tag
118 119
 	e.AuthFn = o.Keyring.Lookup
119 120
 	e.LogFn = func(format string, args ...interface{}) {
... ...
@@ -8,7 +8,9 @@ import (
8 8
 	"io"
9 9
 	"os"
10 10
 	"path"
11
+	"path/filepath"
11 12
 	"runtime"
13
+	"strconv"
12 14
 	"strings"
13 15
 
14 16
 	"k8s.io/kubernetes/pkg/credentialprovider"
... ...
@@ -22,6 +24,12 @@ import (
22 22
 	"github.com/openshift/origin/pkg/util/docker/dockerfile/builder/imageprogress"
23 23
 )
24 24
 
25
+// Mount represents a binding between the current system and the destination client
26
+type Mount struct {
27
+	SourcePath      string
28
+	DestinationPath string
29
+}
30
+
25 31
 // ClientExecutor can run Docker builds from a Docker client.
26 32
 type ClientExecutor struct {
27 33
 	// Client is a client to a Docker daemon.
... ...
@@ -38,6 +46,11 @@ type ClientExecutor struct {
38 38
 	// AllowPull when set will pull images that are not present on
39 39
 	// the daemon.
40 40
 	AllowPull bool
41
+	// TransientMounts are a set of mounts from outside the build
42
+	// to the inside that will not be part of the final image. Any
43
+	// content created inside the mount's destinationPath will be
44
+	// omitted from the final image.
45
+	TransientMounts []Mount
41 46
 
42 47
 	Out, ErrOut io.Writer
43 48
 
... ...
@@ -125,6 +138,8 @@ func (e *ClientExecutor) Build(r io.Reader, args map[string]string) error {
125 125
 	e.LogFn("FROM %s", from)
126 126
 	glog.V(4).Infof("step: FROM %s", from)
127 127
 
128
+	var sharedMount string
129
+
128 130
 	// create a container to execute in, if necessary
129 131
 	mustStart := b.RequiresStart(node)
130 132
 	if e.Container == nil {
... ...
@@ -134,6 +149,23 @@ func (e *ClientExecutor) Build(r io.Reader, args map[string]string) error {
134 134
 			},
135 135
 		}
136 136
 		if mustStart {
137
+			// Transient mounts only make sense on images that will be running processes
138
+			if len(e.TransientMounts) > 0 {
139
+				volumeName, err := randSeq(imageSafeCharacters, 24)
140
+				if err != nil {
141
+					return err
142
+				}
143
+				v, err := e.Client.CreateVolume(docker.CreateVolumeOptions{Name: volumeName})
144
+				if err != nil {
145
+					return err
146
+				}
147
+				defer e.cleanupVolume(volumeName)
148
+				sharedMount = v.Mountpoint
149
+				opts.HostConfig = &docker.HostConfig{
150
+					Binds: []string{sharedMount + ":/tmp/__temporarymount"},
151
+				}
152
+			}
153
+
137 154
 			// TODO: windows support
138 155
 			if len(e.Command) > 0 {
139 156
 				opts.Config.Cmd = e.Command
... ...
@@ -157,9 +189,40 @@ func (e *ClientExecutor) Build(r io.Reader, args map[string]string) error {
157 157
 		defer e.Cleanup()
158 158
 	}
159 159
 
160
+	// copy any source content into the temporary mount path
161
+	if mustStart && len(e.TransientMounts) > 0 {
162
+		var copies []Copy
163
+		for i, mount := range e.TransientMounts {
164
+			source := mount.SourcePath
165
+			copies = append(copies, Copy{
166
+				Src:  source,
167
+				Dest: []string{path.Join("/tmp/__temporarymount", strconv.Itoa(i))},
168
+			})
169
+		}
170
+		if err := e.Copy(copies...); err != nil {
171
+			return err
172
+		}
173
+	}
174
+
160 175
 	// TODO: lazy start
161 176
 	if mustStart && !e.Container.State.Running {
162
-		if err := e.Client.StartContainer(e.Container.ID, e.HostConfig); err != nil {
177
+		var hostConfig docker.HostConfig
178
+		if e.HostConfig != nil {
179
+			hostConfig = *e.HostConfig
180
+		}
181
+
182
+		// mount individual items temporarily
183
+		for i, mount := range e.TransientMounts {
184
+			if len(sharedMount) == 0 {
185
+				return fmt.Errorf("no mount point available for temporary mounts")
186
+			}
187
+			hostConfig.Binds = append(
188
+				hostConfig.Binds,
189
+				fmt.Sprintf("%s:%s:%s", path.Join(sharedMount, strconv.Itoa(i)), mount.DestinationPath, "ro"),
190
+			)
191
+		}
192
+
193
+		if err := e.Client.StartContainer(e.Container.ID, &hostConfig); err != nil {
163 194
 			return err
164 195
 		}
165 196
 		// TODO: is this racy? may have to loop wait in the actual run step
... ...
@@ -276,6 +339,11 @@ func randSeq(source string, n int) (string, error) {
276 276
 	return string(random), nil
277 277
 }
278 278
 
279
+// cleanupVolume attempts to remove the provided volume
280
+func (e *ClientExecutor) cleanupVolume(name string) error {
281
+	return e.Client.RemoveVolume(name)
282
+}
283
+
279 284
 // CleanupImage attempts to remove the provided image.
280 285
 func (e *ClientExecutor) CleanupImage(name string) error {
281 286
 	return e.Client.RemoveImage(name)
... ...
@@ -448,7 +516,15 @@ func (e *ClientExecutor) Archive(src, dst string, allowDecompression, allowDownl
448 448
 			closer = append(closer, func() error { return os.RemoveAll(base) })
449 449
 		}
450 450
 	} else {
451
-		base = e.Directory
451
+		if filepath.IsAbs(src) {
452
+			base = filepath.Dir(src)
453
+			src, err = filepath.Rel(base, src)
454
+			if err != nil {
455
+				return nil, nil, err
456
+			}
457
+		} else {
458
+			base = e.Directory
459
+		}
452 460
 		infos, err = CalcCopyInfo(src, base, allowDecompression, true)
453 461
 	}
454 462
 	if err != nil {