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:
parent
e6546b2cea
commit
e4fe216e45
@ -47,22 +47,18 @@ var listAPIKeys = &cobra.Command{
|
|||||||
Use: "list",
|
Use: "list",
|
||||||
Short: "List the Api keys for headscale",
|
Short: "List the Api keys for headscale",
|
||||||
Aliases: []string{"ls", "show"},
|
Aliases: []string{"ls", "show"},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
format, _ := cmd.Flags().GetString("output")
|
||||||
|
|
||||||
request := &v1.ListApiKeysRequest{}
|
request := &v1.ListApiKeysRequest{}
|
||||||
|
|
||||||
response, err := client.ListApiKeys(ctx, request)
|
response, err := client.ListApiKeys(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("listing api keys: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error getting the list of keys: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if output != "" {
|
if format != "" {
|
||||||
SuccessOutput(response.GetApiKeys(), "", output)
|
return printOutput(cmd, response.GetApiKeys(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
tableData := pterm.TableData{
|
tableData := pterm.TableData{
|
||||||
@ -86,12 +82,10 @@ var listAPIKeys = &cobra.Command{
|
|||||||
|
|
||||||
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("rendering table: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Failed to render pterm table: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,20 +97,14 @@ Creates a new Api key, the Api key is only visible on creation
|
|||||||
and cannot be retrieved again.
|
and cannot be retrieved again.
|
||||||
If you loose a key, create a new one and revoke (expire) the old one.`,
|
If you loose a key, create a new one and revoke (expire) the old one.`,
|
||||||
Aliases: []string{"c", "new"},
|
Aliases: []string{"c", "new"},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
|
||||||
|
|
||||||
request := &v1.CreateApiKeyRequest{}
|
request := &v1.CreateApiKeyRequest{}
|
||||||
|
|
||||||
durationStr, _ := cmd.Flags().GetString("expiration")
|
durationStr, _ := cmd.Flags().GetString("expiration")
|
||||||
|
|
||||||
duration, err := model.ParseDuration(durationStr)
|
duration, err := model.ParseDuration(durationStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("parsing duration: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Could not parse duration: %s\n", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
expiration := time.Now().UTC().Add(time.Duration(duration))
|
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)
|
response, err := client.CreateApiKey(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("creating api key: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Cannot create Api Key: %s\n", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessOutput(response.GetApiKey(), response.GetApiKey(), output)
|
return printOutput(cmd, response.GetApiKey(), response.GetApiKey())
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,25 +124,15 @@ var expireAPIKeyCmd = &cobra.Command{
|
|||||||
Use: "expire",
|
Use: "expire",
|
||||||
Short: "Expire an ApiKey",
|
Short: "Expire an ApiKey",
|
||||||
Aliases: []string{"revoke", "exp", "e"},
|
Aliases: []string{"revoke", "exp", "e"},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
|
||||||
|
|
||||||
id, _ := cmd.Flags().GetUint64("id")
|
id, _ := cmd.Flags().GetUint64("id")
|
||||||
prefix, _ := cmd.Flags().GetString("prefix")
|
prefix, _ := cmd.Flags().GetString("prefix")
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case id == 0 && prefix == "":
|
case id == 0 && prefix == "":
|
||||||
ErrorOutput(
|
return fmt.Errorf("either --id or --prefix must be provided: %w", errMissingParameter)
|
||||||
errMissingParameter,
|
|
||||||
"Either --id or --prefix must be provided",
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
case id != 0 && prefix != "":
|
case id != 0 && prefix != "":
|
||||||
ErrorOutput(
|
return fmt.Errorf("only one of --id or --prefix can be provided: %w", errMissingParameter)
|
||||||
errMissingParameter,
|
|
||||||
"Only one of --id or --prefix can be provided",
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request := &v1.ExpireApiKeyRequest{}
|
request := &v1.ExpireApiKeyRequest{}
|
||||||
@ -170,14 +144,10 @@ var expireAPIKeyCmd = &cobra.Command{
|
|||||||
|
|
||||||
response, err := client.ExpireApiKey(ctx, request)
|
response, err := client.ExpireApiKey(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("expiring api key: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Cannot expire Api Key: %s\n", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessOutput(response, "Key expired", output)
|
return printOutput(cmd, response, "Key expired")
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,25 +155,15 @@ var deleteAPIKeyCmd = &cobra.Command{
|
|||||||
Use: "delete",
|
Use: "delete",
|
||||||
Short: "Delete an ApiKey",
|
Short: "Delete an ApiKey",
|
||||||
Aliases: []string{"remove", "del"},
|
Aliases: []string{"remove", "del"},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
|
||||||
|
|
||||||
id, _ := cmd.Flags().GetUint64("id")
|
id, _ := cmd.Flags().GetUint64("id")
|
||||||
prefix, _ := cmd.Flags().GetString("prefix")
|
prefix, _ := cmd.Flags().GetString("prefix")
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case id == 0 && prefix == "":
|
case id == 0 && prefix == "":
|
||||||
ErrorOutput(
|
return fmt.Errorf("either --id or --prefix must be provided: %w", errMissingParameter)
|
||||||
errMissingParameter,
|
|
||||||
"Either --id or --prefix must be provided",
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
case id != 0 && prefix != "":
|
case id != 0 && prefix != "":
|
||||||
ErrorOutput(
|
return fmt.Errorf("only one of --id or --prefix can be provided: %w", errMissingParameter)
|
||||||
errMissingParameter,
|
|
||||||
"Only one of --id or --prefix can be provided",
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request := &v1.DeleteApiKeyRequest{}
|
request := &v1.DeleteApiKeyRequest{}
|
||||||
@ -215,13 +175,9 @@ var deleteAPIKeyCmd = &cobra.Command{
|
|||||||
|
|
||||||
response, err := client.DeleteApiKey(ctx, request)
|
response, err := client.DeleteApiKey(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("deleting api key: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Cannot delete Api Key: %s\n", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessOutput(response, "Key deleted", output)
|
return printOutput(cmd, response, "Key deleted")
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"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
|
// 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{
|
var createNodeCmd = &cobra.Command{
|
||||||
Use: "create-node",
|
Use: "create-node",
|
||||||
Short: "Create a node that can be registered with `nodes register <>` command",
|
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) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
|
||||||
|
|
||||||
user, err := cmd.Flags().GetString("user")
|
user, err := cmd.Flags().GetString("user")
|
||||||
if err != nil {
|
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")
|
name, err := cmd.Flags().GetString("name")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("getting name flag: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error getting node from flag: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registrationID, err := cmd.Flags().GetString("key")
|
registrationID, err := cmd.Flags().GetString("key")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("getting key flag: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error getting key from flag: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = types.RegistrationIDFromString(registrationID)
|
_, err = types.RegistrationIDFromString(registrationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("parsing machine key: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Failed to parse machine key from flag: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
routes, err := cmd.Flags().GetStringSlice("route")
|
routes, err := cmd.Flags().GetStringSlice("route")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("getting routes flag: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error getting routes from flag: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request := &v1.DebugCreateNodeRequest{
|
request := &v1.DebugCreateNodeRequest{
|
||||||
@ -113,13 +94,9 @@ var createNodeCmd = &cobra.Command{
|
|||||||
|
|
||||||
response, err := client.DebugCreateNode(ctx, request)
|
response, err := client.DebugCreateNode(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("creating node: %w", err)
|
||||||
err,
|
|
||||||
"Cannot create node: "+status.Convert(err).Message(),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessOutput(response.GetNode(), "Node created", output)
|
return printOutput(cmd, response.GetNode(), "Node created")
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -15,14 +16,12 @@ var healthCmd = &cobra.Command{
|
|||||||
Use: "health",
|
Use: "health",
|
||||||
Short: "Check the health of the Headscale server",
|
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.",
|
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) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
|
||||||
|
|
||||||
response, err := client.Health(ctx, &v1.HealthRequest{})
|
response, err := client.Health(ctx, &v1.HealthRequest{})
|
||||||
if err != nil {
|
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{
|
var registerNodeCmd = &cobra.Command{
|
||||||
Use: "register",
|
Use: "register",
|
||||||
Short: "Registers a node to your network",
|
Short: "Registers a node to your network",
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
|
||||||
|
|
||||||
user, err := cmd.Flags().GetString("user")
|
user, err := cmd.Flags().GetString("user")
|
||||||
if err != nil {
|
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")
|
registrationID, err := cmd.Flags().GetString("key")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("getting key flag: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error getting node key from flag: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request := &v1.RegisterNodeRequest{
|
request := &v1.RegisterNodeRequest{
|
||||||
@ -128,19 +122,13 @@ var registerNodeCmd = &cobra.Command{
|
|||||||
|
|
||||||
response, err := client.RegisterNode(ctx, request)
|
response, err := client.RegisterNode(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("registering node: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"Cannot register node: %s\n",
|
|
||||||
status.Convert(err).Message(),
|
|
||||||
),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessOutput(
|
return printOutput(
|
||||||
|
cmd,
|
||||||
response.GetNode(),
|
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",
|
Use: "list",
|
||||||
Short: "List nodes",
|
Short: "List nodes",
|
||||||
Aliases: []string{"ls", "show"},
|
Aliases: []string{"ls", "show"},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
format, _ := cmd.Flags().GetString("output")
|
||||||
|
|
||||||
user, err := cmd.Flags().GetString("user")
|
user, err := cmd.Flags().GetString("user")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output)
|
return fmt.Errorf("getting user flag: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
request := &v1.ListNodesRequest{
|
request := &v1.ListNodesRequest{
|
||||||
@ -162,30 +149,24 @@ var listNodesCmd = &cobra.Command{
|
|||||||
|
|
||||||
response, err := client.ListNodes(ctx, request)
|
response, err := client.ListNodes(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("listing nodes: %w", err)
|
||||||
err,
|
|
||||||
"Cannot get nodes: "+status.Convert(err).Message(),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if output != "" {
|
if format != "" {
|
||||||
SuccessOutput(response.GetNodes(), "", output)
|
return printOutput(cmd, response.GetNodes(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
tableData, err := nodesToPtables(user, response.GetNodes())
|
tableData, err := nodesToPtables(user, response.GetNodes())
|
||||||
if err != nil {
|
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()
|
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("rendering table: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Failed to render pterm table: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,27 +174,18 @@ var listNodeRoutesCmd = &cobra.Command{
|
|||||||
Use: "list-routes",
|
Use: "list-routes",
|
||||||
Short: "List routes available on nodes",
|
Short: "List routes available on nodes",
|
||||||
Aliases: []string{"lsr", "routes"},
|
Aliases: []string{"lsr", "routes"},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
format, _ := cmd.Flags().GetString("output")
|
||||||
|
|
||||||
identifier, err := cmd.Flags().GetUint64("identifier")
|
identifier, err := cmd.Flags().GetUint64("identifier")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("getting identifier flag: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error converting ID to integer: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request := &v1.ListNodesRequest{}
|
request := &v1.ListNodesRequest{}
|
||||||
|
|
||||||
response, err := client.ListNodes(ctx, request)
|
response, err := client.ListNodes(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("listing nodes: %w", err)
|
||||||
err,
|
|
||||||
"Cannot get nodes: "+status.Convert(err).Message(),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes := response.GetNodes()
|
nodes := response.GetNodes()
|
||||||
@ -221,6 +193,7 @@ var listNodeRoutesCmd = &cobra.Command{
|
|||||||
for _, node := range response.GetNodes() {
|
for _, node := range response.GetNodes() {
|
||||||
if node.GetId() == identifier {
|
if node.GetId() == identifier {
|
||||||
nodes = []*v1.Node{node}
|
nodes = []*v1.Node{node}
|
||||||
|
|
||||||
break
|
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)
|
return (n.GetSubnetRoutes() != nil && len(n.GetSubnetRoutes()) > 0) || (n.GetApprovedRoutes() != nil && len(n.GetApprovedRoutes()) > 0) || (n.GetAvailableRoutes() != nil && len(n.GetAvailableRoutes()) > 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
if output != "" {
|
if format != "" {
|
||||||
SuccessOutput(nodes, "", output)
|
return printOutput(cmd, nodes, "")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tableData := nodeRoutesToPtables(nodes)
|
tableData := nodeRoutesToPtables(nodes)
|
||||||
|
|
||||||
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("rendering table: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Failed to render pterm table: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,27 +223,15 @@ var expireNodeCmd = &cobra.Command{
|
|||||||
Short: "Expire (log out) a node in your network",
|
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.",
|
Long: "Expiring a node will keep the node in the database and force it to reauthenticate.",
|
||||||
Aliases: []string{"logout", "exp", "e"},
|
Aliases: []string{"logout", "exp", "e"},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
|
||||||
|
|
||||||
identifier, err := cmd.Flags().GetUint64("identifier")
|
identifier, err := cmd.Flags().GetUint64("identifier")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("getting identifier flag: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error converting ID to integer: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
expiry, err := cmd.Flags().GetString("expiry")
|
expiry, err := cmd.Flags().GetString("expiry")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("getting expiry flag: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error converting expiry to string: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@ -282,13 +240,7 @@ var expireNodeCmd = &cobra.Command{
|
|||||||
if expiry != "" {
|
if expiry != "" {
|
||||||
expiryTime, err = time.Parse(time.RFC3339, expiry)
|
expiryTime, err = time.Parse(time.RFC3339, expiry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("parsing expiry time: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error converting expiry to string: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,37 +251,24 @@ var expireNodeCmd = &cobra.Command{
|
|||||||
|
|
||||||
response, err := client.ExpireNode(ctx, request)
|
response, err := client.ExpireNode(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("expiring node: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"Cannot expire node: %s\n",
|
|
||||||
status.Convert(err).Message(),
|
|
||||||
),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if now.Equal(expiryTime) || now.After(expiryTime) {
|
if now.Equal(expiryTime) || now.After(expiryTime) {
|
||||||
SuccessOutput(response.GetNode(), "Node expired", output)
|
return printOutput(cmd, response.GetNode(), "Node expired")
|
||||||
} else {
|
|
||||||
SuccessOutput(response.GetNode(), "Node expiration updated", output)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return printOutput(cmd, response.GetNode(), "Node expiration updated")
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
var renameNodeCmd = &cobra.Command{
|
var renameNodeCmd = &cobra.Command{
|
||||||
Use: "rename NEW_NAME",
|
Use: "rename NEW_NAME",
|
||||||
Short: "Renames a node in your network",
|
Short: "Renames a node in your network",
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
|
||||||
|
|
||||||
identifier, err := cmd.Flags().GetUint64("identifier")
|
identifier, err := cmd.Flags().GetUint64("identifier")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("getting identifier flag: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error converting ID to integer: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newName := ""
|
newName := ""
|
||||||
@ -344,17 +283,10 @@ var renameNodeCmd = &cobra.Command{
|
|||||||
|
|
||||||
response, err := client.RenameNode(ctx, request)
|
response, err := client.RenameNode(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("renaming node: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"Cannot rename node: %s\n",
|
|
||||||
status.Convert(err).Message(),
|
|
||||||
),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessOutput(response.GetNode(), "Node renamed", output)
|
return printOutput(cmd, response.GetNode(), "Node renamed")
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,16 +294,10 @@ var deleteNodeCmd = &cobra.Command{
|
|||||||
Use: "delete",
|
Use: "delete",
|
||||||
Short: "Delete a node",
|
Short: "Delete a node",
|
||||||
Aliases: []string{"del"},
|
Aliases: []string{"del"},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
|
||||||
|
|
||||||
identifier, err := cmd.Flags().GetUint64("identifier")
|
identifier, err := cmd.Flags().GetUint64("identifier")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("getting identifier flag: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error converting ID to integer: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getRequest := &v1.GetNodeRequest{
|
getRequest := &v1.GetNodeRequest{
|
||||||
@ -380,11 +306,7 @@ var deleteNodeCmd = &cobra.Command{
|
|||||||
|
|
||||||
getResponse, err := client.GetNode(ctx, getRequest)
|
getResponse, err := client.GetNode(ctx, getRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("getting node: %w", err)
|
||||||
err,
|
|
||||||
"Error getting node node: "+status.Convert(err).Message(),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteRequest := &v1.DeleteNodeRequest{
|
deleteRequest := &v1.DeleteNodeRequest{
|
||||||
@ -403,28 +325,20 @@ var deleteNodeCmd = &cobra.Command{
|
|||||||
|
|
||||||
if confirm || force {
|
if confirm || force {
|
||||||
response, err := client.DeleteNode(ctx, deleteRequest)
|
response, err := client.DeleteNode(ctx, deleteRequest)
|
||||||
if output != "" {
|
|
||||||
SuccessOutput(response, "", output)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("deleting node: %w", err)
|
||||||
err,
|
|
||||||
"Error deleting node: "+status.Convert(err).Message(),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessOutput(
|
_ = response // consumed for structured output if needed
|
||||||
|
|
||||||
|
return printOutput(
|
||||||
|
cmd,
|
||||||
map[string]string{"Result": "Node deleted"},
|
map[string]string{"Result": "Node deleted"},
|
||||||
"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 {
|
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 cancel()
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
@ -547,14 +464,12 @@ func nodesToPtables(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var expired string
|
var expired string
|
||||||
if expiry.IsZero() || expiry.After(time.Now()) {
|
if node.GetExpiry() != nil && node.GetExpiry().AsTime().Before(time.Now()) {
|
||||||
expired = pterm.LightGreen("no")
|
|
||||||
} else {
|
|
||||||
expired = pterm.LightRed("yes")
|
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
|
var tagsBuilder strings.Builder
|
||||||
|
|
||||||
for _, tag := range node.GetTags() {
|
for _, tag := range node.GetTags() {
|
||||||
@ -563,29 +478,26 @@ func nodesToPtables(
|
|||||||
|
|
||||||
tags := tagsBuilder.String()
|
tags := tagsBuilder.String()
|
||||||
|
|
||||||
tags = strings.TrimLeft(tags, "\n")
|
|
||||||
|
|
||||||
var user string
|
var user string
|
||||||
if currentUser == "" || (currentUser == node.GetUser().GetName()) {
|
if node.GetUser() != nil {
|
||||||
user = pterm.LightMagenta(node.GetUser().GetName())
|
user = node.GetUser().GetName()
|
||||||
} else {
|
|
||||||
// Shared into this user
|
|
||||||
user = pterm.LightYellow(node.GetUser().GetName())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
IPV4Address string
|
ipAddresses string
|
||||||
IPV6Address string
|
ipAddressesSb485 strings.Builder
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, addr := range node.GetIpAddresses() {
|
for _, addr := range node.GetIpAddresses() {
|
||||||
if netip.MustParseAddr(addr).Is4() {
|
ip, err := netip.ParseAddr(addr)
|
||||||
IPV4Address = addr
|
if err == nil {
|
||||||
} else {
|
ipAddressesSb485.WriteString(ip.String() + "\n")
|
||||||
IPV6Address = addr
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ipAddresses += ipAddressesSb485.String()
|
||||||
|
|
||||||
|
ipAddresses = strings.TrimRight(ipAddresses, "\n")
|
||||||
|
|
||||||
nodeData := []string{
|
nodeData := []string{
|
||||||
strconv.FormatUint(node.GetId(), util.Base10),
|
strconv.FormatUint(node.GetId(), util.Base10),
|
||||||
node.GetName(),
|
node.GetName(),
|
||||||
@ -594,7 +506,7 @@ func nodesToPtables(
|
|||||||
nodeKey.ShortString(),
|
nodeKey.ShortString(),
|
||||||
user,
|
user,
|
||||||
tags,
|
tags,
|
||||||
strings.Join([]string{IPV4Address, IPV6Address}, ", "),
|
ipAddresses,
|
||||||
strconv.FormatBool(ephemeral),
|
strconv.FormatBool(ephemeral),
|
||||||
lastSeenTime,
|
lastSeenTime,
|
||||||
expiryTime,
|
expiryTime,
|
||||||
@ -643,26 +555,16 @@ var tagCmd = &cobra.Command{
|
|||||||
Use: "tag",
|
Use: "tag",
|
||||||
Short: "Manage the tags of a node",
|
Short: "Manage the tags of a node",
|
||||||
Aliases: []string{"tags", "t"},
|
Aliases: []string{"tags", "t"},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
|
||||||
|
|
||||||
// retrieve flags from CLI
|
// retrieve flags from CLI
|
||||||
identifier, err := cmd.Flags().GetUint64("identifier")
|
identifier, err := cmd.Flags().GetUint64("identifier")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("getting identifier flag: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error converting ID to integer: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tagsToSet, err := cmd.Flags().GetStringSlice("tags")
|
tagsToSet, err := cmd.Flags().GetStringSlice("tags")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("getting tags flag: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error retrieving list of tags to add to node, %v", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sending tags to node
|
// Sending tags to node
|
||||||
@ -673,46 +575,30 @@ var tagCmd = &cobra.Command{
|
|||||||
|
|
||||||
resp, err := client.SetTags(ctx, request)
|
resp, err := client.SetTags(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("setting tags: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error while sending tags to headscale: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
SuccessOutput(
|
return printOutput(cmd, resp.GetNode(), "Node updated")
|
||||||
resp.GetNode(),
|
|
||||||
"Node updated",
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
var approveRoutesCmd = &cobra.Command{
|
var approveRoutesCmd = &cobra.Command{
|
||||||
Use: "approve-routes",
|
Use: "approve-routes",
|
||||||
Short: "Manage the approved routes of a node",
|
Short: "Manage the approved routes of a node",
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
|
||||||
|
|
||||||
// retrieve flags from CLI
|
// retrieve flags from CLI
|
||||||
identifier, err := cmd.Flags().GetUint64("identifier")
|
identifier, err := cmd.Flags().GetUint64("identifier")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("getting identifier flag: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error converting ID to integer: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
routes, err := cmd.Flags().GetStringSlice("routes")
|
routes, err := cmd.Flags().GetStringSlice("routes")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("getting routes flag: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error retrieving list of routes to add to node, %v", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sending routes to node
|
// Sending routes to node
|
||||||
@ -723,19 +609,13 @@ var approveRoutesCmd = &cobra.Command{
|
|||||||
|
|
||||||
resp, err := client.SetApprovedRoutes(ctx, request)
|
resp, err := client.SetApprovedRoutes(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("setting approved routes: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error while sending routes to headscale: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
SuccessOutput(
|
return printOutput(cmd, resp.GetNode(), "Node updated")
|
||||||
resp.GetNode(),
|
|
||||||
"Node updated",
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,7 +92,10 @@ var getPolicy = &cobra.Command{
|
|||||||
|
|
||||||
policy = pol.Data
|
policy = pol.Data
|
||||||
} else {
|
} 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 cancel()
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
@ -179,7 +182,10 @@ var setPolicy = &cobra.Command{
|
|||||||
} else {
|
} else {
|
||||||
request := &v1.SetPolicyRequest{Policy: string(policyBytes)}
|
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 cancel()
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
|
|||||||
@ -47,22 +47,16 @@ var listPreAuthKeys = &cobra.Command{
|
|||||||
Use: "list",
|
Use: "list",
|
||||||
Short: "List all preauthkeys",
|
Short: "List all preauthkeys",
|
||||||
Aliases: []string{"ls", "show"},
|
Aliases: []string{"ls", "show"},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
format, _ := cmd.Flags().GetString("output")
|
||||||
|
|
||||||
response, err := client.ListPreAuthKeys(ctx, &v1.ListPreAuthKeysRequest{})
|
response, err := client.ListPreAuthKeys(ctx, &v1.ListPreAuthKeysRequest{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("listing preauthkeys: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Error getting the list of keys: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if output != "" {
|
if format != "" {
|
||||||
SuccessOutput(response.GetPreAuthKeys(), "", output)
|
return printOutput(cmd, response.GetPreAuthKeys(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
tableData := pterm.TableData{
|
tableData := pterm.TableData{
|
||||||
@ -107,12 +101,10 @@ var listPreAuthKeys = &cobra.Command{
|
|||||||
|
|
||||||
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("rendering table: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Failed to render pterm table: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,9 +112,7 @@ var createPreAuthKeyCmd = &cobra.Command{
|
|||||||
Use: "create",
|
Use: "create",
|
||||||
Short: "Creates a new preauthkey",
|
Short: "Creates a new preauthkey",
|
||||||
Aliases: []string{"c", "new"},
|
Aliases: []string{"c", "new"},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
|
||||||
|
|
||||||
user, _ := cmd.Flags().GetUint64("user")
|
user, _ := cmd.Flags().GetUint64("user")
|
||||||
reusable, _ := cmd.Flags().GetBool("reusable")
|
reusable, _ := cmd.Flags().GetBool("reusable")
|
||||||
ephemeral, _ := cmd.Flags().GetBool("ephemeral")
|
ephemeral, _ := cmd.Flags().GetBool("ephemeral")
|
||||||
@ -139,11 +129,7 @@ var createPreAuthKeyCmd = &cobra.Command{
|
|||||||
|
|
||||||
duration, err := model.ParseDuration(durationStr)
|
duration, err := model.ParseDuration(durationStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("parsing duration: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Could not parse duration: %s\n", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
expiration := time.Now().UTC().Add(time.Duration(duration))
|
expiration := time.Now().UTC().Add(time.Duration(duration))
|
||||||
@ -152,14 +138,10 @@ var createPreAuthKeyCmd = &cobra.Command{
|
|||||||
|
|
||||||
response, err := client.CreatePreAuthKey(ctx, request)
|
response, err := client.CreatePreAuthKey(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("creating preauthkey: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Cannot create Pre Auth Key: %s\n", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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",
|
Use: "expire",
|
||||||
Short: "Expire a preauthkey",
|
Short: "Expire a preauthkey",
|
||||||
Aliases: []string{"revoke", "exp", "e"},
|
Aliases: []string{"revoke", "exp", "e"},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
|
||||||
id, _ := cmd.Flags().GetUint64("id")
|
id, _ := cmd.Flags().GetUint64("id")
|
||||||
|
|
||||||
if id == 0 {
|
if id == 0 {
|
||||||
ErrorOutput(
|
return fmt.Errorf("missing --id parameter: %w", errMissingParameter)
|
||||||
errMissingParameter,
|
|
||||||
"Error: missing --id parameter",
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request := &v1.ExpirePreAuthKeyRequest{
|
request := &v1.ExpirePreAuthKeyRequest{
|
||||||
@ -187,14 +162,10 @@ var expirePreAuthKeyCmd = &cobra.Command{
|
|||||||
|
|
||||||
response, err := client.ExpirePreAuthKey(ctx, request)
|
response, err := client.ExpirePreAuthKey(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("expiring preauthkey: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Cannot expire Pre Auth Key: %s\n", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessOutput(response, "Key expired", output)
|
return printOutput(cmd, response, "Key expired")
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,18 +173,11 @@ var deletePreAuthKeyCmd = &cobra.Command{
|
|||||||
Use: "delete",
|
Use: "delete",
|
||||||
Short: "Delete a preauthkey",
|
Short: "Delete a preauthkey",
|
||||||
Aliases: []string{"del", "rm", "d"},
|
Aliases: []string{"del", "rm", "d"},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
|
||||||
id, _ := cmd.Flags().GetUint64("id")
|
id, _ := cmd.Flags().GetUint64("id")
|
||||||
|
|
||||||
if id == 0 {
|
if id == 0 {
|
||||||
ErrorOutput(
|
return fmt.Errorf("missing --id parameter: %w", errMissingParameter)
|
||||||
errMissingParameter,
|
|
||||||
"Error: missing --id parameter",
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request := &v1.DeletePreAuthKeyRequest{
|
request := &v1.DeletePreAuthKeyRequest{
|
||||||
@ -222,13 +186,9 @@ var deletePreAuthKeyCmd = &cobra.Command{
|
|||||||
|
|
||||||
response, err := client.DeletePreAuthKey(ctx, request)
|
response, err := client.DeletePreAuthKey(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("deleting preauthkey: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Cannot delete Pre Auth Key: %s\n", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessOutput(response, "Key deleted", output)
|
return printOutput(cmd, response, "Key deleted")
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/pterm/pterm"
|
"github.com/pterm/pterm"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CLI user errors.
|
// CLI user errors.
|
||||||
@ -28,20 +27,21 @@ func usernameAndIDFlag(cmd *cobra.Command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// usernameAndIDFromFlag returns the username and ID from the flags of the 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, error) {
|
||||||
func usernameAndIDFromFlag(cmd *cobra.Command) (uint64, string) {
|
|
||||||
username, _ := cmd.Flags().GetString("name")
|
username, _ := cmd.Flags().GetString("name")
|
||||||
|
|
||||||
identifier, _ := cmd.Flags().GetInt64("identifier")
|
identifier, _ := cmd.Flags().GetInt64("identifier")
|
||||||
if username == "" && identifier < 0 {
|
if username == "" && identifier < 0 {
|
||||||
ErrorOutput(
|
return 0, "", errFlagRequired
|
||||||
errFlagRequired,
|
|
||||||
"Cannot rename user: "+status.Convert(errFlagRequired).Message(),
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
func init() {
|
||||||
@ -81,9 +81,7 @@ var createUserCmd = &cobra.Command{
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
|
||||||
|
|
||||||
userName := args[0]
|
userName := args[0]
|
||||||
|
|
||||||
log.Trace().Interface(zf.Client, client).Msg("obtained gRPC client")
|
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 pictureURL, _ := cmd.Flags().GetString("picture-url"); pictureURL != "" {
|
||||||
if _, err := url.Parse(pictureURL); err != nil { //nolint:noinlineerr
|
if _, err := url.Parse(pictureURL); err != nil { //nolint:noinlineerr
|
||||||
ErrorOutput(
|
return fmt.Errorf("invalid picture URL: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"Invalid Picture URL: %s",
|
|
||||||
err,
|
|
||||||
),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request.PictureUrl = pictureURL
|
request.PictureUrl = pictureURL
|
||||||
@ -117,14 +108,10 @@ var createUserCmd = &cobra.Command{
|
|||||||
|
|
||||||
response, err := client.CreateUser(ctx, request)
|
response, err := client.CreateUser(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("creating user: %w", err)
|
||||||
err,
|
|
||||||
"Cannot create user: "+status.Convert(err).Message(),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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",
|
Use: "destroy --identifier ID or --name NAME",
|
||||||
Short: "Destroys a user",
|
Short: "Destroys a user",
|
||||||
Aliases: []string{"delete"},
|
Aliases: []string{"delete"},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
id, username, err := usernameAndIDFromFlag(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
id, username := usernameAndIDFromFlag(cmd)
|
|
||||||
request := &v1.ListUsersRequest{
|
request := &v1.ListUsersRequest{
|
||||||
Name: username,
|
Name: username,
|
||||||
Id: id,
|
Id: id,
|
||||||
@ -143,20 +132,11 @@ var destroyUserCmd = &cobra.Command{
|
|||||||
|
|
||||||
users, err := client.ListUsers(ctx, request)
|
users, err := client.ListUsers(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("listing users: %w", err)
|
||||||
err,
|
|
||||||
"Error: "+status.Convert(err).Message(),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(users.GetUsers()) != 1 {
|
if len(users.GetUsers()) != 1 {
|
||||||
err := errMultipleUsersMatch
|
return errMultipleUsersMatch
|
||||||
ErrorOutput(
|
|
||||||
err,
|
|
||||||
"Error: "+status.Convert(err).Message(),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user := users.GetUsers()[0]
|
user := users.GetUsers()[0]
|
||||||
@ -176,17 +156,13 @@ var destroyUserCmd = &cobra.Command{
|
|||||||
|
|
||||||
response, err := client.DeleteUser(ctx, request)
|
response, err := client.DeleteUser(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("destroying user: %w", err)
|
||||||
err,
|
|
||||||
"Cannot destroy user: "+status.Convert(err).Message(),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessOutput(response, "User destroyed", output)
|
return printOutput(cmd, response, "User destroyed")
|
||||||
} else {
|
|
||||||
SuccessOutput(map[string]string{"Result": "User not destroyed"}, "User not destroyed", output)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return printOutput(cmd, map[string]string{"Result": "User not destroyed"}, "User not destroyed")
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,8 +170,8 @@ var listUsersCmd = &cobra.Command{
|
|||||||
Use: "list",
|
Use: "list",
|
||||||
Short: "List all the users",
|
Short: "List all the users",
|
||||||
Aliases: []string{"ls", "show"},
|
Aliases: []string{"ls", "show"},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
format, _ := cmd.Flags().GetString("output")
|
||||||
|
|
||||||
request := &v1.ListUsersRequest{}
|
request := &v1.ListUsersRequest{}
|
||||||
|
|
||||||
@ -215,15 +191,11 @@ var listUsersCmd = &cobra.Command{
|
|||||||
|
|
||||||
response, err := client.ListUsers(ctx, request)
|
response, err := client.ListUsers(ctx, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("listing users: %w", err)
|
||||||
err,
|
|
||||||
"Cannot get users: "+status.Convert(err).Message(),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if output != "" {
|
if format != "" {
|
||||||
SuccessOutput(response.GetUsers(), "", output)
|
return printOutput(cmd, response.GetUsers(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
tableData := pterm.TableData{{"ID", "Name", "Username", "Email", "Created"}}
|
tableData := pterm.TableData{{"ID", "Name", "Username", "Email", "Created"}}
|
||||||
@ -242,12 +214,10 @@ var listUsersCmd = &cobra.Command{
|
|||||||
|
|
||||||
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("rendering table: %w", err)
|
||||||
err,
|
|
||||||
fmt.Sprintf("Failed to render pterm table: %s", err),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,10 +225,12 @@ var renameUserCmd = &cobra.Command{
|
|||||||
Use: "rename",
|
Use: "rename",
|
||||||
Short: "Renames a user",
|
Short: "Renames a user",
|
||||||
Aliases: []string{"mv"},
|
Aliases: []string{"mv"},
|
||||||
Run: grpcRun(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) {
|
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||||
output, _ := cmd.Flags().GetString("output")
|
id, username, err := usernameAndIDFromFlag(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
id, username := usernameAndIDFromFlag(cmd)
|
|
||||||
listReq := &v1.ListUsersRequest{
|
listReq := &v1.ListUsersRequest{
|
||||||
Name: username,
|
Name: username,
|
||||||
Id: id,
|
Id: id,
|
||||||
@ -266,20 +238,11 @@ var renameUserCmd = &cobra.Command{
|
|||||||
|
|
||||||
users, err := client.ListUsers(ctx, listReq)
|
users, err := client.ListUsers(ctx, listReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("listing users: %w", err)
|
||||||
err,
|
|
||||||
"Error: "+status.Convert(err).Message(),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(users.GetUsers()) != 1 {
|
if len(users.GetUsers()) != 1 {
|
||||||
err := errMultipleUsersMatch
|
return errMultipleUsersMatch
|
||||||
ErrorOutput(
|
|
||||||
err,
|
|
||||||
"Error: "+status.Convert(err).Message(),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newName, _ := cmd.Flags().GetString("new-name")
|
newName, _ := cmd.Flags().GetString("new-name")
|
||||||
@ -291,13 +254,9 @@ var renameUserCmd = &cobra.Command{
|
|||||||
|
|
||||||
response, err := client.RenameUser(ctx, renameReq)
|
response, err := client.RenameUser(ctx, renameReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorOutput(
|
return fmt.Errorf("renaming user: %w", err)
|
||||||
err,
|
|
||||||
"Cannot rename user: "+status.Convert(err).Message(),
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessOutput(response.GetUser(), "User renamed", output)
|
return printOutput(cmd, response.GetUser(), "User renamed")
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@ -23,8 +24,14 @@ import (
|
|||||||
const (
|
const (
|
||||||
HeadscaleDateTimeFormat = "2006-01-02 15:04:05"
|
HeadscaleDateTimeFormat = "2006-01-02 15:04:05"
|
||||||
SocketWritePermissions = 0o666
|
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) {
|
func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) {
|
||||||
cfg, err := types.LoadServerConfig()
|
cfg, err := types.LoadServerConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -42,29 +49,28 @@ func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) {
|
|||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// grpcRun wraps a cobra RunFunc, injecting a ready gRPC client and context.
|
// grpcRunE wraps a cobra RunE func, injecting a ready gRPC client and
|
||||||
// Connection lifecycle is managed by the wrapper — callers never see
|
// context. Connection lifecycle is managed by the wrapper — callers
|
||||||
// the underlying conn or cancel func.
|
// never see the underlying conn or cancel func.
|
||||||
func grpcRun(
|
func grpcRunE(
|
||||||
fn func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string),
|
fn func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error,
|
||||||
) func(*cobra.Command, []string) {
|
) func(*cobra.Command, []string) error {
|
||||||
return func(cmd *cobra.Command, args []string) {
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
ctx, client, conn, cancel := newHeadscaleCLIWithConfig()
|
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connecting to headscale: %w", err)
|
||||||
|
}
|
||||||
defer cancel()
|
defer cancel()
|
||||||
defer conn.Close()
|
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()
|
cfg, err := types.LoadCLIConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().
|
return nil, nil, nil, nil, fmt.Errorf("loading configuration: %w", err)
|
||||||
Err(err).
|
|
||||||
Caller().
|
|
||||||
Msgf("Failed to load configuration")
|
|
||||||
os.Exit(-1) // we get here if logging is suppressed (i.e., json output)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().
|
log.Debug().
|
||||||
@ -88,18 +94,23 @@ func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *g
|
|||||||
address = cfg.UnixSocket
|
address = cfg.UnixSocket
|
||||||
|
|
||||||
// Try to give the user better feedback if we cannot write to the headscale
|
// Try to give the user better feedback if we cannot write to the headscale
|
||||||
// socket.
|
// socket. Note: os.OpenFile on a Unix domain socket returns ENXIO on
|
||||||
socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) // nolint
|
// 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 err != nil {
|
||||||
if os.IsPermission(err) {
|
if os.IsPermission(err) {
|
||||||
log.Fatal().
|
cancel()
|
||||||
Err(err).
|
|
||||||
Str("socket", cfg.UnixSocket).
|
|
||||||
Msgf("Unable to read/write to headscale socket, do you have the correct permissions?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = append(
|
||||||
grpcOptions,
|
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
|
// If we are not connecting to a local server, require an API key for authentication
|
||||||
apiKey := cfg.CLI.APIKey
|
apiKey := cfg.CLI.APIKey
|
||||||
if 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,
|
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
|
conn, err := grpc.DialContext(ctx, address, grpcOptions...) //nolint:staticcheck // SA1019: deprecated but supported in 1.x
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Caller().Err(err).Msgf("could not connect: %v", err)
|
cancel()
|
||||||
os.Exit(-1) // we get here if logging is suppressed (i.e., json output)
|
|
||||||
|
return nil, nil, nil, nil, fmt.Errorf("connecting to %s: %w", address, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := v1.NewHeadscaleServiceClient(conn)
|
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 {
|
func output(result any, override string, outputFormat string) string {
|
||||||
var (
|
var (
|
||||||
jsonBytes []byte
|
jsonBytes []byte
|
||||||
@ -157,17 +220,17 @@ func output(result any, override string, outputFormat string) string {
|
|||||||
)
|
)
|
||||||
|
|
||||||
switch outputFormat {
|
switch outputFormat {
|
||||||
case "json":
|
case outputFormatJSON:
|
||||||
jsonBytes, err = json.MarshalIndent(result, "", "\t")
|
jsonBytes, err = json.MarshalIndent(result, "", "\t")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("unmarshalling output")
|
log.Fatal().Err(err).Msg("unmarshalling output")
|
||||||
}
|
}
|
||||||
case "json-line":
|
case outputFormatJSONLine:
|
||||||
jsonBytes, err = json.Marshal(result)
|
jsonBytes, err = json.Marshal(result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("unmarshalling output")
|
log.Fatal().Err(err).Msg("unmarshalling output")
|
||||||
}
|
}
|
||||||
case "yaml":
|
case outputFormatYAML:
|
||||||
jsonBytes, err = yaml.Marshal(result)
|
jsonBytes, err = yaml.Marshal(result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("unmarshalling output")
|
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.
|
// SuccessOutput prints the result to stdout and exits with status code 0.
|
||||||
|
//
|
||||||
|
// Deprecated: use printOutput instead.
|
||||||
func SuccessOutput(result any, override string, outputFormat string) {
|
func SuccessOutput(result any, override string, outputFormat string) {
|
||||||
fmt.Println(output(result, override, outputFormat))
|
fmt.Println(output(result, override, outputFormat))
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorOutput prints an error message to stderr and exits with status code 1.
|
// 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) {
|
func ErrorOutput(errResult error, override string, outputFormat string) {
|
||||||
type errOutput struct {
|
type errOutput struct {
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
@ -216,11 +283,11 @@ func printError(err error, outputFormat string) {
|
|||||||
var formatted []byte
|
var formatted []byte
|
||||||
|
|
||||||
switch outputFormat {
|
switch outputFormat {
|
||||||
case "json":
|
case outputFormatJSON:
|
||||||
formatted, _ = json.MarshalIndent(e, "", "\t") //nolint:errchkjson // errOutput contains only a string field
|
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
|
formatted, _ = json.Marshal(e) //nolint:errchkjson // errOutput contains only a string field
|
||||||
case "yaml":
|
case outputFormatYAML:
|
||||||
formatted, _ = yaml.Marshal(e)
|
formatted, _ = yaml.Marshal(e)
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
||||||
@ -233,7 +300,7 @@ func printError(err error, outputFormat string) {
|
|||||||
|
|
||||||
func HasMachineOutputFlag() bool {
|
func HasMachineOutputFlag() bool {
|
||||||
for _, arg := range os.Args {
|
for _, arg := range os.Args {
|
||||||
if arg == "json" || arg == "json-line" || arg == "yaml" {
|
if arg == outputFormatJSON || arg == outputFormatJSONLine || arg == outputFormatYAML {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user