Repository: datreeio/datree
Pull request #932 (2023-05-03 17:48:46)

feat: exclude paths regex
related issues - #732 

adifayer: @Meyazhagan  Thank you for raising this PR 🤩 We will review it in the upcoming days

Patch:
diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md
index d93fe67..c575773 100644
--- a/DEVELOPER_GUIDE.md
+++ b/DEVELOPER_GUIDE.md
@@ -49,7 +49,7 @@ command, add a folder with the cobra command, and use it in the `cmd/root.go` fi
 
 Datree requires an internet connection to connect to our backend API.  
 While developing locally, API requests will reach our staging environment and be visible on
-the [Staging Dashboard](https://app.staging.datree.com).  
+the [Staging Dashboard](https://app.staging.datree.io).  
 All available API requests can be found under `pkg/cliClient`
 
 #### manual testing
diff --git a/bl/validation/k8sValidator.go b/bl/validation/k8sValidator.go
index 0a352c2..2a9c5c7 100644
--- a/bl/validation/k8sValidator.go
+++ b/bl/validation/k8sValidator.go
@@ -139,6 +139,10 @@ func (val *K8sValidator) GetK8sFiles(filesConfigurationsChan chan *extractor.Fil
 }
 
 func (val *K8sValidator) isK8sFile(fileConfigurations []extractor.Configuration) bool {
+	// if the file is empty, the array is empty and doesn't enter the for loop
+	if len(fileConfigurations) == 0 {
+		return false
+	}
 	for _, configuration := range fileConfigurations {
 		has_apiVersion := configuration.ApiVersion != ""
 		has_kind := configuration.Kind != ""
diff --git a/pkg/fileReader/reader.go b/pkg/fileReader/reader.go
index 3683523..8507dc3 100644
--- a/pkg/fileReader/reader.go
+++ b/pkg/fileReader/reader.go
@@ -3,6 +3,7 @@ package fileReader
 import (
 	"os"
 	"path/filepath"
+	"regexp"
 
 	"github.com/bmatcuk/doublestar/v2"
 )
@@ -55,16 +56,22 @@ func CreateFileReader(opts *FileReaderOptions) *FileReader {
 	return fileReader
 }
 
-func (fr *FileReader) FilterFiles(paths []string) ([]string, error) {
+func (fr *FileReader) FilterFiles(paths []string, excludePattern string) ([]string, error) {
 	var filePaths []string
 
+	excludeRegex, err := regexp.Compile(excludePattern)
+	if err != nil {
+		return nil, err
+	}
+
 	for _, path := range paths {
 		stat, err := fr.stat(path)
 		if err != nil {
 			return []string{}, err
 		}
 
-		if !stat.IsDir() {
+		isMatched := excludePattern != "" && excludeRegex.MatchString(path)
+		if !stat.IsDir() && !isMatched {
 			filePaths = append(filePaths, path)
 		}
 	}


Test patch:
diff --git a/cmd/kustomize/main_test.go b/cmd/kustomize/main_test.go
index 3287df8..d1c04fc 100644
--- a/cmd/kustomize/main_test.go
+++ b/cmd/kustomize/main_test.go
@@ -179,7 +179,7 @@ type ReaderMock struct {
 	mock.Mock
 }
 
-func (rm *ReaderMock) FilterFiles(paths []string) ([]string, error) {
+func (rm *ReaderMock) FilterFiles(paths []string, excludePattern string) ([]string, error) {
 	args := rm.Called(paths)
 	return args.Get(0).([]string), nil
 }
diff --git a/cmd/test/main.go b/cmd/test/main.go
index 4fb36d3..269c9a1 100644
--- a/cmd/test/main.go
+++ b/cmd/test/main.go
@@ -52,6 +52,7 @@ type K8sValidator interface {
 type TestCommandFlags struct {
 	Output               string
 	K8sVersion           string
+	ExcludePattern       string
 	IgnoreMissingSchemas bool
 	OnlyK8sFiles         bool
 	Verbose              bool
@@ -70,6 +71,7 @@ func NewTestCommandFlags() *TestCommandFlags {
 	return &TestCommandFlags{
 		Output:               "",
 		K8sVersion:           "",
+		ExcludePattern:       "",
 		IgnoreMissingSchemas: false,
 		OnlyK8sFiles:         false,
 		Verbose:              false,
@@ -102,7 +104,6 @@ func (flags *TestCommandFlags) Validate() error {
 	}
 
 	err := validateK8sVersionFormatIfProvided(flags.K8sVersion)
-
 	if err != nil {
 		return err
 	}
@@ -112,6 +113,11 @@ func (flags *TestCommandFlags) Validate() error {
 		return err
 	}
 
+	_, err = regexp.Compile(flags.ExcludePattern)
+	if err != nil {
+		return fmt.Errorf("invalid --exclude flag: " + err.Error())
+	}
+
 	return nil
 
 }
@@ -127,7 +133,7 @@ type EvaluationPrinter interface {
 }
 
 type Reader interface {
-	FilterFiles(paths []string) ([]string, error)
+	FilterFiles(paths []string, excludePattern string) ([]string, error)
 }
 
 type LocalConfig interface {
@@ -144,6 +150,7 @@ type CliClient interface {
 type TestCommandData struct {
 	Output                string
 	K8sVersion            string
+	ExcludePattern        string
 	IgnoreMissingSchemas  bool
 	OnlyK8sFiles          bool
 	Verbose               bool
@@ -262,6 +269,7 @@ func (flags *TestCommandFlags) AddFlags(cmd *cobra.Command) {
 
 	cmd.Flags().StringVarP(&flags.K8sVersion, "schema-version", "s", "", "Set kubernetes version to validate against. Defaults to 1.24.0")
 	cmd.Flags().StringVarP(&flags.PolicyName, "policy", "p", "", "Policy name to run against")
+	cmd.Flags().StringVarP(&flags.ExcludePattern, "exclude", "", "", "Exclude paths pattern (regex)")
 
 	cmd.Flags().StringVar(&flags.PolicyConfig, "policy-config", "", "Path for local policies configuration file")
 	cmd.Flags().BoolVar(&flags.OnlyK8sFiles, "only-k8s-files", false, "Evaluate only valid yaml files with the properties 'apiVersion' and 'kind'. Ignore everything else")
@@ -342,6 +350,7 @@ func GenerateTestCommandData(testCommandFlags *TestCommandFlags, localConfigCont
 
 	testCommandOptions := &TestCommandData{Output: testCommandFlags.Output,
 		K8sVersion:            k8sVersion,
+		ExcludePattern:        testCommandFlags.ExcludePattern,
 		IgnoreMissingSchemas:  testCommandFlags.IgnoreMissingSchemas,
 		OnlyK8sFiles:          testCommandFlags.OnlyK8sFiles,
 		Verbose:               testCommandFlags.Verbose,
@@ -414,7 +423,7 @@ func test(ctx *TestCommandContext, paths []string, testCommandData *TestCommandD
 		paths = []string{tempFile.Name()}
 	}
 
-	filesPaths, err := ctx.Reader.FilterFiles(paths)
+	filesPaths, err := ctx.Reader.FilterFiles(paths, testCommandData.ExcludePattern)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/test/main_test.go b/cmd/test/main_test.go
index 166139d..b32cfa3 100644
--- a/cmd/test/main_test.go
+++ b/cmd/test/main_test.go
@@ -191,7 +191,7 @@ type ReaderMock struct {
 	mock.Mock
 }
 
-func (rm *ReaderMock) FilterFiles(paths []string) ([]string, error) {
+func (rm *ReaderMock) FilterFiles(paths []string, excludePattern string) ([]string, error) {
 	args := rm.Called(paths)
 	return args.Get(0).([]string), nil
 }
diff --git a/pkg/fileReader/reader_test.go b/pkg/fileReader/reader_test.go
index 3a9cab2..ea5dfdf 100644
--- a/pkg/fileReader/reader_test.go
+++ b/pkg/fileReader/reader_test.go
@@ -1,6 +1,7 @@
 package fileReader
 
 import (
+	"io/fs"
 	"os"
 	"reflect"
 	"testing"
@@ -118,6 +119,74 @@ func (c *absMock) Abs(path string) (string, error) {
 	return args.Get(0).(string), args.Error(1)
 }
 
+type filterFilesTestCase struct {
+	name string
+	args struct {
+		paths          []string
+		excludePattern string
+	}
+	mock struct {
+		stat struct {
+			response fs.FileInfo
+			err      error
+		}
+	}
+	expected struct {
+		filePaths []string
+	}
+}
+
+func TestFilterFiles(t *testing.T) {
+	stat := statMock{}
+	fileInfo := &MockFileInfo{}
+
+	tests := []filterFilesTestCase{
+		{
+			name: "success",
+			args: struct {
+				paths          []string
+				excludePattern string
+			}{
+				paths:          []string{"file1.yaml", "file2.yaml", "file3-exclude.yaml", "file4.yaml", "file5-exclude.yaml"},
+				excludePattern: "exclude.yaml",
+			},
+			mock: struct {
+				stat struct {
+					response fs.FileInfo
+					err      error
+				}
+			}{
+				stat: struct {
+					response fs.FileInfo
+					err      error
+				}{
+					response: fileInfo,
+					err:      nil,
+				},
+			},
+			expected: struct{ filePaths []string }{
+				filePaths: []string{"file1.yaml", "file2.yaml", "file4.yaml"},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			stat.On("Stat", mock.Anything).Return(tt.mock.stat.response, tt.mock.stat.err)
+			fileInfo.On("IsDir", mock.Anything).Return(false)
+			fileReader := &FileReader{
+				glob: nil,
+				stat: stat.Stat,
+			}
+
+			filteredfiles, _ := fileReader.FilterFiles(tt.args.paths, tt.args.excludePattern)
+			stat.AssertCalled(t, "Stat", tt.expected.filePaths[0])
+			fileInfo.AssertCalled(t, "IsDir")
+			assert.Equal(t, tt.expected.filePaths, filteredfiles)
+		})
+	}
+}
+
 func TestCreateFileReader(t *testing.T) {
 	glob := globMock{}
 	io := ioMock{}