Skip to content

Commit 15157c1

Browse files
authored
chore: add network integration test suite scaffolding (#13072)
* chore: add network integration test suite scaffolding * dean comments
1 parent 73ba36c commit 15157c1

File tree

2 files changed

+322
-0
lines changed

2 files changed

+322
-0
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package integration
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"io"
7+
"net/http"
8+
"net/http/httptest"
9+
"net/netip"
10+
"strings"
11+
"sync/atomic"
12+
"testing"
13+
"time"
14+
15+
"github.com/google/uuid"
16+
"github.com/stretchr/testify/require"
17+
"golang.org/x/xerrors"
18+
"nhooyr.io/websocket"
19+
"tailscale.com/tailcfg"
20+
21+
"cdr.dev/slog"
22+
"github.com/coder/coder/v2/coderd/httpapi"
23+
"github.com/coder/coder/v2/codersdk"
24+
"github.com/coder/coder/v2/tailnet"
25+
"github.com/coder/coder/v2/testutil"
26+
)
27+
28+
func NetworkSetupDefault(*testing.T) {}
29+
30+
func DERPMapTailscale(ctx context.Context, t *testing.T) *tailcfg.DERPMap {
31+
ctx, cancel := context.WithTimeout(ctx, testutil.WaitShort)
32+
defer cancel()
33+
34+
req, err := http.NewRequestWithContext(ctx, "GET", "https://controlplane.tailscale.com/derpmap/default", nil)
35+
require.NoError(t, err)
36+
37+
res, err := http.DefaultClient.Do(req)
38+
require.NoError(t, err)
39+
defer res.Body.Close()
40+
41+
dm := &tailcfg.DERPMap{}
42+
dec := json.NewDecoder(res.Body)
43+
err = dec.Decode(dm)
44+
require.NoError(t, err)
45+
46+
return dm
47+
}
48+
49+
func CoordinatorInMemory(t *testing.T, logger slog.Logger, dm *tailcfg.DERPMap) (coord tailnet.Coordinator, url string) {
50+
coord = tailnet.NewCoordinator(logger)
51+
var coordPtr atomic.Pointer[tailnet.Coordinator]
52+
coordPtr.Store(&coord)
53+
t.Cleanup(func() { _ = coord.Close() })
54+
55+
csvc, err := tailnet.NewClientService(logger, &coordPtr, 10*time.Minute, func() *tailcfg.DERPMap {
56+
return dm
57+
})
58+
require.NoError(t, err)
59+
60+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
61+
idStr := strings.TrimPrefix(r.URL.Path, "/")
62+
id, err := uuid.Parse(idStr)
63+
if err != nil {
64+
httpapi.Write(r.Context(), w, http.StatusBadRequest, codersdk.Response{
65+
Message: "Bad agent id.",
66+
Detail: err.Error(),
67+
})
68+
return
69+
}
70+
71+
conn, err := websocket.Accept(w, r, nil)
72+
if err != nil {
73+
httpapi.Write(r.Context(), w, http.StatusBadRequest, codersdk.Response{
74+
Message: "Failed to accept websocket.",
75+
Detail: err.Error(),
76+
})
77+
return
78+
}
79+
80+
ctx, wsNetConn := codersdk.WebsocketNetConn(r.Context(), conn, websocket.MessageBinary)
81+
defer wsNetConn.Close()
82+
83+
err = csvc.ServeConnV2(ctx, wsNetConn, tailnet.StreamID{
84+
Name: "client-" + id.String(),
85+
ID: id,
86+
Auth: tailnet.SingleTailnetCoordinateeAuth{},
87+
})
88+
if err != nil && !xerrors.Is(err, io.EOF) && !xerrors.Is(err, context.Canceled) {
89+
_ = conn.Close(websocket.StatusInternalError, err.Error())
90+
return
91+
}
92+
}))
93+
t.Cleanup(srv.Close)
94+
95+
return coord, srv.URL
96+
}
97+
98+
func TailnetSetupDRPC(ctx context.Context, t *testing.T, logger slog.Logger,
99+
id, agentID uuid.UUID,
100+
coordinateURL string,
101+
dm *tailcfg.DERPMap,
102+
) *tailnet.Conn {
103+
ip := tailnet.IPFromUUID(id)
104+
conn, err := tailnet.NewConn(&tailnet.Options{
105+
Addresses: []netip.Prefix{netip.PrefixFrom(ip, 128)},
106+
DERPMap: dm,
107+
Logger: logger,
108+
})
109+
require.NoError(t, err)
110+
t.Cleanup(func() { _ = conn.Close() })
111+
112+
//nolint:bodyclose
113+
ws, _, err := websocket.Dial(ctx, coordinateURL+"/"+id.String(), nil)
114+
require.NoError(t, err)
115+
116+
client, err := tailnet.NewDRPCClient(
117+
websocket.NetConn(ctx, ws, websocket.MessageBinary),
118+
logger,
119+
)
120+
require.NoError(t, err)
121+
122+
coord, err := client.Coordinate(ctx)
123+
require.NoError(t, err)
124+
125+
coordination := tailnet.NewRemoteCoordination(logger, coord, conn, agentID)
126+
t.Cleanup(func() { _ = coordination.Close() })
127+
return conn
128+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package integration
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"strconv"
10+
"syscall"
11+
"testing"
12+
13+
"github.com/google/uuid"
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
"tailscale.com/tailcfg"
17+
18+
"cdr.dev/slog"
19+
"cdr.dev/slog/sloggers/slogtest"
20+
"github.com/coder/coder/v2/tailnet"
21+
"github.com/coder/coder/v2/testutil"
22+
)
23+
24+
var (
25+
isChild = flag.Bool("child", false, "Run tests as a child")
26+
childTestID = flag.Int("child-test-id", 0, "Which test is being run")
27+
childCoordinateURL = flag.String("child-coordinate-url", "", "The coordinate url to connect back to")
28+
childAgentID = flag.String("child-agent-id", "", "The agent id of the child")
29+
)
30+
31+
func TestMain(m *testing.M) {
32+
if run := os.Getenv("CODER_TAILNET_TESTS"); run == "" {
33+
_, _ = fmt.Println("skipping tests...")
34+
return
35+
}
36+
if os.Getuid() != 0 {
37+
_, _ = fmt.Println("networking integration tests must run as root")
38+
return
39+
}
40+
flag.Parse()
41+
os.Exit(m.Run())
42+
}
43+
44+
var tests = []Test{{
45+
Name: "Normal",
46+
DERPMap: DERPMapTailscale,
47+
Coordinator: CoordinatorInMemory,
48+
Parent: Parent{
49+
NetworkSetup: NetworkSetupDefault,
50+
TailnetSetup: TailnetSetupDRPC,
51+
Run: func(ctx context.Context, t *testing.T, opts ParentOpts) {
52+
reach := opts.Conn.AwaitReachable(ctx, tailnet.IPFromUUID(opts.AgentID))
53+
assert.True(t, reach)
54+
},
55+
},
56+
Child: Child{
57+
NetworkSetup: NetworkSetupDefault,
58+
TailnetSetup: TailnetSetupDRPC,
59+
Run: func(ctx context.Context, t *testing.T, opts ChildOpts) {
60+
// wait until the parent kills us
61+
<-make(chan struct{})
62+
},
63+
},
64+
}}
65+
66+
//nolint:paralleltest
67+
func TestIntegration(t *testing.T) {
68+
if *isChild {
69+
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
70+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
71+
t.Cleanup(cancel)
72+
73+
agentID, err := uuid.Parse(*childAgentID)
74+
require.NoError(t, err)
75+
76+
test := tests[*childTestID]
77+
test.Child.NetworkSetup(t)
78+
dm := test.DERPMap(ctx, t)
79+
conn := test.Child.TailnetSetup(ctx, t, logger, agentID, uuid.Nil, *childCoordinateURL, dm)
80+
test.Child.Run(ctx, t, ChildOpts{
81+
Logger: logger,
82+
Conn: conn,
83+
AgentID: agentID,
84+
})
85+
return
86+
}
87+
88+
for id, test := range tests {
89+
t.Run(test.Name, func(t *testing.T) {
90+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
91+
t.Cleanup(cancel)
92+
93+
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
94+
parentID, childID := uuid.New(), uuid.New()
95+
dm := test.DERPMap(ctx, t)
96+
_, coordURL := test.Coordinator(t, logger, dm)
97+
98+
child, waitChild := execChild(ctx, id, coordURL, childID)
99+
test.Parent.NetworkSetup(t)
100+
conn := test.Parent.TailnetSetup(ctx, t, logger, parentID, childID, coordURL, dm)
101+
test.Parent.Run(ctx, t, ParentOpts{
102+
Logger: logger,
103+
Conn: conn,
104+
ClientID: parentID,
105+
AgentID: childID,
106+
})
107+
child.Process.Signal(syscall.SIGINT)
108+
<-waitChild
109+
})
110+
}
111+
}
112+
113+
type Test struct {
114+
// Name is the name of the test.
115+
Name string
116+
117+
// DERPMap returns the DERP map to use for both the parent and child. It is
118+
// called once at the beginning of the test.
119+
DERPMap func(ctx context.Context, t *testing.T) *tailcfg.DERPMap
120+
// Coordinator returns a running tailnet coordinator, and the url to reach
121+
// it on.
122+
Coordinator func(t *testing.T, logger slog.Logger, dm *tailcfg.DERPMap) (coord tailnet.Coordinator, url string)
123+
124+
Parent Parent
125+
Child Child
126+
}
127+
128+
// Parent is the struct containing all of the parent specific configurations.
129+
// Functions are invoked in order of struct definition.
130+
type Parent struct {
131+
// NetworkSetup is run before all test code. It can be used to setup
132+
// networking scenarios.
133+
NetworkSetup func(t *testing.T)
134+
135+
// TailnetSetup creates a tailnet network.
136+
TailnetSetup func(
137+
ctx context.Context, t *testing.T, logger slog.Logger,
138+
id, agentID uuid.UUID, coordURL string, dm *tailcfg.DERPMap,
139+
) *tailnet.Conn
140+
141+
Run func(ctx context.Context, t *testing.T, opts ParentOpts)
142+
}
143+
144+
// Child is the struct containing all of the child specific configurations.
145+
// Functions are invoked in order of struct definition.
146+
type Child struct {
147+
// NetworkSetup is run before all test code. It can be used to setup
148+
// networking scenarios.
149+
NetworkSetup func(t *testing.T)
150+
151+
// TailnetSetup creates a tailnet network.
152+
TailnetSetup func(
153+
ctx context.Context, t *testing.T, logger slog.Logger,
154+
id, agentID uuid.UUID, coordURL string, dm *tailcfg.DERPMap,
155+
) *tailnet.Conn
156+
157+
// Run runs the actual test. Parents and children run in separate processes,
158+
// so it's important to ensure no communication happens over memory between
159+
// run functions of parents and children.
160+
Run func(ctx context.Context, t *testing.T, opts ChildOpts)
161+
}
162+
163+
type ParentOpts struct {
164+
Logger slog.Logger
165+
Conn *tailnet.Conn
166+
ClientID uuid.UUID
167+
AgentID uuid.UUID
168+
}
169+
170+
type ChildOpts struct {
171+
Logger slog.Logger
172+
Conn *tailnet.Conn
173+
AgentID uuid.UUID
174+
}
175+
176+
func execChild(ctx context.Context, testID int, coordURL string, agentID uuid.UUID) (*exec.Cmd, <-chan error) {
177+
ch := make(chan error)
178+
binary := os.Args[0]
179+
args := os.Args[1:]
180+
args = append(args,
181+
"--child=true",
182+
"--child-test-id="+strconv.Itoa(testID),
183+
"--child-coordinate-url="+coordURL,
184+
"--child-agent-id="+agentID.String(),
185+
)
186+
187+
cmd := exec.CommandContext(ctx, binary, args...)
188+
cmd.Stdout = os.Stdout
189+
cmd.Stderr = os.Stderr
190+
go func() {
191+
ch <- cmd.Run()
192+
}()
193+
return cmd, ch
194+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy