mirror of
https://github.com/juanfont/headscale.git
synced 2024-12-20 19:09:07 +01:00
462 lines
11 KiB
Go
462 lines
11 KiB
Go
package integration
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net/netip"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/juanfont/headscale"
|
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
|
"github.com/juanfont/headscale/integration/dockertestutil"
|
|
"github.com/juanfont/headscale/integration/hsic"
|
|
"github.com/juanfont/headscale/integration/tsic"
|
|
"github.com/ory/dockertest/v3"
|
|
"github.com/puzpuzpuz/xsync/v2"
|
|
)
|
|
|
|
const (
|
|
scenarioHashLength = 6
|
|
maxWait = 60 * time.Second
|
|
)
|
|
|
|
var (
|
|
errNoHeadscaleAvailable = errors.New("no headscale available")
|
|
errNoNamespaceAvailable = errors.New("no namespace available")
|
|
|
|
// Tailscale started adding TS2021 support in CapabilityVersion>=28 (v1.24.0), but
|
|
// proper support in Headscale was only added for CapabilityVersion>=39 clients (v1.30.0).
|
|
tailscaleVersions2021 = []string{
|
|
"head",
|
|
"unstable",
|
|
"1.32.1",
|
|
"1.30.2",
|
|
}
|
|
|
|
tailscaleVersions2019 = []string{
|
|
"1.28.0",
|
|
"1.26.2",
|
|
"1.24.2",
|
|
"1.22.2",
|
|
"1.20.4",
|
|
"1.18.2",
|
|
"1.16.2",
|
|
}
|
|
|
|
// tailscaleVersionsUnavailable = []string{
|
|
// // These versions seem to fail when fetching from apt.
|
|
// "1.14.6",
|
|
// "1.12.4",
|
|
// "1.10.2",
|
|
// "1.8.7",
|
|
// }.
|
|
|
|
TailscaleVersions = append(
|
|
tailscaleVersions2021,
|
|
tailscaleVersions2019...,
|
|
)
|
|
)
|
|
|
|
type Namespace struct {
|
|
Clients map[string]TailscaleClient
|
|
|
|
createWaitGroup sync.WaitGroup
|
|
joinWaitGroup sync.WaitGroup
|
|
syncWaitGroup sync.WaitGroup
|
|
}
|
|
|
|
// TODO(kradalby): make control server configurable, test correctness with Tailscale SaaS.
|
|
type Scenario struct {
|
|
// TODO(kradalby): support multiple headcales for later, currently only
|
|
// use one.
|
|
controlServers *xsync.MapOf[string, ControlServer]
|
|
|
|
namespaces map[string]*Namespace
|
|
|
|
pool *dockertest.Pool
|
|
network *dockertest.Network
|
|
|
|
headscaleLock sync.Mutex
|
|
}
|
|
|
|
func NewScenario() (*Scenario, error) {
|
|
hash, err := headscale.GenerateRandomStringDNSSafe(scenarioHashLength)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pool, err := dockertest.NewPool("")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not connect to docker: %w", err)
|
|
}
|
|
|
|
pool.MaxWait = maxWait
|
|
|
|
networkName := fmt.Sprintf("hs-%s", hash)
|
|
if overrideNetworkName := os.Getenv("HEADSCALE_TEST_NETWORK_NAME"); overrideNetworkName != "" {
|
|
networkName = overrideNetworkName
|
|
}
|
|
|
|
network, err := dockertestutil.GetFirstOrCreateNetwork(pool, networkName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create or get network: %w", err)
|
|
}
|
|
|
|
// We run the test suite in a docker container that calls a couple of endpoints for
|
|
// readiness checks, this ensures that we can run the tests with individual networks
|
|
// and have the client reach the different containers
|
|
err = dockertestutil.AddContainerToNetwork(pool, network, "headscale-test-suite")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to add test suite container to network: %w", err)
|
|
}
|
|
|
|
return &Scenario{
|
|
controlServers: xsync.NewMapOf[ControlServer](),
|
|
namespaces: make(map[string]*Namespace),
|
|
|
|
pool: pool,
|
|
network: network,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Scenario) Shutdown() error {
|
|
s.controlServers.Range(func(_ string, control ControlServer) bool {
|
|
err := control.Shutdown()
|
|
if err != nil {
|
|
log.Printf(
|
|
"Failed to shut down control: %s",
|
|
fmt.Errorf("failed to tear down control: %w", err),
|
|
)
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
for namespaceName, namespace := range s.namespaces {
|
|
for _, client := range namespace.Clients {
|
|
log.Printf("removing client %s in namespace %s", client.Hostname(), namespaceName)
|
|
err := client.Shutdown()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to tear down client: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := s.pool.RemoveNetwork(s.network); err != nil {
|
|
return fmt.Errorf("failed to remove network: %w", err)
|
|
}
|
|
|
|
// TODO(kradalby): This seem redundant to the previous call
|
|
// if err := s.network.Close(); err != nil {
|
|
// return fmt.Errorf("failed to tear down network: %w", err)
|
|
// }
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Scenario) Namespaces() []string {
|
|
namespaces := make([]string, 0)
|
|
for namespace := range s.namespaces {
|
|
namespaces = append(namespaces, namespace)
|
|
}
|
|
|
|
return namespaces
|
|
}
|
|
|
|
/// Headscale related stuff
|
|
// Note: These functions assume that there is a _single_ headscale instance for now
|
|
|
|
// TODO(kradalby): make port and headscale configurable, multiple instances support?
|
|
func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) {
|
|
s.headscaleLock.Lock()
|
|
defer s.headscaleLock.Unlock()
|
|
|
|
if headscale, ok := s.controlServers.Load("headscale"); ok {
|
|
return headscale, nil
|
|
}
|
|
|
|
headscale, err := hsic.New(s.pool, s.network, opts...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create headscale container: %w", err)
|
|
}
|
|
|
|
err = headscale.WaitForReady()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed reach headscale container: %w", err)
|
|
}
|
|
|
|
s.controlServers.Store("headscale", headscale)
|
|
|
|
return headscale, nil
|
|
}
|
|
|
|
func (s *Scenario) CreatePreAuthKey(namespace string) (*v1.PreAuthKey, error) {
|
|
if headscale, err := s.Headscale(); err == nil {
|
|
key, err := headscale.CreateAuthKey(namespace)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create namespace: %w", err)
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to create namespace: %w", errNoHeadscaleAvailable)
|
|
}
|
|
|
|
func (s *Scenario) CreateNamespace(namespace string) error {
|
|
if headscale, err := s.Headscale(); err == nil {
|
|
err := headscale.CreateNamespace(namespace)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create namespace: %w", err)
|
|
}
|
|
|
|
s.namespaces[namespace] = &Namespace{
|
|
Clients: make(map[string]TailscaleClient),
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("failed to create namespace: %w", errNoHeadscaleAvailable)
|
|
}
|
|
|
|
/// Client related stuff
|
|
|
|
func (s *Scenario) CreateTailscaleNodesInNamespace(
|
|
namespaceStr string,
|
|
requestedVersion string,
|
|
count int,
|
|
) error {
|
|
if namespace, ok := s.namespaces[namespaceStr]; ok {
|
|
for i := 0; i < count; i++ {
|
|
version := requestedVersion
|
|
if requestedVersion == "all" {
|
|
version = TailscaleVersions[i%len(TailscaleVersions)]
|
|
}
|
|
|
|
headscale, err := s.Headscale()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create tailscale node: %w", err)
|
|
}
|
|
|
|
cert := headscale.GetCert()
|
|
hostname := headscale.GetHostname()
|
|
|
|
namespace.createWaitGroup.Add(1)
|
|
|
|
go func() {
|
|
defer namespace.createWaitGroup.Done()
|
|
|
|
// TODO(kradalby): error handle this
|
|
tsClient, err := tsic.New(
|
|
s.pool,
|
|
version,
|
|
s.network,
|
|
tsic.WithHeadscaleTLS(cert),
|
|
tsic.WithHeadscaleName(hostname),
|
|
)
|
|
if err != nil {
|
|
// return fmt.Errorf("failed to add tailscale node: %w", err)
|
|
log.Printf("failed to create tailscale node: %s", err)
|
|
}
|
|
|
|
err = tsClient.WaitForReady()
|
|
if err != nil {
|
|
// return fmt.Errorf("failed to add tailscale node: %w", err)
|
|
log.Printf("failed to wait for tailscaled: %s", err)
|
|
}
|
|
|
|
namespace.Clients[tsClient.Hostname()] = tsClient
|
|
}()
|
|
}
|
|
namespace.createWaitGroup.Wait()
|
|
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("failed to add tailscale node: %w", errNoNamespaceAvailable)
|
|
}
|
|
|
|
func (s *Scenario) RunTailscaleUp(
|
|
namespaceStr, loginServer, authKey string,
|
|
) error {
|
|
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(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)
|
|
}
|
|
}
|
|
|
|
namespace.joinWaitGroup.Wait()
|
|
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("failed to up tailscale node: %w", errNoNamespaceAvailable)
|
|
}
|
|
|
|
func (s *Scenario) CountTailscale() int {
|
|
count := 0
|
|
|
|
for _, namespace := range s.namespaces {
|
|
count += len(namespace.Clients)
|
|
}
|
|
|
|
return count
|
|
}
|
|
|
|
func (s *Scenario) WaitForTailscaleSync() error {
|
|
tsCount := s.CountTailscale()
|
|
|
|
for _, namespace := range s.namespaces {
|
|
for _, client := range namespace.Clients {
|
|
namespace.syncWaitGroup.Add(1)
|
|
|
|
go func(c TailscaleClient) {
|
|
defer namespace.syncWaitGroup.Done()
|
|
|
|
// TODO(kradalby): error handle this
|
|
_ = c.WaitForPeers(tsCount)
|
|
}(client)
|
|
}
|
|
namespace.syncWaitGroup.Wait()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CreateHeadscaleEnv is a conventient method returning a set up Headcale
|
|
// test environment with nodes of all versions, joined to the server with X
|
|
// namespaces.
|
|
func (s *Scenario) CreateHeadscaleEnv(namespaces map[string]int, opts ...hsic.Option) error {
|
|
headscale, err := s.Headscale(opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for namespaceName, clientCount := range namespaces {
|
|
err = s.CreateNamespace(namespaceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = s.CreateTailscaleNodesInNamespace(namespaceName, "all", clientCount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
key, err := s.CreatePreAuthKey(namespaceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = s.RunTailscaleUp(namespaceName, headscale.GetEndpoint(), key.GetKey())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Scenario) GetIPs(namespace string) ([]netip.Addr, error) {
|
|
var ips []netip.Addr
|
|
if ns, ok := s.namespaces[namespace]; ok {
|
|
for _, client := range ns.Clients {
|
|
clientIps, err := client.IPs()
|
|
if err != nil {
|
|
return ips, fmt.Errorf("failed to get ips: %w", err)
|
|
}
|
|
ips = append(ips, clientIps...)
|
|
}
|
|
|
|
return ips, nil
|
|
}
|
|
|
|
return ips, fmt.Errorf("failed to get ips: %w", errNoNamespaceAvailable)
|
|
}
|
|
|
|
func (s *Scenario) GetClients(namespace string) ([]TailscaleClient, error) {
|
|
var clients []TailscaleClient
|
|
if ns, ok := s.namespaces[namespace]; ok {
|
|
for _, client := range ns.Clients {
|
|
clients = append(clients, client)
|
|
}
|
|
|
|
return clients, nil
|
|
}
|
|
|
|
return clients, fmt.Errorf("failed to get clients: %w", errNoNamespaceAvailable)
|
|
}
|
|
|
|
func (s *Scenario) ListTailscaleClients(namespaces ...string) ([]TailscaleClient, error) {
|
|
var allClients []TailscaleClient
|
|
|
|
if len(namespaces) == 0 {
|
|
namespaces = s.Namespaces()
|
|
}
|
|
|
|
for _, namespace := range namespaces {
|
|
clients, err := s.GetClients(namespace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allClients = append(allClients, clients...)
|
|
}
|
|
|
|
return allClients, nil
|
|
}
|
|
|
|
func (s *Scenario) ListTailscaleClientsIPs(namespaces ...string) ([]netip.Addr, error) {
|
|
var allIps []netip.Addr
|
|
|
|
if len(namespaces) == 0 {
|
|
namespaces = s.Namespaces()
|
|
}
|
|
|
|
for _, namespace := range namespaces {
|
|
ips, err := s.GetIPs(namespace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allIps = append(allIps, ips...)
|
|
}
|
|
|
|
return allIps, nil
|
|
}
|
|
|
|
func (s *Scenario) ListTailscaleClientsFQDNs(namespaces ...string) ([]string, error) {
|
|
allFQDNs := make([]string, 0)
|
|
|
|
clients, err := s.ListTailscaleClients(namespaces...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, client := range clients {
|
|
fqdn, err := client.FQDN()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allFQDNs = append(allFQDNs, fqdn)
|
|
}
|
|
|
|
return allFQDNs, nil
|
|
}
|