Skip to content
This repository was archived by the owner on Jul 20, 2026. It is now read-only.

Commit d0a2744

Browse files
authored
Add implementations of service/profile expansion for default commands (#4469)
1 parent fed028e commit d0a2744

2 files changed

Lines changed: 161 additions & 4 deletions

File tree

src/commands/compose/compose.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { IActionContext, UserCancelledError } from '@microsoft/vscode-azext-utils';
7+
import { VoidCommandResponse } from '@microsoft/vscode-container-client';
78
import * as vscode from 'vscode';
89
import { ext } from '../../extensionVariables';
910
import { TaskCommandRunnerFactory } from '../../runtimes/runners/TaskCommandRunnerFactory';
1011
import { Item, createFileItem, quickPickDockerComposeFileItem } from '../../utils/quickPickFile';
1112
import { quickPickWorkspaceFolder } from '../../utils/quickPickWorkspaceFolder';
1213
import { selectComposeCommand } from '../selectCommandTemplate';
13-
import { getComposeProfileList, getComposeProfilesOrServices, getComposeServiceList } from './getComposeSubsetList';
14+
import { getComposeProfileList, getComposeProfilesOrServices, getComposeServiceList, getDefaultCommandComposeProfilesOrServices } from './getComposeSubsetList';
1415

1516
async function compose(context: IActionContext, commands: ('up' | 'down' | 'upSubset')[], message: string, dockerComposeFileUri?: vscode.Uri | string, selectedComposeFileUris?: vscode.Uri[], preselectedServices?: string[], preselectedProfiles?: string[]): Promise<void> {
1617
if (!vscode.workspace.isTrusted) {
@@ -53,7 +54,7 @@ async function compose(context: IActionContext, commands: ('up' | 'down' | 'upSu
5354
}
5455

5556
for (const item of selectedItems) {
56-
const terminalCommand = await selectComposeCommand(
57+
let terminalCommand = await selectComposeCommand(
5758
context,
5859
folder,
5960
command,
@@ -62,8 +63,12 @@ async function compose(context: IActionContext, commands: ('up' | 'down' | 'upSu
6263
build
6364
);
6465

65-
// Add the service list if needed
66-
terminalCommand.command = await addServicesOrProfilesIfNeeded(context, folder, terminalCommand.command, preselectedServices, preselectedProfiles);
66+
if (!terminalCommand.args?.length) {
67+
// Add the service list if needed
68+
terminalCommand.command = await addServicesOrProfilesIfNeeded(context, folder, terminalCommand.command, preselectedServices, preselectedProfiles);
69+
} else {
70+
terminalCommand = await addDefaultCommandServicesOrProfilesIfNeeded(context, folder, terminalCommand, preselectedServices, preselectedProfiles);
71+
}
6772

6873
const client = await ext.orchestratorManager.getClient();
6974
const taskCRF = new TaskCommandRunnerFactory({
@@ -96,6 +101,38 @@ export async function composeRestart(context: IActionContext, dockerComposeFileU
96101

97102
const serviceListPlaceholder = /\${serviceList}/i;
98103
const profileListPlaceholder = /\${profileList}/i;
104+
105+
async function addDefaultCommandServicesOrProfilesIfNeeded(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder, command: VoidCommandResponse, preselectedServices: string[], preselectedProfiles: string[]): Promise<VoidCommandResponse> {
106+
const commandWithoutPlaceholders = {
107+
...command,
108+
args: command.args.filter(arg => typeof arg === 'string' ? !serviceListPlaceholder.test(arg) && !profileListPlaceholder.test(arg) : !serviceListPlaceholder.test(arg.value) && !profileListPlaceholder.test(arg.value)),
109+
};
110+
111+
const { services, profiles } = await getDefaultCommandComposeProfilesOrServices(context, workspaceFolder, commandWithoutPlaceholders, preselectedServices, preselectedProfiles);
112+
113+
// Replace the placeholder args with the actual service and profile arguments
114+
return {
115+
...command,
116+
args: command.args.flatMap(arg => {
117+
if (typeof arg === 'string') {
118+
if (serviceListPlaceholder.test(arg)) {
119+
return services;
120+
} else if (profileListPlaceholder.test(arg)) {
121+
return profiles;
122+
}
123+
} else {
124+
if (serviceListPlaceholder.test(arg.value)) {
125+
return services;
126+
} else if (profileListPlaceholder.test(arg.value)) {
127+
return profiles;
128+
}
129+
}
130+
131+
return [arg];
132+
}),
133+
};
134+
}
135+
99136
async function addServicesOrProfilesIfNeeded(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder, command: string, preselectedServices: string[], preselectedProfiles: string[]): Promise<string> {
100137
const commandWithoutPlaceholders = command.replace(serviceListPlaceholder, '').replace(profileListPlaceholder, '');
101138

src/commands/compose/getComposeSubsetList.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,54 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { IActionContext, IAzureQuickPickItem } from '@microsoft/vscode-azext-utils';
7+
import { CommandLineArgs, PromiseCommandResponse, quoted, VoidCommandResponse } from '@microsoft/vscode-container-client';
78
import * as vscode from 'vscode';
89
import { ext } from '../../extensionVariables';
10+
import { runWithDefaults } from '../../runtimes/runners/runWithDefaults';
911
import { execAsync } from '../../utils/execAsync';
1012

1113
// 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
1214
const composeCommandReplaceRegex = /(\b(up|down)\b).*$/i;
1315

1416
type SubsetType = 'services' | 'profiles';
1517

18+
// We special case the default compose commands into a full VoidCommandResponse object with command and args populated (to help with shell escaping). This method will be called in those cases to get the service or profile lists for a given command.
19+
export async function getDefaultCommandComposeProfilesOrServices(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder, composeCommand: VoidCommandResponse, preselectedServices?: string[], preselectedProfiles?: string[]): Promise<{ services: CommandLineArgs, profiles: CommandLineArgs }> {
20+
const profiles = await getDefaultCommandServiceSubsets(workspaceFolder, composeCommand, 'profiles');
21+
22+
if (preselectedServices?.length && preselectedProfiles?.length) {
23+
throw new Error(vscode.l10n.t('Cannot specify both services and profiles to start. Please choose one or the other.'));
24+
}
25+
26+
// If there are any profiles, we need to ask the user whether they want profiles or services, since they are mutually exclusive to use
27+
// Otherwise, if there are no profiles, we'll automatically assume services
28+
let useProfiles = false;
29+
if (preselectedProfiles?.length) {
30+
useProfiles = true;
31+
} else if (preselectedServices?.length) {
32+
useProfiles = false;
33+
} else if (profiles?.length) {
34+
const profilesOrServices: IAzureQuickPickItem<SubsetType>[] = [
35+
{
36+
label: vscode.l10n.t('Services'),
37+
data: 'services'
38+
},
39+
{
40+
label: vscode.l10n.t('Profiles'),
41+
data: 'profiles'
42+
}
43+
];
44+
45+
useProfiles = 'profiles' === (await context.ui.showQuickPick(profilesOrServices, { placeHolder: vscode.l10n.t('Do you want to start services or profiles?') })).data;
46+
}
47+
48+
return {
49+
profiles: useProfiles ? await getDefaultCommandComposeProfileList(context, workspaceFolder, composeCommand, profiles, preselectedProfiles) : [],
50+
services: !useProfiles ? await getDefaultCommandComposeServiceList(context, workspaceFolder, composeCommand, preselectedServices) : [],
51+
};
52+
}
53+
54+
// In the event that a user has customized a compose command, we treat it as a full command string instead of parsing to command and args like the default command version. This method handles replacing service and profile arguments in that case.
1655
export async function getComposeProfilesOrServices(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder, composeCommand: string, preselectedServices?: string[], preselectedProfiles?: string[]): Promise<{ services: string | undefined, profiles: string | undefined }> {
1756
const profiles = await getServiceSubsets(workspaceFolder, composeCommand, 'profiles');
1857

@@ -48,6 +87,46 @@ export async function getComposeProfilesOrServices(context: IActionContext, work
4887
};
4988
}
5089

90+
// Default command version of the compose profile list method; returns CommandLineArgs instead of a string
91+
async function getDefaultCommandComposeProfileList(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder, composeCommand: VoidCommandResponse, prefetchedProfiles?: string[], preselectedProfiles?: string[]): Promise<CommandLineArgs> {
92+
const profiles = prefetchedProfiles ?? await getDefaultCommandServiceSubsets(workspaceFolder, composeCommand, 'profiles');
93+
94+
if (!profiles?.length) {
95+
// No profiles or isn't supported, nothing to do
96+
return [];
97+
}
98+
99+
// Fetch the previously chosen profiles list. By default, all will be selected.
100+
const workspaceProfileListKey = `vscode-docker.composeProfiles.${workspaceFolder.name}`;
101+
const previousChoices = ext.context.workspaceState.get<string[]>(workspaceProfileListKey, profiles);
102+
const result = preselectedProfiles?.length ? preselectedProfiles : await pickSubsets(context, 'profiles', profiles, previousChoices);
103+
104+
// Update the cache
105+
await ext.context.workspaceState.update(workspaceProfileListKey, result);
106+
107+
return result.flatMap(p => ['--profile', quoted(p)]);
108+
}
109+
110+
// Default command version of the compose service list method; returns CommandLineArgs instead of a string
111+
async function getDefaultCommandComposeServiceList(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder, composeCommand: VoidCommandResponse, preselectedServices?: string[]): Promise<CommandLineArgs> {
112+
const services = await getDefaultCommandServiceSubsets(workspaceFolder, composeCommand, 'services');
113+
114+
if (!services?.length) {
115+
context.errorHandling.suppressReportIssue = true;
116+
throw new Error(vscode.l10n.t('No services were found in the compose document(s). Did you mean to use profiles instead?'));
117+
}
118+
119+
// Fetch the previously chosen services list. By default, all will be selected.
120+
const workspaceServiceListKey = `vscode-docker.composeServices.${workspaceFolder.name}`;
121+
const previousChoices = ext.context.workspaceState.get<string[]>(workspaceServiceListKey, services);
122+
const result = preselectedServices?.length ? preselectedServices : await pickSubsets(context, 'services', services, previousChoices);
123+
124+
// Update the cache
125+
await ext.context.workspaceState.update(workspaceServiceListKey, result);
126+
127+
return result.map(p => quoted(p));
128+
}
129+
51130
export async function getComposeProfileList(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder, composeCommand: string, prefetchedProfiles?: string[], preselectedProfiles?: string[]): Promise<string> {
52131
const profiles = prefetchedProfiles ?? await getServiceSubsets(workspaceFolder, composeCommand, 'profiles');
53132

@@ -112,6 +191,47 @@ async function pickSubsets(context: IActionContext, type: SubsetType, allChoices
112191
return chosenSubsets.map(c => c.data);
113192
}
114193

194+
async function getDefaultCommandServiceSubsets(workspaceFolder: vscode.WorkspaceFolder, composeCommand: VoidCommandResponse, type: SubsetType): Promise<string[] | undefined> {
195+
// TODO: if there are any profiles, then only services with no profiles show up when you query `config --services`. This makes for a lousy UX.
196+
// Bug for that is https://github.com/docker/compose-cli/issues/1964
197+
198+
const configCommand: PromiseCommandResponse<string[]> = {
199+
command: composeCommand.command,
200+
args: [],
201+
parse: (output) => {
202+
// The output of the config command is a list of services / profiles, one per line
203+
// Split them up and remove empty entries
204+
return Promise.resolve(output.split(/\r?\n/im).filter(l => { return l; }));
205+
},
206+
};
207+
208+
const index = composeCommand.args.findIndex(arg => {
209+
if (typeof arg === 'string') {
210+
if (composeCommandReplaceRegex.test(arg)) {
211+
return true;
212+
}
213+
} else if (composeCommandReplaceRegex.test(arg.value)) {
214+
return true;
215+
}
216+
217+
return false;
218+
});
219+
220+
configCommand.args = composeCommand.args.slice(0, index);
221+
configCommand.args.push('config', `--${type}`);
222+
223+
try {
224+
return await runWithDefaults(() => configCommand, { cwd: workspaceFolder.uri?.fsPath });
225+
} catch (err) {
226+
// Profiles is not yet widely supported, so those errors will be eaten--otherwise, rethrow
227+
if (type === 'profiles') {
228+
return undefined;
229+
} else {
230+
throw err;
231+
}
232+
}
233+
}
234+
115235
async function getServiceSubsets(workspaceFolder: vscode.WorkspaceFolder, composeCommand: string, type: SubsetType): Promise<string[] | undefined> {
116236
// TODO: if there are any profiles, then only services with no profiles show up when you query `config --services`. This makes for a lousy UX.
117237
// Bug for that is https://github.com/docker/compose-cli/issues/1964

0 commit comments

Comments
 (0)