Skip to content

Commit

Permalink
Merge pull request #81 from kradalby/integration-tests
Browse files Browse the repository at this point in the history
Add Integration tests
  • Loading branch information
juanfont authored Aug 12, 2021
2 parents 2752149 + 0e1ddf9 commit 9c2a630
Show file tree
Hide file tree
Showing 14 changed files with 408 additions and 5 deletions.
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// integration tests are not needed in docker
// ignoring it let us speed up the integration test
// development
integration_test.go

Dockerfile*
docker-compose*
.dockerignore
.goreleaser.yml
.git
.github
.gitignore
README.md
LICENSE
.vscode

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
config.json
*.key
/db.sqlite
*.sqlite3
15 changes: 11 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
FROM golang:latest AS build
ENV GOPATH /go
COPY . /go/src/headscale

COPY go.mod go.sum /go/src/headscale/
WORKDIR /go/src/headscale
RUN go mod download

COPY . /go/src/headscale

RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale
RUN test -e /go/bin/headscale

FROM scratch
COPY --from=build /go/bin/headscale /go/bin/headscale
FROM ubuntu:latest

COPY --from=build /go/bin/headscale /usr/local/bin/headscale
ENV TZ UTC

EXPOSE 8080/tcp
ENTRYPOINT ["/go/bin/headscale"]
CMD ["headscale"]
9 changes: 9 additions & 0 deletions Dockerfile.tailscale
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM ubuntu:latest

RUN apt-get update \
&& apt-get install -y gnupg curl \
&& curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.gpg | apt-key add - \
&& curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
&& apt-get update \
&& apt-get install -y tailscale \
&& rm -rf /var/lib/apt/lists/*
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ dev: lint test build
test:
@go test -coverprofile=coverage.out ./...

test_integration:
go test -tags integration -timeout 30m ./...

coverprofile_func:
go tool cover -func=coverage.out

Expand Down
1 change: 1 addition & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ func (h *Headscale) watchForKVUpdatesWorker() {
// Serve launches a GIN server with the Headscale API
func (h *Headscale) Serve() error {
r := gin.Default()
r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"healthy": "ok"}) })
r.GET("/key", h.KeyHandler)
r.GET("/register", h.RegisterWebAPI)
r.POST("/machine/:id/map", h.PollNetMapHandler)
Expand Down
2 changes: 1 addition & 1 deletion cmd/headscale/cli/preauthkeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ var createPreAuthKeyCmd = &cobra.Command{
fmt.Println(err)
return
}
fmt.Printf("Key: %s\n", k.Key)
fmt.Printf("%s\n", k.Key)
},
}

Expand Down
12 changes: 12 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,29 @@ go 1.16

require (
github.com/AlecAivazis/survey/v2 v2.0.5
github.com/Microsoft/go-winio v0.5.0 // indirect
github.com/cenkalti/backoff/v3 v3.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
github.com/containerd/continuity v0.1.0 // indirect
github.com/docker/cli v20.10.8+incompatible // indirect
github.com/docker/docker v20.10.8+incompatible // indirect
github.com/efekarakus/termcolor v1.0.1 // indirect
github.com/gin-gonic/gin v1.7.2
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
github.com/klauspost/compress v1.13.1
github.com/lib/pq v1.10.2 // indirect
github.com/mattn/go-sqlite3 v1.14.7 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/opencontainers/runc v1.0.1 // indirect
github.com/ory/dockertest/v3 v3.7.0 // indirect
github.com/rs/zerolog v1.23.0 // indirect
github.com/spf13/cobra v1.1.3
github.com/spf13/viper v1.8.1
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
gopkg.in/yaml.v2 v2.4.0
gorm.io/datatypes v1.0.1
Expand Down
89 changes: 89 additions & 0 deletions go.sum

Large diffs are not rendered by default.

246 changes: 246 additions & 0 deletions integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// +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", hostname}

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
}
}
3 changes: 3 additions & 0 deletions integration_test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
derp.yaml
*.sqlite
*.sqlite3
11 changes: 11 additions & 0 deletions integration_test/etc/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"server_url": "http://headscale:8080",
"listen_addr": "0.0.0.0:8080",
"private_key_path": "private.key",
"derp_map_path": "derp.yaml",
"ephemeral_node_inactivity_timeout": "30m",
"db_type": "sqlite3",
"db_path": "/tmp/integration_test_db.sqlite3",
"acl_policy_path": "",
"log_level": "trace"
}
1 change: 1 addition & 0 deletions integration_test/etc/private.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SEmQwCu+tGywQWEUsf93TpTRUvlB7WhnCdHgWrSXjEA=
4 changes: 4 additions & 0 deletions machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ func (m Machine) toNode() (*tailcfg.Node, error) {
addrs := []netaddr.IPPrefix{}
ip, err := netaddr.ParseIPPrefix(fmt.Sprintf("%s/32", m.IPAddress))
if err != nil {
log.Trace().
Str("func", "toNode").
Str("ip", m.IPAddress).
Msgf("Failed to parse IP Prefix from IP: %s", m.IPAddress)
return nil, err
}
addrs = append(addrs, ip) // missing the ipv6 ?
Expand Down

0 comments on commit 9c2a630

Please sign in to comment.