diff --git a/internal/wrappers/export-http.go b/internal/wrappers/export-http.go index b8d2bfa9d..95d990974 100644 --- a/internal/wrappers/export-http.go +++ b/internal/wrappers/export-http.go @@ -8,6 +8,7 @@ import ( "log" "net/http" "os" + "time" "github.com/checkmarx/ast-cli/internal/logger" commonParams "github.com/checkmarx/ast-cli/internal/params" @@ -51,34 +52,54 @@ func NewExportHTTPWrapper(path string) ExportWrapper { } } +const ( + retryInterval = 5 * time.Second + timeout = 2 * time.Minute +) + func (e *ExportHTTPWrapper) InitiateExportRequest(payload *ExportRequestPayload) (*ExportResponse, error) { clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey) params, err := json.Marshal(payload) if err != nil { - return nil, errors.Wrapf(err, "Failed to parse request body") + return nil, errors.Wrapf(err, "failed to parse request body") } path := fmt.Sprintf("%s/%s", e.path, "requests") - resp, err := SendHTTPRequestWithJSONContentType(http.MethodPost, path, bytes.NewBuffer(params), true, clientTimeout) - if err != nil { - return nil, err - } - defer func() { - _ = resp.Body.Close() - }() - switch resp.StatusCode { - case http.StatusAccepted: - model := ExportResponse{} - decoder := json.NewDecoder(resp.Body) - err = decoder.Decode(&model) + endTime := time.Now().Add(timeout) + retryCount := 0 + + for { + logger.PrintfIfVerbose("Sending export request, attempt %d", retryCount+1) + resp, err := SendHTTPRequestWithJSONContentType(http.MethodPost, path, bytes.NewBuffer(params), true, clientTimeout) if err != nil { - return nil, errors.Wrapf(err, "failed to parse response body") + return nil, err + } + + switch resp.StatusCode { + case http.StatusAccepted: + logger.PrintIfVerbose("Received 202 Accepted response from server.") + model := ExportResponse{} + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&model) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse response body") + } + resp.Body.Close() + return &model, nil + case http.StatusBadRequest: + if time.Now().After(endTime) { + log.Printf("Timeout reached after %d attempts. Last response status code: %d", retryCount+1, resp.StatusCode) + resp.Body.Close() + return nil, errors.Errorf("failed to initiate export request - response status code %d", resp.StatusCode) + } + retryCount++ + logger.PrintfIfVerbose("Received 400 Bad Request. Retrying in %v... (attempt %d/%d)", retryInterval, retryCount, maxRetries) + time.Sleep(retryInterval) + default: + logger.PrintfIfVerbose("Received unexpected status code %d", resp.StatusCode) + resp.Body.Close() + return nil, errors.Errorf("response status code %d", resp.StatusCode) } - return &model, nil - case http.StatusBadRequest: - return nil, errors.Errorf("SBOM report is currently in beta mode and not available for this tenant type") - default: - return nil, errors.Errorf("response status code %d", resp.StatusCode) } } diff --git a/test/integration/scan_test.go b/test/integration/scan_test.go index bddd0e976..578d15579 100644 --- a/test/integration/scan_test.go +++ b/test/integration/scan_test.go @@ -26,9 +26,13 @@ import ( errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" exitCodes "github.com/checkmarx/ast-cli/internal/constants/exit-codes" "github.com/checkmarx/ast-cli/internal/params" + "github.com/checkmarx/ast-cli/internal/services" "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/checkmarx/ast-cli/internal/wrappers/configuration" + "github.com/google/uuid" "github.com/pkg/errors" "github.com/spf13/viper" + asserts "github.com/stretchr/testify/assert" "gotest.tools/assert" ) @@ -1838,3 +1842,25 @@ func validateCheckmarxDomains(t *testing.T, usedDomainsInTests []string) { assert.Assert(t, slices.Contains(usedDomainsInTests, domain), "Domain "+domain+" not found in used domains") } } + +func TestCreateAsyncScan_CallExportServiceBeforeScanFinishWithRetry_Success(t *testing.T) { + createASTIntegrationTestCommand(t) + configuration.LoadConfiguration() + args := []string{ + "scan", "create", + flag(params.ProjectName), generateRandomProjectNameForScan(), + flag(params.SourcesFlag), "data/empty-folder.zip", + flag(params.ScanTypes), "sca", + flag(params.BranchFlag), "main", + flag(params.AsyncFlag), + flag(params.ScanInfoFormatFlag), printer.FormatJSON, + } + scanID, _ := executeCreateScan(t, args) + exportRes, err := services.GetExportPackage(wrappers.NewExportHTTPWrapper("api/sca/export"), scanID) + asserts.Nil(t, err) + assert.Assert(t, exportRes != nil, "Export response should not be nil") +} + +func generateRandomProjectNameForScan() string { + return fmt.Sprint("ast-cli-scan-", uuid.New().String()) +}