From e65ce17f7b0250d41ca066660075751771088927 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 3 Feb 2023 12:24:27 +0100 Subject: [PATCH] Add documentation to integration test framework so tsic, hsic and scenario Signed-off-by: Kristoffer Dalby --- integration/hsic/hsic.go | 36 ++++++++++++++++++++++++++++ integration/scenario.go | 52 +++++++++++++++++++++++++++++++++++++++- integration/tsic/tsic.go | 48 ++++++++++++++++++++++++++++++++++++- 3 files changed, 134 insertions(+), 2 deletions(-) diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index c08755b6..d00efe46 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -41,6 +41,8 @@ type fileInContainer struct { contents []byte } +// HeadscaleInContainer is an implementation of ControlServer which +// sets up a Headscale instance inside a container. type HeadscaleInContainer struct { hostname string @@ -57,8 +59,12 @@ type HeadscaleInContainer struct { filesInContainer []fileInContainer } +// Option represent optional settings that can be given to a +// Headscale instance. type Option = func(c *HeadscaleInContainer) +// WithACLPolicy adds a headscale.ACLPolicy policy to the +// HeadscaleInContainer instance. func WithACLPolicy(acl *headscale.ACLPolicy) Option { return func(hsic *HeadscaleInContainer) { // TODO(kradalby): Move somewhere appropriate @@ -68,6 +74,7 @@ func WithACLPolicy(acl *headscale.ACLPolicy) Option { } } +// WithTLS creates certificates and enables HTTPS. func WithTLS() Option { return func(hsic *HeadscaleInContainer) { 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 { return func(hsic *HeadscaleInContainer) { 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 { return func(hsic *HeadscaleInContainer) { 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 { return func(hsic *HeadscaleInContainer) { 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 { return func(hsic *HeadscaleInContainer) { 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 { return func(hsic *HeadscaleInContainer) { hsic.filesInContainer = append(hsic.filesInContainer, @@ -126,6 +141,7 @@ func WithFileInContainer(path string, contents []byte) Option { } } +// New returns a new HeadscaleInContainer instance. func New( pool *dockertest.Pool, network *dockertest.Network, @@ -244,14 +260,19 @@ func (t *HeadscaleInContainer) hasTLS() bool { return len(t.tlsCert) != 0 && len(t.tlsKey) != 0 } +// Shutdown stops and cleans up the Headscale container. func (t *HeadscaleInContainer) Shutdown() error { 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 { 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( command []string, ) (string, error) { @@ -273,18 +294,23 @@ func (t *HeadscaleInContainer) Execute( return stdout, nil } +// GetIP returns the docker container IP as a string. func (t *HeadscaleInContainer) GetIP() string { return t.container.GetIPInNetwork(t.network) } +// GetPort returns the docker container port as a string. func (t *HeadscaleInContainer) GetPort() string { return fmt.Sprintf("%d", t.port) } +// GetHealthEndpoint returns a health endpoint for the HeadscaleInContainer +// instance. func (t *HeadscaleInContainer) GetHealthEndpoint() string { return fmt.Sprintf("%s/health", t.GetEndpoint()) } +// GetEndpoint returns the Headscale endpoint for the HeadscaleInContainer. func (t *HeadscaleInContainer) GetEndpoint() string { hostEndpoint := fmt.Sprintf("%s:%d", t.GetIP(), @@ -297,14 +323,18 @@ func (t *HeadscaleInContainer) GetEndpoint() string { return fmt.Sprintf("http://%s", hostEndpoint) } +// GetCert returns the public certificate of the HeadscaleInContainer. func (t *HeadscaleInContainer) GetCert() []byte { return t.tlsCert } +// GetHostname returns the hostname of the HeadscaleInContainer. func (t *HeadscaleInContainer) GetHostname() string { return t.hostname } +// WaitForReady blocks until the Headscale instance is ready to +// serve clients. func (t *HeadscaleInContainer) WaitForReady() error { 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( user string, ) error { @@ -349,6 +380,8 @@ func (t *HeadscaleInContainer) CreateUser( 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( user string, reusable bool, @@ -392,6 +425,8 @@ func (t *HeadscaleInContainer) CreateAuthKey( return &preAuthKey, nil } +// ListMachinesInUser list the TailscaleClients (Machine, Headscale internal representation) +// associated with a user. func (t *HeadscaleInContainer) ListMachinesInUser( user string, ) ([]*v1.Machine, error) { @@ -415,6 +450,7 @@ func (t *HeadscaleInContainer) ListMachinesInUser( return nodes, nil } +// WriteFile save file inside the Headscale container. func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error { return integrationutil.WriteFileToContainer(t.pool, t.container, path, data) } diff --git a/integration/scenario.go b/integration/scenario.go index cfdaa97e..9f800238 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -57,12 +57,23 @@ var ( // "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( tailscaleVersions2021, tailscaleVersions2019..., ) ) +// User represents a User in the ControlServer and a map of TailscaleClient's +// associated with the User. type User struct { Clients map[string]TailscaleClient @@ -71,6 +82,10 @@ type User struct { 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. type Scenario struct { // TODO(kradalby): support multiple headcales for later, currently only @@ -85,6 +100,8 @@ type Scenario struct { 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) { hash, err := headscale.GenerateRandomStringDNSSafe(scenarioHashLength) if err != nil { @@ -125,6 +142,10 @@ func NewScenario() (*Scenario, error) { }, 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 { s.controlServers.Range(func(_ string, control ControlServer) bool { err := control.SaveLog("/tmp/control") @@ -168,6 +189,7 @@ func (s *Scenario) Shutdown() error { return 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 { @@ -180,6 +202,9 @@ func (s *Scenario) Users() []string { /// 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.headscaleLock.Lock() @@ -204,6 +229,8 @@ func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) { 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 string, reusable bool, @@ -221,6 +248,8 @@ func (s *Scenario) CreatePreAuthKey( 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 { if headscale, err := s.Headscale(); err == nil { err := headscale.CreateUser(user) @@ -240,6 +269,8 @@ func (s *Scenario) CreateUser(user string) error { /// Client related stuff +// CreateTailscaleNodesInUser creates and adds a new TailscaleClient to a +// User in the Scenario. func (s *Scenario) CreateTailscaleNodesInUser( userStr string, requestedVersion string, @@ -300,6 +331,8 @@ func (s *Scenario) CreateTailscaleNodesInUser( 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 { @@ -328,6 +361,8 @@ func (s *Scenario) RunTailscaleUp( 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 @@ -338,6 +373,8 @@ func (s *Scenario) CountTailscale() int { 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() @@ -358,7 +395,7 @@ func (s *Scenario) WaitForTailscaleSync() error { 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 // users. func (s *Scenario) CreateHeadscaleEnv( @@ -396,6 +433,8 @@ func (s *Scenario) CreateHeadscaleEnv( 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) { var ips []netip.Addr 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) } +// GetIPs 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 { @@ -426,6 +466,8 @@ func (s *Scenario) GetClients(user string) ([]TailscaleClient, error) { 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 @@ -445,6 +487,8 @@ func (s *Scenario) ListTailscaleClients(users ...string) ([]TailscaleClient, err 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 { @@ -463,6 +507,8 @@ func (s *Scenario) FindTailscaleClientByIP(ip netip.Addr) (TailscaleClient, erro 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 @@ -482,6 +528,8 @@ func (s *Scenario) ListTailscaleClientsIPs(users ...string) ([]netip.Addr, error return allIps, nil } +// ListTailscaleClientsIPs returns a list of FQDN based on Users +// passed as parameters. func (s *Scenario) ListTailscaleClientsFQDNs(users ...string) ([]string, error) { allFQDNs := make([]string, 0) @@ -502,6 +550,8 @@ func (s *Scenario) ListTailscaleClientsFQDNs(users ...string) ([]string, error) return allFQDNs, nil } +// WaitForTailscaleLogout blocks execution until all TailscaleClients have +// logged out of the ControlServer. func (s *Scenario) WaitForTailscaleLogout() { for _, user := range s.users { for _, client := range user.Clients { diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go index 9c11bd54..07579735 100644 --- a/integration/tsic/tsic.go +++ b/integration/tsic/tsic.go @@ -36,6 +36,8 @@ var ( 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 { version string hostname string @@ -55,14 +57,22 @@ type TailscaleInContainer struct { withTags []string } +// Option represent optional settings that can be given to a +// Tailscale instance. 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 { return func(tsic *TailscaleInContainer) { 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 { return func(tsic *TailscaleInContainer) { 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 { return func(tsic *TailscaleInContainer) { tsic.headscaleHostname = hsName } } +// WithTags associates the given tags to the Tailscale instance. func WithTags(tags []string) Option { return func(tsic *TailscaleInContainer) { tsic.withTags = tags } } +// WithSSH enables SSH for the Tailscale instance. func WithSSH() Option { return func(tsic *TailscaleInContainer) { tsic.withSSH = true } } +// New returns a new TailscaleInContainer instance. func New( pool *dockertest.Pool, version string, @@ -182,22 +197,29 @@ func (t *TailscaleInContainer) hasTLS() bool { return len(t.headscaleCert) != 0 } +// Shutdown stops and cleans up the Tailscale container. func (t *TailscaleInContainer) Shutdown() error { return t.pool.Purge(t.container) } +// Hostname returns the hostname of the Tailscale instance. func (t *TailscaleInContainer) Hostname() string { return t.hostname } +// Version returns the running Tailscale version of the instance. func (t *TailscaleInContainer) Version() string { return t.version } +// ID returns the Docker container ID of the TailscaleInContainer +// instance. func (t *TailscaleInContainer) ID() string { 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( command []string, ) (string, string, error) { @@ -223,6 +245,8 @@ func (t *TailscaleInContainer) Execute( 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( loginServer, authKey string, ) error { @@ -254,6 +278,8 @@ func (t *TailscaleInContainer) Up( 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( loginServer string, ) (*url.URL, error) { @@ -286,6 +312,7 @@ func (t *TailscaleInContainer) UpWithLoginURL( return loginURL, nil } +// Logout runs the logout routine on the given Tailscale instance. func (t *TailscaleInContainer) Logout() error { _, _, err := t.Execute([]string{"tailscale", "logout"}) if err != nil { @@ -295,6 +322,7 @@ func (t *TailscaleInContainer) Logout() error { return nil } +// IPs returns the netip.Addr of the Tailscale instance. func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) { if t.ips != nil && len(t.ips) != 0 { return t.ips, nil @@ -327,6 +355,7 @@ func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) { return ips, nil } +// Status returns the ipnstate.Status of the Tailscale instance. func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) { command := []string{ "tailscale", @@ -348,6 +377,7 @@ func (t *TailscaleInContainer) Status() (*ipnstate.Status, error) { return &status, err } +// FQDN returns the FQDN as a string of the Tailscale instance. func (t *TailscaleInContainer) FQDN() (string, error) { if t.fqdn != "" { return t.fqdn, nil @@ -361,6 +391,8 @@ func (t *TailscaleInContainer) FQDN() (string, error) { return status.Self.DNSName, nil } +// WaitForReady blocks until the Tailscale (tailscaled) instance is ready +// to login or be used. func (t *TailscaleInContainer) WaitForReady() error { return t.pool.Retry(func() error { 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 { return t.pool.Retry(func() error { 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 { return t.pool.Retry(func() error { status, err := t.Status() @@ -407,32 +442,42 @@ func (t *TailscaleInContainer) WaitForPeers(expected int) error { } type ( + // PingOption repreent optional settings that can be given + // to ping another host. PingOption = func(args *pingArgs) - pingArgs struct { + + pingArgs struct { timeout time.Duration count int direct bool } ) +// WithPingTimeout sets the timeout for the ping command. func WithPingTimeout(timeout time.Duration) PingOption { return func(args *pingArgs) { args.timeout = timeout } } +// WithPingCount sets the count of pings to attempt. func WithPingCount(count int) PingOption { return func(args *pingArgs) { 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 { return func(args *pingArgs) { 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. func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) error { 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 { return integrationutil.WriteFileToContainer(t.pool, t.container, path, data) }