Repository: datreeio/datree
Pull request #897 (2023-03-08 01:27:00)

feat: allow offline mode when connected to the internet
This pull request aims to address the use case where

> CI environments that may not be air gapped (ie. have internet access) but do not want the additional dependency on the external API introduced in their CI pipelines either for security, reproducibility or reliability reasons.

This use case was originally discussed in 
- https://github.com/datreeio/datree/issues/642
- https://github.com/datreeio/datree/issues/890



adifayer: @zmotu Thank you for submitting this PR! We will look into it and provide our review in the next couple of days🤩

Patch:
diff --git a/pkg/cliClient/evaluation.go b/pkg/cliClient/evaluation.go
index c9a8043..a023443 100644
--- a/pkg/cliClient/evaluation.go
+++ b/pkg/cliClient/evaluation.go
@@ -94,7 +94,7 @@ type EvaluationPrerunDataResponse struct {
 
 func (c *CliClient) RequestEvaluationPrerunData(tokenId string, isCi bool) (*EvaluationPrerunDataResponse, error) {
 	if c.networkValidator.IsLocalMode() {
-		return &EvaluationPrerunDataResponse{IsPolicyAsCodeMode: true}, nil
+		return &EvaluationPrerunDataResponse{IsPolicyAsCodeMode: true, PoliciesJson: defaultPolicies.GetDefaultPoliciesStruct()}, nil
 	}
 
 	isCiQueryParam := "isCi=" + strconv.FormatBool(isCi)
diff --git a/pkg/cliClient/validateYamlResult.go b/pkg/cliClient/validateYamlResult.go
deleted file mode 100644
index 8d8e785..0000000
--- a/pkg/cliClient/validateYamlResult.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package cliClient
-
-import (
-	"net/http"
-)
-
-type ValidatedFile struct {
-	Path    string `json:"path"`
-	IsValid bool   `json:"isValid"`
-}
-
-type ValidatedYamlResult struct {
-	Token    string           `json:"token"`
-	ClientId string           `json:"clientId"`
-	Metadata *Metadata        `json:"metadata"`
-	Files    []*ValidatedFile `json:"files"`
-	Status   string           `json:"status"`
-}
-
-func (c *CliClient) SendValidateYamlResult(request *ValidatedYamlResult) {
-	if c.networkValidator.IsLocalMode() {
-		return
-	}
-
-	_, _ = c.httpClient.Request(http.MethodPost, "/cli/yaml-validation/", request, c.flagsHeaders)
-}
diff --git a/pkg/networkValidator/networkValidator.go b/pkg/networkValidator/networkValidator.go
index 0b7c210..858c68e 100644
--- a/pkg/networkValidator/networkValidator.go
+++ b/pkg/networkValidator/networkValidator.go
@@ -33,5 +33,5 @@ func (nv *NetworkValidator) IdentifyNetworkError(err error) error {
 }
 
 func (nv *NetworkValidator) IsLocalMode() bool {
-	return !nv.isBackendAvailable && nv.offlineMode == "local"
+	return nv.offlineMode == "local"
 }


Test patch:
diff --git a/cmd/test/main.go b/cmd/test/main.go
index 2bd9d84..3ffd2bd 100644
--- a/cmd/test/main.go
+++ b/cmd/test/main.go
@@ -157,6 +157,7 @@ type TestCommandData struct {
 	SaveRendered          bool
 	PermissiveSchema      bool
 	Quiet                 bool
+	IsOffline             bool
 }
 
 type TestCommandContext struct {
@@ -293,7 +294,7 @@ func GenerateTestCommandData(testCommandFlags *TestCommandFlags, localConfigCont
 	var err error
 
 	if testCommandFlags.PolicyConfig != "" {
-		if !evaluationPrerunDataResp.IsPolicyAsCodeMode {
+		if localConfigContent.Offline != "local" && !evaluationPrerunDataResp.IsPolicyAsCodeMode {
 			return nil, fmt.Errorf("to use --policy-config flag you must first enable policy-as-code mode: https://hub.datree.io/policy-as-code")
 		}
 
@@ -331,6 +332,7 @@ func GenerateTestCommandData(testCommandFlags *TestCommandFlags, localConfigCont
 		SaveRendered:          testCommandFlags.SaveRendered,
 		PermissiveSchema:      testCommandFlags.PermissiveSchema,
 		Quiet:                 testCommandFlags.Quiet,
+		IsOffline:             localConfigContent.Offline == "local",
 	}
 
 	return testCommandOptions, nil
@@ -465,6 +467,10 @@ func test(ctx *TestCommandContext, paths []string, testCommandData *TestCommandD
 		}
 	}
 
+	if testCommandData.IsOffline {
+		ctx.Printer.PrintMessage("[INFO] You're running Datree in offline mode", "cyan")
+	}
+
 	if err != nil {
 		return err
 	}
diff --git a/pkg/cliClient/cliClient_test.go b/pkg/cliClient/cliClient_test.go
index b78a43e..a69d940 100644
--- a/pkg/cliClient/cliClient_test.go
+++ b/pkg/cliClient/cliClient_test.go
@@ -132,11 +132,10 @@ func TestRequestEvaluationPrerunDataSuccess(t *testing.T) {
 		test_requestEvaluationPrerunData_anonymousSuccess(),
 	}
 
-	httpClientMock := mockHTTPClient{}
-
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			body, _ := json.Marshal(tt.mock.response.body)
+			httpClientMock := mockHTTPClient{}
 			mockedHTTPResponse := httpClient.Response{StatusCode: tt.mock.response.status, Body: body}
 			httpClientMock.On("Request", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(mockedHTTPResponse, tt.mock.response.error).Once()
 
@@ -155,18 +154,45 @@ func TestRequestEvaluationPrerunDataSuccess(t *testing.T) {
 	}
 }
 
+func TestRequestEvaluationPrerunDataLocal(t *testing.T) {
+	tests := []*RequestEvaluationPrerunDataTestCase{
+		test_requestEvaluationPrerunData_network_error("local", nil),
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			body, _ := json.Marshal(tt.mock.response.body)
+			httpClientMock := mockHTTPClient{}
+			mockedHTTPResponse := httpClient.Response{StatusCode: tt.mock.response.status, Body: body}
+			httpClientMock.On("Request", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(mockedHTTPResponse, tt.mock.response.error).Once()
+
+			validator := networkValidator.NewNetworkValidator()
+			validator.SetOfflineMode(tt.args.offline)
+			client := &CliClient{
+				baseUrl:          "http://cli-service.test.io",
+				httpClient:       &httpClientMock,
+				networkValidator: validator,
+			}
+
+			preRunDataResp, err := client.RequestEvaluationPrerunData(tt.args.token, false)
+
+			httpClientMock.AssertNotCalled(t, "Request", tt.expected.request.method, tt.expected.request.uri, tt.expected.request.body, tt.expected.request.headers)
+			assert.Equal(t, tt.expected.response, preRunDataResp)
+			assert.Equal(t, tt.expected.responseErr, err)
+		})
+	}
+}
+
 func TestRequestEvaluationPrerunDataFail(t *testing.T) {
 	tests := []*RequestEvaluationPrerunDataTestCase{
 		test_requestEvaluationPrerunData_error(),
 		test_requestEvaluationPrerunData_network_error("fail", errors.New("Failed since internet connection refused, you can use the following command to set your config to run offline:\ndatree config set offline local")),
-		test_requestEvaluationPrerunData_network_error("local", nil),
 	}
 
-	httpClientMock := mockHTTPClient{}
-
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			body, _ := json.Marshal(tt.mock.response.body)
+			httpClientMock := mockHTTPClient{}
 			mockedHTTPResponse := httpClient.Response{StatusCode: tt.mock.response.status, Body: body}
 			httpClientMock.On("Request", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(mockedHTTPResponse, tt.mock.response.error).Once()
 
diff --git a/pkg/networkValidator/validator_test.go b/pkg/networkValidator/validator_test.go
index 31d0ed1..dea65b2 100644
--- a/pkg/networkValidator/validator_test.go
+++ b/pkg/networkValidator/validator_test.go
@@ -53,7 +53,7 @@ func TestNetworkValidatorOtherError(t *testing.T) {
 	err = test_identifyNetworkError_other_error(validator, "local")
 	isLocalMode = validator.IsLocalMode()
 	assert.Equal(t, nil, err)
-	assert.Equal(t, false, isLocalMode)
+	assert.Equal(t, true, isLocalMode)
 
 }