mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			1252 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1252 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package integration
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"crypto/tls"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"log"
 | |
| 	"net"
 | |
| 	"net/http"
 | |
| 	"net/http/cookiejar"
 | |
| 	"net/netip"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	"sort"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
 | |
| 	"github.com/juanfont/headscale/hscontrol/capver"
 | |
| 	"github.com/juanfont/headscale/hscontrol/types"
 | |
| 	"github.com/juanfont/headscale/hscontrol/util"
 | |
| 	"github.com/juanfont/headscale/integration/dockertestutil"
 | |
| 	"github.com/juanfont/headscale/integration/dsic"
 | |
| 	"github.com/juanfont/headscale/integration/hsic"
 | |
| 	"github.com/juanfont/headscale/integration/tsic"
 | |
| 	"github.com/oauth2-proxy/mockoidc"
 | |
| 	"github.com/ory/dockertest/v3"
 | |
| 	"github.com/ory/dockertest/v3/docker"
 | |
| 	"github.com/puzpuzpuz/xsync/v4"
 | |
| 	"github.com/samber/lo"
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 	"github.com/stretchr/testify/require"
 | |
| 	xmaps "golang.org/x/exp/maps"
 | |
| 	"golang.org/x/sync/errgroup"
 | |
| 	"tailscale.com/envknob"
 | |
| 	"tailscale.com/util/mak"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	scenarioHashLength = 6
 | |
| )
 | |
| 
 | |
| var usePostgresForTest = envknob.Bool("HEADSCALE_INTEGRATION_POSTGRES")
 | |
| 
 | |
| var (
 | |
| 	errNoHeadscaleAvailable = errors.New("no headscale available")
 | |
| 	errNoUserAvailable      = errors.New("no user available")
 | |
| 	errNoClientFound        = errors.New("client not found")
 | |
| 
 | |
| 	// AllVersions represents a list of Tailscale versions the suite
 | |
| 	// uses to test compatibility with the ControlServer.
 | |
| 	//
 | |
| 	// The list contains two special cases, "head" and "unstable" which
 | |
| 	// points to the current tip of Tailscale's main branch and the latest
 | |
| 	// released unstable version.
 | |
| 	//
 | |
| 	// The rest of the version represents Tailscale versions that can be
 | |
| 	// found in Tailscale's apt repository.
 | |
| 	AllVersions = append([]string{"head", "unstable"}, capver.TailscaleLatestMajorMinor(10, true)...)
 | |
| 
 | |
| 	// MustTestVersions is the minimum set of versions we should test.
 | |
| 	// At the moment, this is arbitrarily chosen as:
 | |
| 	//
 | |
| 	// - Two unstable (HEAD and unstable)
 | |
| 	// - Two latest versions
 | |
| 	// - Two oldest supported version.
 | |
| 	MustTestVersions = append(
 | |
| 		AllVersions[0:4],
 | |
| 		AllVersions[len(AllVersions)-2:]...,
 | |
| 	)
 | |
| )
 | |
| 
 | |
| // User represents a User in the ControlServer and a map of TailscaleClient's
 | |
| // associated with the User.
 | |
| type User struct {
 | |
| 	Clients map[string]TailscaleClient
 | |
| 
 | |
| 	createWaitGroup errgroup.Group
 | |
| 	joinWaitGroup   errgroup.Group
 | |
| 	syncWaitGroup   errgroup.Group
 | |
| }
 | |
| 
 | |
| // Scenario is a representation of an environment with one ControlServer and
 | |
| // one or more User's and its associated TailscaleClients.
 | |
| // A Scenario is intended to simplify setting up a new testcase for testing
 | |
| // a ControlServer with TailscaleClients.
 | |
| // 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]
 | |
| 	derpServers    []*dsic.DERPServerInContainer
 | |
| 
 | |
| 	users map[string]*User
 | |
| 
 | |
| 	pool          *dockertest.Pool
 | |
| 	networks      map[string]*dockertest.Network
 | |
| 	mockOIDC      scenarioOIDC
 | |
| 	extraServices map[string][]*dockertest.Resource
 | |
| 
 | |
| 	mu sync.Mutex
 | |
| 
 | |
| 	spec          ScenarioSpec
 | |
| 	userToNetwork map[string]*dockertest.Network
 | |
| 
 | |
| 	testHashPrefix     string
 | |
| 	testDefaultNetwork string
 | |
| }
 | |
| 
 | |
| // ScenarioSpec describes the users, nodes, and network topology to
 | |
| // set up for a given scenario.
 | |
| type ScenarioSpec struct {
 | |
| 	// Users is a list of usernames that will be created.
 | |
| 	// Each created user will get nodes equivalent to NodesPerUser
 | |
| 	Users []string
 | |
| 
 | |
| 	// NodesPerUser is how many nodes should be attached to each user.
 | |
| 	NodesPerUser int
 | |
| 
 | |
| 	// Networks, if set, is the separate Docker networks that should be
 | |
| 	// created and a list of the users that should be placed in those networks.
 | |
| 	// If not set, a single network will be created and all users+nodes will be
 | |
| 	// added there.
 | |
| 	// Please note that Docker networks are not necessarily routable and
 | |
| 	// connections between them might fall back to DERP.
 | |
| 	Networks map[string][]string
 | |
| 
 | |
| 	// ExtraService, if set, is additional a map of network to additional
 | |
| 	// container services that should be set up. These container services
 | |
| 	// typically dont run Tailscale, e.g. web service to test subnet router.
 | |
| 	ExtraService map[string][]extraServiceFunc
 | |
| 
 | |
| 	// Versions is specific list of versions to use for the test.
 | |
| 	Versions []string
 | |
| 
 | |
| 	// OIDCUsers, if populated, will start a Mock OIDC server and populate
 | |
| 	// the user login stack with the given users.
 | |
| 	// If the NodesPerUser is set, it should align with this list to ensure
 | |
| 	// the correct users are logged in.
 | |
| 	// This is because the MockOIDC server can only serve login
 | |
| 	// requests based on a queue it has been given on startup.
 | |
| 	// We currently only populates it with one login request per user.
 | |
| 	OIDCUsers     []mockoidc.MockUser
 | |
| 	OIDCAccessTTL time.Duration
 | |
| 
 | |
| 	MaxWait time.Duration
 | |
| }
 | |
| 
 | |
| func (s *Scenario) prefixedNetworkName(name string) string {
 | |
| 	return s.testHashPrefix + "-" + name
 | |
| }
 | |
| 
 | |
| // NewScenario creates a test Scenario which can be used to bootstraps a ControlServer with
 | |
| // a set of Users and TailscaleClients.
 | |
| func NewScenario(spec ScenarioSpec) (*Scenario, error) {
 | |
| 	pool, err := dockertest.NewPool("")
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("could not connect to docker: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Opportunity to clean up unreferenced networks.
 | |
| 	// This might be a no op, but it is worth a try as we sometime
 | |
| 	// dont clean up nicely after ourselves.
 | |
| 	dockertestutil.CleanUnreferencedNetworks(pool)
 | |
| 	dockertestutil.CleanImagesInCI(pool)
 | |
| 
 | |
| 	if spec.MaxWait == 0 {
 | |
| 		pool.MaxWait = dockertestMaxWait()
 | |
| 	} else {
 | |
| 		pool.MaxWait = spec.MaxWait
 | |
| 	}
 | |
| 
 | |
| 	testHashPrefix := "hs-" + util.MustGenerateRandomStringDNSSafe(scenarioHashLength)
 | |
| 	s := &Scenario{
 | |
| 		controlServers: xsync.NewMapOf[string, ControlServer](),
 | |
| 		users:          make(map[string]*User),
 | |
| 
 | |
| 		pool: pool,
 | |
| 		spec: spec,
 | |
| 
 | |
| 		testHashPrefix:     testHashPrefix,
 | |
| 		testDefaultNetwork: testHashPrefix + "-default",
 | |
| 	}
 | |
| 
 | |
| 	var userToNetwork map[string]*dockertest.Network
 | |
| 	if spec.Networks != nil || len(spec.Networks) != 0 {
 | |
| 		for name, users := range s.spec.Networks {
 | |
| 			networkName := testHashPrefix + "-" + name
 | |
| 			network, err := s.AddNetwork(networkName)
 | |
| 			if err != nil {
 | |
| 				return nil, err
 | |
| 			}
 | |
| 
 | |
| 			for _, user := range users {
 | |
| 				if n2, ok := userToNetwork[user]; ok {
 | |
| 					return nil, fmt.Errorf("users can only have nodes placed in one network: %s into %s but already in %s", user, network.Network.Name, n2.Network.Name)
 | |
| 				}
 | |
| 				mak.Set(&userToNetwork, user, network)
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		_, err := s.AddNetwork(s.testDefaultNetwork)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for network, extras := range spec.ExtraService {
 | |
| 		for _, extra := range extras {
 | |
| 			svc, err := extra(s, network)
 | |
| 			if err != nil {
 | |
| 				return nil, err
 | |
| 			}
 | |
| 			mak.Set(&s.extraServices, s.prefixedNetworkName(network), append(s.extraServices[s.prefixedNetworkName(network)], svc))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	s.userToNetwork = userToNetwork
 | |
| 
 | |
| 	if len(spec.OIDCUsers) != 0 {
 | |
| 		ttl := defaultAccessTTL
 | |
| 		if spec.OIDCAccessTTL != 0 {
 | |
| 			ttl = spec.OIDCAccessTTL
 | |
| 		}
 | |
| 		err = s.runMockOIDC(ttl, spec.OIDCUsers)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return s, nil
 | |
| }
 | |
| 
 | |
| func (s *Scenario) AddNetwork(name string) (*dockertest.Network, error) {
 | |
| 	network, err := dockertestutil.GetFirstOrCreateNetwork(s.pool, name)
 | |
| 	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
 | |
| 	// TODO(kradalby): Can the test-suite be renamed so we can have multiple?
 | |
| 	err = dockertestutil.AddContainerToNetwork(s.pool, network, "headscale-test-suite")
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to add test suite container to network: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	mak.Set(&s.networks, name, network)
 | |
| 
 | |
| 	return network, nil
 | |
| }
 | |
| 
 | |
| func (s *Scenario) Networks() []*dockertest.Network {
 | |
| 	if len(s.networks) == 0 {
 | |
| 		panic("Scenario.Networks called with empty network list")
 | |
| 	}
 | |
| 	return xmaps.Values(s.networks)
 | |
| }
 | |
| 
 | |
| func (s *Scenario) Network(name string) (*dockertest.Network, error) {
 | |
| 	net, ok := s.networks[s.prefixedNetworkName(name)]
 | |
| 	if !ok {
 | |
| 		return nil, fmt.Errorf("no network named: %s", name)
 | |
| 	}
 | |
| 
 | |
| 	return net, nil
 | |
| }
 | |
| 
 | |
| func (s *Scenario) SubnetOfNetwork(name string) (*netip.Prefix, error) {
 | |
| 	net, ok := s.networks[s.prefixedNetworkName(name)]
 | |
| 	if !ok {
 | |
| 		return nil, fmt.Errorf("no network named: %s", name)
 | |
| 	}
 | |
| 
 | |
| 	for _, ipam := range net.Network.IPAM.Config {
 | |
| 		pref, err := netip.ParsePrefix(ipam.Subnet)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		return &pref, nil
 | |
| 	}
 | |
| 
 | |
| 	return nil, fmt.Errorf("no prefix found in network: %s", name)
 | |
| }
 | |
| 
 | |
| func (s *Scenario) Services(name string) ([]*dockertest.Resource, error) {
 | |
| 	res, ok := s.extraServices[s.prefixedNetworkName(name)]
 | |
| 	if !ok {
 | |
| 		return nil, fmt.Errorf("no network named: %s", name)
 | |
| 	}
 | |
| 
 | |
| 	return res, nil
 | |
| }
 | |
| 
 | |
| func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) {
 | |
| 	defer dockertestutil.CleanUnreferencedNetworks(s.pool)
 | |
| 	defer dockertestutil.CleanImagesInCI(s.pool)
 | |
| 
 | |
| 	s.controlServers.Range(func(_ string, control ControlServer) bool {
 | |
| 		stdoutPath, stderrPath, err := control.Shutdown()
 | |
| 		if err != nil {
 | |
| 			log.Printf(
 | |
| 				"Failed to shut down control: %s",
 | |
| 				fmt.Errorf("failed to tear down control: %w", err),
 | |
| 			)
 | |
| 		}
 | |
| 
 | |
| 		if t != nil {
 | |
| 			stdout, err := os.ReadFile(stdoutPath)
 | |
| 			require.NoError(t, err)
 | |
| 			assert.NotContains(t, string(stdout), "panic")
 | |
| 
 | |
| 			stderr, err := os.ReadFile(stderrPath)
 | |
| 			require.NoError(t, err)
 | |
| 			assert.NotContains(t, string(stderr), "panic")
 | |
| 		}
 | |
| 
 | |
| 		return true
 | |
| 	})
 | |
| 
 | |
| 	for userName, user := range s.users {
 | |
| 		for _, client := range user.Clients {
 | |
| 			log.Printf("removing client %s in user %s", client.Hostname(), userName)
 | |
| 			stdoutPath, stderrPath, err := client.Shutdown()
 | |
| 			if err != nil {
 | |
| 				log.Printf("failed to tear down client: %s", err)
 | |
| 			}
 | |
| 
 | |
| 			if t != nil {
 | |
| 				stdout, err := os.ReadFile(stdoutPath)
 | |
| 				require.NoError(t, err)
 | |
| 				assert.NotContains(t, string(stdout), "panic")
 | |
| 
 | |
| 				stderr, err := os.ReadFile(stderrPath)
 | |
| 				require.NoError(t, err)
 | |
| 				assert.NotContains(t, string(stderr), "panic")
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for _, derp := range s.derpServers {
 | |
| 		err := derp.Shutdown()
 | |
| 		if err != nil {
 | |
| 			log.Printf("failed to tear down derp server: %s", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for _, svcs := range s.extraServices {
 | |
| 		for _, svc := range svcs {
 | |
| 			err := svc.Close()
 | |
| 			if err != nil {
 | |
| 				log.Printf("failed to tear down service %q: %s", svc.Container.Name, err)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if s.mockOIDC.r != nil {
 | |
| 		s.mockOIDC.r.Close()
 | |
| 		if err := s.mockOIDC.r.Close(); err != nil {
 | |
| 			log.Printf("failed to tear down oidc server: %s", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for _, network := range s.networks {
 | |
| 		if err := network.Close(); err != nil {
 | |
| 			log.Printf("failed to tear down network: %s", err)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Shutdown shuts down and cleans up all the containers (ControlServer, TailscaleClient)
 | |
| // and networks associated with it.
 | |
| // In addition, it will save the logs of the ControlServer to `/tmp/control` in the
 | |
| // environment running the tests.
 | |
| func (s *Scenario) Shutdown() {
 | |
| 	s.ShutdownAssertNoPanics(nil)
 | |
| }
 | |
| 
 | |
| // Users returns the name of all users associated with the Scenario.
 | |
| func (s *Scenario) Users() []string {
 | |
| 	users := make([]string, 0)
 | |
| 	for user := range s.users {
 | |
| 		users = append(users, user)
 | |
| 	}
 | |
| 
 | |
| 	return users
 | |
| }
 | |
| 
 | |
| /// Headscale related stuff
 | |
| // Note: These functions assume that there is a _single_ headscale instance for now
 | |
| 
 | |
| // Headscale returns a ControlServer instance based on hsic (HeadscaleInContainer)
 | |
| // If the Scenario already has an instance, the pointer to the running container
 | |
| // will be return, otherwise a new instance will be created.
 | |
| // TODO(kradalby): make port and headscale configurable, multiple instances support?
 | |
| func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) {
 | |
| 	s.mu.Lock()
 | |
| 	defer s.mu.Unlock()
 | |
| 
 | |
| 	if headscale, ok := s.controlServers.Load("headscale"); ok {
 | |
| 		return headscale, nil
 | |
| 	}
 | |
| 
 | |
| 	if usePostgresForTest {
 | |
| 		opts = append(opts, hsic.WithPostgres())
 | |
| 	}
 | |
| 
 | |
| 	headscale, err := hsic.New(s.pool, s.Networks(), opts...)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to create headscale container: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	err = headscale.WaitForRunning()
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed reach headscale container: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	s.controlServers.Store("headscale", headscale)
 | |
| 
 | |
| 	return headscale, nil
 | |
| }
 | |
| 
 | |
| // CreatePreAuthKey creates a "pre authentorised key" to be created in the
 | |
| // Headscale instance on behalf of the Scenario.
 | |
| func (s *Scenario) CreatePreAuthKey(
 | |
| 	user uint64,
 | |
| 	reusable bool,
 | |
| 	ephemeral bool,
 | |
| ) (*v1.PreAuthKey, error) {
 | |
| 	if headscale, err := s.Headscale(); err == nil {
 | |
| 		key, err := headscale.CreateAuthKey(user, reusable, ephemeral)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to create user: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		return key, nil
 | |
| 	}
 | |
| 
 | |
| 	return nil, fmt.Errorf("failed to create user: %w", errNoHeadscaleAvailable)
 | |
| }
 | |
| 
 | |
| // CreateUser creates a User to be created in the
 | |
| // Headscale instance on behalf of the Scenario.
 | |
| func (s *Scenario) CreateUser(user string) (*v1.User, error) {
 | |
| 	if headscale, err := s.Headscale(); err == nil {
 | |
| 		u, err := headscale.CreateUser(user)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to create user: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		s.users[user] = &User{
 | |
| 			Clients: make(map[string]TailscaleClient),
 | |
| 		}
 | |
| 
 | |
| 		return u, nil
 | |
| 	}
 | |
| 
 | |
| 	return nil, fmt.Errorf("failed to create user: %w", errNoHeadscaleAvailable)
 | |
| }
 | |
| 
 | |
| /// Client related stuff
 | |
| 
 | |
| func (s *Scenario) CreateTailscaleNode(
 | |
| 	version string,
 | |
| 	opts ...tsic.Option,
 | |
| ) (TailscaleClient, error) {
 | |
| 	headscale, err := s.Headscale()
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to create tailscale node (version: %s): %w", version, err)
 | |
| 	}
 | |
| 
 | |
| 	cert := headscale.GetCert()
 | |
| 	hostname := headscale.GetHostname()
 | |
| 
 | |
| 	s.mu.Lock()
 | |
| 	defer s.mu.Unlock()
 | |
| 	opts = append(opts,
 | |
| 		tsic.WithCACert(cert),
 | |
| 		tsic.WithHeadscaleName(hostname),
 | |
| 	)
 | |
| 
 | |
| 	tsClient, err := tsic.New(
 | |
| 		s.pool,
 | |
| 		version,
 | |
| 		opts...,
 | |
| 	)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf(
 | |
| 			"failed to create tailscale node: %w",
 | |
| 			err,
 | |
| 		)
 | |
| 	}
 | |
| 
 | |
| 	err = tsClient.WaitForNeedsLogin()
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf(
 | |
| 			"failed to wait for tailscaled (%s) to need login: %w",
 | |
| 			tsClient.Hostname(),
 | |
| 			err,
 | |
| 		)
 | |
| 	}
 | |
| 
 | |
| 	return tsClient, nil
 | |
| }
 | |
| 
 | |
| // CreateTailscaleNodesInUser creates and adds a new TailscaleClient to a
 | |
| // User in the Scenario.
 | |
| func (s *Scenario) CreateTailscaleNodesInUser(
 | |
| 	userStr string,
 | |
| 	requestedVersion string,
 | |
| 	count int,
 | |
| 	opts ...tsic.Option,
 | |
| ) error {
 | |
| 	if user, ok := s.users[userStr]; ok {
 | |
| 		var versions []string
 | |
| 		for i := range count {
 | |
| 			version := requestedVersion
 | |
| 			if requestedVersion == "all" {
 | |
| 				if s.spec.Versions != nil {
 | |
| 					version = s.spec.Versions[i%len(s.spec.Versions)]
 | |
| 				} else {
 | |
| 					version = MustTestVersions[i%len(MustTestVersions)]
 | |
| 				}
 | |
| 			}
 | |
| 			versions = append(versions, version)
 | |
| 
 | |
| 			headscale, err := s.Headscale()
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to create tailscale node (version: %s): %w", version, err)
 | |
| 			}
 | |
| 
 | |
| 			cert := headscale.GetCert()
 | |
| 			hostname := headscale.GetHostname()
 | |
| 
 | |
| 			s.mu.Lock()
 | |
| 			opts = append(opts,
 | |
| 				tsic.WithCACert(cert),
 | |
| 				tsic.WithHeadscaleName(hostname),
 | |
| 			)
 | |
| 			s.mu.Unlock()
 | |
| 
 | |
| 			user.createWaitGroup.Go(func() error {
 | |
| 				s.mu.Lock()
 | |
| 				tsClient, err := tsic.New(
 | |
| 					s.pool,
 | |
| 					version,
 | |
| 					opts...,
 | |
| 				)
 | |
| 				s.mu.Unlock()
 | |
| 				if err != nil {
 | |
| 					return fmt.Errorf(
 | |
| 						"failed to create tailscale node: %w",
 | |
| 						err,
 | |
| 					)
 | |
| 				}
 | |
| 
 | |
| 				err = tsClient.WaitForNeedsLogin()
 | |
| 				if err != nil {
 | |
| 					return fmt.Errorf(
 | |
| 						"failed to wait for tailscaled (%s) to need login: %w",
 | |
| 						tsClient.Hostname(),
 | |
| 						err,
 | |
| 					)
 | |
| 				}
 | |
| 
 | |
| 				s.mu.Lock()
 | |
| 				user.Clients[tsClient.Hostname()] = tsClient
 | |
| 				s.mu.Unlock()
 | |
| 
 | |
| 				return nil
 | |
| 			})
 | |
| 		}
 | |
| 		if err := user.createWaitGroup.Wait(); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		log.Printf("testing versions %v, MustTestVersions %v", lo.Uniq(versions), MustTestVersions)
 | |
| 
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	return fmt.Errorf("failed to add tailscale node: %w", errNoUserAvailable)
 | |
| }
 | |
| 
 | |
| // RunTailscaleUp will log in all of the TailscaleClients associated with a
 | |
| // User to the given ControlServer (by URL).
 | |
| func (s *Scenario) RunTailscaleUp(
 | |
| 	userStr, loginServer, authKey string,
 | |
| ) error {
 | |
| 	if user, ok := s.users[userStr]; ok {
 | |
| 		for _, client := range user.Clients {
 | |
| 			c := client
 | |
| 			user.joinWaitGroup.Go(func() error {
 | |
| 				return c.Login(loginServer, authKey)
 | |
| 			})
 | |
| 		}
 | |
| 
 | |
| 		if err := user.joinWaitGroup.Wait(); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		for _, client := range user.Clients {
 | |
| 			err := client.WaitForRunning()
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("%s failed to up tailscale node: %w", client.Hostname(), err)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	return fmt.Errorf("failed to up tailscale node: %w", errNoUserAvailable)
 | |
| }
 | |
| 
 | |
| // CountTailscale returns the total number of TailscaleClients in a Scenario.
 | |
| // This is the sum of Users x TailscaleClients.
 | |
| func (s *Scenario) CountTailscale() int {
 | |
| 	count := 0
 | |
| 
 | |
| 	for _, user := range s.users {
 | |
| 		count += len(user.Clients)
 | |
| 	}
 | |
| 
 | |
| 	return count
 | |
| }
 | |
| 
 | |
| // WaitForTailscaleSync blocks execution until all the TailscaleClient reports
 | |
| // to have all other TailscaleClients present in their netmap.NetworkMap.
 | |
| func (s *Scenario) WaitForTailscaleSync() error {
 | |
| 	tsCount := s.CountTailscale()
 | |
| 
 | |
| 	err := s.WaitForTailscaleSyncWithPeerCount(tsCount - 1)
 | |
| 	if err != nil {
 | |
| 		for _, user := range s.users {
 | |
| 			for _, client := range user.Clients {
 | |
| 				peers, allOnline, _ := client.FailingPeersAsString()
 | |
| 				if !allOnline {
 | |
| 					log.Println(peers)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| // WaitForTailscaleSyncWithPeerCount blocks execution until all the TailscaleClient reports
 | |
| // to have all other TailscaleClients present in their netmap.NetworkMap.
 | |
| func (s *Scenario) WaitForTailscaleSyncWithPeerCount(peerCount int) error {
 | |
| 	for _, user := range s.users {
 | |
| 		for _, client := range user.Clients {
 | |
| 			c := client
 | |
| 			user.syncWaitGroup.Go(func() error {
 | |
| 				return c.WaitForPeers(peerCount)
 | |
| 			})
 | |
| 		}
 | |
| 		if err := user.syncWaitGroup.Wait(); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (s *Scenario) CreateHeadscaleEnvWithLoginURL(
 | |
| 	tsOpts []tsic.Option,
 | |
| 	opts ...hsic.Option,
 | |
| ) error {
 | |
| 	return s.createHeadscaleEnv(true, tsOpts, opts...)
 | |
| }
 | |
| 
 | |
| func (s *Scenario) CreateHeadscaleEnv(
 | |
| 	tsOpts []tsic.Option,
 | |
| 	opts ...hsic.Option,
 | |
| ) error {
 | |
| 	return s.createHeadscaleEnv(false, tsOpts, opts...)
 | |
| }
 | |
| 
 | |
| // CreateHeadscaleEnv starts the headscale environment and the clients
 | |
| // according to the ScenarioSpec passed to the Scenario.
 | |
| func (s *Scenario) createHeadscaleEnv(
 | |
| 	withURL bool,
 | |
| 	tsOpts []tsic.Option,
 | |
| 	opts ...hsic.Option,
 | |
| ) error {
 | |
| 	headscale, err := s.Headscale(opts...)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	sort.Strings(s.spec.Users)
 | |
| 	for _, user := range s.spec.Users {
 | |
| 		u, err := s.CreateUser(user)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		var opts []tsic.Option
 | |
| 		if s.userToNetwork != nil {
 | |
| 			opts = append(tsOpts, tsic.WithNetwork(s.userToNetwork[user]))
 | |
| 		} else {
 | |
| 			opts = append(tsOpts, tsic.WithNetwork(s.networks[s.testDefaultNetwork]))
 | |
| 		}
 | |
| 
 | |
| 		err = s.CreateTailscaleNodesInUser(user, "all", s.spec.NodesPerUser, opts...)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		if withURL {
 | |
| 			err = s.RunTailscaleUpWithURL(user, headscale.GetEndpoint())
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		} else {
 | |
| 			key, err := s.CreatePreAuthKey(u.GetId(), true, false)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			err = s.RunTailscaleUp(user, headscale.GetEndpoint(), key.GetKey())
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (s *Scenario) RunTailscaleUpWithURL(userStr, loginServer string) error {
 | |
| 	log.Printf("running tailscale up for user %s", userStr)
 | |
| 	if user, ok := s.users[userStr]; ok {
 | |
| 		for _, client := range user.Clients {
 | |
| 			tsc := client
 | |
| 			user.joinWaitGroup.Go(func() error {
 | |
| 				loginURL, err := tsc.LoginWithURL(loginServer)
 | |
| 				if err != nil {
 | |
| 					log.Printf("%s failed to run tailscale up: %s", tsc.Hostname(), err)
 | |
| 				}
 | |
| 
 | |
| 				body, err := doLoginURL(tsc.Hostname(), loginURL)
 | |
| 				if err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 
 | |
| 				// If the URL is not a OIDC URL, then we need to
 | |
| 				// run the register command to fully log in the client.
 | |
| 				if !strings.Contains(loginURL.String(), "/oidc/") {
 | |
| 					s.runHeadscaleRegister(userStr, body)
 | |
| 				}
 | |
| 
 | |
| 				return nil
 | |
| 			})
 | |
| 
 | |
| 			log.Printf("client %s is ready", client.Hostname())
 | |
| 		}
 | |
| 
 | |
| 		if err := user.joinWaitGroup.Wait(); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		for _, client := range user.Clients {
 | |
| 			err := client.WaitForRunning()
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf(
 | |
| 					"%s tailscale node has not reached running: %w",
 | |
| 					client.Hostname(),
 | |
| 					err,
 | |
| 				)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	return fmt.Errorf("failed to up tailscale node: %w", errNoUserAvailable)
 | |
| }
 | |
| 
 | |
| // doLoginURL visits the given login URL and returns the body as a
 | |
| // string.
 | |
| func doLoginURL(hostname string, loginURL *url.URL) (string, error) {
 | |
| 	log.Printf("%s login url: %s\n", hostname, loginURL.String())
 | |
| 
 | |
| 	var err error
 | |
| 	hc := &http.Client{
 | |
| 		Transport: LoggingRoundTripper{},
 | |
| 	}
 | |
| 	hc.Jar, err = cookiejar.New(nil)
 | |
| 	if err != nil {
 | |
| 		return "", fmt.Errorf("%s failed to create cookiejar	: %w", hostname, err)
 | |
| 	}
 | |
| 
 | |
| 	log.Printf("%s logging in with url", hostname)
 | |
| 	ctx := context.Background()
 | |
| 	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil)
 | |
| 	resp, err := hc.Do(req)
 | |
| 	if err != nil {
 | |
| 		return "", fmt.Errorf("%s failed to send http request: %w", hostname, err)
 | |
| 	}
 | |
| 
 | |
| 	log.Printf("cookies: %+v", hc.Jar.Cookies(loginURL))
 | |
| 
 | |
| 	if resp.StatusCode != http.StatusOK {
 | |
| 		body, _ := io.ReadAll(resp.Body)
 | |
| 		log.Printf("body: %s", body)
 | |
| 
 | |
| 		return "", fmt.Errorf("%s response code of login request was %w", hostname, err)
 | |
| 	}
 | |
| 
 | |
| 	defer resp.Body.Close()
 | |
| 
 | |
| 	body, err := io.ReadAll(resp.Body)
 | |
| 	if err != nil {
 | |
| 		log.Printf("%s failed to read response body: %s", hostname, err)
 | |
| 
 | |
| 		return "", fmt.Errorf("%s failed to read response body: %w", hostname, err)
 | |
| 	}
 | |
| 
 | |
| 	return string(body), nil
 | |
| }
 | |
| 
 | |
| var errParseAuthPage = errors.New("failed to parse auth page")
 | |
| 
 | |
| func (s *Scenario) runHeadscaleRegister(userStr string, body string) error {
 | |
| 	// 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]
 | |
| 	key = strings.SplitN(key, " ", 2)[0]
 | |
| 	log.Printf("registering node %s", key)
 | |
| 
 | |
| 	if headscale, err := s.Headscale(); err == nil {
 | |
| 		_, err = headscale.Execute(
 | |
| 			[]string{"headscale", "nodes", "register", "--user", userStr, "--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)
 | |
| }
 | |
| 
 | |
| type LoggingRoundTripper struct{}
 | |
| 
 | |
| func (t LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
 | |
| 	noTls := &http.Transport{
 | |
| 		TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // nolint
 | |
| 	}
 | |
| 	resp, err := noTls.RoundTrip(req)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	log.Printf("---")
 | |
| 	log.Printf("method: %s | url: %s", resp.Request.Method, resp.Request.URL.String())
 | |
| 	log.Printf("status: %d | cookies: %+v", resp.StatusCode, resp.Cookies())
 | |
| 
 | |
| 	return resp, nil
 | |
| }
 | |
| 
 | |
| // GetIPs returns all netip.Addr of TailscaleClients associated with a User
 | |
| // in a Scenario.
 | |
| func (s *Scenario) GetIPs(user string) ([]netip.Addr, error) {
 | |
| 	var ips []netip.Addr
 | |
| 	if ns, ok := s.users[user]; 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", errNoUserAvailable)
 | |
| }
 | |
| 
 | |
| // GetClients returns all TailscaleClients associated with a User in a Scenario.
 | |
| func (s *Scenario) GetClients(user string) ([]TailscaleClient, error) {
 | |
| 	var clients []TailscaleClient
 | |
| 	if ns, ok := s.users[user]; ok {
 | |
| 		for _, client := range ns.Clients {
 | |
| 			clients = append(clients, client)
 | |
| 		}
 | |
| 
 | |
| 		return clients, nil
 | |
| 	}
 | |
| 
 | |
| 	return clients, fmt.Errorf("failed to get clients: %w", errNoUserAvailable)
 | |
| }
 | |
| 
 | |
| // ListTailscaleClients returns a list of TailscaleClients given the Users
 | |
| // passed as parameters.
 | |
| func (s *Scenario) ListTailscaleClients(users ...string) ([]TailscaleClient, error) {
 | |
| 	var allClients []TailscaleClient
 | |
| 
 | |
| 	if len(users) == 0 {
 | |
| 		users = s.Users()
 | |
| 	}
 | |
| 
 | |
| 	for _, user := range users {
 | |
| 		clients, err := s.GetClients(user)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		allClients = append(allClients, clients...)
 | |
| 	}
 | |
| 
 | |
| 	return allClients, nil
 | |
| }
 | |
| 
 | |
| // FindTailscaleClientByIP returns a TailscaleClient associated with an IP address
 | |
| // if it exists.
 | |
| func (s *Scenario) FindTailscaleClientByIP(ip netip.Addr) (TailscaleClient, error) {
 | |
| 	clients, err := s.ListTailscaleClients()
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	for _, client := range clients {
 | |
| 		ips, _ := client.IPs()
 | |
| 		for _, ip2 := range ips {
 | |
| 			if ip == ip2 {
 | |
| 				return client, nil
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil, errNoClientFound
 | |
| }
 | |
| 
 | |
| // ListTailscaleClientsIPs returns a list of netip.Addr based on Users
 | |
| // passed as parameters.
 | |
| func (s *Scenario) ListTailscaleClientsIPs(users ...string) ([]netip.Addr, error) {
 | |
| 	var allIps []netip.Addr
 | |
| 
 | |
| 	if len(users) == 0 {
 | |
| 		users = s.Users()
 | |
| 	}
 | |
| 
 | |
| 	for _, user := range users {
 | |
| 		ips, err := s.GetIPs(user)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		allIps = append(allIps, ips...)
 | |
| 	}
 | |
| 
 | |
| 	return allIps, nil
 | |
| }
 | |
| 
 | |
| // ListTailscaleClientsFQDNs returns a list of FQDN based on Users
 | |
| // passed as parameters.
 | |
| func (s *Scenario) ListTailscaleClientsFQDNs(users ...string) ([]string, error) {
 | |
| 	allFQDNs := make([]string, 0)
 | |
| 
 | |
| 	clients, err := s.ListTailscaleClients(users...)
 | |
| 	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
 | |
| }
 | |
| 
 | |
| // WaitForTailscaleLogout blocks execution until all TailscaleClients have
 | |
| // logged out of the ControlServer.
 | |
| func (s *Scenario) WaitForTailscaleLogout() error {
 | |
| 	for _, user := range s.users {
 | |
| 		for _, client := range user.Clients {
 | |
| 			c := client
 | |
| 			user.syncWaitGroup.Go(func() error {
 | |
| 				return c.WaitForNeedsLogin()
 | |
| 			})
 | |
| 		}
 | |
| 		if err := user.syncWaitGroup.Wait(); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // CreateDERPServer creates a new DERP server in a container.
 | |
| func (s *Scenario) CreateDERPServer(version string, opts ...dsic.Option) (*dsic.DERPServerInContainer, error) {
 | |
| 	derp, err := dsic.New(s.pool, version, s.Networks(), opts...)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to create DERP server: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	err = derp.WaitForRunning()
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to reach DERP server: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	s.derpServers = append(s.derpServers, derp)
 | |
| 
 | |
| 	return derp, nil
 | |
| }
 | |
| 
 | |
| type scenarioOIDC struct {
 | |
| 	r   *dockertest.Resource
 | |
| 	cfg *types.OIDCConfig
 | |
| }
 | |
| 
 | |
| func (o *scenarioOIDC) Issuer() string {
 | |
| 	if o.cfg == nil {
 | |
| 		panic("OIDC has not been created")
 | |
| 	}
 | |
| 
 | |
| 	return o.cfg.Issuer
 | |
| }
 | |
| 
 | |
| func (o *scenarioOIDC) ClientSecret() string {
 | |
| 	if o.cfg == nil {
 | |
| 		panic("OIDC has not been created")
 | |
| 	}
 | |
| 
 | |
| 	return o.cfg.ClientSecret
 | |
| }
 | |
| 
 | |
| func (o *scenarioOIDC) ClientID() string {
 | |
| 	if o.cfg == nil {
 | |
| 		panic("OIDC has not been created")
 | |
| 	}
 | |
| 
 | |
| 	return o.cfg.ClientID
 | |
| }
 | |
| 
 | |
| const (
 | |
| 	dockerContextPath      = "../."
 | |
| 	hsicOIDCMockHashLength = 6
 | |
| 	defaultAccessTTL       = 10 * time.Minute
 | |
| )
 | |
| 
 | |
| var errStatusCodeNotOK = errors.New("status code not OK")
 | |
| 
 | |
| func (s *Scenario) runMockOIDC(accessTTL time.Duration, users []mockoidc.MockUser) error {
 | |
| 	port, err := dockertestutil.RandomFreeHostPort()
 | |
| 	if err != nil {
 | |
| 		log.Fatalf("could not find an open port: %s", err)
 | |
| 	}
 | |
| 	portNotation := fmt.Sprintf("%d/tcp", port)
 | |
| 
 | |
| 	hash, _ := util.GenerateRandomStringDNSSafe(hsicOIDCMockHashLength)
 | |
| 
 | |
| 	hostname := "hs-oidcmock-" + hash
 | |
| 
 | |
| 	usersJSON, err := json.Marshal(users)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	mockOidcOptions := &dockertest.RunOptions{
 | |
| 		Name:         hostname,
 | |
| 		Cmd:          []string{"headscale", "mockoidc"},
 | |
| 		ExposedPorts: []string{portNotation},
 | |
| 		PortBindings: map[docker.Port][]docker.PortBinding{
 | |
| 			docker.Port(portNotation): {{HostPort: strconv.Itoa(port)}},
 | |
| 		},
 | |
| 		Networks: s.Networks(),
 | |
| 		Env: []string{
 | |
| 			"MOCKOIDC_ADDR=" + hostname,
 | |
| 			fmt.Sprintf("MOCKOIDC_PORT=%d", port),
 | |
| 			"MOCKOIDC_CLIENT_ID=superclient",
 | |
| 			"MOCKOIDC_CLIENT_SECRET=supersecret",
 | |
| 			"MOCKOIDC_ACCESS_TTL=" + accessTTL.String(),
 | |
| 			"MOCKOIDC_USERS=" + string(usersJSON),
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	headscaleBuildOptions := &dockertest.BuildOptions{
 | |
| 		Dockerfile: hsic.IntegrationTestDockerFileName,
 | |
| 		ContextDir: dockerContextPath,
 | |
| 	}
 | |
| 
 | |
| 	err = s.pool.RemoveContainerByName(hostname)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	s.mockOIDC = scenarioOIDC{}
 | |
| 
 | |
| 	// Add integration test labels if running under hi tool
 | |
| 	dockertestutil.DockerAddIntegrationLabels(mockOidcOptions, "oidc")
 | |
| 
 | |
| 	if pmockoidc, err := s.pool.BuildAndRunWithBuildOptions(
 | |
| 		headscaleBuildOptions,
 | |
| 		mockOidcOptions,
 | |
| 		dockertestutil.DockerRestartPolicy); err == nil {
 | |
| 		s.mockOIDC.r = pmockoidc
 | |
| 	} else {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// headscale needs to set up the provider with a specific
 | |
| 	// IP addr to ensure we get the correct config from the well-known
 | |
| 	// endpoint.
 | |
| 	network := s.Networks()[0]
 | |
| 	ipAddr := s.mockOIDC.r.GetIPInNetwork(network)
 | |
| 
 | |
| 	log.Println("Waiting for headscale mock oidc to be ready for tests")
 | |
| 	hostEndpoint := net.JoinHostPort(ipAddr, strconv.Itoa(port))
 | |
| 
 | |
| 	if err := s.pool.Retry(func() error {
 | |
| 		oidcConfigURL := fmt.Sprintf("http://%s/oidc/.well-known/openid-configuration", hostEndpoint)
 | |
| 		httpClient := &http.Client{}
 | |
| 		ctx := context.Background()
 | |
| 		req, _ := http.NewRequestWithContext(ctx, http.MethodGet, oidcConfigURL, nil)
 | |
| 		resp, err := httpClient.Do(req)
 | |
| 		if err != nil {
 | |
| 			log.Printf("headscale mock OIDC tests is not ready: %s\n", err)
 | |
| 
 | |
| 			return err
 | |
| 		}
 | |
| 		defer resp.Body.Close()
 | |
| 
 | |
| 		if resp.StatusCode != http.StatusOK {
 | |
| 			return errStatusCodeNotOK
 | |
| 		}
 | |
| 
 | |
| 		return nil
 | |
| 	}); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	s.mockOIDC.cfg = &types.OIDCConfig{
 | |
| 		Issuer: fmt.Sprintf(
 | |
| 			"http://%s/oidc",
 | |
| 			hostEndpoint,
 | |
| 		),
 | |
| 		ClientID:                   "superclient",
 | |
| 		ClientSecret:               "supersecret",
 | |
| 		OnlyStartIfOIDCIsAvailable: true,
 | |
| 	}
 | |
| 
 | |
| 	log.Printf("headscale mock oidc is ready for tests at %s", hostEndpoint)
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| type extraServiceFunc func(*Scenario, string) (*dockertest.Resource, error)
 | |
| 
 | |
| func Webservice(s *Scenario, networkName string) (*dockertest.Resource, error) {
 | |
| 	// port, err := dockertestutil.RandomFreeHostPort()
 | |
| 	// if err != nil {
 | |
| 	// 	log.Fatalf("could not find an open port: %s", err)
 | |
| 	// }
 | |
| 	// portNotation := fmt.Sprintf("%d/tcp", port)
 | |
| 
 | |
| 	hash := util.MustGenerateRandomStringDNSSafe(hsicOIDCMockHashLength)
 | |
| 
 | |
| 	hostname := "hs-webservice-" + hash
 | |
| 
 | |
| 	network, ok := s.networks[s.prefixedNetworkName(networkName)]
 | |
| 	if !ok {
 | |
| 		return nil, fmt.Errorf("network does not exist: %s", networkName)
 | |
| 	}
 | |
| 
 | |
| 	webOpts := &dockertest.RunOptions{
 | |
| 		Name: hostname,
 | |
| 		Cmd:  []string{"/bin/sh", "-c", "cd / ; python3 -m http.server --bind :: 80"},
 | |
| 		// ExposedPorts: []string{portNotation},
 | |
| 		// PortBindings: map[docker.Port][]docker.PortBinding{
 | |
| 		// 	docker.Port(portNotation): {{HostPort: strconv.Itoa(port)}},
 | |
| 		// },
 | |
| 		Networks: []*dockertest.Network{network},
 | |
| 		Env:      []string{},
 | |
| 	}
 | |
| 
 | |
| 	// Add integration test labels if running under hi tool
 | |
| 	dockertestutil.DockerAddIntegrationLabels(webOpts, "web")
 | |
| 
 | |
| 	webBOpts := &dockertest.BuildOptions{
 | |
| 		Dockerfile: hsic.IntegrationTestDockerFileName,
 | |
| 		ContextDir: dockerContextPath,
 | |
| 	}
 | |
| 
 | |
| 	web, err := s.pool.BuildAndRunWithBuildOptions(
 | |
| 		webBOpts,
 | |
| 		webOpts,
 | |
| 		dockertestutil.DockerRestartPolicy)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// headscale needs to set up the provider with a specific
 | |
| 	// IP addr to ensure we get the correct config from the well-known
 | |
| 	// endpoint.
 | |
| 	// ipAddr := web.GetIPInNetwork(network)
 | |
| 
 | |
| 	// log.Println("Waiting for headscale mock oidc to be ready for tests")
 | |
| 	// hostEndpoint := net.JoinHostPort(ipAddr, strconv.Itoa(port))
 | |
| 
 | |
| 	// if err := s.pool.Retry(func() error {
 | |
| 	// 	oidcConfigURL := fmt.Sprintf("http://%s/etc/hostname", hostEndpoint)
 | |
| 	// 	httpClient := &http.Client{}
 | |
| 	// 	ctx := context.Background()
 | |
| 	// 	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, oidcConfigURL, nil)
 | |
| 	// 	resp, err := httpClient.Do(req)
 | |
| 	// 	if err != nil {
 | |
| 	// 		log.Printf("headscale mock OIDC tests is not ready: %s\n", err)
 | |
| 
 | |
| 	// 		return err
 | |
| 	// 	}
 | |
| 	// 	defer resp.Body.Close()
 | |
| 
 | |
| 	// 	if resp.StatusCode != http.StatusOK {
 | |
| 	// 		return errStatusCodeNotOK
 | |
| 	// 	}
 | |
| 
 | |
| 	// 	return nil
 | |
| 	// }); err != nil {
 | |
| 	// 	return err
 | |
| 	// }
 | |
| 
 | |
| 	return web, nil
 | |
| }
 |