Skip to content

Commit

Permalink
Support creating an ACI context from palette / contexts view (microso…
Browse files Browse the repository at this point in the history
…ft#2135)

* Create ACI from command

* Add VSCode context for command visibility

* Don't show start/stop/restart
  • Loading branch information
bwateratmsft authored and Dmarch28 committed Mar 4, 2021
1 parent 201cbe0 commit 58c6be6
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 4 deletions.
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 @@ -2527,6 +2537,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 @@ -245,6 +245,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
103 changes: 103 additions & 0 deletions src/commands/context/aci/createAciContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*---------------------------------------------------------------------------------------------
* 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);

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;
}
}
}

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
48 changes: 47 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,14 @@ export class DockerContextManager implements ContextManager, Disposable {

// Create a new client
if (currentContext.Type === 'aci') {
// Currently vscode-docker:aciContext vscode-docker:newSdkContext mean the same thing
// But that probably won't be true in the future, so define both as separate concepts now
await this.setVsCodeContext('vscode-docker:aciContext', true);
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 +119,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 +146,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 +234,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);
}
}

0 comments on commit 58c6be6

Please sign in to comment.