Add pulumi stack rename

`pulumi stack rename` allows you to change the name of an existing
stack. This operation is non-distructive, however it is possible that
the next update will show additional changes to resources, if the
pulumi program uses the value of `getStack()` as part of a resource
name.
This commit is contained in:
Matt Ellis 2019-03-14 15:32:10 -07:00
parent 0a4bf7b991
commit a1bb16407d
10 changed files with 163 additions and 0 deletions

View file

@ -2,6 +2,8 @@
### Improvements
- A new command, `pulumi stack rename` was added. This allows you to change the name of an existing stack in a project. Note: When a stack is renamed, the `pulumi.getStack` function in the SDK will now return a new value. If a stack name is used as part of a resource name, the next `pulumi update` will not understand that the old and new resources are logically the same. We plan to support adding aliases to individual resources so you can handle these cases. See [pulumi/pulumi#458](https://github.com/pulumi/pulumi/issues/458) for discussion on this new feature. For now, if you are unwilling to have `pulumi update` create and destroy these resources, you can rename your stack back to the old name. (fixes [pulumi/pulumi#2402](https://github.com/pulumi/pulumi/issues/2402))
## 0.17.2 (Released March 15, 2019)
### Improvements

View file

@ -174,6 +174,7 @@ func newStackCmd() *cobra.Command {
cmd.AddCommand(newStackRmCmd())
cmd.AddCommand(newStackSelectCmd())
cmd.AddCommand(newStackTagCmd())
cmd.AddCommand(newStackRenameCmd())
return cmd
}

86
cmd/stack_rename.go Normal file
View 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"
"os"
"path/filepath"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/pulumi/pulumi/pkg/backend/display"
"github.com/pulumi/pulumi/pkg/backend/state"
"github.com/pulumi/pulumi/pkg/util/cmdutil"
"github.com/pulumi/pulumi/pkg/workspace"
)
func newStackRenameCmd() *cobra.Command {
var stack string
var cmd = &cobra.Command{
Use: "rename <new-stack-name>",
Args: cmdutil.ExactArgs(1),
Short: "Rename an existing stack",
Long: "Rename an existing stack.\n" +
"\n" +
"Note: Because renaming a stack will change the value of `getStack()` inside a Pulumi program, if this\n" +
"name is used as part of a resource's name, the next `pulumi up` will want to delete the old resource and\n" +
"create a new copy. For now, if you don't want these changes to be applied, you should rename your stack\n" +
"back to its previous name.",
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
opts := display.Options{
Color: cmdutil.GetGlobalColorization(),
}
s, err := requireStack(stack, false, opts, true /*setCurrent*/)
if err != nil {
return err
}
oldConfigPath, err := workspace.DetectProjectStackPath(s.Ref().Name())
if err != nil {
return err
}
newConfigPath, err := workspace.DetectProjectStackPath(tokens.QName(args[0]))
if err != nil {
return err
}
if err := s.Rename(commandContext(), tokens.QName(args[0])); err != nil {
return err
}
if err := os.Rename(oldConfigPath, newConfigPath); err != nil {
return errors.Wrapf(err, "renaming %s to %s", filepath.Base(oldConfigPath), filepath.Base(newConfigPath))
}
if err := state.SetCurrentStack(args[0]); err != nil {
return errors.Wrap(err, "setting current stack")
}
fmt.Printf("Renamed %s\n", s.Ref().String())
return nil
}),
}
cmd.PersistentFlags().StringVarP(
&stack, "stack", "s", "",
"The name of the stack to operate on. Defaults to the current stack")
return cmd
}

View file

@ -96,6 +96,8 @@ type Backend interface {
// ListStacks returns a list of stack summaries for all known stacks in the target backend.
ListStacks(ctx context.Context, projectFilter *tokens.PackageName) ([]StackSummary, error)
RenameStack(ctx context.Context, stackRef StackReference, newName tokens.QName) error
// GetStackCrypter returns an encrypter/decrypter for the given stack's secret config values.
GetStackCrypter(stackRef StackReference) (config.Crypter, error)

View file

@ -37,6 +37,7 @@ import (
"github.com/pulumi/pulumi/pkg/operations"
"github.com/pulumi/pulumi/pkg/resource/config"
"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/tokens"
"github.com/pulumi/pulumi/pkg/util/contract"
@ -214,6 +215,41 @@ func (b *localBackend) RemoveStack(ctx context.Context, stackRef backend.StackRe
return false, b.removeStack(stackName)
}
func (b *localBackend) RenameStack(ctx context.Context, stackRef backend.StackReference, newName tokens.QName) error {
stackName := stackRef.Name()
cfg, snap, _, err := b.getStack(stackName)
if err != nil {
return err
}
// Ensure the destination stack does not already exist.
_, err = os.Stat(b.stackPath(newName))
if err == nil {
return errors.Errorf("a stack named %s already exists", newName)
} else if err != nil && !os.IsNotExist(err) {
return err
}
// Rewrite the checkpoint and save it with the new name.
if err = edit.RenameStack(snap, newName); err != nil {
return err
}
if _, err = b.saveStack(newName, cfg, snap); err != nil {
return err
}
// To remove the old stack, just make a backup of the file and don't write out anything new.
file := b.stackPath(stackName)
backupTarget(file)
// And move the history over as well.
oldHistoryDir := b.historyDirectory(stackName)
newHistoryDir := b.historyDirectory(newName)
return os.Rename(oldHistoryDir, newHistoryDir)
}
func (b *localBackend) GetStackCrypter(stackRef backend.StackReference) (config.Crypter, error) {
return symmetricCrypter(stackRef.Name(), b.stackConfigFile)
}

View file

@ -18,6 +18,8 @@ import (
"context"
"time"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/apitype"
"github.com/pulumi/pulumi/pkg/backend"
"github.com/pulumi/pulumi/pkg/engine"
@ -62,6 +64,10 @@ func (s *localStack) Remove(ctx context.Context, force bool) (bool, error) {
return backend.RemoveStack(ctx, s, force)
}
func (s *localStack) Rename(ctx context.Context, newName tokens.QName) error {
return backend.RenameStack(ctx, s, newName)
}
func (s *localStack) Preview(ctx context.Context, op backend.UpdateOperation) (engine.ResourceChanges, error) {
return backend.PreviewStack(ctx, s, op)
}

View file

@ -591,6 +591,15 @@ func (b *cloudBackend) RemoveStack(ctx context.Context, stackRef backend.StackRe
return b.client.DeleteStack(ctx, stack, force)
}
func (b *cloudBackend) RenameStack(ctx context.Context, stackRef backend.StackReference, newName tokens.QName) error {
stack, err := b.getCloudStackIdentifier(stackRef)
if err != nil {
return err
}
return b.client.RenameStack(ctx, stack, string(newName))
}
// cloudCrypter is an encrypter/decrypter that uses the Pulumi cloud to encrypt/decrypt a stack's secrets.
type cloudCrypter struct {
backend *cloudBackend

View file

@ -411,6 +411,15 @@ func (pc *Client) CreateUpdate(
}, nil
}
func (pc *Client) RenameStack(ctx context.Context, stack StackIdentifier, newName string) error {
req := apitype.StackRenameRequest{
NewName: newName,
}
var resp apitype.ImportStackResponse
return pc.restCall(ctx, "POST", getStackPath(stack, "rename"), nil, &req, &resp)
}
// StartUpdate starts the indicated update. It returns the new version of the update's target stack and the token used
// to authenticate operations on the update if any. Replaces the stack's tags with the updated set.
func (pc *Client) StartUpdate(ctx context.Context, update UpdateIdentifier,

View file

@ -127,6 +127,10 @@ func (s *cloudStack) Remove(ctx context.Context, force bool) (bool, error) {
return backend.RemoveStack(ctx, s, force)
}
func (s *cloudStack) Rename(ctx context.Context, newName tokens.QName) error {
return backend.RenameStack(ctx, s, newName)
}
func (s *cloudStack) Preview(ctx context.Context, op backend.UpdateOperation) (engine.ResourceChanges, error) {
return backend.PreviewStack(ctx, s, op)
}

View file

@ -20,6 +20,8 @@ import (
"path/filepath"
"regexp"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/contract"
"github.com/pkg/errors"
@ -50,6 +52,8 @@ type Stack interface {
// remove this stack.
Remove(ctx context.Context, force bool) (bool, error)
// rename this stack.
Rename(ctx context.Context, newName tokens.QName) error
// list log entries for this stack.
GetLogs(ctx context.Context, query operations.LogQuery) ([]operations.LogEntry, error)
// export this stack's deployment.
@ -63,6 +67,10 @@ func RemoveStack(ctx context.Context, s Stack, force bool) (bool, error) {
return s.Backend().RemoveStack(ctx, s.Ref(), force)
}
func RenameStack(ctx context.Context, s Stack, newName tokens.QName) error {
return s.Backend().RenameStack(ctx, s.Ref(), newName)
}
// PreviewStack previews changes to this stack.
func PreviewStack(ctx context.Context, s Stack, op UpdateOperation) (engine.ResourceChanges, error) {
return s.Backend().Preview(ctx, s.Ref(), op)