diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index c0bba8b3..415118ab 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,5 +1,6 @@ ### Improvements +- `esc env set` now supports --file parameter to read content from a file or stdin [#556](https://github.com/pulumi/esc/pull/556) - `--draft` flag for `esc env set`, `esc env edit`, `esc env versions rollback` to create a change request rather than updating directly. **Warning: this feature is in preview, limited to specific orgs, and subject to change.** [#552](https://github.com/pulumi/esc/pull/552) diff --git a/cmd/esc/cli/env_get.go b/cmd/esc/cli/env_get.go index c568a832..125d4e91 100644 --- a/cmd/esc/cli/env_get.go +++ b/cmd/esc/cli/env_get.go @@ -132,7 +132,7 @@ func newEnvGetCmd(env *envCommand) *cobra.Command { "Set to print just the definition.") cmd.Flags().StringVar( &value, "value", "", - "Set to print just the value in the given format. May be 'dotenv', 'json', 'detailed', or 'shell'") + "Set to print just the value in the given format. May be 'dotenv', 'json', 'detailed', 'shell' or 'string'") cmd.Flags().BoolVar( &showSecrets, "show-secrets", false, "Show static secrets in plaintext rather than ciphertext") diff --git a/cmd/esc/cli/env_open.go b/cmd/esc/cli/env_open.go index 8c06eeff..7b225d5a 100644 --- a/cmd/esc/cli/env_open.go +++ b/cmd/esc/cli/env_open.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "strings" "time" "github.com/pulumi/esc" @@ -78,7 +79,7 @@ func newEnvOpenCmd(envcmd *envCommand) *cobra.Command { "the lifetime of the opened environment in the form HhMm (e.g. 2h, 1h30m, 15m)") cmd.Flags().StringVarP( &format, "format", "f", "json", - "the output format to use. May be 'dotenv', 'json', 'yaml', 'detailed', or 'shell'") + "the output format to use. May be 'dotenv', 'json', 'yaml', 'detailed', 'shell' or 'string'") return cmd } @@ -138,7 +139,12 @@ func (env *envCommand) renderValue( } return nil case "string": - fmt.Fprintf(out, "%v\n", val.ToString(!showSecrets)) + s := val.ToString(!showSecrets) + if strings.HasSuffix(s, "\n") { + fmt.Fprintf(out, "%v", s) + } else { + fmt.Fprintf(out, "%v\n", s) + } return nil default: // NOTE: we shouldn't get here. This was checked at the beginning of the function. diff --git a/cmd/esc/cli/env_set.go b/cmd/esc/cli/env_set.go index 6caf57e8..04ae01e2 100644 --- a/cmd/esc/cli/env_set.go +++ b/cmd/esc/cli/env_set.go @@ -5,9 +5,11 @@ package cli import ( "context" "fmt" + "io" "regexp" "strconv" "strings" + "unicode/utf8" "github.com/ccojocar/zxcvbn-go" "github.com/spf13/cobra" @@ -23,10 +25,11 @@ func newEnvSetCmd(env *envCommand) *cobra.Command { var plaintext bool var rawString bool var draft bool + var file string cmd := &cobra.Command{ Use: "set [/][/] ", - Args: cobra.RangeArgs(2, 3), + Args: cobra.RangeArgs(1, 3), Short: "Set a value within an environment.", Long: "Set a value within an environment\n" + "\n" + @@ -48,19 +51,48 @@ func newEnvSetCmd(env *envCommand) *cobra.Command { if ref.version != "" { return fmt.Errorf("the set command does not accept versions") } - if len(args) < 2 { + + switch { + case file == "" && len(args) < 2: return fmt.Errorf("expected a path and a value") + case file != "" && len(args) < 1: + return fmt.Errorf("expected a path") } path, err := resource.ParsePropertyPath(args[0]) if err != nil { + return fmt.Errorf("invalid path: %w", err) } if len(path) == 0 { return fmt.Errorf("path must contain at least one element") } - input := args[1] + var input string + if file != "" { + var content []byte + switch file { + case "-": + content, err = io.ReadAll(env.esc.stdin) + if err != nil { + return fmt.Errorf("could not read from stdin: %w", err) + } + default: + content, err = env.esc.fs.ReadFile(file) + if err != nil { + return fmt.Errorf("could not read file: %w", err) + } + } + + if !utf8.Valid(content) { + return fmt.Errorf("file content must be valid UTF-8") + } + + input = string(content) + } else { + input = args[1] + } + var yamlValue yaml.Node if rawString { yamlValue.SetString(input) @@ -163,6 +195,7 @@ func newEnvSetCmd(env *envCommand) *cobra.Command { cmd.Flags().BoolVar( &rawString, "string", false, "true to treat the value as a string rather than attempting to parse it as YAML") + cmd.Flags().StringVarP(&file, "file", "f", "", "If set, the value is read from the specified file. Pass `-` to read from standard input.") cmd.Flags().BoolVar( &draft, "draft", false, "true to create a draft rather than saving changes directly, returns a submitted Change Request ID and its URL") diff --git a/cmd/esc/cli/fs.go b/cmd/esc/cli/fs.go index b16df08c..d157b13c 100644 --- a/cmd/esc/cli/fs.go +++ b/cmd/esc/cli/fs.go @@ -15,6 +15,7 @@ type escFS interface { fs.FS CreateTemp(dir, pattern string) (string, io.ReadWriteCloser, error) + ReadFile(filename string) ([]byte, error) Remove(name string) error } @@ -41,3 +42,7 @@ func (defaultFS) CreateTemp(dir, pattern string) (string, io.ReadWriteCloser, er func (defaultFS) Remove(name string) error { return os.Remove(name) } + +func (defaultFS) ReadFile(name string) ([]byte, error) { + return os.ReadFile(name) +} diff --git a/cmd/esc/cli/testdata/env-set-file.yaml b/cmd/esc/cli/testdata/env-set-file.yaml new file mode 100644 index 00000000..2f1ba0a6 --- /dev/null +++ b/cmd/esc/cli/testdata/env-set-file.yaml @@ -0,0 +1,104 @@ +run: | + echo "hello" | esc env set default/test foo -f=- + esc env get default/test + echo "hello world" >test.file + esc env set default/test foo -f=test.file + esc env get default/test + esc env get default/test --value string + esc env set default/test foo -f=test.file --string + esc env get default/test + esc env set default/test -f=test.file || echo -n "" + esc env set -f=test.file || echo -n "" + esc env set default/test -f=binary foo || echo -n "" + esc env set -f=- default/test foo esc env set default/test foo -f=- +> esc env get default/test +# Value +```json +{ + "foo": "hello" +} +``` +# Definition +```yaml +values: + foo: hello + +``` + +> esc env set default/test foo -f=test.file +> esc env get default/test +# Value +```json +{ + "foo": "hello world" +} +``` +# Definition +```yaml +values: + foo: hello world + +``` + +> esc env get default/test --value string +"foo"="hello world" +> esc env set default/test foo -f=test.file --string +> esc env get default/test +# Value +```json +{ + "foo": "hello world\n" +} +``` +# Definition +```yaml +values: + foo: | + hello world + +``` + +> esc env set default/test -f=test.file +> esc env set -f=test.file +> esc env set default/test -f=binary foo +> esc env set -f=- default/test foo +> esc env set --string -f=multiline default/test foo +> esc env get default/test foo --value string +this is a +multiline +file + +--- +> esc env set default/test foo -f=- +> esc env get default/test +> esc env set default/test foo -f=test.file +> esc env get default/test +> esc env get default/test --value string +> esc env set default/test foo -f=test.file --string +> esc env get default/test +> esc env set default/test -f=test.file +Error: expected a path +> esc env set -f=test.file +Error: accepts between 1 and 3 arg(s), received 0 +> esc env set default/test -f=binary foo +Error: file content must be valid UTF-8 +> esc env set -f=- default/test foo +Error: file content must be valid UTF-8 +> esc env set --string -f=multiline default/test foo +> esc env get default/test foo --value string