这是indexloc提供的服务,不要输入任何密码
Skip to content

Check Remote Caching status before linking #1087

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

Merged
merged 5 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 61 additions & 5 deletions cli/internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-retryablehttp"
"github.com/vercel/turborepo/cli/internal/util"
)

type ApiClient struct {
Expand Down Expand Up @@ -317,13 +318,24 @@ func (c *ApiClient) addTeamParam(params *url.Values) {
}
}

// Membership is the relationship between the logged-in user and a particular team
type Membership struct {
Role string `json:"role"`
}

// Team is a Vercel Team object
type Team struct {
ID string `json:"id,omitempty"`
Slug string `json:"slug,omitempty"`
Name string `json:"name,omitempty"`
CreatedAt int `json:"createdAt,omitempty"`
Created string `json:"created,omitempty"`
ID string `json:"id,omitempty"`
Slug string `json:"slug,omitempty"`
Name string `json:"name,omitempty"`
CreatedAt int `json:"createdAt,omitempty"`
Created string `json:"created,omitempty"`
Membership Membership `json:"membership"`
}

// IsOwner returns true if this Team data was fetched by an owner of the team
func (t *Team) IsOwner() bool {
return t.Membership.Role == "OWNER"
}

// Pagination is a Vercel pagination object
Expand Down Expand Up @@ -417,6 +429,50 @@ func (c *ApiClient) GetUser() (*UserResponse, error) {
return userResponse, nil
}

// statusResponse is the server response from /artifacts/status
type statusResponse struct {
Status string `json:"status"`
}

// GetCachingStatus returns the server's perspective on whether or not remove caching
// requests will be allowed.
func (c *ApiClient) GetCachingStatus(teamID string) (util.CachingStatus, error) {
req, err := retryablehttp.NewRequest(http.MethodGet, c.makeUrl("/v8/artifacts/status"), nil)
if err != nil {
return util.CachingStatusDisabled, err
}
req.Header.Set("User-Agent", c.UserAgent())
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.Token)
resp, err := c.HttpClient.Do(req)
if err != nil {
return util.CachingStatusDisabled, err
}
// Explicitly ignore the error from closing the response body. We don't need
// to fail the method if we fail to close the response.
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
b, err := ioutil.ReadAll(resp.Body)
var responseText string
if err != nil {
responseText = fmt.Sprintf("failed to read response: %v", err)
} else {
responseText = string(b)
}
return util.CachingStatusDisabled, fmt.Errorf("failed to get caching status (%v): %s", resp.StatusCode, responseText)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return util.CachingStatusDisabled, fmt.Errorf("failed to read JSN response: %v", err)
}
statusResponse := statusResponse{}
err = json.Unmarshal(body, &statusResponse)
if err != nil {
return util.CachingStatusDisabled, fmt.Errorf("failed to read JSON response: %v", string(body))
}
return util.CachingStatusFromString(statusResponse.Status)
}

type verificationResponse struct {
Token string `json:"token"`
Email string `json:"email"`
Expand Down
112 changes: 91 additions & 21 deletions cli/internal/login/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/vercel/turborepo/cli/internal/fs"
"github.com/vercel/turborepo/cli/internal/ui"
"github.com/vercel/turborepo/cli/internal/util"
"github.com/vercel/turborepo/cli/internal/util/browser"

"github.com/AlecAivazis/survey/v2"
"github.com/fatih/color"
Expand All @@ -28,20 +29,23 @@ type LinkCommand struct {
}

type link struct {
ui cli.Ui
logger hclog.Logger
modifyGitIgnore bool
apiURL string
apiClient linkAPIClient
promptSetup func(location string) (bool, error)
promptTeam func(teams []string) (string, error)
ui cli.Ui
logger hclog.Logger
modifyGitIgnore bool
apiURL string
apiClient linkAPIClient
promptSetup func(location string) (bool, error)
promptTeam func(teams []string) (string, error)
promptEnableCaching func() (bool, error)
openBrowser func(url string) error
}

type linkAPIClient interface {
IsLoggedIn() bool
GetTeams() (*client.TeamsResponse, error)
GetUser() (*client.UserResponse, error)
SetTeamID(teamID string)
GetCachingStatus(teamID string) (util.CachingStatus, error)
}

func getCmd(config *config.Config, ui cli.Ui) *cobra.Command {
Expand All @@ -51,18 +55,26 @@ func getCmd(config *config.Config, ui cli.Ui) *cobra.Command {
Short: "Link your local directory to a Vercel organization and enable remote caching.",
RunE: func(cmd *cobra.Command, args []string) error {
link := &link{
ui: ui,
logger: config.Logger,
modifyGitIgnore: !dontModifyGitIgnore,
apiURL: config.ApiUrl,
apiClient: config.ApiClient,
promptSetup: promptSetup,
promptTeam: promptTeam,
ui: ui,
logger: config.Logger,
modifyGitIgnore: !dontModifyGitIgnore,
apiURL: config.ApiUrl,
apiClient: config.ApiClient,
promptSetup: promptSetup,
promptTeam: promptTeam,
promptEnableCaching: promptEnableCaching,
openBrowser: browser.OpenBrowser,
}
err := link.run()
if err != nil {
if errors.Is(err, errUserCanceled) {
ui.Info("Canceled. Turborepo not set up.")
} else if errors.Is(err, errTryAfterEnable) {
ui.Info("Please run 'turbo link' again after remote caching has been enabled")
} else if errors.Is(err, errNeedCachingEnabled) {
ui.Info("Please contact your account owner to enable remote caching on Vercel.")
} else if errors.Is(err, errOverage) {
ui.Warn("TODO: hobby error message")
} else {
link.logError(err)
}
Expand Down Expand Up @@ -98,7 +110,12 @@ func (c *LinkCommand) Run(args []string) int {
return 0
}

var errUserCanceled = errors.New("Canceled")
var (
errUserCanceled = errors.New("canceled")
errOverage = errors.New("usage limit")
errNeedCachingEnabled = errors.New("caching not enabled")
errTryAfterEnable = errors.New("link after enabling caching")
)

func (l *link) run() error {
dir, err := homedir.Dir()
Expand Down Expand Up @@ -157,21 +174,55 @@ func (l *link) run() error {
if chosenTeamName == "" {
return errUserCanceled
}
isUser := (chosenTeamName == userResponse.User.Name) || (chosenTeamName == userResponse.User.Username)
var chosenTeam client.Team
if (chosenTeamName == userResponse.User.Name) || (chosenTeamName == userResponse.User.Username) {
chosenTeam = client.Team{
ID: userResponse.User.ID,
Name: userResponse.User.Name,
Slug: userResponse.User.Username,
}
var accountID string
if isUser {
accountID = userResponse.User.ID
} else {
for _, team := range teamsResponse.Teams {
if team.Name == chosenTeamName {
chosenTeam = team
break
}
}
accountID = chosenTeam.ID
}

cachingStatus, err := l.apiClient.GetCachingStatus(accountID)
if err != nil {
return err
}
switch cachingStatus {
case util.CachingStatusDisabled:
if isUser || chosenTeam.IsOwner() {
shouldEnable, err := l.promptEnableCaching()
if err != nil {
return err
}
if shouldEnable {
var url string
if isUser {
url = "https://vercel.com/account/billing"
} else {
url = fmt.Sprintf("https://vercel.com/teams/%v/settings/billing", chosenTeam.Slug)
}
err = l.openBrowser(url)
if err != nil {
l.ui.Warn(fmt.Sprintf("Failed to open browser. Please visit %v to enable Remote Caching", url))
} else {
l.ui.Info(fmt.Sprintf("Visit %v in your browser to enable Remote Caching", url))
}
}
return errTryAfterEnable
}
return errNeedCachingEnabled
case util.CachingStatusOverLimit:
return errOverage
case util.CachingStatusEnabled:
default:
}

fs.EnsureDir(filepath.Join(".turbo", "config.json"))
err = config.WriteRepoConfigFile(&config.TurborepoConfig{
TeamId: chosenTeam.ID,
Expand Down Expand Up @@ -203,6 +254,25 @@ func (l *link) logError(err error) {
l.ui.Error(fmt.Sprintf("%s%s", ui.ERROR_PREFIX, color.RedString(" %v", err)))
}

func promptEnableCaching() (bool, error) {
shouldEnable := false
err := survey.AskOne(
&survey.Confirm{
Default: true,
Message: util.Sprintf("Remote Caching was previously disabled for this team. Would you like to enable it now?"),
},
&shouldEnable,
survey.WithValidator(survey.Required),
survey.WithIcons(func(icons *survey.IconSet) {
icons.Question.Format = "gray+hb"
}),
)
if err != nil {
return false, err
}
return shouldEnable, nil
}

func promptSetup(location string) (bool, error) {
shouldSetup := true
err := survey.AskOne(
Expand Down
31 changes: 31 additions & 0 deletions cli/internal/util/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package util

import "fmt"

// CachingStatus represents the api server's perspective
// on whether remote caching should be allowed
type CachingStatus int

const (
// CachingStatusDisabled indicates that the server will not accept or serve artifacts
CachingStatusDisabled CachingStatus = iota
// CachingStatusEnabled indicates that the server will accept and serve artifacts
CachingStatusEnabled
// CachingStatusOverLimit indicates that a usage limit has been hit and the
// server will temporarily not accept or server artifacts
CachingStatusOverLimit
)

// CachingStatusFromString parses a raw string to a caching status enum value
func CachingStatusFromString(raw string) (CachingStatus, error) {
switch raw {
case "disabled":
return CachingStatusDisabled, nil
case "enabled":
return CachingStatusEnabled, nil
case "over_limit":
return CachingStatusOverLimit, nil
default:
return CachingStatusDisabled, fmt.Errorf("unknown caching status: %v", raw)
}
}