Skip to content

Commit d3c42ee

Browse files
committed
When connecting to a recent workspace, log into that deployment
That way the sidebar displays the workspaces that belong to that same deployment. As a side effect, this will now avoid logging into a different deployment first only to switch over to the one the remote belongs to.
1 parent 3aec497 commit d3c42ee

File tree

2 files changed

+104
-76
lines changed

2 files changed

+104
-76
lines changed

src/extension.ts

Lines changed: 70 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
4040
const storage = new Storage(output, ctx.globalState, ctx.secrets, ctx.globalStorageUri, ctx.logUri)
4141

4242
// This client tracks the current login and will be used through the life of
43-
// the plugin to poll workspaces for the current login.
43+
// the plugin to poll workspaces for the current login, as well as being used
44+
// in commands that operate on the current login.
4445
const url = storage.getUrl()
4546
const restClient = await makeCoderSdk(url || "", await storage.getSessionToken(), storage)
4647

@@ -57,33 +58,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
5758
myWorkspacesProvider.setVisibility(event.visible)
5859
})
5960

60-
if (url) {
61-
restClient
62-
.getAuthenticatedUser()
63-
.then(async (user) => {
64-
if (user && user.roles) {
65-
vscode.commands.executeCommand("setContext", "coder.authenticated", true)
66-
if (user.roles.find((role) => role.name === "owner")) {
67-
await vscode.commands.executeCommand("setContext", "coder.isOwner", true)
68-
}
69-
70-
// Fetch and monitor workspaces, now that we know the client is good.
71-
myWorkspacesProvider.fetchAndRefresh()
72-
allWorkspacesProvider.fetchAndRefresh()
73-
}
74-
})
75-
.catch((error) => {
76-
// This should be a failure to make the request, like the header command
77-
// errored.
78-
vscodeProposed.window.showErrorMessage("Failed to check user authentication: " + error.message)
79-
})
80-
.finally(() => {
81-
vscode.commands.executeCommand("setContext", "coder.loaded", true)
82-
})
83-
} else {
84-
vscode.commands.executeCommand("setContext", "coder.loaded", true)
85-
}
86-
61+
// Handle vscode:// URIs.
8762
vscode.window.registerUriHandler({
8863
handleUri: async (uri) => {
8964
const params = new URLSearchParams(uri.query)
@@ -127,12 +102,15 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
127102
await storage.configureCli(toSafeHost(url), url, token)
128103

129104
vscode.commands.executeCommand("coder.open", owner, workspace, agent, folder, openRecent)
105+
} else {
106+
throw new Error(`Unknown path ${uri.path}`)
130107
}
131108
},
132109
})
133110

111+
// Register globally available commands. Many of these have visibility
112+
// controlled by contexts, see `when` in the package.json.
134113
const commands = new Commands(vscodeProposed, restClient, storage)
135-
136114
vscode.commands.registerCommand("coder.login", commands.login.bind(commands))
137115
vscode.commands.registerCommand("coder.logout", commands.logout.bind(commands))
138116
vscode.commands.registerCommand("coder.open", commands.open.bind(commands))
@@ -153,38 +131,71 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
153131
// Since the "onResolveRemoteAuthority:ssh-remote" activation event exists
154132
// in package.json we're able to perform actions before the authority is
155133
// resolved by the remote SSH extension.
156-
if (!vscodeProposed.env.remoteAuthority) {
157-
return
158-
}
159-
const remote = new Remote(vscodeProposed, storage, commands, ctx.extensionMode)
160-
try {
161-
await remote.setup(vscodeProposed.env.remoteAuthority)
162-
} catch (ex) {
163-
if (ex instanceof CertificateError) {
164-
await ex.showModal("Failed to open workspace")
165-
} else if (isAxiosError(ex)) {
166-
const msg = getErrorMessage(ex, "")
167-
const detail = getErrorDetail(ex)
168-
const urlString = axios.getUri(ex.response?.config)
169-
let path = urlString
170-
try {
171-
path = new URL(urlString).pathname
172-
} catch (e) {
173-
// ignore, default to full url
134+
if (vscodeProposed.env.remoteAuthority) {
135+
const remote = new Remote(vscodeProposed, storage, commands, ctx.extensionMode)
136+
try {
137+
const details = await remote.setup(vscodeProposed.env.remoteAuthority)
138+
if (details) {
139+
// Authenticate the plugin client which is used in the sidebar to display
140+
// workspaces belonging to this deployment.
141+
restClient.setHost(details.url)
142+
restClient.setSessionToken(details.token)
174143
}
175-
await vscodeProposed.window.showErrorMessage("Failed to open workspace", {
176-
detail: `API ${ex.response?.config.method?.toUpperCase()} to '${path}' failed with code ${ex.response?.status}.\nMessage: ${msg}\nDetail: ${detail}`,
177-
modal: true,
178-
useCustom: true,
144+
} catch (ex) {
145+
if (ex instanceof CertificateError) {
146+
await ex.showModal("Failed to open workspace")
147+
} else if (isAxiosError(ex)) {
148+
const msg = getErrorMessage(ex, "")
149+
const detail = getErrorDetail(ex)
150+
const urlString = axios.getUri(ex.response?.config)
151+
let path = urlString
152+
try {
153+
path = new URL(urlString).pathname
154+
} catch (e) {
155+
// ignore, default to full url
156+
}
157+
await vscodeProposed.window.showErrorMessage("Failed to open workspace", {
158+
detail: `API ${ex.response?.config.method?.toUpperCase()} to '${path}' failed with code ${ex.response?.status}.\nMessage: ${msg}\nDetail: ${detail}`,
159+
modal: true,
160+
useCustom: true,
161+
})
162+
} else {
163+
await vscodeProposed.window.showErrorMessage("Failed to open workspace", {
164+
detail: (ex as string).toString(),
165+
modal: true,
166+
useCustom: true,
167+
})
168+
}
169+
// Always close remote session when we fail to open a workspace.
170+
await remote.closeRemote()
171+
}
172+
}
173+
174+
// See if the plugin client is authenticated.
175+
if (restClient.getAxiosInstance().defaults.baseURL) {
176+
restClient
177+
.getAuthenticatedUser()
178+
.then(async (user) => {
179+
if (user && user.roles) {
180+
vscode.commands.executeCommand("setContext", "coder.authenticated", true)
181+
if (user.roles.find((role) => role.name === "owner")) {
182+
await vscode.commands.executeCommand("setContext", "coder.isOwner", true)
183+
}
184+
185+
// Fetch and monitor workspaces, now that we know the client is good.
186+
myWorkspacesProvider.fetchAndRefresh()
187+
allWorkspacesProvider.fetchAndRefresh()
188+
}
179189
})
180-
} else {
181-
await vscodeProposed.window.showErrorMessage("Failed to open workspace", {
182-
detail: (ex as string).toString(),
183-
modal: true,
184-
useCustom: true,
190+
.catch((error) => {
191+
// This should be a failure to make the request, like the header command
192+
// errored.
193+
vscode.window.showErrorMessage("Failed to check user authentication: " + error.message)
185194
})
186-
}
187-
// Always close remote session when we fail to open a workspace.
188-
await remote.closeRemote()
195+
.finally(() => {
196+
vscode.commands.executeCommand("setContext", "coder.loaded", true)
197+
})
198+
} else {
199+
vscode.commands.executeCommand("setContext", "coder.loaded", true)
189200
}
190201
}

src/remote.ts

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ import { AuthorityPrefix, parseRemoteAuthority } from "./util"
2121
import { supportsCoderAgentLogDirFlag } from "./version"
2222
import { WorkspaceAction } from "./workspaceAction"
2323

24+
export interface RemoteDetails extends vscode.Disposable {
25+
url: string
26+
token: string
27+
}
28+
2429
export class Remote {
2530
public constructor(
2631
private readonly vscodeProposed: typeof vscode,
@@ -29,7 +34,12 @@ export class Remote {
2934
private readonly mode: vscode.ExtensionMode,
3035
) {}
3136

32-
public async setup(remoteAuthority: string): Promise<vscode.Disposable | undefined> {
37+
/**
38+
* Ensure the workspace specified by the remote authority is ready to receive
39+
* SSH connections. Return undefined if the authority is not for a Coder
40+
* workspace or when explicitly closing the remote.
41+
*/
42+
public async setup(remoteAuthority: string): Promise<RemoteDetails | undefined> {
3343
const parts = parseRemoteAuthority(remoteAuthority)
3444
if (!parts) {
3545
// Not a Coder host.
@@ -63,16 +73,17 @@ export class Remote {
6373
return
6474
}
6575

66-
// It is possible to connect to any previously connected workspace, which
67-
// might not belong to the deployment the plugin is currently logged into.
68-
// For that reason, create a separate REST client instead of using the
69-
// global one generally used by the plugin.
70-
const restClient = await makeCoderSdk(baseUrlRaw, token, this.storage)
76+
// We could use the plugin client, but it is possible for the user to log
77+
// out or log into a different deployment while still connected, which would
78+
// break this connection. We could force close the remote session or
79+
// disallow logging out/in altogether, but for now just use a separate
80+
// client to remain unaffected by whatever the plugin is doing.
81+
const workspaceRestClient = await makeCoderSdk(baseUrlRaw, token, this.storage)
7182
// Store for use in commands.
72-
this.commands.workspaceRestClient = restClient
83+
this.commands.workspaceRestClient = workspaceRestClient
7384

7485
// First thing is to check the version.
75-
const buildInfo = await restClient.getBuildInfo()
86+
const buildInfo = await workspaceRestClient.getBuildInfo()
7687
const parsedVersion = semver.parse(buildInfo.version)
7788
// Server versions before v0.14.1 don't support the vscodessh command!
7889
if (
@@ -98,7 +109,7 @@ export class Remote {
98109
// Next is to find the workspace from the URI scheme provided.
99110
let workspace: Workspace
100111
try {
101-
workspace = await restClient.getWorkspaceByOwnerAndName(parts.username, parts.workspace)
112+
workspace = await workspaceRestClient.getWorkspaceByOwnerAndName(parts.username, parts.workspace)
102113
this.commands.workspace = workspace
103114
} catch (error) {
104115
if (!isAxiosError(error)) {
@@ -149,7 +160,7 @@ export class Remote {
149160
disposables.push(this.registerLabelFormatter(remoteAuthority, workspace.owner_name, workspace.name))
150161

151162
// Initialize any WorkspaceAction notifications (auto-off, upcoming deletion)
152-
const action = await WorkspaceAction.init(this.vscodeProposed, restClient, this.storage)
163+
const action = await WorkspaceAction.init(this.vscodeProposed, workspaceRestClient, this.storage)
153164

154165
// Make sure the workspace has started.
155166
let buildComplete: undefined | (() => void)
@@ -175,7 +186,7 @@ export class Remote {
175186
}),
176187
)
177188

178-
const latestBuild = await restClient.startWorkspace(workspace.id, versionID)
189+
const latestBuild = await workspaceRestClient.startWorkspace(workspace.id, versionID)
179190
workspace = {
180191
...workspace,
181192
latest_build: latestBuild,
@@ -206,7 +217,7 @@ export class Remote {
206217
} as Partial<vscode.Pseudoterminal> as any,
207218
})
208219
// This fetches the initial bunch of logs.
209-
const logs = await restClient.getWorkspaceBuildLogs(workspace.latest_build.id, new Date())
220+
const logs = await workspaceRestClient.getWorkspaceBuildLogs(workspace.latest_build.id, new Date())
210221
logs.forEach((log) => writeEmitter.fire(log.output + "\r\n"))
211222
terminal.show(true)
212223
// This follows the logs for new activity!
@@ -243,7 +254,7 @@ export class Remote {
243254
}
244255
})
245256
writeEmitter.fire("Build complete")
246-
workspace = await restClient.getWorkspace(workspace.id)
257+
workspace = await workspaceRestClient.getWorkspace(workspace.id)
247258
this.commands.workspace = workspace
248259
terminal.dispose()
249260

@@ -372,10 +383,10 @@ export class Remote {
372383
if (newWorkspace.outdated) {
373384
if (!workspace.outdated || !hasShownOutdatedNotification) {
374385
hasShownOutdatedNotification = true
375-
restClient
386+
workspaceRestClient
376387
.getTemplate(newWorkspace.template_id)
377388
.then((template) => {
378-
return restClient.getTemplateVersion(template.active_version_id)
389+
return workspaceRestClient.getTemplateVersion(template.active_version_id)
379390
})
380391
.then((version) => {
381392
let infoMessage = `A new version of your workspace is available.`
@@ -384,7 +395,7 @@ export class Remote {
384395
}
385396
vscode.window.showInformationMessage(infoMessage, "Update").then((action) => {
386397
if (action === "Update") {
387-
vscode.commands.executeCommand("coder.workspace.update", newWorkspace, restClient)
398+
vscode.commands.executeCommand("coder.workspace.update", newWorkspace, workspaceRestClient)
388399
}
389400
})
390401
})
@@ -498,7 +509,7 @@ export class Remote {
498509
// If we didn't write to the SSH config file, connecting would fail with
499510
// "Host not found".
500511
try {
501-
await this.updateSSHConfig(restClient, parts.label, parts.host, hasCoderLogs)
512+
await this.updateSSHConfig(workspaceRestClient, parts.label, parts.host, hasCoderLogs)
502513
} catch (error) {
503514
this.storage.writeToCoderOutputChannel(`Failed to configure SSH: ${error}`)
504515
throw error
@@ -522,7 +533,13 @@ export class Remote {
522533
}),
523534
)
524535

536+
// Returning the URL and token allows the plugin to authenticate its own
537+
// client, for example to display the list of workspaces belonging to this
538+
// deployment in the sidebar. We use our own client in here for reasons
539+
// explained above.
525540
return {
541+
url: baseUrlRaw,
542+
token,
526543
dispose: () => {
527544
eventSource.close()
528545
action.cleanupWorkspaceActions()

0 commit comments

Comments
 (0)