diff --git a/cli/command/image/build/context.go b/cli/command/image/build/context.go index 5d0e96737d..e9af9bd358 100644 --- a/cli/command/image/build/context.go +++ b/cli/command/image/build/context.go @@ -41,6 +41,12 @@ func ValidateContextDirectory(srcPath string, excludes []string) error { if err != nil { return err } + + pm, err := fileutils.NewPatternMatcher(excludes) + if err != nil { + return err + } + return filepath.Walk(contextRoot, func(filePath string, f os.FileInfo, err error) error { if err != nil { if os.IsPermission(err) { @@ -55,7 +61,7 @@ func ValidateContextDirectory(srcPath string, excludes []string) error { // skip this directory/file if it's not in the path, it won't get added to the context if relFilePath, err := filepath.Rel(contextRoot, filePath); err != nil { return err - } else if skip, err := fileutils.Matches(relFilePath, excludes); err != nil { + } else if skip, err := filepathMatches(pm, relFilePath); err != nil { return err } else if skip { if f.IsDir() { @@ -81,6 +87,15 @@ func ValidateContextDirectory(srcPath string, excludes []string) error { }) } +func filepathMatches(matcher *fileutils.PatternMatcher, file string) (bool, error) { + file = filepath.Clean(file) + if file == "." { + // Don't let them exclude everything, kind of silly. + return false, nil + } + return matcher.Matches(file) +} + // DetectArchiveReader detects whether the input stream is an archive or a // Dockerfile and returns a buffered version of input, safe to consume in lieu // of input. If an archive is detected, isArchive is set to true, and to false diff --git a/cli/command/image/build/context_test.go b/cli/command/image/build/context_test.go index e0922542e7..de4d38f75d 100644 --- a/cli/command/image/build/context_test.go +++ b/cli/command/image/build/context_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/fileutils" "gotest.tools/assert" is "gotest.tools/assert/cmp" ) @@ -330,3 +331,86 @@ func TestDetectArchiveReader(t *testing.T) { assert.Check(t, is.Equal(testcase.expected, isArchive), testcase.file) } } + +func mustPatternMatcher(t *testing.T, patterns []string) *fileutils.PatternMatcher { + t.Helper() + pm, err := fileutils.NewPatternMatcher(patterns) + if err != nil { + t.Fatal("failed to construct pattern matcher: ", err) + } + return pm +} + +func TestWildcardMatches(t *testing.T) { + match, _ := filepathMatches(mustPatternMatcher(t, []string{"*"}), "fileutils.go") + if !match { + t.Errorf("failed to get a wildcard match, got %v", match) + } +} + +// A simple pattern match should return true. +func TestPatternMatches(t *testing.T) { + match, _ := filepathMatches(mustPatternMatcher(t, []string{"*.go"}), "fileutils.go") + if !match { + t.Errorf("failed to get a match, got %v", match) + } +} + +// An exclusion followed by an inclusion should return true. +func TestExclusionPatternMatchesPatternBefore(t *testing.T) { + match, _ := filepathMatches(mustPatternMatcher(t, []string{"!fileutils.go", "*.go"}), "fileutils.go") + if !match { + t.Errorf("failed to get true match on exclusion pattern, got %v", match) + } +} + +// A folder pattern followed by an exception should return false. +func TestPatternMatchesFolderExclusions(t *testing.T) { + match, _ := filepathMatches(mustPatternMatcher(t, []string{"docs", "!docs/README.md"}), "docs/README.md") + if match { + t.Errorf("failed to get a false match on exclusion pattern, got %v", match) + } +} + +// A folder pattern followed by an exception should return false. +func TestPatternMatchesFolderWithSlashExclusions(t *testing.T) { + match, _ := filepathMatches(mustPatternMatcher(t, []string{"docs/", "!docs/README.md"}), "docs/README.md") + if match { + t.Errorf("failed to get a false match on exclusion pattern, got %v", match) + } +} + +// A folder pattern followed by an exception should return false. +func TestPatternMatchesFolderWildcardExclusions(t *testing.T) { + match, _ := filepathMatches(mustPatternMatcher(t, []string{"docs/*", "!docs/README.md"}), "docs/README.md") + if match { + t.Errorf("failed to get a false match on exclusion pattern, got %v", match) + } +} + +// A pattern followed by an exclusion should return false. +func TestExclusionPatternMatchesPatternAfter(t *testing.T) { + match, _ := filepathMatches(mustPatternMatcher(t, []string{"*.go", "!fileutils.go"}), "fileutils.go") + if match { + t.Errorf("failed to get false match on exclusion pattern, got %v", match) + } +} + +// A filename evaluating to . should return false. +func TestExclusionPatternMatchesWholeDirectory(t *testing.T) { + match, _ := filepathMatches(mustPatternMatcher(t, []string{"*.go"}), ".") + if match { + t.Errorf("failed to get false match on ., got %v", match) + } +} + +// Matches with no patterns +func TestMatchesWithNoPatterns(t *testing.T) { + matches, err := filepathMatches(mustPatternMatcher(t, []string{}), "/any/path/there") + if err != nil { + t.Fatal(err) + } + if matches { + t.Fatalf("Should not have match anything") + } +}