cli: migrate the CLI and integration tests to the v1 HTTP API

Replace the gRPC client with the generated HTTP client across every
command: locally over the unix socket without auth (matching the previous
local gRPC socket), remotely over TLS with a Bearer API key. Output
rendering and integration tests move to the HTTP client types; the
transport changes, the assertions do not.
This commit is contained in:
Kristoffer Dalby
2026-06-19 06:13:50 +00:00
parent ba90048cfb
commit 8efa5ad1fe
35 changed files with 1654 additions and 1371 deletions
+1 -1
View File
@@ -244,7 +244,7 @@ jobs:
- TestACLDynamicUnknownUserRemoval
- TestAPIAuthenticationBypass
- TestAPIAuthenticationBypassCurl
- TestGRPCAuthenticationBypass
- TestRemoteCLIAuthenticationBypass
- TestCLIWithConfigAuthenticationBypass
- TestAuthKeyLogoutAndReloginSameUser
- TestAuthKeyLogoutAndReloginNewUser
+95 -29
View File
@@ -3,9 +3,10 @@ package cli
import (
"context"
"fmt"
"net/http"
"strconv"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/spf13/cobra"
)
@@ -43,26 +44,36 @@ var listAPIKeys = &cobra.Command{
Use: cmdList,
Short: "List the Api keys for headscale",
Aliases: []string{"ls", cmdShow},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
response, err := client.ListApiKeys(ctx, &v1.ListApiKeysRequest{})
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
resp, err := client.ListApiKeysWithResponse(ctx)
if err != nil {
return fmt.Errorf("listing api keys: %w", err)
}
return printListOutput(cmd, response.GetApiKeys(), func() error {
rows := make([][]string, 0, len(response.GetApiKeys()))
for _, key := range response.GetApiKeys() {
expiration := "-"
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
if key.GetExpiration() != nil {
expiration = ColourTime(key.GetExpiration().AsTime())
apiKeys := resp.JSON200.ApiKeys
return printListOutput(cmd, apiKeys, func() error {
rows := make([][]string, 0, len(apiKeys))
for _, key := range apiKeys {
expiration := "-"
if key.Expiration != nil {
expiration = ColourTime(*key.Expiration)
}
var created string
if key.CreatedAt != nil {
created = key.CreatedAt.Format(HeadscaleDateTimeFormat)
}
rows = append(rows, []string{
strconv.FormatUint(key.GetId(), util.Base10),
key.GetPrefix(),
key.Id,
key.Prefix,
expiration,
key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
created,
})
}
@@ -79,20 +90,24 @@ Creates a new Api key, the Api key is only visible on creation
and cannot be retrieved again.
If you lose a key, create a new one and revoke (expire) the old one.`,
Aliases: []string{"c", cmdNew},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
expiration, err := expirationFromFlag(cmd)
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
expiryTime, err := expirationFromFlag(cmd)
if err != nil {
return err
}
response, err := client.CreateApiKey(ctx, &v1.CreateApiKeyRequest{
Expiration: expiration,
resp, err := client.CreateApiKeyWithResponse(ctx, clientv1.CreateApiKeyJSONRequestBody{
Expiration: &expiryTime,
})
if err != nil {
return fmt.Errorf("creating api key: %w", err)
}
return printOutput(cmd, response.GetApiKey(), response.GetApiKey())
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200.ApiKey, resp.JSON200.ApiKey)
}),
}
@@ -116,21 +131,33 @@ var expireAPIKeyCmd = &cobra.Command{
Use: cmdExpire,
Short: "Expire an ApiKey",
Aliases: []string{"revoke", aliasExp, "e"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
id, prefix, err := apiKeyIDOrPrefix(cmd)
if err != nil {
return err
}
response, err := client.ExpireApiKey(ctx, &v1.ExpireApiKeyRequest{
Id: id,
Prefix: prefix,
})
body := clientv1.ExpireApiKeyJSONRequestBody{}
if id != 0 {
idStr := strconv.FormatUint(id, util.Base10)
body.Id = &idStr
}
if prefix != "" {
body.Prefix = &prefix
}
resp, err := client.ExpireApiKeyWithResponse(ctx, body)
if err != nil {
return fmt.Errorf("expiring api key: %w", err)
}
return printOutput(cmd, response, "Key expired")
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "Key expired")
}),
}
@@ -138,20 +165,59 @@ var deleteAPIKeyCmd = &cobra.Command{
Use: cmdDelete,
Short: "Delete an ApiKey",
Aliases: []string{"remove", aliasDel},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
id, prefix, err := apiKeyIDOrPrefix(cmd)
if err != nil {
return err
}
response, err := client.DeleteApiKey(ctx, &v1.DeleteApiKeyRequest{
Id: id,
Prefix: prefix,
})
// The DELETE route addresses the key by its prefix in the path. When the
// user deletes by --id we resolve the id to its (masked) prefix first,
// since the path segment is required and a query-only id cannot be routed.
if prefix == "" {
prefix, err = apiKeyPrefixForID(ctx, client, id)
if err != nil {
return err
}
}
resp, err := client.DeleteApiKeyWithResponse(ctx, prefix, &clientv1.DeleteApiKeyParams{})
if err != nil {
return fmt.Errorf("deleting api key: %w", err)
}
return printOutput(cmd, response, "Key deleted")
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "Key deleted")
}),
}
// apiKeyPrefixForID resolves an API key id to its display prefix by listing the
// keys. The DELETE endpoint addresses keys by prefix in the URL path, so a
// delete by --id needs the prefix; the returned masked prefix is accepted by
// the server's lookup.
func apiKeyPrefixForID(
ctx context.Context,
client *clientv1.ClientWithResponses,
id uint64,
) (string, error) {
resp, err := client.ListApiKeysWithResponse(ctx)
if err != nil {
return "", fmt.Errorf("listing api keys: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return "", apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
idStr := strconv.FormatUint(id, util.Base10)
for _, key := range resp.JSON200.ApiKeys {
if key.Id == idStr {
return key.Prefix, nil
}
}
return "", fmt.Errorf("%w: api key %d not found", errMissingParameter, id)
}
+45 -40
View File
@@ -3,8 +3,9 @@ package cli
import (
"context"
"fmt"
"net/http"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/spf13/cobra"
)
@@ -33,62 +34,66 @@ var authCmd = &cobra.Command{
var authRegisterCmd = &cobra.Command{
Use: "register",
Short: "Register a node to your network",
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
user, _ := cmd.Flags().GetString("user")
authID, _ := cmd.Flags().GetString("auth-id")
request := &v1.AuthRegisterRequest{
AuthId: authID,
User: user,
}
response, err := client.AuthRegister(ctx, request)
resp, err := client.AuthRegisterWithResponse(ctx, clientv1.AuthRegisterJSONRequestBody{
AuthId: &authID,
User: &user,
})
if err != nil {
return fmt.Errorf("registering node: %w", err)
}
return printOutput(
cmd,
response.GetNode(),
fmt.Sprintf("Node %s registered", response.GetNode().GetGivenName()),
)
}),
}
// authDecisionRunE builds a RunE for an auth decision command (approve or
// reject) that reads the auth-id flag, invokes the given gRPC call, and prints
// the response. errVerb is used in the error message; okMsg is printed on
// success.
func authDecisionRunE[Resp any](
errVerb, okMsg string,
call func(ctx context.Context, client v1.HeadscaleServiceClient, authID string) (Resp, error),
) func(*cobra.Command, []string) error {
return grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
authID, _ := cmd.Flags().GetString("auth-id")
response, err := call(ctx, client, authID)
if err != nil {
return fmt.Errorf("%s auth request: %w", errVerb, err)
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, response, okMsg)
})
node := resp.JSON200.Node
return printOutput(
cmd,
node,
fmt.Sprintf("Node %s registered", node.GivenName),
)
}),
}
var authApproveCmd = &cobra.Command{
Use: "approve",
Short: "Approve a pending authentication request",
RunE: authDecisionRunE("approving", "Auth request approved",
func(ctx context.Context, client v1.HeadscaleServiceClient, authID string) (*v1.AuthApproveResponse, error) {
return client.AuthApprove(ctx, &v1.AuthApproveRequest{AuthId: authID})
}),
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
authID, _ := cmd.Flags().GetString("auth-id")
resp, err := client.AuthApproveWithResponse(ctx, clientv1.AuthApproveJSONRequestBody{AuthId: &authID})
if err != nil {
return fmt.Errorf("approving auth request: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "Auth request approved")
}),
}
var authRejectCmd = &cobra.Command{
Use: "reject",
Short: "Reject a pending authentication request",
RunE: authDecisionRunE("rejecting", "Auth request rejected",
func(ctx context.Context, client v1.HeadscaleServiceClient, authID string) (*v1.AuthRejectResponse, error) {
return client.AuthReject(ctx, &v1.AuthRejectRequest{AuthId: authID})
}),
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
authID, _ := cmd.Flags().GetString("auth-id")
resp, err := client.AuthRejectWithResponse(ctx, clientv1.AuthRejectJSONRequestBody{AuthId: &authID})
if err != nil {
return fmt.Errorf("rejecting auth request: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "Auth request rejected")
}),
}
+14 -11
View File
@@ -3,8 +3,9 @@ package cli
import (
"context"
"fmt"
"net/http"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/spf13/cobra"
)
@@ -32,7 +33,7 @@ var debugCmd = &cobra.Command{
var createNodeCmd = &cobra.Command{
Use: "create-node",
Short: "Create a node that can be registered with `auth register <>` command",
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
user, _ := cmd.Flags().GetString("user")
name, _ := cmd.Flags().GetString("name")
registrationID, _ := cmd.Flags().GetString("key")
@@ -44,18 +45,20 @@ var createNodeCmd = &cobra.Command{
routes, _ := cmd.Flags().GetStringSlice("route")
request := &v1.DebugCreateNodeRequest{
Key: registrationID,
Name: name,
User: user,
Routes: routes,
}
response, err := client.DebugCreateNode(ctx, request)
resp, err := client.DebugCreateNodeWithResponse(ctx, clientv1.DebugCreateNodeJSONRequestBody{
Key: &registrationID,
Name: &name,
User: &user,
Routes: &routes,
})
if err != nil {
return fmt.Errorf("creating node: %w", err)
}
return printOutput(cmd, response.GetNode(), "Node created")
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200.Node, "Node created")
}),
}
+9 -4
View File
@@ -3,8 +3,9 @@ package cli
import (
"context"
"fmt"
"net/http"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/spf13/cobra"
)
@@ -16,12 +17,16 @@ var healthCmd = &cobra.Command{
Use: "health",
Short: "Check the health of the Headscale server",
Long: "Check the health of the Headscale server. This command will return an exit code of 0 if the server is healthy, or 1 if it is not.",
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
response, err := client.Health(ctx, &v1.HealthRequest{})
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
resp, err := client.HealthWithResponse(ctx)
if err != nil {
return fmt.Errorf("checking health: %w", err)
}
return printOutput(cmd, response, "")
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "")
}),
}
+145 -139
View File
@@ -3,17 +3,17 @@ package cli
import (
"context"
"fmt"
"net/http"
"net/netip"
"strconv"
"strings"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/pterm/pterm"
"github.com/samber/lo"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/timestamppb"
"tailscale.com/types/key"
)
@@ -67,24 +67,30 @@ var registerNodeCmd = &cobra.Command{
Use: "register",
Short: "Registers a node to your network",
Deprecated: "use 'headscale auth register --auth-id <id> --user <user>' instead",
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
user, _ := cmd.Flags().GetString("user")
registrationID, _ := cmd.Flags().GetString("key")
request := &v1.RegisterNodeRequest{
Key: registrationID,
User: user,
params := &clientv1.RegisterNodeParams{
User: &user,
Key: &registrationID,
}
response, err := client.RegisterNode(ctx, request)
resp, err := client.RegisterNodeWithResponse(ctx, params)
if err != nil {
return fmt.Errorf("registering node: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
node := resp.JSON200.Node
return printOutput(
cmd,
response.GetNode(),
fmt.Sprintf("Node %s registered", response.GetNode().GetGivenName()),
node,
fmt.Sprintf("Node %s registered", node.GivenName),
)
}),
}
@@ -93,16 +99,27 @@ var listNodesCmd = &cobra.Command{
Use: cmdList,
Short: "List nodes",
Aliases: []string{"ls", cmdShow},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
user, _ := cmd.Flags().GetString("user")
response, err := client.ListNodes(ctx, &v1.ListNodesRequest{User: user})
params := &clientv1.ListNodesParams{}
if user != "" {
params.User = &user
}
resp, err := client.ListNodesWithResponse(ctx, params)
if err != nil {
return fmt.Errorf("listing nodes: %w", err)
}
return printListOutput(cmd, response.GetNodes(), func() error {
tableData, err := nodesToPtables(response.GetNodes())
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
nodes := resp.JSON200.Nodes
return printListOutput(cmd, nodes, func() error {
tableData, err := nodesToPtables(nodes)
if err != nil {
return fmt.Errorf("converting to table: %w", err)
}
@@ -116,27 +133,32 @@ var listNodeRoutesCmd = &cobra.Command{
Use: "list-routes",
Short: "List routes available on nodes",
Aliases: []string{"lsr", "routes"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
response, err := client.ListNodes(ctx, &v1.ListNodesRequest{})
resp, err := client.ListNodesWithResponse(ctx, &clientv1.ListNodesParams{})
if err != nil {
return fmt.Errorf("listing nodes: %w", err)
}
nodes := response.GetNodes()
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
nodes := resp.JSON200.Nodes
if identifier != 0 {
for _, node := range response.GetNodes() {
if node.GetId() == identifier {
nodes = []*v1.Node{node}
idStr := strconv.FormatUint(identifier, util.Base10)
for _, node := range nodes {
if node.Id == idStr {
nodes = []clientv1.Node{node}
break
}
}
}
nodes = lo.Filter(nodes, func(n *v1.Node, _ int) bool {
return len(n.GetSubnetRoutes()) > 0 || len(n.GetApprovedRoutes()) > 0 || len(n.GetAvailableRoutes()) > 0
nodes = lo.Filter(nodes, func(n clientv1.Node, _ int) bool {
return len(n.SubnetRoutes) > 0 || len(n.ApprovedRoutes) > 0 || len(n.AvailableRoutes) > 0
})
return printListOutput(cmd, nodes, func() error {
@@ -152,23 +174,27 @@ var expireNodeCmd = &cobra.Command{
Use --disable to disable key expiry (node will never expire).`,
Aliases: []string{"logout", aliasExp, "e"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
disableExpiry, _ := cmd.Flags().GetBool("disable")
nodeID := strconv.FormatUint(identifier, util.Base10)
// Handle disable expiry - node will never expire.
if disableExpiry {
request := &v1.ExpireNodeRequest{
NodeId: identifier,
DisableExpiry: true,
}
disable := true
response, err := client.ExpireNode(ctx, request)
resp, err := client.ExpireNodeWithResponse(ctx, nodeID, clientv1.ExpireNodeJSONRequestBody{
DisableExpiry: &disable,
})
if err != nil {
return fmt.Errorf("disabling node expiry: %w", err)
}
return printOutput(cmd, response.GetNode(), "Node expiry disabled")
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200.Node, "Node expiry disabled")
}
expiry, _ := cmd.Flags().GetString("expiry")
@@ -186,28 +212,31 @@ Use --disable to disable key expiry (node will never expire).`,
}
}
request := &v1.ExpireNodeRequest{
NodeId: identifier,
Expiry: timestamppb.New(expiryTime),
}
response, err := client.ExpireNode(ctx, request)
resp, err := client.ExpireNodeWithResponse(ctx, nodeID, clientv1.ExpireNodeJSONRequestBody{
Expiry: &expiryTime,
})
if err != nil {
return fmt.Errorf("expiring node: %w", err)
}
if now.Equal(expiryTime) || now.After(expiryTime) {
return printOutput(cmd, response.GetNode(), "Node expired")
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, response.GetNode(), "Node expiration updated")
node := resp.JSON200.Node
if now.Equal(expiryTime) || now.After(expiryTime) {
return printOutput(cmd, node, "Node expired")
}
return printOutput(cmd, node, "Node expiration updated")
}),
}
var renameNodeCmd = &cobra.Command{
Use: "rename NEW_NAME",
Short: "Renames a node in your network",
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
newName := ""
@@ -215,17 +244,16 @@ var renameNodeCmd = &cobra.Command{
newName = args[0]
}
request := &v1.RenameNodeRequest{
NodeId: identifier,
NewName: newName,
}
response, err := client.RenameNode(ctx, request)
resp, err := client.RenameNodeWithResponse(ctx, strconv.FormatUint(identifier, util.Base10), newName)
if err != nil {
return fmt.Errorf("renaming node: %w", err)
}
return printOutput(cmd, response.GetNode(), "Node renamed")
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200.Node, "Node renamed")
}),
}
@@ -233,34 +261,35 @@ var deleteNodeCmd = &cobra.Command{
Use: cmdDelete,
Short: "Delete a node",
Aliases: []string{aliasDel},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
nodeID := strconv.FormatUint(identifier, util.Base10)
getRequest := &v1.GetNodeRequest{
NodeId: identifier,
}
getResponse, err := client.GetNode(ctx, getRequest)
getResponse, err := client.GetNodeWithResponse(ctx, nodeID)
if err != nil {
return fmt.Errorf("getting node: %w", err)
}
deleteRequest := &v1.DeleteNodeRequest{
NodeId: identifier,
if getResponse.StatusCode() != http.StatusOK {
return apiError(getResponse.StatusCode(), getResponse.ApplicationproblemJSONDefault)
}
if !confirmAction(cmd, fmt.Sprintf(
"Do you want to remove the node %s?",
getResponse.GetNode().GetName(),
getResponse.JSON200.Node.Name,
)) {
return printOutput(cmd, map[string]string{colResult: "Node not deleted"}, "Node not deleted")
}
_, err = client.DeleteNode(ctx, deleteRequest)
deleteResponse, err := client.DeleteNodeWithResponse(ctx, nodeID)
if err != nil {
return fmt.Errorf("deleting node: %w", err)
}
if deleteResponse.StatusCode() != http.StatusOK {
return apiError(deleteResponse.StatusCode(), deleteResponse.ApplicationproblemJSONDefault)
}
return printOutput(
cmd,
map[string]string{colResult: "Node deleted"},
@@ -289,23 +318,26 @@ be assigned to nodes.`,
return nil
}
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
if err != nil {
return fmt.Errorf("connecting to headscale: %w", err)
}
defer cancel()
defer conn.Close()
return withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
confirmed := true
changes, err := client.BackfillNodeIPs(ctx, &v1.BackfillNodeIPsRequest{Confirmed: true})
if err != nil {
return fmt.Errorf("backfilling IPs: %w", err)
}
resp, err := client.BackfillNodeIPsWithResponse(ctx, &clientv1.BackfillNodeIPsParams{
Confirmed: &confirmed,
})
if err != nil {
return fmt.Errorf("backfilling IPs: %w", err)
}
return printOutput(cmd, changes, "Node IPs backfilled successfully")
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "Node IPs backfilled successfully")
})
},
}
func nodesToPtables(nodes []*v1.Node) (pterm.TableData, error) {
func nodesToPtables(nodes []clientv1.Node) (pterm.TableData, error) {
tableHeader := []string{
"ID",
"Hostname",
@@ -325,75 +357,49 @@ func nodesToPtables(nodes []*v1.Node) (pterm.TableData, error) {
tableData[0] = tableHeader
for _, node := range nodes {
var ephemeral bool
if node.GetPreAuthKey() != nil && node.GetPreAuthKey().GetEphemeral() {
ephemeral = true
// An absent pre-auth key decodes into a zero NodePreAuthKey, so guard
// on Id before reading its flags.
ephemeral := node.PreAuthKey.Id != "" && node.PreAuthKey.Ephemeral
var lastSeenTime string
if node.LastSeen != nil {
lastSeenTime = node.LastSeen.Format(HeadscaleDateTimeFormat)
}
var (
lastSeen time.Time
lastSeenTime string
)
if node.GetLastSeen() != nil {
lastSeen = node.GetLastSeen().AsTime()
lastSeenTime = lastSeen.Format(HeadscaleDateTimeFormat)
}
var (
expiry time.Time
expiryTime string
)
if node.GetExpiry() != nil {
expiry = node.GetExpiry().AsTime()
expiryTime = expiry.Format(HeadscaleDateTimeFormat)
} else {
expiryTime = "N/A"
expiryTime := "N/A"
if node.Expiry != nil {
expiryTime = node.Expiry.Format(HeadscaleDateTimeFormat)
}
var machineKey key.MachinePublic
err := machineKey.UnmarshalText(
[]byte(node.GetMachineKey()),
)
err := machineKey.UnmarshalText([]byte(node.MachineKey))
if err != nil {
machineKey = key.MachinePublic{}
}
var nodeKey key.NodePublic
err = nodeKey.UnmarshalText(
[]byte(node.GetNodeKey()),
)
err = nodeKey.UnmarshalText([]byte(node.NodeKey))
if err != nil {
return nil, err
}
var online string
if node.GetOnline() {
online := pterm.LightRed("offline")
if node.Online {
online = pterm.LightGreen("online")
} else {
online = pterm.LightRed("offline")
}
var expired string
if node.GetExpiry() != nil && node.GetExpiry().AsTime().Before(time.Now()) {
expired := pterm.LightGreen("no")
if node.Expiry != nil && node.Expiry.Before(time.Now()) {
expired = pterm.LightRed("yes")
} else {
expired = pterm.LightGreen("no")
}
tags := strings.Join(node.GetTags(), "\n")
var user string
if node.GetUser() != nil {
user = node.GetUser().GetName()
}
tags := strings.Join(node.Tags, "\n")
var ipBuilder strings.Builder
for _, addr := range node.GetIpAddresses() {
for _, addr := range node.IpAddresses {
ip, err := netip.ParseAddr(addr)
if err == nil {
if ipBuilder.Len() > 0 {
@@ -407,12 +413,12 @@ func nodesToPtables(nodes []*v1.Node) (pterm.TableData, error) {
ipAddresses := ipBuilder.String()
nodeData := []string{
strconv.FormatUint(node.GetId(), util.Base10),
node.GetName(),
node.GetGivenName(),
node.Id,
node.Name,
node.GivenName,
machineKey.ShortString(),
nodeKey.ShortString(),
user,
node.User.Name,
tags,
ipAddresses,
strconv.FormatBool(ephemeral),
@@ -431,7 +437,7 @@ func nodesToPtables(nodes []*v1.Node) (pterm.TableData, error) {
}
func nodeRoutesToPtables(
nodes []*v1.Node,
nodes []clientv1.Node,
) pterm.TableData {
tableHeader := []string{
"ID",
@@ -445,11 +451,11 @@ func nodeRoutesToPtables(
for _, node := range nodes {
nodeData := []string{
strconv.FormatUint(node.GetId(), util.Base10),
node.GetGivenName(),
strings.Join(node.GetApprovedRoutes(), "\n"),
strings.Join(node.GetAvailableRoutes(), "\n"),
strings.Join(node.GetSubnetRoutes(), "\n"),
node.Id,
node.GivenName,
strings.Join(node.ApprovedRoutes, "\n"),
strings.Join(node.AvailableRoutes, "\n"),
strings.Join(node.SubnetRoutes, "\n"),
}
tableData = append(
tableData,
@@ -464,43 +470,43 @@ var tagCmd = &cobra.Command{
Use: "tag",
Short: "Manage the tags of a node",
Aliases: []string{"tags", "t"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
tagsToSet, _ := cmd.Flags().GetStringSlice("tags")
// Sending tags to node
request := &v1.SetTagsRequest{
NodeId: identifier,
Tags: tagsToSet,
}
resp, err := client.SetTags(ctx, request)
resp, err := client.SetTagsWithResponse(ctx, strconv.FormatUint(identifier, util.Base10), clientv1.SetTagsJSONRequestBody{
Tags: &tagsToSet,
})
if err != nil {
return fmt.Errorf("setting tags: %w", err)
}
return printOutput(cmd, resp.GetNode(), "Node updated")
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200.Node, "Node updated")
}),
}
var approveRoutesCmd = &cobra.Command{
Use: "approve-routes",
Short: "Manage the approved routes of a node",
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
identifier, _ := cmd.Flags().GetUint64("identifier")
routes, _ := cmd.Flags().GetStringSlice("routes")
// Sending routes to node
request := &v1.SetApprovedRoutesRequest{
NodeId: identifier,
Routes: routes,
}
resp, err := client.SetApprovedRoutes(ctx, request)
resp, err := client.SetApprovedRoutesWithResponse(ctx, strconv.FormatUint(identifier, util.Base10), clientv1.SetApprovedRoutesJSONRequestBody{
Routes: &routes,
})
if err != nil {
return fmt.Errorf("setting approved routes: %w", err)
}
return printOutput(cmd, resp.GetNode(), "Node updated")
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200.Node, "Node updated")
}),
}
+40 -19
View File
@@ -4,9 +4,10 @@ import (
"context"
"errors"
"fmt"
"net/http"
"os"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol/db"
"github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/types"
@@ -15,14 +16,13 @@ import (
)
const (
bypassFlag = "bypass-grpc-and-access-database-directly" //nolint:gosec // not a credential
bypassFlag = "bypass-server-and-access-database-directly" //nolint:gosec // not a credential
)
var errAborted = errors.New("command aborted by user")
// bypassDatabase loads the server config and opens the database directly,
// bypassing the gRPC server. The caller is responsible for closing the
// returned database handle.
// bypassDatabase opens the database directly, bypassing the running server.
// The caller must close the returned handle.
func bypassDatabase() (*db.HSDatabase, error) {
cfg, err := types.LoadServerConfig()
if err != nil {
@@ -50,16 +50,16 @@ func openBypassDB(cmd *cobra.Command) (*db.HSDatabase, error) {
func init() {
rootCmd.AddCommand(policyCmd)
getPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing gRPC and does not require the server to be running")
getPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing the API and does not require the server to be running")
policyCmd.AddCommand(getPolicy)
setPolicy.Flags().StringP("file", "f", "", "Path to a policy file in HuJSON format")
setPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing gRPC and does not require the server to be running")
setPolicy.Flags().BoolP(bypassFlag, "", false, "Uses the headscale config to directly access the database, bypassing the API and does not require the server to be running")
mustMarkRequired(setPolicy, "file")
policyCmd.AddCommand(setPolicy)
checkPolicy.Flags().StringP("file", "f", "", "Path to a policy file in HuJSON format")
checkPolicy.Flags().BoolP(bypassFlag, "", false, "Open the database directly (no gRPC, no running server) to resolve user references and to evaluate the policy's tests and sshTests blocks. Required when those checks are needed.")
checkPolicy.Flags().BoolP(bypassFlag, "", false, "Open the database directly (no running server required) to resolve user references and to evaluate the policy's tests and sshTests blocks. Required when those checks are needed.")
mustMarkRequired(checkPolicy, "file")
policyCmd.AddCommand(checkPolicy)
}
@@ -90,13 +90,17 @@ var getPolicy = &cobra.Command{
policyData = pol.Data
} else {
err := withGRPC(func(ctx context.Context, client v1.HeadscaleServiceClient) error {
response, err := client.GetPolicy(ctx, &v1.GetPolicyRequest{})
err := withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
resp, err := client.GetPolicyWithResponse(ctx)
if err != nil {
return fmt.Errorf("loading ACL policy: %w", err)
}
policyData = response.GetPolicy()
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
policyData = resp.JSON200.Policy
return nil
})
@@ -150,14 +154,20 @@ var setPolicy = &cobra.Command{
return fmt.Errorf("setting ACL policy: %w", err)
}
} else {
request := &v1.SetPolicyRequest{Policy: string(policyBytes)}
policyStr := string(policyBytes)
err := withGRPC(func(ctx context.Context, client v1.HeadscaleServiceClient) error {
_, err := client.SetPolicy(ctx, request)
err := withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
resp, err := client.SetPolicyWithResponse(ctx, clientv1.SetPolicyJSONRequestBody{
Policy: &policyStr,
})
if err != nil {
return fmt.Errorf("setting ACL policy: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return nil
})
if err != nil {
@@ -176,8 +186,8 @@ var checkPolicy = &cobra.Command{
Short: "Check the Policy file for errors",
Long: `
Check validates the policy against the server's live users and nodes,
running any "tests" or "sshTests" block. By default the command is a
thin frontend for a gRPC call to a running headscale; pass --` + bypassFlag + ` to
running any "tests" or "sshTests" block. By default the command calls a
running headscale over its API; pass --` + bypassFlag + ` to
open the database directly when headscale is not running.`,
RunE: func(cmd *cobra.Command, args []string) error {
policyPath, _ := cmd.Flags().GetString("file")
@@ -223,10 +233,21 @@ var checkPolicy = &cobra.Command{
return nil
}
err = withGRPC(func(ctx context.Context, client v1.HeadscaleServiceClient) error {
_, err := client.CheckPolicy(ctx, &v1.CheckPolicyRequest{Policy: string(policyBytes)})
policyStr := string(policyBytes)
return err
err = withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
resp, err := client.CheckPolicyWithResponse(ctx, clientv1.CheckPolicyJSONRequestBody{
Policy: &policyStr,
})
if err != nil {
return err
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return nil
})
if err != nil {
return err
+65 -45
View File
@@ -3,10 +3,11 @@ package cli
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/spf13/cobra"
)
@@ -44,37 +45,40 @@ var listPreAuthKeys = &cobra.Command{
Use: cmdList,
Short: "List all preauthkeys",
Aliases: []string{"ls", cmdShow},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
response, err := client.ListPreAuthKeys(ctx, &v1.ListPreAuthKeysRequest{})
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
resp, err := client.ListPreAuthKeysWithResponse(ctx)
if err != nil {
return fmt.Errorf("listing preauthkeys: %w", err)
}
return printListOutput(cmd, response.GetPreAuthKeys(), func() error {
rows := make([][]string, 0, len(response.GetPreAuthKeys()))
for _, key := range response.GetPreAuthKeys() {
expiration := "-"
if key.GetExpiration() != nil {
expiration = ColourTime(key.GetExpiration().AsTime())
}
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
var owner string
if len(key.GetAclTags()) > 0 {
owner = strings.Join(key.GetAclTags(), "\n")
} else if key.GetUser() != nil {
owner = key.GetUser().GetName()
} else {
owner = "-"
preAuthKeys := resp.JSON200.PreAuthKeys
return printListOutput(cmd, preAuthKeys, func() error {
rows := make([][]string, 0, len(preAuthKeys))
for _, key := range preAuthKeys {
expiration := ColourTime(key.Expiration)
owner := "-"
switch {
case len(key.AclTags) > 0:
owner = strings.Join(key.AclTags, "\n")
case key.User.Id != "":
owner = key.User.Name
}
rows = append(rows, []string{
strconv.FormatUint(key.GetId(), util.Base10),
key.GetKey(),
strconv.FormatBool(key.GetReusable()),
strconv.FormatBool(key.GetEphemeral()),
strconv.FormatBool(key.GetUsed()),
key.Id,
key.Key,
strconv.FormatBool(key.Reusable),
strconv.FormatBool(key.Ephemeral),
strconv.FormatBool(key.Used),
expiration,
key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
key.CreatedAt.Format(HeadscaleDateTimeFormat),
owner,
})
}
@@ -97,31 +101,39 @@ var createPreAuthKeyCmd = &cobra.Command{
Use: "create",
Short: "Creates a new preauthkey",
Aliases: []string{"c", cmdNew},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
user, _ := cmd.Flags().GetUint64("user")
reusable, _ := cmd.Flags().GetBool("reusable")
ephemeral, _ := cmd.Flags().GetBool("ephemeral")
tags, _ := cmd.Flags().GetStringSlice("tags")
expiration, err := expirationFromFlag(cmd)
expiryTime, err := expirationFromFlag(cmd)
if err != nil {
return err
}
request := &v1.CreatePreAuthKeyRequest{
User: user,
Reusable: reusable,
Ephemeral: ephemeral,
AclTags: tags,
Expiration: expiration,
userStr := strconv.FormatUint(user, util.Base10)
request := clientv1.CreatePreAuthKeyJSONRequestBody{
User: &userStr,
Reusable: &reusable,
Ephemeral: &ephemeral,
AclTags: &tags,
Expiration: &expiryTime,
}
response, err := client.CreatePreAuthKey(ctx, request)
resp, err := client.CreatePreAuthKeyWithResponse(ctx, request)
if err != nil {
return fmt.Errorf("creating preauthkey: %w", err)
}
return printOutput(cmd, response.GetPreAuthKey(), response.GetPreAuthKey().GetKey())
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
preAuthKey := resp.JSON200.PreAuthKey
return printOutput(cmd, preAuthKey, preAuthKey.Key)
}),
}
@@ -139,22 +151,26 @@ var expirePreAuthKeyCmd = &cobra.Command{
Use: cmdExpire,
Short: "Expire a preauthkey",
Aliases: []string{"revoke", aliasExp, "e"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
id, err := preAuthKeyID(cmd)
if err != nil {
return err
}
request := &v1.ExpirePreAuthKeyRequest{
Id: id,
}
idStr := strconv.FormatUint(id, util.Base10)
response, err := client.ExpirePreAuthKey(ctx, request)
resp, err := client.ExpirePreAuthKeyWithResponse(ctx, clientv1.ExpirePreAuthKeyJSONRequestBody{
Id: &idStr,
})
if err != nil {
return fmt.Errorf("expiring preauthkey: %w", err)
}
return printOutput(cmd, response, "Key expired")
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "Key expired")
}),
}
@@ -162,21 +178,25 @@ var deletePreAuthKeyCmd = &cobra.Command{
Use: cmdDelete,
Short: "Delete a preauthkey",
Aliases: []string{aliasDel, "rm", "d"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
id, err := preAuthKeyID(cmd)
if err != nil {
return err
}
request := &v1.DeletePreAuthKeyRequest{
Id: id,
}
idStr := strconv.FormatUint(id, util.Base10)
response, err := client.DeletePreAuthKey(ctx, request)
resp, err := client.DeletePreAuthKeyWithResponse(ctx, &clientv1.DeletePreAuthKeyParams{
Id: &idStr,
})
if err != nil {
return fmt.Errorf("deleting preauthkey: %w", err)
}
return printOutput(cmd, response, "Key deleted")
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "Key deleted")
}),
}
+4 -2
View File
@@ -213,7 +213,8 @@ func TestFilterPreReleasesIfStable(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
result := filterPreReleasesIfStable(func() string { return tt.currentVersion })(tt.tag)
if result != tt.expectedFilter {
t.Errorf("%s: got %v, want %v\nDescription: %s\nCurrent version: %s, Tag: %s",
t.Errorf(
"%s: got %v, want %v\nDescription: %s\nCurrent version: %s, Tag: %s",
tt.name,
result,
tt.expectedFilter,
@@ -293,7 +294,8 @@ func TestIsPreReleaseVersion(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
result := isPreReleaseVersion(tt.version)
if result != tt.expected {
t.Errorf("%s: got %v, want %v\nDescription: %s\nVersion: %s",
t.Errorf(
"%s: got %v, want %v\nDescription: %s\nVersion: %s",
tt.name,
result,
tt.expected,
+71 -46
View File
@@ -4,10 +4,11 @@ import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
"github.com/rs/zerolog/log"
@@ -45,27 +46,39 @@ func usernameAndIDFromFlag(cmd *cobra.Command) (uint64, string, error) {
// returning the raw flag id and the matched user.
func resolveSingleUser(
ctx context.Context,
client v1.HeadscaleServiceClient,
client *clientv1.ClientWithResponses,
cmd *cobra.Command,
) (uint64, *v1.User, error) {
) (uint64, *clientv1.User, error) {
id, username, err := usernameAndIDFromFlag(cmd)
if err != nil {
return 0, nil, err
}
users, err := client.ListUsers(ctx, &v1.ListUsersRequest{
Name: username,
Id: id,
})
params := &clientv1.ListUsersParams{}
if username != "" {
params.Name = &username
}
if id != 0 {
idStr := strconv.FormatUint(id, util.Base10)
params.Id = &idStr
}
resp, err := client.ListUsersWithResponse(ctx, params)
if err != nil {
return 0, nil, fmt.Errorf("listing users: %w", err)
}
if len(users.GetUsers()) != 1 {
if resp.StatusCode() != http.StatusOK {
return 0, nil, apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
users := resp.JSON200.Users
if len(users) != 1 {
return 0, nil, errMultipleUsersMatch
}
return id, users.GetUsers()[0], nil
return id, &users[0], nil
}
func init() {
@@ -102,19 +115,19 @@ var createUserCmd = &cobra.Command{
return nil
},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
userName := args[0]
log.Trace().Interface(zf.Client, client).Msg("obtained gRPC client")
log.Trace().Interface(zf.Client, client).Msg("obtained API client")
request := &v1.CreateUserRequest{Name: userName}
request := clientv1.CreateUserJSONRequestBody{Name: &userName}
if displayName, _ := cmd.Flags().GetString("display-name"); displayName != "" {
request.DisplayName = displayName
request.DisplayName = &displayName
}
if email, _ := cmd.Flags().GetString("email"); email != "" {
request.Email = email
request.Email = &email
}
if pictureURL, _ := cmd.Flags().GetString("picture-url"); pictureURL != "" {
@@ -122,17 +135,21 @@ var createUserCmd = &cobra.Command{
return fmt.Errorf("invalid picture URL: %w", err)
}
request.PictureUrl = pictureURL
request.PictureUrl = &pictureURL
}
log.Trace().Interface(zf.Request, request).Msg("sending CreateUser request")
response, err := client.CreateUser(ctx, request)
resp, err := client.CreateUserWithResponse(ctx, request)
if err != nil {
return fmt.Errorf("creating user: %w", err)
}
return printOutput(cmd, response.GetUser(), "User created")
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200.User, "User created")
}),
}
@@ -140,27 +157,29 @@ var destroyUserCmd = &cobra.Command{
Use: "destroy --identifier ID or --name NAME",
Short: "Destroys a user",
Aliases: []string{cmdDelete},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
_, user, err := resolveSingleUser(ctx, client, cmd)
if err != nil {
return err
}
if !confirmAction(cmd, fmt.Sprintf(
"Do you want to remove the user %q (%d) and any associated preauthkeys?",
user.GetName(), user.GetId(),
"Do you want to remove the user %q (%s) and any associated preauthkeys?",
user.Name, user.Id,
)) {
return printOutput(cmd, map[string]string{colResult: "User not destroyed"}, "User not destroyed")
}
deleteRequest := &v1.DeleteUserRequest{Id: user.GetId()}
response, err := client.DeleteUser(ctx, deleteRequest)
resp, err := client.DeleteUserWithResponse(ctx, user.Id)
if err != nil {
return fmt.Errorf("destroying user: %w", err)
}
return printOutput(cmd, response, "User destroyed")
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200, "User destroyed")
}),
}
@@ -168,8 +187,8 @@ var listUsersCmd = &cobra.Command{
Use: cmdList,
Short: "List all the users",
Aliases: []string{"ls", cmdShow},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
request := &v1.ListUsersRequest{}
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
params := &clientv1.ListUsersParams{}
id, _ := cmd.Flags().GetInt64("identifier")
username, _ := cmd.Flags().GetString("name")
@@ -178,29 +197,36 @@ var listUsersCmd = &cobra.Command{
// filter by one param at most
switch {
case id > 0:
request.Id = uint64(id)
idStr := strconv.FormatInt(id, util.Base10)
params.Id = &idStr
case username != "":
request.Name = username
params.Name = &username
case email != "":
request.Email = email
params.Email = &email
}
response, err := client.ListUsers(ctx, request)
resp, err := client.ListUsersWithResponse(ctx, params)
if err != nil {
return fmt.Errorf("listing users: %w", err)
}
return printListOutput(cmd, response.GetUsers(), func() error {
rows := make([][]string, 0, len(response.GetUsers()))
for _, user := range response.GetUsers() {
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
users := resp.JSON200.Users
return printListOutput(cmd, users, func() error {
rows := make([][]string, 0, len(users))
for _, user := range users {
rows = append(
rows,
[]string{
strconv.FormatUint(user.GetId(), util.Base10),
user.GetDisplayName(),
user.GetName(),
user.GetEmail(),
user.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
user.Id,
user.DisplayName,
user.Name,
user.Email,
user.CreatedAt.Format(HeadscaleDateTimeFormat),
},
)
}
@@ -214,7 +240,7 @@ var renameUserCmd = &cobra.Command{
Use: "rename",
Short: "Renames a user",
Aliases: []string{"mv"},
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
id, _, err := resolveSingleUser(ctx, client, cmd)
if err != nil {
return err
@@ -222,16 +248,15 @@ var renameUserCmd = &cobra.Command{
newName, _ := cmd.Flags().GetString("new-name")
renameReq := &v1.RenameUserRequest{
OldId: id,
NewName: newName,
}
response, err := client.RenameUser(ctx, renameReq)
resp, err := client.RenameUserWithResponse(ctx, strconv.FormatUint(id, util.Base10), newName)
if err != nil {
return fmt.Errorf("renaming user: %w", err)
}
return printOutput(cmd, response.GetUser(), "User renamed")
if resp.StatusCode() != http.StatusOK {
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
}
return printOutput(cmd, resp.JSON200.User, "User renamed")
}),
}
+155 -110
View File
@@ -6,11 +6,15 @@ import (
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"os"
"slices"
"strings"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/cenkalti/backoff/v5"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
@@ -19,10 +23,6 @@ import (
"github.com/pterm/pterm"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/types/known/timestamppb"
"gopkg.in/yaml.v3"
)
@@ -38,11 +38,45 @@ const (
var (
errAPIKeyNotSet = errors.New("HEADSCALE_CLI_API_KEY environment variable needs to be set")
errMissingParameter = errors.New("missing parameters")
errResponseStatus = errors.New("unexpected response status")
)
// mustMarkRequired marks the named flags as required on cmd, panicking
// if any name does not match a registered flag. This is only called
// from init() where a failure indicates a programming error.
// apiError turns a non-2xx response into an error, surfacing the server's
// RFC7807 problem detail. detail holds the operation context and errors[] the
// wrapped cause (e.g. "name is too long"); both are joined so the server's
// message text is not lost.
func apiError(statusCode int, problem *clientv1.ErrorModel) error {
if problem == nil {
return fmt.Errorf("%w: %d %s", errResponseStatus, statusCode, http.StatusText(statusCode))
}
parts := make([]string, 0, 2)
if problem.Detail != nil && *problem.Detail != "" {
parts = append(parts, *problem.Detail)
}
if problem.Errors != nil {
for _, e := range *problem.Errors {
if e.Message != nil && *e.Message != "" {
parts = append(parts, *e.Message)
}
}
}
if len(parts) == 0 && problem.Title != nil && *problem.Title != "" {
parts = append(parts, *problem.Title)
}
if len(parts) == 0 {
return fmt.Errorf("%w: %d %s", errResponseStatus, statusCode, http.StatusText(statusCode))
}
return fmt.Errorf("%w: %s", errResponseStatus, strings.Join(parts, ": "))
}
// mustMarkRequired marks the named flags as required, panicking on an unknown
// flag. Only called from init(), where a failure is a programming error.
func mustMarkRequired(cmd *cobra.Command, names ...string) {
for _, n := range names {
err := cmd.MarkFlagRequired(n)
@@ -69,45 +103,47 @@ func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) {
return app, nil
}
// grpcRunE wraps a cobra [cobra.Command.RunE] func, injecting a ready
// gRPC client and context. Connection lifecycle is managed by the
// wrapper — callers never see the underlying conn or cancel func.
func grpcRunE(
fn func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error,
// clientRunE wraps a [cobra.Command.RunE] func, injecting a ready API client
// and a context whose timeout/cancel the wrapper owns.
func clientRunE(
fn func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error,
) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
ctx, client, cancel, err := newHeadscaleCLIWithConfig()
if err != nil {
return fmt.Errorf("connecting to headscale: %w", err)
}
defer cancel()
defer conn.Close()
return fn(ctx, client, cmd, args)
}
}
// withGRPC opens a gRPC client, runs fn with it, and tears the
// connection down afterwards. It is the building block for commands
// that branch on a flag before deciding to talk to the server, where
// grpcRunE's whole-RunE wrapping does not fit.
func withGRPC(
fn func(ctx context.Context, client v1.HeadscaleServiceClient) error,
// withClient runs fn with an API client. For commands that branch on a flag
// before talking to the server, where clientRunE's whole-RunE wrapping does
// not fit.
func withClient(
fn func(ctx context.Context, client *clientv1.ClientWithResponses) error,
) error {
ctx, client, conn, cancel, err := newHeadscaleCLIWithConfig()
ctx, client, cancel, err := newHeadscaleCLIWithConfig()
if err != nil {
return fmt.Errorf("connecting to headscale: %w", err)
}
defer cancel()
defer conn.Close()
return fn(ctx, client)
}
func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc, error) {
// newHeadscaleCLIWithConfig builds an HTTP client for the Headscale v1 API.
//
// When cfg.CLI.Address is unset the CLI is assumed to run on the server host
// and talks to the unix socket over HTTP without authentication (local trust).
// Otherwise it talks to the remote TCP address over HTTPS and injects the
// configured API key as a bearer token.
func newHeadscaleCLIWithConfig() (context.Context, *clientv1.ClientWithResponses, context.CancelFunc, error) {
cfg, err := types.LoadCLIConfig()
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("loading configuration: %w", err)
return nil, nil, nil, fmt.Errorf("loading configuration: %w", err)
}
log.Debug().
@@ -116,10 +152,6 @@ func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *g
ctx, cancel := context.WithTimeout(context.Background(), cfg.CLI.Timeout)
grpcOptions := []grpc.DialOption{
grpc.WithBlock(), //nolint:staticcheck // SA1019: deprecated but supported in 1.x
}
address := cfg.CLI.Address
// If the address is not set, we assume that we are on the server hosting [hscontrol].
@@ -128,80 +160,111 @@ func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *g
Str("socket", cfg.UnixSocket).
Msgf("HEADSCALE_CLI_ADDRESS environment is not set, connecting to unix socket.")
address = cfg.UnixSocket
// Try to give the user better feedback if we cannot write to the headscale
// socket. Note: [os.OpenFile] on a Unix domain socket returns ENXIO on
// Linux which is expected — only permission errors are actionable here.
// The actual gRPC connection uses [net.Dial] which handles sockets properly.
socket, err := os.OpenFile(cfg.UnixSocket, os.O_WRONLY, SocketWritePermissions) //nolint
client, err := newSocketClient(cfg.UnixSocket)
if err != nil {
if os.IsPermission(err) {
cancel()
return nil, nil, nil, nil, fmt.Errorf(
"unable to read/write to headscale socket %q, do you have the correct permissions? %w",
cfg.UnixSocket,
err,
)
}
} else {
socket.Close()
}
grpcOptions = append(
grpcOptions,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(util.GrpcSocketDialer),
)
} else {
// If we are not connecting to a local server, require an API key for authentication
apiKey := cfg.CLI.APIKey
if apiKey == "" {
cancel()
return nil, nil, nil, nil, errAPIKeyNotSet
return nil, nil, nil, err
}
grpcOptions = append(
grpcOptions,
grpc.WithPerRPCCredentials(tokenAuth{
token: apiKey,
}),
)
log.Trace().Caller().Str(zf.Address, cfg.UnixSocket).Msg("connecting via unix socket")
if cfg.CLI.Insecure {
tlsConfig := &tls.Config{
// turn of gosec as we are intentionally setting
// insecure.
//nolint:gosec
InsecureSkipVerify: true,
}
grpcOptions = append(
grpcOptions,
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
)
} else {
grpcOptions = append(
grpcOptions,
grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
)
}
return ctx, client, cancel, nil
}
log.Trace().Caller().Str(zf.Address, address).Msg("connecting via gRPC")
// Remote connections require an API key for authentication.
apiKey := cfg.CLI.APIKey
if apiKey == "" {
cancel()
conn, err := grpc.DialContext(ctx, address, grpcOptions...) //nolint:staticcheck // SA1019: deprecated but supported in 1.x
return nil, nil, nil, errAPIKeyNotSet
}
client, err := newRemoteClient(address, apiKey, cfg.CLI.Insecure)
if err != nil {
cancel()
return nil, nil, nil, nil, fmt.Errorf("connecting to %s: %w", address, err)
return nil, nil, nil, err
}
client := v1.NewHeadscaleServiceClient(conn)
log.Trace().Caller().Str(zf.Address, address).Msg("connecting via HTTPS")
return ctx, client, conn, cancel, nil
return ctx, client, cancel, nil
}
// newSocketClient builds an API client that dials the local unix socket. The
// base-URL host is irrelevant; the custom dialer routes every request to the
// socket.
func newSocketClient(socketPath string) (*clientv1.ClientWithResponses, error) {
// Probe for a clearer permission error up front. [os.OpenFile] on a unix
// socket returns ENXIO on Linux (expected); only permission errors are
// actionable. The real connection goes through [net.Dial].
socket, err := os.OpenFile(socketPath, os.O_WRONLY, SocketWritePermissions) //nolint
if err != nil {
if os.IsPermission(err) {
return nil, fmt.Errorf(
"unable to read/write to headscale socket %q, do you have the correct permissions? %w",
socketPath,
err,
)
}
} else {
socket.Close()
}
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialHeadscaleSocket(ctx, socketPath)
},
},
}
return clientv1.NewClientWithResponses(
"http://local",
clientv1.WithHTTPClient(httpClient),
)
}
// dialHeadscaleSocket connects to the unix socket, retrying until it appears or
// ctx (the CLI timeout) expires. The socket is created late in startup (after
// noise key, database, migrations), so a command run right after the server
// starts can race its creation; retrying preserves the old gRPC client's
// blocking-dial tolerance rather than failing on a not-yet-present socket.
func dialHeadscaleSocket(ctx context.Context, socketPath string) (net.Conn, error) {
b := backoff.NewExponentialBackOff()
b.InitialInterval = 50 * time.Millisecond
b.MaxInterval = 1 * time.Second
return backoff.Retry(ctx, func() (net.Conn, error) {
return util.SocketDialer(ctx, socketPath)
}, backoff.WithBackOff(b))
}
// newRemoteClient builds an API client for a remote Headscale over HTTPS,
// honouring insecure (skip TLS verification) and injecting the API key as a
// bearer token on every request.
func newRemoteClient(address, apiKey string, insecure bool) (*clientv1.ClientWithResponses, error) {
transport := &http.Transport{}
if insecure {
transport.TLSClientConfig = &tls.Config{
// turn off gosec as we are intentionally setting insecure.
//nolint:gosec
InsecureSkipVerify: true,
}
}
httpClient := &http.Client{Transport: transport}
return clientv1.NewClientWithResponses(
"https://"+address,
clientv1.WithHTTPClient(httpClient),
clientv1.WithRequestEditorFn(func(_ context.Context, req *http.Request) error {
req.Header.Set("Authorization", "Bearer "+apiKey)
return nil
}),
)
}
// formatOutput serialises result into the requested format. For the
@@ -250,16 +313,16 @@ func printOutput(cmd *cobra.Command, result any, override string) error {
}
// expirationFromFlag parses the --expiration flag as a Prometheus-style
// duration (e.g. "90d", "1h") and returns an absolute timestamp.
func expirationFromFlag(cmd *cobra.Command) (*timestamppb.Timestamp, error) {
// duration (e.g. "90d", "1h") and returns an absolute time.
func expirationFromFlag(cmd *cobra.Command) (time.Time, error) {
durationStr, _ := cmd.Flags().GetString("expiration")
duration, err := model.ParseDuration(durationStr)
if err != nil {
return nil, fmt.Errorf("parsing duration: %w", err)
return time.Time{}, fmt.Errorf("parsing duration: %w", err)
}
return timestamppb.New(time.Now().UTC().Add(time.Duration(duration))), nil
return time.Now().UTC().Add(time.Duration(duration)), nil
}
// confirmAction returns true when the user confirms a prompt, or when
@@ -323,21 +386,3 @@ func hasMachineOutputFlag() bool {
return arg == outputFormatJSON || arg == outputFormatJSONLine || arg == outputFormatYAML
})
}
type tokenAuth struct {
token string
}
// Return value is mapped to request headers.
func (t tokenAuth) GetRequestMetadata(
ctx context.Context,
in ...string,
) (map[string]string, error) {
return map[string]string{
"authorization": "Bearer " + t.token,
}, nil
}
func (tokenAuth) RequireTransportSecurity() bool {
return true
}
+72
View File
@@ -0,0 +1,72 @@
package cli
import (
"context"
"net"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestDialHeadscaleSocketRetriesUntilPresent proves the CLI socket dialer
// tolerates a not-yet-created socket (the server-still-starting race) by
// retrying until it appears, rather than failing immediately like a bare dial.
func TestDialHeadscaleSocketRetriesUntilPresent(t *testing.T) {
sock := filepath.Join(t.TempDir(), "headscale.sock")
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
type result struct {
conn net.Conn
err error
}
done := make(chan result, 1)
go func() {
conn, err := dialHeadscaleSocket(ctx, sock)
done <- result{conn, err}
}()
// Listen only after the dialer has begun, so its backoff must retry the
// absent socket and connect once it exists.
var lc net.ListenConfig
ln, err := lc.Listen(ctx, "unix", sock)
require.NoError(t, err)
defer ln.Close()
go func() {
if conn, _ := ln.Accept(); conn != nil {
conn.Close()
}
}()
res := <-done
require.NoError(t, res.err)
require.NotNil(t, res.conn)
res.conn.Close()
}
// TestDialHeadscaleSocketRespectsDeadline proves the retry is bounded by the
// context: when the socket never appears, the dialer returns an error around the
// deadline instead of hanging.
func TestDialHeadscaleSocketRespectsDeadline(t *testing.T) {
sock := filepath.Join(t.TempDir(), "absent.sock")
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
start := time.Now()
conn, err := dialHeadscaleSocket(ctx, sock)
require.Error(t, err)
assert.Nil(t, conn)
assert.Less(t, time.Since(start), 5*time.Second, "should stop near the deadline, not hang")
}
+3 -1
View File
@@ -10,7 +10,9 @@ import (
"tailscale.com/net/tsaddr"
)
func GrpcSocketDialer(ctx context.Context, addr string) (net.Conn, error) {
// SocketDialer dials a local unix-domain socket, letting the HTTP CLI client
// reach the headscale API over its unix socket.
func SocketDialer(ctx context.Context, addr string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, "unix", addr)
+44 -44
View File
@@ -10,7 +10,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/integration/hsic"
@@ -1232,10 +1232,10 @@ func TestACLAutogroupTagged(t *testing.T) {
require.NoError(t, err)
// Create two pre-auth keys per user: one tagged, one untagged
taggedAuthKey, err := scenario.CreatePreAuthKeyWithTags(user.GetId(), true, false, []string{"tag:test"})
taggedAuthKey, err := scenario.CreatePreAuthKeyWithTags(mustParseID(user.Id), true, false, []string{"tag:test"})
require.NoError(t, err)
untaggedAuthKey, err := scenario.CreatePreAuthKey(user.GetId(), true, false)
untaggedAuthKey, err := scenario.CreatePreAuthKey(mustParseID(user.Id), true, false)
require.NoError(t, err)
// Create nodes with proper naming
@@ -1247,13 +1247,13 @@ func TestACLAutogroupTagged(t *testing.T) {
if i == 0 {
// First node is tagged - use tagged PreAuthKey
authKey = taggedAuthKey.GetKey()
authKey = taggedAuthKey.Key
version = "head"
t.Logf("Creating tagged node for %s", userStr)
} else {
// Second node is untagged - use untagged PreAuthKey
authKey = untaggedAuthKey.GetKey()
authKey = untaggedAuthKey.Key
version = "unstable"
t.Logf("Creating untagged node for %s", userStr)
@@ -1554,7 +1554,7 @@ func TestACLAutogroupSelf(t *testing.T) {
require.NoError(t, err)
// Create a tagged PreAuthKey for the router node (tags-as-identity model)
authKey, err := scenario.CreatePreAuthKeyWithTags(routerUser.GetId(), true, false, []string{"tag:router-node"})
authKey, err := scenario.CreatePreAuthKeyWithTags(mustParseID(routerUser.Id), true, false, []string{"tag:router-node"})
require.NoError(t, err)
// Create router node (tags come from the PreAuthKey).
@@ -1577,7 +1577,7 @@ func TestACLAutogroupSelf(t *testing.T) {
err = routerClient.WaitForNeedsLogin(integrationutil.PeerSyncTimeout())
require.NoError(t, err)
err = routerClient.Login(headscale.GetEndpoint(), authKey.GetKey())
err = routerClient.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
err = routerClient.WaitForRunning(integrationutil.PeerSyncTimeout())
@@ -2008,7 +2008,7 @@ func TestACLPolicyPropagationOverTime(t *testing.T) {
// Get the node list and find the newest node (highest ID)
var (
nodeList []*v1.Node
nodeList []*clientv1.Node
nodeToDeleteID uint64
)
@@ -2019,8 +2019,8 @@ func TestACLPolicyPropagationOverTime(t *testing.T) {
// Find the node with the highest ID (the newest one)
for _, node := range nodeList {
if node.GetId() > nodeToDeleteID {
nodeToDeleteID = node.GetId()
if mustParseID(node.Id) > nodeToDeleteID {
nodeToDeleteID = mustParseID(node.Id)
}
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "iteration %d: Phase 2b - listing nodes before deletion", iteration)
@@ -2205,7 +2205,7 @@ func TestACLTagPropagation(t *testing.T) {
nodes, err := headscale.ListNodes("user1")
require.NoError(t, err)
return user2Clients[0], user1Clients[0], nodes[0].GetId()
return user2Clients[0], user1Clients[0], mustParseID(nodes[0].Id)
},
initialAccess: false, // user2 cannot access user1 (no tag)
tagChange: []string{"tag:shared"}, // add tag:shared
@@ -2256,7 +2256,7 @@ func TestACLTagPropagation(t *testing.T) {
// Create user1's node WITH tag:shared via PreAuthKey
taggedKey, err := scenario.CreatePreAuthKeyWithTags(
userMap["user1"].GetId(), false, false, []string{"tag:shared"},
mustParseID(userMap["user1"].Id), false, false, []string{"tag:shared"},
)
require.NoError(t, err)
@@ -2269,11 +2269,11 @@ func TestACLTagPropagation(t *testing.T) {
tsic.WithNetfilter("off"),
)
require.NoError(t, err)
err = user1Node.Login(headscale.GetEndpoint(), taggedKey.GetKey())
err = user1Node.Login(headscale.GetEndpoint(), taggedKey.Key)
require.NoError(t, err)
// Create user2's node (untagged)
untaggedKey, err := scenario.CreatePreAuthKey(userMap["user2"].GetId(), false, false)
untaggedKey, err := scenario.CreatePreAuthKey(mustParseID(userMap["user2"].Id), false, false)
require.NoError(t, err)
user2Node, err := scenario.CreateTailscaleNode(
@@ -2285,7 +2285,7 @@ func TestACLTagPropagation(t *testing.T) {
tsic.WithNetfilter("off"),
)
require.NoError(t, err)
err = user2Node.Login(headscale.GetEndpoint(), untaggedKey.GetKey())
err = user2Node.Login(headscale.GetEndpoint(), untaggedKey.Key)
require.NoError(t, err)
err = scenario.WaitForTailscaleSync()
@@ -2295,10 +2295,10 @@ func TestACLTagPropagation(t *testing.T) {
allNodes, err := headscale.ListNodes()
require.NoError(t, err)
tagged := findNode(allNodes, func(n *v1.Node) bool { return len(n.GetTags()) > 0 })
tagged := findNode(allNodes, func(n *clientv1.Node) bool { return len(n.Tags) > 0 })
require.NotNil(t, tagged, "expected a tagged node")
return user2Node, user1Node, tagged.GetId()
return user2Node, user1Node, mustParseID(tagged.Id)
},
initialAccess: true, // user2 can access user1 (has tag:shared)
tagChange: []string{"tag:other"}, // replace with tag:other
@@ -2349,7 +2349,7 @@ func TestACLTagPropagation(t *testing.T) {
// Create user1's node with tag:team-a (user2 has NO ACL for this)
taggedKey, err := scenario.CreatePreAuthKeyWithTags(
userMap["user1"].GetId(), false, false, []string{"tag:team-a"},
mustParseID(userMap["user1"].Id), false, false, []string{"tag:team-a"},
)
require.NoError(t, err)
@@ -2362,11 +2362,11 @@ func TestACLTagPropagation(t *testing.T) {
tsic.WithNetfilter("off"),
)
require.NoError(t, err)
err = user1Node.Login(headscale.GetEndpoint(), taggedKey.GetKey())
err = user1Node.Login(headscale.GetEndpoint(), taggedKey.Key)
require.NoError(t, err)
// Create user2's node
untaggedKey, err := scenario.CreatePreAuthKey(userMap["user2"].GetId(), false, false)
untaggedKey, err := scenario.CreatePreAuthKey(mustParseID(userMap["user2"].Id), false, false)
require.NoError(t, err)
user2Node, err := scenario.CreateTailscaleNode(
@@ -2378,7 +2378,7 @@ func TestACLTagPropagation(t *testing.T) {
tsic.WithNetfilter("off"),
)
require.NoError(t, err)
err = user2Node.Login(headscale.GetEndpoint(), untaggedKey.GetKey())
err = user2Node.Login(headscale.GetEndpoint(), untaggedKey.Key)
require.NoError(t, err)
err = scenario.WaitForTailscaleSync()
@@ -2388,10 +2388,10 @@ func TestACLTagPropagation(t *testing.T) {
allNodes, err := headscale.ListNodes()
require.NoError(t, err)
tagged := findNode(allNodes, func(n *v1.Node) bool { return len(n.GetTags()) > 0 })
tagged := findNode(allNodes, func(n *clientv1.Node) bool { return len(n.Tags) > 0 })
require.NotNil(t, tagged, "expected a tagged node")
return user2Node, user1Node, tagged.GetId()
return user2Node, user1Node, mustParseID(tagged.Id)
},
initialAccess: false, // user2 cannot access (tag:team-a not in ACL)
tagChange: []string{"tag:team-b"}, // change to tag:team-b
@@ -2442,7 +2442,7 @@ func TestACLTagPropagation(t *testing.T) {
// Create user1's node with BOTH tags
taggedKey, err := scenario.CreatePreAuthKeyWithTags(
userMap["user1"].GetId(), false, false, []string{"tag:web", "tag:internal"},
mustParseID(userMap["user1"].Id), false, false, []string{"tag:web", "tag:internal"},
)
require.NoError(t, err)
@@ -2455,11 +2455,11 @@ func TestACLTagPropagation(t *testing.T) {
tsic.WithNetfilter("off"),
)
require.NoError(t, err)
err = user1Node.Login(headscale.GetEndpoint(), taggedKey.GetKey())
err = user1Node.Login(headscale.GetEndpoint(), taggedKey.Key)
require.NoError(t, err)
// Create user2's node
untaggedKey, err := scenario.CreatePreAuthKey(userMap["user2"].GetId(), false, false)
untaggedKey, err := scenario.CreatePreAuthKey(mustParseID(userMap["user2"].Id), false, false)
require.NoError(t, err)
user2Node, err := scenario.CreateTailscaleNode(
@@ -2471,7 +2471,7 @@ func TestACLTagPropagation(t *testing.T) {
tsic.WithNetfilter("off"),
)
require.NoError(t, err)
err = user2Node.Login(headscale.GetEndpoint(), untaggedKey.GetKey())
err = user2Node.Login(headscale.GetEndpoint(), untaggedKey.Key)
require.NoError(t, err)
err = scenario.WaitForTailscaleSync()
@@ -2481,10 +2481,10 @@ func TestACLTagPropagation(t *testing.T) {
allNodes, err := headscale.ListNodes()
require.NoError(t, err)
tagged := findNode(allNodes, func(n *v1.Node) bool { return len(n.GetTags()) > 0 })
tagged := findNode(allNodes, func(n *clientv1.Node) bool { return len(n.Tags) > 0 })
require.NotNil(t, tagged, "expected a tagged node")
return user2Node, user1Node, tagged.GetId()
return user2Node, user1Node, mustParseID(tagged.Id)
},
initialAccess: true, // user2 can access (has tag:web)
tagChange: []string{"tag:internal"}, // remove tag:web, keep tag:internal
@@ -2535,7 +2535,7 @@ func TestACLTagPropagation(t *testing.T) {
nodes, err := headscale.ListNodes("user1")
require.NoError(t, err)
return user2Clients[0], user1Clients[0], nodes[0].GetId()
return user2Clients[0], user1Clients[0], mustParseID(nodes[0].Id)
},
initialAccess: false, // user2 cannot access user1 (no tag yet)
tagChange: []string{"tag:server"}, // assign tag:server
@@ -2616,11 +2616,11 @@ func TestACLTagPropagation(t *testing.T) {
allNodes, err := headscale.ListNodes()
assert.NoError(c, err)
node := findNode(allNodes, func(n *v1.Node) bool { return n.GetId() == targetNodeID })
node := findNode(allNodes, func(n *clientv1.Node) bool { return mustParseID(n.Id) == targetNodeID })
assert.NotNil(c, node, "Node should still exist")
if node != nil {
assert.ElementsMatch(c, tt.tagChange, node.GetTags(), "Tags should be updated")
assert.ElementsMatch(c, tt.tagChange, node.Tags, "Tags should be updated")
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying tag change applied")
@@ -2759,7 +2759,7 @@ func TestACLTagPropagationPortSpecific(t *testing.T) {
// Create user1's node WITH tag:webserver
taggedKey, err := scenario.CreatePreAuthKeyWithTags(
userMap["user1"].GetId(), false, false, []string{"tag:webserver"},
mustParseID(userMap["user1"].Id), false, false, []string{"tag:webserver"},
)
require.NoError(t, err)
@@ -2773,11 +2773,11 @@ func TestACLTagPropagationPortSpecific(t *testing.T) {
)
require.NoError(t, err)
err = user1Node.Login(headscale.GetEndpoint(), taggedKey.GetKey())
err = user1Node.Login(headscale.GetEndpoint(), taggedKey.Key)
require.NoError(t, err)
// Create user2's node
untaggedKey, err := scenario.CreatePreAuthKey(userMap["user2"].GetId(), false, false)
untaggedKey, err := scenario.CreatePreAuthKey(mustParseID(userMap["user2"].Id), false, false)
require.NoError(t, err)
user2Node, err := scenario.CreateTailscaleNode(
@@ -2789,7 +2789,7 @@ func TestACLTagPropagationPortSpecific(t *testing.T) {
)
require.NoError(t, err)
err = user2Node.Login(headscale.GetEndpoint(), untaggedKey.GetKey())
err = user2Node.Login(headscale.GetEndpoint(), untaggedKey.Key)
require.NoError(t, err)
err = scenario.WaitForTailscaleSync()
@@ -2799,10 +2799,10 @@ func TestACLTagPropagationPortSpecific(t *testing.T) {
allNodes, err := headscale.ListNodes()
require.NoError(t, err)
tagged := findNode(allNodes, func(n *v1.Node) bool { return len(n.GetTags()) > 0 })
tagged := findNode(allNodes, func(n *clientv1.Node) bool { return len(n.Tags) > 0 })
require.NotNil(t, tagged, "expected a tagged node")
targetNodeID := tagged.GetId()
targetNodeID := mustParseID(tagged.Id)
targetFQDN, err := user1Node.FQDN()
require.NoError(t, err)
@@ -2828,11 +2828,11 @@ func TestACLTagPropagationPortSpecific(t *testing.T) {
allNodes, err := headscale.ListNodes()
assert.NoError(c, err) //nolint:testifylint // CollectT requires assert
node := findNode(allNodes, func(n *v1.Node) bool { return n.GetId() == targetNodeID })
node := findNode(allNodes, func(n *clientv1.Node) bool { return mustParseID(n.Id) == targetNodeID })
assert.NotNil(c, node, "Node should still exist")
if node != nil {
assert.ElementsMatch(c, []string{"tag:sshonly"}, node.GetTags(), "Tags should be updated to sshonly")
assert.ElementsMatch(c, []string{"tag:sshonly"}, node.Tags, "Tags should be updated to sshonly")
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying tag change applied")
@@ -3070,7 +3070,7 @@ func TestACLGroupAfterUserDeletion(t *testing.T) {
nodes, err := headscale.ListNodes("user3")
require.NoError(t, err)
require.Len(t, nodes, 1, "user3 should have exactly one node")
user3NodeID := nodes[0].GetId()
user3NodeID := mustParseID(nodes[0].Id)
// Delete user3's node first (required before deleting the user)
err = headscale.DeleteNode(user3NodeID)
@@ -3081,7 +3081,7 @@ func TestACLGroupAfterUserDeletion(t *testing.T) {
require.NoError(t, err, "user3 should exist")
// Now delete user3 (after their nodes are deleted)
err = headscale.DeleteUser(user3.GetId())
err = headscale.DeleteUser(mustParseID(user3.Id))
require.NoError(t, err)
// Verify user3 is deleted
@@ -3249,13 +3249,13 @@ func TestACLGroupDeletionExactReproduction(t *testing.T) {
nodes, err := headscale.ListNodes(userToDelete)
require.NoError(t, err)
require.Len(t, nodes, 1)
err = headscale.DeleteNode(nodes[0].GetId())
err = headscale.DeleteNode(mustParseID(nodes[0].Id))
require.NoError(t, err)
userToDeleteObj, err := GetUserByName(headscale, userToDelete)
require.NoError(t, err, "user to delete should exist")
err = headscale.DeleteUser(userToDeleteObj.GetId())
err = headscale.DeleteUser(mustParseID(userToDeleteObj.Id))
require.NoError(t, err)
t.Log("Step 2: DONE - user2 deleted, ACL still has user2@ reference")
+46 -51
View File
@@ -11,13 +11,12 @@ import (
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/integrationutil"
"github.com/juanfont/headscale/integration/tsic"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protojson"
)
// TestAPIAuthenticationBypass tests that the API authentication middleware
@@ -215,19 +214,19 @@ func TestAPIAuthenticationBypass(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode,
"Expected 200 status code with valid API key")
// Should be able to parse as protobuf JSON
var response v1.ListUsersResponse
// Should be able to parse as JSON
var response clientv1.ListUsersOutputBody
err = protojson.Unmarshal(body, &response)
require.NoError(t, err, "Response should be valid protobuf JSON with valid API key")
err = json.Unmarshal(body, &response)
require.NoError(t, err, "Response should be valid JSON with valid API key")
// Should contain our test users
users := response.GetUsers()
users := response.Users
assert.Len(t, users, 3, "Should have 3 users")
userNames := make([]string, len(users))
for i, u := range users {
userNames[i] = u.GetName()
userNames[i] = u.Name
}
assert.Contains(t, userNames, "user1")
@@ -406,12 +405,12 @@ func TestAPIAuthenticationBypassCurl(t *testing.T) {
"Curl with valid API key should return 200")
// Should contain user data
var response v1.ListUsersResponse
var response clientv1.ListUsersOutputBody
err = protojson.Unmarshal([]byte(responseBody), &response)
require.NoError(t, err, "Response should be valid protobuf JSON")
err = json.Unmarshal([]byte(responseBody), &response)
require.NoError(t, err, "Response should be valid JSON")
users := response.GetUsers()
users := response.Users
assert.Len(t, users, 2, "Should have 2 users")
})
}
@@ -420,7 +419,7 @@ func TestAPIAuthenticationBypassCurl(t *testing.T) {
// properly blocks unauthorized requests.
// This test verifies that the gRPC API does not have the same bypass issue
// as the HTTP API middleware.
func TestGRPCAuthenticationBypass(t *testing.T) {
func TestRemoteCLIAuthenticationBypass(t *testing.T) {
IntegrationSkip(t)
spec := ScenarioSpec{
@@ -432,14 +431,9 @@ func TestGRPCAuthenticationBypass(t *testing.T) {
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
// We need TLS for remote gRPC connections
err = scenario.CreateHeadscaleEnv(
[]tsic.Option{},
hsic.WithTestName("grpcauthtest"),
hsic.WithConfigEnv(map[string]string{
// Enable gRPC on the standard port
"HEADSCALE_GRPC_LISTEN_ADDR": "0.0.0.0:50443",
}),
hsic.WithTestName("remotecliauth"),
)
require.NoError(t, err)
@@ -460,43 +454,44 @@ func TestGRPCAuthenticationBypass(t *testing.T) {
validAPIKey := strings.TrimSpace(apiKeyOutput)
// Get the gRPC endpoint
// For gRPC, we need to use the hostname and port 50443
grpcAddress := headscale.GetHostname() + ":50443"
// Get the remote HTTP API endpoint as host:port; the CLI dials it over TLS
// with --insecure to skip verification of the test certificate.
remoteAddr := strings.TrimPrefix(strings.TrimPrefix(headscale.GetEndpoint(), "https://"), "http://")
t.Run("gRPC_NoAPIKey", func(t *testing.T) {
t.Run("Remote_NoAPIKey", func(t *testing.T) {
// Test 1: Try to use CLI without API key (should fail)
// When HEADSCALE_CLI_ADDRESS is set but HEADSCALE_CLI_API_KEY is not set,
// the CLI should fail immediately
_, err := headscale.Execute(
[]string{
"sh", "-c",
fmt.Sprintf("HEADSCALE_CLI_ADDRESS=%s HEADSCALE_CLI_INSECURE=true headscale users list --output json 2>&1", grpcAddress),
fmt.Sprintf("HEADSCALE_CLI_ADDRESS=%s HEADSCALE_CLI_INSECURE=true headscale users list --output json 2>&1", remoteAddr),
},
)
// Should fail - CLI exits when API key is missing
assert.Error(t, err,
"gRPC connection without API key should fail")
"remote connection without API key should fail")
})
t.Run("gRPC_InvalidAPIKey", func(t *testing.T) {
t.Run("Remote_InvalidAPIKey", func(t *testing.T) {
// Test 2: Try to use CLI with invalid API key (should fail with auth error)
output, err := headscale.Execute(
[]string{
"sh", "-c",
fmt.Sprintf("HEADSCALE_CLI_ADDRESS=%s HEADSCALE_CLI_API_KEY=invalid-key-12345 HEADSCALE_CLI_INSECURE=true headscale users list --output json 2>&1", grpcAddress),
fmt.Sprintf("HEADSCALE_CLI_ADDRESS=%s HEADSCALE_CLI_API_KEY=invalid-key-12345 HEADSCALE_CLI_INSECURE=true headscale users list --output json 2>&1", remoteAddr),
},
)
// Should fail with authentication error
require.Error(t, err,
"gRPC connection with invalid API key should fail")
"remote connection with invalid API key should fail")
// Should contain authentication error message
outputStr := strings.ToLower(output)
assert.True(t,
strings.Contains(outputStr, "unauthenticated") ||
strings.Contains(outputStr, "unauthorized") ||
strings.Contains(outputStr, "unauthenticated") ||
strings.Contains(outputStr, "invalid token") ||
strings.Contains(outputStr, "validating token") ||
strings.Contains(outputStr, "authentication"),
@@ -504,27 +499,27 @@ func TestGRPCAuthenticationBypass(t *testing.T) {
// Should NOT leak user data
assert.NotContains(t, output, "grpcuser1",
"SECURITY ISSUE: gRPC should not leak user data with invalid auth")
"SECURITY ISSUE: remote API should not leak user data with invalid auth")
assert.NotContains(t, output, "grpcuser2",
"SECURITY ISSUE: gRPC should not leak user data with invalid auth")
"SECURITY ISSUE: remote API should not leak user data with invalid auth")
})
t.Run("gRPC_ValidAPIKey", func(t *testing.T) {
t.Run("Remote_ValidAPIKey", func(t *testing.T) {
// Test 3: Use CLI with valid API key (should succeed)
output, err := headscale.Execute(
[]string{
"sh", "-c",
fmt.Sprintf("HEADSCALE_CLI_ADDRESS=%s HEADSCALE_CLI_API_KEY=%s HEADSCALE_CLI_INSECURE=true headscale users list --output json", grpcAddress, validAPIKey),
fmt.Sprintf("HEADSCALE_CLI_ADDRESS=%s HEADSCALE_CLI_API_KEY=%s HEADSCALE_CLI_INSECURE=true headscale users list --output json", remoteAddr, validAPIKey),
},
)
// Should succeed
require.NoError(t, err,
"gRPC connection with valid API key should succeed, output: %s", output)
"remote connection with valid API key should succeed, output: %s", output)
// CLI outputs the users array directly, not wrapped in [v1.ListUsersResponse]
// Parse as JSON array (CLI uses [json.Marshal], not protojson)
var users []*v1.User
// CLI outputs the users array directly, not wrapped in a response object
// Parse as JSON array (CLI uses [json.Marshal])
var users []*clientv1.User
err = json.Unmarshal([]byte(output), &users)
require.NoError(t, err, "Response should be valid JSON array")
@@ -532,7 +527,7 @@ func TestGRPCAuthenticationBypass(t *testing.T) {
userNames := make([]string, len(users))
for i, u := range users {
userNames[i] = u.GetName()
userNames[i] = u.Name
}
assert.Contains(t, userNames, "grpcuser1")
@@ -544,7 +539,7 @@ func TestGRPCAuthenticationBypass(t *testing.T) {
// with --config flag does not have authentication bypass issues when
// connecting to a remote server.
// Note: When using --config with local unix socket, no auth is needed.
// This test focuses on remote gRPC connections which require API keys.
// This test focuses on remote HTTP connections which require API keys.
func TestCLIWithConfigAuthenticationBypass(t *testing.T) {
IntegrationSkip(t)
@@ -560,9 +555,6 @@ func TestCLIWithConfigAuthenticationBypass(t *testing.T) {
err = scenario.CreateHeadscaleEnv(
[]tsic.Option{},
hsic.WithTestName("cliconfigauth"),
hsic.WithConfigEnv(map[string]string{
"HEADSCALE_GRPC_LISTEN_ADDR": "0.0.0.0:50443",
}),
)
require.NoError(t, err)
@@ -583,7 +575,9 @@ func TestCLIWithConfigAuthenticationBypass(t *testing.T) {
validAPIKey := strings.TrimSpace(apiKeyOutput)
grpcAddress := headscale.GetHostname() + ":50443"
// Remote HTTP API endpoint as host:port; the CLI dials it over TLS with
// insecure verification skipped for the test certificate.
remoteAddr := strings.TrimPrefix(strings.TrimPrefix(headscale.GetEndpoint(), "https://"), "http://")
// Create a config file for testing
configWithoutKey := fmt.Sprintf(`
@@ -591,7 +585,7 @@ cli:
address: %s
timeout: 5s
insecure: true
`, grpcAddress)
`, remoteAddr)
configWithInvalidKey := fmt.Sprintf(`
cli:
@@ -599,7 +593,7 @@ cli:
api_key: invalid-key-12345
timeout: 5s
insecure: true
`, grpcAddress)
`, remoteAddr)
configWithValidKey := fmt.Sprintf(`
cli:
@@ -607,7 +601,7 @@ cli:
api_key: %s
timeout: 5s
insecure: true
`, grpcAddress, validAPIKey)
`, remoteAddr, validAPIKey)
t.Run("CLI_Config_NoAPIKey", func(t *testing.T) {
// Create config file without API key
@@ -649,7 +643,8 @@ cli:
// Should indicate authentication failure
outputStr := strings.ToLower(output)
assert.True(t,
strings.Contains(outputStr, "unauthenticated") ||
strings.Contains(outputStr, "unauthorized") ||
strings.Contains(outputStr, "unauthenticated") ||
strings.Contains(outputStr, "invalid token") ||
strings.Contains(outputStr, "validating token") ||
strings.Contains(outputStr, "authentication"),
@@ -681,9 +676,9 @@ cli:
require.NoError(t, err,
"CLI with valid API key should succeed")
// CLI outputs the users array directly, not wrapped in [v1.ListUsersResponse]
// Parse as JSON array (CLI uses [json.Marshal], not protojson)
var users []*v1.User
// CLI outputs the users array directly, not wrapped in a response object
// Parse as JSON array (CLI uses [json.Marshal])
var users []*clientv1.User
err = json.Unmarshal([]byte(output), &users)
require.NoError(t, err, "Response should be valid JSON array")
@@ -691,7 +686,7 @@ cli:
userNames := make([]string, len(users))
for i, u := range users {
userNames[i] = u.GetName()
userNames[i] = u.Name
}
assert.Contains(t, userNames, "cliuser1")
+42 -43
View File
@@ -4,11 +4,10 @@ import (
"fmt"
"net/netip"
"slices"
"strconv"
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/integration/hsic"
@@ -74,7 +73,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
}
var (
listNodes []*v1.Node
listNodes []*clientv1.Node
nodeCountBeforeLogout int
)
@@ -135,12 +134,12 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
require.NoError(t, err)
for _, userName := range spec.Users {
key, err := scenario.CreatePreAuthKey(userMap[userName].GetId(), true, false)
key, err := scenario.CreatePreAuthKey(mustParseID(userMap[userName].Id), true, false)
if err != nil {
t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
}
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.Key)
if err != nil {
t.Fatalf("failed to run tailscale up for user %s: %s", userName, err)
}
@@ -256,7 +255,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
requireAllClientsNetInfoAndDERP(t, headscale, expectedNodes, "all clients should have NetInfo and DERP after initial login", 3*time.Minute)
var (
listNodes []*v1.Node
listNodes []*clientv1.Node
nodeCountBeforeLogout int
)
@@ -290,7 +289,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
require.NoError(t, err)
// Create a new authkey for user1, to be used for all clients
key, err := scenario.CreatePreAuthKey(userMap["user1"].GetId(), true, false)
key, err := scenario.CreatePreAuthKey(mustParseID(userMap["user1"].Id), true, false)
if err != nil {
t.Fatalf("failed to create pre-auth key for user1: %s", err)
}
@@ -298,13 +297,13 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
// Log in all clients as user1, iterating over the spec only returns the
// clients, not the usernames.
for _, userName := range spec.Users {
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.Key)
if err != nil {
t.Fatalf("failed to run tailscale up for user %s: %s", userName, err)
}
}
var user1Nodes []*v1.Node
var user1Nodes []*clientv1.Node
t.Logf("Validating user1 node count after relogin at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -318,7 +317,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
// Collect expected node IDs for user1 after relogin
expectedUser1Nodes := make([]types.NodeID, 0, len(user1Nodes))
for _, node := range user1Nodes {
expectedUser1Nodes = append(expectedUser1Nodes, types.NodeID(node.GetId()))
expectedUser1Nodes = append(expectedUser1Nodes, types.NodeID(mustParseID(node.Id)))
}
// Validate connection state after relogin as user1
@@ -328,7 +327,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
// Validate that user2 still has their original nodes after user1's re-authentication
// When nodes re-authenticate with a different user's pre-auth key, NEW nodes are created
// for the new user. The original nodes remain with the original user.
var user2Nodes []*v1.Node
var user2Nodes []*clientv1.Node
t.Logf("Validating user2 node persistence after user1 relogin at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -402,7 +401,7 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
requireAllClientsNetInfoAndDERP(t, headscale, expectedNodes, "all clients should have NetInfo and DERP after initial login", 3*time.Minute)
var (
listNodes []*v1.Node
listNodes []*clientv1.Node
nodeCountBeforeLogout int
)
@@ -446,7 +445,7 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
require.NoError(t, err)
for _, userName := range spec.Users {
key, err := scenario.CreatePreAuthKey(userMap[userName].GetId(), true, false)
key, err := scenario.CreatePreAuthKey(mustParseID(userMap[userName].Id), true, false)
if err != nil {
t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
}
@@ -458,12 +457,12 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
"preauthkeys",
"expire",
"--id",
strconv.FormatUint(key.GetId(), 10),
key.Id,
})
require.NoError(t, err)
require.NoError(t, err)
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.Key)
assert.ErrorContains(t, err, "authkey expired")
}
})
@@ -498,14 +497,14 @@ func TestAuthKeyDeleteKey(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap["user1"].GetId()
userID := mustParseID(userMap["user1"].Id)
// Create a pre-auth key - we keep the full key string before it gets redacted
authKey, err := scenario.CreatePreAuthKey(userID, false, false)
require.NoError(t, err)
authKeyString := authKey.GetKey()
authKeyID := authKey.GetId()
authKeyString := authKey.Key
authKeyID := mustParseID(authKey.Id)
t.Logf("Created pre-auth key ID %d: %s", authKeyID, authKeyString)
// Create a tailscale client and log it in with the auth key
@@ -519,7 +518,7 @@ func TestAuthKeyDeleteKey(t *testing.T) {
require.NoError(t, err)
// Wait for the node to be registered
var user1Nodes []*v1.Node
var user1Nodes []*clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
var err error
@@ -529,8 +528,8 @@ func TestAuthKeyDeleteKey(t *testing.T) {
assert.Len(c, user1Nodes, 1)
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for node to be registered")
nodeID := user1Nodes[0].GetId()
nodeName := user1Nodes[0].GetName()
nodeID := mustParseID(user1Nodes[0].Id)
nodeName := user1Nodes[0].Name
t.Logf("Node %d (%s) created successfully with auth_key_id=%d", nodeID, nodeName, authKeyID)
// Verify node is online
@@ -640,7 +639,7 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) {
// Step 1: Verify initial route is advertised, approved, and SERVING
t.Logf("Step 1: Verifying initial route is advertised, approved, and SERVING at %s", time.Now().Format(TimestampFormat))
var initialNode *v1.Node
var initialNode *clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err := headscale.ListNodes()
@@ -650,21 +649,21 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) {
if len(nodes) == 1 {
initialNode = nodes[0]
// Check: 1 announced, 1 approved, 1 serving (subnet route)
assert.Lenf(c, initialNode.GetAvailableRoutes(), 1,
"Node should have 1 available route, got %v", initialNode.GetAvailableRoutes())
assert.Lenf(c, initialNode.GetApprovedRoutes(), 1,
"Node should have 1 approved route, got %v", initialNode.GetApprovedRoutes())
assert.Lenf(c, initialNode.GetSubnetRoutes(), 1,
"Node should have 1 serving (subnet) route, got %v - THIS IS THE BUG if empty", initialNode.GetSubnetRoutes())
assert.Contains(c, initialNode.GetSubnetRoutes(), advertiseRoute,
assert.Lenf(c, initialNode.AvailableRoutes, 1,
"Node should have 1 available route, got %v", initialNode.AvailableRoutes)
assert.Lenf(c, initialNode.ApprovedRoutes, 1,
"Node should have 1 approved route, got %v", initialNode.ApprovedRoutes)
assert.Lenf(c, initialNode.SubnetRoutes, 1,
"Node should have 1 serving (subnet) route, got %v - THIS IS THE BUG if empty", initialNode.SubnetRoutes)
assert.Contains(c, initialNode.SubnetRoutes, advertiseRoute,
"Subnet routes should contain %s", advertiseRoute)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "initial route should be serving")
require.NotNil(t, initialNode, "Initial node should be found")
initialNodeID := initialNode.GetId()
t.Logf("Initial node ID: %d, Available: %v, Approved: %v, Serving: %v",
initialNodeID, initialNode.GetAvailableRoutes(), initialNode.GetApprovedRoutes(), initialNode.GetSubnetRoutes())
initialNodeID := initialNode.Id
t.Logf("Initial node ID: %s, Available: %v, Approved: %v, Serving: %v",
initialNodeID, initialNode.AvailableRoutes, initialNode.ApprovedRoutes, initialNode.SubnetRoutes)
// Step 2: Logout
t.Logf("Step 2: Logging out at %s", time.Now().Format(TimestampFormat))
@@ -694,12 +693,12 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
key, err := scenario.CreatePreAuthKey(userMap[user].GetId(), true, false)
key, err := scenario.CreatePreAuthKey(mustParseID(userMap[user].Id), true, false)
require.NoError(t, err)
// Re-login - the container already has extraLoginArgs with --advertise-routes
// from the initial setup, so routes will be advertised on re-login
err = scenario.RunTailscaleUp(user, headscale.GetEndpoint(), key.GetKey())
err = scenario.RunTailscaleUp(user, headscale.GetEndpoint(), key.Key)
require.NoError(t, err)
// Wait for client to be running
@@ -722,23 +721,23 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) {
if len(nodes) == 1 {
node := nodes[0]
t.Logf("After relogin - Available: %v, Approved: %v, Serving: %v",
node.GetAvailableRoutes(), node.GetApprovedRoutes(), node.GetSubnetRoutes())
node.AvailableRoutes, node.ApprovedRoutes, node.SubnetRoutes)
// This is where issue #2896 manifests:
// - Available shows the route (from [tailcfg.Hostinfo.RoutableIPs])
// - Approved shows the route (from [tailcfg.Node.ApprovedRoutes])
// - BUT Serving ([tailcfg.Node.SubnetRoutes]/[ipnstate.PeerStatus.PrimaryRoutes]) is EMPTY!
assert.Lenf(c, node.GetAvailableRoutes(), 1,
"Node should have 1 available route after relogin, got %v", node.GetAvailableRoutes())
assert.Lenf(c, node.GetApprovedRoutes(), 1,
"Node should have 1 approved route after relogin, got %v", node.GetApprovedRoutes())
assert.Lenf(c, node.GetSubnetRoutes(), 1,
"BUG #2896: Node should have 1 SERVING route after relogin, got %v", node.GetSubnetRoutes())
assert.Contains(c, node.GetSubnetRoutes(), advertiseRoute,
assert.Lenf(c, node.AvailableRoutes, 1,
"Node should have 1 available route after relogin, got %v", node.AvailableRoutes)
assert.Lenf(c, node.ApprovedRoutes, 1,
"Node should have 1 approved route after relogin, got %v", node.ApprovedRoutes)
assert.Lenf(c, node.SubnetRoutes, 1,
"BUG #2896: Node should have 1 SERVING route after relogin, got %v", node.SubnetRoutes)
assert.Contains(c, node.SubnetRoutes, advertiseRoute,
"BUG #2896: Subnet routes should contain %s after relogin", advertiseRoute)
// Also verify node ID was preserved (same node, not new registration)
assert.Equal(c, initialNodeID, node.GetId(),
assert.Equal(c, initialNodeID, node.Id,
"Node ID should be preserved after same-user relogin")
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll,
+137 -137
View File
@@ -11,7 +11,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/integration/hsic"
@@ -86,26 +86,26 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
listUsers, err := headscale.ListUsers()
require.NoError(t, err)
want := []*v1.User{
want := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@test.no",
},
{
Id: 2,
Id: "2",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
},
{
Id: 3,
Id: "3",
Name: "user2",
Email: "user2@test.no",
},
{
Id: 4,
Id: "4",
Name: "user2",
Email: "", // Unverified
Provider: "oidc",
@@ -114,10 +114,10 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
})
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
t.Fatalf("unexpected users: %s", diff)
}
}
@@ -245,34 +245,34 @@ func TestOIDC024UserCreation(t *testing.T) {
emailVerified bool
cliUsers []string
oidcUsers []string
want func(iss string) []*v1.User
want func(iss string) []*clientv1.User
}{
{
name: "no-migration-verified-email",
emailVerified: true,
cliUsers: []string{"user1", "user2"},
oidcUsers: []string{"user1", "user2"},
want: func(iss string) []*v1.User {
return []*v1.User{
want: func(iss string) []*clientv1.User {
return []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@test.no",
},
{
Id: 2,
Id: "2",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: iss + "/user1",
},
{
Id: 3,
Id: "3",
Name: "user2",
Email: "user2@test.no",
},
{
Id: 4,
Id: "4",
Name: "user2",
Email: "user2@headscale.net",
Provider: "oidc",
@@ -286,26 +286,26 @@ func TestOIDC024UserCreation(t *testing.T) {
emailVerified: false,
cliUsers: []string{"user1", "user2"},
oidcUsers: []string{"user1", "user2"},
want: func(iss string) []*v1.User {
return []*v1.User{
want: func(iss string) []*clientv1.User {
return []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@test.no",
},
{
Id: 2,
Id: "2",
Name: "user1",
Provider: "oidc",
ProviderId: iss + "/user1",
},
{
Id: 3,
Id: "3",
Name: "user2",
Email: "user2@test.no",
},
{
Id: 4,
Id: "4",
Name: "user2",
Provider: "oidc",
ProviderId: iss + "/user2",
@@ -318,26 +318,26 @@ func TestOIDC024UserCreation(t *testing.T) {
emailVerified: false,
cliUsers: []string{"user1.headscale.net", "user2.headscale.net"},
oidcUsers: []string{"user1", "user2"},
want: func(iss string) []*v1.User {
return []*v1.User{
want: func(iss string) []*clientv1.User {
return []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1.headscale.net",
Email: "user1.headscale.net@test.no",
},
{
Id: 2,
Id: "2",
Name: "user1",
Provider: "oidc",
ProviderId: iss + "/user1",
},
{
Id: 3,
Id: "3",
Name: "user2.headscale.net",
Email: "user2.headscale.net@test.no",
},
{
Id: 4,
Id: "4",
Name: "user2",
Provider: "oidc",
ProviderId: iss + "/user2",
@@ -393,10 +393,10 @@ func TestOIDC024UserCreation(t *testing.T) {
require.NoError(t, err)
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
})
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
t.Errorf("unexpected users: %s", diff)
}
})
@@ -509,9 +509,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
assert.NoError(ct, err, "Failed to list users during initial validation")
assert.Len(ct, listUsers, 1, "Expected exactly 1 user after first login, got %d", len(listUsers))
wantUsers := []*v1.User{
wantUsers := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
@@ -520,17 +520,17 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
ct.Errorf("User validation failed after first login - unexpected users: %s", diff)
}
}, integrationutil.StatusReadyTimeout, 1*time.Second, "validating user1 creation after initial OIDC login")
t.Logf("Validating initial node creation at %s", time.Now().Format(TimestampFormat))
var listNodes []*v1.Node
var listNodes []*clientv1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
@@ -593,16 +593,16 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
assert.NoError(ct, err, "Failed to list users after user2 login")
assert.Len(ct, listUsers, 2, "Expected exactly 2 users after user2 login, got %d users", len(listUsers))
wantUsers := []*v1.User{
wantUsers := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
},
{
Id: 2,
Id: "2",
Name: "user2",
Email: "user2@headscale.net",
Provider: "oidc",
@@ -611,15 +611,15 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
ct.Errorf("User validation failed after user2 login - expected both user1 and user2: %s", diff)
}
}, integrationutil.StatusReadyTimeout, 1*time.Second, "validating both user1 and user2 exist after second OIDC login")
var listNodesAfterNewUserLogin []*v1.Node
var listNodesAfterNewUserLogin []*clientv1.Node
// First, wait for the new node to be created
t.Logf("Waiting for user2 node creation at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -640,9 +640,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
if len(listNodesAfterNewUserLogin) >= 2 {
// Machine key is the same as the "machine" has not changed,
// but Node key is not as it is a new node
assert.Equal(ct, listNodes[0].GetMachineKey(), listNodesAfterNewUserLogin[0].GetMachineKey(), "Machine key should be preserved from original node")
assert.Equal(ct, listNodesAfterNewUserLogin[0].GetMachineKey(), listNodesAfterNewUserLogin[1].GetMachineKey(), "Both nodes should share the same machine key")
assert.NotEqual(ct, listNodesAfterNewUserLogin[0].GetNodeKey(), listNodesAfterNewUserLogin[1].GetNodeKey(), "Node keys should be different between user1 and user2 nodes")
assert.Equal(ct, listNodes[0].MachineKey, listNodesAfterNewUserLogin[0].MachineKey, "Machine key should be preserved from original node")
assert.Equal(ct, listNodesAfterNewUserLogin[0].MachineKey, listNodesAfterNewUserLogin[1].MachineKey, "Both nodes should share the same machine key")
assert.NotEqual(ct, listNodesAfterNewUserLogin[0].NodeKey, listNodesAfterNewUserLogin[1].NodeKey, "Node keys should be different between user1 and user2 nodes")
}
}, integrationutil.PolicyPropagationTimeout, 2*time.Second, "waiting for node count stabilization at exactly 2 nodes after user2 login")
@@ -650,9 +650,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
var activeUser2NodeID types.NodeID
for _, node := range listNodesAfterNewUserLogin {
if node.GetUser().GetId() == 2 { // user2
activeUser2NodeID = types.NodeID(node.GetId())
t.Logf("Active user2 node: %d (User: %s)", node.GetId(), node.GetUser().GetName())
if node.User.Id == "2" { // user2
activeUser2NodeID = types.NodeID(mustParseID(node.Id))
t.Logf("Active user2 node: %d (User: %s)", mustParseID(node.Id), node.User.Name)
break
}
@@ -685,9 +685,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
// Validate node stability - ensure no phantom nodes
for i, node := range currentNodes {
assert.NotNil(ct, node.GetUser(), "Node %d should have a valid user before logout", i)
assert.NotEmpty(ct, node.GetMachineKey(), "Node %d should have a valid machine key before logout", i)
t.Logf("Pre-logout node %d: User=%s, MachineKey=%s", i, node.GetUser().GetName(), node.GetMachineKey()[:16]+"...")
assert.NotEmpty(ct, node.User.Id, "Node %d should have a valid user before logout", i)
assert.NotEmpty(ct, node.MachineKey, "Node %d should have a valid machine key before logout", i)
t.Logf("Pre-logout node %d: User=%s, MachineKey=%s", i, node.User.Name, node.MachineKey[:16]+"...")
}
}, integrationutil.HAConvergeTimeout, 2*time.Second, "validating stable node count and integrity before user2 logout")
@@ -730,9 +730,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
// Ensure both nodes are still valid (not cleaned up incorrectly)
for i, node := range currentNodes {
assert.NotNil(ct, node.GetUser(), "Node %d should still have a valid user after user2 logout", i)
assert.NotEmpty(ct, node.GetMachineKey(), "Node %d should still have a valid machine key after user2 logout", i)
t.Logf("Post-logout node %d: User=%s, MachineKey=%s", i, node.GetUser().GetName(), node.GetMachineKey()[:16]+"...")
assert.NotEmpty(ct, node.User.Id, "Node %d should still have a valid user after user2 logout", i)
assert.NotEmpty(ct, node.MachineKey, "Node %d should still have a valid machine key after user2 logout", i)
t.Logf("Post-logout node %d: User=%s, MachineKey=%s", i, node.User.Name, node.MachineKey[:16]+"...")
}
}, integrationutil.HAConvergeTimeout, 2*time.Second, "validating node persistence and integrity after user2 logout")
@@ -761,16 +761,16 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
assert.NoError(ct, err, "Failed to list users during final validation")
assert.Len(ct, listUsers, 2, "Should still have exactly 2 users after user1 relogin, got %d", len(listUsers))
wantUsers := []*v1.User{
wantUsers := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
},
{
Id: 2,
Id: "2",
Name: "user2",
Email: "user2@headscale.net",
Provider: "oidc",
@@ -779,15 +779,15 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
ct.Errorf("Final user validation failed - both users should persist after relogin cycle: %s", diff)
}
}, integrationutil.StatusReadyTimeout, 1*time.Second, "validating user persistence after complete relogin cycle (user1->user2->user1)")
var listNodesAfterLoggingBackIn []*v1.Node
var listNodesAfterLoggingBackIn []*clientv1.Node
// Wait for login to complete and nodes to stabilize
t.Logf("Final node validation: checking node stability after user1 relogin at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -806,24 +806,24 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
// Validate that the machine we had when we logged in the first time, has the same
// machine key, but a different ID than the newly logged in version of the same
// machine.
assert.Equal(ct, listNodes[0].GetMachineKey(), listNodesAfterNewUserLogin[0].GetMachineKey(), "Original user1 machine key should match user1 node after user switch")
assert.Equal(ct, listNodes[0].GetNodeKey(), listNodesAfterNewUserLogin[0].GetNodeKey(), "Original user1 node key should match user1 node after user switch")
assert.Equal(ct, listNodes[0].GetId(), listNodesAfterNewUserLogin[0].GetId(), "Original user1 node ID should match user1 node after user switch")
assert.Equal(ct, listNodes[0].GetMachineKey(), listNodesAfterNewUserLogin[1].GetMachineKey(), "User1 and user2 nodes should share the same machine key")
assert.NotEqual(ct, listNodes[0].GetId(), listNodesAfterNewUserLogin[1].GetId(), "User1 and user2 nodes should have different node IDs")
assert.NotEqual(ct, listNodes[0].GetUser().GetId(), listNodesAfterNewUserLogin[1].GetUser().GetId(), "User1 and user2 nodes should belong to different users")
assert.Equal(ct, listNodes[0].MachineKey, listNodesAfterNewUserLogin[0].MachineKey, "Original user1 machine key should match user1 node after user switch")
assert.Equal(ct, listNodes[0].NodeKey, listNodesAfterNewUserLogin[0].NodeKey, "Original user1 node key should match user1 node after user switch")
assert.Equal(ct, listNodes[0].Id, listNodesAfterNewUserLogin[0].Id, "Original user1 node ID should match user1 node after user switch")
assert.Equal(ct, listNodes[0].MachineKey, listNodesAfterNewUserLogin[1].MachineKey, "User1 and user2 nodes should share the same machine key")
assert.NotEqual(ct, listNodes[0].Id, listNodesAfterNewUserLogin[1].Id, "User1 and user2 nodes should have different node IDs")
assert.NotEqual(ct, listNodes[0].User.Id, listNodesAfterNewUserLogin[1].User.Id, "User1 and user2 nodes should belong to different users")
// Even tho we are logging in again with the same user, the previous key has been expired
// and a new one has been generated. The node entry in the database should be the same
// as the user + machinekey still matches.
assert.Equal(ct, listNodes[0].GetMachineKey(), listNodesAfterLoggingBackIn[0].GetMachineKey(), "Machine key should remain consistent after user1 relogin")
assert.NotEqual(ct, listNodes[0].GetNodeKey(), listNodesAfterLoggingBackIn[0].GetNodeKey(), "Node key should be regenerated after user1 relogin")
assert.Equal(ct, listNodes[0].GetId(), listNodesAfterLoggingBackIn[0].GetId(), "Node ID should be preserved for user1 after relogin")
assert.Equal(ct, listNodes[0].MachineKey, listNodesAfterLoggingBackIn[0].MachineKey, "Machine key should remain consistent after user1 relogin")
assert.NotEqual(ct, listNodes[0].NodeKey, listNodesAfterLoggingBackIn[0].NodeKey, "Node key should be regenerated after user1 relogin")
assert.Equal(ct, listNodes[0].Id, listNodesAfterLoggingBackIn[0].Id, "Node ID should be preserved for user1 after relogin")
// The "logged back in" machine should have the same machinekey but a different nodekey
// than the version logged in with a different user.
assert.Equal(ct, listNodesAfterLoggingBackIn[0].GetMachineKey(), listNodesAfterLoggingBackIn[1].GetMachineKey(), "Both final nodes should share the same machine key")
assert.NotEqual(ct, listNodesAfterLoggingBackIn[0].GetNodeKey(), listNodesAfterLoggingBackIn[1].GetNodeKey(), "Final nodes should have different node keys for different users")
assert.Equal(ct, listNodesAfterLoggingBackIn[0].MachineKey, listNodesAfterLoggingBackIn[1].MachineKey, "Both final nodes should share the same machine key")
assert.NotEqual(ct, listNodesAfterLoggingBackIn[0].NodeKey, listNodesAfterLoggingBackIn[1].NodeKey, "Final nodes should have different node keys for different users")
t.Logf("Final validation complete - node counts and key relationships verified at %s", time.Now().Format(TimestampFormat))
}, integrationutil.HAConvergeTimeout, 2*time.Second, "validating final node state after complete user1->user2->user1 relogin cycle with detailed key validation")
@@ -832,9 +832,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
var activeUser1NodeID types.NodeID
for _, node := range listNodesAfterLoggingBackIn {
if node.GetUser().GetId() == 1 { // user1
activeUser1NodeID = types.NodeID(node.GetId())
t.Logf("Active user1 node after relogin: %d (User: %s)", node.GetId(), node.GetUser().GetName())
if node.User.Id == "1" { // user1
activeUser1NodeID = types.NodeID(mustParseID(node.Id))
t.Logf("Active user1 node after relogin: %d (User: %s)", mustParseID(node.Id), node.User.Name)
break
}
@@ -942,9 +942,9 @@ func TestOIDCFollowUpUrl(t *testing.T) {
require.NoError(t, err)
assert.Len(t, listUsers, 1)
wantUsers := []*v1.User{
wantUsers := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
@@ -954,15 +954,15 @@ func TestOIDCFollowUpUrl(t *testing.T) {
sort.Slice(
listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
},
)
if diff := cmp.Diff(
wantUsers,
listUsers,
cmpopts.IgnoreUnexported(v1.User{}),
cmpopts.IgnoreFields(v1.User{}, "CreatedAt"),
cmpopts.IgnoreUnexported(clientv1.User{}),
cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt"),
); diff != "" {
t.Fatalf("unexpected users: %s", diff)
}
@@ -1051,9 +1051,9 @@ func TestOIDCMultipleOpenedLoginUrls(t *testing.T) {
require.NoError(t, err)
assert.Len(t, listUsers, 1)
wantUsers := []*v1.User{
wantUsers := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
@@ -1063,15 +1063,15 @@ func TestOIDCMultipleOpenedLoginUrls(t *testing.T) {
sort.Slice(
listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
},
)
if diff := cmp.Diff(
wantUsers,
listUsers,
cmpopts.IgnoreUnexported(v1.User{}),
cmpopts.IgnoreFields(v1.User{}, "CreatedAt"),
cmpopts.IgnoreUnexported(clientv1.User{}),
cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt"),
); diff != "" {
t.Fatalf("unexpected users: %s", diff)
}
@@ -1159,9 +1159,9 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
assert.NoError(ct, err, "Failed to list users during initial validation")
assert.Len(ct, listUsers, 1, "Expected exactly 1 user after first login, got %d", len(listUsers))
wantUsers := []*v1.User{
wantUsers := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
@@ -1170,17 +1170,17 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
ct.Errorf("User validation failed after first login - unexpected users: %s", diff)
}
}, integrationutil.StatusReadyTimeout, 1*time.Second, "validating user1 creation after initial OIDC login")
t.Logf("Validating initial node creation at %s", time.Now().Format(TimestampFormat))
var initialNodes []*v1.Node
var initialNodes []*clientv1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
@@ -1211,9 +1211,9 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
validateInitialConnection(t, headscale, expectedNodes)
// Store initial node keys for comparison
initialMachineKey := initialNodes[0].GetMachineKey()
initialNodeKey := initialNodes[0].GetNodeKey()
initialNodeID := initialNodes[0].GetId()
initialMachineKey := initialNodes[0].MachineKey
initialNodeKey := initialNodes[0].NodeKey
initialNodeID := initialNodes[0].Id
// Logout user1
err = ts.Logout()
@@ -1262,9 +1262,9 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
assert.NoError(ct, err, "Failed to list users during final validation")
assert.Len(ct, listUsers, 1, "Should still have exactly 1 user after same-user relogin, got %d", len(listUsers))
wantUsers := []*v1.User{
wantUsers := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
@@ -1273,15 +1273,15 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
ct.Errorf("Final user validation failed - user1 should persist after same-user relogin: %s", diff)
}
}, integrationutil.StatusReadyTimeout, 1*time.Second, "validating user1 persistence after same-user OIDC relogin cycle")
var finalNodes []*v1.Node
var finalNodes []*clientv1.Node
t.Logf("Final node validation: checking node stability after same-user relogin at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -1293,19 +1293,19 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
finalNode := finalNodes[0]
// Machine key should be preserved (same physical machine)
assert.Equal(ct, initialMachineKey, finalNode.GetMachineKey(), "Machine key should be preserved for same user same node relogin")
assert.Equal(ct, initialMachineKey, finalNode.MachineKey, "Machine key should be preserved for same user same node relogin")
// Node ID should be preserved (same user, same machine)
assert.Equal(ct, initialNodeID, finalNode.GetId(), "Node ID should be preserved for same user same node relogin")
assert.Equal(ct, initialNodeID, finalNode.Id, "Node ID should be preserved for same user same node relogin")
// Node key should be regenerated (new session after logout)
assert.NotEqual(ct, initialNodeKey, finalNode.GetNodeKey(), "Node key should be regenerated after logout/relogin even for same user")
assert.NotEqual(ct, initialNodeKey, finalNode.NodeKey, "Node key should be regenerated after logout/relogin even for same user")
t.Logf("Final validation complete - same user relogin key relationships verified at %s", time.Now().Format(TimestampFormat))
}, integrationutil.HAConvergeTimeout, 2*time.Second, "validating final node state after same-user OIDC relogin cycle with key preservation validation")
// Security validation: user1's node should be active after relogin
activeUser1NodeID := types.NodeID(finalNodes[0].GetId())
activeUser1NodeID := types.NodeID(mustParseID(finalNodes[0].Id))
t.Logf("Validating user1 node is online after same-user relogin at %s", time.Now().Format(TimestampFormat))
require.EventuallyWithT(t, func(c *assert.CollectT) {
@@ -1389,10 +1389,10 @@ func TestOIDCExpiryAfterRestart(t *testing.T) {
assert.Len(ct, nodes, 1)
node := nodes[0]
assert.NotNil(ct, node.GetExpiry(), "Expiry should be set after OIDC login")
assert.NotNil(ct, node.Expiry, "Expiry should be set after OIDC login")
if node.GetExpiry() != nil {
expiryTime := node.GetExpiry().AsTime()
if node.Expiry != nil {
expiryTime := *node.Expiry
assert.False(ct, expiryTime.IsZero(), "Expiry should not be zero time")
initialExpiry = expiryTime
@@ -1431,10 +1431,10 @@ func TestOIDCExpiryAfterRestart(t *testing.T) {
assert.Len(ct, nodes, 1, "Should still have exactly 1 node after restart")
node := nodes[0]
assert.NotNil(ct, node.GetExpiry(), "Expiry should NOT be nil after restart")
assert.NotNil(ct, node.Expiry, "Expiry should NOT be nil after restart")
if node.GetExpiry() != nil {
expiryTime := node.GetExpiry().AsTime()
if node.Expiry != nil {
expiryTime := *node.Expiry
// This is the bug check - expiry should NOT be zero time
assert.False(ct, expiryTime.IsZero(),
@@ -1567,9 +1567,9 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
assert.Len(ct, nodes, 1)
gatewayNode := nodes[0]
gatewayNodeID = gatewayNode.GetId()
assert.Len(ct, gatewayNode.GetAvailableRoutes(), 1)
assert.Contains(ct, gatewayNode.GetAvailableRoutes(), advertiseRoute)
gatewayNodeID = mustParseID(gatewayNode.Id)
assert.Len(ct, gatewayNode.AvailableRoutes, 1)
assert.Contains(ct, gatewayNode.AvailableRoutes, advertiseRoute)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "route advertisement should propagate to headscale")
// Approve the advertised route
@@ -1586,8 +1586,8 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
assert.Len(ct, nodes, 1)
gatewayNode := nodes[0]
assert.Len(ct, gatewayNode.GetApprovedRoutes(), 1)
assert.Contains(ct, gatewayNode.GetApprovedRoutes(), advertiseRoute)
assert.Len(ct, gatewayNode.ApprovedRoutes, 1)
assert.Contains(ct, gatewayNode.ApprovedRoutes, advertiseRoute)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "route approval should propagate to headscale")
// NOW create the OIDC user by having them join
@@ -1707,10 +1707,10 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
assert.GreaterOrEqual(ct, len(users), 2, "Should have at least 2 users (gateway CLI user + oidcuser)")
// Find gateway CLI user
var gatewayUser *v1.User
var gatewayUser *clientv1.User
for _, user := range users {
if user.GetName() == "gateway" && user.GetProvider() == "" {
if user.Name == "gateway" && user.Provider == "" {
gatewayUser = user
break
}
@@ -1719,14 +1719,14 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
assert.NotNil(ct, gatewayUser, "Should have gateway CLI user")
if gatewayUser != nil {
assert.Equal(ct, "gateway", gatewayUser.GetName())
assert.Equal(ct, "gateway", gatewayUser.Name)
}
// Find OIDC user
var oidcUserFound *v1.User
var oidcUserFound *clientv1.User
for _, user := range users {
if user.GetName() == "oidcuser" && user.GetProvider() == "oidc" {
if user.Name == "oidcuser" && user.Provider == "oidc" {
oidcUserFound = user
break
}
@@ -1735,8 +1735,8 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
assert.NotNil(ct, oidcUserFound, "Should have OIDC user")
if oidcUserFound != nil {
assert.Equal(ct, "oidcuser", oidcUserFound.GetName())
assert.Equal(ct, "oidcuser@headscale.net", oidcUserFound.GetEmail())
assert.Equal(ct, "oidcuser", oidcUserFound.Name)
assert.Equal(ct, "oidcuser@headscale.net", oidcUserFound.Email)
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "headscale should have correct users and nodes")
@@ -1834,7 +1834,7 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) {
// Step 1: Verify initial route is advertised, approved, and SERVING
t.Logf("Step 1: Verifying initial route is advertised, approved, and SERVING at %s", time.Now().Format(TimestampFormat))
var initialNode *v1.Node
var initialNode *clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err := headscale.ListNodes()
@@ -1844,21 +1844,21 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) {
if len(nodes) == 1 {
initialNode = nodes[0]
// Check: 1 announced, 1 approved, 1 serving (subnet route)
assert.Lenf(c, initialNode.GetAvailableRoutes(), 1,
"Node should have 1 available route, got %v", initialNode.GetAvailableRoutes())
assert.Lenf(c, initialNode.GetApprovedRoutes(), 1,
"Node should have 1 approved route, got %v", initialNode.GetApprovedRoutes())
assert.Lenf(c, initialNode.GetSubnetRoutes(), 1,
"Node should have 1 serving (subnet) route, got %v - THIS IS THE BUG if empty", initialNode.GetSubnetRoutes())
assert.Contains(c, initialNode.GetSubnetRoutes(), advertiseRoute,
assert.Lenf(c, initialNode.AvailableRoutes, 1,
"Node should have 1 available route, got %v", initialNode.AvailableRoutes)
assert.Lenf(c, initialNode.ApprovedRoutes, 1,
"Node should have 1 approved route, got %v", initialNode.ApprovedRoutes)
assert.Lenf(c, initialNode.SubnetRoutes, 1,
"Node should have 1 serving (subnet) route, got %v - THIS IS THE BUG if empty", initialNode.SubnetRoutes)
assert.Contains(c, initialNode.SubnetRoutes, advertiseRoute,
"Subnet routes should contain %s", advertiseRoute)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "initial route should be serving")
require.NotNil(t, initialNode, "Initial node should be found")
initialNodeID := initialNode.GetId()
t.Logf("Initial node ID: %d, Available: %v, Approved: %v, Serving: %v",
initialNodeID, initialNode.GetAvailableRoutes(), initialNode.GetApprovedRoutes(), initialNode.GetSubnetRoutes())
initialNodeID := initialNode.Id
t.Logf("Initial node ID: %s, Available: %v, Approved: %v, Serving: %v",
initialNodeID, initialNode.AvailableRoutes, initialNode.ApprovedRoutes, initialNode.SubnetRoutes)
// Step 2: Logout
t.Logf("Step 2: Logging out at %s", time.Now().Format(TimestampFormat))
@@ -1911,23 +1911,23 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) {
if len(nodes) == 1 {
node := nodes[0]
t.Logf("After relogin - Available: %v, Approved: %v, Serving: %v",
node.GetAvailableRoutes(), node.GetApprovedRoutes(), node.GetSubnetRoutes())
node.AvailableRoutes, node.ApprovedRoutes, node.SubnetRoutes)
// This is where issue #2896 manifests:
// - Available shows the route (from [tailcfg.Hostinfo.RoutableIPs])
// - Approved shows the route (from [tailcfg.Node.ApprovedRoutes])
// - BUT Serving ([tailcfg.Node.SubnetRoutes]/[ipnstate.PeerStatus.PrimaryRoutes]) is EMPTY!
assert.Lenf(c, node.GetAvailableRoutes(), 1,
"Node should have 1 available route after relogin, got %v", node.GetAvailableRoutes())
assert.Lenf(c, node.GetApprovedRoutes(), 1,
"Node should have 1 approved route after relogin, got %v", node.GetApprovedRoutes())
assert.Lenf(c, node.GetSubnetRoutes(), 1,
"BUG #2896: Node should have 1 SERVING route after relogin, got %v", node.GetSubnetRoutes())
assert.Contains(c, node.GetSubnetRoutes(), advertiseRoute,
assert.Lenf(c, node.AvailableRoutes, 1,
"Node should have 1 available route after relogin, got %v", node.AvailableRoutes)
assert.Lenf(c, node.ApprovedRoutes, 1,
"Node should have 1 approved route after relogin, got %v", node.ApprovedRoutes)
assert.Lenf(c, node.SubnetRoutes, 1,
"BUG #2896: Node should have 1 SERVING route after relogin, got %v", node.SubnetRoutes)
assert.Contains(c, node.SubnetRoutes, advertiseRoute,
"BUG #2896: Subnet routes should contain %s after relogin", advertiseRoute)
// Also verify node ID was preserved (same node, not new registration)
assert.Equal(c, initialNodeID, node.GetId(),
assert.Equal(c, initialNodeID, node.Id,
"Node ID should be preserved after same-user relogin")
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll,
+6 -6
View File
@@ -6,7 +6,7 @@ import (
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/integrationutil"
@@ -98,7 +98,7 @@ func TestAuthWebFlowLogoutAndReloginSameUser(t *testing.T) {
// Validate initial connection state
validateInitialConnection(t, headscale, expectedNodes)
var listNodes []*v1.Node
var listNodes []*clientv1.Node
t.Logf("Validating initial node count after web auth at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -256,7 +256,7 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
// Validate initial connection state
validateInitialConnection(t, headscale, expectedNodes)
var listNodes []*v1.Node
var listNodes []*clientv1.Node
t.Logf("Validating initial node count after web auth at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -316,7 +316,7 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
t.Logf("all clients logged back in as user1")
var user1Nodes []*v1.Node
var user1Nodes []*clientv1.Node
t.Logf("Validating user1 node count after relogin at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -330,7 +330,7 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
// Collect expected node IDs for user1 after relogin
expectedUser1Nodes := make([]types.NodeID, 0, len(user1Nodes))
for _, node := range user1Nodes {
expectedUser1Nodes = append(expectedUser1Nodes, types.NodeID(node.GetId()))
expectedUser1Nodes = append(expectedUser1Nodes, types.NodeID(mustParseID(node.Id)))
}
// Validate connection state after relogin as user1
@@ -338,7 +338,7 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
// Validate that user2's old nodes still exist in database (but are expired/offline)
// When CLI registration creates new nodes for user1, user2's old nodes remain
var user2Nodes []*v1.Node
var user2Nodes []*clientv1.Node
t.Logf("Validating user2 old nodes remain in database after CLI registration to user1 at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
+41 -42
View File
@@ -1,11 +1,10 @@
package integration
import (
"strconv"
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/integrationutil"
"github.com/juanfont/headscale/integration/tsic"
@@ -55,7 +54,7 @@ func TestApiKeyCommand(t *testing.T) {
assert.Len(t, keys, 5)
var listedAPIKeys []v1.ApiKey
var listedAPIKeys []clientv1.ApiKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(headscale,
@@ -73,43 +72,43 @@ func TestApiKeyCommand(t *testing.T) {
assert.Len(t, listedAPIKeys, 5)
assert.Equal(t, uint64(1), listedAPIKeys[0].GetId())
assert.Equal(t, uint64(2), listedAPIKeys[1].GetId())
assert.Equal(t, uint64(3), listedAPIKeys[2].GetId())
assert.Equal(t, uint64(4), listedAPIKeys[3].GetId())
assert.Equal(t, uint64(5), listedAPIKeys[4].GetId())
assert.Equal(t, "1", listedAPIKeys[0].Id)
assert.Equal(t, "2", listedAPIKeys[1].Id)
assert.Equal(t, "3", listedAPIKeys[2].Id)
assert.Equal(t, "4", listedAPIKeys[3].Id)
assert.Equal(t, "5", listedAPIKeys[4].Id)
assert.NotEmpty(t, listedAPIKeys[0].GetPrefix())
assert.NotEmpty(t, listedAPIKeys[1].GetPrefix())
assert.NotEmpty(t, listedAPIKeys[2].GetPrefix())
assert.NotEmpty(t, listedAPIKeys[3].GetPrefix())
assert.NotEmpty(t, listedAPIKeys[4].GetPrefix())
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].GetExpiration().AsTime().After(time.Now()))
assert.True(t, listedAPIKeys[1].GetExpiration().AsTime().After(time.Now()))
assert.True(t, listedAPIKeys[2].GetExpiration().AsTime().After(time.Now()))
assert.True(t, listedAPIKeys[3].GetExpiration().AsTime().After(time.Now()))
assert.True(t, listedAPIKeys[4].GetExpiration().AsTime().After(time.Now()))
assert.True(t, listedAPIKeys[0].Expiration.After(time.Now()))
assert.True(t, listedAPIKeys[1].Expiration.After(time.Now()))
assert.True(t, listedAPIKeys[2].Expiration.After(time.Now()))
assert.True(t, listedAPIKeys[3].Expiration.After(time.Now()))
assert.True(t, listedAPIKeys[4].Expiration.After(time.Now()))
assert.True(
t,
listedAPIKeys[0].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
listedAPIKeys[0].Expiration.Before(time.Now().Add(time.Hour*26)),
)
assert.True(
t,
listedAPIKeys[1].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
listedAPIKeys[1].Expiration.Before(time.Now().Add(time.Hour*26)),
)
assert.True(
t,
listedAPIKeys[2].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
listedAPIKeys[2].Expiration.Before(time.Now().Add(time.Hour*26)),
)
assert.True(
t,
listedAPIKeys[3].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
listedAPIKeys[3].Expiration.Before(time.Now().Add(time.Hour*26)),
)
assert.True(
t,
listedAPIKeys[4].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
listedAPIKeys[4].Expiration.Before(time.Now().Add(time.Hour*26)),
)
expiredPrefixes := make(map[string]bool)
@@ -122,15 +121,15 @@ func TestApiKeyCommand(t *testing.T) {
"apikeys",
"expire",
"--prefix",
listedAPIKeys[idx].GetPrefix(),
listedAPIKeys[idx].Prefix,
},
)
require.NoError(t, err)
expiredPrefixes[listedAPIKeys[idx].GetPrefix()] = true
expiredPrefixes[listedAPIKeys[idx].Prefix] = true
}
var listedAfterExpireAPIKeys []v1.ApiKey
var listedAfterExpireAPIKeys []clientv1.ApiKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(headscale,
@@ -147,17 +146,17 @@ func TestApiKeyCommand(t *testing.T) {
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for API keys list after expire")
for index := range listedAfterExpireAPIKeys {
if _, ok := expiredPrefixes[listedAfterExpireAPIKeys[index].GetPrefix()]; ok {
if _, ok := expiredPrefixes[listedAfterExpireAPIKeys[index].Prefix]; ok {
// Expired
assert.True(
t,
listedAfterExpireAPIKeys[index].GetExpiration().AsTime().Before(time.Now()),
listedAfterExpireAPIKeys[index].Expiration.Before(time.Now()),
)
} else {
// Not expired
assert.False(
t,
listedAfterExpireAPIKeys[index].GetExpiration().AsTime().Before(time.Now()),
listedAfterExpireAPIKeys[index].Expiration.Before(time.Now()),
)
}
}
@@ -168,11 +167,11 @@ func TestApiKeyCommand(t *testing.T) {
"apikeys",
"delete",
"--prefix",
listedAPIKeys[0].GetPrefix(),
listedAPIKeys[0].Prefix,
})
require.NoError(t, err)
var listedAPIKeysAfterDelete []v1.ApiKey
var listedAPIKeysAfterDelete []clientv1.ApiKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(headscale,
@@ -197,11 +196,11 @@ func TestApiKeyCommand(t *testing.T) {
"apikeys",
"expire",
"--id",
strconv.FormatUint(listedAPIKeysAfterDelete[0].GetId(), 10),
listedAPIKeysAfterDelete[0].Id,
})
require.NoError(t, err)
var listedAPIKeysAfterExpireByID []v1.ApiKey
var listedAPIKeysAfterExpireByID []clientv1.ApiKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(headscale,
@@ -219,25 +218,25 @@ func TestApiKeyCommand(t *testing.T) {
// Verify the key was expired
for idx := range listedAPIKeysAfterExpireByID {
if listedAPIKeysAfterExpireByID[idx].GetId() == listedAPIKeysAfterDelete[0].GetId() {
assert.True(t, listedAPIKeysAfterExpireByID[idx].GetExpiration().AsTime().Before(time.Now()),
if listedAPIKeysAfterExpireByID[idx].Id == listedAPIKeysAfterDelete[0].Id {
assert.True(t, listedAPIKeysAfterExpireByID[idx].Expiration.Before(time.Now()),
"Key expired by ID should have expiration in the past")
}
}
// Test delete by ID (using key at index 1)
deletedKeyID := listedAPIKeysAfterExpireByID[1].GetId()
deletedKeyID := listedAPIKeysAfterExpireByID[1].Id
_, err = headscale.Execute(
[]string{
"headscale",
"apikeys",
"delete",
"--id",
strconv.FormatUint(deletedKeyID, 10),
deletedKeyID,
})
require.NoError(t, err)
var listedAPIKeysAfterDeleteByID []v1.ApiKey
var listedAPIKeysAfterDeleteByID []clientv1.ApiKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(headscale,
@@ -257,7 +256,7 @@ func TestApiKeyCommand(t *testing.T) {
// Verify the specific key was deleted
for idx := range listedAPIKeysAfterDeleteByID {
assert.NotEqual(t, deletedKeyID, listedAPIKeysAfterDeleteByID[idx].GetId(),
assert.NotEqual(t, deletedKeyID, listedAPIKeysAfterDeleteByID[idx].Id,
"Deleted key should not be present in the list")
}
}
@@ -277,7 +276,7 @@ func TestApiKeyCommandValidation(t *testing.T) {
_, err := headscale.Execute([]string{"headscale", "apikeys", "create", "--output", "json"})
require.NoError(t, err)
var listed []v1.ApiKey
var listed []clientv1.ApiKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err := executeAndUnmarshal(headscale,
@@ -288,8 +287,8 @@ func TestApiKeyCommandValidation(t *testing.T) {
assert.Len(c, listed, 1)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for API key list")
prefix := listed[0].GetPrefix()
id := strconv.FormatUint(listed[0].GetId(), 10)
prefix := listed[0].Prefix
id := listed[0].Id
tests := []struct {
name string
+114 -108
View File
@@ -2,12 +2,11 @@ package integration
import (
"fmt"
"strconv"
"strings"
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/integration/hsic"
@@ -43,7 +42,7 @@ func TestNodeCommand(t *testing.T) {
types.MustAuthID().String(),
types.MustAuthID().String(),
}
nodes := make([]*v1.Node, len(regIDs))
nodes := make([]*clientv1.Node, len(regIDs))
require.NoError(t, err)
@@ -65,7 +64,7 @@ func TestNodeCommand(t *testing.T) {
)
require.NoError(t, err)
var node v1.Node
var node clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -94,7 +93,7 @@ func TestNodeCommand(t *testing.T) {
}, integrationutil.ScaledTimeout(15*time.Second), 1*time.Second)
// Test list all nodes after added seconds
var listAll []v1.Node
var listAll []clientv1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
err := executeAndUnmarshal(
@@ -112,23 +111,23 @@ func TestNodeCommand(t *testing.T) {
assert.Len(ct, listAll, len(regIDs), "Should list all nodes after CLI operations")
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
assert.Equal(t, uint64(1), listAll[0].GetId())
assert.Equal(t, uint64(2), listAll[1].GetId())
assert.Equal(t, uint64(3), listAll[2].GetId())
assert.Equal(t, uint64(4), listAll[3].GetId())
assert.Equal(t, uint64(5), listAll[4].GetId())
assert.Equal(t, "1", listAll[0].Id)
assert.Equal(t, "2", listAll[1].Id)
assert.Equal(t, "3", listAll[2].Id)
assert.Equal(t, "4", listAll[3].Id)
assert.Equal(t, "5", listAll[4].Id)
assert.Equal(t, "node-1", listAll[0].GetName())
assert.Equal(t, "node-2", listAll[1].GetName())
assert.Equal(t, "node-3", listAll[2].GetName())
assert.Equal(t, "node-4", listAll[3].GetName())
assert.Equal(t, "node-5", listAll[4].GetName())
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)
otherUserRegIDs := []string{
types.MustAuthID().String(),
types.MustAuthID().String(),
}
otherUserMachines := make([]*v1.Node, len(otherUserRegIDs))
otherUserMachines := make([]*clientv1.Node, len(otherUserRegIDs))
require.NoError(t, err)
@@ -150,7 +149,7 @@ func TestNodeCommand(t *testing.T) {
)
require.NoError(t, err)
var node v1.Node
var node clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -179,7 +178,7 @@ func TestNodeCommand(t *testing.T) {
}, integrationutil.ScaledTimeout(15*time.Second), 1*time.Second)
// Test list all nodes after added otherUser
var listAllWithotherUser []v1.Node
var listAllWithotherUser []clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -199,14 +198,14 @@ func TestNodeCommand(t *testing.T) {
// All nodes, nodes + otherUser
assert.Len(t, listAllWithotherUser, 7)
assert.Equal(t, uint64(6), listAllWithotherUser[5].GetId())
assert.Equal(t, uint64(7), listAllWithotherUser[6].GetId())
assert.Equal(t, "6", listAllWithotherUser[5].Id)
assert.Equal(t, "7", listAllWithotherUser[6].Id)
assert.Equal(t, "otheruser-node-1", listAllWithotherUser[5].GetName())
assert.Equal(t, "otheruser-node-2", listAllWithotherUser[6].GetName())
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
var listOnlyotherUserMachineUser []clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -227,18 +226,18 @@ func TestNodeCommand(t *testing.T) {
assert.Len(t, listOnlyotherUserMachineUser, 2)
assert.Equal(t, uint64(6), listOnlyotherUserMachineUser[0].GetId())
assert.Equal(t, uint64(7), listOnlyotherUserMachineUser[1].GetId())
assert.Equal(t, "6", listOnlyotherUserMachineUser[0].Id)
assert.Equal(t, "7", listOnlyotherUserMachineUser[1].Id)
assert.Equal(
t,
"otheruser-node-1",
listOnlyotherUserMachineUser[0].GetName(),
listOnlyotherUserMachineUser[0].Name,
)
assert.Equal(
t,
"otheruser-node-2",
listOnlyotherUserMachineUser[1].GetName(),
listOnlyotherUserMachineUser[1].Name,
)
// Delete a nodes
@@ -258,7 +257,7 @@ func TestNodeCommand(t *testing.T) {
require.NoError(t, err)
// Test: list main user after node is deleted
var listOnlyMachineUserAfterDelete []v1.Node
var listOnlyMachineUserAfterDelete []clientv1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
err := executeAndUnmarshal(
@@ -304,7 +303,7 @@ func TestNodeExpireCommand(t *testing.T) {
types.MustAuthID().String(),
types.MustAuthID().String(),
}
nodes := make([]*v1.Node, len(regIDs))
nodes := make([]*clientv1.Node, len(regIDs))
for index, regID := range regIDs {
_, err := headscale.Execute(
@@ -324,7 +323,7 @@ func TestNodeExpireCommand(t *testing.T) {
)
require.NoError(t, err)
var node v1.Node
var node clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -350,7 +349,7 @@ func TestNodeExpireCommand(t *testing.T) {
assert.Len(t, nodes, len(regIDs))
var listAll []v1.Node
var listAll []clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -372,7 +371,7 @@ func TestNodeExpireCommand(t *testing.T) {
// With node.expiry defaulting to 0, non-tagged nodes have zero expiry
// (never expire unless explicitly expired).
for i := range 5 {
assert.True(t, listAll[i].GetExpiry().AsTime().IsZero(),
assert.True(t, listAll[i].Expiry == nil || listAll[i].Expiry.IsZero(),
"node %d should have zero expiry (no default node.expiry)", i)
}
@@ -383,13 +382,13 @@ func TestNodeExpireCommand(t *testing.T) {
"nodes",
"expire",
"--identifier",
strconv.FormatUint(listAll[idx].GetId(), 10),
listAll[idx].Id,
},
)
require.NoError(t, err)
}
var listAllAfterExpiry []v1.Node
var listAllAfterExpiry []clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -408,11 +407,14 @@ func TestNodeExpireCommand(t *testing.T) {
assert.Len(t, listAllAfterExpiry, 5)
assert.True(t, listAllAfterExpiry[0].GetExpiry().AsTime().Before(time.Now()))
assert.True(t, listAllAfterExpiry[1].GetExpiry().AsTime().Before(time.Now()))
assert.True(t, listAllAfterExpiry[2].GetExpiry().AsTime().Before(time.Now()))
assert.True(t, listAllAfterExpiry[3].GetExpiry().AsTime().IsZero())
assert.True(t, listAllAfterExpiry[4].GetExpiry().AsTime().IsZero())
require.NotNil(t, listAllAfterExpiry[0].Expiry)
require.NotNil(t, listAllAfterExpiry[1].Expiry)
require.NotNil(t, listAllAfterExpiry[2].Expiry)
assert.True(t, listAllAfterExpiry[0].Expiry.Before(time.Now()))
assert.True(t, listAllAfterExpiry[1].Expiry.Before(time.Now()))
assert.True(t, listAllAfterExpiry[2].Expiry.Before(time.Now()))
assert.True(t, listAllAfterExpiry[3].Expiry == nil || listAllAfterExpiry[3].Expiry.IsZero())
assert.True(t, listAllAfterExpiry[4].Expiry == nil || listAllAfterExpiry[4].Expiry.IsZero())
}
func TestNodeRenameCommand(t *testing.T) {
@@ -440,7 +442,7 @@ func TestNodeRenameCommand(t *testing.T) {
types.MustAuthID().String(),
types.MustAuthID().String(),
}
nodes := make([]*v1.Node, len(regIDs))
nodes := make([]*clientv1.Node, len(regIDs))
require.NoError(t, err)
@@ -462,7 +464,7 @@ func TestNodeRenameCommand(t *testing.T) {
)
require.NoError(t, err)
var node v1.Node
var node clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -488,7 +490,7 @@ func TestNodeRenameCommand(t *testing.T) {
assert.Len(t, nodes, len(regIDs))
var listAll []v1.Node
var listAll []clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -507,11 +509,11 @@ func TestNodeRenameCommand(t *testing.T) {
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")
assert.Contains(t, listAll[0].GivenName, "node-1")
assert.Contains(t, listAll[1].GivenName, "node-2")
assert.Contains(t, listAll[2].GivenName, "node-3")
assert.Contains(t, listAll[3].GivenName, "node-4")
assert.Contains(t, listAll[4].GivenName, "node-5")
for idx := range 3 {
res, err := headscale.Execute(
@@ -520,7 +522,7 @@ func TestNodeRenameCommand(t *testing.T) {
"nodes",
"rename",
"--identifier",
strconv.FormatUint(listAll[idx].GetId(), 10),
listAll[idx].Id,
fmt.Sprintf("newnode-%d", idx+1),
},
)
@@ -529,7 +531,7 @@ func TestNodeRenameCommand(t *testing.T) {
assert.Contains(t, res, "Node renamed")
}
var listAllAfterRename []v1.Node
var listAllAfterRename []clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -548,11 +550,11 @@ func TestNodeRenameCommand(t *testing.T) {
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")
assert.Equal(t, "newnode-1", listAllAfterRename[0].GivenName)
assert.Equal(t, "newnode-2", listAllAfterRename[1].GivenName)
assert.Equal(t, "newnode-3", listAllAfterRename[2].GivenName)
assert.Contains(t, listAllAfterRename[3].GivenName, "node-4")
assert.Contains(t, listAllAfterRename[4].GivenName, "node-5")
// Test failure for too long names
_, err = headscale.Execute(
@@ -561,13 +563,13 @@ func TestNodeRenameCommand(t *testing.T) {
"nodes",
"rename",
"--identifier",
strconv.FormatUint(listAll[4].GetId(), 10),
listAll[4].Id,
strings.Repeat("t", 64),
},
)
require.ErrorContains(t, err, "is too long, max length is 63 bytes")
var listAllAfterRenameAttempt []v1.Node
var listAllAfterRenameAttempt []clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -586,11 +588,11 @@ func TestNodeRenameCommand(t *testing.T) {
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")
assert.Equal(t, "newnode-1", listAllAfterRenameAttempt[0].GivenName)
assert.Equal(t, "newnode-2", listAllAfterRenameAttempt[1].GivenName)
assert.Equal(t, "newnode-3", listAllAfterRenameAttempt[2].GivenName)
assert.Contains(t, listAllAfterRenameAttempt[3].GivenName, "node-4")
assert.Contains(t, listAllAfterRenameAttempt[4].GivenName, "node-5")
}
func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
@@ -623,7 +625,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
u2, err := headscale.CreateUser(user2)
require.NoError(t, err)
var user2Key v1.PreAuthKey
var user2Key clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -632,7 +634,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
"headscale",
"preauthkeys",
"--user",
strconv.FormatUint(u2.GetId(), 10),
u2.Id,
"create",
"--reusable",
"--expiration",
@@ -647,7 +649,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
assert.NoError(c, err)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for user2 preauth key creation")
var listNodes []*v1.Node
var listNodes []*clientv1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
@@ -655,7 +657,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
listNodes, err = headscale.ListNodes()
assert.NoError(ct, err)
assert.Len(ct, listNodes, 1, "Should have exactly 1 node for user1")
assert.Equal(ct, user1, listNodes[0].GetUser().GetName(), "Node should belong to user1")
assert.Equal(ct, user1, listNodes[0].User.Name, "Node should belong to user1")
}, integrationutil.ScaledTimeout(15*time.Second), 1*time.Second)
allClients, err := scenario.ListTailscaleClients()
@@ -679,7 +681,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
"Expected node to be logged out, backend state: %s", status.BackendState)
}, integrationutil.StatusReadyTimeout, 2*time.Second)
err = client.Login(headscale.GetEndpoint(), user2Key.GetKey())
err = client.Login(headscale.GetEndpoint(), user2Key.Key)
require.NoError(t, err)
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -697,9 +699,9 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
listNodes, err = headscale.ListNodes()
assert.NoError(ct, err)
assert.Len(ct, listNodes, 2, "Should have 2 nodes after re-login")
assert.Equal(ct, user1, listNodes[0].GetUser().GetName(), "First node should belong to user1")
assert.Equal(ct, user1, listNodes[0].User.Name, "First node should belong to user1")
// Second node is tagged (created with tagged PreAuthKey), so it shows as "tagged-devices"
assert.Equal(ct, "tagged-devices", listNodes[1].GetUser().GetName(), "Second node should be tagged-devices")
assert.Equal(ct, "tagged-devices", listNodes[1].User.Name, "Second node should be tagged-devices")
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
}
@@ -731,7 +733,7 @@ func TestTaggedNodesCLIOutput(t *testing.T) {
u2, err := headscale.CreateUser(user2)
require.NoError(t, err)
var user2Key v1.PreAuthKey
var user2Key clientv1.PreAuthKey
// Create a tagged PreAuthKey for user2
assert.EventuallyWithT(t, func(c *assert.CollectT) {
@@ -741,7 +743,7 @@ func TestTaggedNodesCLIOutput(t *testing.T) {
"headscale",
"preauthkeys",
"--user",
strconv.FormatUint(u2.GetId(), 10),
u2.Id,
"create",
"--reusable",
"--expiration",
@@ -778,7 +780,7 @@ func TestTaggedNodesCLIOutput(t *testing.T) {
}, integrationutil.StatusReadyTimeout, 2*time.Second)
// Log in with the tagged PreAuthKey (from user2, with tags)
err = client.Login(headscale.GetEndpoint(), user2Key.GetKey())
err = client.Login(headscale.GetEndpoint(), user2Key.Key)
require.NoError(t, err)
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -790,7 +792,7 @@ func TestTaggedNodesCLIOutput(t *testing.T) {
}, integrationutil.StatusReadyTimeout, 2*time.Second)
// Wait for the second node to appear
var listNodes []*v1.Node
var listNodes []*clientv1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
@@ -798,8 +800,8 @@ func TestTaggedNodesCLIOutput(t *testing.T) {
listNodes, err = headscale.ListNodes()
assert.NoError(ct, err)
assert.Len(ct, listNodes, 2, "Should have 2 nodes after re-login with tagged key")
assert.Equal(ct, user1, listNodes[0].GetUser().GetName(), "First node should belong to user1")
assert.Equal(ct, "tagged-devices", listNodes[1].GetUser().GetName(), "Second node should be tagged-devices")
assert.Equal(ct, user1, listNodes[0].User.Name, "First node should belong to user1")
assert.Equal(ct, "tagged-devices", listNodes[1].User.Name, "Second node should be tagged-devices")
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
// Test: tailscale status output should show "tagged-devices" not "userid:2147455555"
@@ -838,7 +840,7 @@ func TestNodeExpireFlagsCommand(t *testing.T) {
})
require.NoError(t, err)
var node v1.Node
var node clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err := executeAndUnmarshal(headscale,
@@ -853,14 +855,14 @@ func TestNodeExpireFlagsCommand(t *testing.T) {
assert.NoError(c, err)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for node registration")
nodeID := strconv.FormatUint(node.GetId(), 10)
nodeID := node.Id
// listNodeByID returns the node with the given id from `nodes list`. The
// expire mutations are verified by reading the node back (authoritative,
// eventually-consistent) rather than trusting the mutation's immediate
// response.
listNodeByID := func(ct *assert.CollectT) *v1.Node {
var nodes []v1.Node
listNodeByID := func(ct *assert.CollectT) *clientv1.Node {
var nodes []clientv1.Node
err := executeAndUnmarshal(headscale,
[]string{"headscale", "nodes", "list", "--output", "json"},
@@ -869,7 +871,7 @@ func TestNodeExpireFlagsCommand(t *testing.T) {
require.NoError(ct, err)
for i := range nodes {
if nodes[i].GetId() == node.GetId() {
if nodes[i].Id == node.Id {
return &nodes[i]
}
}
@@ -896,8 +898,8 @@ func TestNodeExpireFlagsCommand(t *testing.T) {
return
}
assert.False(ct, n.GetExpiry().AsTime().IsZero(), "expiry should be set")
assert.True(ct, n.GetExpiry().AsTime().After(time.Now()), "expiry should be in the future")
assert.True(ct, n.Expiry != nil && !n.Expiry.IsZero(), "expiry should be set")
assert.True(ct, n.Expiry != nil && n.Expiry.After(time.Now()), "expiry should be in the future")
}, integrationutil.ScaledTimeout(15*time.Second), 1*time.Second, "Waiting for future expiry to apply")
// Disable expiry entirely; the node should then report no expiry.
@@ -919,7 +921,7 @@ func TestNodeExpireFlagsCommand(t *testing.T) {
// future — it never expires. A nil expiry deserialises to the Unix
// epoch rather than the zero time, so assert "not in the future"
// rather than IsZero.
assert.False(ct, n.GetExpiry().AsTime().After(time.Now()),
assert.False(ct, n.Expiry != nil && n.Expiry.After(time.Now()),
"disabled node should not have a future expiry")
}, integrationutil.ScaledTimeout(15*time.Second), 1*time.Second, "Waiting for --disable to clear expiry")
}
@@ -943,7 +945,7 @@ func TestNodeCommandValidation(t *testing.T) {
})
require.NoError(t, err)
var node v1.Node
var node clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err := executeAndUnmarshal(headscale,
@@ -953,7 +955,7 @@ func TestNodeCommandValidation(t *testing.T) {
assert.NoError(c, err)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for node registration")
id := strconv.FormatUint(node.GetId(), 10)
id := node.Id
// wantErr is matched with ErrorContains; an empty wantErr only requires
// that the command fails (used where the exact message is not load-bearing).
@@ -977,6 +979,10 @@ func TestNodeCommandValidation(t *testing.T) {
{"approve missing identifier", []string{"nodes", "approve-routes", "--routes", "10.0.0.0/24"}, "identifier"},
{"approve nonexistent", []string{"nodes", "approve-routes", "--identifier", "99999", "--routes", "10.0.0.0/24"}, ""},
{"approve invalid cidr", []string{"nodes", "approve-routes", "--identifier", id, "--routes", "notacidr"}, "parsing route"},
// The deprecated `nodes register` alias drives its own RegisterNode path;
// cover its error cases (the happy path is covered via `auth register`).
{"register nonexistent user", []string{"nodes", "register", "--user", "ghost", "--key", types.MustAuthID().String()}, ""},
{"register invalid key", []string{"nodes", "register", "--user", "user1", "--key", "badkey"}, ""},
}
for _, tt := range tests {
@@ -1041,7 +1047,7 @@ func TestNodeTagCommand(t *testing.T) {
require.NoError(t, scenario.WaitForTailscaleSync())
var nodeID uint64
var nodeID string
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
nodes, err := headscale.ListNodes()
@@ -1049,23 +1055,23 @@ func TestNodeTagCommand(t *testing.T) {
assert.Len(ct, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
assert.Equal(ct, "user1", nodes[0].GetUser().GetName(), "node should start user-owned")
nodeID = nodes[0].Id
assert.Equal(ct, "user1", nodes[0].User.Name, "node should start user-owned")
}
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
idStr := strconv.FormatUint(nodeID, 10)
idStr := nodeID
// Set two tags. The command response is round-tripped (transport check);
// the resulting tag state is asserted via the authoritative list read-back
// below rather than the immediate mutation response.
tagged := assertJSONRoundtrip[*v1.Node](t, headscale, []string{
tagged := assertJSONRoundtrip[*clientv1.Node](t, headscale, []string{
"headscale", "nodes", "tag",
"--identifier", idStr,
"--tags", "tag:test1,tag:test2",
"--output", "json",
})
assert.Equal(t, nodeID, tagged.GetId(), "tag response should be for the same node")
assert.Equal(t, nodeID, tagged.Id, "tag response should be for the same node")
// The node is now a tagged node, presented as the tagged-devices user.
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -1074,8 +1080,8 @@ func TestNodeTagCommand(t *testing.T) {
assert.Len(ct, nodes, 1)
if len(nodes) == 1 {
assert.Equal(ct, "tagged-devices", nodes[0].GetUser().GetName(), "tagged node shows as tagged-devices")
assert.ElementsMatch(ct, []string{"tag:test1", "tag:test2"}, nodes[0].GetTags())
assert.Equal(ct, "tagged-devices", nodes[0].User.Name, "tagged node shows as tagged-devices")
assert.ElementsMatch(ct, []string{"tag:test1", "tag:test2"}, nodes[0].Tags)
}
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
@@ -1135,10 +1141,10 @@ func TestNodeRouteCommands(t *testing.T) {
require.NoError(t, scenario.WaitForTailscaleSync())
// The advertised route should show up as available (but not approved).
var nodeID uint64
var nodeID string
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var nodes []v1.Node
var nodes []clientv1.Node
err := executeAndUnmarshal(headscale,
[]string{"headscale", "nodes", "list-routes", "--output", "json"},
@@ -1148,27 +1154,27 @@ func TestNodeRouteCommands(t *testing.T) {
assert.Len(ct, nodes, 1, "list-routes should show the route-advertising node")
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
assert.Contains(ct, nodes[0].GetAvailableRoutes(), route)
assert.Empty(ct, nodes[0].GetApprovedRoutes())
nodeID = nodes[0].Id
assert.Contains(ct, nodes[0].AvailableRoutes, route)
assert.Empty(ct, nodes[0].ApprovedRoutes)
}
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
idStr := strconv.FormatUint(nodeID, 10)
idStr := nodeID
// Approve the route via the CLI.
approved := assertJSONRoundtrip[*v1.Node](t, headscale, []string{
approved := assertJSONRoundtrip[*clientv1.Node](t, headscale, []string{
"headscale", "nodes", "approve-routes",
"--identifier", idStr,
"--routes=" + route,
"--output", "json",
})
assert.Contains(t, approved.GetApprovedRoutes(), route)
assert.Contains(t, approved.ApprovedRoutes, route)
// list-routes filtered by the identifier should report the approved route
// as a primary subnet route.
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var nodes []v1.Node
var nodes []clientv1.Node
err := executeAndUnmarshal(headscale,
[]string{"headscale", "nodes", "list-routes", "--identifier", idStr, "--output", "json"},
@@ -1178,19 +1184,19 @@ func TestNodeRouteCommands(t *testing.T) {
assert.Len(ct, nodes, 1)
if len(nodes) == 1 {
assert.Contains(ct, nodes[0].GetApprovedRoutes(), route)
assert.Contains(ct, nodes[0].GetSubnetRoutes(), route)
assert.Contains(ct, nodes[0].ApprovedRoutes, route)
assert.Contains(ct, nodes[0].SubnetRoutes, route)
}
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
// Remove all approved routes by passing an empty --routes value.
cleared := assertJSONRoundtrip[*v1.Node](t, headscale, []string{
cleared := assertJSONRoundtrip[*clientv1.Node](t, headscale, []string{
"headscale", "nodes", "approve-routes",
"--identifier", idStr,
"--routes=",
"--output", "json",
})
assert.Empty(t, cleared.GetApprovedRoutes(), "approved routes should be cleared")
assert.Empty(t, cleared.ApprovedRoutes, "approved routes should be cleared")
}
// TestNodeBackfillIPsCommand exercises `nodes backfillips` against live nodes.
@@ -1205,7 +1211,7 @@ func TestNodeBackfillIPsCommand(t *testing.T) {
require.NoError(t, scenario.WaitForTailscaleSync())
var before []*v1.Node
var before []*clientv1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
@@ -1215,7 +1221,7 @@ func TestNodeBackfillIPsCommand(t *testing.T) {
assert.Len(ct, before, 2)
for _, n := range before {
assert.NotEmpty(ct, n.GetIpAddresses(), "node should have IPs before backfill")
assert.NotEmpty(ct, n.IpAddresses, "node should have IPs before backfill")
}
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
@@ -1230,7 +1236,7 @@ func TestNodeBackfillIPsCommand(t *testing.T) {
assert.Len(ct, after, 2)
for _, n := range after {
assert.NotEmpty(ct, n.GetIpAddresses(), "node should still have IPs after backfill")
assert.NotEmpty(ct, n.IpAddresses, "node should still have IPs after backfill")
}
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
}
+1 -1
View File
@@ -152,7 +152,7 @@ func TestPolicyCheckCommand(t *testing.T) {
// --force suppresses the "is the server running?"
// confirmation prompt so the command can run
// non-interactively under the test harness.
cmd = append(cmd, "--bypass-grpc-and-access-database-directly", "--force")
cmd = append(cmd, "--bypass-server-and-access-database-directly", "--force")
}
stdout, err := headscale.Execute(cmd)
+38 -39
View File
@@ -1,11 +1,10 @@
package integration
import (
"strconv"
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/integrationutil"
"github.com/juanfont/headscale/integration/tsic"
@@ -34,12 +33,12 @@ func TestPreAuthKeyCommand(t *testing.T) {
headscale, err := scenario.Headscale()
require.NoError(t, err)
keys := make([]*v1.PreAuthKey, count)
keys := make([]*clientv1.PreAuthKey, count)
require.NoError(t, err)
for index := range count {
var preAuthKey v1.PreAuthKey
var preAuthKey clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err := executeAndUnmarshal(
@@ -68,7 +67,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
assert.Len(t, keys, 3)
var listedPreAuthKeys []v1.PreAuthKey
var listedPreAuthKeys []clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -90,34 +89,34 @@ func TestPreAuthKeyCommand(t *testing.T) {
assert.Equal(
t,
[]uint64{keys[0].GetId(), keys[1].GetId(), keys[2].GetId()},
[]uint64{
listedPreAuthKeys[1].GetId(),
listedPreAuthKeys[2].GetId(),
listedPreAuthKeys[3].GetId(),
[]string{keys[0].Id, keys[1].Id, keys[2].Id},
[]string{
listedPreAuthKeys[1].Id,
listedPreAuthKeys[2].Id,
listedPreAuthKeys[3].Id,
},
)
// New keys show prefix after listing, so check the created keys instead
assert.NotEmpty(t, keys[0].GetKey())
assert.NotEmpty(t, keys[1].GetKey())
assert.NotEmpty(t, keys[2].GetKey())
assert.NotEmpty(t, keys[0].Key)
assert.NotEmpty(t, keys[1].Key)
assert.NotEmpty(t, keys[2].Key)
assert.True(t, listedPreAuthKeys[1].GetExpiration().AsTime().After(time.Now()))
assert.True(t, listedPreAuthKeys[2].GetExpiration().AsTime().After(time.Now()))
assert.True(t, listedPreAuthKeys[3].GetExpiration().AsTime().After(time.Now()))
assert.True(t, listedPreAuthKeys[1].Expiration.After(time.Now()))
assert.True(t, listedPreAuthKeys[2].Expiration.After(time.Now()))
assert.True(t, listedPreAuthKeys[3].Expiration.After(time.Now()))
assert.True(
t,
listedPreAuthKeys[1].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
listedPreAuthKeys[1].Expiration.Before(time.Now().Add(time.Hour*26)),
)
assert.True(
t,
listedPreAuthKeys[2].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
listedPreAuthKeys[2].Expiration.Before(time.Now().Add(time.Hour*26)),
)
assert.True(
t,
listedPreAuthKeys[3].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
listedPreAuthKeys[3].Expiration.Before(time.Now().Add(time.Hour*26)),
)
for index := range listedPreAuthKeys {
@@ -128,7 +127,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
assert.Equal(
t,
[]string{"tag:test1", "tag:test2"},
listedPreAuthKeys[index].GetAclTags(),
listedPreAuthKeys[index].AclTags,
)
}
@@ -139,12 +138,12 @@ func TestPreAuthKeyCommand(t *testing.T) {
"preauthkeys",
"expire",
"--id",
strconv.FormatUint(keys[0].GetId(), 10),
keys[0].Id,
},
)
require.NoError(t, err)
var listedPreAuthKeysAfterExpire []v1.PreAuthKey
var listedPreAuthKeysAfterExpire []clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -161,9 +160,9 @@ func TestPreAuthKeyCommand(t *testing.T) {
assert.NoError(c, err)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth keys list after expire")
assert.True(t, listedPreAuthKeysAfterExpire[1].GetExpiration().AsTime().Before(time.Now()))
assert.True(t, listedPreAuthKeysAfterExpire[2].GetExpiration().AsTime().After(time.Now()))
assert.True(t, listedPreAuthKeysAfterExpire[3].GetExpiration().AsTime().After(time.Now()))
assert.True(t, listedPreAuthKeysAfterExpire[1].Expiration.Before(time.Now()))
assert.True(t, listedPreAuthKeysAfterExpire[2].Expiration.After(time.Now()))
assert.True(t, listedPreAuthKeysAfterExpire[3].Expiration.After(time.Now()))
}
func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
@@ -185,7 +184,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
headscale, err := scenario.Headscale()
require.NoError(t, err)
var preAuthKey v1.PreAuthKey
var preAuthKey clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -205,7 +204,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
assert.NoError(c, err)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth key creation without expiry")
var listedPreAuthKeys []v1.PreAuthKey
var listedPreAuthKeys []clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -225,10 +224,10 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
// There is one key created by [Scenario.CreateHeadscaleEnv]
assert.Len(t, listedPreAuthKeys, 2)
assert.True(t, listedPreAuthKeys[1].GetExpiration().AsTime().After(time.Now()))
assert.True(t, listedPreAuthKeys[1].Expiration.After(time.Now()))
assert.True(
t,
listedPreAuthKeys[1].GetExpiration().AsTime().Before(time.Now().Add(time.Minute*70)),
listedPreAuthKeys[1].Expiration.Before(time.Now().Add(time.Minute*70)),
)
}
@@ -251,7 +250,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
headscale, err := scenario.Headscale()
require.NoError(t, err)
var preAuthReusableKey v1.PreAuthKey
var preAuthReusableKey clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -271,7 +270,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
assert.NoError(c, err)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for reusable preauth key creation")
var preAuthEphemeralKey v1.PreAuthKey
var preAuthEphemeralKey clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -291,10 +290,10 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
assert.NoError(c, err)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for ephemeral preauth key creation")
assert.True(t, preAuthEphemeralKey.GetEphemeral())
assert.False(t, preAuthEphemeralKey.GetReusable())
assert.True(t, preAuthEphemeralKey.Ephemeral)
assert.False(t, preAuthEphemeralKey.Reusable)
var listedPreAuthKeys []v1.PreAuthKey
var listedPreAuthKeys []clientv1.PreAuthKey
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(
@@ -325,7 +324,7 @@ func TestPreAuthKeyDeleteCommand(t *testing.T) {
defer scenario.ShutdownAssertNoPanics(t)
// Create a key to delete.
created := assertJSONRoundtrip[*v1.PreAuthKey](t, headscale, []string{
created := assertJSONRoundtrip[*clientv1.PreAuthKey](t, headscale, []string{
"headscale",
"preauthkeys",
"--user", "1",
@@ -333,7 +332,7 @@ func TestPreAuthKeyDeleteCommand(t *testing.T) {
"--reusable",
"--output", "json",
})
require.NotZero(t, created.GetId())
require.NotEmpty(t, created.Id)
// delete with no --id must be rejected.
_, err := headscale.Execute([]string{"headscale", "preauthkeys", "delete"})
@@ -342,13 +341,13 @@ func TestPreAuthKeyDeleteCommand(t *testing.T) {
// delete the created key by id.
_, err = headscale.Execute([]string{
"headscale", "preauthkeys", "delete",
"--id", strconv.FormatUint(created.GetId(), 10),
"--id", created.Id,
})
require.NoError(t, err)
// The deleted key must be gone from the list.
assert.EventuallyWithT(t, func(c *assert.CollectT) {
var listed []v1.PreAuthKey
var listed []clientv1.PreAuthKey
err := executeAndUnmarshal(headscale,
[]string{"headscale", "preauthkeys", "list", "--output", "json"},
@@ -357,7 +356,7 @@ func TestPreAuthKeyDeleteCommand(t *testing.T) {
assert.NoError(c, err)
for i := range listed {
assert.NotEqual(c, created.GetId(), listed[i].GetId(), "deleted key should not be listed")
assert.NotEqual(c, created.Id, listed[i].Id, "deleted key should not be listed")
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth key list after delete")
}
+3 -3
View File
@@ -3,7 +3,7 @@ package integration
import (
"testing"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -20,10 +20,10 @@ func TestServerInfoCommands(t *testing.T) {
defer scenario.ShutdownAssertNoPanics(t)
t.Run("health", func(t *testing.T) {
health := assertJSONRoundtrip[*v1.HealthResponse](t, headscale, []string{
health := assertJSONRoundtrip[*clientv1.HealthResponseBody](t, headscale, []string{
"headscale", "health", "--output", "json",
})
assert.True(t, health.GetDatabaseConnectivity(), "database should be reachable")
assert.True(t, health.DatabaseConnectivity, "database should be reachable")
})
t.Run("version", func(t *testing.T) {
+9 -11
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"testing"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/tsic"
"github.com/stretchr/testify/require"
@@ -19,8 +20,8 @@ import (
//
// The whole point of the CLI test suite is to guard the transport: every
// command is invoked with `--output json` and the result is unmarshalled into
// the matching gen/go/headscale/v1 Go type, so a change to the gRPC handlers,
// proto definitions or output encoders that breaks a command is caught here.
// the matching HTTP-client Go type, so a change to the API handlers,
// schema definitions or output encoders that breaks a command is caught here.
func executeAndUnmarshal[T any](headscale ControlServer, command []string, result T) error {
str, err := headscale.Execute(command)
@@ -39,7 +40,7 @@ func executeAndUnmarshal[T any](headscale ControlServer, command []string, resul
// assertJSONRoundtrip executes command (which must include `--output json`),
// decodes the stdout into T, then marshals T back to JSON and re-decodes it,
// asserting the serialisation is stable. This is the transport contract guard:
// if the underlying v1 type drifts in a way that loses data, the round-trip
// if the underlying type drifts in a way that loses data, the round-trip
// breaks. The decoded value is returned so callers can assert on real fields.
func assertJSONRoundtrip[T any](t require.TestingT, headscale ControlServer, command []string) T {
var first T
@@ -62,14 +63,11 @@ func assertJSONRoundtrip[T any](t require.TestingT, headscale ControlServer, com
return second
}
// Interface ensuring that we can sort structs from gRPC that
// have an ID field.
type GRPCSortable interface {
GetId() uint64
}
func sortWithID[T GRPCSortable](a, b T) int {
return cmp.Compare(a.GetId(), b.GetId())
// sortWithID orders users by their numeric ID. The HTTP client emits IDs as
// decimal strings, so they are parsed back to integers to preserve numeric
// (not lexicographic) ordering.
func sortWithID(a, b *clientv1.User) int {
return cmp.Compare(mustParseID(a.Id), mustParseID(b.Id))
}
// setupCLIScenario boots a scenario with the given users and nodes-per-user,
+29 -30
View File
@@ -2,14 +2,13 @@ package integration
import (
"encoding/json"
"fmt"
"slices"
"testing"
"time"
tcmp "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/integrationutil"
"github.com/juanfont/headscale/integration/tsic"
@@ -36,7 +35,7 @@ func TestUserCommand(t *testing.T) {
require.NoError(t, err)
var (
listUsers []*v1.User
listUsers []*clientv1.User
result []string
)
@@ -54,7 +53,7 @@ func TestUserCommand(t *testing.T) {
assert.NoError(ct, err)
slices.SortFunc(listUsers, sortWithID)
result = []string{listUsers[0].GetName(), listUsers[1].GetName()}
result = []string{listUsers[0].Name, listUsers[1].Name}
assert.Equal(
ct,
@@ -70,13 +69,13 @@ func TestUserCommand(t *testing.T) {
"users",
"rename",
"--output=json",
fmt.Sprintf("--identifier=%d", listUsers[1].GetId()),
"--identifier=" + listUsers[1].Id,
"--new-name=newname",
},
)
require.NoError(t, err)
var listAfterRenameUsers []*v1.User
var listAfterRenameUsers []*clientv1.User
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
err := executeAndUnmarshal(headscale,
@@ -92,7 +91,7 @@ func TestUserCommand(t *testing.T) {
assert.NoError(ct, err)
slices.SortFunc(listAfterRenameUsers, sortWithID)
result = []string{listAfterRenameUsers[0].GetName(), listAfterRenameUsers[1].GetName()}
result = []string{listAfterRenameUsers[0].Name, listAfterRenameUsers[1].Name}
assert.Equal(
ct,
@@ -102,7 +101,7 @@ func TestUserCommand(t *testing.T) {
)
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
var listByUsername []*v1.User
var listByUsername []*clientv1.User
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(headscale,
@@ -121,19 +120,19 @@ func TestUserCommand(t *testing.T) {
slices.SortFunc(listByUsername, sortWithID)
want := []*v1.User{
want := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@test.no",
},
}
if diff := tcmp.Diff(want, listByUsername, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := tcmp.Diff(want, listByUsername, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
t.Errorf("unexpected users (-want +got):\n%s", diff)
}
var listByID []*v1.User
var listByID []*clientv1.User
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(headscale,
@@ -152,15 +151,15 @@ func TestUserCommand(t *testing.T) {
slices.SortFunc(listByID, sortWithID)
want = []*v1.User{
want = []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@test.no",
},
}
if diff := tcmp.Diff(want, listByID, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := tcmp.Diff(want, listByID, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
t.Errorf("unexpected users (-want +got):\n%s", diff)
}
@@ -177,7 +176,7 @@ func TestUserCommand(t *testing.T) {
require.NoError(t, err)
assert.Contains(t, deleteResult, "User destroyed")
var listAfterIDDelete []*v1.User
var listAfterIDDelete []*clientv1.User
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
err := executeAndUnmarshal(headscale,
@@ -194,15 +193,15 @@ func TestUserCommand(t *testing.T) {
slices.SortFunc(listAfterIDDelete, sortWithID)
want := []*v1.User{
want := []*clientv1.User{
{
Id: 2,
Id: "2",
Name: "newname",
Email: "user2@test.no",
},
}
if diff := tcmp.Diff(want, listAfterIDDelete, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := tcmp.Diff(want, listAfterIDDelete, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
assert.Fail(ct, "unexpected users", "diff (-want +got):\n%s", diff)
}
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
@@ -219,7 +218,7 @@ func TestUserCommand(t *testing.T) {
require.NoError(t, err)
assert.Contains(t, deleteResult, "User destroyed")
var listAfterNameDelete []v1.User
var listAfterNameDelete []clientv1.User
assert.EventuallyWithT(t, func(c *assert.CollectT) {
err = executeAndUnmarshal(headscale,
@@ -249,8 +248,8 @@ func TestUserCreateCommand(t *testing.T) {
defer scenario.ShutdownAssertNoPanics(t)
// Create a user populated with every optional field. The created user is
// returned on stdout and round-tripped through the v1.User type.
created := assertJSONRoundtrip[*v1.User](t, headscale, []string{
// returned on stdout and round-tripped through the User type.
created := assertJSONRoundtrip[*clientv1.User](t, headscale, []string{
"headscale",
"users",
"create",
@@ -261,14 +260,14 @@ func TestUserCreateCommand(t *testing.T) {
"--output", "json",
})
assert.Equal(t, "cli-created", created.GetName())
assert.Equal(t, "CLI Created", created.GetDisplayName())
assert.Equal(t, "cli-created@example.com", created.GetEmail())
assert.Equal(t, "https://example.com/avatar.png", created.GetProfilePicUrl())
assert.Equal(t, "cli-created", created.Name)
assert.Equal(t, "CLI Created", created.DisplayName)
assert.Equal(t, "cli-created@example.com", created.Email)
assert.Equal(t, "https://example.com/avatar.png", created.ProfilePicUrl)
// The created fields must survive a list query (read-after-write) and be
// filterable by email.
var byEmail []*v1.User
var byEmail []*clientv1.User
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
err := executeAndUnmarshal(headscale,
@@ -286,8 +285,8 @@ func TestUserCreateCommand(t *testing.T) {
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for user list by email")
require.Len(t, byEmail, 1)
assert.Equal(t, "cli-created", byEmail[0].GetName())
assert.Equal(t, "CLI Created", byEmail[0].GetDisplayName())
assert.Equal(t, "cli-created", byEmail[0].Name)
assert.Equal(t, "CLI Created", byEmail[0].DisplayName)
}
// TestUserCommandValidation exercises the validation and error permutations of
@@ -325,7 +324,7 @@ func TestUserCommandValidation(t *testing.T) {
case tt.wantEmptyList:
require.NoError(t, err)
var users []v1.User
var users []clientv1.User
require.NoError(t, json.Unmarshal([]byte(out), &users))
require.Empty(t, users)
+11 -11
View File
@@ -3,7 +3,7 @@ package integration
import (
"net/netip"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
@@ -24,19 +24,19 @@ type ControlServer interface {
GetEndpoint() string
WaitForRunning() error
Restart() error
CreateUser(user string) (*v1.User, error)
CreateAuthKey(user uint64, reusable bool, ephemeral bool) (*v1.PreAuthKey, error)
CreateAuthKeyWithTags(user uint64, reusable bool, ephemeral bool, tags []string) (*v1.PreAuthKey, error)
CreateAuthKeyWithOptions(opts hsic.AuthKeyOptions) (*v1.PreAuthKey, error)
CreateUser(user string) (*clientv1.User, error)
CreateAuthKey(user uint64, reusable bool, ephemeral bool) (*clientv1.PreAuthKey, error)
CreateAuthKeyWithTags(user uint64, reusable bool, ephemeral bool, tags []string) (*clientv1.PreAuthKey, error)
CreateAuthKeyWithOptions(opts hsic.AuthKeyOptions) (*clientv1.PreAuthKey, error)
DeleteAuthKey(id uint64) error
ListNodes(users ...string) ([]*v1.Node, error)
ListNodes(users ...string) ([]*clientv1.Node, error)
DeleteNode(nodeID uint64) error
NodesByUser() (map[string][]*v1.Node, error)
NodesByName() (map[string]*v1.Node, error)
ListUsers() ([]*v1.User, error)
MapUsers() (map[string]*v1.User, error)
NodesByUser() (map[string][]*clientv1.Node, error)
NodesByName() (map[string]*clientv1.Node, error)
ListUsers() ([]*clientv1.User, error)
MapUsers() (map[string]*clientv1.User, error)
DeleteUser(userID uint64) error
ApproveRoutes(nodeID uint64, routes []netip.Prefix) (*v1.Node, error)
ApproveRoutes(nodeID uint64, routes []netip.Prefix) (*clientv1.Node, error)
SetNodeTags(nodeID uint64, tags []string) error
GetCert() []byte
GetHostname() string
+53 -53
View File
@@ -10,7 +10,7 @@ import (
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/integrationutil"
@@ -169,12 +169,12 @@ func testEphemeralWithOptions(t *testing.T, opts ...hsic.Option) {
t.Fatalf("failed to create tailscale nodes in user %s: %s", userName, err)
}
key, err := scenario.CreatePreAuthKey(user.GetId(), true, true)
key, err := scenario.CreatePreAuthKey(mustParseID(user.Id), true, true)
if err != nil {
t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
}
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.Key)
if err != nil {
t.Fatalf("failed to run tailscale up for user %s: %s", userName, err)
}
@@ -247,12 +247,12 @@ func TestEphemeral2006DeletedTooQuickly(t *testing.T) {
t.Fatalf("failed to create tailscale nodes in user %s: %s", userName, err)
}
key, err := scenario.CreatePreAuthKey(user.GetId(), true, true)
key, err := scenario.CreatePreAuthKey(mustParseID(user.Id), true, true)
if err != nil {
t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
}
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.Key)
if err != nil {
t.Fatalf("failed to run tailscale up for user %s: %s", userName, err)
}
@@ -402,7 +402,7 @@ func TestTaildrop(t *testing.T) {
network := networks[0]
// Create untagged nodes for user1 using all test versions
user1Key, err := scenario.CreatePreAuthKey(userMap["user1"].GetId(), true, false)
user1Key, err := scenario.CreatePreAuthKey(mustParseID(userMap["user1"].Id), true, false)
require.NoError(t, err)
var user1Clients []TailscaleClient
@@ -414,7 +414,7 @@ func TestTaildrop(t *testing.T) {
)
require.NoError(t, err)
err = client.Login(headscale.GetEndpoint(), user1Key.GetKey())
err = client.Login(headscale.GetEndpoint(), user1Key.Key)
require.NoError(t, err)
err = client.WaitForRunning(integrationutil.PeerSyncTimeout())
@@ -425,7 +425,7 @@ func TestTaildrop(t *testing.T) {
}
// Create untagged nodes for user2 using all test versions
user2Key, err := scenario.CreatePreAuthKey(userMap["user2"].GetId(), true, false)
user2Key, err := scenario.CreatePreAuthKey(mustParseID(userMap["user2"].Id), true, false)
require.NoError(t, err)
var user2Clients []TailscaleClient
@@ -437,7 +437,7 @@ func TestTaildrop(t *testing.T) {
)
require.NoError(t, err)
err = client.Login(headscale.GetEndpoint(), user2Key.GetKey())
err = client.Login(headscale.GetEndpoint(), user2Key.Key)
require.NoError(t, err)
err = client.WaitForRunning(integrationutil.PeerSyncTimeout())
@@ -449,7 +449,7 @@ func TestTaildrop(t *testing.T) {
// Create a tagged device (tags-as-identity: tags come from PreAuthKey)
// Use "head" version to test latest behavior
taggedKey, err := scenario.CreatePreAuthKeyWithTags(userMap["user1"].GetId(), true, false, []string{"tag:server"})
taggedKey, err := scenario.CreatePreAuthKeyWithTags(mustParseID(userMap["user1"].Id), true, false, []string{"tag:server"})
require.NoError(t, err)
taggedClient, err := scenario.CreateTailscaleNode(
@@ -458,7 +458,7 @@ func TestTaildrop(t *testing.T) {
)
require.NoError(t, err)
err = taggedClient.Login(headscale.GetEndpoint(), taggedKey.GetKey())
err = taggedClient.Login(headscale.GetEndpoint(), taggedKey.Key)
require.NoError(t, err)
err = taggedClient.WaitForRunning(integrationutil.PeerSyncTimeout())
@@ -767,8 +767,8 @@ func TestUpdateHostnameFromClient(t *testing.T) {
// Pre-rewrite these were rejected by ApplyHostnameFromHostInfo with
// "invalid characters" and the node was stuck on an invalid-<rand>
// GivenName with the HostName update dropped. The assertions below
// verify both raw preservation ([v1.Node.Name]) and SaaS-matching sanitisation
// ([v1.Node.GivenName]) for each awkward input.
// verify both raw preservation ([clientv1.Node.Name]) and SaaS-matching sanitisation
// ([clientv1.Node.GivenName]) for each awkward input.
hostnames := map[string]string{
"1": "Joe's Mac mini",
"2": "Test@Host",
@@ -814,7 +814,7 @@ func TestUpdateHostnameFromClient(t *testing.T) {
// Wait for nodestore batch processing to complete
// [state.NodeStore] batching timeout is 500ms, so we wait up to 1 second
var nodes []*v1.Node
var nodes []*clientv1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
err := executeAndUnmarshal(
headscale,
@@ -831,18 +831,18 @@ func TestUpdateHostnameFromClient(t *testing.T) {
assert.Len(ct, nodes, 3, "Should have 3 nodes after hostname updates")
for _, node := range nodes {
hostname := hostnames[strconv.FormatUint(node.GetId(), 10)]
assert.Equal(ct, hostname, node.GetName(), "Node name should match hostname")
hostname := hostnames[node.Id]
assert.Equal(ct, hostname, node.Name, "Node name should match hostname")
// GivenName is sanitised via [dnsname.SanitizeHostname] (SaaS algorithm).
assert.Equal(ct, dnsname.SanitizeHostname(hostname), node.GetGivenName(),
assert.Equal(ct, dnsname.SanitizeHostname(hostname), node.GivenName,
"Given name should match SaaS hostname-sanitisation rules")
}
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
// Rename givenName in nodes
for _, node := range nodes {
givenName := fmt.Sprintf("%d-givenname", node.GetId())
givenName := fmt.Sprintf("%s-givenname", node.Id)
_, err = headscale.Execute(
[]string{
"headscale",
@@ -850,7 +850,7 @@ func TestUpdateHostnameFromClient(t *testing.T) {
"rename",
givenName,
"--identifier",
strconv.FormatUint(node.GetId(), 10),
node.Id,
})
require.NoError(t, err)
}
@@ -860,8 +860,8 @@ func TestUpdateHostnameFromClient(t *testing.T) {
// Build a map of expected DNSNames by node ID
expectedDNSNames := make(map[string]string)
for _, node := range nodes {
nodeID := strconv.FormatUint(node.GetId(), 10)
expectedDNSNames[nodeID] = fmt.Sprintf("%d-givenname.headscale.net.", node.GetId())
nodeID := node.Id
expectedDNSNames[nodeID] = fmt.Sprintf("%s-givenname.headscale.net.", node.Id)
}
// Verify from each client's perspective
@@ -931,9 +931,9 @@ func TestUpdateHostnameFromClient(t *testing.T) {
}
for _, node := range nodes {
hostname := hostnames[strconv.FormatUint(node.GetId(), 10)]
givenName := fmt.Sprintf("%d-givenname", node.GetId())
if node.GetName() != hostname+"NEW" || node.GetGivenName() != givenName {
hostname := hostnames[node.Id]
givenName := fmt.Sprintf("%s-givenname", node.Id)
if node.Name != hostname+"NEW" || node.GivenName != givenName {
return false
}
}
@@ -993,15 +993,15 @@ func TestExpireNode(t *testing.T) {
})
require.NoError(t, err)
var node v1.Node
var node clientv1.Node
err = json.Unmarshal([]byte(result), &node)
require.NoError(t, err)
var expiredNodeKey key.NodePublic
err = expiredNodeKey.UnmarshalText([]byte(node.GetNodeKey()))
err = expiredNodeKey.UnmarshalText([]byte(node.NodeKey))
require.NoError(t, err)
t.Logf("Node %s with node_key %s has been expired", node.GetName(), expiredNodeKey.String())
t.Logf("Node %s with node_key %s has been expired", node.Name, expiredNodeKey.String())
// Verify that the expired node has been marked in all peers list.
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -1009,7 +1009,7 @@ func TestExpireNode(t *testing.T) {
status, err := client.Status()
assert.NoError(ct, err)
if client.Hostname() != node.GetName() {
if client.Hostname() != node.Name {
// Check if the expired node appears as expired in this client's peer list
for key, peer := range status.Peer {
if key == expiredNodeKey {
@@ -1025,7 +1025,7 @@ func TestExpireNode(t *testing.T) {
// Verify that the expired node has been marked in all peers list.
for _, client := range allClients {
if client.Hostname() == node.GetName() {
if client.Hostname() == node.Name {
continue
}
@@ -1060,11 +1060,11 @@ func TestExpireNode(t *testing.T) {
peerStatus.Expired,
)
_, stderr, _ := client.Execute([]string{"tailscale", "ping", node.GetName()})
_, stderr, _ := client.Execute([]string{"tailscale", "ping", node.Name})
if !strings.Contains(stderr, "node key has expired") {
c.Errorf(
"expected to be unable to ping expired host %q from %q",
node.GetName(),
node.Name,
client.Hostname(),
)
}
@@ -1111,19 +1111,19 @@ func TestSetNodeExpiryInFuture(t *testing.T) {
)
require.NoError(t, err)
var node v1.Node
var node clientv1.Node
err = json.Unmarshal([]byte(result), &node)
require.NoError(t, err)
require.True(t, node.GetExpiry().AsTime().After(time.Now()))
require.WithinDuration(t, targetExpiry, node.GetExpiry().AsTime(), 2*time.Second)
require.True(t, node.Expiry.After(time.Now()))
require.WithinDuration(t, targetExpiry, *node.Expiry, 2*time.Second)
var nodeKey key.NodePublic
err = nodeKey.UnmarshalText([]byte(node.GetNodeKey()))
err = nodeKey.UnmarshalText([]byte(node.NodeKey))
require.NoError(t, err)
for _, client := range allClients {
if client.Hostname() == node.GetName() {
if client.Hostname() == node.Name {
continue
}
@@ -1208,10 +1208,10 @@ func TestDisableNodeExpiry(t *testing.T) {
)
require.NoError(t, err)
var node v1.Node
var node clientv1.Node
err = json.Unmarshal([]byte(result), &node)
require.NoError(t, err)
require.NotNil(t, node.GetExpiry(), "node should have an expiry set")
require.NotNil(t, node.Expiry, "node should have an expiry set")
// Now disable the expiry.
result, err = headscale.Execute(
@@ -1224,23 +1224,23 @@ func TestDisableNodeExpiry(t *testing.T) {
)
require.NoError(t, err)
var nodeDisabled v1.Node
var nodeDisabled clientv1.Node
err = json.Unmarshal([]byte(result), &nodeDisabled)
require.NoError(t, err)
// Expiry should be nil (or zero time) when disabled.
if nodeDisabled.GetExpiry() != nil {
require.True(t, nodeDisabled.GetExpiry().AsTime().IsZero(),
if nodeDisabled.Expiry != nil {
require.True(t, nodeDisabled.Expiry.IsZero(),
"node expiry should be zero/nil after disabling")
}
var nodeKey key.NodePublic
err = nodeKey.UnmarshalText([]byte(nodeDisabled.GetNodeKey()))
err = nodeKey.UnmarshalText([]byte(nodeDisabled.NodeKey))
require.NoError(t, err)
// Verify peers see the node as not expired.
for _, client := range allClients {
if client.Hostname() == nodeDisabled.GetName() {
if client.Hostname() == nodeDisabled.Name {
continue
}
@@ -1332,7 +1332,7 @@ func TestNodeOnlineStatus(t *testing.T) {
return
}
var nodes []*v1.Node
var nodes []*clientv1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
result, err := headscale.Execute([]string{
"headscale", "nodes", "list", "--output", "json",
@@ -1347,9 +1347,9 @@ func TestNodeOnlineStatus(t *testing.T) {
// All nodes should be online
assert.Truef(
ct,
node.GetOnline(),
node.Online,
"expected %s to have online status in Headscale, marked as offline %s after start",
node.GetName(),
node.Name,
time.Since(start),
)
}
@@ -1537,7 +1537,7 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
require.NoError(t, err)
// Test list all nodes after added otherUser
var nodeList []v1.Node
var nodeList []clientv1.Node
err = executeAndUnmarshal(
headscale,
[]string{
@@ -1551,8 +1551,8 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
)
require.NoError(t, err)
assert.Len(t, nodeList, 2)
assert.True(t, nodeList[0].GetOnline())
assert.True(t, nodeList[1].GetOnline())
assert.True(t, nodeList[0].Online)
assert.True(t, nodeList[1].Online)
// Delete the first node, which is online
_, err = headscale.Execute(
@@ -1562,7 +1562,7 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
"delete",
"--identifier",
// Delete the last added machine
fmt.Sprintf("%d", nodeList[0].GetId()),
nodeList[0].Id,
"--output",
"json",
"--force",
@@ -1571,7 +1571,7 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
require.NoError(t, err)
// Ensure that the node has been deleted, this did not occur due to a panic.
var nodeListAfter []v1.Node
var nodeListAfter []clientv1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
err = executeAndUnmarshal(
headscale,
@@ -1601,6 +1601,6 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
)
require.NoError(t, err)
assert.Len(t, nodeListAfter, 1)
assert.True(t, nodeListAfter[0].GetOnline())
assert.Equal(t, nodeList[1].GetId(), nodeListAfter[0].GetId())
assert.True(t, nodeListAfter[0].Online)
assert.Equal(t, nodeList[1].Id, nodeListAfter[0].Id)
}
+14 -14
View File
@@ -159,10 +159,10 @@ func TestGrantCapRelay(t *testing.T) {
defer func() { _, _, _ = relayR.Shutdown() }()
pakRelay, err := scenario.CreatePreAuthKeyWithTags(
userMap["relay"].GetId(), false, false, []string{"tag:relay"},
mustParseID(userMap["relay"].Id), false, false, []string{"tag:relay"},
)
require.NoError(t, err)
err = relayR.Login(headscale.GetEndpoint(), pakRelay.GetKey())
err = relayR.Login(headscale.GetEndpoint(), pakRelay.Key)
require.NoError(t, err)
err = relayR.WaitForRunning(30 * time.Second)
require.NoError(t, err)
@@ -192,10 +192,10 @@ func TestGrantCapRelay(t *testing.T) {
defer func() { _, _, _ = clientA.Shutdown() }()
pakClientA, err := scenario.CreatePreAuthKeyWithTags(
userMap["clienta"].GetId(), false, false, []string{"tag:client-a"},
mustParseID(userMap["clienta"].Id), false, false, []string{"tag:client-a"},
)
require.NoError(t, err)
err = clientA.Login(headscale.GetEndpoint(), pakClientA.GetKey())
err = clientA.Login(headscale.GetEndpoint(), pakClientA.Key)
require.NoError(t, err)
err = clientA.WaitForRunning(30 * time.Second)
require.NoError(t, err)
@@ -209,10 +209,10 @@ func TestGrantCapRelay(t *testing.T) {
defer func() { _, _, _ = clientB.Shutdown() }()
pakClientB, err := scenario.CreatePreAuthKeyWithTags(
userMap["clientb"].GetId(), false, false, []string{"tag:client-b"},
mustParseID(userMap["clientb"].Id), false, false, []string{"tag:client-b"},
)
require.NoError(t, err)
err = clientB.Login(headscale.GetEndpoint(), pakClientB.GetKey())
err = clientB.Login(headscale.GetEndpoint(), pakClientB.Key)
require.NoError(t, err)
err = clientB.WaitForRunning(30 * time.Second)
require.NoError(t, err)
@@ -637,10 +637,10 @@ func TestGrantCapDrive(t *testing.T) {
defer func() { _, _, _ = sharer.Shutdown() }()
pakSharer, err := scenario.CreatePreAuthKeyWithTags(
userMap["sharer"].GetId(), false, false, []string{"tag:sharer"},
mustParseID(userMap["sharer"].Id), false, false, []string{"tag:sharer"},
)
require.NoError(t, err)
err = sharer.Login(headscale.GetEndpoint(), pakSharer.GetKey())
err = sharer.Login(headscale.GetEndpoint(), pakSharer.Key)
require.NoError(t, err)
err = sharer.WaitForRunning(30 * time.Second)
require.NoError(t, err)
@@ -654,10 +654,10 @@ func TestGrantCapDrive(t *testing.T) {
defer func() { _, _, _ = rwClient.Shutdown() }()
pakRW, err := scenario.CreatePreAuthKeyWithTags(
userMap["rwclient"].GetId(), false, false, []string{"tag:rw-client"},
mustParseID(userMap["rwclient"].Id), false, false, []string{"tag:rw-client"},
)
require.NoError(t, err)
err = rwClient.Login(headscale.GetEndpoint(), pakRW.GetKey())
err = rwClient.Login(headscale.GetEndpoint(), pakRW.Key)
require.NoError(t, err)
err = rwClient.WaitForRunning(30 * time.Second)
require.NoError(t, err)
@@ -671,10 +671,10 @@ func TestGrantCapDrive(t *testing.T) {
defer func() { _, _, _ = roClient.Shutdown() }()
pakRO, err := scenario.CreatePreAuthKeyWithTags(
userMap["roclient"].GetId(), false, false, []string{"tag:ro-client"},
mustParseID(userMap["roclient"].Id), false, false, []string{"tag:ro-client"},
)
require.NoError(t, err)
err = roClient.Login(headscale.GetEndpoint(), pakRO.GetKey())
err = roClient.Login(headscale.GetEndpoint(), pakRO.Key)
require.NoError(t, err)
err = roClient.WaitForRunning(30 * time.Second)
require.NoError(t, err)
@@ -688,10 +688,10 @@ func TestGrantCapDrive(t *testing.T) {
defer func() { _, _, _ = noAccess.Shutdown() }()
pakNA, err := scenario.CreatePreAuthKeyWithTags(
userMap["noaccess"].GetId(), false, false, []string{"tag:no-access"},
mustParseID(userMap["noaccess"].Id), false, false, []string{"tag:no-access"},
)
require.NoError(t, err)
err = noAccess.Login(headscale.GetEndpoint(), pakNA.GetKey())
err = noAccess.Login(headscale.GetEndpoint(), pakNA.Key)
require.NoError(t, err)
err = noAccess.WaitForRunning(30 * time.Second)
require.NoError(t, err)
+23 -10
View File
@@ -18,7 +18,7 @@ import (
"github.com/cenkalti/backoff/v5"
"github.com/google/go-cmp/cmp"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
@@ -538,15 +538,15 @@ func requireAllClientsNetInfoAndDERP(t *testing.T, headscale ControlServer, expe
// assertLastSeenSet validates that a node has a non-nil LastSeen timestamp.
// Critical for ensuring node activity tracking is functioning properly.
func assertLastSeenSet(t *testing.T, node *v1.Node) {
func assertLastSeenSet(t *testing.T, node *clientv1.Node) {
t.Helper()
assert.NotNil(t, node)
assert.NotNil(t, node.GetLastSeen())
assert.NotNil(t, node.LastSeen)
}
func assertLastSeenSetWithCollect(c *assert.CollectT, node *v1.Node) {
func assertLastSeenSetWithCollect(c *assert.CollectT, node *clientv1.Node) {
assert.NotNil(c, node)
assert.NotNil(c, node.GetLastSeen())
assert.NotNil(c, node.LastSeen)
}
// assertCurlSuccessWithCollect asserts that a curl request succeeds with
@@ -1071,14 +1071,14 @@ func oidcMockUser(username string, emailVerified bool) mockoidc.MockUser {
// GetUserByName retrieves a user by name from the headscale server.
// This is a common pattern used when creating preauth keys or managing users.
func GetUserByName(headscale ControlServer, username string) (*v1.User, error) {
func GetUserByName(headscale ControlServer, username string) (*clientv1.User, error) {
users, err := headscale.ListUsers()
if err != nil {
return nil, fmt.Errorf("listing users: %w", err)
}
for _, u := range users {
if u.GetName() == username {
if u.Name == username {
return u, nil
}
}
@@ -1088,7 +1088,7 @@ func GetUserByName(headscale ControlServer, username string) (*v1.User, error) {
// findNode returns the first node in nodes for which match returns true,
// or nil if no node matches.
func findNode(nodes []*v1.Node, match func(*v1.Node) bool) *v1.Node {
func findNode(nodes []*clientv1.Node, match func(*clientv1.Node) bool) *clientv1.Node {
for _, n := range nodes {
if match(n) {
return n
@@ -1098,6 +1098,19 @@ func findNode(nodes []*v1.Node, match func(*v1.Node) bool) *v1.Node {
return nil
}
// mustParseID parses a string ID emitted by the HTTP client types into a
// uint64 for the APIs that still take numeric identifiers (NodeID, user and
// key IDs). It panics on malformed input, which only happens if the server
// emits a non-numeric ID — a bug worth failing the test loudly.
func mustParseID(id string) uint64 {
parsed, err := strconv.ParseUint(id, 10, 64)
if err != nil {
panic(fmt.Sprintf("parsing id %q: %s", id, err))
}
return parsed
}
// FindNewClient finds a client that is in the new list but not in the original list.
// This is useful when dynamically adding nodes during tests and needing to identify
// which client was just added.
@@ -1177,13 +1190,13 @@ func (s *Scenario) AddAndLoginClient(
return nil, fmt.Errorf("getting user: %w", err)
}
authKey, err := s.CreatePreAuthKey(user.GetId(), true, false)
authKey, err := s.CreatePreAuthKey(mustParseID(user.Id), true, false)
if err != nil {
return nil, fmt.Errorf("creating preauth key: %w", err)
}
// Login the new client
err = newClient.Login(headscale.GetEndpoint(), authKey.GetKey())
err = newClient.Login(headscale.GetEndpoint(), authKey.Key)
if err != nil {
return nil, fmt.Errorf("logging in new client: %w", err)
}
+28 -25
View File
@@ -24,7 +24,7 @@ import (
"time"
"github.com/davecgh/go-spew/spew"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
@@ -1095,7 +1095,7 @@ func (t *HeadscaleInContainer) WaitForRunning() error {
// CreateUser adds a new user to the Headscale instance.
func (t *HeadscaleInContainer) CreateUser(
user string,
) (*v1.User, error) {
) (*clientv1.User, error) {
command := []string{
binHeadscale,
"users",
@@ -1115,7 +1115,7 @@ func (t *HeadscaleInContainer) CreateUser(
return nil, err
}
var u v1.User
var u clientv1.User
err = json.Unmarshal([]byte(result), &u)
if err != nil {
@@ -1140,7 +1140,7 @@ type AuthKeyOptions struct {
// CreateAuthKeyWithOptions creates a new "authorisation key" with the specified options.
// This supports both user-owned and tags-only auth keys.
func (t *HeadscaleInContainer) CreateAuthKeyWithOptions(opts AuthKeyOptions) (*v1.PreAuthKey, error) {
func (t *HeadscaleInContainer) CreateAuthKeyWithOptions(opts AuthKeyOptions) (*clientv1.PreAuthKey, error) {
command := []string{
binHeadscale,
}
@@ -1181,7 +1181,7 @@ func (t *HeadscaleInContainer) CreateAuthKeyWithOptions(opts AuthKeyOptions) (*v
return nil, fmt.Errorf("executing create auth key command: %w", err)
}
var preAuthKey v1.PreAuthKey
var preAuthKey clientv1.PreAuthKey
err = json.Unmarshal([]byte(result), &preAuthKey)
if err != nil {
@@ -1197,7 +1197,7 @@ func (t *HeadscaleInContainer) CreateAuthKey(
user uint64,
reusable bool,
ephemeral bool,
) (*v1.PreAuthKey, error) {
) (*clientv1.PreAuthKey, error) {
return t.CreateAuthKeyWithOptions(AuthKeyOptions{
User: &user,
Reusable: reusable,
@@ -1212,7 +1212,7 @@ func (t *HeadscaleInContainer) CreateAuthKeyWithTags(
reusable bool,
ephemeral bool,
tags []string,
) (*v1.PreAuthKey, error) {
) (*clientv1.PreAuthKey, error) {
return t.CreateAuthKeyWithOptions(AuthKeyOptions{
User: &user,
Reusable: reusable,
@@ -1252,8 +1252,8 @@ func (t *HeadscaleInContainer) DeleteAuthKey(
// specific users.
func (t *HeadscaleInContainer) ListNodes(
users ...string,
) ([]*v1.Node, error) {
var ret []*v1.Node
) ([]*clientv1.Node, error) {
var ret []*clientv1.Node
execUnmarshal := func(command []string) error {
result, _, err := dockertestutil.ExecuteCommand(
@@ -1265,7 +1265,7 @@ func (t *HeadscaleInContainer) ListNodes(
return fmt.Errorf("executing list node command: %w", err)
}
var nodes []*v1.Node
var nodes []*clientv1.Node
err = json.Unmarshal([]byte(result), &nodes)
if err != nil {
@@ -1293,8 +1293,11 @@ func (t *HeadscaleInContainer) ListNodes(
}
}
slices.SortFunc(ret, func(a, b *v1.Node) int {
return cmp.Compare(a.GetId(), b.GetId())
slices.SortFunc(ret, func(a, b *clientv1.Node) int {
ai, _ := strconv.ParseUint(a.Id, 10, 64)
bi, _ := strconv.ParseUint(b.Id, 10, 64)
return cmp.Compare(ai, bi)
})
return ret, nil
@@ -1324,37 +1327,37 @@ func (t *HeadscaleInContainer) DeleteNode(nodeID uint64) error {
return nil
}
func (t *HeadscaleInContainer) NodesByUser() (map[string][]*v1.Node, error) {
func (t *HeadscaleInContainer) NodesByUser() (map[string][]*clientv1.Node, error) {
nodes, err := t.ListNodes()
if err != nil {
return nil, err
}
userMap := make(map[string][]*v1.Node)
userMap := make(map[string][]*clientv1.Node)
for _, node := range nodes {
name := node.GetUser().GetName()
name := node.User.Name
userMap[name] = append(userMap[name], node)
}
return userMap, nil
}
func (t *HeadscaleInContainer) NodesByName() (map[string]*v1.Node, error) {
func (t *HeadscaleInContainer) NodesByName() (map[string]*clientv1.Node, error) {
nodes, err := t.ListNodes()
if err != nil {
return nil, err
}
var nameMap map[string]*v1.Node
var nameMap map[string]*clientv1.Node
for _, node := range nodes {
mak.Set(&nameMap, node.GetName(), node)
mak.Set(&nameMap, node.Name, node)
}
return nameMap, nil
}
// ListUsers returns a list of users from Headscale.
func (t *HeadscaleInContainer) ListUsers() ([]*v1.User, error) {
func (t *HeadscaleInContainer) ListUsers() ([]*clientv1.User, error) {
command := []string{binHeadscale, "users", "list", flagOutput, "json"}
result, _, err := dockertestutil.ExecuteCommand(
@@ -1366,7 +1369,7 @@ func (t *HeadscaleInContainer) ListUsers() ([]*v1.User, error) {
return nil, fmt.Errorf("executing list node command: %w", err)
}
var users []*v1.User
var users []*clientv1.User
err = json.Unmarshal([]byte(result), &users)
if err != nil {
@@ -1378,15 +1381,15 @@ func (t *HeadscaleInContainer) ListUsers() ([]*v1.User, error) {
// MapUsers returns a map of users from Headscale. It is keyed by the
// user name.
func (t *HeadscaleInContainer) MapUsers() (map[string]*v1.User, error) {
func (t *HeadscaleInContainer) MapUsers() (map[string]*clientv1.User, error) {
users, err := t.ListUsers()
if err != nil {
return nil, err
}
var userMap map[string]*v1.User
var userMap map[string]*clientv1.User
for _, user := range users {
mak.Set(&userMap, user.GetName(), user)
mak.Set(&userMap, user.Name, user)
}
return userMap, nil
@@ -1545,7 +1548,7 @@ func (h *HeadscaleInContainer) Restart() error {
}
// ApproveRoutes approves routes for a node.
func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (*v1.Node, error) {
func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (*clientv1.Node, error) {
command := []string{
binHeadscale, "nodes", "approve-routes",
flagOutput, "json",
@@ -1567,7 +1570,7 @@ func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (
)
}
var node *v1.Node
var node *clientv1.Node
err = json.Unmarshal([]byte(result), &node)
if err != nil {
+145 -145
View File
@@ -8,14 +8,13 @@ import (
"net/netip"
"slices"
"sort"
"strconv"
"strings"
"testing"
"time"
cmpdiff "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
@@ -87,7 +86,7 @@ func TestEnablingRoutes(t *testing.T) {
err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err)
var nodes []*v1.Node
var nodes []*clientv1.Node
// Wait for route advertisements to propagate to [state.NodeStore]
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
@@ -96,9 +95,9 @@ func TestEnablingRoutes(t *testing.T) {
assert.NoError(ct, err)
for _, node := range nodes {
assert.Len(ct, node.GetAvailableRoutes(), 1)
assert.Empty(ct, node.GetApprovedRoutes())
assert.Empty(ct, node.GetSubnetRoutes())
assert.Len(ct, node.AvailableRoutes, 1)
assert.Empty(ct, node.ApprovedRoutes)
assert.Empty(ct, node.SubnetRoutes)
}
}, integrationutil.ScaledTimeout(10*time.Second), 100*time.Millisecond, "route advertisements should propagate to all nodes")
@@ -119,8 +118,8 @@ func TestEnablingRoutes(t *testing.T) {
for _, node := range nodes {
_, err := headscale.ApproveRoutes(
node.GetId(),
util.MustStringsToPrefixes(node.GetAvailableRoutes()),
mustParseID(node.Id),
util.MustStringsToPrefixes(node.AvailableRoutes),
)
require.NoError(t, err)
}
@@ -133,9 +132,9 @@ func TestEnablingRoutes(t *testing.T) {
assert.NoError(ct, err)
for _, node := range nodes {
assert.Len(ct, node.GetAvailableRoutes(), 1)
assert.Len(ct, node.GetApprovedRoutes(), 1)
assert.Len(ct, node.GetSubnetRoutes(), 1)
assert.Len(ct, node.AvailableRoutes, 1)
assert.Len(ct, node.ApprovedRoutes, 1)
assert.Len(ct, node.SubnetRoutes, 1)
}
}, integrationutil.ScaledTimeout(10*time.Second), 100*time.Millisecond, "route approvals should propagate to all nodes")
@@ -181,18 +180,19 @@ func TestEnablingRoutes(t *testing.T) {
assert.NoError(c, err)
for _, node := range nodes {
if node.GetId() == 1 {
assert.Len(c, node.GetAvailableRoutes(), 1) // 10.0.0.0/24
assert.Len(c, node.GetApprovedRoutes(), 1) // 10.0.1.0/24
assert.Empty(c, node.GetSubnetRoutes())
} else if node.GetId() == 2 {
assert.Len(c, node.GetAvailableRoutes(), 1) // 10.0.1.0/24
assert.Empty(c, node.GetApprovedRoutes())
assert.Empty(c, node.GetSubnetRoutes())
} else {
assert.Len(c, node.GetAvailableRoutes(), 1) // 10.0.2.0/24
assert.Len(c, node.GetApprovedRoutes(), 1) // 10.0.2.0/24
assert.Len(c, node.GetSubnetRoutes(), 1) // 10.0.2.0/24
switch node.Id {
case "1":
assert.Len(c, node.AvailableRoutes, 1) // 10.0.0.0/24
assert.Len(c, node.ApprovedRoutes, 1) // 10.0.1.0/24
assert.Empty(c, node.SubnetRoutes)
case "2":
assert.Len(c, node.AvailableRoutes, 1) // 10.0.1.0/24
assert.Empty(c, node.ApprovedRoutes)
assert.Empty(c, node.SubnetRoutes)
default:
assert.Len(c, node.AvailableRoutes, 1) // 10.0.2.0/24
assert.Len(c, node.ApprovedRoutes, 1) // 10.0.2.0/24
assert.Len(c, node.SubnetRoutes, 1) // 10.0.2.0/24
}
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "route state changes should propagate to nodes")
@@ -333,7 +333,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
requireNoErrSync(t, err)
// Wait for route configuration changes after advertising routes
var nodes []*v1.Node
var nodes []*clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err = headscale.ListNodes()
@@ -400,7 +400,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Client can access webservice through router 1 only")
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id),
[]netip.Prefix{pref},
)
require.NoError(t, err)
@@ -479,11 +479,11 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state - router 1 is primary
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
// Note: Router 2 and 3 are available but not approved
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)),
},
}, "Router 1 should be primary for route "+pref.String())
@@ -498,7 +498,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: HA is now active - if router 1 fails, router 2 can take over")
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id),
[]netip.Prefix{pref},
)
require.NoError(t, err)
@@ -559,12 +559,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state - router 1 still primary, router 2 approved but standby
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
// Note: Router 3 is available but not approved
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)),
},
}, "Router 1 should remain primary after router 2 approval")
@@ -594,12 +594,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state - router 1 primary, router 2 approved (standby)
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
// Note: Router 3 is available but not approved
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)),
},
}, "Router 1 primary with router 2 as standby")
@@ -615,7 +615,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Full HA configuration with 1 PRIMARY + 2 STANDBY routers")
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter3.Hostname(), nodes).GetId(),
mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id),
[]netip.Prefix{pref},
)
require.NoError(t, err)
@@ -702,12 +702,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state - all 3 routers approved, router 1 still primary
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)): {pref},
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)),
},
}, "Router 1 primary with all 3 routers approved")
@@ -782,11 +782,11 @@ func TestHASubnetRouterFailover(t *testing.T) {
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
// Router 1 is disconnected, so not in AvailableRoutes
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)): {pref},
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)),
},
}, "Router 2 should be primary after router 1 failure")
@@ -854,10 +854,10 @@ func TestHASubnetRouterFailover(t *testing.T) {
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
// Routers 1 and 2 are disconnected, so not in AvailableRoutes
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)): {pref},
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)),
},
}, "Router 3 should be primary after router 2 failure")
@@ -931,12 +931,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state - router 3 remains primary after router 1 comes back
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
// Router 2 is still disconnected
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)): {pref},
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)),
},
}, "Router 3 should remain primary after router 1 recovery")
@@ -1012,12 +1012,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state - router 3 remains primary after all routers back online
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)): {pref},
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)),
},
}, "Router 3 should remain primary after full recovery")
@@ -1030,7 +1030,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Router 1 (%s) should become new PRIMARY (lowest ID with approved route)", subRouter1.Hostname())
t.Logf(" Expected: Router 2 (%s) remains STANDBY", subRouter2.Hostname())
t.Logf(" Expected: Router 3 (%s) goes to advertised-only state (no longer serving)", subRouter3.Hostname())
_, err = headscale.ApproveRoutes(MustFindNode(subRouter3.Hostname(), nodes).GetId(), []netip.Prefix{})
_, err = headscale.ApproveRoutes(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id), []netip.Prefix{})
// Wait for nodestore batch processing and route state changes to complete
// [state.NodeStore] batching timeout is 500ms, so we wait up to 10 seconds for route failover
@@ -1097,12 +1097,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state - router 1 is primary after router 3 route disabled
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
// Router 3's route is no longer approved, so not in AvailableRoutes
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)),
},
}, "Router 1 should be primary after router 3 route disabled")
@@ -1116,7 +1116,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Router 2 (%s) should become new PRIMARY (only remaining approved route)", subRouter2.Hostname())
t.Logf(" Expected: Router 1 (%s) goes to advertised-only state", subRouter1.Hostname())
t.Logf(" Expected: Router 3 (%s) remains advertised-only", subRouter3.Hostname())
_, err = headscale.ApproveRoutes(MustFindNode(subRouter1.Hostname(), nodes).GetId(), []netip.Prefix{})
_, err = headscale.ApproveRoutes(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id), []netip.Prefix{})
// Wait for nodestore batch processing and route state changes to complete
// [state.NodeStore] batching timeout is 500ms, so we wait up to 10 seconds for route failover
@@ -1184,11 +1184,11 @@ func TestHASubnetRouterFailover(t *testing.T) {
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
// Router 1's route is no longer approved, so not in AvailableRoutes
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
// Router 3's route is still not approved
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)),
},
}, "Router 2 should be primary after router 1 route disabled")
@@ -1205,8 +1205,8 @@ func TestHASubnetRouterFailover(t *testing.T) {
r1Node := MustFindNode(subRouter1.Hostname(), nodes)
_, err = headscale.ApproveRoutes(
r1Node.GetId(),
util.MustStringsToPrefixes(r1Node.GetAvailableRoutes()),
mustParseID(r1Node.Id),
util.MustStringsToPrefixes(r1Node.AvailableRoutes),
)
// Wait for route state changes after re-enabling r1
@@ -1268,12 +1268,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state after router 1 re-approval
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
// Router 3 route is still not approved
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)),
},
}, "Router 2 should remain primary after router 1 re-approval")
@@ -1290,8 +1290,8 @@ func TestHASubnetRouterFailover(t *testing.T) {
r3Node := MustFindNode(subRouter3.Hostname(), nodes)
_, err = headscale.ApproveRoutes(
r3Node.GetId(),
util.MustStringsToPrefixes(r3Node.GetAvailableRoutes()),
mustParseID(r3Node.Id),
util.MustStringsToPrefixes(r3Node.AvailableRoutes),
)
// Wait for route state changes after re-enabling r3
@@ -1310,12 +1310,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state after router 3 re-approval
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)): {pref},
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)),
},
}, "Router 2 should remain primary after router 3 re-approval")
@@ -1426,7 +1426,7 @@ func TestSubnetRouteACL(t *testing.T) {
requireNoErrSync(t, err)
// Wait for route advertisements to propagate to the server
var nodes []*v1.Node
var nodes []*clientv1.Node
require.EventuallyWithT(t, func(c *assert.CollectT) {
var err error
@@ -1437,12 +1437,12 @@ func TestSubnetRouteACL(t *testing.T) {
// Find the node that should have the route by checking node IDs
var (
routeNode *v1.Node
otherNode *v1.Node
routeNode *clientv1.Node
otherNode *clientv1.Node
)
for _, node := range nodes {
nodeIDStr := strconv.FormatUint(node.GetId(), 10)
nodeIDStr := node.Id
if _, shouldHaveRoute := expectedRoutes[nodeIDStr]; shouldHaveRoute {
routeNode = node
} else {
@@ -1623,7 +1623,7 @@ func TestEnablingExitRoutes(t *testing.T) {
err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err)
var nodes []*v1.Node
var nodes []*clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
var err error
@@ -1654,12 +1654,12 @@ func TestEnablingExitRoutes(t *testing.T) {
// Enable all routes, but do v4 on one and v6 on other to ensure they
// are both added since they are exit routes.
_, err = headscale.ApproveRoutes(
nodes[0].GetId(),
mustParseID(nodes[0].Id),
[]netip.Prefix{tsaddr.AllIPv4()},
)
require.NoError(t, err)
_, err = headscale.ApproveRoutes(
nodes[1].GetId(),
mustParseID(nodes[1].Id),
[]netip.Prefix{tsaddr.AllIPv6()},
)
require.NoError(t, err)
@@ -1754,7 +1754,7 @@ func TestExitRoutesWithAutogroupInternetACL(t *testing.T) {
// so the standard WaitForTailscaleSync wait would deadlock here —
// the post-approval [assert.EventuallyWithT] block below covers the peer
// state we actually care about.
var nodes []*v1.Node
var nodes []*clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err = headscale.ListNodes()
@@ -1770,12 +1770,12 @@ func TestExitRoutesWithAutogroupInternetACL(t *testing.T) {
// alice's exit. The bug fix is about visibility, not which node
// is chosen.
_, err = headscale.ApproveRoutes(
nodes[0].GetId(),
mustParseID(nodes[0].Id),
[]netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()},
)
require.NoError(t, err)
_, err = headscale.ApproveRoutes(
nodes[1].GetId(),
mustParseID(nodes[1].Id),
[]netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()},
)
require.NoError(t, err)
@@ -1902,7 +1902,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
_, _, err = user1c.Execute(command)
require.NoErrorf(t, err, "failed to advertise route: %s", err)
var nodes []*v1.Node
var nodes []*clientv1.Node
// Wait for route advertisements to propagate to [state.NodeStore]
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
@@ -1929,7 +1929,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
// Enable route
_, err = headscale.ApproveRoutes(
nodes[0].GetId(),
mustParseID(nodes[0].Id),
[]netip.Prefix{*pref},
)
require.NoError(t, err)
@@ -2058,7 +2058,7 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
_, _, err = user1c.Execute(command)
require.NoErrorf(t, err, "failed to advertise routes: %s", err)
var nodes []*v1.Node
var nodes []*clientv1.Node
// Wait for route advertisements to propagate (3 routes: v4 exit + v6 exit + subnet).
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
@@ -2084,7 +2084,7 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
}, integrationutil.ScaledTimeout(5*time.Second), integrationutil.FastPoll, "Verifying no routes sent to client before approval")
// Approve exit routes and subnet route.
_, err = headscale.ApproveRoutes(nodes[0].GetId(), []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6(), *route})
_, err = headscale.ApproveRoutes(mustParseID(nodes[0].Id), []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6(), *route})
require.NoError(t, err)
// Wait for route state changes to propagate.
@@ -2151,9 +2151,9 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
}, 10*time.Second, 200*time.Millisecond, "user2 traceroute should go through user1 exit node")
}
func MustFindNode(hostname string, nodes []*v1.Node) *v1.Node {
func MustFindNode(hostname string, nodes []*clientv1.Node) *clientv1.Node {
for _, node := range nodes {
if node.GetName() == hostname {
if node.Name == hostname {
return node
}
}
@@ -2447,7 +2447,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
require.NoErrorf(t, err, "failed to create scenario: %s", err)
defer scenario.ShutdownAssertNoPanics(t)
var nodes []*v1.Node
var nodes []*clientv1.Node
opts := []hsic.Option{
hsic.WithTestName("autoapprovemulti"),
@@ -2565,16 +2565,16 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
// If the approver is a tag, create a tagged PreAuthKey
// (tags-as-identity model: tags come from PreAuthKey, not --advertise-tags)
var pak *v1.PreAuthKey
var pak *clientv1.PreAuthKey
if strings.HasPrefix(tt.approver, "tag:") {
pak, err = scenario.CreatePreAuthKeyWithTags(userMap["user1"].GetId(), false, false, []string{tt.approver})
pak, err = scenario.CreatePreAuthKeyWithTags(mustParseID(userMap["user1"].Id), false, false, []string{tt.approver})
} else {
pak, err = scenario.CreatePreAuthKey(userMap["user1"].GetId(), false, false)
pak, err = scenario.CreatePreAuthKey(mustParseID(userMap["user1"].Id), false, false)
}
require.NoError(t, err)
err = routerUsernet1.Login(headscale.GetEndpoint(), pak.GetKey())
err = routerUsernet1.Login(headscale.GetEndpoint(), pak.Key)
require.NoError(t, err)
}
// extra creation end.
@@ -2646,10 +2646,10 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
routerNode := MustFindNode(routerUsernet1.Hostname(), nodes)
t.Logf("Initial auto-approval check - Router node %s: announced=%v, approved=%v, subnet=%v",
routerNode.GetName(),
routerNode.GetAvailableRoutes(),
routerNode.GetApprovedRoutes(),
routerNode.GetSubnetRoutes())
routerNode.Name,
routerNode.AvailableRoutes,
routerNode.ApprovedRoutes,
routerNode.SubnetRoutes)
requireNodeRouteCountWithCollect(c, routerNode, 1, 1, 1)
}, assertTimeout, 500*time.Millisecond, "Initial route auto-approval: Route should be approved via policy")
@@ -2740,10 +2740,10 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
routerNode := MustFindNode(routerUsernet1.Hostname(), nodes)
t.Logf("After policy removal - Router node %s: announced=%v, approved=%v, subnet=%v",
routerNode.GetName(),
routerNode.GetAvailableRoutes(),
routerNode.GetApprovedRoutes(),
routerNode.GetSubnetRoutes())
routerNode.Name,
routerNode.AvailableRoutes,
routerNode.ApprovedRoutes,
routerNode.SubnetRoutes)
requireNodeRouteCountWithCollect(c, routerNode, 1, 1, 1)
}, assertTimeout, 500*time.Millisecond, "Routes should remain approved after auto-approver removal")
@@ -2791,7 +2791,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
// Disable the route, making it unavailable since it is no longer auto-approved
_, err = headscale.ApproveRoutes(
MustFindNode(routerUsernet1.Hostname(), nodes).GetId(),
mustParseID(MustFindNode(routerUsernet1.Hostname(), nodes).Id),
[]netip.Prefix{},
)
require.NoError(t, err)
@@ -3078,10 +3078,10 @@ func requirePeerSubnetRoutesWithCollect(c *assert.CollectT, status *ipnstate.Pee
}
}
func requireNodeRouteCountWithCollect(c *assert.CollectT, node *v1.Node, announced, approved, subnet int) {
assert.Lenf(c, node.GetAvailableRoutes(), announced, "expected %q announced routes(%v) to have %d route, had %d", node.GetName(), node.GetAvailableRoutes(), announced, len(node.GetAvailableRoutes()))
assert.Lenf(c, node.GetApprovedRoutes(), approved, "expected %q approved routes(%v) to have %d route, had %d", node.GetName(), node.GetApprovedRoutes(), approved, len(node.GetApprovedRoutes()))
assert.Lenf(c, node.GetSubnetRoutes(), subnet, "expected %q subnet routes(%v) to have %d route, had %d", node.GetName(), node.GetSubnetRoutes(), subnet, len(node.GetSubnetRoutes()))
func requireNodeRouteCountWithCollect(c *assert.CollectT, node *clientv1.Node, announced, approved, subnet int) {
assert.Lenf(c, node.AvailableRoutes, announced, "expected %q announced routes(%v) to have %d route, had %d", node.Name, node.AvailableRoutes, announced, len(node.AvailableRoutes))
assert.Lenf(c, node.ApprovedRoutes, approved, "expected %q approved routes(%v) to have %d route, had %d", node.Name, node.ApprovedRoutes, approved, len(node.ApprovedRoutes))
assert.Lenf(c, node.SubnetRoutes, subnet, "expected %q subnet routes(%v) to have %d route, had %d", node.Name, node.SubnetRoutes, subnet, len(node.SubnetRoutes))
}
// TestSubnetRouteACLFiltering tests that a node can only access subnet routes
@@ -3218,7 +3218,7 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err)
var routerNode, nodeNode *v1.Node
var routerNode, nodeNode *clientv1.Node
// Wait for route advertisements to propagate to [state.NodeStore]
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
// List nodes and verify the router has 3 available routes
@@ -3240,8 +3240,8 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
// Approve all routes for the router
_, err = headscale.ApproveRoutes(
routerNode.GetId(),
util.MustStringsToPrefixes(routerNode.GetAvailableRoutes()),
mustParseID(routerNode.Id),
util.MustStringsToPrefixes(routerNode.AvailableRoutes),
)
require.NoError(t, err)
@@ -3409,10 +3409,10 @@ func TestGrantViaSubnetSteering(t *testing.T) {
defer func() { _, _, _ = routerA.Shutdown() }()
pakRouterA, err := scenario.CreatePreAuthKeyWithTags(
userMap["router"].GetId(), false, false, []string{"tag:router-a"},
mustParseID(userMap["router"].Id), false, false, []string{"tag:router-a"},
)
require.NoError(t, err)
err = routerA.Login(headscale.GetEndpoint(), pakRouterA.GetKey())
err = routerA.Login(headscale.GetEndpoint(), pakRouterA.Key)
require.NoError(t, err)
err = routerA.WaitForRunning(30 * time.Second)
require.NoError(t, err)
@@ -3426,10 +3426,10 @@ func TestGrantViaSubnetSteering(t *testing.T) {
defer func() { _, _, _ = routerB.Shutdown() }()
pakRouterB, err := scenario.CreatePreAuthKeyWithTags(
userMap["router"].GetId(), false, false, []string{"tag:router-b"},
mustParseID(userMap["router"].Id), false, false, []string{"tag:router-b"},
)
require.NoError(t, err)
err = routerB.Login(headscale.GetEndpoint(), pakRouterB.GetKey())
err = routerB.Login(headscale.GetEndpoint(), pakRouterB.Key)
require.NoError(t, err)
err = routerB.WaitForRunning(30 * time.Second)
require.NoError(t, err)
@@ -3444,10 +3444,10 @@ func TestGrantViaSubnetSteering(t *testing.T) {
defer func() { _, _, _ = clientA.Shutdown() }()
pakClientA, err := scenario.CreatePreAuthKeyWithTags(
userMap["client"].GetId(), false, false, []string{"tag:group-a"},
mustParseID(userMap["client"].Id), false, false, []string{"tag:group-a"},
)
require.NoError(t, err)
err = clientA.Login(headscale.GetEndpoint(), pakClientA.GetKey())
err = clientA.Login(headscale.GetEndpoint(), pakClientA.Key)
require.NoError(t, err)
err = clientA.WaitForRunning(30 * time.Second)
require.NoError(t, err)
@@ -3462,10 +3462,10 @@ func TestGrantViaSubnetSteering(t *testing.T) {
defer func() { _, _, _ = clientB.Shutdown() }()
pakClientB, err := scenario.CreatePreAuthKeyWithTags(
userMap["client"].GetId(), false, false, []string{"tag:group-b"},
mustParseID(userMap["client"].Id), false, false, []string{"tag:group-b"},
)
require.NoError(t, err)
err = clientB.Login(headscale.GetEndpoint(), pakClientB.GetKey())
err = clientB.Login(headscale.GetEndpoint(), pakClientB.Key)
require.NoError(t, err)
err = clientB.WaitForRunning(30 * time.Second)
require.NoError(t, err)
@@ -3497,21 +3497,21 @@ func TestGrantViaSubnetSteering(t *testing.T) {
routerANode := MustFindNode(routerA.Hostname(), nodes)
t.Logf("Router A %s: announced=%v, approved=%v, subnet=%v",
routerANode.GetName(),
routerANode.GetAvailableRoutes(),
routerANode.GetApprovedRoutes(),
routerANode.GetSubnetRoutes())
assert.Len(c, routerANode.GetAvailableRoutes(), 1, "Router A should have 1 announced route")
assert.Len(c, routerANode.GetApprovedRoutes(), 1, "Router A should have 1 approved route")
routerANode.Name,
routerANode.AvailableRoutes,
routerANode.ApprovedRoutes,
routerANode.SubnetRoutes)
assert.Len(c, routerANode.AvailableRoutes, 1, "Router A should have 1 announced route")
assert.Len(c, routerANode.ApprovedRoutes, 1, "Router A should have 1 approved route")
routerBNode := MustFindNode(routerB.Hostname(), nodes)
t.Logf("Router B %s: announced=%v, approved=%v, subnet=%v",
routerBNode.GetName(),
routerBNode.GetAvailableRoutes(),
routerBNode.GetApprovedRoutes(),
routerBNode.GetSubnetRoutes())
assert.Len(c, routerBNode.GetAvailableRoutes(), 1, "Router B should have 1 announced route")
assert.Len(c, routerBNode.GetApprovedRoutes(), 1, "Router B should have 1 approved route")
routerBNode.Name,
routerBNode.AvailableRoutes,
routerBNode.ApprovedRoutes,
routerBNode.SubnetRoutes)
assert.Len(c, routerBNode.AvailableRoutes, 1, "Router B should have 1 announced route")
assert.Len(c, routerBNode.ApprovedRoutes, 1, "Router B should have 1 approved route")
}, assertTimeout, 500*time.Millisecond, "Both routers should have auto-approved routes")
// Get webservice info.
@@ -3697,7 +3697,7 @@ func TestHASubnetRouterPingFailover(t *testing.T) {
err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err)
var nodes []*v1.Node
var nodes []*clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err = headscale.ListNodes()
@@ -3707,19 +3707,19 @@ func TestHASubnetRouterPingFailover(t *testing.T) {
// Approve routes on both routers.
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id),
[]netip.Prefix{pref},
)
require.NoError(t, err)
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id),
[]netip.Prefix{pref},
)
require.NoError(t, err)
nodeID1 := types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId())
nodeID2 := types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId())
nodeID1 := types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id))
nodeID2 := types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id))
// Wait for HA to be set up: router 1 primary, router 2 standby.
assert.EventuallyWithT(t, func(c *assert.CollectT) {
@@ -3937,7 +3937,7 @@ func TestHASubnetRouterFailoverBothOffline(t *testing.T) {
err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err)
var nodes []*v1.Node
var nodes []*clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err = headscale.ListNodes()
@@ -3947,19 +3947,19 @@ func TestHASubnetRouterFailoverBothOffline(t *testing.T) {
// Approve the route on both routers explicitly.
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id),
[]netip.Prefix{pref},
)
require.NoError(t, err)
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id),
[]netip.Prefix{pref},
)
require.NoError(t, err)
nodeID1 := types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId())
nodeID2 := types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId())
nodeID1 := types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id))
nodeID2 := types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id))
// Sanity: r1 starts as primary (lower NodeID).
assert.EventuallyWithT(t, func(c *assert.CollectT) {
@@ -4142,7 +4142,7 @@ func TestHASubnetRouterFailoverBothOfflineCablePull(t *testing.T) {
err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err)
var nodes []*v1.Node
var nodes []*clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err = headscale.ListNodes()
@@ -4151,18 +4151,18 @@ func TestHASubnetRouterFailoverBothOfflineCablePull(t *testing.T) {
}, propagationTime, 200*time.Millisecond, "nodes registered")
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id),
[]netip.Prefix{pref},
)
require.NoError(t, err)
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id),
[]netip.Prefix{pref},
)
require.NoError(t, err)
nodeID2 := types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId())
nodeID2 := types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id))
// Sanity: r1 starts as primary.
assert.EventuallyWithT(t, func(c *assert.CollectT) {
@@ -4366,7 +4366,7 @@ func TestHASubnetRouterFailoverDockerDisconnect(t *testing.T) {
err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err)
var nodes []*v1.Node
var nodes []*clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err = headscale.ListNodes()
@@ -4375,19 +4375,19 @@ func TestHASubnetRouterFailoverDockerDisconnect(t *testing.T) {
}, propagationTime, 200*time.Millisecond, "nodes registered")
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id),
[]netip.Prefix{pref},
)
require.NoError(t, err)
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id),
[]netip.Prefix{pref},
)
require.NoError(t, err)
nodeID1 := types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId())
nodeID2 := types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId())
nodeID1 := types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id))
nodeID2 := types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id))
// requirePrimary blocks until headscale reports want as the
// primary advertiser for pref.
+10 -10
View File
@@ -21,7 +21,7 @@ import (
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
"github.com/juanfont/headscale/hscontrol/capver"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/integration/dockertestutil"
@@ -520,7 +520,7 @@ func (s *Scenario) CreatePreAuthKey(
user uint64,
reusable bool,
ephemeral bool,
) (*v1.PreAuthKey, error) {
) (*clientv1.PreAuthKey, error) {
if headscale, err := s.Headscale(); err == nil { //nolint:noinlineerr
key, err := headscale.CreateAuthKey(user, reusable, ephemeral)
if err != nil {
@@ -535,7 +535,7 @@ func (s *Scenario) CreatePreAuthKey(
// CreatePreAuthKeyWithOptions creates a "pre authorised key" with the specified options
// to be created in the Headscale instance on behalf of the [Scenario].
func (s *Scenario) CreatePreAuthKeyWithOptions(opts hsic.AuthKeyOptions) (*v1.PreAuthKey, error) {
func (s *Scenario) CreatePreAuthKeyWithOptions(opts hsic.AuthKeyOptions) (*clientv1.PreAuthKey, error) {
headscale, err := s.Headscale()
if err != nil {
return nil, fmt.Errorf("creating preauth key with options: %w", errNoHeadscaleAvailable)
@@ -556,7 +556,7 @@ func (s *Scenario) CreatePreAuthKeyWithTags(
reusable bool,
ephemeral bool,
tags []string,
) (*v1.PreAuthKey, error) {
) (*clientv1.PreAuthKey, error) {
headscale, err := s.Headscale()
if err != nil {
return nil, fmt.Errorf("creating preauth key with tags: %w", errNoHeadscaleAvailable)
@@ -572,7 +572,7 @@ func (s *Scenario) CreatePreAuthKeyWithTags(
// CreateUser creates a [User] to be created in the
// Headscale instance on behalf of the [Scenario].
func (s *Scenario) CreateUser(user string) (*v1.User, error) {
func (s *Scenario) CreateUser(user string) (*clientv1.User, error) {
if headscale, err := s.Headscale(); err == nil { //nolint:noinlineerr
u, err := headscale.CreateUser(user)
if err != nil {
@@ -925,7 +925,7 @@ func (s *Scenario) createHeadscaleEnvWithTags(
}
for _, user := range s.spec.Users {
var u *v1.User
var u *clientv1.User
if s.spec.OIDCSkipUserCreation {
// Only register locally — OIDC login will create the headscale user.
@@ -964,18 +964,18 @@ func (s *Scenario) createHeadscaleEnvWithTags(
}
} else {
// Use tagged PreAuthKey if tags are provided (tags-as-identity model)
var key *v1.PreAuthKey
var key *clientv1.PreAuthKey
if len(preAuthKeyTags) > 0 {
key, err = s.CreatePreAuthKeyWithTags(u.GetId(), true, false, preAuthKeyTags)
key, err = s.CreatePreAuthKeyWithTags(mustParseID(u.Id), true, false, preAuthKeyTags)
} else {
key, err = s.CreatePreAuthKey(u.GetId(), true, false)
key, err = s.CreatePreAuthKey(mustParseID(u.Id), true, false)
}
if err != nil {
return err
}
err = s.RunTailscaleUp(user, headscale.GetEndpoint(), key.GetKey())
err = s.RunTailscaleUp(user, headscale.GetEndpoint(), key.Key)
if err != nil {
return err
}
+1 -1
View File
@@ -137,7 +137,7 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
err = scenario.RunTailscaleUp(
user,
headscale.GetEndpoint(),
key.GetKey(),
key.Key,
)
if err != nil {
t.Fatalf("failed to login: %s", err)
+134 -134
View File
@@ -5,7 +5,7 @@ import (
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/juanfont/headscale/integration/hsic"
@@ -67,19 +67,19 @@ func tagsEqual(actual, expected []string) bool {
}
// assertNodeHasTagsWithCollect asserts that a node has exactly the expected tags (order-independent).
func assertNodeHasTagsWithCollect(c *assert.CollectT, node *v1.Node, expectedTags []string) {
actualTags := node.GetTags()
func assertNodeHasTagsWithCollect(c *assert.CollectT, node *clientv1.Node, expectedTags []string) {
actualTags := node.Tags
sortedActual := append([]string{}, actualTags...)
sortedExpected := append([]string{}, expectedTags...)
sort.Strings(sortedActual)
sort.Strings(sortedExpected)
assert.Equal(c, sortedExpected, sortedActual, "Node %s tags mismatch", node.GetName())
assert.Equal(c, sortedExpected, sortedActual, "Node %s tags mismatch", node.Name)
}
// assertNodeHasNoTagsWithCollect asserts that a node has no tags.
func assertNodeHasNoTagsWithCollect(c *assert.CollectT, node *v1.Node) {
assert.Empty(c, node.GetTags(), "Node %s should have no tags, but has: %v", node.GetName(), node.GetTags())
func assertNodeHasNoTagsWithCollect(c *assert.CollectT, node *clientv1.Node) {
assert.Empty(c, node.Tags, "Node %s should have no tags, but has: %v", node.Name, node.Tags)
}
// assertNodeSelfHasTagsWithCollect asserts that a client's self view has exactly the expected tags.
@@ -148,12 +148,12 @@ func TestTagsAuthKeyWithTagRequestDifferentTag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create a tagged PreAuthKey with tag:valid-owned
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
require.NoError(t, err)
t.Logf("Created tagged PreAuthKey with tags: %v", authKey.GetAclTags())
t.Logf("Created tagged PreAuthKey with tags: %v", authKey.AclTags)
// Create a tailscale client that will try to use --advertise-tags with a DIFFERENT tag
client, err := scenario.CreateTailscaleNode(
@@ -164,7 +164,7 @@ func TestTagsAuthKeyWithTagRequestDifferentTag(t *testing.T) {
require.NoError(t, err)
// Login should fail because the advertised tags don't match the auth key's tags
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
// Document actual behavior - we expect this to fail
if err != nil {
@@ -180,7 +180,7 @@ func TestTagsAuthKeyWithTagRequestDifferentTag(t *testing.T) {
assert.NoError(c, err)
if len(nodes) == 1 {
t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].GetTags())
t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].Tags)
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "checking node state")
@@ -222,12 +222,12 @@ func TestTagsAuthKeyWithTagNoAdvertiseFlag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create a tagged PreAuthKey with tag:valid-owned
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
require.NoError(t, err)
t.Logf("Created tagged PreAuthKey with tags: %v", authKey.GetAclTags())
t.Logf("Created tagged PreAuthKey with tags: %v", authKey.AclTags)
// Create a tailscale client WITHOUT --advertise-tags
client, err := scenario.CreateTailscaleNode(
@@ -238,7 +238,7 @@ func TestTagsAuthKeyWithTagNoAdvertiseFlag(t *testing.T) {
require.NoError(t, err)
// Login with the tagged PreAuthKey
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
// Wait for node to be registered and verify it has the key's tags
@@ -249,7 +249,7 @@ func TestTagsAuthKeyWithTagNoAdvertiseFlag(t *testing.T) {
if len(nodes) == 1 {
node := nodes[0]
t.Logf("Node registered with tags: %v", node.GetTags())
t.Logf("Node registered with tags: %v", node.Tags)
assertNodeHasTagsWithCollect(c, node, []string{"tag:valid-owned"})
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "verifying node inherited tags from auth key")
@@ -294,7 +294,7 @@ func TestTagsAuthKeyWithTagCannotAddViaCLI(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create a tagged PreAuthKey with tag:valid-owned
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
@@ -308,7 +308,7 @@ func TestTagsAuthKeyWithTagCannotAddViaCLI(t *testing.T) {
require.NoError(t, err)
// Initial login
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
// Wait for initial registration
@@ -328,7 +328,7 @@ func TestTagsAuthKeyWithTagCannotAddViaCLI(t *testing.T) {
command := []string{
"tailscale", "up",
"--login-server=" + headscale.GetEndpoint(),
"--authkey=" + authKey.GetKey(),
"--authkey=" + authKey.Key,
"--advertise-tags=tag:valid-owned,tag:second",
}
_, stderr, err := client.Execute(command)
@@ -346,10 +346,10 @@ func TestTagsAuthKeyWithTagCannotAddViaCLI(t *testing.T) {
if len(nodes) == 1 {
// If still only has original tag, that's the expected behavior
if tagsEqual(nodes[0].GetTags(), []string{"tag:valid-owned"}) {
t.Logf("Test 2.3 PASS: Tags unchanged after CLI attempt: %v", nodes[0].GetTags())
if tagsEqual(nodes[0].Tags, []string{"tag:valid-owned"}) {
t.Logf("Test 2.3 PASS: Tags unchanged after CLI attempt: %v", nodes[0].Tags)
} else {
t.Logf("Test 2.3 FAIL: Tags changed unexpectedly to: %v", nodes[0].GetTags())
t.Logf("Test 2.3 FAIL: Tags changed unexpectedly to: %v", nodes[0].Tags)
assert.Fail(c, "Tags should not have changed")
}
}
@@ -394,7 +394,7 @@ func TestTagsAuthKeyWithTagCannotChangeViaCLI(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create a tagged PreAuthKey with tag:valid-owned
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
@@ -408,7 +408,7 @@ func TestTagsAuthKeyWithTagCannotChangeViaCLI(t *testing.T) {
require.NoError(t, err)
// Initial login
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
// Wait for initial registration
@@ -424,7 +424,7 @@ func TestTagsAuthKeyWithTagCannotChangeViaCLI(t *testing.T) {
command := []string{
"tailscale", "up",
"--login-server=" + headscale.GetEndpoint(),
"--authkey=" + authKey.GetKey(),
"--authkey=" + authKey.Key,
"--advertise-tags=tag:second",
}
_, stderr, err := client.Execute(command)
@@ -441,10 +441,10 @@ func TestTagsAuthKeyWithTagCannotChangeViaCLI(t *testing.T) {
assert.NoError(c, err)
if len(nodes) == 1 {
if tagsEqual(nodes[0].GetTags(), []string{"tag:valid-owned"}) {
t.Logf("Test 2.4 PASS: Tags unchanged: %v", nodes[0].GetTags())
if tagsEqual(nodes[0].Tags, []string{"tag:valid-owned"}) {
t.Logf("Test 2.4 PASS: Tags unchanged: %v", nodes[0].Tags)
} else {
t.Logf("Test 2.4 FAIL: Tags changed unexpectedly to: %v", nodes[0].GetTags())
t.Logf("Test 2.4 FAIL: Tags changed unexpectedly to: %v", nodes[0].Tags)
assert.Fail(c, "Tags should not have changed")
}
}
@@ -490,7 +490,7 @@ func TestTagsAuthKeyWithTagAdminOverrideReauthPreserves(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create a tagged PreAuthKey with tag:valid-owned
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, true, false, []string{"tag:valid-owned"})
@@ -504,7 +504,7 @@ func TestTagsAuthKeyWithTagAdminOverrideReauthPreserves(t *testing.T) {
require.NoError(t, err)
// Initial login
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
// Wait for initial registration and get node ID
@@ -516,7 +516,7 @@ func TestTagsAuthKeyWithTagAdminOverrideReauthPreserves(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = mustParseID(nodes[0].Id)
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"})
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration")
@@ -533,7 +533,7 @@ func TestTagsAuthKeyWithTagAdminOverrideReauthPreserves(t *testing.T) {
assert.NoError(c, err)
if len(nodes) == 1 {
t.Logf("After admin assignment, server tags are: %v", nodes[0].GetTags())
t.Logf("After admin assignment, server tags are: %v", nodes[0].Tags)
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:second"})
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying admin tag assignment on server")
@@ -549,7 +549,7 @@ func TestTagsAuthKeyWithTagAdminOverrideReauthPreserves(t *testing.T) {
command := []string{
"tailscale", "up",
"--login-server=" + headscale.GetEndpoint(),
"--authkey=" + authKey.GetKey(),
"--authkey=" + authKey.Key,
"--force-reauth",
}
//nolint:errcheck // Intentionally ignoring error - we check results below
@@ -564,7 +564,7 @@ func TestTagsAuthKeyWithTagAdminOverrideReauthPreserves(t *testing.T) {
if len(nodes) >= 1 {
// Find the most recently updated node (in case a new one was created)
node := nodes[len(nodes)-1]
t.Logf("After reauth, server tags are: %v", node.GetTags())
t.Logf("After reauth, server tags are: %v", node.Tags)
// Expected: admin-assigned tags are preserved through reauth
assertNodeHasTagsWithCollect(c, node, []string{"tag:second"})
@@ -617,7 +617,7 @@ func TestTagsAuthKeyWithTagCLICannotModifyAdminTags(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create a tagged PreAuthKey with tag:valid-owned
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, true, false, []string{"tag:valid-owned"})
@@ -631,7 +631,7 @@ func TestTagsAuthKeyWithTagCLICannotModifyAdminTags(t *testing.T) {
require.NoError(t, err)
// Initial login
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
// Wait for initial registration and get node ID
@@ -643,7 +643,7 @@ func TestTagsAuthKeyWithTagCLICannotModifyAdminTags(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = mustParseID(nodes[0].Id)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration")
@@ -672,7 +672,7 @@ func TestTagsAuthKeyWithTagCLICannotModifyAdminTags(t *testing.T) {
command := []string{
"tailscale", "up",
"--login-server=" + headscale.GetEndpoint(),
"--authkey=" + authKey.GetKey(),
"--authkey=" + authKey.Key,
"--advertise-tags=tag:valid-owned",
}
_, stderr, err := client.Execute(command)
@@ -686,7 +686,7 @@ func TestTagsAuthKeyWithTagCLICannotModifyAdminTags(t *testing.T) {
assert.Len(c, nodes, 1, "Should have exactly 1 node")
if len(nodes) == 1 {
t.Logf("After CLI attempt, server tags are: %v", nodes[0].GetTags())
t.Logf("After CLI attempt, server tags are: %v", nodes[0].Tags)
// Expected: tags should remain unchanged (admin wins)
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned", "tag:second"})
@@ -739,7 +739,7 @@ func TestTagsAuthKeyWithoutTagCannotRequestTags(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create an auth key WITHOUT tags
authKey, err := scenario.CreatePreAuthKey(userID, false, false)
@@ -755,7 +755,7 @@ func TestTagsAuthKeyWithoutTagCannotRequestTags(t *testing.T) {
require.NoError(t, err)
// Login should fail because the auth key has no tags
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
if err != nil {
t.Logf("Test 3.1 PASS: Registration correctly rejected: %v", err)
assert.ErrorContains(t, err, "requested tags")
@@ -768,7 +768,7 @@ func TestTagsAuthKeyWithoutTagCannotRequestTags(t *testing.T) {
assert.NoError(c, err)
if len(nodes) == 1 {
t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].GetTags())
t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].Tags)
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "checking node state")
@@ -810,7 +810,7 @@ func TestTagsAuthKeyWithoutTagRegisterNoTags(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create an auth key WITHOUT tags
authKey, err := scenario.CreatePreAuthKey(userID, false, false)
@@ -824,7 +824,7 @@ func TestTagsAuthKeyWithoutTagRegisterNoTags(t *testing.T) {
require.NoError(t, err)
// Login should succeed
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
// Verify node has no tags
@@ -834,7 +834,7 @@ func TestTagsAuthKeyWithoutTagRegisterNoTags(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
t.Logf("Node registered with tags: %v", nodes[0].GetTags())
t.Logf("Node registered with tags: %v", nodes[0].Tags)
assertNodeHasNoTagsWithCollect(c, nodes[0])
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "verifying node has no tags")
@@ -879,7 +879,7 @@ func TestTagsAuthKeyWithoutTagCannotAddViaCLI(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create an auth key WITHOUT tags
authKey, err := scenario.CreatePreAuthKey(userID, true, false)
@@ -893,7 +893,7 @@ func TestTagsAuthKeyWithoutTagCannotAddViaCLI(t *testing.T) {
require.NoError(t, err)
// Initial login
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
// Wait for initial registration
@@ -913,7 +913,7 @@ func TestTagsAuthKeyWithoutTagCannotAddViaCLI(t *testing.T) {
command := []string{
"tailscale", "up",
"--login-server=" + headscale.GetEndpoint(),
"--authkey=" + authKey.GetKey(),
"--authkey=" + authKey.Key,
"--advertise-tags=tag:valid-owned",
}
_, stderr, err := client.Execute(command)
@@ -929,10 +929,10 @@ func TestTagsAuthKeyWithoutTagCannotAddViaCLI(t *testing.T) {
assert.NoError(c, err)
if len(nodes) == 1 {
if len(nodes[0].GetTags()) == 0 {
if len(nodes[0].Tags) == 0 {
t.Logf("Test 3.3 PASS: Tags still empty after CLI attempt")
} else {
t.Logf("Test 3.3 FAIL: Tags changed to: %v", nodes[0].GetTags())
t.Logf("Test 3.3 FAIL: Tags changed to: %v", nodes[0].Tags)
assert.Fail(c, "Tags should not have changed")
}
}
@@ -978,7 +978,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithReset(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create an auth key WITHOUT tags
authKey, err := scenario.CreatePreAuthKey(userID, true, false)
@@ -992,7 +992,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithReset(t *testing.T) {
require.NoError(t, err)
// Initial login
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
// Wait for initial registration and get node ID
@@ -1004,7 +1004,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithReset(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = mustParseID(nodes[0].Id)
assertNodeHasNoTagsWithCollect(c, nodes[0])
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration")
@@ -1034,7 +1034,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithReset(t *testing.T) {
command := []string{
"tailscale", "up",
"--login-server=" + headscale.GetEndpoint(),
"--authkey=" + authKey.GetKey(),
"--authkey=" + authKey.Key,
"--reset",
}
_, stderr, err := client.Execute(command)
@@ -1047,7 +1047,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithReset(t *testing.T) {
assert.Len(c, nodes, 1, "Should have exactly 1 node")
if len(nodes) == 1 {
t.Logf("After --reset, server tags are: %v", nodes[0].GetTags())
t.Logf("After --reset, server tags are: %v", nodes[0].Tags)
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"})
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "admin tags should be preserved after --reset on server")
@@ -1098,7 +1098,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithEmptyAdvertise(t *testing.T)
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create an auth key WITHOUT tags
authKey, err := scenario.CreatePreAuthKey(userID, true, false)
@@ -1112,7 +1112,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithEmptyAdvertise(t *testing.T)
require.NoError(t, err)
// Initial login
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
// Wait for initial registration and get node ID
@@ -1124,7 +1124,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithEmptyAdvertise(t *testing.T)
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = mustParseID(nodes[0].Id)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration")
@@ -1153,7 +1153,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithEmptyAdvertise(t *testing.T)
command := []string{
"tailscale", "up",
"--login-server=" + headscale.GetEndpoint(),
"--authkey=" + authKey.GetKey(),
"--authkey=" + authKey.Key,
"--advertise-tags=",
}
_, stderr, err := client.Execute(command)
@@ -1166,7 +1166,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithEmptyAdvertise(t *testing.T)
assert.Len(c, nodes, 1, "Should have exactly 1 node")
if len(nodes) == 1 {
t.Logf("After empty --advertise-tags, server tags are: %v", nodes[0].GetTags())
t.Logf("After empty --advertise-tags, server tags are: %v", nodes[0].Tags)
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"})
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "admin tags should be preserved after empty --advertise-tags on server")
@@ -1217,7 +1217,7 @@ func TestTagsAuthKeyWithoutTagCLICannotReduceAdminMultiTag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create an auth key WITHOUT tags
authKey, err := scenario.CreatePreAuthKey(userID, true, false)
@@ -1231,7 +1231,7 @@ func TestTagsAuthKeyWithoutTagCLICannotReduceAdminMultiTag(t *testing.T) {
require.NoError(t, err)
// Initial login
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
// Wait for initial registration and get node ID
@@ -1243,7 +1243,7 @@ func TestTagsAuthKeyWithoutTagCLICannotReduceAdminMultiTag(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = mustParseID(nodes[0].Id)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration")
@@ -1272,7 +1272,7 @@ func TestTagsAuthKeyWithoutTagCLICannotReduceAdminMultiTag(t *testing.T) {
command := []string{
"tailscale", "up",
"--login-server=" + headscale.GetEndpoint(),
"--authkey=" + authKey.GetKey(),
"--authkey=" + authKey.Key,
"--advertise-tags=tag:valid-owned",
}
_, stderr, err := client.Execute(command)
@@ -1285,7 +1285,7 @@ func TestTagsAuthKeyWithoutTagCLICannotReduceAdminMultiTag(t *testing.T) {
assert.Len(c, nodes, 1, "Should have exactly 1 node")
if len(nodes) == 1 {
t.Logf("After CLI reduce attempt, server tags are: %v", nodes[0].GetTags())
t.Logf("After CLI reduce attempt, server tags are: %v", nodes[0].Tags)
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned", "tag:second"})
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "admin tags should be preserved after CLI reduce attempt on server")
@@ -1366,7 +1366,7 @@ func TestTagsUserLoginOwnedTagAtRegistration(t *testing.T) {
assert.Len(c, nodes, 1, "Should have exactly 1 node")
if len(nodes) == 1 {
t.Logf("Node registered with tags: %v", nodes[0].GetTags())
t.Logf("Node registered with tags: %v", nodes[0].Tags)
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"})
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "verifying node has advertised tag")
@@ -1438,9 +1438,9 @@ func TestTagsUserLoginNonExistentTagAtRegistration(t *testing.T) {
t.Logf("Test 1.2 PASS: Registration rejected - no nodes registered")
} else {
// If a node was registered, it should NOT have the non-existent tag
assert.NotContains(c, nodes[0].GetTags(), "tag:nonexistent",
assert.NotContains(c, nodes[0].Tags, "tag:nonexistent",
"Non-existent tag should not be applied to node")
t.Logf("Test 1.2: Node registered with tags: %v (non-existent tag correctly rejected)", nodes[0].GetTags())
t.Logf("Test 1.2: Node registered with tags: %v (non-existent tag correctly rejected)", nodes[0].Tags)
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "checking node registration result")
}
@@ -1506,9 +1506,9 @@ func TestTagsUserLoginUnownedTagAtRegistration(t *testing.T) {
t.Logf("Test 1.3 PASS: Registration rejected - no nodes registered")
} else {
// If a node was registered, it should NOT have the unowned tag
assert.NotContains(c, nodes[0].GetTags(), "tag:valid-unowned",
assert.NotContains(c, nodes[0].Tags, "tag:valid-unowned",
"Unowned tag should not be applied to node (tag:valid-unowned is owned by other-user)")
t.Logf("Test 1.3: Node registered with tags: %v (unowned tag correctly rejected)", nodes[0].GetTags())
t.Logf("Test 1.3: Node registered with tags: %v (unowned tag correctly rejected)", nodes[0].Tags)
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "checking node registration result")
}
@@ -1572,7 +1572,7 @@ func TestTagsUserLoginAddTagViaCLIReauth(t *testing.T) {
assert.NoError(c, err)
if len(nodes) == 1 {
t.Logf("Initial tags: %v", nodes[0].GetTags())
t.Logf("Initial tags: %v", nodes[0].Tags)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "checking initial tags")
@@ -1593,12 +1593,12 @@ func TestTagsUserLoginAddTagViaCLIReauth(t *testing.T) {
assert.NoError(c, err)
if len(nodes) >= 1 {
t.Logf("Test 1.4: After CLI, tags are: %v", nodes[0].GetTags())
t.Logf("Test 1.4: After CLI, tags are: %v", nodes[0].Tags)
if tagsEqual(nodes[0].GetTags(), []string{"tag:valid-owned", "tag:second"}) {
if tagsEqual(nodes[0].Tags, []string{"tag:valid-owned", "tag:second"}) {
t.Logf("Test 1.4 PASS: Both tags present after reauth")
} else {
t.Logf("Test 1.4: Tags are %v (may require manual reauth completion)", nodes[0].GetTags())
t.Logf("Test 1.4: Tags are %v (may require manual reauth completion)", nodes[0].Tags)
}
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "checking tags after CLI")
@@ -1663,7 +1663,7 @@ func TestTagsUserLoginRemoveTagViaCLIReauth(t *testing.T) {
assert.NoError(c, err)
if len(nodes) == 1 {
t.Logf("Initial tags: %v", nodes[0].GetTags())
t.Logf("Initial tags: %v", nodes[0].Tags)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "checking initial tags")
@@ -1684,9 +1684,9 @@ func TestTagsUserLoginRemoveTagViaCLIReauth(t *testing.T) {
assert.NoError(c, err)
if len(nodes) >= 1 {
t.Logf("Test 1.5: After CLI, tags are: %v", nodes[0].GetTags())
t.Logf("Test 1.5: After CLI, tags are: %v", nodes[0].Tags)
if tagsEqual(nodes[0].GetTags(), []string{"tag:valid-owned"}) {
if tagsEqual(nodes[0].Tags, []string{"tag:valid-owned"}) {
t.Logf("Test 1.5 PASS: Only one tag after removal")
}
}
@@ -1757,8 +1757,8 @@ func TestTagsUserLoginCLINoOpAfterAdminAssignment(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
t.Logf("Step 1: Node %d registered with tags: %v", nodeID, nodes[0].GetTags())
nodeID = mustParseID(nodes[0].Id)
t.Logf("Step 1: Node %d registered with tags: %v", nodeID, nodes[0].Tags)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration")
@@ -1772,7 +1772,7 @@ func TestTagsUserLoginCLINoOpAfterAdminAssignment(t *testing.T) {
assert.NoError(c, err)
if len(nodes) == 1 {
t.Logf("Step 2: After admin assignment, server tags: %v", nodes[0].GetTags())
t.Logf("Step 2: After admin assignment, server tags: %v", nodes[0].Tags)
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:second"})
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying admin assignment on server")
@@ -1798,7 +1798,7 @@ func TestTagsUserLoginCLINoOpAfterAdminAssignment(t *testing.T) {
assert.Len(c, nodes, 1, "Should have exactly 1 node")
if len(nodes) == 1 {
t.Logf("Step 3: After CLI, server tags are: %v", nodes[0].GetTags())
t.Logf("Step 3: After CLI, server tags are: %v", nodes[0].Tags)
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:second"})
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "admin tags should be preserved - CLI advertise-tags should be no-op on server")
@@ -1874,7 +1874,7 @@ func TestTagsUserLoginCLICannotRemoveAdminTags(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = mustParseID(nodes[0].Id)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration")
@@ -1888,7 +1888,7 @@ func TestTagsUserLoginCLICannotRemoveAdminTags(t *testing.T) {
assert.NoError(c, err)
if len(nodes) == 1 {
t.Logf("After admin assignment, server tags: %v", nodes[0].GetTags())
t.Logf("After admin assignment, server tags: %v", nodes[0].Tags)
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned", "tag:second"})
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying admin assignment on server")
@@ -1914,7 +1914,7 @@ func TestTagsUserLoginCLICannotRemoveAdminTags(t *testing.T) {
assert.Len(c, nodes, 1, "Should have exactly 1 node")
if len(nodes) == 1 {
t.Logf("Test 1.7: After CLI, server tags are: %v", nodes[0].GetTags())
t.Logf("Test 1.7: After CLI, server tags are: %v", nodes[0].Tags)
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned", "tag:second"})
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "admin tags should be preserved - CLI cannot remove them on server")
@@ -1965,12 +1965,12 @@ func TestTagsAuthKeyWithTagRequestNonExistentTag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create a tagged PreAuthKey with tag:valid-owned
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
require.NoError(t, err)
t.Logf("Created tagged PreAuthKey with tags: %v", authKey.GetAclTags())
t.Logf("Created tagged PreAuthKey with tags: %v", authKey.AclTags)
// Create a tailscale client that will try to use --advertise-tags with a NON-EXISTENT tag
client, err := scenario.CreateTailscaleNode(
@@ -1981,7 +1981,7 @@ func TestTagsAuthKeyWithTagRequestNonExistentTag(t *testing.T) {
require.NoError(t, err)
// Login should fail because ANY advertise-tags is rejected for PreAuthKey registrations
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
if err != nil {
t.Logf("Test 2.7 PASS: Registration correctly rejected with error: %v", err)
assert.ErrorContains(t, err, "requested tags")
@@ -1993,7 +1993,7 @@ func TestTagsAuthKeyWithTagRequestNonExistentTag(t *testing.T) {
assert.NoError(c, err)
if len(nodes) == 1 {
t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].GetTags())
t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].Tags)
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "checking node state")
@@ -2035,12 +2035,12 @@ func TestTagsAuthKeyWithTagRequestUnownedTag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create a tagged PreAuthKey with tag:valid-owned
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
require.NoError(t, err)
t.Logf("Created tagged PreAuthKey with tags: %v", authKey.GetAclTags())
t.Logf("Created tagged PreAuthKey with tags: %v", authKey.AclTags)
// Create a tailscale client that will try to use --advertise-tags with an UNOWNED tag
client, err := scenario.CreateTailscaleNode(
@@ -2051,7 +2051,7 @@ func TestTagsAuthKeyWithTagRequestUnownedTag(t *testing.T) {
require.NoError(t, err)
// Login should fail because ANY advertise-tags is rejected for PreAuthKey registrations
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
if err != nil {
t.Logf("Test 2.8 PASS: Registration correctly rejected with error: %v", err)
assert.ErrorContains(t, err, "requested tags")
@@ -2063,7 +2063,7 @@ func TestTagsAuthKeyWithTagRequestUnownedTag(t *testing.T) {
assert.NoError(c, err)
if len(nodes) == 1 {
t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].GetTags())
t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].Tags)
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "checking node state")
@@ -2109,7 +2109,7 @@ func TestTagsAuthKeyWithoutTagRequestNonExistentTag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create an auth key WITHOUT tags
authKey, err := scenario.CreatePreAuthKey(userID, false, false)
@@ -2125,7 +2125,7 @@ func TestTagsAuthKeyWithoutTagRequestNonExistentTag(t *testing.T) {
require.NoError(t, err)
// Login should fail because ANY advertise-tags is rejected for PreAuthKey registrations
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
if err != nil {
t.Logf("Test 3.7 PASS: Registration correctly rejected: %v", err)
assert.ErrorContains(t, err, "requested tags")
@@ -2137,7 +2137,7 @@ func TestTagsAuthKeyWithoutTagRequestNonExistentTag(t *testing.T) {
assert.NoError(c, err)
if len(nodes) == 1 {
t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].GetTags())
t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].Tags)
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "checking node state")
@@ -2179,7 +2179,7 @@ func TestTagsAuthKeyWithoutTagRequestUnownedTag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create an auth key WITHOUT tags
authKey, err := scenario.CreatePreAuthKey(userID, false, false)
@@ -2195,7 +2195,7 @@ func TestTagsAuthKeyWithoutTagRequestUnownedTag(t *testing.T) {
require.NoError(t, err)
// Login should fail because ANY advertise-tags is rejected for PreAuthKey registrations
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
if err != nil {
t.Logf("Test 3.8 PASS: Registration correctly rejected: %v", err)
assert.ErrorContains(t, err, "requested tags")
@@ -2207,7 +2207,7 @@ func TestTagsAuthKeyWithoutTagRequestUnownedTag(t *testing.T) {
assert.NoError(c, err)
if len(nodes) == 1 {
t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].GetTags())
t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].Tags)
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "checking node state")
@@ -2253,7 +2253,7 @@ func TestTagsAdminAPICannotSetNonExistentTag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create a tagged PreAuthKey to register a node
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
@@ -2266,7 +2266,7 @@ func TestTagsAdminAPICannotSetNonExistentTag(t *testing.T) {
)
require.NoError(t, err)
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
// Wait for registration and get node ID
@@ -2278,8 +2278,8 @@ func TestTagsAdminAPICannotSetNonExistentTag(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
t.Logf("Node %d registered with tags: %v", nodeID, nodes[0].GetTags())
nodeID = mustParseID(nodes[0].Id)
t.Logf("Node %d registered with tags: %v", nodeID, nodes[0].Tags)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for registration")
@@ -2325,7 +2325,7 @@ func TestTagsAdminAPICanSetUnownedTag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create a tagged PreAuthKey to register a node
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
@@ -2338,7 +2338,7 @@ func TestTagsAdminAPICanSetUnownedTag(t *testing.T) {
)
require.NoError(t, err)
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
// Wait for registration and get node ID
@@ -2350,8 +2350,8 @@ func TestTagsAdminAPICanSetUnownedTag(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
t.Logf("Node %d registered with tags: %v", nodeID, nodes[0].GetTags())
nodeID = mustParseID(nodes[0].Id)
t.Logf("Node %d registered with tags: %v", nodeID, nodes[0].Tags)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for registration")
@@ -2413,7 +2413,7 @@ func TestTagsAdminAPICannotRemoveAllTags(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create a tagged PreAuthKey to register a node
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
@@ -2426,7 +2426,7 @@ func TestTagsAdminAPICannotRemoveAllTags(t *testing.T) {
)
require.NoError(t, err)
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
// Wait for registration and get node ID
@@ -2438,8 +2438,8 @@ func TestTagsAdminAPICannotRemoveAllTags(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
t.Logf("Node %d registered with tags: %v", nodeID, nodes[0].GetTags())
nodeID = mustParseID(nodes[0].Id)
t.Logf("Node %d registered with tags: %v", nodeID, nodes[0].Tags)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for registration")
@@ -2560,7 +2560,7 @@ func TestTagsIssue2978ReproTagReplacement(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = mustParseID(nodes[0].Id)
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"})
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration")
@@ -2735,7 +2735,7 @@ func TestTagsAdminAPICannotSetInvalidFormat(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := mustParseID(userMap[tagTestUser].Id)
// Create a tagged PreAuthKey to register a node
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
@@ -2748,7 +2748,7 @@ func TestTagsAdminAPICannotSetInvalidFormat(t *testing.T) {
)
require.NoError(t, err)
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
// Wait for registration and get node ID
@@ -2760,8 +2760,8 @@ func TestTagsAdminAPICannotSetInvalidFormat(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
t.Logf("Node %d registered with tags: %v", nodeID, nodes[0].GetTags())
nodeID = mustParseID(nodes[0].Id)
t.Logf("Node %d registered with tags: %v", nodeID, nodes[0].Tags)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for registration")
@@ -2855,7 +2855,7 @@ func TestTagsUserLoginReauthWithEmptyTagsRemovesAllTags(t *testing.T) {
require.NoError(t, err)
// Verify initial tags
var initialNodeID uint64
var initialNodeID string
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err := headscale.ListNodes()
@@ -2864,9 +2864,9 @@ func TestTagsUserLoginReauthWithEmptyTagsRemovesAllTags(t *testing.T) {
if len(nodes) == 1 {
node := nodes[0]
initialNodeID = node.GetId()
t.Logf("Initial state - Node ID: %d, Tags: %v, User: %s",
node.GetId(), node.GetTags(), node.GetUser().GetName())
initialNodeID = node.Id
t.Logf("Initial state - Node ID: %s, Tags: %v, User: %s",
node.Id, node.Tags, node.User.Name)
// Verify node has the expected tags
assertNodeHasTagsWithCollect(c, node, []string{"tag:valid-owned", "tag:second"})
@@ -2926,25 +2926,25 @@ func TestTagsUserLoginReauthWithEmptyTagsRemovesAllTags(t *testing.T) {
if len(nodes) >= 1 {
node := nodes[0]
t.Logf("After reauth - Node ID: %d, Tags: %v, User: %s",
node.GetId(), node.GetTags(), node.GetUser().GetName())
t.Logf("After reauth - Node ID: %s, Tags: %v, User: %s",
node.Id, node.Tags, node.User.Name)
// Assert: Node should have NO tags
assertNodeHasNoTagsWithCollect(c, node)
// Assert: Node should be owned by the user (not tagged-devices)
assert.Equal(c, tagTestUser, node.GetUser().GetName(),
assert.Equal(c, tagTestUser, node.User.Name,
"Node ownership should return to user %s after untagging", tagTestUser)
// Verify the node ID is still the same (not a new registration)
assert.Equal(c, initialNodeID, node.GetId(),
assert.Equal(c, initialNodeID, node.Id,
"Node ID should remain the same after reauth")
if len(node.GetTags()) == 0 && node.GetUser().GetName() == tagTestUser {
if len(node.Tags) == 0 && node.User.Name == tagTestUser {
t.Logf("Test #2979 (%s) PASS: Node successfully untagged and ownership returned to user", tc.name)
} else {
t.Logf("Test #2979 (%s) FAIL: Expected no tags and user=%s, got tags=%v user=%s",
tc.name, tagTestUser, node.GetTags(), node.GetUser().GetName())
tc.name, tagTestUser, node.Tags, node.User.Name)
}
}
}, integrationutil.HAConvergeTimeout, 1*time.Second, "verifying tags removed and ownership returned")
@@ -2994,7 +2994,7 @@ func TestTagsAuthKeyWithoutUserInheritsTags(t *testing.T) {
Tags: []string{"tag:valid-owned"},
})
require.NoError(t, err)
t.Logf("Created tags-only PreAuthKey with tags: %v", authKey.GetAclTags())
t.Logf("Created tags-only PreAuthKey with tags: %v", authKey.AclTags)
// Create a tailscale client WITHOUT --advertise-tags
client, err := scenario.CreateTailscaleNode(
@@ -3005,7 +3005,7 @@ func TestTagsAuthKeyWithoutUserInheritsTags(t *testing.T) {
require.NoError(t, err)
// Login with the tags-only auth key
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
// Wait for node to be registered and verify it has the key's tags
@@ -3017,7 +3017,7 @@ func TestTagsAuthKeyWithoutUserInheritsTags(t *testing.T) {
if len(nodes) == 1 {
node := nodes[0]
t.Logf("Node registered with tags: %v", node.GetTags())
t.Logf("Node registered with tags: %v", node.Tags)
assertNodeHasTagsWithCollect(c, node, []string{"tag:valid-owned"})
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "verifying node inherited tags from auth key")
@@ -3065,7 +3065,7 @@ func TestTagsAuthKeyWithoutUserRejectsAdvertisedTags(t *testing.T) {
Tags: []string{"tag:valid-owned"},
})
require.NoError(t, err)
t.Logf("Created tags-only PreAuthKey with tags: %v", authKey.GetAclTags())
t.Logf("Created tags-only PreAuthKey with tags: %v", authKey.AclTags)
// Create a tailscale client WITH --advertise-tags for a DIFFERENT tag
client, err := scenario.CreateTailscaleNode(
@@ -3076,7 +3076,7 @@ func TestTagsAuthKeyWithoutUserRejectsAdvertisedTags(t *testing.T) {
require.NoError(t, err)
// Login should fail because ANY advertise-tags is rejected for PreAuthKey registrations
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
if err != nil {
t.Logf("Test 5.2 PASS: Registration correctly rejected with error: %v", err)
assert.ErrorContains(t, err, "requested tags")
@@ -3134,7 +3134,7 @@ func TestTagsAuthKeyConvertToUserViaCLIRegister(t *testing.T) {
Tags: []string{"tag:valid-owned"},
})
require.NoError(t, err)
t.Logf("Created tags-only PreAuthKey (no user) with tags: %v", authKey.GetAclTags())
t.Logf("Created tags-only PreAuthKey (no user) with tags: %v", authKey.AclTags)
client, err := scenario.CreateTailscaleNode(
"head",
@@ -3142,7 +3142,7 @@ func TestTagsAuthKeyConvertToUserViaCLIRegister(t *testing.T) {
)
require.NoError(t, err)
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.Key)
require.NoError(t, err)
err = client.WaitForRunning(integrationutil.PeerSyncTimeout())
@@ -3156,7 +3156,7 @@ func TestTagsAuthKeyConvertToUserViaCLIRegister(t *testing.T) {
if len(nodes) == 1 {
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"})
t.Logf("Initial state - Node ID: %d, Tags: %v", nodes[0].GetId(), nodes[0].GetTags())
t.Logf("Initial state - Node ID: %s, Tags: %v", nodes[0].Id, nodes[0].Tags)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "node should be tagged initially")
@@ -3196,10 +3196,10 @@ func TestTagsAuthKeyConvertToUserViaCLIRegister(t *testing.T) {
if len(nodes) == 1 {
assertNodeHasNoTagsWithCollect(c, nodes[0])
assert.Equal(c, tagTestUser, nodes[0].GetUser().GetName(),
assert.Equal(c, tagTestUser, nodes[0].User.Name,
"Node ownership should be returned to user after untagging")
t.Logf("After conversion - Node ID: %d, Tags: %v, User: %s",
nodes[0].GetId(), nodes[0].GetTags(), nodes[0].GetUser().GetName())
t.Logf("After conversion - Node ID: %s, Tags: %v, User: %s",
nodes[0].Id, nodes[0].Tags, nodes[0].User.Name)
}
}, integrationutil.HAConvergeTimeout, 1*time.Second, "node should be user-owned after conversion via CLI register")
}
+6 -6
View File
@@ -73,8 +73,8 @@ func TestTailscaleRustAxum(t *testing.T) {
var userID uint64
for _, u := range users {
if u.GetName() == "user1" { //nolint:goconst
userID = u.GetId()
if u.Name == "user1" { //nolint:goconst
userID = mustParseID(u.Id)
break
}
@@ -101,7 +101,7 @@ func TestTailscaleRustAxum(t *testing.T) {
tsrsOpts := []tsric.Option{
tsric.WithNetwork(network),
tsric.WithHeadscaleURL(headscaleEndpoint),
tsric.WithAuthKey(pak.GetKey()),
tsric.WithAuthKey(pak.Key),
tsric.WithExtraHosts([]string{headscaleHostname + ":" + headscaleIP}),
}
@@ -142,8 +142,8 @@ func TestTailscaleRustAxum(t *testing.T) {
// Find the tailscale-rs node by hostname prefix
for _, n := range nodes {
if strings.HasPrefix(n.GetGivenName(), "tsrs-") {
addrs := n.GetIpAddresses()
if strings.HasPrefix(n.GivenName, "tsrs-") {
addrs := n.IpAddresses
if len(addrs) > 0 {
rustNodeIPv4 = addrs[0]
}
@@ -152,7 +152,7 @@ func TestTailscaleRustAxum(t *testing.T) {
rustNodeIPv6 = addrs[1]
}
rustNodeName = n.GetGivenName()
rustNodeName = n.GivenName
}
}