From 34997f2c245f570e32bc5267bec9de0e612b5b3c Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 29 Aug 2023 08:33:33 +0200 Subject: [PATCH] Handle errors in integration test setups Thanks @kev-the-dev Closes #1460 Signed-off-by: Kristoffer Dalby --- Dockerfile.tailscale | 16 -- integration/acl_test.go | 4 +- integration/auth_oidc_test.go | 43 ++-- integration/auth_web_flow_test.go | 38 ++-- integration/control.go | 2 +- integration/embedded_derp_test.go | 27 ++- integration/general_test.go | 10 +- integration/hsic/hsic.go | 4 +- integration/scenario.go | 132 ++++++------- integration/scenario_test.go | 4 +- integration/ssh_test.go | 312 ++++++++++++------------------ integration/tailscale.go | 12 +- integration/tsic/tsic.go | 153 ++++++++------- 13 files changed, 357 insertions(+), 400 deletions(-) delete mode 100644 Dockerfile.tailscale diff --git a/Dockerfile.tailscale b/Dockerfile.tailscale deleted file mode 100644 index adf37ed039f..00000000000 --- a/Dockerfile.tailscale +++ /dev/null @@ -1,16 +0,0 @@ -FROM ubuntu:22.04 - -ARG TAILSCALE_VERSION=* -ARG TAILSCALE_CHANNEL=stable - -RUN apt-get update \ - && apt-get install -y gnupg curl ssh dnsutils ca-certificates \ - && adduser --shell=/bin/bash ssh-it-user - -# Tailscale is deliberately split into a second stage so we can cash utils as a seperate layer. -RUN curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.gpg | apt-key add - \ - && curl -fsSL https://pkgs.tailscale.com/${TAILSCALE_CHANNEL}/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \ - && apt-get update \ - && apt-get install -y tailscale=${TAILSCALE_VERSION} \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* diff --git a/integration/acl_test.go b/integration/acl_test.go index 654f557dde5..ebaf8129384 100644 --- a/integration/acl_test.go +++ b/integration/acl_test.go @@ -58,9 +58,9 @@ func aclScenario(t *testing.T, policy *policy.ACLPolicy, clientsPerUser int) *Sc err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{ tsic.WithDockerEntrypoint([]string{ - "/bin/bash", + "/bin/sh", "-c", - "/bin/sleep 3 ; update-ca-certificates ; python3 -m http.server --bind :: 80 & tailscaled --tun=tsdev", + "/bin/sleep 3 ; apk add python3 curl ; update-ca-certificates ; python3 -m http.server --bind :: 80 & tailscaled --tun=tsdev", }), tsic.WithDockerWorkdir("/"), }, diff --git a/integration/auth_oidc_test.go b/integration/auth_oidc_test.go index 9b05af9324f..f0be3e8a8d4 100644 --- a/integration/auth_oidc_test.go +++ b/integration/auth_oidc_test.go @@ -171,7 +171,10 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) { t.Logf("%d successful pings out of %d (before expiry)", success, len(allClients)*len(allIps)) // await all nodes being logged out after OIDC token expiry - scenario.WaitForTailscaleLogout() + err = scenario.WaitForTailscaleLogout() + if err != nil { + t.Errorf("failed to log out tailscale nodes: %s", err) + } err = scenario.Shutdown() if err != nil { @@ -188,7 +191,7 @@ func (s *AuthOIDCScenario) CreateHeadscaleEnv( return err } - err = headscale.WaitForReady() + err = headscale.WaitForRunning() if err != nil { return err } @@ -311,13 +314,9 @@ func (s *AuthOIDCScenario) runTailscaleUp( log.Printf("running tailscale up for user %s", userStr) if user, ok := s.users[userStr]; ok { for _, client := range user.Clients { - user.joinWaitGroup.Add(1) - - go func(c TailscaleClient) { - defer user.joinWaitGroup.Done() - - // TODO(juanfont): error handle this - loginURL, err := c.UpWithLoginURL(loginServer) + c := client + user.joinWaitGroup.Go(func() error { + loginURL, err := c.LoginWithURL(loginServer) if err != nil { log.Printf("failed to run tailscale up: %s", err) } @@ -336,9 +335,14 @@ func (s *AuthOIDCScenario) runTailscaleUp( req, _ := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil) resp, err := httpClient.Do(req) if err != nil { - log.Printf("%s failed to get login url %s: %s", c.Hostname(), loginURL, err) - - return + log.Printf( + "%s failed to get login url %s: %s", + c.Hostname(), + loginURL, + err, + ) + + return err } defer resp.Body.Close() @@ -347,24 +351,23 @@ func (s *AuthOIDCScenario) runTailscaleUp( if err != nil { log.Printf("%s failed to read response body: %s", c.Hostname(), err) - return + return err } log.Printf("Finished request for %s to join tailnet", c.Hostname()) - }(client) - err = client.WaitForReady() - if err != nil { - log.Printf("error waiting for client %s to be ready: %s", client.Hostname(), err) - } + return nil + }) log.Printf("client %s is ready", client.Hostname()) } - user.joinWaitGroup.Wait() + if err := user.joinWaitGroup.Wait(); err != nil { + return err + } for _, client := range user.Clients { - err := client.WaitForReady() + err := client.WaitForRunning() if err != nil { log.Printf("client %s was not ready: %s", client.Hostname(), err) diff --git a/integration/auth_web_flow_test.go b/integration/auth_web_flow_test.go index 7e497f68075..2129b35827e 100644 --- a/integration/auth_web_flow_test.go +++ b/integration/auth_web_flow_test.go @@ -134,7 +134,10 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) { } } - scenario.WaitForTailscaleLogout() + err = scenario.WaitForTailscaleLogout() + if err != nil { + t.Errorf("failed to log out tailscale nodes: %s", err) + } t.Logf("all clients logged out") @@ -218,7 +221,7 @@ func (s *AuthWebFlowScenario) CreateHeadscaleEnv( return err } - err = headscale.WaitForReady() + err = headscale.WaitForRunning() if err != nil { return err } @@ -250,32 +253,37 @@ func (s *AuthWebFlowScenario) runTailscaleUp( log.Printf("running tailscale up for user %s", userStr) if user, ok := s.users[userStr]; ok { for _, client := range user.Clients { - user.joinWaitGroup.Add(1) - - go func(c TailscaleClient) { - defer user.joinWaitGroup.Done() - - // TODO(juanfont): error handle this - loginURL, err := c.UpWithLoginURL(loginServer) + c := client + user.joinWaitGroup.Go(func() error { + loginURL, err := c.LoginWithURL(loginServer) if err != nil { - log.Printf("failed to run tailscale up: %s", err) + log.Printf("failed to run tailscale up (%s): %s", c.Hostname(), err) + + return err } err = s.runHeadscaleRegister(userStr, loginURL) if err != nil { - log.Printf("failed to register client: %s", err) + log.Printf("failed to register client (%s): %s", c.Hostname(), err) + + return err } - }(client) - err := client.WaitForReady() + return nil + }) + + err := client.WaitForRunning() if err != nil { log.Printf("error waiting for client %s to be ready: %s", client.Hostname(), err) } } - user.joinWaitGroup.Wait() + + if err := user.joinWaitGroup.Wait(); err != nil { + return err + } for _, client := range user.Clients { - err := client.WaitForReady() + err := client.WaitForRunning() if err != nil { log.Printf("client %s was not ready: %s", client.Hostname(), err) diff --git a/integration/control.go b/integration/control.go index 65432b3ff29..b41100705a6 100644 --- a/integration/control.go +++ b/integration/control.go @@ -13,7 +13,7 @@ type ControlServer interface { ConnectToNetwork(network *dockertest.Network) error GetHealthEndpoint() string GetEndpoint() string - WaitForReady() error + WaitForRunning() error CreateUser(user string) error CreateAuthKey(user string, reusable bool, ephemeral bool) (*v1.PreAuthKey, error) ListMachinesInUser(user string) ([]*v1.Machine, error) diff --git a/integration/embedded_derp_test.go b/integration/embedded_derp_test.go index e9183cdcbd0..93b227190a7 100644 --- a/integration/embedded_derp_test.go +++ b/integration/embedded_derp_test.go @@ -2,7 +2,6 @@ package integration import ( "fmt" - "log" "net/url" "testing" @@ -105,7 +104,7 @@ func (s *EmbeddedDERPServerScenario) CreateHeadscaleEnv( headscaleURL.Host = fmt.Sprintf("%s:%s", hsServer.GetHostname(), headscaleURL.Port()) - err = hsServer.WaitForReady() + err = hsServer.WaitForRunning() if err != nil { return err } @@ -186,16 +185,11 @@ func (s *EmbeddedDERPServerScenario) CreateTailscaleIsolatedNodesInUser( cert := hsServer.GetCert() - user.createWaitGroup.Add(1) - opts = append(opts, tsic.WithHeadscaleTLS(cert), ) - go func() { - defer user.createWaitGroup.Done() - - // TODO(kradalby): error handle this + user.createWaitGroup.Go(func() error { tsClient, err := tsic.New( s.pool, version, @@ -203,20 +197,23 @@ func (s *EmbeddedDERPServerScenario) CreateTailscaleIsolatedNodesInUser( opts..., ) if err != nil { - // return fmt.Errorf("failed to add tailscale node: %w", err) - log.Printf("failed to create tailscale node: %s", err) + return fmt.Errorf("failed to create tailscale node: %w", err) } - err = tsClient.WaitForReady() + err = tsClient.WaitForRunning() if err != nil { - // return fmt.Errorf("failed to add tailscale node: %w", err) - log.Printf("failed to wait for tailscaled: %s", err) + return fmt.Errorf("failed to wait for tailscaled to need login: %w", err) } user.Clients[tsClient.Hostname()] = tsClient - }() + + return nil + }) + } + + if err := user.createWaitGroup.Wait(); err != nil { + return err } - user.createWaitGroup.Wait() return nil } diff --git a/integration/general_test.go b/integration/general_test.go index f3187e3a937..50f76045b9f 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -108,7 +108,10 @@ func TestAuthKeyLogoutAndRelogin(t *testing.T) { } } - scenario.WaitForTailscaleLogout() + err = scenario.WaitForTailscaleLogout() + if err != nil { + t.Errorf("failed to log out tailscale nodes: %s", err) + } t.Logf("all clients logged out") @@ -261,7 +264,10 @@ func TestEphemeral(t *testing.T) { } } - scenario.WaitForTailscaleLogout() + err = scenario.WaitForTailscaleLogout() + if err != nil { + t.Errorf("failed to log out tailscale nodes: %s", err) + } t.Logf("all clients logged out") diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index d27eb06fcde..e13b7273548 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -428,9 +428,9 @@ func (t *HeadscaleInContainer) GetHostname() string { return t.hostname } -// WaitForReady blocks until the Headscale instance is ready to +// WaitForRunning blocks until the Headscale instance is ready to // serve clients. -func (t *HeadscaleInContainer) WaitForReady() error { +func (t *HeadscaleInContainer) WaitForRunning() error { url := t.GetHealthEndpoint() log.Printf("waiting for headscale to be ready at %s", url) diff --git a/integration/scenario.go b/integration/scenario.go index 927d6c80cbc..654c75e4886 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -16,6 +16,7 @@ import ( "github.com/juanfont/headscale/integration/tsic" "github.com/ory/dockertest/v3" "github.com/puzpuzpuz/xsync/v2" + "golang.org/x/sync/errgroup" ) const ( @@ -33,30 +34,33 @@ var ( tailscaleVersions2021 = []string{ "head", "unstable", - "1.40.0", - "1.38.4", - "1.36.2", - "1.34.2", - "1.32.3", - "1.30.2", + "1.48", + "1.46", + "1.44", + "1.42", + "1.40", + "1.38", + "1.36", + "1.34", + "1.32", + "1.30", } tailscaleVersions2019 = []string{ - "1.28.0", - "1.26.2", - "1.24.2", - "1.22.2", - "1.20.4", + "1.28", + "1.26", + "1.24", + "1.22", + "1.20", + "1.18", } // tailscaleVersionsUnavailable = []string{ // // These versions seem to fail when fetching from apt. - // "1.18.2", - // "1.16.2", - // "1.14.6", - // "1.12.4", - // "1.10.2", - // "1.8.7", + // "1.14.6", + // "1.12.4", + // "1.10.2", + // "1.8.7", // }. // TailscaleVersions represents a list of Tailscale versions the suite @@ -79,9 +83,9 @@ var ( type User struct { Clients map[string]TailscaleClient - createWaitGroup sync.WaitGroup - joinWaitGroup sync.WaitGroup - syncWaitGroup sync.WaitGroup + createWaitGroup errgroup.Group + joinWaitGroup errgroup.Group + syncWaitGroup errgroup.Group } // Scenario is a representation of an environment with one ControlServer and @@ -213,7 +217,7 @@ func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) { return nil, fmt.Errorf("failed to create headscale container: %w", err) } - err = headscale.WaitForReady() + err = headscale.WaitForRunning() if err != nil { return nil, fmt.Errorf("failed reach headscale container: %w", err) } @@ -286,17 +290,12 @@ func (s *Scenario) CreateTailscaleNodesInUser( cert := headscale.GetCert() hostname := headscale.GetHostname() - user.createWaitGroup.Add(1) - opts = append(opts, tsic.WithHeadscaleTLS(cert), tsic.WithHeadscaleName(hostname), ) - go func() { - defer user.createWaitGroup.Done() - - // TODO(kradalby): error handle this + user.createWaitGroup.Go(func() error { tsClient, err := tsic.New( s.pool, version, @@ -304,20 +303,26 @@ func (s *Scenario) CreateTailscaleNodesInUser( opts..., ) if err != nil { - // return fmt.Errorf("failed to add tailscale node: %w", err) - log.Printf("failed to create tailscale node: %s", err) + return fmt.Errorf("failed to create tailscale node: %w", err) + // log.Printf("failed to create tailscale node: %s", err) } - err = tsClient.WaitForReady() + err = tsClient.WaitForNeedsLogin() if err != nil { - // return fmt.Errorf("failed to add tailscale node: %w", err) - log.Printf("failed to wait for tailscaled: %s", err) + return fmt.Errorf( + "failed to wait for tailscaled to be ready for login: %w", + err, + ) } user.Clients[tsClient.Hostname()] = tsClient - }() + + return nil + }) + } + if err := user.createWaitGroup.Wait(); err != nil { + return err } - user.createWaitGroup.Wait() return nil } @@ -332,25 +337,18 @@ func (s *Scenario) RunTailscaleUp( ) error { if user, ok := s.users[userStr]; ok { for _, client := range user.Clients { - user.joinWaitGroup.Add(1) - - go func(c TailscaleClient) { - defer user.joinWaitGroup.Done() - - // TODO(kradalby): error handle this - _ = c.Up(loginServer, authKey) - }(client) - - err := client.WaitForReady() - if err != nil { - log.Printf("error waiting for client %s to be ready: %s", client.Hostname(), err) - } + c := client + user.joinWaitGroup.Go(func() error { + return c.Login(loginServer, authKey) + }) } - user.joinWaitGroup.Wait() + if err := user.joinWaitGroup.Wait(); err != nil { + return err + } for _, client := range user.Clients { - err := client.WaitForReady() + err := client.WaitForRunning() if err != nil { log.Printf("client %s was not ready: %s", client.Hostname(), err) @@ -383,16 +381,14 @@ func (s *Scenario) WaitForTailscaleSync() error { for _, user := range s.users { for _, client := range user.Clients { - user.syncWaitGroup.Add(1) - - go func(c TailscaleClient) { - defer user.syncWaitGroup.Done() - - // TODO(kradalby): error handle this - _ = c.WaitForPeers(tsCount) - }(client) + c := client + user.syncWaitGroup.Go(func() error { + return c.WaitForPeers(tsCount - 1) + }) + } + if err := user.syncWaitGroup.Wait(); err != nil { + return err } - user.syncWaitGroup.Wait() } return nil @@ -555,18 +551,18 @@ func (s *Scenario) ListTailscaleClientsFQDNs(users ...string) ([]string, error) // WaitForTailscaleLogout blocks execution until all TailscaleClients have // logged out of the ControlServer. -func (s *Scenario) WaitForTailscaleLogout() { +func (s *Scenario) WaitForTailscaleLogout() error { for _, user := range s.users { for _, client := range user.Clients { - user.syncWaitGroup.Add(1) - - go func(c TailscaleClient) { - defer user.syncWaitGroup.Done() - - // TODO(kradalby): error handle this - _ = c.WaitForLogout() - }(client) + c := client + user.syncWaitGroup.Go(func() error { + return c.WaitForLogout() + }) + } + if err := user.syncWaitGroup.Wait(); err != nil { + return err } - user.syncWaitGroup.Wait() } + + return nil } diff --git a/integration/scenario_test.go b/integration/scenario_test.go index 31f69530485..c2e563eaae9 100644 --- a/integration/scenario_test.go +++ b/integration/scenario_test.go @@ -44,7 +44,7 @@ func TestHeadscale(t *testing.T) { t.Errorf("failed to create start headcale: %s", err) } - err = headscale.WaitForReady() + err = headscale.WaitForRunning() if err != nil { t.Errorf("headscale failed to become ready: %s", err) } @@ -137,7 +137,7 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) { t.Errorf("failed to create start headcale: %s", err) } - err = headscale.WaitForReady() + err = headscale.WaitForRunning() if err != nil { t.Errorf("headscale failed to become ready: %s", err) } diff --git a/integration/ssh_test.go b/integration/ssh_test.go index c8963b1991c..ce5ecc651e9 100644 --- a/integration/ssh_test.go +++ b/integration/ssh_test.go @@ -41,50 +41,69 @@ var retry = func(times int, sleepInterval time.Duration, return result, stderr, err } -func TestSSHOneUserAllToAll(t *testing.T) { - IntegrationSkip(t) - t.Parallel() - +func sshScenario(t *testing.T, policy *policy.ACLPolicy, clientsPerUser int) *Scenario { + t.Helper() scenario, err := NewScenario() - if err != nil { - t.Errorf("failed to create scenario: %s", err) - } + assert.NoError(t, err) spec := map[string]int{ - "user1": len(TailscaleVersions) - 5, + "user1": clientsPerUser, + "user2": clientsPerUser, } err = scenario.CreateHeadscaleEnv(spec, - []tsic.Option{tsic.WithSSH()}, - hsic.WithACLPolicy( - &policy.ACLPolicy{ - Groups: map[string][]string{ - "group:integration-test": {"user1"}, - }, - ACLs: []policy.ACL{ - { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, - }, - }, - SSHs: []policy.SSH{ - { - Action: "accept", - Sources: []string{"group:integration-test"}, - Destinations: []string{"group:integration-test"}, - Users: []string{"ssh-it-user"}, - }, - }, - }, - ), + []tsic.Option{ + tsic.WithDockerEntrypoint([]string{ + "/bin/sh", + "-c", + "/bin/sleep 3 ; apk add ssh ; update-ca-certificates ; tailscaled --tun=tsdev", + }), + tsic.WithDockerWorkdir("/"), + }, + hsic.WithACLPolicy(policy), + hsic.WithTestName("ssh"), hsic.WithConfigEnv(map[string]string{ "HEADSCALE_EXPERIMENTAL_FEATURE_SSH": "1", }), ) - if err != nil { - t.Errorf("failed to create headscale environment: %s", err) - } + assert.NoError(t, err) + + err = scenario.WaitForTailscaleSync() + assert.NoError(t, err) + + _, err = scenario.ListTailscaleClientsFQDNs() + assert.NoError(t, err) + + return scenario +} + +func TestSSHOneUserAllToAll(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + scenario := sshScenario(t, + &policy.ACLPolicy{ + Groups: map[string][]string{ + "group:integration-test": {"user1"}, + }, + ACLs: []policy.ACL{ + { + Action: "accept", + Sources: []string{"*"}, + Destinations: []string{"*:*"}, + }, + }, + SSHs: []policy.SSH{ + { + Action: "accept", + Sources: []string{"group:integration-test"}, + Destinations: []string{"group:integration-test"}, + Users: []string{"ssh-it-user"}, + }, + }, + }, + len(TailscaleVersions)-5, + ) allClients, err := scenario.ListTailscaleClients() if err != nil { @@ -121,47 +140,29 @@ func TestSSHMultipleUsersAllToAll(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() - if err != nil { - t.Errorf("failed to create scenario: %s", err) - } - - spec := map[string]int{ - "user1": len(TailscaleVersions) - 5, - "user2": len(TailscaleVersions) - 5, - } - - err = scenario.CreateHeadscaleEnv(spec, - []tsic.Option{tsic.WithSSH()}, - hsic.WithACLPolicy( - &policy.ACLPolicy{ - Groups: map[string][]string{ - "group:integration-test": {"user1", "user2"}, - }, - ACLs: []policy.ACL{ - { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, - }, + scenario := sshScenario(t, + &policy.ACLPolicy{ + Groups: map[string][]string{ + "group:integration-test": {"user1", "user2"}, + }, + ACLs: []policy.ACL{ + { + Action: "accept", + Sources: []string{"*"}, + Destinations: []string{"*:*"}, }, - SSHs: []policy.SSH{ - { - Action: "accept", - Sources: []string{"group:integration-test"}, - Destinations: []string{"group:integration-test"}, - Users: []string{"ssh-it-user"}, - }, + }, + SSHs: []policy.SSH{ + { + Action: "accept", + Sources: []string{"group:integration-test"}, + Destinations: []string{"group:integration-test"}, + Users: []string{"ssh-it-user"}, }, }, - ), - hsic.WithConfigEnv(map[string]string{ - "HEADSCALE_EXPERIMENTAL_FEATURE_SSH": "1", - }), + }, + len(TailscaleVersions)-5, ) - if err != nil { - t.Errorf("failed to create headscale environment: %s", err) - } nsOneClients, err := scenario.ListTailscaleClients("user1") if err != nil { @@ -204,40 +205,22 @@ func TestSSHNoSSHConfigured(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() - if err != nil { - t.Errorf("failed to create scenario: %s", err) - } - - spec := map[string]int{ - "user1": len(TailscaleVersions) - 5, - } - - err = scenario.CreateHeadscaleEnv(spec, - []tsic.Option{tsic.WithSSH()}, - hsic.WithACLPolicy( - &policy.ACLPolicy{ - Groups: map[string][]string{ - "group:integration-test": {"user1"}, - }, - ACLs: []policy.ACL{ - { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, - }, + scenario := sshScenario(t, + &policy.ACLPolicy{ + Groups: map[string][]string{ + "group:integration-test": {"user1"}, + }, + ACLs: []policy.ACL{ + { + Action: "accept", + Sources: []string{"*"}, + Destinations: []string{"*:*"}, }, - SSHs: []policy.SSH{}, }, - ), - hsic.WithTestName("sshnoneconfigured"), - hsic.WithConfigEnv(map[string]string{ - "HEADSCALE_EXPERIMENTAL_FEATURE_SSH": "1", - }), + SSHs: []policy.SSH{}, + }, + len(TailscaleVersions)-5, ) - if err != nil { - t.Errorf("failed to create headscale environment: %s", err) - } allClients, err := scenario.ListTailscaleClients() if err != nil { @@ -274,47 +257,29 @@ func TestSSHIsBlockedInACL(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() - if err != nil { - t.Errorf("failed to create scenario: %s", err) - } - - spec := map[string]int{ - "user1": len(TailscaleVersions) - 5, - } - - err = scenario.CreateHeadscaleEnv(spec, - []tsic.Option{tsic.WithSSH()}, - hsic.WithACLPolicy( - &policy.ACLPolicy{ - Groups: map[string][]string{ - "group:integration-test": {"user1"}, - }, - ACLs: []policy.ACL{ - { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:80"}, - }, + scenario := sshScenario(t, + &policy.ACLPolicy{ + Groups: map[string][]string{ + "group:integration-test": {"user1"}, + }, + ACLs: []policy.ACL{ + { + Action: "accept", + Sources: []string{"*"}, + Destinations: []string{"*:80"}, }, - SSHs: []policy.SSH{ - { - Action: "accept", - Sources: []string{"group:integration-test"}, - Destinations: []string{"group:integration-test"}, - Users: []string{"ssh-it-user"}, - }, + }, + SSHs: []policy.SSH{ + { + Action: "accept", + Sources: []string{"group:integration-test"}, + Destinations: []string{"group:integration-test"}, + Users: []string{"ssh-it-user"}, }, }, - ), - hsic.WithTestName("sshisblockedinacl"), - hsic.WithConfigEnv(map[string]string{ - "HEADSCALE_EXPERIMENTAL_FEATURE_SSH": "1", - }), + }, + len(TailscaleVersions)-5, ) - if err != nil { - t.Errorf("failed to create headscale environment: %s", err) - } allClients, err := scenario.ListTailscaleClients() if err != nil { @@ -351,55 +316,36 @@ func TestSSUserOnlyIsolation(t *testing.T) { IntegrationSkip(t) t.Parallel() - scenario, err := NewScenario() - if err != nil { - t.Errorf("failed to create scenario: %s", err) - } - - spec := map[string]int{ - "useracl1": len(TailscaleVersions) - 5, - "useracl2": len(TailscaleVersions) - 5, - } - - err = scenario.CreateHeadscaleEnv(spec, - []tsic.Option{tsic.WithSSH()}, - hsic.WithACLPolicy( - &policy.ACLPolicy{ - Groups: map[string][]string{ - "group:ssh1": {"useracl1"}, - "group:ssh2": {"useracl2"}, + scenario := sshScenario(t, + &policy.ACLPolicy{ + Groups: map[string][]string{ + "group:ssh1": {"user1"}, + "group:ssh2": {"user2"}, + }, + ACLs: []policy.ACL{ + { + Action: "accept", + Sources: []string{"*"}, + Destinations: []string{"*:*"}, }, - ACLs: []policy.ACL{ - { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, - }, + }, + SSHs: []policy.SSH{ + { + Action: "accept", + Sources: []string{"group:ssh1"}, + Destinations: []string{"group:ssh1"}, + Users: []string{"ssh-it-user"}, }, - SSHs: []policy.SSH{ - { - Action: "accept", - Sources: []string{"group:ssh1"}, - Destinations: []string{"group:ssh1"}, - Users: []string{"ssh-it-user"}, - }, - { - Action: "accept", - Sources: []string{"group:ssh2"}, - Destinations: []string{"group:ssh2"}, - Users: []string{"ssh-it-user"}, - }, + { + Action: "accept", + Sources: []string{"group:ssh2"}, + Destinations: []string{"group:ssh2"}, + Users: []string{"ssh-it-user"}, }, }, - ), - hsic.WithTestName("sshtwouseraclblock"), - hsic.WithConfigEnv(map[string]string{ - "HEADSCALE_EXPERIMENTAL_FEATURE_SSH": "1", - }), + }, + len(TailscaleVersions)-5, ) - if err != nil { - t.Errorf("failed to create headscale environment: %s", err) - } ssh1Clients, err := scenario.ListTailscaleClients("useracl1") if err != nil { diff --git a/integration/tailscale.go b/integration/tailscale.go index 166b851fcea..46c87c47ca9 100644 --- a/integration/tailscale.go +++ b/integration/tailscale.go @@ -14,14 +14,18 @@ type TailscaleClient interface { Hostname() string Shutdown() error Version() string - Execute(command []string, options ...dockertestutil.ExecuteCommandOption) (string, string, error) - Up(loginServer, authKey string) error - UpWithLoginURL(loginServer string) (*url.URL, error) + Execute( + command []string, + options ...dockertestutil.ExecuteCommandOption, + ) (string, string, error) + Login(loginServer, authKey string) error + LoginWithURL(loginServer string) (*url.URL, error) Logout() error IPs() ([]netip.Addr, error) FQDN() (string, error) Status() (*ipnstate.Status, error) - WaitForReady() error + WaitForNeedsLogin() error + WaitForRunning() error WaitForLogout() error WaitForPeers(expected int) error Ping(hostnameOrIP string, opts ...tsic.PingOption) error diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go index ffc7e0a90ed..6c397f33f79 100644 --- a/integration/tsic/tsic.go +++ b/integration/tsic/tsic.go @@ -165,7 +165,7 @@ func New( network: network, withEntrypoint: []string{ - "/bin/bash", + "/bin/sh", "-c", "/bin/sleep 3 ; update-ca-certificates ; tailscaled --tun=tsdev", }, @@ -204,13 +204,44 @@ func New( return nil, err } - container, err := pool.BuildAndRunWithBuildOptions( - createTailscaleBuildOptions(version), - tailscaleOptions, - dockertestutil.DockerRestartPolicy, - dockertestutil.DockerAllowLocalIPv6, - dockertestutil.DockerAllowNetworkAdministration, - ) + var container *dockertest.Resource + switch version { + case "head": + buildOptions := &dockertest.BuildOptions{ + Dockerfile: "Dockerfile.tailscale-HEAD", + ContextDir: dockerContextPath, + BuildArgs: []docker.BuildArg{}, + } + + container, err = pool.BuildAndRunWithBuildOptions( + buildOptions, + tailscaleOptions, + dockertestutil.DockerRestartPolicy, + dockertestutil.DockerAllowLocalIPv6, + dockertestutil.DockerAllowNetworkAdministration, + ) + case "unstable": + tailscaleOptions.Repository = "tailscale/tailscale" + tailscaleOptions.Tag = version + + container, err = pool.RunWithOptions( + tailscaleOptions, + dockertestutil.DockerRestartPolicy, + dockertestutil.DockerAllowLocalIPv6, + dockertestutil.DockerAllowNetworkAdministration, + ) + default: + tailscaleOptions.Repository = "tailscale/tailscale" + tailscaleOptions.Tag = "v" + version + + container, err = pool.RunWithOptions( + tailscaleOptions, + dockertestutil.DockerRestartPolicy, + dockertestutil.DockerAllowLocalIPv6, + dockertestutil.DockerAllowNetworkAdministration, + ) + } + if err != nil { return nil, fmt.Errorf( "could not start tailscale container (version: %s): %w", @@ -270,7 +301,7 @@ func (t *TailscaleInContainer) Execute( options..., ) if err != nil { - log.Printf("command stderr: %s\n", stderr) + // log.Printf("command stderr: %s\n", stderr) if stdout != "" { log.Printf("command stdout: %s\n", stdout) @@ -288,18 +319,15 @@ func (t *TailscaleInContainer) Execute( // Up runs the login routine on the given Tailscale instance. // This login mechanism uses the authorised key for authentication. -func (t *TailscaleInContainer) Up( +func (t *TailscaleInContainer) Login( loginServer, authKey string, ) error { command := []string{ "tailscale", "up", - "-login-server", - loginServer, - "--authkey", - authKey, - "--hostname", - t.hostname, + "--login-server=" + loginServer, + "--authkey=" + authKey, + "--hostname=" + t.hostname, } if t.withSSH { @@ -313,7 +341,11 @@ func (t *TailscaleInContainer) Up( } if _, _, err := t.Execute(command); err != nil { - return fmt.Errorf("failed to join tailscale client: %w", err) + return fmt.Errorf( + "failed to join tailscale client (%s): %w", + strings.Join(command, " "), + err, + ) } return nil @@ -321,16 +353,14 @@ func (t *TailscaleInContainer) Up( // Up runs the login routine on the given Tailscale instance. // This login mechanism uses web + command line flow for authentication. -func (t *TailscaleInContainer) UpWithLoginURL( +func (t *TailscaleInContainer) LoginWithURL( loginServer string, ) (*url.URL, error) { command := []string{ "tailscale", "up", - "-login-server", - loginServer, - "--hostname", - t.hostname, + "--login-server=" + loginServer, + "--hostname=" + t.hostname, } _, stderr, err := t.Execute(command) @@ -432,9 +462,31 @@ func (t *TailscaleInContainer) FQDN() (string, error) { return status.Self.DNSName, nil } -// WaitForReady blocks until the Tailscale (tailscaled) instance is ready -// to login or be used. -func (t *TailscaleInContainer) WaitForReady() error { +// WaitForNeedsLogin blocks until the Tailscale (tailscaled) instance has +// started and needs to be logged into. +func (t *TailscaleInContainer) WaitForNeedsLogin() error { + return t.pool.Retry(func() error { + status, err := t.Status() + if err != nil { + return fmt.Errorf("failed to fetch tailscale status: %w", err) + } + + // ipnstate.Status.CurrentTailnet was added in Tailscale 1.22.0 + // https://github.com/tailscale/tailscale/pull/3865 + // + // Before that, we can check the BackendState to see if the + // tailscaled daemon is connected to the control system. + if status.BackendState == "NeedsLogin" { + return nil + } + + return errTailscaleNotConnected + }) +} + +// WaitForRunning blocks until the Tailscale (tailscaled) instance is logged in +// and ready to be used. +func (t *TailscaleInContainer) WaitForRunning() error { return t.pool.Retry(func() error { status, err := t.Status() if err != nil { @@ -484,7 +536,12 @@ func (t *TailscaleInContainer) WaitForPeers(expected int) error { } if peers := status.Peers(); len(peers) != expected { - return errTailscaleWrongPeerCount + return fmt.Errorf( + "%w: expected %d, got %d", + errTailscaleWrongPeerCount, + expected, + len(peers), + ) } return nil @@ -683,47 +740,3 @@ func (t *TailscaleInContainer) Curl(url string, opts ...CurlOption) (string, err func (t *TailscaleInContainer) WriteFile(path string, data []byte) error { return integrationutil.WriteFileToContainer(t.pool, t.container, path, data) } - -func createTailscaleBuildOptions(version string) *dockertest.BuildOptions { - var tailscaleBuildOptions *dockertest.BuildOptions - switch version { - case "head": - tailscaleBuildOptions = &dockertest.BuildOptions{ - Dockerfile: "Dockerfile.tailscale-HEAD", - ContextDir: dockerContextPath, - BuildArgs: []docker.BuildArg{}, - } - case "unstable": - tailscaleBuildOptions = &dockertest.BuildOptions{ - Dockerfile: "Dockerfile.tailscale", - ContextDir: dockerContextPath, - BuildArgs: []docker.BuildArg{ - { - Name: "TAILSCALE_VERSION", - Value: "*", // Installs the latest version https://askubuntu.com/a/824926 - }, - { - Name: "TAILSCALE_CHANNEL", - Value: "unstable", - }, - }, - } - default: - tailscaleBuildOptions = &dockertest.BuildOptions{ - Dockerfile: "Dockerfile.tailscale", - ContextDir: dockerContextPath, - BuildArgs: []docker.BuildArg{ - { - Name: "TAILSCALE_VERSION", - Value: version, - }, - { - Name: "TAILSCALE_CHANNEL", - Value: "stable", - }, - }, - } - } - - return tailscaleBuildOptions -}