1
0
mirror of https://github.com/juanfont/headscale.git synced 2026-02-07 20:04:00 +01:00

cmd/headscale/cli: add confirmAction helper for force/prompt patterns

Centralise the repeated force-flag-check + YesNo-prompt logic into a
single confirmAction(cmd, prompt) helper.  Callers still decide what
to return on decline (error, message, or nil).
This commit is contained in:
Kristoffer Dalby 2026-02-18 14:51:42 +00:00
parent b049b9f6c4
commit d4882758bf
4 changed files with 36 additions and 130 deletions

View File

@ -262,32 +262,23 @@ var deleteNodeCmd = &cobra.Command{
NodeId: identifier,
}
confirm := false
force, _ := cmd.Flags().GetBool("force")
if !force {
confirm = util.YesNo(fmt.Sprintf(
"Do you want to remove the node %s?",
getResponse.GetNode().GetName(),
))
if !confirmAction(cmd, fmt.Sprintf(
"Do you want to remove the node %s?",
getResponse.GetNode().GetName(),
)) {
return printOutput(cmd, map[string]string{"Result": "Node not deleted"}, "Node not deleted")
}
if confirm || force {
response, err := client.DeleteNode(ctx, deleteRequest)
if err != nil {
return fmt.Errorf("deleting node: %w", err)
}
_ = response // consumed for structured output if needed
return printOutput(
cmd,
map[string]string{"Result": "Node deleted"},
"Node deleted",
)
_, err = client.DeleteNode(ctx, deleteRequest)
if err != nil {
return fmt.Errorf("deleting node: %w", err)
}
return printOutput(cmd, map[string]string{"Result": "Node not deleted"}, "Node not deleted")
return printOutput(
cmd,
map[string]string{"Result": "Node deleted"},
"Node deleted",
)
}),
}
@ -307,14 +298,7 @@ If you remove IPv4 or IPv6 prefixes from the config,
it can be run to remove the IPs that should no longer
be assigned to nodes.`,
RunE: func(cmd *cobra.Command, args []string) error {
confirm := false
force, _ := cmd.Flags().GetBool("force")
if !force {
confirm = util.YesNo("Are you sure that you want to assign/remove IPs to/from nodes?")
}
if !confirm && !force {
if !confirmAction(cmd, "Are you sure that you want to assign/remove IPs to/from nodes?") {
return nil
}

View File

@ -10,7 +10,6 @@ import (
"github.com/juanfont/headscale/hscontrol/db"
"github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/spf13/cobra"
"tailscale.com/types/views"
)
@ -49,87 +48,7 @@ var getPolicy = &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
var policyData string
if bypass, _ := cmd.Flags().GetBool(bypassFlag); bypass {
confirm := false
force, _ := cmd.Flags().GetBool("force")
if !force {
confirm = util.YesNo("DO NOT run this command if an instance of headscale is running, are you sure headscale is not running?")
}
if !confirm && !force {
return errAborted
}
cfg, err := types.LoadServerConfig()
if err != nil {
return fmt.Errorf("loading config: %w", err)
}
d, err := db.NewHeadscaleDatabase(cfg, nil)
if err != nil {
return fmt.Errorf("opening database: %w", err)
}
pol, err := d.GetPolicy()
if err != nil {
return fmt.Errorf("loading policy from database: %w", err)
}
policyData = pol.Data
} else {
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
if err != nil {
return fmt.Errorf("connecting to headscale: %w", err)
}
defer cancel()
defer conn.Close()
response, err := client.GetPolicy(ctx, &v1.GetPolicyRequest{})
if err != nil {
return fmt.Errorf("loading ACL policy: %w", err)
}
policyData = response.GetPolicy()
}
// This does not pass output format as we don't support yaml, json or
// json-line output for this command. It is HuJSON already.
fmt.Println(policyData)
return nil
},
}
var setPolicy = &cobra.Command{
Use: "set",
Short: "Updates the ACL Policy",
Long: `
Updates the existing ACL Policy with the provided policy. The policy must be a valid HuJSON object.
This command only works when the acl.policy_mode is set to "db", and the policy will be stored in the database.`,
Aliases: []string{"put", "update"},
RunE: func(cmd *cobra.Command, args []string) error {
policyPath, _ := cmd.Flags().GetString("file")
f, err := os.Open(policyPath)
if err != nil {
return fmt.Errorf("opening policy file: %w", err)
}
defer f.Close()
policyBytes, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("reading policy file: %w", err)
}
if bypass, _ := cmd.Flags().GetBool(bypassFlag); bypass {
confirm := false
force, _ := cmd.Flags().GetBool("force")
if !force {
confirm = util.YesNo("DO NOT run this command if an instance of headscale is running, are you sure headscale is not running?")
}
if !confirm && !force {
if !confirmAction(cmd, "DO NOT run this command if an instance of headscale is running, are you sure headscale is not running?") {
return errAborted
}

View File

@ -8,7 +8,6 @@ import (
"strconv"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
"github.com/pterm/pterm"
"github.com/rs/zerolog/log"
@ -140,28 +139,21 @@ var destroyUserCmd = &cobra.Command{
user := users.GetUsers()[0]
confirm := false
force, _ := cmd.Flags().GetBool("force")
if !force {
confirm = util.YesNo(fmt.Sprintf(
"Do you want to remove the user %q (%d) and any associated preauthkeys?",
user.GetName(), user.GetId(),
))
if !confirmAction(cmd, fmt.Sprintf(
"Do you want to remove the user %q (%d) and any associated preauthkeys?",
user.GetName(), user.GetId(),
)) {
return printOutput(cmd, map[string]string{"Result": "User not destroyed"}, "User not destroyed")
}
if confirm || force {
request := &v1.DeleteUserRequest{Id: user.GetId()}
deleteRequest := &v1.DeleteUserRequest{Id: user.GetId()}
response, err := client.DeleteUser(ctx, request)
if err != nil {
return fmt.Errorf("destroying user: %w", err)
}
return printOutput(cmd, response, "User destroyed")
response, err := client.DeleteUser(ctx, deleteRequest)
if err != nil {
return fmt.Errorf("destroying user: %w", err)
}
return printOutput(cmd, map[string]string{"Result": "User not destroyed"}, "User not destroyed")
return printOutput(cmd, response, "User destroyed")
}),
}

View File

@ -221,6 +221,17 @@ func printOutput(cmd *cobra.Command, result any, override string) error {
return nil
}
// confirmAction returns true when the user confirms a prompt, or when
// --force is set. Callers decide what to do when it returns false.
func confirmAction(cmd *cobra.Command, prompt string) bool {
force, _ := cmd.Flags().GetBool("force")
if force {
return true
}
return util.YesNo(prompt)
}
// printListOutput checks the --output flag: when a machine-readable format is
// requested it serialises data as JSON/YAML; otherwise it calls renderTable
// to produce the human-readable pterm table.