Show a better error when --force needs to be passed to stack rm

When `pulumi stack rm` is run against a stack with resources, the
service will respond with an error if `--force` is not
passed. Previously we would just dump the contents of this error and
it looked something like:

`error: [400] Bad Request: Stack still has resources.`

We now handle this case more gracefully, showing our usual "this stack
still has resources" error like we would for the local backend.

Fixes #2431
This commit is contained in:
Matt Ellis 2019-02-06 19:07:31 -08:00
parent 6f386567f6
commit 687a780b20
3 changed files with 48 additions and 5 deletions

View file

@ -2,6 +2,8 @@
### Improvements
- When trying to `stack rm` a stack managed by pulumi.com that has resources, the error message now informs you to pass `--force` if you really want to remove a stack that still has resources under management, as this would orphan these resources (fixes [pulumi/pulumi#2431](https://github.com/pulumi/pulumi/issues/2431)).
## 0.16.14 (Released January 31th, 2019)
### Improvements

View file

@ -243,13 +243,27 @@ func (pc *Client) CreateStack(
// DeleteStack deletes the indicated stack. If force is true, the stack is deleted even if it contains resources.
func (pc *Client) DeleteStack(ctx context.Context, stack StackIdentifier, force bool) (bool, error) {
path := getStackPath(stack)
if force {
path += "?force=true"
queryObj := struct {
Force bool `url:"force"`
}{
Force: force,
}
// TODO[pulumi/pulumi-service#196] When the service returns a well known response for "this stack still has
// resources and `force` was not true", we should sniff for that message and return a true for the boolean.
return false, pc.restCall(ctx, "DELETE", path, nil, nil, nil)
err := pc.restCall(ctx, "DELETE", path, queryObj, nil, nil)
return isStackHasResourcesError(err), err
}
func isStackHasResourcesError(err error) bool {
if err == nil {
return false
}
errRsp, ok := err.(*apitype.ErrorResponse)
if !ok {
return false
}
return errRsp.Code == 400 && errRsp.Message == "Bad Request: Stack still contains resources."
}
// EncryptValue encrypts a plaintext value in the context of the indicated stack.

View file

@ -12,6 +12,8 @@ import (
"testing"
"time"
"github.com/pulumi/pulumi/pkg/util/contract"
"github.com/stretchr/testify/assert"
"github.com/pulumi/pulumi/pkg/apitype"
@ -207,6 +209,31 @@ func TestStackTagValidation(t *testing.T) {
})
}
func TestRemoveWithResourcesBlocked(t *testing.T) {
if os.Getenv("PULUMI_ACCESS_TOKEN") == "" {
t.Skipf("Skipping: PULUMI_ACCESS_TOKEN is not set")
}
e := ptesting.NewEnvironment(t)
defer func() {
if !t.Failed() {
e.DeleteEnvironment()
}
}()
stackName, err := resource.NewUniqueHex("rm-test-", 8, -1)
contract.AssertNoErrorf(err, "resource.NewUniqueHex sould not fail with no maximum length is set")
e.ImportDirectory(filepath.Join("empty", "nodejs"))
e.RunCommand("pulumi", "stack", "init", stackName)
e.RunCommand("yarn", "link", "@pulumi/pulumi")
e.RunCommand("pulumi", "up", "--non-interactive", "--skip-preview")
_, stderr := e.RunCommandExpectError("pulumi", "stack", "rm", "--yes")
assert.Contains(t, stderr, "--force")
e.RunCommand("pulumi", "destroy", "--skip-preview", "--non-interactive", "--yes")
e.RunCommand("pulumi", "stack", "rm", "--yes")
}
// TestStackOutputs ensures we can export variables from a stack and have them get recorded as outputs.
func TestStackOutputsNodeJS(t *testing.T) {
integration.ProgramTest(t, &integration.ProgramTestOptions{