mirror of
https://github.com/juanfont/headscale.git
synced 2026-07-17 16:36:02 +00:00
all: apply godoc [Name] link conventions across comments
Some checks are pending
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Check Generated Files / check-generated (push) Waiting to run
Build (main) / container (push) Waiting to run
Build (main) / binaries (amd64, darwin) (push) Waiting to run
Build (main) / binaries (amd64, linux) (push) Waiting to run
Build (main) / binaries (arm64, darwin) (push) Waiting to run
Build (main) / binaries (arm64, linux) (push) Waiting to run
NixOS Module Tests / nix-module-check (push) Waiting to run
Tests / test (push) Waiting to run
Some checks are pending
Build / build-nix (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=amd64 GOOS=linux) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=darwin) (push) Waiting to run
Build / build-cross (GOARCH=arm64 GOOS=linux) (push) Waiting to run
Check Generated Files / check-generated (push) Waiting to run
Build (main) / container (push) Waiting to run
Build (main) / binaries (amd64, darwin) (push) Waiting to run
Build (main) / binaries (amd64, linux) (push) Waiting to run
Build (main) / binaries (arm64, darwin) (push) Waiting to run
Build (main) / binaries (arm64, linux) (push) Waiting to run
NixOS Module Tests / nix-module-check (push) Waiting to run
Tests / test (push) Waiting to run
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
This commit is contained in:
parent
17236fd284
commit
4cca63155d
124 changed files with 1037 additions and 1011 deletions
|
|
@ -1268,9 +1268,9 @@ func TestACLAutogroupTagged(t *testing.T) {
|
|||
}
|
||||
|
||||
// Create the tailscale node with appropriate options.
|
||||
// CACert and HeadscaleName are passed explicitly because
|
||||
// nodes created via CreateTailscaleNode are not part of
|
||||
// the standard CreateHeadscaleEnv flow.
|
||||
// [tsic.WithCACert] and [tsic.WithHeadscaleName] are passed explicitly because
|
||||
// nodes created via [Scenario.CreateTailscaleNode] are not part of
|
||||
// the standard [Scenario.CreateHeadscaleEnv] flow.
|
||||
opts := []tsic.Option{
|
||||
tsic.WithCACert(headscale.GetCert()),
|
||||
tsic.WithHeadscaleName(headscale.GetHostname()),
|
||||
|
|
@ -1558,9 +1558,9 @@ func TestACLAutogroupSelf(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Create router node (tags come from the PreAuthKey).
|
||||
// CACert and HeadscaleName are passed explicitly because
|
||||
// nodes created via tsic.New are not part of the standard
|
||||
// CreateHeadscaleEnv flow.
|
||||
// [tsic.WithCACert] and [tsic.WithHeadscaleName] are passed explicitly because
|
||||
// nodes created via [tsic.New] are not part of the standard
|
||||
// [Scenario.CreateHeadscaleEnv] flow.
|
||||
routerClient, err := tsic.New(
|
||||
scenario.Pool(),
|
||||
"unstable",
|
||||
|
|
@ -2084,9 +2084,9 @@ func TestACLPolicyPropagationOverTime(t *testing.T) {
|
|||
err = headscale.SetPolicy(user1ToUser2Policy)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Note: Cannot use WaitForTailscaleSync() here because directional policy means
|
||||
// Note: Cannot use [Scenario.WaitForTailscaleSync] here because directional policy means
|
||||
// user2 nodes don't see user1 nodes in their peer list (asymmetric visibility).
|
||||
// The EventuallyWithT block below will handle waiting for policy propagation.
|
||||
// The [assert.EventuallyWithT] block below will handle waiting for policy propagation.
|
||||
|
||||
// Test ALL connectivity (positive and negative) in one block after policy settles
|
||||
t.Logf("Iteration %d: Phase 3 - Testing all connectivity with directional policy", iteration)
|
||||
|
|
@ -2625,9 +2625,9 @@ func TestACLTagPropagation(t *testing.T) {
|
|||
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying tag change applied")
|
||||
|
||||
// Step 3: Verify final NetMap visibility first (fast signal that
|
||||
// the MapResponse propagated to the client).
|
||||
// the [tailcfg.MapResponse] propagated to the client).
|
||||
// The full propagation chain (docker exec → gRPC → state update →
|
||||
// batcher delay → MapResponse → noise transport → client processing)
|
||||
// batcher delay → [tailcfg.MapResponse] → noise transport → client processing)
|
||||
// can take over 120s on congested CI runners, so use a generous
|
||||
// base timeout.
|
||||
t.Logf("Step 3: Verifying final NetMap visibility (expect visible=%v)", tt.finalAccess)
|
||||
|
|
@ -2653,7 +2653,7 @@ func TestACLTagPropagation(t *testing.T) {
|
|||
}, integrationutil.HASlowConvergeTimeout, integrationutil.SlowPoll, "verifying NetMap visibility propagated after tag change")
|
||||
|
||||
// Step 4: Verify final access state (this is the key test for #2389).
|
||||
// Even though Step 3 confirmed the MapResponse arrived, the full
|
||||
// Even though Step 3 confirmed the [tailcfg.MapResponse] arrived, the full
|
||||
// WireGuard handshake and tunnel establishment can take significant
|
||||
// time on congested CI runners, so use the same generous base
|
||||
// timeout as Step 3.
|
||||
|
|
@ -2858,7 +2858,7 @@ func TestACLTagPropagationPortSpecific(t *testing.T) {
|
|||
// Step 4: Verify HTTP on port 80 now fails (tag:sshonly only allows port 22).
|
||||
// Port-specific filter changes are harder than peer removal because
|
||||
// the WireGuard tunnel stays up and both endpoints must process
|
||||
// the new PacketFilter from the MapResponse.
|
||||
// the new [tailcfg.PacketFilter] from the [tailcfg.MapResponse].
|
||||
t.Log("Step 4: Verifying HTTP access is now blocked (tag:sshonly only allows port 22)")
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
assertCurlFailWithCollect(c, user2Node, targetURL, "HTTP should fail with tag:sshonly (only port 22 allowed)")
|
||||
|
|
@ -3099,7 +3099,7 @@ func TestACLGroupAfterUserDeletion(t *testing.T) {
|
|||
assertCurlDockerHostname(c, user1, url, "user1 should still be able to reach user2 after user3 deletion (stale cache)")
|
||||
}, integrationutil.HAConvergeTimeout, integrationutil.SlowPoll, "user1 -> user2 after user3 deletion")
|
||||
|
||||
// Step 4: Create a NEW user - this triggers updatePolicyManagerUsers() which
|
||||
// Step 4: Create a NEW user - this triggers [State.updatePolicyManagerUsers] which
|
||||
// re-evaluates the policy. According to issue #2967, this is when the bug manifests:
|
||||
// the deleted user3@ in the group causes the entire group to fail resolution.
|
||||
t.Log("Step 4: Creating a new user (user4) to trigger policy re-evaluation")
|
||||
|
|
@ -3275,7 +3275,7 @@ func TestACLGroupDeletionExactReproduction(t *testing.T) {
|
|||
|
||||
t.Log("Step 3: PASSED - connectivity works after user2 deletion")
|
||||
|
||||
// Step 4: Create a NEW user - this triggers updatePolicyManagerUsers()
|
||||
// Step 4: Create a NEW user - this triggers [State.updatePolicyManagerUsers]
|
||||
// According to the reporter, this is when the bug manifests
|
||||
t.Log("Step 4: Creating new user (user4) - this triggers policy re-evaluation")
|
||||
|
||||
|
|
@ -3283,8 +3283,8 @@ func TestACLGroupDeletionExactReproduction(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Step 5: THE CRITICAL TEST - verify connectivity STILL works
|
||||
// Without the fix: DeleteUser didn't update policy, so when CreateUser
|
||||
// triggers updatePolicyManagerUsers(), the stale user2@ is now unknown,
|
||||
// Without the fix: [state.State.DeleteUser] didn't update policy, so when [state.State.CreateUser]
|
||||
// triggers [State.updatePolicyManagerUsers], the stale user2@ is now unknown,
|
||||
// potentially breaking the group.
|
||||
t.Log("Step 5: Verifying connectivity AFTER creating new user (BUG trigger point)")
|
||||
|
||||
|
|
|
|||
|
|
@ -522,8 +522,8 @@ func TestGRPCAuthenticationBypass(t *testing.T) {
|
|||
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)
|
||||
// CLI outputs the users array directly, not wrapped in [v1.ListUsersResponse]
|
||||
// Parse as JSON array (CLI uses [json.Marshal], not protojson)
|
||||
var users []*v1.User
|
||||
|
||||
err = json.Unmarshal([]byte(output), &users)
|
||||
|
|
@ -681,8 +681,8 @@ cli:
|
|||
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)
|
||||
// CLI outputs the users array directly, not wrapped in [v1.ListUsersResponse]
|
||||
// Parse as JSON array (CLI uses [json.Marshal], not protojson)
|
||||
var users []*v1.User
|
||||
|
||||
err = json.Unmarshal([]byte(output), &users)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
|
|||
expectedNodes := collectExpectedNodeIDs(t, allClients)
|
||||
requireAllClientsOnline(t, headscale, expectedNodes, true, "all clients should be connected", integrationutil.ScaledTimeout(120*time.Second))
|
||||
|
||||
// Validate that all nodes have NetInfo and DERP servers before logout
|
||||
// Validate that all nodes have [tailcfg.NetInfo] and DERP servers before logout
|
||||
requireAllClientsNetInfoAndDERP(t, headscale, expectedNodes, "all clients should have NetInfo and DERP before logout", 3*time.Minute)
|
||||
|
||||
// assertClientsState(t, allClients)
|
||||
|
|
@ -161,11 +161,11 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
|
|||
|
||||
requireAllClientsOnline(t, headscale, expectedNodes, true, "all clients should be connected to batcher", integrationutil.ScaledTimeout(120*time.Second))
|
||||
|
||||
// Wait for Tailscale sync before validating NetInfo to ensure proper state propagation
|
||||
// Wait for Tailscale sync before validating [tailcfg.NetInfo] to ensure proper state propagation
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
requireNoErrSync(t, err)
|
||||
|
||||
// Validate that all nodes have NetInfo and DERP servers after reconnection
|
||||
// Validate that all nodes have [tailcfg.NetInfo] and DERP servers after reconnection
|
||||
requireAllClientsNetInfoAndDERP(t, headscale, expectedNodes, "all clients should have NetInfo and DERP after reconnection", 3*time.Minute)
|
||||
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
|
|
@ -475,7 +475,7 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
|
|||
// Steps:
|
||||
// 1. Create node with auth key
|
||||
// 2. DELETE the auth key from database (completely remove it)
|
||||
// 3. Restart node - should successfully reconnect using MachineKey identity.
|
||||
// 3. Restart node - should successfully reconnect using [tailcfg.Node.MachineKey] identity.
|
||||
func TestAuthKeyDeleteKey(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
|
|
@ -561,7 +561,7 @@ func TestAuthKeyDeleteKey(t *testing.T) {
|
|||
|
||||
// Verify node comes back online
|
||||
// This will FAIL without the fix because auth key validation will reject deleted key
|
||||
// With the fix, MachineKey identity allows reconnection even with deleted key
|
||||
// With the fix, [tailcfg.Node.MachineKey] identity allows reconnection even with deleted key
|
||||
requireAllClientsOnline(t, headscale, []types.NodeID{types.NodeID(nodeID)}, true, "node should reconnect after restart despite deleted key", integrationutil.ScaledTimeout(120*time.Second))
|
||||
|
||||
t.Logf("✓ Node successfully reconnected after its auth key was deleted")
|
||||
|
|
@ -725,9 +725,9 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) {
|
|||
node.GetAvailableRoutes(), node.GetApprovedRoutes(), node.GetSubnetRoutes())
|
||||
|
||||
// This is where issue #2896 manifests:
|
||||
// - Available shows the route (from Hostinfo.RoutableIPs)
|
||||
// - Approved shows the route (from ApprovedRoutes)
|
||||
// - BUT Serving (SubnetRoutes/PrimaryRoutes) is EMPTY!
|
||||
// - Available shows the route (from [tailcfg.Hostinfo.RoutableIPs])
|
||||
// - Approved shows the route (from [tailcfg.Node.ApprovedRoutes])
|
||||
// - BUT Serving ([tailcfg.Node.SubnetRoutes]/[ipnstate.PeerStatus.PrimaryRoutes]) is EMPTY!
|
||||
assert.Lenf(c, node.GetAvailableRoutes(), 1,
|
||||
"Node should have 1 available route after relogin, got %v", node.GetAvailableRoutes())
|
||||
assert.Lenf(c, node.GetApprovedRoutes(), 1,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import (
|
|||
func TestOIDCAuthenticationPingAll(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
// Logins to MockOIDC is served by a queue with a strict order,
|
||||
// Logins to [mockoidc.MockOIDC] is served by a queue with a strict order,
|
||||
// if we use more than one node per user, the order of the logins
|
||||
// will not be deterministic and the test will fail.
|
||||
spec := ScenarioSpec{
|
||||
|
|
@ -202,10 +202,10 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) {
|
|||
t.Logf("Waiting %v for OIDC tokens to expire (TTL: %v, spread: %v, buffer: %v)",
|
||||
totalWaitTime, shortAccessTTL, loginTimeSpread, safetyBuffer)
|
||||
|
||||
// EventuallyWithT retries the test function until it passes or times out.
|
||||
// IMPORTANT: Use 'ct' (CollectT) for all assertions inside the function, not 't'.
|
||||
// [assert.EventuallyWithT] retries the test function until it passes or times out.
|
||||
// IMPORTANT: Use 'ct' ([assert.CollectT]) for all assertions inside the function, not 't'.
|
||||
// Using 't' would cause immediate test failure without retries, defeating the purpose
|
||||
// of EventuallyWithT which is designed to handle timing-dependent conditions.
|
||||
// of [assert.EventuallyWithT] which is designed to handle timing-dependent conditions.
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
// Check each client's status individually to provide better diagnostics
|
||||
expiredCount := 0
|
||||
|
|
@ -718,7 +718,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
|
|||
}, integrationutil.StatusReadyTimeout, 1*time.Second, "waiting for user2 logout to complete before user1 relogin")
|
||||
|
||||
// Before logging back in, ensure we still have exactly 2 nodes
|
||||
// Note: We skip validateLogoutComplete here since it expects all nodes to be offline,
|
||||
// Note: We skip [validateLogoutComplete] here since it expects all nodes to be offline,
|
||||
// but in OIDC scenario we maintain both nodes in DB with only active user online
|
||||
|
||||
// Additional validation that nodes are properly maintained during logout
|
||||
|
|
@ -1468,8 +1468,8 @@ func TestOIDCExpiryAfterRestart(t *testing.T) {
|
|||
// 4. Verifies that the OIDC user's node IMMEDIATELY sees the advertised route
|
||||
//
|
||||
// Expected behavior:
|
||||
// - Without fix: OIDC node cannot see the route (PrimaryRoutes is nil/empty)
|
||||
// - With fix: OIDC node immediately sees the route in PrimaryRoutes
|
||||
// - Without fix: OIDC node cannot see the route ([ipnstate.PeerStatus.PrimaryRoutes] is nil/empty)
|
||||
// - With fix: OIDC node immediately sees the route in [ipnstate.PeerStatus.PrimaryRoutes]
|
||||
//
|
||||
// Root cause: The buggy code called a.h.Change(c) immediately after user
|
||||
// creation but BEFORE node registration completed, creating a race condition
|
||||
|
|
@ -1618,8 +1618,8 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
|
|||
// see the gateway's advertised route WITHOUT needing a client restart.
|
||||
//
|
||||
// This is where the bug manifests:
|
||||
// - Without fix: PrimaryRoutes will be nil/empty
|
||||
// - With fix: PrimaryRoutes immediately contains the advertised route
|
||||
// - Without fix: [ipnstate.PeerStatus.PrimaryRoutes] will be nil/empty
|
||||
// - With fix: [ipnstate.PeerStatus.PrimaryRoutes] immediately contains the advertised route
|
||||
t.Logf("Verifying OIDC user can immediately see advertised routes at %s", time.Now().Format(TimestampFormat))
|
||||
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
|
|
@ -1641,7 +1641,7 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
|
|||
assert.NotNil(ct, gatewayPeer, "OIDC user should see gateway as peer")
|
||||
|
||||
if gatewayPeer != nil {
|
||||
// This is the critical assertion - PrimaryRoutes should NOT be nil
|
||||
// This is the critical assertion - [ipnstate.PeerStatus.PrimaryRoutes] should NOT be nil
|
||||
assert.NotNil(ct, gatewayPeer.PrimaryRoutes,
|
||||
"BUG #2888: Gateway peer PrimaryRoutes is nil - ACL policy not applied to new OIDC node!")
|
||||
|
||||
|
|
@ -1652,7 +1652,7 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
|
|||
t.Logf("SUCCESS: OIDC user can see advertised route %s in gateway's PrimaryRoutes", advertiseRoute)
|
||||
}
|
||||
|
||||
// Also verify AllowedIPs includes the route
|
||||
// Also verify [ipnstate.PeerStatus.AllowedIPs] includes the route
|
||||
if gatewayPeer.AllowedIPs != nil && gatewayPeer.AllowedIPs.Len() > 0 {
|
||||
allowedIPs := gatewayPeer.AllowedIPs.AsSlice()
|
||||
t.Logf("Gateway peer AllowedIPs: %v", allowedIPs)
|
||||
|
|
@ -1914,9 +1914,9 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) {
|
|||
node.GetAvailableRoutes(), node.GetApprovedRoutes(), node.GetSubnetRoutes())
|
||||
|
||||
// This is where issue #2896 manifests:
|
||||
// - Available shows the route (from Hostinfo.RoutableIPs)
|
||||
// - Approved shows the route (from ApprovedRoutes)
|
||||
// - BUT Serving (SubnetRoutes/PrimaryRoutes) is EMPTY!
|
||||
// - Available shows the route (from [tailcfg.Hostinfo.RoutableIPs])
|
||||
// - Approved shows the route (from [tailcfg.Node.ApprovedRoutes])
|
||||
// - BUT Serving ([tailcfg.Node.SubnetRoutes]/[ipnstate.PeerStatus.PrimaryRoutes]) is EMPTY!
|
||||
assert.Lenf(c, node.GetAvailableRoutes(), 1,
|
||||
"Node should have 1 available route after relogin, got %v", node.GetAvailableRoutes())
|
||||
assert.Lenf(c, node.GetApprovedRoutes(), 1,
|
||||
|
|
|
|||
|
|
@ -339,7 +339,7 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
|||
assert.NoError(c, err)
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth keys list")
|
||||
|
||||
// There is one key created by "scenario.CreateHeadscaleEnv"
|
||||
// There is one key created by [Scenario.CreateHeadscaleEnv]
|
||||
assert.Len(t, listedPreAuthKeys, 4)
|
||||
|
||||
assert.Equal(
|
||||
|
|
@ -476,7 +476,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
|||
assert.NoError(c, err)
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth keys list")
|
||||
|
||||
// There is one key created by "scenario.CreateHeadscaleEnv"
|
||||
// There is one key created by [Scenario.CreateHeadscaleEnv]
|
||||
assert.Len(t, listedPreAuthKeys, 2)
|
||||
|
||||
assert.True(t, listedPreAuthKeys[1].GetExpiration().AsTime().After(time.Now()))
|
||||
|
|
@ -565,7 +565,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
|||
assert.NoError(c, err)
|
||||
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth keys list after reusable/ephemeral creation")
|
||||
|
||||
// There is one key created by "scenario.CreateHeadscaleEnv"
|
||||
// There is one key created by [Scenario.CreateHeadscaleEnv]
|
||||
assert.Len(t, listedPreAuthKeys, 3)
|
||||
}
|
||||
|
||||
|
|
@ -662,7 +662,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
|
|||
status, err := client.Status()
|
||||
assert.NoError(ct, err)
|
||||
assert.Equal(ct, "Running", status.BackendState, "Expected node to be logged in, backend state: %s", status.BackendState)
|
||||
// With tags-as-identity model, tagged nodes show as TaggedDevices user (2147455555)
|
||||
// With tags-as-identity model, tagged nodes show as [types.TaggedDevices] user (2147455555)
|
||||
// The PreAuthKey was created with tags, so the node is tagged
|
||||
assert.Equal(ct, "userid:2147455555", status.Self.UserID.String(), "Expected node to be logged in as tagged-devices user")
|
||||
}, integrationutil.StatusReadyTimeout, 2*time.Second)
|
||||
|
|
@ -761,7 +761,7 @@ func TestTaggedNodesCLIOutput(t *testing.T) {
|
|||
status, err := client.Status()
|
||||
assert.NoError(ct, err)
|
||||
assert.Equal(ct, "Running", status.BackendState, "Expected node to be logged in, backend state: %s", status.BackendState)
|
||||
// With tags-as-identity model, tagged nodes show as TaggedDevices user (2147455555)
|
||||
// With tags-as-identity model, tagged nodes show as [types.TaggedDevices] user (2147455555)
|
||||
assert.Equal(ct, "userid:2147455555", status.Self.UserID.String(), "Expected node to be logged in as tagged-devices user")
|
||||
}, integrationutil.StatusReadyTimeout, 2*time.Second)
|
||||
|
||||
|
|
|
|||
|
|
@ -72,12 +72,12 @@ func TestDERPVerifyEndpoint(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
// WithHostname is used instead of WithTestName because the hostname
|
||||
// [hsic.WithHostname] is used instead of [hsic.WithTestName] because the hostname
|
||||
// must match the pre-generated TLS certificate created above.
|
||||
// The test name "derpverify" is embedded in the hostname variable.
|
||||
//
|
||||
// WithCACert passes the external DERP server's certificate so
|
||||
// tailscale clients trust it. WithCustomTLS and WithDERPConfig
|
||||
// [tsic.WithCACert] passes the external DERP server's certificate so
|
||||
// tailscale clients trust it. [hsic.WithCustomTLS] and [hsic.WithDERPConfig]
|
||||
// configure headscale to use the external DERP server created
|
||||
// above instead of the default embedded one.
|
||||
err = scenario.CreateHeadscaleEnv([]tsic.Option{tsic.WithCACert(derper.GetCert())},
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ func GetIntegrationRunID() string {
|
|||
return os.Getenv("HEADSCALE_INTEGRATION_RUN_ID")
|
||||
}
|
||||
|
||||
// DockerAddIntegrationLabels adds integration test labels to Docker RunOptions.
|
||||
// DockerAddIntegrationLabels adds integration test labels to Docker [dockertest.RunOptions].
|
||||
// This allows the hi tool to identify containers belonging to specific test runs.
|
||||
// This function should be called before passing RunOptions to dockertest functions.
|
||||
// This function should be called before passing [dockertest.RunOptions] to dockertest functions.
|
||||
func DockerAddIntegrationLabels(opts *dockertest.RunOptions, testType string) {
|
||||
runID := GetIntegrationRunID()
|
||||
if runID == "" {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ func ExecuteCommandTimeout(timeout time.Duration) ExecuteCommandOption {
|
|||
})
|
||||
}
|
||||
|
||||
// buffer is a goroutine safe bytes.buffer.
|
||||
// buffer is a goroutine safe [bytes.Buffer].
|
||||
type buffer struct {
|
||||
store bytes.Buffer
|
||||
mutex sync.Mutex
|
||||
|
|
|
|||
|
|
@ -150,8 +150,9 @@ func DisconnectContainerFromNetwork(
|
|||
return waitContainerRouteAbsent(pool, containerID, network, DockerOpMaxElapsedTime)
|
||||
}
|
||||
|
||||
// ReconnectContainerToNetwork inverts DisconnectContainerFromNetwork
|
||||
// and waits until libnetwork has wired up a fresh IPv4 address.
|
||||
// ReconnectContainerToNetwork is the inverse of
|
||||
// [DisconnectContainerFromNetwork] — re-attaches the container to the
|
||||
// network so traffic can flow again.
|
||||
func ReconnectContainerToNetwork(
|
||||
pool *dockertest.Pool,
|
||||
network *dockertest.Network,
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ func (dsic *DERPServerInContainer) buildEntrypoint(derperArgs string) []string {
|
|||
return []string{"/bin/sh", "-c", strings.Join(commands, " ; ")}
|
||||
}
|
||||
|
||||
// New returns a new TailscaleInContainer instance.
|
||||
// New returns a new [tsic.TailscaleInContainer] instance.
|
||||
func New(
|
||||
pool *dockertest.Pool,
|
||||
version string,
|
||||
|
|
@ -179,7 +179,7 @@ func New(
|
|||
}
|
||||
|
||||
// Install the CA cert so the DERP server trusts its own certificate
|
||||
// and any headscale CA certs passed via WithCACert.
|
||||
// and any headscale CA certs passed via [WithCACert].
|
||||
dsic.caCerts = append(dsic.caCerts, tlsCACert)
|
||||
|
||||
for _, opt := range opts {
|
||||
|
|
@ -319,7 +319,7 @@ func (t *DERPServerInContainer) Version() string {
|
|||
return t.version
|
||||
}
|
||||
|
||||
// ID returns the Docker container ID of the DERPServerInContainer
|
||||
// ID returns the Docker container ID of the [DERPServerInContainer]
|
||||
// instance.
|
||||
func (t *DERPServerInContainer) ID() string {
|
||||
return t.container.Container.ID
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ func derpServerScenario(
|
|||
t.Logf("Run 1: %d successful pings out of %d", success, len(allClients)*len(allHostnames))
|
||||
|
||||
// Let the DERP updater run a couple of times to ensure it does not
|
||||
// break the DERPMap. The updater runs on a 10s interval by default.
|
||||
// break the [tailcfg.DERPMap]. The updater runs on a 10s interval by default.
|
||||
//nolint:forbidigo // Intentional delay: must wait for DERP updater to run multiple times (interval-based)
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
|
|
|
|||
|
|
@ -767,8 +767,8 @@ func TestUpdateHostnameFromClient(t *testing.T) {
|
|||
// Pre-rewrite these were rejected by ApplyHostnameFromHostInfo with
|
||||
// "invalid characters" and the node was stuck on an invalid-<rand>
|
||||
// GivenName with the HostName update dropped. The assertions below
|
||||
// verify both raw preservation (node.Name) and SaaS-matching sanitisation
|
||||
// (node.GivenName) for each awkward input.
|
||||
// verify both raw preservation ([v1.Node.Name]) and SaaS-matching sanitisation
|
||||
// ([v1.Node.GivenName]) for each awkward input.
|
||||
hostnames := map[string]string{
|
||||
"1": "Joe's Mac mini",
|
||||
"2": "Test@Host",
|
||||
|
|
@ -813,7 +813,7 @@ func TestUpdateHostnameFromClient(t *testing.T) {
|
|||
requireNoErrSync(t, err)
|
||||
|
||||
// Wait for nodestore batch processing to complete
|
||||
// NodeStore batching timeout is 500ms, so we wait up to 1 second
|
||||
// [state.NodeStore] batching timeout is 500ms, so we wait up to 1 second
|
||||
var nodes []*v1.Node
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
err := executeAndUnmarshal(
|
||||
|
|
@ -834,7 +834,7 @@ func TestUpdateHostnameFromClient(t *testing.T) {
|
|||
hostname := hostnames[strconv.FormatUint(node.GetId(), 10)]
|
||||
assert.Equal(ct, hostname, node.GetName(), "Node name should match hostname")
|
||||
|
||||
// GivenName is sanitised via dnsname.SanitizeHostname (SaaS algorithm).
|
||||
// GivenName is sanitised via [dnsname.SanitizeHostname] (SaaS algorithm).
|
||||
assert.Equal(ct, dnsname.SanitizeHostname(hostname), node.GetGivenName(),
|
||||
"Given name should match SaaS hostname-sanitisation rules")
|
||||
}
|
||||
|
|
@ -912,7 +912,7 @@ func TestUpdateHostnameFromClient(t *testing.T) {
|
|||
requireNoErrSync(t, err)
|
||||
|
||||
// Wait for nodestore batch processing to complete
|
||||
// NodeStore batching timeout is 500ms, so we wait up to 1 second
|
||||
// [state.NodeStore] batching timeout is 500ms, so we wait up to 1 second
|
||||
assert.Eventually(t, func() bool {
|
||||
err = executeAndUnmarshal(
|
||||
headscale,
|
||||
|
|
@ -987,7 +987,7 @@ func TestExpireNode(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// TODO(kradalby): This is Headscale specific and would not play nicely
|
||||
// with other implementations of the ControlServer interface
|
||||
// with other implementations of the [ControlServer] interface
|
||||
result, err := headscale.Execute([]string{
|
||||
"headscale", "nodes", "expire", "--identifier", "1", "--output", "json",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ import (
|
|||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
// hasCapMatchInPacketFilter checks if any Match entry in the packet
|
||||
// filter contains a CapMatch with the given capability name.
|
||||
// hasCapMatchInPacketFilter checks if any [filter.Match] entry in the packet
|
||||
// filter contains a [filter.CapMatch] with the given capability name.
|
||||
func hasCapMatchInPacketFilter(pf []filter.Match, peerCap tailcfg.PeerCapability) bool {
|
||||
for _, m := range pf {
|
||||
for _, cm := range m.Caps {
|
||||
|
|
@ -31,7 +31,7 @@ func hasCapMatchInPacketFilter(pf []filter.Match, peerCap tailcfg.PeerCapability
|
|||
return false
|
||||
}
|
||||
|
||||
// hasCapMatchForIP checks if any CapMatch with the given capability
|
||||
// hasCapMatchForIP checks if any [filter.CapMatch] with the given capability
|
||||
// has a Dst prefix that contains the given IP. This validates that
|
||||
// the cap is directed at the correct node, not just present.
|
||||
func hasCapMatchForIP(pf []filter.Match, peerCap tailcfg.PeerCapability, ip netip.Addr) bool {
|
||||
|
|
@ -47,7 +47,7 @@ func hasCapMatchForIP(pf []filter.Match, peerCap tailcfg.PeerCapability, ip neti
|
|||
}
|
||||
|
||||
// parsePeerRelay parses a PeerRelay string of the form "ip:port:vni:N"
|
||||
// and returns the address and VNI. Returns zero values on parse failure.
|
||||
// and returns the [netip.AddrPort] and VNI. Returns zero values on parse failure.
|
||||
func parsePeerRelay(pr string) (netip.AddrPort, string, bool) {
|
||||
// Format: "172.18.0.4:58738:vni:1"
|
||||
// Split into: host part "172.18.0.4:58738" and vni part "vni:1"
|
||||
|
|
|
|||
|
|
@ -114,7 +114,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 [TailscaleClient]s for validation purposes.
|
||||
func collectExpectedNodeIDs(t *testing.T, clients []TailscaleClient) []types.NodeID {
|
||||
t.Helper()
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ func collectExpectedNodeIDs(t *testing.T, clients []TailscaleClient) []types.Nod
|
|||
}
|
||||
|
||||
// validateInitialConnection performs comprehensive validation after initial client login.
|
||||
// Validates that all nodes are online and have proper NetInfo/DERP configuration,
|
||||
// Validates that all nodes are online and have proper [tailcfg.NetInfo]/DERP configuration,
|
||||
// essential for ensuring successful initial connection state in relogin tests.
|
||||
func validateInitialConnection(t *testing.T, headscale ControlServer, expectedNodes []types.NodeID) {
|
||||
t.Helper()
|
||||
|
|
@ -150,7 +150,7 @@ func validateLogoutComplete(t *testing.T, headscale ControlServer, expectedNodes
|
|||
}
|
||||
|
||||
// validateReloginComplete performs comprehensive validation after client relogin.
|
||||
// Validates that all nodes are back online with proper NetInfo/DERP configuration,
|
||||
// Validates that all nodes are back online with proper [tailcfg.NetInfo]/DERP configuration,
|
||||
// ensuring successful relogin state restoration in integration tests.
|
||||
func validateReloginComplete(t *testing.T, headscale ControlServer, expectedNodes []types.NodeID) {
|
||||
t.Helper()
|
||||
|
|
@ -256,7 +256,7 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
|
|||
}
|
||||
}
|
||||
|
||||
// Check map responses using buildExpectedOnlineMap
|
||||
// Check map responses using [integrationutil.BuildExpectedOnlineMap]
|
||||
onlineFromMaps := make(map[types.NodeID]bool)
|
||||
onlineMap := integrationutil.BuildExpectedOnlineMap(mapResponses)
|
||||
|
||||
|
|
@ -475,9 +475,9 @@ func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expec
|
|||
t.Logf("All stages completed: nodes are fully offline across all systems")
|
||||
}
|
||||
|
||||
// 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.
|
||||
// requireAllClientsNetInfoAndDERP validates that all nodes have [tailcfg.NetInfo] in the database
|
||||
// and a valid DERP server based on the [tailcfg.NetInfo]. This function follows the pattern of
|
||||
// [requireAllClientsOnline] by using [hsic.HeadscaleInContainer.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) {
|
||||
|
|
@ -510,7 +510,7 @@ func requireAllClientsNetInfoAndDERP(t *testing.T, headscale ControlServer, expe
|
|||
continue
|
||||
}
|
||||
|
||||
// Validate that the node has Hostinfo
|
||||
// Validate that the node has [tailcfg.Hostinfo]
|
||||
assert.NotNil(c, node.Hostinfo, "Node %d (%s) should have Hostinfo for NetInfo validation", nodeID, node.Hostname)
|
||||
|
||||
if node.Hostinfo == nil {
|
||||
|
|
@ -518,7 +518,7 @@ func requireAllClientsNetInfoAndDERP(t *testing.T, headscale ControlServer, expe
|
|||
continue
|
||||
}
|
||||
|
||||
// Validate that the node has NetInfo
|
||||
// Validate that the node has [tailcfg.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 {
|
||||
|
|
@ -553,7 +553,7 @@ func assertLastSeenSetWithCollect(c *assert.CollectT, node *v1.Node) {
|
|||
}
|
||||
|
||||
// assertCurlSuccessWithCollect asserts that a curl request succeeds with
|
||||
// non-empty content. For use inside EventuallyWithT blocks.
|
||||
// non-empty content. For use inside [assert.EventuallyWithT] blocks.
|
||||
func assertCurlSuccessWithCollect(c *assert.CollectT, client TailscaleClient, url, msg string) {
|
||||
result, err := client.Curl(url)
|
||||
assert.NoError(c, err, msg) //nolint:testifylint // CollectT requires assert, not require
|
||||
|
|
@ -562,7 +562,7 @@ func assertCurlSuccessWithCollect(c *assert.CollectT, client TailscaleClient, ur
|
|||
|
||||
// assertCurlDockerHostname curls url and asserts the body is the
|
||||
// 13-byte Docker auto-generated container hostname (12 hex chars +
|
||||
// trailing newline from /etc/hostname). For use inside EventuallyWithT.
|
||||
// trailing newline from /etc/hostname). For use inside [assert.EventuallyWithT].
|
||||
func assertCurlDockerHostname(c *assert.CollectT, client TailscaleClient, url, msg string) {
|
||||
const dockerHostnameLen = 13
|
||||
|
||||
|
|
@ -572,7 +572,7 @@ func assertCurlDockerHostname(c *assert.CollectT, client TailscaleClient, url, m
|
|||
}
|
||||
|
||||
// snapshotClientFilters snapshots each client's current netmap
|
||||
// PacketFilter keyed by hostname. Pair with waitForClientFilterChange.
|
||||
// PacketFilter keyed by hostname. Pair with [waitForClientFilterChange].
|
||||
func snapshotClientFilters(t *testing.T, clients []TailscaleClient) map[string][]filter.Match {
|
||||
t.Helper()
|
||||
|
||||
|
|
@ -611,9 +611,9 @@ func waitForClientFilterChange(t *testing.T, clients []TailscaleClient, baseline
|
|||
}
|
||||
|
||||
// assertCurlFailWithCollect asserts that a curl request fails. Uses
|
||||
// CurlFailFast internally for aggressive timeouts, avoiding wasted
|
||||
// [tsic.TailscaleInContainer.CurlFailFast] internally for aggressive timeouts, avoiding wasted
|
||||
// time on retries when we expect the connection to be blocked.
|
||||
// For use inside EventuallyWithT blocks.
|
||||
// For use inside [assert.EventuallyWithT] blocks.
|
||||
func assertCurlFailWithCollect(c *assert.CollectT, client TailscaleClient, url, msg string) {
|
||||
_, err := client.CurlFailFast(url)
|
||||
assert.Error(c, err, msg)
|
||||
|
|
@ -635,7 +635,7 @@ func assertTailscaleNodesLogout(t assert.TestingT, clients []TailscaleClient) {
|
|||
}
|
||||
|
||||
// assertPingAll verifies that every client can ping every address.
|
||||
// The entire ping matrix is retried via EventuallyWithT to handle
|
||||
// The entire ping matrix is retried via [assert.EventuallyWithT] to handle
|
||||
// transient failures on slow CI runners. The timeout scales with
|
||||
// the number of pings since they run serially and each can take
|
||||
// up to ~2s on CI (docker exec overhead + ping timeout).
|
||||
|
|
@ -660,9 +660,9 @@ func assertPingAll(t *testing.T, clients []TailscaleClient, addrs []string, opts
|
|||
}
|
||||
|
||||
// assertPingAllWithCollect pings every address from every client and
|
||||
// collects failures on the provided CollectT. Pings run serially to
|
||||
// collects failures on the provided [assert.CollectT]. Pings run serially to
|
||||
// avoid overloading the Docker daemon on resource-constrained CI
|
||||
// runners. For use inside EventuallyWithT blocks when the caller
|
||||
// runners. For use inside [assert.EventuallyWithT] blocks when the caller
|
||||
// needs custom timeout or retry control.
|
||||
func assertPingAllWithCollect(c *assert.CollectT, clients []TailscaleClient, addrs []string, opts ...tsic.PingOption) {
|
||||
for _, client := range clients {
|
||||
|
|
@ -897,7 +897,7 @@ func assertValidNetcheck(t *testing.T, client TailscaleClient) {
|
|||
|
||||
// assertCommandOutputContains executes a command with exponential backoff retry until the output
|
||||
// contains the expected string or timeout is reached (10 seconds).
|
||||
// This implements eventual consistency patterns and should be used instead of time.Sleep
|
||||
// This implements eventual consistency patterns and should be used instead of [time.Sleep]
|
||||
// before executing commands that depend on network state propagation.
|
||||
//
|
||||
// Timeout: 10 seconds with exponential backoff
|
||||
|
|
@ -982,42 +982,43 @@ func countMatchingLines(in io.Reader, predicate func(string) bool) (int, error)
|
|||
|
||||
// wildcard returns a wildcard alias (*) for use in policy v2 configurations.
|
||||
// Provides a convenient helper for creating permissive policy rules.
|
||||
// Returns [policyv2.Wildcard].
|
||||
func wildcard() policyv2.Alias {
|
||||
return policyv2.Wildcard
|
||||
}
|
||||
|
||||
// usernamep returns a pointer to a Username as an Alias for policy v2 configurations.
|
||||
// usernamep returns a pointer to a [policyv2.Username] as an [policyv2.Alias] for policy v2 configurations.
|
||||
// Used in ACL rules to reference specific users in network access policies.
|
||||
func usernamep(name string) policyv2.Alias {
|
||||
return new(policyv2.Username(name))
|
||||
}
|
||||
|
||||
// hostp returns a pointer to a Host as an Alias for policy v2 configurations.
|
||||
// hostp returns a pointer to a [policyv2.Host] as an [policyv2.Alias] for policy v2 configurations.
|
||||
// Used in ACL rules to reference specific hosts in network access policies.
|
||||
func hostp(name string) policyv2.Alias {
|
||||
return new(policyv2.Host(name))
|
||||
}
|
||||
|
||||
// groupp returns a pointer to a Group as an Alias for policy v2 configurations.
|
||||
// groupp returns a pointer to a [policyv2.Group] as an [policyv2.Alias] for policy v2 configurations.
|
||||
// Used in ACL rules to reference user groups in network access policies.
|
||||
func groupp(name string) policyv2.Alias {
|
||||
return new(policyv2.Group(name))
|
||||
}
|
||||
|
||||
// tagp returns a pointer to a Tag as an Alias for policy v2 configurations.
|
||||
// tagp returns a pointer to a [policyv2.Tag] as an [policyv2.Alias] for policy v2 configurations.
|
||||
// Used in ACL rules to reference node tags in network access policies.
|
||||
func tagp(name string) policyv2.Alias {
|
||||
return new(policyv2.Tag(name))
|
||||
}
|
||||
|
||||
// prefixp returns a pointer to a Prefix from a CIDR string for policy v2 configurations.
|
||||
// prefixp returns a pointer to a [policyv2.Prefix] from a CIDR string for policy v2 configurations.
|
||||
// Converts CIDR notation to policy prefix format for network range specifications.
|
||||
func prefixp(cidr string) policyv2.Alias {
|
||||
p := policyv2.Prefix(netip.MustParsePrefix(cidr))
|
||||
return &p
|
||||
}
|
||||
|
||||
// aliasWithPorts creates an AliasWithPorts structure from an alias and port ranges.
|
||||
// aliasWithPorts creates an [policyv2.AliasWithPorts] structure from an alias and port ranges.
|
||||
// Combines network targets with specific port restrictions for fine-grained
|
||||
// access control in policy v2 configurations.
|
||||
func aliasWithPorts(alias policyv2.Alias, ports ...tailcfg.PortRange) policyv2.AliasWithPorts {
|
||||
|
|
@ -1027,13 +1028,13 @@ func aliasWithPorts(alias policyv2.Alias, ports ...tailcfg.PortRange) policyv2.A
|
|||
}
|
||||
}
|
||||
|
||||
// usernameOwner returns a Username as an Owner for use in TagOwners policies.
|
||||
// usernameOwner returns a [policyv2.Username] as an [policyv2.Owner] for use in [policyv2.TagOwners] policies.
|
||||
// Specifies which users can assign and manage specific tags in ACL configurations.
|
||||
func usernameOwner(name string) policyv2.Owner {
|
||||
return new(policyv2.Username(name))
|
||||
}
|
||||
|
||||
// groupOwner returns a Group as an Owner for use in TagOwners policies.
|
||||
// groupOwner returns a [policyv2.Group] as an [policyv2.Owner] for use in [policyv2.TagOwners] policies.
|
||||
// Specifies which groups can assign and manage specific tags in ACL configurations.
|
||||
//
|
||||
//nolint:unused
|
||||
|
|
@ -1041,25 +1042,25 @@ func groupOwner(name string) policyv2.Owner {
|
|||
return new(policyv2.Group(name))
|
||||
}
|
||||
|
||||
// usernameApprover returns a Username as an AutoApprover for subnet route policies.
|
||||
// usernameApprover returns a [policyv2.Username] as an [policyv2.AutoApprover] for subnet route policies.
|
||||
// Specifies which users can automatically approve subnet route advertisements.
|
||||
func usernameApprover(name string) policyv2.AutoApprover {
|
||||
return new(policyv2.Username(name))
|
||||
}
|
||||
|
||||
// groupApprover returns a Group as an AutoApprover for subnet route policies.
|
||||
// groupApprover returns a [policyv2.Group] as an [policyv2.AutoApprover] for subnet route policies.
|
||||
// Specifies which groups can automatically approve subnet route advertisements.
|
||||
func groupApprover(name string) policyv2.AutoApprover {
|
||||
return new(policyv2.Group(name))
|
||||
}
|
||||
|
||||
// tagApprover returns a Tag as an AutoApprover for subnet route policies.
|
||||
// tagApprover returns a [policyv2.Tag] as an [policyv2.AutoApprover] for subnet route policies.
|
||||
// Specifies which tagged nodes can automatically approve subnet route advertisements.
|
||||
func tagApprover(name string) policyv2.AutoApprover {
|
||||
return new(policyv2.Tag(name))
|
||||
}
|
||||
|
||||
// oidcMockUser creates a MockUser for OIDC authentication testing.
|
||||
// oidcMockUser creates a [mockoidc.MockUser] for OIDC authentication testing.
|
||||
// Generates consistent test user data with configurable email verification status
|
||||
// for validating OIDC integration flows in headscale authentication tests.
|
||||
func oidcMockUser(username string, emailVerified bool) mockoidc.MockUser {
|
||||
|
|
@ -1193,7 +1194,7 @@ func (s *Scenario) AddAndLoginClient(
|
|||
return newClient, nil
|
||||
}
|
||||
|
||||
// MustAddAndLoginClient is like AddAndLoginClient but fails the test on error.
|
||||
// MustAddAndLoginClient is like [Scenario.AddAndLoginClient] but fails the test on error.
|
||||
func (s *Scenario) MustAddAndLoginClient(
|
||||
t *testing.T,
|
||||
username string,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ var (
|
|||
StatusReadyTimeout = ScaledTimeout(30 * time.Second)
|
||||
)
|
||||
|
||||
// Polling intervals for EventuallyWithT.
|
||||
// Polling intervals for [assert.EventuallyWithT].
|
||||
const (
|
||||
// FastPoll: in-process reads (HA state, route table snapshots).
|
||||
FastPoll = 200 * time.Millisecond
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ func TestEnablingRoutes(t *testing.T) {
|
|||
requireNoErrSync(t, err)
|
||||
|
||||
var nodes []*v1.Node
|
||||
// Wait for route advertisements to propagate to NodeStore
|
||||
// Wait for route advertisements to propagate to [state.NodeStore]
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ func TestEnablingRoutes(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Wait for route approvals to propagate to NodeStore
|
||||
// Wait for route approvals to propagate to [state.NodeStore]
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
|
|
@ -361,7 +361,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
}, propagationTime, 200*time.Millisecond, "Verifying no routes are active before approval")
|
||||
}
|
||||
|
||||
// Declare variables that will be used across multiple EventuallyWithT blocks
|
||||
// Declare variables that will be used across multiple [assert.EventuallyWithT] blocks
|
||||
var (
|
||||
srs1, srs2, srs3 *ipnstate.Status
|
||||
clientStatus *ipnstate.Status
|
||||
|
|
@ -956,7 +956,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Wait for nodestore batch processing to complete and online status to be updated
|
||||
// NodeStore batching timeout is 500ms, so we wait up to 10 seconds for all routers to be online
|
||||
// [state.NodeStore] batching timeout is 500ms, so we wait up to 10 seconds for all routers to be online
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
clientStatus, err = client.Status()
|
||||
assert.NoError(c, err)
|
||||
|
|
@ -1033,7 +1033,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
_, err = headscale.ApproveRoutes(MustFindNode(subRouter3.Hostname(), nodes).GetId(), []netip.Prefix{})
|
||||
|
||||
// Wait for nodestore batch processing and route state changes to complete
|
||||
// NodeStore batching timeout is 500ms, so we wait up to 10 seconds for route failover
|
||||
// [state.NodeStore] batching timeout is 500ms, so we wait up to 10 seconds for route failover
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err = headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
|
|
@ -1119,7 +1119,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
|
|||
_, err = headscale.ApproveRoutes(MustFindNode(subRouter1.Hostname(), nodes).GetId(), []netip.Prefix{})
|
||||
|
||||
// Wait for nodestore batch processing and route state changes to complete
|
||||
// NodeStore batching timeout is 500ms, so we wait up to 10 seconds for route failover
|
||||
// [state.NodeStore] batching timeout is 500ms, so we wait up to 10 seconds for route failover
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err = headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
|
|
@ -1453,7 +1453,7 @@ func TestSubnetRouteACL(t *testing.T) {
|
|||
assert.NotNil(c, routeNode, "could not find node that should have route")
|
||||
assert.NotNil(c, otherNode, "could not find node that should not have route")
|
||||
|
||||
// After NodeStore fix: routes are properly tracked in route manager
|
||||
// After [state.NodeStore] fix: routes are properly tracked in route manager
|
||||
// This test uses a policy with NO auto-approvers, so routes should be:
|
||||
// announced=1, approved=0, subnet=0 (routes announced but not approved)
|
||||
requireNodeRouteCountWithCollect(c, routeNode, 1, 0, 0)
|
||||
|
|
@ -1705,7 +1705,7 @@ func TestEnablingExitRoutes(t *testing.T) {
|
|||
// (verified against a live tailnet on 2026-04-28; see captures
|
||||
// routes-b17/b18 in tscap). The bug was that headscale stripped
|
||||
// autogroup:internet rules from both the client packet filter AND the
|
||||
// matcher source used by Node.CanAccess, breaking exit-node visibility.
|
||||
// matcher source used by [types.Node.CanAccess], breaking exit-node visibility.
|
||||
func TestExitRoutesWithAutogroupInternetACL(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
|
|
@ -1750,9 +1750,9 @@ func TestExitRoutesWithAutogroupInternetACL(t *testing.T) {
|
|||
requireNoErrGetHeadscale(t, err)
|
||||
|
||||
// The autogroup:internet ACL grants no peer visibility until the
|
||||
// exit routes are approved (Node.IsExitNode() flips on approval),
|
||||
// exit routes are approved ([types.Node.IsExitNode] flips on approval),
|
||||
// so the standard WaitForTailscaleSync wait would deadlock here —
|
||||
// the post-approval EventuallyWithT block below covers the peer
|
||||
// the post-approval [assert.EventuallyWithT] block below covers the peer
|
||||
// state we actually care about.
|
||||
var nodes []*v1.Node
|
||||
|
||||
|
|
@ -1792,7 +1792,7 @@ func TestExitRoutesWithAutogroupInternetACL(t *testing.T) {
|
|||
|
||||
// The end-to-end UX assertion: every client must see the OTHER
|
||||
// node as a peer carrying both default-route prefixes in
|
||||
// AllowedIPs. Tailscale derives PeerStatus.ExitNodeOption from
|
||||
// AllowedIPs. Tailscale derives [ipnstate.PeerStatus.ExitNodeOption] from
|
||||
// those AllowedIPs, which is what `tailscale exit-node list`
|
||||
// reads (see tailscale.com/ipn/ipnlocal/local.go).
|
||||
for _, client := range allClients {
|
||||
|
|
@ -1903,7 +1903,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
|
|||
require.NoErrorf(t, err, "failed to advertise route: %s", err)
|
||||
|
||||
var nodes []*v1.Node
|
||||
// Wait for route advertisements to propagate to NodeStore
|
||||
// Wait for route advertisements to propagate to [state.NodeStore]
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
var err error
|
||||
|
||||
|
|
@ -2182,7 +2182,7 @@ func MustFindNode(hostname string, nodes []*v1.Node) *v1.Node {
|
|||
func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
// Timeout for EventuallyWithT assertions.
|
||||
// Timeout for [assert.EventuallyWithT] assertions.
|
||||
// Set generously to account for CI infrastructure variability.
|
||||
assertTimeout := integrationutil.ScaledTimeout(60 * time.Second)
|
||||
|
||||
|
|
@ -2430,7 +2430,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
name := fmt.Sprintf("%s-advertiseduringup-%t-pol-%s", tt.name, advertiseDuringUp, polMode)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Create a deep copy of the policy to avoid mutating the shared test case.
|
||||
// Each subtest modifies AutoApprovers.Routes (add then delete), so we need
|
||||
// Each subtest modifies [policyv2.AutoApproverPolicy.Routes] (add then delete), so we need
|
||||
// an isolated copy to prevent state leakage between sequential test runs.
|
||||
pol := &policyv2.Policy{
|
||||
ACLs: slices.Clone(tt.pol.ACLs),
|
||||
|
|
@ -3017,14 +3017,14 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// assertTracerouteViaIPWithCollect is a version of assertTracerouteViaIP that works with assert.CollectT.
|
||||
// assertTracerouteViaIPWithCollect is a version of [assertTracerouteViaIP] that works with [assert.CollectT].
|
||||
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) //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
|
||||
// Since we're inside [assert.EventuallyWithT], we can't use [require.Greater] with t
|
||||
// but [assert.NotEmpty] above ensures len(tr.Route) > 0
|
||||
if len(tr.Route) > 0 {
|
||||
assert.Equal(c, tr.Route[0].IP.String(), ip.String())
|
||||
}
|
||||
|
|
@ -3219,7 +3219,7 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
|
|||
requireNoErrSync(t, err)
|
||||
|
||||
var routerNode, nodeNode *v1.Node
|
||||
// Wait for route advertisements to propagate to NodeStore
|
||||
// Wait for route advertisements to propagate to [state.NodeStore]
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
// List nodes and verify the router has 3 available routes
|
||||
nodes, err := headscale.NodesByUser()
|
||||
|
|
@ -3858,7 +3858,7 @@ func TestHASubnetRouterPingFailover(t *testing.T) {
|
|||
//
|
||||
// Two assertion sets split the failure surface:
|
||||
// - R1: server-side primary route table restores after reconnect.
|
||||
// If R1 fails, the bug is in state.Connect / primaryRoutes.
|
||||
// If R1 fails, the bug is in [state.State.Connect] / primaryRoutes.
|
||||
// - R2: client's view shows r2 online with the route in PrimaryRoutes.
|
||||
// If R1 passes and R2 fails, the bug is in change broadcast /
|
||||
// mapBatcher / multiChannelNodeConn.
|
||||
|
|
|
|||
|
|
@ -459,7 +459,7 @@ func doSSHWithRetryAsUser(
|
|||
)
|
||||
|
||||
if retry {
|
||||
// Use assert.EventuallyWithT to retry SSH connections for success cases
|
||||
// Use [assert.EventuallyWithT] to retry SSH connections for success cases
|
||||
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
result, stderr, err = client.Execute(command)
|
||||
|
||||
|
|
@ -706,7 +706,7 @@ func findSSHCheckAuthID(t *testing.T, headscale ControlServer) string {
|
|||
return authID
|
||||
}
|
||||
|
||||
// sshCheckPolicy returns a policy with SSH "check" mode for group:integration-test
|
||||
// sshCheckPolicy returns a [policyv2.Policy] with SSH "check" mode for group:integration-test
|
||||
// targeting autogroup:member and autogroup:tagged destinations.
|
||||
func sshCheckPolicy() *policyv2.Policy {
|
||||
return &policyv2.Policy{
|
||||
|
|
@ -739,7 +739,7 @@ func sshCheckPolicy() *policyv2.Policy {
|
|||
}
|
||||
}
|
||||
|
||||
// sshCheckPolicyWithPeriod returns a policy with SSH "check" mode and a
|
||||
// sshCheckPolicyWithPeriod returns a [policyv2.Policy] with SSH "check" mode and a
|
||||
// specified checkPeriod for session duration.
|
||||
func sshCheckPolicyWithPeriod(period time.Duration) *policyv2.Policy {
|
||||
return &policyv2.Policy{
|
||||
|
|
|
|||
|
|
@ -1587,7 +1587,7 @@ func TestTagsUserLoginAddTagViaCLIReauth(t *testing.T) {
|
|||
_, stderr, err := client.Execute(command)
|
||||
t.Logf("CLI result: err=%v, stderr=%s", err, stderr)
|
||||
|
||||
// Check final state - EventuallyWithT handles waiting for propagation
|
||||
// Check final state - [assert.EventuallyWithT] handles waiting for propagation
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err := headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
|
|
@ -1678,7 +1678,7 @@ func TestTagsUserLoginRemoveTagViaCLIReauth(t *testing.T) {
|
|||
_, stderr, err := client.Execute(command)
|
||||
t.Logf("CLI result: err=%v, stderr=%s", err, stderr)
|
||||
|
||||
// Check final state - EventuallyWithT handles waiting for propagation
|
||||
// Check final state - [assert.EventuallyWithT] handles waiting for propagation
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err := headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
|
|
@ -3093,8 +3093,8 @@ func TestTagsAuthKeyWithoutUserRejectsAdvertisedTags(t *testing.T) {
|
|||
// TestTagsAuthKeyConvertToUserViaCLIRegister reproduces the panic from
|
||||
// issue #3038: register a node with a tags-only preauthkey (no user), then
|
||||
// convert it to a user-owned node via "headscale auth register --auth-id <id> --user <user>".
|
||||
// The crash happens in the mapper's generateUserProfiles when node.User is nil
|
||||
// after the tag→user conversion in processReauthTags.
|
||||
// The crash happens in the mapper's generateUserProfiles when [types.Node.User] is nil
|
||||
// after the tag→user conversion in [State.processReauthTags].
|
||||
//
|
||||
// The key detail is using a tags-only PreAuthKey (User: nil). When created under
|
||||
// a user, the node inherits User from the PreAuthKey and the bug is masked.
|
||||
|
|
@ -3124,8 +3124,8 @@ func TestTagsAuthKeyConvertToUserViaCLIRegister(t *testing.T) {
|
|||
requireNoErrGetHeadscale(t, err)
|
||||
|
||||
// Step 1: Create a tags-only preauthkey WITHOUT a user.
|
||||
// This is the critical detail: when PreAuthKey.UserID is nil, the node
|
||||
// enters the NodeStore with node.User == nil. The processReauthTags
|
||||
// This is the critical detail: when [types.PreAuthKey.UserID] is nil, the node
|
||||
// enters the [state.NodeStore] with [types.Node.User] == nil. The [state.State.processReauthTags]
|
||||
// conversion then sets UserID but not User, leaving it nil for the mapper.
|
||||
authKey, err := scenario.CreatePreAuthKeyWithOptions(hsic.AuthKeyOptions{
|
||||
User: nil,
|
||||
|
|
@ -3186,8 +3186,8 @@ func TestTagsAuthKeyConvertToUserViaCLIRegister(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Step 4: Verify node is now user-owned and the mapper didn't panic.
|
||||
// The panic would occur when the mapper builds the MapResponse and calls
|
||||
// node.Owner().Model().ID with a nil User pointer.
|
||||
// The panic would occur when the mapper builds the [tailcfg.MapResponse] and calls
|
||||
// [types.Node.Owner].Model().ID with a nil User pointer.
|
||||
// ShutdownAssertNoPanics in the defer catches any panics in headscale logs.
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err := headscale.ListNodes()
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ func WithCACert(cert []byte) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithNetwork sets the Docker container network.
|
||||
// WithNetwork sets the Docker [dockertest.Network].
|
||||
func WithNetwork(network *dockertest.Network) Option {
|
||||
return func(t *TailscaleRustInContainer) {
|
||||
t.network = network
|
||||
|
|
@ -146,7 +146,7 @@ func (t *TailscaleRustInContainer) buildEntrypoint() []string {
|
|||
return []string{"/bin/sh", "-c", strings.Join(commands, " ; ")}
|
||||
}
|
||||
|
||||
// New creates and starts a new TailscaleRustInContainer instance.
|
||||
// New creates and starts a new [TailscaleRustInContainer] instance.
|
||||
func New(
|
||||
pool *dockertest.Pool,
|
||||
opts ...Option,
|
||||
|
|
@ -280,7 +280,7 @@ func New(
|
|||
return t, nil
|
||||
}
|
||||
|
||||
// Hostname returns the hostname of the TailscaleRustInContainer instance.
|
||||
// Hostname returns the hostname of the [TailscaleRustInContainer] instance.
|
||||
func (t *TailscaleRustInContainer) Hostname() string {
|
||||
return t.hostname
|
||||
}
|
||||
|
|
@ -310,7 +310,7 @@ func (t *TailscaleRustInContainer) SaveLog(path string) (string, string, error)
|
|||
}
|
||||
|
||||
// WriteLogs writes the current stdout/stderr log of the container to
|
||||
// the given io.Writers.
|
||||
// the given [io.Writer]s.
|
||||
func (t *TailscaleRustInContainer) WriteLogs(stdout, stderr io.Writer) error {
|
||||
return dockertestutil.WriteLog(t.pool, t.container, stdout, stderr)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ func TestTailscaleRustAxum(t *testing.T) {
|
|||
|
||||
// Fire several more POSTs and verify the counter advances.
|
||||
// The axum handler returns {"count": N} where N is the pre-increment value.
|
||||
// After the initial EventuallyWithT loop we don't know the exact counter,
|
||||
// After the initial [assert.EventuallyWithT] loop we don't know the exact counter,
|
||||
// but two back-to-back POSTs should return consecutive values.
|
||||
t.Log("Verifying counter increments across multiple requests...")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue