mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-08 19:06:14 -08:00
* chore(go): use lowercase module import path * ci: scope checks for Go module changes
255 lines
6.8 KiB
Go
255 lines
6.8 KiB
Go
package host_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
corehost "github.com/easytier/easytier/easytier-go"
|
|
)
|
|
|
|
func TestWebClientEndToEnd(t *testing.T) {
|
|
endpoint := os.Getenv("EASYTIER_WEB_E2E_ENDPOINT")
|
|
apiURL := os.Getenv("EASYTIER_WEB_E2E_API")
|
|
authToken := os.Getenv("EASYTIER_WEB_E2E_AUTH")
|
|
if endpoint == "" || apiURL == "" || authToken == "" {
|
|
t.Skip("EasyTier WebClient E2E environment is not configured")
|
|
}
|
|
userID, err := strconv.Atoi(os.Getenv("EASYTIER_WEB_E2E_USER_ID"))
|
|
if err != nil || userID == 0 {
|
|
userID = 1
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
host, err := corehost.New(ctx, corehost.Options{})
|
|
if err != nil {
|
|
t.Fatalf("create host: %v", err)
|
|
}
|
|
defer host.Close(ctx)
|
|
application, err := host.CreateInstance(
|
|
ctx,
|
|
instanceConfig(t, 99, "10.154.0.1", 0, false, false),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("create application instance: %v", err)
|
|
}
|
|
defer application.Close(ctx)
|
|
|
|
const machineID = "11111111-2222-4333-8444-555555555555"
|
|
client, err := host.ConnectWebClient(ctx, corehost.WebClientOptions{
|
|
Endpoint: endpoint,
|
|
MachineID: machineID,
|
|
Hostname: "go-host-e2e",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("connect WebClient: %v", err)
|
|
}
|
|
defer client.Close(ctx)
|
|
waitFor(t, ctx, client.Connected, "WebClient connection")
|
|
|
|
base := fmt.Sprintf(
|
|
"%s/api/internal/users/%d/machines/%s/networks",
|
|
strings.TrimRight(apiURL, "/"),
|
|
userID,
|
|
machineID,
|
|
)
|
|
waitFor(t, ctx, func() bool {
|
|
body, status := webRequest(t, ctx, authToken, http.MethodGet, base, nil)
|
|
var response struct {
|
|
Running []json.RawMessage `json:"running_inst_ids"`
|
|
}
|
|
return status == http.StatusOK &&
|
|
json.Unmarshal(body, &response) == nil &&
|
|
len(response.Running) == 1
|
|
}, "application instance heartbeat")
|
|
|
|
const managedID = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"
|
|
networkName := "go-host-managed"
|
|
networkSecret := "test"
|
|
managedConfig := map[string]any{
|
|
"instance_id": managedID,
|
|
"dhcp": true,
|
|
"network_name": networkName,
|
|
"network_secret": networkSecret,
|
|
"networking_method": 2,
|
|
"listener_urls": []string{"tcp://0.0.0.0:11010", "udp://0.0.0.0:11010", "wg://0.0.0.0:11011"},
|
|
"proxy_cidrs": []string{"10.200.0.0/24"},
|
|
"disable_p2p": true,
|
|
"disable_ipv6": true,
|
|
"enable_vpn_portal": true,
|
|
"vpn_portal_listen_port": 11012,
|
|
"vpn_portal_client_network_addr": "10.210.0.0",
|
|
"vpn_portal_client_network_len": 24,
|
|
"data_compress_algo": 2,
|
|
"credential_file": "/unsupported",
|
|
"enable_quic_proxy": true,
|
|
"mapped_listeners": []string{"wg://0.0.0.0:11012"},
|
|
"advanced_settings": true,
|
|
}
|
|
payload, err := json.Marshal(map[string]any{
|
|
"config": managedConfig,
|
|
"save": false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("encode managed network request: %v", err)
|
|
}
|
|
body, status := webRequest(t, ctx, authToken, http.MethodPost, base, payload)
|
|
if status != http.StatusOK {
|
|
t.Fatalf("run managed instance: status=%d body=%s", status, body)
|
|
}
|
|
var managedInstance *corehost.Instance
|
|
waitFor(t, ctx, func() bool {
|
|
for _, instance := range host.Instances() {
|
|
if instance.ID() == managedID {
|
|
managedInstance = instance
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}, "managed instance creation")
|
|
|
|
infoURL := base + "/info"
|
|
body, status = webRequest(
|
|
t,
|
|
ctx,
|
|
authToken,
|
|
http.MethodGet,
|
|
infoURL,
|
|
[]byte(fmt.Sprintf(`{"inst_ids":["%s"]}`, managedID)),
|
|
)
|
|
if status != http.StatusOK ||
|
|
!bytes.Contains(body, []byte(managedID)) ||
|
|
!bytes.Contains(body, []byte(`"running":true`)) ||
|
|
!bytes.Contains(body, []byte("tcp://")) ||
|
|
!bytes.Contains(body, []byte("udp://")) ||
|
|
bytes.Contains(body, []byte("wg://")) {
|
|
t.Fatalf("collect managed status: status=%d body=%s", status, body)
|
|
}
|
|
|
|
portProbe, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("reserve port-forward address: %v", err)
|
|
}
|
|
portForwardAddress := portProbe.Addr().String()
|
|
portForwardPort := portProbe.Addr().(*net.TCPAddr).Port
|
|
if err := portProbe.Close(); err != nil {
|
|
t.Fatalf("release port-forward address: %v", err)
|
|
}
|
|
managedConfig["proxy_cidrs"] = []string{"10.201.0.0/24"}
|
|
managedConfig["disable_relay_data"] = true
|
|
managedConfig["port_forwards"] = []map[string]any{{
|
|
"proto": "tcp",
|
|
"bind_ip": "127.0.0.1",
|
|
"bind_port": portForwardPort,
|
|
"dst_ip": "10.201.0.1",
|
|
"dst_port": 80,
|
|
}}
|
|
payload, err = json.Marshal(map[string]any{
|
|
"managed_network_configs": []map[string]any{{
|
|
"instance_id": managedID,
|
|
"network_config": managedConfig,
|
|
}},
|
|
"config_revision": "revision-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("encode managed network update: %v", err)
|
|
}
|
|
body, status = webRequest(t, ctx, authToken, http.MethodPut, base, payload)
|
|
if status != http.StatusOK {
|
|
t.Fatalf("update managed instance: status=%d body=%s", status, body)
|
|
}
|
|
waitFor(t, ctx, func() bool {
|
|
connection, err := net.DialTimeout(
|
|
"tcp",
|
|
portForwardAddress,
|
|
100*time.Millisecond,
|
|
)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
connection.Close()
|
|
return true
|
|
}, "managed instance hot patch")
|
|
for _, instance := range host.Instances() {
|
|
if instance.ID() == managedID && instance != managedInstance {
|
|
t.Fatal("managed hot patch replaced the running instance")
|
|
}
|
|
}
|
|
|
|
body, status = webRequest(
|
|
t,
|
|
ctx,
|
|
authToken,
|
|
http.MethodDelete,
|
|
base+"/"+managedID,
|
|
nil,
|
|
)
|
|
if status != http.StatusOK {
|
|
t.Fatalf("delete managed instance: status=%d body=%s", status, body)
|
|
}
|
|
waitFor(t, ctx, func() bool {
|
|
instances := host.Instances()
|
|
return len(instances) == 1 && instances[0].ID() == application.ID()
|
|
}, "managed instance deletion")
|
|
}
|
|
|
|
func webRequest(
|
|
t *testing.T,
|
|
ctx context.Context,
|
|
authToken string,
|
|
method string,
|
|
url string,
|
|
body []byte,
|
|
) ([]byte, int) {
|
|
t.Helper()
|
|
request, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body))
|
|
if err != nil {
|
|
t.Fatalf("create %s request: %v", method, err)
|
|
}
|
|
request.Header.Set("X-Internal-Auth", authToken)
|
|
if body != nil {
|
|
request.Header.Set("Content-Type", "application/json")
|
|
}
|
|
response, err := http.DefaultClient.Do(request)
|
|
if err != nil {
|
|
return nil, 0
|
|
}
|
|
defer response.Body.Close()
|
|
encoded, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
t.Fatalf("read %s response: %v", method, err)
|
|
}
|
|
return encoded, response.StatusCode
|
|
}
|
|
|
|
func waitFor(
|
|
t *testing.T,
|
|
ctx context.Context,
|
|
condition func() bool,
|
|
description string,
|
|
) {
|
|
t.Helper()
|
|
ticker := time.NewTicker(50 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
if condition() {
|
|
return
|
|
}
|
|
select {
|
|
case <-ticker.C:
|
|
case <-ctx.Done():
|
|
t.Fatalf("wait for %s: %v", description, ctx.Err())
|
|
}
|
|
}
|
|
}
|