From e4fe216e45037878ce2700387991c9e72b8466c0 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 18 Feb 2026 13:44:35 +0000 Subject: [PATCH] cmd/headscale/cli: switch to RunE with grpcRunE and error returns Rename grpcRun to grpcRunE: the inner closure now returns error and the wrapper returns a cobra RunE-compatible function. Change newHeadscaleCLIWithConfig to return an error instead of calling log.Fatal/os.Exit, making connection failures propagate through the normal error path. Add formatOutput (returns error) and printOutput (writes to stdout) as non-exiting replacements for the old output/SuccessOutput pair. Extract output format string literals into package-level constants. Mark the old ErrorOutput, SuccessOutput and output helpers as deprecated; they remain temporarily for the unconverted commands. Convert all 22 grpcRunE commands from Run+ErrorOutput+SuccessOutput to RunE+fmt.Errorf+printOutput. Change usernameAndIDFromFlag to return an error instead of calling ErrorOutput directly. Update backfillNodeIPsCmd and policy.go callers of newHeadscaleCLIWithConfig for the new 5-return signature while keeping their Run-based pattern for now. --- cmd/headscale/cli/api_key.go | 88 +++------- cmd/headscale/cli/debug.go | 39 +---- cmd/headscale/cli/health.go | 9 +- cmd/headscale/cli/nodes.go | 284 +++++++++---------------------- cmd/headscale/cli/policy.go | 10 +- cmd/headscale/cli/preauthkeys.go | 80 +++------ cmd/headscale/cli/users.go | 123 +++++-------- cmd/headscale/cli/utils.go | 137 +++++++++++---- 8 files changed, 287 insertions(+), 483 deletions(-) diff --git a/cmd/headscale/cli/api_key.go b/cmd/headscale/cli/api_key.go index 346ecf55..d867a444 100644 --- a/cmd/headscale/cli/api_key.go +++ b/cmd/headscale/cli/api_key.go @@ -47,22 +47,18 @@ var listAPIKeys = &cobra.Command{ Use: "list", Short: "List the Api keys for headscale", Aliases: []string{"ls", "show"}, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { + format, _ := cmd.Flags().GetString("output") request := &v1.ListApiKeysRequest{} response, err := client.ListApiKeys(ctx, request) if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error getting the list of keys: %s", err), - output, - ) + return fmt.Errorf("listing api keys: %w", err) } - if output != "" { - SuccessOutput(response.GetApiKeys(), "", output) + if format != "" { + return printOutput(cmd, response.GetApiKeys(), "") } tableData := pterm.TableData{ @@ -86,12 +82,10 @@ var listAPIKeys = &cobra.Command{ err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Failed to render pterm table: %s", err), - output, - ) + return fmt.Errorf("rendering table: %w", err) } + + return nil }), } @@ -103,20 +97,14 @@ Creates a new Api key, the Api key is only visible on creation and cannot be retrieved again. If you loose a key, create a new one and revoke (expire) the old one.`, Aliases: []string{"c", "new"}, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") - + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { request := &v1.CreateApiKeyRequest{} durationStr, _ := cmd.Flags().GetString("expiration") duration, err := model.ParseDuration(durationStr) if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Could not parse duration: %s\n", err), - output, - ) + return fmt.Errorf("parsing duration: %w", err) } expiration := time.Now().UTC().Add(time.Duration(duration)) @@ -125,14 +113,10 @@ If you loose a key, create a new one and revoke (expire) the old one.`, response, err := client.CreateApiKey(ctx, request) if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Cannot create Api Key: %s\n", err), - output, - ) + return fmt.Errorf("creating api key: %w", err) } - SuccessOutput(response.GetApiKey(), response.GetApiKey(), output) + return printOutput(cmd, response.GetApiKey(), response.GetApiKey()) }), } @@ -140,25 +124,15 @@ var expireAPIKeyCmd = &cobra.Command{ Use: "expire", Short: "Expire an ApiKey", Aliases: []string{"revoke", "exp", "e"}, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") - + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { id, _ := cmd.Flags().GetUint64("id") prefix, _ := cmd.Flags().GetString("prefix") switch { case id == 0 && prefix == "": - ErrorOutput( - errMissingParameter, - "Either --id or --prefix must be provided", - output, - ) + return fmt.Errorf("either --id or --prefix must be provided: %w", errMissingParameter) case id != 0 && prefix != "": - ErrorOutput( - errMissingParameter, - "Only one of --id or --prefix can be provided", - output, - ) + return fmt.Errorf("only one of --id or --prefix can be provided: %w", errMissingParameter) } request := &v1.ExpireApiKeyRequest{} @@ -170,14 +144,10 @@ var expireAPIKeyCmd = &cobra.Command{ response, err := client.ExpireApiKey(ctx, request) if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Cannot expire Api Key: %s\n", err), - output, - ) + return fmt.Errorf("expiring api key: %w", err) } - SuccessOutput(response, "Key expired", output) + return printOutput(cmd, response, "Key expired") }), } @@ -185,25 +155,15 @@ var deleteAPIKeyCmd = &cobra.Command{ Use: "delete", Short: "Delete an ApiKey", Aliases: []string{"remove", "del"}, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") - + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { id, _ := cmd.Flags().GetUint64("id") prefix, _ := cmd.Flags().GetString("prefix") switch { case id == 0 && prefix == "": - ErrorOutput( - errMissingParameter, - "Either --id or --prefix must be provided", - output, - ) + return fmt.Errorf("either --id or --prefix must be provided: %w", errMissingParameter) case id != 0 && prefix != "": - ErrorOutput( - errMissingParameter, - "Only one of --id or --prefix can be provided", - output, - ) + return fmt.Errorf("only one of --id or --prefix can be provided: %w", errMissingParameter) } request := &v1.DeleteApiKeyRequest{} @@ -215,13 +175,9 @@ var deleteAPIKeyCmd = &cobra.Command{ response, err := client.DeleteApiKey(ctx, request) if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Cannot delete Api Key: %s\n", err), - output, - ) + return fmt.Errorf("deleting api key: %w", err) } - SuccessOutput(response, "Key deleted", output) + return printOutput(cmd, response, "Key deleted") }), } diff --git a/cmd/headscale/cli/debug.go b/cmd/headscale/cli/debug.go index 0e03457f..49a479f6 100644 --- a/cmd/headscale/cli/debug.go +++ b/cmd/headscale/cli/debug.go @@ -8,7 +8,6 @@ import ( "github.com/juanfont/headscale/hscontrol/types" "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "google.golang.org/grpc/status" ) // Error is used to compare errors as per https://dave.cheney.net/2016/04/07/constant-errors @@ -60,48 +59,30 @@ var debugCmd = &cobra.Command{ var createNodeCmd = &cobra.Command{ Use: "create-node", Short: "Create a node that can be registered with `nodes register <>` command", - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") - + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { user, err := cmd.Flags().GetString("user") if err != nil { - ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) + return fmt.Errorf("getting user flag: %w", err) } name, err := cmd.Flags().GetString("name") if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error getting node from flag: %s", err), - output, - ) + return fmt.Errorf("getting name flag: %w", err) } registrationID, err := cmd.Flags().GetString("key") if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error getting key from flag: %s", err), - output, - ) + return fmt.Errorf("getting key flag: %w", err) } _, err = types.RegistrationIDFromString(registrationID) if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Failed to parse machine key from flag: %s", err), - output, - ) + return fmt.Errorf("parsing machine key: %w", err) } routes, err := cmd.Flags().GetStringSlice("route") if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error getting routes from flag: %s", err), - output, - ) + return fmt.Errorf("getting routes flag: %w", err) } request := &v1.DebugCreateNodeRequest{ @@ -113,13 +94,9 @@ var createNodeCmd = &cobra.Command{ response, err := client.DebugCreateNode(ctx, request) if err != nil { - ErrorOutput( - err, - "Cannot create node: "+status.Convert(err).Message(), - output, - ) + return fmt.Errorf("creating node: %w", err) } - SuccessOutput(response.GetNode(), "Node created", output) + return printOutput(cmd, response.GetNode(), "Node created") }), } diff --git a/cmd/headscale/cli/health.go b/cmd/headscale/cli/health.go index 46769c00..b3b4f430 100644 --- a/cmd/headscale/cli/health.go +++ b/cmd/headscale/cli/health.go @@ -2,6 +2,7 @@ package cli import ( "context" + "fmt" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/spf13/cobra" @@ -15,14 +16,12 @@ var healthCmd = &cobra.Command{ Use: "health", Short: "Check the health of the Headscale server", Long: "Check the health of the Headscale server. This command will return an exit code of 0 if the server is healthy, or 1 if it is not.", - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") - + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { response, err := client.Health(ctx, &v1.HealthRequest{}) if err != nil { - ErrorOutput(err, "Error checking health", output) + return fmt.Errorf("checking health: %w", err) } - SuccessOutput(response, "", output) + return printOutput(cmd, response, "") }), } diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 624b1c29..b190eb03 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -104,21 +104,15 @@ var nodeCmd = &cobra.Command{ var registerNodeCmd = &cobra.Command{ Use: "register", Short: "Registers a node to your network", - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") - + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { user, err := cmd.Flags().GetString("user") if err != nil { - ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) + return fmt.Errorf("getting user flag: %w", err) } registrationID, err := cmd.Flags().GetString("key") if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error getting node key from flag: %s", err), - output, - ) + return fmt.Errorf("getting key flag: %w", err) } request := &v1.RegisterNodeRequest{ @@ -128,19 +122,13 @@ var registerNodeCmd = &cobra.Command{ response, err := client.RegisterNode(ctx, request) if err != nil { - ErrorOutput( - err, - fmt.Sprintf( - "Cannot register node: %s\n", - status.Convert(err).Message(), - ), - output, - ) + return fmt.Errorf("registering node: %w", err) } - SuccessOutput( + return printOutput( + cmd, response.GetNode(), - fmt.Sprintf("Node %s registered", response.GetNode().GetGivenName()), output) + fmt.Sprintf("Node %s registered", response.GetNode().GetGivenName())) }), } @@ -148,12 +136,11 @@ var listNodesCmd = &cobra.Command{ Use: "list", Short: "List nodes", Aliases: []string{"ls", "show"}, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") - + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { + format, _ := cmd.Flags().GetString("output") user, err := cmd.Flags().GetString("user") if err != nil { - ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) + return fmt.Errorf("getting user flag: %w", err) } request := &v1.ListNodesRequest{ @@ -162,30 +149,24 @@ var listNodesCmd = &cobra.Command{ response, err := client.ListNodes(ctx, request) if err != nil { - ErrorOutput( - err, - "Cannot get nodes: "+status.Convert(err).Message(), - output, - ) + return fmt.Errorf("listing nodes: %w", err) } - if output != "" { - SuccessOutput(response.GetNodes(), "", output) + if format != "" { + return printOutput(cmd, response.GetNodes(), "") } tableData, err := nodesToPtables(user, response.GetNodes()) if err != nil { - ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output) + return fmt.Errorf("converting to table: %w", err) } err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Failed to render pterm table: %s", err), - output, - ) + return fmt.Errorf("rendering table: %w", err) } + + return nil }), } @@ -193,27 +174,18 @@ var listNodeRoutesCmd = &cobra.Command{ Use: "list-routes", Short: "List routes available on nodes", Aliases: []string{"lsr", "routes"}, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") - + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { + format, _ := cmd.Flags().GetString("output") identifier, err := cmd.Flags().GetUint64("identifier") if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error converting ID to integer: %s", err), - output, - ) + return fmt.Errorf("getting identifier flag: %w", err) } request := &v1.ListNodesRequest{} response, err := client.ListNodes(ctx, request) if err != nil { - ErrorOutput( - err, - "Cannot get nodes: "+status.Convert(err).Message(), - output, - ) + return fmt.Errorf("listing nodes: %w", err) } nodes := response.GetNodes() @@ -221,6 +193,7 @@ var listNodeRoutesCmd = &cobra.Command{ for _, node := range response.GetNodes() { if node.GetId() == identifier { nodes = []*v1.Node{node} + break } } @@ -230,21 +203,18 @@ var listNodeRoutesCmd = &cobra.Command{ return (n.GetSubnetRoutes() != nil && len(n.GetSubnetRoutes()) > 0) || (n.GetApprovedRoutes() != nil && len(n.GetApprovedRoutes()) > 0) || (n.GetAvailableRoutes() != nil && len(n.GetAvailableRoutes()) > 0) }) - if output != "" { - SuccessOutput(nodes, "", output) - return + if format != "" { + return printOutput(cmd, nodes, "") } tableData := nodeRoutesToPtables(nodes) err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Failed to render pterm table: %s", err), - output, - ) + return fmt.Errorf("rendering table: %w", err) } + + return nil }), } @@ -253,27 +223,15 @@ var expireNodeCmd = &cobra.Command{ Short: "Expire (log out) a node in your network", Long: "Expiring a node will keep the node in the database and force it to reauthenticate.", Aliases: []string{"logout", "exp", "e"}, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") - + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { identifier, err := cmd.Flags().GetUint64("identifier") if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error converting ID to integer: %s", err), - output, - ) + return fmt.Errorf("getting identifier flag: %w", err) } expiry, err := cmd.Flags().GetString("expiry") if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error converting expiry to string: %s", err), - output, - ) - - return + return fmt.Errorf("getting expiry flag: %w", err) } now := time.Now() @@ -282,13 +240,7 @@ var expireNodeCmd = &cobra.Command{ if expiry != "" { expiryTime, err = time.Parse(time.RFC3339, expiry) if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error converting expiry to string: %s", err), - output, - ) - - return + return fmt.Errorf("parsing expiry time: %w", err) } } @@ -299,37 +251,24 @@ var expireNodeCmd = &cobra.Command{ response, err := client.ExpireNode(ctx, request) if err != nil { - ErrorOutput( - err, - fmt.Sprintf( - "Cannot expire node: %s\n", - status.Convert(err).Message(), - ), - output, - ) + return fmt.Errorf("expiring node: %w", err) } if now.Equal(expiryTime) || now.After(expiryTime) { - SuccessOutput(response.GetNode(), "Node expired", output) - } else { - SuccessOutput(response.GetNode(), "Node expiration updated", output) + return printOutput(cmd, response.GetNode(), "Node expired") } + + return printOutput(cmd, response.GetNode(), "Node expiration updated") }), } var renameNodeCmd = &cobra.Command{ Use: "rename NEW_NAME", Short: "Renames a node in your network", - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") - + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { identifier, err := cmd.Flags().GetUint64("identifier") if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error converting ID to integer: %s", err), - output, - ) + return fmt.Errorf("getting identifier flag: %w", err) } newName := "" @@ -344,17 +283,10 @@ var renameNodeCmd = &cobra.Command{ response, err := client.RenameNode(ctx, request) if err != nil { - ErrorOutput( - err, - fmt.Sprintf( - "Cannot rename node: %s\n", - status.Convert(err).Message(), - ), - output, - ) + return fmt.Errorf("renaming node: %w", err) } - SuccessOutput(response.GetNode(), "Node renamed", output) + return printOutput(cmd, response.GetNode(), "Node renamed") }), } @@ -362,16 +294,10 @@ var deleteNodeCmd = &cobra.Command{ Use: "delete", Short: "Delete a node", Aliases: []string{"del"}, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") - + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { identifier, err := cmd.Flags().GetUint64("identifier") if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error converting ID to integer: %s", err), - output, - ) + return fmt.Errorf("getting identifier flag: %w", err) } getRequest := &v1.GetNodeRequest{ @@ -380,11 +306,7 @@ var deleteNodeCmd = &cobra.Command{ getResponse, err := client.GetNode(ctx, getRequest) if err != nil { - ErrorOutput( - err, - "Error getting node node: "+status.Convert(err).Message(), - output, - ) + return fmt.Errorf("getting node: %w", err) } deleteRequest := &v1.DeleteNodeRequest{ @@ -403,28 +325,20 @@ var deleteNodeCmd = &cobra.Command{ if confirm || force { response, err := client.DeleteNode(ctx, deleteRequest) - if output != "" { - SuccessOutput(response, "", output) - - return - } - if err != nil { - ErrorOutput( - err, - "Error deleting node: "+status.Convert(err).Message(), - output, - ) + return fmt.Errorf("deleting node: %w", err) } - SuccessOutput( + _ = response // consumed for structured output if needed + + return printOutput( + cmd, map[string]string{"Result": "Node deleted"}, "Node deleted", - output, ) - } else { - SuccessOutput(map[string]string{"Result": "Node not deleted"}, "Node not deleted", output) } + + return printOutput(cmd, map[string]string{"Result": "Node not deleted"}, "Node not deleted") }), } @@ -454,7 +368,10 @@ be assigned to nodes.`, } if confirm || force { - ctx, client, conn, cancel := newHeadscaleCLIWithConfig() + ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig() + if err != nil { + ErrorOutput(err, fmt.Sprintf("Error connecting: %s", err), output) + } defer cancel() defer conn.Close() @@ -547,14 +464,12 @@ func nodesToPtables( } var expired string - if expiry.IsZero() || expiry.After(time.Now()) { - expired = pterm.LightGreen("no") - } else { + if node.GetExpiry() != nil && node.GetExpiry().AsTime().Before(time.Now()) { expired = pterm.LightRed("yes") + } else { + expired = pterm.LightGreen("no") } - // TODO(kradalby): as part of CLI rework, we should add the posibility to show "unusable" tags as mentioned in - // https://github.com/juanfont/headscale/issues/2981 var tagsBuilder strings.Builder for _, tag := range node.GetTags() { @@ -563,29 +478,26 @@ func nodesToPtables( tags := tagsBuilder.String() - tags = strings.TrimLeft(tags, "\n") - var user string - if currentUser == "" || (currentUser == node.GetUser().GetName()) { - user = pterm.LightMagenta(node.GetUser().GetName()) - } else { - // Shared into this user - user = pterm.LightYellow(node.GetUser().GetName()) + if node.GetUser() != nil { + user = node.GetUser().GetName() } var ( - IPV4Address string - IPV6Address string + ipAddresses string + ipAddressesSb485 strings.Builder ) - for _, addr := range node.GetIpAddresses() { - if netip.MustParseAddr(addr).Is4() { - IPV4Address = addr - } else { - IPV6Address = addr + ip, err := netip.ParseAddr(addr) + if err == nil { + ipAddressesSb485.WriteString(ip.String() + "\n") } } + ipAddresses += ipAddressesSb485.String() + + ipAddresses = strings.TrimRight(ipAddresses, "\n") + nodeData := []string{ strconv.FormatUint(node.GetId(), util.Base10), node.GetName(), @@ -594,7 +506,7 @@ func nodesToPtables( nodeKey.ShortString(), user, tags, - strings.Join([]string{IPV4Address, IPV6Address}, ", "), + ipAddresses, strconv.FormatBool(ephemeral), lastSeenTime, expiryTime, @@ -643,26 +555,16 @@ var tagCmd = &cobra.Command{ Use: "tag", Short: "Manage the tags of a node", Aliases: []string{"tags", "t"}, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") - + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { // retrieve flags from CLI identifier, err := cmd.Flags().GetUint64("identifier") if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error converting ID to integer: %s", err), - output, - ) + return fmt.Errorf("getting identifier flag: %w", err) } tagsToSet, err := cmd.Flags().GetStringSlice("tags") if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error retrieving list of tags to add to node, %v", err), - output, - ) + return fmt.Errorf("getting tags flag: %w", err) } // Sending tags to node @@ -673,46 +575,30 @@ var tagCmd = &cobra.Command{ resp, err := client.SetTags(ctx, request) if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error while sending tags to headscale: %s", err), - output, - ) + return fmt.Errorf("setting tags: %w", err) } if resp != nil { - SuccessOutput( - resp.GetNode(), - "Node updated", - output, - ) + return printOutput(cmd, resp.GetNode(), "Node updated") } + + return nil }), } var approveRoutesCmd = &cobra.Command{ Use: "approve-routes", Short: "Manage the approved routes of a node", - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") - + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { // retrieve flags from CLI identifier, err := cmd.Flags().GetUint64("identifier") if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error converting ID to integer: %s", err), - output, - ) + return fmt.Errorf("getting identifier flag: %w", err) } routes, err := cmd.Flags().GetStringSlice("routes") if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error retrieving list of routes to add to node, %v", err), - output, - ) + return fmt.Errorf("getting routes flag: %w", err) } // Sending routes to node @@ -723,19 +609,13 @@ var approveRoutesCmd = &cobra.Command{ resp, err := client.SetApprovedRoutes(ctx, request) if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error while sending routes to headscale: %s", err), - output, - ) + return fmt.Errorf("setting approved routes: %w", err) } if resp != nil { - SuccessOutput( - resp.GetNode(), - "Node updated", - output, - ) + return printOutput(cmd, resp.GetNode(), "Node updated") } + + return nil }), } diff --git a/cmd/headscale/cli/policy.go b/cmd/headscale/cli/policy.go index 95b39fbd..f7f5ea31 100644 --- a/cmd/headscale/cli/policy.go +++ b/cmd/headscale/cli/policy.go @@ -92,7 +92,10 @@ var getPolicy = &cobra.Command{ policy = pol.Data } else { - ctx, client, conn, cancel := newHeadscaleCLIWithConfig() + ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig() + if err != nil { + ErrorOutput(err, fmt.Sprintf("Error connecting: %s", err), output) + } defer cancel() defer conn.Close() @@ -179,7 +182,10 @@ var setPolicy = &cobra.Command{ } else { request := &v1.SetPolicyRequest{Policy: string(policyBytes)} - ctx, client, conn, cancel := newHeadscaleCLIWithConfig() + ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig() + if err != nil { + ErrorOutput(err, fmt.Sprintf("Error connecting: %s", err), output) + } defer cancel() defer conn.Close() diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go index 1d04693f..bbab36a5 100644 --- a/cmd/headscale/cli/preauthkeys.go +++ b/cmd/headscale/cli/preauthkeys.go @@ -47,22 +47,16 @@ var listPreAuthKeys = &cobra.Command{ Use: "list", Short: "List all preauthkeys", Aliases: []string{"ls", "show"}, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { + format, _ := cmd.Flags().GetString("output") response, err := client.ListPreAuthKeys(ctx, &v1.ListPreAuthKeysRequest{}) if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Error getting the list of keys: %s", err), - output, - ) - - return + return fmt.Errorf("listing preauthkeys: %w", err) } - if output != "" { - SuccessOutput(response.GetPreAuthKeys(), "", output) + if format != "" { + return printOutput(cmd, response.GetPreAuthKeys(), "") } tableData := pterm.TableData{ @@ -107,12 +101,10 @@ var listPreAuthKeys = &cobra.Command{ err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Failed to render pterm table: %s", err), - output, - ) + return fmt.Errorf("rendering table: %w", err) } + + return nil }), } @@ -120,9 +112,7 @@ var createPreAuthKeyCmd = &cobra.Command{ Use: "create", Short: "Creates a new preauthkey", Aliases: []string{"c", "new"}, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") - + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { user, _ := cmd.Flags().GetUint64("user") reusable, _ := cmd.Flags().GetBool("reusable") ephemeral, _ := cmd.Flags().GetBool("ephemeral") @@ -139,11 +129,7 @@ var createPreAuthKeyCmd = &cobra.Command{ duration, err := model.ParseDuration(durationStr) if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Could not parse duration: %s\n", err), - output, - ) + return fmt.Errorf("parsing duration: %w", err) } expiration := time.Now().UTC().Add(time.Duration(duration)) @@ -152,14 +138,10 @@ var createPreAuthKeyCmd = &cobra.Command{ response, err := client.CreatePreAuthKey(ctx, request) if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Cannot create Pre Auth Key: %s\n", err), - output, - ) + return fmt.Errorf("creating preauthkey: %w", err) } - SuccessOutput(response.GetPreAuthKey(), response.GetPreAuthKey().GetKey(), output) + return printOutput(cmd, response.GetPreAuthKey(), response.GetPreAuthKey().GetKey()) }), } @@ -167,18 +149,11 @@ var expirePreAuthKeyCmd = &cobra.Command{ Use: "expire", Short: "Expire a preauthkey", Aliases: []string{"revoke", "exp", "e"}, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { id, _ := cmd.Flags().GetUint64("id") if id == 0 { - ErrorOutput( - errMissingParameter, - "Error: missing --id parameter", - output, - ) - - return + return fmt.Errorf("missing --id parameter: %w", errMissingParameter) } request := &v1.ExpirePreAuthKeyRequest{ @@ -187,14 +162,10 @@ var expirePreAuthKeyCmd = &cobra.Command{ response, err := client.ExpirePreAuthKey(ctx, request) if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Cannot expire Pre Auth Key: %s\n", err), - output, - ) + return fmt.Errorf("expiring preauthkey: %w", err) } - SuccessOutput(response, "Key expired", output) + return printOutput(cmd, response, "Key expired") }), } @@ -202,18 +173,11 @@ var deletePreAuthKeyCmd = &cobra.Command{ Use: "delete", Short: "Delete a preauthkey", Aliases: []string{"del", "rm", "d"}, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { id, _ := cmd.Flags().GetUint64("id") if id == 0 { - ErrorOutput( - errMissingParameter, - "Error: missing --id parameter", - output, - ) - - return + return fmt.Errorf("missing --id parameter: %w", errMissingParameter) } request := &v1.DeletePreAuthKeyRequest{ @@ -222,13 +186,9 @@ var deletePreAuthKeyCmd = &cobra.Command{ response, err := client.DeletePreAuthKey(ctx, request) if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Cannot delete Pre Auth Key: %s\n", err), - output, - ) + return fmt.Errorf("deleting preauthkey: %w", err) } - SuccessOutput(response, "Key deleted", output) + return printOutput(cmd, response, "Key deleted") }), } diff --git a/cmd/headscale/cli/users.go b/cmd/headscale/cli/users.go index b5f28532..254602d2 100644 --- a/cmd/headscale/cli/users.go +++ b/cmd/headscale/cli/users.go @@ -13,7 +13,6 @@ import ( "github.com/pterm/pterm" "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "google.golang.org/grpc/status" ) // CLI user errors. @@ -28,20 +27,21 @@ func usernameAndIDFlag(cmd *cobra.Command) { } // usernameAndIDFromFlag returns the username and ID from the flags of the command. -// If both are empty, it will exit the program with an error. -func usernameAndIDFromFlag(cmd *cobra.Command) (uint64, string) { +func usernameAndIDFromFlag(cmd *cobra.Command) (uint64, string, error) { username, _ := cmd.Flags().GetString("name") identifier, _ := cmd.Flags().GetInt64("identifier") if username == "" && identifier < 0 { - ErrorOutput( - errFlagRequired, - "Cannot rename user: "+status.Convert(errFlagRequired).Message(), - "", - ) + return 0, "", errFlagRequired } - return uint64(identifier), username + // Normalise unset/negative identifiers to 0 so the uint64 + // conversion does not produce a bogus large value. + if identifier < 0 { + identifier = 0 + } + + return uint64(identifier), username, nil //nolint:gosec // identifier is clamped to >= 0 above } func init() { @@ -81,9 +81,7 @@ var createUserCmd = &cobra.Command{ return nil }, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") - + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { userName := args[0] log.Trace().Interface(zf.Client, client).Msg("obtained gRPC client") @@ -100,14 +98,7 @@ var createUserCmd = &cobra.Command{ if pictureURL, _ := cmd.Flags().GetString("picture-url"); pictureURL != "" { if _, err := url.Parse(pictureURL); err != nil { //nolint:noinlineerr - ErrorOutput( - err, - fmt.Sprintf( - "Invalid Picture URL: %s", - err, - ), - output, - ) + return fmt.Errorf("invalid picture URL: %w", err) } request.PictureUrl = pictureURL @@ -117,14 +108,10 @@ var createUserCmd = &cobra.Command{ response, err := client.CreateUser(ctx, request) if err != nil { - ErrorOutput( - err, - "Cannot create user: "+status.Convert(err).Message(), - output, - ) + return fmt.Errorf("creating user: %w", err) } - SuccessOutput(response.GetUser(), "User created", output) + return printOutput(cmd, response.GetUser(), "User created") }), } @@ -132,10 +119,12 @@ var destroyUserCmd = &cobra.Command{ Use: "destroy --identifier ID or --name NAME", Short: "Destroys a user", Aliases: []string{"delete"}, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { + id, username, err := usernameAndIDFromFlag(cmd) + if err != nil { + return err + } - id, username := usernameAndIDFromFlag(cmd) request := &v1.ListUsersRequest{ Name: username, Id: id, @@ -143,20 +132,11 @@ var destroyUserCmd = &cobra.Command{ users, err := client.ListUsers(ctx, request) if err != nil { - ErrorOutput( - err, - "Error: "+status.Convert(err).Message(), - output, - ) + return fmt.Errorf("listing users: %w", err) } if len(users.GetUsers()) != 1 { - err := errMultipleUsersMatch - ErrorOutput( - err, - "Error: "+status.Convert(err).Message(), - output, - ) + return errMultipleUsersMatch } user := users.GetUsers()[0] @@ -176,17 +156,13 @@ var destroyUserCmd = &cobra.Command{ response, err := client.DeleteUser(ctx, request) if err != nil { - ErrorOutput( - err, - "Cannot destroy user: "+status.Convert(err).Message(), - output, - ) + return fmt.Errorf("destroying user: %w", err) } - SuccessOutput(response, "User destroyed", output) - } else { - SuccessOutput(map[string]string{"Result": "User not destroyed"}, "User not destroyed", output) + return printOutput(cmd, response, "User destroyed") } + + return printOutput(cmd, map[string]string{"Result": "User not destroyed"}, "User not destroyed") }), } @@ -194,8 +170,8 @@ var listUsersCmd = &cobra.Command{ Use: "list", Short: "List all the users", Aliases: []string{"ls", "show"}, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { + format, _ := cmd.Flags().GetString("output") request := &v1.ListUsersRequest{} @@ -215,15 +191,11 @@ var listUsersCmd = &cobra.Command{ response, err := client.ListUsers(ctx, request) if err != nil { - ErrorOutput( - err, - "Cannot get users: "+status.Convert(err).Message(), - output, - ) + return fmt.Errorf("listing users: %w", err) } - if output != "" { - SuccessOutput(response.GetUsers(), "", output) + if format != "" { + return printOutput(cmd, response.GetUsers(), "") } tableData := pterm.TableData{{"ID", "Name", "Username", "Email", "Created"}} @@ -242,12 +214,10 @@ var listUsersCmd = &cobra.Command{ err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() if err != nil { - ErrorOutput( - err, - fmt.Sprintf("Failed to render pterm table: %s", err), - output, - ) + return fmt.Errorf("rendering table: %w", err) } + + return nil }), } @@ -255,10 +225,12 @@ var renameUserCmd = &cobra.Command{ Use: "rename", Short: "Renames a user", Aliases: []string{"mv"}, - Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) { - output, _ := cmd.Flags().GetString("output") + RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { + id, username, err := usernameAndIDFromFlag(cmd) + if err != nil { + return err + } - id, username := usernameAndIDFromFlag(cmd) listReq := &v1.ListUsersRequest{ Name: username, Id: id, @@ -266,20 +238,11 @@ var renameUserCmd = &cobra.Command{ users, err := client.ListUsers(ctx, listReq) if err != nil { - ErrorOutput( - err, - "Error: "+status.Convert(err).Message(), - output, - ) + return fmt.Errorf("listing users: %w", err) } if len(users.GetUsers()) != 1 { - err := errMultipleUsersMatch - ErrorOutput( - err, - "Error: "+status.Convert(err).Message(), - output, - ) + return errMultipleUsersMatch } newName, _ := cmd.Flags().GetString("new-name") @@ -291,13 +254,9 @@ var renameUserCmd = &cobra.Command{ response, err := client.RenameUser(ctx, renameReq) if err != nil { - ErrorOutput( - err, - "Cannot rename user: "+status.Convert(err).Message(), - output, - ) + return fmt.Errorf("renaming user: %w", err) } - SuccessOutput(response.GetUser(), "User renamed", output) + return printOutput(cmd, response.GetUser(), "User renamed") }), } diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index eef83cc0..9b4cfa10 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "encoding/json" + "errors" "fmt" "os" @@ -23,8 +24,14 @@ import ( const ( HeadscaleDateTimeFormat = "2006-01-02 15:04:05" SocketWritePermissions = 0o666 + + outputFormatJSON = "json" + outputFormatJSONLine = "json-line" + outputFormatYAML = "yaml" ) +var errAPIKeyNotSet = errors.New("HEADSCALE_CLI_API_KEY environment variable needs to be set") + func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) { cfg, err := types.LoadServerConfig() if err != nil { @@ -42,29 +49,28 @@ func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) { return app, nil } -// grpcRun wraps a cobra RunFunc, injecting a ready gRPC client and context. -// Connection lifecycle is managed by the wrapper — callers never see -// the underlying conn or cancel func. -func grpcRun( - fn func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string), -) func(*cobra.Command, []string) { - return func(cmd *cobra.Command, args []string) { - ctx, client, conn, cancel := newHeadscaleCLIWithConfig() +// grpcRunE wraps a cobra RunE func, injecting a ready gRPC client and +// context. Connection lifecycle is managed by the wrapper — callers +// never see the underlying conn or cancel func. +func grpcRunE( + fn func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error, +) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig() + if err != nil { + return fmt.Errorf("connecting to headscale: %w", err) + } defer cancel() defer conn.Close() - fn(ctx, client, cmd, args) + return fn(ctx, client, cmd, args) } } -func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) { +func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc, error) { cfg, err := types.LoadCLIConfig() if err != nil { - log.Fatal(). - Err(err). - Caller(). - Msgf("Failed to load configuration") - os.Exit(-1) // we get here if logging is suppressed (i.e., json output) + return nil, nil, nil, nil, fmt.Errorf("loading configuration: %w", err) } log.Debug(). @@ -88,18 +94,23 @@ func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *g address = cfg.UnixSocket // Try to give the user better feedback if we cannot write to the headscale - // socket. - socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) // nolint + // socket. Note: os.OpenFile on a Unix domain socket returns ENXIO on + // Linux which is expected — only permission errors are actionable here. + // The actual gRPC connection uses net.Dial which handles sockets properly. + socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) //nolint if err != nil { if os.IsPermission(err) { - log.Fatal(). - Err(err). - Str("socket", cfg.UnixSocket). - Msgf("Unable to read/write to headscale socket, do you have the correct permissions?") - } - } + cancel() - socket.Close() + return nil, nil, nil, nil, fmt.Errorf( + "unable to read/write to headscale socket %q, do you have the correct permissions? %w", + cfg.UnixSocket, + err, + ) + } + } else { + socket.Close() + } grpcOptions = append( grpcOptions, @@ -110,7 +121,9 @@ func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *g // If we are not connecting to a local server, require an API key for authentication apiKey := cfg.CLI.APIKey if apiKey == "" { - log.Fatal().Caller().Msgf("HEADSCALE_CLI_API_KEY environment variable needs to be set") + cancel() + + return nil, nil, nil, nil, errAPIKeyNotSet } grpcOptions = append(grpcOptions, @@ -141,15 +154,65 @@ func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *g conn, err := grpc.DialContext(ctx, address, grpcOptions...) //nolint:staticcheck // SA1019: deprecated but supported in 1.x if err != nil { - log.Fatal().Caller().Err(err).Msgf("could not connect: %v", err) - os.Exit(-1) // we get here if logging is suppressed (i.e., json output) + cancel() + + return nil, nil, nil, nil, fmt.Errorf("connecting to %s: %w", address, err) } client := v1.NewHeadscaleServiceClient(conn) - return ctx, client, conn, cancel + return ctx, client, conn, cancel, nil } +// formatOutput serialises result into the requested format. For the +// default (empty) format the human-readable override string is returned. +func formatOutput(result any, override string, outputFormat string) (string, error) { + switch outputFormat { + case outputFormatJSON: + b, err := json.MarshalIndent(result, "", "\t") + if err != nil { + return "", fmt.Errorf("marshalling JSON output: %w", err) + } + + return string(b), nil + case outputFormatJSONLine: + b, err := json.Marshal(result) + if err != nil { + return "", fmt.Errorf("marshalling JSON-line output: %w", err) + } + + return string(b), nil + case outputFormatYAML: + b, err := yaml.Marshal(result) + if err != nil { + return "", fmt.Errorf("marshalling YAML output: %w", err) + } + + return string(b), nil + default: + return override, nil + } +} + +// printOutput formats result and writes it to stdout. It reads the --output +// flag from cmd to decide the serialisation format. +func printOutput(cmd *cobra.Command, result any, override string) error { + format, _ := cmd.Flags().GetString("output") + + out, err := formatOutput(result, override, format) + if err != nil { + return err + } + + fmt.Println(out) + + return nil +} + +// output formats result into the requested format. It calls log.Fatal on +// marshal failure. +// +// Deprecated: use formatOutput instead. func output(result any, override string, outputFormat string) string { var ( jsonBytes []byte @@ -157,17 +220,17 @@ func output(result any, override string, outputFormat string) string { ) switch outputFormat { - case "json": + case outputFormatJSON: jsonBytes, err = json.MarshalIndent(result, "", "\t") if err != nil { log.Fatal().Err(err).Msg("unmarshalling output") } - case "json-line": + case outputFormatJSONLine: jsonBytes, err = json.Marshal(result) if err != nil { log.Fatal().Err(err).Msg("unmarshalling output") } - case "yaml": + case outputFormatYAML: jsonBytes, err = yaml.Marshal(result) if err != nil { log.Fatal().Err(err).Msg("unmarshalling output") @@ -181,12 +244,16 @@ func output(result any, override string, outputFormat string) string { } // SuccessOutput prints the result to stdout and exits with status code 0. +// +// Deprecated: use printOutput instead. func SuccessOutput(result any, override string, outputFormat string) { fmt.Println(output(result, override, outputFormat)) os.Exit(0) } // ErrorOutput prints an error message to stderr and exits with status code 1. +// +// Deprecated: use fmt.Errorf and return the error instead. func ErrorOutput(errResult error, override string, outputFormat string) { type errOutput struct { Error string `json:"error"` @@ -216,11 +283,11 @@ func printError(err error, outputFormat string) { var formatted []byte switch outputFormat { - case "json": + case outputFormatJSON: formatted, _ = json.MarshalIndent(e, "", "\t") //nolint:errchkjson // errOutput contains only a string field - case "json-line": + case outputFormatJSONLine: formatted, _ = json.Marshal(e) //nolint:errchkjson // errOutput contains only a string field - case "yaml": + case outputFormatYAML: formatted, _ = yaml.Marshal(e) default: fmt.Fprintf(os.Stderr, "Error: %s\n", err) @@ -233,7 +300,7 @@ func printError(err error, outputFormat string) { func HasMachineOutputFlag() bool { for _, arg := range os.Args { - if arg == "json" || arg == "json-line" || arg == "yaml" { + if arg == outputFormatJSON || arg == outputFormatJSONLine || arg == outputFormatYAML { return true } }