aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cli/internal/client/cache.go
diff options
context:
space:
mode:
author简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:44 +0800
committer简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:44 +0800
commitdd84b9d64fb98746a230cd24233ff50a562c39c9 (patch)
treeb583261ef00b3afe72ec4d6dacb31e57779a6faf /cli/internal/client/cache.go
parent0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff)
downloadHydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz
HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip
Diffstat (limited to 'cli/internal/client/cache.go')
-rw-r--r--cli/internal/client/cache.go167
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(&params)
+ // 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(&params)
+ // 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)
+}