diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e14c9101..6595fb308 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/pkg/backend/httpstate/client/client.go b/pkg/backend/httpstate/client/client.go index 5e7939eb8..64e3e51e2 100644 --- a/pkg/backend/httpstate/client/client.go +++ b/pkg/backend/httpstate/client/client.go @@ -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. diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 0560400e1..53bd7b2ab 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -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{