mirror of
https://github.com/juanfont/headscale.git
synced 2026-04-17 17:51:51 +09:00
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.
This commit is contained in:
@@ -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")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -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, "")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user