diff --git a/cmd/esc/cli/env_set.go b/cmd/esc/cli/env_set.go index 626eb3b5..f3552dac 100644 --- a/cmd/esc/cli/env_set.go +++ b/cmd/esc/cli/env_set.go @@ -5,21 +5,23 @@ package cli import ( "context" "fmt" - "regexp" - "strconv" - "github.com/ccojocar/zxcvbn-go" - "github.com/spf13/cobra" - "gopkg.in/yaml.v3" - "github.com/pulumi/esc/syntax/encoding" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + "io" + "os" + "regexp" + "strconv" ) func newEnvSetCmd(env *envCommand) *cobra.Command { var secret bool var plaintext bool + var filename string + var inputYaml bool cmd := &cobra.Command{ Use: "set [/][/] ", @@ -45,7 +47,12 @@ func newEnvSetCmd(env *envCommand) *cobra.Command { if ref.version != "" { return fmt.Errorf("the set command does not accept versions") } - if len(args) < 2 { + reqArgs := 2 + if filename != "" { + // if filename is provided, we only need a path + reqArgs = 1 + } + if len(args) < reqArgs { return fmt.Errorf("expected a path and a value") } @@ -57,17 +64,32 @@ func newEnvSetCmd(env *envCommand) *cobra.Command { return fmt.Errorf("path must contain at least one element") } - var yamlValue yaml.Node - if err := yaml.Unmarshal([]byte(args[1]), &yamlValue); err != nil { - return fmt.Errorf("invalid value: %w", err) + var value []byte + if filename == "" { + value = []byte(args[1]) + } else { + value, err = readFileInput(filename) + if err != nil { + return err + } } - if len(yamlValue.Content) == 0 { - // This can happen when the value is empty (e.g. when "" is present on the command line). Treat this - // as the empty string. - err = yaml.Unmarshal([]byte(`""`), &yamlValue) - contract.IgnoreError(err) + + var yamlValue yaml.Node + if inputYaml { + if err := yaml.Unmarshal(value, &yamlValue); err != nil { + return fmt.Errorf("invalid value: %w", err) + } + if len(yamlValue.Content) == 0 { + // This can happen when the value is empty (e.g. when "" is present on the command line). Treat this + // as the empty string. + err = yaml.Unmarshal([]byte(`""`), &yamlValue) + contract.IgnoreError(err) + } + yamlValue = *yamlValue.Content[0] + } else { + // treat input as a raw string + yamlValue.SetString(string(value)) } - yamlValue = *yamlValue.Content[0] if looksLikeSecret(path, yamlValue) && !secret && !plaintext { return fmt.Errorf("value looks like a secret; rerun with --secret to mark it as such, or --plaintext if you meant to leave it as plaintext") @@ -147,10 +169,30 @@ func newEnvSetCmd(env *envCommand) *cobra.Command { cmd.Flags().BoolVar( &plaintext, "plaintext", false, "true to leave the value in plaintext") + cmd.Flags().StringVar( + &filename, "file", "", "read value from file. use `-` to read from stdin.") + cmd.Flags().BoolVar( + &inputYaml, "yaml", true, "treat value as a structured yaml node.") return cmd } +// readFileInput reads the full content of filename, or stdin if filename is `-` +func readFileInput(filename string) (input []byte, err error) { + if filename == "-" { + input, err = io.ReadAll(os.Stdin) + if err != nil { + return nil, fmt.Errorf("reading stdin: %w", err) + } + } else { + input, err = os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("reading file: %w", err) + } + } + return input, err +} + // keyPattern is the regular expression a configuration key must match before we check (and error) if we think // it is a password var keyPattern = regexp.MustCompile("(?i)passwd|pass|password|pwd|secret|token") diff --git a/syntax/encoding/yaml.go b/syntax/encoding/yaml.go index abc0cb65..ef43b2fa 100644 --- a/syntax/encoding/yaml.go +++ b/syntax/encoding/yaml.go @@ -136,6 +136,7 @@ func (s YAMLSyntax) Set(prefix, path resource.PropertyPath, new yaml.Node) (*yam s.Kind = new.Kind s.Tag = new.Tag s.Value = new.Value + s.Style = new.Style return s.Node, nil }