diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index 4d23e589..bfa13e34 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -244,7 +244,7 @@ jobs: - TestACLDynamicUnknownUserRemoval - TestAPIAuthenticationBypass - TestAPIAuthenticationBypassCurl - - TestGRPCAuthenticationBypass + - TestRemoteCLIAuthenticationBypass - TestCLIWithConfigAuthenticationBypass - TestAuthKeyLogoutAndReloginSameUser - TestAuthKeyLogoutAndReloginNewUser diff --git a/cmd/headscale/cli/api_key.go b/cmd/headscale/cli/api_key.go index 5c684814..82559235 100644 --- a/cmd/headscale/cli/api_key.go +++ b/cmd/headscale/cli/api_key.go @@ -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) +} diff --git a/cmd/headscale/cli/auth.go b/cmd/headscale/cli/auth.go index 85a17a3c..30abe797 100644 --- a/cmd/headscale/cli/auth.go +++ b/cmd/headscale/cli/auth.go @@ -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") + }), } diff --git a/cmd/headscale/cli/debug.go b/cmd/headscale/cli/debug.go index 1f934f0b..64262c00 100644 --- a/cmd/headscale/cli/debug.go +++ b/cmd/headscale/cli/debug.go @@ -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: ®istrationID, + 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") }), } diff --git a/cmd/headscale/cli/health.go b/cmd/headscale/cli/health.go index b3b4f430..6fccb0ac 100644 --- a/cmd/headscale/cli/health.go +++ b/cmd/headscale/cli/health.go @@ -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, "") }), } diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index bc081048..671a68cc 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -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 --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: ®istrationID, } - 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") }), } diff --git a/cmd/headscale/cli/policy.go b/cmd/headscale/cli/policy.go index 09496054..ae69cc8a 100644 --- a/cmd/headscale/cli/policy.go +++ b/cmd/headscale/cli/policy.go @@ -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 diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go index 06e2f07f..f6d4ad2b 100644 --- a/cmd/headscale/cli/preauthkeys.go +++ b/cmd/headscale/cli/preauthkeys.go @@ -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") }), } diff --git a/cmd/headscale/cli/root_test.go b/cmd/headscale/cli/root_test.go index 68d1ae52..fb1fefd1 100644 --- a/cmd/headscale/cli/root_test.go +++ b/cmd/headscale/cli/root_test.go @@ -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, diff --git a/cmd/headscale/cli/users.go b/cmd/headscale/cli/users.go index a74d64e8..71be49cf 100644 --- a/cmd/headscale/cli/users.go +++ b/cmd/headscale/cli/users.go @@ -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") }), } diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 1187de42..8b8c67f7 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -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 -} diff --git a/cmd/headscale/cli/utils_socket_test.go b/cmd/headscale/cli/utils_socket_test.go new file mode 100644 index 00000000..cc9b22ed --- /dev/null +++ b/cmd/headscale/cli/utils_socket_test.go @@ -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") +} diff --git a/hscontrol/util/net.go b/hscontrol/util/net.go index b159f765..c3c15cef 100644 --- a/hscontrol/util/net.go +++ b/hscontrol/util/net.go @@ -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) diff --git a/integration/acl_test.go b/integration/acl_test.go index 54273566..6410a221 100644 --- a/integration/acl_test.go +++ b/integration/acl_test.go @@ -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") diff --git a/integration/api_auth_test.go b/integration/api_auth_test.go index 8dfa6fd5..7195159d 100644 --- a/integration/api_auth_test.go +++ b/integration/api_auth_test.go @@ -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") diff --git a/integration/auth_key_test.go b/integration/auth_key_test.go index 7aa928c5..82db8b81 100644 --- a/integration/auth_key_test.go +++ b/integration/auth_key_test.go @@ -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, diff --git a/integration/auth_oidc_test.go b/integration/auth_oidc_test.go index 96bd980e..7b6b1e7c 100644 --- a/integration/auth_oidc_test.go +++ b/integration/auth_oidc_test.go @@ -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, diff --git a/integration/auth_web_flow_test.go b/integration/auth_web_flow_test.go index 752d78a3..26c23d06 100644 --- a/integration/auth_web_flow_test.go +++ b/integration/auth_web_flow_test.go @@ -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) { diff --git a/integration/cli_apikeys_test.go b/integration/cli_apikeys_test.go index f42de600..fab3a1c7 100644 --- a/integration/cli_apikeys_test.go +++ b/integration/cli_apikeys_test.go @@ -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 diff --git a/integration/cli_nodes_test.go b/integration/cli_nodes_test.go index 4ea21e4e..bbf02331 100644 --- a/integration/cli_nodes_test.go +++ b/integration/cli_nodes_test.go @@ -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) } diff --git a/integration/cli_policy_test.go b/integration/cli_policy_test.go index 7c8eb0bf..20e003ed 100644 --- a/integration/cli_policy_test.go +++ b/integration/cli_policy_test.go @@ -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) diff --git a/integration/cli_preauthkeys_test.go b/integration/cli_preauthkeys_test.go index 63fb43cd..75f76eda 100644 --- a/integration/cli_preauthkeys_test.go +++ b/integration/cli_preauthkeys_test.go @@ -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") } diff --git a/integration/cli_server_test.go b/integration/cli_server_test.go index 51842b15..bab44896 100644 --- a/integration/cli_server_test.go +++ b/integration/cli_server_test.go @@ -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) { diff --git a/integration/cli_test.go b/integration/cli_test.go index 86d3f4b5..aa047e12 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -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, diff --git a/integration/cli_users_test.go b/integration/cli_users_test.go index c2f95bf7..7441f431 100644 --- a/integration/cli_users_test.go +++ b/integration/cli_users_test.go @@ -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) diff --git a/integration/control.go b/integration/control.go index 256cc278..2b2fc651 100644 --- a/integration/control.go +++ b/integration/control.go @@ -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 diff --git a/integration/general_test.go b/integration/general_test.go index 27ce8197..9bdfd8c7 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -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- // 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) } diff --git a/integration/grant_cap_test.go b/integration/grant_cap_test.go index 4c3bc9bf..b3a89733 100644 --- a/integration/grant_cap_test.go +++ b/integration/grant_cap_test.go @@ -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) diff --git a/integration/helpers.go b/integration/helpers.go index 7aa1ae1e..908c7447 100644 --- a/integration/helpers.go +++ b/integration/helpers.go @@ -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) } diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index e20e6466..add0bc48 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -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 { diff --git a/integration/route_test.go b/integration/route_test.go index beff6ff0..387a0ae1 100644 --- a/integration/route_test.go +++ b/integration/route_test.go @@ -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. diff --git a/integration/scenario.go b/integration/scenario.go index ab31546d..7af28161 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -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 } diff --git a/integration/scenario_test.go b/integration/scenario_test.go index ae264221..9fa0d854 100644 --- a/integration/scenario_test.go +++ b/integration/scenario_test.go @@ -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) diff --git a/integration/tags_test.go b/integration/tags_test.go index fa4739a0..27cf1c73 100644 --- a/integration/tags_test.go +++ b/integration/tags_test.go @@ -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") } diff --git a/integration/tsric_test.go b/integration/tsric_test.go index 757e08b6..73a14740 100644 --- a/integration/tsric_test.go +++ b/integration/tsric_test.go @@ -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 } }