diff --git a/extensions/vscode-containers/package.json b/extensions/vscode-containers/package.json index db8469ebd..bceaa8d3e 100644 --- a/extensions/vscode-containers/package.json +++ b/extensions/vscode-containers/package.json @@ -95,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" @@ -115,6 +119,10 @@ "command": "vscode-containers.compose.down.subset", "when": "isWorkspaceTrusted" }, + { + "command": "vscode-containers.compose.pull", + "when": "isWorkspaceTrusted" + }, { "command": "vscode-containers.configure", "when": "isWorkspaceTrusted" @@ -195,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", @@ -232,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", @@ -451,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", @@ -1860,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": [ { @@ -2658,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%", @@ -2788,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%", @@ -3396,6 +3469,7 @@ "containers.commands.composeUpSubset", "containers.commands.composeDown", "containers.commands.composeLogs", + "containers.commands.composePull", "containers.environment", "containers.scaffolding.templatePath", "containers.containerCommand", diff --git a/extensions/vscode-containers/package.nls.json b/extensions/vscode-containers/package.nls.json index d1f111ddb..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", 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/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 8f25c31f3..5d3eb99a3 100644 --- a/extensions/vscode-containers/src/commands/registerCommands.ts +++ b/extensions/vscode-containers/src/commands/registerCommands.ts @@ -10,11 +10,11 @@ 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"; -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"; @@ -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); @@ -149,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 6f838050c..76ab9d67c 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 | vscode.Uri, 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'; @@ -140,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( [