Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Always use public API #104

Merged
merged 2 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions src/experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const EXPERIMENTAL_SETTINGS: string[] = [

export interface IExperimentsService {
getUseLocalSSHProxy(): Promise<boolean>;
getUsePublicAPI(gitpodHost: string): Promise<boolean>;
}

export class ExperimentalSettings extends Disposable implements IExperimentsService {
Expand Down Expand Up @@ -147,10 +146,6 @@ export class ExperimentalSettings extends Disposable implements IExperimentsServ
async getUseLocalSSHProxy(): Promise<boolean> {
return (await this.getRaw<boolean>('gitpod_desktop_use_local_ssh_proxy', { 'platform': os.platform() })) ?? false;
}

async getUsePublicAPI(gitpodHost: string): Promise<boolean> {
return (await this.getRaw<boolean>('gitpod_experimental_publicApi', { gitpodHost })) ?? false;
}
}

export function isUserOverrideSetting(key: string): boolean {
Expand Down
2 changes: 1 addition & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export async function activate(context: vscode.ExtensionContext) {
// Because auth provider implementation is in the same extension, we need to wait for it to activate first
firstLoadPromise.then(async () => {
if (remoteConnectionInfo) {
remoteSession = new RemoteSession(remoteConnectionInfo.connectionInfo, context, remoteService, hostService, sessionService, experiments, logger!, telemetryService!, notificationService);
remoteSession = new RemoteSession(remoteConnectionInfo.connectionInfo, context, remoteService, hostService, sessionService, logger!, telemetryService!, notificationService);
await remoteSession.initialize();
} else if (sessionService.isSignedIn()) {
remoteService.checkForStoppedWorkspaces(async wsInfo => {
Expand Down
59 changes: 20 additions & 39 deletions src/remoteConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@
*--------------------------------------------------------------------------------------------*/

import { Workspace, WorkspaceInstanceStatus_Phase } from '@gitpod/public-api/lib/gitpod/experimental/v1/workspaces_pb';
import { UserSSHPublicKeyValue, WorkspaceInfo } from '@gitpod/gitpod-protocol';
import * as crypto from 'crypto';
import { utils as sshUtils } from 'ssh2';
import { ParsedKey } from 'ssh2-streams';
import * as vscode from 'vscode';
import { Disposable } from './common/dispose';
import { withServerApi } from './internalApi';
import { ITelemetryService, UserFlowTelemetryProperties } from './common/telemetry';
import { addHostToHostFile, checkNewHostInHostkeys } from './ssh/hostfile';
import { ScopeFeature } from './featureSupport';
Expand All @@ -33,8 +31,6 @@ export class RemoteConnector extends Disposable {

public static AUTH_COMPLETE_PATH = '/auth-complete';

private usePublicApi: boolean = false;

constructor(
private readonly context: vscode.ExtensionContext,
private readonly sessionService: ISessionService,
Expand All @@ -55,27 +51,21 @@ export class RemoteConnector extends Disposable {
private async getWorkspaceSSHDestination({ workspaceId, gitpodHost, debugWorkspace }: SSHConnectionParams): Promise<{ destination: SSHDestination; password?: string }> {
const sshKeysSupported = this.sessionService.getScopes().includes(ScopeFeature.SSHPublicKeys);

const [workspaceInfo, ownerToken, registeredSSHKeys] = await withServerApi(this.sessionService.getGitpodToken(), getServiceURL(gitpodHost), service => Promise.all([
this.usePublicApi ? this.sessionService.getAPI().getWorkspace(workspaceId) : service.server.getWorkspace(workspaceId),
this.usePublicApi ? this.sessionService.getAPI().getOwnerToken(workspaceId) : service.server.getOwnerToken(workspaceId),
sshKeysSupported ? (this.usePublicApi ? this.sessionService.getAPI().getSSHKeys() : service.server.getSSHPublicKeys()) : undefined
]), this.logService);
const [workspaceInfo, ownerToken, registeredSSHKeys] = await Promise.all([
this.sessionService.getAPI().getWorkspace(workspaceId),
this.sessionService.getAPI().getOwnerToken(workspaceId),
sshKeysSupported ? (this.sessionService.getAPI().getSSHKeys()) : undefined
]);

const isNotRunning = this.usePublicApi
? !((workspaceInfo as Workspace)?.status?.instance) || (workspaceInfo as Workspace)?.status?.instance?.status?.phase === WorkspaceInstanceStatus_Phase.STOPPING || (workspaceInfo as Workspace)?.status?.instance?.status?.phase === WorkspaceInstanceStatus_Phase.STOPPED
: !((workspaceInfo as WorkspaceInfo).latestInstance) || (workspaceInfo as WorkspaceInfo).latestInstance?.status?.phase === 'stopping' || (workspaceInfo as WorkspaceInfo).latestInstance?.status?.phase === 'stopped';
const isNotRunning = !((workspaceInfo as Workspace)?.status?.instance) || (workspaceInfo as Workspace)?.status?.instance?.status?.phase === WorkspaceInstanceStatus_Phase.STOPPING || (workspaceInfo as Workspace)?.status?.instance?.status?.phase === WorkspaceInstanceStatus_Phase.STOPPED;
if (isNotRunning) {
throw new NoRunningInstanceError(
workspaceId,
this.usePublicApi
? (workspaceInfo as Workspace)?.status?.instance?.status?.phase ? WorkspaceInstanceStatus_Phase[(workspaceInfo as Workspace)?.status?.instance?.status?.phase!] : undefined
: (workspaceInfo as WorkspaceInfo).latestInstance?.status?.phase
(workspaceInfo as Workspace)?.status?.instance?.status?.phase ? WorkspaceInstanceStatus_Phase[(workspaceInfo as Workspace)?.status?.instance?.status?.phase!] : undefined
);
}

const workspaceUrl = this.usePublicApi
? new URL((workspaceInfo as Workspace).status!.instance!.status!.url)
: new URL((workspaceInfo as WorkspaceInfo).latestInstance!.ideUrl);
const workspaceUrl = new URL((workspaceInfo as Workspace).status!.instance!.status!.url);

const sshHostKeyEndPoint = `https://${workspaceUrl.host}/_ssh/host_keys`;
const sshHostKeyResponse = await fetch(sshHostKeyEndPoint);
Expand Down Expand Up @@ -122,18 +112,16 @@ export class RemoteConnector extends Disposable {
let identityKeys = await gatherIdentityFiles(identityFiles, getAgentSock(hostConfiguration), false, this.logService);

if (registeredSSHKeys) {
const registeredKeys = this.usePublicApi
? (registeredSSHKeys as SSHKey[]).map(k => {
const parsedResult = sshUtils.parseKey(k.key);
if (parsedResult instanceof Error || !parsedResult) {
this.logService.error(`Error while parsing SSH public key ${k.name}:`, parsedResult);
return { name: k.name, fingerprint: '' };
}
const registeredKeys = (registeredSSHKeys as SSHKey[]).map(k => {
const parsedResult = sshUtils.parseKey(k.key);
if (parsedResult instanceof Error || !parsedResult) {
this.logService.error(`Error while parsing SSH public key ${k.name}:`, parsedResult);
return { name: k.name, fingerprint: '' };
}

const parsedKey = parsedResult as ParsedKey;
return { name: k.name, fingerprint: crypto.createHash('sha256').update(parsedKey.getPublicSSH()).digest('base64') };
})
: (registeredSSHKeys as UserSSHPublicKeyValue[]).map(k => ({ name: k.name, fingerprint: k.fingerprint }));
const parsedKey = parsedResult as ParsedKey;
return { name: k.name, fingerprint: crypto.createHash('sha256').update(parsedKey.getPublicSSH()).digest('base64') };
});
this.logService.trace(`Registered public keys in Gitpod account:`, registeredKeys.length ? registeredKeys.map(k => `${k.name} SHA256:${k.fingerprint}`).join('\n') : 'None');

identityKeys = identityKeys.filter(k => !!registeredKeys.find(regKey => regKey.fingerprint === k.fingerprint));
Expand All @@ -151,18 +139,14 @@ export class RemoteConnector extends Disposable {
}

private async getLocalSSHWorkspaceSSHDestination({ workspaceId, gitpodHost, debugWorkspace }: SSHConnectionParams): Promise<{ destination: SSHDestination; password?: string }> {
const workspaceInfo = await withServerApi(this.sessionService.getGitpodToken(), getServiceURL(gitpodHost), async service => this.usePublicApi ? this.sessionService.getAPI().getWorkspace(workspaceId) : service.server.getWorkspace(workspaceId), this.logService);
const workspaceInfo = await this.sessionService.getAPI().getWorkspace(workspaceId);

const isNotRunning = this.usePublicApi
? !((workspaceInfo as Workspace)?.status?.instance) || (workspaceInfo as Workspace)?.status?.instance?.status?.phase === WorkspaceInstanceStatus_Phase.STOPPING || (workspaceInfo as Workspace)?.status?.instance?.status?.phase === WorkspaceInstanceStatus_Phase.STOPPED
: !((workspaceInfo as WorkspaceInfo).latestInstance) || (workspaceInfo as WorkspaceInfo).latestInstance?.status?.phase === 'stopping' || (workspaceInfo as WorkspaceInfo).latestInstance?.status?.phase === 'stopped';
const isNotRunning = !((workspaceInfo as Workspace)?.status?.instance) || (workspaceInfo as Workspace)?.status?.instance?.status?.phase === WorkspaceInstanceStatus_Phase.STOPPING || (workspaceInfo as Workspace)?.status?.instance?.status?.phase === WorkspaceInstanceStatus_Phase.STOPPED;

if (isNotRunning) {
throw new NoRunningInstanceError(
workspaceId,
this.usePublicApi
? (workspaceInfo as Workspace)?.status?.instance?.status?.phase ? WorkspaceInstanceStatus_Phase[(workspaceInfo as Workspace)?.status?.instance?.status?.phase!] : undefined
: (workspaceInfo as WorkspaceInfo).latestInstance?.status?.phase
(workspaceInfo as Workspace)?.status?.instance?.status?.phase ? WorkspaceInstanceStatus_Phase[(workspaceInfo as Workspace)?.status?.instance?.status?.phase!] : undefined,
);
}

Expand Down Expand Up @@ -271,9 +255,6 @@ export class RemoteConnector extends Disposable {
location: vscode.ProgressLocation.Notification
},
async () => {
this.usePublicApi = await this.experiments.getUsePublicAPI(params.gitpodHost);
this.logService.info(`Going to use ${this.usePublicApi ? 'public' : 'server'} API`);

const openSSHVersion = await getOpenSSHVersion();

// Always try to run a local ssh connection collect success metrics
Expand Down
65 changes: 21 additions & 44 deletions src/remoteSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ import { NoRunningInstanceError, SSHConnectionParams, SSH_DEST_KEY, getGitpodRem
import { Disposable } from './common/dispose';
import { HeartbeatManager } from './heartbeat';
import { WorkspaceState } from './workspaceState';
import { IExperimentsService } from './experiments';
import { ITelemetryService, UserFlowTelemetryProperties } from './common/telemetry';
import { INotificationService } from './services/notificationService';
import { withServerApi } from './internalApi';
import { ISessionService } from './services/sessionService';
import { IHostService } from './services/hostService';
import { ILogService } from './services/logService';
Expand All @@ -20,8 +18,6 @@ import { IRemoteService } from './services/remoteService';

export class RemoteSession extends Disposable {

private usePublicApi: boolean = false;

private heartbeatManager: HeartbeatManager | undefined;
private workspaceState: WorkspaceState | undefined;
private extensionServiceServer: ExtensionServiceServer | undefined;
Expand All @@ -32,7 +28,6 @@ export class RemoteSession extends Disposable {
private readonly remoteService: IRemoteService,
private readonly hostService: IHostService,
private readonly sessionService: ISessionService,
private readonly experiments: IExperimentsService,
private readonly logService: ILogService,
private readonly telemetryService: ITelemetryService,
private readonly notificationService: INotificationService
Expand Down Expand Up @@ -63,45 +58,27 @@ export class RemoteSession extends Disposable {
try {
this.remoteService.startLocalSSHServiceServer().catch(() => {/* ignore */ });

this.usePublicApi = await this.experiments.getUsePublicAPI(this.connectionInfo.gitpodHost);
this.logService.info(`Going to use ${this.usePublicApi ? 'public' : 'server'} API`);

if (this.usePublicApi) {
this.workspaceState = new WorkspaceState(this.connectionInfo.workspaceId, this.sessionService, this.logService);
this.workspaceState.initialize()
.then(() => {
if (!this.workspaceState!.instanceId || !this.workspaceState!.isWorkspaceRunning) {
vscode.commands.executeCommand('workbench.action.remote.close');
return;
}
const instanceId = this.workspaceState!.instanceId;
if (instanceId !== this.connectionInfo.instanceId) {
this.logService.info(`Updating workspace ${this.connectionInfo.workspaceId} latest instance id ${this.connectionInfo.instanceId} => ${instanceId}`);
this.connectionInfo.instanceId = instanceId;
}

const { sshDestStr } = getGitpodRemoteWindowConnectionInfo(this.context)!;
this.context.globalState.update(`${SSH_DEST_KEY}${sshDestStr}`, { ...this.connectionInfo } as SSHConnectionParams);
});

this._register(this.workspaceState.onWorkspaceWillStop(async () => {
await this.remoteService.saveRestartInfo();
vscode.commands.executeCommand('workbench.action.remote.close');
}));
} else {
const workspaceInfo = await withServerApi(this.sessionService.getGitpodToken(), this.connectionInfo.gitpodHost, service => service.server.getWorkspace(this.connectionInfo.workspaceId), this.logService);
if (!workspaceInfo.latestInstance || workspaceInfo.latestInstance?.status?.phase === 'stopping' || workspaceInfo.latestInstance?.status?.phase === 'stopped') {
throw new NoRunningInstanceError(this.connectionInfo.workspaceId, workspaceInfo.latestInstance?.status?.phase);
}
const instanceId = workspaceInfo.latestInstance.id;
if (instanceId !== this.connectionInfo.instanceId) {
this.logService.info(`Updating workspace ${this.connectionInfo.workspaceId} latest instance id ${this.connectionInfo.instanceId} => ${instanceId}`);
this.connectionInfo.instanceId = instanceId;
}

const { sshDestStr } = getGitpodRemoteWindowConnectionInfo(this.context)!;
this.context.globalState.update(`${SSH_DEST_KEY}${sshDestStr}`, { ...this.connectionInfo } as SSHConnectionParams);
}
this.workspaceState = new WorkspaceState(this.connectionInfo.workspaceId, this.sessionService, this.logService);
this.workspaceState.initialize()
.then(() => {
if (!this.workspaceState!.instanceId || !this.workspaceState!.isWorkspaceRunning) {
vscode.commands.executeCommand('workbench.action.remote.close');
return;
}
const instanceId = this.workspaceState!.instanceId;
if (instanceId !== this.connectionInfo.instanceId) {
this.logService.info(`Updating workspace ${this.connectionInfo.workspaceId} latest instance id ${this.connectionInfo.instanceId} => ${instanceId}`);
this.connectionInfo.instanceId = instanceId;
}

const { sshDestStr } = getGitpodRemoteWindowConnectionInfo(this.context)!;
this.context.globalState.update(`${SSH_DEST_KEY}${sshDestStr}`, { ...this.connectionInfo } as SSHConnectionParams);
});

this._register(this.workspaceState.onWorkspaceWillStop(async () => {
await this.remoteService.saveRestartInfo();
vscode.commands.executeCommand('workbench.action.remote.close');
}));

this.heartbeatManager = new HeartbeatManager(this.connectionInfo, this.workspaceState, this.sessionService, this.logService, this.telemetryService);

Expand Down
Loading