This commit is contained in:
Kristoffer Dalby 2025-07-15 14:51:23 +00:00
parent 024ed59ea9
commit 8253d588c6
31 changed files with 300 additions and 364 deletions

View File

@ -41,6 +41,47 @@ systemctl start headscale
### BREAKING
- **CLI: Remove deprecated flags**
- `--identifier` flag removed - use `--node` or `--user` instead
- `--namespace` flag removed - use `--user` instead
**Command changes:**
```bash
# Before
headscale nodes expire --identifier 123
headscale nodes rename --identifier 123 new-name
headscale nodes delete --identifier 123
headscale nodes move --identifier 123 --user 456
headscale nodes list-routes --identifier 123
# After
headscale nodes expire --node 123
headscale nodes rename --node 123 new-name
headscale nodes delete --node 123
headscale nodes move --node 123 --user 456
headscale nodes list-routes --node 123
# Before
headscale users destroy --identifier 123
headscale users rename --identifier 123 --new-name john
headscale users list --identifier 123
# After
headscale users destroy --user 123
headscale users rename --user 123 --new-name john
headscale users list --user 123
# Before
headscale nodes register --namespace myuser nodekey
headscale nodes list --namespace myuser
headscale preauthkeys create --namespace myuser
# After
headscale nodes register --user myuser nodekey
headscale nodes list --user myuser
headscale preauthkeys create --user myuser
```
- Policy: Zero or empty destination port is no longer allowed
[#2606](https://github.com/juanfont/headscale/pull/2606)

View File

@ -15,7 +15,6 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
)
func init() {
rootCmd.AddCommand(apiKeysCmd)
apiKeysCmd.AddCommand(listAPIKeys)
@ -98,7 +97,6 @@ var listAPIKeys = &cobra.Command{
}
return nil
})
if err != nil {
return
}
@ -148,7 +146,6 @@ If you loose a key, create a new one and revoke (expire) the old one.`,
SuccessOutput(response.GetApiKey(), response.GetApiKey(), output)
return nil
})
if err != nil {
return
}
@ -185,7 +182,6 @@ var expireAPIKeyCmd = &cobra.Command{
SuccessOutput(response, "Key expired", output)
return nil
})
if err != nil {
return
}
@ -222,7 +218,6 @@ var deleteAPIKeyCmd = &cobra.Command{
SuccessOutput(response, "Key deleted", output)
return nil
})
if err != nil {
return
}

View File

@ -15,11 +15,6 @@ const (
errPreAuthKeyMalformed = Error("key is malformed. expected 64 hex characters with `nodekey` prefix")
)
// Error is used to compare errors as per https://dave.cheney.net/2016/04/07/constant-errors
type Error string
func (e Error) Error() string { return string(e) }
func init() {
rootCmd.AddCommand(debugCmd)
@ -30,11 +25,6 @@ func init() {
}
createNodeCmd.Flags().StringP("user", "u", "", "User")
createNodeCmd.Flags().StringP("namespace", "n", "", "User")
createNodeNamespaceFlag := createNodeCmd.Flags().Lookup("namespace")
createNodeNamespaceFlag.Deprecated = deprecateNamespaceMessage
createNodeNamespaceFlag.Hidden = true
err = createNodeCmd.MarkFlagRequired("user")
if err != nil {
log.Fatal().Err(err).Msg("")
@ -60,7 +50,7 @@ var createNodeCmd = &cobra.Command{
Use: "create-node",
Short: "Create a node that can be registered with `nodes register <>` command",
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
user, err := cmd.Flags().GetString("user")
if err != nil {
@ -129,7 +119,6 @@ var createNodeCmd = &cobra.Command{
SuccessOutput(response.GetNode(), "Node created", output)
return nil
})
if err != nil {
return
}

View File

@ -63,12 +63,6 @@ func TestCreateNodeCommandFlags(t *testing.T) {
assert.NotNil(t, routeFlag)
assert.Equal(t, "r", routeFlag.Shorthand)
// Test deprecated namespace flag
namespaceFlag := createNodeCmd.Flags().Lookup("namespace")
assert.NotNil(t, namespaceFlag)
assert.Equal(t, "n", namespaceFlag.Shorthand)
assert.True(t, namespaceFlag.Hidden, "Namespace flag should be hidden")
assert.Equal(t, deprecateNamespaceMessage, namespaceFlag.Deprecated)
}
func TestCreateNodeCommandRequiredFlags(t *testing.T) {
@ -134,8 +128,6 @@ func TestCreateNodeCommandFlagDescriptions(t *testing.T) {
routeFlag := createNodeCmd.Flags().Lookup("route")
assert.Contains(t, routeFlag.Usage, "routes to advertise")
namespaceFlag := createNodeCmd.Flags().Lookup("namespace")
assert.Equal(t, "User", namespaceFlag.Usage) // Same as user flag
}
// Note: We can't easily test the actual execution of create-node because:

View File

@ -22,7 +22,7 @@ var generatePrivateKeyCmd = &cobra.Command{
Use: "private-key",
Short: "Generate a private key for the headscale server",
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
machineKey := key.NewMachine()
machineKeyStr, err := machineKey.MarshalText()

View File

@ -15,6 +15,11 @@ import (
"github.com/spf13/cobra"
)
// Error is used to compare errors as per https://dave.cheney.net/2016/04/07/constant-errors
type Error string
func (e Error) Error() string { return string(e) }
const (
errMockOidcClientIDNotDefined = Error("MOCKOIDC_CLIENT_ID not defined")
errMockOidcClientSecretNotDefined = Error("MOCKOIDC_CLIENT_SECRET not defined")

View File

@ -32,27 +32,13 @@ func init() {
// Display options
listNodesCmd.Flags().BoolP("tags", "t", false, "Show tags")
listNodesCmd.Flags().String("columns", "", "Comma-separated list of columns to display")
// Backward compatibility
listNodesCmd.Flags().StringP("namespace", "n", "", "User")
listNodesNamespaceFlag := listNodesCmd.Flags().Lookup("namespace")
listNodesNamespaceFlag.Deprecated = deprecateNamespaceMessage
listNodesNamespaceFlag.Hidden = true
nodeCmd.AddCommand(listNodesCmd)
listNodeRoutesCmd.Flags().StringP("node", "n", "", "Node identifier (ID, name, hostname, or IP)")
listNodeRoutesCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID) - deprecated, use --node")
identifierFlag := listNodeRoutesCmd.Flags().Lookup("identifier")
identifierFlag.Deprecated = "use --node"
identifierFlag.Hidden = true
nodeCmd.AddCommand(listNodeRoutesCmd)
registerNodeCmd.Flags().StringP("user", "u", "", "User")
registerNodeCmd.Flags().StringP("namespace", "n", "", "User")
registerNodeNamespaceFlag := registerNodeCmd.Flags().Lookup("namespace")
registerNodeNamespaceFlag.Deprecated = deprecateNamespaceMessage
registerNodeNamespaceFlag.Hidden = true
err := registerNodeCmd.MarkFlagRequired("user")
if err != nil {
log.Fatal(err.Error())
@ -65,40 +51,24 @@ func init() {
nodeCmd.AddCommand(registerNodeCmd)
expireNodeCmd.Flags().StringP("node", "n", "", "Node identifier (ID, name, hostname, or IP)")
expireNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID) - deprecated, use --node")
identifierFlag = expireNodeCmd.Flags().Lookup("identifier")
identifierFlag.Deprecated = "use --node"
identifierFlag.Hidden = true
if err != nil {
log.Fatal(err.Error())
}
nodeCmd.AddCommand(expireNodeCmd)
renameNodeCmd.Flags().StringP("node", "n", "", "Node identifier (ID, name, hostname, or IP)")
renameNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID) - deprecated, use --node")
identifierFlag = renameNodeCmd.Flags().Lookup("identifier")
identifierFlag.Deprecated = "use --node"
identifierFlag.Hidden = true
if err != nil {
log.Fatal(err.Error())
}
nodeCmd.AddCommand(renameNodeCmd)
deleteNodeCmd.Flags().StringP("node", "n", "", "Node identifier (ID, name, hostname, or IP)")
deleteNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID) - deprecated, use --node")
identifierFlag = deleteNodeCmd.Flags().Lookup("identifier")
identifierFlag.Deprecated = "use --node"
identifierFlag.Hidden = true
if err != nil {
log.Fatal(err.Error())
}
nodeCmd.AddCommand(deleteNodeCmd)
moveNodeCmd.Flags().StringP("node", "n", "", "Node identifier (ID, name, hostname, or IP)")
moveNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID) - deprecated, use --node")
identifierFlag = moveNodeCmd.Flags().Lookup("identifier")
identifierFlag.Deprecated = "use --node"
identifierFlag.Hidden = true
if err != nil {
log.Fatal(err.Error())
@ -106,24 +76,19 @@ func init() {
moveNodeCmd.Flags().Uint64P("user", "u", 0, "New user")
moveNodeCmd.Flags().StringP("namespace", "n", "", "User")
moveNodeNamespaceFlag := moveNodeCmd.Flags().Lookup("namespace")
moveNodeNamespaceFlag.Deprecated = deprecateNamespaceMessage
moveNodeNamespaceFlag.Hidden = true
err = moveNodeCmd.MarkFlagRequired("user")
if err != nil {
log.Fatal(err.Error())
}
nodeCmd.AddCommand(moveNodeCmd)
tagCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
tagCmd.MarkFlagRequired("identifier")
tagCmd.Flags().StringP("node", "n", "", "Node identifier (ID, name, hostname, or IP)")
tagCmd.MarkFlagRequired("node")
tagCmd.Flags().StringSliceP("tags", "t", []string{}, "List of tags to add to the node")
nodeCmd.AddCommand(tagCmd)
approveRoutesCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
approveRoutesCmd.MarkFlagRequired("identifier")
approveRoutesCmd.Flags().StringP("node", "n", "", "Node identifier (ID, name, hostname, or IP)")
approveRoutesCmd.MarkFlagRequired("node")
approveRoutesCmd.Flags().StringSliceP("routes", "r", []string{}, `List of routes that will be approved (comma-separated, e.g. "10.0.0.0/8,192.168.0.0/24" or empty string to remove all approved routes)`)
nodeCmd.AddCommand(approveRoutesCmd)
@ -140,7 +105,7 @@ var registerNodeCmd = &cobra.Command{
Use: "register",
Short: "Registers a node to your network",
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
user, err := cmd.Flags().GetString("user")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output)
@ -181,7 +146,6 @@ var registerNodeCmd = &cobra.Command{
fmt.Sprintf("Node %s registered", response.GetNode().GetGivenName()), output)
return nil
})
if err != nil {
return
}
@ -207,9 +171,6 @@ var listNodesCmd = &cobra.Command{
if user, _ := cmd.Flags().GetString("user"); user != "" {
request.User = user
}
if namespace, _ := cmd.Flags().GetString("namespace"); namespace != "" {
request.User = namespace // backward compatibility
}
// Handle node filtering (new functionality)
if nodeFlag, _ := cmd.Flags().GetString("node"); nodeFlag != "" {
@ -267,7 +228,6 @@ var listNodesCmd = &cobra.Command{
}
return nil
})
if err != nil {
return
}
@ -279,7 +239,7 @@ var listNodeRoutesCmd = &cobra.Command{
Short: "List routes available on nodes",
Aliases: []string{"lsr", "routes"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
identifier, err := GetNodeIdentifier(cmd)
if err != nil {
ErrorOutput(
@ -339,7 +299,6 @@ var listNodeRoutesCmd = &cobra.Command{
}
return nil
})
if err != nil {
return
}
@ -352,7 +311,7 @@ var expireNodeCmd = &cobra.Command{
Long: "Expiring a node will keep the node in the database and force it to reauthenticate.",
Aliases: []string{"logout", "exp", "e"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
identifier, err := GetNodeIdentifier(cmd)
if err != nil {
@ -385,7 +344,6 @@ var expireNodeCmd = &cobra.Command{
SuccessOutput(response.GetNode(), "Node expired", output)
return nil
})
if err != nil {
return
}
@ -396,7 +354,7 @@ var renameNodeCmd = &cobra.Command{
Use: "rename NEW_NAME",
Short: "Renames a node in your network",
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
identifier, err := GetNodeIdentifier(cmd)
if err != nil {
@ -435,7 +393,6 @@ var renameNodeCmd = &cobra.Command{
SuccessOutput(response.GetNode(), "Node renamed", output)
return nil
})
if err != nil {
return
}
@ -447,7 +404,7 @@ var deleteNodeCmd = &cobra.Command{
Short: "Delete a node",
Aliases: []string{"del"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
identifier, err := GetNodeIdentifier(cmd)
if err != nil {
@ -477,7 +434,6 @@ var deleteNodeCmd = &cobra.Command{
nodeName = getResponse.GetNode().GetName()
return nil
})
if err != nil {
return
}
@ -523,7 +479,6 @@ var deleteNodeCmd = &cobra.Command{
)
return nil
})
if err != nil {
return
}
@ -538,7 +493,7 @@ var moveNodeCmd = &cobra.Command{
Short: "Move node to another user",
Aliases: []string{"mv"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
identifier, err := GetNodeIdentifier(cmd)
if err != nil {
@ -593,7 +548,6 @@ var moveNodeCmd = &cobra.Command{
SuccessOutput(moveResponse.GetNode(), "Node moved to another user", output)
return nil
})
if err != nil {
return
}
@ -618,7 +572,7 @@ it can be run to remove the IPs that should no longer
be assigned to nodes.`,
Run: func(cmd *cobra.Command, args []string) {
var err error
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
confirm := false
prompt := &survey.Confirm{
@ -643,7 +597,6 @@ be assigned to nodes.`,
SuccessOutput(changes, "Node IPs backfilled successfully", output)
return nil
})
if err != nil {
return
}
@ -829,7 +782,7 @@ var tagCmd = &cobra.Command{
Short: "Manage the tags of a node",
Aliases: []string{"tags", "t"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
// retrieve flags from CLI
identifier, err := GetNodeIdentifier(cmd)
@ -876,7 +829,6 @@ var tagCmd = &cobra.Command{
}
return nil
})
if err != nil {
return
}
@ -887,7 +839,7 @@ var approveRoutesCmd = &cobra.Command{
Use: "approve-routes",
Short: "Manage the approved routes of a node",
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
// retrieve flags from CLI
identifier, err := GetNodeIdentifier(cmd)
@ -934,7 +886,6 @@ var approveRoutesCmd = &cobra.Command{
}
return nil
})
if err != nil {
return
}

View File

@ -41,7 +41,7 @@ var getPolicy = &cobra.Command{
Short: "Print the current ACL Policy",
Aliases: []string{"show", "view", "fetch"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
err := WithClient(func(ctx context.Context, client v1.HeadscaleServiceClient) error {
request := &v1.GetPolicyRequest{}
@ -58,7 +58,6 @@ var getPolicy = &cobra.Command{
SuccessOutput("", response.GetPolicy(), "")
return nil
})
if err != nil {
return
}
@ -73,7 +72,7 @@ var setPolicy = &cobra.Command{
This command only works when the acl.policy_mode is set to "db", and the policy will be stored in the database.`,
Aliases: []string{"put", "update"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
policyPath, _ := cmd.Flags().GetString("file")
f, err := os.Open(policyPath)
@ -100,7 +99,6 @@ var setPolicy = &cobra.Command{
SuccessOutput(nil, "Policy updated.", "")
return nil
})
if err != nil {
return
}
@ -111,23 +109,26 @@ var checkPolicy = &cobra.Command{
Use: "check",
Short: "Check the Policy file for errors",
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
policyPath, _ := cmd.Flags().GetString("file")
f, err := os.Open(policyPath)
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error opening the policy file: %s", err), output)
return
}
defer f.Close()
policyBytes, err := io.ReadAll(f)
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error reading the policy file: %s", err), output)
return
}
_, err = policy.NewPolicyManager(policyBytes, nil, views.Slice[types.NodeView]{})
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error parsing the policy file: %s", err), output)
return
}
SuccessOutput(nil, "Policy is valid", "")

View File

@ -15,16 +15,10 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
)
func init() {
rootCmd.AddCommand(preauthkeysCmd)
preauthkeysCmd.PersistentFlags().Uint64P("user", "u", 0, "User identifier (ID)")
preauthkeysCmd.PersistentFlags().StringP("namespace", "n", "", "User")
pakNamespaceFlag := preauthkeysCmd.PersistentFlags().Lookup("namespace")
pakNamespaceFlag.Deprecated = deprecateNamespaceMessage
pakNamespaceFlag.Hidden = true
err := preauthkeysCmd.MarkPersistentFlagRequired("user")
if err != nil {
log.Fatal().Err(err).Msg("")
@ -53,7 +47,7 @@ var listPreAuthKeys = &cobra.Command{
Short: "List the preauthkeys for this user",
Aliases: []string{"ls", "show"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
user, err := cmd.Flags().GetUint64("user")
if err != nil {
@ -130,7 +124,6 @@ var listPreAuthKeys = &cobra.Command{
}
return nil
})
if err != nil {
return
}
@ -142,7 +135,7 @@ var createPreAuthKeyCmd = &cobra.Command{
Short: "Creates a new preauthkey in the specified user",
Aliases: []string{"c", "new"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
user, err := cmd.Flags().GetUint64("user")
if err != nil {
@ -195,7 +188,6 @@ var createPreAuthKeyCmd = &cobra.Command{
SuccessOutput(response.GetPreAuthKey(), response.GetPreAuthKey().GetKey(), output)
return nil
})
if err != nil {
return
}
@ -214,7 +206,7 @@ var expirePreAuthKeyCmd = &cobra.Command{
return nil
},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
user, err := cmd.Flags().GetUint64("user")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output)
@ -240,7 +232,6 @@ var expirePreAuthKeyCmd = &cobra.Command{
SuccessOutput(response, "Key expired", output)
return nil
})
if err != nil {
return
}

View File

@ -14,7 +14,6 @@ import (
"github.com/tcnksm/go-latest"
)
var cfgFile string = ""
func init() {

View File

@ -8,7 +8,6 @@ import (
)
const (
deprecateNamespaceMessage = "use --user"
HeadscaleDateTimeFormat = "2006-01-02 15:04:05"
DefaultAPIKeyExpiry = "90d"
DefaultPreAuthKeyExpiry = "1h"

View File

@ -18,16 +18,12 @@ import (
func usernameAndIDFlag(cmd *cobra.Command) {
cmd.Flags().StringP("user", "u", "", "User identifier (ID, name, or email)")
cmd.Flags().Uint64P("identifier", "i", 0, "User identifier (ID) - deprecated, use --user")
identifierFlag := cmd.Flags().Lookup("identifier")
identifierFlag.Deprecated = "use --user"
identifierFlag.Hidden = true
cmd.Flags().StringP("name", "n", "", "Username")
}
// usernameAndIDFromFlag returns the user ID using smart lookup.
// userIDFromFlag returns the user ID using smart lookup.
// If no user is specified, it will exit the program with an error.
func usernameAndIDFromFlag(cmd *cobra.Command) (uint64, string) {
func userIDFromFlag(cmd *cobra.Command) uint64 {
userID, err := GetUserIdentifier(cmd)
if err != nil {
ErrorOutput(
@ -37,7 +33,7 @@ func usernameAndIDFromFlag(cmd *cobra.Command) (uint64, string) {
)
}
return userID, ""
return userID
}
func init() {
@ -52,11 +48,6 @@ func init() {
listUsersCmd.Flags().Uint64P("id", "", 0, "Filter by user ID")
listUsersCmd.Flags().StringP("name", "n", "", "Filter by username")
listUsersCmd.Flags().StringP("email", "e", "", "Filter by email address")
// Backward compatibility (deprecated)
listUsersCmd.Flags().Uint64P("identifier", "i", 0, "Filter by user ID - deprecated, use --id")
identifierFlag := listUsersCmd.Flags().Lookup("identifier")
identifierFlag.Deprecated = "use --id"
identifierFlag.Hidden = true
listUsersCmd.Flags().String("columns", "", "Comma-separated list of columns to display (ID,Name,Username,Email,Created)")
userCmd.AddCommand(destroyUserCmd)
usernameAndIDFlag(destroyUserCmd)
@ -131,7 +122,6 @@ var createUserCmd = &cobra.Command{
SuccessOutput(response.GetUser(), "User created", output)
return nil
})
if err != nil {
return
}
@ -145,9 +135,8 @@ var destroyUserCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
output := GetOutputFlag(cmd)
id, username := usernameAndIDFromFlag(cmd)
id := userIDFromFlag(cmd)
request := &v1.ListUsersRequest{
Name: username,
Id: id,
}
@ -176,7 +165,6 @@ var destroyUserCmd = &cobra.Command{
user = users.GetUsers()[0]
return nil
})
if err != nil {
return
}
@ -212,7 +200,6 @@ var destroyUserCmd = &cobra.Command{
SuccessOutput(response, "User destroyed", output)
return nil
})
if err != nil {
return
}
@ -247,8 +234,6 @@ var listUsersCmd = &cobra.Command{
// Check specific filter flags
if id, _ := cmd.Flags().GetUint64("id"); id > 0 {
request.Id = id
} else if identifier, _ := cmd.Flags().GetUint64("identifier"); identifier > 0 {
request.Id = identifier // backward compatibility
} else if name, _ := cmd.Flags().GetString("name"); name != "" {
request.Name = name
} else if email, _ := cmd.Flags().GetString("email"); email != "" {
@ -296,7 +281,6 @@ var listUsersCmd = &cobra.Command{
}
return nil
})
if err != nil {
// Error already handled in closure
return
@ -311,12 +295,11 @@ var renameUserCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
output := GetOutputFlag(cmd)
id, username := usernameAndIDFromFlag(cmd)
id := userIDFromFlag(cmd)
newName, _ := cmd.Flags().GetString("new-name")
err := WithClient(func(ctx context.Context, client v1.HeadscaleServiceClient) error {
listReq := &v1.ListUsersRequest{
Name: username,
Id: id,
}
@ -358,7 +341,6 @@ var renameUserCmd = &cobra.Command{
SuccessOutput(response.GetUser(), "User renamed", output)
return nil
})
if err != nil {
return
}

View File

@ -22,10 +22,6 @@ import (
"gopkg.in/yaml.v3"
)
const (
SocketWritePermissions = 0o666
)
func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) {
cfg, err := types.LoadServerConfig()
if err != nil {
@ -75,7 +71,7 @@ func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *g
// 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, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, 0o666) // nolint
if err != nil {
if os.IsPermission(err) {
log.Fatal().
@ -210,15 +206,10 @@ func GetOutputFlag(cmd *cobra.Command) string {
return output
}
// GetNodeIdentifier returns the node ID using smart lookup via gRPC ListNodes call
func GetNodeIdentifier(cmd *cobra.Command) (uint64, error) {
nodeFlag, _ := cmd.Flags().GetString("node")
identifierFlag, _ := cmd.Flags().GetUint64("identifier")
// Check if --identifier (deprecated) was used
if identifierFlag > 0 {
return identifierFlag, nil
}
// Use --node flag
if nodeFlag == "" {
@ -270,7 +261,6 @@ func lookupNodeBySpecifier(specifier string) (uint64, error) {
nodeID = nodes[0].GetId()
return nil
})
if err != nil {
return 0, err
}
@ -295,7 +285,6 @@ func isIPAddress(s string) bool {
func GetUserIdentifier(cmd *cobra.Command) (uint64, error) {
userFlag, _ := cmd.Flags().GetString("user")
nameFlag, _ := cmd.Flags().GetString("name")
identifierFlag, _ := cmd.Flags().GetUint64("identifier")
var specifier string
@ -304,8 +293,6 @@ func GetUserIdentifier(cmd *cobra.Command) (uint64, error) {
specifier = userFlag
} else if nameFlag != "" {
specifier = nameFlag
} else if identifierFlag > 0 {
return identifierFlag, nil // Direct ID, no lookup needed
} else {
return 0, fmt.Errorf("--user flag is required")
}
@ -355,7 +342,6 @@ func lookupUserBySpecifier(specifier string) (uint64, error) {
userID = users[0].GetId()
return nil
})
if err != nil {
return 0, err
}

View File

@ -14,7 +14,7 @@ var versionCmd = &cobra.Command{
Short: "Print the version",
Long: "The version of headscale",
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
output := GetOutputFlag(cmd)
SuccessOutput(map[string]string{
"version": types.Version,
"commit": types.GitCommitHash,

2
go.mod
View File

@ -81,7 +81,7 @@ require (
modernc.org/libc v1.62.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.10.0 // indirect
modernc.org/sqlite v1.37.0 // indirect
modernc.org/sqlite v1.37.0
)
require (

View File

@ -4,6 +4,7 @@ import (
"cmp"
"encoding/json"
"fmt"
"slices"
"strconv"
"strings"
"testing"
@ -18,7 +19,6 @@ import (
"github.com/juanfont/headscale/integration/tsic"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
"tailscale.com/tailcfg"
)
@ -95,7 +95,7 @@ func TestUserCommand(t *testing.T) {
"users",
"rename",
"--output=json",
fmt.Sprintf("--identifier=%d", listUsers[1].GetId()),
fmt.Sprintf("--user=%d", listUsers[1].GetId()),
"--new-name=newname",
},
)
@ -161,7 +161,7 @@ func TestUserCommand(t *testing.T) {
"list",
"--output",
"json",
"--identifier=1",
"--user=1",
},
&listByID,
)
@ -187,7 +187,7 @@ func TestUserCommand(t *testing.T) {
"destroy",
"--force",
// Delete "user1"
"--identifier=1",
"--user=1",
},
)
assert.NoError(t, err)
@ -354,7 +354,10 @@ func TestPreAuthKeyCommand(t *testing.T) {
continue
}
assert.Equal(t, []string{"tag:test1", "tag:test2"}, listedPreAuthKeys[index].GetAclTags())
// Sort tags for consistent comparison
tags := listedPreAuthKeys[index].GetAclTags()
slices.Sort(tags)
assert.Equal(t, []string{"tag:test1", "tag:test2"}, tags)
}
// Test key expiry
@ -869,7 +872,7 @@ func TestNodeTagCommand(t *testing.T) {
"headscale",
"nodes",
"tag",
"-i", "1",
"--node", "1",
"-t", "tag:test",
"--output", "json",
},
@ -884,7 +887,7 @@ func TestNodeTagCommand(t *testing.T) {
"headscale",
"nodes",
"tag",
"-i", "2",
"--node", "2",
"-t", "wrong-tag",
"--output", "json",
},
@ -1259,7 +1262,7 @@ func TestNodeCommand(t *testing.T) {
"headscale",
"nodes",
"delete",
"--identifier",
"--node",
// Delete the last added machine
"4",
"--output",
@ -1385,7 +1388,7 @@ func TestNodeExpireCommand(t *testing.T) {
"headscale",
"nodes",
"expire",
"--identifier",
"--node",
strconv.FormatUint(listAll[idx].GetId(), 10),
},
)
@ -1511,7 +1514,7 @@ func TestNodeRenameCommand(t *testing.T) {
"headscale",
"nodes",
"rename",
"--identifier",
"--node",
strconv.FormatUint(listAll[idx].GetId(), 10),
fmt.Sprintf("newnode-%d", idx+1),
},
@ -1549,7 +1552,7 @@ func TestNodeRenameCommand(t *testing.T) {
"headscale",
"nodes",
"rename",
"--identifier",
"--node",
strconv.FormatUint(listAll[4].GetId(), 10),
strings.Repeat("t", 64),
},
@ -1649,7 +1652,7 @@ func TestNodeMoveCommand(t *testing.T) {
"headscale",
"nodes",
"move",
"--identifier",
"--node",
strconv.FormatUint(node.GetId(), 10),
"--user",
strconv.FormatUint(userMap["new-user"].GetId(), 10),
@ -1687,7 +1690,7 @@ func TestNodeMoveCommand(t *testing.T) {
"headscale",
"nodes",
"move",
"--identifier",
"--node",
nodeID,
"--user",
"999",
@ -1708,7 +1711,7 @@ func TestNodeMoveCommand(t *testing.T) {
"headscale",
"nodes",
"move",
"--identifier",
"--node",
nodeID,
"--user",
strconv.FormatUint(userMap["old-user"].GetId(), 10),
@ -1727,7 +1730,7 @@ func TestNodeMoveCommand(t *testing.T) {
"headscale",
"nodes",
"move",
"--identifier",
"--node",
nodeID,
"--user",
strconv.FormatUint(userMap["old-user"].GetId(), 10),

View File

@ -41,7 +41,7 @@ func TestDebugCommand(t *testing.T) {
// Help text should contain expected information
assert.Contains(t, result, "debug", "help should mention debug command")
assert.Contains(t, result, "debug and testing commands", "help should contain command description")
assert.Contains(t, result, "debugging and testing", "help should contain command description")
assert.Contains(t, result, "create-node", "help should mention create-node subcommand")
})

View File

@ -564,10 +564,10 @@ func TestUpdateHostnameFromClient(t *testing.T) {
_, err = headscale.Execute(
[]string{
"headscale",
"node",
"nodes",
"rename",
givenName,
"--identifier",
"--node",
strconv.FormatUint(node.GetId(), 10),
})
assertNoErr(t, err)
@ -702,7 +702,7 @@ func TestExpireNode(t *testing.T) {
// TODO(kradalby): This is Headscale specific and would not play nicely
// with other implementations of the ControlServer interface
result, err := headscale.Execute([]string{
"headscale", "nodes", "expire", "--identifier", "1", "--output", "json",
"headscale", "nodes", "expire", "--node", "1", "--output", "json",
})
assertNoErr(t, err)
@ -1060,7 +1060,7 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
"headscale",
"nodes",
"delete",
"--identifier",
"--node",
// Delete the last added machine
fmt.Sprintf("%d", nodeList[0].GetId()),
"--output",

View File

@ -320,7 +320,9 @@ func TestGenerateCommandEdgeCases(t *testing.T) {
// Should fail gracefully for non-existent subcommand
assert.Error(t, err, "should fail for non-existent subcommand")
if err != nil {
assert.NotContains(t, err.Error(), "panic", "should not panic on non-existent subcommand")
}
})
t.Run("test_generate_key_format_consistency", func(t *testing.T) {

View File

@ -1122,7 +1122,7 @@ func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (
command := []string{
"headscale", "nodes", "approve-routes",
"--output", "json",
"--identifier", strconv.FormatUint(id, 10),
"--node", strconv.FormatUint(id, 10),
"--routes=" + strings.Join(util.PrefixesToString(routes), ","),
}

View File

@ -112,7 +112,7 @@ func TestRouteCommand(t *testing.T) {
"headscale",
"nodes",
"list-routes",
"--identifier",
"--node",
fmt.Sprintf("%d", nodeID),
},
)
@ -124,7 +124,7 @@ func TestRouteCommand(t *testing.T) {
"headscale",
"nodes",
"approve-routes",
"--identifier",
"--node",
fmt.Sprintf("%d", nodeID),
"--routes",
"10.0.0.0/24",
@ -158,7 +158,7 @@ func TestRouteCommand(t *testing.T) {
"headscale",
"nodes",
"approve-routes",
"--identifier",
"--node",
fmt.Sprintf("%d", nodeID),
"--routes",
"", // Empty string removes all routes
@ -192,7 +192,7 @@ func TestRouteCommand(t *testing.T) {
"headscale",
"nodes",
"list-routes",
"--identifier",
"--node",
fmt.Sprintf("%d", nodeID),
"--output",
"json",
@ -231,7 +231,7 @@ func TestRouteCommandEdgeCases(t *testing.T) {
"headscale",
"nodes",
"list-routes",
"--identifier",
"--node",
"999999",
},
)
@ -246,7 +246,7 @@ func TestRouteCommandEdgeCases(t *testing.T) {
"headscale",
"nodes",
"approve-routes",
"--identifier",
"--node",
"1",
"--routes",
"invalid-cidr",
@ -287,7 +287,7 @@ func TestRouteCommandHelp(t *testing.T) {
// Verify help text contains expected information
assert.Contains(t, result, "list-routes", "help should mention list-routes command")
assert.Contains(t, result, "identifier", "help should mention identifier flag")
assert.Contains(t, result, "node", "help should mention node flag")
})
t.Run("test_approve_routes_help", func(t *testing.T) {
@ -303,7 +303,7 @@ func TestRouteCommandHelp(t *testing.T) {
// Verify help text contains expected information
assert.Contains(t, result, "approve-routes", "help should mention approve-routes command")
assert.Contains(t, result, "identifier", "help should mention identifier flag")
assert.Contains(t, result, "node", "help should mention node flag")
assert.Contains(t, result, "routes", "help should mention routes flag")
})
}