Skip to content

Commit

Permalink
fix: Move prompts handling into status handler
Browse files Browse the repository at this point in the history
This allows to have prompts while there are still status lines being
displayed.

Fixes: #37
  • Loading branch information
codablock committed May 30, 2022
1 parent 47cb42b commit 5d5f316
Show file tree
Hide file tree
Showing 19 changed files with 257 additions and 71 deletions.
9 changes: 5 additions & 4 deletions cmd/kluctl/commands/cmd_delete.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
package commands

import (
"context"
"fmt"
"github.com/kluctl/kluctl/v2/cmd/kluctl/args"
"github.com/kluctl/kluctl/v2/pkg/deployment"
"github.com/kluctl/kluctl/v2/pkg/deployment/commands"
"github.com/kluctl/kluctl/v2/pkg/deployment/utils"
"github.com/kluctl/kluctl/v2/pkg/k8s"
"github.com/kluctl/kluctl/v2/pkg/status"
"github.com/kluctl/kluctl/v2/pkg/types"
k8s2 "github.com/kluctl/kluctl/v2/pkg/types/k8s"
utils2 "github.com/kluctl/kluctl/v2/pkg/utils"
"os"
)

Expand Down Expand Up @@ -59,7 +60,7 @@ func (cmd *deleteCmd) Run() error {
if err != nil {
return err
}
result, err := confirmedDeleteObjects(ctx.targetCtx.SharedContext.K, objects, cmd.DryRun, cmd.Yes)
result, err := confirmedDeleteObjects(ctx.ctx, ctx.targetCtx.SharedContext.K, objects, cmd.DryRun, cmd.Yes)
if err != nil {
return err
}
Expand All @@ -74,14 +75,14 @@ func (cmd *deleteCmd) Run() error {
})
}

func confirmedDeleteObjects(k *k8s.K8sCluster, refs []k8s2.ObjectRef, dryRun bool, forceYes bool) (*types.CommandResult, error) {
func confirmedDeleteObjects(ctx context.Context, k *k8s.K8sCluster, refs []k8s2.ObjectRef, dryRun bool, forceYes bool) (*types.CommandResult, error) {
if len(refs) != 0 {
_, _ = os.Stderr.WriteString("The following objects will be deleted:\n")
for _, ref := range refs {
_, _ = os.Stderr.WriteString(fmt.Sprintf(" %s\n", ref.String()))
}
if !forceYes && !dryRun {
if !utils2.AskForConfirmation(fmt.Sprintf("Do you really want to delete %d objects?", len(refs))) {
if !status.AskForConfirmation(ctx, fmt.Sprintf("Do you really want to delete %d objects?", len(refs))) {
return nil, fmt.Errorf("aborted")
}
}
Expand Down
10 changes: 3 additions & 7 deletions cmd/kluctl/commands/cmd_deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import (
"fmt"
"github.com/kluctl/kluctl/v2/cmd/kluctl/args"
"github.com/kluctl/kluctl/v2/pkg/deployment/commands"
"github.com/kluctl/kluctl/v2/pkg/status"
"github.com/kluctl/kluctl/v2/pkg/types"
"github.com/kluctl/kluctl/v2/pkg/utils"
"time"
)

type deployCmd struct {
Expand Down Expand Up @@ -78,9 +77,6 @@ func (cmd *deployCmd) runCmdDeploy(ctx *commandCtx) error {
}

func (cmd *deployCmd) diffResultCb(diffResult *types.CommandResult) error {
// workaround to ensure that progress has been completely written/updated
time.Sleep(130 * time.Millisecond)

err := outputCommandResult(nil, diffResult)
if err != nil {
return err
Expand All @@ -89,11 +85,11 @@ func (cmd *deployCmd) diffResultCb(diffResult *types.CommandResult) error {
return nil
}
if len(diffResult.Errors) != 0 {
if !utils.AskForConfirmation("\nThe diff resulted in errors, do you still want to proceed?") {
if !status.AskForConfirmation(cliCtx, "The diff resulted in errors, do you still want to proceed?") {
return fmt.Errorf("aborted")
}
} else {
if !utils.AskForConfirmation("\nThe diff succeeded, do you want to proceed?") {
if !status.AskForConfirmation(cliCtx, "The diff succeeded, do you want to proceed?") {
return fmt.Errorf("aborted")
}
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/kluctl/commands/cmd_downscale.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"fmt"
"github.com/kluctl/kluctl/v2/cmd/kluctl/args"
"github.com/kluctl/kluctl/v2/pkg/deployment/commands"
"github.com/kluctl/kluctl/v2/pkg/utils"
"github.com/kluctl/kluctl/v2/pkg/status"
)

type downscaleCmd struct {
Expand Down Expand Up @@ -37,7 +37,7 @@ func (cmd *downscaleCmd) Run() error {
}
return withProjectCommandContext(ptArgs, func(ctx *commandCtx) error {
if !cmd.Yes && !cmd.DryRun {
if !utils.AskForConfirmation(fmt.Sprintf("Do you really want to downscale on context/cluster %s?", ctx.targetCtx.ClusterContext)) {
if !status.AskForConfirmation(cliCtx, fmt.Sprintf("Do you really want to downscale on context/cluster %s?", ctx.targetCtx.ClusterContext)) {
return fmt.Errorf("aborted")
}
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/kluctl/commands/cmd_poke_images.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"fmt"
"github.com/kluctl/kluctl/v2/cmd/kluctl/args"
"github.com/kluctl/kluctl/v2/pkg/deployment/commands"
"github.com/kluctl/kluctl/v2/pkg/utils"
"github.com/kluctl/kluctl/v2/pkg/status"
)

type pokeImagesCmd struct {
Expand Down Expand Up @@ -37,7 +37,7 @@ func (cmd *pokeImagesCmd) Run() error {
}
return withProjectCommandContext(ptArgs, func(ctx *commandCtx) error {
if !cmd.Yes && !cmd.DryRun {
if !utils.AskForConfirmation(fmt.Sprintf("Do you really want to poke images to the context/cluster %s?", ctx.targetCtx.ClusterContext)) {
if !status.AskForConfirmation(cliCtx, fmt.Sprintf("Do you really want to poke images to the context/cluster %s?", ctx.targetCtx.ClusterContext)) {
return fmt.Errorf("aborted")
}
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/kluctl/commands/cmd_prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (cmd *pruneCmd) runCmdPrune(ctx *commandCtx) error {
if err != nil {
return err
}
result, err := confirmedDeleteObjects(ctx.targetCtx.SharedContext.K, objects, cmd.DryRun, cmd.Yes)
result, err := confirmedDeleteObjects(ctx.ctx, ctx.targetCtx.SharedContext.K, objects, cmd.DryRun, cmd.Yes)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/git/auth/list_auth_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (a *ListAuthProvider) BuildAuth(ctx context.Context, gitUrl git_url.GitUrl)
if err != nil {
status.Trace(ctx, "Failed to parse private key: %v", err)
} else {
a.HostKeyCallback = buildVerifyHostCallback(e.KnownHosts)
a.HostKeyCallback = buildVerifyHostCallback(ctx, e.KnownHosts)
return AuthMethodAndCA{
AuthMethod: a,
}
Expand Down
7 changes: 4 additions & 3 deletions pkg/git/auth/ssh_auth_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (a *sshDefaultIdentityAndAgent) ClientConfig() (*ssh.ClientConfig, error) {
User: a.user,
Auth: []ssh.AuthMethod{ssh.PublicKeysCallback(a.Signers)},
}
cc.HostKeyCallback = buildVerifyHostCallback(nil)
cc.HostKeyCallback = buildVerifyHostCallback(a.ctx, nil)
return cc, nil
}

Expand Down Expand Up @@ -148,13 +148,14 @@ func (k *deferredPassphraseKey) getPassphrase() ([]byte, error) {
return passphrase, nil
}

passphraseStr, err := utils.AskForPassword(fmt.Sprintf("Enter passphrase for key '%s'", k.path))
passphraseStr, err := status.AskForPassword(k.ctx, fmt.Sprintf("Enter passphrase for key '%s'", k.path))
if err != nil {
k.a.passphrases[k.path] = nil
return nil, err
}
passphrase = []byte(passphraseStr)
k.a.passphrases[k.path] = passphrase
return []byte(passphraseStr), nil
return passphrase, nil
}

func (k *deferredPassphraseKey) parse() {
Expand Down
14 changes: 8 additions & 6 deletions pkg/git/auth/ssh_known_hosts.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package auth

import (
"context"
"fmt"
"github.com/kluctl/kluctl/v2/pkg/git/auth/goph"
"github.com/kluctl/kluctl/v2/pkg/status"
"github.com/kluctl/kluctl/v2/pkg/utils"
"golang.org/x/crypto/ssh"
"net"
Expand All @@ -14,13 +16,13 @@ import (

var askHostMutex sync.Mutex

func buildVerifyHostCallback(knownHosts []byte) func(hostname string, remote net.Addr, key ssh.PublicKey) error {
func buildVerifyHostCallback(ctx context.Context, knownHosts []byte) func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return verifyHost(hostname, remote, key, knownHosts)
return verifyHost(ctx, hostname, remote, key, knownHosts)
}
}

func verifyHost(host string, remote net.Addr, key ssh.PublicKey, knownHosts []byte) error {
func verifyHost(ctx context.Context, host string, remote net.Addr, key ssh.PublicKey, knownHosts []byte) error {
// Ensure only one prompt happens at a time
askHostMutex.Lock()
defer askHostMutex.Unlock()
Expand Down Expand Up @@ -85,14 +87,14 @@ func verifyHost(host string, remote net.Addr, key ssh.PublicKey, knownHosts []by
return fmt.Errorf("host not found and SSH_KNOWN_HOSTS has been set")
}

if !askIsHostTrusted(host, key) {
if !askIsHostTrusted(ctx, host, key) {
return fmt.Errorf("aborted")
}

return goph.AddKnownHost(host, remote, key, "")
}

func askIsHostTrusted(host string, key ssh.PublicKey) bool {
func askIsHostTrusted(ctx context.Context, host string, key ssh.PublicKey) bool {
prompt := fmt.Sprintf("Unknown Host: %s\nFingerprint: %s\nWould you like to add it? ", host, ssh.FingerprintSHA256(key))
return utils.AskForConfirmation(prompt)
return status.AskForConfirmation(ctx, prompt)
}
6 changes: 6 additions & 0 deletions pkg/status/noop.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package status

import "fmt"

type NoopStatusHandler struct {
}

Expand Down Expand Up @@ -34,6 +36,10 @@ func (n NoopStatusHandler) PlainText(text string) {
func (n NoopStatusHandler) InfoFallback(message string) {
}

func (n NoopStatusHandler) Prompt(password bool, message string) (string, error) {
return "", fmt.Errorf("Prompt not implemented in NoopStatusHandler")
}

var _ StatusHandler = &NoopStatusHandler{}

func (n NoopStatusLine) SetTotal(total int) {
Expand Down
61 changes: 18 additions & 43 deletions pkg/utils/prompts.go → pkg/status/promts.go
Original file line number Diff line number Diff line change
@@ -1,98 +1,73 @@
package utils
package status

import (
"bufio"
"context"
"fmt"
"github.com/kluctl/kluctl/v2/pkg/utils"
"github.com/mattn/go-isatty"
"golang.org/x/term"
"os"
"strings"
"syscall"
)

func doWarn(f string, args ...any) {
_, _ = fmt.Fprintf(os.Stderr, f, args...)
}

// AskForConfirmation uses Scanln to parse user input. A user must type in "yes" or "no" and
// then press enter. It has fuzzy matching, so "y", "Y", "yes", "YES", and "Yes" all count as
// confirmations. If the input is not recognized, it will ask again. The function does not return
// until it gets a valid response from the user. Typically, you should use fmt to print out a question
// before calling askForConfirmation. E.g. fmt.Println("WARNING: Are you sure? (yes/no)")
func AskForConfirmation(prompt string) bool {
func AskForConfirmation(ctx context.Context, prompt string) bool {
if !isatty.IsTerminal(os.Stderr.Fd()) {
doWarn("Not a terminal, suppressed prompt: %s", prompt)
Warning(ctx, "Not a terminal, suppressed prompt: %s", prompt)
return false
}

_, err := os.Stderr.WriteString(prompt + " (y/N) ")
if err != nil {
panic(err)
}

var response string
_, err = fmt.Scanln(&response)
response, err := Prompt(ctx, false, prompt+" (y/N) ")
if err != nil {
return false
}
okayResponses := []string{"y", "Y", "yes", "Yes", "YES"}
nokayResponses := []string{"n", "N", "no", "No", "NO"}
if FindStrInSlice(okayResponses, response) != -1 {
if utils.FindStrInSlice(okayResponses, response) != -1 {
return true
} else if FindStrInSlice(nokayResponses, response) != -1 || response == "" {
} else if utils.FindStrInSlice(nokayResponses, response) != -1 || response == "" {
return false
} else {
fmt.Println("Please type yes or no and then press enter:")
return AskForConfirmation(prompt)
Warning(ctx, "Please type yes or no and then press enter!")
return AskForConfirmation(ctx, prompt)
}
}

func AskForPassword(prompt string) (string, error) {
func AskForPassword(ctx context.Context, prompt string) (string, error) {
if !isatty.IsTerminal(os.Stderr.Fd()) {
err := fmt.Errorf("not a terminal, suppressed credentials prompt: %s", prompt)
doWarn(err.Error())
return "", err
}

_, err := fmt.Fprintf(os.Stderr, "%s: ", prompt)
if err != nil {
Warning(ctx, err.Error())
return "", err
}

bytePassword, err := term.ReadPassword(int(syscall.Stdin))
_, _ = fmt.Fprintf(os.Stderr, "\n")
password, err := Prompt(ctx, true, fmt.Sprintf("%s: ", prompt))
if err != nil {
return "", err
}

password := string(bytePassword)
return strings.TrimSpace(password), nil
}

func AskForCredentials(prompt string) (string, string, error) {
func AskForCredentials(ctx context.Context, prompt string) (string, string, error) {
if !isatty.IsTerminal(os.Stderr.Fd()) {
err := fmt.Errorf("not a terminal, suppressed credentials prompt: %s", prompt)
doWarn(err.Error())
Warning(ctx, err.Error())
return "", "", err
}

reader := bufio.NewReader(os.Stdin)
Info(ctx, prompt)

_, err := fmt.Fprintf(os.Stderr, prompt+"\n")
username, err := Prompt(ctx, false, "Enter Username: ")
if err != nil {
return "", "", err
}

fmt.Fprint(os.Stderr, "Enter Username: ")
username, err := reader.ReadString('\n')
password, err := Prompt(ctx, true, "Enter Password: ")
if err != nil {
return "", "", err
}

password, err := AskForPassword("Enter Password")
if err != nil {
return "", "", err
}

return strings.TrimSpace(username), strings.TrimSpace(password), nil
}
27 changes: 27 additions & 0 deletions pkg/status/simple_status_handler.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package status

import (
"fmt"
"golang.org/x/term"
"os"
"syscall"
)

type simpleStatusHandler struct {
cb func(message string)
trace bool
Expand Down Expand Up @@ -55,6 +62,26 @@ func (s *simpleStatusHandler) InfoFallback(message string) {
s.Info(message)
}

func (s *simpleStatusHandler) Prompt(password bool, message string) (string, error) {
s.cb(message)

if password {
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
_, _ = fmt.Fprintf(os.Stderr, "\n")
if err != nil {
return "", err
}
return string(bytePassword), nil
} else {
var response string
_, err := fmt.Scanln(&response)
if err != nil {
return "", err
}
return response, nil
}
}

func (sl *simpleStatusLine) SetTotal(total int) {
}

Expand Down
7 changes: 7 additions & 0 deletions pkg/status/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ type StatusHandler interface {

PlainText(text string)
InfoFallback(message string)

Prompt(password bool, message string) (string, error)
}

type contextKey struct{}
Expand Down Expand Up @@ -233,3 +235,8 @@ func Error(ctx context.Context, status string, args ...any) {
slh := FromContext(ctx)
slh.Error(fmt.Sprintf(status, args...))
}

func Prompt(ctx context.Context, password bool, message string, args ...any) (string, error) {
slh := FromContext(ctx)
return slh.Prompt(password, fmt.Sprintf(message, args...))
}

0 comments on commit 5d5f316

Please sign in to comment.