Skip to content

Commit

Permalink
feat: use workspace api
Browse files Browse the repository at this point in the history
  • Loading branch information
teodora-sandu committed Apr 9, 2024
1 parent b52c908 commit 5c44a69
Show file tree
Hide file tree
Showing 21 changed files with 712 additions and 100 deletions.
18 changes: 17 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ jobs:
- run:
name: Run unit tests
command: make test
smoke_test:
executor: default
steps:
- checkout
- run:
name: Install tools
command: make tools
- run:
name: Run smoke tests
command: make smoke-test
build:
executor: default
steps:
Expand Down Expand Up @@ -67,9 +77,15 @@ workflows:
name: Unit tests
requires:
- Lint & Format
- smoke_test:
name: Smoke tests
context:
- code-client-go-smoke-tests-token
requires:
- Unit tests
- build:
name: Build
requires:
- Unit tests
- Smoke tests
- Security Scans
- Scan repository for secrets
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ make test

If writing unit tests, use the mocks generated by [GoMock](https://github.com/golang/mock) by running `make generate`.

If writing `pact` or integration tests, use the test implementations in [./internal/util/testutil](./internal/util/testutil).
If writing `pact`, integration, or smoke tests, use the test implementations in [./internal/util/testutil](./internal/util/testutil).

The organisation used by the smoke tests is `ide-consistent-ignores-test` in [https://app.dev.snyk.io](https://app.dev.snyk.io) and we are authenticating using a service account api key.

If you've changed any of the interfaces you may need to re-run `make generate` to generate the mocks again.

Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ test:
.PHONY: testv
testv:
@echo "Testing verbosely..."
@go test -v ./...
@go test -v

.PHONY: smoke-test
smoke-test:
@go test -run="Test_SmokeScan"

.PHONY: generate
generate: $(TOOLS_BIN)/go/mockgen $(TOOLS_BIN)/go/oapi-codegen
Expand Down
46 changes: 16 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,44 +38,30 @@ The HTTP client exposes a `DoCall` function.

Implement the `http.Config` interface to configure the Snyk Code API client from applications.

### Snyk Code Client

Use the Snyk Code Client to make calls to the DeepCode API using the `httpClient` HTTP client created above.

```go
snykCode := deepcode.NewSnykCodeClient(logger, httpClient, testutil.NewTestInstrumentor())
```

The Snyk Code Client exposes the following functions:
- `GetFilters`
- `CreateBundle`
- `ExtendBundle`

### Bundle Manager

Use the Bundle Manager to create bundles using the `snykCode` Snyk Code Client created above and then to extend it by uploading more files to it.

```go
bundleManager := bundle.NewBundleManager(logger, snykCode, testutil.NewTestInstrumentor(), testutil.NewTestCodeInstrumentor())
```

The Bundle Manager exposes the following functions:
- `Create`
- `Upload`

### Code Scanner

Use the Code Scanner to trigger a scan for a Snyk Code workspace using the Bundle Manager created above.
The Code Scanner exposes a `UploadAndAnalyze` function, which can be used like this:

```go
codeScanner := codeclient.NewCodeScanner(
bundleManager,
testutil.NewTestInstrumentor(),
testutil.NewTestErrorReporter(),
import (
"net/http"

"github.com/rs/zerolog"
code "github.com/snyk/code-client-go"
)

logger := zerlog.NewLogger(...)
config := newConfigForMyApp()

codeScanner := code.NewCodeScanner(
httpClient,
config,
codeInstrumentor,
codeErrorReporter,
logger,
)
codeScanner.UploadAndAnalyze(context.Background(), "path/to/workspace", channelForWalkingFiles, changedFiles)
code.UploadAndAnalyze(context.Background(), requestId, "path/to/workspace", channelForWalkingFiles, changedFiles)
```


Expand Down
3 changes: 3 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ type Config interface {
// SnykCodeApi returns the Snyk Code API URL configured to run against, which could be
// the one used by the Local Code Engine.
SnykCodeApi() string

// SnykApi returns the Snyk REST API URL configured to run against,
SnykApi() string
}
14 changes: 14 additions & 0 deletions config/mocks/config.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 17 additions & 4 deletions http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
package http

import (
"errors"
"fmt"
"io"
"net/http"
"time"

Expand Down Expand Up @@ -78,6 +79,7 @@ func (s *httpClient) Do(req *http.Request) (response *http.Response, err error)
return nil, err // no retries for errors
}

// TODO: workspace API error
err = s.checkResponseCode(response)
if err != nil {
if retryErrorCodes[response.StatusCode] {
Expand All @@ -86,9 +88,20 @@ func (s *httpClient) Do(req *http.Request) (response *http.Response, err error)
time.Sleep(5 * time.Second)
continue
}
// return the error on last try
return nil, err
}

// see if there's any errors in the response body
responseBody, readErr := io.ReadAll(response.Body)
defer func(body io.ReadCloser) {
closeErr := body.Close()
if closeErr != nil {
s.logger.Error().Err(closeErr).Msg("Couldn't close response body in call to Snyk Code")
}
}(response.Body)
if readErr == nil && string(responseBody) != "" {
return response, nil
}

return nil, err
}
// no error, we can break the retry loop
Expand All @@ -113,5 +126,5 @@ func (s *httpClient) checkResponseCode(r *http.Response) error {
if r.StatusCode >= 200 && r.StatusCode <= 299 {
return nil
}
return errors.New("Unexpected response code: " + r.Status)
return fmt.Errorf("Unexpected response code: %s", r.Status)
}
31 changes: 30 additions & 1 deletion http/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
package http_test

import (
"io"
"net/http"
"strings"
"testing"

"github.com/golang/mock/gomock"
Expand All @@ -32,6 +34,7 @@ import (
// dummyTransport is a transport struct that always returns the response code specified in the constructor
type dummyTransport struct {
responseCode int
responseBody string
status string
calls int
}
Expand All @@ -41,6 +44,7 @@ func (d *dummyTransport) RoundTrip(_ *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: d.responseCode,
Status: d.status,
Body: io.NopCloser(strings.NewReader(d.responseBody)),
}, nil
}

Expand All @@ -64,11 +68,36 @@ func TestSnykCodeBackendService_DoCall_shouldRetry(t *testing.T) {
require.NoError(t, err)

s := codeClientHTTP.NewHTTPClient(newLogger(t), dummyClientFactory, mockInstrumentor, mockErrorReporter)
_, err = s.Do(req)
res, err := s.Do(req)
assert.Error(t, err)
assert.Nil(t, res)
assert.Equal(t, 3, d.calls)
}
func TestSnykCodeBackendService_DoCall_shouldReturnErrorsFromResponseBody(t *testing.T) {
d := &dummyTransport{responseCode: 502, status: "502 Bad Gateway", responseBody: "errors from response"}
dummyClientFactory := func() *http.Client {
return &http.Client{
Transport: d,
}
}

ctrl := gomock.NewController(t)
mockSpan := mocks.NewMockSpan(ctrl)
mockSpan.EXPECT().GetTraceId().AnyTimes()
mockInstrumentor := mocks.NewMockInstrumentor(ctrl)
mockInstrumentor.EXPECT().StartSpan(gomock.Any(), gomock.Any()).Return(mockSpan).Times(1)
mockInstrumentor.EXPECT().Finish(gomock.Any()).Times(1)
mockErrorReporter := mocks.NewMockErrorReporter(ctrl)

req, err := http.NewRequest(http.MethodGet, "https://httpstat.us/500", nil)
require.NoError(t, err)

s := codeClientHTTP.NewHTTPClient(newLogger(t), dummyClientFactory, mockInstrumentor, mockErrorReporter)
res, err := s.Do(req)
assert.NoError(t, err)
assert.NotNil(t, res)
assert.Equal(t, 3, d.calls)
}
func TestSnykCodeBackendService_doCall_rejected(t *testing.T) {
dummyClientFactory := func() *http.Client {
return &http.Client{}
Expand Down
96 changes: 95 additions & 1 deletion internal/analysis/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,110 @@
package analysis

import (
"context"
_ "embed"
"encoding/json"
"fmt"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/snyk/code-client-go/internal/util"
workspaceClient "github.com/snyk/code-client-go/internal/workspace/2024-03-12"
externalRef3 "github.com/snyk/code-client-go/internal/workspace/2024-03-12/workspaces"
"github.com/snyk/code-client-go/sarif"
)

//go:embed fake.json
var fakeResponse []byte

func RunAnalysis() (*sarif.SarifResponse, error) {
type analysisOrchestrator struct {
workspace workspaceClient.ClientWithResponsesInterface
logger *zerolog.Logger
}

//go:generate mockgen -destination=mocks/analysis.go -source=analysis.go -package mocks
type AnalysisOrchestrator interface {
CreateWorkspace(ctx context.Context, orgId string, requestId string, path string, bundleHash string) (string, error)
RunAnalysis() (*sarif.SarifResponse, error)
}

func NewAnalysisOrchestrator(workspace workspaceClient.ClientWithResponsesInterface, logger *zerolog.Logger) *analysisOrchestrator {
return &analysisOrchestrator{
workspace,
logger,
}
}

func (a *analysisOrchestrator) CreateWorkspace(ctx context.Context, orgId string, requestId string, path string, bundleHash string) (string, error) {
orgUUID := uuid.MustParse(orgId)
a.logger.Info().Str("path", path).Str("bundleHash", bundleHash).Msg("creating workspace")

repositoryUri, err := util.GetRepositoryUrl(path)
if err != nil {
return "", fmt.Errorf("workspace is not a repository, cannot scan, %w", err)
}

a.logger.Info().Str("path", path).Str("repositoryUri", repositoryUri).Str("bundleHash", bundleHash).Msg("creating workspace")

workspaceResponse, err := a.workspace.CreateWorkspaceWithApplicationVndAPIPlusJSONBodyWithResponse(ctx, orgUUID, &workspaceClient.CreateWorkspaceParams{
Version: "2024-03-12~experimental",
SnykRequestId: uuid.MustParse(requestId),
ContentType: "application/vnd.api+json",
UserAgent: "cli",
}, workspaceClient.CreateWorkspaceApplicationVndAPIPlusJSONRequestBody{
Data: struct {
Attributes struct {
BundleId string `json:"bundle_id"`
RepositoryUri string `json:"repository_uri"`
WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"`
} `json:"attributes"`
Type externalRef3.WorkspacePostRequestDataType `json:"type"`
}(struct {
Attributes struct {
BundleId string `json:"bundle_id"`
RepositoryUri string `json:"repository_uri"`
WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"`
}
Type externalRef3.WorkspacePostRequestDataType
}{Attributes: struct {
BundleId string `json:"bundle_id"`
RepositoryUri string `json:"repository_uri"`
WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"`
}(struct {
BundleId string
RepositoryUri string
WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType
}{
BundleId: bundleHash,
RepositoryUri: repositoryUri,
WorkspaceType: "test",
}),
Type: "workspace",
}),
})
if err != nil {
return "", err
}

if workspaceResponse.ApplicationvndApiJSON201 == nil {
var msg string
switch workspaceResponse.StatusCode() {
case 400:
msg = workspaceResponse.ApplicationvndApiJSON400.Errors[0].Detail
case 401:
msg = workspaceResponse.ApplicationvndApiJSON401.Errors[0].Detail
case 403:
msg = workspaceResponse.ApplicationvndApiJSON403.Errors[0].Detail
case 500:
msg = workspaceResponse.ApplicationvndApiJSON500.Errors[0].Detail
}
return "", errors.New(msg)
}

return workspaceResponse.ApplicationvndApiJSON201.Data.Id.String(), nil
}

func (*analysisOrchestrator) RunAnalysis() (*sarif.SarifResponse, error) {
var response sarif.SarifResponse

err := json.Unmarshal(fakeResponse, &response)
Expand Down
Loading

0 comments on commit 5c44a69

Please sign in to comment.