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