Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions cmd/esc/cli/env_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
package cli

import (
"bufio"
"context"
"fmt"
"io"
"os"
"regexp"
"strconv"
"strings"
"unicode/utf8"

"github.com/ccojocar/zxcvbn-go"
"github.com/spf13/cobra"
"golang.org/x/term"
"gopkg.in/yaml.v3"

"github.com/pulumi/esc/syntax/encoding"
Expand All @@ -28,14 +31,17 @@ func newEnvSetCmd(env *envCommand) *cobra.Command {
var file string

cmd := &cobra.Command{
Use: "set [<org-name>/][<project-name>/]<environment-name> <path> <value>",
Use: "set [<org-name>/][<project-name>/]<environment-name> <path> [value]",
Args: cobra.RangeArgs(1, 3),
Short: "Set a value within an environment.",
Long: "Set a value within an environment\n" +
"\n" +
"This command fetches the current definition for the named environment and modifies a\n" +
"value within it. The path to the value to set is a Pulumi property path. The value\n" +
"is interpreted as YAML.\n",
"is interpreted as YAML.\n" +
"\n" +
"When --secret is used and no value is provided, the CLI will interactively prompt\n" +
"for the value with masked input, keeping the secret out of shell history.\n",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
Expand All @@ -53,7 +59,10 @@ func newEnvSetCmd(env *envCommand) *cobra.Command {
}

switch {
case file == "" && len(args) < 2:
case file == "" && len(args) < 2 && !secret:
return fmt.Errorf("expected a path and a value")
// when --secret is passed without a path
case file == "" && len(args) < 1:
return fmt.Errorf("expected a path and a value")
case file != "" && len(args) < 1:
return fmt.Errorf("expected a path")
Expand Down Expand Up @@ -89,6 +98,12 @@ func newEnvSetCmd(env *envCommand) *cobra.Command {
}

input = string(content)
} else if len(args) < 2 && secret {
// When --secret is used without a value argument, prompt interactively.
input, err = readSecret(env.esc.stdin, env.esc.stderr)
if err != nil {
return err
}
} else {
input = args[1]
}
Expand Down Expand Up @@ -117,7 +132,7 @@ func newEnvSetCmd(env *envCommand) *cobra.Command {
}
if secret {
if yamlValue.Kind == yaml.ScalarNode && yamlValue.Tag != "!!str" {
err = yaml.Unmarshal([]byte(strconv.Quote(args[1])), &yamlValue)
err = yaml.Unmarshal([]byte(strconv.Quote(input)), &yamlValue)
if err != nil {
return fmt.Errorf("internal error decoding value; try surrounding the argument in both single and double quotes (e.g. '\"foo\"') (%w)", err)
}
Expand Down Expand Up @@ -214,6 +229,28 @@ func newEnvSetCmd(env *envCommand) *cobra.Command {
return cmd
}

// readSecret prompts the user for a secret value. If stdin is a terminal, it reads
// without echoing the input. Otherwise, it reads a line from stdin.
func readSecret(stdin io.Reader, stderr io.Writer) (string, error) {
if f, ok := stdin.(*os.File); ok && term.IsTerminal(int(f.Fd())) {
fmt.Fprint(stderr, "Enter your secret value: ")
secret, err := term.ReadPassword(int(f.Fd()))
fmt.Fprintln(stderr)
if err != nil {
return "", fmt.Errorf("reading secret: %w", err)
}
return string(secret), nil
}

// Non-interactive: read a single line from stdin.
reader := bufio.NewReader(stdin)
line, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
return "", fmt.Errorf("reading secret from stdin: %w", err)
}
return strings.TrimSuffix(line, "\n"), nil
}

// 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")
Expand Down
52 changes: 52 additions & 0 deletions cmd/esc/cli/testdata/env-set-secret-prompt.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
run: |
esc env init default/test
echo "my-secret-value" | esc env set default/test password --secret
esc env get default/test --show-secrets
echo "1234" | esc env set default/test token --secret
esc env get default/test --show-secrets

---
> esc env init default/test
Environment created: test-user/default/test
> esc env set default/test password --secret
> esc env get default/test --show-secrets
# Value
```json
{
"password": "my-secret-value"
}
```
# Definition
```yaml
values:
password:
fn::secret: my-secret-value

```

> esc env set default/test token --secret
> esc env get default/test --show-secrets
# Value
```json
{
"password": "my-secret-value",
"token": "1234"
}
```
# Definition
```yaml
values:
password:
fn::secret: my-secret-value
token:
fn::secret: '1234'

```


---
> esc env init default/test
> esc env set default/test password --secret
> esc env get default/test --show-secrets
> esc env set default/test token --secret
> esc env get default/test --show-secrets
Loading