diff --git a/coderd/coderd.go b/coderd/coderd.go index 3bfb087ec0c55..8dd8cf36936fb 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -473,6 +473,11 @@ func New(options *Options) *API { Cache: wsconncache.New(api._dialWorkspaceAgentTailnet, 0), } } + api.TailnetClientService, err = tailnet.NewClientService( + api.Logger.Named("tailnetclient"), &api.TailnetCoordinator) + if err != nil { + api.Logger.Fatal(api.ctx, "failed to initialize tailnet client service", slog.Error(err)) + } workspaceAppsLogger := options.Logger.Named("workspaceapps") if options.WorkspaceAppsStatsCollectorOptions.Logger == nil { @@ -1061,6 +1066,7 @@ type API struct { Auditor atomic.Pointer[audit.Auditor] WorkspaceClientCoordinateOverride atomic.Pointer[func(rw http.ResponseWriter) bool] TailnetCoordinator atomic.Pointer[tailnet.Coordinator] + TailnetClientService *tailnet.ClientService QuotaCommitter atomic.Pointer[proto.QuotaCommitter] // WorkspaceProxyHostsFn returns the hosts of healthy workspace proxies // for header reasons. diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 992337915d03d..25ea30bba1eb0 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1387,6 +1387,21 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R } } + version := "1.0" + qv := r.URL.Query().Get("version") + if qv != "" { + version = qv + } + if err := tailnet.ValidateVersion(version); err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Unknown or unsupported API version", + Validations: []codersdk.ValidationError{ + {Field: "version", Detail: err.Error()}, + }, + }) + return + } + api.WebsocketWaitMutex.Lock() api.WebsocketWaitGroup.Add(1) api.WebsocketWaitMutex.Unlock() @@ -1407,8 +1422,8 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R go httpapi.Heartbeat(ctx, conn) defer conn.Close(websocket.StatusNormalClosure, "") - err = (*api.TailnetCoordinator.Load()).ServeClient(wsNetConn, uuid.New(), workspaceAgent.ID) - if err != nil { + err = api.TailnetClientService.ServeClient(ctx, version, wsNetConn, uuid.New(), workspaceAgent.ID) + if err != nil && !xerrors.Is(err, io.EOF) && !xerrors.Is(err, context.Canceled) { _ = conn.Close(websocket.StatusInternalError, err.Error()) return } diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 5f27cec39835f..5232b71113ea9 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -444,6 +444,38 @@ func TestWorkspaceAgentTailnet(t *testing.T) { require.Equal(t, "test", strings.TrimSpace(string(output))) } +func TestWorkspaceAgentClientCoordinate_BadVersion(t *testing.T) { + t.Parallel() + client, db := coderdtest.NewWithDatabase(t, nil) + user := coderdtest.CreateFirstUser(t, client) + + r := dbfake.WorkspaceBuild(t, db, database.Workspace{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent().Do() + + ctx := testutil.Context(t, testutil.WaitShort) + agentToken, err := uuid.Parse(r.AgentToken) + require.NoError(t, err) + //nolint: gocritic // testing + ao, err := db.GetWorkspaceAgentAndOwnerByAuthToken(dbauthz.AsSystemRestricted(ctx), agentToken) + require.NoError(t, err) + + //nolint: bodyclose // closed by ReadBodyAsError + resp, err := client.Request(ctx, http.MethodGet, + fmt.Sprintf("api/v2/workspaceagents/%s/coordinate", ao.WorkspaceAgent.ID), + nil, + codersdk.WithQueryParam("version", "99.99")) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + err = codersdk.ReadBodyAsError(resp) + var sdkErr *codersdk.Error + require.ErrorAs(t, err, &sdkErr) + require.Equal(t, "Unknown or unsupported API version", sdkErr.Message) + require.Len(t, sdkErr.Validations, 1) + require.Equal(t, "version", sdkErr.Validations[0].Field) +} + func TestWorkspaceAgentTailnetDirectDisabled(t *testing.T) { t.Parallel() diff --git a/helm/provisioner/charts/libcoder-0.1.0.tgz b/helm/provisioner/charts/libcoder-0.1.0.tgz index 939508140df95..4fa45395783fb 100644 Binary files a/helm/provisioner/charts/libcoder-0.1.0.tgz and b/helm/provisioner/charts/libcoder-0.1.0.tgz differ