all: fix golangci-lint issues (#3064)
This commit is contained in:
parent
bfb6fd80df
commit
ce580f8245
131 changed files with 3131 additions and 1560 deletions
|
|
@ -1877,7 +1877,7 @@ func TestACLAutogroupSelf(t *testing.T) {
|
|||
|
||||
result, err := client.Curl(url)
|
||||
assert.Empty(t, result, "user1 should not be able to access user2's regular devices (autogroup:self isolation)")
|
||||
assert.Error(t, err, "connection from user1 to user2 regular device should fail")
|
||||
require.Error(t, err, "connection from user1 to user2 regular device should fail")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1896,6 +1896,7 @@ func TestACLAutogroupSelf(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // complex integration test scenario
|
||||
func TestACLPolicyPropagationOverTime(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
|
@ -35,6 +36,7 @@ func TestAPIAuthenticationBypass(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -46,6 +48,7 @@ func TestAPIAuthenticationBypass(t *testing.T) {
|
|||
|
||||
// Create an API key using the CLI
|
||||
var validAPIKey string
|
||||
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
apiKeyOutput, err := headscale.Execute(
|
||||
[]string{
|
||||
|
|
@ -63,7 +66,7 @@ func TestAPIAuthenticationBypass(t *testing.T) {
|
|||
|
||||
// Get the API endpoint
|
||||
endpoint := headscale.GetEndpoint()
|
||||
apiURL := fmt.Sprintf("%s/api/v1/user", endpoint)
|
||||
apiURL := endpoint + "/api/v1/user"
|
||||
|
||||
// Create HTTP client
|
||||
client := &http.Client{
|
||||
|
|
@ -76,11 +79,12 @@ func TestAPIAuthenticationBypass(t *testing.T) {
|
|||
t.Run("HTTP_NoAuthHeader", func(t *testing.T) {
|
||||
// Test 1: Request without any Authorization header
|
||||
// Expected: Should return 401 with ONLY "Unauthorized" text, no user data
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, apiURL, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
|
|
@ -99,6 +103,7 @@ func TestAPIAuthenticationBypass(t *testing.T) {
|
|||
// Should NOT contain user data after "Unauthorized"
|
||||
// This is the security bypass - if users array is present, auth was bypassed
|
||||
var jsonCheck map[string]any
|
||||
|
||||
jsonErr := json.Unmarshal(body, &jsonCheck)
|
||||
|
||||
// If we can unmarshal JSON and it contains "users", that's the bypass
|
||||
|
|
@ -126,12 +131,13 @@ func TestAPIAuthenticationBypass(t *testing.T) {
|
|||
t.Run("HTTP_InvalidAuthHeader", func(t *testing.T) {
|
||||
// Test 2: Request with invalid Authorization header (missing "Bearer " prefix)
|
||||
// Expected: Should return 401 with ONLY "Unauthorized" text, no user data
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, apiURL, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Authorization", "InvalidToken")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
|
|
@ -159,12 +165,13 @@ func TestAPIAuthenticationBypass(t *testing.T) {
|
|||
// Test 3: Request with Bearer prefix but invalid token
|
||||
// Expected: Should return 401 with ONLY "Unauthorized" text, no user data
|
||||
// Note: Both malformed and properly formatted invalid tokens should return 401
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, apiURL, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer invalid-token-12345")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
|
|
@ -191,12 +198,13 @@ func TestAPIAuthenticationBypass(t *testing.T) {
|
|||
t.Run("HTTP_ValidAPIKey", func(t *testing.T) {
|
||||
// Test 4: Request with valid API key
|
||||
// Expected: Should return 200 with user data (this is the authorized case)
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, apiURL, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", validAPIKey))
|
||||
req.Header.Set("Authorization", "Bearer "+validAPIKey)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
|
|
@ -208,16 +216,19 @@ func TestAPIAuthenticationBypass(t *testing.T) {
|
|||
|
||||
// Should be able to parse as protobuf JSON
|
||||
var response v1.ListUsersResponse
|
||||
|
||||
err = protojson.Unmarshal(body, &response)
|
||||
assert.NoError(t, err, "Response should be valid protobuf JSON with valid API key")
|
||||
require.NoError(t, err, "Response should be valid protobuf JSON with valid API key")
|
||||
|
||||
// Should contain our test users
|
||||
users := response.GetUsers()
|
||||
assert.Len(t, users, 3, "Should have 3 users")
|
||||
|
||||
userNames := make([]string, len(users))
|
||||
for i, u := range users {
|
||||
userNames[i] = u.GetName()
|
||||
}
|
||||
|
||||
assert.Contains(t, userNames, "user1")
|
||||
assert.Contains(t, userNames, "user2")
|
||||
assert.Contains(t, userNames, "user3")
|
||||
|
|
@ -234,6 +245,7 @@ func TestAPIAuthenticationBypassCurl(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -254,10 +266,11 @@ func TestAPIAuthenticationBypassCurl(t *testing.T) {
|
|||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
validAPIKey := strings.TrimSpace(apiKeyOutput)
|
||||
|
||||
endpoint := headscale.GetEndpoint()
|
||||
apiURL := fmt.Sprintf("%s/api/v1/user", endpoint)
|
||||
apiURL := endpoint + "/api/v1/user"
|
||||
|
||||
t.Run("Curl_NoAuth", func(t *testing.T) {
|
||||
// Execute curl from inside the headscale container without auth
|
||||
|
|
@ -274,17 +287,24 @@ func TestAPIAuthenticationBypassCurl(t *testing.T) {
|
|||
|
||||
// Parse the output
|
||||
lines := strings.Split(curlOutput, "\n")
|
||||
var httpCode string
|
||||
var responseBody string
|
||||
|
||||
var (
|
||||
httpCode string
|
||||
responseBody string
|
||||
)
|
||||
|
||||
var responseBodySb280 strings.Builder
|
||||
|
||||
for _, line := range lines {
|
||||
if after, ok := strings.CutPrefix(line, "HTTP_CODE:"); ok {
|
||||
httpCode = after
|
||||
} else {
|
||||
responseBody += line
|
||||
responseBodySb280.WriteString(line)
|
||||
}
|
||||
}
|
||||
|
||||
responseBody += responseBodySb280.String()
|
||||
|
||||
// Should return 401
|
||||
assert.Equal(t, "401", httpCode,
|
||||
"Curl without auth should return 401")
|
||||
|
|
@ -320,17 +340,24 @@ func TestAPIAuthenticationBypassCurl(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
lines := strings.Split(curlOutput, "\n")
|
||||
var httpCode string
|
||||
var responseBody string
|
||||
|
||||
var (
|
||||
httpCode string
|
||||
responseBody string
|
||||
)
|
||||
|
||||
var responseBodySb326 strings.Builder
|
||||
|
||||
for _, line := range lines {
|
||||
if after, ok := strings.CutPrefix(line, "HTTP_CODE:"); ok {
|
||||
httpCode = after
|
||||
} else {
|
||||
responseBody += line
|
||||
responseBodySb326.WriteString(line)
|
||||
}
|
||||
}
|
||||
|
||||
responseBody += responseBodySb326.String()
|
||||
|
||||
assert.Equal(t, "401", httpCode)
|
||||
assert.Contains(t, responseBody, "Unauthorized")
|
||||
assert.NotContains(t, responseBody, "testuser1",
|
||||
|
|
@ -346,7 +373,7 @@ func TestAPIAuthenticationBypassCurl(t *testing.T) {
|
|||
"curl",
|
||||
"-s",
|
||||
"-H",
|
||||
fmt.Sprintf("Authorization: Bearer %s", validAPIKey),
|
||||
"Authorization: Bearer " + validAPIKey,
|
||||
"-w",
|
||||
"\nHTTP_CODE:%{http_code}",
|
||||
apiURL,
|
||||
|
|
@ -355,25 +382,34 @@ func TestAPIAuthenticationBypassCurl(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
lines := strings.Split(curlOutput, "\n")
|
||||
var httpCode string
|
||||
var responseBody string
|
||||
|
||||
var (
|
||||
httpCode string
|
||||
responseBody string
|
||||
)
|
||||
|
||||
var responseBodySb361 strings.Builder
|
||||
|
||||
for _, line := range lines {
|
||||
if after, ok := strings.CutPrefix(line, "HTTP_CODE:"); ok {
|
||||
httpCode = after
|
||||
} else {
|
||||
responseBody += line
|
||||
responseBodySb361.WriteString(line)
|
||||
}
|
||||
}
|
||||
|
||||
responseBody += responseBodySb361.String()
|
||||
|
||||
// Should succeed
|
||||
assert.Equal(t, "200", httpCode,
|
||||
"Curl with valid API key should return 200")
|
||||
|
||||
// Should contain user data
|
||||
var response v1.ListUsersResponse
|
||||
|
||||
err = protojson.Unmarshal([]byte(responseBody), &response)
|
||||
assert.NoError(t, err, "Response should be valid protobuf JSON")
|
||||
require.NoError(t, err, "Response should be valid protobuf JSON")
|
||||
|
||||
users := response.GetUsers()
|
||||
assert.Len(t, users, 2, "Should have 2 users")
|
||||
})
|
||||
|
|
@ -391,6 +427,7 @@ func TestGRPCAuthenticationBypass(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -420,11 +457,12 @@ func TestGRPCAuthenticationBypass(t *testing.T) {
|
|||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
validAPIKey := strings.TrimSpace(apiKeyOutput)
|
||||
|
||||
// Get the gRPC endpoint
|
||||
// For gRPC, we need to use the hostname and port 50443
|
||||
grpcAddress := fmt.Sprintf("%s:50443", headscale.GetHostname())
|
||||
grpcAddress := headscale.GetHostname() + ":50443"
|
||||
|
||||
t.Run("gRPC_NoAPIKey", func(t *testing.T) {
|
||||
// Test 1: Try to use CLI without API key (should fail)
|
||||
|
|
@ -452,7 +490,7 @@ func TestGRPCAuthenticationBypass(t *testing.T) {
|
|||
)
|
||||
|
||||
// Should fail with authentication error
|
||||
assert.Error(t, err,
|
||||
require.Error(t, err,
|
||||
"gRPC connection with invalid API key should fail")
|
||||
|
||||
// Should contain authentication error message
|
||||
|
|
@ -481,20 +519,22 @@ func TestGRPCAuthenticationBypass(t *testing.T) {
|
|||
)
|
||||
|
||||
// Should succeed
|
||||
assert.NoError(t, err,
|
||||
require.NoError(t, err,
|
||||
"gRPC connection with valid API key should succeed, output: %s", output)
|
||||
|
||||
// CLI outputs the users array directly, not wrapped in ListUsersResponse
|
||||
// Parse as JSON array (CLI uses json.Marshal, not protojson)
|
||||
var users []*v1.User
|
||||
|
||||
err = json.Unmarshal([]byte(output), &users)
|
||||
assert.NoError(t, err, "Response should be valid JSON array")
|
||||
require.NoError(t, err, "Response should be valid JSON array")
|
||||
assert.Len(t, users, 2, "Should have 2 users")
|
||||
|
||||
userNames := make([]string, len(users))
|
||||
for i, u := range users {
|
||||
userNames[i] = u.GetName()
|
||||
}
|
||||
|
||||
assert.Contains(t, userNames, "grpcuser1")
|
||||
assert.Contains(t, userNames, "grpcuser2")
|
||||
})
|
||||
|
|
@ -513,6 +553,7 @@ func TestCLIWithConfigAuthenticationBypass(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -540,9 +581,10 @@ func TestCLIWithConfigAuthenticationBypass(t *testing.T) {
|
|||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
validAPIKey := strings.TrimSpace(apiKeyOutput)
|
||||
|
||||
grpcAddress := fmt.Sprintf("%s:50443", headscale.GetHostname())
|
||||
grpcAddress := headscale.GetHostname() + ":50443"
|
||||
|
||||
// Create a config file for testing
|
||||
configWithoutKey := fmt.Sprintf(`
|
||||
|
|
@ -602,7 +644,7 @@ cli:
|
|||
)
|
||||
|
||||
// Should fail
|
||||
assert.Error(t, err,
|
||||
require.Error(t, err,
|
||||
"CLI with invalid API key should fail")
|
||||
|
||||
// Should indicate authentication failure
|
||||
|
|
@ -637,20 +679,22 @@ cli:
|
|||
)
|
||||
|
||||
// Should succeed
|
||||
assert.NoError(t, err,
|
||||
require.NoError(t, err,
|
||||
"CLI with valid API key should succeed")
|
||||
|
||||
// CLI outputs the users array directly, not wrapped in ListUsersResponse
|
||||
// Parse as JSON array (CLI uses json.Marshal, not protojson)
|
||||
var users []*v1.User
|
||||
|
||||
err = json.Unmarshal([]byte(output), &users)
|
||||
assert.NoError(t, err, "Response should be valid JSON array")
|
||||
require.NoError(t, err, "Response should be valid JSON array")
|
||||
assert.Len(t, users, 2, "Should have 2 users")
|
||||
|
||||
userNames := make([]string, len(users))
|
||||
for i, u := range users {
|
||||
userNames[i] = u.GetName()
|
||||
}
|
||||
|
||||
assert.Contains(t, userNames, "cliuser1")
|
||||
assert.Contains(t, userNames, "cliuser2")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -69,18 +70,24 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
|
|||
// assertClientsState(t, allClients)
|
||||
|
||||
clientIPs := make(map[TailscaleClient][]netip.Addr)
|
||||
|
||||
for _, client := range allClients {
|
||||
ips, err := client.IPs()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get IPs for client %s: %s", client.Hostname(), err)
|
||||
}
|
||||
|
||||
clientIPs[client] = ips
|
||||
}
|
||||
|
||||
var listNodes []*v1.Node
|
||||
var nodeCountBeforeLogout int
|
||||
var (
|
||||
listNodes []*v1.Node
|
||||
nodeCountBeforeLogout int
|
||||
)
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
listNodes, err = headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, listNodes, len(allClients))
|
||||
|
|
@ -111,6 +118,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
|
|||
t.Logf("Validating node persistence after logout at %s", time.Now().Format(TimestampFormat))
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
listNodes, err = headscale.ListNodes()
|
||||
assert.NoError(ct, err, "Failed to list nodes after logout")
|
||||
assert.Len(ct, listNodes, nodeCountBeforeLogout, "Node count should match before logout count - expected %d nodes, got %d", nodeCountBeforeLogout, len(listNodes))
|
||||
|
|
@ -148,6 +156,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
|
|||
t.Logf("Validating node persistence after relogin at %s", time.Now().Format(TimestampFormat))
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
listNodes, err = headscale.ListNodes()
|
||||
assert.NoError(ct, err, "Failed to list nodes after relogin")
|
||||
assert.Len(ct, listNodes, nodeCountBeforeLogout, "Node count should remain unchanged after relogin - expected %d nodes, got %d", nodeCountBeforeLogout, len(listNodes))
|
||||
|
|
@ -201,6 +210,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
|
|||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
listNodes, err = headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, listNodes, nodeCountBeforeLogout)
|
||||
|
|
@ -255,10 +265,14 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
|
|||
requireAllClientsOnline(t, headscale, expectedNodes, true, "all clients should be connected after initial login", 120*time.Second)
|
||||
requireAllClientsNetInfoAndDERP(t, headscale, expectedNodes, "all clients should have NetInfo and DERP after initial login", 3*time.Minute)
|
||||
|
||||
var listNodes []*v1.Node
|
||||
var nodeCountBeforeLogout int
|
||||
var (
|
||||
listNodes []*v1.Node
|
||||
nodeCountBeforeLogout int
|
||||
)
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
listNodes, err = headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, listNodes, len(allClients))
|
||||
|
|
@ -301,9 +315,11 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
|
|||
}
|
||||
|
||||
var user1Nodes []*v1.Node
|
||||
|
||||
t.Logf("Validating user1 node count after relogin at %s", time.Now().Format(TimestampFormat))
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
user1Nodes, err = headscale.ListNodes("user1")
|
||||
assert.NoError(ct, err, "Failed to list nodes for user1 after relogin")
|
||||
assert.Len(ct, user1Nodes, len(allClients), "User1 should have all %d clients after relogin, got %d nodes", len(allClients), len(user1Nodes))
|
||||
|
|
@ -323,21 +339,24 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
|
|||
// 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
|
||||
|
||||
t.Logf("Validating user2 node persistence after user1 relogin at %s", time.Now().Format(TimestampFormat))
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
user2Nodes, err = headscale.ListNodes("user2")
|
||||
assert.NoError(ct, err, "Failed to list nodes for user2 after user1 relogin")
|
||||
assert.Len(ct, user2Nodes, len(allClients)/2, "User2 should still have %d clients after user1 relogin, got %d nodes", len(allClients)/2, len(user2Nodes))
|
||||
}, 30*time.Second, 2*time.Second, "validating user2 nodes persist after user1 relogin (should not be affected)")
|
||||
|
||||
t.Logf("Validating client login states after user switch at %s", time.Now().Format(TimestampFormat))
|
||||
|
||||
for _, client := range allClients {
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
status, err := client.Status()
|
||||
assert.NoError(ct, err, "Failed to get status for client %s", client.Hostname())
|
||||
assert.Equal(ct, "user1@test.no", status.User[status.Self.UserID].LoginName, "Client %s should be logged in as user1 after user switch, got %s", client.Hostname(), status.User[status.Self.UserID].LoginName)
|
||||
}, 30*time.Second, 2*time.Second, fmt.Sprintf("validating %s is logged in as user1 after auth key user switch", client.Hostname()))
|
||||
}, 30*time.Second, 2*time.Second, "validating %s is logged in as user1 after auth key user switch", client.Hostname())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -352,6 +371,7 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -377,11 +397,13 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
|
|||
// assertClientsState(t, allClients)
|
||||
|
||||
clientIPs := make(map[TailscaleClient][]netip.Addr)
|
||||
|
||||
for _, client := range allClients {
|
||||
ips, err := client.IPs()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get IPs for client %s: %s", client.Hostname(), err)
|
||||
}
|
||||
|
||||
clientIPs[client] = ips
|
||||
}
|
||||
|
||||
|
|
@ -395,10 +417,14 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
|
|||
requireAllClientsOnline(t, headscale, expectedNodes, true, "all clients should be connected after initial login", 120*time.Second)
|
||||
requireAllClientsNetInfoAndDERP(t, headscale, expectedNodes, "all clients should have NetInfo and DERP after initial login", 3*time.Minute)
|
||||
|
||||
var listNodes []*v1.Node
|
||||
var nodeCountBeforeLogout int
|
||||
var (
|
||||
listNodes []*v1.Node
|
||||
nodeCountBeforeLogout int
|
||||
)
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
listNodes, err = headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, listNodes, len(allClients))
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -175,6 +176,7 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) {
|
|||
syncCompleteTime := time.Now()
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
requireNoErrSync(t, err)
|
||||
|
||||
loginDuration := time.Since(syncCompleteTime)
|
||||
t.Logf("Login and sync completed in %v", loginDuration)
|
||||
|
||||
|
|
@ -206,6 +208,7 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) {
|
|||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
// Check each client's status individually to provide better diagnostics
|
||||
expiredCount := 0
|
||||
|
||||
for _, client := range allClients {
|
||||
status, err := client.Status()
|
||||
if assert.NoError(ct, err, "failed to get status for client %s", client.Hostname()) {
|
||||
|
|
@ -355,6 +358,7 @@ func TestOIDC024UserCreation(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -412,6 +416,7 @@ func TestOIDCAuthenticationWithPKCE(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -469,6 +474,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
|||
oidcMockUser("user1", true),
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -507,6 +513,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
|||
listUsers, err := headscale.ListUsers()
|
||||
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{
|
||||
{
|
||||
Id: 1,
|
||||
|
|
@ -527,9 +534,12 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
|||
}, 30*time.Second, 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
|
||||
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
listNodes, err = headscale.ListNodes()
|
||||
assert.NoError(ct, err, "Failed to list nodes during initial validation")
|
||||
assert.Len(ct, listNodes, 1, "Expected exactly 1 node after first login, got %d", len(listNodes))
|
||||
|
|
@ -537,14 +547,19 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
|||
|
||||
// Collect expected node IDs for validation after user1 initial login
|
||||
expectedNodes := make([]types.NodeID, 0, 1)
|
||||
|
||||
var nodeID uint64
|
||||
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
status := ts.MustStatus()
|
||||
assert.NotEmpty(ct, status.Self.ID, "Node ID should be populated in status")
|
||||
|
||||
var err error
|
||||
|
||||
nodeID, err = strconv.ParseUint(string(status.Self.ID), 10, 64)
|
||||
assert.NoError(ct, err, "Failed to parse node ID from status")
|
||||
}, 30*time.Second, 1*time.Second, "waiting for node ID to be populated in status after initial login")
|
||||
|
||||
expectedNodes = append(expectedNodes, types.NodeID(nodeID))
|
||||
|
||||
// Validate initial connection state for user1
|
||||
|
|
@ -582,6 +597,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
|||
listUsers, err := headscale.ListUsers()
|
||||
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{
|
||||
{
|
||||
Id: 1,
|
||||
|
|
@ -637,10 +653,12 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
|||
|
||||
// Security validation: Only user2's node should be active after user switch
|
||||
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())
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -654,6 +672,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
|||
// Check user2 node is online
|
||||
if node, exists := nodeStore[activeUser2NodeID]; exists {
|
||||
assert.NotNil(c, node.IsOnline, "User2 node should have online status")
|
||||
|
||||
if node.IsOnline != nil {
|
||||
assert.True(c, *node.IsOnline, "User2 node should be online after login")
|
||||
}
|
||||
|
|
@ -746,6 +765,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
|||
listUsers, err := headscale.ListUsers()
|
||||
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{
|
||||
{
|
||||
Id: 1,
|
||||
|
|
@ -815,10 +835,12 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
|||
|
||||
// Security validation: Only user1's node should be active after relogin
|
||||
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())
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -832,6 +854,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
|||
// Check user1 node is online
|
||||
if node, exists := nodeStore[activeUser1NodeID]; exists {
|
||||
assert.NotNil(c, node.IsOnline, "User1 node should have online status after relogin")
|
||||
|
||||
if node.IsOnline != nil {
|
||||
assert.True(c, *node.IsOnline, "User1 node should be online after relogin")
|
||||
}
|
||||
|
|
@ -906,6 +929,7 @@ func TestOIDCFollowUpUrl(t *testing.T) {
|
|||
time.Sleep(2 * time.Minute)
|
||||
|
||||
var newUrl *url.URL
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
st, err := ts.Status()
|
||||
assert.NoError(c, err)
|
||||
|
|
@ -1029,7 +1053,7 @@ func TestOIDCMultipleOpenedLoginUrls(t *testing.T) {
|
|||
require.NotEqual(t, redirect1.String(), redirect2.String())
|
||||
|
||||
// complete auth with the first opened "browser tab"
|
||||
_, redirect1, err = doLoginURLWithClient(ts.Hostname(), redirect1, loginClient, true)
|
||||
_, _, err = doLoginURLWithClient(ts.Hostname(), redirect1, loginClient, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
listUsers, err = headscale.ListUsers()
|
||||
|
|
@ -1106,6 +1130,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
|
|||
oidcMockUser("user1", true), // Relogin with same user
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -1145,6 +1170,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
|
|||
listUsers, err := headscale.ListUsers()
|
||||
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{
|
||||
{
|
||||
Id: 1,
|
||||
|
|
@ -1165,9 +1191,12 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
|
|||
}, 30*time.Second, 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
|
||||
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
initialNodes, err = headscale.ListNodes()
|
||||
assert.NoError(ct, err, "Failed to list nodes during initial validation")
|
||||
assert.Len(ct, initialNodes, 1, "Expected exactly 1 node after first login, got %d", len(initialNodes))
|
||||
|
|
@ -1175,14 +1204,19 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
|
|||
|
||||
// Collect expected node IDs for validation after user1 initial login
|
||||
expectedNodes := make([]types.NodeID, 0, 1)
|
||||
|
||||
var nodeID uint64
|
||||
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
status := ts.MustStatus()
|
||||
assert.NotEmpty(ct, status.Self.ID, "Node ID should be populated in status")
|
||||
|
||||
var err error
|
||||
|
||||
nodeID, err = strconv.ParseUint(string(status.Self.ID), 10, 64)
|
||||
assert.NoError(ct, err, "Failed to parse node ID from status")
|
||||
}, 30*time.Second, 1*time.Second, "waiting for node ID to be populated in status after initial login")
|
||||
|
||||
expectedNodes = append(expectedNodes, types.NodeID(nodeID))
|
||||
|
||||
// Validate initial connection state for user1
|
||||
|
|
@ -1239,6 +1273,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
|
|||
listUsers, err := headscale.ListUsers()
|
||||
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{
|
||||
{
|
||||
Id: 1,
|
||||
|
|
@ -1259,6 +1294,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
|
|||
}, 30*time.Second, 1*time.Second, "validating user1 persistence after same-user OIDC relogin cycle")
|
||||
|
||||
var finalNodes []*v1.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) {
|
||||
finalNodes, err = headscale.ListNodes()
|
||||
|
|
@ -1282,6 +1318,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
|
|||
|
||||
// Security validation: user1's node should be active after relogin
|
||||
activeUser1NodeID := types.NodeID(finalNodes[0].GetId())
|
||||
|
||||
t.Logf("Validating user1 node is online after same-user relogin at %s", time.Now().Format(TimestampFormat))
|
||||
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodeStore, err := headscale.DebugNodeStore()
|
||||
|
|
@ -1290,6 +1327,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
|
|||
// Check user1 node is online
|
||||
if node, exists := nodeStore[activeUser1NodeID]; exists {
|
||||
assert.NotNil(c, node.IsOnline, "User1 node should have online status after same-user relogin")
|
||||
|
||||
if node.IsOnline != nil {
|
||||
assert.True(c, *node.IsOnline, "User1 node should be online after same-user relogin")
|
||||
}
|
||||
|
|
@ -1359,6 +1397,7 @@ func TestOIDCExpiryAfterRestart(t *testing.T) {
|
|||
|
||||
// Verify initial expiry is set
|
||||
var initialExpiry time.Time
|
||||
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
nodes, err := headscale.ListNodes()
|
||||
assert.NoError(ct, err)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"testing"
|
||||
|
|
@ -67,6 +66,7 @@ func TestAuthWebFlowLogoutAndReloginSameUser(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -106,22 +106,27 @@ func TestAuthWebFlowLogoutAndReloginSameUser(t *testing.T) {
|
|||
validateInitialConnection(t, headscale, expectedNodes)
|
||||
|
||||
var listNodes []*v1.Node
|
||||
|
||||
t.Logf("Validating initial node count after web auth at %s", time.Now().Format(TimestampFormat))
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
listNodes, err = headscale.ListNodes()
|
||||
assert.NoError(ct, err, "Failed to list nodes after web authentication")
|
||||
assert.Len(ct, listNodes, len(allClients), "Expected %d nodes after web auth, got %d", len(allClients), len(listNodes))
|
||||
}, 30*time.Second, 2*time.Second, "validating node count matches client count after web authentication")
|
||||
|
||||
nodeCountBeforeLogout := len(listNodes)
|
||||
t.Logf("node count before logout: %d", nodeCountBeforeLogout)
|
||||
|
||||
clientIPs := make(map[TailscaleClient][]netip.Addr)
|
||||
|
||||
for _, client := range allClients {
|
||||
ips, err := client.IPs()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get IPs for client %s: %s", client.Hostname(), err)
|
||||
}
|
||||
|
||||
clientIPs[client] = ips
|
||||
}
|
||||
|
||||
|
|
@ -152,6 +157,7 @@ func TestAuthWebFlowLogoutAndReloginSameUser(t *testing.T) {
|
|||
t.Logf("Validating node persistence after logout at %s", time.Now().Format(TimestampFormat))
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
listNodes, err = headscale.ListNodes()
|
||||
assert.NoError(ct, err, "Failed to list nodes after web flow logout")
|
||||
assert.Len(ct, listNodes, nodeCountBeforeLogout, "Node count should remain unchanged after logout - expected %d nodes, got %d", nodeCountBeforeLogout, len(listNodes))
|
||||
|
|
@ -226,6 +232,7 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -240,9 +247,13 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
|
|||
allClients, err := scenario.ListTailscaleClients()
|
||||
requireNoErrListClients(t, err)
|
||||
|
||||
allIps, err := scenario.ListTailscaleClientsIPs()
|
||||
var allIps []netip.Addr
|
||||
|
||||
allIps, err = scenario.ListTailscaleClientsIPs()
|
||||
requireNoErrListClientIPs(t, err)
|
||||
|
||||
_ = allIps // used below after user switch
|
||||
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
requireNoErrSync(t, err)
|
||||
|
||||
|
|
@ -256,13 +267,16 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
|
|||
validateInitialConnection(t, headscale, expectedNodes)
|
||||
|
||||
var listNodes []*v1.Node
|
||||
|
||||
t.Logf("Validating initial node count after web auth at %s", time.Now().Format(TimestampFormat))
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
listNodes, err = headscale.ListNodes()
|
||||
assert.NoError(ct, err, "Failed to list nodes after initial web authentication")
|
||||
assert.Len(ct, listNodes, len(allClients), "Expected %d nodes after web auth, got %d", len(allClients), len(listNodes))
|
||||
}, 30*time.Second, 2*time.Second, "validating node count matches client count after initial web authentication")
|
||||
|
||||
nodeCountBeforeLogout := len(listNodes)
|
||||
t.Logf("node count before logout: %d", nodeCountBeforeLogout)
|
||||
|
||||
|
|
@ -299,7 +313,7 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
|
|||
|
||||
// Register all clients as user1 (this is where cross-user registration happens)
|
||||
// This simulates: headscale nodes register --user user1 --key <key>
|
||||
scenario.runHeadscaleRegister("user1", body)
|
||||
_ = scenario.runHeadscaleRegister("user1", body)
|
||||
}
|
||||
|
||||
// Wait for all clients to reach running state
|
||||
|
|
@ -313,9 +327,11 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
|
|||
t.Logf("all clients logged back in as user1")
|
||||
|
||||
var user1Nodes []*v1.Node
|
||||
|
||||
t.Logf("Validating user1 node count after relogin at %s", time.Now().Format(TimestampFormat))
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
user1Nodes, err = headscale.ListNodes("user1")
|
||||
assert.NoError(ct, err, "Failed to list nodes for user1 after web flow relogin")
|
||||
assert.Len(ct, user1Nodes, len(allClients), "User1 should have all %d clients after web flow relogin, got %d nodes", len(allClients), len(user1Nodes))
|
||||
|
|
@ -333,21 +349,24 @@ 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
|
||||
|
||||
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) {
|
||||
var err error
|
||||
|
||||
user2Nodes, err = headscale.ListNodes("user2")
|
||||
assert.NoError(ct, err, "Failed to list nodes for user2 after CLI registration to user1")
|
||||
assert.Len(ct, user2Nodes, len(allClients)/2, "User2 should still have %d old nodes (likely expired) after CLI registration to user1, got %d nodes", len(allClients)/2, len(user2Nodes))
|
||||
}, 30*time.Second, 2*time.Second, "validating user2 old nodes remain in database after CLI registration to user1")
|
||||
|
||||
t.Logf("Validating client login states after web flow user switch at %s", time.Now().Format(TimestampFormat))
|
||||
|
||||
for _, client := range allClients {
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
status, err := client.Status()
|
||||
assert.NoError(ct, err, "Failed to get status for client %s", client.Hostname())
|
||||
assert.Equal(ct, "user1@test.no", status.User[status.Self.UserID].LoginName, "Client %s should be logged in as user1 after web flow user switch, got %s", client.Hostname(), status.User[status.Self.UserID].LoginName)
|
||||
}, 30*time.Second, 2*time.Second, fmt.Sprintf("validating %s is logged in as user1 after web flow user switch", client.Hostname()))
|
||||
}, 30*time.Second, 2*time.Second, "validating %s is logged in as user1 after web flow user switch", client.Hostname())
|
||||
}
|
||||
|
||||
// Test connectivity after user switch
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ func TestUserCommand(t *testing.T) {
|
|||
"--identifier=1",
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, deleteResult, "User destroyed")
|
||||
|
||||
var listAfterIDDelete []*v1.User
|
||||
|
|
@ -245,7 +245,7 @@ func TestUserCommand(t *testing.T) {
|
|||
"--name=newname",
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, deleteResult, "User destroyed")
|
||||
|
||||
var listAfterNameDelete []v1.User
|
||||
|
|
@ -571,7 +571,9 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
|||
func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
//nolint:goconst // test data, not worth extracting
|
||||
user1 := "user1"
|
||||
//nolint:goconst // test data, not worth extracting
|
||||
user2 := "user2"
|
||||
|
||||
spec := ScenarioSpec{
|
||||
|
|
@ -829,7 +831,7 @@ func TestApiKeyCommand(t *testing.T) {
|
|||
"json",
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, apiResult)
|
||||
|
||||
keys[idx] = apiResult
|
||||
|
|
@ -907,7 +909,7 @@ func TestApiKeyCommand(t *testing.T) {
|
|||
listedAPIKeys[idx].GetPrefix(),
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
expiredPrefixes[listedAPIKeys[idx].GetPrefix()] = true
|
||||
}
|
||||
|
|
@ -952,7 +954,7 @@ func TestApiKeyCommand(t *testing.T) {
|
|||
"--prefix",
|
||||
listedAPIKeys[0].GetPrefix(),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
var listedAPIKeysAfterDelete []v1.ApiKey
|
||||
|
||||
|
|
@ -1071,7 +1073,7 @@ func TestNodeCommand(t *testing.T) {
|
|||
}
|
||||
nodes := make([]*v1.Node, len(regIDs))
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
for index, regID := range regIDs {
|
||||
_, err := headscale.Execute(
|
||||
|
|
@ -1089,7 +1091,7 @@ func TestNodeCommand(t *testing.T) {
|
|||
"json",
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
var node v1.Node
|
||||
|
||||
|
|
@ -1156,7 +1158,7 @@ func TestNodeCommand(t *testing.T) {
|
|||
}
|
||||
otherUserMachines := make([]*v1.Node, len(otherUserRegIDs))
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
for index, regID := range otherUserRegIDs {
|
||||
_, err := headscale.Execute(
|
||||
|
|
@ -1174,7 +1176,7 @@ func TestNodeCommand(t *testing.T) {
|
|||
"json",
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
var node v1.Node
|
||||
|
||||
|
|
@ -1281,7 +1283,7 @@ func TestNodeCommand(t *testing.T) {
|
|||
"--force",
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test: list main user after node is deleted
|
||||
var listOnlyMachineUserAfterDelete []v1.Node
|
||||
|
|
@ -1348,7 +1350,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
|||
"json",
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
var node v1.Node
|
||||
|
||||
|
|
@ -1411,7 +1413,7 @@ func TestNodeExpireCommand(t *testing.T) {
|
|||
strconv.FormatUint(listAll[idx].GetId(), 10),
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
var listAllAfterExpiry []v1.Node
|
||||
|
|
@ -1467,7 +1469,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
|||
}
|
||||
nodes := make([]*v1.Node, len(regIDs))
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
for index, regID := range regIDs {
|
||||
_, err := headscale.Execute(
|
||||
|
|
@ -1549,7 +1551,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
|||
fmt.Sprintf("newnode-%d", idx+1),
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, res, "Node renamed")
|
||||
}
|
||||
|
|
@ -1590,7 +1592,7 @@ func TestNodeRenameCommand(t *testing.T) {
|
|||
strings.Repeat("t", 64),
|
||||
},
|
||||
)
|
||||
assert.ErrorContains(t, err, "must not exceed 63 characters")
|
||||
require.ErrorContains(t, err, "must not exceed 63 characters")
|
||||
|
||||
var listAllAfterRenameAttempt []v1.Node
|
||||
|
||||
|
|
@ -1658,7 +1660,7 @@ func TestPolicyCommand(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
pBytes, _ := json.Marshal(p)
|
||||
pBytes, _ := json.Marshal(p) //nolint:errchkjson
|
||||
|
||||
policyFilePath := "/etc/headscale/policy.json"
|
||||
|
||||
|
|
@ -1745,7 +1747,7 @@ func TestPolicyBrokenConfigCommand(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
pBytes, _ := json.Marshal(p)
|
||||
pBytes, _ := json.Marshal(p) //nolint:errchkjson
|
||||
|
||||
policyFilePath := "/etc/headscale/policy.json"
|
||||
|
||||
|
|
@ -1763,7 +1765,7 @@ func TestPolicyBrokenConfigCommand(t *testing.T) {
|
|||
policyFilePath,
|
||||
},
|
||||
)
|
||||
assert.ErrorContains(t, err, `invalid action "unknown-action"`)
|
||||
require.ErrorContains(t, err, `invalid ACL action: "unknown-action"`)
|
||||
|
||||
// The new policy was invalid, the old one should still be in place, which
|
||||
// is none.
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import (
|
|||
|
||||
type ControlServer interface {
|
||||
Shutdown() (string, string, error)
|
||||
SaveLog(string) (string, string, error)
|
||||
SaveProfile(string) error
|
||||
SaveLog(path string) (string, string, error)
|
||||
SaveProfile(path string) error
|
||||
Execute(command []string) (string, error)
|
||||
WriteFile(path string, content []byte) error
|
||||
ConnectToNetwork(network *dockertest.Network) error
|
||||
|
|
@ -35,12 +35,12 @@ type ControlServer interface {
|
|||
ListUsers() ([]*v1.User, error)
|
||||
MapUsers() (map[string]*v1.User, error)
|
||||
DeleteUser(userID uint64) error
|
||||
ApproveRoutes(uint64, []netip.Prefix) (*v1.Node, error)
|
||||
ApproveRoutes(nodeID uint64, routes []netip.Prefix) (*v1.Node, error)
|
||||
SetNodeTags(nodeID uint64, tags []string) error
|
||||
GetCert() []byte
|
||||
GetHostname() string
|
||||
GetIPInNetwork(network *dockertest.Network) string
|
||||
SetPolicy(*policyv2.Policy) error
|
||||
SetPolicy(pol *policyv2.Policy) error
|
||||
GetAllMapReponses() (map[types.NodeID][]tailcfg.MapResponse, error)
|
||||
PrimaryRoutes() (*routes.DebugRoutes, error)
|
||||
DebugBatcher() (*hscontrol.DebugBatcherInfo, error)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ func TestDERPVerifyEndpoint(t *testing.T) {
|
|||
// Generate random hostname for the headscale instance
|
||||
hash, err := util.GenerateRandomStringDNSSafe(6)
|
||||
require.NoError(t, err)
|
||||
|
||||
testName := "derpverify"
|
||||
hostname := fmt.Sprintf("hs-%s-%s", testName, hash)
|
||||
|
||||
|
|
@ -40,6 +41,7 @@ func TestDERPVerifyEndpoint(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -104,13 +106,16 @@ func DERPVerify(
|
|||
defer c.Close()
|
||||
|
||||
var result error
|
||||
if err := c.Connect(t.Context()); err != nil {
|
||||
|
||||
err := c.Connect(t.Context())
|
||||
if err != nil {
|
||||
result = fmt.Errorf("client Connect: %w", err)
|
||||
}
|
||||
if m, err := c.Recv(); err != nil {
|
||||
|
||||
if m, err := c.Recv(); err != nil { //nolint:noinlineerr
|
||||
result = fmt.Errorf("client first Recv: %w", err)
|
||||
} else if v, ok := m.(derp.ServerInfoMessage); !ok {
|
||||
result = fmt.Errorf("client first Recv was unexpected type %T", v)
|
||||
result = fmt.Errorf("client first Recv was unexpected type %T", v) //nolint:err113
|
||||
}
|
||||
|
||||
if expectSuccess && result != nil {
|
||||
|
|
|
|||
|
|
@ -86,14 +86,13 @@ func TestResolveMagicDNSExtraRecordsPath(t *testing.T) {
|
|||
|
||||
const erPath = "/tmp/extra_records.json"
|
||||
|
||||
extraRecords := []tailcfg.DNSRecord{
|
||||
{
|
||||
Name: "test.myvpn.example.com",
|
||||
Type: "A",
|
||||
Value: "6.6.6.6",
|
||||
},
|
||||
}
|
||||
b, _ := json.Marshal(extraRecords)
|
||||
extraRecords := make([]tailcfg.DNSRecord, 0, 2)
|
||||
extraRecords = append(extraRecords, tailcfg.DNSRecord{
|
||||
Name: "test.myvpn.example.com",
|
||||
Type: "A",
|
||||
Value: "6.6.6.6",
|
||||
})
|
||||
b, _ := json.Marshal(extraRecords) //nolint:errchkjson
|
||||
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{
|
||||
tsic.WithPackages("python3", "curl", "bind-tools"),
|
||||
|
|
@ -133,7 +132,7 @@ func TestResolveMagicDNSExtraRecordsPath(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Write the file directly into place from the docker API.
|
||||
b0, _ := json.Marshal([]tailcfg.DNSRecord{
|
||||
b0, _ := json.Marshal([]tailcfg.DNSRecord{ //nolint:errchkjson
|
||||
{
|
||||
Name: "docker.myvpn.example.com",
|
||||
Type: "A",
|
||||
|
|
@ -155,7 +154,7 @@ func TestResolveMagicDNSExtraRecordsPath(t *testing.T) {
|
|||
Type: "A",
|
||||
Value: "7.7.7.7",
|
||||
})
|
||||
b2, _ := json.Marshal(extraRecords)
|
||||
b2, _ := json.Marshal(extraRecords) //nolint:errchkjson
|
||||
|
||||
err = hs.WriteFile(erPath+"2", b2)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -169,7 +168,7 @@ func TestResolveMagicDNSExtraRecordsPath(t *testing.T) {
|
|||
|
||||
// Write a new file and copy it to the path to ensure the reload
|
||||
// works when a file is copied into place.
|
||||
b3, _ := json.Marshal([]tailcfg.DNSRecord{
|
||||
b3, _ := json.Marshal([]tailcfg.DNSRecord{ //nolint:errchkjson
|
||||
{
|
||||
Name: "copy.myvpn.example.com",
|
||||
Type: "A",
|
||||
|
|
@ -187,7 +186,7 @@ func TestResolveMagicDNSExtraRecordsPath(t *testing.T) {
|
|||
}
|
||||
|
||||
// Write in place to ensure pipe like behaviour works
|
||||
b4, _ := json.Marshal([]tailcfg.DNSRecord{
|
||||
b4, _ := json.Marshal([]tailcfg.DNSRecord{ //nolint:errchkjson
|
||||
{
|
||||
Name: "docker.myvpn.example.com",
|
||||
Type: "A",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ func DockerAddIntegrationLabels(opts *dockertest.RunOptions, testType string) {
|
|||
if opts.Labels == nil {
|
||||
opts.Labels = make(map[string]string)
|
||||
}
|
||||
|
||||
opts.Labels["hi.run-id"] = runID
|
||||
opts.Labels["hi.test-type"] = testType
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,9 +38,10 @@ type buffer struct {
|
|||
|
||||
// Write appends the contents of p to the buffer, growing the buffer as needed. It returns
|
||||
// the number of bytes written.
|
||||
func (b *buffer) Write(p []byte) (n int, err error) {
|
||||
func (b *buffer) Write(p []byte) (int, error) {
|
||||
b.mutex.Lock()
|
||||
defer b.mutex.Unlock()
|
||||
|
||||
return b.store.Write(p)
|
||||
}
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ func (b *buffer) Write(p []byte) (n int, err error) {
|
|||
func (b *buffer) String() string {
|
||||
b.mutex.Lock()
|
||||
defer b.mutex.Unlock()
|
||||
|
||||
return b.store.String()
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +68,8 @@ func ExecuteCommand(
|
|||
}
|
||||
|
||||
for _, opt := range options {
|
||||
if err := opt(&execConfig); err != nil {
|
||||
err := opt(&execConfig)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("execute-command/options: %w", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -105,7 +108,6 @@ func ExecuteCommand(
|
|||
// log.Println("Command: ", cmd)
|
||||
// log.Println("stdout: ", stdout.String())
|
||||
// log.Println("stderr: ", stderr.String())
|
||||
|
||||
return stdout.String(), stderr.String(), fmt.Errorf("command failed, stderr: %s: %w", stderr.String(), ErrDockertestCommandFailed)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ func SaveLog(
|
|||
}
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
err = WriteLog(pool, resource, &stdout, &stderr)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
|
|
@ -55,6 +56,7 @@ func SaveLog(
|
|||
log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
|
||||
|
||||
stdoutPath := path.Join(basePath, resource.Container.Name+".stdout.log")
|
||||
|
||||
err = os.WriteFile(
|
||||
stdoutPath,
|
||||
stdout.Bytes(),
|
||||
|
|
@ -65,6 +67,7 @@ func SaveLog(
|
|||
}
|
||||
|
||||
stderrPath := path.Join(basePath, resource.Container.Name+".stderr.log")
|
||||
|
||||
err = os.WriteFile(
|
||||
stderrPath,
|
||||
stderr.Bytes(),
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@ func GetFirstOrCreateNetwork(pool *dockertest.Pool, name string) (*dockertest.Ne
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("looking up network names: %w", err)
|
||||
}
|
||||
|
||||
if len(networks) == 0 {
|
||||
if _, err := pool.CreateNetwork(name); err == nil {
|
||||
if _, err := pool.CreateNetwork(name); err == nil { //nolint:noinlineerr // intentional inline check
|
||||
// Create does not give us an updated version of the resource, so we need to
|
||||
// get it again.
|
||||
networks, err := pool.NetworksByName(name)
|
||||
|
|
@ -90,6 +91,7 @@ func RandomFreeHostPort() (int, error) {
|
|||
// CleanUnreferencedNetworks removes networks that are not referenced by any containers.
|
||||
func CleanUnreferencedNetworks(pool *dockertest.Pool) error {
|
||||
filter := "name=hs-"
|
||||
|
||||
networks, err := pool.NetworksByName(filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting networks by filter %q: %w", filter, err)
|
||||
|
|
@ -122,6 +124,7 @@ func CleanImagesInCI(pool *dockertest.Pool) error {
|
|||
}
|
||||
|
||||
removedCount := 0
|
||||
|
||||
for _, image := range images {
|
||||
// Only remove dangling (untagged) images to avoid forcing rebuilds
|
||||
// Dangling images have no RepoTags or only have "<none>:<none>"
|
||||
|
|
|
|||
|
|
@ -159,10 +159,12 @@ func New(
|
|||
} else {
|
||||
hostname = fmt.Sprintf("derp-%s-%s", strings.ReplaceAll(version, ".", "-"), hash)
|
||||
}
|
||||
|
||||
tlsCert, tlsKey, err := integrationutil.CreateCertificate(hostname)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating certificates for headscale test: %w", err)
|
||||
}
|
||||
|
||||
dsic := &DERPServerInContainer{
|
||||
version: version,
|
||||
hostname: hostname,
|
||||
|
|
@ -185,6 +187,7 @@ func New(
|
|||
fmt.Fprintf(&cmdArgs, " --a=:%d", dsic.derpPort)
|
||||
fmt.Fprintf(&cmdArgs, " --stun=true")
|
||||
fmt.Fprintf(&cmdArgs, " --stun-port=%d", dsic.stunPort)
|
||||
|
||||
if dsic.withVerifyClientURL != "" {
|
||||
fmt.Fprintf(&cmdArgs, " --verify-client-url=%s", dsic.withVerifyClientURL)
|
||||
}
|
||||
|
|
@ -214,11 +217,13 @@ func New(
|
|||
}
|
||||
|
||||
var container *dockertest.Resource
|
||||
|
||||
buildOptions := &dockertest.BuildOptions{
|
||||
Dockerfile: "Dockerfile.derper",
|
||||
ContextDir: dockerContextPath,
|
||||
BuildArgs: []docker.BuildArg{},
|
||||
}
|
||||
|
||||
switch version {
|
||||
case "head":
|
||||
buildOptions.BuildArgs = append(buildOptions.BuildArgs, docker.BuildArg{
|
||||
|
|
@ -249,6 +254,7 @@ func New(
|
|||
err,
|
||||
)
|
||||
}
|
||||
|
||||
log.Printf("Created %s container\n", hostname)
|
||||
|
||||
dsic.container = container
|
||||
|
|
@ -259,12 +265,14 @@ func New(
|
|||
return nil, fmt.Errorf("writing TLS certificate to container: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(dsic.tlsCert) != 0 {
|
||||
err = dsic.WriteFile(fmt.Sprintf("%s/%s.crt", DERPerCertRoot, dsic.hostname), dsic.tlsCert)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("writing TLS certificate to container: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(dsic.tlsKey) != 0 {
|
||||
err = dsic.WriteFile(fmt.Sprintf("%s/%s.key", DERPerCertRoot, dsic.hostname), dsic.tlsKey)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ package integration
|
|||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -23,8 +26,6 @@ import (
|
|||
"github.com/oauth2-proxy/mockoidc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
|
@ -46,9 +47,16 @@ const (
|
|||
// TimestampFormatRunID is used for generating unique run identifiers
|
||||
// Format: "20060102-150405" provides compact date-time for file/directory names.
|
||||
TimestampFormatRunID = "20060102-150405"
|
||||
|
||||
// stateOnline is the string representation for online state in logs.
|
||||
stateOnline = "online"
|
||||
// stateOffline is the string representation for offline state in logs.
|
||||
stateOffline = "offline"
|
||||
)
|
||||
|
||||
// NodeSystemStatus represents the status of a node across different systems
|
||||
var errNoNewClientFound = errors.New("no new client found")
|
||||
|
||||
// NodeSystemStatus represents the status of a node across different systems.
|
||||
type NodeSystemStatus struct {
|
||||
Batcher bool
|
||||
BatcherConnCount int
|
||||
|
|
@ -105,7 +113,7 @@ func requireNoErrLogout(t *testing.T, err error) {
|
|||
require.NoError(t, err, "failed to log out tailscale nodes")
|
||||
}
|
||||
|
||||
// collectExpectedNodeIDs extracts node IDs from a list of TailscaleClients for validation purposes
|
||||
// collectExpectedNodeIDs extracts node IDs from a list of TailscaleClients for validation purposes.
|
||||
func collectExpectedNodeIDs(t *testing.T, clients []TailscaleClient) []types.NodeID {
|
||||
t.Helper()
|
||||
|
||||
|
|
@ -114,8 +122,10 @@ func collectExpectedNodeIDs(t *testing.T, clients []TailscaleClient) []types.Nod
|
|||
status := client.MustStatus()
|
||||
nodeID, err := strconv.ParseUint(string(status.Self.ID), 10, 64)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedNodes = append(expectedNodes, types.NodeID(nodeID))
|
||||
}
|
||||
|
||||
return expectedNodes
|
||||
}
|
||||
|
||||
|
|
@ -149,15 +159,17 @@ func validateReloginComplete(t *testing.T, headscale ControlServer, expectedNode
|
|||
}
|
||||
|
||||
// requireAllClientsOnline validates that all nodes are online/offline across all headscale systems
|
||||
// requireAllClientsOnline verifies all expected nodes are in the specified online state across all systems
|
||||
// requireAllClientsOnline verifies all expected nodes are in the specified online state across all systems.
|
||||
func requireAllClientsOnline(t *testing.T, headscale ControlServer, expectedNodes []types.NodeID, expectedOnline bool, message string, timeout time.Duration) {
|
||||
t.Helper()
|
||||
|
||||
startTime := time.Now()
|
||||
stateStr := "offline"
|
||||
|
||||
stateStr := stateOffline
|
||||
if expectedOnline {
|
||||
stateStr = "online"
|
||||
stateStr = stateOnline
|
||||
}
|
||||
|
||||
t.Logf("requireAllSystemsOnline: Starting %s validation for %d nodes at %s - %s", stateStr, len(expectedNodes), startTime.Format(TimestampFormat), message)
|
||||
|
||||
if expectedOnline {
|
||||
|
|
@ -165,22 +177,26 @@ func requireAllClientsOnline(t *testing.T, headscale ControlServer, expectedNode
|
|||
requireAllClientsOnlineWithSingleTimeout(t, headscale, expectedNodes, expectedOnline, message, timeout)
|
||||
} else {
|
||||
// For offline validation, use staged approach with component-specific timeouts
|
||||
requireAllClientsOfflineStaged(t, headscale, expectedNodes, message, timeout)
|
||||
requireAllClientsOfflineStaged(t, headscale, expectedNodes)
|
||||
}
|
||||
|
||||
endTime := time.Now()
|
||||
t.Logf("requireAllSystemsOnline: Completed %s validation for %d nodes at %s - Duration: %s - %s", stateStr, len(expectedNodes), endTime.Format(TimestampFormat), endTime.Sub(startTime), message)
|
||||
}
|
||||
|
||||
// requireAllClientsOnlineWithSingleTimeout is the original validation logic for online state
|
||||
// requireAllClientsOnlineWithSingleTimeout is the original validation logic for online state.
|
||||
//
|
||||
//nolint:gocyclo // complex validation with multiple node states
|
||||
func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlServer, expectedNodes []types.NodeID, expectedOnline bool, message string, timeout time.Duration) {
|
||||
t.Helper()
|
||||
|
||||
var prevReport string
|
||||
|
||||
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
// Get batcher state
|
||||
debugInfo, err := headscale.DebugBatcher()
|
||||
assert.NoError(c, err, "Failed to get batcher debug info")
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -188,6 +204,7 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
|
|||
// Get map responses
|
||||
mapResponses, err := headscale.GetAllMapReponses()
|
||||
assert.NoError(c, err, "Failed to get map responses")
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -195,6 +212,7 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
|
|||
// Get nodestore state
|
||||
nodeStore, err := headscale.DebugNodeStore()
|
||||
assert.NoError(c, err, "Failed to get nodestore debug info")
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -265,6 +283,7 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
|
|||
if id == nodeID {
|
||||
continue // Skip self-references
|
||||
}
|
||||
|
||||
expectedPeerMaps++
|
||||
|
||||
if online, exists := peerMap[nodeID]; exists && online {
|
||||
|
|
@ -279,6 +298,7 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.Lenf(c, onlineFromMaps, expectedCount, "MapResponses missing nodes in status check")
|
||||
|
||||
// Update status with map response data
|
||||
|
|
@ -302,10 +322,12 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
|
|||
|
||||
// Verify all systems show nodes in expected state and report failures
|
||||
allMatch := true
|
||||
|
||||
var failureReport strings.Builder
|
||||
|
||||
ids := types.NodeIDs(maps.Keys(nodeStatus))
|
||||
ids := types.NodeIDs(slices.AppendSeq(make([]types.NodeID, 0, len(nodeStatus)), maps.Keys(nodeStatus)))
|
||||
slices.Sort(ids)
|
||||
|
||||
for _, nodeID := range ids {
|
||||
status := nodeStatus[nodeID]
|
||||
systemsMatch := (status.Batcher == expectedOnline) &&
|
||||
|
|
@ -314,10 +336,12 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
|
|||
|
||||
if !systemsMatch {
|
||||
allMatch = false
|
||||
stateStr := "offline"
|
||||
|
||||
stateStr := stateOffline
|
||||
if expectedOnline {
|
||||
stateStr = "online"
|
||||
stateStr = stateOnline
|
||||
}
|
||||
|
||||
failureReport.WriteString(fmt.Sprintf("node:%d is not fully %s (timestamp: %s):\n", nodeID, stateStr, time.Now().Format(TimestampFormat)))
|
||||
failureReport.WriteString(fmt.Sprintf(" - batcher: %t (expected: %t)\n", status.Batcher, expectedOnline))
|
||||
failureReport.WriteString(fmt.Sprintf(" - conn count: %d\n", status.BatcherConnCount))
|
||||
|
|
@ -332,6 +356,7 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
|
|||
t.Logf("Previous report:\n%s", prevReport)
|
||||
t.Logf("Current report:\n%s", failureReport.String())
|
||||
t.Logf("Report diff:\n%s", diff)
|
||||
|
||||
prevReport = failureReport.String()
|
||||
}
|
||||
|
||||
|
|
@ -341,16 +366,17 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
|
|||
assert.Fail(c, failureReport.String())
|
||||
}
|
||||
|
||||
stateStr := "offline"
|
||||
stateStr := stateOffline
|
||||
if expectedOnline {
|
||||
stateStr = "online"
|
||||
stateStr = stateOnline
|
||||
}
|
||||
assert.True(c, allMatch, fmt.Sprintf("Not all %d nodes are %s across all systems (batcher, mapresponses, nodestore)", len(expectedNodes), stateStr))
|
||||
|
||||
assert.True(c, allMatch, "Not all %d nodes are %s across all systems (batcher, mapresponses, nodestore)", len(expectedNodes), stateStr)
|
||||
}, timeout, 2*time.Second, message)
|
||||
}
|
||||
|
||||
// requireAllClientsOfflineStaged validates offline state with staged timeouts for different components
|
||||
func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expectedNodes []types.NodeID, message string, totalTimeout time.Duration) {
|
||||
// requireAllClientsOfflineStaged validates offline state with staged timeouts for different components.
|
||||
func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expectedNodes []types.NodeID) {
|
||||
t.Helper()
|
||||
|
||||
// Stage 1: Verify batcher disconnection (should be immediate)
|
||||
|
|
@ -358,18 +384,22 @@ func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expec
|
|||
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
debugInfo, err := headscale.DebugBatcher()
|
||||
assert.NoError(c, err, "Failed to get batcher debug info")
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
allBatcherOffline := true
|
||||
|
||||
for _, nodeID := range expectedNodes {
|
||||
nodeIDStr := fmt.Sprintf("%d", nodeID)
|
||||
if nodeInfo, exists := debugInfo.ConnectedNodes[nodeIDStr]; exists && nodeInfo.Connected {
|
||||
allBatcherOffline = false
|
||||
|
||||
assert.False(c, nodeInfo.Connected, "Node %d should not be connected in batcher", nodeID)
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(c, allBatcherOffline, "All nodes should be disconnected from batcher")
|
||||
}, 15*time.Second, 1*time.Second, "batcher disconnection validation")
|
||||
|
||||
|
|
@ -378,20 +408,24 @@ func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expec
|
|||
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodeStore, err := headscale.DebugNodeStore()
|
||||
assert.NoError(c, err, "Failed to get nodestore debug info")
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
allNodeStoreOffline := true
|
||||
|
||||
for _, nodeID := range expectedNodes {
|
||||
if node, exists := nodeStore[nodeID]; exists {
|
||||
isOnline := node.IsOnline != nil && *node.IsOnline
|
||||
if isOnline {
|
||||
allNodeStoreOffline = false
|
||||
|
||||
assert.False(c, isOnline, "Node %d should be offline in nodestore", nodeID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(c, allNodeStoreOffline, "All nodes should be offline in nodestore")
|
||||
}, 20*time.Second, 1*time.Second, "nodestore offline validation")
|
||||
|
||||
|
|
@ -400,6 +434,7 @@ func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expec
|
|||
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
mapResponses, err := headscale.GetAllMapReponses()
|
||||
assert.NoError(c, err, "Failed to get map responses")
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -412,7 +447,8 @@ func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expec
|
|||
for nodeID := range onlineMap {
|
||||
if slices.Contains(expectedNodes, nodeID) {
|
||||
allMapResponsesOffline = false
|
||||
assert.False(c, true, "Node %d should not appear in map responses", nodeID)
|
||||
|
||||
assert.Fail(c, fmt.Sprintf("Node %d should not appear in map responses", nodeID))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -422,13 +458,16 @@ func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expec
|
|||
if id == nodeID {
|
||||
continue // Skip self-references
|
||||
}
|
||||
|
||||
if online, exists := peerMap[nodeID]; exists && online {
|
||||
allMapResponsesOffline = false
|
||||
|
||||
assert.False(c, online, "Node %d should not be visible in node %d's map response", nodeID, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(c, allMapResponsesOffline, "All nodes should be absent from peer map responses")
|
||||
}, 60*time.Second, 2*time.Second, "map response propagation validation")
|
||||
|
||||
|
|
@ -438,6 +477,8 @@ func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expec
|
|||
// requireAllClientsNetInfoAndDERP validates that all nodes have NetInfo in the database
|
||||
// and a valid DERP server based on the NetInfo. This function follows the pattern of
|
||||
// requireAllClientsOnline by using hsic.DebugNodeStore to get the database state.
|
||||
//
|
||||
//nolint:unparam // timeout is configurable for flexibility even though callers currently use same value
|
||||
func requireAllClientsNetInfoAndDERP(t *testing.T, headscale ControlServer, expectedNodes []types.NodeID, message string, timeout time.Duration) {
|
||||
t.Helper()
|
||||
|
||||
|
|
@ -448,6 +489,7 @@ func requireAllClientsNetInfoAndDERP(t *testing.T, headscale ControlServer, expe
|
|||
// Get nodestore state
|
||||
nodeStore, err := headscale.DebugNodeStore()
|
||||
assert.NoError(c, err, "Failed to get nodestore debug info")
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -462,12 +504,14 @@ func requireAllClientsNetInfoAndDERP(t *testing.T, headscale ControlServer, expe
|
|||
for _, nodeID := range expectedNodes {
|
||||
node, exists := nodeStore[nodeID]
|
||||
assert.True(c, exists, "Node %d not found in nodestore during NetInfo validation", nodeID)
|
||||
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate that the node has Hostinfo
|
||||
assert.NotNil(c, node.Hostinfo, "Node %d (%s) should have Hostinfo for NetInfo validation", nodeID, node.Hostname)
|
||||
|
||||
if node.Hostinfo == nil {
|
||||
t.Logf("Node %d (%s) missing Hostinfo at %s", nodeID, node.Hostname, time.Now().Format(TimestampFormat))
|
||||
continue
|
||||
|
|
@ -475,6 +519,7 @@ func requireAllClientsNetInfoAndDERP(t *testing.T, headscale ControlServer, expe
|
|||
|
||||
// Validate that the node has NetInfo
|
||||
assert.NotNil(c, node.Hostinfo.NetInfo, "Node %d (%s) should have NetInfo in Hostinfo for DERP connectivity", nodeID, node.Hostname)
|
||||
|
||||
if node.Hostinfo.NetInfo == nil {
|
||||
t.Logf("Node %d (%s) missing NetInfo at %s", nodeID, node.Hostname, time.Now().Format(TimestampFormat))
|
||||
continue
|
||||
|
|
@ -482,7 +527,7 @@ func requireAllClientsNetInfoAndDERP(t *testing.T, headscale ControlServer, expe
|
|||
|
||||
// Validate that the node has a valid DERP server (PreferredDERP should be > 0)
|
||||
preferredDERP := node.Hostinfo.NetInfo.PreferredDERP
|
||||
assert.Greater(c, preferredDERP, 0, "Node %d (%s) should have a valid DERP server (PreferredDERP > 0) for relay connectivity, got %d", nodeID, node.Hostname, preferredDERP)
|
||||
assert.Positive(c, preferredDERP, "Node %d (%s) should have a valid DERP server (PreferredDERP > 0) for relay connectivity, got %d", nodeID, node.Hostname, preferredDERP)
|
||||
|
||||
t.Logf("Node %d (%s) has valid NetInfo with DERP server %d at %s", nodeID, node.Hostname, preferredDERP, time.Now().Format(TimestampFormat))
|
||||
}
|
||||
|
|
@ -496,6 +541,7 @@ 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) {
|
||||
t.Helper()
|
||||
assert.NotNil(t, node)
|
||||
assert.NotNil(t, node.GetLastSeen())
|
||||
}
|
||||
|
|
@ -514,7 +560,7 @@ func assertTailscaleNodesLogout(t assert.TestingT, clients []TailscaleClient) {
|
|||
|
||||
for _, client := range clients {
|
||||
status, err := client.Status()
|
||||
assert.NoError(t, err, "failed to get status for client %s", client.Hostname())
|
||||
assert.NoError(t, err, "failed to get status for client %s", client.Hostname()) //nolint:testifylint // assert.TestingT interface
|
||||
assert.Equal(t, "NeedsLogin", status.BackendState,
|
||||
"client %s should be logged out", client.Hostname())
|
||||
}
|
||||
|
|
@ -523,8 +569,11 @@ func assertTailscaleNodesLogout(t assert.TestingT, clients []TailscaleClient) {
|
|||
// pingAllHelper performs ping tests between all clients and addresses, returning success count.
|
||||
// This is used to validate network connectivity in integration tests.
|
||||
// Returns the total number of successful ping operations.
|
||||
//
|
||||
//nolint:unparam // opts is variadic for extensibility even though callers currently don't pass options
|
||||
func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string, opts ...tsic.PingOption) int {
|
||||
t.Helper()
|
||||
|
||||
success := 0
|
||||
|
||||
for _, client := range clients {
|
||||
|
|
@ -546,6 +595,7 @@ func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string, opts
|
|||
// for validating NAT traversal and relay functionality. Returns success count.
|
||||
func pingDerpAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
|
||||
t.Helper()
|
||||
|
||||
success := 0
|
||||
|
||||
for _, client := range clients {
|
||||
|
|
@ -596,6 +646,8 @@ func isSelfClient(client TailscaleClient, addr string) bool {
|
|||
// assertClientsState validates the status and netmap of a list of clients for general connectivity.
|
||||
// Runs parallel validation of status, netcheck, and netmap for all clients to ensure
|
||||
// they have proper network configuration for all-to-all connectivity tests.
|
||||
//
|
||||
//nolint:unused
|
||||
func assertClientsState(t *testing.T, clients []TailscaleClient) {
|
||||
t.Helper()
|
||||
|
||||
|
|
@ -603,9 +655,12 @@ func assertClientsState(t *testing.T, clients []TailscaleClient) {
|
|||
|
||||
for _, client := range clients {
|
||||
wg.Add(1)
|
||||
|
||||
c := client // Avoid loop pointer
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
assertValidStatus(t, c)
|
||||
assertValidNetcheck(t, c)
|
||||
assertValidNetmap(t, c)
|
||||
|
|
@ -620,6 +675,8 @@ func assertClientsState(t *testing.T, clients []TailscaleClient) {
|
|||
// Checks self node and all peers for essential networking data including hostinfo, addresses,
|
||||
// endpoints, and DERP configuration. Skips validation for Tailscale versions below 1.56.
|
||||
// This test is not suitable for ACL/partial connection tests.
|
||||
//
|
||||
//nolint:unused
|
||||
func assertValidNetmap(t *testing.T, client TailscaleClient) {
|
||||
t.Helper()
|
||||
|
||||
|
|
@ -636,6 +693,7 @@ func assertValidNetmap(t *testing.T, client TailscaleClient) {
|
|||
assert.NoError(c, err, "getting netmap for %q", client.Hostname())
|
||||
|
||||
assert.Truef(c, netmap.SelfNode.Hostinfo().Valid(), "%q does not have Hostinfo", client.Hostname())
|
||||
|
||||
if hi := netmap.SelfNode.Hostinfo(); hi.Valid() {
|
||||
assert.LessOrEqual(c, 1, netmap.SelfNode.Hostinfo().Services().Len(), "%q does not have enough services, got: %v", client.Hostname(), netmap.SelfNode.Hostinfo().Services())
|
||||
}
|
||||
|
|
@ -650,10 +708,11 @@ func assertValidNetmap(t *testing.T, client TailscaleClient) {
|
|||
assert.Falsef(c, netmap.SelfNode.DiscoKey().IsZero(), "%q does not have a valid DiscoKey", client.Hostname())
|
||||
|
||||
for _, peer := range netmap.Peers {
|
||||
assert.NotEqualf(c, "127.3.3.40:0", peer.LegacyDERPString(), "peer (%s) has no home DERP in %q's netmap, got: %s", peer.ComputedName(), client.Hostname(), peer.LegacyDERPString())
|
||||
assert.NotEqualf(c, "127.3.3.40:0", peer.LegacyDERPString(), "peer (%s) has no home DERP in %q's netmap, got: %s", peer.ComputedName(), client.Hostname(), peer.LegacyDERPString()) //nolint:staticcheck // SA1019: testing legacy field
|
||||
assert.NotEqualf(c, 0, peer.HomeDERP(), "peer (%s) has no home DERP in %q's netmap, got: %d", peer.ComputedName(), client.Hostname(), peer.HomeDERP())
|
||||
|
||||
assert.Truef(c, peer.Hostinfo().Valid(), "peer (%s) of %q does not have Hostinfo", peer.ComputedName(), client.Hostname())
|
||||
|
||||
if hi := peer.Hostinfo(); hi.Valid() {
|
||||
assert.LessOrEqualf(c, 3, peer.Hostinfo().Services().Len(), "peer (%s) of %q does not have enough services, got: %v", peer.ComputedName(), client.Hostname(), peer.Hostinfo().Services())
|
||||
|
||||
|
|
@ -680,8 +739,11 @@ func assertValidNetmap(t *testing.T, client TailscaleClient) {
|
|||
// assertValidStatus validates that a client's status has all required fields for proper operation.
|
||||
// Checks self and peer status for essential data including hostinfo, tailscale IPs, endpoints,
|
||||
// and network map presence. This test is not suitable for ACL/partial connection tests.
|
||||
//
|
||||
//nolint:unused
|
||||
func assertValidStatus(t *testing.T, client TailscaleClient) {
|
||||
t.Helper()
|
||||
|
||||
status, err := client.Status(true)
|
||||
if err != nil {
|
||||
t.Fatalf("getting status for %q: %s", client.Hostname(), err)
|
||||
|
|
@ -737,8 +799,11 @@ func assertValidStatus(t *testing.T, client TailscaleClient) {
|
|||
// assertValidNetcheck validates that a client has a proper DERP relay configured.
|
||||
// Ensures the client has discovered and selected a DERP server for relay functionality,
|
||||
// which is essential for NAT traversal and connectivity in restricted networks.
|
||||
//
|
||||
//nolint:unused
|
||||
func assertValidNetcheck(t *testing.T, client TailscaleClient) {
|
||||
t.Helper()
|
||||
|
||||
report, err := client.Netcheck()
|
||||
if err != nil {
|
||||
t.Fatalf("getting status for %q: %s", client.Hostname(), err)
|
||||
|
|
@ -764,7 +829,7 @@ func assertCommandOutputContains(t *testing.T, c TailscaleClient, command []stri
|
|||
}
|
||||
|
||||
if !strings.Contains(stdout, contains) {
|
||||
return struct{}{}, fmt.Errorf("executing command, expected string %q not found in %q", contains, stdout)
|
||||
return struct{}{}, fmt.Errorf("executing command, expected string %q not found in %q", contains, stdout) //nolint:err113
|
||||
}
|
||||
|
||||
return struct{}{}, nil
|
||||
|
|
@ -793,6 +858,7 @@ func didClientUseWebsocketForDERP(t *testing.T, client TailscaleClient) bool {
|
|||
t.Helper()
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
err := client.WriteLogs(buf, buf)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to fetch client logs: %s: %s", client.Hostname(), err)
|
||||
|
|
@ -816,6 +882,7 @@ func countMatchingLines(in io.Reader, predicate func(string) bool) (int, error)
|
|||
scanner := bufio.NewScanner(in)
|
||||
{
|
||||
const logBufferInitialSize = 1024 << 10 // preallocate 1 MiB
|
||||
|
||||
buff := make([]byte, logBufferInitialSize)
|
||||
scanner.Buffer(buff, len(buff))
|
||||
scanner.Split(bufio.ScanLines)
|
||||
|
|
@ -885,6 +952,8 @@ func usernameOwner(name string) policyv2.Owner {
|
|||
|
||||
// groupOwner returns a Group as an Owner for use in TagOwners policies.
|
||||
// Specifies which groups can assign and manage specific tags in ACL configurations.
|
||||
//
|
||||
//nolint:unused
|
||||
func groupOwner(name string) policyv2.Owner {
|
||||
return ptr.To(policyv2.Group(name))
|
||||
}
|
||||
|
|
@ -933,7 +1002,7 @@ func GetUserByName(headscale ControlServer, username string) (*v1.User, error) {
|
|||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("user %s not found", username)
|
||||
return nil, fmt.Errorf("user %s not found", username) //nolint:err113
|
||||
}
|
||||
|
||||
// FindNewClient finds a client that is in the new list but not in the original list.
|
||||
|
|
@ -942,17 +1011,20 @@ func GetUserByName(headscale ControlServer, username string) (*v1.User, error) {
|
|||
func FindNewClient(original, updated []TailscaleClient) (TailscaleClient, error) {
|
||||
for _, client := range updated {
|
||||
isOriginal := false
|
||||
|
||||
for _, origClient := range original {
|
||||
if client.Hostname() == origClient.Hostname() {
|
||||
isOriginal = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isOriginal {
|
||||
return client, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no new client found")
|
||||
|
||||
return nil, errNoNewClientFound
|
||||
}
|
||||
|
||||
// AddAndLoginClient adds a new tailscale client to a user and logs it in.
|
||||
|
|
@ -960,7 +1032,7 @@ func FindNewClient(original, updated []TailscaleClient) (TailscaleClient, error)
|
|||
// 1. Creating a new node
|
||||
// 2. Finding the new node in the client list
|
||||
// 3. Getting the user to create a preauth key
|
||||
// 4. Logging in the new node
|
||||
// 4. Logging in the new node.
|
||||
func (s *Scenario) AddAndLoginClient(
|
||||
t *testing.T,
|
||||
username string,
|
||||
|
|
@ -992,7 +1064,7 @@ func (s *Scenario) AddAndLoginClient(
|
|||
}
|
||||
|
||||
if len(updatedClients) != len(originalClients)+1 {
|
||||
return struct{}{}, fmt.Errorf("expected %d clients, got %d", len(originalClients)+1, len(updatedClients))
|
||||
return struct{}{}, fmt.Errorf("expected %d clients, got %d", len(originalClients)+1, len(updatedClients)) //nolint:err113
|
||||
}
|
||||
|
||||
newClient, err = FindNewClient(originalClients, updatedClients)
|
||||
|
|
@ -1038,5 +1110,6 @@ func (s *Scenario) MustAddAndLoginClient(
|
|||
|
||||
client, err := s.AddAndLoginClient(t, username, version, headscale, tsOpts...)
|
||||
require.NoError(t, err)
|
||||
|
||||
return client
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"archive/tar"
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
|
@ -11,6 +12,7 @@ import (
|
|||
"io"
|
||||
"log"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
|
|
@ -46,6 +48,7 @@ const (
|
|||
tlsKeyPath = "/etc/headscale/tls.key"
|
||||
headscaleDefaultPort = 8080
|
||||
IntegrationTestDockerFileName = "Dockerfile.integration"
|
||||
defaultDirPerm = 0o755
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -198,7 +201,7 @@ func WithPostgres() Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithPolicy sets the policy mode for headscale.
|
||||
// WithPolicyMode sets the policy mode for headscale.
|
||||
func WithPolicyMode(mode types.PolicyMode) Option {
|
||||
return func(hsic *HeadscaleInContainer) {
|
||||
hsic.policyMode = mode
|
||||
|
|
@ -217,6 +220,8 @@ func WithIPAllocationStrategy(strategy types.IPAllocationStrategy) Option {
|
|||
// and only use the embedded DERP server.
|
||||
// It requires WithTLS and WithHostnameAsServerURL to be
|
||||
// set.
|
||||
//
|
||||
//nolint:goconst // env var values like "true" and "headscale" are clearer inline
|
||||
func WithEmbeddedDERPServerOnly() Option {
|
||||
return func(hsic *HeadscaleInContainer) {
|
||||
hsic.env["HEADSCALE_DERP_URLS"] = ""
|
||||
|
|
@ -321,6 +326,8 @@ func (hsic *HeadscaleInContainer) buildEntrypoint() []string {
|
|||
}
|
||||
|
||||
// New returns a new HeadscaleInContainer instance.
|
||||
//
|
||||
//nolint:gocyclo // complex container setup with many options
|
||||
func New(
|
||||
pool *dockertest.Pool,
|
||||
networks []*dockertest.Network,
|
||||
|
|
@ -548,6 +555,7 @@ func New(
|
|||
return nil, fmt.Errorf("starting headscale container: %w\n\nUnable to get diagnostic build output (command may have failed silently)", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Created %s container\n", hsic.hostname)
|
||||
|
||||
hsic.container = container
|
||||
|
|
@ -595,7 +603,8 @@ func New(
|
|||
}
|
||||
|
||||
for _, f := range hsic.filesInContainer {
|
||||
if err := hsic.WriteFile(f.path, f.contents); err != nil {
|
||||
err := hsic.WriteFile(f.path, f.contents)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("writing %q: %w", f.path, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -678,7 +687,7 @@ func (t *HeadscaleInContainer) Shutdown() (string, string, error) {
|
|||
|
||||
// Cleanup postgres container if enabled.
|
||||
if t.postgres {
|
||||
t.pool.Purge(t.pgContainer)
|
||||
_ = t.pool.Purge(t.pgContainer)
|
||||
}
|
||||
|
||||
return stdoutPath, stderrPath, t.pool.Purge(t.container)
|
||||
|
|
@ -697,16 +706,23 @@ func (t *HeadscaleInContainer) SaveLog(path string) (string, string, error) {
|
|||
}
|
||||
|
||||
func (t *HeadscaleInContainer) SaveMetrics(savePath string) error {
|
||||
resp, err := http.Get(fmt.Sprintf("http://%s:9090/metrics", t.hostname))
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://"+net.JoinHostPort(t.hostname, "9090")+"/metrics", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating metrics request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting metrics: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
out, err := os.Create(savePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating file for metrics: %w", err)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copy response to file: %w", err)
|
||||
|
|
@ -717,20 +733,21 @@ func (t *HeadscaleInContainer) SaveMetrics(savePath string) error {
|
|||
|
||||
// extractTarToDirectory extracts a tar archive to a directory.
|
||||
func extractTarToDirectory(tarData []byte, targetDir string) error {
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
err := os.MkdirAll(targetDir, defaultDirPerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating directory %s: %w", targetDir, err)
|
||||
}
|
||||
|
||||
tarReader := tar.NewReader(bytes.NewReader(tarData))
|
||||
|
||||
// Find the top-level directory to strip
|
||||
var topLevelDir string
|
||||
|
||||
firstPass := tar.NewReader(bytes.NewReader(tarData))
|
||||
for {
|
||||
header, err := firstPass.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading tar header: %w", err)
|
||||
}
|
||||
|
|
@ -741,12 +758,13 @@ func extractTarToDirectory(tarData []byte, targetDir string) error {
|
|||
}
|
||||
}
|
||||
|
||||
tarReader = tar.NewReader(bytes.NewReader(tarData))
|
||||
tarReader := tar.NewReader(bytes.NewReader(tarData))
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading tar header: %w", err)
|
||||
}
|
||||
|
|
@ -775,12 +793,15 @@ func extractTarToDirectory(tarData []byte, targetDir string) error {
|
|||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
// Create directory
|
||||
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
|
||||
//nolint:gosec // G115: header.Mode is trusted from tar archive
|
||||
err := os.MkdirAll(targetPath, os.FileMode(header.Mode))
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating directory %s: %w", targetPath, err)
|
||||
}
|
||||
case tar.TypeReg:
|
||||
// Ensure parent directories exist
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||||
err := os.MkdirAll(filepath.Dir(targetPath), defaultDirPerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating parent directories for %s: %w", targetPath, err)
|
||||
}
|
||||
|
||||
|
|
@ -790,14 +811,15 @@ func extractTarToDirectory(tarData []byte, targetDir string) error {
|
|||
return fmt.Errorf("creating file %s: %w", targetPath, err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||
if _, err := io.Copy(outFile, tarReader); err != nil { //nolint:gosec,noinlineerr // trusted tar from test container
|
||||
outFile.Close()
|
||||
return fmt.Errorf("copying file contents: %w", err)
|
||||
}
|
||||
|
||||
outFile.Close()
|
||||
|
||||
// Set file permissions
|
||||
if err := os.Chmod(targetPath, os.FileMode(header.Mode)); err != nil {
|
||||
if err := os.Chmod(targetPath, os.FileMode(header.Mode)); err != nil { //nolint:gosec,noinlineerr // safe mode from tar header
|
||||
return fmt.Errorf("setting file permissions: %w", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -844,10 +866,12 @@ func (t *HeadscaleInContainer) SaveDatabase(savePath string) error {
|
|||
|
||||
// Check if the database file exists and has a schema
|
||||
dbPath := "/tmp/integration_test_db.sqlite3"
|
||||
|
||||
fileInfo, err := t.Execute([]string{"ls", "-la", dbPath})
|
||||
if err != nil {
|
||||
return fmt.Errorf("database file does not exist at %s: %w", dbPath, err)
|
||||
}
|
||||
|
||||
log.Printf("Database file info: %s", fileInfo)
|
||||
|
||||
// Check if the database has any tables (schema)
|
||||
|
|
@ -857,7 +881,7 @@ func (t *HeadscaleInContainer) SaveDatabase(savePath string) error {
|
|||
}
|
||||
|
||||
if strings.TrimSpace(schemaCheck) == "" {
|
||||
return errors.New("database file exists but has no schema (empty database)")
|
||||
return errors.New("database file exists but has no schema (empty database)") //nolint:err113
|
||||
}
|
||||
|
||||
tarFile, err := t.FetchPath("/tmp/integration_test_db.sqlite3")
|
||||
|
|
@ -872,6 +896,7 @@ func (t *HeadscaleInContainer) SaveDatabase(savePath string) error {
|
|||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading tar header: %w", err)
|
||||
}
|
||||
|
|
@ -886,13 +911,15 @@ func (t *HeadscaleInContainer) SaveDatabase(savePath string) error {
|
|||
// Extract the first regular file we find
|
||||
if header.Typeflag == tar.TypeReg {
|
||||
dbPath := path.Join(savePath, t.hostname+".db")
|
||||
|
||||
outFile, err := os.Create(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating database file: %w", err)
|
||||
}
|
||||
|
||||
written, err := io.Copy(outFile, tarReader)
|
||||
written, err := io.Copy(outFile, tarReader) //nolint:gosec // trusted tar from test container
|
||||
outFile.Close()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("copying database file: %w", err)
|
||||
}
|
||||
|
|
@ -906,7 +933,7 @@ func (t *HeadscaleInContainer) SaveDatabase(savePath string) error {
|
|||
|
||||
// Check if we actually wrote something
|
||||
if written == 0 {
|
||||
return fmt.Errorf(
|
||||
return fmt.Errorf( //nolint:err113
|
||||
"database file is empty (size: %d, header size: %d)",
|
||||
written,
|
||||
header.Size,
|
||||
|
|
@ -917,7 +944,7 @@ func (t *HeadscaleInContainer) SaveDatabase(savePath string) error {
|
|||
}
|
||||
}
|
||||
|
||||
return errors.New("no regular file found in database tar archive")
|
||||
return errors.New("no regular file found in database tar archive") //nolint:err113
|
||||
}
|
||||
|
||||
// Execute runs a command inside the Headscale container and returns the
|
||||
|
|
@ -1059,6 +1086,7 @@ func (t *HeadscaleInContainer) CreateUser(
|
|||
}
|
||||
|
||||
var u v1.User
|
||||
|
||||
err = json.Unmarshal([]byte(result), &u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshalling user: %w", err)
|
||||
|
|
@ -1195,6 +1223,7 @@ func (t *HeadscaleInContainer) ListNodes(
|
|||
users ...string,
|
||||
) ([]*v1.Node, error) {
|
||||
var ret []*v1.Node
|
||||
|
||||
execUnmarshal := func(command []string) error {
|
||||
result, _, err := dockertestutil.ExecuteCommand(
|
||||
t.container,
|
||||
|
|
@ -1206,6 +1235,7 @@ func (t *HeadscaleInContainer) ListNodes(
|
|||
}
|
||||
|
||||
var nodes []*v1.Node
|
||||
|
||||
err = json.Unmarshal([]byte(result), &nodes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshalling nodes: %w", err)
|
||||
|
|
@ -1245,7 +1275,7 @@ func (t *HeadscaleInContainer) DeleteNode(nodeID uint64) error {
|
|||
"nodes",
|
||||
"delete",
|
||||
"--identifier",
|
||||
fmt.Sprintf("%d", nodeID),
|
||||
strconv.FormatUint(nodeID, 10),
|
||||
"--output",
|
||||
"json",
|
||||
"--force",
|
||||
|
|
@ -1309,6 +1339,7 @@ func (t *HeadscaleInContainer) ListUsers() ([]*v1.User, error) {
|
|||
}
|
||||
|
||||
var users []*v1.User
|
||||
|
||||
err = json.Unmarshal([]byte(result), &users)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshalling nodes: %w", err)
|
||||
|
|
@ -1439,6 +1470,7 @@ func (h *HeadscaleInContainer) PID() (int, error) {
|
|||
if pidInt == 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
pids = append(pids, pidInt)
|
||||
}
|
||||
|
||||
|
|
@ -1494,6 +1526,7 @@ func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (
|
|||
}
|
||||
|
||||
var node *v1.Node
|
||||
|
||||
err = json.Unmarshal([]byte(result), &node)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshalling node response: %q, error: %w", result, err)
|
||||
|
|
@ -1569,7 +1602,7 @@ func (t *HeadscaleInContainer) GetAllMapReponses() (map[types.NodeID][]tailcfg.M
|
|||
}
|
||||
|
||||
var res map[types.NodeID][]tailcfg.MapResponse
|
||||
if err := json.Unmarshal([]byte(result), &res); err != nil {
|
||||
if err := json.Unmarshal([]byte(result), &res); err != nil { //nolint:noinlineerr
|
||||
return nil, fmt.Errorf("decoding routes response: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -1589,7 +1622,7 @@ func (t *HeadscaleInContainer) PrimaryRoutes() (*routes.DebugRoutes, error) {
|
|||
}
|
||||
|
||||
var debugRoutes routes.DebugRoutes
|
||||
if err := json.Unmarshal([]byte(result), &debugRoutes); err != nil {
|
||||
if err := json.Unmarshal([]byte(result), &debugRoutes); err != nil { //nolint:noinlineerr
|
||||
return nil, fmt.Errorf("decoding routes response: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -1609,7 +1642,7 @@ func (t *HeadscaleInContainer) DebugBatcher() (*hscontrol.DebugBatcherInfo, erro
|
|||
}
|
||||
|
||||
var debugInfo hscontrol.DebugBatcherInfo
|
||||
if err := json.Unmarshal([]byte(result), &debugInfo); err != nil {
|
||||
if err := json.Unmarshal([]byte(result), &debugInfo); err != nil { //nolint:noinlineerr
|
||||
return nil, fmt.Errorf("decoding batcher debug response: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -1629,7 +1662,7 @@ func (t *HeadscaleInContainer) DebugNodeStore() (map[types.NodeID]types.Node, er
|
|||
}
|
||||
|
||||
var nodeStore map[types.NodeID]types.Node
|
||||
if err := json.Unmarshal([]byte(result), &nodeStore); err != nil {
|
||||
if err := json.Unmarshal([]byte(result), &nodeStore); err != nil { //nolint:noinlineerr
|
||||
return nil, fmt.Errorf("decoding nodestore debug response: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -1649,7 +1682,7 @@ func (t *HeadscaleInContainer) DebugFilter() ([]tailcfg.FilterRule, error) {
|
|||
}
|
||||
|
||||
var filterRules []tailcfg.FilterRule
|
||||
if err := json.Unmarshal([]byte(result), &filterRules); err != nil {
|
||||
if err := json.Unmarshal([]byte(result), &filterRules); err != nil { //nolint:noinlineerr
|
||||
return nil, fmt.Errorf("decoding filter response: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ func PeerSyncTimeout() time.Duration {
|
|||
if util.IsCI() {
|
||||
return 120 * time.Second
|
||||
}
|
||||
|
||||
return 60 * time.Second
|
||||
}
|
||||
|
||||
|
|
@ -205,25 +206,27 @@ func BuildExpectedOnlineMap(all map[types.NodeID][]tailcfg.MapResponse) map[type
|
|||
res := make(map[types.NodeID]map[types.NodeID]bool)
|
||||
for nid, mrs := range all {
|
||||
res[nid] = make(map[types.NodeID]bool)
|
||||
|
||||
for _, mr := range mrs {
|
||||
for _, peer := range mr.Peers {
|
||||
if peer.Online != nil {
|
||||
res[nid][types.NodeID(peer.ID)] = *peer.Online
|
||||
res[nid][types.NodeID(peer.ID)] = *peer.Online //nolint:gosec // safe conversion for peer ID
|
||||
}
|
||||
}
|
||||
|
||||
for _, peer := range mr.PeersChanged {
|
||||
if peer.Online != nil {
|
||||
res[nid][types.NodeID(peer.ID)] = *peer.Online
|
||||
res[nid][types.NodeID(peer.ID)] = *peer.Online //nolint:gosec // safe conversion for peer ID
|
||||
}
|
||||
}
|
||||
|
||||
for _, peer := range mr.PeersChangedPatch {
|
||||
if peer.Online != nil {
|
||||
res[nid][types.NodeID(peer.NodeID)] = *peer.Online
|
||||
res[nid][types.NodeID(peer.NodeID)] = *peer.Online //nolint:gosec // safe conversion for peer ID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ func TestEnablingRoutes(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoErrorf(t, err, "failed to create scenario: %s", err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -91,6 +92,7 @@ func TestEnablingRoutes(t *testing.T) {
|
|||
// Wait for route advertisements to propagate to NodeStore
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
nodes, err = headscale.ListNodes()
|
||||
assert.NoError(ct, err)
|
||||
|
||||
|
|
@ -127,6 +129,7 @@ func TestEnablingRoutes(t *testing.T) {
|
|||
// Wait for route approvals to propagate to NodeStore
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
nodes, err = headscale.ListNodes()
|
||||
assert.NoError(ct, err)
|
||||
|
||||
|
|
@ -149,9 +152,11 @@ func TestEnablingRoutes(t *testing.T) {
|
|||
|
||||
assert.NotNil(c, peerStatus.PrimaryRoutes)
|
||||
assert.NotNil(c, peerStatus.AllowedIPs)
|
||||
|
||||
if peerStatus.AllowedIPs != nil {
|
||||
assert.Len(c, peerStatus.AllowedIPs.AsSlice(), 3)
|
||||
}
|
||||
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{netip.MustParsePrefix(expectedRoutes[string(peerStatus.ID)])})
|
||||
}
|
||||
}
|
||||
|
|
@ -172,6 +177,7 @@ func TestEnablingRoutes(t *testing.T) {
|
|||
// Wait for route state changes to propagate to nodes
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
nodes, err = headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
|
||||
|
|
@ -214,6 +220,7 @@ func TestEnablingRoutes(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // complex HA failover test scenario
|
||||
func TestHASubnetRouterFailover(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
|
|
@ -271,6 +278,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
|
||||
prefp, err := scenario.SubnetOfNetwork("usernet1")
|
||||
require.NoError(t, err)
|
||||
|
||||
pref := *prefp
|
||||
t.Logf("usernet1 prefix: %s", pref.String())
|
||||
|
||||
|
|
@ -310,6 +318,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
t.Logf(" - Router 2 (%s): Advertising route %s - will be STANDBY when approved", subRouter2.Hostname(), pref.String())
|
||||
t.Logf(" - Router 3 (%s): Advertising route %s - will be STANDBY when approved", subRouter3.Hostname(), pref.String())
|
||||
t.Logf(" Expected: All 3 routers advertise the same route for redundancy, but only one will be primary at a time")
|
||||
|
||||
for _, client := range allClients[:3] {
|
||||
command := []string{
|
||||
"tailscale",
|
||||
|
|
@ -325,6 +334,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
|
||||
// Wait for route configuration changes after advertising routes
|
||||
var nodes []*v1.Node
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err = headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
|
|
@ -361,13 +371,15 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
)
|
||||
|
||||
// Helper function to check test failure and print route map if needed
|
||||
checkFailureAndPrintRoutes := func(t *testing.T, client TailscaleClient) {
|
||||
checkFailureAndPrintRoutes := func(t *testing.T, client TailscaleClient) { //nolint:thelper
|
||||
if t.Failed() {
|
||||
t.Logf("[%s] Test failed at this checkpoint", time.Now().Format(TimestampFormat))
|
||||
|
||||
status, err := client.Status()
|
||||
if err == nil {
|
||||
printCurrentRouteMap(t, xmaps.Values(status.Peer)...)
|
||||
}
|
||||
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
|
@ -386,6 +398,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
t.Logf(" Expected: Router 1 becomes PRIMARY with route %s active", pref.String())
|
||||
t.Logf(" Expected: Routers 2 & 3 remain with advertised but unapproved routes")
|
||||
t.Logf(" Expected: Client can access webservice through router 1 only")
|
||||
|
||||
_, err = headscale.ApproveRoutes(
|
||||
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
|
||||
[]netip.Prefix{pref},
|
||||
|
|
@ -456,10 +469,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := client.Traceroute(webip)
|
||||
assert.NoError(c, err)
|
||||
|
||||
ip, err := subRouter1.IPv4()
|
||||
if !assert.NoError(c, err, "failed to get IPv4 for subRouter1") {
|
||||
return
|
||||
}
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 1")
|
||||
|
||||
|
|
@ -483,6 +498,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
t.Logf(" Expected: Router 2 becomes STANDBY (approved but not primary)")
|
||||
t.Logf(" Expected: Router 1 remains PRIMARY (no flapping - stability preferred)")
|
||||
t.Logf(" Expected: HA is now active - if router 1 fails, router 2 can take over")
|
||||
|
||||
_, err = headscale.ApproveRoutes(
|
||||
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
|
||||
[]netip.Prefix{pref},
|
||||
|
|
@ -494,6 +510,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
nodes, err = headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, nodes, 6)
|
||||
|
||||
if len(nodes) >= 3 {
|
||||
requireNodeRouteCountWithCollect(c, nodes[0], 1, 1, 1)
|
||||
requireNodeRouteCountWithCollect(c, nodes[1], 1, 1, 0)
|
||||
|
|
@ -569,10 +586,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := client.Traceroute(webip)
|
||||
assert.NoError(c, err)
|
||||
|
||||
ip, err := subRouter1.IPv4()
|
||||
if !assert.NoError(c, err, "failed to get IPv4 for subRouter1") {
|
||||
return
|
||||
}
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, propagationTime, 200*time.Millisecond, "Verifying traceroute still goes through router 1 in HA mode")
|
||||
|
||||
|
|
@ -598,6 +617,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
t.Logf(" Expected: Router 3 becomes second STANDBY (approved but not primary)")
|
||||
t.Logf(" Expected: Router 1 remains PRIMARY, Router 2 remains first STANDBY")
|
||||
t.Logf(" Expected: Full HA configuration with 1 PRIMARY + 2 STANDBY routers")
|
||||
|
||||
_, err = headscale.ApproveRoutes(
|
||||
MustFindNode(subRouter3.Hostname(), nodes).GetId(),
|
||||
[]netip.Prefix{pref},
|
||||
|
|
@ -672,12 +692,14 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
assert.NotEmpty(c, ips, "subRouter1 should have IP addresses")
|
||||
|
||||
var expectedIP netip.Addr
|
||||
|
||||
for _, ip := range ips {
|
||||
if ip.Is4() {
|
||||
expectedIP = ip
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(c, expectedIP.IsValid(), "subRouter1 should have a valid IPv4 address")
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, expectedIP)
|
||||
|
|
@ -705,6 +727,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
t.Logf(" Expected: Router 2 (%s) should automatically become new PRIMARY", subRouter2.Hostname())
|
||||
t.Logf(" Expected: Router 3 remains STANDBY")
|
||||
t.Logf(" Expected: Traffic seamlessly fails over to router 2")
|
||||
|
||||
err = subRouter1.Down()
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -754,10 +777,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := client.Traceroute(webip)
|
||||
assert.NoError(c, err)
|
||||
|
||||
ip, err := subRouter2.IPv4()
|
||||
if !assert.NoError(c, err, "failed to get IPv4 for subRouter2") {
|
||||
return
|
||||
}
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 2 after failover")
|
||||
|
||||
|
|
@ -783,6 +808,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
t.Logf(" Expected: Router 3 (%s) should become new PRIMARY (last remaining router)", subRouter3.Hostname())
|
||||
t.Logf(" Expected: With only 1 router left, HA is effectively disabled")
|
||||
t.Logf(" Expected: Traffic continues through router 3")
|
||||
|
||||
err = subRouter2.Down()
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -825,10 +851,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := client.Traceroute(webip)
|
||||
assert.NoError(c, err)
|
||||
|
||||
ip, err := subRouter3.IPv4()
|
||||
if !assert.NoError(c, err, "failed to get IPv4 for subRouter3") {
|
||||
return
|
||||
}
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 3 after second failover")
|
||||
|
||||
|
|
@ -853,6 +881,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
t.Logf(" Expected: Router 3 remains PRIMARY (stability - no unnecessary failover)")
|
||||
t.Logf(" Expected: Router 1 becomes STANDBY (ready for HA)")
|
||||
t.Logf(" Expected: HA is restored with 2 routers available")
|
||||
|
||||
err = subRouter1.Up()
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -902,10 +931,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := client.Traceroute(webip)
|
||||
assert.NoError(c, err)
|
||||
|
||||
ip, err := subRouter3.IPv4()
|
||||
if !assert.NoError(c, err, "failed to get IPv4 for subRouter3") {
|
||||
return
|
||||
}
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, propagationTime, 200*time.Millisecond, "Verifying traceroute still goes through router 3 after router 1 recovery")
|
||||
|
||||
|
|
@ -932,6 +963,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
t.Logf(" Expected: Router 1 (%s) remains first STANDBY", subRouter1.Hostname())
|
||||
t.Logf(" Expected: Router 2 (%s) becomes second STANDBY", subRouter2.Hostname())
|
||||
t.Logf(" Expected: Full HA restored with all 3 routers online")
|
||||
|
||||
err = subRouter2.Up()
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -982,10 +1014,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := client.Traceroute(webip)
|
||||
assert.NoError(c, err)
|
||||
|
||||
ip, err := subRouter3.IPv4()
|
||||
if !assert.NoError(c, err, "failed to get IPv4 for subRouter3") {
|
||||
return
|
||||
}
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 3 after full recovery")
|
||||
|
||||
|
|
@ -1067,10 +1101,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := client.Traceroute(webip)
|
||||
assert.NoError(c, err)
|
||||
|
||||
ip, err := subRouter1.IPv4()
|
||||
if !assert.NoError(c, err, "failed to get IPv4 for subRouter1") {
|
||||
return
|
||||
}
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 1 after route disable")
|
||||
|
||||
|
|
@ -1153,10 +1189,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := client.Traceroute(webip)
|
||||
assert.NoError(c, err)
|
||||
|
||||
ip, err := subRouter2.IPv4()
|
||||
if !assert.NoError(c, err, "failed to get IPv4 for subRouter2") {
|
||||
return
|
||||
}
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 2 after second route disable")
|
||||
|
||||
|
|
@ -1182,6 +1220,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
t.Logf(" Expected: Router 2 (%s) remains PRIMARY (stability - no unnecessary flapping)", subRouter2.Hostname())
|
||||
t.Logf(" Expected: Router 1 (%s) becomes STANDBY (approved but not primary)", subRouter1.Hostname())
|
||||
t.Logf(" Expected: HA fully restored with Router 2 PRIMARY and Router 1 STANDBY")
|
||||
|
||||
r1Node := MustFindNode(subRouter1.Hostname(), nodes)
|
||||
_, err = headscale.ApproveRoutes(
|
||||
r1Node.GetId(),
|
||||
|
|
@ -1237,10 +1276,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := client.Traceroute(webip)
|
||||
assert.NoError(c, err)
|
||||
|
||||
ip, err := subRouter2.IPv4()
|
||||
if !assert.NoError(c, err, "failed to get IPv4 for subRouter2") {
|
||||
return
|
||||
}
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, propagationTime, 200*time.Millisecond, "Verifying traceroute still goes through router 2 after route re-enable")
|
||||
|
||||
|
|
@ -1266,6 +1307,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
t.Logf(" Expected: Router 2 (%s) remains PRIMARY (stability preferred)", subRouter2.Hostname())
|
||||
t.Logf(" Expected: Routers 1 & 3 are both STANDBY")
|
||||
t.Logf(" Expected: Full HA restored with all 3 routers available")
|
||||
|
||||
r3Node := MustFindNode(subRouter3.Hostname(), nodes)
|
||||
_, err = headscale.ApproveRoutes(
|
||||
r3Node.GetId(),
|
||||
|
|
@ -1315,6 +1357,7 @@ func TestSubnetRouteACL(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoErrorf(t, err, "failed to create scenario: %s", err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -1362,6 +1405,7 @@ func TestSubnetRouteACL(t *testing.T) {
|
|||
sort.SliceStable(allClients, func(i, j int) bool {
|
||||
statusI := allClients[i].MustStatus()
|
||||
statusJ := allClients[j].MustStatus()
|
||||
|
||||
return statusI.Self.ID < statusJ.Self.ID
|
||||
})
|
||||
|
||||
|
|
@ -1391,15 +1435,20 @@ func TestSubnetRouteACL(t *testing.T) {
|
|||
|
||||
// Wait for route advertisements to propagate to the server
|
||||
var nodes []*v1.Node
|
||||
|
||||
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
nodes, err = headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, nodes, 2)
|
||||
|
||||
// Find the node that should have the route by checking node IDs
|
||||
var routeNode *v1.Node
|
||||
var otherNode *v1.Node
|
||||
var (
|
||||
routeNode *v1.Node
|
||||
otherNode *v1.Node
|
||||
)
|
||||
|
||||
for _, node := range nodes {
|
||||
nodeIDStr := strconv.FormatUint(node.GetId(), 10)
|
||||
if _, shouldHaveRoute := expectedRoutes[nodeIDStr]; shouldHaveRoute {
|
||||
|
|
@ -1462,6 +1511,7 @@ func TestSubnetRouteACL(t *testing.T) {
|
|||
srs1PeerStatus := clientStatus.Peer[srs1.Self.PublicKey]
|
||||
|
||||
assert.NotNil(c, srs1PeerStatus, "Router 1 peer should exist")
|
||||
|
||||
if srs1PeerStatus == nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -1552,7 +1602,7 @@ func TestSubnetRouteACL(t *testing.T) {
|
|||
func TestEnablingExitRoutes(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
user := "user2"
|
||||
user := "user2" //nolint:goconst // test-specific value, not related to userToDelete constant
|
||||
|
||||
spec := ScenarioSpec{
|
||||
NodesPerUser: 2,
|
||||
|
|
@ -1560,6 +1610,7 @@ func TestEnablingExitRoutes(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoErrorf(t, err, "failed to create scenario")
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -1581,8 +1632,10 @@ func TestEnablingExitRoutes(t *testing.T) {
|
|||
requireNoErrSync(t, err)
|
||||
|
||||
var nodes []*v1.Node
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
nodes, err = headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, nodes, 2)
|
||||
|
|
@ -1640,6 +1693,7 @@ func TestEnablingExitRoutes(t *testing.T) {
|
|||
peerStatus := status.Peer[peerKey]
|
||||
|
||||
assert.NotNil(c, peerStatus.AllowedIPs)
|
||||
|
||||
if peerStatus.AllowedIPs != nil {
|
||||
assert.Len(c, peerStatus.AllowedIPs.AsSlice(), 4)
|
||||
assert.Contains(c, peerStatus.AllowedIPs.AsSlice(), tsaddr.AllIPv4())
|
||||
|
|
@ -1670,6 +1724,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoErrorf(t, err, "failed to create scenario: %s", err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -1700,10 +1755,12 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
|
|||
if s.User[s.Self.UserID].LoginName == "user1@test.no" {
|
||||
user1c = c
|
||||
}
|
||||
|
||||
if s.User[s.Self.UserID].LoginName == "user2@test.no" {
|
||||
user2c = c
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, user1c)
|
||||
require.NotNil(t, user2c)
|
||||
|
||||
|
|
@ -1720,6 +1777,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
|
|||
// Wait for route advertisements to propagate to NodeStore
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
nodes, err = headscale.ListNodes()
|
||||
assert.NoError(ct, err)
|
||||
assert.Len(ct, nodes, 2)
|
||||
|
|
@ -1750,6 +1808,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
|
|||
// Wait for route state changes to propagate to nodes
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
nodes, err = headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, nodes, 2)
|
||||
|
|
@ -1767,6 +1826,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
|
|||
if peerStatus.PrimaryRoutes != nil {
|
||||
assert.Contains(c, peerStatus.PrimaryRoutes.AsSlice(), *pref)
|
||||
}
|
||||
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{*pref})
|
||||
}
|
||||
}, 10*time.Second, 500*time.Millisecond, "routes should be visible to client")
|
||||
|
|
@ -1793,10 +1853,12 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
|
|||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := user2c.Traceroute(webip)
|
||||
assert.NoError(c, err)
|
||||
|
||||
ip, err := user1c.IPv4()
|
||||
if !assert.NoError(c, err, "failed to get IPv4 for user1c") {
|
||||
return
|
||||
}
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, 5*time.Second, 200*time.Millisecond, "Verifying traceroute goes through subnet router")
|
||||
}
|
||||
|
|
@ -1817,6 +1879,7 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoErrorf(t, err, "failed to create scenario: %s", err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -1844,10 +1907,12 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
|
|||
if s.User[s.Self.UserID].LoginName == "user1@test.no" {
|
||||
user1c = c
|
||||
}
|
||||
|
||||
if s.User[s.Self.UserID].LoginName == "user2@test.no" {
|
||||
user2c = c
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, user1c)
|
||||
require.NotNil(t, user2c)
|
||||
|
||||
|
|
@ -1864,6 +1929,7 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
|
|||
// Wait for route advertisements to propagate to NodeStore
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
nodes, err = headscale.ListNodes()
|
||||
assert.NoError(ct, err)
|
||||
assert.Len(ct, nodes, 2)
|
||||
|
|
@ -1946,6 +2012,7 @@ func MustFindNode(hostname string, nodes []*v1.Node) *v1.Node {
|
|||
return node
|
||||
}
|
||||
}
|
||||
|
||||
panic("node not found")
|
||||
}
|
||||
|
||||
|
|
@ -1965,6 +2032,8 @@ func MustFindNode(hostname string, nodes []*v1.Node) *v1.Node {
|
|||
// - Verify that peers can no longer use node
|
||||
// - Policy is changed back to auto approve route, check that routes already existing is approved.
|
||||
// - Verify that routes can now be seen by peers.
|
||||
//
|
||||
//nolint:gocyclo // complex multi-network auto-approve test scenario
|
||||
func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
|
|
@ -2229,10 +2298,12 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(tt.spec)
|
||||
|
||||
require.NoErrorf(t, err, "failed to create scenario: %s", err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
var nodes []*v1.Node
|
||||
|
||||
opts := []hsic.Option{
|
||||
hsic.WithTestName("autoapprovemulti"),
|
||||
hsic.WithEmbeddedDERPServerOnly(),
|
||||
|
|
@ -2259,7 +2330,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
preAuthKeyTags = []string{tt.approver}
|
||||
if tt.withURL {
|
||||
// For webauth, only user1 can request tags (per tagOwners policy)
|
||||
webauthTagUser = "user1"
|
||||
webauthTagUser = "user1" //nolint:goconst // test value, not a constant
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2288,6 +2359,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
// Add the Docker network route to the auto-approvers
|
||||
// Keep existing auto-approvers (like bigRoute) in place
|
||||
var approvers policyv2.AutoApprovers
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(tt.approver, "tag:"):
|
||||
approvers = append(approvers, tagApprover(tt.approver))
|
||||
|
|
@ -2356,6 +2428,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
} else {
|
||||
pak, err = scenario.CreatePreAuthKey(userMap["user1"].GetId(), false, false)
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
err = routerUsernet1.Login(headscale.GetEndpoint(), pak.GetKey())
|
||||
|
|
@ -2447,11 +2520,13 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
t.Logf("Client %s sees %d peers", client.Hostname(), len(status.Peers()))
|
||||
|
||||
routerPeerFound := false
|
||||
|
||||
for _, peerKey := range status.Peers() {
|
||||
peerStatus := status.Peer[peerKey]
|
||||
|
||||
if peerStatus.ID == routerUsernet1ID.StableID() {
|
||||
routerPeerFound = true
|
||||
|
||||
t.Logf("Client sees router peer %s (ID=%s): AllowedIPs=%v, PrimaryRoutes=%v",
|
||||
peerStatus.HostName,
|
||||
peerStatus.ID,
|
||||
|
|
@ -2459,9 +2534,11 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
peerStatus.PrimaryRoutes)
|
||||
|
||||
assert.NotNil(c, peerStatus.PrimaryRoutes)
|
||||
|
||||
if peerStatus.PrimaryRoutes != nil {
|
||||
assert.Contains(c, peerStatus.PrimaryRoutes.AsSlice(), *route)
|
||||
}
|
||||
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{*route})
|
||||
} else {
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, nil)
|
||||
|
|
@ -2498,10 +2575,12 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := client.Traceroute(webip)
|
||||
assert.NoError(c, err)
|
||||
|
||||
ip, err := routerUsernet1.IPv4()
|
||||
if !assert.NoError(c, err, "failed to get IPv4 for routerUsernet1") {
|
||||
return
|
||||
}
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, assertTimeout, 200*time.Millisecond, "Verifying traceroute goes through auto-approved router")
|
||||
|
||||
|
|
@ -2538,9 +2617,11 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
|
||||
if peerStatus.ID == routerUsernet1ID.StableID() {
|
||||
assert.NotNil(c, peerStatus.PrimaryRoutes)
|
||||
|
||||
if peerStatus.PrimaryRoutes != nil {
|
||||
assert.Contains(c, peerStatus.PrimaryRoutes.AsSlice(), *route)
|
||||
}
|
||||
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{*route})
|
||||
} else {
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, nil)
|
||||
|
|
@ -2560,10 +2641,12 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := client.Traceroute(webip)
|
||||
assert.NoError(c, err)
|
||||
|
||||
ip, err := routerUsernet1.IPv4()
|
||||
if !assert.NoError(c, err, "failed to get IPv4 for routerUsernet1") {
|
||||
return
|
||||
}
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, assertTimeout, 200*time.Millisecond, "Verifying traceroute still goes through router after policy change")
|
||||
|
||||
|
|
@ -2597,6 +2680,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
// Add the route back to the auto approver in the policy, the route should
|
||||
// now become available again.
|
||||
var newApprovers policyv2.AutoApprovers
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(tt.approver, "tag:"):
|
||||
newApprovers = append(newApprovers, tagApprover(tt.approver))
|
||||
|
|
@ -2630,9 +2714,11 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
|
||||
if peerStatus.ID == routerUsernet1ID.StableID() {
|
||||
assert.NotNil(c, peerStatus.PrimaryRoutes)
|
||||
|
||||
if peerStatus.PrimaryRoutes != nil {
|
||||
assert.Contains(c, peerStatus.PrimaryRoutes.AsSlice(), *route)
|
||||
}
|
||||
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{*route})
|
||||
} else {
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, nil)
|
||||
|
|
@ -2652,10 +2738,12 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := client.Traceroute(webip)
|
||||
assert.NoError(c, err)
|
||||
|
||||
ip, err := routerUsernet1.IPv4()
|
||||
if !assert.NoError(c, err, "failed to get IPv4 for routerUsernet1") {
|
||||
return
|
||||
}
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, assertTimeout, 200*time.Millisecond, "Verifying traceroute goes through router after re-approval")
|
||||
|
||||
|
|
@ -2691,11 +2779,13 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
if peerStatus.PrimaryRoutes != nil {
|
||||
assert.Contains(c, peerStatus.PrimaryRoutes.AsSlice(), *route)
|
||||
}
|
||||
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{*route})
|
||||
} else if peerStatus.ID == "2" {
|
||||
if peerStatus.PrimaryRoutes != nil {
|
||||
assert.Contains(c, peerStatus.PrimaryRoutes.AsSlice(), subRoute)
|
||||
}
|
||||
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{subRoute})
|
||||
} else {
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, nil)
|
||||
|
|
@ -2733,9 +2823,11 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
|
||||
if peerStatus.ID == routerUsernet1ID.StableID() {
|
||||
assert.NotNil(c, peerStatus.PrimaryRoutes)
|
||||
|
||||
if peerStatus.PrimaryRoutes != nil {
|
||||
assert.Contains(c, peerStatus.PrimaryRoutes.AsSlice(), *route)
|
||||
}
|
||||
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{*route})
|
||||
} else {
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, nil)
|
||||
|
|
@ -2773,6 +2865,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
if peerStatus.PrimaryRoutes != nil {
|
||||
assert.Contains(c, peerStatus.PrimaryRoutes.AsSlice(), *route)
|
||||
}
|
||||
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{*route})
|
||||
} else if peerStatus.ID == "3" {
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()})
|
||||
|
|
@ -2791,7 +2884,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
func assertTracerouteViaIPWithCollect(c *assert.CollectT, tr util.Traceroute, ip netip.Addr) {
|
||||
assert.NotNil(c, tr)
|
||||
assert.True(c, tr.Success)
|
||||
assert.NoError(c, tr.Err)
|
||||
assert.NoError(c, tr.Err) //nolint:testifylint // using assert.CollectT
|
||||
assert.NotEmpty(c, tr.Route)
|
||||
// Since we're inside EventuallyWithT, we can't use require.Greater with t
|
||||
// but assert.NotEmpty above ensures len(tr.Route) > 0
|
||||
|
|
@ -2805,12 +2898,15 @@ func SortPeerStatus(a, b *ipnstate.PeerStatus) int {
|
|||
}
|
||||
|
||||
func printCurrentRouteMap(t *testing.T, routers ...*ipnstate.PeerStatus) {
|
||||
t.Helper()
|
||||
t.Logf("== Current routing map ==")
|
||||
slices.SortFunc(routers, SortPeerStatus)
|
||||
|
||||
for _, router := range routers {
|
||||
got := filterNonRoutes(router)
|
||||
t.Logf(" Router %s (%s) is serving:", router.HostName, router.ID)
|
||||
t.Logf(" AllowedIPs: %v", got)
|
||||
|
||||
if router.PrimaryRoutes != nil {
|
||||
t.Logf(" PrimaryRoutes: %v", router.PrimaryRoutes.AsSlice())
|
||||
}
|
||||
|
|
@ -2823,6 +2919,7 @@ func filterNonRoutes(status *ipnstate.PeerStatus) []netip.Prefix {
|
|||
if tsaddr.IsExitRoute(p) {
|
||||
return true
|
||||
}
|
||||
|
||||
return !slices.ContainsFunc(status.TailscaleIPs, p.Contains)
|
||||
})
|
||||
}
|
||||
|
|
@ -2874,6 +2971,7 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
|
|||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
|
||||
require.NoErrorf(t, err, "failed to create scenario: %s", err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -3014,6 +3112,7 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
|
|||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
// List nodes and verify the router has 3 available routes
|
||||
var err error
|
||||
|
||||
nodes, err := headscale.NodesByUser()
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, nodes, 2)
|
||||
|
|
@ -3049,10 +3148,12 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
|
|||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := nodeClient.Traceroute(webip)
|
||||
assert.NoError(c, err)
|
||||
|
||||
ip, err := routerClient.IPv4()
|
||||
if !assert.NoError(c, err, "failed to get IPv4 for routerClient") {
|
||||
return
|
||||
}
|
||||
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, 60*time.Second, 200*time.Millisecond, "Verifying traceroute goes through router")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ type User struct {
|
|||
type Scenario struct {
|
||||
// TODO(kradalby): support multiple headcales for later, currently only
|
||||
// use one.
|
||||
controlServers *xsync.MapOf[string, ControlServer]
|
||||
controlServers *xsync.Map[string, ControlServer]
|
||||
derpServers []*dsic.DERPServerInContainer
|
||||
|
||||
users map[string]*User
|
||||
|
|
@ -169,8 +169,8 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
|
|||
// Opportunity to clean up unreferenced networks.
|
||||
// This might be a no op, but it is worth a try as we sometime
|
||||
// dont clean up nicely after ourselves.
|
||||
dockertestutil.CleanUnreferencedNetworks(pool)
|
||||
dockertestutil.CleanImagesInCI(pool)
|
||||
_ = dockertestutil.CleanUnreferencedNetworks(pool)
|
||||
_ = dockertestutil.CleanImagesInCI(pool)
|
||||
|
||||
if spec.MaxWait == 0 {
|
||||
pool.MaxWait = dockertestMaxWait()
|
||||
|
|
@ -180,7 +180,7 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
|
|||
|
||||
testHashPrefix := "hs-" + util.MustGenerateRandomStringDNSSafe(scenarioHashLength)
|
||||
s := &Scenario{
|
||||
controlServers: xsync.NewMapOf[string, ControlServer](),
|
||||
controlServers: xsync.NewMap[string, ControlServer](),
|
||||
users: make(map[string]*User),
|
||||
|
||||
pool: pool,
|
||||
|
|
@ -191,9 +191,11 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
|
|||
}
|
||||
|
||||
var userToNetwork map[string]*dockertest.Network
|
||||
|
||||
if spec.Networks != nil || len(spec.Networks) != 0 {
|
||||
for name, users := range s.spec.Networks {
|
||||
networkName := testHashPrefix + "-" + name
|
||||
|
||||
network, err := s.AddNetwork(networkName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -201,8 +203,9 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
|
|||
|
||||
for _, user := range users {
|
||||
if n2, ok := userToNetwork[user]; ok {
|
||||
return nil, fmt.Errorf("users can only have nodes placed in one network: %s into %s but already in %s", user, network.Network.Name, n2.Network.Name)
|
||||
return nil, fmt.Errorf("users can only have nodes placed in one network: %s into %s but already in %s", user, network.Network.Name, n2.Network.Name) //nolint:err113
|
||||
}
|
||||
|
||||
mak.Set(&userToNetwork, user, network)
|
||||
}
|
||||
}
|
||||
|
|
@ -219,6 +222,7 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mak.Set(&s.extraServices, s.prefixedNetworkName(network), append(s.extraServices[s.prefixedNetworkName(network)], svc))
|
||||
}
|
||||
}
|
||||
|
|
@ -230,6 +234,7 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
|
|||
if spec.OIDCAccessTTL != 0 {
|
||||
ttl = spec.OIDCAccessTTL
|
||||
}
|
||||
|
||||
err = s.runMockOIDC(ttl, spec.OIDCUsers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -268,13 +273,14 @@ func (s *Scenario) Networks() []*dockertest.Network {
|
|||
if len(s.networks) == 0 {
|
||||
panic("Scenario.Networks called with empty network list")
|
||||
}
|
||||
|
||||
return xmaps.Values(s.networks)
|
||||
}
|
||||
|
||||
func (s *Scenario) Network(name string) (*dockertest.Network, error) {
|
||||
net, ok := s.networks[s.prefixedNetworkName(name)]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no network named: %s", name)
|
||||
return nil, fmt.Errorf("no network named: %s", name) //nolint:err113
|
||||
}
|
||||
|
||||
return net, nil
|
||||
|
|
@ -283,11 +289,11 @@ func (s *Scenario) Network(name string) (*dockertest.Network, error) {
|
|||
func (s *Scenario) SubnetOfNetwork(name string) (*netip.Prefix, error) {
|
||||
net, ok := s.networks[s.prefixedNetworkName(name)]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no network named: %s", name)
|
||||
return nil, fmt.Errorf("no network named: %s", name) //nolint:err113
|
||||
}
|
||||
|
||||
if len(net.Network.IPAM.Config) == 0 {
|
||||
return nil, fmt.Errorf("no IPAM config found in network: %s", name)
|
||||
return nil, fmt.Errorf("no IPAM config found in network: %s", name) //nolint:err113
|
||||
}
|
||||
|
||||
pref, err := netip.ParsePrefix(net.Network.IPAM.Config[0].Subnet)
|
||||
|
|
@ -301,15 +307,17 @@ func (s *Scenario) SubnetOfNetwork(name string) (*netip.Prefix, error) {
|
|||
func (s *Scenario) Services(name string) ([]*dockertest.Resource, error) {
|
||||
res, ok := s.extraServices[s.prefixedNetworkName(name)]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no network named: %s", name)
|
||||
return nil, fmt.Errorf("no network named: %s", name) //nolint:err113
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) {
|
||||
defer dockertestutil.CleanUnreferencedNetworks(s.pool)
|
||||
defer dockertestutil.CleanImagesInCI(s.pool)
|
||||
t.Helper()
|
||||
|
||||
defer func() { _ = dockertestutil.CleanUnreferencedNetworks(s.pool) }()
|
||||
defer func() { _ = dockertestutil.CleanImagesInCI(s.pool) }()
|
||||
|
||||
s.controlServers.Range(func(_ string, control ControlServer) bool {
|
||||
stdoutPath, stderrPath, err := control.Shutdown()
|
||||
|
|
@ -334,9 +342,11 @@ func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) {
|
|||
})
|
||||
|
||||
s.mu.Lock()
|
||||
|
||||
for userName, user := range s.users {
|
||||
for _, client := range user.Clients {
|
||||
log.Printf("removing client %s in user %s", client.Hostname(), userName)
|
||||
|
||||
stdoutPath, stderrPath, err := client.Shutdown()
|
||||
if err != nil {
|
||||
log.Printf("tearing down client: %s", err)
|
||||
|
|
@ -353,6 +363,7 @@ func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.mu.Unlock()
|
||||
|
||||
for _, derp := range s.derpServers {
|
||||
|
|
@ -373,13 +384,16 @@ func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) {
|
|||
|
||||
if s.mockOIDC.r != nil {
|
||||
s.mockOIDC.r.Close()
|
||||
if err := s.mockOIDC.r.Close(); err != nil {
|
||||
|
||||
err := s.mockOIDC.r.Close()
|
||||
if err != nil {
|
||||
log.Printf("tearing down oidc server: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, network := range s.networks {
|
||||
if err := network.Close(); err != nil {
|
||||
err := network.Close()
|
||||
if err != nil {
|
||||
log.Printf("tearing down network: %s", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -395,7 +409,7 @@ func (s *Scenario) Shutdown() {
|
|||
|
||||
// Users returns the name of all users associated with the Scenario.
|
||||
func (s *Scenario) Users() []string {
|
||||
users := make([]string, 0)
|
||||
users := make([]string, 0, len(s.users))
|
||||
for user := range s.users {
|
||||
users = append(users, user)
|
||||
}
|
||||
|
|
@ -466,7 +480,7 @@ func (s *Scenario) CreatePreAuthKey(
|
|||
reusable bool,
|
||||
ephemeral bool,
|
||||
) (*v1.PreAuthKey, error) {
|
||||
if headscale, err := s.Headscale(); err == nil {
|
||||
if headscale, err := s.Headscale(); err == nil { //nolint:noinlineerr
|
||||
key, err := headscale.CreateAuthKey(user, reusable, ephemeral)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating user: %w", err)
|
||||
|
|
@ -518,7 +532,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) {
|
||||
if headscale, err := s.Headscale(); err == nil {
|
||||
if headscale, err := s.Headscale(); err == nil { //nolint:noinlineerr
|
||||
u, err := headscale.CreateUser(user)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating user: %w", err)
|
||||
|
|
@ -552,6 +566,7 @@ func (s *Scenario) CreateTailscaleNode(
|
|||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
opts = append(opts,
|
||||
tsic.WithCACert(cert),
|
||||
tsic.WithHeadscaleName(hostname),
|
||||
|
|
@ -591,6 +606,7 @@ func (s *Scenario) CreateTailscaleNodesInUser(
|
|||
) error {
|
||||
if user, ok := s.users[userStr]; ok {
|
||||
var versions []string
|
||||
|
||||
for i := range count {
|
||||
version := requestedVersion
|
||||
if requestedVersion == "all" {
|
||||
|
|
@ -600,6 +616,7 @@ func (s *Scenario) CreateTailscaleNodesInUser(
|
|||
version = MustTestVersions[i%len(MustTestVersions)]
|
||||
}
|
||||
}
|
||||
|
||||
versions = append(versions, version)
|
||||
|
||||
headscale, err := s.Headscale()
|
||||
|
|
@ -623,6 +640,7 @@ func (s *Scenario) CreateTailscaleNodesInUser(
|
|||
extraHosts := []string{hostname + ":" + headscaleIP}
|
||||
|
||||
s.mu.Lock()
|
||||
|
||||
opts = append(opts,
|
||||
tsic.WithCACert(cert),
|
||||
tsic.WithHeadscaleName(hostname),
|
||||
|
|
@ -639,6 +657,7 @@ func (s *Scenario) CreateTailscaleNodesInUser(
|
|||
opts...,
|
||||
)
|
||||
s.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"creating tailscale node: %w",
|
||||
|
|
@ -656,13 +675,17 @@ func (s *Scenario) CreateTailscaleNodesInUser(
|
|||
}
|
||||
|
||||
s.mu.Lock()
|
||||
|
||||
user.Clients[tsClient.Hostname()] = tsClient
|
||||
|
||||
s.mu.Unlock()
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := user.createWaitGroup.Wait(); err != nil {
|
||||
|
||||
err := user.createWaitGroup.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -682,12 +705,14 @@ func (s *Scenario) RunTailscaleUp(
|
|||
if user, ok := s.users[userStr]; ok {
|
||||
for _, client := range user.Clients {
|
||||
c := client
|
||||
|
||||
user.joinWaitGroup.Go(func() error {
|
||||
return c.Login(loginServer, authKey)
|
||||
})
|
||||
}
|
||||
|
||||
if err := user.joinWaitGroup.Wait(); err != nil {
|
||||
err := user.joinWaitGroup.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -749,11 +774,14 @@ func (s *Scenario) WaitForTailscaleSyncPerUser(timeout, retryInterval time.Durat
|
|||
for _, client := range user.Clients {
|
||||
c := client
|
||||
expectedCount := expectedPeers
|
||||
|
||||
user.syncWaitGroup.Go(func() error {
|
||||
return c.WaitForPeers(expectedCount, timeout, retryInterval)
|
||||
})
|
||||
}
|
||||
if err := user.syncWaitGroup.Wait(); err != nil {
|
||||
|
||||
err := user.syncWaitGroup.Wait()
|
||||
if err != nil {
|
||||
allErrors = append(allErrors, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -773,11 +801,14 @@ func (s *Scenario) WaitForTailscaleSyncWithPeerCount(peerCount int, timeout, ret
|
|||
for _, user := range s.users {
|
||||
for _, client := range user.Clients {
|
||||
c := client
|
||||
|
||||
user.syncWaitGroup.Go(func() error {
|
||||
return c.WaitForPeers(peerCount, timeout, retryInterval)
|
||||
})
|
||||
}
|
||||
if err := user.syncWaitGroup.Wait(); err != nil {
|
||||
|
||||
err := user.syncWaitGroup.Wait()
|
||||
if err != nil {
|
||||
allErrors = append(allErrors, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -871,6 +902,7 @@ func (s *Scenario) createHeadscaleEnvWithTags(
|
|||
} else {
|
||||
key, err = s.CreatePreAuthKey(u.GetId(), true, false)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -887,9 +919,11 @@ func (s *Scenario) createHeadscaleEnvWithTags(
|
|||
|
||||
func (s *Scenario) RunTailscaleUpWithURL(userStr, loginServer string) error {
|
||||
log.Printf("running tailscale up for user %s", userStr)
|
||||
|
||||
if user, ok := s.users[userStr]; ok {
|
||||
for _, client := range user.Clients {
|
||||
tsc := client
|
||||
|
||||
user.joinWaitGroup.Go(func() error {
|
||||
loginURL, err := tsc.LoginWithURL(loginServer)
|
||||
if err != nil {
|
||||
|
|
@ -904,7 +938,7 @@ func (s *Scenario) RunTailscaleUpWithURL(userStr, loginServer string) error {
|
|||
// If the URL is not a OIDC URL, then we need to
|
||||
// run the register command to fully log in the client.
|
||||
if !strings.Contains(loginURL.String(), "/oidc/") {
|
||||
s.runHeadscaleRegister(userStr, body)
|
||||
_ = s.runHeadscaleRegister(userStr, body)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -913,7 +947,8 @@ func (s *Scenario) RunTailscaleUpWithURL(userStr, loginServer string) error {
|
|||
log.Printf("client %s is ready", client.Hostname())
|
||||
}
|
||||
|
||||
if err := user.joinWaitGroup.Wait(); err != nil {
|
||||
err := user.joinWaitGroup.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -945,6 +980,7 @@ func newDebugJar() (*debugJar, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &debugJar{
|
||||
inner: jar,
|
||||
store: make(map[string]map[string]map[string]*http.Cookie),
|
||||
|
|
@ -961,20 +997,25 @@ func (j *debugJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
|
|||
if c == nil || c.Name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
domain := c.Domain
|
||||
if domain == "" {
|
||||
domain = u.Hostname()
|
||||
}
|
||||
|
||||
path := c.Path
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
|
||||
if _, ok := j.store[domain]; !ok {
|
||||
j.store[domain] = make(map[string]map[string]*http.Cookie)
|
||||
}
|
||||
|
||||
if _, ok := j.store[domain][path]; !ok {
|
||||
j.store[domain][path] = make(map[string]*http.Cookie)
|
||||
}
|
||||
|
||||
j.store[domain][path][c.Name] = copyCookie(c)
|
||||
}
|
||||
}
|
||||
|
|
@ -989,8 +1030,10 @@ func (j *debugJar) Dump(w io.Writer) {
|
|||
|
||||
for domain, paths := range j.store {
|
||||
fmt.Fprintf(w, "Domain: %s\n", domain)
|
||||
|
||||
for path, byName := range paths {
|
||||
fmt.Fprintf(w, " Path: %s\n", path)
|
||||
|
||||
for _, c := range byName {
|
||||
fmt.Fprintf(
|
||||
w, " %s=%s; Expires=%v; Secure=%v; HttpOnly=%v; SameSite=%v\n",
|
||||
|
|
@ -1046,15 +1089,17 @@ func doLoginURLWithClient(hostname string, loginURL *url.URL, hc *http.Client, f
|
|||
error,
|
||||
) {
|
||||
if hc == nil {
|
||||
return "", nil, fmt.Errorf("%s http client is nil", hostname)
|
||||
return "", nil, fmt.Errorf("%s http client is nil", hostname) //nolint:err113
|
||||
}
|
||||
|
||||
if loginURL == nil {
|
||||
return "", nil, fmt.Errorf("%s login url is nil", hostname)
|
||||
return "", nil, fmt.Errorf("%s login url is nil", hostname) //nolint:err113
|
||||
}
|
||||
|
||||
log.Printf("%s logging in with url: %s", hostname, loginURL.String())
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("%s creating http request: %w", hostname, err)
|
||||
|
|
@ -1066,6 +1111,7 @@ func doLoginURLWithClient(hostname string, loginURL *url.URL, hc *http.Client, f
|
|||
return http.ErrUseLastResponse
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
hc.CheckRedirect = originalRedirect
|
||||
}()
|
||||
|
|
@ -1080,6 +1126,7 @@ func doLoginURLWithClient(hostname string, loginURL *url.URL, hc *http.Client, f
|
|||
if err != nil {
|
||||
return "", nil, fmt.Errorf("%s reading response body: %w", hostname, err)
|
||||
}
|
||||
|
||||
body := string(bodyBytes)
|
||||
|
||||
var redirectURL *url.URL
|
||||
|
|
@ -1093,13 +1140,13 @@ func doLoginURLWithClient(hostname string, loginURL *url.URL, hc *http.Client, f
|
|||
if followRedirects && resp.StatusCode != http.StatusOK {
|
||||
log.Printf("body: %s", body)
|
||||
|
||||
return body, redirectURL, fmt.Errorf("%s unexpected status code %d", hostname, resp.StatusCode)
|
||||
return body, redirectURL, fmt.Errorf("%s unexpected status code %d", hostname, resp.StatusCode) //nolint:err113
|
||||
}
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
log.Printf("body: %s", body)
|
||||
|
||||
return body, redirectURL, fmt.Errorf("%s unexpected status code %d", hostname, resp.StatusCode)
|
||||
return body, redirectURL, fmt.Errorf("%s unexpected status code %d", hostname, resp.StatusCode) //nolint:err113
|
||||
}
|
||||
|
||||
if hc.Jar != nil {
|
||||
|
|
@ -1117,7 +1164,7 @@ var errParseAuthPage = errors.New("parsing auth page")
|
|||
|
||||
func (s *Scenario) runHeadscaleRegister(userStr string, body string) error {
|
||||
// see api.go HTML template
|
||||
codeSep := strings.Split(string(body), "</code>")
|
||||
codeSep := strings.Split(body, "</code>")
|
||||
if len(codeSep) != 2 {
|
||||
return errParseAuthPage
|
||||
}
|
||||
|
|
@ -1126,11 +1173,12 @@ func (s *Scenario) runHeadscaleRegister(userStr string, body string) error {
|
|||
if len(keySep) != 2 {
|
||||
return errParseAuthPage
|
||||
}
|
||||
|
||||
key := keySep[1]
|
||||
key = strings.SplitN(key, " ", 2)[0]
|
||||
log.Printf("registering node %s", key)
|
||||
|
||||
if headscale, err := s.Headscale(); err == nil {
|
||||
if headscale, err := s.Headscale(); err == nil { //nolint:noinlineerr
|
||||
_, err = headscale.Execute(
|
||||
[]string{"headscale", "nodes", "register", "--user", userStr, "--key", key},
|
||||
)
|
||||
|
|
@ -1154,6 +1202,7 @@ func (t LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error
|
|||
noTls := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // nolint
|
||||
}
|
||||
|
||||
resp, err := noTls.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -1173,12 +1222,14 @@ func (t LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error
|
|||
// in a Scenario.
|
||||
func (s *Scenario) GetIPs(user string) ([]netip.Addr, error) {
|
||||
var ips []netip.Addr
|
||||
|
||||
if ns, ok := s.users[user]; ok {
|
||||
for _, client := range ns.Clients {
|
||||
clientIps, err := client.IPs()
|
||||
if err != nil {
|
||||
return ips, fmt.Errorf("getting IPs: %w", err)
|
||||
}
|
||||
|
||||
ips = append(ips, clientIps...)
|
||||
}
|
||||
|
||||
|
|
@ -1191,6 +1242,7 @@ func (s *Scenario) GetIPs(user string) ([]netip.Addr, error) {
|
|||
// GetClients returns all TailscaleClients associated with a User in a Scenario.
|
||||
func (s *Scenario) GetClients(user string) ([]TailscaleClient, error) {
|
||||
var clients []TailscaleClient
|
||||
|
||||
if ns, ok := s.users[user]; ok {
|
||||
for _, client := range ns.Clients {
|
||||
clients = append(clients, client)
|
||||
|
|
@ -1290,11 +1342,14 @@ func (s *Scenario) WaitForTailscaleLogout() error {
|
|||
for _, user := range s.users {
|
||||
for _, client := range user.Clients {
|
||||
c := client
|
||||
|
||||
user.syncWaitGroup.Go(func() error {
|
||||
return c.WaitForNeedsLogin(integrationutil.PeerSyncTimeout())
|
||||
})
|
||||
}
|
||||
if err := user.syncWaitGroup.Wait(); err != nil {
|
||||
|
||||
err := user.syncWaitGroup.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -1361,6 +1416,7 @@ func (s *Scenario) runMockOIDC(accessTTL time.Duration, users []mockoidc.MockUse
|
|||
if err != nil {
|
||||
log.Fatalf("finding open port: %s", err)
|
||||
}
|
||||
|
||||
portNotation := fmt.Sprintf("%d/tcp", port)
|
||||
|
||||
hash, _ := util.GenerateRandomStringDNSSafe(hsicOIDCMockHashLength)
|
||||
|
|
@ -1405,7 +1461,7 @@ func (s *Scenario) runMockOIDC(accessTTL time.Duration, users []mockoidc.MockUse
|
|||
// Add integration test labels if running under hi tool
|
||||
dockertestutil.DockerAddIntegrationLabels(mockOidcOptions, "oidc")
|
||||
|
||||
if pmockoidc, err := s.pool.BuildAndRunWithBuildOptions(
|
||||
if pmockoidc, err := s.pool.BuildAndRunWithBuildOptions( //nolint:noinlineerr
|
||||
headscaleBuildOptions,
|
||||
mockOidcOptions,
|
||||
dockertestutil.DockerRestartPolicy); err == nil {
|
||||
|
|
@ -1421,9 +1477,10 @@ func (s *Scenario) runMockOIDC(accessTTL time.Duration, users []mockoidc.MockUse
|
|||
ipAddr := s.mockOIDC.r.GetIPInNetwork(network)
|
||||
|
||||
log.Println("Waiting for headscale mock oidc to be ready for tests")
|
||||
|
||||
hostEndpoint := net.JoinHostPort(ipAddr, strconv.Itoa(port))
|
||||
|
||||
if err := s.pool.Retry(func() error {
|
||||
if err := s.pool.Retry(func() error { //nolint:noinlineerr
|
||||
oidcConfigURL := fmt.Sprintf("http://%s/oidc/.well-known/openid-configuration", hostEndpoint)
|
||||
httpClient := &http.Client{}
|
||||
ctx := context.Background()
|
||||
|
|
@ -1468,14 +1525,13 @@ func Webservice(s *Scenario, networkName string) (*dockertest.Resource, error) {
|
|||
// log.Fatalf("finding open port: %s", err)
|
||||
// }
|
||||
// portNotation := fmt.Sprintf("%d/tcp", port)
|
||||
|
||||
hash := util.MustGenerateRandomStringDNSSafe(hsicOIDCMockHashLength)
|
||||
|
||||
hostname := "hs-webservice-" + hash
|
||||
|
||||
network, ok := s.networks[s.prefixedNetworkName(networkName)]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("network does not exist: %s", networkName)
|
||||
return nil, fmt.Errorf("network does not exist: %s", networkName) //nolint:err113
|
||||
}
|
||||
|
||||
webOpts := &dockertest.RunOptions{
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ func TestHeadscale(t *testing.T) {
|
|||
user := "test-space"
|
||||
|
||||
scenario, err := NewScenario(ScenarioSpec{})
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
@ -83,6 +84,7 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
|
|||
count := 1
|
||||
|
||||
scenario, err := NewScenario(ScenarioSpec{})
|
||||
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
|
|
|
|||
|
|
@ -493,7 +493,7 @@ func assertSSHTimeout(t *testing.T, client TailscaleClient, peer TailscaleClient
|
|||
|
||||
func assertSSHNoAccessStdError(t *testing.T, err error, stderr string) {
|
||||
t.Helper()
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
|
||||
if !isSSHNoAccessStdError(stderr) {
|
||||
t.Errorf("expected stderr output suggesting access denied, got: %s", stderr)
|
||||
|
|
|
|||
|
|
@ -2502,7 +2502,7 @@ func assertNetmapSelfHasTagsWithCollect(c *assert.CollectT, client TailscaleClie
|
|||
var actualTagsSlice []string
|
||||
|
||||
if nm.SelfNode.Valid() {
|
||||
for _, tag := range nm.SelfNode.Tags().All() {
|
||||
for _, tag := range nm.SelfNode.Tags().All() { //nolint:unqueryvet // not SQLBoiler, tailcfg iterator
|
||||
actualTagsSlice = append(actualTagsSlice, tag)
|
||||
}
|
||||
}
|
||||
|
|
@ -2647,7 +2647,7 @@ func TestTagsIssue2978ReproTagReplacement(t *testing.T) {
|
|||
var netmapTagsAfterFirstCall []string
|
||||
|
||||
if nmErr == nil && nm != nil && nm.SelfNode.Valid() {
|
||||
for _, tag := range nm.SelfNode.Tags().All() {
|
||||
for _, tag := range nm.SelfNode.Tags().All() { //nolint:unqueryvet // not SQLBoiler, tailcfg iterator
|
||||
netmapTagsAfterFirstCall = append(netmapTagsAfterFirstCall, tag)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,8 +52,6 @@ var (
|
|||
errTailscaleNotLoggedIn = errors.New("tailscale not logged in")
|
||||
errTailscaleWrongPeerCount = errors.New("wrong peer count")
|
||||
errTailscaleCannotUpWithoutAuthkey = errors.New("cannot up without authkey")
|
||||
errTailscaleNotConnected = errors.New("tailscale not connected")
|
||||
errTailscaledNotReadyForLogin = errors.New("tailscaled not ready for login")
|
||||
errInvalidClientConfig = errors.New("verifiably invalid client config requested")
|
||||
errInvalidTailscaleImageFormat = errors.New("invalid HEADSCALE_INTEGRATION_TAILSCALE_IMAGE format, expected repository:tag")
|
||||
errTailscaleImageRequiredInCI = errors.New("HEADSCALE_INTEGRATION_TAILSCALE_IMAGE must be set in CI for HEAD version")
|
||||
|
|
@ -297,6 +295,8 @@ func (t *TailscaleInContainer) buildEntrypoint() []string {
|
|||
}
|
||||
|
||||
// New returns a new TailscaleInContainer instance.
|
||||
//
|
||||
//nolint:gocyclo // complex container setup with many options
|
||||
func New(
|
||||
pool *dockertest.Pool,
|
||||
version string,
|
||||
|
|
@ -338,7 +338,7 @@ func New(
|
|||
}
|
||||
|
||||
if tsic.network == nil {
|
||||
return nil, fmt.Errorf("no network set, called from: \n%s", string(debug.Stack()))
|
||||
return nil, fmt.Errorf("no network set, called from: \n%s", string(debug.Stack())) //nolint:err113
|
||||
}
|
||||
|
||||
tailscaleOptions := &dockertest.RunOptions{
|
||||
|
|
@ -586,7 +586,7 @@ func (t *TailscaleInContainer) Version() string {
|
|||
return t.version
|
||||
}
|
||||
|
||||
// ID returns the Docker container ID of the TailscaleInContainer
|
||||
// ContainerID returns the Docker container ID of the TailscaleInContainer
|
||||
// instance.
|
||||
func (t *TailscaleInContainer) ContainerID() string {
|
||||
return t.container.Container.ID
|
||||
|
|
@ -621,7 +621,7 @@ func (t *TailscaleInContainer) Execute(
|
|||
return stdout, stderr, nil
|
||||
}
|
||||
|
||||
// Retrieve container logs.
|
||||
// Logs retrieves the container logs.
|
||||
func (t *TailscaleInContainer) Logs(stdout, stderr io.Writer) error {
|
||||
return dockertestutil.WriteLog(
|
||||
t.pool,
|
||||
|
|
@ -673,7 +673,7 @@ func (t *TailscaleInContainer) Login(
|
|||
) error {
|
||||
command := t.buildLoginCommand(loginServer, authKey)
|
||||
|
||||
if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil {
|
||||
if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil { //nolint:noinlineerr
|
||||
return fmt.Errorf(
|
||||
"%s failed to join tailscale client (%s): %w",
|
||||
t.hostname,
|
||||
|
|
@ -685,11 +685,11 @@ func (t *TailscaleInContainer) Login(
|
|||
return nil
|
||||
}
|
||||
|
||||
// Up runs the login routine on the given Tailscale instance.
|
||||
// LoginWithURL runs the login routine on the given Tailscale instance.
|
||||
// This login mechanism uses web + command line flow for authentication.
|
||||
func (t *TailscaleInContainer) LoginWithURL(
|
||||
loginServer string,
|
||||
) (loginURL *url.URL, err error) {
|
||||
) (*url.URL, error) {
|
||||
command := t.buildLoginCommand(loginServer, "")
|
||||
|
||||
stdout, stderr, err := t.Execute(command)
|
||||
|
|
@ -703,7 +703,7 @@ func (t *TailscaleInContainer) LoginWithURL(
|
|||
}
|
||||
}()
|
||||
|
||||
loginURL, err = util.ParseLoginURLFromCLILogin(stdout + stderr)
|
||||
loginURL, err := util.ParseLoginURLFromCLILogin(stdout + stderr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -713,14 +713,14 @@ func (t *TailscaleInContainer) LoginWithURL(
|
|||
|
||||
// Logout runs the logout routine on the given Tailscale instance.
|
||||
func (t *TailscaleInContainer) Logout() error {
|
||||
stdout, stderr, err := t.Execute([]string{"tailscale", "logout"})
|
||||
_, _, err := t.Execute([]string{"tailscale", "logout"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stdout, stderr, _ = t.Execute([]string{"tailscale", "status"})
|
||||
stdout, stderr, _ := t.Execute([]string{"tailscale", "status"})
|
||||
if !strings.Contains(stdout+stderr, "Logged out.") {
|
||||
return fmt.Errorf("logging out, stdout: %s, stderr: %s", stdout, stderr)
|
||||
return fmt.Errorf("logging out, stdout: %s, stderr: %s", stdout, stderr) //nolint:err113
|
||||
}
|
||||
|
||||
return t.waitForBackendState("NeedsLogin", integrationutil.PeerSyncTimeout())
|
||||
|
|
@ -759,14 +759,14 @@ func (t *TailscaleInContainer) Restart() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Helper that runs `tailscale up` with no arguments.
|
||||
// Up runs `tailscale up` with no arguments.
|
||||
func (t *TailscaleInContainer) Up() error {
|
||||
command := []string{
|
||||
"tailscale",
|
||||
"up",
|
||||
}
|
||||
|
||||
if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil {
|
||||
if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil { //nolint:noinlineerr
|
||||
return fmt.Errorf(
|
||||
"%s failed to bring tailscale client up (%s): %w",
|
||||
t.hostname,
|
||||
|
|
@ -778,14 +778,14 @@ func (t *TailscaleInContainer) Up() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Helper that runs `tailscale down` with no arguments.
|
||||
// Down runs `tailscale down` with no arguments.
|
||||
func (t *TailscaleInContainer) Down() error {
|
||||
command := []string{
|
||||
"tailscale",
|
||||
"down",
|
||||
}
|
||||
|
||||
if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil {
|
||||
if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil { //nolint:noinlineerr
|
||||
return fmt.Errorf(
|
||||
"%s failed to bring tailscale client down (%s): %w",
|
||||
t.hostname,
|
||||
|
|
@ -832,7 +832,7 @@ func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
|
|||
}
|
||||
|
||||
if len(ips) == 0 {
|
||||
return nil, fmt.Errorf("no IPs returned yet for %s", t.hostname)
|
||||
return nil, fmt.Errorf("no IPs returned yet for %s", t.hostname) //nolint:err113
|
||||
}
|
||||
|
||||
return ips, nil
|
||||
|
|
@ -866,7 +866,7 @@ func (t *TailscaleInContainer) IPv4() (netip.Addr, error) {
|
|||
}
|
||||
}
|
||||
|
||||
return netip.Addr{}, fmt.Errorf("no IPv4 address found for %s", t.hostname)
|
||||
return netip.Addr{}, fmt.Errorf("no IPv4 address found for %s", t.hostname) //nolint:err113
|
||||
}
|
||||
|
||||
func (t *TailscaleInContainer) MustIPv4() netip.Addr {
|
||||
|
|
@ -908,7 +908,7 @@ func (t *TailscaleInContainer) Status(save ...bool) (*ipnstate.Status, error) {
|
|||
return nil, fmt.Errorf("unmarshalling tailscale status: %w", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("/tmp/control/%s_status.json", t.hostname), []byte(result), 0o755)
|
||||
err = os.WriteFile(fmt.Sprintf("/tmp/control/%s_status.json", t.hostname), []byte(result), 0o755) //nolint:gosec // test infrastructure log files
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("status netmap to /tmp/control: %w", err)
|
||||
}
|
||||
|
|
@ -968,7 +968,7 @@ func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) {
|
|||
return nil, fmt.Errorf("unmarshalling tailscale netmap: %w", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("/tmp/control/%s_netmap.json", t.hostname), []byte(result), 0o755)
|
||||
err = os.WriteFile(fmt.Sprintf("/tmp/control/%s_netmap.json", t.hostname), []byte(result), 0o755) //nolint:gosec // test infrastructure log files
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("saving netmap to /tmp/control: %w", err)
|
||||
}
|
||||
|
|
@ -1001,6 +1001,8 @@ func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) {
|
|||
|
||||
// watchIPN watches `tailscale debug watch-ipn` for a ipn.Notify object until
|
||||
// it gets one that has a netmap.NetworkMap.
|
||||
//
|
||||
//nolint:unused
|
||||
func (t *TailscaleInContainer) watchIPN(ctx context.Context) (*ipn.Notify, error) {
|
||||
pr, pw := io.Pipe()
|
||||
|
||||
|
|
@ -1211,7 +1213,7 @@ func (t *TailscaleInContainer) waitForBackendState(state string, timeout time.Du
|
|||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("timeout waiting for backend state %s on %s after %v", state, t.hostname, timeout)
|
||||
return fmt.Errorf("timeout waiting for backend state %s on %s after %v", state, t.hostname, timeout) //nolint:err113
|
||||
case <-ticker.C:
|
||||
status, err := t.Status()
|
||||
if err != nil {
|
||||
|
|
@ -1256,7 +1258,7 @@ func (t *TailscaleInContainer) WaitForPeers(expected int, timeout, retryInterval
|
|||
return fmt.Errorf("timeout waiting for %d peers on %s after %v, errors: %w", expected, t.hostname, timeout, multierr.New(lastErrs...))
|
||||
}
|
||||
|
||||
return fmt.Errorf("timeout waiting for %d peers on %s after %v", expected, t.hostname, timeout)
|
||||
return fmt.Errorf("timeout waiting for %d peers on %s after %v", expected, t.hostname, timeout) //nolint:err113
|
||||
case <-ticker.C:
|
||||
status, err := t.Status()
|
||||
if err != nil {
|
||||
|
|
@ -1284,15 +1286,15 @@ func (t *TailscaleInContainer) WaitForPeers(expected int, timeout, retryInterval
|
|||
peer := status.Peer[peerKey]
|
||||
|
||||
if !peer.Online {
|
||||
peerErrors = append(peerErrors, fmt.Errorf("[%s] peer count correct, but %s is not online", t.hostname, peer.HostName))
|
||||
peerErrors = append(peerErrors, fmt.Errorf("[%s] peer count correct, but %s is not online", t.hostname, peer.HostName)) //nolint:err113
|
||||
}
|
||||
|
||||
if peer.HostName == "" {
|
||||
peerErrors = append(peerErrors, fmt.Errorf("[%s] peer count correct, but %s does not have a Hostname", t.hostname, peer.HostName))
|
||||
peerErrors = append(peerErrors, fmt.Errorf("[%s] peer count correct, but %s does not have a Hostname", t.hostname, peer.HostName)) //nolint:err113
|
||||
}
|
||||
|
||||
if peer.Relay == "" {
|
||||
peerErrors = append(peerErrors, fmt.Errorf("[%s] peer count correct, but %s does not have a DERP", t.hostname, peer.HostName))
|
||||
peerErrors = append(peerErrors, fmt.Errorf("[%s] peer count correct, but %s does not have a DERP", t.hostname, peer.HostName)) //nolint:err113
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1355,14 +1357,14 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) err
|
|||
opt(&args)
|
||||
}
|
||||
|
||||
command := []string{
|
||||
command := make([]string, 0, 6)
|
||||
command = append(command,
|
||||
"tailscale", "ping",
|
||||
fmt.Sprintf("--timeout=%s", args.timeout),
|
||||
fmt.Sprintf("--c=%d", args.count),
|
||||
"--until-direct=" + strconv.FormatBool(args.direct),
|
||||
}
|
||||
|
||||
command = append(command, hostnameOrIP)
|
||||
"--until-direct="+strconv.FormatBool(args.direct),
|
||||
hostnameOrIP,
|
||||
)
|
||||
|
||||
result, _, err := t.Execute(
|
||||
command,
|
||||
|
|
@ -1566,19 +1568,19 @@ func (t *TailscaleInContainer) ReadFile(path string) ([]byte, error) {
|
|||
}
|
||||
|
||||
if !strings.Contains(path, hdr.Name) {
|
||||
return nil, fmt.Errorf("file not found in tar archive, looking for: %s, header was: %s", path, hdr.Name)
|
||||
return nil, fmt.Errorf("file not found in tar archive, looking for: %s, header was: %s", path, hdr.Name) //nolint:err113
|
||||
}
|
||||
|
||||
if _, err := io.Copy(&out, tr); err != nil {
|
||||
if _, err := io.Copy(&out, tr); err != nil { //nolint:gosec,noinlineerr // trusted tar from test container
|
||||
return nil, fmt.Errorf("copying file to buffer: %w", err)
|
||||
}
|
||||
|
||||
// Only support reading the first tile
|
||||
break
|
||||
break //nolint:staticcheck // SA4004: intentional - only read first file
|
||||
}
|
||||
|
||||
if out.Len() == 0 {
|
||||
return nil, errors.New("file is empty")
|
||||
return nil, errors.New("file is empty") //nolint:err113
|
||||
}
|
||||
|
||||
return out.Bytes(), nil
|
||||
|
|
@ -1591,7 +1593,7 @@ func (t *TailscaleInContainer) GetNodePrivateKey() (*key.NodePrivate, error) {
|
|||
}
|
||||
|
||||
store := &mem.Store{}
|
||||
if err = store.LoadFromJSON(state); err != nil {
|
||||
if err = store.LoadFromJSON(state); err != nil { //nolint:noinlineerr
|
||||
return nil, fmt.Errorf("unmarshalling state file: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -1606,7 +1608,7 @@ func (t *TailscaleInContainer) GetNodePrivateKey() (*key.NodePrivate, error) {
|
|||
}
|
||||
|
||||
p := &ipn.Prefs{}
|
||||
if err = json.Unmarshal(currentProfile, &p); err != nil {
|
||||
if err = json.Unmarshal(currentProfile, &p); err != nil { //nolint:noinlineerr
|
||||
return nil, fmt.Errorf("unmarshalling current profile state: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -1617,7 +1619,7 @@ func (t *TailscaleInContainer) GetNodePrivateKey() (*key.NodePrivate, error) {
|
|||
// This is useful for verifying that policy changes have propagated to the client.
|
||||
func (t *TailscaleInContainer) PacketFilter() ([]filter.Match, error) {
|
||||
if !util.TailscaleVersionNewerOrEqual("1.56", t.version) {
|
||||
return nil, fmt.Errorf("tsic.PacketFilter() requires Tailscale 1.56+, current version: %s", t.version)
|
||||
return nil, fmt.Errorf("tsic.PacketFilter() requires Tailscale 1.56+, current version: %s", t.version) //nolint:err113
|
||||
}
|
||||
|
||||
nm, err := t.Netmap()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue