Add new 'pulumi state' command for editing state (#2024)
* Add new 'pulumi state' command for editing state This commit adds 'pulumi state unprotect' and 'pulumi state delete', two commands that can be used to unprotect and delete resources from a stack's state, respectively. * Simplify LocateResource * CR: Print yellow 'warning' before editing state * Lots of CR feedback * CR: Only delete protected resources when asked with --force
This commit is contained in:
parent
a71db160e8
commit
730a929c2b
|
@ -160,6 +160,7 @@ func NewPulumiCmd() *cobra.Command {
|
|||
// - Advanced Commands:
|
||||
cmd.AddCommand(newCancelCmd())
|
||||
cmd.AddCommand(newRefreshCmd())
|
||||
cmd.AddCommand(newStateCmd())
|
||||
// - Other Commands:
|
||||
cmd.AddCommand(newLogsCmd())
|
||||
cmd.AddCommand(newPluginCmd())
|
||||
|
|
175
cmd/state.go
Normal file
175
cmd/state.go
Normal file
|
@ -0,0 +1,175 @@
|
|||
// Copyright 2016-2018, Pulumi Corporation.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/apitype"
|
||||
"github.com/pulumi/pulumi/pkg/backend/display"
|
||||
"github.com/pulumi/pulumi/pkg/diag/colors"
|
||||
"github.com/pulumi/pulumi/pkg/resource"
|
||||
"github.com/pulumi/pulumi/pkg/resource/deploy"
|
||||
"github.com/pulumi/pulumi/pkg/resource/edit"
|
||||
"github.com/pulumi/pulumi/pkg/resource/stack"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
survey "gopkg.in/AlecAivazis/survey.v1"
|
||||
surveycore "gopkg.in/AlecAivazis/survey.v1/core"
|
||||
)
|
||||
|
||||
func newStateCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "state",
|
||||
Short: "Edit the current stack's state",
|
||||
Long: `Edit the current stack's state
|
||||
|
||||
Subcommands of this command can be used to surgically edit parts of a stack's state. These can be useful when
|
||||
troubleshooting a stack or when performing specific edits that otherwise would require editing the state file by hand.`,
|
||||
Args: cmdutil.NoArgs,
|
||||
}
|
||||
|
||||
cmd.AddCommand(newStateDeleteCommand())
|
||||
cmd.AddCommand(newStateUnprotectCommand())
|
||||
return cmd
|
||||
}
|
||||
|
||||
// locateStackResource attempts to find a unique resource associated with the given URN in the given snapshot. If the
|
||||
// given URN is ambiguous and this is an interactive terminal, it prompts the user to select one of the resources in
|
||||
// the list of resources with identical URNs to operate upon.
|
||||
func locateStackResource(opts display.Options, snap *deploy.Snapshot, urn resource.URN) (*resource.State, error) {
|
||||
candidateResources := edit.LocateResource(snap, urn)
|
||||
switch {
|
||||
case len(candidateResources) == 0: // resource was not found
|
||||
return nil, errors.Errorf("No such resource %q exists in the current state", urn)
|
||||
case len(candidateResources) == 1: // resource was unambiguously found
|
||||
return candidateResources[0], nil
|
||||
}
|
||||
|
||||
// If there exist multiple resources that have the requested URN, prompt the user to select one if we're running
|
||||
// interactively. If we're not, early exit.
|
||||
if !cmdutil.Interactive() {
|
||||
errorMsg := "Resource URN ambiguously referred to multiple resources. Did you mean:\n"
|
||||
for _, res := range candidateResources {
|
||||
errorMsg += fmt.Sprintf(" %s\n", res.ID)
|
||||
}
|
||||
return nil, errors.New(errorMsg)
|
||||
}
|
||||
|
||||
// Note: this is done to adhere to the same color scheme as the `pulumi new` picker, which also does this.
|
||||
surveycore.DisableColor = true
|
||||
surveycore.QuestionIcon = ""
|
||||
surveycore.SelectFocusIcon = opts.Color.Colorize(colors.BrightGreen + ">" + colors.Reset)
|
||||
prompt := "Multiple resources with the given URN exist, please select the one to edit:"
|
||||
prompt = opts.Color.Colorize(colors.SpecPrompt + prompt + colors.Reset)
|
||||
|
||||
var options []string
|
||||
optionMap := make(map[string]*resource.State)
|
||||
for _, ambiguousResource := range candidateResources {
|
||||
// Prompt the user to select from a list of IDs, since these resources are known to all have the same URN.
|
||||
message := fmt.Sprintf("%q", ambiguousResource.ID)
|
||||
if ambiguousResource.Protect {
|
||||
message += " (Protected)"
|
||||
}
|
||||
|
||||
if ambiguousResource.Delete {
|
||||
message += " (Pending Deletion)"
|
||||
}
|
||||
|
||||
options = append(options, message)
|
||||
optionMap[message] = ambiguousResource
|
||||
}
|
||||
|
||||
var option string
|
||||
if err := survey.AskOne(&survey.Select{
|
||||
Message: prompt,
|
||||
Options: options,
|
||||
PageSize: len(options),
|
||||
}, &option, nil); err != nil {
|
||||
return nil, errors.New("no resource selected")
|
||||
}
|
||||
|
||||
return optionMap[option], nil
|
||||
}
|
||||
|
||||
// runStateEdit runs the given state edit function on a resource with the given URN in the current stack.
|
||||
func runStateEdit(urn resource.URN, operation edit.OperationFunc) error {
|
||||
return runTotalStateEdit(func(opts display.Options, snap *deploy.Snapshot) error {
|
||||
res, err := locateStackResource(opts, snap, urn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return operation(snap, res)
|
||||
})
|
||||
}
|
||||
|
||||
// runTotalStateEdit runs a snapshot-mutating function on the entirity of the current stack's snapshot. Before mutating
|
||||
// the snapshot, the user is prompted for confirmation if the current session is interactive.
|
||||
func runTotalStateEdit(operation func(opts display.Options, snap *deploy.Snapshot) error) error {
|
||||
opts := display.Options{
|
||||
Color: cmdutil.GetGlobalColorization(),
|
||||
}
|
||||
s, err := requireCurrentStack(true, opts, true /*setCurrent*/)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snap, err := s.Snapshot(commandContext())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cmdutil.Interactive() {
|
||||
confirm := false
|
||||
surveycore.DisableColor = true
|
||||
surveycore.QuestionIcon = ""
|
||||
surveycore.SelectFocusIcon = opts.Color.Colorize(colors.BrightGreen + ">" + colors.Reset)
|
||||
prompt := opts.Color.Colorize(colors.Yellow + "warning" + colors.Reset + ": ")
|
||||
prompt += "This command will edit your stack's state directly. Confirm?"
|
||||
if err = survey.AskOne(&survey.Confirm{
|
||||
Message: prompt,
|
||||
}, &confirm, nil); err != nil || !confirm {
|
||||
return errors.New("confirmation declined")
|
||||
}
|
||||
}
|
||||
|
||||
// The `operation` callback will mutate `snap` in-place. In order to validate the correctness of the transformation
|
||||
// that we are doing here, we verify the integrity of the snapshot before the mutation. If the snapshot was valid
|
||||
// before we mutated it, we'll assert that we didn't make it invalid by mutating it.
|
||||
stackIsAlreadyHosed := snap.VerifyIntegrity() != nil
|
||||
if err = operation(opts, snap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the stack is already broken, don't bother verifying the integrity here.
|
||||
if !stackIsAlreadyHosed {
|
||||
contract.AssertNoErrorf(snap.VerifyIntegrity(), "state edit produced an invalid snapshot")
|
||||
}
|
||||
|
||||
// Once we've mutated the snapshot, import it back into the backend so that it can be persisted.
|
||||
bytes, err := json.Marshal(stack.SerializeDeployment(snap))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dep := apitype.UntypedDeployment{
|
||||
Version: apitype.DeploymentSchemaVersionCurrent,
|
||||
Deployment: bytes,
|
||||
}
|
||||
return s.ImportDeployment(commandContext(), &dep)
|
||||
}
|
86
cmd/state_delete.go
Normal file
86
cmd/state_delete.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
// Copyright 2016-2018, Pulumi Corporation.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/diag"
|
||||
"github.com/pulumi/pulumi/pkg/resource"
|
||||
"github.com/pulumi/pulumi/pkg/resource/deploy"
|
||||
"github.com/pulumi/pulumi/pkg/resource/edit"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var force bool // Force deletion of protected resources
|
||||
|
||||
func newStateDeleteCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "Deletes a resource from a stack's state",
|
||||
Long: `Deletes a resource from a stack's state
|
||||
|
||||
This command deletes a resource from a stack's state, as long as it is safe to do so. Resources can't be deleted if
|
||||
there exist other resources that depend on it or are parented to it. Protected resources will not be deleted unless
|
||||
it is specifically requested using the --force flag.`,
|
||||
Args: cmdutil.ExactArgs(1),
|
||||
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
|
||||
urn := resource.URN(args[0])
|
||||
err := runStateEdit(urn, doDeletion)
|
||||
if err != nil {
|
||||
switch e := err.(type) {
|
||||
case edit.ResourceHasDependenciesError:
|
||||
message := "This resource can't be safely deleted because the following resources depend on it:\n"
|
||||
for _, dependentResource := range e.Dependencies {
|
||||
depUrn := dependentResource.URN
|
||||
message += fmt.Sprintf(" * %-15q (%s)\n", depUrn.Name(), depUrn)
|
||||
}
|
||||
|
||||
message += "\nDelete those resources first before deleting this one."
|
||||
return errors.New(message)
|
||||
case edit.ResourceProtectedError:
|
||||
return errors.New(
|
||||
"This resource can't be safely deleted because it is protected. " +
|
||||
"Re-run this command with --force to force deletion")
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Println("Resource deleted successfully")
|
||||
return nil
|
||||
}),
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&force, "force", false, "Force deletion of protected resources")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// doDeletion implements edit.OperationFunc and deletes a resource from the snapshot. If the `force` flag is present,
|
||||
// doDeletion will unprotect the resource before deleting it.
|
||||
func doDeletion(snap *deploy.Snapshot, res *resource.State) error {
|
||||
if !force {
|
||||
return edit.DeleteResource(snap, res)
|
||||
}
|
||||
|
||||
if res.Protect {
|
||||
cmdutil.Diag().Warningf(diag.RawMessage("" /*urn*/, "deleting protected resource due to presence of --force"))
|
||||
res.Protect = false
|
||||
}
|
||||
|
||||
return edit.DeleteResource(snap, res)
|
||||
}
|
84
cmd/state_unprotect.go
Normal file
84
cmd/state_unprotect.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
// Copyright 2016-2018, Pulumi Corporation.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/backend/display"
|
||||
"github.com/pulumi/pulumi/pkg/resource/deploy"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/resource"
|
||||
"github.com/pulumi/pulumi/pkg/resource/edit"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newStateUnprotectCommand() *cobra.Command {
|
||||
var unprotectAll bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "unprotect",
|
||||
Short: "Unprotect resources in a stack's state",
|
||||
Long: `Unprotect resource in a stack's state
|
||||
|
||||
This command clears the 'protect' bit on one or more resources, allowing those resources to be deleted.`,
|
||||
Args: cmdutil.MaximumNArgs(1),
|
||||
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
|
||||
if unprotectAll {
|
||||
return unprotectAllResources()
|
||||
}
|
||||
|
||||
if len(args) != 1 {
|
||||
return errors.New("must provide a URN corresponding to a resource")
|
||||
}
|
||||
|
||||
urn := resource.URN(args[0])
|
||||
return unprotectResource(urn)
|
||||
}),
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&unprotectAll, "all", false, "Unprotect all resources in the checkpoint")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func unprotectAllResources() error {
|
||||
err := runTotalStateEdit(func(_ display.Options, snap *deploy.Snapshot) error {
|
||||
for _, res := range snap.Resources {
|
||||
err := edit.UnprotectResource(snap, res)
|
||||
contract.AssertNoError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("All resources successfully unprotected")
|
||||
return nil
|
||||
}
|
||||
|
||||
func unprotectResource(urn resource.URN) error {
|
||||
err := runStateEdit(urn, edit.UnprotectResource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Resource successfully unprotected")
|
||||
return nil
|
||||
}
|
17
pkg/resource/edit/doc.go
Normal file
17
pkg/resource/edit/doc.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2016-2018, Pulumi Corporation.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package edit contains functions suitable for editing a snapshot in-place. It is designed to be used by higher-level
|
||||
// tools that present a means for users to surgically edit their state.
|
||||
package edit
|
41
pkg/resource/edit/errors.go
Normal file
41
pkg/resource/edit/errors.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2016-2018, Pulumi Corporation.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package edit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/resource"
|
||||
)
|
||||
|
||||
// ResourceHasDependenciesError is returned by DeleteResource if a resource can't be deleted due to the presence of
|
||||
// resources that depend directly or indirectly upon it.
|
||||
type ResourceHasDependenciesError struct {
|
||||
Condemned *resource.State
|
||||
Dependencies []*resource.State
|
||||
}
|
||||
|
||||
func (r ResourceHasDependenciesError) Error() string {
|
||||
return fmt.Sprintf("Can't delete resource %q due to dependent resources", r.Condemned.URN)
|
||||
}
|
||||
|
||||
// ResourceProtectedError is returned by DeleteResource if a resource is protected.
|
||||
type ResourceProtectedError struct {
|
||||
Condemned *resource.State
|
||||
}
|
||||
|
||||
func (ResourceProtectedError) Error() string {
|
||||
return "Can't delete protected resource"
|
||||
}
|
90
pkg/resource/edit/operations.go
Normal file
90
pkg/resource/edit/operations.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
// Copyright 2016-2018, Pulumi Corporation.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package edit
|
||||
|
||||
import (
|
||||
"github.com/pulumi/pulumi/pkg/resource"
|
||||
"github.com/pulumi/pulumi/pkg/resource/deploy"
|
||||
"github.com/pulumi/pulumi/pkg/resource/graph"
|
||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
)
|
||||
|
||||
// OperationFunc is the type of functions that edit resources within a snapshot. The edits are made in-place to the
|
||||
// given snapshot and pertain to the specific passed-in resource.
|
||||
type OperationFunc func(*deploy.Snapshot, *resource.State) error
|
||||
|
||||
// DeleteResource deletes a given resource from the snapshot, if it is possible to do so. A resource can only be deleted
|
||||
// from a stack if there do not exist any resources that depend on it or descend from it. If such a resource does exist,
|
||||
// DeleteResource will return an error instance of `ResourceHasDependenciesError`.
|
||||
func DeleteResource(snapshot *deploy.Snapshot, condemnedRes *resource.State) error {
|
||||
contract.Require(snapshot != nil, "snapshot")
|
||||
contract.Require(condemnedRes != nil, "state")
|
||||
|
||||
if condemnedRes.Protect {
|
||||
return ResourceProtectedError{condemnedRes}
|
||||
}
|
||||
|
||||
dg := graph.NewDependencyGraph(snapshot.Resources)
|
||||
dependencies := dg.DependingOn(condemnedRes)
|
||||
if len(dependencies) != 0 {
|
||||
return ResourceHasDependenciesError{Condemned: condemnedRes, Dependencies: dependencies}
|
||||
}
|
||||
|
||||
// If there are no resources that depend on condemnedRes, iterate through the snapshot and keep everything that's
|
||||
// not condemnedRes.
|
||||
var newSnapshot []*resource.State
|
||||
var children []*resource.State
|
||||
for _, res := range snapshot.Resources {
|
||||
// While iterating, keep track of the set of resources that are parented to our condemned resource. We'll only
|
||||
// actually perform the deletion if this set is empty, otherwise it is not legal to delete the resource.
|
||||
if res.Parent == condemnedRes.URN {
|
||||
children = append(children, res)
|
||||
}
|
||||
|
||||
if res != condemnedRes {
|
||||
newSnapshot = append(newSnapshot, res)
|
||||
}
|
||||
}
|
||||
|
||||
// If there exists a resource that is the child of condemnedRes, we can't delete it.
|
||||
if len(children) != 0 {
|
||||
return ResourceHasDependenciesError{Condemned: condemnedRes, Dependencies: children}
|
||||
}
|
||||
|
||||
// Otherwise, we're good to go. Writing the new resource list into the snapshot persists the mutations that we have
|
||||
// made above.
|
||||
snapshot.Resources = newSnapshot
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnprotectResource unprotects a resource.
|
||||
func UnprotectResource(_ *deploy.Snapshot, res *resource.State) error {
|
||||
res.Protect = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocateResource returns all resources in the given shapshot that have the given URN.
|
||||
func LocateResource(snap *deploy.Snapshot, urn resource.URN) []*resource.State {
|
||||
contract.Require(snap != nil, "snap")
|
||||
|
||||
var resources []*resource.State
|
||||
for _, res := range snap.Resources {
|
||||
if res.URN == urn {
|
||||
resources = append(resources, res)
|
||||
}
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
260
pkg/resource/edit/operations_test.go
Normal file
260
pkg/resource/edit/operations_test.go
Normal file
|
@ -0,0 +1,260 @@
|
|||
// Copyright 2016-2018, Pulumi Corporation.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package edit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/resource"
|
||||
"github.com/pulumi/pulumi/pkg/resource/deploy"
|
||||
"github.com/pulumi/pulumi/pkg/resource/deploy/providers"
|
||||
"github.com/pulumi/pulumi/pkg/tokens"
|
||||
"github.com/pulumi/pulumi/pkg/version"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func NewResource(name string, provider *resource.State, deps ...resource.URN) *resource.State {
|
||||
prov := ""
|
||||
if provider != nil {
|
||||
p, err := providers.NewReference(provider.URN, provider.ID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
prov = p.String()
|
||||
}
|
||||
|
||||
t := tokens.Type("a:b:c")
|
||||
return &resource.State{
|
||||
Type: t,
|
||||
URN: resource.NewURN("test", "test", "", t, tokens.QName(name)),
|
||||
Inputs: resource.PropertyMap{},
|
||||
Outputs: resource.PropertyMap{},
|
||||
Dependencies: deps,
|
||||
Provider: prov,
|
||||
}
|
||||
}
|
||||
|
||||
func NewProviderResource(pkg, name, id string, deps ...resource.URN) *resource.State {
|
||||
t := providers.MakeProviderType(tokens.Package(pkg))
|
||||
return &resource.State{
|
||||
Type: t,
|
||||
URN: resource.NewURN("test", "test", "", t, tokens.QName(name)),
|
||||
ID: resource.ID(id),
|
||||
Inputs: resource.PropertyMap{},
|
||||
Outputs: resource.PropertyMap{},
|
||||
Dependencies: deps,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSnapshot(resources []*resource.State) *deploy.Snapshot {
|
||||
return deploy.NewSnapshot(deploy.Manifest{
|
||||
Time: time.Now(),
|
||||
Version: version.Version,
|
||||
Plugins: nil,
|
||||
}, resources, nil)
|
||||
}
|
||||
|
||||
func TestDeletion(t *testing.T) {
|
||||
pA := NewProviderResource("a", "p1", "0")
|
||||
a := NewResource("a", pA)
|
||||
b := NewResource("b", pA)
|
||||
c := NewResource("c", pA)
|
||||
snap := NewSnapshot([]*resource.State{
|
||||
pA,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
})
|
||||
|
||||
err := DeleteResource(snap, b)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, snap.Resources, 3)
|
||||
assert.Equal(t, []*resource.State{pA, a, c}, snap.Resources)
|
||||
}
|
||||
|
||||
func TestFailedDeletionProviderDependency(t *testing.T) {
|
||||
pA := NewProviderResource("a", "p1", "0")
|
||||
a := NewResource("a", pA)
|
||||
b := NewResource("b", pA)
|
||||
c := NewResource("c", pA)
|
||||
snap := NewSnapshot([]*resource.State{
|
||||
pA,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
})
|
||||
|
||||
err := DeleteResource(snap, pA)
|
||||
assert.Error(t, err)
|
||||
depErr, ok := err.(ResourceHasDependenciesError)
|
||||
if !assert.True(t, ok) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
assert.Contains(t, depErr.Dependencies, a)
|
||||
assert.Contains(t, depErr.Dependencies, b)
|
||||
assert.Contains(t, depErr.Dependencies, c)
|
||||
assert.Len(t, snap.Resources, 4)
|
||||
assert.Equal(t, []*resource.State{pA, a, b, c}, snap.Resources)
|
||||
}
|
||||
|
||||
func TestFailedDeletionRegularDependency(t *testing.T) {
|
||||
pA := NewProviderResource("a", "p1", "0")
|
||||
a := NewResource("a", pA)
|
||||
b := NewResource("b", pA, a.URN)
|
||||
c := NewResource("c", pA)
|
||||
snap := NewSnapshot([]*resource.State{
|
||||
pA,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
})
|
||||
|
||||
err := DeleteResource(snap, a)
|
||||
assert.Error(t, err)
|
||||
depErr, ok := err.(ResourceHasDependenciesError)
|
||||
if !assert.True(t, ok) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
assert.NotContains(t, depErr.Dependencies, pA)
|
||||
assert.NotContains(t, depErr.Dependencies, a)
|
||||
assert.Contains(t, depErr.Dependencies, b)
|
||||
assert.NotContains(t, depErr.Dependencies, c)
|
||||
assert.Len(t, snap.Resources, 4)
|
||||
assert.Equal(t, []*resource.State{pA, a, b, c}, snap.Resources)
|
||||
}
|
||||
|
||||
func TestFailedDeletionProtected(t *testing.T) {
|
||||
pA := NewProviderResource("a", "p1", "0")
|
||||
a := NewResource("a", pA)
|
||||
a.Protect = true
|
||||
snap := NewSnapshot([]*resource.State{
|
||||
pA,
|
||||
a,
|
||||
})
|
||||
|
||||
err := DeleteResource(snap, a)
|
||||
assert.Error(t, err)
|
||||
_, ok := err.(ResourceProtectedError)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestFailedDeletionParentDependency(t *testing.T) {
|
||||
pA := NewProviderResource("a", "p1", "0")
|
||||
a := NewResource("a", pA)
|
||||
b := NewResource("b", pA)
|
||||
b.Parent = a.URN
|
||||
c := NewResource("c", pA)
|
||||
c.Parent = a.URN
|
||||
snap := NewSnapshot([]*resource.State{
|
||||
pA,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
})
|
||||
|
||||
err := DeleteResource(snap, a)
|
||||
assert.Error(t, err)
|
||||
depErr, ok := err.(ResourceHasDependenciesError)
|
||||
if !assert.True(t, ok) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
assert.NotContains(t, depErr.Dependencies, pA)
|
||||
assert.NotContains(t, depErr.Dependencies, a)
|
||||
assert.Contains(t, depErr.Dependencies, b)
|
||||
assert.Contains(t, depErr.Dependencies, c)
|
||||
assert.Len(t, snap.Resources, 4)
|
||||
assert.Equal(t, []*resource.State{pA, a, b, c}, snap.Resources)
|
||||
}
|
||||
|
||||
func TestUnprotectResource(t *testing.T) {
|
||||
pA := NewProviderResource("a", "p1", "0")
|
||||
a := NewResource("a", pA)
|
||||
a.Protect = true
|
||||
b := NewResource("b", pA)
|
||||
c := NewResource("c", pA)
|
||||
snap := NewSnapshot([]*resource.State{
|
||||
pA,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
})
|
||||
|
||||
err := UnprotectResource(snap, a)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, snap.Resources, 4)
|
||||
assert.Equal(t, []*resource.State{pA, a, b, c}, snap.Resources)
|
||||
assert.False(t, a.Protect)
|
||||
}
|
||||
|
||||
func TestLocateResourceNotFound(t *testing.T) {
|
||||
pA := NewProviderResource("a", "p1", "0")
|
||||
a := NewResource("a", pA)
|
||||
b := NewResource("b", pA)
|
||||
c := NewResource("c", pA)
|
||||
snap := NewSnapshot([]*resource.State{
|
||||
pA,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
})
|
||||
|
||||
ty := tokens.Type("a:b:c")
|
||||
urn := resource.NewURN("test", "test", "", ty, "not-present")
|
||||
resList := LocateResource(snap, urn)
|
||||
assert.Nil(t, resList)
|
||||
}
|
||||
|
||||
func TestLocateResourceAmbiguous(t *testing.T) {
|
||||
pA := NewProviderResource("a", "p1", "0")
|
||||
a := NewResource("a", pA)
|
||||
b := NewResource("b", pA)
|
||||
aPending := NewResource("a", pA)
|
||||
aPending.Delete = true
|
||||
snap := NewSnapshot([]*resource.State{
|
||||
pA,
|
||||
a,
|
||||
b,
|
||||
aPending,
|
||||
})
|
||||
|
||||
resList := LocateResource(snap, a.URN)
|
||||
assert.Len(t, resList, 2)
|
||||
assert.Contains(t, resList, a)
|
||||
assert.Contains(t, resList, aPending)
|
||||
assert.NotContains(t, resList, pA)
|
||||
assert.NotContains(t, resList, b)
|
||||
}
|
||||
|
||||
func TestLocateResourceExact(t *testing.T) {
|
||||
pA := NewProviderResource("a", "p1", "0")
|
||||
a := NewResource("a", pA)
|
||||
b := NewResource("b", pA)
|
||||
c := NewResource("c", pA)
|
||||
snap := NewSnapshot([]*resource.State{
|
||||
pA,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
})
|
||||
|
||||
resList := LocateResource(snap, a.URN)
|
||||
assert.Len(t, resList, 1)
|
||||
assert.Contains(t, resList, a)
|
||||
}
|
Loading…
Reference in a new issue