mirror of
https://github.com/jsiebens/ionscale.git
synced 2026-03-31 15:07:49 +01:00
362 lines
9.6 KiB
Go
362 lines
9.6 KiB
Go
package sc
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"github.com/bufbuild/connect-go"
|
|
petname "github.com/dustinkirkland/golang-petname"
|
|
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/jsiebens/ionscale/tests/tsn"
|
|
"github.com/jsiebens/mockoidc"
|
|
mockoidcv1 "github.com/jsiebens/mockoidc/pkg/gen/mockoidc/v1"
|
|
"github.com/jsiebens/mockoidc/pkg/gen/mockoidc/v1/mockoidcv1connect"
|
|
"github.com/ory/dockertest/v3"
|
|
"github.com/ory/dockertest/v3/docker"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/types/known/durationpb"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const DefaultTargetVersion = "stable"
|
|
|
|
var (
|
|
setupOnce sync.Once
|
|
targetVersion string
|
|
pool *dockertest.Pool
|
|
)
|
|
|
|
type Scenario struct {
|
|
t *testing.T
|
|
pool *dockertest.Pool
|
|
network *dockertest.Network
|
|
mockoidc *dockertest.Resource
|
|
ionscale *dockertest.Resource
|
|
resources []*dockertest.Resource
|
|
ionscaleClient ionscaleconnect.IonscaleServiceClient
|
|
mockoidcClient mockoidcv1connect.MockOIDCServiceClient
|
|
}
|
|
|
|
func (s *Scenario) CreateTailnet() *api.Tailnet {
|
|
name := petname.Generate(3, "-")
|
|
createTailnetResponse, err := s.ionscaleClient.CreateTailnet(context.Background(), connect.NewRequest(&api.CreateTailnetRequest{Name: name}))
|
|
require.NoError(s.t, err)
|
|
return createTailnetResponse.Msg.GetTailnet()
|
|
}
|
|
|
|
func (s *Scenario) CreateAuthKey(tailnetID uint64, ephemeral bool, tags ...string) string {
|
|
if len(tags) == 0 {
|
|
tags = []string{"tag:test"}
|
|
}
|
|
key, err := s.ionscaleClient.CreateAuthKey(context.Background(), connect.NewRequest(&api.CreateAuthKeyRequest{TailnetId: tailnetID, Ephemeral: ephemeral, Tags: tags, Expiry: durationpb.New(60 * time.Minute)}))
|
|
require.NoError(s.t, err)
|
|
return key.Msg.Value
|
|
}
|
|
|
|
func (s *Scenario) ListMachines(tailnetID uint64) []*api.Machine {
|
|
machines, err := s.ionscaleClient.ListMachines(context.Background(), connect.NewRequest(&api.ListMachinesRequest{TailnetId: tailnetID}))
|
|
require.NoError(s.t, err)
|
|
return machines.Msg.Machines
|
|
}
|
|
|
|
func (s *Scenario) AuthorizeMachines(tailnetID uint64) {
|
|
machines := s.ListMachines(tailnetID)
|
|
for _, m := range machines {
|
|
_, err := s.ionscaleClient.AuthorizeMachine(context.Background(), connect.NewRequest(&api.AuthorizeMachineRequest{MachineId: m.Id}))
|
|
require.NoError(s.t, err)
|
|
}
|
|
}
|
|
|
|
func (s *Scenario) ExpireMachines(tailnetID uint64) {
|
|
machines := s.ListMachines(tailnetID)
|
|
for _, m := range machines {
|
|
_, err := s.ionscaleClient.ExpireMachine(context.Background(), connect.NewRequest(&api.ExpireMachineRequest{MachineId: m.Id}))
|
|
require.NoError(s.t, err)
|
|
}
|
|
}
|
|
|
|
func (s *Scenario) SetACLPolicy(tailnetID uint64, policy *api.ACLPolicy) {
|
|
_, err := s.ionscaleClient.SetACLPolicy(context.Background(), connect.NewRequest(&api.SetACLPolicyRequest{TailnetId: tailnetID, Policy: policy}))
|
|
require.NoError(s.t, err)
|
|
}
|
|
|
|
func (s *Scenario) SetIAMPolicy(tailnetID uint64, policy *api.IAMPolicy) {
|
|
_, err := s.ionscaleClient.SetIAMPolicy(context.Background(), connect.NewRequest(&api.SetIAMPolicyRequest{TailnetId: tailnetID, Policy: policy}))
|
|
require.NoError(s.t, err)
|
|
}
|
|
|
|
func (s *Scenario) EnableMachineAutorization(tailnetID uint64) {
|
|
_, err := s.ionscaleClient.EnableMachineAuthorization(context.Background(), connect.NewRequest(&api.EnableMachineAuthorizationRequest{TailnetId: tailnetID}))
|
|
require.NoError(s.t, err)
|
|
}
|
|
|
|
func (s *Scenario) PushOIDCUser(sub, email, preferredUsername string) {
|
|
_, err := s.mockoidcClient.PushUser(context.Background(), connect.NewRequest(&mockoidcv1.PushUserRequest{Subject: sub, Email: email, PreferredUsername: preferredUsername}))
|
|
require.NoError(s.t, err)
|
|
}
|
|
|
|
func (s *Scenario) printIonscaleLogs() error {
|
|
var stdout bytes.Buffer
|
|
|
|
err := s.pool.Client.Logs(docker.LogsOptions{
|
|
Context: context.TODO(),
|
|
Container: s.ionscale.Container.ID,
|
|
OutputStream: &stdout,
|
|
ErrorStream: io.Discard,
|
|
Tail: "all",
|
|
RawTerminal: false,
|
|
Stdout: true,
|
|
Stderr: true,
|
|
Follow: false,
|
|
Timestamps: false,
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = os.Stdout.Write(stdout.Bytes())
|
|
|
|
return err
|
|
}
|
|
|
|
type TailscaleNodeConfig struct {
|
|
Hostname string
|
|
}
|
|
|
|
type TailscaleNodeOpt = func(*TailscaleNodeConfig)
|
|
|
|
func WithName(name string) TailscaleNodeOpt {
|
|
return func(config *TailscaleNodeConfig) {
|
|
config.Hostname = name
|
|
}
|
|
}
|
|
|
|
func (s *Scenario) NewTailscaleNode(opts ...TailscaleNodeOpt) *tsn.TailscaleNode {
|
|
config := &TailscaleNodeConfig{Hostname: petname.Generate(3, "-")}
|
|
for _, o := range opts {
|
|
o(config)
|
|
}
|
|
|
|
runOpts := &dockertest.RunOptions{
|
|
Repository: fmt.Sprintf("ts-%s", strings.Replace(targetVersion, ".", "-", -1)),
|
|
Hostname: config.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(
|
|
runOpts,
|
|
restartPolicy,
|
|
)
|
|
require.NoError(s.t, err)
|
|
|
|
err = s.pool.Retry(portCheck(resource.GetPort("1055/tcp")))
|
|
require.NoError(s.t, err)
|
|
|
|
s.resources = append(s.resources, resource)
|
|
|
|
return tsn.New(s.t, config.Hostname, "https://ionscale", resource, s.pool.Retry)
|
|
}
|
|
|
|
func Run(t *testing.T, f func(s *Scenario)) {
|
|
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 verbose() {
|
|
_ = s.printIonscaleLogs()
|
|
}
|
|
|
|
if s.ionscale != nil {
|
|
_ = pool.Purge(s.ionscale)
|
|
}
|
|
|
|
if s.mockoidc != nil {
|
|
_ = pool.Purge(s.mockoidc)
|
|
}
|
|
|
|
if s.network != nil {
|
|
_ = s.network.Close()
|
|
}
|
|
|
|
s.resources = nil
|
|
s.network = nil
|
|
}()
|
|
|
|
s.pool, err = dockertest.NewPool("")
|
|
require.NoError(t, err)
|
|
|
|
s.network, err = pool.CreateNetwork("ionscale-test")
|
|
require.NoError(s.t, err)
|
|
|
|
currentPath, err := os.Getwd()
|
|
require.NoError(s.t, err)
|
|
|
|
// run mockoidc container
|
|
{
|
|
mockoidcOpts := &dockertest.RunOptions{
|
|
Hostname: "mockoidc",
|
|
Repository: "ghcr.io/jsiebens/mockoidc",
|
|
Networks: []*dockertest.Network{s.network},
|
|
ExposedPorts: []string{"80"},
|
|
Cmd: []string{"--listen-addr", ":80", "--server-url", "http://mockoidc"},
|
|
}
|
|
|
|
s.mockoidc, err = pool.RunWithOptions(mockoidcOpts, restartPolicy)
|
|
require.NoError(s.t, err)
|
|
|
|
port := s.mockoidc.GetPort("80/tcp")
|
|
err = pool.Retry(httpCheck(port, "/oidc/.well-known/openid-configuration"))
|
|
require.NoError(s.t, err)
|
|
|
|
s.mockoidcClient = mockoidc.NewClient(fmt.Sprintf("http://localhost:%s", port), true)
|
|
}
|
|
|
|
ionscale := &dockertest.RunOptions{
|
|
Hostname: "ionscale",
|
|
Repository: "ionscale-test",
|
|
Mounts: []string{
|
|
fmt.Sprintf("%s/config:/etc/ionscale", currentPath),
|
|
},
|
|
Networks: []*dockertest.Network{s.network},
|
|
ExposedPorts: []string{"443"},
|
|
Cmd: []string{"server", "--config", "/etc/ionscale/config.yaml"},
|
|
}
|
|
|
|
s.ionscale, err = pool.RunWithOptions(ionscale, restartPolicy)
|
|
require.NoError(s.t, err)
|
|
|
|
port := s.ionscale.GetPort("443/tcp")
|
|
|
|
addr := fmt.Sprintf("https://localhost:%s", port)
|
|
auth, err := ionscaleclt.LoadClientAuth(addr, "804ecd57365342254ce6647da5c249e85c10a0e51e74856bfdf292a2136b4249")
|
|
require.NoError(s.t, err)
|
|
|
|
s.ionscaleClient, err = ionscaleclt.NewClient(auth, addr, true)
|
|
require.NoError(s.t, err)
|
|
|
|
err = pool.Retry(func() error {
|
|
_, err := s.ionscaleClient.GetVersion(context.Background(), connect.NewRequest(&api.GetVersionRequest{}))
|
|
return err
|
|
})
|
|
require.NoError(s.t, 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: "../",
|
|
Dockerfile: "tests/docker/tailscale/Dockerfile",
|
|
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)
|
|
}
|
|
}
|
|
|
|
func verbose() bool {
|
|
return os.Getenv("IONSCALE_TESTS_VERBOSE") == "true"
|
|
}
|