Skip to content

Better support for server definitions defined at the workspace folder level #1628

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

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,10 @@
{
"command": "vscode-objectscript.openISCDocument",
"when": "vscode-objectscript.connectActive && workspaceFolderCount != 0"
},
{
"command": "vscode-objectscript.connectFolderToServerNamespace",
"when": "!vscode-objectscript.connectActive && vscode-objectscript.explorerRootCount == 0 && workspaceFolderCount != 0"
}
],
"view/title": [
Expand Down
25 changes: 24 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,29 @@ export class AtelierAPI {
this.namespace = namespace;
}

/**
* Manually set the connection spec for this object,
* where `connSpec` is the return value of `getResolvedConnectionSpec()`.
*/
public setConnSpec(serverName: string, connSpec: any): void {
const {
webServer: { scheme, host, port, pathPrefix = "" },
username,
password,
} = connSpec;
this._config.username = username;
this._config.password = password;
this._config.https = scheme == "https";
this._config.host = host;
this._config.port = port;
this._config.pathPrefix = pathPrefix;
this._config.apiVersion = this._config.apiVersion || DEFAULT_API_VERSION;
this._config.serverVersion = this._config.serverVersion || DEFAULT_SERVER_VERSION;
this._config.docker = false;
this._config.active = true;
this._config.serverName = serverName;
}

public get active(): boolean {
const { host = "", port = 0 } = this.config;
return !!this._config.active && host.length > 0 && port > 0;
Expand Down Expand Up @@ -185,7 +208,7 @@ export class AtelierAPI {
this.configName = workspaceFolderName;
const conn = config("conn", workspaceFolderName);
let serverName = workspaceFolderName.toLowerCase();
if (config("intersystems.servers").has(serverName)) {
if (config("intersystems.servers", workspaceFolderName).has(serverName)) {
this.externalServer = true;
} else if (
!(conn["docker-compose"] && extensionContext.extension.extensionKind !== vscode.ExtensionKind.Workspace) &&
Expand Down
19 changes: 1 addition & 18 deletions src/commands/addServerNamespaceToWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
FILESYSTEM_SCHEMA,
FILESYSTEM_READONLY_SCHEMA,
filesystemSchemas,
smExtensionId,
serverManagerApi,
} from "../extension";
import { cspAppsForUri, getWsFolder, handleError, notIsfs } from "../utils";
import { pickProject } from "./project";
Expand All @@ -18,7 +18,6 @@ import { isfsConfig, IsfsUriParam } from "../utils/FileProviderUtil";
* @returns An object containing `serverName` and `namespace`, or `undefined`.
*/
export async function pickServerAndNamespace(message?: string): Promise<{ serverName: string; namespace: string }> {
const serverManagerApi = await getServerManagerApi();
if (!serverManagerApi) {
vscode.window.showErrorMessage(
`${
Expand Down Expand Up @@ -168,22 +167,6 @@ export async function addServerNamespaceToWorkspace(resource?: vscode.Uri): Prom
}
}

export async function getServerManagerApi(): Promise<any> {
const targetExtension = vscode.extensions.getExtension(smExtensionId);
if (!targetExtension) {
return undefined;
}
if (!targetExtension.isActive) {
await targetExtension.activate();
}
const api = targetExtension.exports;

if (!api) {
return undefined;
}
return api;
}

/** Prompt the user to fill in the `path` and `query` of `uri`. */
async function modifyWsFolderUri(uri: vscode.Uri): Promise<vscode.Uri | undefined> {
if (notIsfs(uri)) return;
Expand Down
114 changes: 80 additions & 34 deletions src/commands/connectFolderToServerNamespace.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import * as vscode from "vscode";
import { AtelierAPI } from "../api";
import { panel, resolveConnectionSpec, getResolvedConnectionSpec, smExtensionId } from "../extension";
import { notIsfs } from "../utils";
import {
panel,
resolveConnectionSpec,
getResolvedConnectionSpec,
serverManagerApi,
resolveUsernameAndPassword,
} from "../extension";
import { handleError, isUnauthenticated, notIsfs } from "../utils";

interface ConnSettings {
server: string;
Expand All @@ -14,7 +20,6 @@ export async function connectFolderToServerNamespace(): Promise<void> {
vscode.window.showErrorMessage("No folders in the workspace.", "Dismiss");
return;
}
const serverManagerApi = await getServerManagerApi();
if (!serverManagerApi) {
vscode.window.showErrorMessage(
"Connecting a folder to a server namespace requires the [InterSystems Server Manager extension](https://marketplace.visualstudio.com/items?itemName=intersystems-community.servermanager) to be installed and enabled.",
Expand All @@ -31,42 +36,71 @@ export async function connectFolderToServerNamespace(): Promise<void> {
return {
label: folder.name,
description: folder.uri.fsPath,
detail: !conn.server ? undefined : `Currently connected to ${conn.ns} on ${conn.server}`,
detail:
!conn.server || !conn.active
? "No active server connection"
: `Currently connected to ${conn.ns} on ${conn.server}`,
};
});
if (!items.length) {
vscode.window.showErrorMessage("No local folders in the workspace.", "Dismiss");
return;
}
const pick =
items.length === 1 && !items[0].detail
items.length == 1 && !items[0].detail.startsWith("Currently")
? items[0]
: await vscode.window.showQuickPick(items, { title: "Pick a folder" });
const folder = vscode.workspace.workspaceFolders.find((el) => el.name === pick.label);
// Get user's choice of server
const options: vscode.QuickPickOptions = {};
const serverName: string = await serverManagerApi.pickServer(undefined, options);
const serverName: string = await serverManagerApi.pickServer(folder, options);
if (!serverName) {
return;
}
// Get its namespace list
const uri = vscode.Uri.parse(`isfs://${serverName}/?ns=%SYS`);
await resolveConnectionSpec(serverName);
await resolveConnectionSpec(serverName, undefined, folder);
// Prepare a displayable form of its connection spec as a hint to the user
// This will never return the default value (second parameter) because we only just resolved the connection spec.
const connSpec = getResolvedConnectionSpec(serverName, undefined);
const connDisplayString = `${connSpec.webServer.scheme}://${connSpec.webServer.host}:${connSpec.webServer.port}/${connSpec.webServer.pathPrefix}`;
// Connect and fetch namespaces
const api = new AtelierAPI(uri);
const allNamespaces: string[] | undefined = await api
const api = new AtelierAPI(vscode.Uri.parse(`isfs://${serverName}/?ns=%SYS`));
const serverConf = vscode.workspace
.getConfiguration("intersystems", folder)
.inspect<{ [key: string]: any }>("servers");
if (
serverConf.workspaceFolderValue &&
typeof serverConf.workspaceFolderValue[serverName] == "object" &&
!(serverConf.workspaceValue && typeof serverConf.workspaceValue[serverName] == "object")
) {
// Need to manually set connection info if the server is defined at the workspace folder level
api.setConnSpec(serverName, connSpec);
}
const allNamespaces: string[] = await api
.serverInfo(false)
.then((data) => data.result.content.namespaces)
.catch((reason) => {
// Notify user about serverInfo failure
vscode.window.showErrorMessage(
reason.message || `Failed to fetch namespace list from server at ${connDisplayString}`,
"Dismiss"
);
.catch(async (error) => {
if (error?.statusCode == 401 && isUnauthenticated(api.config.username)) {
// Attempt to resolve username and password and try again
const newSpec = await resolveUsernameAndPassword(api.config.serverName, connSpec);
if (newSpec) {
// We were able to resolve credentials, so try again
api.setConnSpec(api.config.serverName, newSpec);
return api
.serverInfo(false)
.then((data) => data.result.content.namespaces)
.catch(async (err) => {
handleError(err, `Failed to fetch namespace list from server at ${connDisplayString}.`);
return undefined;
});
} else {
handleError(
`Unauthenticated access rejected by '${api.serverId}'.`,
`Failed to fetch namespace list from server at ${connDisplayString}.`
);
return undefined;
}
}
handleError(error, `Failed to fetch namespace list from server at ${connDisplayString}.`);
return undefined;
});
// Clear the panel entry created by the connection
Expand All @@ -90,22 +124,34 @@ export async function connectFolderToServerNamespace(): Promise<void> {
}
// Update folder's config object
const config = vscode.workspace.getConfiguration("objectscript", folder);
const conn: any = config.inspect("conn").workspaceFolderValue;
await config.update("conn", { ...conn, server: serverName, ns: namespace, active: true });
}

async function getServerManagerApi(): Promise<any> {
const targetExtension = vscode.extensions.getExtension(smExtensionId);
if (!targetExtension) {
return undefined;
}
if (!targetExtension.isActive) {
await targetExtension.activate();
}
const api = targetExtension.exports;

if (!api) {
return undefined;
if (vscode.workspace.workspaceFile && items.length == 1) {
// Ask the user if they want to enable the connection at the workspace or folder level.
// Only allow this when there is a single client-side folder in the workspace because
// the server may be configured at the workspace folder level.
const answer = await vscode.window.showQuickPick(
[
{ label: `Workspace Folder ${folder.name}`, detail: folder.uri.toString(true) },
{ label: "Workspace File", detail: vscode.workspace.workspaceFile.toString(true) },
],
{ title: "Store the server connection at the workspace or folder level?" }
);
if (!answer) return;
if (answer.label == "Workspace File") {
// Enable the connection at the workspace level
const conn: any = config.inspect("conn").workspaceValue;
await config.update(
"conn",
{ ...conn, server: serverName, ns: namespace, active: true },
vscode.ConfigurationTarget.Workspace
);
return;
}
}
return api;
// Enable the connection at the workspace folder level
const conn: any = config.inspect("conn").workspaceFolderValue;
await config.update(
"conn",
{ ...conn, server: serverName, ns: namespace, active: true },
vscode.ConfigurationTarget.WorkspaceFolder
);
}
53 changes: 35 additions & 18 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ let reporter: TelemetryReporter;

export let checkingConnection = false;

let serverManagerApi: serverManager.ServerManagerAPI;
export let serverManagerApi: serverManager.ServerManagerAPI;

/** Map of the intersystems.server connection specs we have resolved via the API to that extension */
const resolvedConnSpecs = new Map<string, any>();
Expand All @@ -227,22 +227,26 @@ const resolvedConnSpecs = new Map<string, any>();
* @param serverName authority element of an isfs uri, or `objectscript.conn.server` property, or the name of a root folder with an `objectscript.conn.docker-compose` property object
* @param uri if passed, re-check the `objectscript.conn.docker-compose` case in case servermanager API couldn't do that because we're still running our own `activate` method.
*/
export async function resolveConnectionSpec(serverName: string, uri?: vscode.Uri): Promise<void> {
export async function resolveConnectionSpec(
serverName: string,
uri?: vscode.Uri,
scope?: vscode.ConfigurationScope
): Promise<void> {
if (!serverManagerApi || !serverManagerApi.getServerSpec || serverName === "") {
return;
}
if (resolvedConnSpecs.has(serverName)) {
// Already resolved
return;
}
if (!vscode.workspace.getConfiguration("intersystems.servers", null).has(serverName)) {
if (!vscode.workspace.getConfiguration("intersystems.servers", scope).has(serverName)) {
// When not a defined server see it already resolved as a foldername that matches case-insensitively
if (getResolvedConnectionSpec(serverName, undefined)) {
return;
}
}

let connSpec = await serverManagerApi.getServerSpec(serverName);
let connSpec = await serverManagerApi.getServerSpec(serverName, scope);

if (!connSpec && uri) {
// Caller passed uri as a signal to process any docker-compose settings
Expand Down Expand Up @@ -305,6 +309,24 @@ async function resolvePassword(serverSpec, ignoreUnauthenticated = false): Promi
}
}

/** Resolve credentials for `serverName` and returned the complete connection spec if successful */
export async function resolveUsernameAndPassword(serverName: string, oldSpec: any): Promise<any> {
const newSpec: { name: string; username?: string; password?: string } = {
name: serverName,
username: oldSpec?.username,
};
await resolvePassword(newSpec, true);
if (newSpec.password) {
// Update the connection spec
resolvedConnSpecs.set(serverName, {
...oldSpec,
username: newSpec.username,
password: newSpec.password,
});
return resolvedConnSpecs.get(serverName);
}
}

/** Accessor for the cache of resolved connection specs */
export function getResolvedConnectionSpec(key: string, dflt: any): any {
let spec = resolvedConnSpecs.get(key);
Expand Down Expand Up @@ -462,25 +484,20 @@ export async function checkConnection(
if (isUnauthenticated(username)) {
vscode.window.showErrorMessage(
`Unauthenticated access rejected by '${api.serverId}'.${
!api.externalServer ? " Connection has been disabled." : ""
!api.config.serverName ? " Connection has been disabled." : ""
}`,
"Dismiss"
);
if (api.externalServer) {
if (api.config.serverName) {
// Attempt to resolve a username and password
const newSpec: { name: string; username?: string; password?: string } = {
name: api.config.serverName,
username,
};
await resolvePassword(newSpec, true);
if (newSpec.password) {
// Update the connection spec and try again
const oldSpec = getResolvedConnectionSpec(
api.config.serverName,
vscode.workspace.getConfiguration("intersystems.servers", uri).get(api.config.serverName)
);
const newSpec = await resolveUsernameAndPassword(api.config.serverName, oldSpec);
if (newSpec) {
// We were able to resolve credentials, so try again
await workspaceState.update(wsKey + ":password", newSpec.password);
resolvedConnSpecs.set(api.config.serverName, {
...resolvedConnSpecs.get(api.config.serverName),
username: newSpec.username,
password: newSpec.password,
});
api = new AtelierAPI(apiTarget, false);
await api
.serverInfo(true, serverInfoTimeout)
Expand Down