diff --git a/package.json b/package.json index 4a0ec5560c..e39776b842 100644 --- a/package.json +++ b/package.json @@ -1242,6 +1242,112 @@ ] } } + }, + { + "type": "docker-compose", + "properties": { + "dockerCompose": { + "description": "%vscode-docker.tasks.docker-compose.dockerCompose.description%", + "properties": { + "up": { + "description": "%vscode-docker.tasks.docker-compose.dockerCompose.up.description%", + "properties": { + "detached": { + "type": "boolean", + "description": "%vscode-docker.tasks.docker-compose.dockerCompose.up.detached%", + "default": true + }, + "build": { + "type": "boolean", + "description": "%vscode-docker.tasks.docker-compose.dockerCompose.up.build%", + "default": true + }, + "scale": { + "type": "object", + "description": "%vscode-docker.tasks.docker-compose.dockerCompose.up.scale%", + "additionalProperties": { + "type": "number" + } + }, + "services": { + "type": "array", + "description": "%vscode-docker.tasks.docker-compose.dockerCompose.up.services%", + "items": { + "type": "string" + } + }, + "customOptions": { + "type": "string", + "description": "%vscode-docker.tasks.docker-compose.dockerCompose.up.customOptions%" + } + } + }, + "down": { + "description": "%vscode-docker.tasks.docker-compose.dockerCompose.down.description%", + "properties": { + "removeImages": { + "type": "string", + "description": "%vscode-docker.tasks.docker-compose.dockerCompose.down.removeImages%", + "enum": [ + "all", + "local" + ] + }, + "removeVolumes": { + "type": "boolean", + "description": "%vscode-docker.tasks.docker-compose.dockerCompose.down.removeVolumes%", + "default": false + }, + "customOptions": { + "type": "string", + "description": "%vscode-docker.tasks.docker-compose.dockerCompose.down.customOptions%" + } + } + }, + "files": { + "type": "array", + "description": "%vscode-docker.tasks.docker-compose.dockerCompose.files.description%", + "items": { + "type": "string" + } + } + }, + "oneOf": [ + { + "required": [ + "up" + ], + "not": { + "enum": [ + "down" + ] + } + }, + { + "required": [ + "down" + ], + "not": { + "enum": [ + "up" + ] + } + } + ], + "default": { + "up": { + "detached": true, + "build": true + }, + "files": [ + "${workspaceFolder}/docker-compose.yml" + ] + } + } + }, + "required": [ + "dockerCompose" + ] } ], "languages": [ diff --git a/package.nls.json b/package.nls.json index ef828824a4..610a30c436 100644 --- a/package.nls.json +++ b/package.nls.json @@ -98,6 +98,18 @@ "vscode-docker.tasks.docker-run.python.args": "Arguments passed to the Python app.", "vscode-docker.tasks.docker-run.python.wait": "Whether to wait for debugger to attach.", "vscode-docker.tasks.docker-run.python.debugPort": "The port that the debugger will listen on.", + "vscode-docker.tasks.docker-compose.dockerCompose.description": "Options for the `docker-compose` command.", + "vscode-docker.tasks.docker-compose.dockerCompose.up.description": "Options for the `docker-compose up` command.", + "vscode-docker.tasks.docker-compose.dockerCompose.up.detached": "Whether or not to run detached.", + "vscode-docker.tasks.docker-compose.dockerCompose.up.build": "Whether or not to build.", + "vscode-docker.tasks.docker-compose.dockerCompose.up.scale": "The scale for each service.", + "vscode-docker.tasks.docker-compose.dockerCompose.up.services": "A subset of services to start.", + "vscode-docker.tasks.docker-compose.dockerCompose.up.customOptions": "Any other options to add to the `docker-compose up` command.", + "vscode-docker.tasks.docker-compose.dockerCompose.down.description": "Options for the `docker-compose down` command.", + "vscode-docker.tasks.docker-compose.dockerCompose.down.removeImages": "Images to remove.", + "vscode-docker.tasks.docker-compose.dockerCompose.down.removeVolumes": "Whether or not to remove named and anonymous volumes.", + "vscode-docker.tasks.docker-compose.dockerCompose.down.customOptions": "Any other options to add to the `docker-compose down` command.", + "vscode-docker.tasks.docker-compose.dockerCompose.files.description": "The docker-compose files to include, in order.", "vscode-docker.config.docker.promptForRegistryWhenPushingImages": "Prompt for registry selection if the image is not explicitly tagged.", "vscode-docker.config.template.build.template": "The command template.", "vscode-docker.config.template.build.label": "The label displayed to the user.", diff --git a/src/commands/compose.ts b/src/commands/compose/compose.ts similarity index 67% rename from src/commands/compose.ts rename to src/commands/compose/compose.ts index d5dad32f3f..9f63e8de60 100644 --- a/src/commands/compose.ts +++ b/src/commands/compose/compose.ts @@ -5,13 +5,13 @@ import * as vscode from 'vscode'; import { IActionContext } from 'vscode-azureextensionui'; -import { isNewContextType } from '../docker/Contexts'; -import { ext } from '../extensionVariables'; -import { localize } from "../localize"; -import { executeAsTask } from '../utils/executeAsTask'; -import { createFileItem, Item, quickPickDockerComposeFileItem } from '../utils/quickPickFile'; -import { quickPickWorkspaceFolder } from '../utils/quickPickWorkspaceFolder'; -import { selectComposeCommand } from './selectCommandTemplate'; +import { rewriteComposeCommandIfNeeded } from '../../docker/Contexts'; +import { localize } from "../../localize"; +import { executeAsTask } from '../../utils/executeAsTask'; +import { createFileItem, Item, quickPickDockerComposeFileItem } from '../../utils/quickPickFile'; +import { quickPickWorkspaceFolder } from '../../utils/quickPickWorkspaceFolder'; +import { selectComposeCommand } from '../selectCommandTemplate'; +import { getComposeServiceList } from './getComposeServiceList'; async function compose(context: IActionContext, commands: ('up' | 'down')[], message: string, dockerComposeFileUri?: vscode.Uri, selectedComposeFileUris?: vscode.Uri[]): Promise { const folder: vscode.WorkspaceFolder = await quickPickWorkspaceFolder(localize('vscode-docker.commands.compose.workspaceFolder', 'To run Docker compose you must first open a folder or workspace in VS Code.')); @@ -38,27 +38,27 @@ async function compose(context: IActionContext, commands: ('up' | 'down')[], mes for (const command of commands) { if (selectedItems.length === 0) { - const terminalCommand = await selectComposeCommand( + // Push a dummy item in so that we can use the looping logic below + selectedItems.push(undefined); + } + + for (const item of selectedItems) { + let terminalCommand = await selectComposeCommand( context, folder, command, - undefined, + item?.relativeFilePath, detached, build ); - await executeAsTask(context, await rewriteCommandForNewCliIfNeeded(terminalCommand), 'Docker Compose', { addDockerEnv: true, workspaceFolder: folder }); - } else { - for (const item of selectedItems) { - const terminalCommand = await selectComposeCommand( - context, - folder, - command, - item.relativeFilePath, - detached, - build - ); - await executeAsTask(context, await rewriteCommandForNewCliIfNeeded(terminalCommand), 'Docker Compose', { addDockerEnv: true, workspaceFolder: folder }); - } + + // Add the service list if needed + terminalCommand = await addServicesListIfNeeded(context, folder, terminalCommand); + + // Rewrite for the new CLI if needed + terminalCommand = await rewriteComposeCommandIfNeeded(terminalCommand); + + await executeAsTask(context, terminalCommand, 'Docker Compose', { addDockerEnv: true, workspaceFolder: folder }); } } } @@ -75,10 +75,10 @@ export async function composeRestart(context: IActionContext, dockerComposeFileU return await compose(context, ['down', 'up'], localize('vscode-docker.commands.compose.chooseRestart', 'Choose Docker Compose file to restart'), dockerComposeFileUri, selectedComposeFileUris); } -export async function rewriteCommandForNewCliIfNeeded(command: string): Promise { - if (isNewContextType((await ext.dockerContextManager.getCurrentContext()).ContextType)) { - // Replace 'docker-compose ' at the start of a string with 'docker compose ', and '--build' anywhere with '' - return command.replace(/^docker-compose /, 'docker compose ').replace(/--build/, ''); +const serviceListPlaceholder = /\${serviceList}/i; +async function addServicesListIfNeeded(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder, command: string): Promise { + if (serviceListPlaceholder.test(command)) { + return command.replace(serviceListPlaceholder, await getComposeServiceList(context, workspaceFolder, command)); } else { return command; } diff --git a/src/commands/compose/getComposeServiceList.ts b/src/commands/compose/getComposeServiceList.ts new file mode 100644 index 0000000000..326a9bf931 --- /dev/null +++ b/src/commands/compose/getComposeServiceList.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { IActionContext, IAzureQuickPickItem } from 'vscode-azureextensionui'; +import { ext } from '../../extensionVariables'; +import { localize } from '../../localize'; +import { execAsync } from '../../utils/spawnAsync'; + +// Matches an `up` or `down` and everything after it--so that it can be replaced with `config --services`, to get a service list using all of the files originally part of the compose command +const composeCommandReplaceRegex = /(\b(up|down)\b).*$/i; + +export async function getComposeServiceList(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder, composeCommand: string): Promise { + const services = await getServices(workspaceFolder, composeCommand); + + // Fetch the previously chosen services list. By default, all will be selected. + const workspaceServiceListKey = `vscode-docker.composeServices.${workspaceFolder.name}`; + const previousChoices = ext.context.workspaceState.get(workspaceServiceListKey, services); + + const pickChoices: IAzureQuickPickItem[] = services.map(s => ({ + label: s, + data: s, + picked: previousChoices.some(p => p === s), + })); + + const subsetChoices = + await ext.ui.showQuickPick( + pickChoices, + { + canPickMany: true, + placeHolder: localize('vscode-docker.getComposeServiceList.choose', 'Choose services to start'), + } + ); + + context.telemetry.measurements.totalServices = pickChoices.length; + context.telemetry.measurements.chosenServices = subsetChoices.length; + + // Update the cache + await ext.context.workspaceState.update(workspaceServiceListKey, subsetChoices.map(c => c.data)); + + return subsetChoices.map(c => c.data).join(' '); +} + +async function getServices(workspaceFolder: vscode.WorkspaceFolder, composeCommand: string): Promise { + // Start by getting a new command with the exact same files list (replaces the "up ..." or "down ..." with "config --services") + const configCommand = composeCommand.replace(composeCommandReplaceRegex, 'config --services'); + + const { stdout } = await execAsync(configCommand, { cwd: workspaceFolder.uri.fsPath }); + + // The output of the config command is a list of services, one per line + // Split them up and remove empty entries + return stdout.split(/\r?\n/im).filter(l => { return l; }); +} diff --git a/src/commands/containers/composeGroup.ts b/src/commands/containers/composeGroup.ts index 0acb45fa08..bf3bde9b8c 100644 --- a/src/commands/containers/composeGroup.ts +++ b/src/commands/containers/composeGroup.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { IActionContext } from 'vscode-azureextensionui'; +import { rewriteComposeCommandIfNeeded } from '../../docker/Contexts'; import { localize } from '../../localize'; import { ContainerGroupTreeItem } from '../../tree/containers/ContainerGroupTreeItem'; import { ContainerTreeItem } from '../../tree/containers/ContainerTreeItem'; import { executeAsTask } from '../../utils/executeAsTask'; import { isWindows } from '../../utils/osUtils'; -import { rewriteCommandForNewCliIfNeeded } from '../compose'; export async function composeGroupLogs(context: IActionContext, node: ContainerGroupTreeItem): Promise { return composeGroup(context, 'logs', node, '-f --tail 1000'); @@ -34,7 +34,7 @@ async function composeGroup(context: IActionContext, composeCommand: 'logs' | 'r const terminalCommand = `docker-compose ${filesArgument} ${composeCommand} ${additionalArguments || ''}`; - await executeAsTask(context, await rewriteCommandForNewCliIfNeeded(terminalCommand), 'Docker Compose', { addDockerEnv: true, cwd: workingDirectory, }); + await executeAsTask(context, await rewriteComposeCommandIfNeeded(terminalCommand), 'Docker Compose', { addDockerEnv: true, cwd: workingDirectory, }); } function getComposeWorkingDirectory(node: ContainerGroupTreeItem): string | undefined { diff --git a/src/commands/containers/confirmAllAffectedContainers.ts b/src/commands/containers/confirmAllAffectedContainers.ts index ea31f3bd82..1cc59b7e3a 100644 --- a/src/commands/containers/confirmAllAffectedContainers.ts +++ b/src/commands/containers/confirmAllAffectedContainers.ts @@ -10,7 +10,7 @@ import { getComposeProjectName, NonComposeGroupName } from '../../tree/container import { ContainerTreeItem } from '../../tree/containers/ContainerTreeItem'; export async function confirmAllAffectedContainers(context: IActionContext, nodes: ContainerTreeItem[]): Promise { - if ((await ext.dockerContextManager.getCurrentContext()).ContextType !== 'aci' || + if (await ext.dockerContextManager.getCurrentContextType() !== 'aci' || nodes.every(n => getComposeProjectName(n.containerItem) === NonComposeGroupName)) { // If we're not in an ACI context, or every node in the list is not part of any ACI container group, return unchanged return nodes.map(n => n.containerId); diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 720d521b87..df4b80477a 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -9,7 +9,7 @@ import { ext } from "../extensionVariables"; import { scaffold } from "../scaffolding/scaffold"; import { scaffoldCompose } from "../scaffolding/scaffoldCompose"; import { scaffoldDebugConfig } from "../scaffolding/scaffoldDebugConfig"; -import { composeDown, composeRestart, composeUp } from "./compose"; +import { composeDown, composeRestart, composeUp } from "./compose/compose"; import { attachShellContainer } from "./containers/attachShellContainer"; import { browseContainer } from "./containers/browseContainer"; import { composeGroupDown, composeGroupLogs, composeGroupRestart } from "./containers/composeGroup"; diff --git a/src/commands/selectCommandTemplate.ts b/src/commands/selectCommandTemplate.ts index 5257e9dce1..e170311512 100644 --- a/src/commands/selectCommandTemplate.ts +++ b/src/commands/selectCommandTemplate.ts @@ -101,7 +101,7 @@ export async function selectComposeCommand(context: IActionContext, folder: vsco // Exported only for tests export async function selectCommandTemplate(context: IActionContext, command: TemplateCommand, matchContext: string[], folder: vscode.WorkspaceFolder | undefined, additionalVariables: { [key: string]: string }): Promise { // Get the current context type - const currentContextType = (await ext.dockerContextManager.getCurrentContext()).ContextType; + const currentContextType = await ext.dockerContextManager.getCurrentContextType(); // Get the configured settings values const config = vscode.workspace.getConfiguration('docker'); diff --git a/src/docker/ContextManager.ts b/src/docker/ContextManager.ts index 399fe0ea18..abac6cbef1 100644 --- a/src/docker/ContextManager.ts +++ b/src/docker/ContextManager.ts @@ -17,7 +17,7 @@ import { localize } from '../localize'; import { AsyncLazy } from '../utils/lazy'; import { isWindows } from '../utils/osUtils'; import { execAsync, spawnAsync } from '../utils/spawnAsync'; -import { DockerContext, DockerContextInspection, isNewContextType } from './Contexts'; +import { ContextType, DockerContext, DockerContextInspection, isNewContextType } from './Contexts'; // CONSIDER // Any of the commands related to Docker context can take a very long time to execute (a minute or longer) @@ -48,6 +48,7 @@ export interface ContextManager { refresh(): Promise; getContexts(): Promise; getCurrentContext(): Promise; + getCurrentContextType(): Promise; inspect(actionContext: IActionContext, contextName: string): Promise; use(actionContext: IActionContext, contextName: string): Promise; @@ -164,6 +165,10 @@ export class DockerContextManager implements ContextManager, Disposable { return contexts.find(c => c.Current); } + public async getCurrentContextType(): Promise { + return (await this.getCurrentContext()).ContextType; + } + public async inspect(actionContext: IActionContext, contextName: string): Promise { const { stdout } = await execAsync(`docker context inspect ${contextName}`, { timeout: 10000 }); diff --git a/src/docker/Contexts.ts b/src/docker/Contexts.ts index a8f3469fbf..bf51b1eb66 100644 --- a/src/docker/Contexts.ts +++ b/src/docker/Contexts.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ext } from '../extensionVariables'; import { DockerObject } from './Common'; export type ContextType = 'aci' | 'moby'; @@ -22,6 +23,7 @@ export interface DockerContextInspection { readonly [key: string]: unknown; } +// This method cannot be async and use `ext.dockerContextManager.getCurrentContextType()` like the below, because it is used internally by `DockerContextManager.refresh()` export function isNewContextType(contextType: ContextType): boolean { switch (contextType) { case 'moby': @@ -31,3 +33,21 @@ export function isNewContextType(contextType: ContextType): boolean { return true; } } + +export async function getComposeCliCommand(): Promise { + return isNewContextType(await ext.dockerContextManager.getCurrentContextType()) ? 'docker compose' : 'docker-compose'; +} + +// This method is needed because in certain scenarios--e.g. command customization--the compose command is defined by the user +// In order to support backwards compatibility, we rewrite the command, rather than building it correctly from the beginning with `getComposeCliCommand()` +export async function rewriteComposeCommandIfNeeded(command: string): Promise { + // Replace 'docker-compose' or 'docker compose' at the start of a string with the correct compose CLI command + command = command.replace(/^docker(-|\s+)compose/i, await getComposeCliCommand()); + + if (isNewContextType(await ext.dockerContextManager.getCurrentContextType())) { + // For new contexts, replace '--build' anywhere with '' + command = command.replace(/--build/i, ''); + } + + return command; +} diff --git a/src/tasks/DockerComposeTaskDefinitionBase.ts b/src/tasks/DockerComposeTaskDefinitionBase.ts new file mode 100644 index 0000000000..acf54e6893 --- /dev/null +++ b/src/tasks/DockerComposeTaskDefinitionBase.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TaskDefinitionBase } from './TaskDefinitionBase'; + +export interface DockerComposeUpOptions { + up?: { + detached?: boolean; + build?: boolean; + scale?: { [service: string]: number }; + services?: string[]; + customOptions?: string; + }; + down?: never; +} + +export interface DockerComposeDownOptions { + up?: never; + down?: { + removeImages?: 'all' | 'local'; + removeVolumes?: boolean; + customOptions?: string; + }; +} + +export type DockerComposeOptions = (DockerComposeUpOptions | DockerComposeDownOptions) & { files?: string[] }; + +export interface DockerComposeTaskDefinitionBase extends TaskDefinitionBase { + dockerCompose?: DockerComposeOptions; +} diff --git a/src/tasks/DockerComposeTaskProvider.ts b/src/tasks/DockerComposeTaskProvider.ts new file mode 100644 index 0000000000..3dfab4ca69 --- /dev/null +++ b/src/tasks/DockerComposeTaskProvider.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fse from 'fs-extra'; +import * as path from 'path'; +import { Task } from 'vscode'; +import { getComposeCliCommand } from '../docker/Contexts'; +import { localize } from '../localize'; +import { cloneObject } from '../utils/cloneObject'; +import { CommandLineBuilder } from '../utils/commandLineBuilder'; +import { resolveVariables } from '../utils/resolveVariables'; +import { DockerComposeOptions, DockerComposeTaskDefinitionBase } from './DockerComposeTaskDefinitionBase'; +import { DockerTaskProvider } from './DockerTaskProvider'; +import { DockerComposeTaskContext, throwIfCancellationRequested } from './TaskHelper'; + +// This doesn't need to be extended so redefining for parity is simplest +export type DockerComposeTaskDefinition = DockerComposeTaskDefinitionBase; + +export interface DockerComposeTask extends Task { + definition: DockerComposeTaskDefinition; +} + +export class DockerComposeTaskProvider extends DockerTaskProvider { + public constructor() { super('docker-build', undefined) } + + protected async executeTaskInternal(context: DockerComposeTaskContext, task: DockerComposeTask): Promise { + const definition = cloneObject(task.definition); + definition.dockerCompose = definition.dockerCompose || {}; + definition.dockerCompose.files = definition.dockerCompose.files || []; + + await this.validateResolvedDefinition(context, definition.dockerCompose); + + const commandLine = await this.resolveCommandLine(definition.dockerCompose); + + // Because BuildKit outputs everything to stderr, we will not treat output there as a failure + await context.terminal.executeCommandInTerminal( + commandLine, + context.folder, + false, // rejectOnStderr + undefined, // stdoutBuffer + Buffer.alloc(10 * 1024), // stderrBuffer + context.cancellationToken + ); + throwIfCancellationRequested(context); + } + + private async validateResolvedDefinition(context: DockerComposeTaskContext, dockerCompose: DockerComposeOptions): Promise { + if (dockerCompose.up && dockerCompose.down) { + throw new Error(localize('vscode-docker.tasks.composeProvider.bothUpAndDown', 'Both "up" and "down" properties are present in the docker-compose task.')); + } + + if (!dockerCompose.up && !dockerCompose.down) { + throw new Error(localize('vscode-docker.tasks.composeProvider.noUpOrDown', 'Neither "up" nor "down" properties are present in the docker-compose task.')); + } + + for (const file of dockerCompose.files) { + if (!(await fse.pathExists(path.resolve(context.folder.uri.fsPath, resolveVariables(file, context.folder))))) { + throw new Error(localize('vscode-docker.tasks.composeProvider.invalidFile', 'One or more docker-compose files does not exist or could not be accessed.')); + } + } + } + + private async resolveCommandLine(options: DockerComposeOptions): Promise { + if (options.up) { + // CommandLineBuilder requires key-value objects to be string => string, but scale is string => number + // So, convert it to string => string + const scaleAsString: { [key: string]: string } = {}; + if (options.up.scale) { + for (const key of Object.keys(options.up.scale)) { + scaleAsString[key] = options.up.scale[key].toString(); + } + } + + return CommandLineBuilder + .create(await getComposeCliCommand()) + .withArrayArgs('-f', options.files) + .withArg('up') + .withFlagArg('--detach', !!options.up.detached) + .withFlagArg('--build', !!options.up.build) + .withKeyValueArgs('--scale', scaleAsString) + .withArg(options.up.customOptions) + .withArg(options.up.services?.join(' ')); + } else { + // Validation earlier guarantees that if up is not defined, down must be + return CommandLineBuilder + .create(await getComposeCliCommand()) + .withArrayArgs('-f', options.files) + .withArg('down') + .withNamedArg('--rmi', options.down.removeImages) + .withFlagArg('--volumes', options.down.removeVolumes) + .withArg(options.down.customOptions); + } + } +} diff --git a/src/tasks/TaskHelper.ts b/src/tasks/TaskHelper.ts index 1322fc3737..e54ce7a479 100644 --- a/src/tasks/TaskHelper.ts +++ b/src/tasks/TaskHelper.ts @@ -15,6 +15,7 @@ import { pathNormalize } from '../utils/pathNormalize'; import { resolveVariables } from '../utils/resolveVariables'; import { DockerBuildOptions } from './DockerBuildTaskDefinitionBase'; import { DockerBuildTask, DockerBuildTaskDefinition, DockerBuildTaskProvider } from './DockerBuildTaskProvider'; +import { DockerComposeTaskProvider } from './DockerComposeTaskProvider'; import { DockerPseudoterminal } from './DockerPseudoterminal'; import { DockerContainerVolume, DockerRunOptions, DockerRunTaskDefinitionBase } from './DockerRunTaskDefinitionBase'; import { DockerRunTask, DockerRunTaskDefinition, DockerRunTaskProvider } from './DockerRunTaskProvider'; @@ -49,7 +50,6 @@ export interface DockerTaskExecutionContext extends DockerTaskContext { terminal: DockerPseudoterminal; } -// tslint:disable-next-line: no-empty-interface export interface DockerBuildTaskContext extends DockerTaskExecutionContext { imageName?: string; buildTaskResult?: string; @@ -60,6 +60,9 @@ export interface DockerRunTaskContext extends DockerTaskExecutionContext { buildDefinition?: DockerBuildTaskDefinition; } +// This doesn't need to be extended so redefining for parity is simplest +export type DockerComposeTaskContext = DockerTaskExecutionContext; + export interface TaskHelper { preBuild?(context: DockerBuildTaskContext, buildDefinition: DockerBuildTaskDefinition): Promise; getDockerBuildOptions(context: DockerBuildTaskContext, buildDefinition: DockerBuildTaskDefinition): Promise; @@ -90,6 +93,13 @@ export function registerTaskProviders(ctx: ExtensionContext): void { new DockerRunTaskProvider(helpers) ) ); + + ctx.subscriptions.push( + tasks.registerTaskProvider( + 'docker-compose', + new DockerComposeTaskProvider() + ) + ); } export function hasTask(taskLabel: string, folder: WorkspaceFolder): boolean { diff --git a/test/commands/selectCommandTemplate.test.ts b/test/commands/selectCommandTemplate.test.ts index e629c11282..a85e306881 100644 --- a/test/commands/selectCommandTemplate.test.ts +++ b/test/commands/selectCommandTemplate.test.ts @@ -591,6 +591,10 @@ async function runWithCommandSetting( ContextType: contextType, } as DockerContext; }, + + getCurrentContextType: async () => { + return Promise.resolve(contextType); + } }; try {