Skip to content

Commit

Permalink
Add ability to launch a subset of Compose services (microsoft#2514)
Browse files Browse the repository at this point in the history
* New `docker-compose` task
* New `${serviceList}` token for docker-compose up customizable command
  • Loading branch information
bwateratmsft authored and Dmarch28 committed Mar 4, 2021
1 parent 63c71a3 commit 7130ca3
Show file tree
Hide file tree
Showing 14 changed files with 373 additions and 33 deletions.
106 changes: 106 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
12 changes: 12 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
52 changes: 26 additions & 26 deletions src/commands/compose.ts → src/commands/compose/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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.'));
Expand All @@ -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 });
}
}
}
Expand All @@ -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<string> {
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<string> {
if (serviceListPlaceholder.test(command)) {
return command.replace(serviceListPlaceholder, await getComposeServiceList(context, workspaceFolder, command));
} else {
return command;
}
Expand Down
55 changes: 55 additions & 0 deletions src/commands/compose/getComposeServiceList.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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<string[]>(workspaceServiceListKey, services);

const pickChoices: IAzureQuickPickItem<string>[] = 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<string[]> {
// 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; });
}
4 changes: 2 additions & 2 deletions src/commands/containers/composeGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
return composeGroup(context, 'logs', node, '-f --tail 1000');
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/containers/confirmAllAffectedContainers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]> {
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);
Expand Down
2 changes: 1 addition & 1 deletion src/commands/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/commands/selectCommandTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
// 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');
Expand Down
7 changes: 6 additions & 1 deletion src/docker/ContextManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -48,6 +48,7 @@ export interface ContextManager {
refresh(): Promise<void>;
getContexts(): Promise<DockerContext[]>;
getCurrentContext(): Promise<DockerContext>;
getCurrentContextType(): Promise<ContextType>;

inspect(actionContext: IActionContext, contextName: string): Promise<DockerContextInspection>;
use(actionContext: IActionContext, contextName: string): Promise<void>;
Expand Down Expand Up @@ -164,6 +165,10 @@ export class DockerContextManager implements ContextManager, Disposable {
return contexts.find(c => c.Current);
}

public async getCurrentContextType(): Promise<ContextType> {
return (await this.getCurrentContext()).ContextType;
}

public async inspect(actionContext: IActionContext, contextName: string): Promise<DockerContextInspection> {
const { stdout } = await execAsync(`docker context inspect ${contextName}`, { timeout: 10000 });

Expand Down
Loading

0 comments on commit 7130ca3

Please sign in to comment.