package integration import ( "encoding/json" "fmt" "sort" "strconv" "testing" "time" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/tsic" "github.com/stretchr/testify/assert" ) func executeAndUnmarshal[T any](headscale ControlServer, command []string, result T) error { str, err := headscale.Execute(command) if err != nil { return err } err = json.Unmarshal([]byte(str), result) if err != nil { return err } return nil } func TestUserCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() scenario, err := NewScenario() assertNoErr(t, err) defer scenario.Shutdown() spec := map[string]int{ "user1": 0, "user2": 0, } err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins")) assertNoErr(t, err) headscale, err := scenario.Headscale() assertNoErr(t, err) var listUsers []v1.User err = executeAndUnmarshal(headscale, []string{ "headscale", "users", "list", "--output", "json", }, &listUsers, ) assertNoErr(t, err) result := []string{listUsers[0].Name, listUsers[1].Name} sort.Strings(result) assert.Equal( t, []string{"user1", "user2"}, result, ) _, err = headscale.Execute( []string{ "headscale", "users", "rename", "--output", "json", "user2", "newname", }, ) assertNoErr(t, err) var listAfterRenameUsers []v1.User err = executeAndUnmarshal(headscale, []string{ "headscale", "users", "list", "--output", "json", }, &listAfterRenameUsers, ) assertNoErr(t, err) result = []string{listAfterRenameUsers[0].Name, listAfterRenameUsers[1].Name} sort.Strings(result) assert.Equal( t, []string{"newname", "user1"}, result, ) } func TestPreAuthKeyCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() user := "preauthkeyspace" count := 3 scenario, err := NewScenario() assertNoErr(t, err) defer scenario.Shutdown() spec := map[string]int{ user: 0, } err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipak")) assertNoErr(t, err) headscale, err := scenario.Headscale() assertNoErr(t, err) keys := make([]*v1.PreAuthKey, count) assertNoErr(t, err) for index := 0; index < count; index++ { var preAuthKey v1.PreAuthKey err := executeAndUnmarshal( headscale, []string{ "headscale", "preauthkeys", "--user", user, "create", "--reusable", "--expiration", "24h", "--output", "json", "--tags", "tag:test1,tag:test2", }, &preAuthKey, ) assertNoErr(t, err) keys[index] = &preAuthKey } assert.Len(t, keys, 3) var listedPreAuthKeys []v1.PreAuthKey err = executeAndUnmarshal( headscale, []string{ "headscale", "preauthkeys", "--user", user, "list", "--output", "json", }, &listedPreAuthKeys, ) assertNoErr(t, err) // There is one key created by "scenario.CreateHeadscaleEnv" assert.Len(t, listedPreAuthKeys, 4) assert.Equal( t, []string{keys[0].Id, keys[1].Id, keys[2].Id}, []string{listedPreAuthKeys[1].Id, listedPreAuthKeys[2].Id, listedPreAuthKeys[3].Id}, ) assert.NotEmpty(t, listedPreAuthKeys[1].Key) assert.NotEmpty(t, listedPreAuthKeys[2].Key) assert.NotEmpty(t, listedPreAuthKeys[3].Key) assert.True(t, listedPreAuthKeys[1].Expiration.AsTime().After(time.Now())) assert.True(t, listedPreAuthKeys[2].Expiration.AsTime().After(time.Now())) assert.True(t, listedPreAuthKeys[3].Expiration.AsTime().After(time.Now())) assert.True( t, listedPreAuthKeys[1].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), ) assert.True( t, listedPreAuthKeys[2].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), ) assert.True( t, listedPreAuthKeys[3].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), ) for index := range listedPreAuthKeys { if index == 0 { continue } assert.Equal(t, listedPreAuthKeys[index].AclTags, []string{"tag:test1", "tag:test2"}) } // Test key expiry _, err = headscale.Execute( []string{ "headscale", "preauthkeys", "--user", user, "expire", listedPreAuthKeys[1].Key, }, ) assertNoErr(t, err) var listedPreAuthKeysAfterExpire []v1.PreAuthKey err = executeAndUnmarshal( headscale, []string{ "headscale", "preauthkeys", "--user", user, "list", "--output", "json", }, &listedPreAuthKeysAfterExpire, ) assertNoErr(t, err) assert.True(t, listedPreAuthKeysAfterExpire[1].Expiration.AsTime().Before(time.Now())) assert.True(t, listedPreAuthKeysAfterExpire[2].Expiration.AsTime().After(time.Now())) assert.True(t, listedPreAuthKeysAfterExpire[3].Expiration.AsTime().After(time.Now())) } func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) { IntegrationSkip(t) t.Parallel() user := "pre-auth-key-without-exp-user" scenario, err := NewScenario() assertNoErr(t, err) defer scenario.Shutdown() spec := map[string]int{ user: 0, } err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipaknaexp")) assertNoErr(t, err) headscale, err := scenario.Headscale() assertNoErr(t, err) var preAuthKey v1.PreAuthKey err = executeAndUnmarshal( headscale, []string{ "headscale", "preauthkeys", "--user", user, "create", "--reusable", "--output", "json", }, &preAuthKey, ) assertNoErr(t, err) var listedPreAuthKeys []v1.PreAuthKey err = executeAndUnmarshal( headscale, []string{ "headscale", "preauthkeys", "--user", user, "list", "--output", "json", }, &listedPreAuthKeys, ) assertNoErr(t, err) // There is one key created by "scenario.CreateHeadscaleEnv" assert.Len(t, listedPreAuthKeys, 2) assert.True(t, listedPreAuthKeys[1].Expiration.AsTime().After(time.Now())) assert.True( t, listedPreAuthKeys[1].Expiration.AsTime().Before(time.Now().Add(time.Minute*70)), ) } func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) { IntegrationSkip(t) t.Parallel() user := "pre-auth-key-reus-ephm-user" scenario, err := NewScenario() assertNoErr(t, err) defer scenario.Shutdown() spec := map[string]int{ user: 0, } err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clipakresueeph")) assertNoErr(t, err) headscale, err := scenario.Headscale() assertNoErr(t, err) var preAuthReusableKey v1.PreAuthKey err = executeAndUnmarshal( headscale, []string{ "headscale", "preauthkeys", "--user", user, "create", "--reusable=true", "--output", "json", }, &preAuthReusableKey, ) assertNoErr(t, err) var preAuthEphemeralKey v1.PreAuthKey err = executeAndUnmarshal( headscale, []string{ "headscale", "preauthkeys", "--user", user, "create", "--ephemeral=true", "--output", "json", }, &preAuthEphemeralKey, ) assertNoErr(t, err) assert.True(t, preAuthEphemeralKey.GetEphemeral()) assert.False(t, preAuthEphemeralKey.GetReusable()) var listedPreAuthKeys []v1.PreAuthKey err = executeAndUnmarshal( headscale, []string{ "headscale", "preauthkeys", "--user", user, "list", "--output", "json", }, &listedPreAuthKeys, ) assertNoErr(t, err) // There is one key created by "scenario.CreateHeadscaleEnv" assert.Len(t, listedPreAuthKeys, 3) } func TestEnablingRoutes(t *testing.T) { IntegrationSkip(t) t.Parallel() user := "enable-routing" scenario, err := NewScenario() assertNoErrf(t, "failed to create scenario: %s", err) defer scenario.Shutdown() spec := map[string]int{ user: 3, } err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clienableroute")) assertNoErrHeadscaleEnv(t, err) allClients, err := scenario.ListTailscaleClients() assertNoErrListClients(t, err) err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) headscale, err := scenario.Headscale() assertNoErrGetHeadscale(t, err) // advertise routes using the up command for i, client := range allClients { routeStr := fmt.Sprintf("10.0.%d.0/24", i) command := []string{ "tailscale", "set", "--advertise-routes=" + routeStr, } _, _, err := client.Execute(command) assertNoErrf(t, "failed to advertise route: %s", err) } err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) var routes []*v1.Route err = executeAndUnmarshal( headscale, []string{ "headscale", "routes", "list", "--output", "json", }, &routes, ) assertNoErr(t, err) assert.Len(t, routes, 3) for _, route := range routes { assert.Equal(t, route.Advertised, true) assert.Equal(t, route.Enabled, false) assert.Equal(t, route.IsPrimary, false) } for _, route := range routes { _, err = headscale.Execute( []string{ "headscale", "routes", "enable", "--route", strconv.Itoa(int(route.Id)), }) assertNoErr(t, err) } var enablingRoutes []*v1.Route err = executeAndUnmarshal( headscale, []string{ "headscale", "routes", "list", "--output", "json", }, &enablingRoutes, ) assertNoErr(t, err) assert.Len(t, enablingRoutes, 3) for _, route := range enablingRoutes { assert.Equal(t, route.Advertised, true) assert.Equal(t, route.Enabled, true) assert.Equal(t, route.IsPrimary, true) } routeIDToBeDisabled := enablingRoutes[0].Id _, err = headscale.Execute( []string{ "headscale", "routes", "disable", "--route", strconv.Itoa(int(routeIDToBeDisabled)), }) assertNoErr(t, err) var disablingRoutes []*v1.Route err = executeAndUnmarshal( headscale, []string{ "headscale", "routes", "list", "--output", "json", }, &disablingRoutes, ) assertNoErr(t, err) for _, route := range disablingRoutes { assert.Equal(t, true, route.Advertised) if route.Id == routeIDToBeDisabled { assert.Equal(t, route.Enabled, false) assert.Equal(t, route.IsPrimary, false) } else { assert.Equal(t, route.Enabled, true) assert.Equal(t, route.IsPrimary, true) } } } func TestApiKeyCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() count := 5 scenario, err := NewScenario() assertNoErr(t, err) defer scenario.Shutdown() spec := map[string]int{ "user1": 0, "user2": 0, } err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins")) assertNoErr(t, err) headscale, err := scenario.Headscale() assertNoErr(t, err) keys := make([]string, count) for idx := 0; idx < count; idx++ { apiResult, err := headscale.Execute( []string{ "headscale", "apikeys", "create", "--expiration", "24h", "--output", "json", }, ) assert.Nil(t, err) assert.NotEmpty(t, apiResult) keys[idx] = apiResult } assert.Len(t, keys, 5) var listedAPIKeys []v1.ApiKey err = executeAndUnmarshal(headscale, []string{ "headscale", "apikeys", "list", "--output", "json", }, &listedAPIKeys, ) assert.Nil(t, err) assert.Len(t, listedAPIKeys, 5) assert.Equal(t, uint64(1), listedAPIKeys[0].Id) assert.Equal(t, uint64(2), listedAPIKeys[1].Id) assert.Equal(t, uint64(3), listedAPIKeys[2].Id) assert.Equal(t, uint64(4), listedAPIKeys[3].Id) assert.Equal(t, uint64(5), listedAPIKeys[4].Id) assert.NotEmpty(t, listedAPIKeys[0].Prefix) assert.NotEmpty(t, listedAPIKeys[1].Prefix) assert.NotEmpty(t, listedAPIKeys[2].Prefix) assert.NotEmpty(t, listedAPIKeys[3].Prefix) assert.NotEmpty(t, listedAPIKeys[4].Prefix) assert.True(t, listedAPIKeys[0].Expiration.AsTime().After(time.Now())) assert.True(t, listedAPIKeys[1].Expiration.AsTime().After(time.Now())) assert.True(t, listedAPIKeys[2].Expiration.AsTime().After(time.Now())) assert.True(t, listedAPIKeys[3].Expiration.AsTime().After(time.Now())) assert.True(t, listedAPIKeys[4].Expiration.AsTime().After(time.Now())) assert.True( t, listedAPIKeys[0].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), ) assert.True( t, listedAPIKeys[1].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), ) assert.True( t, listedAPIKeys[2].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), ) assert.True( t, listedAPIKeys[3].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), ) assert.True( t, listedAPIKeys[4].Expiration.AsTime().Before(time.Now().Add(time.Hour*26)), ) expiredPrefixes := make(map[string]bool) // Expire three keys for idx := 0; idx < 3; idx++ { _, err := headscale.Execute( []string{ "headscale", "apikeys", "expire", "--prefix", listedAPIKeys[idx].Prefix, }, ) assert.Nil(t, err) expiredPrefixes[listedAPIKeys[idx].Prefix] = true } var listedAfterExpireAPIKeys []v1.ApiKey err = executeAndUnmarshal(headscale, []string{ "headscale", "apikeys", "list", "--output", "json", }, &listedAfterExpireAPIKeys, ) assert.Nil(t, err) for index := range listedAfterExpireAPIKeys { if _, ok := expiredPrefixes[listedAfterExpireAPIKeys[index].Prefix]; ok { // Expired assert.True( t, listedAfterExpireAPIKeys[index].Expiration.AsTime().Before(time.Now()), ) } else { // Not expired assert.False( t, listedAfterExpireAPIKeys[index].Expiration.AsTime().Before(time.Now()), ) } } } func TestNodeTagCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() scenario, err := NewScenario() assertNoErr(t, err) defer scenario.Shutdown() spec := map[string]int{ "user1": 0, } err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins")) assertNoErr(t, err) headscale, err := scenario.Headscale() assertNoErr(t, err) machineKeys := []string{ "mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", "mkey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c", } nodes := make([]*v1.Node, len(machineKeys)) assert.Nil(t, err) for index, machineKey := range machineKeys { _, err := headscale.Execute( []string{ "headscale", "debug", "create-node", "--name", fmt.Sprintf("node-%d", index+1), "--user", "user1", "--key", machineKey, "--output", "json", }, ) assert.Nil(t, err) var node v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "--user", "user1", "register", "--key", machineKey, "--output", "json", }, &node, ) assert.Nil(t, err) nodes[index] = &node } assert.Len(t, nodes, len(machineKeys)) var node v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "tag", "-i", "1", "-t", "tag:test", "--output", "json", }, &node, ) assert.Nil(t, err) assert.Equal(t, []string{"tag:test"}, node.ForcedTags) // try to set a wrong tag and retrieve the error type errOutput struct { Error string `json:"error"` } var errorOutput errOutput err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "tag", "-i", "2", "-t", "wrong-tag", "--output", "json", }, &errorOutput, ) assert.Nil(t, err) assert.Contains(t, errorOutput.Error, "tag must start with the string 'tag:'") // Test list all nodes after added seconds resultMachines := make([]*v1.Node, len(machineKeys)) err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "list", "--output", "json", }, &resultMachines, ) assert.Nil(t, err) found := false for _, node := range resultMachines { if node.ForcedTags != nil { for _, tag := range node.ForcedTags { if tag == "tag:test" { found = true } } } } assert.Equal( t, true, found, "should find a node with the tag 'tag:test' in the list of nodes", ) } func TestNodeCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() scenario, err := NewScenario() assertNoErr(t, err) defer scenario.Shutdown() spec := map[string]int{ "node-user": 0, "other-user": 0, } err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins")) assertNoErr(t, err) headscale, err := scenario.Headscale() assertNoErr(t, err) // Pregenerated machine keys machineKeys := []string{ "mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", "mkey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c", "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", "mkey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1", "mkey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", } nodes := make([]*v1.Node, len(machineKeys)) assert.Nil(t, err) for index, machineKey := range machineKeys { _, err := headscale.Execute( []string{ "headscale", "debug", "create-node", "--name", fmt.Sprintf("node-%d", index+1), "--user", "node-user", "--key", machineKey, "--output", "json", }, ) assert.Nil(t, err) var node v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "--user", "node-user", "register", "--key", machineKey, "--output", "json", }, &node, ) assert.Nil(t, err) nodes[index] = &node } assert.Len(t, nodes, len(machineKeys)) // Test list all nodes after added seconds var listAll []v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "list", "--output", "json", }, &listAll, ) assert.Nil(t, err) assert.Len(t, listAll, 5) assert.Equal(t, uint64(1), listAll[0].Id) assert.Equal(t, uint64(2), listAll[1].Id) assert.Equal(t, uint64(3), listAll[2].Id) assert.Equal(t, uint64(4), listAll[3].Id) assert.Equal(t, uint64(5), listAll[4].Id) assert.Equal(t, "node-1", listAll[0].Name) assert.Equal(t, "node-2", listAll[1].Name) assert.Equal(t, "node-3", listAll[2].Name) assert.Equal(t, "node-4", listAll[3].Name) assert.Equal(t, "node-5", listAll[4].Name) otherUserMachineKeys := []string{ "mkey:b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e", "mkey:dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584", } otherUserMachines := make([]*v1.Node, len(otherUserMachineKeys)) assert.Nil(t, err) for index, machineKey := range otherUserMachineKeys { _, err := headscale.Execute( []string{ "headscale", "debug", "create-node", "--name", fmt.Sprintf("otherUser-node-%d", index+1), "--user", "other-user", "--key", machineKey, "--output", "json", }, ) assert.Nil(t, err) var node v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "--user", "other-user", "register", "--key", machineKey, "--output", "json", }, &node, ) assert.Nil(t, err) otherUserMachines[index] = &node } assert.Len(t, otherUserMachines, len(otherUserMachineKeys)) // Test list all nodes after added otherUser var listAllWithotherUser []v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "list", "--output", "json", }, &listAllWithotherUser, ) assert.Nil(t, err) // All nodes, nodes + otherUser assert.Len(t, listAllWithotherUser, 7) assert.Equal(t, uint64(6), listAllWithotherUser[5].Id) assert.Equal(t, uint64(7), listAllWithotherUser[6].Id) assert.Equal(t, "otherUser-node-1", listAllWithotherUser[5].Name) assert.Equal(t, "otherUser-node-2", listAllWithotherUser[6].Name) // Test list all nodes after added otherUser var listOnlyotherUserMachineUser []v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "list", "--user", "other-user", "--output", "json", }, &listOnlyotherUserMachineUser, ) assert.Nil(t, err) assert.Len(t, listOnlyotherUserMachineUser, 2) assert.Equal(t, uint64(6), listOnlyotherUserMachineUser[0].Id) assert.Equal(t, uint64(7), listOnlyotherUserMachineUser[1].Id) assert.Equal( t, "otherUser-node-1", listOnlyotherUserMachineUser[0].Name, ) assert.Equal( t, "otherUser-node-2", listOnlyotherUserMachineUser[1].Name, ) // Delete a nodes _, err = headscale.Execute( []string{ "headscale", "nodes", "delete", "--identifier", // Delete the last added machine "4", "--output", "json", "--force", }, ) assert.Nil(t, err) // Test: list main user after node is deleted var listOnlyMachineUserAfterDelete []v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "list", "--user", "node-user", "--output", "json", }, &listOnlyMachineUserAfterDelete, ) assert.Nil(t, err) assert.Len(t, listOnlyMachineUserAfterDelete, 4) } func TestNodeExpireCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() scenario, err := NewScenario() assertNoErr(t, err) defer scenario.Shutdown() spec := map[string]int{ "node-expire-user": 0, } err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins")) assertNoErr(t, err) headscale, err := scenario.Headscale() assertNoErr(t, err) // Pregenerated machine keys machineKeys := []string{ "mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", "mkey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c", "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", "mkey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1", "mkey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", } nodes := make([]*v1.Node, len(machineKeys)) for index, machineKey := range machineKeys { _, err := headscale.Execute( []string{ "headscale", "debug", "create-node", "--name", fmt.Sprintf("node-%d", index+1), "--user", "node-expire-user", "--key", machineKey, "--output", "json", }, ) assert.Nil(t, err) var node v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "--user", "node-expire-user", "register", "--key", machineKey, "--output", "json", }, &node, ) assert.Nil(t, err) nodes[index] = &node } assert.Len(t, nodes, len(machineKeys)) var listAll []v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "list", "--output", "json", }, &listAll, ) assert.Nil(t, err) assert.Len(t, listAll, 5) assert.True(t, listAll[0].Expiry.AsTime().IsZero()) assert.True(t, listAll[1].Expiry.AsTime().IsZero()) assert.True(t, listAll[2].Expiry.AsTime().IsZero()) assert.True(t, listAll[3].Expiry.AsTime().IsZero()) assert.True(t, listAll[4].Expiry.AsTime().IsZero()) for idx := 0; idx < 3; idx++ { _, err := headscale.Execute( []string{ "headscale", "nodes", "expire", "--identifier", fmt.Sprintf("%d", listAll[idx].Id), }, ) assert.Nil(t, err) } var listAllAfterExpiry []v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "list", "--output", "json", }, &listAllAfterExpiry, ) assert.Nil(t, err) assert.Len(t, listAllAfterExpiry, 5) assert.True(t, listAllAfterExpiry[0].Expiry.AsTime().Before(time.Now())) assert.True(t, listAllAfterExpiry[1].Expiry.AsTime().Before(time.Now())) assert.True(t, listAllAfterExpiry[2].Expiry.AsTime().Before(time.Now())) assert.True(t, listAllAfterExpiry[3].Expiry.AsTime().IsZero()) assert.True(t, listAllAfterExpiry[4].Expiry.AsTime().IsZero()) } func TestNodeRenameCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() scenario, err := NewScenario() assertNoErr(t, err) defer scenario.Shutdown() spec := map[string]int{ "node-rename-command": 0, } err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins")) assertNoErr(t, err) headscale, err := scenario.Headscale() assertNoErr(t, err) // Pregenerated machine keys machineKeys := []string{ "mkey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", "mkey:8bc13285cee598acf76b1824a6f4490f7f2e3751b201e28aeb3b07fe81d5b4a1", "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", "mkey:6abd00bb5fdda622db51387088c68e97e71ce58e7056aa54f592b6a8219d524c", "mkey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", } nodes := make([]*v1.Node, len(machineKeys)) assert.Nil(t, err) for index, machineKey := range machineKeys { _, err := headscale.Execute( []string{ "headscale", "debug", "create-node", "--name", fmt.Sprintf("node-%d", index+1), "--user", "node-rename-command", "--key", machineKey, "--output", "json", }, ) assertNoErr(t, err) var node v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "--user", "node-rename-command", "register", "--key", machineKey, "--output", "json", }, &node, ) assertNoErr(t, err) nodes[index] = &node } assert.Len(t, nodes, len(machineKeys)) var listAll []v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "list", "--output", "json", }, &listAll, ) assert.Nil(t, err) assert.Len(t, listAll, 5) assert.Contains(t, listAll[0].GetGivenName(), "node-1") assert.Contains(t, listAll[1].GetGivenName(), "node-2") assert.Contains(t, listAll[2].GetGivenName(), "node-3") assert.Contains(t, listAll[3].GetGivenName(), "node-4") assert.Contains(t, listAll[4].GetGivenName(), "node-5") for idx := 0; idx < 3; idx++ { _, err := headscale.Execute( []string{ "headscale", "nodes", "rename", "--identifier", fmt.Sprintf("%d", listAll[idx].Id), fmt.Sprintf("newnode-%d", idx+1), }, ) assert.Nil(t, err) } var listAllAfterRename []v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "list", "--output", "json", }, &listAllAfterRename, ) assert.Nil(t, err) assert.Len(t, listAllAfterRename, 5) assert.Equal(t, "newnode-1", listAllAfterRename[0].GetGivenName()) assert.Equal(t, "newnode-2", listAllAfterRename[1].GetGivenName()) assert.Equal(t, "newnode-3", listAllAfterRename[2].GetGivenName()) assert.Contains(t, listAllAfterRename[3].GetGivenName(), "node-4") assert.Contains(t, listAllAfterRename[4].GetGivenName(), "node-5") // Test failure for too long names result, err := headscale.Execute( []string{ "headscale", "nodes", "rename", "--identifier", fmt.Sprintf("%d", listAll[4].Id), "testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaachine12345678901234567890", }, ) assert.Nil(t, err) assert.Contains(t, result, "not be over 63 chars") var listAllAfterRenameAttempt []v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "list", "--output", "json", }, &listAllAfterRenameAttempt, ) assert.Nil(t, err) assert.Len(t, listAllAfterRenameAttempt, 5) assert.Equal(t, "newnode-1", listAllAfterRenameAttempt[0].GetGivenName()) assert.Equal(t, "newnode-2", listAllAfterRenameAttempt[1].GetGivenName()) assert.Equal(t, "newnode-3", listAllAfterRenameAttempt[2].GetGivenName()) assert.Contains(t, listAllAfterRenameAttempt[3].GetGivenName(), "node-4") assert.Contains(t, listAllAfterRenameAttempt[4].GetGivenName(), "node-5") } func TestNodeMoveCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() scenario, err := NewScenario() assertNoErr(t, err) defer scenario.Shutdown() spec := map[string]int{ "old-user": 0, "new-user": 0, } err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clins")) assertNoErr(t, err) headscale, err := scenario.Headscale() assertNoErr(t, err) // Randomly generated node key machineKey := "mkey:688411b767663479632d44140f08a9fde87383adc7cdeb518f62ce28a17ef0aa" _, err = headscale.Execute( []string{ "headscale", "debug", "create-node", "--name", "nomad-node", "--user", "old-user", "--key", machineKey, "--output", "json", }, ) assert.Nil(t, err) var node v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "--user", "old-user", "register", "--key", machineKey, "--output", "json", }, &node, ) assert.Nil(t, err) assert.Equal(t, uint64(1), node.Id) assert.Equal(t, "nomad-node", node.Name) assert.Equal(t, node.User.Name, "old-user") nodeID := fmt.Sprintf("%d", node.Id) err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "move", "--identifier", nodeID, "--user", "new-user", "--output", "json", }, &node, ) assert.Nil(t, err) assert.Equal(t, node.User.Name, "new-user") var allNodes []v1.Node err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "list", "--output", "json", }, &allNodes, ) assert.Nil(t, err) assert.Len(t, allNodes, 1) assert.Equal(t, allNodes[0].Id, node.Id) assert.Equal(t, allNodes[0].User, node.User) assert.Equal(t, allNodes[0].User.Name, "new-user") moveToNonExistingNSResult, err := headscale.Execute( []string{ "headscale", "nodes", "move", "--identifier", nodeID, "--user", "non-existing-user", "--output", "json", }, ) assert.Nil(t, err) assert.Contains( t, moveToNonExistingNSResult, "user not found", ) assert.Equal(t, node.User.Name, "new-user") err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "move", "--identifier", nodeID, "--user", "old-user", "--output", "json", }, &node, ) assert.Nil(t, err) assert.Equal(t, node.User.Name, "old-user") err = executeAndUnmarshal( headscale, []string{ "headscale", "nodes", "move", "--identifier", nodeID, "--user", "old-user", "--output", "json", }, &node, ) assert.Nil(t, err) assert.Equal(t, node.User.Name, "old-user") }