4e077b053c
This commit adds integration tests to headscale. They are currently quite simple, but it lays the groundwork for more comprehensive testing and ensuring we dont break things with the official tailscale client. The test works by leveraging Docker (via dockertest) to spin up a Headscale container, and a number of tailscale containers (10). Each tailscale container is joined to the headscale and then "passed on" to the tests. Currently three tests have been implemented: - Have all tailscale containers join headscale (in the setup process) - Get IP from each container (I plan to extend this with cross-ping) - List nodes with headscales CLI and verify all has been registered This test depends on Docker, and currently, I have not looked into hooking it into Github Actions.
246 lines
6.4 KiB
Go
246 lines
6.4 KiB
Go
// +build integration
|
|
|
|
package headscale
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/ory/dockertest/v3"
|
|
"github.com/ory/dockertest/v3/docker"
|
|
"inet.af/netaddr"
|
|
|
|
"gopkg.in/check.v1"
|
|
)
|
|
|
|
var _ = check.Suite(&IntegrationSuite{})
|
|
|
|
type IntegrationSuite struct{}
|
|
|
|
var integrationTmpDir string
|
|
var ih Headscale
|
|
|
|
var pool dockertest.Pool
|
|
var network dockertest.Network
|
|
var headscale dockertest.Resource
|
|
var tailscaleCount int = 10
|
|
var tailscales map[string]dockertest.Resource
|
|
|
|
func executeCommand(resource *dockertest.Resource, cmd []string) (string, error) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
|
|
exitCode, err := resource.Exec(
|
|
cmd,
|
|
dockertest.ExecOptions{
|
|
StdOut: &stdout,
|
|
StdErr: &stderr,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if exitCode != 0 {
|
|
fmt.Println("Command: ", cmd)
|
|
fmt.Println("stdout: ", stdout.String())
|
|
fmt.Println("stderr: ", stderr.String())
|
|
return "", fmt.Errorf("command failed with: %s", stderr.String())
|
|
}
|
|
|
|
return stdout.String(), nil
|
|
}
|
|
|
|
func dockerRestartPolicy(config *docker.HostConfig) {
|
|
// set AutoRemove to true so that stopped container goes away by itself
|
|
config.AutoRemove = true
|
|
config.RestartPolicy = docker.RestartPolicy{
|
|
Name: "no",
|
|
}
|
|
}
|
|
|
|
func (s *IntegrationSuite) SetUpSuite(c *check.C) {
|
|
var err error
|
|
h = Headscale{
|
|
dbType: "sqlite3",
|
|
dbString: "integration_test_db.sqlite3",
|
|
}
|
|
|
|
if ppool, err := dockertest.NewPool(""); err == nil {
|
|
pool = *ppool
|
|
} else {
|
|
log.Fatalf("Could not connect to docker: %s", err)
|
|
}
|
|
|
|
if pnetwork, err := pool.CreateNetwork("headscale-test"); err == nil {
|
|
network = *pnetwork
|
|
} else {
|
|
log.Fatalf("Could not create network: %s", err)
|
|
}
|
|
|
|
headscaleBuildOptions := &dockertest.BuildOptions{
|
|
Dockerfile: "Dockerfile",
|
|
ContextDir: ".",
|
|
}
|
|
|
|
tailscaleBuildOptions := &dockertest.BuildOptions{
|
|
Dockerfile: "Dockerfile.tailscale",
|
|
ContextDir: ".",
|
|
}
|
|
|
|
currentPath, err := os.Getwd()
|
|
if err != nil {
|
|
log.Fatalf("Could not determine current path: %s", err)
|
|
}
|
|
|
|
headscaleOptions := &dockertest.RunOptions{
|
|
Name: "headscale",
|
|
Mounts: []string{
|
|
fmt.Sprintf("%s/integration_test/etc:/etc/headscale", currentPath),
|
|
fmt.Sprintf("%s/derp.yaml:/etc/headscale/derp.yaml", currentPath),
|
|
},
|
|
Networks: []*dockertest.Network{&network},
|
|
// Cmd: []string{"sleep", "3600"},
|
|
Cmd: []string{"headscale", "serve"},
|
|
PortBindings: map[docker.Port][]docker.PortBinding{
|
|
"8080/tcp": []docker.PortBinding{{HostPort: "8080"}},
|
|
},
|
|
Env: []string{},
|
|
}
|
|
|
|
fmt.Println("Creating headscale container")
|
|
if pheadscale, err := pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, dockerRestartPolicy); err == nil {
|
|
headscale = *pheadscale
|
|
} else {
|
|
log.Fatalf("Could not start resource: %s", err)
|
|
}
|
|
fmt.Println("Created headscale container")
|
|
|
|
fmt.Println("Creating tailscale containers")
|
|
tailscales = make(map[string]dockertest.Resource)
|
|
for i := 0; i < tailscaleCount; i++ {
|
|
hostname := fmt.Sprintf("tailscale%d", i)
|
|
tailscaleOptions := &dockertest.RunOptions{
|
|
Name: hostname,
|
|
Networks: []*dockertest.Network{&network},
|
|
// Make the container run until killed
|
|
// Cmd: []string{"sleep", "3600"},
|
|
Cmd: []string{"tailscaled", "--tun=userspace-networking", "--socks5-server=localhost:1055"},
|
|
Env: []string{},
|
|
}
|
|
|
|
if pts, err := pool.BuildAndRunWithBuildOptions(tailscaleBuildOptions, tailscaleOptions, dockerRestartPolicy); err == nil {
|
|
tailscales[hostname] = *pts
|
|
} else {
|
|
log.Fatalf("Could not start resource: %s", err)
|
|
}
|
|
fmt.Printf("Created %s container\n", hostname)
|
|
}
|
|
|
|
fmt.Println("Waiting for headscale to be ready")
|
|
hostEndpoint := fmt.Sprintf("localhost:%s", headscale.GetPort("8080/tcp"))
|
|
|
|
if err := pool.Retry(func() error {
|
|
url := fmt.Sprintf("http://%s/health", hostEndpoint)
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("status code not OK")
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
log.Fatalf("Could not connect to docker: %s", err)
|
|
}
|
|
fmt.Println("headscale container is ready")
|
|
|
|
fmt.Println("Creating headscale namespace")
|
|
result, err := executeCommand(
|
|
&headscale,
|
|
[]string{"headscale", "namespaces", "create", "test"},
|
|
)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
fmt.Println("Creating pre auth key")
|
|
authKey, err := executeCommand(
|
|
&headscale,
|
|
[]string{"headscale", "-n", "test", "preauthkeys", "create", "--reusable", "--expiration", "24h"},
|
|
)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
headscaleEndpoint := fmt.Sprintf("http://headscale:%s", headscale.GetPort("8080/tcp"))
|
|
|
|
fmt.Printf("Joining tailscale containers to headscale at %s\n", headscaleEndpoint)
|
|
for hostname, tailscale := range tailscales {
|
|
command := []string{"tailscale", "up", "-login-server", headscaleEndpoint, "--authkey", strings.TrimSuffix(authKey, "\n"), "--hostname", headscaleEndpoint}
|
|
|
|
fmt.Println("Join command:", command)
|
|
fmt.Printf("Running join command for %s\n", hostname)
|
|
result, err = executeCommand(
|
|
&tailscale,
|
|
command,
|
|
)
|
|
fmt.Println("tailscale result: ", result)
|
|
c.Assert(err, check.IsNil)
|
|
fmt.Printf("%s joined\n", hostname)
|
|
}
|
|
}
|
|
|
|
func (s *IntegrationSuite) TearDownSuite(c *check.C) {
|
|
if err := pool.Purge(&headscale); err != nil {
|
|
log.Printf("Could not purge resource: %s\n", err)
|
|
}
|
|
|
|
for _, tailscale := range tailscales {
|
|
if err := pool.Purge(&tailscale); err != nil {
|
|
log.Printf("Could not purge resource: %s\n", err)
|
|
}
|
|
}
|
|
|
|
if err := network.Close(); err != nil {
|
|
log.Printf("Could not close network: %s\n", err)
|
|
}
|
|
}
|
|
|
|
func (s *IntegrationSuite) TestListNodes(c *check.C) {
|
|
fmt.Println("Listing nodes")
|
|
result, err := executeCommand(
|
|
&headscale,
|
|
[]string{"headscale", "-n", "test", "nodes", "list"},
|
|
)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
for hostname, _ := range tailscales {
|
|
c.Assert(strings.Contains(result, hostname), check.Equals, true)
|
|
}
|
|
}
|
|
|
|
func (s *IntegrationSuite) TestGetIpAddresses(c *check.C) {
|
|
ipPrefix := netaddr.MustParseIPPrefix("100.64.0.0/10")
|
|
ips := make(map[string]netaddr.IP)
|
|
for hostname, tailscale := range tailscales {
|
|
command := []string{"tailscale", "ip"}
|
|
|
|
result, err := executeCommand(
|
|
&tailscale,
|
|
command,
|
|
)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
ip, err := netaddr.ParseIP(strings.TrimSuffix(result, "\n"))
|
|
c.Assert(err, check.IsNil)
|
|
|
|
fmt.Printf("IP for %s: %s", hostname, result)
|
|
|
|
// c.Assert(ip.Valid(), check.IsTrue)
|
|
c.Assert(ip.Is4(), check.Equals, true)
|
|
c.Assert(ipPrefix.Contains(ip), check.Equals, true)
|
|
|
|
ips[hostname] = ip
|
|
}
|
|
}
|