Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support creating an ACI context from palette / contexts view #2135

Merged
merged 11 commits into from
Jul 8, 2020
25 changes: 22 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
"onCommand:vscode-docker.contexts.configureExplorer",
"onCommand:vscode-docker.contexts.refresh",
"onCommand:vscode-docker.contexts.help",
"onCommand:vscode-docker.contexts.create.aci",
"onCommand:workbench.action.tasks.runTask",
"onDebugInitialConfigurations",
"onDebugResolve:docker-coreclr",
Expand Down Expand Up @@ -140,6 +141,10 @@
{
"command": "vscode-docker.registries.reconnectRegistry",
"when": "never"
},
{
"command": "vscode-docker.contexts.create.aci",
"when": "vscode-docker:newCliPresent"
}
],
"editor/context": [
Expand Down Expand Up @@ -302,6 +307,11 @@
"when": "view == dockerVolumes",
"group": "navigation@9"
},
{
"command": "vscode-docker.contexts.create.aci",
"when": "view == vscode-docker.views.dockerContexts && vscode-docker:newCliPresent",
"group": "navigation@1"
},
{
"command": "vscode-docker.contexts.configureExplorer",
"when": "view == vscode-docker.views.dockerContexts",
Expand Down Expand Up @@ -346,17 +356,17 @@
},
{
"command": "vscode-docker.containers.start",
"when": "view == dockerContainers && viewItem =~ /^(created|dead|exited|paused|terminated)Container$/i",
"when": "view == dockerContainers && viewItem =~ /^(created|dead|exited|paused|terminated)Container$/i && vscode-docker:aciContext != true",
"group": "containers_1_general@5"
},
{
"command": "vscode-docker.containers.stop",
"when": "view == dockerContainers && viewItem =~ /^(paused|restarting|running)Container$/i",
"when": "view == dockerContainers && viewItem =~ /^(paused|restarting|running)Container$/i && vscode-docker:aciContext != true",
"group": "containers_1_general@6"
},
{
"command": "vscode-docker.containers.restart",
"when": "view == dockerContainers && viewItem =~ /^runningContainer$/i",
"when": "view == dockerContainers && viewItem =~ /^runningContainer$/i && vscode-docker:aciContext != true",
"group": "containers_1_general@7"
},
{
Expand Down Expand Up @@ -2522,6 +2532,15 @@
"title": "%vscode-docker.commands.contexts.help%",
"category": "%vscode-docker.commands.category.contexts%",
"icon": "$(question)"
},
{
"command": "vscode-docker.contexts.create.aci",
"title": "%vscode-docker.commands.contexts.create.aci%",
"category": "%vscode-docker.commands.category.contexts%",
"icon": {
"light": "resources/light/add.svg",
"dark": "resources/dark/add.svg"
}
}
],
"views": {
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@
"vscode-docker.commands.contexts.configureExplorer": "Configure Explorer...",
"vscode-docker.commands.contexts.refresh": "Refresh",
"vscode-docker.commands.contexts.help": "Docker Context Help",
"vscode-docker.commands.contexts.create.aci": "Create Azure Container Instances Context...",
"vscode-docker.commands.help": "Docker Help",
"vscode-docker.commands.category.docker": "Docker",
"vscode-docker.commands.category.dockerContainers": "Docker Containers",
Expand Down
104 changes: 104 additions & 0 deletions src/commands/context/aci/createAciContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* 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, LocationListStep, parseError, ResourceGroupListStep } 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());
LocationListStep.addStep(wizardContext, promptSteps);

// 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);
ext.outputChannel.appendLine(creatingNewContext);
progress.report({ message: creatingNewContext });

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

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

// TODO: get the real error code
if (error.errorType === '1234') {
bwateratmsft marked this conversation as resolved.
Show resolved Hide resolved
// 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;
}
}
}

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;
}
}
2 changes: 2 additions & 0 deletions src/commands/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { selectContainer } from "./containers/selectContainer";
import { startContainer } from "./containers/startContainer";
import { stopContainer } from "./containers/stopContainer";
import { viewContainerLogs } from "./containers/viewContainerLogs";
import { createAciContext } from "./context/aci/createAciContext";
import { configureDockerContextsExplorer, dockerContextsHelp } from "./context/DockerContextsViewCommands";
import { inspectDockerContext } from "./context/inspectDockerContext";
import { removeDockerContext } from "./context/removeDockerContext";
Expand Down Expand Up @@ -163,6 +164,7 @@ export function registerCommands(): void {
registerCommand('vscode-docker.contexts.inspect', inspectDockerContext);
registerCommand('vscode-docker.contexts.remove', removeDockerContext);
registerCommand('vscode-docker.contexts.use', useDockerContext);
registerCommand('vscode-docker.contexts.create.aci', createAciContext);

registerLocalCommand('vscode-docker.installDocker', installDocker);

Expand Down
46 changes: 45 additions & 1 deletion src/docker/ContextManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as fse from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import { URL } from 'url';
import { Event, EventEmitter, workspace } from 'vscode';
import { commands, Event, EventEmitter, workspace } from 'vscode';
import { Disposable } from 'vscode';
import { callWithTelemetryAndErrorHandling, IActionContext } from 'vscode-azureextensionui';
import { LineSplitter } from '../debugging/coreclr/lineSplitter';
Expand Down Expand Up @@ -40,6 +40,8 @@ const defaultContext: Partial<DockerContext> = {
Description: 'Current DOCKER_HOST based configuration',
};

type VSCodeContext = 'vscode-docker:aciContext' | 'vscode-docker:newSdkContext' | 'vscode-docker:newCliPresent';

export interface ContextManager {
readonly onContextChanged: Event<DockerContext>;
refresh(): Promise<void>;
Expand All @@ -48,20 +50,25 @@ export interface ContextManager {
inspect(actionContext: IActionContext, contextName: string): Promise<DockerContextInspection>;
use(actionContext: IActionContext, contextName: string): Promise<void>;
remove(actionContext: IActionContext, contextName: string): Promise<void>;

isNewCli(): Promise<boolean>;
}

// TODO: consider a periodic refresh as a catch-all; but make sure it compares old data to new before firing a change event
// TODO: so that non-changes don't result in everything getting refreshed
export class DockerContextManager implements ContextManager, Disposable {
private readonly emitter: EventEmitter<DockerContext> = new EventEmitter<DockerContext>();
private readonly contextsCache: AsyncLazy<DockerContext[]>;
private readonly newCli: AsyncLazy<boolean>;
private readonly configFileWatcher: fs.FSWatcher;
private readonly contextFolderWatcher: fs.FSWatcher;
private refreshing: boolean = false;

public constructor() {
this.contextsCache = new AsyncLazy(async () => this.loadContexts());

this.newCli = new AsyncLazy(async () => this.getCliVersion());

/* eslint-disable @typescript-eslint/tslint/config */
this.configFileWatcher = fs.watch(dockerConfigFile, async () => this.refresh());
this.contextFolderWatcher = fs.watch(dockerContextsFolder, async () => this.refresh());
Expand Down Expand Up @@ -96,8 +103,12 @@ export class DockerContextManager implements ContextManager, Disposable {

// Create a new client
if (currentContext.Type === 'aci') {
await this.setVsCodeContext('vscode-docker:aciContext', true);
bwateratmsft marked this conversation as resolved.
Show resolved Hide resolved
await this.setVsCodeContext('vscode-docker:newSdkContext', true);
ext.dockerClient = new DockerServeClient();
} else {
await this.setVsCodeContext('vscode-docker:aciContext', false);
await this.setVsCodeContext('vscode-docker:newSdkContext', false);
ext.dockerClient = new DockerodeApiClient(currentContext);
}

Expand All @@ -106,6 +117,9 @@ export class DockerContextManager implements ContextManager, Disposable {
} finally {
this.refreshing = false;
}

// Lastly, trigger a CLI version check but don't wait
void this.newCli.getValue();
}

public async getContexts(): Promise<DockerContext[]> {
Expand All @@ -130,6 +144,10 @@ export class DockerContextManager implements ContextManager, Disposable {
await spawnAsync(removeCmd, ContextCmdExecOptions);
}

public async isNewCli(): Promise<boolean> {
return this.newCli.getValue();
}

private async loadContexts(): Promise<DockerContext[]> {
let loadResult = await callWithTelemetryAndErrorHandling(ext.dockerClient ? 'docker-context.change' : 'docker-context.initialize', async (actionContext: IActionContext) => {
try {
Expand Down Expand Up @@ -214,4 +232,30 @@ export class DockerContextManager implements ContextManager, Disposable {

return loadResult;
}

private async getCliVersion(): Promise<boolean> {
let result: boolean = false;
const contexts = await this.contextsCache.getValue();

if (contexts.some(c => c.Type === 'aci')) {
// If there are any ACI contexts we automatically know it's the new CLI
result = true;
} else {
// Otherwise we look at the output of `docker serve --help`
// TODO: this is not a very good heuristic
const { stdout } = await execAsync('docker serve --help');

if (/^\s*Start an api server/i.test(stdout)) {
result = true;
}
}

// Set the VSCode context to the result (which may expose commands, etc.)
await this.setVsCodeContext('vscode-docker:newCliPresent', result);
return result;
}

private async setVsCodeContext(vsCodeContext: VSCodeContext, value: boolean): Promise<void> {
return commands.executeCommand('setContext', vsCodeContext, value);
}
}