Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 0 additions & 1 deletion extensions/mssql/src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ export const outputContentTypeShowError = "showError";
export const outputContentTypeShowWarning = "showWarning";
export const outputServiceLocalhost = "http://localhost:";
export const localhost = "localhost";
export const localhostIP = "127.0.0.1";
export const defaultContainerName = "sql_server_container";
export const msgContentProviderSqlOutputHtml = "dist/html/sqlOutput.ejs";
export const contentProviderMinFile = "dist/js/app.min.js";
Expand Down
19 changes: 19 additions & 0 deletions extensions/mssql/src/controllers/connectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { Logger } from "../models/logger";
import { getServerTypes } from "../models/connectionInfo";
import * as AzureConstants from "../azure/constants";
import { ChangePasswordService } from "../services/changePasswordService";
import { checkIfConnectionIsDockerContainer } from "../deployment/dockerUtils";

/**
* Information for a document's connection. Exported for testing purposes.
Expand Down Expand Up @@ -1133,6 +1134,24 @@ export default class ConnectionManager {
await this.connectionStore.saveProfilePasswordIfNeeded(profile);
}

public async checkForDockerConnection(profile: IConnectionProfile): Promise<string> {
if (!profile.containerName) {
const serverInfo = this.getServerInfo(profile);
let machineName = "";
if (serverInfo) {
machineName = (serverInfo as any)["machineName"];
}
const containerName = await checkIfConnectionIsDockerContainer(machineName);
if (containerName) {
profile.containerName = containerName;
// if the connection is a docker container, make sure to set the container name for future use
await this.connectionStore.saveProfile(profile);
return containerName;
}
}
return "";
}

/**
* Creates a new connection with provided credentials.
* @param fileUri file URI for the connection. If not provided, a new URI will be generated.
Expand Down
55 changes: 16 additions & 39 deletions extensions/mssql/src/deployment/dockerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import {
defaultPortNumber,
docker,
dockerDeploymentLoggerChannelName,
localhost,
localhostIP,
Platform,
windowsDockerDesktopExecutable,
x64,
Expand Down Expand Up @@ -131,6 +129,10 @@ export const COMMANDS = {
command: "docker",
args: ["ps", "-a", "--format", "{{.Names}}"],
}),
GET_CONTAINER_NAME_FROM_ID: (containerId: string): DockerCommand => ({
command: "docker",
args: ["ps", "-a", "--filter", `id=${containerId}`, "--format", "{{.Names}}"],
}),
INSPECT: (id: string): DockerCommand => ({
command: "docker",
args: ["inspect", sanitizeContainerInput(id)],
Expand All @@ -151,11 +153,11 @@ export const COMMANDS = {
"-e",
"ACCEPT_EULA=Y",
"-e",
`SA_PASSWORD=${password}`,
`\'SA_PASSWORD=${password}\'`,
"-p",
`${port}:${defaultPortNumber}`,
`\'${port}:${defaultPortNumber}\'`,
"--name",
sanitizeContainerInput(name),
`\'${sanitizeContainerInput(name)}\'`,
];

if (hostname) {
Expand Down Expand Up @@ -916,42 +918,17 @@ async function getUsedPortsFromContainers(containerIds: string[]): Promise<Set<n
}

/**
* Finds a Docker container by checking if its exposed ports match the server name.
* It inspects each container to find a match with the server name.
* Determines whether a connection is running inside a Docker container.
*
* Inspects the `machineName` from the connection's server info. For Docker connections,
* the machine name is set to the UUID corresponding to the container's ID.
*
* @param machineName The machine name hosting the connection, as reported in its server info.
*/
async function findContainerByPort(containerIds: string[], serverName: string): Promise<string> {
if (serverName === localhost || serverName === localhostIP) {
serverName += `,${defaultPortNumber}`;
}
for (const id of containerIds) {
try {
const inspect = await execDockerCommand(COMMANDS.INSPECT_CONTAINER(id));
const ports = inspect.match(/"HostPort":\s*"(\d+)"/g);

if (ports?.some((p) => serverName.includes(p.match(/\d+/)?.[0] || ""))) {
const nameMatch = inspect.match(/"Name"\s*:\s*"\/([^"]+)"/);
if (nameMatch) return nameMatch[1];
}
} catch {
// skip container if inspection fails
}
}

return undefined;
}

/**
* Checks if a connection is a Docker container by inspecting the server name.
*/
export async function checkIfConnectionIsDockerContainer(serverName: string): Promise<string> {
if (!serverName.includes(localhost) && !serverName.includes(localhostIP)) return "";

export async function checkIfConnectionIsDockerContainer(machineName: string): Promise<string> {
try {
const stdout = await execDockerCommand(COMMANDS.GET_CONTAINERS());
const containerIds = stdout.split("\n").filter(Boolean);
if (!containerIds.length) return undefined;

return await findContainerByPort(containerIds, serverName);
const stdout = await execDockerCommand(COMMANDS.GET_CONTAINER_NAME_FROM_ID(machineName));
return stdout.trim();
} catch {
return undefined;
}
Expand Down
46 changes: 32 additions & 14 deletions extensions/mssql/src/objectExplorer/nodes/connectionNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ const createDisconnectedNodeContextValue = (
};
};

const createConnectedNodeContextValue = (
connectionProfile: ConnectionProfile,
): vscodeMssql.TreeNodeContextValue => {
let nodeSubType = connectionProfile.database ? DATABASE_SUBTYPE : undefined;
if (connectionProfile.containerName) nodeSubType = dockerContainer;
return {
type: SERVER_NODE_CONNECTED,
filterable: false,
hasFilters: false,
subType: nodeSubType,
};
};

export class ConnectionNode extends TreeNodeInfo {
constructor(connectionProfile: ConnectionProfile, parentNode?: TreeNodeInfo) {
const displayName = ConnInfo.getConnectionDisplayName(connectionProfile);
Expand Down Expand Up @@ -252,20 +265,7 @@ export class ConnectionNode extends TreeNodeInfo {
connectionProfile: ConnectionProfile;
}) {
const { nodeInfo, sessionId, parentNode, connectionProfile } = options;
let subType;
if (connectionProfile.containerName && connectionProfile.database) {
subType = `${Constants.dockerContainerDatabase}`;
} else if (connectionProfile.containerName) {
subType = dockerContainer;
} else if (connectionProfile.database) {
subType = DATABASE_SUBTYPE;
}
this.context = {
type: SERVER_NODE_CONNECTED,
filterable: nodeInfo.filterableProperties?.length > 0,
hasFilters: false,
subType: subType ?? "",
};
this.context = createConnectedNodeContextValue(connectionProfile);
this.collapsibleState = vscode.TreeItemCollapsibleState.Expanded;
this.nodePath = nodeInfo.nodePath;
this.nodeStatus = nodeInfo.nodeStatus;
Expand Down Expand Up @@ -315,4 +315,22 @@ export class ConnectionNode extends TreeNodeInfo {
this.iconPath = ObjectExplorerUtils.iconPath(iconName);
}
}

public updateToDockerConnection(containerName: string): ConnectionNode {
this.connectionProfile.containerName = containerName;
if (this.nodeType === SERVER_NODE_DISCONNECTED) {
this.iconPath = ObjectExplorerUtils.iconPath(ICON_DOCKER_SERVER_DISCONNECTED);
this.context = createDisconnectedNodeContextValue(this.connectionProfile);
this.nodeSubType = disconnectedDockerContainer;
} else if (this.nodeType === SERVER_NODE_CONNECTED) {
this.iconPath = ObjectExplorerUtils.iconPath(ICON_DOCKER_SERVER_CONNECTED);
this.context = createConnectedNodeContextValue(this.connectionProfile);
if (this.connectionProfile.database) {
this.nodeSubType = `${Constants.dockerContainerDatabase}`;
} else {
this.nodeSubType = dockerContainer;
}
}
return this;
}
}
24 changes: 7 additions & 17 deletions extensions/mssql/src/objectExplorer/objectExplorerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import {
} from "../models/contracts/objectExplorer/getSessionIdRequest";
import { Logger } from "../models/logger";
import VscodeWrapper from "../controllers/vscodeWrapper";
import { checkIfConnectionIsDockerContainer, restartContainer } from "../deployment/dockerUtils";
import { restartContainer } from "../deployment/dockerUtils";
import { ExpandErrorNode } from "./nodes/expandErrorNode";
import { NoItemsNode } from "./nodes/noItemNode";
import { ConnectionNode } from "./nodes/connectionNode";
Expand Down Expand Up @@ -710,21 +710,6 @@ export class ObjectExplorerService {
return undefined;
}

// Check if connection is a Docker container
const serverName = connectionProfile.connectionString
? connectionProfile.connectionString.match(/^Server=([^;]+)/)?.[1]
: connectionProfile.server;

if (serverName && !connectionProfile.containerName) {
const containerName = await checkIfConnectionIsDockerContainer(serverName);
if (containerName) {
connectionProfile.containerName = containerName;
}

// if the connnection is a docker container, make sure to set the container name for future use
await this._connectionManager.connectionStore.saveProfile(connectionProfile);
}

if (!connectionProfile.id) {
connectionProfile.id = Utils.generateGuid();
}
Expand Down Expand Up @@ -805,7 +790,12 @@ export class ObjectExplorerService {
) {
await this._connectionManager.connect(nodeUri, connectionNode.connectionProfile);
}
if (isNewConnection) {
const dockerConnectionContainerName =
await this._connectionManager.checkForDockerConnection(connectionProfile);
if (dockerConnectionContainerName) {
connectionNode = connectionNode.updateToDockerConnection(dockerConnectionContainerName);
}
if (isNewConnection || dockerConnectionContainerName) {
this.addConnectionNode(connectionNode);
}

Expand Down
24 changes: 9 additions & 15 deletions extensions/mssql/test/unit/dockerUtilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1097,31 +1097,25 @@ suite("Docker Utilities", () => {
}),
});

// 1. Non-localhost server: should return ""
let result = await dockerUtils.checkIfConnectionIsDockerContainer("some.remote.host");
assert.strictEqual(result, "", "Should return empty string for non-localhost address");

// 2. Docker command fails: should return undefined
// 1. Docker command fails: should return undefined
spawnStub.returns(createFailureProcess(new Error("spawn failed")) as any);
result = await dockerUtils.checkIfConnectionIsDockerContainer("localhost");
let result = await dockerUtils.checkIfConnectionIsDockerContainer("dockercontainerid");
assert.strictEqual(result, undefined, "Should return undefined on spawn failure");

// Reset spawnStub for next test
spawnStub.resetHistory();
spawnStub.returns(createSuccessProcess("") as any); // simulate no containers

// 3. Docker command returns no containers: should return undefined
result = await dockerUtils.checkIfConnectionIsDockerContainer("127.0.0.1");
assert.strictEqual(result, undefined, "Should return undefined when no containers exist");
// 2. Docker command returns no containers: should return empty string
result = await dockerUtils.checkIfConnectionIsDockerContainer("dockercontainerid");
assert.strictEqual(result, "", "Should return empty string when no containers exist");

// 4. Containers exist and one matches the port: should return the container id
// 3. Containers exist and one matches the port: should return the container id
spawnStub.resetHistory();
spawnStub.returns(
createSuccessProcess(`"HostPort": "1433", "Name": "/testContainer",\n`) as any,
); // simulate container with port 1433
spawnStub.returns(createSuccessProcess(`dockercontainerid`) as any); // simulate container with port 1433

result = await dockerUtils.checkIfConnectionIsDockerContainer("localhost, 1433");
assert.strictEqual(result, "testContainer", "Should return matched container ID");
result = await dockerUtils.checkIfConnectionIsDockerContainer("dockercontainerid");
assert.ok(result, "Should return container name");
});

test("findAvailablePort: should find next available port", async () => {
Expand Down
Loading