Browse code

Fix .dockerignore when ignoring unreadable dirs

The initial `ValidateContextDirectory` implementation fails loudly when a file
lacks read permissions in the current context. However that situation is valid
if the file is included in the `.dockerignore` patterns.

Docker-DCO-1.1-Signed-off-by: Bruno ReniƩ <brutasse@gmail.com> (github: brutasse)

Bruno ReniƩ authored on 2014/07/07 21:23:07
Showing 7 changed files
... ...
@@ -163,30 +163,27 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
163 163
 		if _, err = os.Stat(filename); os.IsNotExist(err) {
164 164
 			return fmt.Errorf("no Dockerfile found in %s", cmd.Arg(0))
165 165
 		}
166
-		if err = utils.ValidateContextDirectory(root); err != nil {
166
+		var excludes []string
167
+		ignore, err := ioutil.ReadFile(path.Join(root, ".dockerignore"))
168
+		if err != nil && !os.IsNotExist(err) {
169
+			return fmt.Errorf("Error reading .dockerignore: '%s'", err)
170
+		}
171
+		for _, pattern := range strings.Split(string(ignore), "\n") {
172
+			ok, err := filepath.Match(pattern, "Dockerfile")
173
+			if err != nil {
174
+				return fmt.Errorf("Bad .dockerignore pattern: '%s', error: %s", pattern, err)
175
+			}
176
+			if ok {
177
+				return fmt.Errorf("Dockerfile was excluded by .dockerignore pattern '%s'", pattern)
178
+			}
179
+			excludes = append(excludes, pattern)
180
+		}
181
+		if err = utils.ValidateContextDirectory(root, excludes); err != nil {
167 182
 			return fmt.Errorf("Error checking context is accessible: '%s'. Please check permissions and try again.", err)
168 183
 		}
169 184
 		options := &archive.TarOptions{
170 185
 			Compression: archive.Uncompressed,
171
-		}
172
-		if ignore, err := ioutil.ReadFile(path.Join(root, ".dockerignore")); err != nil && !os.IsNotExist(err) {
173
-			return fmt.Errorf("Error reading .dockerignore: '%s'", err)
174
-		} else if err == nil {
175
-			for _, pattern := range strings.Split(string(ignore), "\n") {
176
-				if pattern == "" {
177
-					continue
178
-				}
179
-
180
-				ok, err := filepath.Match(pattern, "Dockerfile")
181
-				if err != nil {
182
-					utils.Errorf("Bad .dockerignore pattern: '%s', error: %s", pattern, err)
183
-					continue
184
-				}
185
-				if ok {
186
-					return fmt.Errorf("Dockerfile was excluded by .dockerignore pattern '%s'", pattern)
187
-				}
188
-				options.Excludes = append(options.Excludes, pattern)
189
-			}
186
+			Excludes:    excludes,
190 187
 		}
191 188
 		context, err = archive.TarWithOptions(root, options)
192 189
 		if err != nil {
... ...
@@ -349,23 +349,16 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
349 349
 					return nil
350 350
 				}
351 351
 
352
-				for _, exclude := range options.Excludes {
353
-					matched, err := filepath.Match(exclude, relFilePath)
354
-					if err != nil {
355
-						utils.Errorf("Error matching: %s (pattern: %s)", relFilePath, exclude)
356
-						return err
357
-					}
358
-					if matched {
359
-						if filepath.Clean(relFilePath) == "." {
360
-							utils.Errorf("Can't exclude whole path, excluding pattern: %s", exclude)
361
-							continue
362
-						}
363
-						utils.Debugf("Skipping excluded path: %s", relFilePath)
364
-						if f.IsDir() {
365
-							return filepath.SkipDir
366
-						}
367
-						return nil
352
+				skip, err := utils.Matches(relFilePath, options.Excludes)
353
+				if err != nil {
354
+					utils.Debugf("Error matching %s\n", relFilePath, err)
355
+					return nil
356
+				}
357
+				if skip {
358
+					if f.IsDir() {
359
+						return filepath.SkipDir
368 360
 					}
361
+					return nil
369 362
 				}
370 363
 
371 364
 				if err := addTarFile(filePath, relFilePath, tw, twBuf); err != nil {
372 365
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+directoryWeCantStat
0 1
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+FROM busybox
1
+ADD . /foo/
0 2
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+foo
... ...
@@ -475,9 +475,32 @@ func TestBuildWithInaccessibleFilesInContext(t *testing.T) {
475 475
 		deleteImages("testlinksok")
476 476
 
477 477
 	}
478
+	{
479
+		// This is used to ensure we don't try to add inaccessible files when they are ignored by a .dockerignore pattern
480
+		pathToInaccessibleDirectoryBuildDirectory := filepath.Join(buildDirectory, "ignoredinaccessible")
481
+		pathToDirectoryWithoutReadAccess := filepath.Join(pathToInaccessibleDirectoryBuildDirectory, "directoryWeCantStat")
482
+		pathToFileInDirectoryWithoutReadAccess := filepath.Join(pathToDirectoryWithoutReadAccess, "bar")
483
+		err := os.Chown(pathToDirectoryWithoutReadAccess, 0, 0)
484
+		errorOut(err, t, fmt.Sprintf("failed to chown directory to root: %s", err))
485
+		err = os.Chmod(pathToDirectoryWithoutReadAccess, 0444)
486
+		errorOut(err, t, fmt.Sprintf("failed to chmod directory to 755: %s", err))
487
+		err = os.Chmod(pathToFileInDirectoryWithoutReadAccess, 0700)
488
+		errorOut(err, t, fmt.Sprintf("failed to chmod file to 444: %s", err))
489
+
490
+		buildCommandStatement := fmt.Sprintf("%s build -t ignoredinaccessible .", dockerBinary)
491
+		buildCmd := exec.Command("su", "unprivilegeduser", "-c", buildCommandStatement)
492
+		buildCmd.Dir = pathToInaccessibleDirectoryBuildDirectory
493
+		out, exitCode, err := runCommandWithOutput(buildCmd)
494
+		if err != nil || exitCode != 0 {
495
+			t.Fatalf("build should have worked: %s %s", err, out)
496
+		}
497
+		deleteImages("ignoredinaccessible")
498
+
499
+	}
478 500
 	deleteImages("inaccessiblefiles")
479 501
 	logDone("build - ADD from context with inaccessible files must fail")
480 502
 	logDone("build - ADD from context with accessible links must work")
503
+	logDone("build - ADD from context with ignored inaccessible files must work")
481 504
 }
482 505
 
483 506
 func TestBuildForceRm(t *testing.T) {
... ...
@@ -684,16 +684,27 @@ func TreeSize(dir string) (size int64, err error) {
684 684
 // ValidateContextDirectory checks if all the contents of the directory
685 685
 // can be read and returns an error if some files can't be read
686 686
 // symlinks which point to non-existing files don't trigger an error
687
-func ValidateContextDirectory(srcPath string) error {
687
+func ValidateContextDirectory(srcPath string, excludes []string) error {
688 688
 	var finalError error
689 689
 
690 690
 	filepath.Walk(filepath.Join(srcPath, "."), func(filePath string, f os.FileInfo, err error) error {
691 691
 		// skip this directory/file if it's not in the path, it won't get added to the context
692
-		_, err = filepath.Rel(srcPath, filePath)
692
+		relFilePath, err := filepath.Rel(srcPath, filePath)
693 693
 		if err != nil && os.IsPermission(err) {
694 694
 			return nil
695 695
 		}
696 696
 
697
+		skip, err := Matches(relFilePath, excludes)
698
+		if err != nil {
699
+			finalError = err
700
+		}
701
+		if skip {
702
+			if f.IsDir() {
703
+				return filepath.SkipDir
704
+			}
705
+			return nil
706
+		}
707
+
697 708
 		if _, err := os.Stat(filePath); err != nil && os.IsPermission(err) {
698 709
 			finalError = fmt.Errorf("can't stat '%s'", filePath)
699 710
 			return err
... ...
@@ -726,3 +737,23 @@ func StringsContainsNoCase(slice []string, s string) bool {
726 726
 	}
727 727
 	return false
728 728
 }
729
+
730
+// Matches returns true if relFilePath matches any of the patterns
731
+func Matches(relFilePath string, patterns []string) (bool, error) {
732
+	for _, exclude := range patterns {
733
+		matched, err := filepath.Match(exclude, relFilePath)
734
+		if err != nil {
735
+			Errorf("Error matching: %s (pattern: %s)", relFilePath, exclude)
736
+			return false, err
737
+		}
738
+		if matched {
739
+			if filepath.Clean(relFilePath) == "." {
740
+				Errorf("Can't exclude whole path, excluding pattern: %s", exclude)
741
+				continue
742
+			}
743
+			Debugf("Skipping excluded path: %s", relFilePath)
744
+			return true, nil
745
+		}
746
+	}
747
+	return false, nil
748
+}