Skip to content

Commit 421c0d1

Browse files
authored
chore: add nginx topology to tailnet tests (#13188)
1 parent 677be9a commit 421c0d1

File tree

6 files changed

+253
-167
lines changed

6 files changed

+253
-167
lines changed

.github/workflows/ci.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,10 @@ jobs:
410410
- name: Setup Go
411411
uses: ./.github/actions/setup-go
412412

413+
# Used by some integration tests.
414+
- name: Install Nginx
415+
run: sudo apt-get update && sudo apt-get install -y nginx
416+
413417
- name: Run Tests
414418
run: make test-tailnet-integration
415419

codersdk/organizations.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func ProvisionerTypeValid[T ProvisionerType | string](pt T) error {
3434
case string(ProvisionerTypeEcho), string(ProvisionerTypeTerraform):
3535
return nil
3636
default:
37-
return fmt.Errorf("provisioner type '%s' is not supported", pt)
37+
return xerrors.Errorf("provisioner type '%s' is not supported", pt)
3838
}
3939
}
4040

provisionerd/provisionerd_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,7 @@ func TestProvisionerd(t *testing.T) {
611611
server := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
612612
// This is the dial out to Coderd, which in this unit test will always fail.
613613
connectAttemptedClose.Do(func() { close(connectAttempted) })
614-
return nil, fmt.Errorf("client connection always fails")
614+
return nil, xerrors.New("client connection always fails")
615615
}, provisionerd.LocalProvisioners{
616616
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{}),
617617
})

tailnet/test/integration/integration.go

Lines changed: 230 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@ import (
77
"context"
88
"fmt"
99
"io"
10+
"net"
1011
"net/http"
1112
"net/netip"
1213
"net/url"
14+
"os"
15+
"os/exec"
16+
"path/filepath"
1317
"strconv"
1418
"strings"
19+
"sync"
1520
"sync/atomic"
21+
"syscall"
1622
"testing"
1723
"time"
1824

@@ -41,7 +47,34 @@ var (
4147
Client2ID = uuid.MustParse("00000000-0000-0000-0000-000000000002")
4248
)
4349

44-
type ServerOptions struct {
50+
type TestTopology struct {
51+
Name string
52+
// SetupNetworking creates interfaces and network namespaces for the test.
53+
// The most simple implementation is NetworkSetupDefault, which only creates
54+
// a network namespace shared for all tests.
55+
SetupNetworking func(t *testing.T, logger slog.Logger) TestNetworking
56+
57+
// Server is the server starter for the test. It is executed in the server
58+
// subprocess.
59+
Server ServerStarter
60+
// StartClient gets called in each client subprocess. It's expected to
61+
// create the tailnet.Conn and ensure connectivity to it's peer.
62+
StartClient func(t *testing.T, logger slog.Logger, serverURL *url.URL, myID uuid.UUID, peerID uuid.UUID) *tailnet.Conn
63+
64+
// RunTests is the main test function. It's called in each of the client
65+
// subprocesses. If tests can only run once, they should check the client ID
66+
// and return early if it's not the expected one.
67+
RunTests func(t *testing.T, logger slog.Logger, serverURL *url.URL, myID uuid.UUID, peerID uuid.UUID, conn *tailnet.Conn)
68+
}
69+
70+
type ServerStarter interface {
71+
// StartServer should start the server and return once it's listening. It
72+
// should not block once it's listening. Cleanup should be handled by
73+
// t.Cleanup.
74+
StartServer(t *testing.T, logger slog.Logger, listenAddr string)
75+
}
76+
77+
type SimpleServerOptions struct {
4578
// FailUpgradeDERP will make the DERP server fail to handle the initial DERP
4679
// upgrade in a way that causes the client to fallback to
4780
// DERP-over-WebSocket fallback automatically.
@@ -54,8 +87,10 @@ type ServerOptions struct {
5487
DERPWebsocketOnly bool
5588
}
5689

90+
var _ ServerStarter = SimpleServerOptions{}
91+
5792
//nolint:revive
58-
func (o ServerOptions) Router(t *testing.T, logger slog.Logger) *chi.Mux {
93+
func (o SimpleServerOptions) Router(t *testing.T, logger slog.Logger) *chi.Mux {
5994
coord := tailnet.NewCoordinator(logger)
6095
var coordPtr atomic.Pointer[tailnet.Coordinator]
6196
coordPtr.Store(&coord)
@@ -157,6 +192,76 @@ func (o ServerOptions) Router(t *testing.T, logger slog.Logger) *chi.Mux {
157192
return r
158193
}
159194

195+
func (o SimpleServerOptions) StartServer(t *testing.T, logger slog.Logger, listenAddr string) {
196+
srv := http.Server{
197+
Addr: listenAddr,
198+
Handler: o.Router(t, logger),
199+
ReadTimeout: 10 * time.Second,
200+
}
201+
serveDone := make(chan struct{})
202+
go func() {
203+
defer close(serveDone)
204+
err := srv.ListenAndServe()
205+
if err != nil && !xerrors.Is(err, http.ErrServerClosed) {
206+
t.Error("HTTP server error:", err)
207+
}
208+
}()
209+
t.Cleanup(func() {
210+
_ = srv.Close()
211+
<-serveDone
212+
})
213+
}
214+
215+
type NGINXServerOptions struct {
216+
SimpleServerOptions
217+
}
218+
219+
var _ ServerStarter = NGINXServerOptions{}
220+
221+
func (o NGINXServerOptions) StartServer(t *testing.T, logger slog.Logger, listenAddr string) {
222+
host, nginxPortStr, err := net.SplitHostPort(listenAddr)
223+
require.NoError(t, err)
224+
225+
nginxPort, err := strconv.Atoi(nginxPortStr)
226+
require.NoError(t, err)
227+
228+
serverPort := nginxPort + 1
229+
serverListenAddr := net.JoinHostPort(host, strconv.Itoa(serverPort))
230+
231+
o.SimpleServerOptions.StartServer(t, logger, serverListenAddr)
232+
startNginx(t, nginxPortStr, serverListenAddr)
233+
}
234+
235+
func startNginx(t *testing.T, listenPort, serverAddr string) {
236+
cfg := `events {}
237+
http {
238+
server {
239+
listen ` + listenPort + `;
240+
server_name _;
241+
location / {
242+
proxy_pass http://` + serverAddr + `;
243+
proxy_http_version 1.1;
244+
proxy_set_header Upgrade $http_upgrade;
245+
proxy_set_header Connection "upgrade";
246+
proxy_set_header Host $host;
247+
proxy_set_header X-Real-IP $remote_addr;
248+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
249+
proxy_set_header X-Forwarded-Proto $scheme;
250+
proxy_set_header X-Forwarded-Host $server_name;
251+
}
252+
}
253+
}
254+
`
255+
256+
dir := t.TempDir()
257+
cfgPath := filepath.Join(dir, "nginx.conf")
258+
err := os.WriteFile(cfgPath, []byte(cfg), 0o600)
259+
require.NoError(t, err)
260+
261+
// ExecBackground will handle cleanup.
262+
_, _ = ExecBackground(t, "server.nginx", nil, "nginx", []string{"-c", cfgPath})
263+
}
264+
160265
// StartClientDERP creates a client connection to the server for coordination
161266
// and creates a tailnet.Conn which will only use DERP to connect to the peer.
162267
func StartClientDERP(t *testing.T, logger slog.Logger, serverURL *url.URL, myID, peerID uuid.UUID) *tailnet.Conn {
@@ -296,3 +401,126 @@ func basicDERPMap(t *testing.T, serverURL *url.URL) *tailcfg.DERPMap {
296401
},
297402
}
298403
}
404+
405+
// ExecBackground starts a subprocess with the given flags and returns a
406+
// channel that will receive the error when the subprocess exits. The returned
407+
// function can be used to close the subprocess.
408+
//
409+
// processName is used to identify the subprocess in logs.
410+
//
411+
// Optionally, a network namespace can be passed to run the subprocess in.
412+
//
413+
// Do not call close then wait on the channel. Use the returned value from the
414+
// function instead in this case.
415+
//
416+
// Cleanup is handled automatically if you don't care about monitoring the
417+
// process manually.
418+
func ExecBackground(t *testing.T, processName string, netNS *os.File, name string, args []string) (<-chan error, func() error) {
419+
if netNS != nil {
420+
// We use nsenter to enter the namespace.
421+
// We can't use `setns` easily from Golang in the parent process because
422+
// you can't execute the syscall in the forked child thread before it
423+
// execs.
424+
// We can't use `setns` easily from Golang in the child process because
425+
// by the time you call it, the process has already created multiple
426+
// threads.
427+
args = append([]string{"--net=/proc/self/fd/3", name}, args...)
428+
name = "nsenter"
429+
}
430+
431+
cmd := exec.Command(name, args...)
432+
if netNS != nil {
433+
cmd.ExtraFiles = []*os.File{netNS}
434+
}
435+
436+
out := &testWriter{
437+
name: processName,
438+
t: t,
439+
}
440+
t.Cleanup(out.Flush)
441+
cmd.Stdout = out
442+
cmd.Stderr = out
443+
cmd.SysProcAttr = &syscall.SysProcAttr{
444+
Pdeathsig: syscall.SIGTERM,
445+
}
446+
err := cmd.Start()
447+
require.NoError(t, err)
448+
449+
waitErr := make(chan error, 1)
450+
go func() {
451+
err := cmd.Wait()
452+
waitErr <- err
453+
close(waitErr)
454+
}()
455+
456+
closeFn := func() error {
457+
_ = cmd.Process.Signal(syscall.SIGTERM)
458+
select {
459+
case <-time.After(5 * time.Second):
460+
_ = cmd.Process.Kill()
461+
case err := <-waitErr:
462+
return err
463+
}
464+
return <-waitErr
465+
}
466+
467+
t.Cleanup(func() {
468+
select {
469+
case err := <-waitErr:
470+
if err != nil {
471+
t.Logf("subprocess exited: " + err.Error())
472+
}
473+
return
474+
default:
475+
}
476+
477+
_ = closeFn()
478+
})
479+
480+
return waitErr, closeFn
481+
}
482+
483+
type testWriter struct {
484+
mut sync.Mutex
485+
name string
486+
t *testing.T
487+
488+
capturedLines []string
489+
}
490+
491+
func (w *testWriter) Write(p []byte) (n int, err error) {
492+
w.mut.Lock()
493+
defer w.mut.Unlock()
494+
str := string(p)
495+
split := strings.Split(str, "\n")
496+
for _, s := range split {
497+
if s == "" {
498+
continue
499+
}
500+
501+
// If a line begins with "\s*--- (PASS|FAIL)" or is just PASS or FAIL,
502+
// then it's a test result line. We want to capture it and log it later.
503+
trimmed := strings.TrimSpace(s)
504+
if strings.HasPrefix(trimmed, "--- PASS") || strings.HasPrefix(trimmed, "--- FAIL") || trimmed == "PASS" || trimmed == "FAIL" {
505+
// Also fail the test if we see a FAIL line.
506+
if strings.Contains(trimmed, "FAIL") {
507+
w.t.Errorf("subprocess logged test failure: %s: \t%s", w.name, s)
508+
}
509+
510+
w.capturedLines = append(w.capturedLines, s)
511+
continue
512+
}
513+
514+
w.t.Logf("%s output: \t%s", w.name, s)
515+
}
516+
return len(p), nil
517+
}
518+
519+
func (w *testWriter) Flush() {
520+
w.mut.Lock()
521+
defer w.mut.Unlock()
522+
for _, s := range w.capturedLines {
523+
w.t.Logf("%s output: \t%s", w.name, s)
524+
}
525+
w.capturedLines = nil
526+
}

0 commit comments

Comments
 (0)