mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: use tailnet v2 API for coordination (#11638)
This one is huge, and I'm sorry. The problem is that once I change `tailnet.Conn` to start doing v2 behavior, I kind of have to change it everywhere, including in CoderSDK (CLI), the agent, wsproxy, and ServerTailnet. There is still a bit more cleanup to do, and I need to add code so that when we lose connection to the Coordinator, we mark all peers as LOST, but that will be in a separate PR since this is big enough!
This commit is contained in:
+24
-8
@@ -30,6 +30,7 @@ import (
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/xerrors"
|
||||
"storj.io/drpc"
|
||||
"tailscale.com/net/speedtest"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/netlogtype"
|
||||
@@ -47,6 +48,7 @@ import (
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/coder/v2/tailnet"
|
||||
tailnetproto "github.com/coder/coder/v2/tailnet/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -86,7 +88,7 @@ type Options struct {
|
||||
|
||||
type Client interface {
|
||||
Manifest(ctx context.Context) (agentsdk.Manifest, error)
|
||||
Listen(ctx context.Context) (net.Conn, error)
|
||||
Listen(ctx context.Context) (drpc.Conn, error)
|
||||
DERPMapUpdates(ctx context.Context) (<-chan agentsdk.DERPMapUpdate, io.Closer, error)
|
||||
ReportStats(ctx context.Context, log slog.Logger, statsChan <-chan *agentsdk.Stats, setInterval func(time.Duration)) (io.Closer, error)
|
||||
PostLifecycle(ctx context.Context, state agentsdk.PostLifecycleRequest) error
|
||||
@@ -1058,20 +1060,34 @@ func (a *agent) runCoordinator(ctx context.Context, network *tailnet.Conn) error
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
coordinator, err := a.client.Listen(ctx)
|
||||
conn, err := a.client.Listen(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer coordinator.Close()
|
||||
defer func() {
|
||||
cErr := conn.Close()
|
||||
if cErr != nil {
|
||||
a.logger.Debug(ctx, "error closing drpc connection", slog.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
tClient := tailnetproto.NewDRPCTailnetClient(conn)
|
||||
coordinate, err := tClient.Coordinate(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to connect to the coordinate endpoint: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
cErr := coordinate.Close()
|
||||
if cErr != nil {
|
||||
a.logger.Debug(ctx, "error closing Coordinate client", slog.Error(err))
|
||||
}
|
||||
}()
|
||||
a.logger.Info(ctx, "connected to coordination endpoint")
|
||||
sendNodes, errChan := tailnet.ServeCoordinator(coordinator, func(nodes []*tailnet.Node) error {
|
||||
return network.UpdateNodes(nodes, false)
|
||||
})
|
||||
network.SetNodeCallback(sendNodes)
|
||||
coordination := tailnet.NewRemoteCoordination(a.logger, coordinate, network, uuid.Nil)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case err := <-errChan:
|
||||
case err := <-coordination.Error():
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
+48
-37
@@ -1664,9 +1664,11 @@ func TestAgent_UpdatedDERP(t *testing.T) {
|
||||
require.NotNil(t, originalDerpMap)
|
||||
|
||||
coordinator := tailnet.NewCoordinator(logger)
|
||||
defer func() {
|
||||
// use t.Cleanup so the coordinator closing doesn't deadlock with in-memory
|
||||
// coordination
|
||||
t.Cleanup(func() {
|
||||
_ = coordinator.Close()
|
||||
}()
|
||||
})
|
||||
agentID := uuid.New()
|
||||
statsCh := make(chan *agentsdk.Stats, 50)
|
||||
fs := afero.NewMemMapFs()
|
||||
@@ -1681,41 +1683,42 @@ func TestAgent_UpdatedDERP(t *testing.T) {
|
||||
statsCh,
|
||||
coordinator,
|
||||
)
|
||||
closer := agent.New(agent.Options{
|
||||
uut := agent.New(agent.Options{
|
||||
Client: client,
|
||||
Filesystem: fs,
|
||||
Logger: logger.Named("agent"),
|
||||
ReconnectingPTYTimeout: time.Minute,
|
||||
})
|
||||
defer func() {
|
||||
_ = closer.Close()
|
||||
}()
|
||||
t.Cleanup(func() {
|
||||
_ = uut.Close()
|
||||
})
|
||||
|
||||
// Setup a client connection.
|
||||
newClientConn := func(derpMap *tailcfg.DERPMap) *codersdk.WorkspaceAgentConn {
|
||||
newClientConn := func(derpMap *tailcfg.DERPMap, name string) *codersdk.WorkspaceAgentConn {
|
||||
conn, err := tailnet.NewConn(&tailnet.Options{
|
||||
Addresses: []netip.Prefix{netip.PrefixFrom(tailnet.IP(), 128)},
|
||||
DERPMap: derpMap,
|
||||
Logger: logger.Named("client"),
|
||||
Logger: logger.Named(name),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
clientConn, serverConn := net.Pipe()
|
||||
serveClientDone := make(chan struct{})
|
||||
t.Cleanup(func() {
|
||||
_ = clientConn.Close()
|
||||
_ = serverConn.Close()
|
||||
t.Logf("closing conn %s", name)
|
||||
_ = conn.Close()
|
||||
<-serveClientDone
|
||||
})
|
||||
go func() {
|
||||
defer close(serveClientDone)
|
||||
err := coordinator.ServeClient(serverConn, uuid.New(), agentID)
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
sendNode, _ := tailnet.ServeCoordinator(clientConn, func(nodes []*tailnet.Node) error {
|
||||
return conn.UpdateNodes(nodes, false)
|
||||
testCtx, testCtxCancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(testCtxCancel)
|
||||
clientID := uuid.New()
|
||||
coordination := tailnet.NewInMemoryCoordination(
|
||||
testCtx, logger,
|
||||
clientID, agentID,
|
||||
coordinator, conn)
|
||||
t.Cleanup(func() {
|
||||
t.Logf("closing coordination %s", name)
|
||||
err := coordination.Close()
|
||||
if err != nil {
|
||||
t.Logf("error closing in-memory coordination: %s", err.Error())
|
||||
}
|
||||
})
|
||||
conn.SetNodeCallback(sendNode)
|
||||
// Force DERP.
|
||||
conn.SetBlockEndpoints(true)
|
||||
|
||||
@@ -1724,6 +1727,7 @@ func TestAgent_UpdatedDERP(t *testing.T) {
|
||||
CloseFunc: func() error { return codersdk.ErrSkipClose },
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
t.Logf("closing sdkConn %s", name)
|
||||
_ = sdkConn.Close()
|
||||
})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
@@ -1734,7 +1738,7 @@ func TestAgent_UpdatedDERP(t *testing.T) {
|
||||
|
||||
return sdkConn
|
||||
}
|
||||
conn1 := newClientConn(originalDerpMap)
|
||||
conn1 := newClientConn(originalDerpMap, "client1")
|
||||
|
||||
// Change the DERP map.
|
||||
newDerpMap, _ := tailnettest.RunDERPAndSTUN(t)
|
||||
@@ -1753,27 +1757,34 @@ func TestAgent_UpdatedDERP(t *testing.T) {
|
||||
DERPMap: newDerpMap,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Logf("client Pushed DERPMap update")
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
conn := closer.TailnetConn()
|
||||
conn := uut.TailnetConn()
|
||||
if conn == nil {
|
||||
return false
|
||||
}
|
||||
regionIDs := conn.DERPMap().RegionIDs()
|
||||
return len(regionIDs) == 1 && regionIDs[0] == 2 && conn.Node().PreferredDERP == 2
|
||||
preferredDERP := conn.Node().PreferredDERP
|
||||
t.Logf("agent Conn DERPMap with regionIDs %v, PreferredDERP %d", regionIDs, preferredDERP)
|
||||
return len(regionIDs) == 1 && regionIDs[0] == 2 && preferredDERP == 2
|
||||
}, testutil.WaitLong, testutil.IntervalFast)
|
||||
t.Logf("agent got the new DERPMap")
|
||||
|
||||
// Connect from a second client and make sure it uses the new DERP map.
|
||||
conn2 := newClientConn(newDerpMap)
|
||||
conn2 := newClientConn(newDerpMap, "client2")
|
||||
require.Equal(t, []int{2}, conn2.DERPMap().RegionIDs())
|
||||
t.Log("conn2 got the new DERPMap")
|
||||
|
||||
// If the first client gets a DERP map update, it should be able to
|
||||
// reconnect just fine.
|
||||
conn1.SetDERPMap(newDerpMap)
|
||||
require.Equal(t, []int{2}, conn1.DERPMap().RegionIDs())
|
||||
t.Log("set the new DERPMap on conn1")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
require.True(t, conn1.AwaitReachable(ctx))
|
||||
t.Log("conn1 reached agent with new DERP")
|
||||
}
|
||||
|
||||
func TestAgent_Speedtest(t *testing.T) {
|
||||
@@ -2050,22 +2061,22 @@ func setupAgent(t *testing.T, metadata agentsdk.Manifest, ptyTimeout time.Durati
|
||||
Logger: logger.Named("client"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
clientConn, serverConn := net.Pipe()
|
||||
serveClientDone := make(chan struct{})
|
||||
t.Cleanup(func() {
|
||||
_ = clientConn.Close()
|
||||
_ = serverConn.Close()
|
||||
_ = conn.Close()
|
||||
<-serveClientDone
|
||||
})
|
||||
go func() {
|
||||
defer close(serveClientDone)
|
||||
coordinator.ServeClient(serverConn, uuid.New(), metadata.AgentID)
|
||||
}()
|
||||
sendNode, _ := tailnet.ServeCoordinator(clientConn, func(nodes []*tailnet.Node) error {
|
||||
return conn.UpdateNodes(nodes, false)
|
||||
testCtx, testCtxCancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(testCtxCancel)
|
||||
clientID := uuid.New()
|
||||
coordination := tailnet.NewInMemoryCoordination(
|
||||
testCtx, logger,
|
||||
clientID, metadata.AgentID,
|
||||
coordinator, conn)
|
||||
t.Cleanup(func() {
|
||||
err := coordination.Close()
|
||||
if err != nil {
|
||||
t.Logf("error closing in-mem coordination: %s", err.Error())
|
||||
}
|
||||
})
|
||||
conn.SetNodeCallback(sendNode)
|
||||
agentConn := codersdk.NewWorkspaceAgentConn(conn, codersdk.WorkspaceAgentConnOptions{
|
||||
AgentID: metadata.AgentID,
|
||||
})
|
||||
|
||||
@@ -3,19 +3,26 @@ package agenttest
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/xerrors"
|
||||
"storj.io/drpc"
|
||||
"storj.io/drpc/drpcmux"
|
||||
"storj.io/drpc/drpcserver"
|
||||
"tailscale.com/tailcfg"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
drpcsdk "github.com/coder/coder/v2/codersdk/drpc"
|
||||
"github.com/coder/coder/v2/tailnet"
|
||||
"github.com/coder/coder/v2/tailnet/proto"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
@@ -24,11 +31,31 @@ func NewClient(t testing.TB,
|
||||
agentID uuid.UUID,
|
||||
manifest agentsdk.Manifest,
|
||||
statsChan chan *agentsdk.Stats,
|
||||
coordinator tailnet.CoordinatorV1,
|
||||
coordinator tailnet.Coordinator,
|
||||
) *Client {
|
||||
if manifest.AgentID == uuid.Nil {
|
||||
manifest.AgentID = agentID
|
||||
}
|
||||
coordPtr := atomic.Pointer[tailnet.Coordinator]{}
|
||||
coordPtr.Store(&coordinator)
|
||||
mux := drpcmux.New()
|
||||
drpcService := &tailnet.DRPCService{
|
||||
CoordPtr: &coordPtr,
|
||||
Logger: logger,
|
||||
// TODO: handle DERPMap too!
|
||||
DerpMapUpdateFrequency: time.Hour,
|
||||
DerpMapFn: func() *tailcfg.DERPMap { panic("not implemented") },
|
||||
}
|
||||
err := proto.DRPCRegisterTailnet(mux, drpcService)
|
||||
require.NoError(t, err)
|
||||
server := drpcserver.NewWithOptions(mux, drpcserver.Options{
|
||||
Log: func(err error) {
|
||||
if xerrors.Is(err, io.EOF) {
|
||||
return
|
||||
}
|
||||
logger.Debug(context.Background(), "drpc server error", slog.Error(err))
|
||||
},
|
||||
})
|
||||
return &Client{
|
||||
t: t,
|
||||
logger: logger.Named("client"),
|
||||
@@ -36,6 +63,7 @@ func NewClient(t testing.TB,
|
||||
manifest: manifest,
|
||||
statsChan: statsChan,
|
||||
coordinator: coordinator,
|
||||
server: server,
|
||||
derpMapUpdates: make(chan agentsdk.DERPMapUpdate),
|
||||
}
|
||||
}
|
||||
@@ -47,7 +75,8 @@ type Client struct {
|
||||
manifest agentsdk.Manifest
|
||||
metadata map[string]agentsdk.Metadata
|
||||
statsChan chan *agentsdk.Stats
|
||||
coordinator tailnet.CoordinatorV1
|
||||
coordinator tailnet.Coordinator
|
||||
server *drpcserver.Server
|
||||
LastWorkspaceAgent func()
|
||||
PatchWorkspaceLogs func() error
|
||||
GetServiceBannerFunc func() (codersdk.ServiceBannerConfig, error)
|
||||
@@ -63,20 +92,29 @@ func (c *Client) Manifest(_ context.Context) (agentsdk.Manifest, error) {
|
||||
return c.manifest, nil
|
||||
}
|
||||
|
||||
func (c *Client) Listen(_ context.Context) (net.Conn, error) {
|
||||
clientConn, serverConn := net.Pipe()
|
||||
func (c *Client) Listen(_ context.Context) (drpc.Conn, error) {
|
||||
conn, lis := drpcsdk.MemTransportPipe()
|
||||
closed := make(chan struct{})
|
||||
c.LastWorkspaceAgent = func() {
|
||||
_ = serverConn.Close()
|
||||
_ = clientConn.Close()
|
||||
_ = conn.Close()
|
||||
_ = lis.Close()
|
||||
<-closed
|
||||
}
|
||||
c.t.Cleanup(c.LastWorkspaceAgent)
|
||||
serveCtx, cancel := context.WithCancel(context.Background())
|
||||
c.t.Cleanup(cancel)
|
||||
auth := tailnet.AgentTunnelAuth{}
|
||||
streamID := tailnet.StreamID{
|
||||
Name: "agenttest",
|
||||
ID: c.agentID,
|
||||
Auth: auth,
|
||||
}
|
||||
serveCtx = tailnet.WithStreamID(serveCtx, streamID)
|
||||
go func() {
|
||||
_ = c.coordinator.ServeAgent(serverConn, c.agentID, "")
|
||||
_ = c.server.Serve(serveCtx, lis)
|
||||
close(closed)
|
||||
}()
|
||||
return clientConn, nil
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *Client) ReportStats(ctx context.Context, _ slog.Logger, statsChan <-chan *agentsdk.Stats, setInterval func(time.Duration)) (io.Closer, error) {
|
||||
|
||||
Reference in New Issue
Block a user