Skip to content

Commit

Permalink
Support Server Manager being able to handle `objectscript.conn.docker…
Browse files Browse the repository at this point in the history
…-compose` type connections (intersystems-community#1471)
  • Loading branch information
gjsjohnmurray authored Feb 6, 2025
1 parent c084a5e commit 75f6ea9
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 55 deletions.
37 changes: 36 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,13 @@ export class AtelierAPI {
if (schemas.includes(wsOrFile.scheme)) {
workspaceFolderName = wsOrFile.authority;
const parts = workspaceFolderName.split(":");
if (parts.length === 2 && config("intersystems.servers").has(parts[0].toLowerCase())) {
if (
parts.length === 2 &&
(config("intersystems.servers").has(parts[0].toLowerCase()) ||
vscode.workspace.workspaceFolders.find(
(ws) => ws.uri.scheme === "file" && ws.name.toLowerCase() === parts[0].toLowerCase()
))
) {
workspaceFolderName = parts[0];
namespace = parts[1];
} else {
Expand Down Expand Up @@ -227,6 +233,35 @@ export class AtelierAPI {
if (this._config.ns === "" && this.externalServer) {
this._config.active = false;
}
} else if (conn["docker-compose"]) {
// Provided a docker-compose type connection spec has previously been resolved we can use its values
const resolvedSpec = getResolvedConnectionSpec(workspaceFolderName, undefined);
if (resolvedSpec) {
const {
webServer: { scheme, host, port, pathPrefix = "" },
username,
password,
} = resolvedSpec;
this._config = {
serverName: "",
active: true,
apiVersion: workspaceState.get(this.configName.toLowerCase() + ":apiVersion", DEFAULT_API_VERSION),
serverVersion: workspaceState.get(this.configName.toLowerCase() + ":serverVersion", DEFAULT_SERVER_VERSION),
https: scheme === "https",
ns,
host,
port,
username,
password,
pathPrefix,
docker: true,
dockerService: conn["docker-compose"].service,
};
} else {
this._config = conn;
this._config.ns = ns;
this._config.serverName = "";
}
} else {
this._config = conn;
this._config.ns = ns;
Expand Down
178 changes: 127 additions & 51 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,18 +214,52 @@ const resolvedConnSpecs = new Map<string, any>();
/**
* If servermanager extension is available, fetch the connection spec unless already cached.
* Prompt for credentials if necessary.
* @param serverName authority element of an isfs uri, or `objectscript.conn.server` property
* @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): Promise<void> {
if (serverManagerApi && serverManagerApi.getServerSpec) {
if (serverName && serverName !== "" && !resolvedConnSpecs.has(serverName)) {
const connSpec = await serverManagerApi.getServerSpec(serverName);
if (connSpec) {
await resolvePassword(connSpec);
resolvedConnSpecs.set(serverName, connSpec);
export async function resolveConnectionSpec(serverName: string, uri?: vscode.Uri): Promise<void> {
if (!serverManagerApi || !serverManagerApi.getServerSpec || serverName === "") {
return;
}
if (resolvedConnSpecs.has(serverName)) {
// Already resolved
return;
}
if (!vscode.workspace.getConfiguration("intersystems.servers", null).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);

if (!connSpec && uri) {
// Caller passed uri as a signal to process any docker-compose settings
const { configName } = connectionTarget(uri);
if (config("conn", configName)["docker-compose"]) {
const serverForUri = await asyncServerForUri(uri);
if (serverForUri) {
connSpec = {
name: serverForUri.serverName,
webServer: {
scheme: serverForUri.scheme,
host: serverForUri.host,
port: serverForUri.port,
pathPrefix: serverForUri.pathPrefix,
},
username: serverForUri.username,
password: serverForUri.password ? serverForUri.password : undefined,
description: `Server for workspace folder '${serverName}'`,
};
}
}
}

if (connSpec) {
await resolvePassword(connSpec);
resolvedConnSpecs.set(serverName, connSpec);
}
}

async function resolvePassword(serverSpec, ignoreUnauthenticated = false): Promise<void> {
Expand Down Expand Up @@ -260,7 +294,22 @@ async function resolvePassword(serverSpec, ignoreUnauthenticated = false): Promi

/** Accessor for the cache of resolved connection specs */
export function getResolvedConnectionSpec(key: string, dflt: any): any {
return resolvedConnSpecs.has(key) ? resolvedConnSpecs.get(key) : dflt;
let spec = resolvedConnSpecs.get(key);
if (spec) {
return spec;
}

// Try a case-insensitive match
key = resolvedConnSpecs.keys().find((oneKey) => oneKey.toLowerCase() === key.toLowerCase());
if (key) {
spec = resolvedConnSpecs.get(key);
if (spec) {
return spec;
}
}

// Return the default if not found
return dflt;
}

export async function checkConnection(
Expand Down Expand Up @@ -731,15 +780,20 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
vscode.workspace.workspaceFolders?.map((workspaceFolder) => {
const uri = workspaceFolder.uri;
const { configName } = connectionTarget(uri);
const serverName = notIsfs(uri) ? config("conn", configName).server : configName;
const conn = config("conn", configName);

// When docker-compose object is defined don't fall back to server name, which may have come from user-level settings
const serverName = notIsfs(uri) && !conn["docker-compose"] ? conn.server : configName;
toCheck.set(serverName, uri);
});
for await (const oneToCheck of toCheck) {
const serverName = oneToCheck[0];
const uri = oneToCheck[1];
try {
try {
await resolveConnectionSpec(serverName);
// Pass the uri to resolveConnectionSpec so it will fall back to docker-compose logic if required.
// Necessary because we are in our activate method, so its call to the Server Manager API cannot call back to our API to do that.
await resolveConnectionSpec(serverName, uri);
} finally {
await checkConnection(true, uri, true);
}
Expand Down Expand Up @@ -1517,46 +1571,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {

// The API we export
const extensionApi = {
serverForUri(uri: vscode.Uri): any {
const { apiTarget } = connectionTarget(uri);
const api = new AtelierAPI(apiTarget);

// This function intentionally no longer exposes the password for a named server UNLESS it is already exposed as plaintext in settings.
// API client extensions should use Server Manager 3's authentication provider to request a missing password themselves,
// which will require explicit user consent to divulge the password to the requesting extension.

const {
serverName,
active,
host = "",
https,
port,
pathPrefix,
username,
password,
ns = "",
apiVersion,
serverVersion,
} = api.config;
return {
serverName,
active,
scheme: https ? "https" : "http",
host,
port,
pathPrefix,
username,
password:
serverName === ""
? password
: vscode.workspace
.getConfiguration(`intersystems.servers.${serverName.toLowerCase()}`, uri)
.get("password"),
namespace: ns,
apiVersion: active ? apiVersion : undefined,
serverVersion: active ? serverVersion : undefined,
};
},
serverForUri,
asyncServerForUri,
serverDocumentUriForUri(uri: vscode.Uri): vscode.Uri {
const { apiTarget } = connectionTarget(uri);
if (typeof apiTarget === "string") {
Expand Down Expand Up @@ -1588,6 +1604,66 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
return extensionApi;
}

// This function is exported as one of our API functions but is also used internally
// for example to implement the async variant capable of resolving docker port number.
function serverForUri(uri: vscode.Uri): any {
const { apiTarget } = connectionTarget(uri);
const api = new AtelierAPI(apiTarget);

// This function intentionally no longer exposes the password for a named server UNLESS it is already exposed as plaintext in settings.
// API client extensions should use Server Manager 3's authentication provider to request a missing password themselves,
// which will require explicit user consent to divulge the password to the requesting extension.
const {
serverName,
active,
host = "",
https,
port,
pathPrefix,
username,
password,
ns = "",
apiVersion,
serverVersion,
} = api.config;
return {
serverName,
active,
scheme: https ? "https" : "http",
host,
port,
pathPrefix,
username,
password:
serverName === ""
? password
: vscode.workspace.getConfiguration(`intersystems.servers.${serverName.toLowerCase()}`, uri).get("password"),
namespace: ns,
apiVersion: active ? apiVersion : undefined,
serverVersion: active ? serverVersion : undefined,
};
}

// An async variant capable of resolving docker port number.
// It is exported as one of our API functions but is also used internally.
async function asyncServerForUri(uri: vscode.Uri): Promise<any> {
const server = serverForUri(uri);
if (!server.port) {
let { apiTarget } = connectionTarget(uri);
if (apiTarget instanceof vscode.Uri) {
apiTarget = vscode.workspace.getWorkspaceFolder(apiTarget)?.name;
}
const { port: dockerPort, docker: withDocker } = await portFromDockerCompose(apiTarget);
if (withDocker && dockerPort) {
server.port = dockerPort;
server.host = "localhost";
server.pathPrefix = "";
server.https = false;
}
}
return server;
}

export function deactivate(): void {
if (workspaceState) {
workspaceState.update("openedClasses", openedClasses);
Expand Down
8 changes: 5 additions & 3 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,21 +488,23 @@ async function composeCommand(cwd?: string): Promise<string> {
});
}

export async function portFromDockerCompose(): Promise<{ port: number; docker: boolean; service?: string }> {
export async function portFromDockerCompose(
workspaceFolderName?: string
): Promise<{ port: number; docker: boolean; service?: string }> {
// When running remotely, behave as if there is no docker-compose object within objectscript.conn
if (extensionContext.extension.extensionKind === vscode.ExtensionKind.Workspace) {
return { docker: false, port: null };
}

// Seek a valid docker-compose object within objectscript.conn
const { "docker-compose": dockerCompose = {} } = config("conn");
const { "docker-compose": dockerCompose = {} } = config("conn", workspaceFolderName);
const { service, file = "docker-compose.yml", internalPort = 52773, envFile } = dockerCompose;
if (!internalPort || !file || !service || service === "") {
return { docker: false, port: null };
}

const result = { port: null, docker: true, service };
const workspaceFolder = uriOfWorkspaceFolder();
const workspaceFolder = uriOfWorkspaceFolder(workspaceFolderName);
if (!workspaceFolder) {
// No workspace folders are open
return { docker: false, port: null };
Expand Down

0 comments on commit 75f6ea9

Please sign in to comment.