mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
feat: add single tailnet support to moons (#8587)
This commit is contained in:
@ -125,6 +125,15 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
r.Use(apiKeyMiddleware)
|
||||
r.Post("/", api.reconnectingPTYSignedToken)
|
||||
})
|
||||
|
||||
r.With(
|
||||
apiKeyMiddlewareOptional,
|
||||
httpmw.ExtractWorkspaceProxy(httpmw.ExtractWorkspaceProxyConfig{
|
||||
DB: options.Database,
|
||||
Optional: true,
|
||||
}),
|
||||
httpmw.RequireAPIKeyOrWorkspaceProxyAuth(),
|
||||
).Get("/workspaceagents/{workspaceagent}/legacy", api.agentIsLegacy)
|
||||
r.Route("/workspaceproxies", func(r chi.Router) {
|
||||
r.Use(
|
||||
api.moonsEnabledMW,
|
||||
@ -143,6 +152,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
Optional: false,
|
||||
}),
|
||||
)
|
||||
r.Get("/coordinate", api.workspaceProxyCoordinate)
|
||||
r.Post("/issue-signed-app-token", api.workspaceProxyIssueSignedAppToken)
|
||||
r.Post("/register", api.workspaceProxyRegister)
|
||||
r.Post("/goingaway", api.workspaceProxyGoingAway)
|
||||
|
@ -25,7 +25,8 @@ import (
|
||||
)
|
||||
|
||||
type ProxyOptions struct {
|
||||
Name string
|
||||
Name string
|
||||
Experiments codersdk.Experiments
|
||||
|
||||
TLSCertificates []tls.Certificate
|
||||
AppHostname string
|
||||
@ -118,6 +119,7 @@ func NewWorkspaceProxy(t *testing.T, coderdAPI *coderd.API, owner *codersdk.Clie
|
||||
|
||||
wssrv, err := wsproxy.New(ctx, &wsproxy.Options{
|
||||
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
|
||||
Experiments: options.Experiments,
|
||||
DashboardURL: coderdAPI.AccessURL,
|
||||
AccessURL: accessURL,
|
||||
AppHostname: options.AppHostname,
|
||||
|
78
enterprise/coderd/workspaceproxycoordinate.go
Normal file
78
enterprise/coderd/workspaceproxycoordinate.go
Normal file
@ -0,0 +1,78 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"nhooyr.io/websocket"
|
||||
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/enterprise/tailnet"
|
||||
"github.com/coder/coder/enterprise/wsproxy/wsproxysdk"
|
||||
)
|
||||
|
||||
// @Summary Agent is legacy
|
||||
// @ID agent-is-legacy
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Param workspaceagent path string true "Workspace Agent ID" format(uuid)
|
||||
// @Success 200 {object} wsproxysdk.AgentIsLegacyResponse
|
||||
// @Router /workspaceagents/{workspaceagent}/legacy [get]
|
||||
// @x-apidocgen {"skip": true}
|
||||
func (api *API) agentIsLegacy(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
agentID, ok := httpmw.ParseUUIDParam(rw, r, "workspaceagent")
|
||||
if !ok {
|
||||
httpapi.Write(r.Context(), rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Missing UUID in URL.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
node := (*api.AGPL.TailnetCoordinator.Load()).Node(agentID)
|
||||
httpapi.Write(ctx, rw, http.StatusOK, wsproxysdk.AgentIsLegacyResponse{
|
||||
Found: node != nil,
|
||||
Legacy: node != nil &&
|
||||
len(node.Addresses) > 0 &&
|
||||
node.Addresses[0].Addr() == codersdk.WorkspaceAgentIP,
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Workspace Proxy Coordinate
|
||||
// @ID workspace-proxy-coordinate
|
||||
// @Security CoderSessionToken
|
||||
// @Tags Enterprise
|
||||
// @Success 101
|
||||
// @Router /workspaceproxies/me/coordinate [get]
|
||||
// @x-apidocgen {"skip": true}
|
||||
func (api *API) workspaceProxyCoordinate(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
api.AGPL.WebsocketWaitMutex.Lock()
|
||||
api.AGPL.WebsocketWaitGroup.Add(1)
|
||||
api.AGPL.WebsocketWaitMutex.Unlock()
|
||||
defer api.AGPL.WebsocketWaitGroup.Done()
|
||||
|
||||
conn, err := websocket.Accept(rw, r, nil)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to accept websocket.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
id := uuid.New()
|
||||
sub := (*api.AGPL.TailnetCoordinator.Load()).ServeMultiAgent(id)
|
||||
nc := websocket.NetConn(ctx, conn, websocket.MessageText)
|
||||
defer nc.Close()
|
||||
|
||||
err = tailnet.ServeWorkspaceProxy(ctx, nc, sub)
|
||||
if err != nil {
|
||||
_ = conn.Close(websocket.StatusInternalError, err.Error())
|
||||
}
|
||||
}
|
158
enterprise/coderd/workspaceproxycoordinator_test.go
Normal file
158
enterprise/coderd/workspaceproxycoordinator_test.go
Normal file
@ -0,0 +1,158 @@
|
||||
package coderd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/moby/moby/pkg/namesgenerator"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"tailscale.com/types/key"
|
||||
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/enterprise/coderd/coderdenttest"
|
||||
"github.com/coder/coder/enterprise/coderd/license"
|
||||
"github.com/coder/coder/enterprise/wsproxy/wsproxysdk"
|
||||
agpl "github.com/coder/coder/tailnet"
|
||||
"github.com/coder/coder/testutil"
|
||||
)
|
||||
|
||||
// workspaceProxyCoordinate and agentIsLegacy are both tested by wsproxy tests.
|
||||
|
||||
func Test_agentIsLegacy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Legacy", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.Experiments = []string{
|
||||
string(codersdk.ExperimentMoons),
|
||||
"*",
|
||||
}
|
||||
|
||||
var (
|
||||
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
db, pubsub = dbtestutil.NewDB(t)
|
||||
logger = slogtest.Make(t, nil)
|
||||
coordinator = agpl.NewCoordinator(logger)
|
||||
client, _ = coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
DeploymentValues: dv,
|
||||
Coordinator: coordinator,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspaceProxy: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
nodeID := uuid.New()
|
||||
ma := coordinator.ServeMultiAgent(nodeID)
|
||||
defer ma.Close()
|
||||
require.NoError(t, ma.UpdateSelf(&agpl.Node{
|
||||
ID: 55,
|
||||
AsOf: time.Unix(1689653252, 0),
|
||||
Key: key.NewNode().Public(),
|
||||
DiscoKey: key.NewDisco().Public(),
|
||||
PreferredDERP: 0,
|
||||
DERPLatency: map[string]float64{
|
||||
"0": 1.0,
|
||||
},
|
||||
DERPForcedWebsocket: map[int]string{},
|
||||
Addresses: []netip.Prefix{netip.PrefixFrom(codersdk.WorkspaceAgentIP, 128)},
|
||||
AllowedIPs: []netip.Prefix{netip.PrefixFrom(codersdk.WorkspaceAgentIP, 128)},
|
||||
Endpoints: []string{"192.168.1.1:18842"},
|
||||
}))
|
||||
|
||||
proxyRes, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{
|
||||
Name: namesgenerator.GetRandomName(1),
|
||||
Icon: "/emojis/flag.png",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
proxyClient := wsproxysdk.New(client.URL)
|
||||
proxyClient.SetSessionToken(proxyRes.ProxyToken)
|
||||
|
||||
legacyRes, err := proxyClient.AgentIsLegacy(ctx, nodeID)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, legacyRes.Found)
|
||||
assert.True(t, legacyRes.Legacy)
|
||||
})
|
||||
|
||||
t.Run("NotLegacy", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.Experiments = []string{
|
||||
string(codersdk.ExperimentMoons),
|
||||
"*",
|
||||
}
|
||||
|
||||
var (
|
||||
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
db, pubsub = dbtestutil.NewDB(t)
|
||||
logger = slogtest.Make(t, nil)
|
||||
coordinator = agpl.NewCoordinator(logger)
|
||||
client, _ = coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
DeploymentValues: dv,
|
||||
Coordinator: coordinator,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspaceProxy: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
nodeID := uuid.New()
|
||||
ma := coordinator.ServeMultiAgent(nodeID)
|
||||
defer ma.Close()
|
||||
require.NoError(t, ma.UpdateSelf(&agpl.Node{
|
||||
ID: 55,
|
||||
AsOf: time.Unix(1689653252, 0),
|
||||
Key: key.NewNode().Public(),
|
||||
DiscoKey: key.NewDisco().Public(),
|
||||
PreferredDERP: 0,
|
||||
DERPLatency: map[string]float64{
|
||||
"0": 1.0,
|
||||
},
|
||||
DERPForcedWebsocket: map[int]string{},
|
||||
Addresses: []netip.Prefix{netip.PrefixFrom(agpl.IPFromUUID(nodeID), 128)},
|
||||
AllowedIPs: []netip.Prefix{netip.PrefixFrom(agpl.IPFromUUID(nodeID), 128)},
|
||||
Endpoints: []string{"192.168.1.1:18842"},
|
||||
}))
|
||||
|
||||
proxyRes, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{
|
||||
Name: namesgenerator.GetRandomName(1),
|
||||
Icon: "/emojis/flag.png",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
proxyClient := wsproxysdk.New(client.URL)
|
||||
proxyClient.SetSessionToken(proxyRes.ProxyToken)
|
||||
|
||||
legacyRes, err := proxyClient.AgentIsLegacy(ctx, nodeID)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, legacyRes.Found)
|
||||
assert.False(t, legacyRes.Legacy)
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user