Skip to content

Commit e0406d5

Browse files
committed
Scope CLI and CLI config by label
Every time you log in or out we would add or remove the cli and its config in a single directory, meaning logging into another deployment would wipe out the binary and config for the previous one. Now, similar to the ssh config blocks, the cli and its config files are scoped by their label, which is derived from the deployment URL, so they can coexist.
1 parent 7cf4ad2 commit e0406d5

File tree

4 files changed

+91
-38
lines changed

4 files changed

+91
-38
lines changed

src/commands.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,13 @@ export class Commands {
154154
this.restClient.setHost(url)
155155
this.restClient.setSessionToken(token)
156156

157-
// Store these to be used in later sessions and in the cli.
157+
// Store these to be used in later sessions.
158158
await this.storage.setURL(url)
159159
await this.storage.setSessionToken(token)
160160

161+
// Store on disk to be used by the cli.
162+
await this.storage.configureCli(toSafeHost(url), url, token)
163+
161164
await vscode.commands.executeCommand("setContext", "coder.authenticated", true)
162165
if (user.roles.find((role) => role.name === "owner")) {
163166
await vscode.commands.executeCommand("setContext", "coder.isOwner", true)
@@ -198,6 +201,12 @@ export class Commands {
198201
* Log out from the currently logged-in deployment.
199202
*/
200203
public async logout(): Promise<void> {
204+
const url = this.storage.getUrl()
205+
if (!url) {
206+
// Sanity check; command should not be available if no url.
207+
throw new Error("You are not logged in")
208+
}
209+
201210
// Clear from the REST client. An empty url will indicate to other parts of
202211
// the code that we are logged out.
203212
this.restClient.setHost("")
@@ -207,6 +216,9 @@ export class Commands {
207216
await this.storage.setURL(undefined)
208217
await this.storage.setSessionToken(undefined)
209218

219+
// Clear from disk.
220+
await this.storage.configureCli(toSafeHost(url), undefined, undefined)
221+
210222
await vscode.commands.executeCommand("setContext", "coder.authenticated", false)
211223
vscode.window.showInformationMessage("You've been logged out of Coder!", "Login").then((action) => {
212224
if (action === "Login") {

src/extension.ts

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Commands } from "./commands"
88
import { CertificateError, getErrorDetail } from "./error"
99
import { Remote } from "./remote"
1010
import { Storage } from "./storage"
11+
import { toSafeHost } from "./util"
1112
import { WorkspaceQuery, WorkspaceProvider } from "./workspacesProvider"
1213

1314
export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
@@ -108,6 +109,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
108109
// hit enter and move on.
109110
const url = await commands.maybeAskUrl(params.get("url"), storage.getUrl())
110111
if (url) {
112+
restClient.setHost(url)
111113
await storage.setURL(url)
112114
} else {
113115
throw new Error("url must be provided or specified as a query parameter")
@@ -117,9 +119,13 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
117119
// prompted to sign in again, so we do not need to ensure it is set.
118120
const token = params.get("token")
119121
if (token) {
122+
restClient.setSessionToken(token)
120123
await storage.setSessionToken(token)
121124
}
122125

126+
// Store on disk to be used by the cli.
127+
await storage.configureCli(toSafeHost(url), url, token)
128+
123129
vscode.commands.executeCommand("coder.open", owner, workspace, agent, folder, openRecent)
124130
}
125131
},

src/remote.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -610,15 +610,15 @@ export class Remote {
610610

611611
let binaryPath: string | undefined
612612
if (this.mode === vscode.ExtensionMode.Production) {
613-
binaryPath = await this.storage.fetchBinary(restClient)
613+
binaryPath = await this.storage.fetchBinary(restClient, label)
614614
} else {
615615
try {
616616
// In development, try to use `/tmp/coder` as the binary path.
617617
// This is useful for debugging with a custom bin!
618618
binaryPath = path.join(os.tmpdir(), "coder")
619619
await fs.stat(binaryPath)
620620
} catch (ex) {
621-
binaryPath = await this.storage.fetchBinary(restClient)
621+
binaryPath = await this.storage.fetchBinary(restClient, label)
622622
}
623623
}
624624

@@ -647,8 +647,8 @@ export class Remote {
647647
Host: hostName,
648648
ProxyCommand: `${escape(binaryPath)}${headerArg} vscodessh --network-info-dir ${escape(
649649
this.storage.getNetworkInfoPath(),
650-
)}${logArg} --session-token-file ${escape(this.storage.getSessionTokenPath())} --url-file ${escape(
651-
this.storage.getURLPath(),
650+
)}${logArg} --session-token-file ${escape(this.storage.getSessionTokenPath(label))} --url-file ${escape(
651+
this.storage.getURLPath(label),
652652
)} %h`,
653653
ConnectTimeout: "0",
654654
StrictHostKeyChecking: "no",

src/storage.ts

+68-33
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Api } from "coder/site/src/api/api"
22
import { createWriteStream } from "fs"
33
import fs from "fs/promises"
4-
import { ensureDir } from "fs-extra"
54
import { IncomingMessage } from "http"
65
import path from "path"
76
import prettyBytes from "pretty-bytes"
@@ -24,14 +23,13 @@ export class Storage {
2423

2524
/**
2625
* Add the URL to the list of recently accessed URLs in global storage, then
27-
* set it as the last used URL and update it on disk for the cli.
26+
* set it as the last used URL.
2827
*
29-
* If the URL is falsey, then remove it as the currently accessed URL and do
30-
* not touch the history.
28+
* If the URL is falsey, then remove it as the last used URL and do not touch
29+
* the history.
3130
*/
3231
public async setURL(url?: string): Promise<void> {
3332
await this.memento.update("url", url)
34-
this.updateUrl(url)
3533
if (url) {
3634
const history = this.withUrlHistory(url)
3735
await this.memento.update("urlHistory", history)
@@ -64,15 +62,13 @@ export class Storage {
6462
}
6563

6664
/**
67-
* Set or unset the last used token and update it on disk for the cli.
65+
* Set or unset the last used token.
6866
*/
6967
public async setSessionToken(sessionToken?: string): Promise<void> {
7068
if (!sessionToken) {
7169
await this.secrets.delete("sessionToken")
72-
this.updateSessionToken(undefined)
7370
} else {
7471
await this.secrets.store("sessionToken", sessionToken)
75-
this.updateSessionToken(sessionToken)
7672
}
7773
}
7874

@@ -109,16 +105,20 @@ export class Storage {
109105
}
110106

111107
/**
112-
* Download and return the path to a working binary using the provided client.
108+
* Download and return the path to a working binary for the deployment with
109+
* the provided label using the provided client. If the label is empty, use
110+
* the old deployment-unaware path instead.
111+
*
113112
* If there is already a working binary and it matches the server version,
114113
* return that, skipping the download. If it does not match but downloads are
115114
* disabled, return whatever we have and log a warning. Otherwise throw if
116115
* unable to download a working binary, whether because of network issues or
117116
* downloads being disabled.
118117
*/
119-
public async fetchBinary(restClient: Api): Promise<string> {
118+
public async fetchBinary(restClient: Api, label: string | string): Promise<string> {
120119
const baseUrl = restClient.getAxiosInstance().defaults.baseURL
121120
this.output.appendLine(`Using deployment URL: ${baseUrl}`)
121+
this.output.appendLine(`Using deployment label: ${label}`)
122122

123123
// Settings can be undefined when set to their defaults (true in this case),
124124
// so explicitly check against false.
@@ -133,7 +133,7 @@ export class Storage {
133133
// Check if there is an existing binary and whether it looks valid. If it
134134
// is valid and matches the server, or if it does not match the server but
135135
// downloads are disabled, we can return early.
136-
const binPath = path.join(this.getBinaryCachePath(), cli.name())
136+
const binPath = path.join(this.getBinaryCachePath(label), cli.name())
137137
this.output.appendLine(`Using binary path: ${binPath}`)
138138
const stat = await cli.stat(binPath)
139139
if (stat === undefined) {
@@ -351,13 +351,21 @@ export class Storage {
351351
}
352352
}
353353

354-
// getBinaryCachePath returns the path where binaries are cached.
355-
// The caller must ensure it exists before use.
356-
public getBinaryCachePath(): string {
354+
/**
355+
* Return the directory for a deployment with the provided label to where its
356+
* binary is cached.
357+
*
358+
* If the label is empty, read the old deployment-unaware config instead.
359+
*
360+
* The caller must ensure this directory exists before use.
361+
*/
362+
public getBinaryCachePath(label: string): string {
357363
const configPath = vscode.workspace.getConfiguration().get("coder.binaryDestination")
358364
return configPath && String(configPath).trim().length > 0
359365
? path.resolve(String(configPath))
360-
: path.join(this.globalStorageUri.fsPath, "bin")
366+
: label
367+
? path.join(this.globalStorageUri.fsPath, label, "bin")
368+
: path.join(this.globalStorageUri.fsPath, "bin")
361369
}
362370

363371
// getNetworkInfoPath returns the path where network information
@@ -377,17 +385,31 @@ export class Storage {
377385
}
378386

379387
/**
380-
* Return the path to the session token file for the cli.
388+
* Return the directory for the deployment with the provided label to where
389+
* its session token is stored.
390+
*
391+
* If the label is empty, read the old deployment-unaware config instead.
392+
*
393+
* The caller must ensure this directory exists before use.
381394
*/
382-
public getSessionTokenPath(): string {
383-
return path.join(this.globalStorageUri.fsPath, "session_token")
395+
public getSessionTokenPath(label: string): string {
396+
return label
397+
? path.join(this.globalStorageUri.fsPath, label, "session_token")
398+
: path.join(this.globalStorageUri.fsPath, "session_token")
384399
}
385400

386401
/**
387-
* Return the path to the URL file for the cli.
402+
* Return the directory for the deployment with the provided label to where
403+
* its url is stored.
404+
*
405+
* If the label is empty, read the old deployment-unaware config instead.
406+
*
407+
* The caller must ensure this directory exists before use.
388408
*/
389-
public getURLPath(): string {
390-
return path.join(this.globalStorageUri.fsPath, "url")
409+
public getURLPath(label: string): string {
410+
return label
411+
? path.join(this.globalStorageUri.fsPath, label, "url")
412+
: path.join(this.globalStorageUri.fsPath, "url")
391413
}
392414

393415
public writeToCoderOutputChannel(message: string) {
@@ -399,28 +421,41 @@ export class Storage {
399421
}
400422

401423
/**
402-
* Update or remove the URL on disk which can be used by the CLI via
403-
* --url-file.
424+
* Configure the CLI for the deployment with the provided label.
404425
*/
405-
private async updateUrl(url: string | undefined): Promise<void> {
426+
public async configureCli(label: string, url: string | undefined, token: string | undefined | null) {
427+
await Promise.all([this.updateUrlForCli(label, url), this.updateTokenForCli(label, token)])
428+
}
429+
430+
/**
431+
* Update or remove the URL for the deployment with the provided label on disk
432+
* which can be used by the CLI via --url-file.
433+
*
434+
* If the label is empty, read the old deployment-unaware config instead.
435+
*/
436+
private async updateUrlForCli(label: string, url: string | undefined): Promise<void> {
437+
const urlPath = this.getURLPath(label)
406438
if (url) {
407-
await ensureDir(this.globalStorageUri.fsPath)
408-
await fs.writeFile(this.getURLPath(), url)
439+
await fs.mkdir(path.dirname(urlPath), { recursive: true })
440+
await fs.writeFile(urlPath, url)
409441
} else {
410-
await fs.rm(this.getURLPath(), { force: true })
442+
await fs.rm(urlPath, { force: true })
411443
}
412444
}
413445

414446
/**
415-
* Update or remove the session token on disk which can be used by the CLI
416-
* via --session-token-file.
447+
* Update or remove the session token for a deployment with the provided label
448+
* on disk which can be used by the CLI via --session-token-file.
449+
*
450+
* If the label is empty, read the old deployment-unaware config instead.
417451
*/
418-
private async updateSessionToken(token: string | undefined) {
452+
private async updateTokenForCli(label: string, token: string | undefined | null) {
453+
const tokenPath = this.getSessionTokenPath(label)
419454
if (token) {
420-
await ensureDir(this.globalStorageUri.fsPath)
421-
await fs.writeFile(this.getSessionTokenPath(), token)
455+
await fs.mkdir(path.dirname(tokenPath), { recursive: true })
456+
await fs.writeFile(tokenPath, token)
422457
} else {
423-
await fs.rm(this.getSessionTokenPath(), { force: true })
458+
await fs.rm(tokenPath, { force: true })
424459
}
425460
}
426461

0 commit comments

Comments
 (0)