Compare commits

...

29 Commits

Author SHA1 Message Date
Juan Font
70e08462b3 Merge branch 'main' into web-auth-flow-tests 2022-11-13 16:51:53 +01:00
Juan Font Alonso
a231ece825 Added ts2019 buildtag to CI config
Otherwise we are getting utils.go:119:6: `decode` is unused (deadcode)

Signed-off-by: Juan Font Alonso <juanfontalonso@gmail.com>
2022-11-13 13:34:08 +01:00
Juan Font Alonso
ae43d82a33 Fixed gofumpt linting 2022-11-13 13:18:00 +01:00
Juan Font Alonso
ad3c36fd07 Wait for the tailscale clients to be ready in the web auth CreateHeadscaleEnv
Signed-off-by: Juan Font Alonso <juanfontalonso@gmail.com>
2022-11-13 13:13:29 +01:00
Juan Font Alonso
f176503448 Added WaitForReady() to Tailscale interface
When using running `tailscale up` in the AuthKey flow process, the tailscale client immediately enters PollMap after registration - avoiding a race condition.

When using the web auth (up -> go to the Control website -> CLI `register`) the client is polling checking if it has been authorized. If we immediately ask for the client IP, as done in CreateHeadscaleEnv() we might have the client in NotReady status.

This method provides a way to wait for the client to be ready.

Signed-off-by: Juan Font Alonso <juanfontalonso@gmail.com>
2022-11-13 13:13:29 +01:00
Juan Font Alonso
f7ad88aa08 Print the name of the registered machine 2022-11-13 13:13:29 +01:00
Juan Font Alonso
f63d22655c Fixed conflict 2022-11-13 13:13:29 +01:00
Juan Font Alonso
89c468fc43 Added makefile and .github 2022-11-13 13:13:29 +01:00
Kristoffer Dalby
b0fda6b216 Merge branch 'main' into web-auth-flow-tests 2022-11-10 16:15:17 +00:00
Juan Font Alonso
154fb59bdb Run auth test in main actions file 2022-11-04 16:14:57 +01:00
Juan Font
d3e9703fb5 Update integration/auth_web_flow_test.go
Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2022-11-04 16:11:10 +01:00
Juan Font
7ce3f8c7d1 Update Makefile
Co-authored-by: Kristoffer Dalby <kristoffer@dalby.cc>
2022-11-04 16:10:59 +01:00
Kristoffer Dalby
58c8633cc1 Merge branch 'main' into web-auth-flow-tests 2022-11-04 13:05:39 +01:00
Juan Font Alonso
b3f5af30a4 Linting fixes + disable interfacebloat linter 2022-11-04 11:41:54 +01:00
Kristoffer Dalby
9f64ac8a33 Fix bitrotted versions in gh ci
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-04 11:41:54 +01:00
Kristoffer Dalby
aa1cc05cfb Run on correct change
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-04 11:41:54 +01:00
Kristoffer Dalby
670ef9a93e Add experimental kradalby gh runner
Remove old v2 runner in favour of self-hosted

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2022-11-04 11:41:54 +01:00
Juan Font Alonso
987abcfdce More linting fixes 2022-11-04 00:27:00 +01:00
Juan Font Alonso
c70f5696dc Linting fixes 2022-11-04 00:23:20 +01:00
Juan Font Alonso
825e88311e Renamed integration tests for auth 2022-11-04 00:16:19 +01:00
Juan Font Alonso
bbc8cb11da Added makefile and .github 2022-11-04 00:11:34 +01:00
Juan Font Alonso
3a6ef6bece Work in progress for web auth flow 2022-11-04 00:10:36 +01:00
Juan Font Alonso
b2dc480f22 Return the real port of the container 2022-11-04 00:05:01 +01:00
Juan Font Alonso
5d7eae46f8 Always attempt to parse login url 2022-11-04 00:04:04 +01:00
Juan Font Alonso
45cb0f3fa3 Typo 2022-11-03 23:59:06 +01:00
Juan Font Alonso
658478cba3 Add web flow auth basic integration test 2022-11-03 17:40:09 +01:00
Juan Font Alonso
ec90e9d716 Update Tailscale interface with new Execute signature 2022-11-03 17:00:23 +01:00
Juan Font Alonso
181f1eeb4f Added method to run tailscale up without authkey 2022-11-03 16:56:19 +01:00
Juan Font Alonso
e270cf6d20 Return stderr in tsic.Execute 2022-11-03 16:50:20 +01:00
9 changed files with 306 additions and 18 deletions

View File

@@ -0,0 +1,35 @@
name: Integration Test v2
on: [pull_request]
jobs:
integration-test-v2:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v14.1
with:
files: |
*.nix
go.*
**/*.go
integration_test/
config-example.yaml
- uses: cachix/install-nix-action@v16
if: steps.changed-files.outputs.any_changed == 'true'
- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
run: nix develop --command -- make test_integration_v2_auth_web_flow

View File

@@ -36,6 +36,9 @@ linters:
- makezero
- maintidx
# Limits the methods of an interface to 10. We have more in integration tests
- interfacebloat
# We might want to enable this, but it might be a lot of work
- cyclop
- nestif

View File

@@ -68,6 +68,17 @@ test_integration_v2_general:
golang:1 \
go test $(TAGS) -failfast ./... -timeout 60m -parallel 6
test_integration_v2_auth_web_flow:
docker run \
-t --rm \
-v ~/.cache/hs-integration-go:/go \
--name headscale-test-suite \
-v $$PWD:$$PWD -w $$PWD/integration \
-v /var/run/docker.sock:/var/run/docker.sock \
golang:1 \
go test ./... -timeout 60m -parallel 6 -run TestAuthWebFlow
coverprofile_func:
go tool cover -func=coverage.out

View File

@@ -134,7 +134,7 @@ var registerNodeCmd = &cobra.Command{
return
}
SuccessOutput(response.Machine, "Machine register", output)
SuccessOutput(response.Machine, fmt.Sprintf("Machine %s registered", response.Machine.GivenName), output)
},
}

View File

@@ -0,0 +1,192 @@
package integration
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strings"
"testing"
)
var errParseAuthPage = errors.New("failed to parse auth page")
type AuthWebFlowScenario struct {
*Scenario
}
func TestAuthWebFlowAuthenticationPingAll(t *testing.T) {
IntegrationSkip(t)
baseScenario, err := NewScenario()
if err != nil {
t.Errorf("failed to create scenario: %s", err)
}
scenario := AuthWebFlowScenario{
Scenario: baseScenario,
}
spec := map[string]int{
"namespace1": len(TailscaleVersions),
"namespace2": len(TailscaleVersions),
}
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)
}
allIps, err := scenario.ListTailscaleClientsIPs()
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)
}
success := 0
for _, client := range allClients {
for _, ip := range allIps {
err := client.Ping(ip.String())
if err != nil {
t.Errorf("failed to ping %s from %s: %s", ip, client.Hostname(), err)
} else {
success++
}
}
}
t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps))
err = scenario.Shutdown()
if err != nil {
t.Errorf("failed to tear down scenario: %s", err)
}
}
func (s *AuthWebFlowScenario) CreateHeadscaleEnv(namespaces map[string]int) error {
err := s.StartHeadscale()
if err != nil {
return err
}
err = s.Headscale().WaitForReady()
if err != nil {
return err
}
for namespaceName, clientCount := range namespaces {
log.Printf("creating namespace %s with %d clients", namespaceName, clientCount)
err = s.CreateNamespace(namespaceName)
if err != nil {
return err
}
err = s.CreateTailscaleNodesInNamespace(namespaceName, "all", clientCount)
if err != nil {
return err
}
err = s.runTailscaleUp(namespaceName, s.Headscale().GetEndpoint())
if err != nil {
return err
}
}
return nil
}
func (s *AuthWebFlowScenario) runTailscaleUp(
namespaceStr, loginServer string,
) error {
log.Printf("running tailscale up for namespace %s", namespaceStr)
if namespace, ok := s.namespaces[namespaceStr]; ok {
for _, client := range namespace.Clients {
namespace.joinWaitGroup.Add(1)
go func(c TailscaleClient) {
defer namespace.joinWaitGroup.Done()
// TODO(juanfont): error handle this
loginURL, err := c.UpWithLoginURL(loginServer)
if err != nil {
log.Printf("failed to run tailscale up: %s", err)
}
err = s.runHeadscaleRegister(namespaceStr, loginURL)
if err != nil {
log.Printf("failed to register client: %s", err)
}
err = c.WaitForReady()
if err != nil {
log.Printf("error waiting for client %s to be ready: %s", c.Hostname(), err)
}
}(client)
}
namespace.joinWaitGroup.Wait()
return nil
}
return fmt.Errorf("failed to up tailscale node: %w", errNoNamespaceAvailable)
}
func (s *AuthWebFlowScenario) runHeadscaleRegister(namespaceStr string, loginURL *url.URL) error {
log.Printf("loginURL: %s", loginURL)
loginURL.Host = fmt.Sprintf("%s:8080", s.Headscale().GetIP())
loginURL.Scheme = "http"
httpClient := &http.Client{}
ctx := context.Background()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil)
resp, err := httpClient.Do(req)
if err != nil {
return err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
defer resp.Body.Close()
// see api.go HTML template
codeSep := strings.Split(string(body), "</code>")
if len(codeSep) != 2 {
return errParseAuthPage
}
keySep := strings.Split(codeSep[0], "key ")
if len(keySep) != 2 {
return errParseAuthPage
}
key := keySep[1]
log.Printf("registering node %s", key)
if headscale, ok := s.controlServers["headscale"]; ok {
_, err = headscale.Execute([]string{"headscale", "-n", namespaceStr, "nodes", "register", "--key", key})
if err != nil {
log.Printf("failed to register node: %s", err)
return err
}
return nil
}
return fmt.Errorf("failed to find headscale: %w", errNoHeadscaleAvailable)
}

View File

@@ -168,7 +168,7 @@ func TestTaildrop(t *testing.T) {
for _, client := range allClients {
command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", client.Hostname())}
if _, err := client.Execute(command); err != nil {
if _, _, err := client.Execute(command); err != nil {
t.Errorf("failed to create taildrop file on %s, err: %s", client.Hostname(), err)
}
@@ -193,7 +193,7 @@ func TestTaildrop(t *testing.T) {
client.Hostname(),
peer.Hostname(),
)
_, err := client.Execute(command)
_, _, err := client.Execute(command)
return err
})
@@ -214,7 +214,7 @@ func TestTaildrop(t *testing.T) {
"get",
"/tmp/",
}
if _, err := client.Execute(command); err != nil {
if _, _, err := client.Execute(command); err != nil {
t.Errorf("failed to get taildrop file on %s, err: %s", client.Hostname(), err)
}
@@ -234,7 +234,7 @@ func TestTaildrop(t *testing.T) {
peer.Hostname(),
)
result, err := client.Execute(command)
result, _, err := client.Execute(command)
if err != nil {
t.Errorf("failed to execute command to ls taildrop: %s", err)
}
@@ -306,7 +306,7 @@ func TestResolveMagicDNS(t *testing.T) {
"tailscale",
"ip", peerFQDN,
}
result, err := client.Execute(command)
result, _, err := client.Execute(command)
if err != nil {
t.Errorf(
"failed to execute resolve/ip command %s from %s: %s",

View File

@@ -179,9 +179,7 @@ func (t *HeadscaleInContainer) GetIP() string {
}
func (t *HeadscaleInContainer) GetPort() string {
portProto := fmt.Sprintf("%d/tcp", t.port)
return t.container.GetPort(portProto)
return fmt.Sprintf("%d", t.port)
}
func (t *HeadscaleInContainer) GetHealthEndpoint() string {

View File

@@ -2,6 +2,7 @@ package integration
import (
"net/netip"
"net/url"
"tailscale.com/ipn/ipnstate"
)
@@ -10,11 +11,13 @@ type TailscaleClient interface {
Hostname() string
Shutdown() error
Version() string
Execute(command []string) (string, error)
Execute(command []string) (string, string, error)
Up(loginServer, authKey string) error
UpWithLoginURL(loginServer string) (*url.URL, error)
IPs() ([]netip.Addr, error)
FQDN() (string, error)
Status() (*ipnstate.Status, error)
WaitForReady() error
WaitForPeers(expected int) error
Ping(hostnameOrIP string) error
}

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"log"
"net/netip"
"net/url"
"strings"
"github.com/cenkalti/backoff/v4"
@@ -25,6 +26,7 @@ var (
errTailscalePingFailed = errors.New("ping failed")
errTailscaleNotLoggedIn = errors.New("tailscale not logged in")
errTailscaleWrongPeerCount = errors.New("wrong peer count")
errTailscaleNotConnected = errors.New("tailscale not connected")
)
type TailscaleInContainer struct {
@@ -110,7 +112,7 @@ func (t *TailscaleInContainer) Version() string {
func (t *TailscaleInContainer) Execute(
command []string,
) (string, error) {
) (string, string, error) {
log.Println("command", command)
log.Printf("running command for %s\n", t.hostname)
stdout, stderr, err := dockertestutil.ExecuteCommand(
@@ -126,13 +128,13 @@ func (t *TailscaleInContainer) Execute(
}
if strings.Contains(stderr, "NeedsLogin") {
return "", errTailscaleNotLoggedIn
return stdout, stderr, errTailscaleNotLoggedIn
}
return "", err
return stdout, stderr, err
}
return stdout, nil
return stdout, stderr, nil
}
func (t *TailscaleInContainer) Up(
@@ -149,13 +151,42 @@ func (t *TailscaleInContainer) Up(
t.hostname,
}
if _, err := t.Execute(command); err != nil {
if _, _, err := t.Execute(command); err != nil {
return fmt.Errorf("failed to join tailscale client: %w", err)
}
return nil
}
func (t *TailscaleInContainer) UpWithLoginURL(
loginServer string,
) (*url.URL, error) {
command := []string{
"tailscale",
"up",
"-login-server",
loginServer,
"--hostname",
t.hostname,
}
_, stderr, _ := t.Execute(command)
urlStr := strings.ReplaceAll(stderr, "\nTo authenticate, visit:\n\n\t", "")
urlStr = strings.TrimSpace(urlStr)
// parse URL
loginURL, err := url.Parse(urlStr)
if err != nil {
log.Printf("Could not parse login URL: %s", err)
log.Printf("Original join command result: %s", stderr)
return nil, err
}
return loginURL, nil
}
func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
if t.ips != nil && len(t.ips) != 0 {
return t.ips, nil
@@ -168,7 +199,7 @@ func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
"ip",
}
result, err := t.Execute(command)
result, _, err := t.Execute(command)
if err != nil {
return []netip.Addr{}, fmt.Errorf("failed to join tailscale client: %w", err)
}
@@ -195,7 +226,7 @@ func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) {
"--json",
}
result, err := t.Execute(command)
result, _, err := t.Execute(command)
if err != nil {
return nil, fmt.Errorf("failed to execute tailscale status command: %w", err)
}
@@ -222,6 +253,21 @@ func (t *TailscaleInContainer) FQDN() (string, error) {
return status.Self.DNSName, nil
}
func (t *TailscaleInContainer) WaitForReady() error {
return t.pool.Retry(func() error {
status, err := t.Status()
if err != nil {
return fmt.Errorf("failed to fetch tailscale status: %w", err)
}
if status.CurrentTailnet != nil {
return nil
}
return errTailscaleNotConnected
})
}
func (t *TailscaleInContainer) WaitForPeers(expected int) error {
return t.pool.Retry(func() error {
status, err := t.Status()
@@ -248,7 +294,7 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string) error {
hostnameOrIP,
}
result, err := t.Execute(command)
result, _, err := t.Execute(command)
if err != nil {
log.Printf(
"failed to run ping command from %s to %s, err: %s",