mirror of
https://github.com/juanfont/headscale.git
synced 2025-07-28 16:13:43 +00:00
derp
This commit is contained in:
parent
024ed59ea9
commit
8253d588c6
41
CHANGELOG.md
41
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)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
// For now, we test the command structure and that it's properly wired up.
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
// For now, we test the command structure and flag configuration.
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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", "")
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ import (
|
||||
"github.com/tcnksm/go-latest"
|
||||
)
|
||||
|
||||
|
||||
var cfgFile string = ""
|
||||
|
||||
func init() {
|
||||
|
@ -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.
|
||||
// For now, we test the command structure and basic properties.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -172,4 +172,4 @@ func TestOutputWithEmptyData(t *testing.T) {
|
||||
emptyMap := map[string]string{}
|
||||
result = output(emptyMap, "fallback", "json")
|
||||
assert.Equal(t, "{}", result)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
2
go.mod
2
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 (
|
||||
|
@ -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),
|
||||
|
@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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), ","),
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user