diff options
| author | 2023-04-28 01:36:44 +0800 | |
|---|---|---|
| committer | 2023-04-28 01:36:44 +0800 | |
| commit | dd84b9d64fb98746a230cd24233ff50a562c39c9 (patch) | |
| tree | b583261ef00b3afe72ec4d6dacb31e57779a6faf /cli/internal/client/cache.go | |
| parent | 0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff) | |
| download | HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip | |
Diffstat (limited to 'cli/internal/client/cache.go')
| -rw-r--r-- | cli/internal/client/cache.go | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/cli/internal/client/cache.go b/cli/internal/client/cache.go new file mode 100644 index 0000000..11ad87a --- /dev/null +++ b/cli/internal/client/cache.go @@ -0,0 +1,167 @@ +package client + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/hashicorp/go-retryablehttp" + "github.com/vercel/turbo/cli/internal/ci" + "github.com/vercel/turbo/cli/internal/util" +) + +// PutArtifact uploads an artifact associated with a given hash string to the remote cache +func (c *APIClient) PutArtifact(hash string, artifactBody []byte, duration int, tag string) error { + if err := c.okToRequest(); err != nil { + return err + } + params := url.Values{} + c.addTeamParam(¶ms) + // only add a ? if it's actually needed (makes logging cleaner) + encoded := params.Encode() + if encoded != "" { + encoded = "?" + encoded + } + + requestURL := c.makeURL("/v8/artifacts/" + hash + encoded) + allowAuth := true + if c.usePreflight { + resp, latestRequestURL, err := c.doPreflight(requestURL, http.MethodPut, "Content-Type, x-artifact-duration, Authorization, User-Agent, x-artifact-tag") + if err != nil { + return fmt.Errorf("pre-flight request failed before trying to store in HTTP cache: %w", err) + } + requestURL = latestRequestURL + headers := resp.Header.Get("Access-Control-Allow-Headers") + allowAuth = strings.Contains(strings.ToLower(headers), strings.ToLower("Authorization")) + } + + req, err := retryablehttp.NewRequest(http.MethodPut, requestURL, artifactBody) + req.Header.Set("Content-Type", "application/octet-stream") + req.Header.Set("x-artifact-duration", fmt.Sprintf("%v", duration)) + if allowAuth { + req.Header.Set("Authorization", "Bearer "+c.token) + } + req.Header.Set("User-Agent", c.userAgent()) + if ci.IsCi() { + req.Header.Set("x-artifact-client-ci", ci.Constant()) + } + if tag != "" { + req.Header.Set("x-artifact-tag", tag) + } + if err != nil { + return fmt.Errorf("[WARNING] Invalid cache URL: %w", err) + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return fmt.Errorf("[ERROR] Failed to store files in HTTP cache: %w", err) + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode == http.StatusForbidden { + return c.handle403(resp.Body) + } + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("[ERROR] Failed to store files in HTTP cache: %s against URL %s", resp.Status, requestURL) + } + return nil +} + +// FetchArtifact attempts to retrieve the build artifact with the given hash from the remote cache +func (c *APIClient) FetchArtifact(hash string) (*http.Response, error) { + return c.getArtifact(hash, http.MethodGet) +} + +// ArtifactExists attempts to determine if the build artifact with the given hash exists in the Remote Caching server +func (c *APIClient) ArtifactExists(hash string) (*http.Response, error) { + return c.getArtifact(hash, http.MethodHead) +} + +// getArtifact attempts to retrieve the build artifact with the given hash from the remote cache +func (c *APIClient) getArtifact(hash string, httpMethod string) (*http.Response, error) { + if httpMethod != http.MethodHead && httpMethod != http.MethodGet { + return nil, fmt.Errorf("invalid httpMethod %v, expected GET or HEAD", httpMethod) + } + + if err := c.okToRequest(); err != nil { + return nil, err + } + params := url.Values{} + c.addTeamParam(¶ms) + // only add a ? if it's actually needed (makes logging cleaner) + encoded := params.Encode() + if encoded != "" { + encoded = "?" + encoded + } + + requestURL := c.makeURL("/v8/artifacts/" + hash + encoded) + allowAuth := true + if c.usePreflight { + resp, latestRequestURL, err := c.doPreflight(requestURL, http.MethodGet, "Authorization, User-Agent") + if err != nil { + return nil, fmt.Errorf("pre-flight request failed before trying to fetch files in HTTP cache: %w", err) + } + requestURL = latestRequestURL + headers := resp.Header.Get("Access-Control-Allow-Headers") + allowAuth = strings.Contains(strings.ToLower(headers), strings.ToLower("Authorization")) + } + + req, err := retryablehttp.NewRequest(httpMethod, requestURL, nil) + if allowAuth { + req.Header.Set("Authorization", "Bearer "+c.token) + } + req.Header.Set("User-Agent", c.userAgent()) + if err != nil { + return nil, fmt.Errorf("invalid cache URL: %w", err) + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch artifact: %v", err) + } else if resp.StatusCode == http.StatusForbidden { + err = c.handle403(resp.Body) + _ = resp.Body.Close() + return nil, err + } + return resp, nil +} + +func (c *APIClient) handle403(body io.Reader) error { + raw, err := ioutil.ReadAll(body) + if err != nil { + return fmt.Errorf("failed to read response %v", err) + } + apiError := &apiError{} + err = json.Unmarshal(raw, apiError) + if err != nil { + return fmt.Errorf("failed to read response (%v): %v", string(raw), err) + } + disabledErr, err := apiError.cacheDisabled() + if err != nil { + return err + } + return disabledErr +} + +type apiError struct { + Code string `json:"code"` + Message string `json:"message"` +} + +func (ae *apiError) cacheDisabled() (*util.CacheDisabledError, error) { + if strings.HasPrefix(ae.Code, "remote_caching_") { + statusString := ae.Code[len("remote_caching_"):] + status, err := util.CachingStatusFromString(statusString) + if err != nil { + return nil, err + } + return &util.CacheDisabledError{ + Status: status, + Message: ae.Message, + }, nil + } + return nil, fmt.Errorf("unknown status %v: %v", ae.Code, ae.Message) +} |
