Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
13078: cli(state upgrade): Prompt for project names for detached stacks r=abhinav a=abhinav

When running 'pulumi state upgrade', supply the
ProjectsForDetachedStacks option to the file state backend so that we
get asked to fill in project names for stacks where we could not guess
them automatically.

The implementation of the prompt is straightforward:
For each stack, ask a question with the survey package
and feed the result back to the filestate backend.

Testing this is a bit complicated because terminals are involved.
The test for this uses the go-expect and vt10x libraries
recommended in the documentation for survey.
It uses them to simulate a terminal emulator and acts on the output.
The pty library is used to create a compatible pseduo-terminal.
Unfortunately, these test libraries rely on Unix APIs and are not
available on Windows, so the test will not run on Windows machines.

Resolves #12600

---

Preview

![Kapture 2023-05-31 at 19 25 57](https://github.com/pulumi/pulumi/assets/41730/69fcf37d-0267-40cc-9002-6514f1cf9ad5)


13254: cli(state/rename): Validate new name before renaming r=abhinav a=abhinav

Check that resource names are valid
before renaming a resource and possibly corrupting the stack.

Resource names are `QName`s, so we rely on tokens.IsQName for this.

Resolves #11746


13256: Changelog and go.mod updates for v3.73.0 r=pulumi-bot a=pulumi-bot

bors merge

Co-authored-by: Abhinav Gupta <abhinav@pulumi.com>
Co-authored-by: github-actions <github-actions@github.com>
  • Loading branch information
3 people committed Jun 22, 2023
4 parents 5203472 + d5fea17 + b2aea50 + ccf9a4f commit ed5a008
Show file tree
Hide file tree
Showing 35 changed files with 399 additions and 93 deletions.
70 changes: 70 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,75 @@
# Changelog

## 3.73.0 (2023-06-22)


### Features

- [programgen] Allow traversing unknown properties from resources when skipping resource type checking
[#13180](https://github.com/pulumi/pulumi/pull/13180)


### Bug Fixes

- [backend/filestate] Fix auto-opt-in to project mode.
[#13243](https://github.com/pulumi/pulumi/pull/13243)

- [cli] `pulumi convert` will now cleanup temporary pulumi-convert directories when the command is finished.
[#13185](https://github.com/pulumi/pulumi/pull/13185)

- [cli] Fix Markdown formatting issues in command usage.
[#13225](https://github.com/pulumi/pulumi/pull/13225)

- [cli] Fix `stack rm` removing config files for the wrong project.
[#13227](https://github.com/pulumi/pulumi/pull/13227)

- [cli/config] No longer error on directory read permissions when searching for project files.
[#13211](https://github.com/pulumi/pulumi/pull/13211)

- [cli/display] Fix diff display partially parsing JSON/YAML from strings.

- [cli/display] Fix large integers displaying in scientific notation.
[#13209](https://github.com/pulumi/pulumi/pull/13209)

- [cli/display] Update summary is now correctly shown when `advisory` and `disabled` policy events are encountered.
[#13218](https://github.com/pulumi/pulumi/pull/13218)

- [cli/display] Fix formatting bugs in display causing text like (MISSING) showing in output.
[#13228](https://github.com/pulumi/pulumi/pull/13228)

- [cli/display] On Windows, make `pulumi state unprotect` command suggestion use double-quotes instead of single-quotes.
[#13236](https://github.com/pulumi/pulumi/pull/13236)

- [cli/new] `pulumi new` now correctly supports numeric stack names.
[#13220](https://github.com/pulumi/pulumi/pull/13220)

- [cli/new] Fix empty config values being added to the config file as part of `new`.
[#13233](https://github.com/pulumi/pulumi/pull/13233)

- [cli/plugin] Fixes the output of plugin rm --yes command to explicitly say that plugins were removed
[#13216](https://github.com/pulumi/pulumi/pull/13216)

- [engine] Fix wildcards in IgnoreChanges.
[#13005](https://github.com/pulumi/pulumi/pull/13005)

- [engine] Fix ignoreChanges setting ignore array indexes to zero.
[#13005](https://github.com/pulumi/pulumi/pull/13005)

- [sdk/nodejs] Write port to stdout as a string so Node doesn't colorize the output
[#13204](https://github.com/pulumi/pulumi/pull/13204)

- [sdk/python] Allow tuples as Sequence input values to resources.
[#13210](https://github.com/pulumi/pulumi/pull/13210)

- [sdkgen/python] Python SDK only prints a Function Invoke result's deprecation messages when using getters rather than on instantiation.
[#13213](https://github.com/pulumi/pulumi/pull/13213)


### Miscellaneous

- [cli] Make no retry attempts for the Pulumi new version query. This should speed up the CLI in certain environments.
[#13215](https://github.com/pulumi/pulumi/pull/13215)

## 3.72.2 (2023-06-17)


Expand Down

This file was deleted.

This file was deleted.

@@ -0,0 +1,4 @@
changes:
- type: feat
scope: cli/state
description: The upgrade command now prompts the user to supply project names for stacks for which the project name could not be automatically guessed.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

@@ -0,0 +1,4 @@
changes:
- type: fix
scope: cli/state
description: Disallow renaming resources to invalid names that will corrupt the state.
6 changes: 6 additions & 0 deletions pkg/cmd/pulumi/state_rename.go
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
"github.com/pulumi/pulumi/pkg/v3/resource/edit"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"

Expand Down Expand Up @@ -50,6 +51,11 @@ func stateRenameOperation(urn resource.URN, newResourceName string, opts display
return errors.New("The input URN does not correspond to an existing resource")
}

if !tokens.IsQName(newResourceName) {
return fmt.Errorf("invalid name %q: "+
"resource names may only contain alphanumerics, underscores, hyphens, dots, and slashes", newResourceName)
}

inputResource := existingResources[0]
oldUrn := inputResource.URN
// update the URN with only the name part changed
Expand Down
38 changes: 38 additions & 0 deletions pkg/cmd/pulumi/state_rename_test.go
Expand Up @@ -67,6 +67,44 @@ func TestRenameProvider(t *testing.T) {
}
}

func TestStateRename_invalidName(t *testing.T) {
t.Parallel()

prov := resource.URN("urn:pulumi:dev::xxx-dev::kubernetes::provider")
res := resource.URN("urn:pulumi:dev::xxx-dev::kubernetes:core/v1:Namespace::amazon_cloudwatchNamespace")

snap := deploy.Snapshot{
Resources: []*resource.State{
{
URN: prov,
ID: "provider-id",
Type: "pulumi:provider:kubernetes",
},
{
URN: res,
ID: "res-id",
Type: "kubernetes:core/v1:Namespace",
},
},
}
require.NoError(t, snap.VerifyIntegrity(),
"invalid test: snapshot is already broken")

err := stateRenameOperation(
res,
"urn:pulumi:dev::xxx-dev::eks:index:Cluster$kubernetes:core/v1:Namespace::amazon_cloudwatchNamespace",
display.Options{},
&snap,
)
require.Error(t, err)
assert.ErrorContains(t, err, "invalid name")
assert.ErrorContains(t, err, "names may only contain alphanumerics")

// The state must still be valid, and the resource name unchanged.
require.NoError(t, snap.VerifyIntegrity(), "snapshot is broken after rename")
assert.Equal(t, res, snap.Resources[1].URN)
}

// Regression test for https://github.com/pulumi/pulumi/issues/13179.
//
// Defines a state with a two resources, one parented to the other,
Expand Down
93 changes: 92 additions & 1 deletion pkg/cmd/pulumi/state_upgrade.go
Expand Up @@ -20,10 +20,14 @@ import (
"io"
"os"

survey "github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/pulumi/pulumi/pkg/v3/backend"
"github.com/pulumi/pulumi/pkg/v3/backend/display"
"github.com/pulumi/pulumi/pkg/v3/backend/filestate"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"

Expand Down Expand Up @@ -54,6 +58,7 @@ This only has an effect on self-managed backends.
type stateUpgradeCmd struct {
Stdin io.Reader // defaults to os.Stdin
Stdout io.Writer // defaults to os.Stdout
Stderr io.Writer // defaults to os.Stderr

// Used to mock out the currentBackend function for testing.
// Defaults to currentBackend function.
Expand All @@ -67,6 +72,9 @@ func (cmd *stateUpgradeCmd) Run(ctx context.Context) error {
if cmd.Stdin == nil {
cmd.Stdin = os.Stdin
}
if cmd.Stderr == nil {
cmd.Stderr = os.Stderr
}

if cmd.currentBackend == nil {
cmd.currentBackend = currentBackend
Expand Down Expand Up @@ -101,5 +109,88 @@ func (cmd *stateUpgradeCmd) Run(ctx context.Context) error {
return nil
}

return lb.Upgrade(ctx, nil /*opts*/)
var opts filestate.UpgradeOptions
// If we're in interactive mode, prompt for the project name
// for each stack that doesn't have one.
if cmdutil.Interactive() {
opts.ProjectsForDetachedStacks = cmd.projectsForDetachedStacks
}
return lb.Upgrade(ctx, &opts)
}

func (cmd *stateUpgradeCmd) projectsForDetachedStacks(stacks []tokens.Name) ([]tokens.Name, error) {
projects := make([]tokens.Name, len(stacks))
err := (&stateUpgradeProjectNameWidget{
Stdin: cmd.Stdin,
Stdout: cmd.Stdout,
Stderr: cmd.Stderr,
}).Prompt(stacks, projects)
return projects, err
}

// stateUpgradeProjectNameWidget is a widget that prompts the user
// for a project name for every stack that doesn't have one.
//
// It is used by the 'pulumi state upgrade' command
// when it encounters stacks without a project name.
type stateUpgradeProjectNameWidget struct {
Stdin io.Reader // required
Stdout io.Writer // required
Stderr io.Writer // required
}

// Prompt prompts the user for a project name for each stack
// and stores the result in the corresponding index of projects.
//
// The length of projects must be equal to the length of stacks.
func (w *stateUpgradeProjectNameWidget) Prompt(stacks, projects []tokens.Name) error {
contract.Assertf(len(stacks) == len(projects),
"length of stacks (%d) must equal length of projects (%d)", len(stacks), len(projects))

if len(stacks) == 0 {
// Nothing to prompt for.
return nil
}

stdin, ok1 := w.Stdin.(terminal.FileReader)
stdout, ok2 := w.Stdout.(terminal.FileWriter)
if !ok1 || !ok2 {
// We're not using a real terminal, so we can't prompt.
// Pretend we're in non-interactive mode.
return nil
}

fmt.Fprintln(stdout, "Found stacks without a project name.")
fmt.Fprintln(stdout, "Please enter a project name for each stack, or enter to skip that stack.")
for i, stack := range stacks {
var project string
err := survey.AskOne(
&survey.Input{
Message: fmt.Sprintf("Stack %s", stack),
Help: "Enter a name for the project, or press enter to skip",
},
&project,
survey.WithStdio(stdin, stdout, w.Stderr),
survey.WithValidator(w.validateProject),
)
if err != nil {
return fmt.Errorf("prompt for %q: %w", stack, err)
}

projects[i] = tokens.Name(project)
}

return nil
}

func (w *stateUpgradeProjectNameWidget) validateProject(ans any) error {
proj, ok := ans.(string)
contract.Assertf(ok, "widget should have a string output, got %T", ans)

if proj == "" {
// The user wants to skip this stack.
return nil
}

return tokens.ValidateProjectName(proj)
}

0 comments on commit ed5a008

Please sign in to comment.