chore: some initial integration tests

This commit is contained in:
Johan Siebens
2023-01-02 13:23:10 +01:00
parent 527fb34560
commit d6a564b7a9
10 changed files with 1150 additions and 1 deletions
+1 -1
View File
@@ -32,5 +32,5 @@ jobs:
go-version: 1.19
- name: Build
run: |
go test ./...
go test -v -short ./...
go build cmd/ionscale/main.go
+32
View File
@@ -0,0 +1,32 @@
name: Integration Tests
on: workflow_dispatch
jobs:
integration:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ts_version:
- "1.34.1"
- "1.32.3"
- "1.30.2"
- "1.28.0"
- "1.26.2",
- "1.24.2",
- "1.22.2",
env:
IONSCALE_TESTS_TS_TARGET_VERSION: ${{ matrix.ts_version }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
- name: Run Integration Tests
run: |
go test -v ./tests
+19
View File
@@ -63,6 +63,8 @@ require (
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/logger v0.2.0 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
github.com/aws/aws-sdk-go-v2 v1.11.2 // indirect
github.com/aws/aws-sdk-go-v2/config v1.11.0 // indirect
@@ -77,18 +79,26 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 // indirect
github.com/aws/smithy-go v1.9.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/digitalocean/godo v1.41.0 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/docker/cli v20.10.16+incompatible // indirect
github.com/docker/docker v20.10.16+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect
github.com/glebarez/go-sqlite v1.19.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa // indirect
github.com/googleapis/gax-go/v2 v2.4.0 // indirect
@@ -119,15 +129,24 @@ require (
github.com/miekg/dns v1.1.50 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
github.com/opencontainers/runc v1.1.2 // indirect
github.com/ory/dockertest/v3 v3.9.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tkuchiki/go-timezone v0.2.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opencensus.io v0.23.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
+565
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
http_listen_addr: ":8080"
server_url: "http://localhost:8080"
tls:
disable: true
force_https: false
keys:
system_admin_key: "804ecd57365342254ce6647da5c249e85c10a0e51e74856bfdf292a2136b4249"
database:
type: sqlite
url: /opt/ionscale.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)
logging:
level: trace
+19
View File
@@ -0,0 +1,19 @@
FROM golang:1.19-buster as builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . ./
RUN go build -v -o ionscale cmd/ionscale/main.go
FROM debian:buster-slim
RUN set -x && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
ca-certificates && \
rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/ionscale /usr/local/bin/ionscale
ENTRYPOINT ["/usr/local/bin/ionscale"]
+11
View File
@@ -0,0 +1,11 @@
FROM alpine:3.14.0
ARG TAILSCALE_VERSION
RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*
WORKDIR /app
ENV TSFILE=tailscale_${TAILSCALE_VERSION}_amd64.tgz
RUN wget https://pkgs.tailscale.com/stable/${TSFILE} && tar xzf ${TSFILE} --strip-components=1
RUN mkdir -p /var/run/tailscale /var/cache/tailscale /var/lib/tailscale /.cache
+80
View File
@@ -0,0 +1,80 @@
package tests
import (
"github.com/jsiebens/ionscale/tests/sc"
"github.com/stretchr/testify/assert"
"testing"
)
func TestPing(t *testing.T) {
sc.Run(t, func(s sc.Scenario) {
tailnet := s.CreateTailnet("pingtest")
key := s.CreateAuthKey(tailnet.Id, true)
nodeA := s.NewTailscaleNode("pingtest-a")
nodeB := s.NewTailscaleNode("pingtest-b")
nodeA.Up(key)
nodeB.Up(key)
nodeA.WaitForPeers(1)
nodeA.Ping("pingtest-b")
nodeA.Ping(nodeB.IPv4())
nodeA.Ping(nodeB.IPv6())
})
}
func TestGetIPs(t *testing.T) {
sc.Run(t, func(s sc.Scenario) {
tailnet := s.CreateTailnet("tailnet01")
authKey := s.CreateAuthKey(tailnet.Id, false)
tsNode := s.NewTailscaleNode("testip")
tsNode.Up(authKey)
ip4 := tsNode.IPv4()
ip6 := tsNode.IPv6()
var found = false
machines := s.ListMachines(tailnet.Id)
for _, m := range machines {
if m.Name == tsNode.Hostname() {
found = true
assert.Equal(t, m.Ipv4, ip4)
assert.Equal(t, m.Ipv6, ip6)
}
}
assert.True(t, found)
})
}
func TestNodeWithSameHostname(t *testing.T) {
sc.Run(t, func(s sc.Scenario) {
tailnet := s.CreateTailnet("tailnet01")
authKey := s.CreateAuthKey(tailnet.Id, false)
tsNode := s.NewTailscaleNode("test")
_ = tsNode.Up(authKey)
for i := 0; i < 5; i++ {
tc := s.NewTailscaleNode("test")
_ = tc.Up(authKey)
}
machines := make(map[string]bool)
for _, m := range s.ListMachines(tailnet.Id) {
machines[m.Name] = true
}
assert.Equal(t, map[string]bool{
"test": true,
"test-1": true,
"test-2": true,
"test-3": true,
"test-4": true,
"test-5": true,
}, machines)
})
}
+136
View File
@@ -0,0 +1,136 @@
package sc
import (
"bytes"
"encoding/json"
"fmt"
"github.com/ory/dockertest/v3"
"strings"
"tailscale.com/ipn/ipnstate"
"testing"
)
type TailscaleNode interface {
Hostname() string
Up(authkey string) ipnstate.Status
IPv4() string
IPv6() string
WaitForPeers(expected int)
Ping(target string)
}
type tailscaleNode struct {
t *testing.T
loginServer string
hostname string
resource *dockertest.Resource
}
func (t *tailscaleNode) Hostname() string {
return t.hostname
}
func (t *tailscaleNode) Up(authkey string) ipnstate.Status {
t.mustExecTailscaleCmd("up", "--login-server", t.loginServer, "--authkey", authkey)
return t.waitForReady()
}
func (t *tailscaleNode) IPv4() string {
return t.mustExecTailscaleCmd("ip", "-4")
}
func (t *tailscaleNode) IPv6() string {
return t.mustExecTailscaleCmd("ip", "-6")
}
func (t *tailscaleNode) waitForReady() ipnstate.Status {
var status ipnstate.Status
err := pool.Retry(func() error {
out, err := t.execTailscaleCmd("status", "--json")
if err != nil {
return err
}
if err := json.Unmarshal([]byte(out), &status); err != nil {
return err
}
if status.CurrentTailnet != nil {
return nil
}
return fmt.Errorf("not connected")
})
if err != nil {
t.t.Fatal(err)
}
return status
}
func (t *tailscaleNode) WaitForPeers(expected int) {
err := pool.Retry(func() error {
out, err := t.execTailscaleCmd("status", "--json")
if err != nil {
return err
}
var status ipnstate.Status
if err := json.Unmarshal([]byte(out), &status); err != nil {
return err
}
if len(status.Peers()) != expected {
return fmt.Errorf("incorrect peer count")
}
return nil
})
if err != nil {
t.t.Fatal(err)
}
}
func (t *tailscaleNode) Ping(target string) {
result, err := t.execTailscaleCmd("ping", "--timeout=1s", "--c=10", "--until-direct=true", target)
if err != nil {
t.t.Fatal(err)
}
if !strings.Contains(result, "pong") && !strings.Contains(result, "is local") {
t.t.Fatal("ping failed")
}
}
func (t *tailscaleNode) execTailscaleCmd(cmd ...string) (string, error) {
i := append([]string{"/app/tailscale", "--socket=/tmp/tailscaled.sock"}, cmd...)
return execCmd(t.resource, i...)
}
func (t *tailscaleNode) mustExecTailscaleCmd(cmd ...string) string {
i := append([]string{"/app/tailscale", "--socket=/tmp/tailscaled.sock"}, cmd...)
s, err := execCmd(t.resource, i...)
if err != nil {
t.t.Fatal(err)
}
return s
}
func execCmd(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 err != nil {
return strings.TrimSpace(stdout.String()), err
}
if exitCode != 0 {
return strings.TrimSpace(stdout.String()), fmt.Errorf("command failed with: %s", stderr.String())
}
return strings.TrimSpace(stdout.String()), nil
}
+271
View File
@@ -0,0 +1,271 @@
package sc
import (
"context"
"fmt"
"github.com/bufbuild/connect-go"
ionscaleclt "github.com/jsiebens/ionscale/pkg/client/ionscale"
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
ionscaleconnect "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1/ionscalev1connect"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"google.golang.org/protobuf/types/known/durationpb"
"io"
"log"
"net"
"net/http"
"os"
"strings"
"sync"
"testing"
"time"
)
const DefaultTargetVersion = "1.34.1"
var (
setupOnce sync.Once
targetVersion string
pool *dockertest.Pool
)
type Scenario interface {
NewTailscaleNode(hostname string) TailscaleNode
ListMachines(tailnetID uint64) []*api.Machine
CreateAuthKey(tailnetID uint64, ephemeral bool) string
CreateTailnet(name string) *api.Tailnet
}
type scenario struct {
t *testing.T
pool *dockertest.Pool
network *dockertest.Network
ionscale *dockertest.Resource
resources []*dockertest.Resource
client ionscaleconnect.IonscaleServiceClient
}
func (s *scenario) CreateTailnet(name string) *api.Tailnet {
createTailnetResponse, err := s.client.CreateTailnet(context.Background(), connect.NewRequest(&api.CreateTailnetRequest{Name: name}))
if err != nil {
s.t.Fatal(err)
}
return createTailnetResponse.Msg.GetTailnet()
}
func (s *scenario) CreateAuthKey(tailnetID uint64, ephemeral bool) string {
key, err := s.client.CreateAuthKey(context.Background(), connect.NewRequest(&api.CreateAuthKeyRequest{TailnetId: tailnetID, Ephemeral: ephemeral, Tags: []string{"tag:test"}, Expiry: durationpb.New(60 * time.Minute)}))
if err != nil {
s.t.Fatal(err)
}
return key.Msg.Value
}
func (s *scenario) ListMachines(tailnetID uint64) []*api.Machine {
machines, err := s.client.ListMachines(context.Background(), connect.NewRequest(&api.ListMachinesRequest{TailnetId: tailnetID}))
if err != nil {
s.t.Fatal(err)
}
return machines.Msg.Machines
}
func (s *scenario) NewTailscaleNode(hostname string) TailscaleNode {
tailscaleOptions := &dockertest.RunOptions{
Repository: fmt.Sprintf("ts-%s", strings.Replace(targetVersion, ".", "-", -1)),
Hostname: hostname,
Networks: []*dockertest.Network{s.network},
ExposedPorts: []string{"1055"},
Cmd: []string{
"/app/tailscaled", "--tun", "userspace-networking", "--socks5-server", "0.0.0.0:1055", "--socket", "/tmp/tailscaled.sock",
},
}
resource, err := s.pool.RunWithOptions(
tailscaleOptions,
restartPolicy,
)
if err != nil {
s.t.Fatal(err)
}
err = s.pool.Retry(portCheck(resource.GetPort("1055/tcp")))
if err != nil {
s.t.Fatal(err)
}
s.resources = append(s.resources, resource)
return &tailscaleNode{
t: s.t,
loginServer: "http://ionscale:8080",
hostname: hostname,
resource: resource,
}
}
func Run(t *testing.T, f func(s Scenario)) {
t.Parallel()
if testing.Short() {
t.Skip("skipped due to -short flag")
}
setupOnce.Do(prepareDockerPoolAndImages)
if pool == nil {
t.FailNow()
}
var err error
s := &scenario{t: t}
defer func() {
for _, r := range s.resources {
_ = pool.Purge(r)
}
if s.ionscale != nil {
_ = pool.Purge(s.ionscale)
}
if s.network != nil {
_ = s.network.Close()
}
s.resources = nil
s.network = nil
}()
if s.pool, err = dockertest.NewPool(""); err != nil {
t.Fatal(err)
}
s.network, err = pool.CreateNetwork("ionscale-test")
if err != nil {
t.Fatal(err)
}
currentPath, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
ionscale := &dockertest.RunOptions{
Hostname: "ionscale",
Repository: "ionscale-test",
Mounts: []string{
fmt.Sprintf("%s/config:/etc/ionscale", currentPath),
},
Networks: []*dockertest.Network{s.network},
ExposedPorts: []string{"8080"},
Cmd: []string{"server", "--config", "/etc/ionscale/config.yaml"},
}
s.ionscale, err = pool.RunWithOptions(ionscale, restartPolicy)
if err != nil {
t.Fatal(err)
}
port := s.ionscale.GetPort("8080/tcp")
err = pool.Retry(httpCheck(port, "/key"))
if err != nil {
t.Fatal(err)
}
auth, err := ionscaleclt.LoadClientAuth("804ecd57365342254ce6647da5c249e85c10a0e51e74856bfdf292a2136b4249")
if err != nil {
t.Fatal(err)
}
s.client, err = ionscaleclt.NewClient(auth, fmt.Sprintf("http://localhost:%s", port), true)
if err != nil {
t.Fatal(err)
}
f(s)
}
func restartPolicy(config *docker.HostConfig) {
config.AutoRemove = true
config.RestartPolicy = docker.RestartPolicy{
Name: "no",
}
}
func portCheck(port string) func() error {
return func() error {
conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%s", port))
if err != nil {
return err
}
defer conn.Close()
return nil
}
}
func httpCheck(port string, path string) func() error {
return func() error {
url := fmt.Sprintf("http://localhost:%s%s", port, path)
resp, err := http.Get(url)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("status code not OK")
}
return nil
}
}
func prepareDockerPoolAndImages() {
targetVersion = os.Getenv("IONSCALE_TESTS_TS_TARGET_VERSION")
if targetVersion == "" {
targetVersion = DefaultTargetVersion
}
pool, _ = dockertest.NewPool("")
buildOpts := &dockertest.BuildOptions{
ContextDir: "./docker/tailscale",
BuildArgs: []docker.BuildArg{
{
Name: "TAILSCALE_VERSION",
Value: targetVersion,
},
},
}
err := pool.Client.BuildImage(docker.BuildImageOptions{
Name: fmt.Sprintf("ts-%s", strings.Replace(targetVersion, ".", "-", -1)),
Dockerfile: buildOpts.Dockerfile,
OutputStream: io.Discard,
ContextDir: buildOpts.ContextDir,
BuildArgs: buildOpts.BuildArgs,
Platform: buildOpts.Platform,
})
if err != nil {
log.Fatal(err)
}
buildOpts = &dockertest.BuildOptions{
ContextDir: "../",
Dockerfile: "tests/docker/ionscale/Dockerfile",
}
err = pool.Client.BuildImage(docker.BuildImageOptions{
Name: "ionscale-test",
Dockerfile: buildOpts.Dockerfile,
OutputStream: io.Discard,
ContextDir: buildOpts.ContextDir,
BuildArgs: buildOpts.BuildArgs,
Platform: buildOpts.Platform,
})
if err != nil {
log.Fatal(err)
}
}