Skip to content

Commit

Permalink
Add ability to deploy from ACR / Docker Hub to ACI (#2265)
Browse files Browse the repository at this point in the history
* Implement ACI deployment

* Container action warning
  • Loading branch information
bwateratmsft authored Sep 8, 2020
1 parent 148edbe commit e53eb2d
Show file tree
Hide file tree
Showing 24 changed files with 349 additions and 134 deletions.
27 changes: 21 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"onCommand:vscode-docker.registries.copyImageDigest",
"onCommand:vscode-docker.registries.deleteImage",
"onCommand:vscode-docker.registries.deployImageToAzure",
"onCommand:vscode-docker.registries.deployImageToAci",
"onCommand:vscode-docker.registries.disconnectRegistry",
"onCommand:vscode-docker.registries.dockerHub.openInBrowser",
"onCommand:vscode-docker.registries.help",
Expand Down Expand Up @@ -145,6 +146,10 @@
{
"command": "vscode-docker.contexts.create.aci",
"when": "vscode-docker:newCliPresent"
},
{
"command": "vscode-docker.registries.deployImageToAci",
"when": "vscode-docker:newCliPresent"
}
],
"editor/context": [
Expand Down Expand Up @@ -356,17 +361,17 @@
},
{
"command": "vscode-docker.containers.start",
"when": "view == dockerContainers && viewItem =~ /^(created|dead|exited|paused|terminated)Container$/i && vscode-docker:aciContext != true",
"when": "view == dockerContainers && viewItem =~ /^(created|dead|exited|paused|terminated)Container$/i",
"group": "containers_1_general@5"
},
{
"command": "vscode-docker.containers.stop",
"when": "view == dockerContainers && viewItem =~ /^(paused|restarting|running)Container$/i && vscode-docker:aciContext != true",
"when": "view == dockerContainers && viewItem =~ /^(paused|restarting|running)Container$/i",
"group": "containers_1_general@6"
},
{
"command": "vscode-docker.containers.restart",
"when": "view == dockerContainers && viewItem =~ /^runningContainer$/i && vscode-docker:aciContext != true",
"when": "view == dockerContainers && viewItem =~ /^runningContainer$/i",
"group": "containers_1_general@7"
},
{
Expand Down Expand Up @@ -469,6 +474,11 @@
"when": "view == dockerRegistries && viewItem =~ /(DockerV2|DockerHubV2);Tag;/ && isAzureAccountInstalled",
"group": "regs_tag_1_general@3"
},
{
"command": "vscode-docker.registries.deployImageToAci",
"when": "view == dockerRegistries && viewItem =~ /(DockerV2|DockerHubV2);Tag;/ && vscode-docker:newCliPresent",
"group": "regs_tag_1_general@4"
},
{
"command": "vscode-docker.registries.azure.untagImage",
"when": "view == dockerRegistries && viewItem == azure;DockerV2;Tag;",
Expand Down Expand Up @@ -551,17 +561,17 @@
},
{
"command": "vscode-docker.contexts.inspect",
"when": "view == vscode-docker.views.dockerContexts && viewItem =~ /Context$/i",
"when": "view == vscode-docker.views.dockerContexts && viewItem =~ /Context/i",
"group": "contexts_1_general@1"
},
{
"command": "vscode-docker.contexts.use",
"when": "view == vscode-docker.views.dockerContexts && viewItem =~ /Context$/i",
"when": "view == vscode-docker.views.dockerContexts && viewItem =~ /Context/i",
"group": "contexts_1_general@2"
},
{
"command": "vscode-docker.contexts.remove",
"when": "view == vscode-docker.views.dockerContexts && viewItem =~ /^customContext$/i",
"when": "view == vscode-docker.views.dockerContexts && viewItem =~ /^customContext/i",
"group": "contexts_2_destructive@1"
}
]
Expand Down Expand Up @@ -2498,6 +2508,11 @@
"title": "%vscode-docker.commands.registries.deployImageToAzure%",
"category": "%vscode-docker.commands.category.dockerRegistries%"
},
{
"command": "vscode-docker.registries.deployImageToAci",
"title": "%vscode-docker.commands.registries.deployImageToAci%",
"category": "%vscode-docker.commands.category.dockerRegistries%"
},
{
"command": "vscode-docker.registries.disconnectRegistry",
"title": "%vscode-docker.commands.registries.disconnectRegistry%",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@
"vscode-docker.commands.registries.copyImageDigest": "Copy Image Digest",
"vscode-docker.commands.registries.deleteImage": "Delete Image...",
"vscode-docker.commands.registries.deployImageToAzure": "Deploy Image to Azure App Service...",
"vscode-docker.commands.registries.deployImageToAci": "Deploy Image to Azure Container Instances...",
"vscode-docker.commands.registries.disconnectRegistry": "Disconnect",
"vscode-docker.commands.registries.dockerHub.openInBrowser": "Open in Browser",
"vscode-docker.commands.registries.help": "Registries Help",
Expand Down
36 changes: 36 additions & 0 deletions src/commands/containers/confirmAllAffectedContainers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { DialogResponses, IActionContext } from 'vscode-azureextensionui';
import { ext } from '../../extensionVariables';
import { localize } from '../../localize';
import { getComposeProjectName, NonComposeGroupName } from '../../tree/containers/ContainersTreeItem';
import { ContainerTreeItem } from '../../tree/containers/ContainerTreeItem';

export async function confirmAllAffectedContainers(context: IActionContext, nodes: ContainerTreeItem[]): Promise<string[]> {
if ((await ext.dockerContextManager.getCurrentContext()).Type !== '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);
}

const groupsSet = new Set<string>();

nodes.forEach(n => {
const groupName = getComposeProjectName(n.containerItem);

groupsSet.add(groupName === NonComposeGroupName ? n.containerId : groupName);
});

const groupsList = Array.from(groupsSet);
const groupsConfirm = groupsList.map(g => `\'${g}\'`).join(', ');

const confirm = localize('vscode-docker.commands.containers.aciContainerActionWarning.confirm', 'ACI containers can only be started or stopped in a group. This action will apply to all containers in {0}. Do you want to proceed?', groupsConfirm);

// No need to check result - cancel will throw a UserCancelledError
await ext.ui.showWarningMessage(confirm, { modal: true }, DialogResponses.yes);

return groupsList;
}
7 changes: 5 additions & 2 deletions src/commands/containers/restartContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ext } from '../../extensionVariables';
import { localize } from '../../localize';
import { ContainerTreeItem } from '../../tree/containers/ContainerTreeItem';
import { multiSelectNodes } from '../../utils/multiSelectNodes';
import { confirmAllAffectedContainers } from './confirmAllAffectedContainers';

export async function restartContainer(context: IActionContext, node?: ContainerTreeItem, nodes?: ContainerTreeItem[]): Promise<void> {
nodes = await multiSelectNodes(
Expand All @@ -19,9 +20,11 @@ export async function restartContainer(context: IActionContext, node?: Container
nodes
);

const references = await confirmAllAffectedContainers(context, nodes);

await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: localize('vscode-docker.commands.containers.restart.restarting', 'Restarting Container(s)...') }, async () => {
await Promise.all(nodes.map(async n => {
await ext.dockerClient.restartContainer(context, n.containerId);
await Promise.all(references.map(async ref => {
await ext.dockerClient.restartContainer(context, ref);
}));
});
}
7 changes: 5 additions & 2 deletions src/commands/containers/startContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ext } from '../../extensionVariables';
import { localize } from '../../localize';
import { ContainerTreeItem } from '../../tree/containers/ContainerTreeItem';
import { multiSelectNodes } from '../../utils/multiSelectNodes';
import { confirmAllAffectedContainers } from './confirmAllAffectedContainers';

export async function startContainer(context: IActionContext, node?: ContainerTreeItem, nodes?: ContainerTreeItem[]): Promise<void> {
nodes = await multiSelectNodes(
Expand All @@ -19,9 +20,11 @@ export async function startContainer(context: IActionContext, node?: ContainerTr
nodes
);

const references = await confirmAllAffectedContainers(context, nodes);

await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: localize('vscode-docker.commands.containers.start.starting', 'Starting Container(s)...') }, async () => {
await Promise.all(nodes.map(async n => {
await ext.dockerClient.startContainer(context, n.containerId);
await Promise.all(references.map(async ref => {
await ext.dockerClient.startContainer(context, ref);
}));
});
}
7 changes: 5 additions & 2 deletions src/commands/containers/stopContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ext } from '../../extensionVariables';
import { localize } from '../../localize';
import { ContainerTreeItem } from '../../tree/containers/ContainerTreeItem';
import { multiSelectNodes } from '../../utils/multiSelectNodes';
import { confirmAllAffectedContainers } from './confirmAllAffectedContainers';

export async function stopContainer(context: IActionContext, node?: ContainerTreeItem, nodes?: ContainerTreeItem[]): Promise<void> {
nodes = await multiSelectNodes(
Expand All @@ -19,9 +20,11 @@ export async function stopContainer(context: IActionContext, node?: ContainerTre
nodes
);

const references = await confirmAllAffectedContainers(context, nodes);

await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: localize('vscode-docker.commands.containers.stop.stopping', 'Stopping Container(s)...') }, async () => {
await Promise.all(nodes.map(async n => {
await ext.dockerClient.stopContainer(context, n.containerId);
await Promise.all(references.map(async ref => {
await ext.dockerClient.stopContainer(context, ref);
}));
});
}
99 changes: 2 additions & 97 deletions src/commands/context/aci/createAciContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,104 +3,9 @@
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Progress } from 'vscode';
import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, IActionContext, IResourceGroupWizardContext, parseError, ResourceGroupListStep } from 'vscode-azureextensionui';
import { IActionContext } from 'vscode-azureextensionui';
import { ext } from '../../../extensionVariables';
import { localize } from '../../../localize';
import { RegistryApi } from '../../../tree/registries/all/RegistryApi';
import { AzureAccountTreeItem } from '../../../tree/registries/azure/AzureAccountTreeItem';
import { azureRegistryProviderId } from '../../../tree/registries/azure/azureRegistryProvider';
import { execAsync } from '../../../utils/spawnAsync';

interface IAciWizardContext extends IResourceGroupWizardContext {
contextName: string;
}

export async function createAciContext(actionContext: IActionContext): Promise<void> {
const wizardContext: IActionContext & Partial<IAciWizardContext> = {
...actionContext,
};

// Set up the prompt steps
const promptSteps: AzureWizardPromptStep<IAciWizardContext>[] = [
new ContextNameStep(),
];

// Create a temporary azure account tree item since Azure might not be connected
const azureAccountTreeItem = new AzureAccountTreeItem(ext.registriesRoot, { id: azureRegistryProviderId, api: RegistryApi.DockerV2 });

// Add a subscription prompt step (skipped if there is exactly one subscription)
const subscriptionStep = await azureAccountTreeItem.getSubscriptionPromptStep(wizardContext);
if (subscriptionStep) {
promptSteps.push(subscriptionStep);
}

// Add additional prompt steps
promptSteps.push(new ResourceGroupListStep());

// Set up the execute steps
const executeSteps: AzureWizardExecuteStep<IAciWizardContext>[] = [
new AciContextCreateStep(),
];

const title = localize('vscode-docker.commands.contexts.create.aci.title', 'Create new Azure Container Instances context');

const wizard = new AzureWizard(wizardContext, { title, promptSteps, executeSteps });
await wizard.prompt();
await wizard.execute();
}

class ContextNameStep extends AzureWizardPromptStep<IAciWizardContext> {
public async prompt(context: IAciWizardContext): Promise<void> {
context.contextName = await ext.ui.showInputBox({ prompt: localize('vscode-docker.commands.contexts.create.aci.enterContextName', 'Enter context name'), validateInput: validateContextName });
}

public shouldPrompt(wizardContext: IActionContext): boolean {
return true;
}
}

class AciContextCreateStep extends AzureWizardExecuteStep<IAciWizardContext> {
public priority: number = 200;

public async execute(wizardContext: IAciWizardContext, progress: Progress<{ message?: string; increment?: number }>): Promise<void> {
const creatingNewContext: string = localize('vscode-docker.commands.contexts.create.aci.creatingContext', 'Creating ACI context "{0}"...', wizardContext.contextName);
const createdContext: string = localize('vscode-docker.commands.contexts.create.aci.createdContext', 'Created ACI context "{0}".', wizardContext.contextName);
ext.outputChannel.appendLine(creatingNewContext);
progress.report({ message: creatingNewContext });

const command = `docker context create aci ${wizardContext.contextName} --subscription-id ${wizardContext.subscriptionId} --resource-group ${wizardContext.resourceGroup.name}`;

try {
await execAsync(command);
} catch (err) {
const error = parseError(err);

if (error.errorType === '5' || /not logged in/i.test(error.message)) {
// If error is due to being not logged in, we'll go through login and try again
await execAsync('docker login azure');
await execAsync(command);
} else {
// Otherwise rethrow
throw err;
}
}

ext.outputChannel.appendLine(createdContext);
progress.report({ message: createdContext });
}

public shouldExecute(context: IAciWizardContext): boolean {
return true;
}
}

// Slightly more strict than CLI
const contextNameRegex = /^[a-z0-9][a-z0-9_-]+$/i;
function validateContextName(value: string | undefined): string | undefined {
if (!contextNameRegex.test(value)) {
return localize('vscode-docker.tree.contexts.create.aci.contextNameValidation', 'Context names must be start with an alphanumeric character and can only contain alphanumeric characters, underscores, and dashes.');
} else {
return undefined;
}
await ext.contextsRoot.createChild(actionContext);
}
3 changes: 2 additions & 1 deletion src/commands/context/inspectDockerContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export async function inspectDockerContext(actionContext: IActionContext, node?:
if (!node) {
node = await ext.contextsTree.showTreeItemPicker<ContextTreeItem>(ContextTreeItem.allContextRegExp, {
...actionContext,
noItemFoundErrorMessage: localize('vscode-docker.commands.contexts.inspect.noContexts', 'No Docker contexts are available to inspect')
noItemFoundErrorMessage: localize('vscode-docker.commands.contexts.inspect.noContexts', 'No Docker contexts are available to inspect'),
suppressCreatePick: true,
});
}

Expand Down
3 changes: 2 additions & 1 deletion src/commands/context/removeDockerContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export async function removeDockerContext(actionContext: IActionContext, node?:
if (!node) {
node = await ext.contextsTree.showTreeItemPicker<ContextTreeItem>(ContextTreeItem.removableContextRegExp, {
...actionContext,
noItemFoundErrorMessage: localize('vscode-docker.commands.contexts.remove.noContexts', 'No Docker contexts are available to remove')
noItemFoundErrorMessage: localize('vscode-docker.commands.contexts.remove.noContexts', 'No Docker contexts are available to remove'),
suppressCreatePick: true,
});
}

Expand Down
3 changes: 2 additions & 1 deletion src/commands/context/useDockerContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export async function useDockerContext(actionContext: IActionContext, node?: Con
if (!node) {
node = await ext.contextsTree.showTreeItemPicker<ContextTreeItem>(ContextTreeItem.allContextRegExp, {
...actionContext,
noItemFoundErrorMessage: localize('vscode-docker.commands.contexts.use.noContexts', 'No Docker contexts are available to use')
noItemFoundErrorMessage: localize('vscode-docker.commands.contexts.use.noContexts', 'No Docker contexts are available to use'),
suppressCreatePick: !(await ext.dockerContextManager.isNewCli()),
});
invokedFromCommandPalette = true;
}
Expand Down
3 changes: 0 additions & 3 deletions src/commands/images/tagImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ const KnownRegistries: { type: string, regex: RegExp }[] = [

export function addImageTaggingTelemetry(context: IActionContext, fullImageName: string, propertyPostfix: '.before' | '.after' | ''): void {
try {
let defaultRegistryPath: string = vscode.workspace.getConfiguration('docker').get('defaultRegistryPath', '');
let properties: TelemetryProperties = {};

let [repository, tag] = extractRegExGroups(fullImageName, /^(.*):(.*)$/, [fullImageName, '']);
Expand All @@ -77,8 +76,6 @@ export function addImageTaggingTelemetry(context: IActionContext, fullImageName:
}
properties.hasTag = String(!!tag);
properties.numSlashes = String(numberMatches(repository.match(/\//g)));
properties.isDefaultRegistryPathInName = String(repository.startsWith(`${defaultRegistryPath}/`));
properties.isDefaultRegistryPathSet = String(!!defaultRegistryPath);

let knownRegistry = KnownRegistries.find(kr => !!repository.match(kr.regex));
if (knownRegistry) {
Expand Down
2 changes: 2 additions & 0 deletions src/commands/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { registerWorkspaceCommand } from "./registerWorkspaceCommand";
import { createAzureRegistry } from "./registries/azure/createAzureRegistry";
import { deleteAzureRegistry } from "./registries/azure/deleteAzureRegistry";
import { deleteAzureRepository } from "./registries/azure/deleteAzureRepository";
import { deployImageToAci } from "./registries/azure/deployImageToAci";
import { openInAzurePortal } from "./registries/azure/openInAzurePortal";
import { buildImageInAzure } from "./registries/azure/tasks/buildImageInAzure";
import { runAzureTask } from "./registries/azure/tasks/runAzureTask";
Expand Down Expand Up @@ -132,6 +133,7 @@ export function registerCommands(): void {
registerCommand('vscode-docker.registries.copyImageDigest', copyRemoteImageDigest);
registerCommand('vscode-docker.registries.deleteImage', deleteRemoteImage);
registerCommand('vscode-docker.registries.deployImageToAzure', deployImageToAzure);
registerCommand('vscode-docker.registries.deployImageToAci', deployImageToAci);
registerCommand('vscode-docker.registries.disconnectRegistry', disconnectRegistry);
registerCommand('vscode-docker.registries.help', registryHelp);
registerWorkspaceCommand('vscode-docker.registries.logInToDockerCli', logInToDockerCli);
Expand Down
Loading

0 comments on commit e53eb2d

Please sign in to comment.