Skip to content

Commit 75f6ea9

Browse files
Support Server Manager being able to handle objectscript.conn.docker-compose type connections (#1471)
1 parent c084a5e commit 75f6ea9

File tree

3 files changed

+168
-55
lines changed

3 files changed

+168
-55
lines changed

src/api/index.ts

+36-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,13 @@ export class AtelierAPI {
110110
if (schemas.includes(wsOrFile.scheme)) {
111111
workspaceFolderName = wsOrFile.authority;
112112
const parts = workspaceFolderName.split(":");
113-
if (parts.length === 2 && config("intersystems.servers").has(parts[0].toLowerCase())) {
113+
if (
114+
parts.length === 2 &&
115+
(config("intersystems.servers").has(parts[0].toLowerCase()) ||
116+
vscode.workspace.workspaceFolders.find(
117+
(ws) => ws.uri.scheme === "file" && ws.name.toLowerCase() === parts[0].toLowerCase()
118+
))
119+
) {
114120
workspaceFolderName = parts[0];
115121
namespace = parts[1];
116122
} else {
@@ -227,6 +233,35 @@ export class AtelierAPI {
227233
if (this._config.ns === "" && this.externalServer) {
228234
this._config.active = false;
229235
}
236+
} else if (conn["docker-compose"]) {
237+
// Provided a docker-compose type connection spec has previously been resolved we can use its values
238+
const resolvedSpec = getResolvedConnectionSpec(workspaceFolderName, undefined);
239+
if (resolvedSpec) {
240+
const {
241+
webServer: { scheme, host, port, pathPrefix = "" },
242+
username,
243+
password,
244+
} = resolvedSpec;
245+
this._config = {
246+
serverName: "",
247+
active: true,
248+
apiVersion: workspaceState.get(this.configName.toLowerCase() + ":apiVersion", DEFAULT_API_VERSION),
249+
serverVersion: workspaceState.get(this.configName.toLowerCase() + ":serverVersion", DEFAULT_SERVER_VERSION),
250+
https: scheme === "https",
251+
ns,
252+
host,
253+
port,
254+
username,
255+
password,
256+
pathPrefix,
257+
docker: true,
258+
dockerService: conn["docker-compose"].service,
259+
};
260+
} else {
261+
this._config = conn;
262+
this._config.ns = ns;
263+
this._config.serverName = "";
264+
}
230265
} else {
231266
this._config = conn;
232267
this._config.ns = ns;

src/extension.ts

+127-51
Original file line numberDiff line numberDiff line change
@@ -214,18 +214,52 @@ const resolvedConnSpecs = new Map<string, any>();
214214
/**
215215
* If servermanager extension is available, fetch the connection spec unless already cached.
216216
* Prompt for credentials if necessary.
217-
* @param serverName authority element of an isfs uri, or `objectscript.conn.server` property
217+
* @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
218+
* @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.
218219
*/
219-
export async function resolveConnectionSpec(serverName: string): Promise<void> {
220-
if (serverManagerApi && serverManagerApi.getServerSpec) {
221-
if (serverName && serverName !== "" && !resolvedConnSpecs.has(serverName)) {
222-
const connSpec = await serverManagerApi.getServerSpec(serverName);
223-
if (connSpec) {
224-
await resolvePassword(connSpec);
225-
resolvedConnSpecs.set(serverName, connSpec);
220+
export async function resolveConnectionSpec(serverName: string, uri?: vscode.Uri): Promise<void> {
221+
if (!serverManagerApi || !serverManagerApi.getServerSpec || serverName === "") {
222+
return;
223+
}
224+
if (resolvedConnSpecs.has(serverName)) {
225+
// Already resolved
226+
return;
227+
}
228+
if (!vscode.workspace.getConfiguration("intersystems.servers", null).has(serverName)) {
229+
// When not a defined server see it already resolved as a foldername that matches case-insensitively
230+
if (getResolvedConnectionSpec(serverName, undefined)) {
231+
return;
232+
}
233+
}
234+
235+
let connSpec = await serverManagerApi.getServerSpec(serverName);
236+
237+
if (!connSpec && uri) {
238+
// Caller passed uri as a signal to process any docker-compose settings
239+
const { configName } = connectionTarget(uri);
240+
if (config("conn", configName)["docker-compose"]) {
241+
const serverForUri = await asyncServerForUri(uri);
242+
if (serverForUri) {
243+
connSpec = {
244+
name: serverForUri.serverName,
245+
webServer: {
246+
scheme: serverForUri.scheme,
247+
host: serverForUri.host,
248+
port: serverForUri.port,
249+
pathPrefix: serverForUri.pathPrefix,
250+
},
251+
username: serverForUri.username,
252+
password: serverForUri.password ? serverForUri.password : undefined,
253+
description: `Server for workspace folder '${serverName}'`,
254+
};
226255
}
227256
}
228257
}
258+
259+
if (connSpec) {
260+
await resolvePassword(connSpec);
261+
resolvedConnSpecs.set(serverName, connSpec);
262+
}
229263
}
230264

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

261295
/** Accessor for the cache of resolved connection specs */
262296
export function getResolvedConnectionSpec(key: string, dflt: any): any {
263-
return resolvedConnSpecs.has(key) ? resolvedConnSpecs.get(key) : dflt;
297+
let spec = resolvedConnSpecs.get(key);
298+
if (spec) {
299+
return spec;
300+
}
301+
302+
// Try a case-insensitive match
303+
key = resolvedConnSpecs.keys().find((oneKey) => oneKey.toLowerCase() === key.toLowerCase());
304+
if (key) {
305+
spec = resolvedConnSpecs.get(key);
306+
if (spec) {
307+
return spec;
308+
}
309+
}
310+
311+
// Return the default if not found
312+
return dflt;
264313
}
265314

266315
export async function checkConnection(
@@ -731,15 +780,20 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
731780
vscode.workspace.workspaceFolders?.map((workspaceFolder) => {
732781
const uri = workspaceFolder.uri;
733782
const { configName } = connectionTarget(uri);
734-
const serverName = notIsfs(uri) ? config("conn", configName).server : configName;
783+
const conn = config("conn", configName);
784+
785+
// When docker-compose object is defined don't fall back to server name, which may have come from user-level settings
786+
const serverName = notIsfs(uri) && !conn["docker-compose"] ? conn.server : configName;
735787
toCheck.set(serverName, uri);
736788
});
737789
for await (const oneToCheck of toCheck) {
738790
const serverName = oneToCheck[0];
739791
const uri = oneToCheck[1];
740792
try {
741793
try {
742-
await resolveConnectionSpec(serverName);
794+
// Pass the uri to resolveConnectionSpec so it will fall back to docker-compose logic if required.
795+
// 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.
796+
await resolveConnectionSpec(serverName, uri);
743797
} finally {
744798
await checkConnection(true, uri, true);
745799
}
@@ -1517,46 +1571,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
15171571

15181572
// The API we export
15191573
const extensionApi = {
1520-
serverForUri(uri: vscode.Uri): any {
1521-
const { apiTarget } = connectionTarget(uri);
1522-
const api = new AtelierAPI(apiTarget);
1523-
1524-
// This function intentionally no longer exposes the password for a named server UNLESS it is already exposed as plaintext in settings.
1525-
// API client extensions should use Server Manager 3's authentication provider to request a missing password themselves,
1526-
// which will require explicit user consent to divulge the password to the requesting extension.
1527-
1528-
const {
1529-
serverName,
1530-
active,
1531-
host = "",
1532-
https,
1533-
port,
1534-
pathPrefix,
1535-
username,
1536-
password,
1537-
ns = "",
1538-
apiVersion,
1539-
serverVersion,
1540-
} = api.config;
1541-
return {
1542-
serverName,
1543-
active,
1544-
scheme: https ? "https" : "http",
1545-
host,
1546-
port,
1547-
pathPrefix,
1548-
username,
1549-
password:
1550-
serverName === ""
1551-
? password
1552-
: vscode.workspace
1553-
.getConfiguration(`intersystems.servers.${serverName.toLowerCase()}`, uri)
1554-
.get("password"),
1555-
namespace: ns,
1556-
apiVersion: active ? apiVersion : undefined,
1557-
serverVersion: active ? serverVersion : undefined,
1558-
};
1559-
},
1574+
serverForUri,
1575+
asyncServerForUri,
15601576
serverDocumentUriForUri(uri: vscode.Uri): vscode.Uri {
15611577
const { apiTarget } = connectionTarget(uri);
15621578
if (typeof apiTarget === "string") {
@@ -1588,6 +1604,66 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
15881604
return extensionApi;
15891605
}
15901606

1607+
// This function is exported as one of our API functions but is also used internally
1608+
// for example to implement the async variant capable of resolving docker port number.
1609+
function serverForUri(uri: vscode.Uri): any {
1610+
const { apiTarget } = connectionTarget(uri);
1611+
const api = new AtelierAPI(apiTarget);
1612+
1613+
// This function intentionally no longer exposes the password for a named server UNLESS it is already exposed as plaintext in settings.
1614+
// API client extensions should use Server Manager 3's authentication provider to request a missing password themselves,
1615+
// which will require explicit user consent to divulge the password to the requesting extension.
1616+
const {
1617+
serverName,
1618+
active,
1619+
host = "",
1620+
https,
1621+
port,
1622+
pathPrefix,
1623+
username,
1624+
password,
1625+
ns = "",
1626+
apiVersion,
1627+
serverVersion,
1628+
} = api.config;
1629+
return {
1630+
serverName,
1631+
active,
1632+
scheme: https ? "https" : "http",
1633+
host,
1634+
port,
1635+
pathPrefix,
1636+
username,
1637+
password:
1638+
serverName === ""
1639+
? password
1640+
: vscode.workspace.getConfiguration(`intersystems.servers.${serverName.toLowerCase()}`, uri).get("password"),
1641+
namespace: ns,
1642+
apiVersion: active ? apiVersion : undefined,
1643+
serverVersion: active ? serverVersion : undefined,
1644+
};
1645+
}
1646+
1647+
// An async variant capable of resolving docker port number.
1648+
// It is exported as one of our API functions but is also used internally.
1649+
async function asyncServerForUri(uri: vscode.Uri): Promise<any> {
1650+
const server = serverForUri(uri);
1651+
if (!server.port) {
1652+
let { apiTarget } = connectionTarget(uri);
1653+
if (apiTarget instanceof vscode.Uri) {
1654+
apiTarget = vscode.workspace.getWorkspaceFolder(apiTarget)?.name;
1655+
}
1656+
const { port: dockerPort, docker: withDocker } = await portFromDockerCompose(apiTarget);
1657+
if (withDocker && dockerPort) {
1658+
server.port = dockerPort;
1659+
server.host = "localhost";
1660+
server.pathPrefix = "";
1661+
server.https = false;
1662+
}
1663+
}
1664+
return server;
1665+
}
1666+
15911667
export function deactivate(): void {
15921668
if (workspaceState) {
15931669
workspaceState.update("openedClasses", openedClasses);

src/utils/index.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -488,21 +488,23 @@ async function composeCommand(cwd?: string): Promise<string> {
488488
});
489489
}
490490

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

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

504506
const result = { port: null, docker: true, service };
505-
const workspaceFolder = uriOfWorkspaceFolder();
507+
const workspaceFolder = uriOfWorkspaceFolder(workspaceFolderName);
506508
if (!workspaceFolder) {
507509
// No workspace folders are open
508510
return { docker: false, port: null };

0 commit comments

Comments
 (0)