|
4 | 4 | *--------------------------------------------------------------------------------------------*/ |
5 | 5 |
|
6 | 6 | import { IActionContext, IAzureQuickPickItem } from '@microsoft/vscode-azext-utils'; |
| 7 | +import { CommandLineArgs, PromiseCommandResponse, quoted, VoidCommandResponse } from '@microsoft/vscode-container-client'; |
7 | 8 | import * as vscode from 'vscode'; |
8 | 9 | import { ext } from '../../extensionVariables'; |
| 10 | +import { runWithDefaults } from '../../runtimes/runners/runWithDefaults'; |
9 | 11 | import { execAsync } from '../../utils/execAsync'; |
10 | 12 |
|
11 | 13 | // 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 |
12 | 14 | const composeCommandReplaceRegex = /(\b(up|down)\b).*$/i; |
13 | 15 |
|
14 | 16 | type SubsetType = 'services' | 'profiles'; |
15 | 17 |
|
| 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. |
16 | 55 | export async function getComposeProfilesOrServices(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder, composeCommand: string, preselectedServices?: string[], preselectedProfiles?: string[]): Promise<{ services: string | undefined, profiles: string | undefined }> { |
17 | 56 | const profiles = await getServiceSubsets(workspaceFolder, composeCommand, 'profiles'); |
18 | 57 |
|
@@ -48,6 +87,46 @@ export async function getComposeProfilesOrServices(context: IActionContext, work |
48 | 87 | }; |
49 | 88 | } |
50 | 89 |
|
| 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 | + |
51 | 130 | export async function getComposeProfileList(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder, composeCommand: string, prefetchedProfiles?: string[], preselectedProfiles?: string[]): Promise<string> { |
52 | 131 | const profiles = prefetchedProfiles ?? await getServiceSubsets(workspaceFolder, composeCommand, 'profiles'); |
53 | 132 |
|
@@ -112,6 +191,47 @@ async function pickSubsets(context: IActionContext, type: SubsetType, allChoices |
112 | 191 | return chosenSubsets.map(c => c.data); |
113 | 192 | } |
114 | 193 |
|
| 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 | + |
115 | 235 | async function getServiceSubsets(workspaceFolder: vscode.WorkspaceFolder, composeCommand: string, type: SubsetType): Promise<string[] | undefined> { |
116 | 236 | // 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. |
117 | 237 | // Bug for that is https://github.com/docker/compose-cli/issues/1964 |
|
0 commit comments