Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check the uniqueness of the project name during pulumi new #3065

Merged
merged 2 commits into from
Aug 23, 2019

Conversation

mikhailshilkov
Copy link
Member

Fixes #2476.

During pulumi new, check if there is an existing project with the name equal to the entered value, and prevent going forward if so.

@justinvp I wasn't able to find any existing tests for the parts that I touched. Let me know if you think we should have some tests here.

@mikhailshilkov mikhailshilkov changed the title Check project existence during pulumi new Check the uniqueness of the project name during pulumi new Aug 12, 2019
Copy link
Member

@justinvp justinvp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments, but otherwise LGTM. Thanks!

I wasn't able to find any existing tests for the parts that I touched. Let me know if you think we should have some tests here.

Test coverage has been lacking in this area and is something we want to improve. Tests are always welcome, but I understand it may be a larger effort to get some initial test helpers/infrastructure in place to more easily test this command incrementally going forward. So unless you see a way to add some simple tests now, we can just track getting that in place as a separate effort.

cmd/new.go Outdated Show resolved Hide resolved
pkg/backend/filestate/backend.go Outdated Show resolved Hide resolved
@@ -473,6 +473,16 @@ func (b *cloudBackend) Logout() error {
return workspace.DeleteAccessToken(b.CloudURL())
}

// DoesProjectExist returns true if a project with the given name exists in this backend, or false otherwise.
func (b *cloudBackend) DoesProjectExist(ctx context.Context, projectName string) (bool, error) {
owner, err := b.client.GetPulumiAccountName(ctx)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we get to adding the ideal experience for #2421 (prompting for the owner), I think we'll need to modify this to allow passing the owner to DoesProjectExist since this is always only checking if the project exists in the user's account.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Moreover, this is already a problem now:

  1. We ask for a project name, we get proj1 and it's unique in the user's account.
  2. We ask for a stack name, we get org1/stack1 back.
  3. Project proj1 already exists in org1. Boom.

It seems that we are not able to validate the project name before we get the org name, or am I wrong?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that we are not able to validate the project name before we get the org name, or am I wrong?

Right. I think we just live with this gotcha until #3076 is addressed.

cmd/new.go Outdated
@@ -367,6 +371,29 @@ func errorIfNotEmptyDirectory(path string) error {
return nil
}

func validateProjectName(projectName string, opts display.Options) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the pulumi new command does accept a --name (or -n) flag and we validate this early on and exit early if the specified project name isn't valid. But that validation isn't calling the new DoesProjectExist.

So even with the changes in this PR I could do:

pulumi new -n exists

And pulumi will happily continue and prompt for the project description and stack name, and will create the stack in the existing project assuming I choose a stack name that doesn't conflict with an existing stack name in the existing project.

I wonder if we should add another check that calls this function to ensure the project doesn't exist earlier on. Perhaps after this block:

pulumi/cmd/new.go

Lines 121 to 127 in 1a698cb

// If we're going to be creating a stack, get the current backend, which
// will kick off the login flow (if not already logged-in).
if !generateOnly {
if _, err = currentBackend(opts); err != nil {
return err
}
}

Something like:

// Ensure the project doesn't already exist.
if !generateOnly && name != "" {
	if err := validateProjectName(name, opts); err != nil {
		return err
	}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, when we get to the ideal fix for #2421 (prompting for the owner), we'll probably need to make some modifications to such a check as it'll need an owner to perform the "does exist" check. Perhaps it'd only do the check if it has an owner at this point (e.g. the user is logged in to the cloud backend and isn't a member of any orgs, and hence the user account would be the owner). Or if we decided to add an --owner flag and the flag was specified.

cmd/new.go Outdated
return err
}

exists, err := b.DoesProjectExist(commandContext(), projectName)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the --generate-only (or -g) flag is specified, we won't be creating a stack. I wonder if in that case we should just skip getting the backend and calling b.DoesProjectExist?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the project name still needs to be unique in this case, doesn't it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the project name still needs to be unique in this case, doesn't it?

It does, but when -g is specified, no actual stack is going to be created as part of the pulumi new command invocation. I suppose it is likely they'll run pulumi stack init as a next step at some point (or indirectly via running pulumi up and it prompting to create a stack). And having new check to ensure the project is unique before any of these subsequent commands could be helpful. However, by the time they run the command that creates the stack, the projects on the server could have changed (although unlikely), or they'll want to create a stack in a different org, in which case the check in the user's org was pointless and would artificially block them when they just want to emit the project on disk.

So I guess I lean slightly towards not doing the check when -g is specified. It's not creating any stacks, so don't bother with with the uniqueness check. But I could be convinced otherwise.

@mikhailshilkov
Copy link
Member Author

@justinvp I feel that there are enough scenario permutations here to warrant spending time on building a test suite. I created two basic tests - could you have a look and check whether they are on the proper level?

I had to mock the console input to be able to provide user's input: otherwise, we'll lose many scenarios.

The next step would be to mock the backend to test the name validation. What would be the optimal way to do so? Mock the whole backend like backend_test does? What's the best way to inject the mock backend? Adding it as an extra parameter of newNewCmdEx will impact lots of code in new and up command, as they all rely on currentBackend now.

@justinvp
Copy link
Member

I created two basic tests - could you have a look and check whether they are on the proper level?

These look fine to me, but would love @ellismg to take a look.

The next step would be to mock the backend to test the name validation. What would be the optimal way to do so? Mock the whole backend like backend_test does? What's the best way to inject the mock backend? Adding it as an extra parameter of newNewCmdEx will impact lots of code in new and up command, as they all rely on currentBackend now.

@ellismg, what would you recommend?

@mikhailshilkov mikhailshilkov force-pushed the mikhailshilkov/gh-2476 branch 2 times, most recently from 42e5e55 to 5a8e037 Compare August 19, 2019 15:27
@ellismg
Copy link
Contributor

ellismg commented Aug 19, 2019

@ellismg, what would you recommend?

I don't think I have any silver bullets here. I think that mocking the backend is correct (we could probably just return not implemented errors for codepaths we did not think would be hit). As far as injecting it goes, I think that passing it to newNewCmdEx somehow is the best way, but I agree it will be slightly disruptive. I'm not really sure of a better way.

Long term, we probably want the backend injected into any command that needs it, so we could mock more of the interactive portions of the CLI.

@mikhailshilkov
Copy link
Member Author

@justinvp @ellismg Thanks a ton for your input!

I've restructured the code a bit further to split the actual function from the command which makes testing and injection a bit easier. I've also injected a mock backend with a naughty assignment - I might refactor it to an argument later on. I added a new test which depends on the mocked backend.

This looks promising, so I'll go ahead and try to create enough test cases to cover the combinations for the issue in question.

@mikhailshilkov
Copy link
Member Author

I added several more tests, I believe this is ready for another review

Copy link
Member

@justinvp justinvp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM otherwise. Thanks for the tests and getting this infrastructure in place, Mikhail!

cmd/util.go Outdated Show resolved Hide resolved
cmd/new_test.go Outdated
assert.Equal(t, uniqueProjectName, proj.Name.String())
}

func TestCreatingProjectWithEnteredName(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference between SpecifiedName and EnteredName? In both of these tests uniqueProjectName is being specified as a the name arg.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch! I wasn't careful enough and let it slip in. The Entered ones shouldn't have name specified.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a nit, but can we rename the functions to make the distinction more clear? Maybe something like:

  • TestCreatingProjectWithArgsSpecifiedName
  • TestCreatingProjectWithPromptedName

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

cmd/new_test.go Outdated Show resolved Hide resolved
Copy link
Member

@justinvp justinvp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple nits on the test names, but LGTM otherwise.

cmd/new_test.go Outdated
assert.Equal(t, uniqueProjectName, proj.Name.String())
}

func TestCreatingProjectWithEnteredName(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a nit, but can we rename the functions to make the distinction more clear? Maybe something like:

  • TestCreatingProjectWithArgsSpecifiedName
  • TestCreatingProjectWithPromptedName

@mikhailshilkov mikhailshilkov merged commit ccc7ec1 into master Aug 23, 2019
@pulumi-bot pulumi-bot deleted the mikhailshilkov/gh-2476 branch August 23, 2019 07:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

pulumi new: Require a unique project instead of unique stack
3 participants