-
Notifications
You must be signed in to change notification settings - Fork 1
refactor: http client and deepcode client [IDE-195] #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| package http | ||
| package config | ||
|
|
||
| // Config defines the configurable options for the HTTP client. | ||
| // | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,8 +17,16 @@ | |
| package deepcode | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "github.com/snyk/code-client-go/config" | ||
| "github.com/snyk/code-client-go/internal/util/encoding" | ||
| "io" | ||
| "net/http" | ||
| "net/url" | ||
| "regexp" | ||
| "strconv" | ||
|
|
||
| "github.com/rs/zerolog" | ||
|
|
@@ -62,17 +70,21 @@ type BundleResponse struct { | |
| } | ||
|
|
||
| type snykCodeClient struct { | ||
| httpClient codeClientHTTP.HTTPClient | ||
| instrumentor observability.Instrumentor | ||
| logger *zerolog.Logger | ||
| httpClient codeClientHTTP.HTTPClient | ||
| instrumentor observability.Instrumentor | ||
| errorReporter observability.ErrorReporter | ||
| logger *zerolog.Logger | ||
| config config.Config | ||
| } | ||
|
|
||
| func NewSnykCodeClient( | ||
| logger *zerolog.Logger, | ||
| httpClient codeClientHTTP.HTTPClient, | ||
| instrumentor observability.Instrumentor, | ||
| errorReporter observability.ErrorReporter, | ||
| config config.Config, | ||
| ) *snykCodeClient { | ||
| return &snykCodeClient{httpClient, instrumentor, logger} | ||
| return &snykCodeClient{httpClient, instrumentor, errorReporter, logger, config} | ||
| } | ||
|
|
||
| func (s *snykCodeClient) GetFilters(ctx context.Context) ( | ||
|
|
@@ -86,7 +98,12 @@ func (s *snykCodeClient) GetFilters(ctx context.Context) ( | |
| span := s.instrumentor.StartSpan(ctx, method) | ||
| defer s.instrumentor.Finish(span) | ||
|
|
||
| responseBody, err := s.httpClient.DoCall(span.Context(), "GET", "/filters", nil) | ||
| host, err := s.Host() | ||
| if err != nil { | ||
| return FiltersResponse{ConfigFiles: nil, Extensions: nil}, err | ||
| } | ||
|
|
||
| responseBody, err := s.Request(host, http.MethodGet, "/filters", nil) | ||
| if err != nil { | ||
| return FiltersResponse{ConfigFiles: nil, Extensions: nil}, err | ||
| } | ||
|
|
@@ -110,12 +127,17 @@ func (s *snykCodeClient) CreateBundle( | |
| span := s.instrumentor.StartSpan(ctx, method) | ||
| defer s.instrumentor.Finish(span) | ||
|
|
||
| host, err := s.Host() | ||
| if err != nil { | ||
| return "", nil, err | ||
| } | ||
|
|
||
| requestBody, err := json.Marshal(filesToFilehashes) | ||
| if err != nil { | ||
| return "", nil, err | ||
| } | ||
|
|
||
| responseBody, err := s.httpClient.DoCall(span.Context(), "POST", "/bundle", requestBody) | ||
| responseBody, err := s.Request(host, http.MethodPost, "/bundle", requestBody) | ||
| if err != nil { | ||
| return "", nil, err | ||
| } | ||
|
|
@@ -143,6 +165,11 @@ func (s *snykCodeClient) ExtendBundle( | |
| span := s.instrumentor.StartSpan(ctx, method) | ||
| defer s.instrumentor.Finish(span) | ||
|
|
||
| host, err := s.Host() | ||
| if err != nil { | ||
| return "", nil, err | ||
| } | ||
|
|
||
| requestBody, err := json.Marshal(ExtendBundleRequest{ | ||
| Files: files, | ||
| RemovedFiles: removedFiles, | ||
|
|
@@ -151,11 +178,111 @@ func (s *snykCodeClient) ExtendBundle( | |
| return "", nil, err | ||
| } | ||
|
|
||
| responseBody, err := s.httpClient.DoCall(span.Context(), "PUT", "/bundle/"+bundleHash, requestBody) | ||
| responseBody, err := s.Request(host, http.MethodPut, "/bundle/"+bundleHash, requestBody) | ||
| if err != nil { | ||
| return "", nil, err | ||
| } | ||
| var bundleResponse BundleResponse | ||
| err = json.Unmarshal(responseBody, &bundleResponse) | ||
| return bundleResponse.BundleHash, bundleResponse.MissingFiles, err | ||
| } | ||
|
|
||
| // This is only exported for tests. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be an option, to just use the same package?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're doing blackbox testing here. Technically I should not test this function but the code I extracted from |
||
| func (s *snykCodeClient) Host() (string, error) { | ||
| var codeApiRegex = regexp.MustCompile(`^(deeproxy\.)?`) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is not correct - for local code engine and on Fedramp, it is not always deeproxy.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is how this code works in https://github.com/snyk/snyk-ls/blob/75df46322994145b2d1ca8ca9e464dc1394357c4/infrastructure/code/snyk_code_http_client.go#L589-L607 too. Are we thinking that's incorrect too? |
||
|
|
||
| snykCodeApiUrl := s.config.SnykCodeApi() | ||
| if !s.config.IsFedramp() { | ||
| return snykCodeApiUrl, nil | ||
| } | ||
| u, err := url.Parse(snykCodeApiUrl) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| u.Host = codeApiRegex.ReplaceAllString(u.Host, "api.") | ||
|
|
||
| organization := s.config.Organization() | ||
| if organization == "" { | ||
| return "", errors.New("Organization is required in a fedramp environment") | ||
| } | ||
|
|
||
| u.Path = "/hidden/orgs/" + organization + "/code" | ||
|
|
||
| return u.String(), nil | ||
| } | ||
|
|
||
| func (s *snykCodeClient) Request( | ||
| host string, | ||
| method string, | ||
| path string, | ||
| requestBody []byte, | ||
| ) ([]byte, error) { | ||
| log := s.logger.With().Str("method", "deepcode.Request").Logger() | ||
|
|
||
| bodyBuffer, err := s.encodeIfNeeded(method, requestBody) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| req, err := http.NewRequest(method, host+path, bodyBuffer) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| s.addHeaders(method, req) | ||
|
|
||
| response, err := s.httpClient.Do(req) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer func() { | ||
| closeErr := response.Body.Close() | ||
| if closeErr != nil { | ||
| s.logger.Error().Err(closeErr).Msg("Couldn't close response body in call to Snyk Code") | ||
| } | ||
| }() | ||
| responseBody, err := io.ReadAll(response.Body) | ||
| if err != nil { | ||
| log.Error().Err(err).Msg("error reading response body") | ||
| s.errorReporter.CaptureError(err, observability.ErrorReporterOptions{ErrorDiagnosticPath: req.RequestURI}) | ||
| return nil, err | ||
| } | ||
|
|
||
| return responseBody, nil | ||
| } | ||
|
|
||
| func (s *snykCodeClient) addHeaders(method string, req *http.Request) { | ||
| // Setting a chosen org name for the request | ||
| org := s.config.Organization() | ||
| if org != "" { | ||
| req.Header.Set("snyk-org-name", org) | ||
| } | ||
| // https://www.keycdn.com/blog/http-cache-headers | ||
| req.Header.Set("Cache-Control", "private, max-age=0, no-cache") | ||
| if s.mustBeEncoded(method) { | ||
| req.Header.Set("Content-Type", "application/octet-stream") | ||
| req.Header.Set("Content-Encoding", "gzip") | ||
| } else { | ||
| req.Header.Set("Content-Type", "application/json") | ||
| } | ||
| } | ||
|
|
||
| func (s *snykCodeClient) encodeIfNeeded(method string, requestBody []byte) (*bytes.Buffer, error) { | ||
| b := new(bytes.Buffer) | ||
| mustBeEncoded := s.mustBeEncoded(method) | ||
| if mustBeEncoded { | ||
| enc := encoding.NewEncoder(b) | ||
| _, err := enc.Write(requestBody) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| } else { | ||
| b = bytes.NewBuffer(requestBody) | ||
| } | ||
| return b, nil | ||
| } | ||
|
|
||
| func (s *snykCodeClient) mustBeEncoded(method string) bool { | ||
| return method == http.MethodPost || method == http.MethodPut | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should consider using the Function Options Pattern, as the constructor parameters are growing quite a bit :).
I also wonder, if we do that, if we should assume sensible defaults for errorReporter, e.g implementing an error reporter that just logs, an instrumentor that does nothing etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great idea, that's very similar to what I did in #23 for the code scanner. This deepcode client is hopefully not going to be here very long so I think I will leave it as is for now and look at doing what you suggested after PR#23 gets merged.