Skip to content

Commit 9c992fe

Browse files
committed
refactor: consistent credential API, lightweight binary lookup, quieter logging
Make storeToken resolve its own binary internally via the injected BinaryResolver, matching readToken and deleteToken. Remove the binPath parameter from both storeToken and configure since callers no longer need to supply it. Add locateBinary to CliManager as a cheap stat-only lookup that the container resolver tries before falling back to fetchBinary. Downgrade implementation-detail log messages from info/warn to debug, keeping info for significant events (server version, download start, stored/deleted token).
1 parent 5599ed5 commit 9c992fe

File tree

7 files changed

+227
-258
lines changed

7 files changed

+227
-258
lines changed

src/core/cliCredentialManager.ts

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { execFile } from "node:child_process";
22
import { promisify } from "node:util";
33

4+
import { isKeyringEnabled } from "../cliConfig";
45
import { getHeaderArgs } from "../headers";
56

67
import type { WorkspaceConfiguration } from "vscode";
@@ -23,10 +24,8 @@ export function isKeyringSupported(): boolean {
2324
}
2425

2526
/**
26-
* Delegates credential storage to the Coder CLI to keep the credentials in sync.
27-
*
28-
* For operations that don't have a binary path available (readToken, deleteToken),
29-
* uses the injected BinaryResolver to fetch/locate the CLI binary.
27+
* Delegates credential storage to the Coder CLI. All operations resolve the
28+
* binary via the injected BinaryResolver before invoking it.
3029
*/
3130
export class CliCredentialManager {
3231
constructor(
@@ -35,18 +34,22 @@ export class CliCredentialManager {
3534
) {}
3635

3736
/**
38-
* Store a token by running:
39-
* CODER_SESSION_TOKEN=<token> <bin> login --use-token-as-session <url>
40-
*
41-
* The token is passed via environment variable so it never appears in
42-
* process argument lists.
37+
* Store a token via `coder login --use-token-as-session`.
38+
* Token is passed via CODER_SESSION_TOKEN env var, never in args.
4339
*/
4440
public async storeToken(
45-
binPath: string,
4641
url: string,
4742
token: string,
4843
configs: Pick<WorkspaceConfiguration, "get">,
4944
): Promise<void> {
45+
let binPath: string;
46+
try {
47+
binPath = await this.resolveBinary(url);
48+
} catch (error) {
49+
this.logger.debug("Could not resolve CLI binary for token store:", error);
50+
throw error;
51+
}
52+
5053
const args = [
5154
...getHeaderArgs(configs),
5255
"login",
@@ -59,27 +62,28 @@ export class CliCredentialManager {
5962
});
6063
this.logger.info("Stored token via CLI for", url);
6164
} catch (error) {
62-
this.logger.error("Failed to store token via CLI:", error);
65+
this.logger.debug("Failed to store token via CLI:", error);
6366
throw error;
6467
}
6568
}
6669

6770
/**
68-
* Read a token by running:
69-
* <bin> login token --url <url>
70-
*
71-
* Resolves the CLI binary automatically. Returns trimmed stdout,
72-
* or undefined if the binary can't be resolved or the CLI returns no token.
71+
* Read a token via `coder login token --url`. Returns trimmed stdout,
72+
* or undefined on any failure (resolver, CLI, empty output).
7373
*/
7474
public async readToken(
7575
url: string,
7676
configs: Pick<WorkspaceConfiguration, "get">,
7777
): Promise<string | undefined> {
78+
if (!isKeyringEnabled(configs)) {
79+
return undefined;
80+
}
81+
7882
let binPath: string;
7983
try {
8084
binPath = await this.resolveBinary(url);
8185
} catch (error) {
82-
this.logger.warn("Could not resolve CLI binary for token read:", error);
86+
this.logger.debug("Could not resolve CLI binary for token read:", error);
8387
return undefined;
8488
}
8589

@@ -89,27 +93,30 @@ export class CliCredentialManager {
8993
const token = stdout.trim();
9094
return token || undefined;
9195
} catch (error) {
92-
this.logger.warn("Failed to read token via CLI:", error);
96+
this.logger.debug("Failed to read token via CLI:", error);
9397
return undefined;
9498
}
9599
}
96100

97101
/**
98-
* Delete a token by running:
99-
* <bin> logout --url <url>
100-
*
101-
* Resolves the CLI binary automatically. Best-effort: logs warnings
102-
* on failure but never throws.
102+
* Delete a token via `coder logout --url`. Best-effort: never throws.
103103
*/
104104
async deleteToken(
105105
url: string,
106106
configs: Pick<WorkspaceConfiguration, "get">,
107107
): Promise<void> {
108+
if (!isKeyringEnabled(configs)) {
109+
return;
110+
}
111+
108112
let binPath: string;
109113
try {
110114
binPath = await this.resolveBinary(url);
111115
} catch (error) {
112-
this.logger.warn("Could not resolve CLI binary for token delete:", error);
116+
this.logger.debug(
117+
"Could not resolve CLI binary for token delete:",
118+
error,
119+
);
113120
return;
114121
}
115122

@@ -118,7 +125,7 @@ export class CliCredentialManager {
118125
await execFileAsync(binPath, args);
119126
this.logger.info("Deleted token via CLI for", url);
120127
} catch (error) {
121-
this.logger.warn("Failed to delete token via CLI:", error);
128+
this.logger.debug("Failed to delete token via CLI:", error);
122129
}
123130
}
124131
}

src/core/cliManager.ts

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as semver from "semver";
1010
import * as vscode from "vscode";
1111

1212
import { errToStr } from "../api/api-helper";
13-
import { isKeyringEnabled, shouldUseKeyring } from "../cliConfig";
13+
import { shouldUseKeyring } from "../cliConfig";
1414
import * as pgp from "../pgp";
1515
import { toSafeHost } from "../util";
1616
import { vscodeProposed } from "../vscodeProposed";
@@ -39,6 +39,23 @@ export class CliManager {
3939
this.binaryLock = new BinaryLock(output);
4040
}
4141

42+
/**
43+
* Return the path to a cached CLI binary for a deployment URL.
44+
* Stat check only — no network, no subprocess. Throws if absent.
45+
*/
46+
public async locateBinary(url: string): Promise<string> {
47+
const safeHostname = toSafeHost(url);
48+
const binPath = path.join(
49+
this.pathResolver.getBinaryCachePath(safeHostname),
50+
cliUtils.name(),
51+
);
52+
const stat = await cliUtils.stat(binPath);
53+
if (!stat) {
54+
throw new Error(`No CLI binary found at ${binPath}`);
55+
}
56+
return binPath;
57+
}
58+
4259
/**
4360
* Download and return the path to a working binary for the deployment with
4461
* the provided hostname using the provided client. If the hostname is empty,
@@ -60,7 +77,10 @@ export class CliManager {
6077
// Settings can be undefined when set to their defaults (true in this case),
6178
// so explicitly check against false.
6279
const enableDownloads = cfg.get("enableDownloads") !== false;
63-
this.output.info("Downloads are", enableDownloads ? "enabled" : "disabled");
80+
this.output.debug(
81+
"Downloads are",
82+
enableDownloads ? "enabled" : "disabled",
83+
);
6484

6585
// Get the build info to compare with the existing binary version, if any,
6686
// and to log for debugging.
@@ -80,18 +100,18 @@ export class CliManager {
80100
this.pathResolver.getBinaryCachePath(safeHostname),
81101
cliUtils.name(),
82102
);
83-
this.output.info("Using binary path", binPath);
103+
this.output.debug("Using binary path", binPath);
84104
const stat = await cliUtils.stat(binPath);
85105
if (stat === undefined) {
86106
this.output.info("No existing binary found, starting download");
87107
} else {
88-
this.output.info("Existing binary size is", prettyBytes(stat.size));
108+
this.output.debug("Existing binary size is", prettyBytes(stat.size));
89109
try {
90110
const version = await cliUtils.version(binPath);
91-
this.output.info("Existing binary version is", version);
111+
this.output.debug("Existing binary version is", version);
92112
// If we have the right version we can avoid the request entirely.
93113
if (version === buildInfo.version) {
94-
this.output.info(
114+
this.output.debug(
95115
"Using existing binary since it matches the server version",
96116
);
97117
return binPath;
@@ -130,19 +150,19 @@ export class CliManager {
130150
binPath,
131151
progressLogPath,
132152
);
133-
this.output.info("Acquired download lock");
153+
this.output.debug("Acquired download lock");
134154

135155
// If we waited for another process, re-check if binary is now ready
136156
if (lockResult.waited) {
137157
const latestBuildInfo = await restClient.getBuildInfo();
138-
this.output.info("Got latest server version", latestBuildInfo.version);
158+
this.output.debug("Got latest server version", latestBuildInfo.version);
139159

140160
const recheckAfterWait = await this.checkBinaryVersion(
141161
binPath,
142162
latestBuildInfo.version,
143163
);
144164
if (recheckAfterWait.matches) {
145-
this.output.info(
165+
this.output.debug(
146166
"Using existing binary since it matches the latest server version",
147167
);
148168
return binPath;
@@ -174,7 +194,7 @@ export class CliManager {
174194
} finally {
175195
if (lockResult) {
176196
await lockResult.release();
177-
this.output.info("Released download lock");
197+
this.output.debug("Released download lock");
178198
}
179199
}
180200
}
@@ -721,12 +741,7 @@ export class CliManager {
721741
* Both URL and token are required. Empty tokens are allowed (e.g. mTLS
722742
* authentication) but the URL must be a non-empty string.
723743
*/
724-
public async configure(
725-
url: string,
726-
token: string,
727-
featureSet: FeatureSet,
728-
binPath: string,
729-
) {
744+
public async configure(url: string, token: string, featureSet: FeatureSet) {
730745
if (!url) {
731746
throw new Error("URL is required to configure the CLI");
732747
}
@@ -735,12 +750,7 @@ export class CliManager {
735750
const configs = vscode.workspace.getConfiguration();
736751
if (shouldUseKeyring(configs, featureSet)) {
737752
try {
738-
await this.cliCredentialManager.storeToken(
739-
binPath,
740-
url,
741-
token,
742-
configs,
743-
);
753+
await this.cliCredentialManager.storeToken(url, token, configs);
744754
} catch (error) {
745755
this.output.error("Failed to store token via CLI keyring:", error);
746756
vscode.window
@@ -775,9 +785,7 @@ export class CliManager {
775785
public async clearCredentials(url: string): Promise<void> {
776786
const safeHostname = toSafeHost(url);
777787
const configs = vscode.workspace.getConfiguration();
778-
if (isKeyringEnabled(configs)) {
779-
await this.cliCredentialManager.deleteToken(url, configs);
780-
}
788+
await this.cliCredentialManager.deleteToken(url, configs);
781789

782790
const tokenPath = this.pathResolver.getSessionTokenPath(safeHostname);
783791
const urlPath = this.pathResolver.getUrlPath(safeHostname);

src/core/container.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,12 @@ export class ServiceContainer implements vscode.Disposable {
4040
this.cliCredentialManager = new CliCredentialManager(
4141
this.logger,
4242
async (url) => {
43-
const client = CoderApi.create(url, "", this.logger);
44-
return this.cliManager.fetchBinary(client);
43+
try {
44+
return await this.cliManager.locateBinary(url);
45+
} catch {
46+
const client = CoderApi.create(url, "", this.logger);
47+
return this.cliManager.fetchBinary(client);
48+
}
4549
},
4650
);
4751
this.cliManager = new CliManager(

src/login/loginCoordinator.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,10 @@ export class LoginCoordinator implements vscode.Disposable {
243243
}
244244

245245
// Try keyring token (picks up tokens written by `coder login` in the terminal)
246-
const keyringToken = await this.getCliKeyringToken(deployment);
246+
const keyringToken = await this.cliCredentialManager.readToken(
247+
deployment.url,
248+
vscode.workspace.getConfiguration(),
249+
);
247250
if (
248251
keyringToken &&
249252
keyringToken !== providedToken &&
@@ -302,18 +305,6 @@ export class LoginCoordinator implements vscode.Disposable {
302305
}
303306
}
304307

305-
/**
306-
* Read a token from the CLI keyring.
307-
*/
308-
private async getCliKeyringToken(
309-
deployment: Deployment,
310-
): Promise<string | undefined> {
311-
return this.cliCredentialManager.readToken(
312-
deployment.url,
313-
vscode.workspace.getConfiguration(),
314-
);
315-
}
316-
317308
/**
318309
* Shows auth error via dialog or logs it for autoLogin.
319310
*/

src/remote/remote.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -241,12 +241,7 @@ export class Remote {
241241

242242
// Write token to keyring or file (after CLI version is known)
243243
if (baseUrlRaw && token !== undefined) {
244-
await this.cliManager.configure(
245-
baseUrlRaw,
246-
token,
247-
featureSet,
248-
binaryPath,
249-
);
244+
await this.cliManager.configure(baseUrlRaw, token, featureSet);
250245
}
251246

252247
// Listen for token changes for this deployment
@@ -261,7 +256,6 @@ export class Remote {
261256
auth.url,
262257
auth.token,
263258
featureSet,
264-
binaryPath,
265259
);
266260
this.logger.info(
267261
"Updated CLI config with new token for remote deployment",

0 commit comments

Comments
 (0)