Skip to content

Commit 5088d1c

Browse files
authored
Support ssh gateway when connecting from workspace explorer (#89)
1 parent 0a75be3 commit 5088d1c

File tree

10 files changed

+241
-83
lines changed

10 files changed

+241
-83
lines changed

src/commands/workspaces.ts

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { ITelemetryService } from '../common/telemetry';
1717
import { IRemoteService } from '../services/remoteService';
1818
import { WrapError } from '../common/utils';
1919
import { getOpenSSHVersion } from '../ssh/sshVersion';
20+
import { IExperimentsService } from '../experiments';
2021

2122
function getCommandName(command: string) {
2223
return command.replace('gitpod.workspaces.', '').replace(/(?:_inline|_context)(?:@\d)?$/, '');
@@ -56,6 +57,7 @@ export class ConnectInNewWindowCommand implements Command {
5657
private readonly remoteService: IRemoteService,
5758
private readonly sessionService: ISessionService,
5859
private readonly hostService: IHostService,
60+
private readonly experimentsService: IExperimentsService,
5961
private readonly telemetryService: ITelemetryService,
6062
private readonly logService: ILogService,
6163
) { }
@@ -92,13 +94,6 @@ export class ConnectInNewWindowCommand implements Command {
9294
location: getCommandLocation(this.id, treeItem)
9395
});
9496

95-
const domain = getLocalSSHDomain(this.hostService.gitpodHost);
96-
const sshHostname = `${wsData.id}.${domain}`;
97-
const sshDest = new SSHDestination(sshHostname, wsData.id);
98-
99-
// TODO: remove this, should not be needed
100-
await this.context.globalState.update(`${SSH_DEST_KEY}${sshDest.toRemoteSSHString()}`, { workspaceId: wsData.id, gitpodHost: this.hostService.gitpodHost, instanceId: '' } as SSHConnectionParams);
101-
10297
let wsState = new WorkspaceState(wsData!.id, this.sessionService, this.logService);
10398
try {
10499
await wsState.initialize();
@@ -113,6 +108,8 @@ export class ConnectInNewWindowCommand implements Command {
113108
cancellable: true
114109
},
115110
async (_, cancelToken) => {
111+
await this.initializeLocalSSH(wsData!.id);
112+
116113
if (wsState.isWorkspaceStopped) {
117114
// Start workspace automatically
118115
await this.sessionService.getAPI().startWorkspace(wsData!.id);
@@ -123,18 +120,42 @@ export class ConnectInNewWindowCommand implements Command {
123120
return;
124121
}
125122

126-
await this.initializeLocalSSH(wsData!.id);
127-
128123
await raceCancellationError(eventToPromise(wsState.onWorkspaceRunning), cancelToken);
124+
wsData = wsState.workspaceData; // Update wsData with latest info after workspace is running
125+
}
126+
127+
let sshDest: SSHDestination;
128+
let password: string | undefined;
129+
if (await this.experimentsService.getUseLocalSSHProxy()) {
130+
const domain = getLocalSSHDomain(this.hostService.gitpodHost);
131+
const sshHostname = `${wsData!.id}.${domain}`;
132+
sshDest = new SSHDestination(sshHostname, wsData!.id);
133+
} else {
134+
({ destination: sshDest, password } = await this.remoteService.getWorkspaceSSHDestination(wsData!));
129135
}
130136

137+
if (password) {
138+
try {
139+
await this.remoteService.showSSHPasswordModal(wsData!, password);
140+
} catch {
141+
return;
142+
}
143+
}
144+
145+
// TODO: remove this, should not be needed
146+
await this.context.globalState.update(`${SSH_DEST_KEY}${sshDest.toRemoteSSHString()}`, { workspaceId: wsData!.id, gitpodHost: this.hostService.gitpodHost, instanceId: '' } as SSHConnectionParams);
147+
131148
await vscode.commands.executeCommand(
132149
'vscode.openFolder',
133150
vscode.Uri.parse(`vscode-remote://ssh-remote+${sshDest.toRemoteSSHString()}${wsData!.recentFolders[0] || `/workspace/${wsData!.repo}`}`),
134151
{ forceNewWindow: true }
135152
);
136153
}
137154
);
155+
} catch (e) {
156+
this.logService.error(e);
157+
this.telemetryService.sendTelemetryException(new WrapError('Error runnning connectInNewWindow command', e));
158+
throw e;
138159
} finally {
139160
wsState.dispose();
140161
}
@@ -154,7 +175,7 @@ export class ConnectInNewWindowCommand implements Command {
154175
}
155176
} catch (e) {
156177
const openSSHVersion = await getOpenSSHVersion();
157-
this.telemetryService.sendTelemetryException(new WrapError('Local SSH: failed to initialize local SSH', e, 'Unknown'), {
178+
this.telemetryService.sendTelemetryException(new WrapError('Local SSH: failed to initialize local SSH', e), {
158179
gitpodHost: this.hostService.gitpodHost,
159180
openSSHVersion,
160181
workspaceId
@@ -173,10 +194,11 @@ export class ConnectInNewWindowCommandContext extends ConnectInNewWindowCommand
173194
remoteService: IRemoteService,
174195
sessionService: ISessionService,
175196
hostService: IHostService,
197+
experimentsService: IExperimentsService,
176198
telemetryService: ITelemetryService,
177199
logService: ILogService,
178200
) {
179-
super(context, remoteService, sessionService, hostService, telemetryService, logService);
201+
super(context, remoteService, sessionService, hostService, experimentsService, telemetryService, logService);
180202
}
181203
}
182204

@@ -190,6 +212,7 @@ export class ConnectInCurrentWindowCommand implements Command {
190212
private readonly remoteService: IRemoteService,
191213
private readonly sessionService: ISessionService,
192214
private readonly hostService: IHostService,
215+
private readonly experimentsService: IExperimentsService,
193216
private readonly telemetryService: ITelemetryService,
194217
private readonly logService: ILogService,
195218
) { }
@@ -226,13 +249,6 @@ export class ConnectInCurrentWindowCommand implements Command {
226249
location: getCommandLocation(this.id, treeItem)
227250
});
228251

229-
const domain = getLocalSSHDomain(this.hostService.gitpodHost);
230-
const sshHostname = `${wsData.id}.${domain}`;
231-
const sshDest = new SSHDestination(sshHostname, wsData.id);
232-
233-
// TODO: remove this, should not be needed
234-
await this.context.globalState.update(`${SSH_DEST_KEY}${sshDest.toRemoteSSHString()}`, { workspaceId: wsData.id, gitpodHost: this.hostService.gitpodHost, instanceId: '' } as SSHConnectionParams);
235-
236252
let wsState = new WorkspaceState(wsData!.id, this.sessionService, this.logService);
237253
try {
238254
await wsState.initialize();
@@ -247,6 +263,8 @@ export class ConnectInCurrentWindowCommand implements Command {
247263
cancellable: true
248264
},
249265
async (_, cancelToken) => {
266+
await this.initializeLocalSSH(wsData!.id);
267+
250268
if (wsState.isWorkspaceStopped) {
251269
// Start workspace automatically
252270
await this.sessionService.getAPI().startWorkspace(wsData!.id);
@@ -257,18 +275,42 @@ export class ConnectInCurrentWindowCommand implements Command {
257275
return;
258276
}
259277

260-
await this.initializeLocalSSH(wsData!.id);
261-
262278
await raceCancellationError(eventToPromise(wsState.onWorkspaceRunning), cancelToken);
279+
wsData = wsState.workspaceData; // Update wsData with latest info after workspace is running
280+
}
281+
282+
let sshDest: SSHDestination;
283+
let password: string | undefined;
284+
if (await this.experimentsService.getUseLocalSSHProxy()) {
285+
const domain = getLocalSSHDomain(this.hostService.gitpodHost);
286+
const sshHostname = `${wsData!.id}.${domain}`;
287+
sshDest = new SSHDestination(sshHostname, wsData!.id);
288+
} else {
289+
({ destination: sshDest, password } = await this.remoteService.getWorkspaceSSHDestination(wsData!));
263290
}
264291

292+
if (password) {
293+
try {
294+
await this.remoteService.showSSHPasswordModal(wsData!, password);
295+
} catch {
296+
return;
297+
}
298+
}
299+
300+
// TODO: remove this, should not be needed
301+
await this.context.globalState.update(`${SSH_DEST_KEY}${sshDest.toRemoteSSHString()}`, { workspaceId: wsData!.id, gitpodHost: this.hostService.gitpodHost, instanceId: '' } as SSHConnectionParams);
302+
265303
await vscode.commands.executeCommand(
266304
'vscode.openFolder',
267305
vscode.Uri.parse(`vscode-remote://ssh-remote+${sshDest.toRemoteSSHString()}${wsData!.recentFolders[0] || `/workspace/${wsData!.repo}`}`),
268306
{ forceNewWindow: false }
269307
);
270308
}
271309
);
310+
} catch (e) {
311+
this.logService.error(e);
312+
this.telemetryService.sendTelemetryException(new WrapError('Error runnning connectInCurrentWindow command', e));
313+
throw e;
272314
} finally {
273315
wsState.dispose();
274316
}
@@ -288,7 +330,7 @@ export class ConnectInCurrentWindowCommand implements Command {
288330
}
289331
} catch (e) {
290332
const openSSHVersion = await getOpenSSHVersion();
291-
this.telemetryService.sendTelemetryException(new WrapError('Local SSH: failed to initialize local SSH', e, 'Unknown'), {
333+
this.telemetryService.sendTelemetryException(new WrapError('Local SSH: failed to initialize local SSH', e), {
292334
gitpodHost: this.hostService.gitpodHost,
293335
openSSHVersion,
294336
workspaceId
@@ -307,10 +349,11 @@ export class ConnectInCurrentWindowCommandContext extends ConnectInCurrentWindow
307349
remoteService: IRemoteService,
308350
sessionService: ISessionService,
309351
hostService: IHostService,
352+
experimentsService: IExperimentsService,
310353
telemetryService: ITelemetryService,
311354
logService: ILogService,
312355
) {
313-
super(context, remoteService, sessionService, hostService, telemetryService, logService);
356+
super(context, remoteService, sessionService, hostService, experimentsService, telemetryService, logService);
314357
}
315358
}
316359

@@ -322,10 +365,11 @@ export class ConnectInCurrentWindowCommandContext_1 extends ConnectInCurrentWind
322365
remoteService: IRemoteService,
323366
sessionService: ISessionService,
324367
hostService: IHostService,
368+
experimentsService: IExperimentsService,
325369
telemetryService: ITelemetryService,
326370
logService: ILogService,
327371
) {
328-
super(context, remoteService, sessionService, hostService, telemetryService, logService);
372+
super(context, remoteService, sessionService, hostService, experimentsService, telemetryService, logService);
329373
}
330374
}
331375

@@ -337,10 +381,11 @@ export class ConnectInCurrentWindowCommandInline extends ConnectInCurrentWindowC
337381
remoteService: IRemoteService,
338382
sessionService: ISessionService,
339383
hostService: IHostService,
384+
experimentsService: IExperimentsService,
340385
telemetryService: ITelemetryService,
341386
logService: ILogService,
342387
) {
343-
super(context, remoteService, sessionService, hostService, telemetryService, logService);
388+
super(context, remoteService, sessionService, hostService, experimentsService, telemetryService, logService);
344389
}
345390
}
346391

@@ -352,10 +397,11 @@ export class ConnectInCurrentWindowCommandInline_1 extends ConnectInCurrentWindo
352397
remoteService: IRemoteService,
353398
sessionService: ISessionService,
354399
hostService: IHostService,
400+
experimentsService: IExperimentsService,
355401
telemetryService: ITelemetryService,
356402
logService: ILogService,
357403
) {
358-
super(context, remoteService, sessionService, hostService, telemetryService, logService);
404+
super(context, remoteService, sessionService, hostService, experimentsService, telemetryService, logService);
359405
}
360406
}
361407

src/common/metrics.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,12 @@ import fetch from 'node-fetch-commonjs';
99

1010
const metricsHostMap = new Map<string, string>();
1111

12-
export async function addCounter(gitpodHost: string | undefined, name: string, labels: Record<string, string>, value: number, logService: ILogService) {
12+
export async function addCounter(gitpodHost: string, name: string, labels: Record<string, string>, value: number, logService: ILogService) {
1313
const data = {
1414
name,
1515
labels,
1616
value,
1717
};
18-
if (!gitpodHost) {
19-
logService.error('Missing \'gitpodHost\' in metrics add counter');
20-
return;
21-
}
2218
if (!isBuiltFromGHA) {
2319
logService.trace('Local metrics add counter', data);
2420
return;
@@ -41,18 +37,14 @@ export async function addCounter(gitpodHost: string | undefined, name: string, l
4137
}
4238
}
4339

44-
export async function addHistogram(gitpodHost: string | undefined, name: string, labels: Record<string, string>, count: number, sum: number, buckets: number[], logService: ILogService) {
40+
export async function addHistogram(gitpodHost: string, name: string, labels: Record<string, string>, count: number, sum: number, buckets: number[], logService: ILogService) {
4541
const data = {
4642
name,
4743
labels,
4844
count,
4945
sum,
5046
buckets,
5147
};
52-
if (!gitpodHost) {
53-
logService.error('Missing \'gitpodHost\' in metrics add histogram');
54-
return;
55-
}
5648
if (!isBuiltFromGHA) {
5749
logService.trace('Local metrics add histogram', data);
5850
return;

src/experiments.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ const EXPERIMENTAL_SETTINGS = [
1717
// 'gitpod.remote.useLocalSSHServer',
1818
];
1919

20-
export class ExperimentalSettings extends Disposable {
20+
export interface IExperimentsService {
21+
getUseLocalSSHProxy(): Promise<boolean>;
22+
getUsePublicAPI(gitpodHost: string): Promise<boolean>
23+
}
24+
25+
export class ExperimentalSettings extends Disposable implements IExperimentsService {
2126
private configcatClient: configcatcommon.IConfigCatClient;
2227
private extensionVersion: semver.SemVer;
2328

src/extension.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export async function activate(context: vscode.ExtensionContext) {
8383
const sessionService = new SessionService(hostService, logger, telemetryService);
8484
context.subscriptions.push(sessionService);
8585

86-
const remoteService = new RemoteService(context, hostService, telemetryService, sessionService, logger);
86+
const remoteService = new RemoteService(context, hostService, sessionService, notificationService, telemetryService, logger);
8787
context.subscriptions.push(remoteService);
8888

8989
const experiments = new ExperimentalSettings(packageJSON.configcatKey, packageJSON.version, context, sessionService, hostService, logger);
@@ -109,7 +109,7 @@ export async function activate(context: vscode.ExtensionContext) {
109109
remoteConnectionInfo = getGitpodRemoteWindowConnectionInfo(context);
110110
vscode.commands.executeCommand('setContext', 'gitpod.remoteConnection', !!remoteConnectionInfo);
111111

112-
const workspacesExplorerView = new WorkspacesExplorerView(context, commandManager, remoteService, sessionService, hostService, telemetryService, logger);
112+
const workspacesExplorerView = new WorkspacesExplorerView(context, commandManager, remoteService, sessionService, hostService, experiments, telemetryService, logger);
113113
context.subscriptions.push(workspacesExplorerView);
114114

115115
if (remoteConnectionInfo) {

src/publicApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ export function rawWorkspaceToWorkspaceData(rawWorkspaces: Workspace | Workspace
326326
workspaceUrl: ws.status!.instance!.status!.url,
327327
phase: WorkspaceInstanceStatus_Phase[ws.status!.instance!.status!.phase ?? WorkspaceInstanceStatus_Phase.UNSPECIFIED].toLowerCase() as WorkspacePhase,
328328
description: ws.description,
329-
lastUsed: ws.status!.instance!.createdAt?.toDate(),
329+
lastUsed: ws.status!.instance!.createdAt!.toDate(),
330330
recentFolders: ws.status!.instance!.status!.recentFolders
331331
};
332332
};

src/remoteSession.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Disposable } from './common/dispose';
1010
import { HeartbeatManager } from './heartbeat';
1111
import { WorkspaceState } from './workspaceState';
1212
import { ISyncExtension, NoSettingsSyncSession, NoSyncStoreError, SettingsSync, SyncResource, parseSyncData } from './settingsSync';
13-
import { ExperimentalSettings } from './experiments';
13+
import { IExperimentsService } from './experiments';
1414
import { ITelemetryService, UserFlowTelemetryProperties } from './common/telemetry';
1515
import { INotificationService } from './services/notificationService';
1616
import { retry } from './common/async';
@@ -36,7 +36,7 @@ export class RemoteSession extends Disposable {
3636
private readonly hostService: IHostService,
3737
private readonly sessionService: ISessionService,
3838
private readonly settingsSync: SettingsSync,
39-
private readonly experiments: ExperimentalSettings,
39+
private readonly experiments: IExperimentsService,
4040
private readonly logService: ILogService,
4141
private readonly telemetryService: ITelemetryService,
4242
private readonly notificationService: INotificationService
@@ -78,7 +78,7 @@ export class RemoteSession extends Disposable {
7878
this.workspaceState = new WorkspaceState(this.connectionInfo.workspaceId, this.sessionService, this.logService);
7979
await this.workspaceState.initialize();
8080
if (!this.workspaceState.instanceId || !this.workspaceState.isWorkspaceRunning) {
81-
throw new NoRunningInstanceError(this.connectionInfo.workspaceId, this.workspaceState.phase);
81+
throw new NoRunningInstanceError(this.connectionInfo.workspaceId, this.workspaceState.workspaceData.phase);
8282
}
8383

8484
this._register(this.workspaceState.onWorkspaceWillStop(async () => {

src/services/localSSHMetrics.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class LocalSSHMetricsReporter {
2020
.catch(e => this.logService.error('Error while reporting metrics', e));
2121
}
2222

23-
reportPingExtensionStatus(gitpodHost: string | undefined, status: 'success' | 'failure') {
23+
reportPingExtensionStatus(gitpodHost: string, status: 'success' | 'failure') {
2424
addCounter(gitpodHost, 'vscode_desktop_ping_extension_server_total', { status }, 1, this.logService)
2525
.catch(e => this.logService.error('Error while reporting metrics', e));
2626
}

0 commit comments

Comments
 (0)