From d6c39e65a59eb4671b8bf15cb83ca208c5ed0706 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 18 Feb 2026 14:30:07 +0000 Subject: [PATCH] cmd/headscale/cli: add printListOutput to centralise table-vs-JSON branching Add a helper that checks the --output flag and either serialises as JSON/YAML or invokes a table-rendering callback. This removes the repeated format,_ := cmd.Flags().GetString("output") + if-branch from the five list commands. --- cmd/headscale/cli/api_key.go | 49 +++++++----------- cmd/headscale/cli/nodes.go | 48 +++++------------- cmd/headscale/cli/preauthkeys.go | 87 ++++++++++++++------------------ cmd/headscale/cli/users.go | 41 ++++++--------- cmd/headscale/cli/utils.go | 16 ++++++ 5 files changed, 102 insertions(+), 139 deletions(-) diff --git a/cmd/headscale/cli/api_key.go b/cmd/headscale/cli/api_key.go index d867a444..262c9e6e 100644 --- a/cmd/headscale/cli/api_key.go +++ b/cmd/headscale/cli/api_key.go @@ -48,44 +48,33 @@ var listAPIKeys = &cobra.Command{ Short: "List the Api keys for headscale", Aliases: []string{"ls", "show"}, 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) + response, err := client.ListApiKeys(ctx, &v1.ListApiKeysRequest{}) if err != nil { return fmt.Errorf("listing api keys: %w", err) } - if format != "" { - return printOutput(cmd, response.GetApiKeys(), "") - } - - tableData := pterm.TableData{ - {"ID", "Prefix", "Expiration", "Created"}, - } - - for _, key := range response.GetApiKeys() { - expiration := "-" - - if key.GetExpiration() != nil { - expiration = ColourTime(key.GetExpiration().AsTime()) + return printListOutput(cmd, response.GetApiKeys(), func() error { + tableData := pterm.TableData{ + {"ID", "Prefix", "Expiration", "Created"}, } - tableData = append(tableData, []string{ - strconv.FormatUint(key.GetId(), util.Base10), - key.GetPrefix(), - expiration, - key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat), - }) - } + for _, key := range response.GetApiKeys() { + expiration := "-" - err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() - if err != nil { - return fmt.Errorf("rendering table: %w", err) - } + if key.GetExpiration() != nil { + expiration = ColourTime(key.GetExpiration().AsTime()) + } - return nil + tableData = append(tableData, []string{ + strconv.FormatUint(key.GetId(), util.Base10), + key.GetPrefix(), + expiration, + key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat), + }) + } + + return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() + }) }), } diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index db7f538d..a13f8b56 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -136,36 +136,24 @@ var listNodesCmd = &cobra.Command{ Short: "List nodes", Aliases: []string{"ls", "show"}, 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 { return fmt.Errorf("getting user flag: %w", err) } - request := &v1.ListNodesRequest{ - User: user, - } - - response, err := client.ListNodes(ctx, request) + response, err := client.ListNodes(ctx, &v1.ListNodesRequest{User: user}) if err != nil { return fmt.Errorf("listing nodes: %w", err) } - if format != "" { - return printOutput(cmd, response.GetNodes(), "") - } + return printListOutput(cmd, response.GetNodes(), func() error { + tableData, err := nodesToPtables(user, response.GetNodes()) + if err != nil { + return fmt.Errorf("converting to table: %w", err) + } - tableData, err := nodesToPtables(user, response.GetNodes()) - if err != nil { - return fmt.Errorf("converting to table: %w", err) - } - - err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() - if err != nil { - return fmt.Errorf("rendering table: %w", err) - } - - return nil + return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() + }) }), } @@ -174,15 +162,12 @@ var listNodeRoutesCmd = &cobra.Command{ Short: "List routes available on nodes", Aliases: []string{"lsr", "routes"}, 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 { return fmt.Errorf("getting identifier flag: %w", err) } - request := &v1.ListNodesRequest{} - - response, err := client.ListNodes(ctx, request) + response, err := client.ListNodes(ctx, &v1.ListNodesRequest{}) if err != nil { return fmt.Errorf("listing nodes: %w", err) } @@ -202,18 +187,9 @@ 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 format != "" { - return printOutput(cmd, nodes, "") - } - - tableData := nodeRoutesToPtables(nodes) - - err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() - if err != nil { - return fmt.Errorf("rendering table: %w", err) - } - - return nil + return printListOutput(cmd, nodes, func() error { + return pterm.DefaultTable.WithHasHeader().WithData(nodeRoutesToPtables(nodes)).Render() + }) }), } diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go index bbab36a5..f9390b72 100644 --- a/cmd/headscale/cli/preauthkeys.go +++ b/cmd/headscale/cli/preauthkeys.go @@ -48,63 +48,54 @@ var listPreAuthKeys = &cobra.Command{ Short: "List all preauthkeys", Aliases: []string{"ls", "show"}, 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 { return fmt.Errorf("listing preauthkeys: %w", err) } - if format != "" { - return printOutput(cmd, response.GetPreAuthKeys(), "") - } - - tableData := pterm.TableData{ - { - "ID", - "Key/Prefix", - "Reusable", - "Ephemeral", - "Used", - "Expiration", - "Created", - "Owner", - }, - } - - for _, key := range response.GetPreAuthKeys() { - expiration := "-" - if key.GetExpiration() != nil { - expiration = ColourTime(key.GetExpiration().AsTime()) + return printListOutput(cmd, response.GetPreAuthKeys(), func() error { + tableData := pterm.TableData{ + { + "ID", + "Key/Prefix", + "Reusable", + "Ephemeral", + "Used", + "Expiration", + "Created", + "Owner", + }, } - var owner string - if len(key.GetAclTags()) > 0 { - owner = strings.Join(key.GetAclTags(), "\n") - } else if key.GetUser() != nil { - owner = key.GetUser().GetName() - } else { - owner = "-" + for _, key := range response.GetPreAuthKeys() { + expiration := "-" + if key.GetExpiration() != nil { + expiration = ColourTime(key.GetExpiration().AsTime()) + } + + var owner string + if len(key.GetAclTags()) > 0 { + owner = strings.Join(key.GetAclTags(), "\n") + } else if key.GetUser() != nil { + owner = key.GetUser().GetName() + } else { + owner = "-" + } + + tableData = append(tableData, []string{ + strconv.FormatUint(key.GetId(), 10), + key.GetKey(), + strconv.FormatBool(key.GetReusable()), + strconv.FormatBool(key.GetEphemeral()), + strconv.FormatBool(key.GetUsed()), + expiration, + key.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"), + owner, + }) } - tableData = append(tableData, []string{ - strconv.FormatUint(key.GetId(), 10), - key.GetKey(), - strconv.FormatBool(key.GetReusable()), - strconv.FormatBool(key.GetEphemeral()), - strconv.FormatBool(key.GetUsed()), - expiration, - key.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"), - owner, - }) - } - - err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() - if err != nil { - return fmt.Errorf("rendering table: %w", err) - } - - return nil + return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() + }) }), } diff --git a/cmd/headscale/cli/users.go b/cmd/headscale/cli/users.go index 254602d2..61ea5b16 100644 --- a/cmd/headscale/cli/users.go +++ b/cmd/headscale/cli/users.go @@ -171,8 +171,6 @@ var listUsersCmd = &cobra.Command{ Short: "List all the users", Aliases: []string{"ls", "show"}, RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error { - format, _ := cmd.Flags().GetString("output") - request := &v1.ListUsersRequest{} id, _ := cmd.Flags().GetInt64("identifier") @@ -194,30 +192,23 @@ var listUsersCmd = &cobra.Command{ return fmt.Errorf("listing users: %w", err) } - if format != "" { - return printOutput(cmd, response.GetUsers(), "") - } + return printListOutput(cmd, response.GetUsers(), func() error { + tableData := pterm.TableData{{"ID", "Name", "Username", "Email", "Created"}} + for _, user := range response.GetUsers() { + tableData = append( + tableData, + []string{ + strconv.FormatUint(user.GetId(), 10), + user.GetDisplayName(), + user.GetName(), + user.GetEmail(), + user.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"), + }, + ) + } - tableData := pterm.TableData{{"ID", "Name", "Username", "Email", "Created"}} - for _, user := range response.GetUsers() { - tableData = append( - tableData, - []string{ - strconv.FormatUint(user.GetId(), 10), - user.GetDisplayName(), - user.GetName(), - user.GetEmail(), - user.GetCreatedAt().AsTime().Format("2006-01-02 15:04:05"), - }, - ) - } - - err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() - if err != nil { - return fmt.Errorf("rendering table: %w", err) - } - - return nil + return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() + }) }), } diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 765c2be8..781a9085 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -209,6 +209,22 @@ func printOutput(cmd *cobra.Command, result any, override string) error { return nil } +// 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. +func printListOutput( + cmd *cobra.Command, + data any, + renderTable func() error, +) error { + format, _ := cmd.Flags().GetString("output") + if format != "" { + return printOutput(cmd, data, "") + } + + return renderTable() +} + // printError writes err to stderr, formatting it as JSON/YAML when the // --output flag requests machine-readable output. Used exclusively by // Execute() so that every error surfaces in the format the caller asked for.