Skip to content
Merged
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
1 change: 0 additions & 1 deletion .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,3 @@ jobs:
version: latest
args: --timeout 4m
only-new-issues: true

2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

### Bug fixes

- fix(config): secret key no longer shown in plain text during `exo config add` #810

- fix(nlb): API error swallowed on load-balancer update (e.g. duplicate name conflict reported as "operation is nil") #806
- fix(config): panic when used without a default account set #798

Expand Down
42 changes: 40 additions & 2 deletions cmd/config/config_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import (
"fmt"
"io"
"os"
"os/signal"
"strings"

"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
"golang.org/x/term"

exocmd "github.com/exoscale/cli/cmd"
"github.com/exoscale/cli/pkg/account"
Expand Down Expand Up @@ -128,6 +130,36 @@ func addConfigAccount(firstRun bool) error {
return saveConfig(filePath, &config)
}

// readPasswordInterruptible reads a password from the terminal (no echo) while
// catching SIGINT (Ctrl+C). term.ReadPassword enables ISIG on the fd, which
// would otherwise deliver SIGINT directly to the process and kill it before any
// cancellation message can be printed. By intercepting the signal we can exit
// gracefully with the expected "Error: Operation Cancelled" output.
func readPasswordInterruptible() ([]byte, error) {
fd := int(os.Stdin.Fd())

sigCh := make(chan os.Signal, 1)
doneCh := make(chan struct{})
signal.Notify(sigCh, os.Interrupt)

go func() {
select {
case _, ok := <-sigCh:
if ok {
fmt.Println()
fmt.Fprintln(os.Stderr, "Error: Operation Cancelled")
os.Exit(exocmd.ExitCodeInterrupt)
}
case <-doneCh:
}
}()

b, err := term.ReadPassword(fd)
signal.Stop(sigCh)
close(doneCh)
return b, err
}

// readInputWithContext reads a line from stdin with context cancellation support.
// Returns io.EOF if Ctrl+C or Ctrl+D is pressed, allowing graceful cancellation.
// Silent exit behavior matches promptui.Select's interrupt handling.
Expand Down Expand Up @@ -181,16 +213,22 @@ func promptAccountInformation() (*account.Account, error) {
account.Key = apiKey

// Prompt for Secret Key with validation
secretKey, err := readInputWithContext(ctx, reader, "Secret Key")
fmt.Printf("[+] Secret Key: ") //nolint:errcheck
secretKeyBytes, err := readPasswordInterruptible()
fmt.Println() //nolint:errcheck
if err != nil {
return nil, err
}
secretKey := strings.TrimSpace(string(secretKeyBytes))
for secretKey == "" {
fmt.Println("Secret Key cannot be empty")
secretKey, err = readInputWithContext(ctx, reader, "Secret Key")
fmt.Printf("[+] Secret Key: ") //nolint:errcheck
secretKeyBytes, err = readPasswordInterruptible()
fmt.Println() //nolint:errcheck
if err != nil {
return nil, err
}
secretKey = strings.TrimSpace(string(secretKeyBytes))
}
account.Secret = secretKey

Expand Down
Loading