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.
This commit is contained in:
Kristoffer Dalby
2026-02-18 14:30:07 +00:00
parent 8891ec9835
commit d6c39e65a5
5 changed files with 102 additions and 139 deletions

View File

@@ -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()
})
}),
}

View File

@@ -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()
})
}),
}

View File

@@ -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()
})
}),
}

View File

@@ -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()
})
}),
}

View File

@@ -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.