Skip to content
Open
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
10 changes: 8 additions & 2 deletions pkg/karmadactl/addons/init/disable_option.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,14 @@ func (o *CommandAddonsDisableOption) Validate(args []string) error {
// Run start disable Karmada addons
func (o *CommandAddonsDisableOption) Run(args []string) error {
fmt.Printf("Disable Karmada addon %s\n", args)
if !o.Force && !cmdutil.DeleteConfirmation() {
return nil
if !o.Force {
confirmed, err := cmdutil.DeleteConfirmation()
if err != nil {
return fmt.Errorf("failed to get user confirmation: %w", err)
}
if !confirmed {
return nil
}
}

var disableAddons = map[string]*Addon{}
Expand Down
12 changes: 9 additions & 3 deletions pkg/karmadactl/deinit/deinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,15 @@ func removeLabels(node *corev1.Node, removesLabel string) {
// Run start delete
func (o *CommandDeInitOption) Run() error {
fmt.Println("removes Karmada from Kubernetes")
// delete confirmation,exit the delete action when false.
if !o.Force && !util.DeleteConfirmation() {
return nil
// delete confirmation, exit the delete action when user declines or input fails.
if !o.Force {
confirmed, err := util.DeleteConfirmation()
if err != nil {
return fmt.Errorf("failed to get user confirmation: %w", err)
}
if !confirmed {
return nil
}
}

if err := o.delete(); err != nil {
Expand Down
36 changes: 19 additions & 17 deletions pkg/karmadactl/util/deleteconfirmation.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,28 @@ package util

import (
"fmt"
"os"
"strings"
)

// DeleteConfirmation delete karmada resource confirmation
func DeleteConfirmation() bool {
fmt.Print("Please type (y)es or (n)o and then press enter:")
var response string
_, err := fmt.Scanln(&response)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// DeleteConfirmation prompts the user for a yes/no confirmation.
// It returns true if the user confirms with "y" or "yes", false on "n" or "no",
// and an error if stdin cannot be read (e.g. non-interactive environment).
func DeleteConfirmation() (bool, error) {
for {
fmt.Print("Please type (y)es or (n)o and then press enter:")
var response string
_, err := fmt.Scanln(&response)
if err != nil {
return false, fmt.Errorf("failed to read user confirmation: %w", err)
}
Comment on lines +29 to +34

switch strings.ToLower(response) {
case "y", "yes":
return true
case "n", "no":
return false
default:
return DeleteConfirmation()
switch strings.ToLower(response) {
case "y", "yes":
return true, nil
case "n", "no":
return false, nil
default:
fmt.Println("invalid input, please type (y)es or (n)o")
}
}
}
Comment on lines 19 to 45
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation using fmt.Scanln is fragile for interactive CLI usage. It returns an error (such as "unexpected newline") if the user simply presses Enter, which causes the command to terminate immediately instead of re-prompting. It also fails if the user enters multiple words.

Using bufio.Scanner to read the entire line is more robust. It allows handling empty lines gracefully by continuing the loop and ensures that any extra input on the line is consumed.

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

// DeleteConfirmation prompts the user for a yes/no confirmation.
// It returns true if the user confirms with "y" or "yes", false on "n" or "no",
// and an error if stdin cannot be read (e.g. non-interactive environment).
func DeleteConfirmation() (bool, error) {
	scanner := bufio.NewScanner(os.Stdin)
	for {
		fmt.Print("Please type (y)es or (n)o and then press enter: ")
		if !scanner.Scan() {
			if err := scanner.Err(); err != nil {
				return false, fmt.Errorf("failed to read user confirmation: %w", err)
			}
			return false, fmt.Errorf("failed to read user confirmation: unexpected EOF")
		}

		response := strings.TrimSpace(scanner.Text())
		switch strings.ToLower(response) {
		case "y", "yes":
			return true, nil
		case "n", "no":
			return false, nil
		case "":
			continue
		default:
			fmt.Println("invalid input, please type (y)es or (n)o")
		}
	}
}

124 changes: 124 additions & 0 deletions pkg/karmadactl/util/deleteconfirmation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
Copyright 2024 The Karmada Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package util

import (
"os"
"strings"
"testing"
)

// substituteStdin replaces os.Stdin with a pipe seeded with input and returns
// a cleanup func that restores the original stdin.
func substituteStdin(t *testing.T, input string) func() {
t.Helper()
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("failed to create pipe: %v", err)
}
if _, err = w.WriteString(input); err != nil {
t.Fatalf("failed to write to pipe: %v", err)
}
w.Close()

orig := os.Stdin
os.Stdin = r
return func() {
os.Stdin = orig
r.Close()
}
}

func TestDeleteConfirmation(t *testing.T) {
tests := []struct {
name string
input string
wantOk bool
wantErr bool
}{
{
name: "DeleteConfirmation_WithYes_ReturnsTrue",
input: "yes\n",
wantOk: true,
wantErr: false,
},
{
name: "DeleteConfirmation_WithY_ReturnsTrue",
input: "y\n",
wantOk: true,
wantErr: false,
},
{
name: "DeleteConfirmation_WithNo_ReturnsFalse",
input: "no\n",
wantOk: false,
wantErr: false,
},
{
name: "DeleteConfirmation_WithN_ReturnsFalse",
input: "n\n",
wantOk: false,
wantErr: false,
},
{
name: "DeleteConfirmation_WithUppercaseYES_ReturnsTrue",
input: "YES\n",
wantOk: true,
wantErr: false,
},
{
name: "DeleteConfirmation_WithUppercaseNO_ReturnsFalse",
input: "NO\n",
wantOk: false,
wantErr: false,
},
{
name: "DeleteConfirmation_WithEmptyInput_ReturnsError",
input: "",
wantOk: false,
wantErr: true,
},
{
// Exercises the retry loop: first input is unrecognized,
// second input is valid. Relies on the iterative for-loop
// rather than recursion.
name: "DeleteConfirmation_WithInvalidThenYes_ReturnsTrue",
input: "maybe\nyes\n",
wantOk: true,
wantErr: false,
},
Comment on lines +89 to +103
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
restore := substituteStdin(t, tt.input)
defer restore()

got, err := DeleteConfirmation()
if (err != nil) != tt.wantErr {
t.Errorf("DeleteConfirmation() error = %v, wantErr = %v", err, tt.wantErr)
return
}
if !tt.wantErr && got != tt.wantOk {
t.Errorf("DeleteConfirmation() = %v, want %v", got, tt.wantOk)
}
if tt.wantErr && !strings.Contains(err.Error(), "failed to read user confirmation") {
t.Errorf("expected error to contain 'failed to read user confirmation', got: %v", err)
}
})
}
}
Loading