diff --git a/integration/general_test.go b/integration/general_test.go index 3d7063da..7292c459 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -1,7 +1,11 @@ package integration import ( + "fmt" "testing" + "time" + + "github.com/rs/zerolog/log" ) func TestPingAllByIP(t *testing.T) { @@ -112,3 +116,142 @@ func TestPingAllByHostname(t *testing.T) { t.Errorf("failed to tear down scenario: %s", err) } } + +func TestTaildrop(t *testing.T) { + IntegrationSkip(t) + + retry := func(times int, sleepInverval time.Duration, doWork func() error) error { + var err error + for attempts := 0; attempts < times; attempts++ { + err = doWork() + if err == nil { + return nil + } + time.Sleep(sleepInverval) + } + + return err + } + + scenario, err := NewScenario() + if err != nil { + t.Errorf("failed to create scenario: %s", err) + } + + spec := map[string]int{ + // Omit 1.16.2 (-1) because it does not have the FQDN field + "taildrop": len(TailscaleVersions) - 1, + } + + err = scenario.CreateHeadscaleEnv(spec) + if err != nil { + t.Errorf("failed to create headscale environment: %s", err) + } + + allClients, err := scenario.ListTailscaleClients() + if err != nil { + t.Errorf("failed to get clients: %s", err) + } + + err = scenario.WaitForTailscaleSync() + if err != nil { + t.Errorf("failed wait for tailscale clients to be in sync: %s", err) + } + + // This will essentially fetch and cache all the FQDNs + _, err = scenario.ListTailscaleClientsFQDNs() + if err != nil { + t.Errorf("failed to get FQDNs: %s", err) + } + + for _, client := range allClients { + command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", client.Hostname())} + + if _, err := client.Execute(command); err != nil { + t.Errorf("failed to create taildrop file on %s, err: %s", client.Hostname(), err) + } + + for _, peer := range allClients { + if client.Hostname() == peer.Hostname() { + continue + } + + // It is safe to ignore this error as we handled it when caching it + peerFQDN, _ := peer.FQDN() + + t.Run(fmt.Sprintf("%s-%s", client.Hostname(), peer.Hostname()), func(t *testing.T) { + command := []string{ + "tailscale", "file", "cp", + fmt.Sprintf("/tmp/file_from_%s", client.Hostname()), + fmt.Sprintf("%s:", peerFQDN), + } + + err := retry(10, 1*time.Second, func() error { + t.Logf( + "Sending file from %s to %s\n", + client.Hostname(), + peer.Hostname(), + ) + _, err := client.Execute(command) + + return err + }) + if err != nil { + t.Errorf( + "failed to send taildrop file on %s, err: %s", + client.Hostname(), + err, + ) + } + }) + } + } + + for _, client := range allClients { + command := []string{ + "tailscale", "file", + "get", + "/tmp/", + } + if _, err := client.Execute(command); err != nil { + t.Errorf("failed to get taildrop file on %s, err: %s", client.Hostname(), err) + } + + for _, peer := range allClients { + if client.Hostname() == peer.Hostname() { + continue + } + + t.Run(fmt.Sprintf("%s-%s", client.Hostname(), peer.Hostname()), func(t *testing.T) { + command := []string{ + "ls", + fmt.Sprintf("/tmp/file_from_%s", peer.Hostname()), + } + log.Printf( + "Checking file in %s from %s\n", + client.Hostname(), + peer.Hostname(), + ) + + result, err := client.Execute(command) + if err != nil { + t.Errorf("failed to execute command to ls taildrop: %s", err) + } + + log.Printf("Result for %s: %s\n", peer.Hostname(), result) + if fmt.Sprintf("/tmp/file_from_%s\n", peer.Hostname()) != result { + t.Errorf( + "taildrop result is not correct %s, wanted %s", + result, + fmt.Sprintf("/tmp/file_from_%s\n", peer.Hostname()), + ) + } + }) + } + } + + err = scenario.Shutdown() + if err != nil { + t.Errorf("failed to tear down scenario: %s", err) + } +} diff --git a/integration/tailscale.go b/integration/tailscale.go index 2d3ec72a..298817e3 100644 --- a/integration/tailscale.go +++ b/integration/tailscale.go @@ -10,6 +10,7 @@ type TailscaleClient interface { Hostname() string Shutdown() error Version() string + Execute(command []string) (string, error) Up(loginServer, authKey string) error IPs() ([]netip.Addr, error) FQDN() (string, error) diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go index d62ea1de..11ce7c4a 100644 --- a/integration/tsic/tsic.go +++ b/integration/tsic/tsic.go @@ -34,6 +34,10 @@ type TailscaleInContainer struct { pool *dockertest.Pool container *dockertest.Resource network *dockertest.Network + + // "cache" + ips []netip.Addr + fqdn string } func New( @@ -104,6 +108,33 @@ func (t *TailscaleInContainer) Version() string { return t.version } +func (t *TailscaleInContainer) Execute( + command []string, +) (string, error) { + log.Println("command", command) + log.Printf("running command for %s\n", t.hostname) + stdout, stderr, err := dockertestutil.ExecuteCommand( + t.container, + command, + []string{}, + ) + if err != nil { + log.Printf("command stderr: %s\n", stderr) + + if strings.Contains(stderr, "NeedsLogin") { + return "", errTailscaleNotLoggedIn + } + + return "", err + } + + if stdout != "" { + log.Printf("command stdout: %s\n", stdout) + } + + return stdout, nil +} + func (t *TailscaleInContainer) Up( loginServer, authKey string, ) error { @@ -118,30 +149,19 @@ func (t *TailscaleInContainer) Up( t.hostname, } - log.Println("Join command:", command) - log.Printf("Running join command for %s\n", t.hostname) - stdout, stderr, err := dockertestutil.ExecuteCommand( - t.container, - command, - []string{}, - ) - if err != nil { - log.Printf("tailscale join stderr: %s\n", stderr) - - return err + if _, err := t.Execute(command); err != nil { + return fmt.Errorf("failed to join tailscale client: %w", err) } - if stdout != "" { - log.Printf("tailscale join stdout: %s\n", stdout) - } - - log.Printf("%s joined\n", t.hostname) - return nil } // TODO(kradalby): Make cached/lazy. func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) { + if t.ips != nil && len(t.ips) != 0 { + return t.ips, nil + } + ips := make([]netip.Addr, 0) command := []string{ @@ -149,19 +169,9 @@ func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) { "ip", } - result, stderr, err := dockertestutil.ExecuteCommand( - t.container, - command, - []string{}, - ) + result, err := t.Execute(command) if err != nil { - log.Printf("failed commands stderr: %s\n", stderr) - - if strings.Contains(stderr, "NeedsLogin") { - return []netip.Addr{}, errTailscaleNotLoggedIn - } - - return []netip.Addr{}, err + return []netip.Addr{}, fmt.Errorf("failed to join tailscale client: %w", err) } for _, address := range strings.Split(result, "\n") { @@ -186,11 +196,7 @@ func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) { "--json", } - result, _, err := dockertestutil.ExecuteCommand( - t.container, - command, - []string{}, - ) + result, err := t.Execute(command) if err != nil { return nil, fmt.Errorf("failed to execute tailscale status command: %w", err) } @@ -205,6 +211,10 @@ func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) { } func (t *TailscaleInContainer) FQDN() (string, error) { + if t.fqdn != "" { + return t.fqdn, nil + } + status, err := t.Status() if err != nil { return "", fmt.Errorf("failed to get FQDN: %w", err) @@ -239,11 +249,7 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string) error { hostnameOrIP, } - result, _, err := dockertestutil.ExecuteCommand( - t.container, - command, - []string{}, - ) + result, err := t.Execute(command) if err != nil { log.Printf( "failed to run ping command from %s to %s, err: %s",