integration

package
v0.29.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 1, 2026 License: BSD-3-Clause Imports: 54 Imported by: 0

README

Integration testing

Headscale's integration tests start a real Headscale server and run scenarios against real Tailscale clients across supported versions, all inside Docker. They are the safety net that keeps us honest about Tailscale protocol compatibility.

This file documents how to write integration tests. For how to run them, see ../cmd/hi/README.md.

Tests live in files ending with _test.go; the framework lives in the rest of this directory (scenario.go, tailscale.go, helpers, and the hsic/, tsic/, dockertestutil/ packages).

Running tests

For local runs, use cmd/hi:

go run ./cmd/hi doctor
go run ./cmd/hi run "TestPingAllByIP"

Alternatively, act runs the GitHub Actions workflow locally:

act pull_request -W .github/workflows/test-integration.yaml

Each test runs as a separate workflow on GitHub Actions. To add a new test, run go generate inside ../cmd/gh-action-integration-generator/ and commit the generated workflow file.

Framework overview

The integration framework has four layers:

  • scenario.goScenario orchestrates a test environment: a Headscale server, one or more users, and a collection of Tailscale clients. NewScenario(spec) returns a ready-to-use environment.
  • hsic/ — "Headscale Integration Container": wraps a Headscale server in Docker. Options for config, DB backend, DERP, OIDC, etc.
  • tsic/ — "Tailscale Integration Container": wraps a single Tailscale client. Options for version, hostname, auth method, etc.
  • dockertestutil/ — low-level Docker helpers (networks, container lifecycle, IsRunningInContainer() detection).

Tests compose these pieces via ScenarioSpec and CreateHeadscaleEnv rather than calling Docker directly.

Required scaffolding

IntegrationSkip(t)

Every integration test function must call IntegrationSkip(t) as its first statement. Without it, the test runs in the wrong environment and fails with confusing errors.

func TestMyScenario(t *testing.T) {
    IntegrationSkip(t)
    // ... rest of the test
}

IntegrationSkip is defined in integration/scenario_test.go:15 and:

  • skips the test when not running inside the Docker test container (dockertestutil.IsRunningInContainer()),
  • skips when -short is passed to go test.
Scenario setup

The canonical setup creates users, clients, and the Headscale server in one shot:

func TestMyScenario(t *testing.T) {
    IntegrationSkip(t)
    t.Parallel()

    spec := ScenarioSpec{
        NodesPerUser: 2,
        Users:        []string{"alice", "bob"},
    }
    scenario, err := NewScenario(spec)
    require.NoError(t, err)
    defer scenario.ShutdownAssertNoPanics(t)

    err = scenario.CreateHeadscaleEnv(
        []tsic.Option{tsic.WithSSH()},
        hsic.WithTestName("myscenario"),
    )
    require.NoError(t, err)

    allClients, err := scenario.ListTailscaleClients()
    require.NoError(t, err)

    headscale, err := scenario.Headscale()
    require.NoError(t, err)

    // ... assertions
}

Review scenario.go and hsic/options.go / tsic/options.go for the full option set (DERP, OIDC, policy files, DB backend, ACL grants, exit-node config, etc.).

The EventuallyWithT pattern

Integration tests operate on a distributed system with real async propagation: clients advertise state, the server processes it, updates stream to peers. Direct assertions after state changes fail intermittently. Wrap external calls in assert.EventuallyWithT:

assert.EventuallyWithT(t, func(c *assert.CollectT) {
    status, err := client.Status()
    assert.NoError(c, err)
    for _, peerKey := range status.Peers() {
        peerStatus := status.Peer[peerKey]
        requirePeerSubnetRoutesWithCollect(c, peerStatus, expectedRoutes)
    }
}, 10*time.Second, 500*time.Millisecond, "client should see expected routes")
External calls that need wrapping

These read distributed state and may reflect stale data until propagation completes:

  • headscale.ListNodes()
  • client.Status()
  • client.Curl()
  • client.Traceroute()
  • client.Execute() when the command reads state
Blocking operations that must NOT be wrapped

State-mutating commands run exactly once and either succeed or fail immediately — not eventually. Wrapping them in EventuallyWithT hides real failures behind retry.

Use client.MustStatus() when you only need an ID for a blocking call:

// CORRECT — mutation runs once
for _, client := range allClients {
    status := client.MustStatus()
    _, _, err := client.Execute([]string{
        "tailscale", "set",
        "--advertise-routes=" + expectedRoutes[string(status.Self.ID)],
    })
    require.NoErrorf(t, err, "failed to advertise route: %s", err)
}

Typical blocking operations: any tailscale set (routes, exit node, accept-routes, ssh), node registration via the CLI, user creation via gRPC.

The four rules
  1. One external call per EventuallyWithT block. Related assertions on the result of a single call go together in the same block.

    Loop exception: iterating over a collection of clients (or peers) and calling Status() on each inside a single block is allowed — it is the same logical "check all clients" operation. The rule applies to distinct calls like ListNodes() + Status(), which must be split into separate blocks.

  2. Never nest EventuallyWithT calls. A nested retry loop multiplies timing windows and makes failures impossible to diagnose.

  3. Use *WithCollect helper variants inside the block. Regular helpers use require and abort on the first failed assertion, preventing retry.

  4. Always provide a descriptive final message — it appears on failure and is your only clue about what the test was waiting for.

Variable scoping

Variables used across multiple EventuallyWithT blocks must be declared at function scope. Inside the block, assign with =, not :=:= creates a shadow invisible to the outer scope:

var nodes []*v1.Node
var err error
assert.EventuallyWithT(t, func(c *assert.CollectT) {
    nodes, err = headscale.ListNodes()   // = not :=
    assert.NoError(c, err)
    assert.Len(c, nodes, 2)
    requireNodeRouteCountWithCollect(c, nodes[0], 2, 2, 2)
}, 10*time.Second, 500*time.Millisecond, "nodes should have expected routes")

// nodes is usable here because it was declared at function scope
Helper functions

Inside EventuallyWithT blocks, use the *WithCollect variants so assertion failures restart the wait loop instead of failing the test immediately:

  • requirePeerSubnetRoutesWithCollect(c, status, expected)integration/route_test.go:2941
  • requireNodeRouteCountWithCollect(c, node, announced, approved, subnet)integration/route_test.go:2958
  • assertTracerouteViaIPWithCollect(c, traceroute, ip)integration/route_test.go:2898

When you write a new helper to be called inside EventuallyWithT, it must accept *assert.CollectT as its first parameter, not *testing.T.

Identifying nodes by property, not position

The order of headscale.ListNodes() is not stable. Tests that index nodes[0] will break when node ordering changes. Look nodes up by ID, hostname, or tag:

// WRONG — relies on array position
require.Len(t, nodes[0].GetAvailableRoutes(), 1)

// CORRECT — find the node that should have the route
expectedRoutes := map[string]string{"1": "10.33.0.0/16"}
for _, node := range nodes {
    nodeIDStr := fmt.Sprintf("%d", node.GetId())
    if route, shouldHaveRoute := expectedRoutes[nodeIDStr]; shouldHaveRoute {
        assert.Contains(t, node.GetAvailableRoutes(), route)
    }
}

Full example: advertising and approving a route

func TestRouteAdvertisementBasic(t *testing.T) {
    IntegrationSkip(t)
    t.Parallel()

    spec := ScenarioSpec{
        NodesPerUser: 2,
        Users:        []string{"user1"},
    }
    scenario, err := NewScenario(spec)
    require.NoError(t, err)
    defer scenario.ShutdownAssertNoPanics(t)

    err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("route"))
    require.NoError(t, err)

    allClients, err := scenario.ListTailscaleClients()
    require.NoError(t, err)

    headscale, err := scenario.Headscale()
    require.NoError(t, err)

    // --- Blocking: advertise the route on one client ---
    router := allClients[0]
    _, _, err = router.Execute([]string{
        "tailscale", "set",
        "--advertise-routes=10.33.0.0/16",
    })
    require.NoErrorf(t, err, "advertising route: %s", err)

    // --- Eventually: headscale should see the announced route ---
    var nodes []*v1.Node
    assert.EventuallyWithT(t, func(c *assert.CollectT) {
        nodes, err = headscale.ListNodes()
        assert.NoError(c, err)
        assert.Len(c, nodes, 2)

        for _, node := range nodes {
            if node.GetName() == router.Hostname() {
                requireNodeRouteCountWithCollect(c, node, 1, 0, 0)
            }
        }
    }, 10*time.Second, 500*time.Millisecond, "route should be announced")

    // --- Blocking: approve the route via headscale CLI ---
    var routerNode *v1.Node
    for _, node := range nodes {
        if node.GetName() == router.Hostname() {
            routerNode = node
            break
        }
    }
    require.NotNil(t, routerNode)

    _, err = headscale.ApproveRoutes(routerNode.GetId(), []string{"10.33.0.0/16"})
    require.NoError(t, err)

    // --- Eventually: a peer should see the approved route ---
    peer := allClients[1]
    assert.EventuallyWithT(t, func(c *assert.CollectT) {
        status, err := peer.Status()
        assert.NoError(c, err)
        for _, peerKey := range status.Peers() {
            if peerKey == router.PublicKey() {
                requirePeerSubnetRoutesWithCollect(c,
                    status.Peer[peerKey],
                    []netip.Prefix{netip.MustParsePrefix("10.33.0.0/16")})
            }
        }
    }, 10*time.Second, 500*time.Millisecond, "peer should see approved route")
}

Common pitfalls

  • Forgetting IntegrationSkip(t): the test runs outside Docker and fails in confusing ways. Always the first line.
  • Using require inside EventuallyWithT: aborts after the first iteration instead of retrying. Use assert.* + the *WithCollect helpers.
  • Mixing mutation and query in one EventuallyWithT: hides real failures. Keep mutation outside, query inside.
  • Assuming node ordering: look up by property.
  • Ignoring err from client.Status(): retry only retries the whole block; don't silently drop errors from mid-block calls.
  • Timeouts too tight: 5s is reasonable for local state, 10s for state that must propagate through the map poll cycle. Don't go lower to "speed up the test" — you just make it flaky.

Debugging failing tests

Tests save comprehensive artefacts to control_logs/{runID}/. Read them in this order: server stderr, client stderr, MapResponse JSON, database snapshot. The full debugging workflow, heuristics, and failure patterns are documented in ../cmd/hi/README.md.

Documentation

Index

Constants

View Source
const (

	// TimestampFormat is the standard timestamp format used across all integration tests
	// Format: "2006-01-02T15-04-05.999999999" provides high precision timestamps
	// suitable for debugging and log correlation in integration tests.
	TimestampFormat = "2006-01-02T15-04-05.999999999"

	// TimestampFormatRunID is used for generating unique run identifiers
	// Format: "20060102-150405" provides compact date-time for file/directory names.
	TimestampFormatRunID = "20060102-150405"
)

Variables

View Source
var (

	// AllVersions represents a list of Tailscale versions the suite
	// uses to test compatibility with the [ControlServer].
	//
	// The list contains two special cases, "head" and "unstable" which
	// points to the current tip of Tailscale's main branch and the latest
	// released unstable version.
	//
	// The rest of the version represents Tailscale versions that can be
	// found in Tailscale's apt repository.
	AllVersions = append([]string{"head", "unstable"}, capver.TailscaleLatestMajorMinor(capver.SupportedMajorMinorVersions, true)...)

	// MustTestVersions is the minimum set of versions we should test.
	// At the moment, this is arbitrarily chosen as:
	//
	// - Two unstable (HEAD and unstable)
	// - Two latest versions
	// - Two oldest supported version.
	MustTestVersions = append(
		AllVersions[0:4],
		AllVersions[len(AllVersions)-2:]...,
	)
)

Functions

func GetUserByName added in v0.27.0

func GetUserByName(headscale ControlServer, username string) (*v1.User, error)

GetUserByName retrieves a user by name from the headscale server. This is a common pattern used when creating preauth keys or managing users.

func Webservice added in v0.26.0

func Webservice(s *Scenario, networkName string) (*dockertest.Resource, error)

Types

type ControlServer

type ControlServer interface {
	Shutdown() (string, string, error)
	SaveLog(path string) (string, string, error)
	ReadLog() (string, string, error)
	SaveProfile(path string) error
	Execute(command []string) (string, error)
	WriteFile(path string, content []byte) error
	ConnectToNetwork(network *dockertest.Network) error
	GetHealthEndpoint() string
	GetEndpoint() string
	WaitForRunning() error
	Restart() error
	CreateUser(user string) (*v1.User, error)
	CreateAuthKey(user uint64, reusable bool, ephemeral bool) (*v1.PreAuthKey, error)
	CreateAuthKeyWithTags(user uint64, reusable bool, ephemeral bool, tags []string) (*v1.PreAuthKey, error)
	CreateAuthKeyWithOptions(opts hsic.AuthKeyOptions) (*v1.PreAuthKey, error)
	DeleteAuthKey(id uint64) error
	ListNodes(users ...string) ([]*v1.Node, error)
	DeleteNode(nodeID uint64) error
	NodesByUser() (map[string][]*v1.Node, error)
	NodesByName() (map[string]*v1.Node, error)
	ListUsers() ([]*v1.User, error)
	MapUsers() (map[string]*v1.User, error)
	DeleteUser(userID uint64) 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(pol *policyv2.Policy) error
	GetAllMapReponses() (map[types.NodeID][]tailcfg.MapResponse, error)
	PrimaryRoutes() (*types.DebugRoutes, error)
	DebugBatcher() (*hscontrol.DebugBatcherInfo, error)
	DebugNodeStore() (map[types.NodeID]types.Node, error)
	DebugFilter() ([]tailcfg.FilterRule, error)
}

type LoggingRoundTripper added in v0.26.0

type LoggingRoundTripper struct {
	Hostname string
}

func (LoggingRoundTripper) RoundTrip added in v0.26.0

func (t LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)

type NetworkSpec added in v0.29.0

type NetworkSpec struct {
	// Users is the list of usernames whose nodes will be placed on this network.
	Users []string

	// Subnet, if set, is the CIDR for the Docker network (e.g. "198.51.100.0/24").
	// When empty, Docker auto-assigns a subnet from its default pool (RFC1918).
	// Use RFC 5737 TEST-NET ranges for networks that must be reachable through
	// Tailscale exit nodes, since Tailscale's shrinkDefaultRoute strips RFC1918
	// ranges from exit node forwarding filters.
	Subnet string
}

NetworkSpec describes a Docker network for the test scenario.

type NodeSystemStatus added in v0.27.0

type NodeSystemStatus struct {
	Batcher          bool
	BatcherConnCount int
	MapResponses     bool
	NodeStore        bool
}

NodeSystemStatus represents the status of a node across different systems.

type Scenario

type Scenario struct {
	// contains filtered or unexported fields
}

Scenario is a representation of an environment with one ControlServer and one or more User's and its associated [TailscaleClient]s. A Scenario is intended to simplify setting up a new testcase for testing a ControlServer with [TailscaleClient]s. TODO(kradalby): make control server configurable, test correctness with Tailscale SaaS.

func NewScenario

func NewScenario(spec ScenarioSpec) (*Scenario, error)

NewScenario creates a test Scenario which can be used to bootstraps a ControlServer with a set of [User]s and [TailscaleClient]s.

func (*Scenario) AddAndLoginClient added in v0.27.0

func (s *Scenario) AddAndLoginClient(
	t *testing.T,
	username string,
	version string,
	headscale ControlServer,
	tsOpts ...tsic.Option,
) (TailscaleClient, error)

AddAndLoginClient adds a new tailscale client to a user and logs it in. This combines the common pattern of: 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.

func (*Scenario) AddNetwork added in v0.26.0

func (s *Scenario) AddNetwork(name string) (*dockertest.Network, error)

func (*Scenario) AddNetworkWithSubnet added in v0.29.0

func (s *Scenario) AddNetworkWithSubnet(name, subnet string) (*dockertest.Network, error)

func (*Scenario) CountTailscale

func (s *Scenario) CountTailscale() int

CountTailscale returns the total number of [TailscaleClient]s in a Scenario. This is the sum of Users x TailscaleClients.

func (*Scenario) CreateDERPServer added in v0.24.0

func (s *Scenario) CreateDERPServer(version string, opts ...dsic.Option) (*dsic.DERPServerInContainer, error)

CreateDERPServer creates a new DERP server in a container.

func (*Scenario) CreateHeadscaleEnv

func (s *Scenario) CreateHeadscaleEnv(
	tsOpts []tsic.Option,
	opts ...hsic.Option,
) error

func (*Scenario) CreateHeadscaleEnvWithLoginURL added in v0.26.0

func (s *Scenario) CreateHeadscaleEnvWithLoginURL(
	tsOpts []tsic.Option,
	opts ...hsic.Option,
) error

func (*Scenario) CreatePreAuthKey

func (s *Scenario) CreatePreAuthKey(
	user uint64,
	reusable bool,
	ephemeral bool,
) (*v1.PreAuthKey, error)

CreatePreAuthKey creates a "pre authentorised key" to be created in the Headscale instance on behalf of the Scenario.

func (*Scenario) CreatePreAuthKeyWithOptions added in v0.28.0

func (s *Scenario) CreatePreAuthKeyWithOptions(opts hsic.AuthKeyOptions) (*v1.PreAuthKey, error)

CreatePreAuthKeyWithOptions creates a "pre authorised key" with the specified options to be created in the Headscale instance on behalf of the Scenario.

func (*Scenario) CreatePreAuthKeyWithTags added in v0.28.0

func (s *Scenario) CreatePreAuthKeyWithTags(
	user uint64,
	reusable bool,
	ephemeral bool,
	tags []string,
) (*v1.PreAuthKey, error)

CreatePreAuthKeyWithTags creates a "pre authorised key" with the specified tags to be created in the Headscale instance on behalf of the Scenario.

func (*Scenario) CreateTailscaleNode added in v0.25.0

func (s *Scenario) CreateTailscaleNode(
	version string,
	opts ...tsic.Option,
) (TailscaleClient, error)

func (*Scenario) CreateTailscaleNodesInUser added in v0.19.0

func (s *Scenario) CreateTailscaleNodesInUser(
	userStr string,
	requestedVersion string,
	count int,
	opts ...tsic.Option,
) error

CreateTailscaleNodesInUser creates and adds a new TailscaleClient to a User in the Scenario.

func (*Scenario) CreateUser added in v0.19.0

func (s *Scenario) CreateUser(user string) (*v1.User, error)

CreateUser creates a User to be created in the Headscale instance on behalf of the Scenario.

func (*Scenario) FindTailscaleClientByIP added in v0.21.0

func (s *Scenario) FindTailscaleClientByIP(ip netip.Addr) (TailscaleClient, error)

FindTailscaleClientByIP returns a TailscaleClient associated with an IP address if it exists.

func (*Scenario) GetClients

func (s *Scenario) GetClients(user string) ([]TailscaleClient, error)

GetClients returns all [TailscaleClient]s associated with a User in a Scenario.

func (*Scenario) GetIPs

func (s *Scenario) GetIPs(user string) ([]netip.Addr, error)

GetIPs returns all netip.Addr of [TailscaleClient]s associated with a User in a Scenario.

func (*Scenario) GetOrCreateUser added in v0.27.0

func (s *Scenario) GetOrCreateUser(userStr string) *User

GetOrCreateUser gets or creates a user in the Scenario.

func (*Scenario) Headscale

func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error)

Headscale returns a ControlServer instance based on hsic (hsic.HeadscaleInContainer). If the Scenario already has an instance, the pointer to the running container will be return, otherwise a new instance will be created. TODO(kradalby): make port and headscale configurable, multiple instances support?

func (*Scenario) ListTailscaleClients

func (s *Scenario) ListTailscaleClients(users ...string) ([]TailscaleClient, error)

ListTailscaleClients returns a list of [TailscaleClient]s given the [User]s passed as parameters.

func (*Scenario) ListTailscaleClientsFQDNs

func (s *Scenario) ListTailscaleClientsFQDNs(users ...string) ([]string, error)

ListTailscaleClientsFQDNs returns a list of FQDN based on [User]s passed as parameters.

func (*Scenario) ListTailscaleClientsIPs

func (s *Scenario) ListTailscaleClientsIPs(users ...string) ([]netip.Addr, error)

ListTailscaleClientsIPs returns a list of netip.Addr based on [User]s passed as parameters.

func (*Scenario) MustAddAndLoginClient added in v0.27.0

func (s *Scenario) MustAddAndLoginClient(
	t *testing.T,
	username string,
	version string,
	headscale ControlServer,
	tsOpts ...tsic.Option,
) TailscaleClient

MustAddAndLoginClient is like Scenario.AddAndLoginClient but fails the test on error.

func (*Scenario) Network added in v0.26.0

func (s *Scenario) Network(name string) (*dockertest.Network, error)

func (*Scenario) Networks added in v0.26.0

func (s *Scenario) Networks() []*dockertest.Network

func (*Scenario) Pool added in v0.27.0

func (s *Scenario) Pool() *dockertest.Pool

Pool returns the dockertest.Pool for the scenario.

func (*Scenario) RunTailscaleUp

func (s *Scenario) RunTailscaleUp(
	userStr, loginServer, authKey string,
) error

RunTailscaleUp will log in all of the [TailscaleClient]s associated with a User to the given ControlServer (by URL).

func (*Scenario) RunTailscaleUpWithURL added in v0.26.0

func (s *Scenario) RunTailscaleUpWithURL(userStr, loginServer string) error

func (*Scenario) Services added in v0.26.0

func (s *Scenario) Services(name string) ([]*dockertest.Resource, error)

func (*Scenario) Shutdown

func (s *Scenario) Shutdown()

Shutdown shuts down and cleans up all the containers (ControlServer, TailscaleClient) and networks associated with it. In addition, it will save the logs of the ControlServer to `/tmp/control` in the environment running the tests.

func (*Scenario) ShutdownAssertNoPanics added in v0.23.0

func (s *Scenario) ShutdownAssertNoPanics(t *testing.T)

func (*Scenario) SubnetOfNetwork added in v0.26.0

func (s *Scenario) SubnetOfNetwork(name string) (*netip.Prefix, error)

func (*Scenario) Users added in v0.19.0

func (s *Scenario) Users() []string

Users returns the name of all users associated with the Scenario.

func (*Scenario) WaitForTailscaleLogout added in v0.18.0

func (s *Scenario) WaitForTailscaleLogout() error

WaitForTailscaleLogout blocks execution until all [TailscaleClient]s have logged out of the ControlServer.

func (*Scenario) WaitForTailscaleSync

func (s *Scenario) WaitForTailscaleSync() error

WaitForTailscaleSync blocks execution until all the TailscaleClient reports to have all other [TailscaleClient]s present in their netmap.NetworkMap.

func (*Scenario) WaitForTailscaleSyncPerUser added in v0.27.0

func (s *Scenario) WaitForTailscaleSyncPerUser(timeout, retryInterval time.Duration, opts ...SyncOption) error

WaitForTailscaleSyncPerUser blocks until each TailscaleClient has the expected per-user peer count (necessary for policies like autogroup:self where cross-user peers are invisible).

func (*Scenario) WaitForTailscaleSyncWithPeerCount added in v0.23.0

func (s *Scenario) WaitForTailscaleSyncWithPeerCount(peerCount int, timeout, retryInterval time.Duration) error

WaitForTailscaleSyncWithPeerCount blocks execution until all the TailscaleClient reports to have all other [TailscaleClient]s present in their netmap.NetworkMap.

type ScenarioSpec added in v0.26.0

type ScenarioSpec struct {
	// Users is a list of usernames that will be created.
	// Each created user will get nodes equivalent to NodesPerUser
	Users []string

	// NodesPerUser is how many nodes should be attached to each user.
	NodesPerUser int

	// Networks, if set, is the separate Docker networks that should be
	// created and a list of the users that should be placed in those networks.
	// If not set, a single network will be created and all users+nodes will be
	// added there.
	// Please note that Docker networks are not necessarily routable and
	// connections between them might fall back to DERP.
	Networks map[string]NetworkSpec

	// ExtraService, if set, is additional a map of network to additional
	// container services that should be set up. These container services
	// typically dont run Tailscale, e.g. web service to test subnet router.
	ExtraService map[string][]extraServiceFunc

	// Versions is specific list of versions to use for the test.
	Versions []string

	// OIDCSkipUserCreation, if true, skips creating users via headscale CLI
	// during environment setup. Useful for OIDC tests where the SSH policy
	// references users by name, since OIDC login creates users automatically
	// and pre-creating them via CLI causes duplicate user records.
	OIDCSkipUserCreation bool

	// OIDCUsers, if populated, will start a Mock OIDC server and populate
	// the user login stack with the given users.
	// If the NodesPerUser is set, it should align with this list to ensure
	// the correct users are logged in.
	// This is because the MockOIDC server can only serve login
	// requests based on a queue it has been given on startup.
	// We currently only populates it with one login request per user.
	OIDCUsers     []mockoidc.MockUser
	OIDCAccessTTL time.Duration

	MaxWait time.Duration
}

ScenarioSpec describes the users, nodes, and network topology to set up for a given scenario.

type SyncOption added in v0.29.0

type SyncOption func(*syncOptions)

SyncOption configures Scenario.WaitForTailscaleSyncPerUser.

func WithPreBarrier added in v0.29.0

func WithPreBarrier(barrier func(context.Context) error) SyncOption

WithPreBarrier runs a precondition check before per-user peer-count waits begin, sharing the outer timeout via context. Use to gate on a server-side signal (e.g. policy compile) that the peer-count alone cannot observe.

type TailscaleClient

type TailscaleClient interface {
	Hostname() string
	Shutdown() (string, string, error)
	Version() string
	Execute(
		command []string,
		options ...dockertestutil.ExecuteCommandOption,
	) (string, string, error)
	Login(loginServer, authKey string) error
	LoginWithURL(loginServer string) (*url.URL, error)
	Logout() error
	Restart() error
	Up() error
	Down() error
	IPs() ([]netip.Addr, error)
	MustIPs() []netip.Addr
	IPv4() (netip.Addr, error)
	MustIPv4() netip.Addr
	MustIPv6() netip.Addr
	FQDN() (string, error)
	MustFQDN() string
	Status(...bool) (*ipnstate.Status, error)
	MustStatus() *ipnstate.Status
	Netmap() (*netmap.NetworkMap, error)
	DebugDERPRegion(region string) (*ipnstate.DebugDERPRegionReport, error)
	GetNodePrivateKey() (*key.NodePrivate, error)
	Netcheck() (*netcheck.Report, error)
	WaitForNeedsLogin(timeout time.Duration) error
	WaitForRunning(timeout time.Duration) error
	WaitForPeers(expected int, timeout, retryInterval time.Duration) error
	Ping(hostnameOrIP string, opts ...tsic.PingOption) error
	Curl(url string, opts ...tsic.CurlOption) (string, error)
	CurlFailFast(url string) (string, error)
	Traceroute(netip.Addr) (util.Traceroute, error)
	ContainerID() string
	MustID() types.NodeID
	ReadFile(path string) ([]byte, error)
	PacketFilter() ([]filter.Match, error)
	ConnectToNetwork(network *dockertest.Network) error
	DisconnectFromNetwork(network *dockertest.Network) error
	ReconnectToNetwork(network *dockertest.Network) error

	// FailingPeersAsString returns a formatted-ish multi-line-string of peers in the client
	// and a bool indicating if the clients online count and peer count is equal.
	FailingPeersAsString() (string, bool, error)

	WriteLogs(stdout, stderr io.Writer) error
}

nolint

func FindNewClient added in v0.27.0

func FindNewClient(original, updated []TailscaleClient) (TailscaleClient, error)

FindNewClient finds a client that is in the new list but not in the original list. This is useful when dynamically adding nodes during tests and needing to identify which client was just added.

type User added in v0.19.0

type User struct {
	Clients map[string]TailscaleClient
	// contains filtered or unexported fields
}

User represents a User in the ControlServer and a map of TailscaleClient's associated with the User.

Directories

Path Synopsis
Package tsric provides a TailscaleRustInContainer (tsric) implementation that runs the tailscale-rs axum example inside a Docker container for integration testing with headscale.
Package tsric provides a TailscaleRustInContainer (tsric) implementation that runs the tailscale-rs axum example inside a Docker container for integration testing with headscale.
wasmic
wasmclient command
This package only does something when built for GOOS=js (see main.go).
This package only does something when built for GOOS=js (see main.go).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL