diff --git a/pkg/backend/filestate/backend.go b/pkg/backend/filestate/backend.go index 644dbb106896..d5da7f3cf715 100644 --- a/pkg/backend/filestate/backend.go +++ b/pkg/backend/filestate/backend.go @@ -340,8 +340,37 @@ func (b *localBackend) DoesProjectExist(ctx context.Context, projectName string) return false, nil } +// Confirm the specified stack's project doesn't contradict the Pulumi.yaml of the current project. If the CWD +// is not in a Pulumi project, does not contradict. If the project name in Pulumi.yaml is "foo", a stack with a +// name of bar/foo should not work. +func currentProjectContradictsWorkspace(stack localBackendReference) bool { + if stack.project == "" { + return false + } + + projPath, err := workspace.DetectProjectPath() + if err != nil { + return false + } + + if projPath == "" { + return false + } + + proj, err := workspace.LoadProject(projPath) + if err != nil { + return false + } + + return proj.Name.String() != stack.project.String() +} + func (b *localBackend) CreateStack(ctx context.Context, stackRef backend.StackReference, opts interface{}) (backend.Stack, error) { + localStackRef, is := stackRef.(localBackendReference) + if !is { + return nil, fmt.Errorf("bad stack reference type") + } err := b.Lock(ctx, stackRef) if err != nil { @@ -349,9 +378,13 @@ func (b *localBackend) CreateStack(ctx context.Context, stackRef backend.StackRe } defer b.Unlock(ctx, stackRef) + if currentProjectContradictsWorkspace(localStackRef) { + return nil, fmt.Errorf("provided project name %q doesn't match Pulumi.yaml", localStackRef.project) + } + contract.Requiref(opts == nil, "opts", "local stacks do not support any options") - stackName := stackRef.FullyQualifiedName() + stackName := localStackRef.FullyQualifiedName() if stackName == "" { return nil, errors.New("invalid empty stack name") } @@ -373,14 +406,19 @@ func (b *localBackend) CreateStack(ctx context.Context, stackRef backend.StackRe return nil, err } - stack := newStack(stackRef, file, nil, b) + stack := newStack(localStackRef, file, nil, b) fmt.Printf("Created stack '%s'\n", stack.Ref()) return stack, nil } func (b *localBackend) GetStack(ctx context.Context, stackRef backend.StackReference) (backend.Stack, error) { - stackName := stackRef.FullyQualifiedName() + localStackRef, is := stackRef.(localBackendReference) + if !is { + return nil, fmt.Errorf("bad stack reference type") + } + + stackName := localStackRef.FullyQualifiedName() snapshot, path, err := b.getStack(ctx, stackName) switch { @@ -389,7 +427,7 @@ func (b *localBackend) GetStack(ctx context.Context, stackRef backend.StackRefer case err != nil: return nil, err default: - return newStack(stackRef, path, snapshot, b), nil + return newStack(localStackRef, path, snapshot, b), nil } } @@ -596,6 +634,15 @@ func (b *localBackend) apply( events chan<- engine.Event) (*deploy.Plan, sdkDisplay.ResourceChanges, result.Result) { stackRef := stack.Ref() + localStackRef, is := stackRef.(localBackendReference) + if !is { + return nil, nil, result.Error("bad stack reference type") + } + + if currentProjectContradictsWorkspace(localStackRef) { + return nil, nil, result.Errorf("provided project name %q doesn't match Pulumi.yaml", localStackRef.project) + } + stackName := stackRef.FullyQualifiedName() actionLabel := backend.ActionLabel(kind, opts.DryRun) diff --git a/pkg/backend/filestate/backend_test.go b/pkg/backend/filestate/backend_test.go index 2dd609324c3e..f3a55be89547 100644 --- a/pkg/backend/filestate/backend_test.go +++ b/pkg/backend/filestate/backend_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" user "github.com/tweekmonster/luser" "github.com/pulumi/pulumi/pkg/v3/backend" @@ -565,3 +566,58 @@ func TestCanRenameStack(t *testing.T) { assert.Equal(t, "testproj/b", newBStack.String()) assert.FileExists(t, path.Join(tmpDir, ".pulumi", "stacks", "testproj", "b.json")) } + +func chdir(t *testing.T, dir string) { + cwd, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(dir)) // Set directory + t.Cleanup(func() { + require.NoError(t, os.Chdir(cwd)) // Restore directory + restoredDir, err := os.Getwd() + require.NoError(t, err) + require.Equal(t, cwd, restoredDir) + }) +} + +//nolint:paralleltest // mutates cwd +func TestProjectNameMustMatch(t *testing.T) { + // Login to a temp dir filestate backend + tmpDir := t.TempDir() + b, err := New(cmdutil.Diag(), "file://"+filepath.ToSlash(tmpDir)) + require.NoError(t, err) + ctx := context.Background() + + // Create a new project + projectDir := t.TempDir() + pyaml := filepath.Join(projectDir, "Pulumi.yaml") + err = os.WriteFile(pyaml, []byte("name: my-project\nruntime: test"), 0600) + require.NoError(t, err) + + chdir(t, projectDir) + + // Create a new non-project stack + aRef, err := b.ParseStackReference("a") + assert.NoError(t, err) + assert.Equal(t, "a", aRef.String()) + aStack, err := b.CreateStack(ctx, aRef, nil) + assert.NoError(t, err) + assert.Equal(t, "a", aStack.Ref().String()) + assert.FileExists(t, path.Join(tmpDir, ".pulumi", "stacks", "a.json")) + + // Create a new project stack with the wrong project name + bRef, err := b.ParseStackReference("not-my-project/b") + assert.NoError(t, err) + assert.Equal(t, "not-my-project/b", bRef.String()) + bStack, err := b.CreateStack(ctx, bRef, nil) + assert.Error(t, err) + assert.Nil(t, bStack) + + // Create a new project stack with the right project name + cRef, err := b.ParseStackReference("my-project/c") + assert.NoError(t, err) + assert.Equal(t, "my-project/c", cRef.String()) + cStack, err := b.CreateStack(ctx, cRef, nil) + assert.NoError(t, err) + assert.Equal(t, "my-project/c", cStack.Ref().String()) + assert.FileExists(t, path.Join(tmpDir, ".pulumi", "stacks", "my-project", "c.json")) +} diff --git a/pkg/backend/filestate/stack.go b/pkg/backend/filestate/stack.go index 5829543008a4..ae15e2d13d58 100644 --- a/pkg/backend/filestate/stack.go +++ b/pkg/backend/filestate/stack.go @@ -38,13 +38,13 @@ type Stack interface { // localStack is a local stack descriptor. type localStack struct { - ref backend.StackReference // the stack's reference (qualified name). - path string // a path to the stack's checkpoint file on disk. - snapshot *deploy.Snapshot // a snapshot representing the latest deployment state. - b *localBackend // a pointer to the backend this stack belongs to. + ref localBackendReference // the stack's reference (qualified name). + path string // a path to the stack's checkpoint file on disk. + snapshot *deploy.Snapshot // a snapshot representing the latest deployment state. + b *localBackend // a pointer to the backend this stack belongs to. } -func newStack(ref backend.StackReference, path string, snapshot *deploy.Snapshot, b *localBackend) Stack { +func newStack(ref localBackendReference, path string, snapshot *deploy.Snapshot, b *localBackend) Stack { return &localStack{ ref: ref, path: path,