mirror of
https://github.com/juanfont/headscale.git
synced 2026-07-10 18:18:19 +09:00
cli: migrate the CLI and integration tests to the v1 HTTP API
Replace the gRPC client with the generated HTTP client across every command: locally over the unix socket without auth (matching the previous local gRPC socket), remotely over TLS with a Bearer API key. Output rendering and integration tests move to the HTTP client types; the transport changes, the assertions do not.
This commit is contained in:
@@ -244,7 +244,7 @@ jobs:
|
||||
- TestACLDynamicUnknownUserRemoval
|
||||
- TestAPIAuthenticationBypass
|
||||
- TestAPIAuthenticationBypassCurl
|
||||
- TestGRPCAuthenticationBypass
|
||||
- TestRemoteCLIAuthenticationBypass
|
||||
- TestCLIWithConfigAuthenticationBypass
|
||||
- TestAuthKeyLogoutAndReloginSameUser
|
||||
- TestAuthKeyLogoutAndReloginNewUser
|
||||
|
||||
@@ -3,9 +3,10 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -43,26 +44,36 @@ var listAPIKeys = &cobra.Command{
|
||||
Use: cmdList,
|
||||
Short: "List the Api keys for headscale",
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
response, err := client.ListApiKeys(ctx, &v1.ListApiKeysRequest{})
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
resp, err := client.ListApiKeysWithResponse(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing api keys: %w", err)
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetApiKeys(), func() error {
|
||||
rows := make([][]string, 0, len(response.GetApiKeys()))
|
||||
for _, key := range response.GetApiKeys() {
|
||||
expiration := "-"
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
if key.GetExpiration() != nil {
|
||||
expiration = ColourTime(key.GetExpiration().AsTime())
|
||||
apiKeys := resp.JSON200.ApiKeys
|
||||
|
||||
return printListOutput(cmd, apiKeys, func() error {
|
||||
rows := make([][]string, 0, len(apiKeys))
|
||||
for _, key := range apiKeys {
|
||||
expiration := "-"
|
||||
if key.Expiration != nil {
|
||||
expiration = ColourTime(*key.Expiration)
|
||||
}
|
||||
|
||||
var created string
|
||||
if key.CreatedAt != nil {
|
||||
created = key.CreatedAt.Format(HeadscaleDateTimeFormat)
|
||||
}
|
||||
|
||||
rows = append(rows, []string{
|
||||
strconv.FormatUint(key.GetId(), util.Base10),
|
||||
key.GetPrefix(),
|
||||
key.Id,
|
||||
key.Prefix,
|
||||
expiration,
|
||||
key.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
|
||||
created,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -79,20 +90,24 @@ Creates a new Api key, the Api key is only visible on creation
|
||||
and cannot be retrieved again.
|
||||
If you lose a key, create a new one and revoke (expire) the old one.`,
|
||||
Aliases: []string{"c", cmdNew},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
expiration, err := expirationFromFlag(cmd)
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
expiryTime, err := expirationFromFlag(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := client.CreateApiKey(ctx, &v1.CreateApiKeyRequest{
|
||||
Expiration: expiration,
|
||||
resp, err := client.CreateApiKeyWithResponse(ctx, clientv1.CreateApiKeyJSONRequestBody{
|
||||
Expiration: &expiryTime,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating api key: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetApiKey(), response.GetApiKey())
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.ApiKey, resp.JSON200.ApiKey)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -116,21 +131,33 @@ var expireAPIKeyCmd = &cobra.Command{
|
||||
Use: cmdExpire,
|
||||
Short: "Expire an ApiKey",
|
||||
Aliases: []string{"revoke", aliasExp, "e"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
id, prefix, err := apiKeyIDOrPrefix(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := client.ExpireApiKey(ctx, &v1.ExpireApiKeyRequest{
|
||||
Id: id,
|
||||
Prefix: prefix,
|
||||
})
|
||||
body := clientv1.ExpireApiKeyJSONRequestBody{}
|
||||
|
||||
if id != 0 {
|
||||
idStr := strconv.FormatUint(id, util.Base10)
|
||||
body.Id = &idStr
|
||||
}
|
||||
|
||||
if prefix != "" {
|
||||
body.Prefix = &prefix
|
||||
}
|
||||
|
||||
resp, err := client.ExpireApiKeyWithResponse(ctx, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expiring api key: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Key expired")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Key expired")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -138,20 +165,59 @@ var deleteAPIKeyCmd = &cobra.Command{
|
||||
Use: cmdDelete,
|
||||
Short: "Delete an ApiKey",
|
||||
Aliases: []string{"remove", aliasDel},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
id, prefix, err := apiKeyIDOrPrefix(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := client.DeleteApiKey(ctx, &v1.DeleteApiKeyRequest{
|
||||
Id: id,
|
||||
Prefix: prefix,
|
||||
})
|
||||
// The DELETE route addresses the key by its prefix in the path. When the
|
||||
// user deletes by --id we resolve the id to its (masked) prefix first,
|
||||
// since the path segment is required and a query-only id cannot be routed.
|
||||
if prefix == "" {
|
||||
prefix, err = apiKeyPrefixForID(ctx, client, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.DeleteApiKeyWithResponse(ctx, prefix, &clientv1.DeleteApiKeyParams{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting api key: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "Key deleted")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Key deleted")
|
||||
}),
|
||||
}
|
||||
|
||||
// apiKeyPrefixForID resolves an API key id to its display prefix by listing the
|
||||
// keys. The DELETE endpoint addresses keys by prefix in the URL path, so a
|
||||
// delete by --id needs the prefix; the returned masked prefix is accepted by
|
||||
// the server's lookup.
|
||||
func apiKeyPrefixForID(
|
||||
ctx context.Context,
|
||||
client *clientv1.ClientWithResponses,
|
||||
id uint64,
|
||||
) (string, error) {
|
||||
resp, err := client.ListApiKeysWithResponse(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("listing api keys: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return "", apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
idStr := strconv.FormatUint(id, util.Base10)
|
||||
for _, key := range resp.JSON200.ApiKeys {
|
||||
if key.Id == idStr {
|
||||
return key.Prefix, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%w: api key %d not found", errMissingParameter, id)
|
||||
}
|
||||
|
||||
+45
-40
@@ -3,8 +3,9 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -33,62 +34,66 @@ var authCmd = &cobra.Command{
|
||||
var authRegisterCmd = &cobra.Command{
|
||||
Use: "register",
|
||||
Short: "Register a node to your network",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
authID, _ := cmd.Flags().GetString("auth-id")
|
||||
|
||||
request := &v1.AuthRegisterRequest{
|
||||
AuthId: authID,
|
||||
User: user,
|
||||
}
|
||||
|
||||
response, err := client.AuthRegister(ctx, request)
|
||||
resp, err := client.AuthRegisterWithResponse(ctx, clientv1.AuthRegisterJSONRequestBody{
|
||||
AuthId: &authID,
|
||||
User: &user,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("registering node: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(
|
||||
cmd,
|
||||
response.GetNode(),
|
||||
fmt.Sprintf("Node %s registered", response.GetNode().GetGivenName()),
|
||||
)
|
||||
}),
|
||||
}
|
||||
|
||||
// authDecisionRunE builds a RunE for an auth decision command (approve or
|
||||
// reject) that reads the auth-id flag, invokes the given gRPC call, and prints
|
||||
// the response. errVerb is used in the error message; okMsg is printed on
|
||||
// success.
|
||||
func authDecisionRunE[Resp any](
|
||||
errVerb, okMsg string,
|
||||
call func(ctx context.Context, client v1.HeadscaleServiceClient, authID string) (Resp, error),
|
||||
) func(*cobra.Command, []string) error {
|
||||
return grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
authID, _ := cmd.Flags().GetString("auth-id")
|
||||
|
||||
response, err := call(ctx, client, authID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s auth request: %w", errVerb, err)
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, okMsg)
|
||||
})
|
||||
node := resp.JSON200.Node
|
||||
|
||||
return printOutput(
|
||||
cmd,
|
||||
node,
|
||||
fmt.Sprintf("Node %s registered", node.GivenName),
|
||||
)
|
||||
}),
|
||||
}
|
||||
|
||||
var authApproveCmd = &cobra.Command{
|
||||
Use: "approve",
|
||||
Short: "Approve a pending authentication request",
|
||||
RunE: authDecisionRunE("approving", "Auth request approved",
|
||||
func(ctx context.Context, client v1.HeadscaleServiceClient, authID string) (*v1.AuthApproveResponse, error) {
|
||||
return client.AuthApprove(ctx, &v1.AuthApproveRequest{AuthId: authID})
|
||||
}),
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
authID, _ := cmd.Flags().GetString("auth-id")
|
||||
|
||||
resp, err := client.AuthApproveWithResponse(ctx, clientv1.AuthApproveJSONRequestBody{AuthId: &authID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("approving auth request: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Auth request approved")
|
||||
}),
|
||||
}
|
||||
|
||||
var authRejectCmd = &cobra.Command{
|
||||
Use: "reject",
|
||||
Short: "Reject a pending authentication request",
|
||||
RunE: authDecisionRunE("rejecting", "Auth request rejected",
|
||||
func(ctx context.Context, client v1.HeadscaleServiceClient, authID string) (*v1.AuthRejectResponse, error) {
|
||||
return client.AuthReject(ctx, &v1.AuthRejectRequest{AuthId: authID})
|
||||
}),
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
authID, _ := cmd.Flags().GetString("auth-id")
|
||||
|
||||
resp, err := client.AuthRejectWithResponse(ctx, clientv1.AuthRejectJSONRequestBody{AuthId: &authID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("rejecting auth request: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "Auth request rejected")
|
||||
}),
|
||||
}
|
||||
|
||||
+14
-11
@@ -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")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -16,12 +17,16 @@ var healthCmd = &cobra.Command{
|
||||
Use: "health",
|
||||
Short: "Check the health of the Headscale server",
|
||||
Long: "Check the health of the Headscale server. This command will return an exit code of 0 if the server is healthy, or 1 if it is not.",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
response, err := client.Health(ctx, &v1.HealthRequest{})
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
resp, err := client.HealthWithResponse(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking health: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "")
|
||||
}),
|
||||
}
|
||||
|
||||
+145
-139
@@ -3,17 +3,17 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
@@ -67,24 +67,30 @@ var registerNodeCmd = &cobra.Command{
|
||||
Use: "register",
|
||||
Short: "Registers a node to your network",
|
||||
Deprecated: "use 'headscale auth register --auth-id <id> --user <user>' instead",
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
user, _ := cmd.Flags().GetString("user")
|
||||
registrationID, _ := cmd.Flags().GetString("key")
|
||||
|
||||
request := &v1.RegisterNodeRequest{
|
||||
Key: registrationID,
|
||||
User: user,
|
||||
params := &clientv1.RegisterNodeParams{
|
||||
User: &user,
|
||||
Key: ®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")
|
||||
}),
|
||||
}
|
||||
|
||||
+40
-19
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -213,7 +213,8 @@ func TestFilterPreReleasesIfStable(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := filterPreReleasesIfStable(func() string { return tt.currentVersion })(tt.tag)
|
||||
if result != tt.expectedFilter {
|
||||
t.Errorf("%s: got %v, want %v\nDescription: %s\nCurrent version: %s, Tag: %s",
|
||||
t.Errorf(
|
||||
"%s: got %v, want %v\nDescription: %s\nCurrent version: %s, Tag: %s",
|
||||
tt.name,
|
||||
result,
|
||||
tt.expectedFilter,
|
||||
@@ -293,7 +294,8 @@ func TestIsPreReleaseVersion(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isPreReleaseVersion(tt.version)
|
||||
if result != tt.expected {
|
||||
t.Errorf("%s: got %v, want %v\nDescription: %s\nVersion: %s",
|
||||
t.Errorf(
|
||||
"%s: got %v, want %v\nDescription: %s\nVersion: %s",
|
||||
tt.name,
|
||||
result,
|
||||
tt.expected,
|
||||
|
||||
+71
-46
@@ -4,10 +4,11 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -45,27 +46,39 @@ func usernameAndIDFromFlag(cmd *cobra.Command) (uint64, string, error) {
|
||||
// returning the raw flag id and the matched user.
|
||||
func resolveSingleUser(
|
||||
ctx context.Context,
|
||||
client v1.HeadscaleServiceClient,
|
||||
client *clientv1.ClientWithResponses,
|
||||
cmd *cobra.Command,
|
||||
) (uint64, *v1.User, error) {
|
||||
) (uint64, *clientv1.User, error) {
|
||||
id, username, err := usernameAndIDFromFlag(cmd)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
users, err := client.ListUsers(ctx, &v1.ListUsersRequest{
|
||||
Name: username,
|
||||
Id: id,
|
||||
})
|
||||
params := &clientv1.ListUsersParams{}
|
||||
if username != "" {
|
||||
params.Name = &username
|
||||
}
|
||||
|
||||
if id != 0 {
|
||||
idStr := strconv.FormatUint(id, util.Base10)
|
||||
params.Id = &idStr
|
||||
}
|
||||
|
||||
resp, err := client.ListUsersWithResponse(ctx, params)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("listing users: %w", err)
|
||||
}
|
||||
|
||||
if len(users.GetUsers()) != 1 {
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return 0, nil, apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
users := resp.JSON200.Users
|
||||
if len(users) != 1 {
|
||||
return 0, nil, errMultipleUsersMatch
|
||||
}
|
||||
|
||||
return id, users.GetUsers()[0], nil
|
||||
return id, &users[0], nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -102,19 +115,19 @@ var createUserCmd = &cobra.Command{
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
userName := args[0]
|
||||
|
||||
log.Trace().Interface(zf.Client, client).Msg("obtained gRPC client")
|
||||
log.Trace().Interface(zf.Client, client).Msg("obtained API client")
|
||||
|
||||
request := &v1.CreateUserRequest{Name: userName}
|
||||
request := clientv1.CreateUserJSONRequestBody{Name: &userName}
|
||||
|
||||
if displayName, _ := cmd.Flags().GetString("display-name"); displayName != "" {
|
||||
request.DisplayName = displayName
|
||||
request.DisplayName = &displayName
|
||||
}
|
||||
|
||||
if email, _ := cmd.Flags().GetString("email"); email != "" {
|
||||
request.Email = email
|
||||
request.Email = &email
|
||||
}
|
||||
|
||||
if pictureURL, _ := cmd.Flags().GetString("picture-url"); pictureURL != "" {
|
||||
@@ -122,17 +135,21 @@ var createUserCmd = &cobra.Command{
|
||||
return fmt.Errorf("invalid picture URL: %w", err)
|
||||
}
|
||||
|
||||
request.PictureUrl = pictureURL
|
||||
request.PictureUrl = &pictureURL
|
||||
}
|
||||
|
||||
log.Trace().Interface(zf.Request, request).Msg("sending CreateUser request")
|
||||
|
||||
response, err := client.CreateUser(ctx, request)
|
||||
resp, err := client.CreateUserWithResponse(ctx, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating user: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetUser(), "User created")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.User, "User created")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -140,27 +157,29 @@ var destroyUserCmd = &cobra.Command{
|
||||
Use: "destroy --identifier ID or --name NAME",
|
||||
Short: "Destroys a user",
|
||||
Aliases: []string{cmdDelete},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
_, user, err := resolveSingleUser(ctx, client, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !confirmAction(cmd, fmt.Sprintf(
|
||||
"Do you want to remove the user %q (%d) and any associated preauthkeys?",
|
||||
user.GetName(), user.GetId(),
|
||||
"Do you want to remove the user %q (%s) and any associated preauthkeys?",
|
||||
user.Name, user.Id,
|
||||
)) {
|
||||
return printOutput(cmd, map[string]string{colResult: "User not destroyed"}, "User not destroyed")
|
||||
}
|
||||
|
||||
deleteRequest := &v1.DeleteUserRequest{Id: user.GetId()}
|
||||
|
||||
response, err := client.DeleteUser(ctx, deleteRequest)
|
||||
resp, err := client.DeleteUserWithResponse(ctx, user.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("destroying user: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response, "User destroyed")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200, "User destroyed")
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -168,8 +187,8 @@ var listUsersCmd = &cobra.Command{
|
||||
Use: cmdList,
|
||||
Short: "List all the users",
|
||||
Aliases: []string{"ls", cmdShow},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
request := &v1.ListUsersRequest{}
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
params := &clientv1.ListUsersParams{}
|
||||
|
||||
id, _ := cmd.Flags().GetInt64("identifier")
|
||||
username, _ := cmd.Flags().GetString("name")
|
||||
@@ -178,29 +197,36 @@ var listUsersCmd = &cobra.Command{
|
||||
// filter by one param at most
|
||||
switch {
|
||||
case id > 0:
|
||||
request.Id = uint64(id)
|
||||
idStr := strconv.FormatInt(id, util.Base10)
|
||||
params.Id = &idStr
|
||||
case username != "":
|
||||
request.Name = username
|
||||
params.Name = &username
|
||||
case email != "":
|
||||
request.Email = email
|
||||
params.Email = &email
|
||||
}
|
||||
|
||||
response, err := client.ListUsers(ctx, request)
|
||||
resp, err := client.ListUsersWithResponse(ctx, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing users: %w", err)
|
||||
}
|
||||
|
||||
return printListOutput(cmd, response.GetUsers(), func() error {
|
||||
rows := make([][]string, 0, len(response.GetUsers()))
|
||||
for _, user := range response.GetUsers() {
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
users := resp.JSON200.Users
|
||||
|
||||
return printListOutput(cmd, users, func() error {
|
||||
rows := make([][]string, 0, len(users))
|
||||
for _, user := range users {
|
||||
rows = append(
|
||||
rows,
|
||||
[]string{
|
||||
strconv.FormatUint(user.GetId(), util.Base10),
|
||||
user.GetDisplayName(),
|
||||
user.GetName(),
|
||||
user.GetEmail(),
|
||||
user.GetCreatedAt().AsTime().Format(HeadscaleDateTimeFormat),
|
||||
user.Id,
|
||||
user.DisplayName,
|
||||
user.Name,
|
||||
user.Email,
|
||||
user.CreatedAt.Format(HeadscaleDateTimeFormat),
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -214,7 +240,7 @@ var renameUserCmd = &cobra.Command{
|
||||
Use: "rename",
|
||||
Short: "Renames a user",
|
||||
Aliases: []string{"mv"},
|
||||
RunE: grpcRunE(func(ctx context.Context, client v1.HeadscaleServiceClient, cmd *cobra.Command, args []string) error {
|
||||
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
|
||||
id, _, err := resolveSingleUser(ctx, client, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -222,16 +248,15 @@ var renameUserCmd = &cobra.Command{
|
||||
|
||||
newName, _ := cmd.Flags().GetString("new-name")
|
||||
|
||||
renameReq := &v1.RenameUserRequest{
|
||||
OldId: id,
|
||||
NewName: newName,
|
||||
}
|
||||
|
||||
response, err := client.RenameUser(ctx, renameReq)
|
||||
resp, err := client.RenameUserWithResponse(ctx, strconv.FormatUint(id, util.Base10), newName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("renaming user: %w", err)
|
||||
}
|
||||
|
||||
return printOutput(cmd, response.GetUser(), "User renamed")
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return apiError(resp.StatusCode(), resp.ApplicationproblemJSONDefault)
|
||||
}
|
||||
|
||||
return printOutput(cmd, resp.JSON200.User, "User renamed")
|
||||
}),
|
||||
}
|
||||
|
||||
+155
-110
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -10,7 +10,9 @@ import (
|
||||
"tailscale.com/net/tsaddr"
|
||||
)
|
||||
|
||||
func GrpcSocketDialer(ctx context.Context, addr string) (net.Conn, error) {
|
||||
// SocketDialer dials a local unix-domain socket, letting the HTTP CLI client
|
||||
// reach the headscale API over its unix socket.
|
||||
func SocketDialer(ctx context.Context, addr string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
|
||||
return d.DialContext(ctx, "unix", addr)
|
||||
|
||||
+44
-44
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -4,11 +4,10 @@ import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/integration/hsic"
|
||||
@@ -74,7 +73,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
|
||||
}
|
||||
|
||||
var (
|
||||
listNodes []*v1.Node
|
||||
listNodes []*clientv1.Node
|
||||
nodeCountBeforeLogout int
|
||||
)
|
||||
|
||||
@@ -135,12 +134,12 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, userName := range spec.Users {
|
||||
key, err := scenario.CreatePreAuthKey(userMap[userName].GetId(), true, false)
|
||||
key, err := scenario.CreatePreAuthKey(mustParseID(userMap[userName].Id), true, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
|
||||
}
|
||||
|
||||
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
|
||||
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.Key)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run tailscale up for user %s: %s", userName, err)
|
||||
}
|
||||
@@ -256,7 +255,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
|
||||
requireAllClientsNetInfoAndDERP(t, headscale, expectedNodes, "all clients should have NetInfo and DERP after initial login", 3*time.Minute)
|
||||
|
||||
var (
|
||||
listNodes []*v1.Node
|
||||
listNodes []*clientv1.Node
|
||||
nodeCountBeforeLogout int
|
||||
)
|
||||
|
||||
@@ -290,7 +289,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a new authkey for user1, to be used for all clients
|
||||
key, err := scenario.CreatePreAuthKey(userMap["user1"].GetId(), true, false)
|
||||
key, err := scenario.CreatePreAuthKey(mustParseID(userMap["user1"].Id), true, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create pre-auth key for user1: %s", err)
|
||||
}
|
||||
@@ -298,13 +297,13 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
|
||||
// Log in all clients as user1, iterating over the spec only returns the
|
||||
// clients, not the usernames.
|
||||
for _, userName := range spec.Users {
|
||||
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
|
||||
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.Key)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run tailscale up for user %s: %s", userName, err)
|
||||
}
|
||||
}
|
||||
|
||||
var user1Nodes []*v1.Node
|
||||
var user1Nodes []*clientv1.Node
|
||||
|
||||
t.Logf("Validating user1 node count after relogin at %s", time.Now().Format(TimestampFormat))
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
@@ -318,7 +317,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
|
||||
// Collect expected node IDs for user1 after relogin
|
||||
expectedUser1Nodes := make([]types.NodeID, 0, len(user1Nodes))
|
||||
for _, node := range user1Nodes {
|
||||
expectedUser1Nodes = append(expectedUser1Nodes, types.NodeID(node.GetId()))
|
||||
expectedUser1Nodes = append(expectedUser1Nodes, types.NodeID(mustParseID(node.Id)))
|
||||
}
|
||||
|
||||
// Validate connection state after relogin as user1
|
||||
@@ -328,7 +327,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
|
||||
// Validate that user2 still has their original nodes after user1's re-authentication
|
||||
// When nodes re-authenticate with a different user's pre-auth key, NEW nodes are created
|
||||
// for the new user. The original nodes remain with the original user.
|
||||
var user2Nodes []*v1.Node
|
||||
var user2Nodes []*clientv1.Node
|
||||
|
||||
t.Logf("Validating user2 node persistence after user1 relogin at %s", time.Now().Format(TimestampFormat))
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
@@ -402,7 +401,7 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
|
||||
requireAllClientsNetInfoAndDERP(t, headscale, expectedNodes, "all clients should have NetInfo and DERP after initial login", 3*time.Minute)
|
||||
|
||||
var (
|
||||
listNodes []*v1.Node
|
||||
listNodes []*clientv1.Node
|
||||
nodeCountBeforeLogout int
|
||||
)
|
||||
|
||||
@@ -446,7 +445,7 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, userName := range spec.Users {
|
||||
key, err := scenario.CreatePreAuthKey(userMap[userName].GetId(), true, false)
|
||||
key, err := scenario.CreatePreAuthKey(mustParseID(userMap[userName].Id), true, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
|
||||
}
|
||||
@@ -458,12 +457,12 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
|
||||
"preauthkeys",
|
||||
"expire",
|
||||
"--id",
|
||||
strconv.FormatUint(key.GetId(), 10),
|
||||
key.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
|
||||
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.Key)
|
||||
assert.ErrorContains(t, err, "authkey expired")
|
||||
}
|
||||
})
|
||||
@@ -498,14 +497,14 @@ func TestAuthKeyDeleteKey(t *testing.T) {
|
||||
userMap, err := headscale.MapUsers()
|
||||
require.NoError(t, err)
|
||||
|
||||
userID := userMap["user1"].GetId()
|
||||
userID := mustParseID(userMap["user1"].Id)
|
||||
|
||||
// Create a pre-auth key - we keep the full key string before it gets redacted
|
||||
authKey, err := scenario.CreatePreAuthKey(userID, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
authKeyString := authKey.GetKey()
|
||||
authKeyID := authKey.GetId()
|
||||
authKeyString := authKey.Key
|
||||
authKeyID := mustParseID(authKey.Id)
|
||||
t.Logf("Created pre-auth key ID %d: %s", authKeyID, authKeyString)
|
||||
|
||||
// Create a tailscale client and log it in with the auth key
|
||||
@@ -519,7 +518,7 @@ func TestAuthKeyDeleteKey(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the node to be registered
|
||||
var user1Nodes []*v1.Node
|
||||
var user1Nodes []*clientv1.Node
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
var err error
|
||||
@@ -529,8 +528,8 @@ func TestAuthKeyDeleteKey(t *testing.T) {
|
||||
assert.Len(c, user1Nodes, 1)
|
||||
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for node to be registered")
|
||||
|
||||
nodeID := user1Nodes[0].GetId()
|
||||
nodeName := user1Nodes[0].GetName()
|
||||
nodeID := mustParseID(user1Nodes[0].Id)
|
||||
nodeName := user1Nodes[0].Name
|
||||
t.Logf("Node %d (%s) created successfully with auth_key_id=%d", nodeID, nodeName, authKeyID)
|
||||
|
||||
// Verify node is online
|
||||
@@ -640,7 +639,7 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) {
|
||||
// Step 1: Verify initial route is advertised, approved, and SERVING
|
||||
t.Logf("Step 1: Verifying initial route is advertised, approved, and SERVING at %s", time.Now().Format(TimestampFormat))
|
||||
|
||||
var initialNode *v1.Node
|
||||
var initialNode *clientv1.Node
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err := headscale.ListNodes()
|
||||
@@ -650,21 +649,21 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) {
|
||||
if len(nodes) == 1 {
|
||||
initialNode = nodes[0]
|
||||
// Check: 1 announced, 1 approved, 1 serving (subnet route)
|
||||
assert.Lenf(c, initialNode.GetAvailableRoutes(), 1,
|
||||
"Node should have 1 available route, got %v", initialNode.GetAvailableRoutes())
|
||||
assert.Lenf(c, initialNode.GetApprovedRoutes(), 1,
|
||||
"Node should have 1 approved route, got %v", initialNode.GetApprovedRoutes())
|
||||
assert.Lenf(c, initialNode.GetSubnetRoutes(), 1,
|
||||
"Node should have 1 serving (subnet) route, got %v - THIS IS THE BUG if empty", initialNode.GetSubnetRoutes())
|
||||
assert.Contains(c, initialNode.GetSubnetRoutes(), advertiseRoute,
|
||||
assert.Lenf(c, initialNode.AvailableRoutes, 1,
|
||||
"Node should have 1 available route, got %v", initialNode.AvailableRoutes)
|
||||
assert.Lenf(c, initialNode.ApprovedRoutes, 1,
|
||||
"Node should have 1 approved route, got %v", initialNode.ApprovedRoutes)
|
||||
assert.Lenf(c, initialNode.SubnetRoutes, 1,
|
||||
"Node should have 1 serving (subnet) route, got %v - THIS IS THE BUG if empty", initialNode.SubnetRoutes)
|
||||
assert.Contains(c, initialNode.SubnetRoutes, advertiseRoute,
|
||||
"Subnet routes should contain %s", advertiseRoute)
|
||||
}
|
||||
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "initial route should be serving")
|
||||
|
||||
require.NotNil(t, initialNode, "Initial node should be found")
|
||||
initialNodeID := initialNode.GetId()
|
||||
t.Logf("Initial node ID: %d, Available: %v, Approved: %v, Serving: %v",
|
||||
initialNodeID, initialNode.GetAvailableRoutes(), initialNode.GetApprovedRoutes(), initialNode.GetSubnetRoutes())
|
||||
initialNodeID := initialNode.Id
|
||||
t.Logf("Initial node ID: %s, Available: %v, Approved: %v, Serving: %v",
|
||||
initialNodeID, initialNode.AvailableRoutes, initialNode.ApprovedRoutes, initialNode.SubnetRoutes)
|
||||
|
||||
// Step 2: Logout
|
||||
t.Logf("Step 2: Logging out at %s", time.Now().Format(TimestampFormat))
|
||||
@@ -694,12 +693,12 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) {
|
||||
userMap, err := headscale.MapUsers()
|
||||
require.NoError(t, err)
|
||||
|
||||
key, err := scenario.CreatePreAuthKey(userMap[user].GetId(), true, false)
|
||||
key, err := scenario.CreatePreAuthKey(mustParseID(userMap[user].Id), true, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Re-login - the container already has extraLoginArgs with --advertise-routes
|
||||
// from the initial setup, so routes will be advertised on re-login
|
||||
err = scenario.RunTailscaleUp(user, headscale.GetEndpoint(), key.GetKey())
|
||||
err = scenario.RunTailscaleUp(user, headscale.GetEndpoint(), key.Key)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for client to be running
|
||||
@@ -722,23 +721,23 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) {
|
||||
if len(nodes) == 1 {
|
||||
node := nodes[0]
|
||||
t.Logf("After relogin - Available: %v, Approved: %v, Serving: %v",
|
||||
node.GetAvailableRoutes(), node.GetApprovedRoutes(), node.GetSubnetRoutes())
|
||||
node.AvailableRoutes, node.ApprovedRoutes, node.SubnetRoutes)
|
||||
|
||||
// This is where issue #2896 manifests:
|
||||
// - Available shows the route (from [tailcfg.Hostinfo.RoutableIPs])
|
||||
// - Approved shows the route (from [tailcfg.Node.ApprovedRoutes])
|
||||
// - BUT Serving ([tailcfg.Node.SubnetRoutes]/[ipnstate.PeerStatus.PrimaryRoutes]) is EMPTY!
|
||||
assert.Lenf(c, node.GetAvailableRoutes(), 1,
|
||||
"Node should have 1 available route after relogin, got %v", node.GetAvailableRoutes())
|
||||
assert.Lenf(c, node.GetApprovedRoutes(), 1,
|
||||
"Node should have 1 approved route after relogin, got %v", node.GetApprovedRoutes())
|
||||
assert.Lenf(c, node.GetSubnetRoutes(), 1,
|
||||
"BUG #2896: Node should have 1 SERVING route after relogin, got %v", node.GetSubnetRoutes())
|
||||
assert.Contains(c, node.GetSubnetRoutes(), advertiseRoute,
|
||||
assert.Lenf(c, node.AvailableRoutes, 1,
|
||||
"Node should have 1 available route after relogin, got %v", node.AvailableRoutes)
|
||||
assert.Lenf(c, node.ApprovedRoutes, 1,
|
||||
"Node should have 1 approved route after relogin, got %v", node.ApprovedRoutes)
|
||||
assert.Lenf(c, node.SubnetRoutes, 1,
|
||||
"BUG #2896: Node should have 1 SERVING route after relogin, got %v", node.SubnetRoutes)
|
||||
assert.Contains(c, node.SubnetRoutes, advertiseRoute,
|
||||
"BUG #2896: Subnet routes should contain %s after relogin", advertiseRoute)
|
||||
|
||||
// Also verify node ID was preserved (same node, not new registration)
|
||||
assert.Equal(c, initialNodeID, node.GetId(),
|
||||
assert.Equal(c, initialNodeID, node.Id,
|
||||
"Node ID should be preserved after same-user relogin")
|
||||
}
|
||||
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll,
|
||||
|
||||
+137
-137
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/integration/hsic"
|
||||
@@ -86,26 +86,26 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
|
||||
listUsers, err := headscale.ListUsers()
|
||||
require.NoError(t, err)
|
||||
|
||||
want := []*v1.User{
|
||||
want := []*clientv1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Id: "1",
|
||||
Name: "user1",
|
||||
Email: "user1@test.no",
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Id: "2",
|
||||
Name: "user1",
|
||||
Email: "user1@headscale.net",
|
||||
Provider: "oidc",
|
||||
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
|
||||
},
|
||||
{
|
||||
Id: 3,
|
||||
Id: "3",
|
||||
Name: "user2",
|
||||
Email: "user2@test.no",
|
||||
},
|
||||
{
|
||||
Id: 4,
|
||||
Id: "4",
|
||||
Name: "user2",
|
||||
Email: "", // Unverified
|
||||
Provider: "oidc",
|
||||
@@ -114,10 +114,10 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
|
||||
}
|
||||
|
||||
sort.Slice(listUsers, func(i, j int) bool {
|
||||
return listUsers[i].GetId() < listUsers[j].GetId()
|
||||
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
|
||||
})
|
||||
|
||||
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
||||
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
|
||||
t.Fatalf("unexpected users: %s", diff)
|
||||
}
|
||||
}
|
||||
@@ -245,34 +245,34 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
emailVerified bool
|
||||
cliUsers []string
|
||||
oidcUsers []string
|
||||
want func(iss string) []*v1.User
|
||||
want func(iss string) []*clientv1.User
|
||||
}{
|
||||
{
|
||||
name: "no-migration-verified-email",
|
||||
emailVerified: true,
|
||||
cliUsers: []string{"user1", "user2"},
|
||||
oidcUsers: []string{"user1", "user2"},
|
||||
want: func(iss string) []*v1.User {
|
||||
return []*v1.User{
|
||||
want: func(iss string) []*clientv1.User {
|
||||
return []*clientv1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Id: "1",
|
||||
Name: "user1",
|
||||
Email: "user1@test.no",
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Id: "2",
|
||||
Name: "user1",
|
||||
Email: "user1@headscale.net",
|
||||
Provider: "oidc",
|
||||
ProviderId: iss + "/user1",
|
||||
},
|
||||
{
|
||||
Id: 3,
|
||||
Id: "3",
|
||||
Name: "user2",
|
||||
Email: "user2@test.no",
|
||||
},
|
||||
{
|
||||
Id: 4,
|
||||
Id: "4",
|
||||
Name: "user2",
|
||||
Email: "user2@headscale.net",
|
||||
Provider: "oidc",
|
||||
@@ -286,26 +286,26 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
emailVerified: false,
|
||||
cliUsers: []string{"user1", "user2"},
|
||||
oidcUsers: []string{"user1", "user2"},
|
||||
want: func(iss string) []*v1.User {
|
||||
return []*v1.User{
|
||||
want: func(iss string) []*clientv1.User {
|
||||
return []*clientv1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Id: "1",
|
||||
Name: "user1",
|
||||
Email: "user1@test.no",
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Id: "2",
|
||||
Name: "user1",
|
||||
Provider: "oidc",
|
||||
ProviderId: iss + "/user1",
|
||||
},
|
||||
{
|
||||
Id: 3,
|
||||
Id: "3",
|
||||
Name: "user2",
|
||||
Email: "user2@test.no",
|
||||
},
|
||||
{
|
||||
Id: 4,
|
||||
Id: "4",
|
||||
Name: "user2",
|
||||
Provider: "oidc",
|
||||
ProviderId: iss + "/user2",
|
||||
@@ -318,26 +318,26 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
emailVerified: false,
|
||||
cliUsers: []string{"user1.headscale.net", "user2.headscale.net"},
|
||||
oidcUsers: []string{"user1", "user2"},
|
||||
want: func(iss string) []*v1.User {
|
||||
return []*v1.User{
|
||||
want: func(iss string) []*clientv1.User {
|
||||
return []*clientv1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Id: "1",
|
||||
Name: "user1.headscale.net",
|
||||
Email: "user1.headscale.net@test.no",
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Id: "2",
|
||||
Name: "user1",
|
||||
Provider: "oidc",
|
||||
ProviderId: iss + "/user1",
|
||||
},
|
||||
{
|
||||
Id: 3,
|
||||
Id: "3",
|
||||
Name: "user2.headscale.net",
|
||||
Email: "user2.headscale.net@test.no",
|
||||
},
|
||||
{
|
||||
Id: 4,
|
||||
Id: "4",
|
||||
Name: "user2",
|
||||
Provider: "oidc",
|
||||
ProviderId: iss + "/user2",
|
||||
@@ -393,10 +393,10 @@ func TestOIDC024UserCreation(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Slice(listUsers, func(i, j int) bool {
|
||||
return listUsers[i].GetId() < listUsers[j].GetId()
|
||||
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
|
||||
})
|
||||
|
||||
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
||||
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
|
||||
t.Errorf("unexpected users: %s", diff)
|
||||
}
|
||||
})
|
||||
@@ -509,9 +509,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
||||
assert.NoError(ct, err, "Failed to list users during initial validation")
|
||||
assert.Len(ct, listUsers, 1, "Expected exactly 1 user after first login, got %d", len(listUsers))
|
||||
|
||||
wantUsers := []*v1.User{
|
||||
wantUsers := []*clientv1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Id: "1",
|
||||
Name: "user1",
|
||||
Email: "user1@headscale.net",
|
||||
Provider: "oidc",
|
||||
@@ -520,17 +520,17 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
||||
}
|
||||
|
||||
sort.Slice(listUsers, func(i, j int) bool {
|
||||
return listUsers[i].GetId() < listUsers[j].GetId()
|
||||
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
|
||||
})
|
||||
|
||||
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
||||
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
|
||||
ct.Errorf("User validation failed after first login - unexpected users: %s", diff)
|
||||
}
|
||||
}, integrationutil.StatusReadyTimeout, 1*time.Second, "validating user1 creation after initial OIDC login")
|
||||
|
||||
t.Logf("Validating initial node creation at %s", time.Now().Format(TimestampFormat))
|
||||
|
||||
var listNodes []*v1.Node
|
||||
var listNodes []*clientv1.Node
|
||||
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
@@ -593,16 +593,16 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
||||
assert.NoError(ct, err, "Failed to list users after user2 login")
|
||||
assert.Len(ct, listUsers, 2, "Expected exactly 2 users after user2 login, got %d users", len(listUsers))
|
||||
|
||||
wantUsers := []*v1.User{
|
||||
wantUsers := []*clientv1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Id: "1",
|
||||
Name: "user1",
|
||||
Email: "user1@headscale.net",
|
||||
Provider: "oidc",
|
||||
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Id: "2",
|
||||
Name: "user2",
|
||||
Email: "user2@headscale.net",
|
||||
Provider: "oidc",
|
||||
@@ -611,15 +611,15 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
||||
}
|
||||
|
||||
sort.Slice(listUsers, func(i, j int) bool {
|
||||
return listUsers[i].GetId() < listUsers[j].GetId()
|
||||
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
|
||||
})
|
||||
|
||||
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
||||
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
|
||||
ct.Errorf("User validation failed after user2 login - expected both user1 and user2: %s", diff)
|
||||
}
|
||||
}, integrationutil.StatusReadyTimeout, 1*time.Second, "validating both user1 and user2 exist after second OIDC login")
|
||||
|
||||
var listNodesAfterNewUserLogin []*v1.Node
|
||||
var listNodesAfterNewUserLogin []*clientv1.Node
|
||||
// First, wait for the new node to be created
|
||||
t.Logf("Waiting for user2 node creation at %s", time.Now().Format(TimestampFormat))
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
@@ -640,9 +640,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
||||
if len(listNodesAfterNewUserLogin) >= 2 {
|
||||
// Machine key is the same as the "machine" has not changed,
|
||||
// but Node key is not as it is a new node
|
||||
assert.Equal(ct, listNodes[0].GetMachineKey(), listNodesAfterNewUserLogin[0].GetMachineKey(), "Machine key should be preserved from original node")
|
||||
assert.Equal(ct, listNodesAfterNewUserLogin[0].GetMachineKey(), listNodesAfterNewUserLogin[1].GetMachineKey(), "Both nodes should share the same machine key")
|
||||
assert.NotEqual(ct, listNodesAfterNewUserLogin[0].GetNodeKey(), listNodesAfterNewUserLogin[1].GetNodeKey(), "Node keys should be different between user1 and user2 nodes")
|
||||
assert.Equal(ct, listNodes[0].MachineKey, listNodesAfterNewUserLogin[0].MachineKey, "Machine key should be preserved from original node")
|
||||
assert.Equal(ct, listNodesAfterNewUserLogin[0].MachineKey, listNodesAfterNewUserLogin[1].MachineKey, "Both nodes should share the same machine key")
|
||||
assert.NotEqual(ct, listNodesAfterNewUserLogin[0].NodeKey, listNodesAfterNewUserLogin[1].NodeKey, "Node keys should be different between user1 and user2 nodes")
|
||||
}
|
||||
}, integrationutil.PolicyPropagationTimeout, 2*time.Second, "waiting for node count stabilization at exactly 2 nodes after user2 login")
|
||||
|
||||
@@ -650,9 +650,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
||||
var activeUser2NodeID types.NodeID
|
||||
|
||||
for _, node := range listNodesAfterNewUserLogin {
|
||||
if node.GetUser().GetId() == 2 { // user2
|
||||
activeUser2NodeID = types.NodeID(node.GetId())
|
||||
t.Logf("Active user2 node: %d (User: %s)", node.GetId(), node.GetUser().GetName())
|
||||
if node.User.Id == "2" { // user2
|
||||
activeUser2NodeID = types.NodeID(mustParseID(node.Id))
|
||||
t.Logf("Active user2 node: %d (User: %s)", mustParseID(node.Id), node.User.Name)
|
||||
|
||||
break
|
||||
}
|
||||
@@ -685,9 +685,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
||||
|
||||
// Validate node stability - ensure no phantom nodes
|
||||
for i, node := range currentNodes {
|
||||
assert.NotNil(ct, node.GetUser(), "Node %d should have a valid user before logout", i)
|
||||
assert.NotEmpty(ct, node.GetMachineKey(), "Node %d should have a valid machine key before logout", i)
|
||||
t.Logf("Pre-logout node %d: User=%s, MachineKey=%s", i, node.GetUser().GetName(), node.GetMachineKey()[:16]+"...")
|
||||
assert.NotEmpty(ct, node.User.Id, "Node %d should have a valid user before logout", i)
|
||||
assert.NotEmpty(ct, node.MachineKey, "Node %d should have a valid machine key before logout", i)
|
||||
t.Logf("Pre-logout node %d: User=%s, MachineKey=%s", i, node.User.Name, node.MachineKey[:16]+"...")
|
||||
}
|
||||
}, integrationutil.HAConvergeTimeout, 2*time.Second, "validating stable node count and integrity before user2 logout")
|
||||
|
||||
@@ -730,9 +730,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
||||
|
||||
// Ensure both nodes are still valid (not cleaned up incorrectly)
|
||||
for i, node := range currentNodes {
|
||||
assert.NotNil(ct, node.GetUser(), "Node %d should still have a valid user after user2 logout", i)
|
||||
assert.NotEmpty(ct, node.GetMachineKey(), "Node %d should still have a valid machine key after user2 logout", i)
|
||||
t.Logf("Post-logout node %d: User=%s, MachineKey=%s", i, node.GetUser().GetName(), node.GetMachineKey()[:16]+"...")
|
||||
assert.NotEmpty(ct, node.User.Id, "Node %d should still have a valid user after user2 logout", i)
|
||||
assert.NotEmpty(ct, node.MachineKey, "Node %d should still have a valid machine key after user2 logout", i)
|
||||
t.Logf("Post-logout node %d: User=%s, MachineKey=%s", i, node.User.Name, node.MachineKey[:16]+"...")
|
||||
}
|
||||
}, integrationutil.HAConvergeTimeout, 2*time.Second, "validating node persistence and integrity after user2 logout")
|
||||
|
||||
@@ -761,16 +761,16 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
||||
assert.NoError(ct, err, "Failed to list users during final validation")
|
||||
assert.Len(ct, listUsers, 2, "Should still have exactly 2 users after user1 relogin, got %d", len(listUsers))
|
||||
|
||||
wantUsers := []*v1.User{
|
||||
wantUsers := []*clientv1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Id: "1",
|
||||
Name: "user1",
|
||||
Email: "user1@headscale.net",
|
||||
Provider: "oidc",
|
||||
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Id: "2",
|
||||
Name: "user2",
|
||||
Email: "user2@headscale.net",
|
||||
Provider: "oidc",
|
||||
@@ -779,15 +779,15 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
||||
}
|
||||
|
||||
sort.Slice(listUsers, func(i, j int) bool {
|
||||
return listUsers[i].GetId() < listUsers[j].GetId()
|
||||
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
|
||||
})
|
||||
|
||||
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
||||
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
|
||||
ct.Errorf("Final user validation failed - both users should persist after relogin cycle: %s", diff)
|
||||
}
|
||||
}, integrationutil.StatusReadyTimeout, 1*time.Second, "validating user persistence after complete relogin cycle (user1->user2->user1)")
|
||||
|
||||
var listNodesAfterLoggingBackIn []*v1.Node
|
||||
var listNodesAfterLoggingBackIn []*clientv1.Node
|
||||
// Wait for login to complete and nodes to stabilize
|
||||
t.Logf("Final node validation: checking node stability after user1 relogin at %s", time.Now().Format(TimestampFormat))
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
@@ -806,24 +806,24 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
||||
// Validate that the machine we had when we logged in the first time, has the same
|
||||
// machine key, but a different ID than the newly logged in version of the same
|
||||
// machine.
|
||||
assert.Equal(ct, listNodes[0].GetMachineKey(), listNodesAfterNewUserLogin[0].GetMachineKey(), "Original user1 machine key should match user1 node after user switch")
|
||||
assert.Equal(ct, listNodes[0].GetNodeKey(), listNodesAfterNewUserLogin[0].GetNodeKey(), "Original user1 node key should match user1 node after user switch")
|
||||
assert.Equal(ct, listNodes[0].GetId(), listNodesAfterNewUserLogin[0].GetId(), "Original user1 node ID should match user1 node after user switch")
|
||||
assert.Equal(ct, listNodes[0].GetMachineKey(), listNodesAfterNewUserLogin[1].GetMachineKey(), "User1 and user2 nodes should share the same machine key")
|
||||
assert.NotEqual(ct, listNodes[0].GetId(), listNodesAfterNewUserLogin[1].GetId(), "User1 and user2 nodes should have different node IDs")
|
||||
assert.NotEqual(ct, listNodes[0].GetUser().GetId(), listNodesAfterNewUserLogin[1].GetUser().GetId(), "User1 and user2 nodes should belong to different users")
|
||||
assert.Equal(ct, listNodes[0].MachineKey, listNodesAfterNewUserLogin[0].MachineKey, "Original user1 machine key should match user1 node after user switch")
|
||||
assert.Equal(ct, listNodes[0].NodeKey, listNodesAfterNewUserLogin[0].NodeKey, "Original user1 node key should match user1 node after user switch")
|
||||
assert.Equal(ct, listNodes[0].Id, listNodesAfterNewUserLogin[0].Id, "Original user1 node ID should match user1 node after user switch")
|
||||
assert.Equal(ct, listNodes[0].MachineKey, listNodesAfterNewUserLogin[1].MachineKey, "User1 and user2 nodes should share the same machine key")
|
||||
assert.NotEqual(ct, listNodes[0].Id, listNodesAfterNewUserLogin[1].Id, "User1 and user2 nodes should have different node IDs")
|
||||
assert.NotEqual(ct, listNodes[0].User.Id, listNodesAfterNewUserLogin[1].User.Id, "User1 and user2 nodes should belong to different users")
|
||||
|
||||
// Even tho we are logging in again with the same user, the previous key has been expired
|
||||
// and a new one has been generated. The node entry in the database should be the same
|
||||
// as the user + machinekey still matches.
|
||||
assert.Equal(ct, listNodes[0].GetMachineKey(), listNodesAfterLoggingBackIn[0].GetMachineKey(), "Machine key should remain consistent after user1 relogin")
|
||||
assert.NotEqual(ct, listNodes[0].GetNodeKey(), listNodesAfterLoggingBackIn[0].GetNodeKey(), "Node key should be regenerated after user1 relogin")
|
||||
assert.Equal(ct, listNodes[0].GetId(), listNodesAfterLoggingBackIn[0].GetId(), "Node ID should be preserved for user1 after relogin")
|
||||
assert.Equal(ct, listNodes[0].MachineKey, listNodesAfterLoggingBackIn[0].MachineKey, "Machine key should remain consistent after user1 relogin")
|
||||
assert.NotEqual(ct, listNodes[0].NodeKey, listNodesAfterLoggingBackIn[0].NodeKey, "Node key should be regenerated after user1 relogin")
|
||||
assert.Equal(ct, listNodes[0].Id, listNodesAfterLoggingBackIn[0].Id, "Node ID should be preserved for user1 after relogin")
|
||||
|
||||
// The "logged back in" machine should have the same machinekey but a different nodekey
|
||||
// than the version logged in with a different user.
|
||||
assert.Equal(ct, listNodesAfterLoggingBackIn[0].GetMachineKey(), listNodesAfterLoggingBackIn[1].GetMachineKey(), "Both final nodes should share the same machine key")
|
||||
assert.NotEqual(ct, listNodesAfterLoggingBackIn[0].GetNodeKey(), listNodesAfterLoggingBackIn[1].GetNodeKey(), "Final nodes should have different node keys for different users")
|
||||
assert.Equal(ct, listNodesAfterLoggingBackIn[0].MachineKey, listNodesAfterLoggingBackIn[1].MachineKey, "Both final nodes should share the same machine key")
|
||||
assert.NotEqual(ct, listNodesAfterLoggingBackIn[0].NodeKey, listNodesAfterLoggingBackIn[1].NodeKey, "Final nodes should have different node keys for different users")
|
||||
|
||||
t.Logf("Final validation complete - node counts and key relationships verified at %s", time.Now().Format(TimestampFormat))
|
||||
}, integrationutil.HAConvergeTimeout, 2*time.Second, "validating final node state after complete user1->user2->user1 relogin cycle with detailed key validation")
|
||||
@@ -832,9 +832,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
||||
var activeUser1NodeID types.NodeID
|
||||
|
||||
for _, node := range listNodesAfterLoggingBackIn {
|
||||
if node.GetUser().GetId() == 1 { // user1
|
||||
activeUser1NodeID = types.NodeID(node.GetId())
|
||||
t.Logf("Active user1 node after relogin: %d (User: %s)", node.GetId(), node.GetUser().GetName())
|
||||
if node.User.Id == "1" { // user1
|
||||
activeUser1NodeID = types.NodeID(mustParseID(node.Id))
|
||||
t.Logf("Active user1 node after relogin: %d (User: %s)", mustParseID(node.Id), node.User.Name)
|
||||
|
||||
break
|
||||
}
|
||||
@@ -942,9 +942,9 @@ func TestOIDCFollowUpUrl(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, listUsers, 1)
|
||||
|
||||
wantUsers := []*v1.User{
|
||||
wantUsers := []*clientv1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Id: "1",
|
||||
Name: "user1",
|
||||
Email: "user1@headscale.net",
|
||||
Provider: "oidc",
|
||||
@@ -954,15 +954,15 @@ func TestOIDCFollowUpUrl(t *testing.T) {
|
||||
|
||||
sort.Slice(
|
||||
listUsers, func(i, j int) bool {
|
||||
return listUsers[i].GetId() < listUsers[j].GetId()
|
||||
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
|
||||
},
|
||||
)
|
||||
|
||||
if diff := cmp.Diff(
|
||||
wantUsers,
|
||||
listUsers,
|
||||
cmpopts.IgnoreUnexported(v1.User{}),
|
||||
cmpopts.IgnoreFields(v1.User{}, "CreatedAt"),
|
||||
cmpopts.IgnoreUnexported(clientv1.User{}),
|
||||
cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt"),
|
||||
); diff != "" {
|
||||
t.Fatalf("unexpected users: %s", diff)
|
||||
}
|
||||
@@ -1051,9 +1051,9 @@ func TestOIDCMultipleOpenedLoginUrls(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, listUsers, 1)
|
||||
|
||||
wantUsers := []*v1.User{
|
||||
wantUsers := []*clientv1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Id: "1",
|
||||
Name: "user1",
|
||||
Email: "user1@headscale.net",
|
||||
Provider: "oidc",
|
||||
@@ -1063,15 +1063,15 @@ func TestOIDCMultipleOpenedLoginUrls(t *testing.T) {
|
||||
|
||||
sort.Slice(
|
||||
listUsers, func(i, j int) bool {
|
||||
return listUsers[i].GetId() < listUsers[j].GetId()
|
||||
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
|
||||
},
|
||||
)
|
||||
|
||||
if diff := cmp.Diff(
|
||||
wantUsers,
|
||||
listUsers,
|
||||
cmpopts.IgnoreUnexported(v1.User{}),
|
||||
cmpopts.IgnoreFields(v1.User{}, "CreatedAt"),
|
||||
cmpopts.IgnoreUnexported(clientv1.User{}),
|
||||
cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt"),
|
||||
); diff != "" {
|
||||
t.Fatalf("unexpected users: %s", diff)
|
||||
}
|
||||
@@ -1159,9 +1159,9 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
|
||||
assert.NoError(ct, err, "Failed to list users during initial validation")
|
||||
assert.Len(ct, listUsers, 1, "Expected exactly 1 user after first login, got %d", len(listUsers))
|
||||
|
||||
wantUsers := []*v1.User{
|
||||
wantUsers := []*clientv1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Id: "1",
|
||||
Name: "user1",
|
||||
Email: "user1@headscale.net",
|
||||
Provider: "oidc",
|
||||
@@ -1170,17 +1170,17 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
|
||||
}
|
||||
|
||||
sort.Slice(listUsers, func(i, j int) bool {
|
||||
return listUsers[i].GetId() < listUsers[j].GetId()
|
||||
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
|
||||
})
|
||||
|
||||
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
||||
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
|
||||
ct.Errorf("User validation failed after first login - unexpected users: %s", diff)
|
||||
}
|
||||
}, integrationutil.StatusReadyTimeout, 1*time.Second, "validating user1 creation after initial OIDC login")
|
||||
|
||||
t.Logf("Validating initial node creation at %s", time.Now().Format(TimestampFormat))
|
||||
|
||||
var initialNodes []*v1.Node
|
||||
var initialNodes []*clientv1.Node
|
||||
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
@@ -1211,9 +1211,9 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
|
||||
validateInitialConnection(t, headscale, expectedNodes)
|
||||
|
||||
// Store initial node keys for comparison
|
||||
initialMachineKey := initialNodes[0].GetMachineKey()
|
||||
initialNodeKey := initialNodes[0].GetNodeKey()
|
||||
initialNodeID := initialNodes[0].GetId()
|
||||
initialMachineKey := initialNodes[0].MachineKey
|
||||
initialNodeKey := initialNodes[0].NodeKey
|
||||
initialNodeID := initialNodes[0].Id
|
||||
|
||||
// Logout user1
|
||||
err = ts.Logout()
|
||||
@@ -1262,9 +1262,9 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
|
||||
assert.NoError(ct, err, "Failed to list users during final validation")
|
||||
assert.Len(ct, listUsers, 1, "Should still have exactly 1 user after same-user relogin, got %d", len(listUsers))
|
||||
|
||||
wantUsers := []*v1.User{
|
||||
wantUsers := []*clientv1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Id: "1",
|
||||
Name: "user1",
|
||||
Email: "user1@headscale.net",
|
||||
Provider: "oidc",
|
||||
@@ -1273,15 +1273,15 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
|
||||
}
|
||||
|
||||
sort.Slice(listUsers, func(i, j int) bool {
|
||||
return listUsers[i].GetId() < listUsers[j].GetId()
|
||||
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
|
||||
})
|
||||
|
||||
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
||||
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
|
||||
ct.Errorf("Final user validation failed - user1 should persist after same-user relogin: %s", diff)
|
||||
}
|
||||
}, integrationutil.StatusReadyTimeout, 1*time.Second, "validating user1 persistence after same-user OIDC relogin cycle")
|
||||
|
||||
var finalNodes []*v1.Node
|
||||
var finalNodes []*clientv1.Node
|
||||
|
||||
t.Logf("Final node validation: checking node stability after same-user relogin at %s", time.Now().Format(TimestampFormat))
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
@@ -1293,19 +1293,19 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
|
||||
finalNode := finalNodes[0]
|
||||
|
||||
// Machine key should be preserved (same physical machine)
|
||||
assert.Equal(ct, initialMachineKey, finalNode.GetMachineKey(), "Machine key should be preserved for same user same node relogin")
|
||||
assert.Equal(ct, initialMachineKey, finalNode.MachineKey, "Machine key should be preserved for same user same node relogin")
|
||||
|
||||
// Node ID should be preserved (same user, same machine)
|
||||
assert.Equal(ct, initialNodeID, finalNode.GetId(), "Node ID should be preserved for same user same node relogin")
|
||||
assert.Equal(ct, initialNodeID, finalNode.Id, "Node ID should be preserved for same user same node relogin")
|
||||
|
||||
// Node key should be regenerated (new session after logout)
|
||||
assert.NotEqual(ct, initialNodeKey, finalNode.GetNodeKey(), "Node key should be regenerated after logout/relogin even for same user")
|
||||
assert.NotEqual(ct, initialNodeKey, finalNode.NodeKey, "Node key should be regenerated after logout/relogin even for same user")
|
||||
|
||||
t.Logf("Final validation complete - same user relogin key relationships verified at %s", time.Now().Format(TimestampFormat))
|
||||
}, integrationutil.HAConvergeTimeout, 2*time.Second, "validating final node state after same-user OIDC relogin cycle with key preservation validation")
|
||||
|
||||
// Security validation: user1's node should be active after relogin
|
||||
activeUser1NodeID := types.NodeID(finalNodes[0].GetId())
|
||||
activeUser1NodeID := types.NodeID(mustParseID(finalNodes[0].Id))
|
||||
|
||||
t.Logf("Validating user1 node is online after same-user relogin at %s", time.Now().Format(TimestampFormat))
|
||||
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
@@ -1389,10 +1389,10 @@ func TestOIDCExpiryAfterRestart(t *testing.T) {
|
||||
assert.Len(ct, nodes, 1)
|
||||
|
||||
node := nodes[0]
|
||||
assert.NotNil(ct, node.GetExpiry(), "Expiry should be set after OIDC login")
|
||||
assert.NotNil(ct, node.Expiry, "Expiry should be set after OIDC login")
|
||||
|
||||
if node.GetExpiry() != nil {
|
||||
expiryTime := node.GetExpiry().AsTime()
|
||||
if node.Expiry != nil {
|
||||
expiryTime := *node.Expiry
|
||||
assert.False(ct, expiryTime.IsZero(), "Expiry should not be zero time")
|
||||
|
||||
initialExpiry = expiryTime
|
||||
@@ -1431,10 +1431,10 @@ func TestOIDCExpiryAfterRestart(t *testing.T) {
|
||||
assert.Len(ct, nodes, 1, "Should still have exactly 1 node after restart")
|
||||
|
||||
node := nodes[0]
|
||||
assert.NotNil(ct, node.GetExpiry(), "Expiry should NOT be nil after restart")
|
||||
assert.NotNil(ct, node.Expiry, "Expiry should NOT be nil after restart")
|
||||
|
||||
if node.GetExpiry() != nil {
|
||||
expiryTime := node.GetExpiry().AsTime()
|
||||
if node.Expiry != nil {
|
||||
expiryTime := *node.Expiry
|
||||
|
||||
// This is the bug check - expiry should NOT be zero time
|
||||
assert.False(ct, expiryTime.IsZero(),
|
||||
@@ -1567,9 +1567,9 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
|
||||
assert.Len(ct, nodes, 1)
|
||||
|
||||
gatewayNode := nodes[0]
|
||||
gatewayNodeID = gatewayNode.GetId()
|
||||
assert.Len(ct, gatewayNode.GetAvailableRoutes(), 1)
|
||||
assert.Contains(ct, gatewayNode.GetAvailableRoutes(), advertiseRoute)
|
||||
gatewayNodeID = mustParseID(gatewayNode.Id)
|
||||
assert.Len(ct, gatewayNode.AvailableRoutes, 1)
|
||||
assert.Contains(ct, gatewayNode.AvailableRoutes, advertiseRoute)
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "route advertisement should propagate to headscale")
|
||||
|
||||
// Approve the advertised route
|
||||
@@ -1586,8 +1586,8 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
|
||||
assert.Len(ct, nodes, 1)
|
||||
|
||||
gatewayNode := nodes[0]
|
||||
assert.Len(ct, gatewayNode.GetApprovedRoutes(), 1)
|
||||
assert.Contains(ct, gatewayNode.GetApprovedRoutes(), advertiseRoute)
|
||||
assert.Len(ct, gatewayNode.ApprovedRoutes, 1)
|
||||
assert.Contains(ct, gatewayNode.ApprovedRoutes, advertiseRoute)
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "route approval should propagate to headscale")
|
||||
|
||||
// NOW create the OIDC user by having them join
|
||||
@@ -1707,10 +1707,10 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
|
||||
assert.GreaterOrEqual(ct, len(users), 2, "Should have at least 2 users (gateway CLI user + oidcuser)")
|
||||
|
||||
// Find gateway CLI user
|
||||
var gatewayUser *v1.User
|
||||
var gatewayUser *clientv1.User
|
||||
|
||||
for _, user := range users {
|
||||
if user.GetName() == "gateway" && user.GetProvider() == "" {
|
||||
if user.Name == "gateway" && user.Provider == "" {
|
||||
gatewayUser = user
|
||||
break
|
||||
}
|
||||
@@ -1719,14 +1719,14 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
|
||||
assert.NotNil(ct, gatewayUser, "Should have gateway CLI user")
|
||||
|
||||
if gatewayUser != nil {
|
||||
assert.Equal(ct, "gateway", gatewayUser.GetName())
|
||||
assert.Equal(ct, "gateway", gatewayUser.Name)
|
||||
}
|
||||
|
||||
// Find OIDC user
|
||||
var oidcUserFound *v1.User
|
||||
var oidcUserFound *clientv1.User
|
||||
|
||||
for _, user := range users {
|
||||
if user.GetName() == "oidcuser" && user.GetProvider() == "oidc" {
|
||||
if user.Name == "oidcuser" && user.Provider == "oidc" {
|
||||
oidcUserFound = user
|
||||
break
|
||||
}
|
||||
@@ -1735,8 +1735,8 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
|
||||
assert.NotNil(ct, oidcUserFound, "Should have OIDC user")
|
||||
|
||||
if oidcUserFound != nil {
|
||||
assert.Equal(ct, "oidcuser", oidcUserFound.GetName())
|
||||
assert.Equal(ct, "oidcuser@headscale.net", oidcUserFound.GetEmail())
|
||||
assert.Equal(ct, "oidcuser", oidcUserFound.Name)
|
||||
assert.Equal(ct, "oidcuser@headscale.net", oidcUserFound.Email)
|
||||
}
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "headscale should have correct users and nodes")
|
||||
|
||||
@@ -1834,7 +1834,7 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) {
|
||||
// Step 1: Verify initial route is advertised, approved, and SERVING
|
||||
t.Logf("Step 1: Verifying initial route is advertised, approved, and SERVING at %s", time.Now().Format(TimestampFormat))
|
||||
|
||||
var initialNode *v1.Node
|
||||
var initialNode *clientv1.Node
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err := headscale.ListNodes()
|
||||
@@ -1844,21 +1844,21 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) {
|
||||
if len(nodes) == 1 {
|
||||
initialNode = nodes[0]
|
||||
// Check: 1 announced, 1 approved, 1 serving (subnet route)
|
||||
assert.Lenf(c, initialNode.GetAvailableRoutes(), 1,
|
||||
"Node should have 1 available route, got %v", initialNode.GetAvailableRoutes())
|
||||
assert.Lenf(c, initialNode.GetApprovedRoutes(), 1,
|
||||
"Node should have 1 approved route, got %v", initialNode.GetApprovedRoutes())
|
||||
assert.Lenf(c, initialNode.GetSubnetRoutes(), 1,
|
||||
"Node should have 1 serving (subnet) route, got %v - THIS IS THE BUG if empty", initialNode.GetSubnetRoutes())
|
||||
assert.Contains(c, initialNode.GetSubnetRoutes(), advertiseRoute,
|
||||
assert.Lenf(c, initialNode.AvailableRoutes, 1,
|
||||
"Node should have 1 available route, got %v", initialNode.AvailableRoutes)
|
||||
assert.Lenf(c, initialNode.ApprovedRoutes, 1,
|
||||
"Node should have 1 approved route, got %v", initialNode.ApprovedRoutes)
|
||||
assert.Lenf(c, initialNode.SubnetRoutes, 1,
|
||||
"Node should have 1 serving (subnet) route, got %v - THIS IS THE BUG if empty", initialNode.SubnetRoutes)
|
||||
assert.Contains(c, initialNode.SubnetRoutes, advertiseRoute,
|
||||
"Subnet routes should contain %s", advertiseRoute)
|
||||
}
|
||||
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "initial route should be serving")
|
||||
|
||||
require.NotNil(t, initialNode, "Initial node should be found")
|
||||
initialNodeID := initialNode.GetId()
|
||||
t.Logf("Initial node ID: %d, Available: %v, Approved: %v, Serving: %v",
|
||||
initialNodeID, initialNode.GetAvailableRoutes(), initialNode.GetApprovedRoutes(), initialNode.GetSubnetRoutes())
|
||||
initialNodeID := initialNode.Id
|
||||
t.Logf("Initial node ID: %s, Available: %v, Approved: %v, Serving: %v",
|
||||
initialNodeID, initialNode.AvailableRoutes, initialNode.ApprovedRoutes, initialNode.SubnetRoutes)
|
||||
|
||||
// Step 2: Logout
|
||||
t.Logf("Step 2: Logging out at %s", time.Now().Format(TimestampFormat))
|
||||
@@ -1911,23 +1911,23 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) {
|
||||
if len(nodes) == 1 {
|
||||
node := nodes[0]
|
||||
t.Logf("After relogin - Available: %v, Approved: %v, Serving: %v",
|
||||
node.GetAvailableRoutes(), node.GetApprovedRoutes(), node.GetSubnetRoutes())
|
||||
node.AvailableRoutes, node.ApprovedRoutes, node.SubnetRoutes)
|
||||
|
||||
// This is where issue #2896 manifests:
|
||||
// - Available shows the route (from [tailcfg.Hostinfo.RoutableIPs])
|
||||
// - Approved shows the route (from [tailcfg.Node.ApprovedRoutes])
|
||||
// - BUT Serving ([tailcfg.Node.SubnetRoutes]/[ipnstate.PeerStatus.PrimaryRoutes]) is EMPTY!
|
||||
assert.Lenf(c, node.GetAvailableRoutes(), 1,
|
||||
"Node should have 1 available route after relogin, got %v", node.GetAvailableRoutes())
|
||||
assert.Lenf(c, node.GetApprovedRoutes(), 1,
|
||||
"Node should have 1 approved route after relogin, got %v", node.GetApprovedRoutes())
|
||||
assert.Lenf(c, node.GetSubnetRoutes(), 1,
|
||||
"BUG #2896: Node should have 1 SERVING route after relogin, got %v", node.GetSubnetRoutes())
|
||||
assert.Contains(c, node.GetSubnetRoutes(), advertiseRoute,
|
||||
assert.Lenf(c, node.AvailableRoutes, 1,
|
||||
"Node should have 1 available route after relogin, got %v", node.AvailableRoutes)
|
||||
assert.Lenf(c, node.ApprovedRoutes, 1,
|
||||
"Node should have 1 approved route after relogin, got %v", node.ApprovedRoutes)
|
||||
assert.Lenf(c, node.SubnetRoutes, 1,
|
||||
"BUG #2896: Node should have 1 SERVING route after relogin, got %v", node.SubnetRoutes)
|
||||
assert.Contains(c, node.SubnetRoutes, advertiseRoute,
|
||||
"BUG #2896: Subnet routes should contain %s after relogin", advertiseRoute)
|
||||
|
||||
// Also verify node ID was preserved (same node, not new registration)
|
||||
assert.Equal(c, initialNodeID, node.GetId(),
|
||||
assert.Equal(c, initialNodeID, node.Id,
|
||||
"Node ID should be preserved after same-user relogin")
|
||||
}
|
||||
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll,
|
||||
|
||||
@@ -6,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) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/integration/hsic"
|
||||
"github.com/juanfont/headscale/integration/integrationutil"
|
||||
"github.com/juanfont/headscale/integration/tsic"
|
||||
@@ -55,7 +54,7 @@ func TestApiKeyCommand(t *testing.T) {
|
||||
|
||||
assert.Len(t, keys, 5)
|
||||
|
||||
var listedAPIKeys []v1.ApiKey
|
||||
var listedAPIKeys []clientv1.ApiKey
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(headscale,
|
||||
@@ -73,43 +72,43 @@ func TestApiKeyCommand(t *testing.T) {
|
||||
|
||||
assert.Len(t, listedAPIKeys, 5)
|
||||
|
||||
assert.Equal(t, uint64(1), listedAPIKeys[0].GetId())
|
||||
assert.Equal(t, uint64(2), listedAPIKeys[1].GetId())
|
||||
assert.Equal(t, uint64(3), listedAPIKeys[2].GetId())
|
||||
assert.Equal(t, uint64(4), listedAPIKeys[3].GetId())
|
||||
assert.Equal(t, uint64(5), listedAPIKeys[4].GetId())
|
||||
assert.Equal(t, "1", listedAPIKeys[0].Id)
|
||||
assert.Equal(t, "2", listedAPIKeys[1].Id)
|
||||
assert.Equal(t, "3", listedAPIKeys[2].Id)
|
||||
assert.Equal(t, "4", listedAPIKeys[3].Id)
|
||||
assert.Equal(t, "5", listedAPIKeys[4].Id)
|
||||
|
||||
assert.NotEmpty(t, listedAPIKeys[0].GetPrefix())
|
||||
assert.NotEmpty(t, listedAPIKeys[1].GetPrefix())
|
||||
assert.NotEmpty(t, listedAPIKeys[2].GetPrefix())
|
||||
assert.NotEmpty(t, listedAPIKeys[3].GetPrefix())
|
||||
assert.NotEmpty(t, listedAPIKeys[4].GetPrefix())
|
||||
assert.NotEmpty(t, listedAPIKeys[0].Prefix)
|
||||
assert.NotEmpty(t, listedAPIKeys[1].Prefix)
|
||||
assert.NotEmpty(t, listedAPIKeys[2].Prefix)
|
||||
assert.NotEmpty(t, listedAPIKeys[3].Prefix)
|
||||
assert.NotEmpty(t, listedAPIKeys[4].Prefix)
|
||||
|
||||
assert.True(t, listedAPIKeys[0].GetExpiration().AsTime().After(time.Now()))
|
||||
assert.True(t, listedAPIKeys[1].GetExpiration().AsTime().After(time.Now()))
|
||||
assert.True(t, listedAPIKeys[2].GetExpiration().AsTime().After(time.Now()))
|
||||
assert.True(t, listedAPIKeys[3].GetExpiration().AsTime().After(time.Now()))
|
||||
assert.True(t, listedAPIKeys[4].GetExpiration().AsTime().After(time.Now()))
|
||||
assert.True(t, listedAPIKeys[0].Expiration.After(time.Now()))
|
||||
assert.True(t, listedAPIKeys[1].Expiration.After(time.Now()))
|
||||
assert.True(t, listedAPIKeys[2].Expiration.After(time.Now()))
|
||||
assert.True(t, listedAPIKeys[3].Expiration.After(time.Now()))
|
||||
assert.True(t, listedAPIKeys[4].Expiration.After(time.Now()))
|
||||
|
||||
assert.True(
|
||||
t,
|
||||
listedAPIKeys[0].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
|
||||
listedAPIKeys[0].Expiration.Before(time.Now().Add(time.Hour*26)),
|
||||
)
|
||||
assert.True(
|
||||
t,
|
||||
listedAPIKeys[1].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
|
||||
listedAPIKeys[1].Expiration.Before(time.Now().Add(time.Hour*26)),
|
||||
)
|
||||
assert.True(
|
||||
t,
|
||||
listedAPIKeys[2].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
|
||||
listedAPIKeys[2].Expiration.Before(time.Now().Add(time.Hour*26)),
|
||||
)
|
||||
assert.True(
|
||||
t,
|
||||
listedAPIKeys[3].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
|
||||
listedAPIKeys[3].Expiration.Before(time.Now().Add(time.Hour*26)),
|
||||
)
|
||||
assert.True(
|
||||
t,
|
||||
listedAPIKeys[4].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
|
||||
listedAPIKeys[4].Expiration.Before(time.Now().Add(time.Hour*26)),
|
||||
)
|
||||
|
||||
expiredPrefixes := make(map[string]bool)
|
||||
@@ -122,15 +121,15 @@ func TestApiKeyCommand(t *testing.T) {
|
||||
"apikeys",
|
||||
"expire",
|
||||
"--prefix",
|
||||
listedAPIKeys[idx].GetPrefix(),
|
||||
listedAPIKeys[idx].Prefix,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
expiredPrefixes[listedAPIKeys[idx].GetPrefix()] = true
|
||||
expiredPrefixes[listedAPIKeys[idx].Prefix] = true
|
||||
}
|
||||
|
||||
var listedAfterExpireAPIKeys []v1.ApiKey
|
||||
var listedAfterExpireAPIKeys []clientv1.ApiKey
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(headscale,
|
||||
@@ -147,17 +146,17 @@ func TestApiKeyCommand(t *testing.T) {
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for API keys list after expire")
|
||||
|
||||
for index := range listedAfterExpireAPIKeys {
|
||||
if _, ok := expiredPrefixes[listedAfterExpireAPIKeys[index].GetPrefix()]; ok {
|
||||
if _, ok := expiredPrefixes[listedAfterExpireAPIKeys[index].Prefix]; ok {
|
||||
// Expired
|
||||
assert.True(
|
||||
t,
|
||||
listedAfterExpireAPIKeys[index].GetExpiration().AsTime().Before(time.Now()),
|
||||
listedAfterExpireAPIKeys[index].Expiration.Before(time.Now()),
|
||||
)
|
||||
} else {
|
||||
// Not expired
|
||||
assert.False(
|
||||
t,
|
||||
listedAfterExpireAPIKeys[index].GetExpiration().AsTime().Before(time.Now()),
|
||||
listedAfterExpireAPIKeys[index].Expiration.Before(time.Now()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -168,11 +167,11 @@ func TestApiKeyCommand(t *testing.T) {
|
||||
"apikeys",
|
||||
"delete",
|
||||
"--prefix",
|
||||
listedAPIKeys[0].GetPrefix(),
|
||||
listedAPIKeys[0].Prefix,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var listedAPIKeysAfterDelete []v1.ApiKey
|
||||
var listedAPIKeysAfterDelete []clientv1.ApiKey
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(headscale,
|
||||
@@ -197,11 +196,11 @@ func TestApiKeyCommand(t *testing.T) {
|
||||
"apikeys",
|
||||
"expire",
|
||||
"--id",
|
||||
strconv.FormatUint(listedAPIKeysAfterDelete[0].GetId(), 10),
|
||||
listedAPIKeysAfterDelete[0].Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var listedAPIKeysAfterExpireByID []v1.ApiKey
|
||||
var listedAPIKeysAfterExpireByID []clientv1.ApiKey
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(headscale,
|
||||
@@ -219,25 +218,25 @@ func TestApiKeyCommand(t *testing.T) {
|
||||
|
||||
// Verify the key was expired
|
||||
for idx := range listedAPIKeysAfterExpireByID {
|
||||
if listedAPIKeysAfterExpireByID[idx].GetId() == listedAPIKeysAfterDelete[0].GetId() {
|
||||
assert.True(t, listedAPIKeysAfterExpireByID[idx].GetExpiration().AsTime().Before(time.Now()),
|
||||
if listedAPIKeysAfterExpireByID[idx].Id == listedAPIKeysAfterDelete[0].Id {
|
||||
assert.True(t, listedAPIKeysAfterExpireByID[idx].Expiration.Before(time.Now()),
|
||||
"Key expired by ID should have expiration in the past")
|
||||
}
|
||||
}
|
||||
|
||||
// Test delete by ID (using key at index 1)
|
||||
deletedKeyID := listedAPIKeysAfterExpireByID[1].GetId()
|
||||
deletedKeyID := listedAPIKeysAfterExpireByID[1].Id
|
||||
_, err = headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"apikeys",
|
||||
"delete",
|
||||
"--id",
|
||||
strconv.FormatUint(deletedKeyID, 10),
|
||||
deletedKeyID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var listedAPIKeysAfterDeleteByID []v1.ApiKey
|
||||
var listedAPIKeysAfterDeleteByID []clientv1.ApiKey
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(headscale,
|
||||
@@ -257,7 +256,7 @@ func TestApiKeyCommand(t *testing.T) {
|
||||
|
||||
// Verify the specific key was deleted
|
||||
for idx := range listedAPIKeysAfterDeleteByID {
|
||||
assert.NotEqual(t, deletedKeyID, listedAPIKeysAfterDeleteByID[idx].GetId(),
|
||||
assert.NotEqual(t, deletedKeyID, listedAPIKeysAfterDeleteByID[idx].Id,
|
||||
"Deleted key should not be present in the list")
|
||||
}
|
||||
}
|
||||
@@ -277,7 +276,7 @@ func TestApiKeyCommandValidation(t *testing.T) {
|
||||
_, err := headscale.Execute([]string{"headscale", "apikeys", "create", "--output", "json"})
|
||||
require.NoError(t, err)
|
||||
|
||||
var listed []v1.ApiKey
|
||||
var listed []clientv1.ApiKey
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err := executeAndUnmarshal(headscale,
|
||||
@@ -288,8 +287,8 @@ func TestApiKeyCommandValidation(t *testing.T) {
|
||||
assert.Len(c, listed, 1)
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for API key list")
|
||||
|
||||
prefix := listed[0].GetPrefix()
|
||||
id := strconv.FormatUint(listed[0].GetId(), 10)
|
||||
prefix := listed[0].Prefix
|
||||
id := listed[0].Id
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
+114
-108
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/integration/hsic"
|
||||
"github.com/juanfont/headscale/integration/integrationutil"
|
||||
"github.com/juanfont/headscale/integration/tsic"
|
||||
@@ -34,12 +33,12 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||
headscale, err := scenario.Headscale()
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := make([]*v1.PreAuthKey, count)
|
||||
keys := make([]*clientv1.PreAuthKey, count)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
for index := range count {
|
||||
var preAuthKey v1.PreAuthKey
|
||||
var preAuthKey clientv1.PreAuthKey
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err := executeAndUnmarshal(
|
||||
@@ -68,7 +67,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||
|
||||
assert.Len(t, keys, 3)
|
||||
|
||||
var listedPreAuthKeys []v1.PreAuthKey
|
||||
var listedPreAuthKeys []clientv1.PreAuthKey
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(
|
||||
@@ -90,34 +89,34 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||
|
||||
assert.Equal(
|
||||
t,
|
||||
[]uint64{keys[0].GetId(), keys[1].GetId(), keys[2].GetId()},
|
||||
[]uint64{
|
||||
listedPreAuthKeys[1].GetId(),
|
||||
listedPreAuthKeys[2].GetId(),
|
||||
listedPreAuthKeys[3].GetId(),
|
||||
[]string{keys[0].Id, keys[1].Id, keys[2].Id},
|
||||
[]string{
|
||||
listedPreAuthKeys[1].Id,
|
||||
listedPreAuthKeys[2].Id,
|
||||
listedPreAuthKeys[3].Id,
|
||||
},
|
||||
)
|
||||
|
||||
// New keys show prefix after listing, so check the created keys instead
|
||||
assert.NotEmpty(t, keys[0].GetKey())
|
||||
assert.NotEmpty(t, keys[1].GetKey())
|
||||
assert.NotEmpty(t, keys[2].GetKey())
|
||||
assert.NotEmpty(t, keys[0].Key)
|
||||
assert.NotEmpty(t, keys[1].Key)
|
||||
assert.NotEmpty(t, keys[2].Key)
|
||||
|
||||
assert.True(t, listedPreAuthKeys[1].GetExpiration().AsTime().After(time.Now()))
|
||||
assert.True(t, listedPreAuthKeys[2].GetExpiration().AsTime().After(time.Now()))
|
||||
assert.True(t, listedPreAuthKeys[3].GetExpiration().AsTime().After(time.Now()))
|
||||
assert.True(t, listedPreAuthKeys[1].Expiration.After(time.Now()))
|
||||
assert.True(t, listedPreAuthKeys[2].Expiration.After(time.Now()))
|
||||
assert.True(t, listedPreAuthKeys[3].Expiration.After(time.Now()))
|
||||
|
||||
assert.True(
|
||||
t,
|
||||
listedPreAuthKeys[1].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
|
||||
listedPreAuthKeys[1].Expiration.Before(time.Now().Add(time.Hour*26)),
|
||||
)
|
||||
assert.True(
|
||||
t,
|
||||
listedPreAuthKeys[2].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
|
||||
listedPreAuthKeys[2].Expiration.Before(time.Now().Add(time.Hour*26)),
|
||||
)
|
||||
assert.True(
|
||||
t,
|
||||
listedPreAuthKeys[3].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
|
||||
listedPreAuthKeys[3].Expiration.Before(time.Now().Add(time.Hour*26)),
|
||||
)
|
||||
|
||||
for index := range listedPreAuthKeys {
|
||||
@@ -128,7 +127,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||
assert.Equal(
|
||||
t,
|
||||
[]string{"tag:test1", "tag:test2"},
|
||||
listedPreAuthKeys[index].GetAclTags(),
|
||||
listedPreAuthKeys[index].AclTags,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -139,12 +138,12 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||
"preauthkeys",
|
||||
"expire",
|
||||
"--id",
|
||||
strconv.FormatUint(keys[0].GetId(), 10),
|
||||
keys[0].Id,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
var listedPreAuthKeysAfterExpire []v1.PreAuthKey
|
||||
var listedPreAuthKeysAfterExpire []clientv1.PreAuthKey
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(
|
||||
@@ -161,9 +160,9 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
||||
assert.NoError(c, err)
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth keys list after expire")
|
||||
|
||||
assert.True(t, listedPreAuthKeysAfterExpire[1].GetExpiration().AsTime().Before(time.Now()))
|
||||
assert.True(t, listedPreAuthKeysAfterExpire[2].GetExpiration().AsTime().After(time.Now()))
|
||||
assert.True(t, listedPreAuthKeysAfterExpire[3].GetExpiration().AsTime().After(time.Now()))
|
||||
assert.True(t, listedPreAuthKeysAfterExpire[1].Expiration.Before(time.Now()))
|
||||
assert.True(t, listedPreAuthKeysAfterExpire[2].Expiration.After(time.Now()))
|
||||
assert.True(t, listedPreAuthKeysAfterExpire[3].Expiration.After(time.Now()))
|
||||
}
|
||||
|
||||
func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
||||
@@ -185,7 +184,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
||||
headscale, err := scenario.Headscale()
|
||||
require.NoError(t, err)
|
||||
|
||||
var preAuthKey v1.PreAuthKey
|
||||
var preAuthKey clientv1.PreAuthKey
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(
|
||||
@@ -205,7 +204,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
||||
assert.NoError(c, err)
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth key creation without expiry")
|
||||
|
||||
var listedPreAuthKeys []v1.PreAuthKey
|
||||
var listedPreAuthKeys []clientv1.PreAuthKey
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(
|
||||
@@ -225,10 +224,10 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
||||
// There is one key created by [Scenario.CreateHeadscaleEnv]
|
||||
assert.Len(t, listedPreAuthKeys, 2)
|
||||
|
||||
assert.True(t, listedPreAuthKeys[1].GetExpiration().AsTime().After(time.Now()))
|
||||
assert.True(t, listedPreAuthKeys[1].Expiration.After(time.Now()))
|
||||
assert.True(
|
||||
t,
|
||||
listedPreAuthKeys[1].GetExpiration().AsTime().Before(time.Now().Add(time.Minute*70)),
|
||||
listedPreAuthKeys[1].Expiration.Before(time.Now().Add(time.Minute*70)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -251,7 +250,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
||||
headscale, err := scenario.Headscale()
|
||||
require.NoError(t, err)
|
||||
|
||||
var preAuthReusableKey v1.PreAuthKey
|
||||
var preAuthReusableKey clientv1.PreAuthKey
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(
|
||||
@@ -271,7 +270,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
||||
assert.NoError(c, err)
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for reusable preauth key creation")
|
||||
|
||||
var preAuthEphemeralKey v1.PreAuthKey
|
||||
var preAuthEphemeralKey clientv1.PreAuthKey
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(
|
||||
@@ -291,10 +290,10 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
||||
assert.NoError(c, err)
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for ephemeral preauth key creation")
|
||||
|
||||
assert.True(t, preAuthEphemeralKey.GetEphemeral())
|
||||
assert.False(t, preAuthEphemeralKey.GetReusable())
|
||||
assert.True(t, preAuthEphemeralKey.Ephemeral)
|
||||
assert.False(t, preAuthEphemeralKey.Reusable)
|
||||
|
||||
var listedPreAuthKeys []v1.PreAuthKey
|
||||
var listedPreAuthKeys []clientv1.PreAuthKey
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(
|
||||
@@ -325,7 +324,7 @@ func TestPreAuthKeyDeleteCommand(t *testing.T) {
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
// Create a key to delete.
|
||||
created := assertJSONRoundtrip[*v1.PreAuthKey](t, headscale, []string{
|
||||
created := assertJSONRoundtrip[*clientv1.PreAuthKey](t, headscale, []string{
|
||||
"headscale",
|
||||
"preauthkeys",
|
||||
"--user", "1",
|
||||
@@ -333,7 +332,7 @@ func TestPreAuthKeyDeleteCommand(t *testing.T) {
|
||||
"--reusable",
|
||||
"--output", "json",
|
||||
})
|
||||
require.NotZero(t, created.GetId())
|
||||
require.NotEmpty(t, created.Id)
|
||||
|
||||
// delete with no --id must be rejected.
|
||||
_, err := headscale.Execute([]string{"headscale", "preauthkeys", "delete"})
|
||||
@@ -342,13 +341,13 @@ func TestPreAuthKeyDeleteCommand(t *testing.T) {
|
||||
// delete the created key by id.
|
||||
_, err = headscale.Execute([]string{
|
||||
"headscale", "preauthkeys", "delete",
|
||||
"--id", strconv.FormatUint(created.GetId(), 10),
|
||||
"--id", created.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// The deleted key must be gone from the list.
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
var listed []v1.PreAuthKey
|
||||
var listed []clientv1.PreAuthKey
|
||||
|
||||
err := executeAndUnmarshal(headscale,
|
||||
[]string{"headscale", "preauthkeys", "list", "--output", "json"},
|
||||
@@ -357,7 +356,7 @@ func TestPreAuthKeyDeleteCommand(t *testing.T) {
|
||||
assert.NoError(c, err)
|
||||
|
||||
for i := range listed {
|
||||
assert.NotEqual(c, created.GetId(), listed[i].GetId(), "deleted key should not be listed")
|
||||
assert.NotEqual(c, created.Id, listed[i].Id, "deleted key should not be listed")
|
||||
}
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth key list after delete")
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package integration
|
||||
import (
|
||||
"testing"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -20,10 +20,10 @@ func TestServerInfoCommands(t *testing.T) {
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
t.Run("health", func(t *testing.T) {
|
||||
health := assertJSONRoundtrip[*v1.HealthResponse](t, headscale, []string{
|
||||
health := assertJSONRoundtrip[*clientv1.HealthResponseBody](t, headscale, []string{
|
||||
"headscale", "health", "--output", "json",
|
||||
})
|
||||
assert.True(t, health.GetDatabaseConnectivity(), "database should be reachable")
|
||||
assert.True(t, health.DatabaseConnectivity, "database should be reachable")
|
||||
})
|
||||
|
||||
t.Run("version", func(t *testing.T) {
|
||||
|
||||
+9
-11
@@ -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,
|
||||
|
||||
@@ -2,14 +2,13 @@ package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tcmp "github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/integration/hsic"
|
||||
"github.com/juanfont/headscale/integration/integrationutil"
|
||||
"github.com/juanfont/headscale/integration/tsic"
|
||||
@@ -36,7 +35,7 @@ func TestUserCommand(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
var (
|
||||
listUsers []*v1.User
|
||||
listUsers []*clientv1.User
|
||||
result []string
|
||||
)
|
||||
|
||||
@@ -54,7 +53,7 @@ func TestUserCommand(t *testing.T) {
|
||||
assert.NoError(ct, err)
|
||||
|
||||
slices.SortFunc(listUsers, sortWithID)
|
||||
result = []string{listUsers[0].GetName(), listUsers[1].GetName()}
|
||||
result = []string{listUsers[0].Name, listUsers[1].Name}
|
||||
|
||||
assert.Equal(
|
||||
ct,
|
||||
@@ -70,13 +69,13 @@ func TestUserCommand(t *testing.T) {
|
||||
"users",
|
||||
"rename",
|
||||
"--output=json",
|
||||
fmt.Sprintf("--identifier=%d", listUsers[1].GetId()),
|
||||
"--identifier=" + listUsers[1].Id,
|
||||
"--new-name=newname",
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
var listAfterRenameUsers []*v1.User
|
||||
var listAfterRenameUsers []*clientv1.User
|
||||
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
err := executeAndUnmarshal(headscale,
|
||||
@@ -92,7 +91,7 @@ func TestUserCommand(t *testing.T) {
|
||||
assert.NoError(ct, err)
|
||||
|
||||
slices.SortFunc(listAfterRenameUsers, sortWithID)
|
||||
result = []string{listAfterRenameUsers[0].GetName(), listAfterRenameUsers[1].GetName()}
|
||||
result = []string{listAfterRenameUsers[0].Name, listAfterRenameUsers[1].Name}
|
||||
|
||||
assert.Equal(
|
||||
ct,
|
||||
@@ -102,7 +101,7 @@ func TestUserCommand(t *testing.T) {
|
||||
)
|
||||
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
|
||||
|
||||
var listByUsername []*v1.User
|
||||
var listByUsername []*clientv1.User
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(headscale,
|
||||
@@ -121,19 +120,19 @@ func TestUserCommand(t *testing.T) {
|
||||
|
||||
slices.SortFunc(listByUsername, sortWithID)
|
||||
|
||||
want := []*v1.User{
|
||||
want := []*clientv1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Id: "1",
|
||||
Name: "user1",
|
||||
Email: "user1@test.no",
|
||||
},
|
||||
}
|
||||
|
||||
if diff := tcmp.Diff(want, listByUsername, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
||||
if diff := tcmp.Diff(want, listByUsername, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
|
||||
t.Errorf("unexpected users (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
var listByID []*v1.User
|
||||
var listByID []*clientv1.User
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(headscale,
|
||||
@@ -152,15 +151,15 @@ func TestUserCommand(t *testing.T) {
|
||||
|
||||
slices.SortFunc(listByID, sortWithID)
|
||||
|
||||
want = []*v1.User{
|
||||
want = []*clientv1.User{
|
||||
{
|
||||
Id: 1,
|
||||
Id: "1",
|
||||
Name: "user1",
|
||||
Email: "user1@test.no",
|
||||
},
|
||||
}
|
||||
|
||||
if diff := tcmp.Diff(want, listByID, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
||||
if diff := tcmp.Diff(want, listByID, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
|
||||
t.Errorf("unexpected users (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
@@ -177,7 +176,7 @@ func TestUserCommand(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, deleteResult, "User destroyed")
|
||||
|
||||
var listAfterIDDelete []*v1.User
|
||||
var listAfterIDDelete []*clientv1.User
|
||||
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
err := executeAndUnmarshal(headscale,
|
||||
@@ -194,15 +193,15 @@ func TestUserCommand(t *testing.T) {
|
||||
|
||||
slices.SortFunc(listAfterIDDelete, sortWithID)
|
||||
|
||||
want := []*v1.User{
|
||||
want := []*clientv1.User{
|
||||
{
|
||||
Id: 2,
|
||||
Id: "2",
|
||||
Name: "newname",
|
||||
Email: "user2@test.no",
|
||||
},
|
||||
}
|
||||
|
||||
if diff := tcmp.Diff(want, listAfterIDDelete, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
||||
if diff := tcmp.Diff(want, listAfterIDDelete, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
|
||||
assert.Fail(ct, "unexpected users", "diff (-want +got):\n%s", diff)
|
||||
}
|
||||
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
|
||||
@@ -219,7 +218,7 @@ func TestUserCommand(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, deleteResult, "User destroyed")
|
||||
|
||||
var listAfterNameDelete []v1.User
|
||||
var listAfterNameDelete []clientv1.User
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(headscale,
|
||||
@@ -249,8 +248,8 @@ func TestUserCreateCommand(t *testing.T) {
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
// Create a user populated with every optional field. The created user is
|
||||
// returned on stdout and round-tripped through the v1.User type.
|
||||
created := assertJSONRoundtrip[*v1.User](t, headscale, []string{
|
||||
// returned on stdout and round-tripped through the User type.
|
||||
created := assertJSONRoundtrip[*clientv1.User](t, headscale, []string{
|
||||
"headscale",
|
||||
"users",
|
||||
"create",
|
||||
@@ -261,14 +260,14 @@ func TestUserCreateCommand(t *testing.T) {
|
||||
"--output", "json",
|
||||
})
|
||||
|
||||
assert.Equal(t, "cli-created", created.GetName())
|
||||
assert.Equal(t, "CLI Created", created.GetDisplayName())
|
||||
assert.Equal(t, "cli-created@example.com", created.GetEmail())
|
||||
assert.Equal(t, "https://example.com/avatar.png", created.GetProfilePicUrl())
|
||||
assert.Equal(t, "cli-created", created.Name)
|
||||
assert.Equal(t, "CLI Created", created.DisplayName)
|
||||
assert.Equal(t, "cli-created@example.com", created.Email)
|
||||
assert.Equal(t, "https://example.com/avatar.png", created.ProfilePicUrl)
|
||||
|
||||
// The created fields must survive a list query (read-after-write) and be
|
||||
// filterable by email.
|
||||
var byEmail []*v1.User
|
||||
var byEmail []*clientv1.User
|
||||
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
err := executeAndUnmarshal(headscale,
|
||||
@@ -286,8 +285,8 @@ func TestUserCreateCommand(t *testing.T) {
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for user list by email")
|
||||
|
||||
require.Len(t, byEmail, 1)
|
||||
assert.Equal(t, "cli-created", byEmail[0].GetName())
|
||||
assert.Equal(t, "CLI Created", byEmail[0].GetDisplayName())
|
||||
assert.Equal(t, "cli-created", byEmail[0].Name)
|
||||
assert.Equal(t, "CLI Created", byEmail[0].DisplayName)
|
||||
}
|
||||
|
||||
// TestUserCommandValidation exercises the validation and error permutations of
|
||||
@@ -325,7 +324,7 @@ func TestUserCommandValidation(t *testing.T) {
|
||||
case tt.wantEmptyList:
|
||||
require.NoError(t, err)
|
||||
|
||||
var users []v1.User
|
||||
var users []clientv1.User
|
||||
|
||||
require.NoError(t, json.Unmarshal([]byte(out), &users))
|
||||
require.Empty(t, users)
|
||||
|
||||
+11
-11
@@ -3,7 +3,7 @@ package integration
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol"
|
||||
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
@@ -24,19 +24,19 @@ type ControlServer interface {
|
||||
GetEndpoint() string
|
||||
WaitForRunning() error
|
||||
Restart() error
|
||||
CreateUser(user string) (*v1.User, error)
|
||||
CreateAuthKey(user uint64, reusable bool, ephemeral bool) (*v1.PreAuthKey, error)
|
||||
CreateAuthKeyWithTags(user uint64, reusable bool, ephemeral bool, tags []string) (*v1.PreAuthKey, error)
|
||||
CreateAuthKeyWithOptions(opts hsic.AuthKeyOptions) (*v1.PreAuthKey, error)
|
||||
CreateUser(user string) (*clientv1.User, error)
|
||||
CreateAuthKey(user uint64, reusable bool, ephemeral bool) (*clientv1.PreAuthKey, error)
|
||||
CreateAuthKeyWithTags(user uint64, reusable bool, ephemeral bool, tags []string) (*clientv1.PreAuthKey, error)
|
||||
CreateAuthKeyWithOptions(opts hsic.AuthKeyOptions) (*clientv1.PreAuthKey, error)
|
||||
DeleteAuthKey(id uint64) error
|
||||
ListNodes(users ...string) ([]*v1.Node, error)
|
||||
ListNodes(users ...string) ([]*clientv1.Node, error)
|
||||
DeleteNode(nodeID uint64) error
|
||||
NodesByUser() (map[string][]*v1.Node, error)
|
||||
NodesByName() (map[string]*v1.Node, error)
|
||||
ListUsers() ([]*v1.User, error)
|
||||
MapUsers() (map[string]*v1.User, error)
|
||||
NodesByUser() (map[string][]*clientv1.Node, error)
|
||||
NodesByName() (map[string]*clientv1.Node, error)
|
||||
ListUsers() ([]*clientv1.User, error)
|
||||
MapUsers() (map[string]*clientv1.User, error)
|
||||
DeleteUser(userID uint64) error
|
||||
ApproveRoutes(nodeID uint64, routes []netip.Prefix) (*v1.Node, error)
|
||||
ApproveRoutes(nodeID uint64, routes []netip.Prefix) (*clientv1.Node, error)
|
||||
SetNodeTags(nodeID uint64, tags []string) error
|
||||
GetCert() []byte
|
||||
GetHostname() string
|
||||
|
||||
+53
-53
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/integration/hsic"
|
||||
"github.com/juanfont/headscale/integration/integrationutil"
|
||||
@@ -169,12 +169,12 @@ func testEphemeralWithOptions(t *testing.T, opts ...hsic.Option) {
|
||||
t.Fatalf("failed to create tailscale nodes in user %s: %s", userName, err)
|
||||
}
|
||||
|
||||
key, err := scenario.CreatePreAuthKey(user.GetId(), true, true)
|
||||
key, err := scenario.CreatePreAuthKey(mustParseID(user.Id), true, true)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
|
||||
}
|
||||
|
||||
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
|
||||
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.Key)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run tailscale up for user %s: %s", userName, err)
|
||||
}
|
||||
@@ -247,12 +247,12 @@ func TestEphemeral2006DeletedTooQuickly(t *testing.T) {
|
||||
t.Fatalf("failed to create tailscale nodes in user %s: %s", userName, err)
|
||||
}
|
||||
|
||||
key, err := scenario.CreatePreAuthKey(user.GetId(), true, true)
|
||||
key, err := scenario.CreatePreAuthKey(mustParseID(user.Id), true, true)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
|
||||
}
|
||||
|
||||
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
|
||||
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.Key)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run tailscale up for user %s: %s", userName, err)
|
||||
}
|
||||
@@ -402,7 +402,7 @@ func TestTaildrop(t *testing.T) {
|
||||
network := networks[0]
|
||||
|
||||
// Create untagged nodes for user1 using all test versions
|
||||
user1Key, err := scenario.CreatePreAuthKey(userMap["user1"].GetId(), true, false)
|
||||
user1Key, err := scenario.CreatePreAuthKey(mustParseID(userMap["user1"].Id), true, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
var user1Clients []TailscaleClient
|
||||
@@ -414,7 +414,7 @@ func TestTaildrop(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.Login(headscale.GetEndpoint(), user1Key.GetKey())
|
||||
err = client.Login(headscale.GetEndpoint(), user1Key.Key)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.WaitForRunning(integrationutil.PeerSyncTimeout())
|
||||
@@ -425,7 +425,7 @@ func TestTaildrop(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create untagged nodes for user2 using all test versions
|
||||
user2Key, err := scenario.CreatePreAuthKey(userMap["user2"].GetId(), true, false)
|
||||
user2Key, err := scenario.CreatePreAuthKey(mustParseID(userMap["user2"].Id), true, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
var user2Clients []TailscaleClient
|
||||
@@ -437,7 +437,7 @@ func TestTaildrop(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.Login(headscale.GetEndpoint(), user2Key.GetKey())
|
||||
err = client.Login(headscale.GetEndpoint(), user2Key.Key)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.WaitForRunning(integrationutil.PeerSyncTimeout())
|
||||
@@ -449,7 +449,7 @@ func TestTaildrop(t *testing.T) {
|
||||
|
||||
// Create a tagged device (tags-as-identity: tags come from PreAuthKey)
|
||||
// Use "head" version to test latest behavior
|
||||
taggedKey, err := scenario.CreatePreAuthKeyWithTags(userMap["user1"].GetId(), true, false, []string{"tag:server"})
|
||||
taggedKey, err := scenario.CreatePreAuthKeyWithTags(mustParseID(userMap["user1"].Id), true, false, []string{"tag:server"})
|
||||
require.NoError(t, err)
|
||||
|
||||
taggedClient, err := scenario.CreateTailscaleNode(
|
||||
@@ -458,7 +458,7 @@ func TestTaildrop(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = taggedClient.Login(headscale.GetEndpoint(), taggedKey.GetKey())
|
||||
err = taggedClient.Login(headscale.GetEndpoint(), taggedKey.Key)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = taggedClient.WaitForRunning(integrationutil.PeerSyncTimeout())
|
||||
@@ -767,8 +767,8 @@ func TestUpdateHostnameFromClient(t *testing.T) {
|
||||
// Pre-rewrite these were rejected by ApplyHostnameFromHostInfo with
|
||||
// "invalid characters" and the node was stuck on an invalid-<rand>
|
||||
// GivenName with the HostName update dropped. The assertions below
|
||||
// verify both raw preservation ([v1.Node.Name]) and SaaS-matching sanitisation
|
||||
// ([v1.Node.GivenName]) for each awkward input.
|
||||
// verify both raw preservation ([clientv1.Node.Name]) and SaaS-matching sanitisation
|
||||
// ([clientv1.Node.GivenName]) for each awkward input.
|
||||
hostnames := map[string]string{
|
||||
"1": "Joe's Mac mini",
|
||||
"2": "Test@Host",
|
||||
@@ -814,7 +814,7 @@ func TestUpdateHostnameFromClient(t *testing.T) {
|
||||
|
||||
// Wait for nodestore batch processing to complete
|
||||
// [state.NodeStore] batching timeout is 500ms, so we wait up to 1 second
|
||||
var nodes []*v1.Node
|
||||
var nodes []*clientv1.Node
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
err := executeAndUnmarshal(
|
||||
headscale,
|
||||
@@ -831,18 +831,18 @@ func TestUpdateHostnameFromClient(t *testing.T) {
|
||||
assert.Len(ct, nodes, 3, "Should have 3 nodes after hostname updates")
|
||||
|
||||
for _, node := range nodes {
|
||||
hostname := hostnames[strconv.FormatUint(node.GetId(), 10)]
|
||||
assert.Equal(ct, hostname, node.GetName(), "Node name should match hostname")
|
||||
hostname := hostnames[node.Id]
|
||||
assert.Equal(ct, hostname, node.Name, "Node name should match hostname")
|
||||
|
||||
// GivenName is sanitised via [dnsname.SanitizeHostname] (SaaS algorithm).
|
||||
assert.Equal(ct, dnsname.SanitizeHostname(hostname), node.GetGivenName(),
|
||||
assert.Equal(ct, dnsname.SanitizeHostname(hostname), node.GivenName,
|
||||
"Given name should match SaaS hostname-sanitisation rules")
|
||||
}
|
||||
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
|
||||
|
||||
// Rename givenName in nodes
|
||||
for _, node := range nodes {
|
||||
givenName := fmt.Sprintf("%d-givenname", node.GetId())
|
||||
givenName := fmt.Sprintf("%s-givenname", node.Id)
|
||||
_, err = headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
@@ -850,7 +850,7 @@ func TestUpdateHostnameFromClient(t *testing.T) {
|
||||
"rename",
|
||||
givenName,
|
||||
"--identifier",
|
||||
strconv.FormatUint(node.GetId(), 10),
|
||||
node.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -860,8 +860,8 @@ func TestUpdateHostnameFromClient(t *testing.T) {
|
||||
// Build a map of expected DNSNames by node ID
|
||||
expectedDNSNames := make(map[string]string)
|
||||
for _, node := range nodes {
|
||||
nodeID := strconv.FormatUint(node.GetId(), 10)
|
||||
expectedDNSNames[nodeID] = fmt.Sprintf("%d-givenname.headscale.net.", node.GetId())
|
||||
nodeID := node.Id
|
||||
expectedDNSNames[nodeID] = fmt.Sprintf("%s-givenname.headscale.net.", node.Id)
|
||||
}
|
||||
|
||||
// Verify from each client's perspective
|
||||
@@ -931,9 +931,9 @@ func TestUpdateHostnameFromClient(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, node := range nodes {
|
||||
hostname := hostnames[strconv.FormatUint(node.GetId(), 10)]
|
||||
givenName := fmt.Sprintf("%d-givenname", node.GetId())
|
||||
if node.GetName() != hostname+"NEW" || node.GetGivenName() != givenName {
|
||||
hostname := hostnames[node.Id]
|
||||
givenName := fmt.Sprintf("%s-givenname", node.Id)
|
||||
if node.Name != hostname+"NEW" || node.GivenName != givenName {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -993,15 +993,15 @@ func TestExpireNode(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var node v1.Node
|
||||
var node clientv1.Node
|
||||
err = json.Unmarshal([]byte(result), &node)
|
||||
require.NoError(t, err)
|
||||
|
||||
var expiredNodeKey key.NodePublic
|
||||
err = expiredNodeKey.UnmarshalText([]byte(node.GetNodeKey()))
|
||||
err = expiredNodeKey.UnmarshalText([]byte(node.NodeKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("Node %s with node_key %s has been expired", node.GetName(), expiredNodeKey.String())
|
||||
t.Logf("Node %s with node_key %s has been expired", node.Name, expiredNodeKey.String())
|
||||
|
||||
// Verify that the expired node has been marked in all peers list.
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
@@ -1009,7 +1009,7 @@ func TestExpireNode(t *testing.T) {
|
||||
status, err := client.Status()
|
||||
assert.NoError(ct, err)
|
||||
|
||||
if client.Hostname() != node.GetName() {
|
||||
if client.Hostname() != node.Name {
|
||||
// Check if the expired node appears as expired in this client's peer list
|
||||
for key, peer := range status.Peer {
|
||||
if key == expiredNodeKey {
|
||||
@@ -1025,7 +1025,7 @@ func TestExpireNode(t *testing.T) {
|
||||
|
||||
// Verify that the expired node has been marked in all peers list.
|
||||
for _, client := range allClients {
|
||||
if client.Hostname() == node.GetName() {
|
||||
if client.Hostname() == node.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1060,11 +1060,11 @@ func TestExpireNode(t *testing.T) {
|
||||
peerStatus.Expired,
|
||||
)
|
||||
|
||||
_, stderr, _ := client.Execute([]string{"tailscale", "ping", node.GetName()})
|
||||
_, stderr, _ := client.Execute([]string{"tailscale", "ping", node.Name})
|
||||
if !strings.Contains(stderr, "node key has expired") {
|
||||
c.Errorf(
|
||||
"expected to be unable to ping expired host %q from %q",
|
||||
node.GetName(),
|
||||
node.Name,
|
||||
client.Hostname(),
|
||||
)
|
||||
}
|
||||
@@ -1111,19 +1111,19 @@ func TestSetNodeExpiryInFuture(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
var node v1.Node
|
||||
var node clientv1.Node
|
||||
err = json.Unmarshal([]byte(result), &node)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, node.GetExpiry().AsTime().After(time.Now()))
|
||||
require.WithinDuration(t, targetExpiry, node.GetExpiry().AsTime(), 2*time.Second)
|
||||
require.True(t, node.Expiry.After(time.Now()))
|
||||
require.WithinDuration(t, targetExpiry, *node.Expiry, 2*time.Second)
|
||||
|
||||
var nodeKey key.NodePublic
|
||||
err = nodeKey.UnmarshalText([]byte(node.GetNodeKey()))
|
||||
err = nodeKey.UnmarshalText([]byte(node.NodeKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, client := range allClients {
|
||||
if client.Hostname() == node.GetName() {
|
||||
if client.Hostname() == node.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1208,10 +1208,10 @@ func TestDisableNodeExpiry(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
var node v1.Node
|
||||
var node clientv1.Node
|
||||
err = json.Unmarshal([]byte(result), &node)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, node.GetExpiry(), "node should have an expiry set")
|
||||
require.NotNil(t, node.Expiry, "node should have an expiry set")
|
||||
|
||||
// Now disable the expiry.
|
||||
result, err = headscale.Execute(
|
||||
@@ -1224,23 +1224,23 @@ func TestDisableNodeExpiry(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
var nodeDisabled v1.Node
|
||||
var nodeDisabled clientv1.Node
|
||||
err = json.Unmarshal([]byte(result), &nodeDisabled)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Expiry should be nil (or zero time) when disabled.
|
||||
if nodeDisabled.GetExpiry() != nil {
|
||||
require.True(t, nodeDisabled.GetExpiry().AsTime().IsZero(),
|
||||
if nodeDisabled.Expiry != nil {
|
||||
require.True(t, nodeDisabled.Expiry.IsZero(),
|
||||
"node expiry should be zero/nil after disabling")
|
||||
}
|
||||
|
||||
var nodeKey key.NodePublic
|
||||
err = nodeKey.UnmarshalText([]byte(nodeDisabled.GetNodeKey()))
|
||||
err = nodeKey.UnmarshalText([]byte(nodeDisabled.NodeKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify peers see the node as not expired.
|
||||
for _, client := range allClients {
|
||||
if client.Hostname() == nodeDisabled.GetName() {
|
||||
if client.Hostname() == nodeDisabled.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1332,7 +1332,7 @@ func TestNodeOnlineStatus(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
var nodes []*v1.Node
|
||||
var nodes []*clientv1.Node
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
result, err := headscale.Execute([]string{
|
||||
"headscale", "nodes", "list", "--output", "json",
|
||||
@@ -1347,9 +1347,9 @@ func TestNodeOnlineStatus(t *testing.T) {
|
||||
// All nodes should be online
|
||||
assert.Truef(
|
||||
ct,
|
||||
node.GetOnline(),
|
||||
node.Online,
|
||||
"expected %s to have online status in Headscale, marked as offline %s after start",
|
||||
node.GetName(),
|
||||
node.Name,
|
||||
time.Since(start),
|
||||
)
|
||||
}
|
||||
@@ -1537,7 +1537,7 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test list all nodes after added otherUser
|
||||
var nodeList []v1.Node
|
||||
var nodeList []clientv1.Node
|
||||
err = executeAndUnmarshal(
|
||||
headscale,
|
||||
[]string{
|
||||
@@ -1551,8 +1551,8 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, nodeList, 2)
|
||||
assert.True(t, nodeList[0].GetOnline())
|
||||
assert.True(t, nodeList[1].GetOnline())
|
||||
assert.True(t, nodeList[0].Online)
|
||||
assert.True(t, nodeList[1].Online)
|
||||
|
||||
// Delete the first node, which is online
|
||||
_, err = headscale.Execute(
|
||||
@@ -1562,7 +1562,7 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
|
||||
"delete",
|
||||
"--identifier",
|
||||
// Delete the last added machine
|
||||
fmt.Sprintf("%d", nodeList[0].GetId()),
|
||||
nodeList[0].Id,
|
||||
"--output",
|
||||
"json",
|
||||
"--force",
|
||||
@@ -1571,7 +1571,7 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Ensure that the node has been deleted, this did not occur due to a panic.
|
||||
var nodeListAfter []v1.Node
|
||||
var nodeListAfter []clientv1.Node
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
err = executeAndUnmarshal(
|
||||
headscale,
|
||||
@@ -1601,6 +1601,6 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, nodeListAfter, 1)
|
||||
assert.True(t, nodeListAfter[0].GetOnline())
|
||||
assert.Equal(t, nodeList[1].GetId(), nodeListAfter[0].GetId())
|
||||
assert.True(t, nodeListAfter[0].Online)
|
||||
assert.Equal(t, nodeList[1].Id, nodeListAfter[0].Id)
|
||||
}
|
||||
|
||||
@@ -159,10 +159,10 @@ func TestGrantCapRelay(t *testing.T) {
|
||||
defer func() { _, _, _ = relayR.Shutdown() }()
|
||||
|
||||
pakRelay, err := scenario.CreatePreAuthKeyWithTags(
|
||||
userMap["relay"].GetId(), false, false, []string{"tag:relay"},
|
||||
mustParseID(userMap["relay"].Id), false, false, []string{"tag:relay"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = relayR.Login(headscale.GetEndpoint(), pakRelay.GetKey())
|
||||
err = relayR.Login(headscale.GetEndpoint(), pakRelay.Key)
|
||||
require.NoError(t, err)
|
||||
err = relayR.WaitForRunning(30 * time.Second)
|
||||
require.NoError(t, err)
|
||||
@@ -192,10 +192,10 @@ func TestGrantCapRelay(t *testing.T) {
|
||||
defer func() { _, _, _ = clientA.Shutdown() }()
|
||||
|
||||
pakClientA, err := scenario.CreatePreAuthKeyWithTags(
|
||||
userMap["clienta"].GetId(), false, false, []string{"tag:client-a"},
|
||||
mustParseID(userMap["clienta"].Id), false, false, []string{"tag:client-a"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = clientA.Login(headscale.GetEndpoint(), pakClientA.GetKey())
|
||||
err = clientA.Login(headscale.GetEndpoint(), pakClientA.Key)
|
||||
require.NoError(t, err)
|
||||
err = clientA.WaitForRunning(30 * time.Second)
|
||||
require.NoError(t, err)
|
||||
@@ -209,10 +209,10 @@ func TestGrantCapRelay(t *testing.T) {
|
||||
defer func() { _, _, _ = clientB.Shutdown() }()
|
||||
|
||||
pakClientB, err := scenario.CreatePreAuthKeyWithTags(
|
||||
userMap["clientb"].GetId(), false, false, []string{"tag:client-b"},
|
||||
mustParseID(userMap["clientb"].Id), false, false, []string{"tag:client-b"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = clientB.Login(headscale.GetEndpoint(), pakClientB.GetKey())
|
||||
err = clientB.Login(headscale.GetEndpoint(), pakClientB.Key)
|
||||
require.NoError(t, err)
|
||||
err = clientB.WaitForRunning(30 * time.Second)
|
||||
require.NoError(t, err)
|
||||
@@ -637,10 +637,10 @@ func TestGrantCapDrive(t *testing.T) {
|
||||
defer func() { _, _, _ = sharer.Shutdown() }()
|
||||
|
||||
pakSharer, err := scenario.CreatePreAuthKeyWithTags(
|
||||
userMap["sharer"].GetId(), false, false, []string{"tag:sharer"},
|
||||
mustParseID(userMap["sharer"].Id), false, false, []string{"tag:sharer"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = sharer.Login(headscale.GetEndpoint(), pakSharer.GetKey())
|
||||
err = sharer.Login(headscale.GetEndpoint(), pakSharer.Key)
|
||||
require.NoError(t, err)
|
||||
err = sharer.WaitForRunning(30 * time.Second)
|
||||
require.NoError(t, err)
|
||||
@@ -654,10 +654,10 @@ func TestGrantCapDrive(t *testing.T) {
|
||||
defer func() { _, _, _ = rwClient.Shutdown() }()
|
||||
|
||||
pakRW, err := scenario.CreatePreAuthKeyWithTags(
|
||||
userMap["rwclient"].GetId(), false, false, []string{"tag:rw-client"},
|
||||
mustParseID(userMap["rwclient"].Id), false, false, []string{"tag:rw-client"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = rwClient.Login(headscale.GetEndpoint(), pakRW.GetKey())
|
||||
err = rwClient.Login(headscale.GetEndpoint(), pakRW.Key)
|
||||
require.NoError(t, err)
|
||||
err = rwClient.WaitForRunning(30 * time.Second)
|
||||
require.NoError(t, err)
|
||||
@@ -671,10 +671,10 @@ func TestGrantCapDrive(t *testing.T) {
|
||||
defer func() { _, _, _ = roClient.Shutdown() }()
|
||||
|
||||
pakRO, err := scenario.CreatePreAuthKeyWithTags(
|
||||
userMap["roclient"].GetId(), false, false, []string{"tag:ro-client"},
|
||||
mustParseID(userMap["roclient"].Id), false, false, []string{"tag:ro-client"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = roClient.Login(headscale.GetEndpoint(), pakRO.GetKey())
|
||||
err = roClient.Login(headscale.GetEndpoint(), pakRO.Key)
|
||||
require.NoError(t, err)
|
||||
err = roClient.WaitForRunning(30 * time.Second)
|
||||
require.NoError(t, err)
|
||||
@@ -688,10 +688,10 @@ func TestGrantCapDrive(t *testing.T) {
|
||||
defer func() { _, _, _ = noAccess.Shutdown() }()
|
||||
|
||||
pakNA, err := scenario.CreatePreAuthKeyWithTags(
|
||||
userMap["noaccess"].GetId(), false, false, []string{"tag:no-access"},
|
||||
mustParseID(userMap["noaccess"].Id), false, false, []string{"tag:no-access"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = noAccess.Login(headscale.GetEndpoint(), pakNA.GetKey())
|
||||
err = noAccess.Login(headscale.GetEndpoint(), pakNA.Key)
|
||||
require.NoError(t, err)
|
||||
err = noAccess.WaitForRunning(30 * time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
+23
-10
@@ -18,7 +18,7 @@ import (
|
||||
|
||||
"github.com/cenkalti/backoff/v5"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
@@ -538,15 +538,15 @@ func requireAllClientsNetInfoAndDERP(t *testing.T, headscale ControlServer, expe
|
||||
|
||||
// assertLastSeenSet validates that a node has a non-nil LastSeen timestamp.
|
||||
// Critical for ensuring node activity tracking is functioning properly.
|
||||
func assertLastSeenSet(t *testing.T, node *v1.Node) {
|
||||
func assertLastSeenSet(t *testing.T, node *clientv1.Node) {
|
||||
t.Helper()
|
||||
assert.NotNil(t, node)
|
||||
assert.NotNil(t, node.GetLastSeen())
|
||||
assert.NotNil(t, node.LastSeen)
|
||||
}
|
||||
|
||||
func assertLastSeenSetWithCollect(c *assert.CollectT, node *v1.Node) {
|
||||
func assertLastSeenSetWithCollect(c *assert.CollectT, node *clientv1.Node) {
|
||||
assert.NotNil(c, node)
|
||||
assert.NotNil(c, node.GetLastSeen())
|
||||
assert.NotNil(c, node.LastSeen)
|
||||
}
|
||||
|
||||
// assertCurlSuccessWithCollect asserts that a curl request succeeds with
|
||||
@@ -1071,14 +1071,14 @@ func oidcMockUser(username string, emailVerified bool) mockoidc.MockUser {
|
||||
|
||||
// GetUserByName retrieves a user by name from the headscale server.
|
||||
// This is a common pattern used when creating preauth keys or managing users.
|
||||
func GetUserByName(headscale ControlServer, username string) (*v1.User, error) {
|
||||
func GetUserByName(headscale ControlServer, username string) (*clientv1.User, error) {
|
||||
users, err := headscale.ListUsers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing users: %w", err)
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
if u.GetName() == username {
|
||||
if u.Name == username {
|
||||
return u, nil
|
||||
}
|
||||
}
|
||||
@@ -1088,7 +1088,7 @@ func GetUserByName(headscale ControlServer, username string) (*v1.User, error) {
|
||||
|
||||
// findNode returns the first node in nodes for which match returns true,
|
||||
// or nil if no node matches.
|
||||
func findNode(nodes []*v1.Node, match func(*v1.Node) bool) *v1.Node {
|
||||
func findNode(nodes []*clientv1.Node, match func(*clientv1.Node) bool) *clientv1.Node {
|
||||
for _, n := range nodes {
|
||||
if match(n) {
|
||||
return n
|
||||
@@ -1098,6 +1098,19 @@ func findNode(nodes []*v1.Node, match func(*v1.Node) bool) *v1.Node {
|
||||
return nil
|
||||
}
|
||||
|
||||
// mustParseID parses a string ID emitted by the HTTP client types into a
|
||||
// uint64 for the APIs that still take numeric identifiers (NodeID, user and
|
||||
// key IDs). It panics on malformed input, which only happens if the server
|
||||
// emits a non-numeric ID — a bug worth failing the test loudly.
|
||||
func mustParseID(id string) uint64 {
|
||||
parsed, err := strconv.ParseUint(id, 10, 64)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("parsing id %q: %s", id, err))
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
// FindNewClient finds a client that is in the new list but not in the original list.
|
||||
// This is useful when dynamically adding nodes during tests and needing to identify
|
||||
// which client was just added.
|
||||
@@ -1177,13 +1190,13 @@ func (s *Scenario) AddAndLoginClient(
|
||||
return nil, fmt.Errorf("getting user: %w", err)
|
||||
}
|
||||
|
||||
authKey, err := s.CreatePreAuthKey(user.GetId(), true, false)
|
||||
authKey, err := s.CreatePreAuthKey(mustParseID(user.Id), true, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating preauth key: %w", err)
|
||||
}
|
||||
|
||||
// Login the new client
|
||||
err = newClient.Login(headscale.GetEndpoint(), authKey.GetKey())
|
||||
err = newClient.Login(headscale.GetEndpoint(), authKey.Key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("logging in new client: %w", err)
|
||||
}
|
||||
|
||||
+28
-25
@@ -24,7 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
"github.com/juanfont/headscale/hscontrol"
|
||||
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
@@ -1095,7 +1095,7 @@ func (t *HeadscaleInContainer) WaitForRunning() error {
|
||||
// CreateUser adds a new user to the Headscale instance.
|
||||
func (t *HeadscaleInContainer) CreateUser(
|
||||
user string,
|
||||
) (*v1.User, error) {
|
||||
) (*clientv1.User, error) {
|
||||
command := []string{
|
||||
binHeadscale,
|
||||
"users",
|
||||
@@ -1115,7 +1115,7 @@ func (t *HeadscaleInContainer) CreateUser(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var u v1.User
|
||||
var u clientv1.User
|
||||
|
||||
err = json.Unmarshal([]byte(result), &u)
|
||||
if err != nil {
|
||||
@@ -1140,7 +1140,7 @@ type AuthKeyOptions struct {
|
||||
|
||||
// CreateAuthKeyWithOptions creates a new "authorisation key" with the specified options.
|
||||
// This supports both user-owned and tags-only auth keys.
|
||||
func (t *HeadscaleInContainer) CreateAuthKeyWithOptions(opts AuthKeyOptions) (*v1.PreAuthKey, error) {
|
||||
func (t *HeadscaleInContainer) CreateAuthKeyWithOptions(opts AuthKeyOptions) (*clientv1.PreAuthKey, error) {
|
||||
command := []string{
|
||||
binHeadscale,
|
||||
}
|
||||
@@ -1181,7 +1181,7 @@ func (t *HeadscaleInContainer) CreateAuthKeyWithOptions(opts AuthKeyOptions) (*v
|
||||
return nil, fmt.Errorf("executing create auth key command: %w", err)
|
||||
}
|
||||
|
||||
var preAuthKey v1.PreAuthKey
|
||||
var preAuthKey clientv1.PreAuthKey
|
||||
|
||||
err = json.Unmarshal([]byte(result), &preAuthKey)
|
||||
if err != nil {
|
||||
@@ -1197,7 +1197,7 @@ func (t *HeadscaleInContainer) CreateAuthKey(
|
||||
user uint64,
|
||||
reusable bool,
|
||||
ephemeral bool,
|
||||
) (*v1.PreAuthKey, error) {
|
||||
) (*clientv1.PreAuthKey, error) {
|
||||
return t.CreateAuthKeyWithOptions(AuthKeyOptions{
|
||||
User: &user,
|
||||
Reusable: reusable,
|
||||
@@ -1212,7 +1212,7 @@ func (t *HeadscaleInContainer) CreateAuthKeyWithTags(
|
||||
reusable bool,
|
||||
ephemeral bool,
|
||||
tags []string,
|
||||
) (*v1.PreAuthKey, error) {
|
||||
) (*clientv1.PreAuthKey, error) {
|
||||
return t.CreateAuthKeyWithOptions(AuthKeyOptions{
|
||||
User: &user,
|
||||
Reusable: reusable,
|
||||
@@ -1252,8 +1252,8 @@ func (t *HeadscaleInContainer) DeleteAuthKey(
|
||||
// specific users.
|
||||
func (t *HeadscaleInContainer) ListNodes(
|
||||
users ...string,
|
||||
) ([]*v1.Node, error) {
|
||||
var ret []*v1.Node
|
||||
) ([]*clientv1.Node, error) {
|
||||
var ret []*clientv1.Node
|
||||
|
||||
execUnmarshal := func(command []string) error {
|
||||
result, _, err := dockertestutil.ExecuteCommand(
|
||||
@@ -1265,7 +1265,7 @@ func (t *HeadscaleInContainer) ListNodes(
|
||||
return fmt.Errorf("executing list node command: %w", err)
|
||||
}
|
||||
|
||||
var nodes []*v1.Node
|
||||
var nodes []*clientv1.Node
|
||||
|
||||
err = json.Unmarshal([]byte(result), &nodes)
|
||||
if err != nil {
|
||||
@@ -1293,8 +1293,11 @@ func (t *HeadscaleInContainer) ListNodes(
|
||||
}
|
||||
}
|
||||
|
||||
slices.SortFunc(ret, func(a, b *v1.Node) int {
|
||||
return cmp.Compare(a.GetId(), b.GetId())
|
||||
slices.SortFunc(ret, func(a, b *clientv1.Node) int {
|
||||
ai, _ := strconv.ParseUint(a.Id, 10, 64)
|
||||
bi, _ := strconv.ParseUint(b.Id, 10, 64)
|
||||
|
||||
return cmp.Compare(ai, bi)
|
||||
})
|
||||
|
||||
return ret, nil
|
||||
@@ -1324,37 +1327,37 @@ func (t *HeadscaleInContainer) DeleteNode(nodeID uint64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *HeadscaleInContainer) NodesByUser() (map[string][]*v1.Node, error) {
|
||||
func (t *HeadscaleInContainer) NodesByUser() (map[string][]*clientv1.Node, error) {
|
||||
nodes, err := t.ListNodes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userMap := make(map[string][]*v1.Node)
|
||||
userMap := make(map[string][]*clientv1.Node)
|
||||
for _, node := range nodes {
|
||||
name := node.GetUser().GetName()
|
||||
name := node.User.Name
|
||||
userMap[name] = append(userMap[name], node)
|
||||
}
|
||||
|
||||
return userMap, nil
|
||||
}
|
||||
|
||||
func (t *HeadscaleInContainer) NodesByName() (map[string]*v1.Node, error) {
|
||||
func (t *HeadscaleInContainer) NodesByName() (map[string]*clientv1.Node, error) {
|
||||
nodes, err := t.ListNodes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var nameMap map[string]*v1.Node
|
||||
var nameMap map[string]*clientv1.Node
|
||||
for _, node := range nodes {
|
||||
mak.Set(&nameMap, node.GetName(), node)
|
||||
mak.Set(&nameMap, node.Name, node)
|
||||
}
|
||||
|
||||
return nameMap, nil
|
||||
}
|
||||
|
||||
// ListUsers returns a list of users from Headscale.
|
||||
func (t *HeadscaleInContainer) ListUsers() ([]*v1.User, error) {
|
||||
func (t *HeadscaleInContainer) ListUsers() ([]*clientv1.User, error) {
|
||||
command := []string{binHeadscale, "users", "list", flagOutput, "json"}
|
||||
|
||||
result, _, err := dockertestutil.ExecuteCommand(
|
||||
@@ -1366,7 +1369,7 @@ func (t *HeadscaleInContainer) ListUsers() ([]*v1.User, error) {
|
||||
return nil, fmt.Errorf("executing list node command: %w", err)
|
||||
}
|
||||
|
||||
var users []*v1.User
|
||||
var users []*clientv1.User
|
||||
|
||||
err = json.Unmarshal([]byte(result), &users)
|
||||
if err != nil {
|
||||
@@ -1378,15 +1381,15 @@ func (t *HeadscaleInContainer) ListUsers() ([]*v1.User, error) {
|
||||
|
||||
// MapUsers returns a map of users from Headscale. It is keyed by the
|
||||
// user name.
|
||||
func (t *HeadscaleInContainer) MapUsers() (map[string]*v1.User, error) {
|
||||
func (t *HeadscaleInContainer) MapUsers() (map[string]*clientv1.User, error) {
|
||||
users, err := t.ListUsers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var userMap map[string]*v1.User
|
||||
var userMap map[string]*clientv1.User
|
||||
for _, user := range users {
|
||||
mak.Set(&userMap, user.GetName(), user)
|
||||
mak.Set(&userMap, user.Name, user)
|
||||
}
|
||||
|
||||
return userMap, nil
|
||||
@@ -1545,7 +1548,7 @@ func (h *HeadscaleInContainer) Restart() error {
|
||||
}
|
||||
|
||||
// ApproveRoutes approves routes for a node.
|
||||
func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (*v1.Node, error) {
|
||||
func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (*clientv1.Node, error) {
|
||||
command := []string{
|
||||
binHeadscale, "nodes", "approve-routes",
|
||||
flagOutput, "json",
|
||||
@@ -1567,7 +1570,7 @@ func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (
|
||||
)
|
||||
}
|
||||
|
||||
var node *v1.Node
|
||||
var node *clientv1.Node
|
||||
|
||||
err = json.Unmarshal([]byte(result), &node)
|
||||
if err != nil {
|
||||
|
||||
+145
-145
@@ -8,14 +8,13 @@ import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cmpdiff "github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
clientv1 "github.com/juanfont/headscale/gen/client/v1"
|
||||
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
@@ -87,7 +86,7 @@ func TestEnablingRoutes(t *testing.T) {
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
requireNoErrSync(t, err)
|
||||
|
||||
var nodes []*v1.Node
|
||||
var nodes []*clientv1.Node
|
||||
// Wait for route advertisements to propagate to [state.NodeStore]
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
@@ -96,9 +95,9 @@ func TestEnablingRoutes(t *testing.T) {
|
||||
assert.NoError(ct, err)
|
||||
|
||||
for _, node := range nodes {
|
||||
assert.Len(ct, node.GetAvailableRoutes(), 1)
|
||||
assert.Empty(ct, node.GetApprovedRoutes())
|
||||
assert.Empty(ct, node.GetSubnetRoutes())
|
||||
assert.Len(ct, node.AvailableRoutes, 1)
|
||||
assert.Empty(ct, node.ApprovedRoutes)
|
||||
assert.Empty(ct, node.SubnetRoutes)
|
||||
}
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), 100*time.Millisecond, "route advertisements should propagate to all nodes")
|
||||
|
||||
@@ -119,8 +118,8 @@ func TestEnablingRoutes(t *testing.T) {
|
||||
|
||||
for _, node := range nodes {
|
||||
_, err := headscale.ApproveRoutes(
|
||||
node.GetId(),
|
||||
util.MustStringsToPrefixes(node.GetAvailableRoutes()),
|
||||
mustParseID(node.Id),
|
||||
util.MustStringsToPrefixes(node.AvailableRoutes),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -133,9 +132,9 @@ func TestEnablingRoutes(t *testing.T) {
|
||||
assert.NoError(ct, err)
|
||||
|
||||
for _, node := range nodes {
|
||||
assert.Len(ct, node.GetAvailableRoutes(), 1)
|
||||
assert.Len(ct, node.GetApprovedRoutes(), 1)
|
||||
assert.Len(ct, node.GetSubnetRoutes(), 1)
|
||||
assert.Len(ct, node.AvailableRoutes, 1)
|
||||
assert.Len(ct, node.ApprovedRoutes, 1)
|
||||
assert.Len(ct, node.SubnetRoutes, 1)
|
||||
}
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), 100*time.Millisecond, "route approvals should propagate to all nodes")
|
||||
|
||||
@@ -181,18 +180,19 @@ func TestEnablingRoutes(t *testing.T) {
|
||||
assert.NoError(c, err)
|
||||
|
||||
for _, node := range nodes {
|
||||
if node.GetId() == 1 {
|
||||
assert.Len(c, node.GetAvailableRoutes(), 1) // 10.0.0.0/24
|
||||
assert.Len(c, node.GetApprovedRoutes(), 1) // 10.0.1.0/24
|
||||
assert.Empty(c, node.GetSubnetRoutes())
|
||||
} else if node.GetId() == 2 {
|
||||
assert.Len(c, node.GetAvailableRoutes(), 1) // 10.0.1.0/24
|
||||
assert.Empty(c, node.GetApprovedRoutes())
|
||||
assert.Empty(c, node.GetSubnetRoutes())
|
||||
} else {
|
||||
assert.Len(c, node.GetAvailableRoutes(), 1) // 10.0.2.0/24
|
||||
assert.Len(c, node.GetApprovedRoutes(), 1) // 10.0.2.0/24
|
||||
assert.Len(c, node.GetSubnetRoutes(), 1) // 10.0.2.0/24
|
||||
switch node.Id {
|
||||
case "1":
|
||||
assert.Len(c, node.AvailableRoutes, 1) // 10.0.0.0/24
|
||||
assert.Len(c, node.ApprovedRoutes, 1) // 10.0.1.0/24
|
||||
assert.Empty(c, node.SubnetRoutes)
|
||||
case "2":
|
||||
assert.Len(c, node.AvailableRoutes, 1) // 10.0.1.0/24
|
||||
assert.Empty(c, node.ApprovedRoutes)
|
||||
assert.Empty(c, node.SubnetRoutes)
|
||||
default:
|
||||
assert.Len(c, node.AvailableRoutes, 1) // 10.0.2.0/24
|
||||
assert.Len(c, node.ApprovedRoutes, 1) // 10.0.2.0/24
|
||||
assert.Len(c, node.SubnetRoutes, 1) // 10.0.2.0/24
|
||||
}
|
||||
}
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "route state changes should propagate to nodes")
|
||||
@@ -333,7 +333,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
requireNoErrSync(t, err)
|
||||
|
||||
// Wait for route configuration changes after advertising routes
|
||||
var nodes []*v1.Node
|
||||
var nodes []*clientv1.Node
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err = headscale.ListNodes()
|
||||
@@ -400,7 +400,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
t.Logf(" Expected: Client can access webservice through router 1 only")
|
||||
|
||||
_, err = headscale.ApproveRoutes(
|
||||
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
|
||||
mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id),
|
||||
[]netip.Prefix{pref},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -479,11 +479,11 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
// Validate primary routes table state - router 1 is primary
|
||||
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
|
||||
AvailableRoutes: map[types.NodeID][]netip.Prefix{
|
||||
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
|
||||
// Note: Router 2 and 3 are available but not approved
|
||||
},
|
||||
PrimaryRoutes: map[string]types.NodeID{
|
||||
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()),
|
||||
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)),
|
||||
},
|
||||
}, "Router 1 should be primary for route "+pref.String())
|
||||
|
||||
@@ -498,7 +498,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
t.Logf(" Expected: HA is now active - if router 1 fails, router 2 can take over")
|
||||
|
||||
_, err = headscale.ApproveRoutes(
|
||||
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
|
||||
mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id),
|
||||
[]netip.Prefix{pref},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -559,12 +559,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
// Validate primary routes table state - router 1 still primary, router 2 approved but standby
|
||||
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
|
||||
AvailableRoutes: map[types.NodeID][]netip.Prefix{
|
||||
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
|
||||
// Note: Router 3 is available but not approved
|
||||
},
|
||||
PrimaryRoutes: map[string]types.NodeID{
|
||||
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()),
|
||||
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)),
|
||||
},
|
||||
}, "Router 1 should remain primary after router 2 approval")
|
||||
|
||||
@@ -594,12 +594,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
// Validate primary routes table state - router 1 primary, router 2 approved (standby)
|
||||
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
|
||||
AvailableRoutes: map[types.NodeID][]netip.Prefix{
|
||||
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
|
||||
// Note: Router 3 is available but not approved
|
||||
},
|
||||
PrimaryRoutes: map[string]types.NodeID{
|
||||
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()),
|
||||
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)),
|
||||
},
|
||||
}, "Router 1 primary with router 2 as standby")
|
||||
|
||||
@@ -615,7 +615,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
t.Logf(" Expected: Full HA configuration with 1 PRIMARY + 2 STANDBY routers")
|
||||
|
||||
_, err = headscale.ApproveRoutes(
|
||||
MustFindNode(subRouter3.Hostname(), nodes).GetId(),
|
||||
mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id),
|
||||
[]netip.Prefix{pref},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -702,12 +702,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
// Validate primary routes table state - all 3 routers approved, router 1 still primary
|
||||
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
|
||||
AvailableRoutes: map[types.NodeID][]netip.Prefix{
|
||||
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)): {pref},
|
||||
},
|
||||
PrimaryRoutes: map[string]types.NodeID{
|
||||
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()),
|
||||
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)),
|
||||
},
|
||||
}, "Router 1 primary with all 3 routers approved")
|
||||
|
||||
@@ -782,11 +782,11 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
|
||||
AvailableRoutes: map[types.NodeID][]netip.Prefix{
|
||||
// Router 1 is disconnected, so not in AvailableRoutes
|
||||
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)): {pref},
|
||||
},
|
||||
PrimaryRoutes: map[string]types.NodeID{
|
||||
pref.String(): types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()),
|
||||
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)),
|
||||
},
|
||||
}, "Router 2 should be primary after router 1 failure")
|
||||
|
||||
@@ -854,10 +854,10 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
|
||||
AvailableRoutes: map[types.NodeID][]netip.Prefix{
|
||||
// Routers 1 and 2 are disconnected, so not in AvailableRoutes
|
||||
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)): {pref},
|
||||
},
|
||||
PrimaryRoutes: map[string]types.NodeID{
|
||||
pref.String(): types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()),
|
||||
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)),
|
||||
},
|
||||
}, "Router 3 should be primary after router 2 failure")
|
||||
|
||||
@@ -931,12 +931,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
// Validate primary routes table state - router 3 remains primary after router 1 comes back
|
||||
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
|
||||
AvailableRoutes: map[types.NodeID][]netip.Prefix{
|
||||
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
|
||||
// Router 2 is still disconnected
|
||||
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)): {pref},
|
||||
},
|
||||
PrimaryRoutes: map[string]types.NodeID{
|
||||
pref.String(): types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()),
|
||||
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)),
|
||||
},
|
||||
}, "Router 3 should remain primary after router 1 recovery")
|
||||
|
||||
@@ -1012,12 +1012,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
// Validate primary routes table state - router 3 remains primary after all routers back online
|
||||
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
|
||||
AvailableRoutes: map[types.NodeID][]netip.Prefix{
|
||||
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)): {pref},
|
||||
},
|
||||
PrimaryRoutes: map[string]types.NodeID{
|
||||
pref.String(): types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()),
|
||||
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)),
|
||||
},
|
||||
}, "Router 3 should remain primary after full recovery")
|
||||
|
||||
@@ -1030,7 +1030,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
t.Logf(" Expected: Router 1 (%s) should become new PRIMARY (lowest ID with approved route)", subRouter1.Hostname())
|
||||
t.Logf(" Expected: Router 2 (%s) remains STANDBY", subRouter2.Hostname())
|
||||
t.Logf(" Expected: Router 3 (%s) goes to advertised-only state (no longer serving)", subRouter3.Hostname())
|
||||
_, err = headscale.ApproveRoutes(MustFindNode(subRouter3.Hostname(), nodes).GetId(), []netip.Prefix{})
|
||||
_, err = headscale.ApproveRoutes(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id), []netip.Prefix{})
|
||||
|
||||
// Wait for nodestore batch processing and route state changes to complete
|
||||
// [state.NodeStore] batching timeout is 500ms, so we wait up to 10 seconds for route failover
|
||||
@@ -1097,12 +1097,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
// Validate primary routes table state - router 1 is primary after router 3 route disabled
|
||||
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
|
||||
AvailableRoutes: map[types.NodeID][]netip.Prefix{
|
||||
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
|
||||
// Router 3's route is no longer approved, so not in AvailableRoutes
|
||||
},
|
||||
PrimaryRoutes: map[string]types.NodeID{
|
||||
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()),
|
||||
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)),
|
||||
},
|
||||
}, "Router 1 should be primary after router 3 route disabled")
|
||||
|
||||
@@ -1116,7 +1116,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
t.Logf(" Expected: Router 2 (%s) should become new PRIMARY (only remaining approved route)", subRouter2.Hostname())
|
||||
t.Logf(" Expected: Router 1 (%s) goes to advertised-only state", subRouter1.Hostname())
|
||||
t.Logf(" Expected: Router 3 (%s) remains advertised-only", subRouter3.Hostname())
|
||||
_, err = headscale.ApproveRoutes(MustFindNode(subRouter1.Hostname(), nodes).GetId(), []netip.Prefix{})
|
||||
_, err = headscale.ApproveRoutes(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id), []netip.Prefix{})
|
||||
|
||||
// Wait for nodestore batch processing and route state changes to complete
|
||||
// [state.NodeStore] batching timeout is 500ms, so we wait up to 10 seconds for route failover
|
||||
@@ -1184,11 +1184,11 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
|
||||
AvailableRoutes: map[types.NodeID][]netip.Prefix{
|
||||
// Router 1's route is no longer approved, so not in AvailableRoutes
|
||||
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
|
||||
// Router 3's route is still not approved
|
||||
},
|
||||
PrimaryRoutes: map[string]types.NodeID{
|
||||
pref.String(): types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()),
|
||||
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)),
|
||||
},
|
||||
}, "Router 2 should be primary after router 1 route disabled")
|
||||
|
||||
@@ -1205,8 +1205,8 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
|
||||
r1Node := MustFindNode(subRouter1.Hostname(), nodes)
|
||||
_, err = headscale.ApproveRoutes(
|
||||
r1Node.GetId(),
|
||||
util.MustStringsToPrefixes(r1Node.GetAvailableRoutes()),
|
||||
mustParseID(r1Node.Id),
|
||||
util.MustStringsToPrefixes(r1Node.AvailableRoutes),
|
||||
)
|
||||
|
||||
// Wait for route state changes after re-enabling r1
|
||||
@@ -1268,12 +1268,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
// Validate primary routes table state after router 1 re-approval
|
||||
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
|
||||
AvailableRoutes: map[types.NodeID][]netip.Prefix{
|
||||
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
|
||||
// Router 3 route is still not approved
|
||||
},
|
||||
PrimaryRoutes: map[string]types.NodeID{
|
||||
pref.String(): types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()),
|
||||
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)),
|
||||
},
|
||||
}, "Router 2 should remain primary after router 1 re-approval")
|
||||
|
||||
@@ -1290,8 +1290,8 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
|
||||
r3Node := MustFindNode(subRouter3.Hostname(), nodes)
|
||||
_, err = headscale.ApproveRoutes(
|
||||
r3Node.GetId(),
|
||||
util.MustStringsToPrefixes(r3Node.GetAvailableRoutes()),
|
||||
mustParseID(r3Node.Id),
|
||||
util.MustStringsToPrefixes(r3Node.AvailableRoutes),
|
||||
)
|
||||
|
||||
// Wait for route state changes after re-enabling r3
|
||||
@@ -1310,12 +1310,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
||||
// Validate primary routes table state after router 3 re-approval
|
||||
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
|
||||
AvailableRoutes: map[types.NodeID][]netip.Prefix{
|
||||
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id)): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)): {pref},
|
||||
types.NodeID(mustParseID(MustFindNode(subRouter3.Hostname(), nodes).Id)): {pref},
|
||||
},
|
||||
PrimaryRoutes: map[string]types.NodeID{
|
||||
pref.String(): types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()),
|
||||
pref.String(): types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id)),
|
||||
},
|
||||
}, "Router 2 should remain primary after router 3 re-approval")
|
||||
|
||||
@@ -1426,7 +1426,7 @@ func TestSubnetRouteACL(t *testing.T) {
|
||||
requireNoErrSync(t, err)
|
||||
|
||||
// Wait for route advertisements to propagate to the server
|
||||
var nodes []*v1.Node
|
||||
var nodes []*clientv1.Node
|
||||
|
||||
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
var err error
|
||||
@@ -1437,12 +1437,12 @@ func TestSubnetRouteACL(t *testing.T) {
|
||||
|
||||
// Find the node that should have the route by checking node IDs
|
||||
var (
|
||||
routeNode *v1.Node
|
||||
otherNode *v1.Node
|
||||
routeNode *clientv1.Node
|
||||
otherNode *clientv1.Node
|
||||
)
|
||||
|
||||
for _, node := range nodes {
|
||||
nodeIDStr := strconv.FormatUint(node.GetId(), 10)
|
||||
nodeIDStr := node.Id
|
||||
if _, shouldHaveRoute := expectedRoutes[nodeIDStr]; shouldHaveRoute {
|
||||
routeNode = node
|
||||
} else {
|
||||
@@ -1623,7 +1623,7 @@ func TestEnablingExitRoutes(t *testing.T) {
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
requireNoErrSync(t, err)
|
||||
|
||||
var nodes []*v1.Node
|
||||
var nodes []*clientv1.Node
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
var err error
|
||||
@@ -1654,12 +1654,12 @@ func TestEnablingExitRoutes(t *testing.T) {
|
||||
// Enable all routes, but do v4 on one and v6 on other to ensure they
|
||||
// are both added since they are exit routes.
|
||||
_, err = headscale.ApproveRoutes(
|
||||
nodes[0].GetId(),
|
||||
mustParseID(nodes[0].Id),
|
||||
[]netip.Prefix{tsaddr.AllIPv4()},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
_, err = headscale.ApproveRoutes(
|
||||
nodes[1].GetId(),
|
||||
mustParseID(nodes[1].Id),
|
||||
[]netip.Prefix{tsaddr.AllIPv6()},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -1754,7 +1754,7 @@ func TestExitRoutesWithAutogroupInternetACL(t *testing.T) {
|
||||
// so the standard WaitForTailscaleSync wait would deadlock here —
|
||||
// the post-approval [assert.EventuallyWithT] block below covers the peer
|
||||
// state we actually care about.
|
||||
var nodes []*v1.Node
|
||||
var nodes []*clientv1.Node
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err = headscale.ListNodes()
|
||||
@@ -1770,12 +1770,12 @@ func TestExitRoutesWithAutogroupInternetACL(t *testing.T) {
|
||||
// alice's exit. The bug fix is about visibility, not which node
|
||||
// is chosen.
|
||||
_, err = headscale.ApproveRoutes(
|
||||
nodes[0].GetId(),
|
||||
mustParseID(nodes[0].Id),
|
||||
[]netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
_, err = headscale.ApproveRoutes(
|
||||
nodes[1].GetId(),
|
||||
mustParseID(nodes[1].Id),
|
||||
[]netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -1902,7 +1902,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
|
||||
_, _, err = user1c.Execute(command)
|
||||
require.NoErrorf(t, err, "failed to advertise route: %s", err)
|
||||
|
||||
var nodes []*v1.Node
|
||||
var nodes []*clientv1.Node
|
||||
// Wait for route advertisements to propagate to [state.NodeStore]
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
@@ -1929,7 +1929,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
|
||||
|
||||
// Enable route
|
||||
_, err = headscale.ApproveRoutes(
|
||||
nodes[0].GetId(),
|
||||
mustParseID(nodes[0].Id),
|
||||
[]netip.Prefix{*pref},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -2058,7 +2058,7 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
|
||||
_, _, err = user1c.Execute(command)
|
||||
require.NoErrorf(t, err, "failed to advertise routes: %s", err)
|
||||
|
||||
var nodes []*v1.Node
|
||||
var nodes []*clientv1.Node
|
||||
// Wait for route advertisements to propagate (3 routes: v4 exit + v6 exit + subnet).
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
@@ -2084,7 +2084,7 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
|
||||
}, integrationutil.ScaledTimeout(5*time.Second), integrationutil.FastPoll, "Verifying no routes sent to client before approval")
|
||||
|
||||
// Approve exit routes and subnet route.
|
||||
_, err = headscale.ApproveRoutes(nodes[0].GetId(), []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6(), *route})
|
||||
_, err = headscale.ApproveRoutes(mustParseID(nodes[0].Id), []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6(), *route})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for route state changes to propagate.
|
||||
@@ -2151,9 +2151,9 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
|
||||
}, 10*time.Second, 200*time.Millisecond, "user2 traceroute should go through user1 exit node")
|
||||
}
|
||||
|
||||
func MustFindNode(hostname string, nodes []*v1.Node) *v1.Node {
|
||||
func MustFindNode(hostname string, nodes []*clientv1.Node) *clientv1.Node {
|
||||
for _, node := range nodes {
|
||||
if node.GetName() == hostname {
|
||||
if node.Name == hostname {
|
||||
return node
|
||||
}
|
||||
}
|
||||
@@ -2447,7 +2447,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
require.NoErrorf(t, err, "failed to create scenario: %s", err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
var nodes []*v1.Node
|
||||
var nodes []*clientv1.Node
|
||||
|
||||
opts := []hsic.Option{
|
||||
hsic.WithTestName("autoapprovemulti"),
|
||||
@@ -2565,16 +2565,16 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
|
||||
// If the approver is a tag, create a tagged PreAuthKey
|
||||
// (tags-as-identity model: tags come from PreAuthKey, not --advertise-tags)
|
||||
var pak *v1.PreAuthKey
|
||||
var pak *clientv1.PreAuthKey
|
||||
if strings.HasPrefix(tt.approver, "tag:") {
|
||||
pak, err = scenario.CreatePreAuthKeyWithTags(userMap["user1"].GetId(), false, false, []string{tt.approver})
|
||||
pak, err = scenario.CreatePreAuthKeyWithTags(mustParseID(userMap["user1"].Id), false, false, []string{tt.approver})
|
||||
} else {
|
||||
pak, err = scenario.CreatePreAuthKey(userMap["user1"].GetId(), false, false)
|
||||
pak, err = scenario.CreatePreAuthKey(mustParseID(userMap["user1"].Id), false, false)
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
err = routerUsernet1.Login(headscale.GetEndpoint(), pak.GetKey())
|
||||
err = routerUsernet1.Login(headscale.GetEndpoint(), pak.Key)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
// extra creation end.
|
||||
@@ -2646,10 +2646,10 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
|
||||
routerNode := MustFindNode(routerUsernet1.Hostname(), nodes)
|
||||
t.Logf("Initial auto-approval check - Router node %s: announced=%v, approved=%v, subnet=%v",
|
||||
routerNode.GetName(),
|
||||
routerNode.GetAvailableRoutes(),
|
||||
routerNode.GetApprovedRoutes(),
|
||||
routerNode.GetSubnetRoutes())
|
||||
routerNode.Name,
|
||||
routerNode.AvailableRoutes,
|
||||
routerNode.ApprovedRoutes,
|
||||
routerNode.SubnetRoutes)
|
||||
|
||||
requireNodeRouteCountWithCollect(c, routerNode, 1, 1, 1)
|
||||
}, assertTimeout, 500*time.Millisecond, "Initial route auto-approval: Route should be approved via policy")
|
||||
@@ -2740,10 +2740,10 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
|
||||
routerNode := MustFindNode(routerUsernet1.Hostname(), nodes)
|
||||
t.Logf("After policy removal - Router node %s: announced=%v, approved=%v, subnet=%v",
|
||||
routerNode.GetName(),
|
||||
routerNode.GetAvailableRoutes(),
|
||||
routerNode.GetApprovedRoutes(),
|
||||
routerNode.GetSubnetRoutes())
|
||||
routerNode.Name,
|
||||
routerNode.AvailableRoutes,
|
||||
routerNode.ApprovedRoutes,
|
||||
routerNode.SubnetRoutes)
|
||||
|
||||
requireNodeRouteCountWithCollect(c, routerNode, 1, 1, 1)
|
||||
}, assertTimeout, 500*time.Millisecond, "Routes should remain approved after auto-approver removal")
|
||||
@@ -2791,7 +2791,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
|
||||
// Disable the route, making it unavailable since it is no longer auto-approved
|
||||
_, err = headscale.ApproveRoutes(
|
||||
MustFindNode(routerUsernet1.Hostname(), nodes).GetId(),
|
||||
mustParseID(MustFindNode(routerUsernet1.Hostname(), nodes).Id),
|
||||
[]netip.Prefix{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -3078,10 +3078,10 @@ func requirePeerSubnetRoutesWithCollect(c *assert.CollectT, status *ipnstate.Pee
|
||||
}
|
||||
}
|
||||
|
||||
func requireNodeRouteCountWithCollect(c *assert.CollectT, node *v1.Node, announced, approved, subnet int) {
|
||||
assert.Lenf(c, node.GetAvailableRoutes(), announced, "expected %q announced routes(%v) to have %d route, had %d", node.GetName(), node.GetAvailableRoutes(), announced, len(node.GetAvailableRoutes()))
|
||||
assert.Lenf(c, node.GetApprovedRoutes(), approved, "expected %q approved routes(%v) to have %d route, had %d", node.GetName(), node.GetApprovedRoutes(), approved, len(node.GetApprovedRoutes()))
|
||||
assert.Lenf(c, node.GetSubnetRoutes(), subnet, "expected %q subnet routes(%v) to have %d route, had %d", node.GetName(), node.GetSubnetRoutes(), subnet, len(node.GetSubnetRoutes()))
|
||||
func requireNodeRouteCountWithCollect(c *assert.CollectT, node *clientv1.Node, announced, approved, subnet int) {
|
||||
assert.Lenf(c, node.AvailableRoutes, announced, "expected %q announced routes(%v) to have %d route, had %d", node.Name, node.AvailableRoutes, announced, len(node.AvailableRoutes))
|
||||
assert.Lenf(c, node.ApprovedRoutes, approved, "expected %q approved routes(%v) to have %d route, had %d", node.Name, node.ApprovedRoutes, approved, len(node.ApprovedRoutes))
|
||||
assert.Lenf(c, node.SubnetRoutes, subnet, "expected %q subnet routes(%v) to have %d route, had %d", node.Name, node.SubnetRoutes, subnet, len(node.SubnetRoutes))
|
||||
}
|
||||
|
||||
// TestSubnetRouteACLFiltering tests that a node can only access subnet routes
|
||||
@@ -3218,7 +3218,7 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
requireNoErrSync(t, err)
|
||||
|
||||
var routerNode, nodeNode *v1.Node
|
||||
var routerNode, nodeNode *clientv1.Node
|
||||
// Wait for route advertisements to propagate to [state.NodeStore]
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
// List nodes and verify the router has 3 available routes
|
||||
@@ -3240,8 +3240,8 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
|
||||
|
||||
// Approve all routes for the router
|
||||
_, err = headscale.ApproveRoutes(
|
||||
routerNode.GetId(),
|
||||
util.MustStringsToPrefixes(routerNode.GetAvailableRoutes()),
|
||||
mustParseID(routerNode.Id),
|
||||
util.MustStringsToPrefixes(routerNode.AvailableRoutes),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -3409,10 +3409,10 @@ func TestGrantViaSubnetSteering(t *testing.T) {
|
||||
defer func() { _, _, _ = routerA.Shutdown() }()
|
||||
|
||||
pakRouterA, err := scenario.CreatePreAuthKeyWithTags(
|
||||
userMap["router"].GetId(), false, false, []string{"tag:router-a"},
|
||||
mustParseID(userMap["router"].Id), false, false, []string{"tag:router-a"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = routerA.Login(headscale.GetEndpoint(), pakRouterA.GetKey())
|
||||
err = routerA.Login(headscale.GetEndpoint(), pakRouterA.Key)
|
||||
require.NoError(t, err)
|
||||
err = routerA.WaitForRunning(30 * time.Second)
|
||||
require.NoError(t, err)
|
||||
@@ -3426,10 +3426,10 @@ func TestGrantViaSubnetSteering(t *testing.T) {
|
||||
defer func() { _, _, _ = routerB.Shutdown() }()
|
||||
|
||||
pakRouterB, err := scenario.CreatePreAuthKeyWithTags(
|
||||
userMap["router"].GetId(), false, false, []string{"tag:router-b"},
|
||||
mustParseID(userMap["router"].Id), false, false, []string{"tag:router-b"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = routerB.Login(headscale.GetEndpoint(), pakRouterB.GetKey())
|
||||
err = routerB.Login(headscale.GetEndpoint(), pakRouterB.Key)
|
||||
require.NoError(t, err)
|
||||
err = routerB.WaitForRunning(30 * time.Second)
|
||||
require.NoError(t, err)
|
||||
@@ -3444,10 +3444,10 @@ func TestGrantViaSubnetSteering(t *testing.T) {
|
||||
defer func() { _, _, _ = clientA.Shutdown() }()
|
||||
|
||||
pakClientA, err := scenario.CreatePreAuthKeyWithTags(
|
||||
userMap["client"].GetId(), false, false, []string{"tag:group-a"},
|
||||
mustParseID(userMap["client"].Id), false, false, []string{"tag:group-a"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = clientA.Login(headscale.GetEndpoint(), pakClientA.GetKey())
|
||||
err = clientA.Login(headscale.GetEndpoint(), pakClientA.Key)
|
||||
require.NoError(t, err)
|
||||
err = clientA.WaitForRunning(30 * time.Second)
|
||||
require.NoError(t, err)
|
||||
@@ -3462,10 +3462,10 @@ func TestGrantViaSubnetSteering(t *testing.T) {
|
||||
defer func() { _, _, _ = clientB.Shutdown() }()
|
||||
|
||||
pakClientB, err := scenario.CreatePreAuthKeyWithTags(
|
||||
userMap["client"].GetId(), false, false, []string{"tag:group-b"},
|
||||
mustParseID(userMap["client"].Id), false, false, []string{"tag:group-b"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = clientB.Login(headscale.GetEndpoint(), pakClientB.GetKey())
|
||||
err = clientB.Login(headscale.GetEndpoint(), pakClientB.Key)
|
||||
require.NoError(t, err)
|
||||
err = clientB.WaitForRunning(30 * time.Second)
|
||||
require.NoError(t, err)
|
||||
@@ -3497,21 +3497,21 @@ func TestGrantViaSubnetSteering(t *testing.T) {
|
||||
|
||||
routerANode := MustFindNode(routerA.Hostname(), nodes)
|
||||
t.Logf("Router A %s: announced=%v, approved=%v, subnet=%v",
|
||||
routerANode.GetName(),
|
||||
routerANode.GetAvailableRoutes(),
|
||||
routerANode.GetApprovedRoutes(),
|
||||
routerANode.GetSubnetRoutes())
|
||||
assert.Len(c, routerANode.GetAvailableRoutes(), 1, "Router A should have 1 announced route")
|
||||
assert.Len(c, routerANode.GetApprovedRoutes(), 1, "Router A should have 1 approved route")
|
||||
routerANode.Name,
|
||||
routerANode.AvailableRoutes,
|
||||
routerANode.ApprovedRoutes,
|
||||
routerANode.SubnetRoutes)
|
||||
assert.Len(c, routerANode.AvailableRoutes, 1, "Router A should have 1 announced route")
|
||||
assert.Len(c, routerANode.ApprovedRoutes, 1, "Router A should have 1 approved route")
|
||||
|
||||
routerBNode := MustFindNode(routerB.Hostname(), nodes)
|
||||
t.Logf("Router B %s: announced=%v, approved=%v, subnet=%v",
|
||||
routerBNode.GetName(),
|
||||
routerBNode.GetAvailableRoutes(),
|
||||
routerBNode.GetApprovedRoutes(),
|
||||
routerBNode.GetSubnetRoutes())
|
||||
assert.Len(c, routerBNode.GetAvailableRoutes(), 1, "Router B should have 1 announced route")
|
||||
assert.Len(c, routerBNode.GetApprovedRoutes(), 1, "Router B should have 1 approved route")
|
||||
routerBNode.Name,
|
||||
routerBNode.AvailableRoutes,
|
||||
routerBNode.ApprovedRoutes,
|
||||
routerBNode.SubnetRoutes)
|
||||
assert.Len(c, routerBNode.AvailableRoutes, 1, "Router B should have 1 announced route")
|
||||
assert.Len(c, routerBNode.ApprovedRoutes, 1, "Router B should have 1 approved route")
|
||||
}, assertTimeout, 500*time.Millisecond, "Both routers should have auto-approved routes")
|
||||
|
||||
// Get webservice info.
|
||||
@@ -3697,7 +3697,7 @@ func TestHASubnetRouterPingFailover(t *testing.T) {
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
requireNoErrSync(t, err)
|
||||
|
||||
var nodes []*v1.Node
|
||||
var nodes []*clientv1.Node
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err = headscale.ListNodes()
|
||||
@@ -3707,19 +3707,19 @@ func TestHASubnetRouterPingFailover(t *testing.T) {
|
||||
|
||||
// Approve routes on both routers.
|
||||
_, err = headscale.ApproveRoutes(
|
||||
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
|
||||
mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id),
|
||||
[]netip.Prefix{pref},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = headscale.ApproveRoutes(
|
||||
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
|
||||
mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id),
|
||||
[]netip.Prefix{pref},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
nodeID1 := types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId())
|
||||
nodeID2 := types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId())
|
||||
nodeID1 := types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id))
|
||||
nodeID2 := types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id))
|
||||
|
||||
// Wait for HA to be set up: router 1 primary, router 2 standby.
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
@@ -3937,7 +3937,7 @@ func TestHASubnetRouterFailoverBothOffline(t *testing.T) {
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
requireNoErrSync(t, err)
|
||||
|
||||
var nodes []*v1.Node
|
||||
var nodes []*clientv1.Node
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err = headscale.ListNodes()
|
||||
@@ -3947,19 +3947,19 @@ func TestHASubnetRouterFailoverBothOffline(t *testing.T) {
|
||||
|
||||
// Approve the route on both routers explicitly.
|
||||
_, err = headscale.ApproveRoutes(
|
||||
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
|
||||
mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id),
|
||||
[]netip.Prefix{pref},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = headscale.ApproveRoutes(
|
||||
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
|
||||
mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id),
|
||||
[]netip.Prefix{pref},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
nodeID1 := types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId())
|
||||
nodeID2 := types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId())
|
||||
nodeID1 := types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id))
|
||||
nodeID2 := types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id))
|
||||
|
||||
// Sanity: r1 starts as primary (lower NodeID).
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
@@ -4142,7 +4142,7 @@ func TestHASubnetRouterFailoverBothOfflineCablePull(t *testing.T) {
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
requireNoErrSync(t, err)
|
||||
|
||||
var nodes []*v1.Node
|
||||
var nodes []*clientv1.Node
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err = headscale.ListNodes()
|
||||
@@ -4151,18 +4151,18 @@ func TestHASubnetRouterFailoverBothOfflineCablePull(t *testing.T) {
|
||||
}, propagationTime, 200*time.Millisecond, "nodes registered")
|
||||
|
||||
_, err = headscale.ApproveRoutes(
|
||||
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
|
||||
mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id),
|
||||
[]netip.Prefix{pref},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = headscale.ApproveRoutes(
|
||||
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
|
||||
mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id),
|
||||
[]netip.Prefix{pref},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
nodeID2 := types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId())
|
||||
nodeID2 := types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id))
|
||||
|
||||
// Sanity: r1 starts as primary.
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
@@ -4366,7 +4366,7 @@ func TestHASubnetRouterFailoverDockerDisconnect(t *testing.T) {
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
requireNoErrSync(t, err)
|
||||
|
||||
var nodes []*v1.Node
|
||||
var nodes []*clientv1.Node
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err = headscale.ListNodes()
|
||||
@@ -4375,19 +4375,19 @@ func TestHASubnetRouterFailoverDockerDisconnect(t *testing.T) {
|
||||
}, propagationTime, 200*time.Millisecond, "nodes registered")
|
||||
|
||||
_, err = headscale.ApproveRoutes(
|
||||
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
|
||||
mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id),
|
||||
[]netip.Prefix{pref},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = headscale.ApproveRoutes(
|
||||
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
|
||||
mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id),
|
||||
[]netip.Prefix{pref},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
nodeID1 := types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId())
|
||||
nodeID2 := types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId())
|
||||
nodeID1 := types.NodeID(mustParseID(MustFindNode(subRouter1.Hostname(), nodes).Id))
|
||||
nodeID2 := types.NodeID(mustParseID(MustFindNode(subRouter2.Hostname(), nodes).Id))
|
||||
|
||||
// requirePrimary blocks until headscale reports want as the
|
||||
// primary advertiser for pref.
|
||||
|
||||
+10
-10
@@ -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
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
|
||||
err = scenario.RunTailscaleUp(
|
||||
user,
|
||||
headscale.GetEndpoint(),
|
||||
key.GetKey(),
|
||||
key.Key,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to login: %s", err)
|
||||
|
||||
+134
-134
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user