diff --git a/extensions/vscode-containers/package.json b/extensions/vscode-containers/package.json index c9656f64..947d55de 100644 --- a/extensions/vscode-containers/package.json +++ b/extensions/vscode-containers/package.json @@ -403,7 +403,17 @@ }, { "command": "vscode-containers.containers.start", - "when": "view == vscode-containers.views.containers && viewItem =~ /^(created|dead|exited|paused|terminated)Container$/i", + "when": "view == vscode-containers.views.containers && viewItem =~ /^(created|dead|exited|terminated)Container$/i", + "group": "containers_2_active@5" + }, + { + "command": "vscode-containers.containers.unpause", + "when": "view == vscode-containers.views.containers && viewItem =~ /^pausedContainer$/i", + "group": "containers_2_active@5" + }, + { + "command": "vscode-containers.containers.pause", + "when": "view == vscode-containers.views.containers && viewItem =~ /^runningContainer$/i", "group": "containers_2_active@5" }, { @@ -2723,6 +2733,11 @@ "title": "%vscode-containers.commands.containers.group.remove%", "category": "%vscode-containers.commands.category.containers%" }, + { + "command": "vscode-containers.containers.pause", + "title": "%vscode-containers.commands.containers.pause%", + "category": "%vscode-containers.commands.category.containers%" + }, { "command": "vscode-containers.containers.restart", "title": "%vscode-containers.commands.containers.restart%", @@ -2743,6 +2758,11 @@ "title": "%vscode-containers.commands.containers.stop%", "category": "%vscode-containers.commands.category.containers%" }, + { + "command": "vscode-containers.containers.unpause", + "title": "%vscode-containers.commands.containers.unpause%", + "category": "%vscode-containers.commands.category.containers%" + }, { "command": "vscode-containers.containers.stats", "title": "%vscode-containers.commands.containers.stats%", diff --git a/extensions/vscode-containers/package.nls.json b/extensions/vscode-containers/package.nls.json index 349b01c7..a11c2439 100644 --- a/extensions/vscode-containers/package.nls.json +++ b/extensions/vscode-containers/package.nls.json @@ -240,6 +240,7 @@ "vscode-containers.commands.containers.downloadFile": "Download...", "vscode-containers.commands.containers.inspect": "Inspect", "vscode-containers.commands.containers.openFile": "Open", + "vscode-containers.commands.containers.pause": "Pause", "vscode-containers.commands.containers.prune": "Prune...", "vscode-containers.commands.containers.refresh": "Refresh", "vscode-containers.commands.containers.remove": "Remove...", @@ -248,6 +249,7 @@ "vscode-containers.commands.containers.select": "Select container", "vscode-containers.commands.containers.start": "Start", "vscode-containers.commands.containers.stop": "Stop", + "vscode-containers.commands.containers.unpause": "Unpause", "vscode-containers.commands.containers.stats": "Stats", "vscode-containers.commands.containers.viewLogs": "View Logs", "vscode-containers.commands.containers.composeGroup.logs": "Compose Logs", diff --git a/extensions/vscode-containers/src/commands/containers/pauseContainer.ts b/extensions/vscode-containers/src/commands/containers/pauseContainer.ts new file mode 100644 index 00000000..4b37ee23 --- /dev/null +++ b/extensions/vscode-containers/src/commands/containers/pauseContainer.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IActionContext } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import { ext } from '../../extensionVariables'; +import { ContainerTreeItem } from '../../tree/containers/ContainerTreeItem'; +import { multiSelectNodes } from '../../utils/multiSelectNodes'; + +export async function pauseContainer(context: IActionContext, node?: ContainerTreeItem, nodes?: ContainerTreeItem[]): Promise { + nodes = await multiSelectNodes( + { ...context, noItemFoundErrorMessage: vscode.l10n.t('No containers are available to pause') }, + ext.containersTree, + /^runningContainer$/i, + node, + nodes + ); + + await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Pausing Container(s)...') }, async () => { + await ext.runWithDefaults(client => + client.pauseContainers({ container: nodes.map(n => n.containerId) }) + ); + }); +} diff --git a/extensions/vscode-containers/src/commands/containers/startContainer.ts b/extensions/vscode-containers/src/commands/containers/startContainer.ts index c0c12b77..f409c355 100644 --- a/extensions/vscode-containers/src/commands/containers/startContainer.ts +++ b/extensions/vscode-containers/src/commands/containers/startContainer.ts @@ -13,7 +13,7 @@ export async function startContainer(context: IActionContext, node?: ContainerTr nodes = await multiSelectNodes( { ...context, noItemFoundErrorMessage: vscode.l10n.t('No containers are available to start') }, ext.containersTree, - /^(created|dead|exited|paused|terminated)Container$/i, + /^(created|dead|exited|terminated)Container$/i, node, nodes ); diff --git a/extensions/vscode-containers/src/commands/containers/unpauseContainer.ts b/extensions/vscode-containers/src/commands/containers/unpauseContainer.ts new file mode 100644 index 00000000..986ab982 --- /dev/null +++ b/extensions/vscode-containers/src/commands/containers/unpauseContainer.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IActionContext } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import { ext } from '../../extensionVariables'; +import { ContainerTreeItem } from '../../tree/containers/ContainerTreeItem'; +import { multiSelectNodes } from '../../utils/multiSelectNodes'; + +export async function unpauseContainer(context: IActionContext, node?: ContainerTreeItem, nodes?: ContainerTreeItem[]): Promise { + nodes = await multiSelectNodes( + { ...context, noItemFoundErrorMessage: vscode.l10n.t('No containers are available to unpause') }, + ext.containersTree, + /^pausedContainer$/i, + node, + nodes + ); + + await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Unpausing Container(s)...') }, async () => { + await ext.runWithDefaults(client => + client.unpauseContainers({ container: nodes.map(n => n.containerId) }) + ); + }); +} diff --git a/extensions/vscode-containers/src/commands/registerCommands.ts b/extensions/vscode-containers/src/commands/registerCommands.ts index cea77c8f..aab69066 100644 --- a/extensions/vscode-containers/src/commands/registerCommands.ts +++ b/extensions/vscode-containers/src/commands/registerCommands.ts @@ -19,6 +19,7 @@ import { configureContainersExplorer } from "./containers/configureContainersExp import { downloadContainerFile } from "./containers/files/downloadContainerFile"; import { openContainerFile } from "./containers/files/openContainerFile"; import { inspectContainer } from "./containers/inspectContainer"; +import { pauseContainer } from "./containers/pauseContainer"; import { pruneContainers } from "./containers/pruneContainers"; import { removeContainer } from "./containers/removeContainer"; import { removeContainerGroup } from "./containers/removeContainerGroup"; @@ -27,6 +28,7 @@ import { selectContainer } from "./containers/selectContainer"; import { startContainer } from "./containers/startContainer"; import { stats } from "./containers/stats"; import { stopContainer } from "./containers/stopContainer"; +import { unpauseContainer } from "./containers/unpauseContainer"; import { viewContainerLogs } from "./containers/viewContainerLogs"; import { configureDockerContextsExplorer, dockerContextsHelp } from "./context/DockerContextsViewCommands"; import { inspectDockerContext } from "./context/inspectDockerContext"; @@ -134,6 +136,7 @@ export function registerCommands(): void { registerCommand('vscode-containers.containers.filter', filterContainersTree); registerCommand('vscode-containers.containers.clearFilter', clearContainersFilter); registerCommand('vscode-containers.containers.openFile', openContainerFile); + registerCommand('vscode-containers.containers.pause', pauseContainer); registerCommand('vscode-containers.containers.prune', pruneContainers); registerCommand('vscode-containers.containers.remove', removeContainer); registerCommand('vscode-containers.containers.group.remove', removeContainerGroup); @@ -141,6 +144,7 @@ export function registerCommands(): void { registerCommand('vscode-containers.containers.select', selectContainer); registerCommand('vscode-containers.containers.start', startContainer); registerCommand('vscode-containers.containers.stop', stopContainer); + registerCommand('vscode-containers.containers.unpause', unpauseContainer); registerWorkspaceCommand('vscode-containers.containers.stats', stats); registerWorkspaceCommand('vscode-containers.containers.viewLogs', viewContainerLogs); registerWorkspaceCommand('vscode-containers.containers.composeGroup.logs', composeGroupLogs); diff --git a/packages/vscode-container-client/src/clients/DockerClientBase/DockerClientBase.ts b/packages/vscode-container-client/src/clients/DockerClientBase/DockerClientBase.ts index 928732c1..fb566b68 100644 --- a/packages/vscode-container-client/src/clients/DockerClientBase/DockerClientBase.ts +++ b/packages/vscode-container-client/src/clients/DockerClientBase/DockerClientBase.ts @@ -57,6 +57,7 @@ import type { LoginCommandOptions, LogoutCommandOptions, LogsForContainerCommandOptions, + PauseContainersCommandOptions, PruneContainersCommandOptions, PruneContainersItem, PruneImagesCommandOptions, @@ -80,6 +81,7 @@ import type { StatPathItem, StopContainersCommandOptions, TagImageCommandOptions, + UnpauseContainersCommandOptions, UseContextCommandOptions, VersionCommandOptions, VersionItem, @@ -862,6 +864,58 @@ export abstract class DockerClientBase extends ConfigurableClient implements ICo //#endregion + //#region PauseContainers Command + + protected getPauseContainersCommandArgs(options: PauseContainersCommandOptions): CommandLineArgs { + return composeArgs( + withArg('container', 'pause'), + withArg(...toArray(options.container)), + )(); + } + + protected parsePauseContainersCommandOutput( + options: PauseContainersCommandOptions, + output: string, + strict: boolean, + ): Promise> { + return Promise.resolve(asIds(output)); + } + + pauseContainers(options: PauseContainersCommandOptions): Promise>> { + return this.makeCommandResponse( + this.getPauseContainersCommandArgs(options), + (output, strict) => this.parsePauseContainersCommandOutput(options, output, strict), + ); + } + + //#endregion + + //#region UnpauseContainers Command + + protected getUnpauseContainersCommandArgs(options: UnpauseContainersCommandOptions): CommandLineArgs { + return composeArgs( + withArg('container', 'unpause'), + withArg(...toArray(options.container)), + )(); + } + + protected parseUnpauseContainersCommandOutput( + options: UnpauseContainersCommandOptions, + output: string, + strict: boolean, + ): Promise> { + return Promise.resolve(asIds(output)); + } + + unpauseContainers(options: UnpauseContainersCommandOptions): Promise>> { + return this.makeCommandResponse( + this.getUnpauseContainersCommandArgs(options), + (output, strict) => this.parseUnpauseContainersCommandOutput(options, output, strict), + ); + } + + //#endregion + //#region RestartContainers Command protected getRestartContainersCommandArgs(options: RestartContainersCommandOptions): CommandLineArgs { diff --git a/packages/vscode-container-client/src/clients/WslcClient/WslcClient.ts b/packages/vscode-container-client/src/clients/WslcClient/WslcClient.ts index 224d4262..618959f1 100644 --- a/packages/vscode-container-client/src/clients/WslcClient/WslcClient.ts +++ b/packages/vscode-container-client/src/clients/WslcClient/WslcClient.ts @@ -34,6 +34,7 @@ import type { ListNetworksCommandOptions, ListVolumeItem, ListVolumesCommandOptions, + PauseContainersCommandOptions, PruneContainersCommandOptions, PruneImagesCommandOptions, PruneNetworksCommandOptions, @@ -46,6 +47,7 @@ import type { RemoveNetworksCommandOptions, RestartContainersCommandOptions, RunContainerCommandOptions, + UnpauseContainersCommandOptions, VersionCommandOptions, VersionItem, WriteFileCommandOptions, @@ -366,6 +368,18 @@ export class WslcClient extends DockerClientBase { return Promise.reject(new CommandNotSupportedError('wslc does not support the restart command.')); } + // wslc has no `pause` subcommand. Reject rather than silently inheriting + // a command line that wslc would reject. + public override pauseContainers(options: PauseContainersCommandOptions): Promise>> { + return Promise.reject(new CommandNotSupportedError('wslc does not support the pause command.')); + } + + // wslc has no `unpause` subcommand. Reject rather than silently inheriting + // a command line that wslc would reject. + public override unpauseContainers(options: UnpauseContainersCommandOptions): Promise>> { + return Promise.reject(new CommandNotSupportedError('wslc does not support the unpause command.')); + } + protected override getInspectContainersCommandArgs(options: InspectContainersCommandOptions): CommandLineArgs { return this.getWslcInspectCommandArgs('container', options.containers); } diff --git a/packages/vscode-container-client/src/contracts/ContainerClient.ts b/packages/vscode-container-client/src/contracts/ContainerClient.ts index a714e466..23e0b642 100644 --- a/packages/vscode-container-client/src/contracts/ContainerClient.ts +++ b/packages/vscode-container-client/src/contracts/ContainerClient.ts @@ -906,6 +906,40 @@ type StartContainersCommand = { startContainers(options: StartContainersCommandOptions): Promise>>; }; +// Pause Containers Command Types + +export type PauseContainersCommandOptions = CommonCommandOptions & { + /** + * Containers to pause + */ + container: Array; +}; + +type PauseContainersCommand = { + /** + * Generate a CommandResponse for pausing container(s). + * @param options Command options + */ + pauseContainers(options: PauseContainersCommandOptions): Promise>>; +}; + +// Unpause Containers Command Types + +export type UnpauseContainersCommandOptions = CommonCommandOptions & { + /** + * Containers to unpause + */ + container: Array; +}; + +type UnpauseContainersCommand = { + /** + * Generate a CommandResponse for unpausing container(s). + * @param options Command options + */ + unpauseContainers(options: UnpauseContainersCommandOptions): Promise>>; +}; + // Restart Containers Command Types export type RestartContainersCommandOptions = CommonCommandOptions & { @@ -1883,6 +1917,8 @@ export interface IContainersClient extends ExecContainerCommand, ListContainersCommand, StartContainersCommand, + PauseContainersCommand, + UnpauseContainersCommand, RestartContainersCommand, StopContainersCommand, RemoveContainersCommand, diff --git a/packages/vscode-container-client/src/test/ContainersClientE2E.test.ts b/packages/vscode-container-client/src/test/ContainersClientE2E.test.ts index 9c34e2b7..741eba0e 100644 --- a/packages/vscode-container-client/src/test/ContainersClientE2E.test.ts +++ b/packages/vscode-container-client/src/test/ContainersClientE2E.test.ts @@ -634,6 +634,32 @@ describe('(integration) ContainersClientE2E', function () { expect(startedContainer.state.toLowerCase()).to.equal('running'); }); + it('PauseAndUnpauseContainersCommands', async function () { + if (clientTypeToTest === 'wslc') { + this.skip(); // wslc has no `pause` or `unpause` subcommands + } + + const pausedContainers = await defaultRunner.getCommandRunner()( + client.pauseContainers({ container: [testContainerId] }) + ); + + expect(pausedContainers).to.be.an('array'); + expect(pausedContainers).to.include(testContainerId); + + const pausedContainer = (await validateContainerExists(client, defaultRunner, { containerId: testContainerId }))!; + expect(pausedContainer.state.toLowerCase()).to.equal('paused'); + + const unpausedContainers = await defaultRunner.getCommandRunner()( + client.unpauseContainers({ container: [testContainerId] }) + ); + + expect(unpausedContainers).to.be.an('array'); + expect(unpausedContainers).to.include(testContainerId); + + const runningContainer = (await validateContainerExists(client, defaultRunner, { containerId: testContainerId }))!; + expect(runningContainer.state.toLowerCase()).to.equal('running'); + }); + it('RestartContainersCommand', async function () { if (clientTypeToTest === 'wslc') { this.skip(); // wslc has no `restart` subcommand diff --git a/packages/vscode-container-client/src/test/clients/DockerClient/DockerClient.test.ts b/packages/vscode-container-client/src/test/clients/DockerClient/DockerClient.test.ts index 73dec79a..e2b30c93 100644 --- a/packages/vscode-container-client/src/test/clients/DockerClient/DockerClient.test.ts +++ b/packages/vscode-container-client/src/test/clients/DockerClient/DockerClient.test.ts @@ -17,6 +17,36 @@ import type { BuildImageCommandOptions, RunContainerCommandOptions } from '../.. describe('(unit) DockerClient', () => { const client = new DockerClient(); + describe('#pauseContainers()', () => { + it('generates the command and parses paused container IDs', async () => { + const commandResult = await client.pauseContainers({ container: ['abc', 'def'] }); + + expect(commandResult).to.have.a.property('command', 'docker'); + expect(commandResult.args).to.deep.equal([ + escaped('container'), + escaped('pause'), + escaped('abc'), + escaped('def'), + ]); + expect(await commandResult.parse('abc\ndef\n', true)).to.deep.equal(['abc', 'def']); + }); + }); + + describe('#unpauseContainers()', () => { + it('generates the command and parses unpaused container IDs', async () => { + const commandResult = await client.unpauseContainers({ container: ['abc', 'def'] }); + + expect(commandResult).to.have.a.property('command', 'docker'); + expect(commandResult.args).to.deep.equal([ + escaped('container'), + escaped('unpause'), + escaped('abc'), + escaped('def'), + ]); + expect(await commandResult.parse('abc\ndef\n', true)).to.deep.equal(['abc', 'def']); + }); + }); + describe('#listImagesAsync()', () => { it('parses date formats', async () => { const commandResult = await client.listImages({}); diff --git a/packages/vscode-container-client/src/test/clients/WslcClient/WslcCanary.test.ts b/packages/vscode-container-client/src/test/clients/WslcClient/WslcCanary.test.ts index 1e403741..6843a092 100644 --- a/packages/vscode-container-client/src/test/clients/WslcClient/WslcCanary.test.ts +++ b/packages/vscode-container-client/src/test/clients/WslcClient/WslcCanary.test.ts @@ -63,6 +63,8 @@ describe('(integration) WslcCanary', function () { { description: '`events`', args: 'events --help', unrecognizedToken: 'events', workaround: 'WslcClient.getEventStream rejects with CommandNotSupportedError' }, { description: '`info`', args: 'info --help', unrecognizedToken: 'info', workaround: 'WslcClient.getInfoCommandArgs/parseInfoCommandOutput synthesize a linux InfoItem' }, { description: '`container restart`', args: 'container restart --help', unrecognizedToken: 'restart', workaround: 'WslcClient.restartContainers rejects with CommandNotSupportedError' }, + { description: '`container pause`', args: 'container pause --help', unrecognizedToken: 'pause', workaround: 'WslcClient.pauseContainers rejects with CommandNotSupportedError' }, + { description: '`container unpause`', args: 'container unpause --help', unrecognizedToken: 'unpause', workaround: 'WslcClient.unpauseContainers rejects with CommandNotSupportedError' }, ]; cases.forEach(({ description, args, unrecognizedToken, workaround }) => { diff --git a/packages/vscode-container-client/src/test/clients/WslcClient/WslcClient.test.ts b/packages/vscode-container-client/src/test/clients/WslcClient/WslcClient.test.ts index 7e2be0d0..fbf9df5f 100644 --- a/packages/vscode-container-client/src/test/clients/WslcClient/WslcClient.test.ts +++ b/packages/vscode-container-client/src/test/clients/WslcClient/WslcClient.test.ts @@ -499,6 +499,14 @@ describe('(unit) WslcClient', () => { it('restartContainers rejects with CommandNotSupportedError', async () => { await expectRejection(client.restartContainers({ container: ['abc'] })); }); + + it('pauseContainers rejects with CommandNotSupportedError', async () => { + await expectRejection(client.pauseContainers({ container: ['abc'] })); + }); + + it('unpauseContainers rejects with CommandNotSupportedError', async () => { + await expectRejection(client.unpauseContainers({ container: ['abc'] })); + }); }); describe('#listVolumes()', () => {