diff --git a/extensions/vscode-containers/package.json b/extensions/vscode-containers/package.json index cc659ddd..14de22db 100644 --- a/extensions/vscode-containers/package.json +++ b/extensions/vscode-containers/package.json @@ -79,6 +79,10 @@ "command": "vscode-containers.containers.composeGroup.logs", "when": "config.containers.containers.groupBy == 'Compose Project Name'" }, + { + "command": "vscode-containers.containers.composeGroup.pull", + "when": "config.containers.containers.groupBy == 'Compose Project Name'" + }, { "command": "vscode-containers.containers.composeGroup.start", "when": "config.containers.containers.groupBy == 'Compose Project Name'" @@ -99,6 +103,10 @@ "command": "vscode-containers.compose.down", "when": "isWorkspaceTrusted" }, + { + "command": "vscode-containers.compose.pull", + "when": "isWorkspaceTrusted" + }, { "command": "vscode-containers.compose.restart", "when": "isWorkspaceTrusted" @@ -175,6 +183,11 @@ "command": "vscode-containers.compose.down", "group": "containers" }, + { + "when": "isWorkspaceTrusted && editorLangId == dockercompose", + "command": "vscode-containers.compose.pull", + "group": "containers" + }, { "when": "isWorkspaceTrusted && editorLangId == dockercompose", "command": "vscode-containers.compose.restart", @@ -212,6 +225,11 @@ "command": "vscode-containers.compose.down", "group": "containers" }, + { + "when": "isWorkspaceTrusted && resourceLangId == dockercompose", + "command": "vscode-containers.compose.pull", + "group": "containers" + }, { "when": "isWorkspaceTrusted && resourceLangId == dockercompose", "command": "vscode-containers.compose.restart", @@ -442,15 +460,20 @@ "group": "composeGroup_1_general@1" }, { - "command": "vscode-containers.containers.composeGroup.start", + "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", + "command": "vscode-containers.containers.composeGroup.start", "when": "view == vscode-containers.views.containers && viewItem =~ /composeGroup$/i", "group": "composeGroup_1_general@3" }, + { + "command": "vscode-containers.containers.composeGroup.stop", + "when": "view == vscode-containers.views.containers && viewItem =~ /composeGroup$/i", + "group": "composeGroup_1_general@4" + }, { "command": "vscode-containers.containers.composeGroup.restart", "when": "view == vscode-containers.views.containers && viewItem =~ /composeGroup$/i", @@ -2633,6 +2656,11 @@ "title": "%vscode-containers.commands.compose.down%", "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.compose.restart", "title": "%vscode-containers.commands.compose.restart%", @@ -2758,6 +2786,11 @@ "title": "%vscode-containers.commands.containers.composeGroup.logs%", "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.composeGroup.start", "title": "%vscode-containers.commands.containers.composeGroup.start%", diff --git a/extensions/vscode-containers/package.nls.json b/extensions/vscode-containers/package.nls.json index 349b01c7..11aac756 100644 --- a/extensions/vscode-containers/package.nls.json +++ b/extensions/vscode-containers/package.nls.json @@ -226,6 +226,7 @@ "vscode-containers.config.containers.orchestratorClient.finchCompose": "Finch Compose", "vscode-containers.config.deprecated": "This setting has been deprecated and will be removed in a future release.", "vscode-containers.commands.compose.down": "Compose Down", + "vscode-containers.commands.compose.pull": "Compose Pull", "vscode-containers.commands.compose.restart": "Compose Restart", "vscode-containers.commands.compose.up": "Compose Up", "vscode-containers.commands.compose.up.subset": "Compose Up - Select Services", @@ -251,6 +252,7 @@ "vscode-containers.commands.containers.stats": "Stats", "vscode-containers.commands.containers.viewLogs": "View Logs", "vscode-containers.commands.containers.composeGroup.logs": "Compose Logs", + "vscode-containers.commands.containers.composeGroup.pull": "Compose Pull", "vscode-containers.commands.containers.composeGroup.start": "Compose Start", "vscode-containers.commands.containers.composeGroup.stop": "Compose Stop", "vscode-containers.commands.containers.composeGroup.restart": "Compose Restart", diff --git a/extensions/vscode-containers/src/commands/compose/compose.ts b/extensions/vscode-containers/src/commands/compose/compose.ts index 0338c2a7..33967a74 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' | 'pull' | 'upSubset' | 'downSubset')[], message: string, dockerComposeFileUri?: vscode.Uri | string, selectedComposeFileUris?: vscode.Uri[], preselectedServices?: string[], preselectedProfiles?: string[]): Promise { if (!vscode.workspace.isTrusted) { throw new UserCancelledError('enforceTrust'); } @@ -55,14 +55,17 @@ async function compose(context: IActionContext, commands: ('up' | 'down' | 'upSu } for (const item of selectedItems) { - let terminalCommand = await selectComposeCommand( - context, - folder, - command, - item?.relativeFilePath, - detached, - build - ); + const client = await ext.orchestratorManager.getClient(); + let terminalCommand = command === 'pull' + ? await client.pull({ files: item?.relativeFilePath ? [item.relativeFilePath] : undefined }) + : await selectComposeCommand( + context, + folder, + command, + item?.relativeFilePath, + detached, + build + ); if (!terminalCommand.args?.length) { // Add the service list if needed @@ -73,7 +76,6 @@ async function compose(context: IActionContext, commands: ('up' | 'down' | 'upSu terminalCommand = await addDefaultCommandServicesOrProfilesIfNeeded(context, folder, terminalCommand, preselectedServices, preselectedProfiles); } - const client = await ext.orchestratorManager.getClient(); const taskCRF = new TaskCommandRunnerFactory({ taskName: client.displayName, workspaceFolder: folder, @@ -98,6 +100,10 @@ export async function composeDown(context: IActionContext, dockerComposeFileUri? return await compose(context, ['down'], vscode.l10n.t('Choose compose file to take down'), dockerComposeFileUri, selectedComposeFileUris); } +export async function composePull(context: IActionContext, dockerComposeFileUri?: vscode.Uri | string, selectedComposeFileUris?: vscode.Uri[]): Promise { + return await compose(context, ['pull'], vscode.l10n.t('Choose compose file whose images to pull'), dockerComposeFileUri, selectedComposeFileUris); +} + export async function composeDownSubset(context: IActionContext, dockerComposeFileUri?: vscode.Uri | string, selectedComposeFileUris?: vscode.Uri[], preselectedServices?: string[], preselectedProfiles?: string[]): Promise { return await compose(context, ['downSubset'], vscode.l10n.t('Choose compose file to take down'), dockerComposeFileUri, selectedComposeFileUris, preselectedServices, preselectedProfiles); } diff --git a/extensions/vscode-containers/src/commands/containers/composeGroup.ts b/extensions/vscode-containers/src/commands/containers/composeGroup.ts index 17dc9729..f988988d 100644 --- a/extensions/vscode-containers/src/commands/containers/composeGroup.ts +++ b/extensions/vscode-containers/src/commands/containers/composeGroup.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { IActionContext } from '@microsoft/vscode-azext-utils'; -import { CommonOrchestratorCommandOptions, IContainerOrchestratorClient, LogsCommandOptions, VoidCommandResponse } from '@microsoft/vscode-container-client'; +import { CommonOrchestratorCommandOptions, IContainerOrchestratorClient, LogsCommandOptions, VoidCommandResponse } from '@microsoft/vscode-container-client'; import * as path from 'path'; import { l10n, Uri, workspace } from 'vscode'; -import { ext } from '../../extensionVariables'; +import { ext } from '../../extensionVariables'; import { TaskCommandRunnerFactory } from '../../runtimes/runners/TaskCommandRunnerFactory'; import { ContainerGroupTreeItem } from '../../tree/containers/ContainerGroupTreeItem'; import { ContainerTreeItem } from '../../tree/containers/ContainerTreeItem'; @@ -41,9 +41,13 @@ export async function composeGroupRestart(context: IActionContext, node: Contain return composeGroup(context, (client, options) => client.restart(options), node); } -export async function composeGroupDown(context: IActionContext, node: ContainerGroupTreeItem): Promise { +export async function composeGroupDown(context: IActionContext, node: ContainerGroupTreeItem): Promise { return composeGroup(context, (client, options) => client.down(options), node); -} +} + +export async function composeGroupPull(context: IActionContext, node: ContainerGroupTreeItem): Promise { + return composeGroup(context, (client, options) => client.pull(options), node); +} type AdditionalOptions = Omit; diff --git a/extensions/vscode-containers/src/commands/registerCommands.ts b/extensions/vscode-containers/src/commands/registerCommands.ts index cea77c8f..afb331ba 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, composePull, composeRestart, composeUp, composeUpSubset } from "./compose/compose"; import { askCopilot } from "./containers/askCopilot"; import { attachShellContainer } from "./containers/attachShellContainer"; import { browseContainer } 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"; @@ -120,6 +120,7 @@ export function registerCommands(): void { registerWorkspaceCommand('vscode-containers.debugging.initializeForDebugging', scaffoldDebugConfig); registerWorkspaceCommand('vscode-containers.compose.down', composeDown); + registerWorkspaceCommand('vscode-containers.compose.pull', composePull); registerWorkspaceCommand('vscode-containers.compose.restart', composeRestart); registerWorkspaceCommand('vscode-containers.compose.up', composeUp); registerWorkspaceCommand('vscode-containers.compose.up.subset', composeUpSubset); @@ -144,6 +145,7 @@ export function registerCommands(): void { registerWorkspaceCommand('vscode-containers.containers.stats', stats); registerWorkspaceCommand('vscode-containers.containers.viewLogs', viewContainerLogs); registerWorkspaceCommand('vscode-containers.containers.composeGroup.logs', composeGroupLogs); + registerWorkspaceCommand('vscode-containers.containers.composeGroup.pull', composeGroupPull); registerWorkspaceCommand('vscode-containers.containers.composeGroup.start', composeGroupStart); registerWorkspaceCommand('vscode-containers.containers.composeGroup.stop', composeGroupStop); registerWorkspaceCommand('vscode-containers.containers.composeGroup.restart', composeGroupRestart); diff --git a/extensions/vscode-containers/src/runtimes/OrchestratorRuntimeManager.ts b/extensions/vscode-containers/src/runtimes/OrchestratorRuntimeManager.ts index d63d0bc0..135bb34e 100644 --- a/extensions/vscode-containers/src/runtimes/OrchestratorRuntimeManager.ts +++ b/extensions/vscode-containers/src/runtimes/OrchestratorRuntimeManager.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DockerComposeClient, IContainerOrchestratorClient } from '@microsoft/vscode-container-client'; +import { DockerComposeClient, IContainerOrchestratorClient } from '@microsoft/vscode-container-client'; import * as vscode from 'vscode'; import { configPrefix } from '../constants'; import { RuntimeManager } from './RuntimeManager'; @@ -44,6 +44,6 @@ interface ComposeV2ableOrchestratorClient extends IContainerOrchestratorClient { composeV2: boolean; } -export function isComposeV2ableOrchestratorClient(maybeClient: IContainerOrchestratorClient): maybeClient is ComposeV2ableOrchestratorClient { - return 'composeV2' in maybeClient && typeof (maybeClient as ComposeV2ableOrchestratorClient).composeV2 === 'boolean'; -} +export function isComposeV2ableOrchestratorClient(maybeClient: IContainerOrchestratorClient): maybeClient is ComposeV2ableOrchestratorClient { + return 'composeV2' in maybeClient && typeof (maybeClient as ComposeV2ableOrchestratorClient).composeV2 === 'boolean'; +} diff --git a/packages/vscode-container-client/src/clients/DockerComposeClientBase/DockerComposeClientBase.ts b/packages/vscode-container-client/src/clients/DockerComposeClientBase/DockerComposeClientBase.ts index 4f95ef2f..a59aabc2 100644 --- a/packages/vscode-container-client/src/clients/DockerComposeClientBase/DockerComposeClientBase.ts +++ b/packages/vscode-container-client/src/clients/DockerComposeClientBase/DockerComposeClientBase.ts @@ -21,7 +21,8 @@ import type { ConfigItem, DownCommandOptions, IContainerOrchestratorClient, - LogsCommandOptions, + LogsCommandOptions, + PullCommandOptions, RestartCommandOptions, StartCommandOptions, StopCommandOptions, @@ -144,9 +145,30 @@ export abstract class DockerComposeClientBase extends ConfigurableClient impleme return this.makeVoidCommandResponse(this.getDownCommandArgs(options)); } - //#endregion Down command - - //#region Start command + //#endregion Down command + + //#region Pull command + + protected getPullCommandArgs(options: PullCommandOptions): CommandLineArgs { + return composeArgs( + withComposeArg(this.composeV2), + withCommonOrchestratorArgs(options), + withArg('pull'), + )(); + } + + /** + * Generates the necessary information for pulling service images with Docker Compose + * @param options Standard orchestrator pull command options + * @returns A CommandResponse indicating how to run a Docker Compose pull command + */ + public pull(options: PullCommandOptions): Promise { + return this.makeVoidCommandResponse(this.getPullCommandArgs(options)); + } + + //#endregion Pull command + + //#region Start command protected getStartCommandArgs(options: StartCommandOptions): CommandLineArgs { return composeArgs( diff --git a/packages/vscode-container-client/src/contracts/ContainerOrchestratorClient.ts b/packages/vscode-container-client/src/contracts/ContainerOrchestratorClient.ts index 1dd593fd..9fe5e5e6 100644 --- a/packages/vscode-container-client/src/contracts/ContainerOrchestratorClient.ts +++ b/packages/vscode-container-client/src/contracts/ContainerOrchestratorClient.ts @@ -139,6 +139,17 @@ type DownCommand = { down(options: DownCommandOptions): Promise; }; +// Pull command types +export type PullCommandOptions = CommonOrchestratorCommandOptions; + +type PullCommand = { + /** + * Generate a {@link VoidCommandResponse} for pulling service images with a container orchestrator + * @param options Command options + */ + pull(options: PullCommandOptions): Promise; +}; + // Start command types export type StartCommandOptions = CommonOrchestratorCommandOptions & { /** @@ -247,6 +258,7 @@ export interface IContainerOrchestratorClient extends CheckOrchestratorInstallCommand, UpCommand, DownCommand, + PullCommand, StartCommand, StopCommand, RestartCommand, diff --git a/packages/vscode-container-client/src/test/clients/DockerComposeClient/DockerComposeClient.test.ts b/packages/vscode-container-client/src/test/clients/DockerComposeClient/DockerComposeClient.test.ts index 95b694bb..867d036e 100644 --- a/packages/vscode-container-client/src/test/clients/DockerComposeClient/DockerComposeClient.test.ts +++ b/packages/vscode-container-client/src/test/clients/DockerComposeClient/DockerComposeClient.test.ts @@ -20,7 +20,7 @@ describe('(unit) DockerComposeClient', () => { const client = new DockerComposeClient(); client.composeV2 = false; - it('Should produce the expected lack of quoting/escaping customOptions', async () => { + it('Should produce the expected lack of quoting/escaping customOptions', async () => { const options: UpCommandOptions = { ...commonOptions, detached: true, @@ -40,5 +40,15 @@ describe('(unit) DockerComposeClient', () => { expect(bashQuoted).to.deep.equal(['--file', '\'docker-compose.yml\'', 'up', '--detach', '--build', '--timeout 10 --wait']); expect(noShellQuotedWindows).to.deep.equal(['--file', '"docker-compose.yml"', 'up', '--detach', '--build', '--timeout 10 --wait']); expect(noShellQuotedLinux).to.deep.equal(['--file', 'docker-compose.yml', 'up', '--detach', '--build', '--timeout 10 --wait']); - }); -}); + }); + + it('Should produce the expected compose pull command', async () => { + const commandResponse = await client.pull(commonOptions); + + expect(new NoShell(false).quote(commandResponse.args)).to.deep.equal([ + '--file', + 'docker-compose.yml', + 'pull', + ]); + }); +}); diff --git a/packages/vscode-container-client/src/test/clients/NerdctlComposeClient/NerdctlComposeClient.test.ts b/packages/vscode-container-client/src/test/clients/NerdctlComposeClient/NerdctlComposeClient.test.ts index a466f326..a1a13b42 100644 --- a/packages/vscode-container-client/src/test/clients/NerdctlComposeClient/NerdctlComposeClient.test.ts +++ b/packages/vscode-container-client/src/test/clients/NerdctlComposeClient/NerdctlComposeClient.test.ts @@ -51,4 +51,11 @@ describe('(unit) NerdctlComposeClient', () => { expect(args).to.deep.equal(['compose', '--file', 'docker-compose.yml', 'down', '--volumes']); expect(args).to.not.include('--timeout'); }); + + it('Should pull all service images with the V2 compose syntax', async () => { + const response = await client.pull(commonOptions); + const args = new NoShell(false).quote(response.args); + + expect(args).to.deep.equal(['compose', '--file', 'docker-compose.yml', 'pull']); + }); });