Get integration test netmap from watch-ipn command (#1729)

This commit is contained in:
Kristoffer Dalby 2024-02-18 23:22:18 +01:00 committed by GitHub
parent 3f162c212c
commit 5dbd59ca55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 244 additions and 34 deletions

View file

@ -212,15 +212,9 @@ func (h *Headscale) handlePoll(
return
}
// TODO(kradalby): Figure out why patch changes does
// not show up in output from `tailscale debug netmap`.
// stateUpdate := types.StateUpdate{
// Type: types.StatePeerChangedPatch,
// ChangePatches: []*tailcfg.PeerChange{&change},
// }
stateUpdate := types.StateUpdate{
Type: types.StatePeerChanged,
ChangeNodes: types.Nodes{node},
Type: types.StatePeerChangedPatch,
ChangePatches: []*tailcfg.PeerChange{&change},
}
if stateUpdate.Valid() {
ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-peers-patch", node.Hostname)

13
hscontrol/util/util.go Normal file
View file

@ -0,0 +1,13 @@
package util
import "tailscale.com/util/cmpver"
func TailscaleVersionNewerOrEqual(minimum, toCheck string) bool {
if cmpver.Compare(minimum, toCheck) <= 0 ||
toCheck == "unstable" ||
toCheck == "head" {
return true
}
return false
}

View file

@ -0,0 +1,95 @@
package util
import "testing"
func TestTailscaleVersionNewerOrEqual(t *testing.T) {
type args struct {
minimum string
toCheck string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "is-equal",
args: args{
minimum: "1.56",
toCheck: "1.56",
},
want: true,
},
{
name: "is-newer-head",
args: args{
minimum: "1.56",
toCheck: "head",
},
want: true,
},
{
name: "is-newer-unstable",
args: args{
minimum: "1.56",
toCheck: "unstable",
},
want: true,
},
{
name: "is-newer-patch",
args: args{
minimum: "1.56.1",
toCheck: "1.56.1",
},
want: true,
},
{
name: "is-older-patch-same-minor",
args: args{
minimum: "1.56.1",
toCheck: "1.56.0",
},
want: false,
},
{
name: "is-older-unstable",
args: args{
minimum: "1.56",
toCheck: "1.55",
},
want: false,
},
{
name: "is-older-one-stable",
args: args{
minimum: "1.56",
toCheck: "1.54",
},
want: false,
},
{
name: "is-older-five-stable",
args: args{
minimum: "1.56",
toCheck: "1.46",
},
want: false,
},
{
name: "is-older-patch",
args: args{
minimum: "1.56",
toCheck: "1.48.1",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := TailscaleVersionNewerOrEqual(tt.args.minimum, tt.args.toCheck); got != tt.want {
t.Errorf("TailscaleVersionNewerThan() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -70,8 +70,6 @@ func TestDERPServerScenario(t *testing.T) {
err = scenario.WaitForTailscaleSync()
assertNoErrSync(t, err)
assertClientsState(t, allClients)
allHostnames, err := scenario.ListTailscaleClientsFQDNs()
assertNoErrListFQDN(t, err)

View file

@ -1,9 +1,11 @@
package tsic
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/netip"
"net/url"
@ -16,6 +18,7 @@ import (
"github.com/juanfont/headscale/integration/integrationutil"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/netcheck"
"tailscale.com/types/netmap"
@ -522,27 +525,122 @@ func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) {
}
// Netmap returns the current Netmap (netmap.NetworkMap) of the Tailscale instance.
// Only works with Tailscale 1.56.1 and newer.
// Only works with Tailscale 1.56 and newer.
// Panics if version is lower then minimum.
// func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) {
// if !util.TailscaleVersionNewerOrEqual("1.56", t.version) {
// panic(fmt.Sprintf("tsic.Netmap() called with unsupported version: %s", t.version))
// }
// command := []string{
// "tailscale",
// "debug",
// "netmap",
// }
// result, stderr, err := t.Execute(command)
// if err != nil {
// fmt.Printf("stderr: %s\n", stderr)
// return nil, fmt.Errorf("failed to execute tailscale debug netmap command: %w", err)
// }
// var nm netmap.NetworkMap
// err = json.Unmarshal([]byte(result), &nm)
// if err != nil {
// return nil, fmt.Errorf("failed to unmarshal tailscale netmap: %w", err)
// }
// return &nm, err
// }
// Netmap returns the current Netmap (netmap.NetworkMap) of the Tailscale instance.
// This implementation is based on getting the netmap from `tailscale debug watch-ipn`
// as there seem to be some weirdness omitting endpoint and DERP info if we use
// Patch updates.
// This implementation works on all supported versions.
func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) {
command := []string{
"tailscale",
"debug",
"netmap",
}
// watch-ipn will only give an update if something is happening,
// since we send keep alives, the worst case for this should be
// 1 minute, but set a slightly more conservative time.
ctx, _ := context.WithTimeout(context.Background(), 3*time.Minute)
result, stderr, err := t.Execute(command)
notify, err := t.watchIPN(ctx)
if err != nil {
fmt.Printf("stderr: %s\n", stderr)
return nil, fmt.Errorf("failed to execute tailscale debug netmap command: %w", err)
return nil, err
}
var nm netmap.NetworkMap
err = json.Unmarshal([]byte(result), &nm)
if notify.NetMap == nil {
return nil, fmt.Errorf("no netmap present in ipn.Notify")
}
return notify.NetMap, nil
}
// watchIPN watches `tailscale debug watch-ipn` for a ipn.Notify object until
// it gets one that has a netmap.NetworkMap.
func (t *TailscaleInContainer) watchIPN(ctx context.Context) (*ipn.Notify, error) {
pr, pw := io.Pipe()
type result struct {
notify *ipn.Notify
err error
}
resultChan := make(chan result, 1)
// There is no good way to kill the goroutine with watch-ipn,
// so make a nice func to send a kill command to issue when
// we are done.
killWatcher := func() {
stdout, stderr, err := t.Execute([]string{
"/bin/sh", "-c", `kill $(ps aux | grep "tailscale debug watch-ipn" | grep -v grep | awk '{print $1}') || true`,
})
if err != nil {
return nil, fmt.Errorf("failed to unmarshal tailscale netmap: %w", err)
log.Printf("failed to kill tailscale watcher, \nstdout: %s\nstderr: %s\nerr: %s", stdout, stderr, err)
}
}
return &nm, err
go func() {
_, _ = t.container.Exec(
// Prior to 1.56, the initial "Connected." message was printed to stdout,
// filter out with grep.
[]string{"/bin/sh", "-c", `tailscale debug watch-ipn | grep -v "Connected."`},
dockertest.ExecOptions{
// The interesting output is sent to stdout, so ignore stderr.
StdOut: pw,
// StdErr: pw,
},
)
}()
go func() {
decoder := json.NewDecoder(pr)
for decoder.More() {
var notify ipn.Notify
if err := decoder.Decode(&notify); err != nil {
resultChan <- result{nil, fmt.Errorf("parse notify: %w", err)}
}
if notify.NetMap != nil {
resultChan <- result{&notify, nil}
}
}
}()
select {
case <-ctx.Done():
killWatcher()
return nil, ctx.Err()
case result := <-resultChan:
killWatcher()
if result.err != nil {
return nil, result.err
}
return result.notify, nil
}
}
// Netcheck returns the current Netcheck Report (netcheck.Report) of the Tailscale instance.

View file

@ -3,12 +3,12 @@ package integration
import (
"os"
"strings"
"sync"
"testing"
"time"
"github.com/juanfont/headscale/integration/tsic"
"github.com/stretchr/testify/assert"
"tailscale.com/util/cmpver"
)
const (
@ -127,11 +127,21 @@ func pingDerpAllHelper(t *testing.T, clients []TailscaleClient, addrs []string)
func assertClientsState(t *testing.T, clients []TailscaleClient) {
t.Helper()
var wg sync.WaitGroup
for _, client := range clients {
assertValidStatus(t, client)
assertValidNetmap(t, client)
assertValidNetcheck(t, client)
wg.Add(1)
c := client // Avoid loop pointer
go func() {
defer wg.Done()
assertValidStatus(t, c)
assertValidNetcheck(t, c)
assertValidNetmap(t, c)
}()
}
t.Logf("waiting for client state checks to finish")
wg.Wait()
}
// assertValidNetmap asserts that the netmap of a client has all
@ -144,11 +154,13 @@ func assertClientsState(t *testing.T, clients []TailscaleClient) {
func assertValidNetmap(t *testing.T, client TailscaleClient) {
t.Helper()
if cmpver.Compare("1.56.1", client.Version()) <= 0 ||
!strings.Contains(client.Hostname(), "unstable") ||
!strings.Contains(client.Hostname(), "head") {
return
}
// if !util.TailscaleVersionNewerOrEqual("1.56", client.Version()) {
// t.Logf("%q has version %q, skipping netmap check...", client.Hostname(), client.Version())
// return
// }
t.Logf("Checking netmap of %q", client.Hostname())
netmap, err := client.Netmap()
if err != nil {
@ -177,7 +189,7 @@ func assertValidNetmap(t *testing.T, client TailscaleClient) {
assert.LessOrEqualf(t, 3, peer.Hostinfo().Services().Len(), "peer (%s) of %q does not have enough services, got: %v", peer.ComputedName(), client.Hostname(), peer.Hostinfo().Services())
// Netinfo is not always set
assert.Truef(t, hi.NetInfo().Valid(), "peer (%s) of %q does not have NetInfo", peer.ComputedName(), client.Hostname())
// assert.Truef(t, hi.NetInfo().Valid(), "peer (%s) of %q does not have NetInfo", peer.ComputedName(), client.Hostname())
if ni := hi.NetInfo(); ni.Valid() {
assert.NotEqualf(t, 0, ni.PreferredDERP(), "peer (%s) has no home DERP in %q's netmap, got: %s", peer.ComputedName(), client.Hostname(), peer.Hostinfo().NetInfo().PreferredDERP())
}