mirror of
https://github.com/juanfont/headscale.git
synced 2024-12-22 07:57:34 +00:00
Add documentation to integration test framework
so tsic, hsic and scenario Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
parent
b190ec8edc
commit
e65ce17f7b
@ -41,6 +41,8 @@ type fileInContainer struct {
|
|||||||
contents []byte
|
contents []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HeadscaleInContainer is an implementation of ControlServer which
|
||||||
|
// sets up a Headscale instance inside a container.
|
||||||
type HeadscaleInContainer struct {
|
type HeadscaleInContainer struct {
|
||||||
hostname string
|
hostname string
|
||||||
|
|
||||||
@ -57,8 +59,12 @@ type HeadscaleInContainer struct {
|
|||||||
filesInContainer []fileInContainer
|
filesInContainer []fileInContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Option represent optional settings that can be given to a
|
||||||
|
// Headscale instance.
|
||||||
type Option = func(c *HeadscaleInContainer)
|
type Option = func(c *HeadscaleInContainer)
|
||||||
|
|
||||||
|
// WithACLPolicy adds a headscale.ACLPolicy policy to the
|
||||||
|
// HeadscaleInContainer instance.
|
||||||
func WithACLPolicy(acl *headscale.ACLPolicy) Option {
|
func WithACLPolicy(acl *headscale.ACLPolicy) Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
// TODO(kradalby): Move somewhere appropriate
|
// TODO(kradalby): Move somewhere appropriate
|
||||||
@ -68,6 +74,7 @@ func WithACLPolicy(acl *headscale.ACLPolicy) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithTLS creates certificates and enables HTTPS.
|
||||||
func WithTLS() Option {
|
func WithTLS() Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
cert, key, err := createCertificate()
|
cert, key, err := createCertificate()
|
||||||
@ -84,6 +91,8 @@ func WithTLS() Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithConfigEnv takes a map of environment variables that
|
||||||
|
// can be used to override Headscale configuration.
|
||||||
func WithConfigEnv(configEnv map[string]string) Option {
|
func WithConfigEnv(configEnv map[string]string) Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
for key, value := range configEnv {
|
for key, value := range configEnv {
|
||||||
@ -92,12 +101,15 @@ func WithConfigEnv(configEnv map[string]string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithPort sets the port on where to run Headscale.
|
||||||
func WithPort(port int) Option {
|
func WithPort(port int) Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
hsic.port = port
|
hsic.port = port
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithTestName sets a a name for the test, this will be reflected
|
||||||
|
// in the Docker container name.
|
||||||
func WithTestName(testName string) Option {
|
func WithTestName(testName string) Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
hash, _ := headscale.GenerateRandomStringDNSSafe(hsicHashLength)
|
hash, _ := headscale.GenerateRandomStringDNSSafe(hsicHashLength)
|
||||||
@ -107,6 +119,8 @@ func WithTestName(testName string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithHostnameAsServerURL sets the Headscale ServerURL based on
|
||||||
|
// the Hostname.
|
||||||
func WithHostnameAsServerURL() Option {
|
func WithHostnameAsServerURL() Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
hsic.env["HEADSCALE_SERVER_URL"] = fmt.Sprintf("http://%s",
|
hsic.env["HEADSCALE_SERVER_URL"] = fmt.Sprintf("http://%s",
|
||||||
@ -116,6 +130,7 @@ func WithHostnameAsServerURL() Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithFileInContainer adds a file to the container at the given path.
|
||||||
func WithFileInContainer(path string, contents []byte) Option {
|
func WithFileInContainer(path string, contents []byte) Option {
|
||||||
return func(hsic *HeadscaleInContainer) {
|
return func(hsic *HeadscaleInContainer) {
|
||||||
hsic.filesInContainer = append(hsic.filesInContainer,
|
hsic.filesInContainer = append(hsic.filesInContainer,
|
||||||
@ -126,6 +141,7 @@ func WithFileInContainer(path string, contents []byte) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New returns a new HeadscaleInContainer instance.
|
||||||
func New(
|
func New(
|
||||||
pool *dockertest.Pool,
|
pool *dockertest.Pool,
|
||||||
network *dockertest.Network,
|
network *dockertest.Network,
|
||||||
@ -244,14 +260,19 @@ func (t *HeadscaleInContainer) hasTLS() bool {
|
|||||||
return len(t.tlsCert) != 0 && len(t.tlsKey) != 0
|
return len(t.tlsCert) != 0 && len(t.tlsKey) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown stops and cleans up the Headscale container.
|
||||||
func (t *HeadscaleInContainer) Shutdown() error {
|
func (t *HeadscaleInContainer) Shutdown() error {
|
||||||
return t.pool.Purge(t.container)
|
return t.pool.Purge(t.container)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveLog saves the current stdout log of the container to a path
|
||||||
|
// on the host system.
|
||||||
func (t *HeadscaleInContainer) SaveLog(path string) error {
|
func (t *HeadscaleInContainer) SaveLog(path string) error {
|
||||||
return dockertestutil.SaveLog(t.pool, t.container, path)
|
return dockertestutil.SaveLog(t.pool, t.container, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute runs a command inside the Headscale container and returns the
|
||||||
|
// result of stdout as a string.
|
||||||
func (t *HeadscaleInContainer) Execute(
|
func (t *HeadscaleInContainer) Execute(
|
||||||
command []string,
|
command []string,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
@ -273,18 +294,23 @@ func (t *HeadscaleInContainer) Execute(
|
|||||||
return stdout, nil
|
return stdout, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIP returns the docker container IP as a string.
|
||||||
func (t *HeadscaleInContainer) GetIP() string {
|
func (t *HeadscaleInContainer) GetIP() string {
|
||||||
return t.container.GetIPInNetwork(t.network)
|
return t.container.GetIPInNetwork(t.network)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPort returns the docker container port as a string.
|
||||||
func (t *HeadscaleInContainer) GetPort() string {
|
func (t *HeadscaleInContainer) GetPort() string {
|
||||||
return fmt.Sprintf("%d", t.port)
|
return fmt.Sprintf("%d", t.port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHealthEndpoint returns a health endpoint for the HeadscaleInContainer
|
||||||
|
// instance.
|
||||||
func (t *HeadscaleInContainer) GetHealthEndpoint() string {
|
func (t *HeadscaleInContainer) GetHealthEndpoint() string {
|
||||||
return fmt.Sprintf("%s/health", t.GetEndpoint())
|
return fmt.Sprintf("%s/health", t.GetEndpoint())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEndpoint returns the Headscale endpoint for the HeadscaleInContainer.
|
||||||
func (t *HeadscaleInContainer) GetEndpoint() string {
|
func (t *HeadscaleInContainer) GetEndpoint() string {
|
||||||
hostEndpoint := fmt.Sprintf("%s:%d",
|
hostEndpoint := fmt.Sprintf("%s:%d",
|
||||||
t.GetIP(),
|
t.GetIP(),
|
||||||
@ -297,14 +323,18 @@ func (t *HeadscaleInContainer) GetEndpoint() string {
|
|||||||
return fmt.Sprintf("http://%s", hostEndpoint)
|
return fmt.Sprintf("http://%s", hostEndpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCert returns the public certificate of the HeadscaleInContainer.
|
||||||
func (t *HeadscaleInContainer) GetCert() []byte {
|
func (t *HeadscaleInContainer) GetCert() []byte {
|
||||||
return t.tlsCert
|
return t.tlsCert
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHostname returns the hostname of the HeadscaleInContainer.
|
||||||
func (t *HeadscaleInContainer) GetHostname() string {
|
func (t *HeadscaleInContainer) GetHostname() string {
|
||||||
return t.hostname
|
return t.hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WaitForReady blocks until the Headscale instance is ready to
|
||||||
|
// serve clients.
|
||||||
func (t *HeadscaleInContainer) WaitForReady() error {
|
func (t *HeadscaleInContainer) WaitForReady() error {
|
||||||
url := t.GetHealthEndpoint()
|
url := t.GetHealthEndpoint()
|
||||||
|
|
||||||
@ -332,6 +362,7 @@ func (t *HeadscaleInContainer) WaitForReady() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateUser adds a new user to the Headscale instance.
|
||||||
func (t *HeadscaleInContainer) CreateUser(
|
func (t *HeadscaleInContainer) CreateUser(
|
||||||
user string,
|
user string,
|
||||||
) error {
|
) error {
|
||||||
@ -349,6 +380,8 @@ func (t *HeadscaleInContainer) CreateUser(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateAuthKey creates a new "authorisation key" for a User that can be used
|
||||||
|
// to authorise a TailscaleClient with the Headscale instance.
|
||||||
func (t *HeadscaleInContainer) CreateAuthKey(
|
func (t *HeadscaleInContainer) CreateAuthKey(
|
||||||
user string,
|
user string,
|
||||||
reusable bool,
|
reusable bool,
|
||||||
@ -392,6 +425,8 @@ func (t *HeadscaleInContainer) CreateAuthKey(
|
|||||||
return &preAuthKey, nil
|
return &preAuthKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListMachinesInUser list the TailscaleClients (Machine, Headscale internal representation)
|
||||||
|
// associated with a user.
|
||||||
func (t *HeadscaleInContainer) ListMachinesInUser(
|
func (t *HeadscaleInContainer) ListMachinesInUser(
|
||||||
user string,
|
user string,
|
||||||
) ([]*v1.Machine, error) {
|
) ([]*v1.Machine, error) {
|
||||||
@ -415,6 +450,7 @@ func (t *HeadscaleInContainer) ListMachinesInUser(
|
|||||||
return nodes, nil
|
return nodes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteFile save file inside the Headscale container.
|
||||||
func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error {
|
func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error {
|
||||||
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
|
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
|
||||||
}
|
}
|
||||||
|
@ -57,12 +57,23 @@ var (
|
|||||||
// "1.8.7",
|
// "1.8.7",
|
||||||
// }.
|
// }.
|
||||||
|
|
||||||
|
// TailscaleVersions 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.
|
||||||
TailscaleVersions = append(
|
TailscaleVersions = append(
|
||||||
tailscaleVersions2021,
|
tailscaleVersions2021,
|
||||||
tailscaleVersions2019...,
|
tailscaleVersions2019...,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// User represents a User in the ControlServer and a map of TailscaleClient's
|
||||||
|
// associated with the User.
|
||||||
type User struct {
|
type User struct {
|
||||||
Clients map[string]TailscaleClient
|
Clients map[string]TailscaleClient
|
||||||
|
|
||||||
@ -71,6 +82,10 @@ type User struct {
|
|||||||
syncWaitGroup sync.WaitGroup
|
syncWaitGroup sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
// TODO(kradalby): make control server configurable, test correctness with Tailscale SaaS.
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
// TODO(kradalby): support multiple headcales for later, currently only
|
// TODO(kradalby): support multiple headcales for later, currently only
|
||||||
@ -85,6 +100,8 @@ type Scenario struct {
|
|||||||
headscaleLock sync.Mutex
|
headscaleLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewScenario creates a test Scenario which can be used to bootstraps a ControlServer with
|
||||||
|
// a set of Users and TailscaleClients.
|
||||||
func NewScenario() (*Scenario, error) {
|
func NewScenario() (*Scenario, error) {
|
||||||
hash, err := headscale.GenerateRandomStringDNSSafe(scenarioHashLength)
|
hash, err := headscale.GenerateRandomStringDNSSafe(scenarioHashLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -125,6 +142,10 @@ func NewScenario() (*Scenario, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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() error {
|
func (s *Scenario) Shutdown() error {
|
||||||
s.controlServers.Range(func(_ string, control ControlServer) bool {
|
s.controlServers.Range(func(_ string, control ControlServer) bool {
|
||||||
err := control.SaveLog("/tmp/control")
|
err := control.SaveLog("/tmp/control")
|
||||||
@ -168,6 +189,7 @@ func (s *Scenario) Shutdown() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Users returns the name of all users associated with the Scenario.
|
||||||
func (s *Scenario) Users() []string {
|
func (s *Scenario) Users() []string {
|
||||||
users := make([]string, 0)
|
users := make([]string, 0)
|
||||||
for user := range s.users {
|
for user := range s.users {
|
||||||
@ -180,6 +202,9 @@ func (s *Scenario) Users() []string {
|
|||||||
/// Headscale related stuff
|
/// Headscale related stuff
|
||||||
// Note: These functions assume that there is a _single_ headscale instance for now
|
// 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?
|
// TODO(kradalby): make port and headscale configurable, multiple instances support?
|
||||||
func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) {
|
func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) {
|
||||||
s.headscaleLock.Lock()
|
s.headscaleLock.Lock()
|
||||||
@ -204,6 +229,8 @@ func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) {
|
|||||||
return headscale, nil
|
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(
|
func (s *Scenario) CreatePreAuthKey(
|
||||||
user string,
|
user string,
|
||||||
reusable bool,
|
reusable bool,
|
||||||
@ -221,6 +248,8 @@ func (s *Scenario) CreatePreAuthKey(
|
|||||||
return nil, fmt.Errorf("failed to create user: %w", errNoHeadscaleAvailable)
|
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) error {
|
func (s *Scenario) CreateUser(user string) error {
|
||||||
if headscale, err := s.Headscale(); err == nil {
|
if headscale, err := s.Headscale(); err == nil {
|
||||||
err := headscale.CreateUser(user)
|
err := headscale.CreateUser(user)
|
||||||
@ -240,6 +269,8 @@ func (s *Scenario) CreateUser(user string) error {
|
|||||||
|
|
||||||
/// Client related stuff
|
/// Client related stuff
|
||||||
|
|
||||||
|
// CreateTailscaleNodesInUser creates and adds a new TailscaleClient to a
|
||||||
|
// User in the Scenario.
|
||||||
func (s *Scenario) CreateTailscaleNodesInUser(
|
func (s *Scenario) CreateTailscaleNodesInUser(
|
||||||
userStr string,
|
userStr string,
|
||||||
requestedVersion string,
|
requestedVersion string,
|
||||||
@ -300,6 +331,8 @@ func (s *Scenario) CreateTailscaleNodesInUser(
|
|||||||
return fmt.Errorf("failed to add tailscale node: %w", errNoUserAvailable)
|
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(
|
func (s *Scenario) RunTailscaleUp(
|
||||||
userStr, loginServer, authKey string,
|
userStr, loginServer, authKey string,
|
||||||
) error {
|
) error {
|
||||||
@ -328,6 +361,8 @@ func (s *Scenario) RunTailscaleUp(
|
|||||||
return fmt.Errorf("failed to up tailscale node: %w", errNoUserAvailable)
|
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 {
|
func (s *Scenario) CountTailscale() int {
|
||||||
count := 0
|
count := 0
|
||||||
|
|
||||||
@ -338,6 +373,8 @@ func (s *Scenario) CountTailscale() int {
|
|||||||
return count
|
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 {
|
func (s *Scenario) WaitForTailscaleSync() error {
|
||||||
tsCount := s.CountTailscale()
|
tsCount := s.CountTailscale()
|
||||||
|
|
||||||
@ -358,7 +395,7 @@ func (s *Scenario) WaitForTailscaleSync() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateHeadscaleEnv is a conventient method returning a set up Headcale
|
// CreateHeadscaleEnv is a conventient method returning a complete Headcale
|
||||||
// test environment with nodes of all versions, joined to the server with X
|
// test environment with nodes of all versions, joined to the server with X
|
||||||
// users.
|
// users.
|
||||||
func (s *Scenario) CreateHeadscaleEnv(
|
func (s *Scenario) CreateHeadscaleEnv(
|
||||||
@ -396,6 +433,8 @@ func (s *Scenario) CreateHeadscaleEnv(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIPs returns all netip.Addr of TailscaleClients associated with a User
|
||||||
|
// in a Scenario.
|
||||||
func (s *Scenario) GetIPs(user string) ([]netip.Addr, error) {
|
func (s *Scenario) GetIPs(user string) ([]netip.Addr, error) {
|
||||||
var ips []netip.Addr
|
var ips []netip.Addr
|
||||||
if ns, ok := s.users[user]; ok {
|
if ns, ok := s.users[user]; ok {
|
||||||
@ -413,6 +452,7 @@ func (s *Scenario) GetIPs(user string) ([]netip.Addr, error) {
|
|||||||
return ips, fmt.Errorf("failed to get ips: %w", errNoUserAvailable)
|
return ips, fmt.Errorf("failed to get ips: %w", errNoUserAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIPs returns all TailscaleClients associated with a User in a Scenario.
|
||||||
func (s *Scenario) GetClients(user string) ([]TailscaleClient, error) {
|
func (s *Scenario) GetClients(user string) ([]TailscaleClient, error) {
|
||||||
var clients []TailscaleClient
|
var clients []TailscaleClient
|
||||||
if ns, ok := s.users[user]; ok {
|
if ns, ok := s.users[user]; ok {
|
||||||
@ -426,6 +466,8 @@ func (s *Scenario) GetClients(user string) ([]TailscaleClient, error) {
|
|||||||
return clients, fmt.Errorf("failed to get clients: %w", errNoUserAvailable)
|
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) {
|
func (s *Scenario) ListTailscaleClients(users ...string) ([]TailscaleClient, error) {
|
||||||
var allClients []TailscaleClient
|
var allClients []TailscaleClient
|
||||||
|
|
||||||
@ -445,6 +487,8 @@ func (s *Scenario) ListTailscaleClients(users ...string) ([]TailscaleClient, err
|
|||||||
return allClients, nil
|
return allClients, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindTailscaleClientByIP returns a TailscaleClient associated with an IP address
|
||||||
|
// if it exists.
|
||||||
func (s *Scenario) FindTailscaleClientByIP(ip netip.Addr) (TailscaleClient, error) {
|
func (s *Scenario) FindTailscaleClientByIP(ip netip.Addr) (TailscaleClient, error) {
|
||||||
clients, err := s.ListTailscaleClients()
|
clients, err := s.ListTailscaleClients()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -463,6 +507,8 @@ func (s *Scenario) FindTailscaleClientByIP(ip netip.Addr) (TailscaleClient, erro
|
|||||||
return nil, errNoClientFound
|
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) {
|
func (s *Scenario) ListTailscaleClientsIPs(users ...string) ([]netip.Addr, error) {
|
||||||
var allIps []netip.Addr
|
var allIps []netip.Addr
|
||||||
|
|
||||||
@ -482,6 +528,8 @@ func (s *Scenario) ListTailscaleClientsIPs(users ...string) ([]netip.Addr, error
|
|||||||
return allIps, nil
|
return allIps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListTailscaleClientsIPs returns a list of FQDN based on Users
|
||||||
|
// passed as parameters.
|
||||||
func (s *Scenario) ListTailscaleClientsFQDNs(users ...string) ([]string, error) {
|
func (s *Scenario) ListTailscaleClientsFQDNs(users ...string) ([]string, error) {
|
||||||
allFQDNs := make([]string, 0)
|
allFQDNs := make([]string, 0)
|
||||||
|
|
||||||
@ -502,6 +550,8 @@ func (s *Scenario) ListTailscaleClientsFQDNs(users ...string) ([]string, error)
|
|||||||
return allFQDNs, nil
|
return allFQDNs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WaitForTailscaleLogout blocks execution until all TailscaleClients have
|
||||||
|
// logged out of the ControlServer.
|
||||||
func (s *Scenario) WaitForTailscaleLogout() {
|
func (s *Scenario) WaitForTailscaleLogout() {
|
||||||
for _, user := range s.users {
|
for _, user := range s.users {
|
||||||
for _, client := range user.Clients {
|
for _, client := range user.Clients {
|
||||||
|
@ -36,6 +36,8 @@ var (
|
|||||||
errTailscaleNotLoggedOut = errors.New("tailscale not logged out")
|
errTailscaleNotLoggedOut = errors.New("tailscale not logged out")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TailscaleInContainer is an implementation of TailscaleClient which
|
||||||
|
// sets up a Tailscale instance inside a container.
|
||||||
type TailscaleInContainer struct {
|
type TailscaleInContainer struct {
|
||||||
version string
|
version string
|
||||||
hostname string
|
hostname string
|
||||||
@ -55,14 +57,22 @@ type TailscaleInContainer struct {
|
|||||||
withTags []string
|
withTags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Option represent optional settings that can be given to a
|
||||||
|
// Tailscale instance.
|
||||||
type Option = func(c *TailscaleInContainer)
|
type Option = func(c *TailscaleInContainer)
|
||||||
|
|
||||||
|
// WithHeadscaleTLS takes the certificate of the Headscale instance
|
||||||
|
// and adds it to the trusted surtificate of the Tailscale container.
|
||||||
func WithHeadscaleTLS(cert []byte) Option {
|
func WithHeadscaleTLS(cert []byte) Option {
|
||||||
return func(tsic *TailscaleInContainer) {
|
return func(tsic *TailscaleInContainer) {
|
||||||
tsic.headscaleCert = cert
|
tsic.headscaleCert = cert
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithOrCreateNetwork sets the Docker container network to use with
|
||||||
|
// the Tailscale instance, if the parameter is nil, a new network,
|
||||||
|
// isolating the TailscaleClient, will be created. If a network is
|
||||||
|
// passed, the Tailscale instance will join the given network.
|
||||||
func WithOrCreateNetwork(network *dockertest.Network) Option {
|
func WithOrCreateNetwork(network *dockertest.Network) Option {
|
||||||
return func(tsic *TailscaleInContainer) {
|
return func(tsic *TailscaleInContainer) {
|
||||||
if network != nil {
|
if network != nil {
|
||||||
@ -83,24 +93,29 @@ func WithOrCreateNetwork(network *dockertest.Network) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithHeadscaleName set the name of the headscale instance,
|
||||||
|
// mostly useful in combination with TLS and WithHeadscaleTLS.
|
||||||
func WithHeadscaleName(hsName string) Option {
|
func WithHeadscaleName(hsName string) Option {
|
||||||
return func(tsic *TailscaleInContainer) {
|
return func(tsic *TailscaleInContainer) {
|
||||||
tsic.headscaleHostname = hsName
|
tsic.headscaleHostname = hsName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithTags associates the given tags to the Tailscale instance.
|
||||||
func WithTags(tags []string) Option {
|
func WithTags(tags []string) Option {
|
||||||
return func(tsic *TailscaleInContainer) {
|
return func(tsic *TailscaleInContainer) {
|
||||||
tsic.withTags = tags
|
tsic.withTags = tags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithSSH enables SSH for the Tailscale instance.
|
||||||
func WithSSH() Option {
|
func WithSSH() Option {
|
||||||
return func(tsic *TailscaleInContainer) {
|
return func(tsic *TailscaleInContainer) {
|
||||||
tsic.withSSH = true
|
tsic.withSSH = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New returns a new TailscaleInContainer instance.
|
||||||
func New(
|
func New(
|
||||||
pool *dockertest.Pool,
|
pool *dockertest.Pool,
|
||||||
version string,
|
version string,
|
||||||
@ -182,22 +197,29 @@ func (t *TailscaleInContainer) hasTLS() bool {
|
|||||||
return len(t.headscaleCert) != 0
|
return len(t.headscaleCert) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown stops and cleans up the Tailscale container.
|
||||||
func (t *TailscaleInContainer) Shutdown() error {
|
func (t *TailscaleInContainer) Shutdown() error {
|
||||||
return t.pool.Purge(t.container)
|
return t.pool.Purge(t.container)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hostname returns the hostname of the Tailscale instance.
|
||||||
func (t *TailscaleInContainer) Hostname() string {
|
func (t *TailscaleInContainer) Hostname() string {
|
||||||
return t.hostname
|
return t.hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Version returns the running Tailscale version of the instance.
|
||||||
func (t *TailscaleInContainer) Version() string {
|
func (t *TailscaleInContainer) Version() string {
|
||||||
return t.version
|
return t.version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID returns the Docker container ID of the TailscaleInContainer
|
||||||
|
// instance.
|
||||||
func (t *TailscaleInContainer) ID() string {
|
func (t *TailscaleInContainer) ID() string {
|
||||||
return t.container.Container.ID
|
return t.container.Container.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute runs a command inside the Tailscale container and returns the
|
||||||
|
// result of stdout as a string.
|
||||||
func (t *TailscaleInContainer) Execute(
|
func (t *TailscaleInContainer) Execute(
|
||||||
command []string,
|
command []string,
|
||||||
) (string, string, error) {
|
) (string, string, error) {
|
||||||
@ -223,6 +245,8 @@ func (t *TailscaleInContainer) Execute(
|
|||||||
return stdout, stderr, nil
|
return stdout, stderr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Up runs the login routine on the given Tailscale instance.
|
||||||
|
// This login mechanism uses the authorised key for authentication.
|
||||||
func (t *TailscaleInContainer) Up(
|
func (t *TailscaleInContainer) Up(
|
||||||
loginServer, authKey string,
|
loginServer, authKey string,
|
||||||
) error {
|
) error {
|
||||||
@ -254,6 +278,8 @@ func (t *TailscaleInContainer) Up(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Up runs the login routine on the given Tailscale instance.
|
||||||
|
// This login mechanism uses web + command line flow for authentication.
|
||||||
func (t *TailscaleInContainer) UpWithLoginURL(
|
func (t *TailscaleInContainer) UpWithLoginURL(
|
||||||
loginServer string,
|
loginServer string,
|
||||||
) (*url.URL, error) {
|
) (*url.URL, error) {
|
||||||
@ -286,6 +312,7 @@ func (t *TailscaleInContainer) UpWithLoginURL(
|
|||||||
return loginURL, nil
|
return loginURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logout runs the logout routine on the given Tailscale instance.
|
||||||
func (t *TailscaleInContainer) Logout() error {
|
func (t *TailscaleInContainer) Logout() error {
|
||||||
_, _, err := t.Execute([]string{"tailscale", "logout"})
|
_, _, err := t.Execute([]string{"tailscale", "logout"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -295,6 +322,7 @@ func (t *TailscaleInContainer) Logout() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPs returns the netip.Addr of the Tailscale instance.
|
||||||
func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
|
func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
|
||||||
if t.ips != nil && len(t.ips) != 0 {
|
if t.ips != nil && len(t.ips) != 0 {
|
||||||
return t.ips, nil
|
return t.ips, nil
|
||||||
@ -327,6 +355,7 @@ func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
|
|||||||
return ips, nil
|
return ips, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status returns the ipnstate.Status of the Tailscale instance.
|
||||||
func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) {
|
func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) {
|
||||||
command := []string{
|
command := []string{
|
||||||
"tailscale",
|
"tailscale",
|
||||||
@ -348,6 +377,7 @@ func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) {
|
|||||||
return &status, err
|
return &status, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FQDN returns the FQDN as a string of the Tailscale instance.
|
||||||
func (t *TailscaleInContainer) FQDN() (string, error) {
|
func (t *TailscaleInContainer) FQDN() (string, error) {
|
||||||
if t.fqdn != "" {
|
if t.fqdn != "" {
|
||||||
return t.fqdn, nil
|
return t.fqdn, nil
|
||||||
@ -361,6 +391,8 @@ func (t *TailscaleInContainer) FQDN() (string, error) {
|
|||||||
return status.Self.DNSName, nil
|
return status.Self.DNSName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WaitForReady blocks until the Tailscale (tailscaled) instance is ready
|
||||||
|
// to login or be used.
|
||||||
func (t *TailscaleInContainer) WaitForReady() error {
|
func (t *TailscaleInContainer) WaitForReady() error {
|
||||||
return t.pool.Retry(func() error {
|
return t.pool.Retry(func() error {
|
||||||
status, err := t.Status()
|
status, err := t.Status()
|
||||||
@ -376,6 +408,7 @@ func (t *TailscaleInContainer) WaitForReady() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WaitForLogout blocks until the Tailscale instance has logged out.
|
||||||
func (t *TailscaleInContainer) WaitForLogout() error {
|
func (t *TailscaleInContainer) WaitForLogout() error {
|
||||||
return t.pool.Retry(func() error {
|
return t.pool.Retry(func() error {
|
||||||
status, err := t.Status()
|
status, err := t.Status()
|
||||||
@ -391,6 +424,8 @@ func (t *TailscaleInContainer) WaitForLogout() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WaitForPeers blocks until N number of peers is present in the
|
||||||
|
// Peer list of the Tailscale instance.
|
||||||
func (t *TailscaleInContainer) WaitForPeers(expected int) error {
|
func (t *TailscaleInContainer) WaitForPeers(expected int) error {
|
||||||
return t.pool.Retry(func() error {
|
return t.pool.Retry(func() error {
|
||||||
status, err := t.Status()
|
status, err := t.Status()
|
||||||
@ -407,32 +442,42 @@ func (t *TailscaleInContainer) WaitForPeers(expected int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
// PingOption repreent optional settings that can be given
|
||||||
|
// to ping another host.
|
||||||
PingOption = func(args *pingArgs)
|
PingOption = func(args *pingArgs)
|
||||||
pingArgs struct {
|
|
||||||
|
pingArgs struct {
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
count int
|
count int
|
||||||
direct bool
|
direct bool
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WithPingTimeout sets the timeout for the ping command.
|
||||||
func WithPingTimeout(timeout time.Duration) PingOption {
|
func WithPingTimeout(timeout time.Duration) PingOption {
|
||||||
return func(args *pingArgs) {
|
return func(args *pingArgs) {
|
||||||
args.timeout = timeout
|
args.timeout = timeout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithPingCount sets the count of pings to attempt.
|
||||||
func WithPingCount(count int) PingOption {
|
func WithPingCount(count int) PingOption {
|
||||||
return func(args *pingArgs) {
|
return func(args *pingArgs) {
|
||||||
args.count = count
|
args.count = count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithPingUntilDirect decides if the ping should only succeed
|
||||||
|
// if a direct connection is established or if successful
|
||||||
|
// DERP ping is sufficient.
|
||||||
func WithPingUntilDirect(direct bool) PingOption {
|
func WithPingUntilDirect(direct bool) PingOption {
|
||||||
return func(args *pingArgs) {
|
return func(args *pingArgs) {
|
||||||
args.direct = direct
|
args.direct = direct
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ping executes the Tailscale ping command and pings a hostname
|
||||||
|
// or IP. It accepts a series of PingOption.
|
||||||
// TODO(kradalby): Make multiping, go routine magic.
|
// TODO(kradalby): Make multiping, go routine magic.
|
||||||
func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) error {
|
func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) error {
|
||||||
args := pingArgs{
|
args := pingArgs{
|
||||||
@ -475,6 +520,7 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) err
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteFile save file inside the Tailscale container.
|
||||||
func (t *TailscaleInContainer) WriteFile(path string, data []byte) error {
|
func (t *TailscaleInContainer) WriteFile(path string, data []byte) error {
|
||||||
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
|
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user