diff --git a/pkg/api/undocumented/undocumented.go b/pkg/api/undocumented/undocumented.go index e224cd219..7c9a5707d 100644 --- a/pkg/api/undocumented/undocumented.go +++ b/pkg/api/undocumented/undocumented.go @@ -17,12 +17,30 @@ import ( "github.com/fastly/cli/pkg/useragent" ) -// EdgeComputeTrial is the API endpoint for activating a compute trial. -const EdgeComputeTrial = "/customer/%s/edge-compute-trial" +// EntitledProductCheck is the API endpoint for checking whether a user already +// has paid access to the specified product. +const EntitledProductCheck = "/entitled-products/%s" + +// EntitledProductMessageCompute is shown to a user who doesn't yet have paid +// access to the Compute product. +const EntitledProductMessageCompute = "By creating this Compute service, you acknowledge that the service is a trial service for evaluation purposes subject to Fastly’s terms of service (www.fastly.com/terms)." + +// ProductCompute is the ID for the Compute product. +const ProductCompute = "compute" // RequestTimeout is the timeout for the API network request. const RequestTimeout = 5 * time.Second +// EntitledProductResponse represents the API response for requesting a +// customer's entitlement data. +type EntitledProductResponse struct { + AccessLevel string `json:"access_level"` + CustomerID string `json:"customer_id"` + HasAccess bool `json:"has_access"` + HasPermToDisable bool `json:"has_permission_to_disable"` + HasPermToEnable bool `json:"has_permission_to_enable"` +} + // APIError models a custom error for undocumented API calls. type APIError struct { Err error @@ -101,7 +119,11 @@ func Call(opts CallOptions) (data []byte, err error) { Remediation: fsterr.NetworkRemediation, } } - return data, NewError(err, 0) + statusCode := http.StatusInternalServerError + if res != nil { + statusCode = res.StatusCode + } + return data, NewError(err, statusCode) } defer res.Body.Close() // #nosec G307 diff --git a/pkg/commands/compute/deploy.go b/pkg/commands/compute/deploy.go index f11426f11..e4e3e2ac6 100644 --- a/pkg/commands/compute/deploy.go +++ b/pkg/commands/compute/deploy.go @@ -1,6 +1,7 @@ package compute import ( + "encoding/json" "errors" "fmt" "io" @@ -163,7 +164,7 @@ func (c *DeployCommand) Exec(in io.Reader, out io.Writer) (err error) { text.Break(out) } - fnActivateTrial, serviceID, err := c.Setup(out) + serviceID, err := c.Setup(out) if err != nil { return err } @@ -190,7 +191,7 @@ func (c *DeployCommand) Exec(in io.Reader, out io.Writer) (err error) { var serviceVersion *fastly.Version if noExistingService { - serviceID, serviceVersion, err = c.NewService(manifestFilename, fnActivateTrial, spinner, in, out) + serviceID, serviceVersion, err = c.NewService(manifestFilename, spinner, in, out) if err != nil { return err } @@ -351,14 +352,7 @@ func validStatusCodeRange(status int) bool { // - Acquire the Service ID/Version. // - Validate there is a package to deploy. // - Determine if a trial needs to be activated on the user's account. -func (c *DeployCommand) Setup(out io.Writer) (fnActivateTrial Activator, serviceID string, err error) { - defaultActivator := func(_ string) error { return nil } - - token, s := c.Globals.Token() - if s == lookup.SourceUndefined { - return defaultActivator, "", fsterr.ErrNoToken - } - +func (c *DeployCommand) Setup(out io.Writer) (serviceID string, err error) { // IMPORTANT: We don't handle the error when looking up the Service ID. // This is because later in the Exec() flow we might create a 'new' service. serviceID, source, flag, err := argparser.ServiceID(c.ServiceName, *c.Globals.Manifest, c.Globals.APIClient, c.Globals.ErrLog) @@ -369,7 +363,7 @@ func (c *DeployCommand) Setup(out io.Writer) (fnActivateTrial Activator, service if c.PackagePath == "" { projectName, source := c.Globals.Manifest.Name() if source == manifest.SourceUndefined { - return defaultActivator, serviceID, fsterr.ErrReadingManifest + return serviceID, fsterr.ErrReadingManifest } c.PackagePath = filepath.Join("pkg", fmt.Sprintf("%s.tar.gz", sanitize.BaseName(projectName))) } @@ -379,13 +373,10 @@ func (c *DeployCommand) Setup(out io.Writer) (fnActivateTrial Activator, service c.Globals.ErrLog.AddWithContext(err, map[string]any{ "Package path": c.PackagePath, }) - return defaultActivator, serviceID, err + return serviceID, err } - endpoint, _ := c.Globals.APIEndpoint() - fnActivateTrial = preconfigureActivateTrial(endpoint, token, c.Globals.HTTPClient, c.Globals.Env.DebugMode) - - return fnActivateTrial, serviceID, err + return serviceID, err } // validatePackage checks the package and returns its path, which can change @@ -487,44 +478,8 @@ func packageSize(path string) (size int64, err error) { return fi.Size(), nil } -// Activator represents a function that calls an undocumented API endpoint for -// activating a Compute free trial on the given customer account. -// -// It is preconfigured with the Fastly API endpoint, a user token and a simple -// HTTP Client. -// -// This design allows us to pass an Activator rather than passing multiple -// unrelated arguments through several nested functions. -type Activator func(customerID string) error - -// preconfigureActivateTrial activates a free trial on the customer account. -func preconfigureActivateTrial(endpoint, token string, httpClient api.HTTPClient, debugMode string) Activator { - debug, _ := strconv.ParseBool(debugMode) - return func(customerID string) error { - _, err := undocumented.Call(undocumented.CallOptions{ - APIEndpoint: endpoint, - HTTPClient: httpClient, - Method: http.MethodPost, - Path: fmt.Sprintf(undocumented.EdgeComputeTrial, customerID), - Token: token, - Debug: debug, - }) - if err != nil { - apiErr, ok := err.(undocumented.APIError) - if !ok { - return err - } - // 409 Conflict == The Compute trial has already been created. - if apiErr.StatusCode != http.StatusConflict { - return fmt.Errorf("%w: %d %s", err, apiErr.StatusCode, http.StatusText(apiErr.StatusCode)) - } - } - return nil - } -} - // NewService handles creating a new service when no Service ID is found. -func (c *DeployCommand) NewService(manifestFilename string, fnActivateTrial Activator, spinner text.Spinner, in io.Reader, out io.Writer) (string, *fastly.Version, error) { +func (c *DeployCommand) NewService(manifestFilename string, spinner text.Spinner, in io.Reader, out io.Writer) (string, *fastly.Version, error) { var ( err error serviceID string @@ -571,7 +526,7 @@ func (c *DeployCommand) NewService(manifestFilename string, fnActivateTrial Acti // There is no service and so we'll do a one time creation of the service // // NOTE: we're shadowing the `serviceID` and `serviceVersion` variables. - serviceID, serviceVersion, err = createService(c.Globals, serviceName, fnActivateTrial, spinner, out) + serviceID, serviceVersion, err = createService(c.Globals, serviceName, spinner, in, out) if err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ "Service name": serviceName, @@ -601,16 +556,7 @@ func (c *DeployCommand) NewService(manifestFilename string, fnActivateTrial Acti } // createService creates a service to associate with the compute package. -// -// NOTE: If the creation of the service fails because the user has not -// activated a free trial, then we'll trigger the trial for their account. -func createService( - g *global.Data, - serviceName string, - fnActivateTrial Activator, - spinner text.Spinner, - out io.Writer, -) (serviceID string, serviceVersion *fastly.Version, err error) { +func createService(g *global.Data, serviceName string, spinner text.Spinner, in io.Reader, out io.Writer) (serviceID string, serviceVersion *fastly.Version, err error) { f := g.Flags apiClient := g.APIClient errLog := g.ErrLog @@ -619,6 +565,59 @@ func createService( text.Break(out) } + // Before we create the service, we first check if the user has either paid + // access to the Compute product or is already on a trial (i.e. `has_access` + // will be `true`). If `has_access` is `false`, then we'll display a message + // to explain that the service we're about to create will be part of a trial + // access to the Compute product. The `has_access` will be `true` once the + // service is created and the user creates a new service using the CLI (as + // this means we don't keep showing them the 'trial' message unnecessarily). + // The API will internally handle the service trial activation (if needed). + apiEndpoint, _ := g.APIEndpoint() + token, s := g.Token() + if s == lookup.SourceUndefined { + return "", nil, fsterr.ErrNoToken + } + debug, _ := strconv.ParseBool(g.Env.DebugMode) + data, err := undocumented.Call(undocumented.CallOptions{ + APIEndpoint: apiEndpoint, + HTTPClient: g.HTTPClient, + Method: http.MethodGet, + Path: fmt.Sprintf(undocumented.EntitledProductCheck, undocumented.ProductCompute), + Token: token, + Debug: debug, + }) + if err != nil { + if apiErr, ok := err.(undocumented.APIError); ok { + err = fmt.Errorf("%w: %d %s", err, apiErr.StatusCode, http.StatusText(apiErr.StatusCode)) + } + err = fmt.Errorf("error checking entitlement to the Compute product: %w", err) + return "", nil, fsterr.RemediationError{ + Inner: err, + Remediation: fsterr.ComputeAccessRemediation, + } + } + + var epr undocumented.EntitledProductResponse + if err := json.Unmarshal(data, &epr); err != nil { + return "", nil, fsterr.RemediationError{ + Inner: err, + Remediation: fsterr.ComputeAccessRemediation, + } + } + + if !epr.HasAccess { + text.Info(out, undocumented.EntitledProductMessageCompute+"\n\n") + cont, err := text.AskYesNo(out, "Are you sure you want to continue? [y/N]: ", in) + if err != nil { + return "", nil, err + } + if !cont { + return "", nil, fsterr.ErrComputeTrialStopped + } + text.Break(out) + } + err = spinner.Start() if err != nil { return "", nil, err @@ -631,60 +630,15 @@ func createService( Type: fastly.ToPointer("wasm"), }) if err != nil { - if strings.Contains(err.Error(), trialNotActivated) { - user, err := apiClient.GetCurrentUser() - if err != nil { - err = fmt.Errorf("unable to identify user associated with the given token: %w", err) - spinner.StopFailMessage(msg) - spinErr := spinner.StopFail() - if spinErr != nil { - return "", nil, fmt.Errorf(text.SpinnerErrWrapper, spinErr, err) - } - return serviceID, serviceVersion, fsterr.RemediationError{ - Inner: err, - Remediation: "To ensure you have access to the Compute platform we need your Customer ID. " + fsterr.AuthRemediation, - } - } - - customerID := fastly.ToValue(user.CustomerID) - err = fnActivateTrial(customerID) - if err != nil { - err = fmt.Errorf("error creating service: you do not have the Compute free trial enabled on your Fastly account") - spinner.StopFailMessage(msg) - spinErr := spinner.StopFail() - if spinErr != nil { - return "", nil, fmt.Errorf(text.SpinnerErrWrapper, spinErr, err) - } - return serviceID, serviceVersion, fsterr.RemediationError{ - Inner: err, - Remediation: fsterr.ComputeTrialRemediation, - } - } - - errLog.AddWithContext(err, map[string]any{ - "Service Name": serviceName, - "Customer ID": customerID, - }) - - spinner.StopFailMessage(msg) - err = spinner.StopFail() - if err != nil { - return "", nil, err - } - - return createService(g, serviceName, fnActivateTrial, spinner, out) - } - spinner.StopFailMessage(msg) spinErr := spinner.StopFail() if spinErr != nil { return "", nil, spinErr } - errLog.AddWithContext(err, map[string]any{ "Service Name": serviceName, }) - return serviceID, serviceVersion, fmt.Errorf("error creating service: %w", err) + return "", nil, fmt.Errorf("error creating service: %w", err) } spinner.StopMessage(msg) @@ -1211,7 +1165,7 @@ func (c *DeployCommand) ExistingServiceVersion(serviceID string, out io.Writer) }) return serviceVersion, fsterr.RemediationError{ Inner: fmt.Errorf("invalid service type: %s", serviceType), - Remediation: "Ensure the provided Service ID is associated with a 'Wasm' Fastly Service and not a 'VCL' Fastly service. " + fsterr.ComputeTrialRemediation, + Remediation: "Ensure the provided Service ID is associated with a 'Compute' Fastly Service and not a 'CDN' Fastly service. " + fsterr.ComputeAccessRemediation, } } diff --git a/pkg/commands/compute/deploy_test.go b/pkg/commands/compute/deploy_test.go index f60b6fa49..ac9a50b34 100644 --- a/pkg/commands/compute/deploy_test.go +++ b/pkg/commands/compute/deploy_test.go @@ -1,11 +1,10 @@ package compute_test import ( - "context" + "errors" "fmt" "io" "net/http" - "net/url" "os" "path/filepath" "strings" @@ -16,7 +15,7 @@ import ( "github.com/fastly/cli/pkg/app" "github.com/fastly/cli/pkg/commands/compute" - "github.com/fastly/cli/pkg/errors" + fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/manifest" "github.com/fastly/cli/pkg/mock" @@ -94,10 +93,8 @@ func TestDeploy(t *testing.T) { args []string dontWantOutput []string // There are two times the HTTPClient is used. - // The first is if we need to activate a free trial. + // The first is if we need to bypass the entitlement API call. // The second is when we ping for service availability. - // In this test case the free trial activation isn't used. - // So we only define a single HTTP client call for service availability. httpClientRes []*http.Response httpClientErr []error manifest string @@ -113,7 +110,7 @@ func TestDeploy(t *testing.T) { name: "no fastly.toml manifest", args: args("compute deploy --token 123"), wantError: "error reading fastly.toml", - wantRemediationError: errors.ComputeInitRemediation, + wantRemediationError: fsterr.ComputeInitRemediation, noManifest: true, }, { @@ -123,7 +120,30 @@ func TestDeploy(t *testing.T) { // // Additionally it validates that the specified path (files generated by // the testutil.NewEnv()) cause no issues. - name: "path with no service ID", + name: "success with no service ID", + args: args("compute deploy --token 123 -v --package pkg/package.tar.gz"), + api: mock.API{ + ActivateVersionFn: activateVersionOk, + CreateBackendFn: createBackendOK, + CreateDomainFn: createDomainOK, + CreateServiceFn: createServiceOK, + GetPackageFn: getPackageOk, + ListDomainsFn: listDomainsOk, + UpdatePackageFn: updatePackageOk, + }, + stdin: []string{ + "Y", // when prompted to create a new service + }, + wantOutput: []string{ + "Deployed package (service 12345, version 1)", + }, + }, + { + // This test validates a new service creation for someone either without + // paid access to the Compute product, or who are already using the trial + // and so they don't need to be reminded each time that their service is + // part of a trial. + name: "success with no service ID and showing trial message", args: args("compute deploy --token 123 -v --package pkg/package.tar.gz"), api: mock.API{ ActivateVersionFn: activateVersionOk, @@ -136,7 +156,7 @@ func TestDeploy(t *testing.T) { }, httpClientRes: []*http.Response{ { - Body: io.NopCloser(strings.NewReader("success")), + Body: io.NopCloser(strings.NewReader(`{"has_access":false}`)), Status: http.StatusText(http.StatusOK), StatusCode: http.StatusOK, }, @@ -146,16 +166,23 @@ func TestDeploy(t *testing.T) { }, stdin: []string{ "Y", // when prompted to create a new service + "", // when prompted for a service name use the default + "Y", // when prompted to approve the trial account setup + "", // this is so we generate a backend name using a built-in formula + "", // this stops prompting for backends }, wantOutput: []string{ + "INFO: By creating this Compute service,", + "you acknowledge that the service is a trial service", "Deployed package (service 12345, version 1)", }, }, - // Same validation as above with the exception that we use the default path - // parsing logic (i.e. we don't explicitly pass a path via `-p` flag). { - name: "empty service ID", - args: args("compute deploy --token 123 -v"), + // This test is the same as above but instead we don't configure a Y + // response to the prompt asking if they want to start the trial account. + // So this leads to an error. + name: "error when declining trial account setup", + args: args("compute deploy --token 123 -v --package pkg/package.tar.gz"), api: mock.API{ ActivateVersionFn: activateVersionOk, CreateBackendFn: createBackendOK, @@ -167,7 +194,7 @@ func TestDeploy(t *testing.T) { }, httpClientRes: []*http.Response{ { - Body: io.NopCloser(strings.NewReader("success")), + Body: io.NopCloser(strings.NewReader(`{"has_access":false}`)), Status: http.StatusText(http.StatusOK), StatusCode: http.StatusOK, }, @@ -178,6 +205,44 @@ func TestDeploy(t *testing.T) { stdin: []string{ "Y", // when prompted to create a new service }, + wantError: "deploy stopped by user", + }, + { + // This test validates what happens when the entitlement check fails. + name: "error checking compute entitlement", + args: args("compute deploy --token 123 -v --package pkg/package.tar.gz"), + httpClientRes: []*http.Response{ + { + Body: nil, + Status: http.StatusText(http.StatusBadRequest), + StatusCode: http.StatusBadRequest, + }, + }, + httpClientErr: []error{ + errors.New("whoops"), + }, + stdin: []string{ + "Y", // when prompted to create a new service + }, + wantError: "error checking entitlement to the Compute product: whoops: 400 Bad Request", + }, + // Same validation as above with the exception that we use the default path + // parsing logic (i.e. we don't explicitly pass a path via `-p` flag). + { + name: "empty service ID", + args: args("compute deploy --token 123 -v"), + api: mock.API{ + ActivateVersionFn: activateVersionOk, + CreateBackendFn: createBackendOK, + CreateDomainFn: createDomainOK, + CreateServiceFn: createServiceOK, + GetPackageFn: getPackageOk, + ListDomainsFn: listDomainsOk, + UpdatePackageFn: updatePackageOk, + }, + stdin: []string{ + "Y", // when prompted to create a new service + }, wantOutput: []string{ "Deployed package (service 12345, version 1)", }, @@ -220,7 +285,7 @@ func TestDeploy(t *testing.T) { args: args("compute deploy --package pkg/package.tar.gz --token 123"), reduceSizeLimit: true, wantError: "package size is too large", - wantRemediationError: errors.PackageSizeRemediation, + wantRemediationError: fsterr.PackageSizeRemediation, }, // The following test doesn't just validate the package API error behaviour // but as a side effect it validates that when deleting the created @@ -263,113 +328,6 @@ func TestDeploy(t *testing.T) { }, wantError: fmt.Sprintf("error creating service: %s", testutil.Err.Error()), }, - // The following test mocks the service creation to fail with a specific - // error value that will result in the code trying to activate a free trial - // for the customer's account. - // - // Specifically this test will fail the initial API call to get the - // customer's details and so we expect it to return that error (as we can't - // activate a free trial without knowing the customer ID). - { - name: "service create error due to no trial activated and error getting user", - args: args("compute deploy --token 123"), - api: mock.API{ - CreateServiceFn: createServiceErrorNoTrial, - DeleteServiceFn: deleteServiceOK, - GetCurrentUserFn: getCurrentUserError, - }, - stdin: []string{ - "Y", // when prompted to create a new service - }, - wantError: fmt.Sprintf("unable to identify user associated with the given token: %s", testutil.Err.Error()), - wantOutput: []string{ - "Creating service", - }, - }, - // The following test mocks the HTTP client to return a 400 Bad Request, - // which is then coerced into a generic 'no free trial' error. - { - name: "service create error due to no trial activated and error activating trial", - args: args("compute deploy --token 123"), - api: mock.API{ - CreateServiceFn: createServiceErrorNoTrial, - DeleteServiceFn: deleteServiceOK, - GetCurrentUserFn: getCurrentUser, - }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader(testutil.Err.Error())), - Status: http.StatusText(http.StatusBadRequest), - StatusCode: http.StatusBadRequest, - }, - }, - httpClientErr: []error{ - nil, - }, - stdin: []string{ - "Y", // when prompted to create a new service - }, - wantError: "error creating service: you do not have the Compute free trial enabled on your Fastly account", - wantRemediationError: errors.ComputeTrialRemediation, - wantOutput: []string{ - "Creating service", - }, - }, - // The following test mocks the HTTP client to return a timeout error, - // which is then coerced into a generic 'no free trial' error. - { - name: "service create error due to no trial activated and activating trial timeout", - args: args("compute deploy --token 123"), - api: mock.API{ - CreateServiceFn: createServiceErrorNoTrial, - DeleteServiceFn: deleteServiceOK, - GetCurrentUserFn: getCurrentUser, - }, - httpClientRes: []*http.Response{ - nil, - }, - httpClientErr: []error{ - &url.Error{Err: context.DeadlineExceeded}, - }, - stdin: []string{ - "Y", // when prompted to create a new service - }, - wantError: "error creating service: you do not have the Compute free trial enabled on your Fastly account", - wantRemediationError: errors.ComputeTrialRemediation, - wantOutput: []string{ - "Creating service", - }, - }, - // The following test mocks the HTTP client to return successfully when - // trying to activate the free trial. - { - name: "service create success", - args: args("compute deploy --token 123"), - api: mock.API{ - ActivateVersionFn: activateVersionOk, - CreateBackendFn: createBackendOK, - CreateServiceFn: createServiceOK, - GetPackageFn: getPackageOk, - ListDomainsFn: listDomainsOk, - UpdatePackageFn: updatePackageOk, - }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, - stdin: []string{ - "Y", // when prompted to create a new service - }, - wantOutput: []string{ - "Creating service", - }, - }, // The following test doesn't provide a Service ID by either a flag nor the // manifest, so this will result in the deploy script attempting to create // a new service. We mock the service creation to be successful while we @@ -500,16 +458,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, wantOutput: []string{ "Uploading package", "Activating service", @@ -532,16 +480,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, wantOutput: []string{ "Uploading package", "Activating service", @@ -568,16 +506,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, noManifest: true, wantOutput: []string{ "Using fastly.toml within --package archive:", @@ -602,16 +530,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, wantOutput: []string{ "Uploading package", "Activating service", @@ -631,16 +549,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, wantOutput: []string{ "Uploading package", "Activating service", @@ -660,16 +568,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, wantOutput: []string{ "Uploading package", "Activating service", @@ -690,16 +588,6 @@ func TestDeploy(t *testing.T) { UpdatePackageFn: updatePackageOk, UpdateVersionFn: updateVersionOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, wantOutput: []string{ "Uploading package", "Activating service", @@ -723,16 +611,6 @@ func TestDeploy(t *testing.T) { ListDomainsFn: listDomainsOk, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -778,16 +656,6 @@ func TestDeploy(t *testing.T) { ListDomainsFn: listDomainsOk, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -830,16 +698,6 @@ func TestDeploy(t *testing.T) { ListDomainsFn: listDomainsOk, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -883,16 +741,6 @@ func TestDeploy(t *testing.T) { ListDomainsFn: listDomainsOk, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -941,16 +789,6 @@ func TestDeploy(t *testing.T) { ListDomainsFn: listDomainsOk, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, wantOutput: []string{ "SUCCESS: Deployed package (service 12345, version 1)", }, @@ -970,16 +808,6 @@ func TestDeploy(t *testing.T) { ListDomainsFn: listDomainsOk, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, stdin: []string{ "Y", // when prompted to create a new service "foobar", // when prompted for service name @@ -1011,16 +839,6 @@ func TestDeploy(t *testing.T) { ListDomainsFn: listDomainsOk, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, stdin: []string{ "Y", // when prompted to create a new service "foobar", // when prompted for service name @@ -1056,16 +874,6 @@ func TestDeploy(t *testing.T) { ListDomainsFn: listDomainsOk, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, stdin: []string{ "Y", // when prompted to create a new service "foobar", // when prompted for service name @@ -1095,16 +903,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, wantOutput: []string{ "SUCCESS: Deployed package (service 12345, version 1)", }, @@ -1132,16 +930,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -1184,16 +972,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -1241,16 +1019,6 @@ func TestDeploy(t *testing.T) { UpdateConfigStoreItemFn: updateConfigStoreItemOK, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -1303,16 +1071,6 @@ func TestDeploy(t *testing.T) { UpdateConfigStoreItemFn: updateConfigStoreItemOK, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -1365,16 +1123,6 @@ func TestDeploy(t *testing.T) { UpdateConfigStoreItemFn: updateConfigStoreItemOK, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -1420,16 +1168,6 @@ func TestDeploy(t *testing.T) { UpdateConfigStoreItemFn: updateConfigStoreItemOK, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -1477,16 +1215,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -1525,16 +1253,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -1574,16 +1292,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -1624,16 +1332,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -1671,16 +1369,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -1728,16 +1416,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -1790,16 +1468,6 @@ func TestDeploy(t *testing.T) { ListKVStoresFn: listKVStoresEmpty, ListVersionsFn: testutil.ListVersions, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -1839,16 +1507,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -1894,16 +1552,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -1952,16 +1600,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -2007,16 +1645,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -2073,16 +1701,6 @@ func TestDeploy(t *testing.T) { ListVersionsFn: testutil.ListVersions, UpdatePackageFn: updatePackageOk, }, - httpClientRes: []*http.Response{ - { - Body: io.NopCloser(strings.NewReader("success")), - Status: http.StatusText(http.StatusOK), - StatusCode: http.StatusOK, - }, - }, - httpClientErr: []error{ - nil, - }, manifest: ` name = "package" manifest_version = 2 @@ -2159,6 +1777,20 @@ func TestDeploy(t *testing.T) { if testcase.httpClientRes != nil || testcase.httpClientErr != nil { opts.HTTPClient = mock.HTMLClient(testcase.httpClientRes, testcase.httpClientErr) + } else { + // Default to mocking Compute entitlement check to be successful. + opts.HTTPClient = mock.HTMLClient( + []*http.Response{ + { + Body: io.NopCloser(strings.NewReader(`{"has_access":true}`)), + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + }, + }, + []error{ + nil, + }, + ) } if testcase.reduceSizeLimit { @@ -2251,22 +1883,6 @@ func createServiceError(*fastly.CreateServiceInput) (*fastly.Service, error) { return nil, testutil.Err } -// NOTE: We don't return testutil.Err but a very specific error message so that -// the Deploy logic will drop into a nested logic block. -func createServiceErrorNoTrial(*fastly.CreateServiceInput) (*fastly.Service, error) { - return nil, fmt.Errorf("Valid values for 'type' are: 'vcl'") -} - -func getCurrentUser() (*fastly.User, error) { - return &fastly.User{ - CustomerID: fastly.ToPointer("abc"), - }, nil -} - -func getCurrentUserError() (*fastly.User, error) { - return nil, testutil.Err -} - func deleteServiceOK(_ *fastly.DeleteServiceInput) error { return nil } diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index 091005bac..3d096968f 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -171,3 +171,7 @@ var ErrInvalidEnableDisableFlagCombo = RemediationError{ Inner: fmt.Errorf("invalid flag combination: --enable and --disable"), Remediation: "Use either --enable or --disable, not both.", } + +// ErrComputeTrialStopped means the user declined to go ahead and create a trial +// account to deploy their Compute application. +var ErrComputeTrialStopped = errors.New("deploy stopped by user") diff --git a/pkg/errors/remediation_error.go b/pkg/errors/remediation_error.go index 14e990993..200e9ae37 100644 --- a/pkg/errors/remediation_error.go +++ b/pkg/errors/remediation_error.go @@ -150,9 +150,9 @@ var ComputeBuildRemediation = strings.Join([]string{ "See more at https://www.fastly.com/documentation/reference/compute/fastly-toml", }, " ") -// ComputeTrialRemediation suggests contacting customer manager to enable the -// free trial feature flag. -var ComputeTrialRemediation = "For more help with this error see fastly.help/cli/ecp-feature" +// ComputeAccessRemediation directs users to an official help page when there are +// issues checking the entitlement to the Compute product. +var ComputeAccessRemediation = "For more help with this error see https://www.fastly.com/documentation/help/cli/ecp-feature/ and contact https://support.fastly.com/" // ProfileRemediation suggests no profiles exist. var ProfileRemediation = "Run `fastly profile create ` to create a profile, or `fastly profile list` to view available profiles (at least one profile should be set as 'default')." diff --git a/pkg/mock/client.go b/pkg/mock/client.go index fd714cf84..94f4ca8bc 100644 --- a/pkg/mock/client.go +++ b/pkg/mock/client.go @@ -32,7 +32,7 @@ type HTTPClient struct { // Get mocks a HTTP Client Get request. func (c HTTPClient) Get(p string, _ *fastly.RequestOptions) (*http.Response, error) { - fmt.Printf("p: %#v\n", p) + fmt.Printf("(c HTTPClient) Get(p): %#v\n", p) // IMPORTANT: Have to increment on defer as index is already 0 by this point. // This is opposite to the Do() method which is -1 at the time it's called. defer func() { c.Index++ }() @@ -41,8 +41,8 @@ func (c HTTPClient) Get(p string, _ *fastly.RequestOptions) (*http.Response, err // Do mocks a HTTP Client Do operation. func (c HTTPClient) Do(r *http.Request) (*http.Response, error) { - fmt.Printf("r.URL: %#v\n", r.URL.String()) - fmt.Printf("r: %#v\n", r) + fmt.Printf("(c HTTPClient) Do(r *http.Request): r.URL: %#v\n", r.URL.String()) + fmt.Printf("(c HTTPClient) Do(r *http.Request): r: %#v\n", r) c.Index++ return c.Responses[c.Index], c.Errors[c.Index] }