From 6d09fe32df2ac7eca3db08c3b4a61dddcb7d756e Mon Sep 17 00:00:00 2001 From: Paul Stack Date: Fri, 17 Jul 2020 13:14:10 +0300 Subject: [PATCH] Add the ability to copy configs between stacks (#4971) --- CHANGELOG.md | 3 + pkg/cmd/pulumi/config.go | 156 +++++++++++++++++++- pkg/cmd/pulumi/crypto.go | 2 +- pkg/cmd/pulumi/history.go | 2 +- pkg/go.sum | 6 + scripts/go.sum | 5 + sdk/go.mod | 2 + sdk/go.sum | 5 + sdk/go/common/resource/config/crypt.go | 18 +++ sdk/go/common/resource/config/map.go | 14 ++ sdk/go/common/resource/config/map_test.go | 76 ++++++++++ sdk/go/common/resource/config/value.go | 87 +++++++++++ sdk/go/common/resource/config/value_test.go | 37 +++++ tests/go.sum | 6 + 14 files changed, 415 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90a7ab569..236afb780 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ CHANGELOG - Add support for streamInvoke during update [#4990](https://github.com/pulumi/pulumi/pull/4990) + +- Add ability to copy configuration values between stacks + [#4971](https://github.com/pulumi/pulumi/pull/4971) - Add logic to parce pulumi venv on github action [#4994](https://github.com/pulumi/pulumi/pull/4994) diff --git a/pkg/cmd/pulumi/config.go b/pkg/cmd/pulumi/config.go index 6446c4b0f..fbe0226cb 100644 --- a/pkg/cmd/pulumi/config.go +++ b/pkg/cmd/pulumi/config.go @@ -80,10 +80,162 @@ func newConfigCmd() *cobra.Command { cmd.AddCommand(newConfigRmCmd(&stack)) cmd.AddCommand(newConfigSetCmd(&stack)) cmd.AddCommand(newConfigRefreshCmd(&stack)) + cmd.AddCommand(newConfigCopyCmd(&stack)) return cmd } +func newConfigCopyCmd(stack *string) *cobra.Command { + var path bool + var destinationStackName string + + cpCommand := &cobra.Command{ + Use: "cp [key]", + Short: "Copy config to another stack", + Long: "Copies the config from the current stack to the destination stack. If `key` is omitted,\n" + + "then all of the config from the current stack will be copied to the destination stack.", + Args: cmdutil.MaximumNArgs(1), + Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error { + opts := display.Options{ + Color: cmdutil.GetGlobalColorization(), + } + + // Get current stack and ensure that it is a different stack to the destination stack + currentStack, err := requireStack(*stack, false, opts, true /*setCurrent*/) + if err != nil { + return err + } + if currentStack.Ref().Name().String() == destinationStackName { + return errors.New("current stack and destination stack are the same") + } + currentProjectStack, err := loadProjectStack(currentStack) + if err != nil { + return err + } + + // Get the destination stack + destinationStack, err := requireStack(destinationStackName, false, opts, false /*setCurrent*/) + if err != nil { + return err + } + destinationProjectStack, err := loadProjectStack(destinationStack) + if err != nil { + return err + } + + // Do we need to copy a single value or the entire map + if len(args) > 0 { + // A single key was specified so we only need to copy that specific value + return copySingleConfigKey(args[0], path, currentStack, currentProjectStack, destinationStack, + destinationProjectStack) + } + + return copyEntireConfigMap(currentStack, currentProjectStack, destinationStack, destinationProjectStack) + }), + } + + cpCommand.PersistentFlags().BoolVar( + &path, "path", false, + "The key contains a path to a property in a map or list to set") + cpCommand.PersistentFlags().StringVarP( + &destinationStackName, "dest", "d", "", + "The name of the new stack to copy the config to") + + return cpCommand +} + +func copySingleConfigKey(configKey string, path bool, currentStack backend.Stack, + currentProjectStack *workspace.ProjectStack, destinationStack backend.Stack, + destinationProjectStack *workspace.ProjectStack) error { + var decrypter config.Decrypter + key, err := parseConfigKey(configKey) + if err != nil { + return errors.Wrap(err, "invalid configuration key") + } + + v, ok, err := currentProjectStack.Config.Get(key, path) + if err != nil { + return err + } + if ok { + if v.Secure() { + var err error + if decrypter, err = getStackDecrypter(currentStack); err != nil { + return errors.Wrap(err, "could not create a decrypter") + } + } else { + decrypter = config.NewPanicCrypter() + } + + encrypter, cerr := getStackEncrypter(destinationStack) + if cerr != nil { + return cerr + } + + val, err := v.Copy(decrypter, encrypter) + if err != nil { + return err + } + + err = destinationProjectStack.Config.Set(key, val, path) + if err != nil { + return err + } + + return saveProjectStack(destinationStack, destinationProjectStack) + } + + return errors.Errorf( + "configuration key '%s' not found for stack '%s'", prettyKey(key), currentStack.Ref()) +} + +func copyEntireConfigMap(currentStack backend.Stack, + currentProjectStack *workspace.ProjectStack, destinationStack backend.Stack, + destinationProjectStack *workspace.ProjectStack) error { + + var decrypter config.Decrypter + currentConfig := currentProjectStack.Config + if currentConfig.HasSecureValue() { + dec, decerr := getStackDecrypter(currentStack) + if decerr != nil { + return decerr + } + decrypter = dec + } else { + decrypter = config.NewPanicCrypter() + } + + encrypter, cerr := getStackEncrypter(destinationStack) + if cerr != nil { + return cerr + } + + newProjectConfig, err := currentConfig.Copy(decrypter, encrypter) + if err != nil { + return err + } + + var requiresSaving bool + for key, val := range newProjectConfig { + err = destinationProjectStack.Config.Set(key, val, false) + if err != nil { + return err + } + requiresSaving = true + } + + // The use of `requiresSaving` here ensures that there was actually some config + // that needed saved, otherwise it's an unnecessary save call + if requiresSaving { + err := saveProjectStack(destinationStack, destinationProjectStack) + if err != nil { + return err + } + } + + return nil +} + func newConfigGetCmd(stack *string) *cobra.Command { var jsonOut bool var path bool @@ -431,7 +583,7 @@ func listConfig(stack backend.Stack, showSecrets bool, jsonOut bool) error { // By default, we will use a blinding decrypter to show "[secret]". If requested, display secrets in plaintext. decrypter := config.NewBlindingDecrypter() if cfg.HasSecureValue() && showSecrets { - dec, decerr := getStackDencrypter(stack) + dec, decerr := getStackDecrypter(stack) if decerr != nil { return decerr } @@ -518,7 +670,7 @@ func getConfig(stack backend.Stack, key config.Key, path, jsonOut bool) error { var d config.Decrypter if v.Secure() { var err error - if d, err = getStackDencrypter(stack); err != nil { + if d, err = getStackDecrypter(stack); err != nil { return errors.Wrap(err, "could not create a decrypter") } } else { diff --git a/pkg/cmd/pulumi/crypto.go b/pkg/cmd/pulumi/crypto.go index 25a927e36..4722b5de2 100644 --- a/pkg/cmd/pulumi/crypto.go +++ b/pkg/cmd/pulumi/crypto.go @@ -37,7 +37,7 @@ func getStackEncrypter(s backend.Stack) (config.Encrypter, error) { return sm.Encrypter() } -func getStackDencrypter(s backend.Stack) (config.Decrypter, error) { +func getStackDecrypter(s backend.Stack) (config.Decrypter, error) { sm, err := getStackSecretsManager(s) if err != nil { return nil, err diff --git a/pkg/cmd/pulumi/history.go b/pkg/cmd/pulumi/history.go index 517a61508..622f03edf 100644 --- a/pkg/cmd/pulumi/history.go +++ b/pkg/cmd/pulumi/history.go @@ -61,7 +61,7 @@ This command lists data about previous updates for a stack.`, } var decrypter config.Decrypter if showSecrets { - crypter, err := getStackDencrypter(s) + crypter, err := getStackDecrypter(s) if err != nil { return errors.Wrap(err, "decrypting secrets") } diff --git a/pkg/go.sum b/pkg/go.sum index d28de09eb..01b768ddd 100644 --- a/pkg/go.sum +++ b/pkg/go.sum @@ -134,6 +134,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -323,12 +325,16 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= diff --git a/scripts/go.sum b/scripts/go.sum index 11c30bbdc..19641dca7 100644 --- a/scripts/go.sum +++ b/scripts/go.sum @@ -24,6 +24,7 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -80,10 +81,14 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= diff --git a/sdk/go.mod b/sdk/go.mod index d33bb5618..adce6163c 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -15,6 +15,8 @@ require ( github.com/golang/protobuf v1.3.5 github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 github.com/hashicorp/go-multierror v1.0.0 + github.com/kr/pretty v0.2.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.6 // indirect github.com/mattn/go-runewidth v0.0.8 // indirect github.com/mitchellh/go-ps v1.0.0 diff --git a/sdk/go.sum b/sdk/go.sum index b4a839f18..c6ba5d919 100644 --- a/sdk/go.sum +++ b/sdk/go.sum @@ -32,6 +32,7 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -104,10 +105,14 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= diff --git a/sdk/go/common/resource/config/crypt.go b/sdk/go/common/resource/config/crypt.go index 4b115e627..b0f97c8d2 100644 --- a/sdk/go/common/resource/config/crypt.go +++ b/sdk/go/common/resource/config/crypt.go @@ -208,3 +208,21 @@ func decryptAES256GCM(ciphertext []byte, key []byte, nonce []byte) (string, erro return string(msg), err } + +// Crypter that just adds a prefix to the plaintext string when encrypting, +// and removes the prefix from the ciphertext when decrypting, for use in tests. +type prefixCrypter struct { + prefix string +} + +func newPrefixCrypter(prefix string) Crypter { + return prefixCrypter{prefix: prefix} +} + +func (c prefixCrypter) DecryptValue(ciphertext string) (string, error) { + return strings.TrimPrefix(ciphertext, c.prefix), nil +} + +func (c prefixCrypter) EncryptValue(plaintext string) (string, error) { + return c.prefix + plaintext, nil +} diff --git a/sdk/go/common/resource/config/map.go b/sdk/go/common/resource/config/map.go index c90e88a44..aeb32e0ab 100644 --- a/sdk/go/common/resource/config/map.go +++ b/sdk/go/common/resource/config/map.go @@ -43,6 +43,20 @@ func (m Map) Decrypt(decrypter Decrypter) (map[Key]string, error) { return r, nil } +func (m Map) Copy(decrypter Decrypter, encrypter Encrypter) (Map, error) { + newConfig := make(Map) + for k, c := range m { + val, err := c.Copy(decrypter, encrypter) + if err != nil { + return nil, err + } + + newConfig[k] = val + } + + return newConfig, nil +} + // HasSecureValue returns true if the config map contains a secure (encrypted) value. func (m Map) HasSecureValue() bool { for _, v := range m { diff --git a/sdk/go/common/resource/config/map_test.go b/sdk/go/common/resource/config/map_test.go index b07588ad8..6d0c7f470 100644 --- a/sdk/go/common/resource/config/map_test.go +++ b/sdk/go/common/resource/config/map_test.go @@ -1170,6 +1170,82 @@ func TestSetFail(t *testing.T) { } } +func TestCopyMap(t *testing.T) { + tests := []struct { + Config Map + Expected Map + }{ + { + Config: Map{ + MustMakeKey("my", "testKey"): NewValue("testValue"), + }, + Expected: Map{ + MustMakeKey("my", "testKey"): NewValue("testValue"), + }, + }, + { + Config: Map{ + MustMakeKey("my", "testKey"): NewSecureValue("stackAsecurevalue"), + }, + Expected: Map{ + MustMakeKey("my", "testKey"): NewSecureValue("stackBsecurevalue"), + }, + }, + { + Config: Map{ + MustMakeKey("my", "testKey"): NewObjectValue(`{"inner":"value"}`), + }, + Expected: Map{ + MustMakeKey("my", "testKey"): NewObjectValue(`{"inner":"value"}`), + }, + }, + { + Config: Map{ + MustMakeKey("my", "testKey"): NewSecureObjectValue(`{"inner":{"secure":"stackAsecurevalue"}}`), + }, + Expected: Map{ + MustMakeKey("my", "testKey"): NewSecureObjectValue(`{"inner":{"secure":"stackBsecurevalue"}}`), + }, + }, + { + Config: Map{ + //nolint:lll + MustMakeKey("my", "testKey"): NewSecureObjectValue(`[{"inner":{"secure":"stackAsecurevalue"}},{"secure":"stackAsecurevalue2"}]`), + }, + Expected: Map{ + //nolint:lll + MustMakeKey("my", "testKey"): NewSecureObjectValue(`[{"inner":{"secure":"stackBsecurevalue"}},{"secure":"stackBsecurevalue2"}]`), + }, + }, + { + Config: Map{ + MustMakeKey("my", "test.Key"): NewValue("testValue"), + }, + Expected: Map{ + MustMakeKey("my", "test.Key"): NewValue("testValue"), + }, + }, + { + Config: Map{ + MustMakeKey("my", "name"): NewObjectValue(`[["value"]]`), + }, + Expected: Map{ + MustMakeKey("my", "name"): NewObjectValue(`[["value"]]`), + }, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%v", test), func(t *testing.T) { + newConfig, err := test.Config.Copy(newPrefixCrypter("stackA"), newPrefixCrypter("stackB")) + assert.NoError(t, err) + + assert.Equal(t, test.Expected, newConfig) + }) + } + +} + func roundtripMapYAML(m Map) (Map, error) { return roundtripMap(m, yaml.Marshal, yaml.Unmarshal) } diff --git a/sdk/go/common/resource/config/value.go b/sdk/go/common/resource/config/value.go index d27b2b896..907f22bd7 100644 --- a/sdk/go/common/resource/config/value.go +++ b/sdk/go/common/resource/config/value.go @@ -72,6 +72,46 @@ func (c Value) Value(decrypter Decrypter) (string, error) { return decrypter.DecryptValue(c.value) } +func (c Value) Copy(decrypter Decrypter, encrypter Encrypter) (Value, error) { + var val Value + raw, err := c.Value(decrypter) + if err != nil { + return Value{}, err + } + if c.Secure() { + if c.Object() { + objVal, err := c.ToObject() + if err != nil { + return Value{}, err + } + encryptedObj, err := reencryptObject(objVal, decrypter, encrypter) + if err != nil { + return Value{}, err + } + json, err := json.Marshal(encryptedObj) + if err != nil { + return Value{}, err + } + + val = NewSecureObjectValue(string(json)) + } else { + enc, eerr := encrypter.EncryptValue(raw) + if eerr != nil { + return Value{}, eerr + } + val = NewSecureValue(enc) + } + } else { + if c.Object() { + val = NewObjectValue(raw) + } else { + val = NewValue(raw) + } + } + + return val, nil +} + func (c Value) SecureValues(decrypter Decrypter) ([]string, error) { d := NewTrackingDecrypter(decrypter) if _, err := c.Value(d); err != nil { @@ -240,6 +280,53 @@ func isSecureValue(v interface{}) (bool, string) { return false, "" } +func reencryptObject(v interface{}, decrypter Decrypter, encrypter Encrypter) (interface{}, error) { + reencryptIt := func(val interface{}) (interface{}, error) { + if isSecure, secureVal := isSecureValue(val); isSecure { + newVal := NewSecureValue(secureVal) + raw, err := newVal.Value(decrypter) + if err != nil { + return nil, err + } + + encVal, err := encrypter.EncryptValue(raw) + if err != nil { + return nil, err + } + + m := make(map[string]string) + m["secure"] = encVal + + return m, nil + } + return reencryptObject(val, decrypter, encrypter) + } + + switch t := v.(type) { + case map[string]interface{}: + m := make(map[string]interface{}) + for key, val := range t { + encrypted, err := reencryptIt(val) + if err != nil { + return nil, err + } + m[key] = encrypted + } + return m, nil + case []interface{}: + a := make([]interface{}, len(t)) + for i, val := range t { + encrypted, err := reencryptIt(val) + if err != nil { + return nil, err + } + a[i] = encrypted + } + return a, nil + } + return v, nil +} + // decryptObject returns a new object with all secure values in the object converted to decrypted strings. func decryptObject(v interface{}, decrypter Decrypter) (interface{}, error) { decryptIt := func(val interface{}) (interface{}, error) { diff --git a/sdk/go/common/resource/config/value_test.go b/sdk/go/common/resource/config/value_test.go index a1bb81181..443e151ce 100644 --- a/sdk/go/common/resource/config/value_test.go +++ b/sdk/go/common/resource/config/value_test.go @@ -245,6 +245,43 @@ func TestSecureValues(t *testing.T) { } } +func TestCopyValue(t *testing.T) { + tests := []struct { + Val Value + Expected Value + }{ + { + Val: NewValue("value"), + Expected: NewValue("value"), + }, + { + Val: NewObjectValue(`{"foo":"bar"}`), + Expected: NewObjectValue(`{"foo":"bar"}`), + }, + { + Val: NewSecureObjectValue(`{"foo":{"secure":"stackAsecurevalue"}}`), + Expected: NewSecureObjectValue(`{"foo":{"secure":"stackBsecurevalue"}}`), + }, + { + Val: NewSecureValue("stackAsecurevalue"), + Expected: NewSecureValue("stackBsecurevalue"), + }, + { + Val: NewSecureObjectValue(`["a",{"secure":"stackAalpha"},{"test":{"secure":"stackAbeta"}}]`), + Expected: NewSecureObjectValue(`["a",{"secure":"stackBalpha"},{"test":{"secure":"stackBbeta"}}]`), + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%v", test), func(t *testing.T) { + newConfig, err := test.Val.Copy(newPrefixCrypter("stackA"), newPrefixCrypter("stackB")) + assert.NoError(t, err) + + assert.Equal(t, test.Expected, newConfig) + }) + } +} + func roundtripValueYAML(v Value) (Value, error) { return roundtripValue(v, yaml.Marshal, yaml.Unmarshal) } diff --git a/tests/go.sum b/tests/go.sum index 47fe9e994..f66992b86 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -130,6 +130,8 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -314,12 +316,16 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=