diff --git a/changelog/pending/20221123--cli-config-new-package--preserve-comments.yaml b/changelog/pending/20221123--cli-config-new-package--preserve-comments.yaml new file mode 100644 index 000000000000..7b3be5a40c5e --- /dev/null +++ b/changelog/pending/20221123--cli-config-new-package--preserve-comments.yaml @@ -0,0 +1,4 @@ +changes: +- type: feat + scope: cli/config,new,package + description: Preserve comments on editing of project and config files. diff --git a/pkg/cmd/pulumi/new.go b/pkg/cmd/pulumi/new.go index 6391a443588b..0aa85c491383 100644 --- a/pkg/cmd/pulumi/new.go +++ b/pkg/cmd/pulumi/new.go @@ -27,8 +27,6 @@ import ( "strings" "unicode" - "gopkg.in/yaml.v3" - survey "github.com/AlecAivazis/survey/v2" surveycore "github.com/AlecAivazis/survey/v2/core" "github.com/opentracing/opentracing-go" @@ -38,7 +36,6 @@ import ( "github.com/pulumi/pulumi/pkg/v3/backend/display" "github.com/pulumi/pulumi/pkg/v3/backend/state" "github.com/pulumi/pulumi/pkg/v3/engine" - "github.com/pulumi/pulumi/pkg/v3/util/yamlutil" "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/config" @@ -257,63 +254,23 @@ func runNew(ctx context.Context, args newArgs) error { fmt.Println() // Load the project, update the name & description, remove the template section, and save it. - proj, path, err := readProjectWithPath() - root := filepath.Dir(path) + proj, root, err := readProject() if err != nil { return err } - - if filepath.Ext(path) == ".yaml" { - filedata, err := os.ReadFile(path) - if err != nil { - return err - } - var workspaceDocument yaml.Node - err = yaml.Unmarshal(filedata, &workspaceDocument) - if err != nil { - return err - } - - proj.Name = tokens.PackageName(args.name) - err = yamlutil.Insert(&workspaceDocument, "name", args.name) - if err != nil { - return err - } - proj.Description = &args.description - err = yamlutil.Insert(&workspaceDocument, "description", args.description) - if err != nil { - return err - } - proj.Template = nil - err = yamlutil.Delete(&workspaceDocument, "template") - if err != nil { - return err - } - if proj.Runtime.Name() == "python" { - // If the template does give virtualenv use it, else default to "venv" - if len(proj.Runtime.Options()) == 0 { - proj.Runtime.SetOption("virtualenv", "venv") - err = yamlutil.Insert(&workspaceDocument, "runtime", strings.TrimSpace(` -name: python -options: - virtualenv: venv -`)) - if err != nil { - return err - } - } - } - - contract.Assert(len(workspaceDocument.Content) == 1) - projFile, err := yaml.Marshal(workspaceDocument.Content[0]) - if err != nil { - return err + proj.Name = tokens.PackageName(args.name) + proj.Description = &args.description + proj.Template = nil + // Workaround for python, most of our templates don't specify a venv but we want to use one + if proj.Runtime.Name() == "python" { + // If the template does give virtualenv use it, else default to "venv" + if _, has := proj.Runtime.Options()["virtualenv"]; !has { + proj.Runtime.SetOption("virtualenv", "venv") } + } - err = os.WriteFile(path, projFile, 0600) - if err != nil { - return err - } + if err = workspace.SaveProject(proj); err != nil { + return fmt.Errorf("saving project: %w", err) } appendFileName := "Pulumi.yaml.append" diff --git a/pkg/cmd/pulumi/policy_new.go b/pkg/cmd/pulumi/policy_new.go index 4be30af35481..0a391204d8b0 100644 --- a/pkg/cmd/pulumi/policy_new.go +++ b/pkg/cmd/pulumi/policy_new.go @@ -19,7 +19,6 @@ import ( "errors" "fmt" "os" - "path/filepath" "sort" "strings" @@ -28,14 +27,12 @@ import ( "github.com/opentracing/opentracing-go" "github.com/pulumi/pulumi/pkg/v3/backend/display" "github.com/pulumi/pulumi/pkg/v3/engine" - "github.com/pulumi/pulumi/pkg/v3/util/yamlutil" "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" "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/workspace" "github.com/spf13/cobra" - "gopkg.in/yaml.v3" ) type newPolicyArgs struct { @@ -178,41 +175,16 @@ func runNewPolicyPack(ctx context.Context, args newPolicyArgs) error { return err } - if filepath.Ext(projPath) == ".yaml" { - filedata, err := os.ReadFile(projPath) - if err != nil { - return err - } - var workspaceDocument yaml.Node - err = yaml.Unmarshal(filedata, &workspaceDocument) - if err != nil { - return err - } - if proj.Runtime.Name() == "python" { - // If the template does give virtualenv use it, else default to "venv" - if len(proj.Runtime.Options()) == 0 { - proj.Runtime.SetOption("virtualenv", "venv") - err = yamlutil.Insert(&workspaceDocument, "runtime", strings.TrimSpace(` -name: python -options: -virtualenv: venv -`)) - if err != nil { - return err - } - } - } - - contract.Assert(len(workspaceDocument.Content) == 1) - projFile, err := yaml.Marshal(workspaceDocument.Content[0]) - if err != nil { - return err + // Workaround for python, most of our templates don't specify a venv but we want to use one + if proj.Runtime.Name() == "python" { + // If the template does give virtualenv use it, else default to "venv" + if _, has := proj.Runtime.Options()["virtualenv"]; !has { + proj.Runtime.SetOption("virtualenv", "venv") } + } - err = os.WriteFile(projPath, projFile, 0600) - if err != nil { - return err - } + if err = proj.Save(projPath); err != nil { + return fmt.Errorf("saving project: %w", err) } // Install dependencies. diff --git a/pkg/util/yamlutil/node_tools.go b/pkg/util/yamlutil/node_tools.go deleted file mode 100644 index 7252a6677fae..000000000000 --- a/pkg/util/yamlutil/node_tools.go +++ /dev/null @@ -1,177 +0,0 @@ -package yamlutil - -import ( - "fmt" - - "gopkg.in/yaml.v3" -) - -type yamlError struct { - *yaml.Node - err string -} - -func (e yamlError) Error() string { - return fmt.Sprintf("[%v:%v] %v", e.Node.Line, e.Node.Column, e.err) -} - -func YamlErrorf(node *yaml.Node, format string, a ...interface{}) error { - return yamlError{Node: node, err: fmt.Sprintf(format, a...)} -} - -// Get attempts to obtain the value at an index in a sequence or in a mapping. Returns the node and true if successful; -// nil and false if out of bounds or not found; and nil, false, and an error if any error occurs. -func Get(l *yaml.Node, key interface{}) (*yaml.Node, bool, error) { - switch l.Kind { - case yaml.DocumentNode: - // Automatically recurse as documents contain a single element - return Get(l.Content[0], key) - case yaml.SequenceNode: - if idx, ok := key.(int); ok { - if len(l.Content) > idx { - return l.Content[idx], true, nil - } - return nil, false, nil - } - return nil, false, YamlErrorf(l, "unsupported index type %T, found sequence node and expected int index", key) - case yaml.MappingNode: - for i, v := range l.Content { - if i%2 == 1 { - continue - } - - switch k := key.(type) { - case string: - if v.Tag == "str" && v.Value == k { - return l.Content[i+1], true, nil - } - default: - return nil, false, YamlErrorf(l, "unsupported key type %T, found mapping node and expected string key", key) - } - } - // failure to get is not an error - return nil, false, nil - case yaml.ScalarNode: - return nil, false, YamlErrorf(l, "failed to get key %v from scalar: %v", key, l.Value) - case yaml.AliasNode: - return nil, false, YamlErrorf(l, "aliases not supported in project files: %v", l.Value) - default: - return nil, false, YamlErrorf(l, "failed to parse yaml node: %v", l.Value) - } -} - -func Set(l *yaml.Node, value string) error { - var newNode yaml.Node - err := yaml.Unmarshal([]byte(value), &newNode) - if err != nil { - return err - } - - *l = *newNode.Content[0] - - return nil -} - -func Insert(l *yaml.Node, key interface{}, value string) error { - var newNode yaml.Node - err := yaml.Unmarshal([]byte(value), &newNode) - if err != nil { - return err - } - newNode = *newNode.Content[0] - - switch l.Kind { - case yaml.DocumentNode: - // Automatically recurse as documents contain a single element - return Insert(l.Content[0], key, value) - case yaml.SequenceNode: - if idx, ok := key.(int); ok { - if len(l.Content) > idx { - l.Content[idx] = &newNode - } else if len(l.Content) == idx { - l.Content = append(l.Content, &newNode) - } - return YamlErrorf(l, "index %v out of bounds of node: %v", idx, l.Value) - } - return YamlErrorf(l, "unsupported index type %T, found sequence node and expected int index", key) - case yaml.MappingNode: - for i, v := range l.Content { - if i%2 == 1 { - continue - } - - switch k := key.(type) { - case string: - if v.Tag == "!!str" && v.Value == k { - l.Content[i+1] = &newNode - return nil - } - default: - return YamlErrorf(l, "unsupported key type %T, found mapping node and expected string key", key) - } - } - - var keyNode yaml.Node - switch k := key.(type) { - case string: - err := Set(&keyNode, k) - if err != nil { - return err - } - l.Content = append(l.Content, &keyNode, &newNode) - return nil - default: - return YamlErrorf(l, "unsupported key type %T, found mapping node and expected string key", key) - } - case yaml.ScalarNode: - return YamlErrorf(l, "failed to get key %v from scalar: %v", key, l.Content) - case yaml.AliasNode: - return YamlErrorf(l, "aliases not supported in project files: %v", l.Content) - default: - return YamlErrorf(l, "failed to parse yaml node: %v", l.Content) - } -} - -func Delete(l *yaml.Node, key interface{}) error { - switch l.Kind { - case yaml.DocumentNode: - // Automatically recurse as documents contain a single element - return Delete(l.Content[0], key) - case yaml.SequenceNode: - if idx, ok := key.(int); ok { - var content []*yaml.Node - content = append(content, l.Content[:idx]...) - content = append(content, l.Content[idx+1:]...) - l.Content = content - return nil - } - return YamlErrorf(l, "unsupported index type %T, found sequence node and expected int index", key) - - case yaml.MappingNode: - for idx, v := range l.Content { - if idx%2 == 1 { - continue - } - - switch k := key.(type) { - case string: - if v.Tag == "!!str" && v.Value == k { - var content []*yaml.Node - content = append(content, l.Content[:idx]...) - content = append(content, l.Content[idx+2:]...) - l.Content = content - return nil - } - default: - return YamlErrorf(l, "unsupported key type %T, found mapping node and expected string key", key) - } - } - return nil - case yaml.ScalarNode: - return YamlErrorf(l, "failed to get key %v from scalar: %v", key, l.Content) - case yaml.AliasNode: - return YamlErrorf(l, "aliases not supported in project files: %v", l.Content) - default: - return YamlErrorf(l, "failed to parse yaml node: %v", l.Content) - } -} diff --git a/pkg/util/yamlutil/node_tools_test.go b/pkg/util/yamlutil/node_tools_test.go deleted file mode 100644 index 5f209f39e8b4..000000000000 --- a/pkg/util/yamlutil/node_tools_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package yamlutil - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" -) - -func assertYaml(t *testing.T, before, after string, mutation func(node *yaml.Node) error) { - t.Helper() - var beforeNode yaml.Node - err := yaml.Unmarshal([]byte(before), &beforeNode) - assert.NoError(t, err) - err = mutation(&beforeNode) - assert.NoError(t, err) - afterBytes, err := yaml.Marshal(beforeNode.Content[0]) - assert.NoError(t, err) - - assert.Equal(t, strings.TrimSpace(after), strings.TrimSpace(string(afterBytes))) -} - -func TestInsertNode(t *testing.T) { - t.Parallel() - - assertYaml(t, ` -foo: baz -`, ` -foo: bar -`, func(node *yaml.Node) error { return Insert(node, "foo", "bar") }) -} - -func TestInsertNodeNew(t *testing.T) { - t.Parallel() - - assertYaml(t, ` -# comment -existing: node # comment -`, ` -# comment -existing: node # comment -foo: bar -`, func(node *yaml.Node) error { return Insert(node, "foo", "bar") }) -} - -func TestInsertNodeOverwrite(t *testing.T) { - t.Parallel() - - assertYaml(t, ` -foo: 1 -# header -bar: 2 # this should become 42 -# trailer -quux: 3 -`, ` -foo: 1 -# header -bar: 42 -# trailer -quux: 3 -`, func(node *yaml.Node) error { return Insert(node, "bar", "42") }) -} - -func TestDeleteNode(t *testing.T) { - t.Parallel() - - assertYaml(t, ` -foo: 1 -# header -bar: 2 # this should become 42 -# trailer -quux: 3 -`, ` -foo: 1 -# trailer -quux: 3 -`, func(node *yaml.Node) error { return Delete(node, "bar") }) -} diff --git a/sdk/go.mod b/sdk/go.mod index 55965918e657..17186a2fbc46 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -34,6 +34,7 @@ require ( golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24 google.golang.org/grpc v1.29.1 gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 pgregory.net/rapid v0.4.7 sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 ) @@ -85,5 +86,4 @@ require ( gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) diff --git a/sdk/go.sum b/sdk/go.sum index 951a4d526e5f..e6e8785805d9 100644 --- a/sdk/go.sum +++ b/sdk/go.sum @@ -303,8 +303,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= diff --git a/sdk/go/common/encoding/marshal.go b/sdk/go/common/encoding/marshal.go index 81cd0d23e94c..c04bedf66d9a 100644 --- a/sdk/go/common/encoding/marshal.go +++ b/sdk/go/common/encoding/marshal.go @@ -22,7 +22,8 @@ import ( "io/ioutil" "path/filepath" - yaml "gopkg.in/yaml.v2" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/yamlutil" + yaml "gopkg.in/yaml.v3" ) var ( @@ -97,7 +98,14 @@ type yamlMarshaler struct { } func (m *yamlMarshaler) Marshal(v interface{}) ([]byte, error) { - return yaml.Marshal(v) + if r, ok := v.(yamlutil.HasRawValue); ok { + if len(r.RawValue()) > 0 { + // Attempt a comment preserving edit: + return yamlutil.Edit(r.RawValue(), v) + } + } + + return yamlutil.YamlEncode(v) } func (m *yamlMarshaler) Unmarshal(data []byte, v interface{}) error { diff --git a/sdk/go/common/util/yamlutil/edit.go b/sdk/go/common/util/yamlutil/edit.go new file mode 100644 index 000000000000..38ea0c08b68b --- /dev/null +++ b/sdk/go/common/util/yamlutil/edit.go @@ -0,0 +1,128 @@ +// 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 yamlutil + +import ( + "gopkg.in/yaml.v3" +) + +type HasRawValue interface { + RawValue() []byte +} + +// Edit does a deep comparison on original and new and returns a YAML document that modifies only +// the nodes changed between original and new. +func Edit(original []byte, new interface{}) ([]byte, error) { + var err error + var oldDoc yaml.Node + err = yaml.Unmarshal(original, &oldDoc) + if err != nil { + return nil, err + } + + newBytes, err := yaml.Marshal(new) + if err != nil { + return nil, err + } + var newValue yaml.Node + err = yaml.Unmarshal(newBytes, &newValue) + if err != nil { + return nil, err + } + + newValue, err = editNodes(&oldDoc, &newValue) + if err != nil { + return nil, err + } + + return YamlEncode(&newValue) +} + +func editNodes(original, new *yaml.Node) (yaml.Node, error) { + if original.Kind != new.Kind { + return *new, nil + } + + ret := *original + ret.Tag = new.Tag + ret.Value = new.Value + + switch original.Kind { + case yaml.DocumentNode, yaml.SequenceNode: + var minLen int + var content []*yaml.Node + if len(new.Content) < len(original.Content) { + minLen = len(new.Content) + } else { + minLen = len(original.Content) + } + + for i := 0; i < minLen; i++ { + item, err := editNodes(original.Content[i], new.Content[i]) + if err != nil { + return ret, err + } + content = append(content, &item) + } + // Any excess nodes in the new value are copied verbatim + content = append(content, new.Content[minLen:]...) + + ret.Content = content + return ret, nil + case yaml.MappingNode: + origKeys := make(map[string]int) + newKeys := make(map[string]int) + var newKeyList []string + + var content []*yaml.Node + for i := 0; i < len(original.Content); i += 2 { + origKeys[original.Content[i].Value] = i + } + for i := 0; i < len(new.Content); i += 2 { + value := new.Content[i].Value + newKeys[value] = i + newKeyList = append(newKeyList, value) + } + for _, k := range newKeyList { + newIdx := newKeys[k] + origIdx, has := origKeys[k] + var err error + var key yaml.Node + var value yaml.Node + if has { + key, err = editNodes(original.Content[origIdx], new.Content[newIdx]) + if err != nil { + return ret, err + } + value, err = editNodes(original.Content[origIdx+1], new.Content[newIdx+1]) + if err != nil { + return ret, err + } + } else { + key = *new.Content[newIdx] + value = *new.Content[newIdx+1] + } + content = append(content, &key) + content = append(content, &value) + } + + ret.Content = content + return ret, nil + default: // alias and scalar nodes + + ret.Content = new.Content + return ret, nil + } +} diff --git a/sdk/go/common/util/yamlutil/edit_test.go b/sdk/go/common/util/yamlutil/edit_test.go new file mode 100644 index 000000000000..11d7126153da --- /dev/null +++ b/sdk/go/common/util/yamlutil/edit_test.go @@ -0,0 +1,116 @@ +// 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 yamlutil + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func assertYamlEdit(t *testing.T, original string, edited interface{}, expected string) { + t.Helper() + + actualValue, err := Edit([]byte(original), edited) + assert.NoError(t, err) + + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(actualValue))) +} + +type Foo struct { + Foo int `yaml:"foo,omitempty"` + Bar string `yaml:"bar,omitempty"` + Baz string `yaml:"baz,omitempty"` + Quux int `yaml:"quux,omitempty"` + List []string `yaml:"list,omitempty"` + ListFoo []*Foo `yaml:"listFoo,omitempty"` +} + +func TestEdit(t *testing.T) { + // Covers 100% of the happy path statements + t.Parallel() + + assertYamlEdit(t, ` +# header +foo: ["an illegal list"] # not a valid node in the foo Struct, tests coverage of unlike kinds +bar: baz # test2 +list: ["1","2", + "3" # test3 +] #test4 +listFoo: + - bar: "bar1" + # test5 + - bar: "bar2" # nestedComment1 + list: ["a", "b", c] # nestedComment2 + # trailer + - bar: "bar3" + +# footer +`, Foo{ + Foo: 1, + Baz: "quux", + List: []string{"1", "two", "pi", "e*2"}, + ListFoo: []*Foo{ + {Bar: "barOne"}, + {Bar: "barTwo", List: []string{"a", "bee", "cee"}}, + }, + }, ` +# header +foo: 1 +baz: quux +list: ["1", "two", "pi", # test3 + e*2] #test4 +listFoo: + - bar: "barOne" + # test5 + - bar: "barTwo" # nestedComment1 + list: ["a", "bee", cee] # nestedComment2 + # trailer + +# footer +`) +} + +func TestEditEmpty(t *testing.T) { + + // Covers 100% of the happy path statements + t.Parallel() + + assertYamlEdit(t, ``, Foo{ + Foo: 1, + Baz: "quux", + List: []string{"1", "two", "pi", "e*2"}, + ListFoo: []*Foo{ + {Bar: "barOne"}, + {Bar: "barTwo", List: []string{"a", "bee", "cee"}}, + }, + }, ` +foo: 1 +baz: quux +list: + - "1" + - two + - pi + - e*2 +listFoo: + - bar: barOne + - bar: barTwo + list: + - a + - bee + - cee +`) +} diff --git a/sdk/go/common/util/yamlutil/encode.go b/sdk/go/common/util/yamlutil/encode.go new file mode 100644 index 000000000000..3cfe6b1f8630 --- /dev/null +++ b/sdk/go/common/util/yamlutil/encode.go @@ -0,0 +1,34 @@ +// 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 yamlutil + +import ( + "bytes" + "fmt" + + yaml "gopkg.in/yaml.v3" +) + +// Encodes a value using a two space indentation. +func YamlEncode(v interface{}) ([]byte, error) { + var b bytes.Buffer + yamlEncoder := yaml.NewEncoder(&b) + yamlEncoder.SetIndent(2) + err := yamlEncoder.Encode(v) + if err != nil { + return nil, fmt.Errorf("error marshaling value %#v: %w", v, err) + } + return b.Bytes(), nil +} diff --git a/sdk/go/common/workspace/loaders.go b/sdk/go/common/workspace/loaders.go index c5e6f9ba5b2f..003efffe9643 100644 --- a/sdk/go/common/workspace/loaders.go +++ b/sdk/go/common/workspace/loaders.go @@ -131,6 +131,7 @@ func (singleton *projectLoader) load(path string) (*Project, error) { return nil, fmt.Errorf("could not unmarshal '%s': %w", path, err) } + project.raw = b singleton.internal[path] = &project return &project, nil } @@ -248,6 +249,7 @@ func (singleton *projectStackLoader) load(project *Project, path string) (*Proje projectStack.Config = make(config.Map) } + projectStack.raw = b singleton.internal[path] = &projectStack return &projectStack, nil } diff --git a/sdk/go/common/workspace/project.go b/sdk/go/common/workspace/project.go index 5581fb607963..fa59e15684e7 100644 --- a/sdk/go/common/workspace/project.go +++ b/sdk/go/common/workspace/project.go @@ -178,6 +178,13 @@ type Project struct { // Handle additional keys, albeit in a way that will remove comments and trivia. AdditionalKeys map[string]interface{} `yaml:",inline"` + + // The original byte representation of the file, used to attempt trivia-preserving edits + raw []byte +} + +func (proj Project) RawValue() []byte { + return proj.raw } func isPrimitiveValue(value interface{}) bool { @@ -531,6 +538,13 @@ type PolicyPackProject struct { Website *string `json:"website,omitempty" yaml:"website,omitempty"` // License is the optional license governing this project's usage. License *string `json:"license,omitempty" yaml:"license,omitempty"` + + // The original byte representation of the file, used to attempt trivia-preserving edits + raw []byte +} + +func (proj PolicyPackProject) RawValue() []byte { + return proj.raw } func (proj *PolicyPackProject) Validate() error { @@ -574,6 +588,13 @@ type ProjectStack struct { EncryptionSalt string `json:"encryptionsalt,omitempty" yaml:"encryptionsalt,omitempty"` // Config is an optional config bag. Config config.Map `json:"config,omitempty" yaml:"config,omitempty"` + + // The original byte representation of the file, used to attempt trivia-preserving edits + raw []byte +} + +func (ps ProjectStack) RawValue() []byte { + return ps.raw } // Save writes a project definition to a file. diff --git a/sdk/go/common/workspace/project_test.go b/sdk/go/common/workspace/project_test.go index c147522e1666..ae8bc8e2e229 100644 --- a/sdk/go/common/workspace/project_test.go +++ b/sdk/go/common/workspace/project_test.go @@ -882,7 +882,7 @@ func TestProjectLoadYAML(t *testing.T) { // Test nested bad key _, err = loadProjectFromText(t, "hello:\n 6: bad") - assert.Contains(t, err.Error(), "expected only string keys, got '%!s(int=6)'") + assert.Contains(t, err.Error(), "project is missing a 'name' attribute") // Test lack of name _, err = loadProjectFromText(t, "{}") diff --git a/tests/config_test.go b/tests/config_test.go index 7232e28fe271..ab9278a79da7 100644 --- a/tests/config_test.go +++ b/tests/config_test.go @@ -243,7 +243,7 @@ config: pulumi-test:d: a: D pulumi-test:e: - - E + - E $` b, err = os.ReadFile(filepath.Join(e.CWD, "Pulumi.test.yaml")) assert.NoError(t, err) @@ -263,10 +263,10 @@ config: pulumi-test:d: a: D pulumi-test:e: - - E + - E pulumi-test:f: g: - - F + - F $` b, err = os.ReadFile(filepath.Join(e.CWD, "Pulumi.test.yaml")) assert.NoError(t, err) diff --git a/tests/go.mod b/tests/go.mod index 78f6b6c118ee..d4f8abb87f98 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -11,6 +11,7 @@ replace ( require ( github.com/blang/semver v3.5.1+incompatible github.com/golang/protobuf v1.5.2 + github.com/hexops/autogold v1.3.0 github.com/pulumi/pulumi/pkg/v3 v3.34.1 github.com/pulumi/pulumi/sdk/v3 v3.49.0 github.com/stretchr/testify v1.8.0 @@ -106,6 +107,8 @@ require ( github.com/hashicorp/hcl/v2 v2.14.0 // indirect github.com/hashicorp/vault/api v1.1.1 // indirect github.com/hashicorp/vault/sdk v0.2.1 // indirect + github.com/hexops/gotextdiff v1.0.3 // indirect + github.com/hexops/valast v1.4.0 // indirect github.com/ijc/Gotty v0.0.0-20170406111628-a8b993ba6abd // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect @@ -126,6 +129,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/natefinch/atomic v1.0.1 // indirect + github.com/nightlyone/lockfile v1.0.0 // indirect github.com/opentracing/basictracer-go v1.1.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pgavlin/goldmark v1.1.33-0.20200616210433-b5eb04559386 // indirect @@ -142,6 +146,7 @@ require ( github.com/segmentio/asm v1.1.3 // indirect github.com/segmentio/encoding v0.3.5 // indirect github.com/sergi/go-diff v1.2.0 // indirect + github.com/shurcooL/go-goon v0.0.0-20210110234559-7585751d9a17 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/spf13/cobra v1.5.0 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -167,6 +172,7 @@ require ( golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect + golang.org/x/tools v0.1.11 // indirect golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect google.golang.org/api v0.91.0 // indirect google.golang.org/appengine v1.6.7 // indirect @@ -174,7 +180,7 @@ require ( google.golang.org/protobuf v1.28.1 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/frand v1.4.2 // indirect + mvdan.cc/gofumpt v0.1.0 // indirect ) diff --git a/tests/go.sum b/tests/go.sum index 1ea3ef6e4c15..4cb3af3cfce3 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -1023,6 +1023,13 @@ github.com/hashicorp/vault/sdk v0.2.1/go.mod h1:WfUiO1vYzfBkz1TmoE4ZGU7HD0T0Cl/r github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hetznercloud/hcloud-go v1.33.1/go.mod h1:XX/TQub3ge0yWR2yHWmnDVIrB+MQbda1pHxkUmDlUME= github.com/hetznercloud/hcloud-go v1.35.0/go.mod h1:mepQwR6va27S3UQthaEPGS86jtzSY9xWL1e9dyxXpgA= +github.com/hexops/autogold v0.8.1/go.mod h1:97HLDXyG23akzAoRYJh/2OBs3kd80eHyKPvZw0S5ZBY= +github.com/hexops/autogold v1.3.0 h1:IEtGNPxBeBu8RMn8eKWh/Ll9dVNgSnJ7bp/qHgMQ14o= +github.com/hexops/autogold v1.3.0/go.mod h1:d4hwi2rid66Sag+BVuHgwakW/EmaFr8vdTSbWDbrDRI= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hexops/valast v1.4.0 h1:sFzyxPDP0riFQUzSBXTCCrAbbIndHPWMndxuEjXdZlc= +github.com/hexops/valast v1.4.0/go.mod h1:uVjKZ0smVuYlgCSPz9NRi5A04sl7lp6GtFWsROKDgEs= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -1288,6 +1295,8 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAmxBiA= +github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= @@ -1462,6 +1471,7 @@ github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= @@ -1497,6 +1507,9 @@ github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/go-goon v0.0.0-20210110234559-7585751d9a17 h1:lRAUE0dIvigSSFAmaM2dfg7OH8T+a8zJ5smEh09a/GI= +github.com/shurcooL/go-goon v0.0.0-20210110234559-7585751d9a17/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= @@ -2035,6 +2048,7 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210218084038-e8e29180ff58/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210223095934-7937bea0104d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2195,6 +2209,7 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -2207,6 +2222,7 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2568,6 +2584,9 @@ k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/ k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= +mvdan.cc/gofumpt v0.0.0-20210107193838-d24d34e18d44/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48= +mvdan.cc/gofumpt v0.1.0 h1:hsVv+Y9UsZ/mFZTxJZuHVI6shSQCtzZ11h1JEFPAZLw= +mvdan.cc/gofumpt v0.1.0/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= pgregory.net/rapid v0.4.7 h1:MTNRktPuv5FNqOO151TM9mDTa+XHcX6ypYeISDVD14g= diff --git a/tests/integration/construct_nested_component/go/go.mod b/tests/integration/construct_nested_component/go/go.mod index 7e090efb9dcf..e87fe928e7ff 100644 --- a/tests/integration/construct_nested_component/go/go.mod +++ b/tests/integration/construct_nested_component/go/go.mod @@ -62,7 +62,6 @@ require ( google.golang.org/grpc v1.49.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/frand v1.4.2 // indirect sourcegraph.com/sourcegraph/appdash v0.0.0-20211028080628-e2786a622600 // indirect diff --git a/tests/roundtrip_test.go b/tests/roundtrip_test.go new file mode 100644 index 000000000000..4617519969fc --- /dev/null +++ b/tests/roundtrip_test.go @@ -0,0 +1,184 @@ +// Copyright 2016-2021, 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. + +// nolint: lll +package tests + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/hexops/autogold" + "github.com/stretchr/testify/require" + + "github.com/pulumi/pulumi/pkg/v3/testing/integration" + ptesting "github.com/pulumi/pulumi/sdk/v3/go/common/testing" + "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" +) + +func TestProjectRoundtripComments(t *testing.T) { + t.Parallel() + + e := ptesting.NewEnvironment(t) + defer func() { + if !t.Failed() { + e.DeleteEnvironment() + } + }() + + pulumiProject := ` +# 🔴 header comment +name: pulumi-test +runtime: yaml +config: + first-value: + type: string + default: first + second-value: + type: string + third-value: + type: array + items: + type: string + default: [third] # 🟠 comment after array +# 🟡 comment before resources +resources: + my-bucket: + type: aws:s3:bucket + # 🟢 comment before props, note the indentation is excessive, will change to 2 spaces + properties: + # 🔵 comment before prop + bucket: test-123 # 🟣 comment after prop +# 🟥 footer comment +` + + integration.CreatePulumiRepo(e, pulumiProject) + projFilename := filepath.Join(e.CWD, fmt.Sprintf("%s.yaml", workspace.ProjectFile)) + // TODO: Replace this with config set --project after implemented. + proj, err := workspace.LoadProject(projFilename) + require.NoError(t, err) + ty := "string" + proj.Config["new-value"] = workspace.ProjectConfigType{ + Type: &ty, + Description: "💜 a new value added to config, expect unicode to be escaped", + } + err = proj.Save(projFilename) + require.NoError(t, err) + + projData, err := os.ReadFile(projFilename) + require.NoError(t, err) + + // Project file: + want := autogold.Want("roundtrip-project", `# 🔴 header comment +name: pulumi-test +runtime: yaml +config: + first-value: + type: string + default: first + new-value: + type: string + description: "\U0001F49C a new value added to config, expect unicode to be escaped" + second-value: + type: string + third-value: + type: array + items: + type: string + default: [third] # 🟠 comment after array +# 🟡 comment before resources +resources: + my-bucket: + # 🟢 comment before props, note the indentation is excessive, will change to 2 spaces + properties: + # 🔵 comment before prop + bucket: test-123 # 🟣 comment after prop + type: aws:s3:bucket +# 🟥 footer comment +`) + want.Equal(t, string(projData)) +} + +func TestConfigRoundtripComments(t *testing.T) { + t.Parallel() + + e := ptesting.NewEnvironment(t) + defer func() { + if !t.Failed() { + e.DeleteEnvironment() + } + }() + + pulumiProject := ` +name: foo +runtime: yaml +` + + integration.CreatePulumiRepo(e, pulumiProject) + e.SetBackend(e.LocalURL()) + e.RunCommand("pulumi", "stack", "init", "test") + e.Passphrase = "TestConfigRoundtripComments" + configFilename := filepath.Join(e.CWD, fmt.Sprintf("%s.test.yaml", workspace.ProjectFile)) + + err := os.WriteFile(configFilename, []byte(` +encryptionsalt: v1:ThS5UPxP9qc=:v1:UZYAXF+ylaJ0rGhv:9OTvBnOEDFgxs7btjzSu+LZ470vLpg== +# 🔴 header comment +config: + foo:a: some-value # 🟠 comment after value + # 🟡 comment before key + foo:b: some-value + foo:c: + a: A + b: B + c: + - 1 + - 2 + - 3 # 🟢 comment in array + # 🔵 comment after array + foo:d: + secure: v1:T1ftqhY0hqr+EJK6:+jvd5PMecFx80tcavzuZY4tLatgIfoe/xR72GA== # 🟣 comment on secret + +# 🟥 footer comment`), 0600) + require.NoError(t, err) + e.RunCommand("pulumi", "config", "set", "e", "E") + e.RunCommand("pulumi", "config", "set", "--path", "c.c[2]", "three") + + projData, err := os.ReadFile(configFilename) + require.NoError(t, err) + + // Project file: + want := autogold.Want("roundtrip-config", `encryptionsalt: v1:ThS5UPxP9qc=:v1:UZYAXF+ylaJ0rGhv:9OTvBnOEDFgxs7btjzSu+LZ470vLpg== +# 🔴 header comment +config: + foo:a: some-value # 🟠 comment after value + # 🟡 comment before key + foo:b: some-value + foo:c: + a: A + b: B + c: + - 1 + - 2 + - three # 🟢 comment in array + # 🔵 comment after array + foo:d: + secure: v1:T1ftqhY0hqr+EJK6:+jvd5PMecFx80tcavzuZY4tLatgIfoe/xR72GA== # 🟣 comment on secret + foo:e: E + +# 🟥 footer comment +`) + want.Equal(t, string(projData)) +}