pulumi/cmd/state.go
Matt Ellis 307ee72b5f Use existing secrets manager when roundtripping
There are a few operations we do (stack rename, importing and edits)
where we will materialize a `deploy.Snapshot` from an existing
deployment, mutate it in somewhay, and then store it.

In these cases, we will just re-use the secrets manager that was used
to build the snapshot when we re-serialize it. This is less than ideal
in some cases, because many of these operations could run on an
"encrypted" copy of the Snapshot, where Inputs and Outputs have not
been decrypted.

Unfortunately, our system now is not set up in a great way to support
this and adding something like a `deploy.EncryptedSnapshot` would
require large scale code duplications.

So, for now, we'll take the hit of decrypting and re-encrypting, but
long term introducing a `deploy.EncryptedSnapshot` may be nice as it
would let us elide the encryption/decryption steps in some places and
would also make it clear what parts of our system have access to the
plaintext values of secrets.
2019-05-10 17:07:52 -07:00

185 lines
6.7 KiB
Go

// 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/result"
"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 a given stack.
func runStateEdit(stackName string, urn resource.URN, operation edit.OperationFunc) result.Result {
return runTotalStateEdit(stackName, 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 given stack's snapshot. Before mutating
// the snapshot, the user is prompted for confirmation if the current session is interactive.
func runTotalStateEdit(stackName string,
operation func(opts display.Options, snap *deploy.Snapshot) error) result.Result {
opts := display.Options{
Color: cmdutil.GetGlobalColorization(),
}
s, err := requireStack(stackName, true, opts, true /*setCurrent*/)
if err != nil {
return result.FromError(err)
}
snap, err := s.Snapshot(commandContext())
if err != nil {
return result.FromError(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 {
fmt.Println("confirmation declined")
return result.Bail()
}
}
// 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 result.FromError(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")
}
sdep, err := stack.SerializeDeployment(snap, snap.SecretsManager)
if err != nil {
return result.FromError(errors.Wrap(err, "serializing deployment"))
}
// Once we've mutated the snapshot, import it back into the backend so that it can be persisted.
bytes, err := json.Marshal(sdep)
if err != nil {
return result.FromError(err)
}
dep := apitype.UntypedDeployment{
Version: apitype.DeploymentSchemaVersionCurrent,
Deployment: bytes,
}
return result.WrapIfNonNil(s.ImportDeployment(commandContext(), &dep))
}