Skip to content
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

Support Server Manager being able to handle objectscript.conn.docker-compose type connections #1471

Merged
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
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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that someone would want to do this for non-file workspace folders?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but at this point I'm coding defensively to minimize the chance of unexpected side-effects from my changes.

))
) {
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the standard vscode.workspace.getConfiguration() API instead of this old custom one? It would be nice to phase that out in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this stage I'd prefer not to make unnecessary changes. The config(...) function does a number of things that mean I don't think vscode.workspace.getConfiguration() is a drop-in replacement.


// 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> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean for Language Server? Does it have to change to support these docker compose connections?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. It will continue to ask the ObjectScript extension for the connection details (host, port, pathPrefix etc) as usual. This extension does the magic.

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