diff --git a/CHANGELOG.md b/CHANGELOG.md index adeac96f..e2fdd58d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/cmd/headscale/cli/api_key.go b/cmd/headscale/cli/api_key.go index a4d9ac0e..dbbcef64 100644 --- a/cmd/headscale/cli/api_key.go +++ b/cmd/headscale/cli/api_key.go @@ -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 } diff --git a/cmd/headscale/cli/client.go b/cmd/headscale/cli/client.go index 65bd9eba..e95b84ce 100644 --- a/cmd/headscale/cli/client.go +++ b/cmd/headscale/cli/client.go @@ -2,7 +2,7 @@ package cli import ( "context" - + v1 "github.com/juanfont/headscale/gen/go/headscale/v1" ) @@ -11,6 +11,6 @@ func WithClient(fn func(context.Context, v1.HeadscaleServiceClient) error) error ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() - + return fn(ctx, client) -} \ No newline at end of file +} diff --git a/cmd/headscale/cli/configtest_test.go b/cmd/headscale/cli/configtest_test.go index 4bee4a87..0d14cd12 100644 --- a/cmd/headscale/cli/configtest_test.go +++ b/cmd/headscale/cli/configtest_test.go @@ -37,10 +37,10 @@ func TestConfigTestCommandHelp(t *testing.T) { // 1. It depends on configuration files being present // 2. It calls log.Fatal() which would exit the test process // 3. It tries to initialize a full Headscale server -// +// // In a real refactor, we would: // 1. Extract the configuration validation logic to a testable function // 2. Return errors instead of calling log.Fatal() // 3. Accept configuration as a parameter instead of loading from global state // -// For now, we test the command structure and that it's properly wired up. \ No newline at end of file +// For now, we test the command structure and that it's properly wired up. diff --git a/cmd/headscale/cli/debug.go b/cmd/headscale/cli/debug.go index 331e9771..4591eaf9 100644 --- a/cmd/headscale/cli/debug.go +++ b/cmd/headscale/cli/debug.go @@ -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 } diff --git a/cmd/headscale/cli/debug_test.go b/cmd/headscale/cli/debug_test.go index 2d1becb1..ea352b75 100644 --- a/cmd/headscale/cli/debug_test.go +++ b/cmd/headscale/cli/debug_test.go @@ -41,7 +41,7 @@ func TestCreateNodeCommandInDebugCommand(t *testing.T) { func TestCreateNodeCommandFlags(t *testing.T) { // Test that create-node has the required flags - + // Test name flag nameFlag := createNodeCmd.Flags().Lookup("name") assert.NotNil(t, nameFlag) @@ -63,22 +63,16 @@ 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) { // Test that required flags are marked as required // We can't easily test the actual requirement enforcement without executing the command // But we can test that the flags exist and have the expected properties - + // These flags should be required based on the init() function requiredFlags := []string{"name", "user", "key"} - + for _, flagName := range requiredFlags { flag := createNodeCmd.Flags().Lookup(flagName) assert.NotNil(t, flag, "Required flag %s should exist", flagName) @@ -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: @@ -149,4 +141,4 @@ func TestCreateNodeCommandFlagDescriptions(t *testing.T) { // 3. Return errors instead of calling ErrorOutput/SuccessOutput // 4. Add validation functions that can be tested independently // -// For now, we test the command structure and flag configuration. \ No newline at end of file +// For now, we test the command structure and flag configuration. diff --git a/cmd/headscale/cli/generate.go b/cmd/headscale/cli/generate.go index 35906411..e49be33d 100644 --- a/cmd/headscale/cli/generate.go +++ b/cmd/headscale/cli/generate.go @@ -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() diff --git a/cmd/headscale/cli/generate_test.go b/cmd/headscale/cli/generate_test.go index df788c47..de14637e 100644 --- a/cmd/headscale/cli/generate_test.go +++ b/cmd/headscale/cli/generate_test.go @@ -18,17 +18,17 @@ func TestGenerateCommand(t *testing.T) { Use: "headscale", Short: "headscale - a Tailscale control server", } - + cmd.AddCommand(generateCmd) - + out := new(bytes.Buffer) cmd.SetOut(out) cmd.SetErr(out) cmd.SetArgs([]string{"generate", "--help"}) - + err := cmd.Execute() require.NoError(t, err) - + outStr := out.String() assert.Contains(t, outStr, "Generate commands") assert.Contains(t, outStr, "private-key") @@ -42,17 +42,17 @@ func TestGenerateCommandAlias(t *testing.T) { Use: "headscale", Short: "headscale - a Tailscale control server", } - + cmd.AddCommand(generateCmd) - + out := new(bytes.Buffer) cmd.SetOut(out) cmd.SetErr(out) cmd.SetArgs([]string{"gen", "--help"}) - + err := cmd.Execute() require.NoError(t, err) - + outStr := out.String() assert.Contains(t, outStr, "Generate commands") } @@ -77,7 +77,7 @@ func TestGeneratePrivateKeyCommand(t *testing.T) { expectYAML: false, }, { - name: "yaml output", + name: "yaml output", args: []string{"generate", "private-key", "--output", "yaml"}, expectJSON: false, expectYAML: true, @@ -89,15 +89,15 @@ func TestGeneratePrivateKeyCommand(t *testing.T) { // Note: This command calls SuccessOutput which exits the process // We can't test the actual execution easily without mocking // Instead, we test the command structure and that it exists - + cmd := &cobra.Command{ Use: "headscale", Short: "headscale - a Tailscale control server", } - + cmd.AddCommand(generateCmd) cmd.PersistentFlags().StringP("output", "o", "", "Output format") - + // Test that the command exists and can be found privateKeyCmd, _, err := cmd.Find([]string{"generate", "private-key"}) require.NoError(t, err) @@ -112,17 +112,17 @@ func TestGeneratePrivateKeyHelp(t *testing.T) { Use: "headscale", Short: "headscale - a Tailscale control server", } - + cmd.AddCommand(generateCmd) - + out := new(bytes.Buffer) cmd.SetOut(out) cmd.SetErr(out) cmd.SetArgs([]string{"generate", "private-key", "--help"}) - + err := cmd.Execute() require.NoError(t, err) - + outStr := out.String() assert.Contains(t, outStr, "Generate a private key for the headscale server") assert.Contains(t, outStr, "Usage:") @@ -132,10 +132,10 @@ func TestGeneratePrivateKeyHelp(t *testing.T) { func TestPrivateKeyGeneration(t *testing.T) { // We can't easily test the full command because it calls SuccessOutput which exits // But we can test that the key generation produces valid output format - + // This is testing the core logic that would be in the command // In a real refactor, we'd extract this to a testable function - + // For now, we can test that the command structure is correct assert.NotNil(t, generatePrivateKeyCmd) assert.Equal(t, "private-key", generatePrivateKeyCmd.Use) @@ -148,7 +148,7 @@ func TestGenerateCommandStructure(t *testing.T) { assert.Equal(t, "generate", generateCmd.Use) assert.Equal(t, "Generate commands", generateCmd.Short) assert.Contains(t, generateCmd.Aliases, "gen") - + // Test that private-key is a subcommand found := false for _, subcmd := range generateCmd.Commands() { @@ -167,31 +167,31 @@ func validatePrivateKeyOutput(t *testing.T, output string, format string) { var result map[string]interface{} err := json.Unmarshal([]byte(output), &result) require.NoError(t, err, "Output should be valid JSON") - + privateKey, exists := result["private_key"] require.True(t, exists, "JSON should contain private_key field") - + keyStr, ok := privateKey.(string) require.True(t, ok, "private_key should be a string") require.NotEmpty(t, keyStr, "private_key should not be empty") - + // Basic validation that it looks like a machine key assert.True(t, strings.HasPrefix(keyStr, "mkey:"), "Machine key should start with mkey:") - + case "yaml": var result map[string]interface{} err := yaml.Unmarshal([]byte(output), &result) require.NoError(t, err, "Output should be valid YAML") - + privateKey, exists := result["private_key"] require.True(t, exists, "YAML should contain private_key field") - + keyStr, ok := privateKey.(string) require.True(t, ok, "private_key should be a string") require.NotEmpty(t, keyStr, "private_key should not be empty") - + assert.True(t, strings.HasPrefix(keyStr, "mkey:"), "Machine key should start with mkey:") - + default: // Default format should just be the key itself assert.True(t, strings.HasPrefix(output, "mkey:"), "Default output should be the machine key") @@ -203,7 +203,7 @@ func validatePrivateKeyOutput(t *testing.T, output string, format string) { func TestPrivateKeyOutputFormats(t *testing.T) { // Test cases for different output formats // These test the validation logic we would use after refactoring - + tests := []struct { format string sample string @@ -213,7 +213,7 @@ func TestPrivateKeyOutputFormats(t *testing.T) { sample: `{"private_key": "mkey:abcd1234567890abcd1234567890abcd1234567890abcd1234567890abcd1234"}`, }, { - format: "yaml", + format: "yaml", sample: "private_key: mkey:abcd1234567890abcd1234567890abcd1234567890abcd1234567890abcd1234\n", }, { @@ -221,10 +221,10 @@ func TestPrivateKeyOutputFormats(t *testing.T) { sample: "mkey:abcd1234567890abcd1234567890abcd1234567890abcd1234567890abcd1234", }, } - + for _, tt := range tests { t.Run("format_"+tt.format, func(t *testing.T) { validatePrivateKeyOutput(t, tt.sample, tt.format) }) } -} \ No newline at end of file +} diff --git a/cmd/headscale/cli/mockoidc.go b/cmd/headscale/cli/mockoidc.go index 9969f7c6..e3c30a36 100644 --- a/cmd/headscale/cli/mockoidc.go +++ b/cmd/headscale/cli/mockoidc.go @@ -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") diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index d22dcccc..5202a04a 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -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 } @@ -202,15 +166,12 @@ var listNodesCmd = &cobra.Command{ err = WithClient(func(ctx context.Context, client v1.HeadscaleServiceClient) error { request := &v1.ListNodesRequest{} - + // Handle user filtering (existing functionality) 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 != "" { // Use smart lookup to determine filter type @@ -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 { @@ -412,7 +370,7 @@ var renameNodeCmd = &cobra.Command{ if len(args) > 0 { newName = args[0] } - + err = WithClient(func(ctx context.Context, client v1.HeadscaleServiceClient) error { request := &v1.RenameNodeRequest{ NodeId: identifier, @@ -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 } @@ -502,7 +458,7 @@ var deleteNodeCmd = &cobra.Command{ deleteRequest := &v1.DeleteNodeRequest{ NodeId: identifier, } - + response, err := client.DeleteNode(ctx, deleteRequest) if output != "" { SuccessOutput(response, "", output) @@ -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,8 +782,8 @@ 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) if err != nil { @@ -876,7 +829,6 @@ var tagCmd = &cobra.Command{ } return nil }) - if err != nil { return } @@ -887,8 +839,8 @@ 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) if err != nil { @@ -934,7 +886,6 @@ var approveRoutesCmd = &cobra.Command{ } return nil }) - if err != nil { return } diff --git a/cmd/headscale/cli/policy.go b/cmd/headscale/cli/policy.go index a939ed8a..5998d0d8 100644 --- a/cmd/headscale/cli/policy.go +++ b/cmd/headscale/cli/policy.go @@ -41,8 +41,8 @@ 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", "") diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go index 507f7050..0a7ca896 100644 --- a/cmd/headscale/cli/preauthkeys.go +++ b/cmd/headscale/cli/preauthkeys.go @@ -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 } diff --git a/cmd/headscale/cli/root.go b/cmd/headscale/cli/root.go index 86d150a6..b9ecee32 100644 --- a/cmd/headscale/cli/root.go +++ b/cmd/headscale/cli/root.go @@ -14,7 +14,6 @@ import ( "github.com/tcnksm/go-latest" ) - var cfgFile string = "" func init() { diff --git a/cmd/headscale/cli/serve_test.go b/cmd/headscale/cli/serve_test.go index f48282f2..39ae67f3 100644 --- a/cmd/headscale/cli/serve_test.go +++ b/cmd/headscale/cli/serve_test.go @@ -28,11 +28,11 @@ func TestServeCommandArgs(t *testing.T) { // Test that the Args function is defined and accepts any arguments // The current implementation always returns nil (accepts any args) assert.NotNil(t, serveCmd.Args) - + // Test the args function directly err := serveCmd.Args(serveCmd, []string{}) assert.NoError(t, err, "Args function should accept empty arguments") - + err = serveCmd.Args(serveCmd, []string{"extra", "args"}) assert.NoError(t, err, "Args function should accept extra arguments") } @@ -48,7 +48,7 @@ func TestServeCommandStructure(t *testing.T) { // Test basic command structure assert.Equal(t, "serve", serveCmd.Name()) assert.Equal(t, "Launches the headscale server", serveCmd.Short) - + // Test that it has no subcommands (it's a leaf command) subcommands := serveCmd.Commands() assert.Empty(t, subcommands, "Serve command should not have subcommands") @@ -67,4 +67,4 @@ func TestServeCommandStructure(t *testing.T) { // 4. Add graceful shutdown capabilities for testing // 5. Allow server startup to be cancelled via context // -// For now, we test the command structure and basic properties. \ No newline at end of file +// For now, we test the command structure and basic properties. diff --git a/cmd/headscale/cli/table_filter.go b/cmd/headscale/cli/table_filter.go index d2b0bcdb..b2a2ec85 100644 --- a/cmd/headscale/cli/table_filter.go +++ b/cmd/headscale/cli/table_filter.go @@ -8,10 +8,9 @@ import ( ) const ( - deprecateNamespaceMessage = "use --user" - HeadscaleDateTimeFormat = "2006-01-02 15:04:05" - DefaultAPIKeyExpiry = "90d" - DefaultPreAuthKeyExpiry = "1h" + HeadscaleDateTimeFormat = "2006-01-02 15:04:05" + DefaultAPIKeyExpiry = "90d" + DefaultPreAuthKeyExpiry = "1h" ) // FilterTableColumns filters table columns based on --columns flag @@ -23,7 +22,7 @@ func FilterTableColumns(cmd *cobra.Command, tableData pterm.TableData) pterm.Tab headers := tableData[0] wantedColumns := strings.Split(columns, ",") - + // Find column indices var indices []int for _, wanted := range wantedColumns { @@ -53,4 +52,4 @@ func FilterTableColumns(cmd *cobra.Command, tableData pterm.TableData) pterm.Tab } return filtered -} \ No newline at end of file +} diff --git a/cmd/headscale/cli/users.go b/cmd/headscale/cli/users.go index d68f2735..ddbbc713 100644 --- a/cmd/headscale/cli/users.go +++ b/cmd/headscale/cli/users.go @@ -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) @@ -117,7 +108,7 @@ var createUserCmd = &cobra.Command{ err := WithClient(func(ctx context.Context, client v1.HeadscaleServiceClient) error { log.Trace().Interface("client", client).Msg("Obtained gRPC client") log.Trace().Interface("request", request).Msg("Sending CreateUser request") - + response, err := client.CreateUser(ctx, request) if err != nil { ErrorOutput( @@ -131,7 +122,6 @@ var createUserCmd = &cobra.Command{ SuccessOutput(response.GetUser(), "User created", output) return nil }) - if err != nil { return } @@ -145,10 +135,9 @@ 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, + Id: id, } var user *v1.User @@ -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,13 +295,12 @@ 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, + Id: id, } users, err := client.ListUsers(ctx, listReq) @@ -358,7 +341,6 @@ var renameUserCmd = &cobra.Command{ SuccessOutput(response.GetUser(), "User renamed", output) return nil }) - if err != nil { return } diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index b2c29baf..fcdc99ed 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -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,21 +206,16 @@ 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 == "" { return 0, fmt.Errorf("--node flag is required") } - + // Use smart lookup via gRPC return lookupNodeBySpecifier(nodeFlag) } @@ -232,10 +223,10 @@ func GetNodeIdentifier(cmd *cobra.Command) (uint64, error) { // lookupNodeBySpecifier performs smart lookup of a node by ID, name, hostname, or IP func lookupNodeBySpecifier(specifier string) (uint64, error) { var nodeID uint64 - + err := WithClient(func(ctx context.Context, client v1.HeadscaleServiceClient) error { request := &v1.ListNodesRequest{} - + // Detect what type of specifier this is and set appropriate filter if id, err := strconv.ParseUint(specifier, 10, 64); err == nil && id > 0 { // Looks like a numeric ID @@ -247,17 +238,17 @@ func lookupNodeBySpecifier(specifier string) (uint64, error) { // Treat as hostname/name request.Name = specifier } - + response, err := client.ListNodes(ctx, request) if err != nil { return fmt.Errorf("failed to lookup node: %w", err) } - + nodes := response.GetNodes() if len(nodes) == 0 { return fmt.Errorf("no node found matching '%s'", specifier) } - + if len(nodes) > 1 { var nodeInfo []string for _, node := range nodes { @@ -265,16 +256,15 @@ func lookupNodeBySpecifier(specifier string) (uint64, error) { } return fmt.Errorf("multiple nodes found matching '%s': %s", specifier, strings.Join(nodeInfo, ", ")) } - + // Exactly one match - this is what we want nodeID = nodes[0].GetId() return nil }) - if err != nil { return 0, err } - + return nodeID, nil } @@ -295,21 +285,18 @@ 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 - + // Determine which flag was used (prefer --user, fall back to legacy flags) if userFlag != "" { 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") } - + // Use smart lookup via gRPC return lookupUserBySpecifier(specifier) } @@ -317,10 +304,10 @@ func GetUserIdentifier(cmd *cobra.Command) (uint64, error) { // lookupUserBySpecifier performs smart lookup of a user by ID, name, or email func lookupUserBySpecifier(specifier string) (uint64, error) { var userID uint64 - + err := WithClient(func(ctx context.Context, client v1.HeadscaleServiceClient) error { request := &v1.ListUsersRequest{} - + // Detect what type of specifier this is and set appropriate filter if id, err := strconv.ParseUint(specifier, 10, 64); err == nil && id > 0 { // Looks like a numeric ID @@ -332,17 +319,17 @@ func lookupUserBySpecifier(specifier string) (uint64, error) { // Treat as username request.Name = specifier } - + response, err := client.ListUsers(ctx, request) if err != nil { return fmt.Errorf("failed to lookup user: %w", err) } - + users := response.GetUsers() if len(users) == 0 { return fmt.Errorf("no user found matching '%s'", specifier) } - + if len(users) > 1 { var userInfo []string for _, user := range users { @@ -350,15 +337,14 @@ func lookupUserBySpecifier(specifier string) (uint64, error) { } return fmt.Errorf("multiple users found matching '%s': %s", specifier, strings.Join(userInfo, ", ")) } - + // Exactly one match - this is what we want userID = users[0].GetId() return nil }) - if err != nil { return 0, err } - + return userID, nil } diff --git a/cmd/headscale/cli/utils_test.go b/cmd/headscale/cli/utils_test.go index 380c255d..9fc0d619 100644 --- a/cmd/headscale/cli/utils_test.go +++ b/cmd/headscale/cli/utils_test.go @@ -172,4 +172,4 @@ func TestOutputWithEmptyData(t *testing.T) { emptyMap := map[string]string{} result = output(emptyMap, "fallback", "json") assert.Equal(t, "{}", result) -} \ No newline at end of file +} diff --git a/cmd/headscale/cli/version.go b/cmd/headscale/cli/version.go index 07289c76..1c2b34f3 100644 --- a/cmd/headscale/cli/version.go +++ b/cmd/headscale/cli/version.go @@ -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, diff --git a/cmd/headscale/cli/version_test.go b/cmd/headscale/cli/version_test.go index e383e02a..e2c91b68 100644 --- a/cmd/headscale/cli/version_test.go +++ b/cmd/headscale/cli/version_test.go @@ -39,7 +39,7 @@ func TestVersionCommandFlags(t *testing.T) { func TestVersionCommandRun(t *testing.T) { // Test that Run function is set assert.NotNil(t, versionCmd.Run) - + // We can't easily test the actual execution without mocking SuccessOutput // but we can verify the function exists and has the right signature -} \ No newline at end of file +} diff --git a/go.mod b/go.mod index 399cc807..f6cb8d62 100644 --- a/go.mod +++ b/go.mod @@ -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 ( diff --git a/integration/cli_test.go b/integration/cli_test.go index 7f4f9936..0d2bf41d 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -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 @@ -604,7 +607,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) { assert.EventuallyWithT(t, func(ct *assert.CollectT) { status, err := client.Status() assert.NoError(ct, err) - assert.NotContains(ct, []string{"Starting", "Running"}, status.BackendState, + assert.NotContains(ct, []string{"Starting", "Running"}, status.BackendState, "Expected node to be logged out, backend state: %s", status.BackendState) }, 30*time.Second, 2*time.Second) @@ -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), diff --git a/integration/debug_cli_test.go b/integration/debug_cli_test.go index 6727db31..e81ee7bf 100644 --- a/integration/debug_cli_test.go +++ b/integration/debug_cli_test.go @@ -38,10 +38,10 @@ func TestDebugCommand(t *testing.T) { }, ) assertNoErr(t, err) - + // 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") }) @@ -56,7 +56,7 @@ func TestDebugCommand(t *testing.T) { }, ) assertNoErr(t, err) - + // Help text should contain expected information assert.Contains(t, result, "create-node", "help should mention create-node command") assert.Contains(t, result, "name", "help should mention name flag") @@ -100,7 +100,7 @@ func TestDebugCreateNodeCommand(t *testing.T) { nodeName := "debug-test-node" // Generate a mock registration key (64 hex chars with nodekey prefix) registrationKey := "nodekey:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" - + result, err := headscale.Execute( []string{ "headscale", @@ -112,7 +112,7 @@ func TestDebugCreateNodeCommand(t *testing.T) { }, ) assertNoErr(t, err) - + // Should output node creation confirmation assert.Contains(t, result, "Node created", "should confirm node creation") assert.Contains(t, result, nodeName, "should mention the created node name") @@ -122,7 +122,7 @@ func TestDebugCreateNodeCommand(t *testing.T) { // Test debug create-node with advertised routes nodeName := "debug-route-node" registrationKey := "nodekey:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" - + result, err := headscale.Execute( []string{ "headscale", @@ -136,7 +136,7 @@ func TestDebugCreateNodeCommand(t *testing.T) { }, ) assertNoErr(t, err) - + // Should output node creation confirmation assert.Contains(t, result, "Node created", "should confirm node creation") assert.Contains(t, result, nodeName, "should mention the created node name") @@ -146,7 +146,7 @@ func TestDebugCreateNodeCommand(t *testing.T) { // Test debug create-node with JSON output nodeName := "debug-json-node" registrationKey := "nodekey:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321" - + result, err := headscale.Execute( []string{ "headscale", @@ -159,7 +159,7 @@ func TestDebugCreateNodeCommand(t *testing.T) { }, ) assertNoErr(t, err) - + // Should produce valid JSON output var node v1.Node err = json.Unmarshal([]byte(result), &node) @@ -200,7 +200,7 @@ func TestDebugCreateNodeCommandValidation(t *testing.T) { t.Run("test_debug_create_node_missing_name", func(t *testing.T) { // Test debug create-node with missing name flag registrationKey := "nodekey:1111111111111111111111111111111111111111111111111111111111111111" - + _, err := headscale.Execute( []string{ "headscale", @@ -217,7 +217,7 @@ func TestDebugCreateNodeCommandValidation(t *testing.T) { t.Run("test_debug_create_node_missing_user", func(t *testing.T) { // Test debug create-node with missing user flag registrationKey := "nodekey:2222222222222222222222222222222222222222222222222222222222222222" - + _, err := headscale.Execute( []string{ "headscale", @@ -265,7 +265,7 @@ func TestDebugCreateNodeCommandValidation(t *testing.T) { t.Run("test_debug_create_node_nonexistent_user", func(t *testing.T) { // Test debug create-node with non-existent user registrationKey := "nodekey:3333333333333333333333333333333333333333333333333333333333333333" - + _, err := headscale.Execute( []string{ "headscale", @@ -285,7 +285,7 @@ func TestDebugCreateNodeCommandValidation(t *testing.T) { nodeName := "duplicate-node" registrationKey1 := "nodekey:4444444444444444444444444444444444444444444444444444444444444444" registrationKey2 := "nodekey:5555555555555555555555555555555555555555555555555555555555555555" - + // Create first node _, err := headscale.Execute( []string{ @@ -298,7 +298,7 @@ func TestDebugCreateNodeCommandValidation(t *testing.T) { }, ) assertNoErr(t, err) - + // Try to create second node with same name _, err = headscale.Execute( []string{ @@ -348,7 +348,7 @@ func TestDebugCreateNodeCommandEdgeCases(t *testing.T) { // Test debug create-node with invalid route format nodeName := "invalid-route-node" registrationKey := "nodekey:6666666666666666666666666666666666666666666666666666666666666666" - + _, err := headscale.Execute( []string{ "headscale", @@ -368,7 +368,7 @@ func TestDebugCreateNodeCommandEdgeCases(t *testing.T) { // Test debug create-node with empty route nodeName := "empty-route-node" registrationKey := "nodekey:7777777777777777777777777777777777777777777777777777777777777777" - + result, err := headscale.Execute( []string{ "headscale", @@ -395,7 +395,7 @@ func TestDebugCreateNodeCommandEdgeCases(t *testing.T) { longName += "-very-long-segment" } registrationKey := "nodekey:8888888888888888888888888888888888888888888888888888888888888888" - + _, _ = headscale.Execute( []string{ "headscale", @@ -420,4 +420,4 @@ func TestDebugCreateNodeCommandEdgeCases(t *testing.T) { ) }, "should handle very long node names gracefully") }) -} \ No newline at end of file +} diff --git a/integration/embedded_derp_test.go b/integration/embedded_derp_test.go index 051b9261..e9ba69dd 100644 --- a/integration/embedded_derp_test.go +++ b/integration/embedded_derp_test.go @@ -145,9 +145,9 @@ func derpServerScenario( assert.NoError(ct, err, "Failed to get status for client %s", client.Hostname()) for _, health := range status.Health { - assert.NotContains(ct, health, "could not connect to any relay server", + assert.NotContains(ct, health, "could not connect to any relay server", "Client %s should be connected to DERP relay", client.Hostname()) - assert.NotContains(ct, health, "could not connect to the 'Headscale Embedded DERP' relay server.", + assert.NotContains(ct, health, "could not connect to the 'Headscale Embedded DERP' relay server.", "Client %s should be connected to Headscale Embedded DERP", client.Hostname()) } }, 30*time.Second, 2*time.Second) @@ -166,9 +166,9 @@ func derpServerScenario( assert.NoError(ct, err, "Failed to get status for client %s", client.Hostname()) for _, health := range status.Health { - assert.NotContains(ct, health, "could not connect to any relay server", + assert.NotContains(ct, health, "could not connect to any relay server", "Client %s should be connected to DERP relay after first run", client.Hostname()) - assert.NotContains(ct, health, "could not connect to the 'Headscale Embedded DERP' relay server.", + assert.NotContains(ct, health, "could not connect to the 'Headscale Embedded DERP' relay server.", "Client %s should be connected to Headscale Embedded DERP after first run", client.Hostname()) } }, 30*time.Second, 2*time.Second) @@ -191,9 +191,9 @@ func derpServerScenario( assert.NoError(ct, err, "Failed to get status for client %s", client.Hostname()) for _, health := range status.Health { - assert.NotContains(ct, health, "could not connect to any relay server", + assert.NotContains(ct, health, "could not connect to any relay server", "Client %s should be connected to DERP relay after second run", client.Hostname()) - assert.NotContains(ct, health, "could not connect to the 'Headscale Embedded DERP' relay server.", + assert.NotContains(ct, health, "could not connect to the 'Headscale Embedded DERP' relay server.", "Client %s should be connected to Headscale Embedded DERP after second run", client.Hostname()) } }, 30*time.Second, 2*time.Second) diff --git a/integration/general_test.go b/integration/general_test.go index 0e1a8da5..da37bce4 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -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", diff --git a/integration/generate_cli_test.go b/integration/generate_cli_test.go index 35d9ae5a..5e2c3dc8 100644 --- a/integration/generate_cli_test.go +++ b/integration/generate_cli_test.go @@ -37,7 +37,7 @@ func TestGenerateCommand(t *testing.T) { }, ) assertNoErr(t, err) - + // Help text should contain expected information assert.Contains(t, result, "generate", "help should mention generate command") assert.Contains(t, result, "Generate commands", "help should contain command description") @@ -54,7 +54,7 @@ func TestGenerateCommand(t *testing.T) { }, ) assertNoErr(t, err) - + // Should work with alias assert.Contains(t, result, "generate", "alias should work and show generate help") assert.Contains(t, result, "private-key", "alias help should mention private-key subcommand") @@ -71,7 +71,7 @@ func TestGenerateCommand(t *testing.T) { }, ) assertNoErr(t, err) - + // Help text should contain expected information assert.Contains(t, result, "private-key", "help should mention private-key command") assert.Contains(t, result, "Generate a private key", "help should contain command description") @@ -105,17 +105,17 @@ func TestGeneratePrivateKeyCommand(t *testing.T) { }, ) assertNoErr(t, err) - + // Should output a private key assert.NotEmpty(t, result, "private key generation should produce output") - + // Private key should start with expected prefix trimmed := strings.TrimSpace(result) - assert.True(t, strings.HasPrefix(trimmed, "privkey:"), + assert.True(t, strings.HasPrefix(trimmed, "privkey:"), "private key should start with 'privkey:' prefix, got: %s", trimmed) - + // Should be reasonable length (64+ hex characters after prefix) - assert.True(t, len(trimmed) > 70, + assert.True(t, len(trimmed) > 70, "private key should be reasonable length, got length: %d", len(trimmed)) }) @@ -130,21 +130,21 @@ func TestGeneratePrivateKeyCommand(t *testing.T) { }, ) assertNoErr(t, err) - + // Should produce valid JSON output var keyData map[string]interface{} err = json.Unmarshal([]byte(result), &keyData) assert.NoError(t, err, "private key generation should produce valid JSON output") - + // Should contain private_key field privateKey, exists := keyData["private_key"] assert.True(t, exists, "JSON output should contain 'private_key' field") assert.NotEmpty(t, privateKey, "private_key field should not be empty") - + // Private key should be a string with correct format privateKeyStr, ok := privateKey.(string) assert.True(t, ok, "private_key should be a string") - assert.True(t, strings.HasPrefix(privateKeyStr, "privkey:"), + assert.True(t, strings.HasPrefix(privateKeyStr, "privkey:"), "private key should start with 'privkey:' prefix") }) @@ -159,7 +159,7 @@ func TestGeneratePrivateKeyCommand(t *testing.T) { }, ) assertNoErr(t, err) - + // Should produce YAML output assert.NotEmpty(t, result, "YAML output should not be empty") assert.Contains(t, result, "private_key:", "YAML output should contain private_key field") @@ -169,7 +169,7 @@ func TestGeneratePrivateKeyCommand(t *testing.T) { t.Run("test_generate_private_key_multiple_calls", func(t *testing.T) { // Test that multiple calls generate different keys var keys []string - + for i := 0; i < 3; i++ { result, err := headscale.Execute( []string{ @@ -179,13 +179,13 @@ func TestGeneratePrivateKeyCommand(t *testing.T) { }, ) assertNoErr(t, err) - + trimmed := strings.TrimSpace(result) keys = append(keys, trimmed) - assert.True(t, strings.HasPrefix(trimmed, "privkey:"), + assert.True(t, strings.HasPrefix(trimmed, "privkey:"), "each generated private key should have correct prefix") } - + // All keys should be different assert.NotEqual(t, keys[0], keys[1], "generated keys should be different") assert.NotEqual(t, keys[1], keys[2], "generated keys should be different") @@ -221,12 +221,12 @@ func TestGeneratePrivateKeyCommandValidation(t *testing.T) { "args", }, ) - + // Should either succeed (ignoring extra args) or fail gracefully if err == nil { // If successful, should still produce valid key trimmed := strings.TrimSpace(result) - assert.True(t, strings.HasPrefix(trimmed, "privkey:"), + assert.True(t, strings.HasPrefix(trimmed, "privkey:"), "should produce valid private key even with extra args") } else { // If failed, should be a reasonable error, not a panic @@ -244,7 +244,7 @@ func TestGeneratePrivateKeyCommandValidation(t *testing.T) { "--output", "invalid-format", }, ) - + // Should handle invalid output format gracefully // Might succeed with default format or fail gracefully if err == nil { @@ -265,10 +265,10 @@ func TestGeneratePrivateKeyCommandValidation(t *testing.T) { }, ) assertNoErr(t, err) - + // Should still generate valid private key trimmed := strings.TrimSpace(result) - assert.True(t, strings.HasPrefix(trimmed, "privkey:"), + assert.True(t, strings.HasPrefix(trimmed, "privkey:"), "should generate valid private key with config flag") }) } @@ -298,7 +298,7 @@ func TestGenerateCommandEdgeCases(t *testing.T) { "generate", }, ) - + // Should show help or list available subcommands if err == nil { assert.Contains(t, result, "private-key", "should show available subcommands") @@ -317,10 +317,12 @@ func TestGenerateCommandEdgeCases(t *testing.T) { "nonexistent-command", }, ) - + // Should fail gracefully for non-existent subcommand assert.Error(t, err, "should fail for non-existent subcommand") - assert.NotContains(t, err.Error(), "panic", "should not panic on 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) { @@ -333,24 +335,24 @@ func TestGenerateCommandEdgeCases(t *testing.T) { }, ) assertNoErr(t, err) - + trimmed := strings.TrimSpace(result) - + // Check format consistency - assert.True(t, strings.HasPrefix(trimmed, "privkey:"), + assert.True(t, strings.HasPrefix(trimmed, "privkey:"), "private key should start with 'privkey:' prefix") - + // Should be hex characters after prefix keyPart := strings.TrimPrefix(trimmed, "privkey:") - assert.True(t, len(keyPart) == 64, + assert.True(t, len(keyPart) == 64, "private key should be 64 hex characters after prefix, got length: %d", len(keyPart)) - + // Should only contain valid hex characters for _, char := range keyPart { - assert.True(t, - (char >= '0' && char <= '9') || - (char >= 'a' && char <= 'f') || - (char >= 'A' && char <= 'F'), + assert.True(t, + (char >= '0' && char <= '9') || + (char >= 'a' && char <= 'f') || + (char >= 'A' && char <= 'F'), "private key should only contain hex characters, found: %c", char) } }) @@ -365,7 +367,7 @@ func TestGenerateCommandEdgeCases(t *testing.T) { }, ) assertNoErr(t, err1) - + result2, err2 := headscale.Execute( []string{ "headscale", @@ -374,18 +376,18 @@ func TestGenerateCommandEdgeCases(t *testing.T) { }, ) assertNoErr(t, err2) - + // Both should produce valid keys (though different values) trimmed1 := strings.TrimSpace(result1) trimmed2 := strings.TrimSpace(result2) - - assert.True(t, strings.HasPrefix(trimmed1, "privkey:"), + + assert.True(t, strings.HasPrefix(trimmed1, "privkey:"), "generate command should produce valid key") - assert.True(t, strings.HasPrefix(trimmed2, "privkey:"), + assert.True(t, strings.HasPrefix(trimmed2, "privkey:"), "gen alias should produce valid key") - + // Keys should be different (they're randomly generated) - assert.NotEqual(t, trimmed1, trimmed2, + assert.NotEqual(t, trimmed1, trimmed2, "different calls should produce different keys") }) -} \ No newline at end of file +} diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index c300a205..d8857e2c 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -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), ","), } diff --git a/integration/routes_cli_test.go b/integration/routes_cli_test.go index b0f69896..e7819a0c 100644 --- a/integration/routes_cli_test.go +++ b/integration/routes_cli_test.go @@ -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", @@ -284,10 +284,10 @@ func TestRouteCommandHelp(t *testing.T) { }, ) assertNoErr(t, err) - + // 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) { @@ -300,10 +300,10 @@ func TestRouteCommandHelp(t *testing.T) { }, ) assertNoErr(t, err) - + // 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") }) -} \ No newline at end of file +} diff --git a/integration/serve_cli_test.go b/integration/serve_cli_test.go index ac6c41d0..58262772 100644 --- a/integration/serve_cli_test.go +++ b/integration/serve_cli_test.go @@ -40,7 +40,7 @@ func TestServeCommand(t *testing.T) { }, ) assertNoErr(t, err) - + // Help text should contain expected information assert.Contains(t, result, "serve", "help should mention serve command") assert.Contains(t, result, "Launches the headscale server", "help should contain command description") @@ -83,7 +83,7 @@ func TestServeCommandValidation(t *testing.T) { // We'll test that it accepts extra args without crashing immediately ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() - + // Use a goroutine to test that the command doesn't immediately fail done := make(chan error, 1) go func() { @@ -97,7 +97,7 @@ func TestServeCommandValidation(t *testing.T) { ) done <- err }() - + select { case err := <-done: // If it returns an error quickly, it should be about args validation @@ -132,28 +132,28 @@ func TestServeCommandHealthCheck(t *testing.T) { t.Run("test_serve_health_endpoint", func(t *testing.T) { // Test that the serve command starts a server that responds to health checks // This is effectively testing that the server is running and accessible - + // Get the server endpoint endpoint := headscale.GetEndpoint() assert.NotEmpty(t, endpoint, "headscale endpoint should not be empty") - + // Make a simple HTTP request to verify the server is running healthURL := fmt.Sprintf("%s/health", endpoint) - + // Use a timeout to avoid hanging client := &http.Client{ Timeout: 5 * time.Second, } - + resp, err := client.Get(healthURL) if err != nil { // If we can't connect, check if it's because server isn't ready - assert.Contains(t, err.Error(), "connection", + assert.Contains(t, err.Error(), "connection", "health check failure should be connection-related if server not ready") } else { defer resp.Body.Close() // If we can connect, verify we get a reasonable response - assert.True(t, resp.StatusCode >= 200 && resp.StatusCode < 500, + assert.True(t, resp.StatusCode >= 200 && resp.StatusCode < 500, "health endpoint should return reasonable status code") } }) @@ -162,24 +162,24 @@ func TestServeCommandHealthCheck(t *testing.T) { // Test that the serve command starts a server with API endpoints endpoint := headscale.GetEndpoint() assert.NotEmpty(t, endpoint, "headscale endpoint should not be empty") - + // Try to access a known API endpoint (version info) // This tests that the gRPC gateway is running versionURL := fmt.Sprintf("%s/api/v1/version", endpoint) - + client := &http.Client{ Timeout: 5 * time.Second, } - + resp, err := client.Get(versionURL) if err != nil { // Connection errors are acceptable if server isn't fully ready - assert.Contains(t, err.Error(), "connection", + assert.Contains(t, err.Error(), "connection", "API endpoint failure should be connection-related if server not ready") } else { defer resp.Body.Close() // If we can connect, check that we get some response - assert.True(t, resp.StatusCode >= 200 && resp.StatusCode < 500, + assert.True(t, resp.StatusCode >= 200 && resp.StatusCode < 500, "API endpoint should return reasonable status code") } }) @@ -205,7 +205,7 @@ func TestServeCommandServerBehavior(t *testing.T) { t.Run("test_serve_accepts_connections", func(t *testing.T) { // Test that the server accepts connections from clients // This is a basic integration test to ensure serve works - + // Create a user for testing user := spec.Users[0] _, err := headscale.Execute( @@ -217,7 +217,7 @@ func TestServeCommandServerBehavior(t *testing.T) { }, ) assertNoErr(t, err) - + // Create a pre-auth key result, err := headscale.Execute( []string{ @@ -229,7 +229,7 @@ func TestServeCommandServerBehavior(t *testing.T) { }, ) assertNoErr(t, err) - + // Verify the preauth key creation worked assert.NotEmpty(t, result, "preauth key creation should produce output") assert.Contains(t, result, "key", "preauth key output should contain key field") @@ -238,7 +238,7 @@ func TestServeCommandServerBehavior(t *testing.T) { t.Run("test_serve_handles_node_operations", func(t *testing.T) { // Test that the server can handle basic node operations _ = spec.Users[0] // Test user for context - + // List nodes (should work even if empty) result, err := headscale.Execute( []string{ @@ -249,10 +249,10 @@ func TestServeCommandServerBehavior(t *testing.T) { }, ) assertNoErr(t, err) - + // Should return valid JSON array (even if empty) trimmed := strings.TrimSpace(result) - assert.True(t, strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]"), + assert.True(t, strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]"), "nodes list should return JSON array") }) @@ -267,12 +267,12 @@ func TestServeCommandServerBehavior(t *testing.T) { }, ) assertNoErr(t, err) - + // Should return valid JSON array trimmed := strings.TrimSpace(result) - assert.True(t, strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]"), + assert.True(t, strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]"), "users list should return JSON array") - + // Should contain our test user assert.Contains(t, result, spec.Users[0], "users list should contain test user") }) @@ -299,7 +299,7 @@ func TestServeCommandEdgeCases(t *testing.T) { // Test that the server can handle multiple rapid commands // This tests the server's ability to handle concurrent requests user := spec.Users[0] - + // Create user first _, err := headscale.Execute( []string{ @@ -310,7 +310,7 @@ func TestServeCommandEdgeCases(t *testing.T) { }, ) assertNoErr(t, err) - + // Execute multiple commands rapidly for i := 0; i < 3; i++ { result, err := headscale.Execute( @@ -334,7 +334,7 @@ func TestServeCommandEdgeCases(t *testing.T) { }, ) assertNoErr(t, err) - + // Basic help should work result, err := headscale.Execute( []string{ @@ -357,7 +357,7 @@ func TestServeCommandEdgeCases(t *testing.T) { ) // Should fail gracefully for non-existent commands assert.Error(t, err, "should fail gracefully for non-existent commands") - + // Should not cause server to crash (we can still execute other commands) result, err := headscale.Execute( []string{ @@ -369,4 +369,4 @@ func TestServeCommandEdgeCases(t *testing.T) { assertNoErr(t, err) assert.NotEmpty(t, result, "server should still work after malformed request") }) -} \ No newline at end of file +} diff --git a/integration/utils.go b/integration/utils.go index a7ab048b..7aecbd25 100644 --- a/integration/utils.go +++ b/integration/utils.go @@ -24,7 +24,7 @@ const ( // derpPingTimeout defines the timeout for individual DERP ping operations // Used in DERP connectivity tests to verify relay server communication derpPingTimeout = 2 * time.Second - + // derpPingCount defines the number of ping attempts for DERP connectivity tests // Higher count provides better reliability assessment of DERP connectivity derpPingCount = 10 @@ -317,7 +317,7 @@ func assertValidNetcheck(t *testing.T, client TailscaleClient) { // assertCommandOutputContains executes a command with exponential backoff retry until the output // contains the expected string or timeout is reached (10 seconds). -// This implements eventual consistency patterns and should be used instead of time.Sleep +// This implements eventual consistency patterns and should be used instead of time.Sleep // before executing commands that depend on network state propagation. // // Timeout: 10 seconds with exponential backoff diff --git a/integration/version_cli_test.go b/integration/version_cli_test.go index fe905626..be71fb62 100644 --- a/integration/version_cli_test.go +++ b/integration/version_cli_test.go @@ -35,10 +35,10 @@ func TestVersionCommand(t *testing.T) { }, ) assertNoErr(t, err) - + // Version output should contain version information assert.NotEmpty(t, result, "version output should not be empty") - // In development, version is "dev", in releases it would be semver like "1.0.0" + // In development, version is "dev", in releases it would be semver like "1.0.0" trimmed := strings.TrimSpace(result) assert.True(t, trimmed == "dev" || len(trimmed) > 2, "version should be 'dev' or valid version string") }) @@ -53,7 +53,7 @@ func TestVersionCommand(t *testing.T) { }, ) assertNoErr(t, err) - + // Help text should contain expected information assert.Contains(t, result, "version", "help should mention version command") assert.Contains(t, result, "version of headscale", "help should contain command description") @@ -81,7 +81,7 @@ func TestVersionCommand(t *testing.T) { }, ) }, "version command should handle extra arguments gracefully") - + // If it succeeds, should still contain version info if err == nil { assert.NotEmpty(t, result, "version output should not be empty") @@ -140,4 +140,4 @@ func TestVersionCommandEdgeCases(t *testing.T) { ) }, "version command should handle invalid flags gracefully") }) -} \ No newline at end of file +}