Repository: datreeio/datree (go)

Issue:
support not-strict mode for schema validation
**Is your feature request related to a problem? Please describe.**
today, I can't add  additional properties to my K8s objects because it will fail the schema step ([example](https://github.com/Azure/ip-masq-agent-v2/blob/master/examples/ip-masq-agent.yaml))

**Describe the solution you'd like**
a flag to pass the CLI to use none strict schema for the schema validation step

**Describe alternatives you've considered**
n/a

**Additional context**
By default, kubeconform runs in a none strict mode. We pass the `--strict` flag to force the strict mode in every schema validation check.

Pull request:
feat: add `--permissive-schema` flag
Close #775 

**Changes proposed in the PR**
- Add a flag `--permissive-schema` to support non-strict k8files validation.
- Fix failing test cases.

**Testing Instruction**
1. Copy the content of the [file](https://github.com/Azure/ip-masq-agent-v2/blob/master/examples/ip-masq-agent.yaml) in your local and save it (in my case _test.yaml_).
2. Checkout to my branch and build the project
3. Run the `datree test test.yaml` command without `--permissive-schema` flag
4. Verify it will throw errors like below
```console
❌  k8s schema validation error: For field spec.template.spec.volumes.0.projected.sources.0: Additional property name is not allowed

❌  k8s schema validation error: For field spec.template.spec.volumes.0.projected.sources.0: Additional property optional is not allowed

❌  k8s schema validation error: For field spec.template.spec.volumes.0.projected.sources.0: Additional property items is not allowed

❌  k8s schema validation error: For field spec.template.spec.volumes.0.projected.sources.1: Additional property items is not allowed

❌  k8s schema validation error: For field spec.template.spec.volumes.0.projected.sources.1: Additional property name is not allowed

❌  k8s schema validation error: For field spec.template.spec.volumes.0.projected.sources.1: Additional property optional is not allowed
```
5. Now try the same command with the `--permissive-schema` flag
Full command: `datree test test.yaml --permissive-schema`
6. Verify you don't get error _Additional property optional is not allowed_


**Additional comments**
- I'm not sure, we need to update the documentation here: https://hub.datree.io/setup/schema-validation

Patch:
diff --git a/bl/validation/k8sValidator.go b/bl/validation/k8sValidator.go
index b27c673..83035fe 100644
--- a/bl/validation/k8sValidator.go
+++ b/bl/validation/k8sValidator.go
@@ -29,10 +29,10 @@ func New() *K8sValidator {
 	return &K8sValidator{}
 }
 
-func (val *K8sValidator) InitClient(k8sVersion string, ignoreMissingSchemas bool, userProvidedSchemaLocations []string) {
+func (val *K8sValidator) InitClient(k8sVersion string, ignoreMissingSchemas bool, userProvidedSchemaLocations []string, permissiveSchema bool) {
 	val.isOffline = checkIsOffline()
 	val.areThereCustomSchemaLocations = len(userProvidedSchemaLocations) > 0
-	val.validationClient = newKubeconformValidator(k8sVersion, ignoreMissingSchemas, getAllSchemaLocations(userProvidedSchemaLocations, val.isOffline))
+	val.validationClient = newKubeconformValidator(k8sVersion, ignoreMissingSchemas, getAllSchemaLocations(userProvidedSchemaLocations, val.isOffline), permissiveSchema)
 }
 
 func checkIsOffline() bool {
@@ -212,8 +212,8 @@ func (val *K8sValidator) validateResource(filepath string) (bool, []error, *vali
 	return isValid, validationErrors, warning, nil
 }
 
-func newKubeconformValidator(k8sVersion string, ignoreMissingSchemas bool, schemaLocations []string) ValidationClient {
-	v, _ := kubeconformValidator.New(schemaLocations, kubeconformValidator.Opts{Strict: true, KubernetesVersion: k8sVersion, IgnoreMissingSchemas: ignoreMissingSchemas})
+func newKubeconformValidator(k8sVersion string, ignoreMissingSchemas bool, schemaLocations []string, permissiveSchema bool) ValidationClient {
+	v, _ := kubeconformValidator.New(schemaLocations, kubeconformValidator.Opts{Strict: !permissiveSchema, KubernetesVersion: k8sVersion, IgnoreMissingSchemas: ignoreMissingSchemas})
 	return v
 }
 


Test patch:
diff --git a/bl/validation/k8sValidator_test.go b/bl/validation/k8sValidator_test.go
index c7e8aa7..40a58e7 100644
--- a/bl/validation/k8sValidator_test.go
+++ b/bl/validation/k8sValidator_test.go
@@ -226,7 +226,7 @@ func test_validateResource_offline_with_local_schema(t *testing.T) {
 	k8sValidator := &K8sValidator{
 		validationClient: newKubeconformValidator("1.21.0", false, getAllSchemaLocations([]string{
 			"some-path-to-non-existing-file-to-get-404.yaml",
-		}, true)),
+		}, true), false),
 		isOffline:                     true,
 		areThereCustomSchemaLocations: true,
 	}
@@ -241,7 +241,7 @@ func test_validateResource_offline_with_local_schema(t *testing.T) {
 
 func test_validateResource_offline_without_custom_schema_location(t *testing.T) {
 	k8sValidator := &K8sValidator{
-		validationClient:              newKubeconformValidator("1.21.0", false, getAllSchemaLocations([]string{}, true)),
+		validationClient:              newKubeconformValidator("1.21.0", false, getAllSchemaLocations([]string{}, true), true),
 		isOffline:                     true,
 		areThereCustomSchemaLocations: false,
 	}
diff --git a/cmd/kustomize/main_test.go b/cmd/kustomize/main_test.go
index c63a4f7..0a8cff3 100644
--- a/cmd/kustomize/main_test.go
+++ b/cmd/kustomize/main_test.go
@@ -137,7 +137,7 @@ func (kv *k8sValidatorMock) GetK8sFiles(filesConfigurationsChan chan *extractor.
 	return args.Get(0).(chan *extractor.FileConfigurations), args.Get(1).(chan *extractor.FileConfigurations)
 }
 
-func (kv *k8sValidatorMock) InitClient(k8sVersion string, ignoreMissingSchemas bool, schemaLocations []string) {
+func (kv *k8sValidatorMock) InitClient(k8sVersion string, ignoreMissingSchemas bool, schemaLocations []string, permissiveSchema bool) {
 }
 
 type PrinterMock struct {
diff --git a/cmd/test/main.go b/cmd/test/main.go
index 4446130..fc8e237 100644
--- a/cmd/test/main.go
+++ b/cmd/test/main.go
@@ -45,7 +45,7 @@ type Messager interface {
 
 type K8sValidator interface {
 	ValidateResources(filesConfigurations chan *extractor.FileConfigurations, concurrency int, skipSchemaValidation bool) (chan *extractor.FileConfigurations, chan *extractor.InvalidFile, chan *validation.FileWithWarning)
-	InitClient(k8sVersion string, ignoreMissingSchemas bool, schemaLocations []string)
+	InitClient(k8sVersion string, ignoreMissingSchemas bool, schemaLocations []string, permissiveSchema bool)
 	GetK8sFiles(filesConfigurationsChan chan *extractor.FileConfigurations, concurrency int) (chan *extractor.FileConfigurations, chan *extractor.FileConfigurations)
 }
 
@@ -61,6 +61,7 @@ type TestCommandFlags struct {
 	NoRecord             bool
 	SkipValidation       string
 	SaveRendered         bool
+	PermissiveSchema     bool
 }
 
 // TestCommandFlags constructor
@@ -75,6 +76,7 @@ func NewTestCommandFlags() *TestCommandFlags {
 		SchemaLocations:      make([]string, 0),
 		SkipValidation:       "",
 		SaveRendered:         false,
+		PermissiveSchema:     false,
 	}
 }
 
@@ -151,6 +153,7 @@ type TestCommandData struct {
 	ClientId              string
 	SkipSchemaValidation  bool
 	SaveRendered          bool
+	PermissiveSchema      bool
 }
 
 type TestCommandContext struct {
@@ -265,7 +268,8 @@ func (flags *TestCommandFlags) AddFlags(cmd *cobra.Command) {
 	// kubeconform flag
 	cmd.Flags().StringArrayVarP(&flags.SchemaLocations, "schema-location", "", []string{}, "Override schemas location search path (can be specified multiple times)")
 	cmd.Flags().BoolVarP(&flags.IgnoreMissingSchemas, "ignore-missing-schemas", "", false, "Ignore missing schemas when executing schema validation step")
-	cmd.Flags().BoolVarP(&flags.SaveRendered, "save-rendered", "", false, "Don't delete rendered files after the policy check (e.g helm, kustomize)")
+	cmd.Flags().BoolVarP(&flags.SaveRendered, "save-rendered", "", false, "Don't delete rendered files after the policy check (e.g. helm, kustomize)")
+	cmd.Flags().BoolVarP(&flags.PermissiveSchema, "permissive-schema", "", false, "Perform non-strict schema validation (i.e. allow additional properties)")
 }
 
 func GenerateTestCommandData(testCommandFlags *TestCommandFlags, localConfigContent *localConfig.LocalConfig, evaluationPrerunDataResp *cliClient.EvaluationPrerunDataResponse) (*TestCommandData, error) {
@@ -321,6 +325,7 @@ func GenerateTestCommandData(testCommandFlags *TestCommandFlags, localConfigCont
 		PromptRegistrationURL: evaluationPrerunDataResp.PromptRegistrationURL,
 		SkipSchemaValidation:  testCommandFlags.SkipValidation == "schema",
 		SaveRendered:          testCommandFlags.SaveRendered,
+		PermissiveSchema:      testCommandFlags.PermissiveSchema,
 	}
 
 	return testCommandOptions, nil
@@ -490,7 +495,7 @@ func evaluate(ctx *TestCommandContext, filesPaths []string, testCommandData *Tes
 
 	validationManager := NewValidationManager()
 
-	ctx.K8sValidator.InitClient(testCommandData.K8sVersion, testCommandData.IgnoreMissingSchemas, testCommandData.SchemaLocations)
+	ctx.K8sValidator.InitClient(testCommandData.K8sVersion, testCommandData.IgnoreMissingSchemas, testCommandData.SchemaLocations, testCommandData.PermissiveSchema)
 
 	concurrency := 100
 	var wg sync.WaitGroup
diff --git a/cmd/test/main_test.go b/cmd/test/main_test.go
index f31f641..1ab7965 100644
--- a/cmd/test/main_test.go
+++ b/cmd/test/main_test.go
@@ -137,7 +137,7 @@ func (kv *K8sValidatorMock) GetK8sFiles(filesConfigurationsChan chan *extractor.
 	return args.Get(0).(chan *extractor.FileConfigurations), args.Get(1).(chan *extractor.FileConfigurations)
 }
 
-func (kv *K8sValidatorMock) InitClient(k8sVersion string, ignoreMissingSchemas bool, schemaLocations []string) {
+func (kv *K8sValidatorMock) InitClient(k8sVersion string, ignoreMissingSchemas bool, schemaLocations []string, permissiveSchema bool) {
 }
 
 type PrinterMock struct {
@@ -242,7 +242,7 @@ func TestTestFlow(t *testing.T) {
 			readerMock.On("FilterFiles", mock.Anything).Return(tt.args.path, nil)
 			filesExtractorMock.On("ExtractFilesConfigurations", mock.Anything, 100).Return(tt.mock.ExtractFilesConfigurations.filesConfigurationsChan, tt.mock.ExtractFilesConfigurations.invalidFilesChan)
 			k8sValidatorMock.On("ValidateResources", mock.Anything, 100).Return(tt.mock.ValidateResources.k8sFilesConfigurationsChan, tt.mock.ValidateResources.k8sInvalidFilesChan, tt.mock.ValidateResources.filesWithWarningsChan)
-			k8sValidatorMock.On("InitClient", mock.Anything, mock.Anything, mock.Anything).Return()
+			k8sValidatorMock.On("InitClient", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return()
 			evaluatorMock.On("Evaluate", mock.Anything, mock.Anything, mock.Anything).Return(tt.mock.Evaluate.policyCheckResultData, tt.mock.Evaluate.err)
 			evaluatorMock.On("SendEvaluationResult", mock.Anything).Return(tt.mock.SendEvaluationResult.sendEvaluationResultsResponse, tt.mock.SendEvaluationResult.err)
 
@@ -690,7 +690,7 @@ func setup() {
 
 	k8sValidatorMock.On("ValidateResources", mock.Anything, mock.Anything, mock.Anything).Return(filesConfigurationsChan, invalidK8sFilesChan, k8sValidationWarningsChan, newErrorsChan())
 	k8sValidatorMock.On("GetK8sFiles", mock.Anything, mock.Anything).Return(filesConfigurationsChan, ignoredFilesChan, newErrorsChan())
-	k8sValidatorMock.On("InitClient", mock.Anything, mock.Anything, mock.Anything).Return()
+	k8sValidatorMock.On("InitClient", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return()
 
 	filesExtractorMock := &FilesExtractorMock{}
 	filesExtractorMock.On("ExtractFilesConfigurations", mock.Anything, 100).Return(filesConfigurationsChan, invalidFilesChan)