From fe7eb34d7a0cdf61560b98b5fb4d29c176b37537 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Fri, 15 Nov 2024 14:52:43 +0400 Subject: [PATCH] feat: add support for WorkspaceUpdates to WebsocketDialer --- Makefile | 15 ++-- codersdk/workspacesdk/dialer.go | 69 +++++++++++--- codersdk/workspacesdk/dialer_test.go | 90 ++++++++++++++++++- codersdk/workspacesdk/workspacesdk.go | 11 --- enterprise/wsproxy/wsproxysdk/wsproxysdk.go | 15 ---- tailnet/service_test.go | 42 +++------ tailnet/tailnettest/subscriptionmock.go | 68 ++++++++++++++ tailnet/tailnettest/tailnettest.go | 2 + .../workspaceupdatesprovidermock.go | 71 +++++++++++++++ 9 files changed, 305 insertions(+), 78 deletions(-) create mode 100644 tailnet/tailnettest/subscriptionmock.go create mode 100644 tailnet/tailnettest/workspaceupdatesprovidermock.go diff --git a/Makefile b/Makefile index f9d7d1bf58382..88664710a067b 100644 --- a/Makefile +++ b/Makefile @@ -482,6 +482,13 @@ DB_GEN_FILES := \ coderd/database/dbauthz/dbauthz.go \ coderd/database/dbmock/dbmock.go +TAILNETTEST_MOCKS := \ + tailnet/tailnettest/coordinatormock.go \ + tailnet/tailnettest/coordinateemock.go \ + tailnet/tailnettest/workspaceupdatesprovidermock.go \ + tailnet/tailnettest/subscriptionmock.go + + # all gen targets should be added here and to gen/mark-fresh gen: \ tailnet/proto/tailnet.pb.go \ @@ -506,8 +513,7 @@ gen: \ site/e2e/provisionerGenerated.ts \ site/src/theme/icons.json \ examples/examples.gen.json \ - tailnet/tailnettest/coordinatormock.go \ - tailnet/tailnettest/coordinateemock.go \ + $(TAILNETTEST_MOCKS) \ coderd/database/pubsub/psmock/psmock.go .PHONY: gen @@ -536,8 +542,7 @@ gen/mark-fresh: site/e2e/provisionerGenerated.ts \ site/src/theme/icons.json \ examples/examples.gen.json \ - tailnet/tailnettest/coordinatormock.go \ - tailnet/tailnettest/coordinateemock.go \ + $(TAILNETTEST_MOCKS) \ coderd/database/pubsub/psmock/psmock.go \ " @@ -570,7 +575,7 @@ coderd/database/dbmock/dbmock.go: coderd/database/db.go coderd/database/querier. coderd/database/pubsub/psmock/psmock.go: coderd/database/pubsub/pubsub.go go generate ./coderd/database/pubsub/psmock -tailnet/tailnettest/coordinatormock.go tailnet/tailnettest/coordinateemock.go: tailnet/coordinator.go +$(TAILNETTEST_MOCKS): tailnet/coordinator.go tailnet/service.go go generate ./tailnet/tailnettest/ tailnet/proto/tailnet.pb.go: tailnet/proto/tailnet.proto diff --git a/codersdk/workspacesdk/dialer.go b/codersdk/workspacesdk/dialer.go index b15c13aa978f9..99bc90ec4c9f8 100644 --- a/codersdk/workspacesdk/dialer.go +++ b/codersdk/workspacesdk/dialer.go @@ -25,14 +25,26 @@ var permanentErrorStatuses = []int{ } type WebsocketDialer struct { - logger slog.Logger - dialOptions *websocket.DialOptions - url *url.URL + logger slog.Logger + dialOptions *websocket.DialOptions + url *url.URL + // workspaceUpdatesReq != nil means that the dialer should call the WorkspaceUpdates RPC and + // return the corresponding client + workspaceUpdatesReq *proto.WorkspaceUpdatesRequest + resumeTokenFailed bool connected chan error isFirst bool } +type WebsocketDialerOption func(*WebsocketDialer) + +func WithWorkspaceUpdates(req *proto.WorkspaceUpdatesRequest) WebsocketDialerOption { + return func(w *WebsocketDialer) { + w.workspaceUpdatesReq = req + } +} + func (w *WebsocketDialer) Dial(ctx context.Context, r tailnet.ResumeTokenController, ) ( tailnet.ControlProtocolClients, error, @@ -41,14 +53,27 @@ func (w *WebsocketDialer) Dial(ctx context.Context, r tailnet.ResumeTokenControl u := new(url.URL) *u = *w.url + q := u.Query() if r != nil && !w.resumeTokenFailed { if token, ok := r.Token(); ok { - q := u.Query() q.Set("resume_token", token) - u.RawQuery = q.Encode() w.logger.Debug(ctx, "using resume token on dial") } } + // The current version includes additions + // + // 2.1 GetAnnouncementBanners on the Agent API (version locked to Tailnet API) + // 2.2 PostTelemetry on the Tailnet API + // 2.3 RefreshResumeToken, WorkspaceUpdates + // + // Resume tokens and telemetry are optional, and fail gracefully. So we use version 2.0 for + // maximum compatibility if we don't need WorkspaceUpdates. If we do, we use 2.3. + if w.workspaceUpdatesReq != nil { + q.Add("version", "2.3") + } else { + q.Add("version", "2.0") + } + u.RawQuery = q.Encode() // nolint:bodyclose ws, res, err := websocket.Dial(ctx, u.String(), w.dialOptions) @@ -115,12 +140,23 @@ func (w *WebsocketDialer) Dial(ctx context.Context, r tailnet.ResumeTokenControl return tailnet.ControlProtocolClients{}, err } + var updates tailnet.WorkspaceUpdatesClient + if w.workspaceUpdatesReq != nil { + updates, err = client.WorkspaceUpdates(context.Background(), w.workspaceUpdatesReq) + if err != nil { + w.logger.Debug(ctx, "failed to create WorkspaceUpdates stream", slog.Error(err)) + _ = ws.Close(websocket.StatusInternalError, "") + return tailnet.ControlProtocolClients{}, err + } + } + return tailnet.ControlProtocolClients{ - Closer: client.DRPCConn(), - Coordinator: coord, - DERP: derps, - ResumeToken: client, - Telemetry: client, + Closer: client.DRPCConn(), + Coordinator: coord, + DERP: derps, + ResumeToken: client, + Telemetry: client, + WorkspaceUpdates: updates, }, nil } @@ -128,12 +164,19 @@ func (w *WebsocketDialer) Connected() <-chan error { return w.connected } -func NewWebsocketDialer(logger slog.Logger, u *url.URL, opts *websocket.DialOptions) *WebsocketDialer { - return &WebsocketDialer{ +func NewWebsocketDialer( + logger slog.Logger, u *url.URL, websocketOptions *websocket.DialOptions, + dialerOptions ...WebsocketDialerOption, +) *WebsocketDialer { + w := &WebsocketDialer{ logger: logger, - dialOptions: opts, + dialOptions: websocketOptions, url: u, connected: make(chan error, 1), isFirst: true, } + for _, o := range dialerOptions { + o(w) + } + return w } diff --git a/codersdk/workspacesdk/dialer_test.go b/codersdk/workspacesdk/dialer_test.go index 5247d5c7834da..c10325f9b7184 100644 --- a/codersdk/workspacesdk/dialer_test.go +++ b/codersdk/workspacesdk/dialer_test.go @@ -9,8 +9,10 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "nhooyr.io/websocket" "tailscale.com/tailcfg" @@ -21,7 +23,7 @@ import ( "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/coder/v2/tailnet" - "github.com/coder/coder/v2/tailnet/proto" + tailnetproto "github.com/coder/coder/v2/tailnet/proto" "github.com/coder/coder/v2/tailnet/tailnettest" "github.com/coder/coder/v2/testutil" ) @@ -102,6 +104,7 @@ func TestWebsocketDialer_TokenController(t *testing.T) { require.Equal(t, "", gotToken) clients = testutil.RequireRecvCtx(ctx, t, clientCh) + require.Nil(t, clients.WorkspaceUpdates) clients.Closer.Close() err = testutil.RequireRecvCtx(ctx, t, wsErr) @@ -273,7 +276,7 @@ func TestWebsocketDialer_UplevelVersion(t *testing.T) { logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - sVer := apiversion.New(proto.CurrentMajor, proto.CurrentMinor-1) + sVer := apiversion.New(2, 2) // the following matches what Coderd does; // c.f. coderd/workspaceagents.go: workspaceAgentClientCoordinate @@ -291,7 +294,10 @@ func TestWebsocketDialer_UplevelVersion(t *testing.T) { svrURL, err := url.Parse(svr.URL) require.NoError(t, err) - uut := workspacesdk.NewWebsocketDialer(logger, svrURL, &websocket.DialOptions{}) + uut := workspacesdk.NewWebsocketDialer( + logger, svrURL, &websocket.DialOptions{}, + workspacesdk.WithWorkspaceUpdates(&tailnetproto.WorkspaceUpdatesRequest{}), + ) errCh := make(chan error, 1) go func() { @@ -307,6 +313,84 @@ func TestWebsocketDialer_UplevelVersion(t *testing.T) { require.NotEmpty(t, sdkErr.Helper) } +func TestWebsocketDialer_WorkspaceUpdates(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + logger := slogtest.Make(t, &slogtest.Options{ + IgnoreErrors: true, + }).Leveled(slog.LevelDebug) + + fCoord := tailnettest.NewFakeCoordinator() + var coord tailnet.Coordinator = fCoord + coordPtr := atomic.Pointer[tailnet.Coordinator]{} + coordPtr.Store(&coord) + ctrl := gomock.NewController(t) + mProvider := tailnettest.NewMockWorkspaceUpdatesProvider(ctrl) + + svc, err := tailnet.NewClientService(tailnet.ClientServiceOptions{ + Logger: logger, + CoordPtr: &coordPtr, + DERPMapUpdateFrequency: time.Hour, + DERPMapFn: func() *tailcfg.DERPMap { return &tailcfg.DERPMap{} }, + WorkspaceUpdatesProvider: mProvider, + }) + require.NoError(t, err) + + wsErr := make(chan error, 1) + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // need 2.3 for WorkspaceUpdates RPC + cVer := r.URL.Query().Get("version") + assert.Equal(t, "2.3", cVer) + + sws, err := websocket.Accept(w, r, nil) + if !assert.NoError(t, err) { + return + } + wsCtx, nc := codersdk.WebsocketNetConn(ctx, sws, websocket.MessageBinary) + // streamID can be empty because we don't call RPCs in this test. + wsErr <- svc.ServeConnV2(wsCtx, nc, tailnet.StreamID{}) + })) + defer svr.Close() + svrURL, err := url.Parse(svr.URL) + require.NoError(t, err) + + userID := uuid.UUID{88} + + mSub := tailnettest.NewMockSubscription(ctrl) + updateCh := make(chan *tailnetproto.WorkspaceUpdate, 1) + mProvider.EXPECT().Subscribe(gomock.Any(), userID).Times(1).Return(mSub, nil) + mSub.EXPECT().Updates().MinTimes(1).Return(updateCh) + mSub.EXPECT().Close().Times(1).Return(nil) + + uut := workspacesdk.NewWebsocketDialer( + logger, svrURL, &websocket.DialOptions{}, + workspacesdk.WithWorkspaceUpdates(&tailnetproto.WorkspaceUpdatesRequest{ + WorkspaceOwnerId: userID[:], + }), + ) + + clients, err := uut.Dial(ctx, nil) + require.NoError(t, err) + require.NotNil(t, clients.WorkspaceUpdates) + + wsID := uuid.UUID{99} + expectedUpdate := &tailnetproto.WorkspaceUpdate{ + UpsertedWorkspaces: []*tailnetproto.Workspace{ + {Id: wsID[:]}, + }, + } + updateCh <- expectedUpdate + + gotUpdate, err := clients.WorkspaceUpdates.Recv() + require.NoError(t, err) + require.Equal(t, wsID[:], gotUpdate.GetUpsertedWorkspaces()[0].GetId()) + + clients.Closer.Close() + + err = testutil.RequireRecvCtx(ctx, t, wsErr) + require.NoError(t, err) +} + type fakeResumeTokenController struct { ctx context.Context t testing.TB diff --git a/codersdk/workspacesdk/workspacesdk.go b/codersdk/workspacesdk/workspacesdk.go index 2724b733ea16a..34add580cbc4f 100644 --- a/codersdk/workspacesdk/workspacesdk.go +++ b/codersdk/workspacesdk/workspacesdk.go @@ -216,17 +216,6 @@ func (c *Client) DialAgent(dialCtx context.Context, agentID uuid.UUID, options * if err != nil { return nil, xerrors.Errorf("parse url: %w", err) } - q := coordinateURL.Query() - // The current version includes additions - // - // 2.1 GetAnnouncementBanners on the Agent API (version locked to Tailnet API) - // 2.2 PostTelemetry on the Tailnet API - // 2.3 RefreshResumeToken, WorkspaceUpdates - // - // Since resume tokens and telemetry are optional, and fail gracefully, and we don't use - // WorkspaceUpdates to talk to a single agent, we ask for version 2.0 for maximum compatibility - q.Add("version", "2.0") - coordinateURL.RawQuery = q.Encode() dialer := NewWebsocketDialer(options.Logger, coordinateURL, &websocket.DialOptions{ HTTPClient: c.client.HTTPClient, diff --git a/enterprise/wsproxy/wsproxysdk/wsproxysdk.go b/enterprise/wsproxy/wsproxysdk/wsproxysdk.go index 9825f43c4129d..93fc93abd4add 100644 --- a/enterprise/wsproxy/wsproxysdk/wsproxysdk.go +++ b/enterprise/wsproxy/wsproxysdk/wsproxysdk.go @@ -21,18 +21,6 @@ import ( agpl "github.com/coder/coder/v2/tailnet" ) -// TailnetAPIVersion is the version of the Tailnet API we use for wsproxy. -// -// # The current version of the Tailnet API includes additions -// -// 2.1 GetAnnouncementBanners on the Agent API (version locked to Tailnet API) -// 2.2 PostTelemetry on the Tailnet API -// 2.3 RefreshResumeToken, WorkspaceUpdates -// -// Since resume tokens and telemetry are optional, and fail gracefully, and we don't use -// WorkspaceUpdates in the wsproxy, we ask for version 2.0 for maximum compatibility -const TailnetAPIVersion = "2.0" - // Client is a HTTP client for a subset of Coder API routes that external // proxies need. type Client struct { @@ -518,9 +506,6 @@ func (c *Client) TailnetDialer() (*workspacesdk.WebsocketDialer, error) { if err != nil { return nil, xerrors.Errorf("parse url: %w", err) } - q := coordinateURL.Query() - q.Add("version", TailnetAPIVersion) - coordinateURL.RawQuery = q.Encode() coordinateHeaders := make(http.Header) tokenHeader := codersdk.SessionTokenHeader if c.SDKClient.SessionTokenHeader != "" { diff --git a/tailnet/service_test.go b/tailnet/service_test.go index f5a01cc2fbacc..b4c249391f28c 100644 --- a/tailnet/service_test.go +++ b/tailnet/service_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "golang.org/x/xerrors" "tailscale.com/tailcfg" @@ -236,8 +237,8 @@ func TestClientUserCoordinateeAuth(t *testing.T) { agentID2 := uuid.UUID{0x02} clientID := uuid.UUID{0x03} - updatesCh := make(chan *proto.WorkspaceUpdate, 1) - updatesProvider := &fakeUpdatesProvider{ch: updatesCh} + ctrl := gomock.NewController(t) + updatesProvider := tailnettest.NewMockWorkspaceUpdatesProvider(ctrl) fCoord, client := createUpdateService(t, ctx, clientID, updatesProvider) @@ -271,8 +272,10 @@ func TestWorkspaceUpdates(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitShort) + ctrl := gomock.NewController(t) + updatesProvider := tailnettest.NewMockWorkspaceUpdatesProvider(ctrl) + mSub := tailnettest.NewMockSubscription(ctrl) updatesCh := make(chan *proto.WorkspaceUpdate, 1) - updatesProvider := &fakeUpdatesProvider{ch: updatesCh} clientID := uuid.UUID{0x03} wsID := uuid.UUID{0x04} @@ -293,6 +296,11 @@ func TestWorkspaceUpdates(t *testing.T) { DeletedAgents: []*proto.Agent{}, } updatesCh <- expected + updatesProvider.EXPECT().Subscribe(gomock.Any(), clientID). + Times(1). + Return(mSub, nil) + mSub.EXPECT().Updates().MinTimes(1).Return(updatesCh) + mSub.EXPECT().Close().Times(1).Return(nil) updatesStream, err := client.WorkspaceUpdates(ctx, &proto.WorkspaceUpdatesRequest{ WorkspaceOwnerId: tailnet.UUIDToByteSlice(clientID), @@ -354,34 +362,6 @@ func createUpdateService(t *testing.T, ctx context.Context, clientID uuid.UUID, return fCoord, client } -type fakeUpdatesProvider struct { - ch chan *proto.WorkspaceUpdate -} - -func (*fakeUpdatesProvider) Close() error { - return nil -} - -func (f *fakeUpdatesProvider) Subscribe(context.Context, uuid.UUID) (tailnet.Subscription, error) { - return &fakeSubscription{ch: f.ch}, nil -} - -type fakeSubscription struct { - ch chan *proto.WorkspaceUpdate -} - -func (*fakeSubscription) Close() error { - return nil -} - -func (f *fakeSubscription) Updates() <-chan *proto.WorkspaceUpdate { - return f.ch -} - -var _ tailnet.Subscription = (*fakeSubscription)(nil) - -var _ tailnet.WorkspaceUpdatesProvider = (*fakeUpdatesProvider)(nil) - type fakeTunnelAuth struct{} // AuthorizeTunnel implements tailnet.TunnelAuthorizer. diff --git a/tailnet/tailnettest/subscriptionmock.go b/tailnet/tailnettest/subscriptionmock.go new file mode 100644 index 0000000000000..05c5042351daa --- /dev/null +++ b/tailnet/tailnettest/subscriptionmock.go @@ -0,0 +1,68 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/coder/coder/v2/tailnet (interfaces: Subscription) +// +// Generated by this command: +// +// mockgen -destination ./subscriptionmock.go -package tailnettest github.com/coder/coder/v2/tailnet Subscription +// + +// Package tailnettest is a generated GoMock package. +package tailnettest + +import ( + reflect "reflect" + + proto "github.com/coder/coder/v2/tailnet/proto" + gomock "go.uber.org/mock/gomock" +) + +// MockSubscription is a mock of Subscription interface. +type MockSubscription struct { + ctrl *gomock.Controller + recorder *MockSubscriptionMockRecorder +} + +// MockSubscriptionMockRecorder is the mock recorder for MockSubscription. +type MockSubscriptionMockRecorder struct { + mock *MockSubscription +} + +// NewMockSubscription creates a new mock instance. +func NewMockSubscription(ctrl *gomock.Controller) *MockSubscription { + mock := &MockSubscription{ctrl: ctrl} + mock.recorder = &MockSubscriptionMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSubscription) EXPECT() *MockSubscriptionMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockSubscription) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockSubscriptionMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockSubscription)(nil).Close)) +} + +// Updates mocks base method. +func (m *MockSubscription) Updates() <-chan *proto.WorkspaceUpdate { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Updates") + ret0, _ := ret[0].(<-chan *proto.WorkspaceUpdate) + return ret0 +} + +// Updates indicates an expected call of Updates. +func (mr *MockSubscriptionMockRecorder) Updates() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Updates", reflect.TypeOf((*MockSubscription)(nil).Updates)) +} diff --git a/tailnet/tailnettest/tailnettest.go b/tailnet/tailnettest/tailnettest.go index e793365937310..9217376020cf3 100644 --- a/tailnet/tailnettest/tailnettest.go +++ b/tailnet/tailnettest/tailnettest.go @@ -26,6 +26,8 @@ import ( //go:generate mockgen -destination ./coordinatormock.go -package tailnettest github.com/coder/coder/v2/tailnet Coordinator //go:generate mockgen -destination ./coordinateemock.go -package tailnettest github.com/coder/coder/v2/tailnet Coordinatee +//go:generate mockgen -destination ./workspaceupdatesprovidermock.go -package tailnettest github.com/coder/coder/v2/tailnet WorkspaceUpdatesProvider +//go:generate mockgen -destination ./subscriptionmock.go -package tailnettest github.com/coder/coder/v2/tailnet Subscription type derpAndSTUNCfg struct { DisableSTUN bool diff --git a/tailnet/tailnettest/workspaceupdatesprovidermock.go b/tailnet/tailnettest/workspaceupdatesprovidermock.go new file mode 100644 index 0000000000000..02eadc4ca9e5a --- /dev/null +++ b/tailnet/tailnettest/workspaceupdatesprovidermock.go @@ -0,0 +1,71 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/coder/coder/v2/tailnet (interfaces: WorkspaceUpdatesProvider) +// +// Generated by this command: +// +// mockgen -destination ./workspaceupdatesprovidermock.go -package tailnettest github.com/coder/coder/v2/tailnet WorkspaceUpdatesProvider +// + +// Package tailnettest is a generated GoMock package. +package tailnettest + +import ( + context "context" + reflect "reflect" + + tailnet "github.com/coder/coder/v2/tailnet" + uuid "github.com/google/uuid" + gomock "go.uber.org/mock/gomock" +) + +// MockWorkspaceUpdatesProvider is a mock of WorkspaceUpdatesProvider interface. +type MockWorkspaceUpdatesProvider struct { + ctrl *gomock.Controller + recorder *MockWorkspaceUpdatesProviderMockRecorder +} + +// MockWorkspaceUpdatesProviderMockRecorder is the mock recorder for MockWorkspaceUpdatesProvider. +type MockWorkspaceUpdatesProviderMockRecorder struct { + mock *MockWorkspaceUpdatesProvider +} + +// NewMockWorkspaceUpdatesProvider creates a new mock instance. +func NewMockWorkspaceUpdatesProvider(ctrl *gomock.Controller) *MockWorkspaceUpdatesProvider { + mock := &MockWorkspaceUpdatesProvider{ctrl: ctrl} + mock.recorder = &MockWorkspaceUpdatesProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockWorkspaceUpdatesProvider) EXPECT() *MockWorkspaceUpdatesProviderMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockWorkspaceUpdatesProvider) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockWorkspaceUpdatesProviderMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockWorkspaceUpdatesProvider)(nil).Close)) +} + +// Subscribe mocks base method. +func (m *MockWorkspaceUpdatesProvider) Subscribe(arg0 context.Context, arg1 uuid.UUID) (tailnet.Subscription, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Subscribe", arg0, arg1) + ret0, _ := ret[0].(tailnet.Subscription) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Subscribe indicates an expected call of Subscribe. +func (mr *MockWorkspaceUpdatesProviderMockRecorder) Subscribe(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockWorkspaceUpdatesProvider)(nil).Subscribe), arg0, arg1) +} 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