diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 7a1b63ab2567..f1dc61ffda39 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -3,7 +3,10 @@ - [language/dotnet] - Updated Pulumi dotnet packages to use grpc-dotnet instead of grpc. [#9149](https://github.com/pulumi/pulumi/pull/9149) +- [cli/config] Rename the `config` property in `Pulumi.yaml` to `stackConfigDir`. The `config` key will continue to be supported. + [#9145](https://github.com/pulumi/pulumi/pull/9145) + ### Bug Fixes [sdk/nodejs] - Fix uncaught error "ENOENT: no such file or directory" when an error occurs during the stack up - [#9065](https://github.com/pulumi/pulumi/issues/9065) \ No newline at end of file + [#9065](https://github.com/pulumi/pulumi/issues/9065) diff --git a/sdk/go/common/workspace/paths.go b/sdk/go/common/workspace/paths.go index c8e9ba44b2db..bcdad5a9dfe0 100644 --- a/sdk/go/common/workspace/paths.go +++ b/sdk/go/common/workspace/paths.go @@ -1,4 +1,4 @@ -// Copyright 2016-2018, Pulumi Corporation. +// Copyright 2016-2022, Pulumi Corporation. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -98,8 +98,28 @@ func DetectProjectStackPath(stackName tokens.QName) (string, error) { return "", err } - return filepath.Join(filepath.Dir(projPath), proj.Config, fmt.Sprintf("%s.%s%s", ProjectFile, qnameFileName(stackName), - filepath.Ext(projPath))), nil + fileName := fmt.Sprintf("%s.%s%s", ProjectFile, qnameFileName(stackName), filepath.Ext(projPath)) + + // Back compat: StackConfigDir used to be called Config. + configValue, hasConfigValue := proj.Config.(string) + hasConfigValue = hasConfigValue && configValue != "" + + if proj.StackConfigDir != "" { + // If config and stackConfigDir are both set return an error + if hasConfigValue { + return "", fmt.Errorf("can not set `config` and `stackConfigDir`, remove the `config` entry") + } + + return filepath.Join(filepath.Dir(projPath), proj.StackConfigDir, fileName), nil + } + + // Back compat: If StackConfigDir is not present and Config is given and it's a non-empty string use it + // for the stacks directory. + if hasConfigValue { + return filepath.Join(filepath.Dir(projPath), configValue, fileName), nil + } + + return filepath.Join(filepath.Dir(projPath), fileName), nil } // DetectProjectPathFrom locates the closest project from the given path, searching "upwards" in the directory diff --git a/sdk/go/common/workspace/paths_test.go b/sdk/go/common/workspace/paths_test.go new file mode 100644 index 000000000000..3c8f5a025c84 --- /dev/null +++ b/sdk/go/common/workspace/paths_test.go @@ -0,0 +1,117 @@ +// Copyright 2016-2022, 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 workspace + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" + "github.com/stretchr/testify/assert" +) + +// In the tests below we use temporary directories and then expect DetectProjectAndPath to return a path to +// that directory. However DetectProjectAndPath will do symlink resolution, while ioutil.TempDir normally does +// not. This can lead to asserts especially on macos where TmpDir will have returned /var/folders/XX, but +// after sym link resolution that is /private/var/folders/XX. +func mkTempDir(t *testing.T, pattern string) string { + tmpDir, err := ioutil.TempDir("", pattern) + assert.NoError(t, err) + result, err := filepath.EvalSymlinks(tmpDir) + assert.NoError(t, err) + return result +} + +//nolint:paralleltest // Theses test use and change the current working directory +func TestDetectProjectAndPath(t *testing.T) { + tmpDir := mkTempDir(t, "TestDetectProjectAndPath") + cwd, err := os.Getwd() + assert.NoError(t, err) + defer func() { err := os.Chdir(cwd); assert.NoError(t, err) }() + err = os.Chdir(tmpDir) + assert.NoError(t, err) + + yamlPath := filepath.Join(tmpDir, "Pulumi.yaml") + yamlContents := + "name: some_project\ndescription: Some project\nruntime: nodejs\n" + + err = os.WriteFile(yamlPath, []byte(yamlContents), 0600) + assert.NoError(t, err) + + project, path, err := DetectProjectAndPath() + assert.NoError(t, err) + assert.Equal(t, yamlPath, path) + assert.Equal(t, tokens.PackageName("some_project"), project.Name) + assert.Equal(t, "Some project", *project.Description) + assert.Equal(t, "nodejs", project.Runtime.name) +} + +//nolint:paralleltest // Theses test use and change the current working directory +func TestProjectStackPath(t *testing.T) { + expectedPath := func(expectedPath string) func(t *testing.T, projectDir, path string, err error) { + return func(t *testing.T, projectDir, path string, err error) { + assert.NoError(t, err) + assert.Equal(t, filepath.Join(projectDir, expectedPath), path) + } + } + + tests := []struct { + name string + yamlContents string + validate func(t *testing.T, projectDir, path string, err error) + }{{ + "WithoutStackConfigDir", + "name: some_project\ndescription: Some project\nruntime: nodejs\n", + expectedPath("Pulumi.my_stack.yaml"), + }, { + "WithStackConfigDir", + "name: some_project\ndescription: Some project\nruntime: nodejs\nstackConfigDir: stacks\n", + expectedPath(filepath.Join("stacks", "Pulumi.my_stack.yaml")), + }, { + "WithConfig", + "name: some_project\ndescription: Some project\nruntime: nodejs\nconfig: stacks\n", + expectedPath(filepath.Join("stacks", "Pulumi.my_stack.yaml")), + }, { + "WithBoth", + "name: some_project\ndescription: Some project\nruntime: nodejs\nconfig: stacksA\nstackConfigDir: stacksB\n", + func(t *testing.T, projectDir, path string, err error) { + assert.Error(t, err) + assert.Equal(t, "can not set `config` and `stackConfigDir`, remove the `config` entry", err.Error()) + }, + }} + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + tmpDir := mkTempDir(t, "TestProjectStackPath") + cwd, err := os.Getwd() + assert.NoError(t, err) + defer func() { err := os.Chdir(cwd); assert.NoError(t, err) }() + err = os.Chdir(tmpDir) + assert.NoError(t, err) + + err = os.WriteFile( + filepath.Join(tmpDir, "Pulumi.yaml"), + []byte(tt.yamlContents), + 0600) + assert.NoError(t, err) + + path, err := DetectProjectStackPath("my_stack") + tt.validate(t, tmpDir, path, err) + }) + } +} diff --git a/sdk/go/common/workspace/project.go b/sdk/go/common/workspace/project.go index 19e86ce63c88..cb6cf2cd1d3e 100644 --- a/sdk/go/common/workspace/project.go +++ b/sdk/go/common/workspace/project.go @@ -1,4 +1,4 @@ -// Copyright 2016-2018, Pulumi Corporation. +// Copyright 2016-2022, Pulumi Corporation. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -87,8 +87,12 @@ type Project struct { // License is the optional license governing this project's usage. License *string `json:"license,omitempty" yaml:"license,omitempty"` - // Config indicates where to store the Pulumi..yaml files, combined with the folder Pulumi.yaml is in. - Config string `json:"config,omitempty" yaml:"config,omitempty"` + // Config has been renamed to StackConfigDir. + Config interface{} `json:"config,omitempty" yaml:"config,omitempty"` + + // StackConfigDir indicates where to store the Pulumi..yaml files, combined with the folder + // Pulumi.yaml is in. + StackConfigDir string `json:"stackConfigDir,omitempty" yaml:"stackConfigDir,omitempty"` // Template is an optional template manifest, if this project is a template. Template *ProjectTemplate `json:"template,omitempty" yaml:"template,omitempty"`