Browse code

Have .dockerignore support Dockerfile/.dockerignore

If .dockerignore mentions either then the client will send them to the
daemon but the daemon will erase them after the Dockerfile has been parsed
to simulate them never being sent in the first place.

an events test kept failing for me so I tried to fix that too

Closes #8330

Signed-off-by: Doug Davis <dug@us.ibm.com>

Doug Davis authored on 2014/10/24 06:30:11
Showing 16 changed files
... ...
@@ -14,7 +14,6 @@ import (
14 14
 	"os"
15 15
 	"os/exec"
16 16
 	"path"
17
-	"path/filepath"
18 17
 	"runtime"
19 18
 	"strconv"
20 19
 	"strings"
... ...
@@ -30,6 +29,7 @@ import (
30 30
 	"github.com/docker/docker/nat"
31 31
 	"github.com/docker/docker/opts"
32 32
 	"github.com/docker/docker/pkg/archive"
33
+	"github.com/docker/docker/pkg/fileutils"
33 34
 	flag "github.com/docker/docker/pkg/mflag"
34 35
 	"github.com/docker/docker/pkg/parsers"
35 36
 	"github.com/docker/docker/pkg/parsers/filters"
... ...
@@ -143,32 +143,32 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
143 143
 		if _, err = os.Stat(filename); os.IsNotExist(err) {
144 144
 			return fmt.Errorf("no Dockerfile found in %s", cmd.Arg(0))
145 145
 		}
146
-		var excludes []string
147
-		ignore, err := ioutil.ReadFile(path.Join(root, ".dockerignore"))
148
-		if err != nil && !os.IsNotExist(err) {
149
-			return fmt.Errorf("Error reading .dockerignore: '%s'", err)
146
+		var includes []string = []string{"."}
147
+
148
+		excludes, err := utils.ReadDockerIgnore(path.Join(root, ".dockerignore"))
149
+		if err != nil {
150
+			return err
150 151
 		}
151
-		for _, pattern := range strings.Split(string(ignore), "\n") {
152
-			pattern = strings.TrimSpace(pattern)
153
-			if pattern == "" {
154
-				continue
155
-			}
156
-			pattern = filepath.Clean(pattern)
157
-			ok, err := filepath.Match(pattern, "Dockerfile")
158
-			if err != nil {
159
-				return fmt.Errorf("Bad .dockerignore pattern: '%s', error: %s", pattern, err)
160
-			}
161
-			if ok {
162
-				return fmt.Errorf("Dockerfile was excluded by .dockerignore pattern '%s'", pattern)
163
-			}
164
-			excludes = append(excludes, pattern)
152
+
153
+		// If .dockerignore mentions .dockerignore or Dockerfile
154
+		// then make sure we send both files over to the daemon
155
+		// because Dockerfile is, obviously, needed no matter what, and
156
+		// .dockerignore is needed to know if either one needs to be
157
+		// removed.  The deamon will remove them for us, if needed, after it
158
+		// parses the Dockerfile.
159
+		keepThem1, _ := fileutils.Matches(".dockerignore", excludes)
160
+		keepThem2, _ := fileutils.Matches("Dockerfile", excludes)
161
+		if keepThem1 || keepThem2 {
162
+			includes = append(includes, ".dockerignore", "Dockerfile")
165 163
 		}
164
+
166 165
 		if err = utils.ValidateContextDirectory(root, excludes); err != nil {
167 166
 			return fmt.Errorf("Error checking context is accessible: '%s'. Please check permissions and try again.", err)
168 167
 		}
169 168
 		options := &archive.TarOptions{
170
-			Compression: archive.Uncompressed,
171
-			Excludes:    excludes,
169
+			Compression:     archive.Uncompressed,
170
+			ExcludePatterns: excludes,
171
+			IncludeFiles:    includes,
172 172
 		}
173 173
 		context, err = archive.TarWithOptions(root, options)
174 174
 		if err != nil {
... ...
@@ -31,6 +31,7 @@ import (
31 31
 	"github.com/docker/docker/builder/parser"
32 32
 	"github.com/docker/docker/daemon"
33 33
 	"github.com/docker/docker/engine"
34
+	"github.com/docker/docker/pkg/fileutils"
34 35
 	"github.com/docker/docker/pkg/tarsum"
35 36
 	"github.com/docker/docker/registry"
36 37
 	"github.com/docker/docker/runconfig"
... ...
@@ -136,30 +137,10 @@ func (b *Builder) Run(context io.Reader) (string, error) {
136 136
 		}
137 137
 	}()
138 138
 
139
-	filename := path.Join(b.contextPath, "Dockerfile")
140
-
141
-	fi, err := os.Stat(filename)
142
-	if os.IsNotExist(err) {
143
-		return "", fmt.Errorf("Cannot build a directory without a Dockerfile")
144
-	}
145
-	if fi.Size() == 0 {
146
-		return "", ErrDockerfileEmpty
147
-	}
148
-
149
-	f, err := os.Open(filename)
150
-	if err != nil {
139
+	if err := b.readDockerfile("Dockerfile"); err != nil {
151 140
 		return "", err
152 141
 	}
153 142
 
154
-	defer f.Close()
155
-
156
-	ast, err := parser.Parse(f)
157
-	if err != nil {
158
-		return "", err
159
-	}
160
-
161
-	b.dockerfile = ast
162
-
163 143
 	// some initializations that would not have been supplied by the caller.
164 144
 	b.Config = &runconfig.Config{}
165 145
 	b.TmpContainers = map[string]struct{}{}
... ...
@@ -185,6 +166,53 @@ func (b *Builder) Run(context io.Reader) (string, error) {
185 185
 	return b.image, nil
186 186
 }
187 187
 
188
+// Reads a Dockerfile from the current context. It assumes that the
189
+// 'filename' is a relative path from the root of the context
190
+func (b *Builder) readDockerfile(filename string) error {
191
+	filename = path.Join(b.contextPath, filename)
192
+
193
+	fi, err := os.Stat(filename)
194
+	if os.IsNotExist(err) {
195
+		return fmt.Errorf("Cannot build a directory without a Dockerfile")
196
+	}
197
+	if fi.Size() == 0 {
198
+		return ErrDockerfileEmpty
199
+	}
200
+
201
+	f, err := os.Open(filename)
202
+	if err != nil {
203
+		return err
204
+	}
205
+
206
+	b.dockerfile, err = parser.Parse(f)
207
+	f.Close()
208
+
209
+	if err != nil {
210
+		return err
211
+	}
212
+
213
+	// After the Dockerfile has been parsed, we need to check the .dockerignore
214
+	// file for either "Dockerfile" or ".dockerignore", and if either are
215
+	// present then erase them from the build context. These files should never
216
+	// have been sent from the client but we did send them to make sure that
217
+	// we had the Dockerfile to actually parse, and then we also need the
218
+	// .dockerignore file to know whether either file should be removed.
219
+	// Note that this assumes the Dockerfile has been read into memory and
220
+	// is now safe to be removed.
221
+
222
+	excludes, _ := utils.ReadDockerIgnore(path.Join(b.contextPath, ".dockerignore"))
223
+	if rm, _ := fileutils.Matches(".dockerignore", excludes); rm == true {
224
+		os.Remove(path.Join(b.contextPath, ".dockerignore"))
225
+		b.context.(tarsum.BuilderContext).Remove(".dockerignore")
226
+	}
227
+	if rm, _ := fileutils.Matches("Dockerfile", excludes); rm == true {
228
+		os.Remove(path.Join(b.contextPath, "Dockerfile"))
229
+		b.context.(tarsum.BuilderContext).Remove("Dockerfile")
230
+	}
231
+
232
+	return nil
233
+}
234
+
188 235
 // This method is the entrypoint to all statement handling routines.
189 236
 //
190 237
 // Almost all nodes will have this structure:
... ...
@@ -390,7 +390,15 @@ func calcCopyInfo(b *Builder, cmdName string, cInfos *[]*copyInfo, origPath stri
390 390
 
391 391
 	for _, fileInfo := range b.context.GetSums() {
392 392
 		absFile := path.Join(b.contextPath, fileInfo.Name())
393
-		if strings.HasPrefix(absFile, absOrigPath) || absFile == absOrigPathNoSlash {
393
+		// Any file in the context that starts with the given path will be
394
+		// picked up and its hashcode used.  However, we'll exclude the
395
+		// root dir itself.  We do this for a coupel of reasons:
396
+		// 1 - ADD/COPY will not copy the dir itself, just its children
397
+		//     so there's no reason to include it in the hash calc
398
+		// 2 - the metadata on the dir will change when any child file
399
+		//     changes.  This will lead to a miss in the cache check if that
400
+		//     child file is in the .dockerignore list.
401
+		if strings.HasPrefix(absFile, absOrigPath) && absFile != absOrigPathNoSlash {
394 402
 			subfiles = append(subfiles, fileInfo.Sum())
395 403
 		}
396 404
 	}
... ...
@@ -891,8 +891,8 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) {
891 891
 	}
892 892
 
893 893
 	archive, err := archive.TarWithOptions(basePath, &archive.TarOptions{
894
-		Compression: archive.Uncompressed,
895
-		Includes:    filter,
894
+		Compression:  archive.Uncompressed,
895
+		IncludeFiles: filter,
896 896
 	})
897 897
 	if err != nil {
898 898
 		container.Unmount()
... ...
@@ -300,8 +300,8 @@ func (a *Driver) Put(id string) {
300 300
 func (a *Driver) Diff(id, parent string) (archive.Archive, error) {
301 301
 	// AUFS doesn't need the parent layer to produce a diff.
302 302
 	return archive.TarWithOptions(path.Join(a.rootPath(), "diff", id), &archive.TarOptions{
303
-		Compression: archive.Uncompressed,
304
-		Excludes:    []string{".wh..wh.*"},
303
+		Compression:     archive.Uncompressed,
304
+		ExcludePatterns: []string{".wh..wh.*"},
305 305
 	})
306 306
 }
307 307
 
... ...
@@ -154,6 +154,12 @@ Exclusion patterns match files or directories relative to the source repository
154 154
 that will be excluded from the context. Globbing is done using Go's
155 155
 [filepath.Match](http://golang.org/pkg/path/filepath#Match) rules.
156 156
 
157
+> **Note**:
158
+> The `.dockerignore` file can even be used to ignore the `Dockerfile` and
159
+> `.dockerignore` files. This might be useful if you are copying files from
160
+> the root of the build context into your new containter but do not want to 
161
+> include the `Dockerfile` or `.dockerignore` files (e.g. `ADD . /someDir/`).
162
+
157 163
 The following example shows the use of the `.dockerignore` file to exclude the
158 164
 `.git` directory from the context. Its effect can be seen in the changed size of
159 165
 the uploaded context.
... ...
@@ -57,7 +57,7 @@ func (s *TagStore) CmdLoad(job *engine.Job) engine.Status {
57 57
 		excludes[i] = k
58 58
 		i++
59 59
 	}
60
-	if err := chrootarchive.Untar(repoFile, repoDir, &archive.TarOptions{Excludes: excludes}); err != nil {
60
+	if err := chrootarchive.Untar(repoFile, repoDir, &archive.TarOptions{ExcludePatterns: excludes}); err != nil {
61 61
 		return job.Error(err)
62 62
 	}
63 63
 
... ...
@@ -3131,28 +3131,106 @@ func TestBuildDockerignoringDockerfile(t *testing.T) {
3131 3131
 	name := "testbuilddockerignoredockerfile"
3132 3132
 	defer deleteImages(name)
3133 3133
 	dockerfile := `
3134
-        FROM scratch`
3134
+        FROM busybox
3135
+		ADD . /tmp/
3136
+		RUN ! ls /tmp/Dockerfile
3137
+		RUN ls /tmp/.dockerignore`
3135 3138
 	ctx, err := fakeContext(dockerfile, map[string]string{
3136
-		"Dockerfile":    "FROM scratch",
3139
+		"Dockerfile":    dockerfile,
3137 3140
 		".dockerignore": "Dockerfile\n",
3138 3141
 	})
3139 3142
 	if err != nil {
3140 3143
 		t.Fatal(err)
3141 3144
 	}
3142
-	defer ctx.Close()
3143
-	if _, err = buildImageFromContext(name, ctx, true); err == nil {
3144
-		t.Fatalf("Didn't get expected error from ignoring Dockerfile")
3145
+	if _, err = buildImageFromContext(name, ctx, true); err != nil {
3146
+		t.Fatalf("Didn't ignore Dockerfile correctly:%s", err)
3145 3147
 	}
3146 3148
 
3147 3149
 	// now try it with ./Dockerfile
3148 3150
 	ctx.Add(".dockerignore", "./Dockerfile\n")
3149
-	if _, err = buildImageFromContext(name, ctx, true); err == nil {
3150
-		t.Fatalf("Didn't get expected error from ignoring ./Dockerfile")
3151
+	if _, err = buildImageFromContext(name, ctx, true); err != nil {
3152
+		t.Fatalf("Didn't ignore ./Dockerfile correctly:%s", err)
3151 3153
 	}
3152 3154
 
3153 3155
 	logDone("build - test .dockerignore of Dockerfile")
3154 3156
 }
3155 3157
 
3158
+func TestBuildDockerignoringDockerignore(t *testing.T) {
3159
+	name := "testbuilddockerignoredockerignore"
3160
+	defer deleteImages(name)
3161
+	dockerfile := `
3162
+        FROM busybox
3163
+		ADD . /tmp/
3164
+		RUN ! ls /tmp/.dockerignore
3165
+		RUN ls /tmp/Dockerfile`
3166
+	ctx, err := fakeContext(dockerfile, map[string]string{
3167
+		"Dockerfile":    dockerfile,
3168
+		".dockerignore": ".dockerignore\n",
3169
+	})
3170
+	defer ctx.Close()
3171
+	if err != nil {
3172
+		t.Fatal(err)
3173
+	}
3174
+	if _, err = buildImageFromContext(name, ctx, true); err != nil {
3175
+		t.Fatalf("Didn't ignore .dockerignore correctly:%s", err)
3176
+	}
3177
+	logDone("build - test .dockerignore of .dockerignore")
3178
+}
3179
+
3180
+func TestBuildDockerignoreTouchDockerfile(t *testing.T) {
3181
+	var id1 string
3182
+	var id2 string
3183
+
3184
+	name := "testbuilddockerignoretouchdockerfile"
3185
+	defer deleteImages(name)
3186
+	dockerfile := `
3187
+        FROM busybox
3188
+		ADD . /tmp/`
3189
+	ctx, err := fakeContext(dockerfile, map[string]string{
3190
+		"Dockerfile":    dockerfile,
3191
+		".dockerignore": "Dockerfile\n",
3192
+	})
3193
+	defer ctx.Close()
3194
+	if err != nil {
3195
+		t.Fatal(err)
3196
+	}
3197
+
3198
+	if id1, err = buildImageFromContext(name, ctx, true); err != nil {
3199
+		t.Fatalf("Didn't build it correctly:%s", err)
3200
+	}
3201
+
3202
+	if id2, err = buildImageFromContext(name, ctx, true); err != nil {
3203
+		t.Fatalf("Didn't build it correctly:%s", err)
3204
+	}
3205
+	if id1 != id2 {
3206
+		t.Fatalf("Didn't use the cache - 1")
3207
+	}
3208
+
3209
+	// Now make sure touching Dockerfile doesn't invalidate the cache
3210
+	if err = ctx.Add("Dockerfile", dockerfile+"\n# hi"); err != nil {
3211
+		t.Fatalf("Didn't add Dockerfile: %s", err)
3212
+	}
3213
+	if id2, err = buildImageFromContext(name, ctx, true); err != nil {
3214
+		t.Fatalf("Didn't build it correctly:%s", err)
3215
+	}
3216
+	if id1 != id2 {
3217
+		t.Fatalf("Didn't use the cache - 2")
3218
+	}
3219
+
3220
+	// One more time but just 'touch' it instead of changing the content
3221
+	if err = ctx.Add("Dockerfile", dockerfile+"\n# hi"); err != nil {
3222
+		t.Fatalf("Didn't add Dockerfile: %s", err)
3223
+	}
3224
+	if id2, err = buildImageFromContext(name, ctx, true); err != nil {
3225
+		t.Fatalf("Didn't build it correctly:%s", err)
3226
+	}
3227
+	if id1 != id2 {
3228
+		t.Fatalf("Didn't use the cache - 3")
3229
+	}
3230
+
3231
+	logDone("build - test .dockerignore touch dockerfile")
3232
+}
3233
+
3156 3234
 func TestBuildDockerignoringWholeDir(t *testing.T) {
3157 3235
 	name := "testbuilddockerignorewholedir"
3158 3236
 	defer deleteImages(name)
... ...
@@ -10,14 +10,13 @@ import (
10 10
 )
11 11
 
12 12
 func TestEventsUntag(t *testing.T) {
13
-	out, _, _ := dockerCmd(t, "images", "-q")
14
-	image := strings.Split(out, "\n")[0]
13
+	image := "busybox"
15 14
 	dockerCmd(t, "tag", image, "utest:tag1")
16 15
 	dockerCmd(t, "tag", image, "utest:tag2")
17 16
 	dockerCmd(t, "rmi", "utest:tag1")
18 17
 	dockerCmd(t, "rmi", "utest:tag2")
19 18
 	eventsCmd := exec.Command("timeout", "0.2", dockerBinary, "events", "--since=1")
20
-	out, _, _ = runCommandWithOutput(eventsCmd)
19
+	out, _, _ := runCommandWithOutput(eventsCmd)
21 20
 	events := strings.Split(out, "\n")
22 21
 	nEvents := len(events)
23 22
 	// The last element after the split above will be an empty string, so we
... ...
@@ -30,11 +30,11 @@ type (
30 30
 	ArchiveReader io.Reader
31 31
 	Compression   int
32 32
 	TarOptions    struct {
33
-		Includes    []string
34
-		Excludes    []string
35
-		Compression Compression
36
-		NoLchown    bool
37
-		Name        string
33
+		IncludeFiles    []string
34
+		ExcludePatterns []string
35
+		Compression     Compression
36
+		NoLchown        bool
37
+		Name            string
38 38
 	}
39 39
 
40 40
 	// Archiver allows the reuse of most utility functions of this package
... ...
@@ -378,7 +378,7 @@ func escapeName(name string) string {
378 378
 }
379 379
 
380 380
 // TarWithOptions creates an archive from the directory at `path`, only including files whose relative
381
-// paths are included in `options.Includes` (if non-nil) or not in `options.Excludes`.
381
+// paths are included in `options.IncludeFiles` (if non-nil) or not in `options.ExcludePatterns`.
382 382
 func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) {
383 383
 	pipeReader, pipeWriter := io.Pipe()
384 384
 
... ...
@@ -401,12 +401,14 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
401 401
 		// mutating the filesystem and we can see transient errors
402 402
 		// from this
403 403
 
404
-		if options.Includes == nil {
405
-			options.Includes = []string{"."}
404
+		if options.IncludeFiles == nil {
405
+			options.IncludeFiles = []string{"."}
406 406
 		}
407 407
 
408
+		seen := make(map[string]bool)
409
+
408 410
 		var renamedRelFilePath string // For when tar.Options.Name is set
409
-		for _, include := range options.Includes {
411
+		for _, include := range options.IncludeFiles {
410 412
 			filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error {
411 413
 				if err != nil {
412 414
 					log.Debugf("Tar: Can't stat file %s to tar: %s", srcPath, err)
... ...
@@ -420,10 +422,19 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
420 420
 					return nil
421 421
 				}
422 422
 
423
-				skip, err := fileutils.Matches(relFilePath, options.Excludes)
424
-				if err != nil {
425
-					log.Debugf("Error matching %s", relFilePath, err)
426
-					return err
423
+				skip := false
424
+
425
+				// If "include" is an exact match for the current file
426
+				// then even if there's an "excludePatterns" pattern that
427
+				// matches it, don't skip it. IOW, assume an explicit 'include'
428
+				// is asking for that file no matter what - which is true
429
+				// for some files, like .dockerignore and Dockerfile (sometimes)
430
+				if include != relFilePath {
431
+					skip, err = fileutils.Matches(relFilePath, options.ExcludePatterns)
432
+					if err != nil {
433
+						log.Debugf("Error matching %s", relFilePath, err)
434
+						return err
435
+					}
427 436
 				}
428 437
 
429 438
 				if skip {
... ...
@@ -433,6 +444,11 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
433 433
 					return nil
434 434
 				}
435 435
 
436
+				if seen[relFilePath] {
437
+					return nil
438
+				}
439
+				seen[relFilePath] = true
440
+
436 441
 				// Rename the base resource
437 442
 				if options.Name != "" && filePath == srcPath+"/"+filepath.Base(relFilePath) {
438 443
 					renamedRelFilePath = relFilePath
... ...
@@ -487,7 +503,7 @@ loop:
487 487
 		// This keeps "../" as-is, but normalizes "/../" to "/"
488 488
 		hdr.Name = filepath.Clean(hdr.Name)
489 489
 
490
-		for _, exclude := range options.Excludes {
490
+		for _, exclude := range options.ExcludePatterns {
491 491
 			if strings.HasPrefix(hdr.Name, exclude) {
492 492
 				continue loop
493 493
 			}
... ...
@@ -563,8 +579,8 @@ func Untar(archive io.Reader, dest string, options *TarOptions) error {
563 563
 	if options == nil {
564 564
 		options = &TarOptions{}
565 565
 	}
566
-	if options.Excludes == nil {
567
-		options.Excludes = []string{}
566
+	if options.ExcludePatterns == nil {
567
+		options.ExcludePatterns = []string{}
568 568
 	}
569 569
 	decompressedArchive, err := DecompressStream(archive)
570 570
 	if err != nil {
... ...
@@ -165,8 +165,8 @@ func TestTarUntar(t *testing.T) {
165 165
 		Gzip,
166 166
 	} {
167 167
 		changes, err := tarUntar(t, origin, &TarOptions{
168
-			Compression: c,
169
-			Excludes:    []string{"3"},
168
+			Compression:     c,
169
+			ExcludePatterns: []string{"3"},
170 170
 		})
171 171
 
172 172
 		if err != nil {
... ...
@@ -196,8 +196,8 @@ func TestTarWithOptions(t *testing.T) {
196 196
 		opts       *TarOptions
197 197
 		numChanges int
198 198
 	}{
199
-		{&TarOptions{Includes: []string{"1"}}, 1},
200
-		{&TarOptions{Excludes: []string{"2"}}, 1},
199
+		{&TarOptions{IncludeFiles: []string{"1"}}, 1},
200
+		{&TarOptions{ExcludePatterns: []string{"2"}}, 1},
201 201
 	}
202 202
 	for _, testCase := range cases {
203 203
 		changes, err := tarUntar(t, origin, testCase.opts)
... ...
@@ -50,8 +50,8 @@ func Untar(tarArchive io.Reader, dest string, options *archive.TarOptions) error
50 50
 	if options == nil {
51 51
 		options = &archive.TarOptions{}
52 52
 	}
53
-	if options.Excludes == nil {
54
-		options.Excludes = []string{}
53
+	if options.ExcludePatterns == nil {
54
+		options.ExcludePatterns = []string{}
55 55
 	}
56 56
 
57 57
 	var (
... ...
@@ -40,7 +40,7 @@ func TestChrootTarUntar(t *testing.T) {
40 40
 	if err := os.MkdirAll(dest, 0700); err != nil {
41 41
 		t.Fatal(err)
42 42
 	}
43
-	if err := Untar(stream, dest, &archive.TarOptions{Excludes: []string{"lolo"}}); err != nil {
43
+	if err := Untar(stream, dest, &archive.TarOptions{ExcludePatterns: []string{"lolo"}}); err != nil {
44 44
 		t.Fatal(err)
45 45
 	}
46 46
 }
47 47
new file mode 100644
... ...
@@ -0,0 +1,20 @@
0
+package tarsum
1
+
2
+// This interface extends TarSum by adding the Remove method.  In general
3
+// there was concern about adding this method to TarSum itself so instead
4
+// it is being added just to "BuilderContext" which will then only be used
5
+// during the .dockerignore file processing - see builder/evaluator.go
6
+type BuilderContext interface {
7
+	TarSum
8
+	Remove(string)
9
+}
10
+
11
+func (bc *tarSum) Remove(filename string) {
12
+	for i, fis := range bc.sums {
13
+		if fis.Name() == filename {
14
+			bc.sums = append(bc.sums[:i], bc.sums[i+1:]...)
15
+			// Note, we don't just return because there could be
16
+			// more than one with this name
17
+		}
18
+	}
19
+}
... ...
@@ -1,6 +1,7 @@
1 1
 package utils
2 2
 
3 3
 import (
4
+	"bufio"
4 5
 	"bytes"
5 6
 	"crypto/rand"
6 7
 	"crypto/sha1"
... ...
@@ -492,3 +493,34 @@ func StringsContainsNoCase(slice []string, s string) bool {
492 492
 	}
493 493
 	return false
494 494
 }
495
+
496
+// Reads a .dockerignore file and returns the list of file patterns
497
+// to ignore. Note this will trim whitespace from each line as well
498
+// as use GO's "clean" func to get the shortest/cleanest path for each.
499
+func ReadDockerIgnore(path string) ([]string, error) {
500
+	// Note that a missing .dockerignore file isn't treated as an error
501
+	reader, err := os.Open(path)
502
+	if err != nil {
503
+		if !os.IsNotExist(err) {
504
+			return nil, fmt.Errorf("Error reading '%s': %v", path, err)
505
+		}
506
+		return nil, nil
507
+	}
508
+	defer reader.Close()
509
+
510
+	scanner := bufio.NewScanner(reader)
511
+	var excludes []string
512
+
513
+	for scanner.Scan() {
514
+		pattern := strings.TrimSpace(scanner.Text())
515
+		if pattern == "" {
516
+			continue
517
+		}
518
+		pattern = filepath.Clean(pattern)
519
+		excludes = append(excludes, pattern)
520
+	}
521
+	if err = scanner.Err(); err != nil {
522
+		return nil, fmt.Errorf("Error reading '%s': %v", path, err)
523
+	}
524
+	return excludes, nil
525
+}
... ...
@@ -47,9 +47,9 @@ func (v *Volume) Export(resource, name string) (io.ReadCloser, error) {
47 47
 		basePath = path.Dir(basePath)
48 48
 	}
49 49
 	return archive.TarWithOptions(basePath, &archive.TarOptions{
50
-		Compression: archive.Uncompressed,
51
-		Name:        name,
52
-		Includes:    filter,
50
+		Compression:  archive.Uncompressed,
51
+		Name:         name,
52
+		IncludeFiles: filter,
53 53
 	})
54 54
 }
55 55