From 7b4b5fe342417ad39ac07873bb82770785f6e615 Mon Sep 17 00:00:00 2001 From: Veer Jain Date: Sun, 30 Aug 2026 14:26:52 -0400 Subject: [PATCH 1/2] feat: add compose pull command --- extensions/vscode-containers/package.json | 41 ++++++++++++++++++- extensions/vscode-containers/package.nls.json | 9 +++- .../src/commands/compose/compose.ts | 6 ++- .../src/commands/registerCommands.ts | 3 +- .../src/commands/selectCommandTemplate.ts | 7 +++- 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/extensions/vscode-containers/package.json b/extensions/vscode-containers/package.json index db8469ebd..a9b1e17a1 100644 --- a/extensions/vscode-containers/package.json +++ b/extensions/vscode-containers/package.json @@ -30,6 +30,7 @@ }, "homepage": "https://github.com/microsoft/vscode-containers/blob/main/extensions/vscode-containers/README.md", "activationEvents": [ + "onCommand:vscode-containers.compose.pull", "onTaskType:docker-build", "onTaskType:docker-run", "onTaskType:docker-compose", @@ -212,6 +213,11 @@ "command": "vscode-containers.compose.down", "group": "containers" }, + { + "command": "vscode-containers.compose.pull", + "when": "isWorkspaceTrusted && resourceLangId == dockercompose", + "group": "composeGroup_1_general@1_5" + }, { "when": "isWorkspaceTrusted && resourceLangId == dockercompose", "command": "vscode-containers.compose.restart", @@ -2629,6 +2635,34 @@ "%vscode-containers.config.containers.orchestratorClient.nerdctlCompose%", "%vscode-containers.config.containers.orchestratorClient.finchCompose%" ] + }, + "containers.commands.composePull": { + "type": "array", + "items": { + "type": "object", + "properties": { + "template": { + "type": "string", + "description": "%vscode-containers.config.template.composePull.template%" + }, + "label": { + "type": "string", + "description": "%vscode-containers.config.template.composePull.label%" + }, + "match": { + "type": "string", + "description": "%vscode-containers.config.template.composePull.match%" + } + } + }, + "default": [ + { + "template": "${composeCommand} ${configurationFile} pull", + "label": "Pull images for compose project" + } + ], + "description": "%vscode-containers.config.template.composePull.description%", + "scope": "machine-overridable" } } }, @@ -2638,6 +2672,11 @@ "title": "%vscode-containers.commands.compose.down%", "category": "%vscode-containers.commands.category.containersGeneric%" }, + { + "command": "vscode-containers.compose.pull", + "title": "%vscode-containers.commands.composePull%", + "category": "%vscode-containers.commands.category.docker%" + }, { "command": "vscode-containers.compose.restart", "title": "%vscode-containers.commands.compose.restart%", @@ -3449,4 +3488,4 @@ "vscode-languageclient": "catalog:", "zod": "catalog:" } -} +} \ No newline at end of file diff --git a/extensions/vscode-containers/package.nls.json b/extensions/vscode-containers/package.nls.json index d1f111ddb..994b8ae17 100644 --- a/extensions/vscode-containers/package.nls.json +++ b/extensions/vscode-containers/package.nls.json @@ -367,5 +367,10 @@ "vscode-containers.walkthrough.containersStart.learn.title": "Learn More", "vscode-containers.walkthrough.containersStart.learn.description": "Great job! You've now completed Getting Started with Container Tools. But don't stop here! There are plenty of ways to become more proficient building containerized applications using the Container Tools extension.\n\nFor example, you can [debug a container](https://code.visualstudio.com/docs/containers/debug-common), or [run multiple containers](https://code.visualstudio.com/docs/containers/docker-compose) at once using Docker Compose.\n\nCheck out our [documentation](https://code.visualstudio.com/docs/containers/overview) to make the most of the Container Tools extension for VS Code!", "vscode-containers.copilot.containersConfigTool.displayName": "Get Container Tools Configuration", - "vscode-containers.copilot.containersConfigTool.userDescription": "Gets the configuration for the Container Tools extension." -} + "vscode-containers.copilot.containersConfigTool.userDescription": "Gets the configuration for the Container Tools extension.", + "vscode-containers.config.template.composePull.template": "The command template.", + "vscode-containers.config.template.composePull.label": "The label displayed to the user.", + "vscode-containers.config.template.composePull.match": "The regular expression for choosing the right template. Checked against compose YAML files, folder name, etc.", + "vscode-containers.config.template.composePull.description": "Command templates for compose pull commands.", + "vscode-containers.commands.composePull": "Compose Pull" +} \ No newline at end of file diff --git a/extensions/vscode-containers/src/commands/compose/compose.ts b/extensions/vscode-containers/src/commands/compose/compose.ts index 0338c2a76..58ac60317 100644 --- a/extensions/vscode-containers/src/commands/compose/compose.ts +++ b/extensions/vscode-containers/src/commands/compose/compose.ts @@ -14,7 +14,7 @@ import { quickPickWorkspaceFolder } from '../../utils/quickPickWorkspaceFolder'; import { selectComposeCommand } from '../selectCommandTemplate'; import { getComposeProfileList, getComposeProfilesOrServices, getComposeServiceList, getDefaultCommandComposeProfilesOrServices } from './getComposeSubsetList'; -async function compose(context: IActionContext, commands: ('up' | 'down' | 'upSubset' | 'downSubset')[], message: string, dockerComposeFileUri?: vscode.Uri | string, selectedComposeFileUris?: vscode.Uri[], preselectedServices?: string[], preselectedProfiles?: string[]): Promise { +async function compose(context: IActionContext, commands: ('up' | 'down' | 'upSubset' | 'downSubset' | 'pull')[], message: string, dockerComposeFileUri?: vscode.Uri | string, selectedComposeFileUris?: vscode.Uri[], preselectedServices?: string[], preselectedProfiles?: string[]): Promise { if (!vscode.workspace.isTrusted) { throw new UserCancelledError('enforceTrust'); } @@ -106,6 +106,10 @@ export async function composeRestart(context: IActionContext, dockerComposeFileU return await compose(context, ['down', 'up'], vscode.l10n.t('Choose compose file to restart'), dockerComposeFileUri, selectedComposeFileUris); } +export async function composePull(context: IActionContext, dockerComposeFileUri?: vscode.Uri, selectedComposeFileUris?: vscode.Uri[]): Promise { + return await compose(context, ['pull'], vscode.l10n.t('Choose compose file to pull'), dockerComposeFileUri, selectedComposeFileUris); +} + const serviceListPlaceholder = /\${serviceList}/i; const profileListPlaceholder = /\${profileList}/i; diff --git a/extensions/vscode-containers/src/commands/registerCommands.ts b/extensions/vscode-containers/src/commands/registerCommands.ts index 8f25c31f3..938119f3b 100644 --- a/extensions/vscode-containers/src/commands/registerCommands.ts +++ b/extensions/vscode-containers/src/commands/registerCommands.ts @@ -10,7 +10,7 @@ import { scaffold } from "../scaffolding/scaffold"; import { scaffoldCompose } from "../scaffolding/scaffoldCompose"; import { scaffoldDebugConfig } from "../scaffolding/scaffoldDebugConfig"; import { chooseContainerRuntime } from "./chooseContainerRuntime"; -import { composeDown, composeDownSubset, composeRestart, composeUp, composeUpSubset } from "./compose/compose"; +import { composeDown, composeDownSubset, composeRestart, composeUp, composeUpSubset, composePull } from "./compose/compose"; import { askCopilot } from "./containers/askCopilot"; import { attachShellContainer } from "./containers/attachShellContainer"; import { browseContainerExternal, browseContainerIntegrated } from "./containers/browseContainer"; @@ -124,6 +124,7 @@ export function registerCommands(): void { registerWorkspaceCommand('vscode-containers.compose.up', composeUp); registerWorkspaceCommand('vscode-containers.compose.up.subset', composeUpSubset); registerWorkspaceCommand('vscode-containers.compose.down.subset', composeDownSubset); + registerWorkspaceCommand('vscode-containers.compose.pull', composePull); registerCommand('vscode-containers.pruneSystem', pruneSystem); registerWorkspaceCommand('vscode-containers.containers.attachShell', attachShellContainer); diff --git a/extensions/vscode-containers/src/commands/selectCommandTemplate.ts b/extensions/vscode-containers/src/commands/selectCommandTemplate.ts index 6f838050c..c697bfc97 100644 --- a/extensions/vscode-containers/src/commands/selectCommandTemplate.ts +++ b/extensions/vscode-containers/src/commands/selectCommandTemplate.ts @@ -13,7 +13,7 @@ import { ext } from '../extensionVariables'; import { isComposeV2ableOrchestratorClient } from '../runtimes/OrchestratorRuntimeManager'; import { resolveVariables } from '../utils/resolveVariables'; -type TemplateCommand = 'build' | 'run' | 'runInteractive' | 'attach' | 'logs' | 'composeUp' | 'composeDown' | 'composeUpSubset' | 'composeDownSubset' | 'composeLogs'; +type TemplateCommand = 'build' | 'run' | 'runInteractive' | 'attach' | 'logs' | 'composeUp' | 'composeDown' | 'composeUpSubset' | 'composeDownSubset' | 'composeLogs' | 'composePull'; type TemplatePicker = (items: IAzureQuickPickItem[], options: IAzureQuickPickOptions) => Promise>; @@ -110,7 +110,7 @@ export async function selectComposeLogsCommand(context: IActionContext, folder: ); } -export async function selectComposeCommand(context: IActionContext, folder: vscode.WorkspaceFolder, composeCommand: 'up' | 'down' | 'upSubset' | 'downSubset', configurationFile?: string, detached?: boolean, build?: boolean): Promise { +export async function selectComposeCommand(context: IActionContext, folder: vscode.WorkspaceFolder, composeCommand: 'up' | 'down' | 'upSubset' | 'downSubset' | 'pull', configurationFile?: string, detached?: boolean, build?: boolean): Promise { let template: TemplateCommand; switch (composeCommand) { @@ -123,6 +123,9 @@ export async function selectComposeCommand(context: IActionContext, folder: vsco case 'downSubset': template = 'composeDownSubset'; break; + case 'pull': + template = 'composePull'; + break; case 'upSubset': default: template = 'composeUpSubset'; From b22428f6336eb84fff6777073231134b75e0d7af Mon Sep 17 00:00:00 2001 From: Veer Jain Date: Fri, 16 Oct 2026 11:42:42 -0400 Subject: [PATCH 2/2] fix(compose-pull): address review feedback, add unit tests, restore clean manifest formatting --- extensions/vscode-containers/package.json | 115 ++++++++++++------ extensions/vscode-containers/package.nls.json | 15 +-- .../src/commands/containers/composeGroup.ts | 17 ++- .../src/commands/registerCommands.ts | 3 +- .../src/commands/selectCommandTemplate.ts | 9 +- .../commands/selectCommandTemplate.test.ts | 69 ++++++++++- 6 files changed, 175 insertions(+), 53 deletions(-) diff --git a/extensions/vscode-containers/package.json b/extensions/vscode-containers/package.json index a9b1e17a1..bceaa8d3e 100644 --- a/extensions/vscode-containers/package.json +++ b/extensions/vscode-containers/package.json @@ -30,7 +30,6 @@ }, "homepage": "https://github.com/microsoft/vscode-containers/blob/main/extensions/vscode-containers/README.md", "activationEvents": [ - "onCommand:vscode-containers.compose.pull", "onTaskType:docker-build", "onTaskType:docker-run", "onTaskType:docker-compose", @@ -96,6 +95,10 @@ "command": "vscode-containers.containers.composeGroup.down", "when": "never" }, + { + "command": "vscode-containers.containers.composeGroup.pull", + "when": "never" + }, { "command": "vscode-containers.compose.down", "when": "isWorkspaceTrusted" @@ -116,6 +119,10 @@ "command": "vscode-containers.compose.down.subset", "when": "isWorkspaceTrusted" }, + { + "command": "vscode-containers.compose.pull", + "when": "isWorkspaceTrusted" + }, { "command": "vscode-containers.configure", "when": "isWorkspaceTrusted" @@ -196,6 +203,11 @@ "command": "vscode-containers.compose.down.subset", "group": "containers" }, + { + "when": "isWorkspaceTrusted && editorLangId == dockercompose", + "command": "vscode-containers.compose.pull", + "group": "containers" + }, { "when": "isWorkspaceTrusted && editorLangId == dockerfile", "command": "vscode-containers.images.build", @@ -213,11 +225,6 @@ "command": "vscode-containers.compose.down", "group": "containers" }, - { - "command": "vscode-containers.compose.pull", - "when": "isWorkspaceTrusted && resourceLangId == dockercompose", - "group": "composeGroup_1_general@1_5" - }, { "when": "isWorkspaceTrusted && resourceLangId == dockercompose", "command": "vscode-containers.compose.restart", @@ -238,6 +245,11 @@ "command": "vscode-containers.compose.down.subset", "group": "containers" }, + { + "when": "isWorkspaceTrusted && resourceLangId == dockercompose", + "command": "vscode-containers.compose.pull", + "group": "containers" + }, { "when": "isWorkspaceTrusted && resourceLangId == dockerfile", "command": "vscode-containers.images.build", @@ -457,6 +469,11 @@ "when": "view == vscode-containers.views.containers && viewItem =~ /composeGroup$/i", "group": "composeGroup_1_general@2" }, + { + "command": "vscode-containers.containers.composeGroup.pull", + "when": "view == vscode-containers.views.containers && viewItem =~ /composeGroup$/i", + "group": "composeGroup_1_general@2" + }, { "command": "vscode-containers.containers.composeGroup.stop", "when": "view == vscode-containers.views.containers && viewItem =~ /composeGroup$/i", @@ -1866,6 +1883,46 @@ "advanced" ] }, + "containers.commands.composePull": { + "oneOf": [ + { + "type": "array", + "items": { + "properties": { + "template": { + "type": "string", + "description": "%vscode-containers.config.template.composePull.template%" + }, + "label": { + "type": "string", + "description": "%vscode-containers.config.template.composePull.label%" + }, + "match": { + "type": "string", + "description": "%vscode-containers.config.template.composePull.match%" + } + }, + "required": [ + "label", + "template" + ] + } + }, + { + "type": "string" + } + ], + "default": [ + { + "label": "Compose Pull", + "template": "${composeCommand} ${configurationFile} pull" + } + ], + "description": "%vscode-containers.config.template.composePull.description%", + "tags": [ + "advanced" + ] + }, "containers.commands.composeUp": { "oneOf": [ { @@ -2635,34 +2692,6 @@ "%vscode-containers.config.containers.orchestratorClient.nerdctlCompose%", "%vscode-containers.config.containers.orchestratorClient.finchCompose%" ] - }, - "containers.commands.composePull": { - "type": "array", - "items": { - "type": "object", - "properties": { - "template": { - "type": "string", - "description": "%vscode-containers.config.template.composePull.template%" - }, - "label": { - "type": "string", - "description": "%vscode-containers.config.template.composePull.label%" - }, - "match": { - "type": "string", - "description": "%vscode-containers.config.template.composePull.match%" - } - } - }, - "default": [ - { - "template": "${composeCommand} ${configurationFile} pull", - "label": "Pull images for compose project" - } - ], - "description": "%vscode-containers.config.template.composePull.description%", - "scope": "machine-overridable" } } }, @@ -2672,11 +2701,6 @@ "title": "%vscode-containers.commands.compose.down%", "category": "%vscode-containers.commands.category.containersGeneric%" }, - { - "command": "vscode-containers.compose.pull", - "title": "%vscode-containers.commands.composePull%", - "category": "%vscode-containers.commands.category.docker%" - }, { "command": "vscode-containers.compose.restart", "title": "%vscode-containers.commands.compose.restart%", @@ -2697,6 +2721,11 @@ "title": "%vscode-containers.commands.compose.down.subset%", "category": "%vscode-containers.commands.category.containersGeneric%" }, + { + "command": "vscode-containers.compose.pull", + "title": "%vscode-containers.commands.compose.pull%", + "category": "%vscode-containers.commands.category.containersGeneric%" + }, { "command": "vscode-containers.configure", "title": "%vscode-containers.commands.configure%", @@ -2827,6 +2856,11 @@ "title": "%vscode-containers.commands.containers.composeGroup.down%", "category": "%vscode-containers.commands.category.containers%" }, + { + "command": "vscode-containers.containers.composeGroup.pull", + "title": "%vscode-containers.commands.containers.composeGroup.pull%", + "category": "%vscode-containers.commands.category.containers%" + }, { "command": "vscode-containers.containers.filter", "title": "%vscode-containers.commands.containers.filter%", @@ -3435,6 +3469,7 @@ "containers.commands.composeUpSubset", "containers.commands.composeDown", "containers.commands.composeLogs", + "containers.commands.composePull", "containers.environment", "containers.scaffolding.templatePath", "containers.containerCommand", @@ -3488,4 +3523,4 @@ "vscode-languageclient": "catalog:", "zod": "catalog:" } -} \ No newline at end of file +} diff --git a/extensions/vscode-containers/package.nls.json b/extensions/vscode-containers/package.nls.json index 994b8ae17..6baf49563 100644 --- a/extensions/vscode-containers/package.nls.json +++ b/extensions/vscode-containers/package.nls.json @@ -168,6 +168,10 @@ "vscode-containers.config.template.composeDown.label": "The label displayed to the user.", "vscode-containers.config.template.composeDown.match": "The regular expression for choosing the right template. Checked against compose YAML files, folder name, etc.", "vscode-containers.config.template.composeDown.description": "Command templates for compose down commands.", + "vscode-containers.config.template.composePull.template": "The command template.", + "vscode-containers.config.template.composePull.label": "The label displayed to the user.", + "vscode-containers.config.template.composePull.match": "The regular expression for choosing the right template. Checked against compose YAML files, folder name, etc.", + "vscode-containers.config.template.composePull.description": "Command templates for compose pull commands.", "vscode-containers.config.containers.containers.groupBy": "The property to use to group containers in Containers view: ContainerId, ContainerName, CreatedTime, FullTag, ImageId, Networks, Ports, Registry, RegistryAndPath, Repository, RepositoryName, RepositoryNameShort, RepositoryNameAndTag, State, Status, Tag, or None", "vscode-containers.config.containers.containers.groupByLabel": "The items will be grouped by the value of this container label (e.g. `com.microsoft.created-by`)", "vscode-containers.config.containers.containers.description": "Any secondary properties to display for a container (an array). Possible elements include: ContainerId, ContainerName, CreatedTime, FullTag, ImageId, Networks, Ports, Registry, RegistryAndPath, Repository, RepositoryName, RepositoryNameShort, RepositoryNameAndTag, State, Status, and Tag", @@ -230,6 +234,7 @@ "vscode-containers.commands.compose.up": "Compose Up", "vscode-containers.commands.compose.up.subset": "Compose Up - Select Services", "vscode-containers.commands.compose.down.subset": "Compose Down - Select Services", + "vscode-containers.commands.compose.pull": "Compose Pull", "vscode-containers.commands.configure": "Add Docker Files to Workspace...", "vscode-containers.commands.configureCompose": "Add Compose Files to Workspace...", "vscode-containers.commands.containers.attachShell": "Attach Shell", @@ -256,6 +261,7 @@ "vscode-containers.commands.containers.composeGroup.stop": "Compose Stop", "vscode-containers.commands.containers.composeGroup.restart": "Compose Restart", "vscode-containers.commands.containers.composeGroup.down": "Compose Down", + "vscode-containers.commands.containers.composeGroup.pull": "Compose Pull", "vscode-containers.commands.debugging.initializeForDebugging": "Initialize for container debugging", "vscode-containers.commands.help": "Container Help", "vscode-containers.commands.help.reportIssue": "Report Issue", @@ -367,10 +373,5 @@ "vscode-containers.walkthrough.containersStart.learn.title": "Learn More", "vscode-containers.walkthrough.containersStart.learn.description": "Great job! You've now completed Getting Started with Container Tools. But don't stop here! There are plenty of ways to become more proficient building containerized applications using the Container Tools extension.\n\nFor example, you can [debug a container](https://code.visualstudio.com/docs/containers/debug-common), or [run multiple containers](https://code.visualstudio.com/docs/containers/docker-compose) at once using Docker Compose.\n\nCheck out our [documentation](https://code.visualstudio.com/docs/containers/overview) to make the most of the Container Tools extension for VS Code!", "vscode-containers.copilot.containersConfigTool.displayName": "Get Container Tools Configuration", - "vscode-containers.copilot.containersConfigTool.userDescription": "Gets the configuration for the Container Tools extension.", - "vscode-containers.config.template.composePull.template": "The command template.", - "vscode-containers.config.template.composePull.label": "The label displayed to the user.", - "vscode-containers.config.template.composePull.match": "The regular expression for choosing the right template. Checked against compose YAML files, folder name, etc.", - "vscode-containers.config.template.composePull.description": "Command templates for compose pull commands.", - "vscode-containers.commands.composePull": "Compose Pull" -} \ No newline at end of file + "vscode-containers.copilot.containersConfigTool.userDescription": "Gets the configuration for the Container Tools extension." +} diff --git a/extensions/vscode-containers/src/commands/containers/composeGroup.ts b/extensions/vscode-containers/src/commands/containers/composeGroup.ts index 17dc97295..75f3d42a2 100644 --- a/extensions/vscode-containers/src/commands/containers/composeGroup.ts +++ b/extensions/vscode-containers/src/commands/containers/composeGroup.ts @@ -11,7 +11,7 @@ import { ext } from '../../extensionVariables'; import { TaskCommandRunnerFactory } from '../../runtimes/runners/TaskCommandRunnerFactory'; import { ContainerGroupTreeItem } from '../../tree/containers/ContainerGroupTreeItem'; import { ContainerTreeItem } from '../../tree/containers/ContainerTreeItem'; -import { selectComposeLogsCommand } from '../selectCommandTemplate'; +import { selectComposeLogsCommand, selectComposeCommand } from '../selectCommandTemplate'; export async function composeGroupLogs(context: IActionContext, node: ContainerGroupTreeItem): Promise { return composeGroup(context, async (client, options) => { @@ -45,6 +45,21 @@ export async function composeGroupDown(context: IActionContext, node: ContainerG return composeGroup(context, (client, options) => client.down(options), node); } +export async function composeGroupPull(context: IActionContext, node: ContainerGroupTreeItem): Promise { + return composeGroup(context, async (_client, options) => { + const labels = await getComposeGroupLabels(node); + const workingDirectory = labels && getComposeWorkingDirectory(labels); + + if (!workingDirectory) { + context.errorHandling.suppressReportIssue = true; + throw new Error(l10n.t('Unable to determine compose project info for container group \'{0}\'.', node.label)); + } + + const folder = workspace.getWorkspaceFolder(Uri.file(workingDirectory)) ?? Uri.file(workingDirectory); + return selectComposeCommand(context, folder, 'pull', options.files?.map(f => `-f "${f}"`).join(' ')); + }, node); +} + type AdditionalOptions = Omit; async function composeGroup( diff --git a/extensions/vscode-containers/src/commands/registerCommands.ts b/extensions/vscode-containers/src/commands/registerCommands.ts index 938119f3b..5d3eb99a3 100644 --- a/extensions/vscode-containers/src/commands/registerCommands.ts +++ b/extensions/vscode-containers/src/commands/registerCommands.ts @@ -14,7 +14,7 @@ import { composeDown, composeDownSubset, composeRestart, composeUp, composeUpSub import { askCopilot } from "./containers/askCopilot"; import { attachShellContainer } from "./containers/attachShellContainer"; import { browseContainerExternal, browseContainerIntegrated } from "./containers/browseContainer"; -import { composeGroupDown, composeGroupLogs, composeGroupRestart, composeGroupStart, composeGroupStop } from "./containers/composeGroup"; +import { composeGroupDown, composeGroupLogs, composeGroupPull, composeGroupRestart, composeGroupStart, composeGroupStop } from "./containers/composeGroup"; import { configureContainersExplorer } from "./containers/configureContainersExplorer"; import { downloadContainerFile } from "./containers/files/downloadContainerFile"; import { openContainerFile } from "./containers/files/openContainerFile"; @@ -150,6 +150,7 @@ export function registerCommands(): void { registerWorkspaceCommand('vscode-containers.containers.composeGroup.stop', composeGroupStop); registerWorkspaceCommand('vscode-containers.containers.composeGroup.restart', composeGroupRestart); registerWorkspaceCommand('vscode-containers.containers.composeGroup.down', composeGroupDown); + registerWorkspaceCommand('vscode-containers.containers.composeGroup.pull', composeGroupPull); registerWorkspaceCommand('vscode-containers.images.build', buildImage); registerCommand('vscode-containers.images.configureExplorer', configureImagesExplorer); diff --git a/extensions/vscode-containers/src/commands/selectCommandTemplate.ts b/extensions/vscode-containers/src/commands/selectCommandTemplate.ts index c697bfc97..76ab9d67c 100644 --- a/extensions/vscode-containers/src/commands/selectCommandTemplate.ts +++ b/extensions/vscode-containers/src/commands/selectCommandTemplate.ts @@ -110,7 +110,7 @@ export async function selectComposeLogsCommand(context: IActionContext, folder: ); } -export async function selectComposeCommand(context: IActionContext, folder: vscode.WorkspaceFolder, composeCommand: 'up' | 'down' | 'upSubset' | 'downSubset' | 'pull', configurationFile?: string, detached?: boolean, build?: boolean): Promise { +export async function selectComposeCommand(context: IActionContext, folder: vscode.WorkspaceFolder | vscode.Uri, composeCommand: 'up' | 'down' | 'upSubset' | 'downSubset' | 'pull', configurationFile?: string, detached?: boolean, build?: boolean): Promise { let template: TemplateCommand; switch (composeCommand) { @@ -143,12 +143,15 @@ export async function selectComposeCommand(context: IActionContext, folder: vsco fullComposeCommand = orchestratorClient.commandName; } + const folderName = folder instanceof vscode.Uri ? path.basename(folder.fsPath) : folder.name; + const formattedConfigFile = !configurationFile ? '' : configurationFile.startsWith('-f ') ? configurationFile : `-f "${configurationFile}"`; + return await selectCommandTemplate( context, template, - [folder.name, configurationFile], + [folderName, configurationFile], folder, - { 'configurationFile': configurationFile ? `-f "${configurationFile}"` : '', 'detached': detached ? '-d' : '', 'build': build ? '--build' : '', 'composeCommand': fullComposeCommand } + { 'configurationFile': formattedConfigFile, 'detached': detached ? '-d' : '', 'build': build ? '--build' : '', 'composeCommand': fullComposeCommand } ); } diff --git a/extensions/vscode-containers/src/test/commands/selectCommandTemplate.test.ts b/extensions/vscode-containers/src/test/commands/selectCommandTemplate.test.ts index 4379d6d65..aa32106a0 100644 --- a/extensions/vscode-containers/src/test/commands/selectCommandTemplate.test.ts +++ b/extensions/vscode-containers/src/test/commands/selectCommandTemplate.test.ts @@ -5,9 +5,10 @@ import { IActionContext, IAzureQuickPickItem } from '@microsoft/vscode-azext-utils'; import { WslcClient } from '@microsoft/vscode-container-client'; +import { quoted } from '@microsoft/vscode-processutils'; import assert from 'assert'; import * as vscode from 'vscode'; -import { CommandTemplate, selectBuildCommand, selectCommandTemplate } from '../../commands/selectCommandTemplate'; +import { CommandTemplate, selectBuildCommand, selectCommandTemplate, selectComposeCommand } from '../../commands/selectCommandTemplate'; import { ext } from '../../extensionVariables'; const DefaultPickIndex = 0; @@ -34,6 +35,72 @@ suite("(unit) selectCommandTemplate", () => { } }); + test("selectComposeCommand routes 'pull' to composePull template (Compose V2)", async () => { + const originalOrchestratorManager = ext.orchestratorManager; + ext.orchestratorManager = { + getClient: async () => ({ commandName: 'docker', composeV2: true }), + } as unknown as typeof ext.orchestratorManager; + + try { + const context = { + telemetry: { properties: {}, measurements: {} }, + errorHandling: { issueProperties: {} }, + } as IActionContext; + const folder = { name: 'test', uri: vscode.Uri.file('/test') } as vscode.WorkspaceFolder; + const result = await selectComposeCommand(context, folder, 'pull', 'docker-compose.yml'); + + assert.equal(result.command, 'docker'); + assert.ok(result.args); + assert.deepStrictEqual(result.args, ['compose', '-f', quoted('docker-compose.yml'), 'pull']); + } finally { + ext.orchestratorManager = originalOrchestratorManager; + } + }); + + test("selectComposeCommand routes 'pull' to composePull template (Compose V1)", async () => { + const originalOrchestratorManager = ext.orchestratorManager; + ext.orchestratorManager = { + getClient: async () => ({ commandName: 'docker-compose', composeV2: false }), + } as unknown as typeof ext.orchestratorManager; + + try { + const context = { + telemetry: { properties: {}, measurements: {} }, + errorHandling: { issueProperties: {} }, + } as IActionContext; + const folder = { name: 'test', uri: vscode.Uri.file('/test') } as vscode.WorkspaceFolder; + const result = await selectComposeCommand(context, folder, 'pull', 'docker-compose.yml'); + + assert.equal(result.command, 'docker-compose'); + assert.ok(result.args); + assert.deepStrictEqual(result.args, ['-f', quoted('docker-compose.yml'), 'pull']); + } finally { + ext.orchestratorManager = originalOrchestratorManager; + } + }); + + test("selectComposeCommand supports vscode.Uri folder and pre-formatted options", async () => { + const originalOrchestratorManager = ext.orchestratorManager; + ext.orchestratorManager = { + getClient: async () => ({ commandName: 'docker', composeV2: true }), + } as unknown as typeof ext.orchestratorManager; + + try { + const context = { + telemetry: { properties: {}, measurements: {} }, + errorHandling: { issueProperties: {} }, + } as IActionContext; + const folderUri = vscode.Uri.file('/test/workspace'); + const result = await selectComposeCommand(context, folderUri, 'pull', '-f "docker-compose.yml"'); + + assert.equal(result.command, 'docker'); + assert.ok(result.args); + assert.deepStrictEqual(result.args, ['compose', '-f', quoted('docker-compose.yml'), 'pull']); + } finally { + ext.orchestratorManager = originalOrchestratorManager; + } + }); + test("One constrained from settings (match)", async () => { const result = await runWithCommandSetting( [