Skip to content
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: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@
"type": "boolean",
"default": false
},
"coder.disableNotifications": {
"markdownDescription": "Disable all notification prompts from the Coder deployment (workspace updates, scheduling reminders, resource alerts, etc.). Notifications are delivered by your Coder server and displayed by this extension.",
"type": "boolean",
"default": false
},
"coder.disableUpdateNotifications": {
"markdownDescription": "Disable notifications when workspace template updates are available.",
"type": "boolean",
Expand Down
3 changes: 2 additions & 1 deletion src/api/coderApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { watchConfigurationChanges } from "../configWatcher";
import { ClientCertificateError } from "../error/clientCertificateError";
import { toError } from "../error/errorUtils";
import { ServerCertificateError } from "../error/serverCertificateError";
import { getHeaderCommand, getHeaders } from "../headers";
import { getHeaders } from "../headers";
import { EventStreamLogger } from "../logging/eventStreamLogger";
import {
createRequestMeta,
Expand All @@ -35,6 +35,7 @@ import {
HttpClientLogLevel,
} from "../logging/types";
import { sizeOf } from "../logging/utils";
import { getHeaderCommand } from "../settings/headers";
import { HttpStatusCode, WebSocketCloseCode } from "../websocket/codes";
import {
type UnidirectionalStream,
Expand Down
2 changes: 1 addition & 1 deletion src/api/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
import { spawn } from "node:child_process";
import * as vscode from "vscode";

import { type CliAuth, getGlobalFlags } from "../cliConfig";
import { type FeatureSet } from "../featureSet";
import { type CliAuth, getGlobalFlags } from "../settings/cli";
import { escapeCommandArg } from "../util";
import { type UnidirectionalStream } from "../websocket/eventStreamConnection";

Expand Down
2 changes: 1 addition & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import * as vscode from "vscode";

import { createWorkspaceIdentifier, extractAgents } from "./api/api-helper";
import { type CoderApi } from "./api/coderApi";
import { getGlobalFlags, resolveCliAuth } from "./cliConfig";
import { type CliManager } from "./core/cliManager";
import * as cliUtils from "./core/cliUtils";
import { type ServiceContainer } from "./core/container";
Expand All @@ -28,6 +27,7 @@ import {
RECOMMENDED_SSH_SETTINGS,
applySettingOverrides,
} from "./remote/userSettings";
import { getGlobalFlags, resolveCliAuth } from "./settings/cli";
import { escapeCommandArg, toRemoteAuthority, toSafeHost } from "./util";
import { vscodeProposed } from "./vscodeProposed";
import {
Expand Down
4 changes: 2 additions & 2 deletions src/core/cliCredentialManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import path from "node:path";
import { promisify } from "node:util";
import * as semver from "semver";

import { isKeyringEnabled } from "../cliConfig";
import { isAbortError } from "../error/errorUtils";
import { featureSetForVersion } from "../featureSet";
import { getHeaderArgs } from "../headers";
import { isKeyringEnabled } from "../settings/cli";
import { getHeaderArgs } from "../settings/headers";
import { renameWithRetry, tempFilePath, toSafeHost } from "../util";

import * as cliUtils from "./cliUtils";
Expand Down
2 changes: 1 addition & 1 deletion src/core/cliManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import * as semver from "semver";
import * as vscode from "vscode";

import { errToStr } from "../api/api-helper";
import { isKeyringEnabled } from "../cliConfig";
import * as pgp from "../pgp";
import { withCancellableProgress, withOptionalProgress } from "../progress";
import { isKeyringEnabled } from "../settings/cli";
import { tempFilePath, toSafeHost } from "../util";
import { vscodeProposed } from "../vscodeProposed";

Expand Down
42 changes: 3 additions & 39 deletions src/headers.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,10 @@
import * as os from "node:os";

import { execCommand } from "./command/exec";
import { type Logger } from "./logging/logger";
import { escapeCommandArg } from "./util";

import type { WorkspaceConfiguration } from "vscode";

export function getHeaderCommand(
config: Pick<WorkspaceConfiguration, "get">,
): string | undefined {
const cmd =
config.get<string>("coder.headerCommand")?.trim() ||
process.env.CODER_HEADER_COMMAND?.trim();

return cmd || undefined;
}

export function getHeaderArgs(
config: Pick<WorkspaceConfiguration, "get">,
): string[] {
// Escape a command line to be executed by the Coder binary, so ssh doesn't substitute variables.
const escapeSubcommand: (str: string) => string =
os.platform() === "win32"
? // On Windows variables are %VAR%, and we need to use double quotes.
(str) => escapeCommandArg(str).replace(/%/g, "%%")
: // On *nix we can use single quotes to escape $VARS.
// Note single quotes cannot be escaped inside single quotes.
(str) => `'${str.replace(/'/g, "'\\''")}'`;

const command = getHeaderCommand(config);
if (!command) {
return [];
}
return ["--header-command", escapeSubcommand(command)];
}

/**
* getHeaders executes the header command and parses the headers from stdout.
* Both stdout and stderr are logged on error but stderr is otherwise ignored.
* Throws an error if the process exits with non-zero or the JSON is invalid.
* Returns undefined if there is no header command set. No effort is made to
* validate the JSON other than making sure it can be parsed.
* Executes the header command and parses headers from stdout.
* Throws on non-zero exit or malformed output. Returns empty headers if no
* command is set.
*/
export async function getHeaders(
url: string | undefined,
Expand Down
6 changes: 5 additions & 1 deletion src/inbox.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as vscode from "vscode";

import { areNotificationsDisabled } from "./settings/notifications";

import type {
Workspace,
GetInboxNotificationResponse,
Expand Down Expand Up @@ -53,7 +55,9 @@ export class Inbox implements vscode.Disposable {
socket.addEventListener("message", (data) => {
if (data.parseError) {
logger.error("Failed to parse inbox message", data.parseError);
} else {
} else if (
!areNotificationsDisabled(vscode.workspace.getConfiguration())
) {
vscode.window.showInformationMessage(
data.parsedMessage.notification.title,
);
Expand Down
2 changes: 1 addition & 1 deletion src/login/loginCoordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import * as vscode from "vscode";

import { CoderApi } from "../api/coderApi";
import { needToken } from "../api/utils";
import { isKeyringEnabled } from "../cliConfig";
import { CertificateError } from "../error/certificateError";
import { OAuthAuthorizer } from "../oauth/authorizer";
import { buildOAuthTokenData } from "../oauth/utils";
import { withOptionalProgress } from "../progress";
import { maybeAskAuthMethod, maybeAskUrl } from "../promptUtils";
import { isKeyringEnabled } from "../settings/cli";
import { vscodeProposed } from "../vscodeProposed";

import type { User } from "coder/site/src/api/typesGenerated";
Expand Down
16 changes: 8 additions & 8 deletions src/remote/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,6 @@ import { extractAgents } from "../api/api-helper";
import { AuthInterceptor } from "../api/authInterceptor";
import { CoderApi } from "../api/coderApi";
import { needToken } from "../api/utils";
import {
type CliAuth,
getGlobalFlags,
getGlobalFlagsRaw,
getSshFlags,
resolveCliAuth,
} from "../cliConfig";
import { type Commands } from "../commands";
import { watchConfigurationChanges } from "../configWatcher";
import { type CliManager } from "../core/cliManager";
Expand All @@ -37,11 +30,18 @@ import { type PathResolver } from "../core/pathResolver";
import { type SecretsManager } from "../core/secretsManager";
import { toError } from "../error/errorUtils";
import { featureSetForVersion, type FeatureSet } from "../featureSet";
import { getHeaderCommand } from "../headers";
import { Inbox } from "../inbox";
import { type Logger } from "../logging/logger";
import { type LoginCoordinator } from "../login/loginCoordinator";
import { OAuthSessionManager } from "../oauth/sessionManager";
import {
type CliAuth,
getGlobalFlags,
getGlobalFlagsRaw,
getSshFlags,
resolveCliAuth,
} from "../settings/cli";
import { getHeaderCommand } from "../settings/headers";
import {
AuthorityPrefix,
escapeCommandArg,
Expand Down
2 changes: 1 addition & 1 deletion src/remote/workspaceStateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import type {
import type * as vscode from "vscode";

import type { CoderApi } from "../api/coderApi";
import type { CliAuth } from "../cliConfig";
import type { FeatureSet } from "../featureSet";
import type { Logger } from "../logging/logger";
import type { CliAuth } from "../settings/cli";

/**
* Manages workspace and agent state transitions until ready for SSH connection.
Expand Down
7 changes: 4 additions & 3 deletions src/cliConfig.ts → src/settings/cli.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { isKeyringSupported } from "./core/cliCredentialManager";
import { isKeyringSupported } from "../core/cliCredentialManager";
import { escapeCommandArg } from "../util";

import { getHeaderArgs } from "./headers";
import { escapeCommandArg } from "./util";

import type { WorkspaceConfiguration } from "vscode";

import type { FeatureSet } from "./featureSet";
import type { FeatureSet } from "../featureSet";

export type CliAuth =
| { mode: "global-config"; configDir: string }
Expand Down
36 changes: 36 additions & 0 deletions src/settings/headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as os from "node:os";

import { escapeCommandArg } from "../util";

import type { WorkspaceConfiguration } from "vscode";

/** Returns the header command from settings or the CODER_HEADER_COMMAND env var. */
export function getHeaderCommand(
config: Pick<WorkspaceConfiguration, "get">,
): string | undefined {
const cmd =
config.get<string>("coder.headerCommand")?.trim() ||
process.env.CODER_HEADER_COMMAND?.trim();

return cmd || undefined;
}

/** Returns `--header-command` CLI args, escaped for the current platform. */
export function getHeaderArgs(
config: Pick<WorkspaceConfiguration, "get">,
): string[] {
// Escape a command line to be executed by the Coder binary, so ssh doesn't substitute variables.
const escapeSubcommand: (str: string) => string =
os.platform() === "win32"
? // On Windows variables are %VAR%, and we need to use double quotes.
(str) => escapeCommandArg(str).replace(/%/g, "%%")
: // On *nix we can use single quotes to escape $VARS.
// Note single quotes cannot be escaped inside single quotes.
(str) => `'${str.replace(/'/g, "'\\''")}'`;

const command = getHeaderCommand(config);
if (!command) {
return [];
}
return ["--header-command", escapeSubcommand(command)];
}
18 changes: 18 additions & 0 deletions src/settings/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { WorkspaceConfiguration } from "vscode";

/** Whether all deployment notifications are disabled. */
export function areNotificationsDisabled(
cfg: Pick<WorkspaceConfiguration, "get">,
): boolean {
return cfg.get<boolean>("coder.disableNotifications", false);
}

/** Whether workspace update notifications are disabled (blanket or update-specific). */
export function areUpdateNotificationsDisabled(
cfg: Pick<WorkspaceConfiguration, "get">,
): boolean {
return (
areNotificationsDisabled(cfg) ||
cfg.get<boolean>("coder.disableUpdateNotifications", false)
);
}
30 changes: 19 additions & 11 deletions src/workspace/workspaceMonitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import { formatDistanceToNowStrict } from "date-fns";
import * as vscode from "vscode";

import { createWorkspaceIdentifier, errToStr } from "../api/api-helper";
import { type CoderApi } from "../api/coderApi";
import { type ContextManager } from "../core/contextManager";
import { type Logger } from "../logging/logger";
import {
areNotificationsDisabled,
areUpdateNotificationsDisabled,
} from "../settings/notifications";
import { vscodeProposed } from "../vscodeProposed";
import { type UnidirectionalStream } from "../websocket/eventStreamConnection";

import type { CoderApi } from "../api/coderApi";
import type { ContextManager } from "../core/contextManager";
import type { Logger } from "../logging/logger";
import type { UnidirectionalStream } from "../websocket/eventStreamConnection";

/**
* Monitor a single workspace using a WebSocket for events like shutdown and deletion.
Expand Down Expand Up @@ -130,7 +135,11 @@ export class WorkspaceMonitor implements vscode.Disposable {
}

private maybeNotify(workspace: Workspace) {
this.maybeNotifyOutdated(workspace);
const cfg = vscode.workspace.getConfiguration();
if (areNotificationsDisabled(cfg)) {
return;
}
this.maybeNotifyOutdated(workspace, cfg);
this.maybeNotifyAutostop(workspace);
if (this.completedInitialSetup) {
// This instance might be created before the workspace is running
Expand Down Expand Up @@ -204,13 +213,12 @@ export class WorkspaceMonitor implements vscode.Disposable {
return timeLeft >= 0 && timeLeft <= notifyTime;
}

private maybeNotifyOutdated(workspace: Workspace) {
private maybeNotifyOutdated(
workspace: Workspace,
cfg: Pick<vscode.WorkspaceConfiguration, "get">,
) {
if (!this.notifiedOutdated && workspace.outdated) {
// Check if update notifications are disabled
const disableNotifications = vscode.workspace
.getConfiguration("coder")
.get<boolean>("disableUpdateNotifications", false);
if (disableNotifications) {
if (areUpdateNotificationsDisabled(cfg)) {
return;
}

Expand Down
Loading