From d4882758bf0e518fd15eecd14aea3f23ae09831f Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 18 Feb 2026 14:51:42 +0000 Subject: [PATCH] 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). --- cmd/headscale/cli/nodes.go | 44 +++++++------------- cmd/headscale/cli/policy.go | 83 +------------------------------------ cmd/headscale/cli/users.go | 28 +++++-------- cmd/headscale/cli/utils.go | 11 +++++ 4 files changed, 36 insertions(+), 130 deletions(-) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 878144c2..632095d9 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -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 } diff --git a/cmd/headscale/cli/policy.go b/cmd/headscale/cli/policy.go index 1708eec2..270b75db 100644 --- a/cmd/headscale/cli/policy.go +++ b/cmd/headscale/cli/policy.go @@ -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 } diff --git a/cmd/headscale/cli/users.go b/cmd/headscale/cli/users.go index ea8b1b24..3210fa26 100644 --- a/cmd/headscale/cli/users.go +++ b/cmd/headscale/cli/users.go @@ -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") }), } diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 52f47215..4857a4f6 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -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.