[CLI] - Add commands for config set-all and rm-all (#6373)

This commit is contained in:
Komal 2021-02-19 21:55:58 -08:00 committed by GitHub
parent 1731053b18
commit 4882c9fec5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 302 additions and 15 deletions

View file

@ -18,6 +18,10 @@ CHANGELOG
operations.
[#6251](https://github.com/pulumi/pulumi/pull/6251)
- [cli] Added commands `config set-all` and `config rm-all` to set and remove multiple configuration keys.
[#6373](https://github.com/pulumi/pulumi/pulls/6373)
### Bug Fixes
- [sdk/nodejs] Don't error when loading multiple copies of the same version of a Node.js
@ -30,8 +34,6 @@ CHANGELOG
- [sdk/python] Fixed a change to `Output.all()` that raised an error if no inputs are passed in.
[#6381](https://github.com/pulumi/pulumi/pull/6381)
### Bug Fixes
## 2.21.0 (2021-02-17)
### Improvements
@ -72,7 +74,7 @@ CHANGELOG
- [sdk/python] Fixed a bug in `contains_unknowns` where outputs with a property named "values" failed with a TypeError.
[#6264](https://github.com/pulumi/pulumi/pull/6264)
- [sdk/python] Allowed keyword args in Output.all() to create a dict.
[#6269](https://github.com/pulumi/pulumi/pull/6269)

View file

@ -78,7 +78,9 @@ func newConfigCmd() *cobra.Command {
cmd.AddCommand(newConfigGetCmd(&stack))
cmd.AddCommand(newConfigRmCmd(&stack))
cmd.AddCommand(newConfigRmAllCmd(&stack))
cmd.AddCommand(newConfigSetCmd(&stack))
cmd.AddCommand(newConfigSetAllCmd(&stack))
cmd.AddCommand(newConfigRefreshCmd(&stack))
cmd.AddCommand(newConfigCopyCmd(&stack))
@ -245,9 +247,9 @@ func newConfigGetCmd(stack *string) *cobra.Command {
Short: "Get a single configuration value",
Long: "Get a single configuration value.\n\n" +
"The `--path` flag can be used to get a value inside a map or list:\n\n" +
" - `pulumi config get --path outer.inner` will get the value of the `inner` key, " +
" - `pulumi config get --path outer.inner` will get the value of the `inner` key, " +
"if the value of `outer` is a map `inner: value`.\n" +
" - `pulumi config get --path names[0]` will get the value of the first item, " +
" - `pulumi config get --path names[0]` will get the value of the first item, " +
"if the value of `names` is a list.",
Args: cmdutil.SpecificArgs([]string{"key"}),
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
@ -286,9 +288,9 @@ func newConfigRmCmd(stack *string) *cobra.Command {
Short: "Remove configuration value",
Long: "Remove configuration value.\n\n" +
"The `--path` flag can be used to remove a value inside a map or list:\n\n" +
" - `pulumi config rm --path outer.inner` will remove the `inner` key, " +
" - `pulumi config rm --path outer.inner` will remove the `inner` key, " +
"if the value of `outer` is a map `inner: value`.\n" +
" - `pulumi config rm --path names[0]` will remove the first item, " +
" - `pulumi config rm --path names[0]` will remove the first item, " +
"if the value of `names` is a list.",
Args: cmdutil.SpecificArgs([]string{"key"}),
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
@ -326,6 +328,56 @@ func newConfigRmCmd(stack *string) *cobra.Command {
return rmCmd
}
func newConfigRmAllCmd(stack *string) *cobra.Command {
var path bool
rmAllCmd := &cobra.Command{
Use: "rm-all <key1> <key2> <key3> ...",
Short: "Remove multiple configuration values",
Long: "Remove multiple configuration values.\n\n" +
"The `--path` flag indicates that keys should be parsed within maps or lists:\n\n" +
" - `pulumi config rm-all --path outer.inner foo[0] key1` will remove the \n" +
" `inner` key of the `outer` map, the first key of the `foo` list and `key1`.\n" +
" - `pulumi config rm-all outer.inner foo[0] key1` will remove the literal" +
" `outer.inner`, `foo[0]` and `key1` keys",
Args: cmdutil.MinimumNArgs(1),
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
opts := display.Options{
Color: cmdutil.GetGlobalColorization(),
}
s, err := requireStack(*stack, true, opts, false /*setCurrent*/)
if err != nil {
return err
}
ps, err := loadProjectStack(s)
if err != nil {
return err
}
for _, arg := range args {
key, err := parseConfigKey(arg)
if err != nil {
return errors.Wrap(err, "invalid configuration key")
}
err = ps.Config.Remove(key, path)
if err != nil {
return err
}
}
return saveProjectStack(s, ps)
}),
}
rmAllCmd.PersistentFlags().BoolVar(
&path, "path", false,
"Parse the keys as paths in a map or list rather than raw strings")
return rmAllCmd
}
func newConfigRefreshCmd(stack *string) *cobra.Command {
var force bool
refreshCmd := &cobra.Command{
@ -409,12 +461,12 @@ func newConfigSetCmd(stack *string) *cobra.Command {
"If a value is not present on the command line, pulumi will prompt for the value. Multi-line values\n" +
"may be set by piping a file to standard in.\n\n" +
"The `--path` flag can be used to set a value inside a map or list:\n\n" +
" - `pulumi config set --path names[0] a` " +
" - `pulumi config set --path names[0] a` " +
"will set the value to a list with the first item `a`.\n" +
" - `pulumi config set --path parent.nested value` " +
" - `pulumi config set --path parent.nested value` " +
"will set the value of `parent` to a map `nested: value`.\n" +
" - `pulumi config set --path '[\"parent.name\"].[\"nested.name\"]' value` will set the value of \n" +
" `parent.name` to a map `nested.name: value`.",
" - `pulumi config set --path '[\"parent.name\"].[\"nested.name\"]' value` will set the value of \n" +
" `parent.name` to a map `nested.name: value`.",
Args: cmdutil.RangeArgs(1, 2),
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
opts := display.Options{
@ -507,6 +559,116 @@ func newConfigSetCmd(stack *string) *cobra.Command {
return setCmd
}
func newConfigSetAllCmd(stack *string) *cobra.Command {
var plaintextArgs []string
var secretArgs []string
var path bool
setCmd := &cobra.Command{
Use: "set-all --plaintext key1=value1 --plaintext key2=value2 --secret key3=value3",
Short: "Set multiple configuration values",
Long: "pulumi set-all allows you to set multiple configuration values in one command.\n\n" +
"Each key-value pair must be preceded by either the `--secret` or the `--plaintext` flag to denote whether \n" +
"it should be encrypted:\n\n" +
" - `pulumi config set-all --secret key1=value1 --plaintext key2=value --secret key3=value3`\n\n" +
"The `--path` flag can be used to set values inside a map or list:\n\n" +
" - `pulumi config set-all --path --plaintext \"names[0]\"=a --plaintext \"names[1]\"=b` \n" +
" will set the value to a list with the first item `a` and second item `b`.\n" +
" - `pulumi config set-all --path --plaintext parent.nested=value --plaintext parent.other=value2` \n" +
" will set the value of `parent` to a map `{nested: value, other: value2}`.\n" +
" - `pulumi config set-all --path --plaintext '[\"parent.name\"].[\"nested.name\"]'=value` will set the \n" +
" value of `parent.name` to a map `nested.name: value`.",
Args: cmdutil.NoArgs,
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
opts := display.Options{
Color: cmdutil.GetGlobalColorization(),
}
// Ensure the stack exists.
s, err := requireStack(*stack, true, opts, false /*setCurrent*/)
if err != nil {
return err
}
ps, err := loadProjectStack(s)
if err != nil {
return err
}
for _, ptArg := range plaintextArgs {
key, value, err := parseKeyValuePair(ptArg)
if err != nil {
return err
}
v := config.NewValue(value)
err = ps.Config.Set(key, v, path)
if err != nil {
return err
}
}
for _, sArg := range secretArgs {
key, value, err := parseKeyValuePair(sArg)
if err != nil {
return err
}
c, cerr := getStackEncrypter(s)
if cerr != nil {
return cerr
}
enc, eerr := c.EncryptValue(value)
if eerr != nil {
return eerr
}
v := config.NewSecureValue(enc)
err = ps.Config.Set(key, v, path)
if err != nil {
return err
}
}
return saveProjectStack(s, ps)
}),
}
setCmd.PersistentFlags().BoolVar(
&path, "path", false,
"Parse the keys as paths in a map or list rather than raw strings")
setCmd.PersistentFlags().StringArrayVar(
&plaintextArgs, "plaintext", []string{},
"Marks a value as plaintext (unencrypted)")
setCmd.PersistentFlags().StringArrayVar(
&secretArgs, "secret", []string{},
"Marks a value as secret to be encrypted")
return setCmd
}
func parseKeyValuePair(pair string) (config.Key, string, error) {
// Split the arg on the first '=' to separate key and value.
splitArg := strings.SplitN(pair, "=", 2)
// Check if the key is wrapped in quote marks and split on the '=' following the wrapping quote.
firstChar := string([]rune(pair)[0])
if firstChar == "\"" || firstChar == "'" {
pair = strings.TrimPrefix(pair, firstChar)
splitArg = strings.SplitN(pair, fmt.Sprintf("%s=", firstChar), 2)
}
if len(splitArg) < 2 {
return config.Key{}, "", errors.New("config value must be in the form [key]=[value]")
}
key, err := parseConfigKey(splitArg[0])
if err != nil {
return config.Key{}, "", errors.Wrap(err, "invalid configuration key")
}
value := splitArg[1]
return key, value, nil
}
var stackConfigFile string
func getProjectStackPath(stack backend.Stack) (string, error) {
@ -531,7 +693,7 @@ func saveProjectStack(stack backend.Stack, ps *workspace.ProjectStack) error {
}
func parseConfigKey(key string) (config.Key, error) {
// As a convience, we'll treat any key with no delimiter as if:
// As a convenience, we'll treat any key with no delimiter as if:
// <program-name>:<key> had been written instead
if !strings.Contains(key, tokens.TokenDelimiter) {
proj, err := workspace.DetectProject()
@ -741,8 +903,8 @@ func looksLikeSecret(k config.Key, v string) bool {
// Compute the strength use the resulting entropy to flag whether this looks like a secret.
info := zxcvbn.PasswordStrength(v, nil)
entropyPerChar := info.Entropy / float64(len(v))
return (info.Entropy >= entropyThreshold ||
(info.Entropy >= (entropyThreshold/2) && entropyPerChar >= entropyPerCharThreshold))
return info.Entropy >= entropyThreshold ||
(info.Entropy >= (entropyThreshold/2) && entropyPerChar >= entropyPerCharThreshold)
}
// getStackConfiguration loads configuration information for a given stack. If stackConfigFile is non empty,

View file

@ -38,6 +38,6 @@ func TestSecretDetection(t *testing.T) {
assert.True(t, looksLikeSecret(config.MustMakeKey("test", "token"), "1415fc1f4eaeb5e096ee58c1480016638fff29bf"))
assert.True(t, looksLikeSecret(config.MustMakeKey("test", "apiToken"), "1415fc1f4eaeb5e096ee58c1480016638fff29bf"))
// The key name does not match the, so even though this "looks like" a secret, we say it is not.
// The key name does not match the pattern, so even though this "looks like" a secret, we say it is not.
assert.False(t, looksLikeSecret(config.MustMakeKey("test", "okay"), "1415fc1f4eaeb5e096ee58c1480016638fff29bf"))
}

View file

@ -44,6 +44,12 @@ func MaximumNArgs(n int) cobra.PositionalArgs {
return ArgsFunc(cobra.MaximumNArgs(n))
}
// MinimumNArgs is the same as cobra.MinimumNArgs, except it is wrapped with ArgsFunc to provide standard
// Pulumi error handling.
func MinimumNArgs(n int) cobra.PositionalArgs {
return ArgsFunc(cobra.MinimumNArgs(n))
}
// ExactArgs is the same as cobra.ExactArgs, except it is wrapped with ArgsFunc to provide standard
// Pulumi error handling.
func ExactArgs(n int) cobra.PositionalArgs {

117
tests/config_test.go Normal file
View file

@ -0,0 +1,117 @@
// 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.
package tests
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/pulumi/pulumi/pkg/v2/testing/integration"
ptesting "github.com/pulumi/pulumi/sdk/v2/go/common/testing"
)
func TestConfigCommands(t *testing.T) {
t.Run("SanityTest", func(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer func() {
if !t.Failed() {
e.DeleteEnvironment()
}
}()
integration.CreateBasicPulumiRepo(e)
e.SetBackend(e.LocalURL())
e.RunCommand("pulumi", "stack", "init", "test")
// check config is empty
stdout, _ := e.RunCommand("pulumi", "config")
assert.Equal(t, "KEY VALUE", strings.Trim(stdout, "\r\n"))
// set a bunch of config
e.RunCommand("pulumi", "config", "set-all",
"--plaintext", "key1=value1",
"--plaintext", "outer.inner=value2",
"--secret", "my_token=my_secret_token",
"--plaintext", "myList[0]=foo")
// check that it all exists
stdout, _ = e.RunCommand("pulumi", "config", "get", "key1")
assert.Equal(t, "value1", strings.Trim(stdout, "\r\n"))
stdout, _ = e.RunCommand("pulumi", "config", "get", "outer.inner")
assert.Equal(t, "value2", strings.Trim(stdout, "\r\n"))
stdout, _ = e.RunCommand("pulumi", "config", "get", "my_token")
assert.Equal(t, "my_secret_token", strings.Trim(stdout, "\r\n"))
stdout, _ = e.RunCommand("pulumi", "config", "get", "myList[0]")
assert.Equal(t, "foo", strings.Trim(stdout, "\r\n"))
// check that the nested config does not exist because we didn't use path
_, stderr := e.RunCommandExpectError("pulumi", "config", "get", "outer")
assert.Equal(t, "error: configuration key 'outer' not found for stack 'test'", strings.Trim(stderr, "\r\n"))
_, stderr = e.RunCommandExpectError("pulumi", "config", "get", "myList")
assert.Equal(t, "error: configuration key 'myList' not found for stack 'test'", strings.Trim(stderr, "\r\n"))
// set the nested config using --path
e.RunCommand("pulumi", "config", "set-all", "--path",
"--plaintext", "outer.inner=value2",
"--plaintext", "myList[0]=foo")
// check that the nested config now exists
stdout, _ = e.RunCommand("pulumi", "config", "get", "outer")
assert.Equal(t, "{\"inner\":\"value2\"}", strings.Trim(stdout, "\r\n"))
stdout, _ = e.RunCommand("pulumi", "config", "get", "myList")
assert.Equal(t, "[\"foo\"]", strings.Trim(stdout, "\r\n"))
// remove the nested config values
e.RunCommand("pulumi", "config", "rm-all", "--path", "outer.inner", "myList[0]")
// check that it worked
stdout, _ = e.RunCommand("pulumi", "config", "get", "outer")
assert.Equal(t, "{}", strings.Trim(stdout, "\r\n"))
stdout, _ = e.RunCommand("pulumi", "config", "get", "myList")
assert.Equal(t, "[]", strings.Trim(stdout, "\r\n"))
// remove other config values
e.RunCommand("pulumi", "config", "rm-all",
"outer.inner", "myList[0]", "outer", "myList", "key1", "my_token")
// check that you can add keys with '=' in it
e.RunCommand("pulumi", "config", "set-all",
"--plaintext", "\"foo=\"=value2", "--plaintext", "'=some-weird=key='=value3")
// check that they registered correctly
stdout, _ = e.RunCommand("pulumi", "config", "get", "foo=")
assert.Equal(t, "value2", strings.Trim(stdout, "\r\n"))
stdout, _ = e.RunCommand("pulumi", "config", "get", "=some-weird=key=")
assert.Equal(t, "value3", strings.Trim(stdout, "\r\n"))
// remove the config key
e.RunCommand("pulumi", "config", "rm-all", "foo=", "=some-weird=key=")
// check config is empty again
stdout, _ = e.RunCommand("pulumi", "config")
assert.Equal(t, "KEY VALUE", strings.Trim(stdout, "\r\n"))
e.RunCommand("pulumi", "stack", "rm", "--yes")
})
}