|
@ -1,6 +1,10 @@
|
|||
## 0.0.19 - 24 Sept 2017
|
||||
## 0.0.19 - 10 Oct 2017
|
||||
|
||||
* Add an automatic refresh option for the explorer (`"docker.explorerRefreshInterval": 1000`)
|
||||
* Add support fro Multi-Root Workspaces
|
||||
* Add support for DockerHub and Azure Container Registries
|
||||
* `docker-compose` now runs detached and always invokes a build (e.g. `docker-compose -f docker-compose.yml -d --build`)
|
||||
* `docker system prune` command no longer prompts for confirmation
|
||||
|
||||
## 0.0.18 - 18 Sept 2017
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { DockerNode } from "../explorer/dockerExplorer";
|
||||
import { ImageNode } from "../explorer/models/imageNode";
|
||||
import DockerInspectDocumentContentProvider from "../documentContentProviders/dockerInspect";
|
||||
import { quickPickImage } from "./utils/quick-pick-image";
|
||||
import { reporter } from "../telemetry/telemetry";
|
||||
|
||||
export default async function inspectImage(context?: DockerNode) {
|
||||
export default async function inspectImage(context?: ImageNode) {
|
||||
|
||||
let imageToInspect: Docker.ImageDesc;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as vscode from 'vscode';
|
||||
import { ContainerItem, quickPickContainer } from './utils/quick-pick-container';
|
||||
import { DockerEngineType, docker } from './utils/docker-endpoint';
|
||||
import { DockerNode } from '../explorer/dockerExplorer';
|
||||
import { ContainerNode } from '../explorer/models/containerNode';
|
||||
import { reporter } from '../telemetry/telemetry';
|
||||
const teleCmdId: string = 'vscode-docker.container.open-shell';
|
||||
|
||||
|
@ -10,7 +10,7 @@ const engineTypeShellCommands = {
|
|||
[DockerEngineType.Windows]: "powershell"
|
||||
}
|
||||
|
||||
export async function openShellContainer(context?: DockerNode) {
|
||||
export async function openShellContainer(context?: ContainerNode) {
|
||||
let containerToAttach: Docker.ContainerDesc;
|
||||
|
||||
if (context && context.containerDesc) {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import vscode = require('vscode');
|
||||
import { ImageItem, quickPickImage } from './utils/quick-pick-image';
|
||||
import { reporter } from '../telemetry/telemetry';
|
||||
import { DockerNode } from '../explorer/dockerExplorer';
|
||||
import { ImageNode } from '../explorer/models/imageNode';
|
||||
const teleCmdId: string = 'vscode-docker.image.push';
|
||||
|
||||
export async function pushImage(context?: DockerNode) {
|
||||
export async function pushImage(context?: ImageNode) {
|
||||
let imageToPush: Docker.ImageDesc;
|
||||
let imageName: string = "";
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import vscode = require('vscode');
|
||||
import { docker } from './utils/docker-endpoint';
|
||||
import { reporter } from '../telemetry/telemetry';
|
||||
import { DockerNode } from '../explorer/dockerExplorer';
|
||||
import { ContainerNode } from '../explorer/models/containerNode';
|
||||
import { dockerExplorerProvider } from '../dockerExtension';
|
||||
import { ContainerItem, quickPickContainer } from './utils/quick-pick-container';
|
||||
|
||||
const teleCmdId: string = 'vscode-docker.container.remove';
|
||||
|
||||
export async function removeContainer(context?: DockerNode) {
|
||||
export async function removeContainer(context?: ContainerNode) {
|
||||
|
||||
let containersToRemove: Docker.ContainerDesc[];
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@ import { docker } from './utils/docker-endpoint';
|
|||
import { ImageItem, quickPickImage } from './utils/quick-pick-image';
|
||||
import vscode = require('vscode');
|
||||
import { reporter } from '../telemetry/telemetry';
|
||||
import { DockerNode } from '../explorer/dockerExplorer';
|
||||
import { ImageNode } from "../explorer/models/imageNode";
|
||||
import { dockerExplorerProvider } from '../dockerExtension';
|
||||
|
||||
const teleCmdId: string = 'vscode-docker.image.remove';
|
||||
|
||||
export async function removeImage(context?: DockerNode) {
|
||||
export async function removeImage(context?: ImageNode) {
|
||||
|
||||
let imagesToRemove: Docker.ImageDesc[];
|
||||
|
||||
|
@ -34,11 +34,9 @@ export async function removeImage(context?: DockerNode) {
|
|||
imageCounter++;
|
||||
if (err) {
|
||||
vscode.window.showErrorMessage(err.message);
|
||||
dockerExplorerProvider.refreshImages();
|
||||
reject();
|
||||
}
|
||||
if (imageCounter === numImages) {
|
||||
dockerExplorerProvider.refreshImages();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import vscode = require('vscode');
|
||||
import { ContainerItem, quickPickContainer } from './utils/quick-pick-container';
|
||||
import { DockerNode } from '../explorer/dockerExplorer';
|
||||
import { ContainerNode } from '../explorer/models/containerNode';
|
||||
import { reporter } from '../telemetry/telemetry';
|
||||
const teleCmdId: string = 'vscode-docker.container.show-logs';
|
||||
|
||||
export async function showLogsContainer(context?: DockerNode) {
|
||||
export async function showLogsContainer(context?: ContainerNode) {
|
||||
|
||||
let containerToLog: Docker.ContainerDesc;
|
||||
|
||||
|
|
|
@ -4,11 +4,11 @@ import { DockerEngineType, docker } from './utils/docker-endpoint';
|
|||
import * as cp from 'child_process';
|
||||
import os = require('os');
|
||||
import { reporter } from '../telemetry/telemetry';
|
||||
import { DockerNode } from '../explorer/dockerExplorer';
|
||||
import { ImageNode } from '../explorer/models/imageNode';
|
||||
|
||||
const teleCmdId: string = 'vscode-docker.container.start';
|
||||
|
||||
export async function startContainer(context?:DockerNode, interactive?: boolean) {
|
||||
export async function startContainer(context?: ImageNode, interactive?: boolean) {
|
||||
let imageName: string;
|
||||
let imageToStart: Docker.ImageDesc;
|
||||
|
||||
|
@ -44,7 +44,7 @@ export async function startContainer(context?:DockerNode, interactive?: boolean)
|
|||
}
|
||||
}
|
||||
|
||||
export async function startContainerInteractive(context: DockerNode) {
|
||||
export async function startContainerInteractive(context: ImageNode) {
|
||||
await startContainer(context, true);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { docker } from './utils/docker-endpoint';
|
||||
import { ContainerItem, quickPickContainer } from './utils/quick-pick-container';
|
||||
import { reporter } from '../telemetry/telemetry';
|
||||
import { DockerNode } from '../explorer/dockerExplorer';
|
||||
import { ContainerNode } from '../explorer/models/containerNode';
|
||||
import { dockerExplorerProvider } from '../dockerExtension';
|
||||
|
||||
import vscode = require('vscode');
|
||||
|
||||
const teleCmdId: string = 'vscode-docker.container.stop';
|
||||
|
||||
export async function stopContainer(context?: DockerNode) {
|
||||
export async function stopContainer(context?: ContainerNode) {
|
||||
|
||||
let containersToStop: Docker.ContainerDesc[];
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@ import vscode = require('vscode');
|
|||
import { ImageItem, quickPickImage } from './utils/quick-pick-image';
|
||||
import { docker } from './utils/docker-endpoint';
|
||||
import { reporter } from '../telemetry/telemetry';
|
||||
import { DockerNode } from '../explorer/dockerExplorer';
|
||||
import { ImageNode } from "../explorer/models/imageNode";
|
||||
|
||||
const teleCmdId: string = 'vscode-docker.image.tag';
|
||||
|
||||
export async function tagImage(context?: DockerNode) {
|
||||
export async function tagImage(context?: ImageNode) {
|
||||
|
||||
let imageName: string;
|
||||
let imageToTag: Docker.ImageDesc;
|
||||
|
|
|
@ -26,6 +26,12 @@ import DockerInspectDocumentContentProvider, { SCHEME as DOCKER_INSPECT_SCHEME }
|
|||
import { DockerExplorerProvider } from './explorer/dockerExplorer';
|
||||
import { removeContainer } from './commands/remove-container';
|
||||
import { LanguageClient, LanguageClientOptions, SettingMonitor, ServerOptions, TransportKind } from 'vscode-languageclient';
|
||||
import { WebAppCreator } from './explorer/deploy/webAppCreator';
|
||||
import { AzureImageNode } from './explorer/models/azureRegistryNodes';
|
||||
import { DockerHubImageNode } from './explorer/models/dockerHubNodes';
|
||||
import { AzureAccountWrapper } from './explorer/deploy/azureAccountWrapper';
|
||||
import * as util from "./explorer/deploy/util";
|
||||
import { dockerHubLogout } from './explorer/models/dockerHubUtils';
|
||||
|
||||
export const FROM_DIRECTIVE_PATTERN = /^\s*FROM\s*([\w-\/:]*)(\s*AS\s*[a-z][a-z0-9-_\\.]*)?$/i;
|
||||
export const COMPOSE_FILE_GLOB_PATTERN = '**/[dD]ocker-[cC]ompose*.{yaml,yml}';
|
||||
|
@ -47,6 +53,8 @@ export function activate(ctx: vscode.ExtensionContext): void {
|
|||
|
||||
ctx.subscriptions.push(new Reporter(ctx));
|
||||
|
||||
const outputChannel = util.getOutputChannel();
|
||||
const azureAccount = new AzureAccountWrapper(ctx);
|
||||
|
||||
dockerExplorerProvider = new DockerExplorerProvider();
|
||||
vscode.window.registerTreeDataProvider('dockerExplorer', dockerExplorerProvider);
|
||||
|
@ -80,6 +88,13 @@ export function activate(ctx: vscode.ExtensionContext): void {
|
|||
ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.compose.down', composeDown));
|
||||
ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.system.prune', systemPrune));
|
||||
|
||||
ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.createWebApp', async (context?: AzureImageNode | DockerHubImageNode) => {
|
||||
const wizard = new WebAppCreator(outputChannel, azureAccount, context);
|
||||
const result = await wizard.run();
|
||||
}));
|
||||
|
||||
ctx.subscriptions.push(vscode.commands.registerCommand('vscode-docker.dockerHubLogout', dockerHubLogout));
|
||||
|
||||
activateLanguageClient(ctx);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ExtensionContext, Extension, extensions, Disposable } from 'vscode';
|
||||
import { ServiceClientCredentials } from 'ms-rest';
|
||||
import { AzureEnvironment } from 'ms-rest-azure';
|
||||
import { SubscriptionClient, SubscriptionModels } from 'azure-arm-resource';
|
||||
import { AzureAccount, AzureSession, AzureLoginStatus } from '../../typings/azure-account.api';
|
||||
import * as util from './util';
|
||||
|
||||
export class NotSignedInError extends Error { }
|
||||
|
||||
export class CredentialError extends Error { }
|
||||
|
||||
export class AzureAccountWrapper {
|
||||
readonly accountApi: AzureAccount;
|
||||
|
||||
constructor(readonly extensionConext: ExtensionContext) {
|
||||
this.accountApi = extensions.getExtension<AzureAccount>('ms-vscode.azure-account')!.exports;
|
||||
}
|
||||
|
||||
getAzureSessions(): AzureSession[] {
|
||||
const status = this.signInStatus;
|
||||
if (status !== 'LoggedIn') {
|
||||
throw new NotSignedInError(status)
|
||||
}
|
||||
return this.accountApi.sessions;
|
||||
}
|
||||
|
||||
getCredentialByTenantId(tenantId: string): ServiceClientCredentials {
|
||||
const session = this.getAzureSessions().find((s, i, array) => s.tenantId.toLowerCase() === tenantId.toLowerCase());
|
||||
|
||||
if (session) {
|
||||
return session.credentials;
|
||||
}
|
||||
|
||||
throw new CredentialError(`Failed to get credential, tenant ${tenantId} not found.`);
|
||||
}
|
||||
|
||||
get signInStatus(): AzureLoginStatus {
|
||||
return this.accountApi.status;
|
||||
}
|
||||
|
||||
getFilteredSubscriptions(): SubscriptionModels.Subscription[] {
|
||||
return this.accountApi.filters.map<SubscriptionModels.Subscription>(filter => {
|
||||
return {
|
||||
id: filter.subscription.id,
|
||||
subscriptionId: filter.subscription.subscriptionId,
|
||||
tenantId: filter.session.tenantId,
|
||||
displayName: filter.subscription.displayName,
|
||||
state: filter.subscription.state,
|
||||
subscriptionPolicies: filter.subscription.subscriptionPolicies,
|
||||
authorizationSource: filter.subscription.authorizationSource
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getAllSubscriptions(): Promise<SubscriptionModels.Subscription[]> {
|
||||
const tasks = new Array<Promise<SubscriptionModels.Subscription[]>>();
|
||||
|
||||
this.getAzureSessions().forEach((s, i, array) => {
|
||||
const client = new SubscriptionClient(s.credentials);
|
||||
const tenantId = s.tenantId;
|
||||
tasks.push(util.listAll(client.subscriptions, client.subscriptions.list()).then(result => {
|
||||
return result.map<SubscriptionModels.Subscription>((value) => {
|
||||
// The list() API doesn't include tenantId information in the subscription object,
|
||||
// however many places that uses subscription objects will be needing it, so we just create
|
||||
// a copy of the subscription object with the tenantId value.
|
||||
return {
|
||||
id: value.id,
|
||||
subscriptionId: value.subscriptionId,
|
||||
tenantId: tenantId,
|
||||
displayName: value.displayName,
|
||||
state: value.state,
|
||||
subscriptionPolicies: value.subscriptionPolicies,
|
||||
authorizationSource: value.authorizationSource
|
||||
};
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
const results = await Promise.all(tasks);
|
||||
const subscriptions = new Array<SubscriptionModels.Subscription>();
|
||||
|
||||
results.forEach((result) => result.forEach((subscription) => subscriptions.push(subscription)));
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
async getLocationsBySubscription(subscription: SubscriptionModels.Subscription): Promise<SubscriptionModels.Location[]> {
|
||||
const credential = this.getCredentialByTenantId(subscription.tenantId);
|
||||
const client = new SubscriptionClient(credential);
|
||||
const locations = <SubscriptionModels.Location[]>(await client.subscriptions.listLocations(subscription.subscriptionId));
|
||||
return locations;
|
||||
}
|
||||
|
||||
registerSessionsChangedListener(listener: (e: void) => any, thisArg: any): Disposable {
|
||||
return this.accountApi.onSessionsChanged(listener, thisArg, this.extensionConext.subscriptions);
|
||||
}
|
||||
|
||||
registerFiltersChangedListener(listener: (e: void) => any, thisArg: any): Disposable {
|
||||
return this.accountApi.onFiltersChanged(listener, thisArg, this.extensionConext.subscriptions);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ServiceClientCredentials } from 'ms-rest';
|
||||
import { SubscriptionModels } from 'azure-arm-resource';
|
||||
import { AzureAccountWrapper } from './azureAccountWrapper';
|
||||
import WebSiteManagementClient = require('azure-arm-website');
|
||||
import * as vscode from 'vscode';
|
||||
import * as WebSiteModels from '../../node_modules/azure-arm-website/lib/models';
|
||||
|
||||
|
||||
export interface PartialList<T> extends Array<T> {
|
||||
nextLink?: string;
|
||||
}
|
||||
|
||||
export async function listAll<T>(client: { listNext(nextPageLink: string): Promise<PartialList<T>>; }, first: Promise<PartialList<T>>): Promise<T[]> {
|
||||
const all: T[] = [];
|
||||
|
||||
for (let list = await first; list.length || list.nextLink; list = list.nextLink ? await client.listNext(list.nextLink) : []) {
|
||||
all.push(...list);
|
||||
}
|
||||
|
||||
return all;
|
||||
}
|
||||
|
||||
export function waitForWebSiteState(webSiteManagementClient: WebSiteManagementClient, site: WebSiteModels.Site, state: string, intervalMs = 5000, timeoutMs = 60000): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const func = async (count: number) => {
|
||||
const currentSite = await webSiteManagementClient.webApps.get(site.resourceGroup, site.name);
|
||||
if (currentSite.state.toLowerCase() === state.toLowerCase()) {
|
||||
resolve();
|
||||
} else {
|
||||
count += intervalMs;
|
||||
|
||||
if (count < timeoutMs) {
|
||||
setTimeout(func, intervalMs, count);
|
||||
} else {
|
||||
reject(new Error(`Timeout waiting for Web Site "${site.name}" state "${state}".`));
|
||||
}
|
||||
}
|
||||
};
|
||||
setTimeout(func, intervalMs, intervalMs);
|
||||
});
|
||||
}
|
||||
|
||||
export function getSignInCommandString(): string {
|
||||
return 'azure-account.login';
|
||||
}
|
||||
|
||||
export function getWebAppPublishCredential(azureAccount: AzureAccountWrapper, subscription: SubscriptionModels.Subscription, site: WebSiteModels.Site): Promise<WebSiteModels.User> {
|
||||
const credentials = azureAccount.getCredentialByTenantId(subscription.tenantId);
|
||||
const websiteClient = new WebSiteManagementClient(credentials, subscription.subscriptionId);
|
||||
return websiteClient.webApps.listPublishingCredentials(site.resourceGroup, site.name);
|
||||
}
|
||||
|
||||
// Output channel for the extension
|
||||
const outputChannel = vscode.window.createOutputChannel("Azure App Service");
|
||||
|
||||
export function getOutputChannel(): vscode.OutputChannel {
|
||||
return outputChannel;
|
||||
}
|
|
@ -0,0 +1,614 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { AzureAccountWrapper } from './azureAccountWrapper';
|
||||
import { WizardBase, WizardResult, WizardStep, SubscriptionStepBase, QuickPickItemWithData, UserCancelledError } from './wizard';
|
||||
import { SubscriptionModels, ResourceManagementClient, ResourceModels } from 'azure-arm-resource';
|
||||
import WebSiteManagementClient = require('azure-arm-website');
|
||||
import * as WebSiteModels from '../../node_modules/azure-arm-website/lib/models';
|
||||
import * as util from './util';
|
||||
import { AzureImageNode } from '../models/azureRegistryNodes';
|
||||
import { DockerHubImageNode } from '../models/dockerHubNodes';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
export class WebAppCreator extends WizardBase {
|
||||
constructor(output: vscode.OutputChannel, readonly azureAccount: AzureAccountWrapper, context: AzureImageNode | DockerHubImageNode, subscription?: SubscriptionModels.Subscription) {
|
||||
super(output);
|
||||
this.steps.push(new SubscriptionStep(this, azureAccount, subscription));
|
||||
this.steps.push(new ResourceGroupStep(this, azureAccount));
|
||||
this.steps.push(new AppServicePlanStep(this, azureAccount));
|
||||
this.steps.push(new WebsiteStep(this, azureAccount, context));
|
||||
this.steps.push(new ShellScriptStep(this, azureAccount));
|
||||
|
||||
}
|
||||
|
||||
async run(promptOnly = false): Promise<WizardResult> {
|
||||
// If not signed in, execute the sign in command and wait for it...
|
||||
if (this.azureAccount.signInStatus !== 'LoggedIn') {
|
||||
await vscode.commands.executeCommand(util.getSignInCommandString());
|
||||
}
|
||||
// Now check again, if still not signed in, cancel.
|
||||
if (this.azureAccount.signInStatus !== 'LoggedIn') {
|
||||
return {
|
||||
status: 'Cancelled',
|
||||
step: this.steps[0],
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
return super.run(promptOnly);
|
||||
}
|
||||
|
||||
get createdWebSite(): WebSiteModels.Site {
|
||||
const websiteStep = this.steps.find(step => step instanceof WebsiteStep);
|
||||
return (<WebsiteStep>websiteStep).website;
|
||||
}
|
||||
|
||||
protected beforeExecute(step: WizardStep, stepIndex: number) {
|
||||
if (stepIndex == 0) {
|
||||
this.writeline('Start creating new Web App...');
|
||||
}
|
||||
}
|
||||
|
||||
protected onExecuteError(step: WizardStep, stepIndex: number, error: Error) {
|
||||
if (error instanceof UserCancelledError) {
|
||||
return;
|
||||
}
|
||||
this.writeline(`Failed to create new Web App - ${error.message}`);
|
||||
this.writeline('');
|
||||
}
|
||||
}
|
||||
|
||||
class WebAppCreatorStepBase extends WizardStep {
|
||||
protected constructor(wizard: WizardBase, stepTitle: string, readonly azureAccount: AzureAccountWrapper) {
|
||||
super(wizard, stepTitle);
|
||||
}
|
||||
|
||||
protected getSelectedSubscription(): SubscriptionModels.Subscription {
|
||||
const subscriptionStep = <SubscriptionStep>this.wizard.findStep(step => step instanceof SubscriptionStep, 'The Wizard must have a SubscriptionStep.');
|
||||
|
||||
if (!subscriptionStep.subscription) {
|
||||
throw new Error('A subscription must be selected first.');
|
||||
}
|
||||
|
||||
return subscriptionStep.subscription;
|
||||
}
|
||||
|
||||
protected getSelectedResourceGroup(): ResourceModels.ResourceGroup {
|
||||
const resourceGroupStep = <ResourceGroupStep>this.wizard.findStep(step => step instanceof ResourceGroupStep, 'The Wizard must have a ResourceGroupStep.');
|
||||
|
||||
if (!resourceGroupStep.resourceGroup) {
|
||||
throw new Error('A resource group must be selected first.');
|
||||
}
|
||||
|
||||
return resourceGroupStep.resourceGroup;
|
||||
}
|
||||
|
||||
protected getSelectedAppServicePlan(): WebSiteModels.AppServicePlan {
|
||||
const appServicePlanStep = <AppServicePlanStep>this.wizard.findStep(step => step instanceof AppServicePlanStep, 'The Wizard must have a AppServicePlanStep.');
|
||||
|
||||
if (!appServicePlanStep.servicePlan) {
|
||||
throw new Error('An App Service Plan must be selected first.');
|
||||
}
|
||||
|
||||
return appServicePlanStep.servicePlan;
|
||||
}
|
||||
|
||||
protected getWebSite(): WebSiteModels.Site {
|
||||
const websiteStep = <WebsiteStep>this.wizard.findStep(step => step instanceof WebsiteStep, 'The Wizard must have a WebsiteStep.');
|
||||
|
||||
if (!websiteStep.website) {
|
||||
throw new Error('A website must be created first.');
|
||||
}
|
||||
|
||||
return websiteStep.website;
|
||||
}
|
||||
|
||||
protected getImageInfo(): { serverUrl: string, serverUser: string, serverPassword: string} {
|
||||
const websiteStep = <WebsiteStep>this.wizard.findStep(step => step instanceof WebsiteStep, 'The Wizard must have a WebsiteStep.');
|
||||
if (!websiteStep.website) {
|
||||
throw new Error('A website must be created first.');
|
||||
}
|
||||
|
||||
return websiteStep.imageInfo;
|
||||
}
|
||||
}
|
||||
|
||||
class SubscriptionStep extends SubscriptionStepBase {
|
||||
constructor(wizard: WizardBase, azureAccount: AzureAccountWrapper, subscrption?: SubscriptionModels.Subscription) {
|
||||
super(wizard, 'Select subscription', azureAccount);
|
||||
this._subscription = subscrption;
|
||||
}
|
||||
|
||||
async prompt(): Promise<void> {
|
||||
if (!!this.subscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
const quickPickItems = await this.getSubscriptionsAsQuickPickItems();
|
||||
const quickPickOptions = { placeHolder: `Select the subscription where the new Web App will be created in. (${this.stepProgressText})` };
|
||||
const result = await this.showQuickPick(quickPickItems, quickPickOptions);
|
||||
this._subscription = result.data;
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
this.wizard.writeline(`The new Web App will be created in subscription "${this.subscription.displayName}" (${this.subscription.subscriptionId}).`);
|
||||
}
|
||||
}
|
||||
|
||||
class ResourceGroupStep extends WebAppCreatorStepBase {
|
||||
private _createNew: boolean;
|
||||
private _rg: ResourceModels.ResourceGroup;
|
||||
|
||||
constructor(wizard: WizardBase, azureAccount: AzureAccountWrapper) {
|
||||
super(wizard, 'Select or create resource group', azureAccount);
|
||||
}
|
||||
|
||||
async prompt(): Promise<void> {
|
||||
const createNewItem: QuickPickItemWithData<ResourceModels.ResourceGroup> = {
|
||||
label: '$(plus) Create New Resource Group',
|
||||
description: '',
|
||||
data: null
|
||||
};
|
||||
const quickPickItems = [createNewItem];
|
||||
const quickPickOptions = { placeHolder: `Select the resource group where the new Web App will be created in. (${this.stepProgressText})` };
|
||||
const subscription = this.getSelectedSubscription();
|
||||
const resourceClient = new ResourceManagementClient(this.azureAccount.getCredentialByTenantId(subscription.tenantId), subscription.subscriptionId);
|
||||
var resourceGroups: ResourceModels.ResourceGroup[];
|
||||
var locations: SubscriptionModels.Location[];
|
||||
const resourceGroupsTask = util.listAll(resourceClient.resourceGroups, resourceClient.resourceGroups.list());
|
||||
const locationsTask = this.azureAccount.getLocationsBySubscription(this.getSelectedSubscription());
|
||||
await Promise.all([resourceGroupsTask, locationsTask]).then(results => {
|
||||
resourceGroups = results[0];
|
||||
locations = results[1];
|
||||
resourceGroups.forEach(rg => {
|
||||
quickPickItems.push({
|
||||
label: rg.name,
|
||||
description: `(${locations.find(l => l.name.toLowerCase() === rg.location.toLowerCase()).displayName})`,
|
||||
detail: '',
|
||||
data: rg
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const result = await this.showQuickPick(quickPickItems, quickPickOptions);
|
||||
|
||||
if (result !== createNewItem) {
|
||||
const rg = result.data;
|
||||
this._createNew = false;
|
||||
this._rg = rg;
|
||||
return;
|
||||
}
|
||||
|
||||
const newRgName = await this.showInputBox({
|
||||
prompt: 'Enter the name of the new resource group.',
|
||||
validateInput: (value: string) => {
|
||||
value = value.trim();
|
||||
|
||||
if (resourceGroups.findIndex(rg => rg.name.localeCompare(value) === 0) >= 0) {
|
||||
return `Resource group name "${value}" already exists.`;
|
||||
}
|
||||
|
||||
if (!value.match(/^[a-z0-9.\-_()]{0,89}[a-z0-9\-_()]$/ig)) {
|
||||
return 'Resource group name should be 1-90 characters long and can only include alphanumeric characters, periods, ' +
|
||||
'underscores, hyphens and parenthesis and cannot end in a period.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
const locationPickItems = locations.map<QuickPickItemWithData<SubscriptionModels.Location>>(location => {
|
||||
return {
|
||||
label: location.displayName,
|
||||
description: `(${location.name})`,
|
||||
detail: '',
|
||||
data: location
|
||||
};
|
||||
});
|
||||
const locationPickOptions = { placeHolder: 'Select the location of the new resource group.' };
|
||||
const pickedLocation = await this.showQuickPick(locationPickItems, locationPickOptions);
|
||||
|
||||
this._createNew = true;
|
||||
this._rg = {
|
||||
name: newRgName,
|
||||
location: pickedLocation.data.name
|
||||
}
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
if (!this._createNew) {
|
||||
this.wizard.writeline(`Existing resource group "${this._rg.name} (${this._rg.location})" will be used.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.wizard.writeline(`Creating new resource group "${this._rg.name} (${this._rg.location})"...`);
|
||||
const subscription = this.getSelectedSubscription();
|
||||
const resourceClient = new ResourceManagementClient(this.azureAccount.getCredentialByTenantId(subscription.tenantId), subscription.subscriptionId);
|
||||
this._rg = await resourceClient.resourceGroups.createOrUpdate(this._rg.name, this._rg);
|
||||
this.wizard.writeline(`Resource group created.`);
|
||||
}
|
||||
|
||||
get resourceGroup(): ResourceModels.ResourceGroup {
|
||||
return this._rg;
|
||||
}
|
||||
|
||||
get createNew(): boolean {
|
||||
return this._createNew;
|
||||
}
|
||||
}
|
||||
|
||||
class AppServicePlanStep extends WebAppCreatorStepBase {
|
||||
private _createNew: boolean;
|
||||
private _plan: WebSiteModels.AppServicePlan;
|
||||
|
||||
constructor(wizard: WizardBase, azureAccount: AzureAccountWrapper) {
|
||||
super(wizard, 'Select or create App Service Plan', azureAccount);
|
||||
}
|
||||
|
||||
async prompt(): Promise<void> {
|
||||
const createNewItem: QuickPickItemWithData<WebSiteModels.AppServicePlan> = {
|
||||
label: '$(plus) Create New App Service Plan',
|
||||
description: '',
|
||||
data: null
|
||||
};
|
||||
const quickPickItems = [createNewItem];
|
||||
const quickPickOptions = { placeHolder: `Select the App Service Plan for the new Web App. (${this.stepProgressText})` };
|
||||
const subscription = this.getSelectedSubscription();
|
||||
const client = new WebSiteManagementClient(this.azureAccount.getCredentialByTenantId(subscription.tenantId), subscription.subscriptionId);
|
||||
// You can create a web app and associate it with a plan from another resource group.
|
||||
// That's why we use list instead of listByResourceGroup below; and show resource group name in the quick pick list.
|
||||
const plans = await util.listAll(client.appServicePlans, client.appServicePlans.list());
|
||||
|
||||
plans.forEach(plan => {
|
||||
// Currently we only support Linux web apps.
|
||||
if (plan.kind.toLowerCase() === 'linux') {
|
||||
quickPickItems.push({
|
||||
label: plan.appServicePlanName,
|
||||
description: `${plan.sku.name} (${plan.geoRegion})`,
|
||||
detail: plan.resourceGroup,
|
||||
data: plan
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const pickedItem = await this.showQuickPick(quickPickItems, quickPickOptions);
|
||||
|
||||
if (pickedItem !== createNewItem) {
|
||||
this._createNew = false;
|
||||
this._plan = pickedItem.data;
|
||||
return;
|
||||
}
|
||||
|
||||
// Prompt for new plan information.
|
||||
const rg = this.getSelectedResourceGroup();
|
||||
const newPlanName = await this.showInputBox({
|
||||
prompt: 'Enter the name of the new App Service Plan.',
|
||||
validateInput: (value: string) => {
|
||||
value = value.trim();
|
||||
|
||||
if (plans.findIndex(plan => plan.resourceGroup.toLowerCase() === rg.name && value.localeCompare(plan.name) === 0) >= 0) {
|
||||
return `App Service Plan name "${value}" already exists in resource group "${rg.name}".`;
|
||||
}
|
||||
|
||||
if (!value.match(/^[a-z0-9\-]{0,39}$/ig)) {
|
||||
return 'App Service Plan name should be 1-40 characters long and can only include alphanumeric characters and hyphens.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Prompt for Pricing tier
|
||||
const pricingTiers: QuickPickItemWithData<WebSiteModels.SkuDescription>[] = [];
|
||||
const availableSkus = this.getPlanSkus();
|
||||
availableSkus.forEach(sku => {
|
||||
pricingTiers.push({
|
||||
label: sku.name,
|
||||
description: sku.tier,
|
||||
detail: '',
|
||||
data: sku
|
||||
});
|
||||
});
|
||||
const pickedSkuItem = await this.showQuickPick(pricingTiers, { placeHolder: 'Choose your pricing tier.' });
|
||||
const newPlanSku = pickedSkuItem.data;
|
||||
this._createNew = true;
|
||||
this._plan = {
|
||||
appServicePlanName: newPlanName,
|
||||
kind: 'linux', // Currently we only support Linux web apps.
|
||||
sku: newPlanSku,
|
||||
location: rg.location,
|
||||
reserved: true // The secret property - must be set to true to make it a Linux plan. Confirmed by the team who owns this API.
|
||||
};
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
if (!this._createNew) {
|
||||
this.wizard.writeline(`Existing App Service Plan "${this._plan.appServicePlanName} (${this._plan.sku.name})" will be used.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.wizard.writeline(`Creating new App Service Plan "${this._plan.appServicePlanName} (${this._plan.sku.name})"...`);
|
||||
const subscription = this.getSelectedSubscription();
|
||||
const rg = this.getSelectedResourceGroup();
|
||||
const websiteClient = new WebSiteManagementClient(this.azureAccount.getCredentialByTenantId(subscription.tenantId), subscription.subscriptionId);
|
||||
this._plan = await websiteClient.appServicePlans.createOrUpdate(rg.name, this._plan.appServicePlanName, this._plan);
|
||||
|
||||
this.wizard.writeline(`App Service Plan created.`);
|
||||
}
|
||||
|
||||
get servicePlan(): WebSiteModels.AppServicePlan {
|
||||
return this._plan;
|
||||
}
|
||||
|
||||
get createNew(): boolean {
|
||||
return this._createNew;
|
||||
}
|
||||
|
||||
private getPlanSkus(): WebSiteModels.SkuDescription[] {
|
||||
return [
|
||||
{
|
||||
name: 'S1',
|
||||
tier: 'Standard',
|
||||
size: 'S1',
|
||||
family: 'S',
|
||||
capacity: 1
|
||||
},
|
||||
{
|
||||
name: 'S2',
|
||||
tier: 'Standard',
|
||||
size: 'S2',
|
||||
family: 'S',
|
||||
capacity: 1
|
||||
},
|
||||
{
|
||||
name: 'S3',
|
||||
tier: 'Standard',
|
||||
size: 'S3',
|
||||
family: 'S',
|
||||
capacity: 1
|
||||
},
|
||||
{
|
||||
name: 'B1',
|
||||
tier: 'Basic',
|
||||
size: 'B1',
|
||||
family: 'B',
|
||||
capacity: 1
|
||||
},
|
||||
{
|
||||
name: 'B2',
|
||||
tier: 'Basic',
|
||||
size: 'B2',
|
||||
family: 'B',
|
||||
capacity: 1
|
||||
},
|
||||
{
|
||||
name: 'B3',
|
||||
tier: 'Basic',
|
||||
size: 'B3',
|
||||
family: 'B',
|
||||
capacity: 1
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class WebsiteStep extends WebAppCreatorStepBase {
|
||||
private _website: WebSiteModels.Site;
|
||||
private _serverUrl: string;
|
||||
private _serverUserName: string;
|
||||
private _serverPassword: string;
|
||||
private _imageName: string;
|
||||
|
||||
constructor(wizard: WizardBase, azureAccount: AzureAccountWrapper, context: AzureImageNode | DockerHubImageNode) {
|
||||
super(wizard, 'Create Web App', azureAccount);
|
||||
|
||||
this._serverUrl = context.serverUrl;
|
||||
this._serverPassword = context.password;
|
||||
this._serverUserName = context.userName;
|
||||
this._imageName = context.label;
|
||||
|
||||
}
|
||||
|
||||
async prompt(): Promise<void> {
|
||||
const subscription = this.getSelectedSubscription();
|
||||
const client = new WebSiteManagementClient(this.azureAccount.getCredentialByTenantId(subscription.tenantId), subscription.subscriptionId);
|
||||
let siteName: string;
|
||||
let siteNameOkay = false;
|
||||
|
||||
while (!siteNameOkay) {
|
||||
siteName = await this.showInputBox({
|
||||
prompt: `Enter a globally unique name for the new Web App. (${this.stepProgressText})`,
|
||||
validateInput: (value: string) => {
|
||||
value = value ? value.trim() : '';
|
||||
|
||||
if (!value.match(/^[a-z0-9\-]{1,60}$/ig)) {
|
||||
return 'App name should be 1-60 characters long and can only include alphanumeric characters and hyphens.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Check if the name has already been taken...
|
||||
const nameAvailability = await client.checkNameAvailability(siteName, 'site');
|
||||
siteNameOkay = nameAvailability.nameAvailable;
|
||||
|
||||
if (!siteNameOkay) {
|
||||
await vscode.window.showWarningMessage(nameAvailability.message);
|
||||
}
|
||||
}
|
||||
|
||||
let linuxFXVersion: string;
|
||||
if (this._serverUrl.length > 0) {
|
||||
// azure container registry
|
||||
linuxFXVersion = 'DOCKER|' + this._serverUrl + '/' + this._imageName;
|
||||
} else {
|
||||
// dockerhub
|
||||
linuxFXVersion = 'DOCKER|' + this._serverUserName + '/' + this._imageName;
|
||||
}
|
||||
|
||||
const rg = this.getSelectedResourceGroup();
|
||||
const plan = this.getSelectedAppServicePlan();
|
||||
|
||||
this._website = {
|
||||
name: siteName.trim(),
|
||||
kind: 'app,linux',
|
||||
location: rg.location,
|
||||
serverFarmId: plan.id,
|
||||
siteConfig: {
|
||||
linuxFxVersion: linuxFXVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
this.wizard.writeline(`Creating new Web App "${this._website.name}"...`);
|
||||
const subscription = this.getSelectedSubscription();
|
||||
const rg = this.getSelectedResourceGroup();
|
||||
const websiteClient = new WebSiteManagementClient(this.azureAccount.getCredentialByTenantId(subscription.tenantId), subscription.subscriptionId);
|
||||
|
||||
// If the plan is also newly created, its resource ID won't be available at this step's prompt stage, but should be available now.
|
||||
if (!this._website.serverFarmId) {
|
||||
this._website.serverFarmId = this.getSelectedAppServicePlan().id;
|
||||
}
|
||||
|
||||
this._website = await websiteClient.webApps.createOrUpdate(rg.name, this._website.name, this._website);
|
||||
|
||||
this.wizard.writeline('Updating Application Settings...');
|
||||
let appSettings: WebSiteModels.StringDictionary;
|
||||
|
||||
if (this._serverUrl.length > 0) {
|
||||
// azure container registry
|
||||
appSettings = {
|
||||
"id": this._website.id, "name": "appsettings", "location": this._website.location, "type": "Microsoft.Web/sites/config", "properties": {
|
||||
"DOCKER_REGISTRY_SERVER_URL": 'https://' + this._serverUrl, "DOCKER_REGISTRY_SERVER_USERNAME": this._serverUserName, "DOCKER_REGISTRY_SERVER_PASSWORD": this._serverPassword
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// dockerhub - dont set docker_registry_server_url
|
||||
appSettings = {
|
||||
"id": this._website.id, "name": "appsettings", "location": this._website.location, "type": "Microsoft.Web/sites/config", "properties": {
|
||||
"DOCKER_REGISTRY_SERVER_USERNAME": this._serverUserName, "DOCKER_REGISTRY_SERVER_PASSWORD": this._serverPassword
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
await websiteClient.webApps.updateApplicationSettings(rg.name, this._website.name, appSettings);
|
||||
this._website.siteConfig = await websiteClient.webApps.getConfiguration(rg.name, this._website.name);
|
||||
|
||||
this.wizard.writeline(`Restarting Site...`);
|
||||
await websiteClient.webApps.stop(rg.name, this._website.name);
|
||||
await websiteClient.webApps.start(rg.name, this._website.name);
|
||||
|
||||
this.wizard.writeline(`Web App "${this._website.name}" ready: https://${this._website.defaultHostName}`);
|
||||
this.wizard.writeline('');
|
||||
|
||||
}
|
||||
|
||||
get website(): WebSiteModels.Site {
|
||||
return this._website;
|
||||
}
|
||||
|
||||
get imageInfo(): { serverUrl: string, serverUser: string, serverPassword: string} {
|
||||
return {
|
||||
serverUrl: this._serverUrl,
|
||||
serverUser: this._serverUserName,
|
||||
serverPassword: this._serverPassword
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ShellScriptStep extends WebAppCreatorStepBase {
|
||||
constructor(wizard: WizardBase, azureAccount: AzureAccountWrapper) {
|
||||
super(wizard, 'Create Web App', azureAccount);
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const subscription = this.getSelectedSubscription();
|
||||
const rg = this.getSelectedResourceGroup();
|
||||
const plan = this.getSelectedAppServicePlan();
|
||||
const site = this.getWebSite();
|
||||
const imageInfo = this.getImageInfo();
|
||||
|
||||
const script = scriptTemplate.replace('%SUBSCRIPTION_NAME%', subscription.displayName)
|
||||
.replace('%RG_NAME%', rg.name)
|
||||
.replace('%LOCATION%', rg.location)
|
||||
.replace('%PLAN_NAME%', plan.name)
|
||||
.replace('%PLAN_SKU%', plan.sku.name)
|
||||
.replace('%SITE_NAME%', site.name)
|
||||
.replace('%IMAGENAME%', site.siteConfig.linuxFxVersion)
|
||||
.replace('%SERVERPASSWORD%','********')
|
||||
.replace('%SERVERURL%', imageInfo.serverUrl)
|
||||
.replace('%SERVERUSER%', imageInfo.serverUser);
|
||||
|
||||
let uri: vscode.Uri;
|
||||
if (vscode.workspace.rootPath) {
|
||||
let count = 0;
|
||||
const maxCount = 1024;
|
||||
|
||||
while (count < maxCount) {
|
||||
uri = vscode.Uri.file(path.join(vscode.workspace.rootPath, `deploy-${site.name}${count === 0 ? '' : count.toString()}.sh`));
|
||||
if (!vscode.workspace.textDocuments.find(doc => doc.uri.fsPath === uri.fsPath) && !fs.existsSync(uri.fsPath)) {
|
||||
uri = uri.with({ scheme: 'untitled' });
|
||||
break;
|
||||
} else {
|
||||
uri = null;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (uri) {
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
const editor = await vscode.window.showTextDocument(doc);
|
||||
await editor.edit(editorBuilder => editorBuilder.insert(new vscode.Position(0, 0), script));
|
||||
} else {
|
||||
const doc = await vscode.workspace.openTextDocument({ content: script, language: 'shellscript' });
|
||||
await vscode.window.showTextDocument(doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface LinuxRuntimeStack {
|
||||
name: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
const scriptTemplate = 'SUBSCRIPTION="%SUBSCRIPTION_NAME%"\n\
|
||||
RESOURCEGROUP="%RG_NAME%"\n\
|
||||
LOCATION="%LOCATION%"\n\
|
||||
PLANNAME="%PLAN_NAME%"\n\
|
||||
PLANSKU="%PLAN_SKU%"\n\
|
||||
SITENAME="%SITE_NAME%"\n\
|
||||
IMAGENAME="%IMAGENAME%"\n\
|
||||
SERVERPASSWORD="%SERVERPASSWORD%"\n\
|
||||
SERVERURL="%SERVERURL%"\n\
|
||||
SERVERUSER="%SERVERUSER%"\n\
|
||||
\n\
|
||||
# login supports device login, username/password, and service principals\n\
|
||||
# see https://docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest#az_login\n\
|
||||
az login\n\
|
||||
# list all of the available subscriptions\n\
|
||||
az account list -o table\n\
|
||||
# set the default subscription for subsequent operations\n\
|
||||
az account set --subscription $SUBSCRIPTION\n\
|
||||
# create a resource group for your application\n\
|
||||
az group create --name $RESOURCEGROUP --location $LOCATION\n\
|
||||
# create an appservice plan (a machine) where your site will run\n\
|
||||
az appservice plan create --name $PLANNAME --location $LOCATION --is-linux --sku $PLANSKU --resource-group $RESOURCEGROUP\n\
|
||||
# create the web application on the plan\n\
|
||||
az webapp create --name $SITENAME --plan $PLANNAME --deployment-container-image-name $IMAGENAME --resource-group $RESOURCEGROUP\n\
|
||||
\n\
|
||||
# configure the container information\n\
|
||||
az webapp config container set --docker-custom-image-name $IMAGENAME --docker-registry-server-url $SERVERURL --docker-registry-server-user $SERVERUSER --docker-registry-server-password $SERVERPASSWORD --name $SITENAME\n\
|
||||
\n\
|
||||
# restart and browse to the site\n\
|
||||
az webapp restart --name $SITENAME --resource-group $RESOURCEGROUP\n\
|
||||
az webapp browse --name $SITENAME --resource-group $RESOURCEGROUP\n\
|
||||
';
|
|
@ -0,0 +1,210 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { AzureAccountWrapper } from './azureAccountWrapper';
|
||||
import { SubscriptionModels } from 'azure-arm-resource';
|
||||
|
||||
export type WizardStatus = 'PromptCompleted' | 'Completed' | 'Faulted' | 'Cancelled';
|
||||
|
||||
export class WizardBase {
|
||||
private readonly _steps: WizardStep[] = [];
|
||||
private _result: WizardResult;
|
||||
|
||||
protected constructor(protected readonly output: vscode.OutputChannel) { }
|
||||
|
||||
async run(promptOnly = false): Promise<WizardResult> {
|
||||
// Go through the prompts...
|
||||
for (var i = 0; i < this.steps.length; i++) {
|
||||
const step = this.steps[i];
|
||||
|
||||
try {
|
||||
await this.steps[i].prompt();
|
||||
} catch (err) {
|
||||
if (err instanceof UserCancelledError) {
|
||||
return {
|
||||
status: 'Cancelled',
|
||||
step: step,
|
||||
error: err
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'Faulted',
|
||||
step: step,
|
||||
error: err
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (promptOnly) {
|
||||
return {
|
||||
status: 'PromptCompleted',
|
||||
step: this.steps[this.steps.length - 1],
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
return this.execute();
|
||||
}
|
||||
|
||||
async execute(): Promise<WizardResult> {
|
||||
// Execute each step...
|
||||
this.output.show(true);
|
||||
for (var i = 0; i < this.steps.length; i++) {
|
||||
const step = this.steps[i];
|
||||
|
||||
try {
|
||||
this.beforeExecute(step, i);
|
||||
await this.steps[i].execute();
|
||||
} catch (err) {
|
||||
this.onExecuteError(step, i, err);
|
||||
if (err instanceof UserCancelledError) {
|
||||
this._result = {
|
||||
status: 'Cancelled',
|
||||
step: step,
|
||||
error: err
|
||||
};
|
||||
} else {
|
||||
this._result = {
|
||||
status: 'Faulted',
|
||||
step: step,
|
||||
error: err
|
||||
};
|
||||
}
|
||||
return this._result;
|
||||
}
|
||||
}
|
||||
|
||||
this._result = {
|
||||
status: 'Completed',
|
||||
step: this.steps[this.steps.length - 1],
|
||||
error: null
|
||||
};
|
||||
|
||||
return this._result;
|
||||
}
|
||||
|
||||
get steps(): WizardStep[] {
|
||||
return this._steps;
|
||||
}
|
||||
|
||||
findStep(predicate: (step: WizardStep) => boolean, errorMessage: string): WizardStep {
|
||||
const step = this.steps.find(predicate);
|
||||
|
||||
if (!step) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
write(text: string) {
|
||||
this.output.append(text);
|
||||
}
|
||||
|
||||
writeline(text: string) {
|
||||
this.output.appendLine(text);
|
||||
}
|
||||
|
||||
protected beforeExecute(step: WizardStep, stepIndex: number) { }
|
||||
|
||||
protected onExecuteError(step: WizardStep, stepIndex: number, error: Error) { }
|
||||
}
|
||||
|
||||
export interface WizardResult {
|
||||
status: WizardStatus;
|
||||
step: WizardStep;
|
||||
error: Error;
|
||||
}
|
||||
|
||||
export class WizardStep {
|
||||
protected constructor(readonly wizard: WizardBase, readonly stepTitle: string) { }
|
||||
|
||||
async prompt(): Promise<void> { }
|
||||
async execute(): Promise<void> { }
|
||||
|
||||
get stepIndex(): number {
|
||||
return this.wizard.steps.findIndex(step => step === this);
|
||||
}
|
||||
|
||||
get stepProgressText(): string {
|
||||
return `Step ${this.stepIndex + 1}/${this.wizard.steps.length}`;
|
||||
}
|
||||
|
||||
async showQuickPick<T>(items: QuickPickItemWithData<T>[], options: vscode.QuickPickOptions, token?: vscode.CancellationToken): Promise<QuickPickItemWithData<T>> {
|
||||
const result = await vscode.window.showQuickPick(items, options, token);
|
||||
|
||||
if (!result) {
|
||||
throw new UserCancelledError();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async showInputBox(options?: vscode.InputBoxOptions, token?: vscode.CancellationToken): Promise<string> {
|
||||
const result = await vscode.window.showInputBox(options, token);
|
||||
|
||||
if (!result) {
|
||||
throw new UserCancelledError();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class SubscriptionStepBase extends WizardStep {
|
||||
constructor(wizard: WizardBase, title: string, readonly azureAccount: AzureAccountWrapper, protected _subscription?: SubscriptionModels.Subscription) {
|
||||
super(wizard, title);
|
||||
}
|
||||
|
||||
protected async getSubscriptionsAsQuickPickItems(): Promise<QuickPickItemWithData<SubscriptionModels.Subscription>[]> {
|
||||
const quickPickItems: QuickPickItemWithData<SubscriptionModels.Subscription>[] = [];
|
||||
|
||||
await Promise.all([this.azureAccount.getFilteredSubscriptions(), this.azureAccount.getAllSubscriptions()]).then(results => {
|
||||
const inFilterSubscriptions = results[0];
|
||||
const otherSubscriptions = results[1];
|
||||
|
||||
inFilterSubscriptions.forEach(s => {
|
||||
const index = otherSubscriptions.findIndex(other => other.subscriptionId === s.subscriptionId);
|
||||
if (index >= 0) { // Remove duplicated items from "all subscriptions".
|
||||
otherSubscriptions.splice(index, 1);
|
||||
}
|
||||
|
||||
const item = {
|
||||
label: `📌 ${s.displayName}`,
|
||||
description: '',
|
||||
detail: s.subscriptionId,
|
||||
data: s
|
||||
};
|
||||
|
||||
quickPickItems.push(item);
|
||||
});
|
||||
|
||||
otherSubscriptions.forEach(s => {
|
||||
const item = {
|
||||
label: s.displayName,
|
||||
description: '',
|
||||
detail: s.subscriptionId,
|
||||
data: s
|
||||
};
|
||||
|
||||
quickPickItems.push(item);
|
||||
});
|
||||
});
|
||||
|
||||
return quickPickItems;
|
||||
}
|
||||
|
||||
get subscription(): SubscriptionModels.Subscription {
|
||||
return this._subscription;
|
||||
}
|
||||
}
|
||||
|
||||
export interface QuickPickItemWithData<T> extends vscode.QuickPickItem {
|
||||
data: T;
|
||||
}
|
||||
|
||||
export class UserCancelledError extends Error { }
|
|
@ -1,22 +1,19 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { docker } from '../commands/utils/docker-endpoint';
|
||||
import { NodeBase } from './models/nodeBase';
|
||||
import { RootNode } from './models/rootNode';
|
||||
|
||||
export class DockerExplorerProvider implements vscode.TreeDataProvider<DockerNode> {
|
||||
export class DockerExplorerProvider implements vscode.TreeDataProvider<NodeBase> {
|
||||
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<DockerNode | undefined> = new vscode.EventEmitter<DockerNode | undefined>();
|
||||
readonly onDidChangeTreeData: vscode.Event<DockerNode | undefined> = this._onDidChangeTreeData.event;
|
||||
private _imagesNode: DockerNode;
|
||||
private _containersNode: DockerNode;
|
||||
private _imageCache: Docker.ImageDesc[];
|
||||
private _containerCache: Docker.ContainerDesc[];
|
||||
private _imageDebounceTimer: NodeJS.Timer;
|
||||
private _containerDebounceTimer: NodeJS.Timer;
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<NodeBase> = new vscode.EventEmitter<NodeBase>();
|
||||
readonly onDidChangeTreeData: vscode.Event<NodeBase> = this._onDidChangeTreeData.event;
|
||||
private _imagesNode: RootNode;
|
||||
private _containersNode: RootNode;
|
||||
private _registriesNode: RootNode
|
||||
|
||||
refresh(): void {
|
||||
this._onDidChangeTreeData.fire(this._imagesNode);
|
||||
this._onDidChangeTreeData.fire(this._containersNode);
|
||||
this._onDidChangeTreeData.fire(this._registriesNode);
|
||||
}
|
||||
|
||||
refreshImages(): void {
|
||||
|
@ -27,245 +24,37 @@ export class DockerExplorerProvider implements vscode.TreeDataProvider<DockerNod
|
|||
this._onDidChangeTreeData.fire(this._imagesNode);
|
||||
}
|
||||
|
||||
autoRefreshImages(): void {
|
||||
|
||||
const configOptions: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration('docker');
|
||||
const refreshInterval: number = configOptions.get<number>('explorerRefreshInterval', 1000);
|
||||
|
||||
// https://github.com/Microsoft/vscode/issues/30535
|
||||
// if (this._imagesNode.collapsibleState === vscode.TreeItemCollapsibleState.Collapsed) {
|
||||
// clearInterval(this._imageDebounceTimer);
|
||||
// return;
|
||||
// }
|
||||
|
||||
clearInterval(this._imageDebounceTimer);
|
||||
|
||||
if (refreshInterval > 0) {
|
||||
this._imageDebounceTimer = setInterval(async () => {
|
||||
|
||||
const opts = {
|
||||
"filters": {
|
||||
"dangling": ["false"]
|
||||
}
|
||||
};
|
||||
|
||||
let needToRefresh: boolean = false;
|
||||
let found: boolean = false;
|
||||
|
||||
const images: Docker.ImageDesc[] = await docker.getImageDescriptors(opts);
|
||||
|
||||
if (this._imageCache.length !== images.length) {
|
||||
needToRefresh = true;
|
||||
} else {
|
||||
for (let i: number = 0; i < this._imageCache.length; i++) {
|
||||
let before: string = JSON.stringify(this._imageCache[i]);
|
||||
for (let j: number = 0; j < images.length; j++) {
|
||||
let after: string = JSON.stringify(images[j]);
|
||||
if (before === after) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
needToRefresh = true;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needToRefresh) {
|
||||
this._onDidChangeTreeData.fire(this._imagesNode);
|
||||
this._imageCache = images;
|
||||
}
|
||||
|
||||
}, refreshInterval);
|
||||
}
|
||||
|
||||
refreshRegistries(): void {
|
||||
this._onDidChangeTreeData.fire(this._registriesNode);
|
||||
}
|
||||
|
||||
getTreeItem(element: NodeBase): vscode.TreeItem {
|
||||
return element.getTreeItem();
|
||||
}
|
||||
|
||||
containersAutoRefresh(): void {
|
||||
const configOptions: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration('docker');
|
||||
const refreshInterval = configOptions.get('explorerRefreshInterval', 1000);
|
||||
|
||||
// https://github.com/Microsoft/vscode/issues/30535
|
||||
// if (this._containersNode.collapsibleState === vscode.TreeItemCollapsibleState.Collapsed) {
|
||||
// clearInterval(this._containerDebounceTimer);
|
||||
// return;
|
||||
// }
|
||||
|
||||
clearInterval(this._containerDebounceTimer);
|
||||
|
||||
if (refreshInterval > 0) {
|
||||
this._containerDebounceTimer = setInterval(async () => {
|
||||
|
||||
const opts = {
|
||||
"filters": {
|
||||
"status": ["created", "restarting", "running", "paused", "exited", "dead"]
|
||||
}
|
||||
};
|
||||
|
||||
let needToRefresh: boolean = false;
|
||||
let found: boolean = false;
|
||||
|
||||
const containers: Docker.ContainerDesc[] = await docker.getContainerDescriptors(opts);
|
||||
|
||||
if (this._containerCache.length !== containers.length) {
|
||||
needToRefresh = true;
|
||||
} else {
|
||||
for (let i = 0; i < this._containerCache.length; i++) {
|
||||
let img: Docker.ContainerDesc = this._containerCache[i];
|
||||
for (let j = 0; j < containers.length; j++) {
|
||||
// can't do a full object compare because "Status" keeps changing for running containers
|
||||
if (img.Id === containers[j].Id &&
|
||||
img.Image === containers[j].Image &&
|
||||
img.State === containers[j].State) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
needToRefresh = true;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needToRefresh) {
|
||||
this._onDidChangeTreeData.fire(this._containersNode);
|
||||
this._containerCache = containers;
|
||||
}
|
||||
|
||||
}, refreshInterval);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getTreeItem(element: DockerNode): vscode.TreeItem {
|
||||
return element;
|
||||
}
|
||||
|
||||
async getChildren(element?: DockerNode): Promise<DockerNode[]> {
|
||||
return this.getDockerNodes(element);
|
||||
}
|
||||
|
||||
private async getDockerNodes(element?: DockerNode): Promise<DockerNode[]> {
|
||||
let iconPath: any = {};
|
||||
let contextValue: string = "";
|
||||
let node: DockerNode;
|
||||
const nodes: DockerNode[] = [];
|
||||
|
||||
async getChildren(element?: NodeBase): Promise<NodeBase[]> {
|
||||
if (!element) {
|
||||
this._imagesNode = new DockerNode('Images', vscode.TreeItemCollapsibleState.Collapsed, 'rootImages', null);
|
||||
this._containersNode = new DockerNode('Containers', vscode.TreeItemCollapsibleState.Collapsed, 'rootContainers', null);
|
||||
nodes.push(this._imagesNode);
|
||||
nodes.push(this._containersNode);
|
||||
} else {
|
||||
|
||||
if (element.contextValue === 'rootImages') {
|
||||
|
||||
let opts = {
|
||||
"filters": {
|
||||
"dangling": ["false"]
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const images: Docker.ImageDesc[] = await docker.getImageDescriptors(opts);
|
||||
this._imageCache = images;
|
||||
this.autoRefreshImages();
|
||||
|
||||
if (!images || images.length == 0) {
|
||||
return [];
|
||||
} else {
|
||||
iconPath = {
|
||||
light: path.join(__filename, '..', '..', '..', 'images', 'light', 'application.svg'),
|
||||
dark: path.join(__filename, '..', '..', '..', 'images', 'dark', 'application.svg')
|
||||
};
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
contextValue = "dockerImage";
|
||||
if (!images[i].RepoTags) {
|
||||
let node = new DockerNode("<none>:<none>", vscode.TreeItemCollapsibleState.None, contextValue, iconPath);
|
||||
node.imageDesc = images[i];
|
||||
nodes.push(node);
|
||||
} else {
|
||||
for (let j = 0; j < images[i].RepoTags.length; j++) {
|
||||
let node = new DockerNode(images[i].RepoTags[j], vscode.TreeItemCollapsibleState.None, contextValue, iconPath);
|
||||
node.imageDesc = images[i];
|
||||
nodes.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage('Unable to connect to Docker, is the Docker daemon running?');
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (element.contextValue === 'rootContainers') {
|
||||
|
||||
let opts = {
|
||||
"filters": {
|
||||
"status": ["created", "restarting", "running", "paused", "exited", "dead"]
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
const containers: Docker.ContainerDesc[] = await docker.getContainerDescriptors(opts);
|
||||
this._containerCache = containers;
|
||||
this.containersAutoRefresh();
|
||||
|
||||
if (!containers || containers.length == 0) {
|
||||
return [];
|
||||
} else {
|
||||
for (let i = 0; i < containers.length; i++) {
|
||||
if (['exited', 'dead'].includes(containers[i].State)) {
|
||||
contextValue = "dockerContainerStopped";
|
||||
iconPath = {
|
||||
light: path.join(__filename, '..', '..', '..', 'images', 'light', 'stoppedContainer.svg'),
|
||||
dark: path.join(__filename, '..', '..', '..', 'images', 'dark', 'stoppedContainer.svg')
|
||||
};
|
||||
} else {
|
||||
contextValue = "dockerContainerRunning";
|
||||
iconPath = {
|
||||
light: path.join(__filename, '..', '..', '..', 'images', 'light', 'runningContainer.svg'),
|
||||
dark: path.join(__filename, '..', '..', '..', 'images', 'dark', 'runningContainer.svg')
|
||||
};
|
||||
}
|
||||
|
||||
const containerName = containers[i].Names[0].substring(1);
|
||||
let node = new DockerNode(`${containers[i].Image} (${containerName}) [${containers[i].Status}]`, vscode.TreeItemCollapsibleState.None, contextValue, iconPath);
|
||||
node.containerDesc = containers[i];
|
||||
nodes.push(node);
|
||||
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage('Unable to connect to Docker, is the Docker daemon running?');
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
return this.getRootNodes();
|
||||
}
|
||||
return nodes;
|
||||
return element.getChildren(element);
|
||||
}
|
||||
|
||||
private async getRootNodes(): Promise<RootNode[]> {
|
||||
const rootNodes: RootNode[] = [];
|
||||
let node: RootNode;
|
||||
|
||||
node = new RootNode('Images', 'imagesRootNode', this._onDidChangeTreeData);
|
||||
this._imagesNode = node;
|
||||
rootNodes.push(node);
|
||||
|
||||
node = new RootNode('Containers', 'containersRootNode', this._onDidChangeTreeData);
|
||||
this._containersNode = node;
|
||||
rootNodes.push(node);
|
||||
|
||||
node = new RootNode('Registries', 'registriesRootNode', this._onDidChangeTreeData);
|
||||
this._registriesNode = node;
|
||||
rootNodes.push(node);
|
||||
|
||||
return rootNodes;
|
||||
}
|
||||
}
|
||||
|
||||
export class DockerNode extends vscode.TreeItem {
|
||||
|
||||
constructor(public readonly label: string,
|
||||
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
|
||||
public readonly contextValue: string,
|
||||
public readonly iconPath: any
|
||||
) {
|
||||
|
||||
super(label, collapsibleState);
|
||||
}
|
||||
|
||||
public containerDesc: Docker.ContainerDesc;
|
||||
public imageDesc: Docker.ImageDesc;
|
||||
|
||||
}
|
|
@ -0,0 +1,264 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
import request = require('request-promise');
|
||||
|
||||
import { NodeBase } from './nodeBase';
|
||||
import { SubscriptionClient, ResourceManagementClient, SubscriptionModels } from 'azure-arm-resource';
|
||||
import { AzureAccount, AzureSession } from '../../typings/azure-account.api';
|
||||
import { RegistryType } from './registryType';
|
||||
|
||||
const azureAccount: AzureAccount = vscode.extensions.getExtension<AzureAccount>('ms-vscode.azure-account')!.exports;
|
||||
|
||||
export class AzureRegistryNode extends NodeBase {
|
||||
|
||||
constructor(
|
||||
public readonly label: string,
|
||||
public readonly contextValue: string,
|
||||
public readonly iconPath: any = {}
|
||||
) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
public type: RegistryType;
|
||||
public subscription: SubscriptionModels.Subscription;
|
||||
public userName: string;
|
||||
public password: string;
|
||||
|
||||
getTreeItem(): vscode.TreeItem {
|
||||
return {
|
||||
label: this.label,
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
|
||||
contextValue: this.contextValue,
|
||||
iconPath: this.iconPath
|
||||
}
|
||||
}
|
||||
|
||||
async getChildren(element: AzureRegistryNode): Promise<AzureRepositoryNode[]> {
|
||||
const repoNodes: AzureRepositoryNode[] = [];
|
||||
let node: AzureRepositoryNode;
|
||||
|
||||
const tenantId: string = element.subscription.tenantId;
|
||||
const session: AzureSession = azureAccount.sessions.find((s, i, array) => s.tenantId.toLowerCase() === tenantId.toLowerCase());
|
||||
const { accessToken, refreshToken } = await acquireToken(session);
|
||||
|
||||
if (accessToken && refreshToken) {
|
||||
let refreshTokenARC;
|
||||
let accessTokenARC;
|
||||
|
||||
await request.post('https://' + element.label + '/oauth2/exchange', {
|
||||
form: {
|
||||
grant_type: 'access_token_refresh_token',
|
||||
service: element.label,
|
||||
tenant: tenantId,
|
||||
refresh_token: refreshToken,
|
||||
access_token: accessToken
|
||||
}
|
||||
}, (err, httpResponse, body) => {
|
||||
if (body.length > 0) {
|
||||
refreshTokenARC = JSON.parse(body).refresh_token;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
await request.post('https://' + element.label + '/oauth2/token', {
|
||||
form: {
|
||||
grant_type: 'refresh_token',
|
||||
service: element.label,
|
||||
scope: 'registry:catalog:*',
|
||||
refresh_token: refreshTokenARC
|
||||
}
|
||||
}, (err, httpResponse, body) => {
|
||||
if (body.length > 0) {
|
||||
accessTokenARC = JSON.parse(body).access_token;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
await request.get('https://' + element.label + '/v2/_catalog', {
|
||||
auth: {
|
||||
bearer: accessTokenARC
|
||||
}
|
||||
}, (err, httpResponse, body) => {
|
||||
if (body.length > 0) {
|
||||
const repositories = JSON.parse(body).repositories;
|
||||
for (let i = 0; i < repositories.length; i++) {
|
||||
node = new AzureRepositoryNode(repositories[i], "azureRepository");
|
||||
node.repository = element.label;
|
||||
node.subscription = element.subscription;
|
||||
node.accessTokenARC = accessTokenARC;
|
||||
node.refreshTokenARC = refreshTokenARC;
|
||||
node.userName = element.userName;
|
||||
node.password = element.password;
|
||||
repoNodes.push(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return repoNodes;
|
||||
}
|
||||
}
|
||||
|
||||
export class AzureRepositoryNode extends NodeBase {
|
||||
|
||||
constructor(
|
||||
public readonly label: string,
|
||||
public readonly contextValue: string,
|
||||
public readonly iconPath = {
|
||||
light: path.join(__filename, '..', '..', '..', '..', 'images', 'light', 'Repository_16x.svg'),
|
||||
dark: path.join(__filename, '..', '..', '..', '..', 'images', 'dark', 'Repository_16x.svg')
|
||||
}
|
||||
) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
public repository: string;
|
||||
public subscription: any;
|
||||
public accessTokenARC: string;
|
||||
public refreshTokenARC: string;
|
||||
public userName: string;
|
||||
public password: string;
|
||||
|
||||
getTreeItem(): vscode.TreeItem {
|
||||
return {
|
||||
label: this.label,
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
|
||||
contextValue: this.contextValue,
|
||||
iconPath: this.iconPath
|
||||
}
|
||||
}
|
||||
|
||||
async getChildren(element: AzureRepositoryNode): Promise<AzureImageNode[]> {
|
||||
const imageNodes: AzureImageNode[] = [];
|
||||
let node: AzureImageNode;
|
||||
|
||||
const { accessToken, refreshToken } = await acquireToken(element.subscription.session);
|
||||
|
||||
if (accessToken && refreshToken) {
|
||||
const tenantId = element.subscription.tenantId;
|
||||
let refreshTokenARC;
|
||||
let accessTokenARC;
|
||||
|
||||
await request.post('https://' + element.repository + '/oauth2/exchange', {
|
||||
form: {
|
||||
grant_type: 'access_token_refresh_token',
|
||||
service: element.repository,
|
||||
tenant: tenantId,
|
||||
refresh_token: refreshToken,
|
||||
access_token: accessToken
|
||||
}
|
||||
}, (err, httpResponse, body) => {
|
||||
if (body.length > 0) {
|
||||
refreshTokenARC = JSON.parse(body).refresh_token;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
await request.post('https://' + element.repository + '/oauth2/token', {
|
||||
form: {
|
||||
grant_type: 'refresh_token',
|
||||
service: element.repository,
|
||||
scope: 'repository:' + element.label + ':pull',
|
||||
refresh_token: refreshTokenARC
|
||||
}
|
||||
}, (err, httpResponse, body) => {
|
||||
if (body.length > 0) {
|
||||
accessTokenARC = JSON.parse(body).access_token;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
await request.get('https://' + element.repository + '/v2/' + element.label + '/tags/list', {
|
||||
auth: {
|
||||
bearer: accessTokenARC
|
||||
}
|
||||
}, (err, httpResponse, body) => {
|
||||
if (err) { return []; }
|
||||
if (body.length > 0) {
|
||||
const tags = JSON.parse(body).tags;
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
node = new AzureImageNode(element.label + ':' + tags[i], 'azureImageTag');
|
||||
node.serverUrl = element.repository;
|
||||
node.userName = element.userName;
|
||||
node.password = element.password;
|
||||
imageNodes.push(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return imageNodes;
|
||||
}
|
||||
}
|
||||
|
||||
export class AzureImageNode extends NodeBase {
|
||||
constructor(
|
||||
public readonly label: string,
|
||||
public readonly contextValue: string
|
||||
) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
public serverUrl: string;
|
||||
public userName: string;
|
||||
public password: string;
|
||||
|
||||
getTreeItem(): vscode.TreeItem {
|
||||
return {
|
||||
label: this.label,
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.None,
|
||||
contextValue: this.contextValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AzureNotSignedInNode extends NodeBase {
|
||||
constructor() {
|
||||
super('Sign in to Azure...');
|
||||
}
|
||||
|
||||
getTreeItem(): vscode.TreeItem {
|
||||
return {
|
||||
label: this.label,
|
||||
command: {
|
||||
title: this.label,
|
||||
command: 'azure-account.login'
|
||||
},
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AzureLoadingNode extends NodeBase {
|
||||
constructor() {
|
||||
super('Loading...');
|
||||
}
|
||||
|
||||
getTreeItem(): vscode.TreeItem {
|
||||
return {
|
||||
label: this.label,
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function acquireToken(session: AzureSession) {
|
||||
return new Promise<{ accessToken: string; refreshToken: string; }>((resolve, reject) => {
|
||||
const credentials: any = session.credentials;
|
||||
const environment: any = session.environment;
|
||||
credentials.context.acquireToken(environment.activeDirectoryResourceId, credentials.username, credentials.clientId, function (err: any, result: any) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({
|
||||
accessToken: result.accessToken,
|
||||
refreshToken: result.refreshToken
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import * as vscode from 'vscode';
|
||||
import { NodeBase } from './nodeBase';
|
||||
|
||||
export class ContainerNode extends NodeBase {
|
||||
|
||||
constructor(
|
||||
public readonly label: string,
|
||||
public readonly contextValue: string,
|
||||
public readonly iconPath: any = {}
|
||||
) {
|
||||
super(label)
|
||||
}
|
||||
|
||||
public containerDesc: Docker.ContainerDesc;
|
||||
|
||||
getTreeItem(): vscode.TreeItem {
|
||||
return {
|
||||
label: this.label,
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.None,
|
||||
contextValue: this.contextValue,
|
||||
iconPath: this.iconPath
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as dockerHub from './dockerHubUtils';
|
||||
import { NodeBase } from './nodeBase';
|
||||
|
||||
|
||||
export class DockerHubOrgNode extends NodeBase {
|
||||
|
||||
|
||||
constructor(
|
||||
public readonly label: string,
|
||||
public readonly contextValue: string,
|
||||
public readonly iconPath: any = {}
|
||||
) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
public repository: string;
|
||||
public userName: string;
|
||||
public password: string;
|
||||
public token: string;
|
||||
|
||||
getTreeItem(): vscode.TreeItem {
|
||||
return {
|
||||
label: this.label,
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
|
||||
contextValue: this.contextValue,
|
||||
iconPath: this.iconPath
|
||||
}
|
||||
}
|
||||
|
||||
async getChildren(element: DockerHubOrgNode): Promise<DockerHubRepositoryNode[]> {
|
||||
const repoNodes: DockerHubRepositoryNode[] = [];
|
||||
let node: DockerHubRepositoryNode;
|
||||
|
||||
const user: dockerHub.User = await dockerHub.getUser();
|
||||
const myRepos: dockerHub.Repository[] = await dockerHub.getRepositories(user.username);
|
||||
|
||||
for (let i = 0; i < myRepos.length; i++) {
|
||||
const myRepo: dockerHub.RepositoryInfo = await dockerHub.getRepositoryInfo(myRepos[i]);
|
||||
let iconPath = {
|
||||
light: path.join(__filename, '..', '..', '..', '..', 'images', 'light', 'Repository_16x.svg'),
|
||||
dark: path.join(__filename, '..', '..', '..', '..', 'images', 'dark', 'Repository_16x.svg')
|
||||
};
|
||||
node = new DockerHubRepositoryNode(myRepo.name, 'dockerHubRepository', iconPath);
|
||||
node.repository = myRepo;
|
||||
node.userName = element.userName;
|
||||
node.password = element.password;
|
||||
repoNodes.push(node);
|
||||
}
|
||||
return repoNodes;
|
||||
}
|
||||
}
|
||||
|
||||
export class DockerHubRepositoryNode extends NodeBase {
|
||||
|
||||
constructor(
|
||||
public readonly label: string,
|
||||
public readonly contextValue: string,
|
||||
public readonly iconPath: any = {}
|
||||
) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
public repository: any;
|
||||
public userName: string;
|
||||
public password: string;
|
||||
|
||||
getTreeItem(): vscode.TreeItem {
|
||||
return {
|
||||
label: this.label,
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
|
||||
contextValue: this.contextValue,
|
||||
iconPath: this.iconPath
|
||||
}
|
||||
}
|
||||
|
||||
async getChildren(element: DockerHubRepositoryNode): Promise<DockerHubImageNode[]> {
|
||||
const imageNodes: DockerHubImageNode[] = [];
|
||||
let node: DockerHubImageNode;
|
||||
|
||||
const myTags: dockerHub.Tag[] = await dockerHub.getRepositoryTags({namespace: element.repository.namespace, name: element.repository.name});
|
||||
for (let i = 0; i < myTags.length; i++) {
|
||||
node = new DockerHubImageNode(`${element.repository.name}:${myTags[i].name}`, 'dockerHubImageTag');
|
||||
node.password = element.password;
|
||||
node.userName = element.userName;
|
||||
imageNodes.push(node);
|
||||
}
|
||||
|
||||
return imageNodes;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export class DockerHubImageNode extends NodeBase {
|
||||
constructor(
|
||||
public readonly label: string,
|
||||
public readonly contextValue: string
|
||||
) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
// this needs to be empty string for DockerHub
|
||||
public serverUrl: string = '';
|
||||
public userName: string;
|
||||
public password: string;
|
||||
|
||||
getTreeItem(): vscode.TreeItem {
|
||||
return {
|
||||
label: this.label,
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.None,
|
||||
contextValue: this.contextValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as keytarType from 'keytar';
|
||||
import request = require('request-promise');
|
||||
|
||||
let _token: Token;
|
||||
|
||||
export interface Token {
|
||||
token: string
|
||||
};
|
||||
|
||||
export interface User {
|
||||
company: string
|
||||
date_joined: string
|
||||
full_name: string
|
||||
gravatar_email: string
|
||||
gravatar_url: string
|
||||
id: string
|
||||
is_admin: boolean
|
||||
is_staff: boolean
|
||||
location: string
|
||||
profile_url: string
|
||||
type: string
|
||||
username: string
|
||||
};
|
||||
|
||||
export interface Repository {
|
||||
namespace: string
|
||||
name: string
|
||||
};
|
||||
|
||||
export interface RepositoryInfo {
|
||||
user: string
|
||||
name: string
|
||||
namespace: string
|
||||
repository_type: string
|
||||
status: number
|
||||
description: string
|
||||
is_private: boolean
|
||||
is_automated: boolean
|
||||
can_edit: boolean
|
||||
star_count: number
|
||||
pull_count: number
|
||||
last_updated: string
|
||||
build_on_cloud: any
|
||||
has_starred: boolean
|
||||
full_description: string
|
||||
affiliation: string
|
||||
permissions: {
|
||||
read: boolean
|
||||
write: boolean
|
||||
admin: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
creator: number
|
||||
full_size: number
|
||||
id: number
|
||||
image_id: any
|
||||
images: Image[]
|
||||
last_updated: string
|
||||
last_updater: number
|
||||
name: string
|
||||
repository: number
|
||||
v2: boolean
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
architecture: string
|
||||
features: any
|
||||
os: string
|
||||
os_features: any
|
||||
os_version: any
|
||||
size: number
|
||||
variant: any
|
||||
}
|
||||
|
||||
export function dockerHubLogout(): void {
|
||||
|
||||
const keytar: typeof keytarType = require(`${vscode.env.appRoot}/node_modules/keytar`);
|
||||
if (keytar) {
|
||||
keytar.deletePassword('vscode-docker', 'dockerhub.token');
|
||||
keytar.deletePassword('vscode-docker', 'dockerhub.password');
|
||||
keytar.deletePassword('vscode-docker', 'dockerhub.username');
|
||||
}
|
||||
_token = null;
|
||||
}
|
||||
|
||||
export async function dockerHubLogin(): Promise<{ username: string, password: string, token: string }> {
|
||||
|
||||
const username: string = await vscode.window.showInputBox({ prompt: 'Username' });
|
||||
if (username) {
|
||||
const password: string = await vscode.window.showInputBox({ prompt: 'Password', password: true });
|
||||
if (password) {
|
||||
_token = await login(username, password);
|
||||
if (_token) {
|
||||
return { username: username, password: password, token: <string>_token.token };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
export function setDockerHubToken(token: string) {
|
||||
_token = { token: token };
|
||||
}
|
||||
|
||||
async function login(username: string, password: string): Promise<Token> {
|
||||
let t: Token;
|
||||
|
||||
let options = {
|
||||
method: 'POST',
|
||||
uri: 'https://hub.docker.com/v2/users/login',
|
||||
body: {
|
||||
username: username,
|
||||
password: password
|
||||
},
|
||||
json: true
|
||||
}
|
||||
|
||||
try {
|
||||
t = await request(options);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
vscode.window.showErrorMessage(error.error.detail);
|
||||
}
|
||||
|
||||
return t;
|
||||
|
||||
}
|
||||
|
||||
export async function getUser(): Promise<User> {
|
||||
let u: User;
|
||||
|
||||
let options = {
|
||||
method: 'GET',
|
||||
uri: 'https://hub.docker.com/v2/user/',
|
||||
headers: {
|
||||
Authorization: 'JWT ' + _token.token
|
||||
},
|
||||
json: true
|
||||
}
|
||||
|
||||
try {
|
||||
u = await request(options);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
vscode.window.showErrorMessage('Docker: Unable to retrieve User information');
|
||||
}
|
||||
|
||||
return u;
|
||||
|
||||
}
|
||||
|
||||
export async function getRepositories(username: string): Promise<Repository[]> {
|
||||
let repos: Repository[];
|
||||
|
||||
let options = {
|
||||
method: 'GET',
|
||||
uri: `https://hub.docker.com/v2/users/${username}/repositories/`,
|
||||
headers: {
|
||||
Authorization: 'JWT ' + _token.token
|
||||
},
|
||||
json: true
|
||||
}
|
||||
|
||||
try {
|
||||
repos = await request(options);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
vscode.window.showErrorMessage('Docker: Unable to retrieve Repositories');
|
||||
}
|
||||
|
||||
return repos;
|
||||
}
|
||||
|
||||
export async function getRepositoryInfo(repository: Repository): Promise<any> {
|
||||
|
||||
let res: any;
|
||||
|
||||
let options = {
|
||||
method: 'GET',
|
||||
uri: `https://hub.docker.com/v2/repositories/${repository.namespace}/${repository.name}/`,
|
||||
headers: {
|
||||
Authorization: 'JWT ' + _token.token
|
||||
},
|
||||
json: true
|
||||
}
|
||||
|
||||
try {
|
||||
res = await request(options);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
vscode.window.showErrorMessage('Docker: Unable to get Repository Details');
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function getRepositoryTags(repository: Repository): Promise<Tag[]> {
|
||||
let tagsPage: any;
|
||||
|
||||
let options = {
|
||||
method: 'GET',
|
||||
uri: `https://hub.docker.com/v2/repositories/${repository.namespace}/${repository.name}/tags?page_size=100&page=1`,
|
||||
headers: {
|
||||
Authorization: 'JWT ' + _token.token
|
||||
},
|
||||
json: true
|
||||
}
|
||||
|
||||
try {
|
||||
tagsPage = await request(options);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
vscode.window.showErrorMessage('Docker: Unable to retrieve Repository Tags');
|
||||
}
|
||||
|
||||
return <Tag[]>tagsPage.results;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { NodeBase } from './nodeBase';
|
||||
|
||||
export class ImageNode extends NodeBase {
|
||||
|
||||
constructor(
|
||||
public readonly label: string,
|
||||
public readonly contextValue: string,
|
||||
public readonly eventEmitter: vscode.EventEmitter<NodeBase>
|
||||
) {
|
||||
super(label)
|
||||
}
|
||||
|
||||
public imageDesc: Docker.ImageDesc
|
||||
|
||||
getTreeItem(): vscode.TreeItem {
|
||||
return {
|
||||
label: this.label,
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.None,
|
||||
contextValue: "localImageNode",
|
||||
iconPath: {
|
||||
light: path.join(__filename, '..', '..', '..', '..', 'images', 'light', 'application.svg'),
|
||||
dark: path.join(__filename, '..', '..', '..', '..', 'images', 'dark', 'application.svg')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no children
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class NodeBase {
|
||||
readonly label: string;
|
||||
|
||||
protected constructor(label: string) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
getTreeItem(): vscode.TreeItem {
|
||||
return {
|
||||
label: this.label,
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.None
|
||||
};
|
||||
}
|
||||
|
||||
async getChildren(element): Promise<NodeBase[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as dockerHub from './dockerHubUtils'
|
||||
import * as keytarType from 'keytar';
|
||||
import * as ContainerModels from '../../node_modules/azure-arm-containerregistry/lib/models';
|
||||
import * as ContainerOps from '../../node_modules/azure-arm-containerregistry/lib/operations';
|
||||
import ContainerRegistryManagementClient = require('azure-arm-containerregistry');
|
||||
import { AzureAccount, AzureSession } from '../../typings/azure-account.api';
|
||||
import { AzureRegistryNode, AzureLoadingNode, AzureNotSignedInNode } from './azureRegistryNodes';
|
||||
import { DockerHubOrgNode } from './dockerHubNodes';
|
||||
import { NodeBase } from './nodeBase';
|
||||
import { RegistryType } from './registryType';
|
||||
import { ServiceClientCredentials } from 'ms-rest';
|
||||
import { SubscriptionClient, ResourceManagementClient, SubscriptionModels } from 'azure-arm-resource';
|
||||
|
||||
const ContainerRegistryManagement = require('azure-arm-containerregistry');
|
||||
|
||||
const azureAccount: AzureAccount = vscode.extensions.getExtension<AzureAccount>('ms-vscode.azure-account')!.exports;
|
||||
|
||||
export class RegistryRootNode extends NodeBase {
|
||||
private _keytar: typeof keytarType;
|
||||
|
||||
constructor(
|
||||
public readonly label: string,
|
||||
public readonly contextValue: string,
|
||||
public readonly eventEmitter: vscode.EventEmitter<NodeBase>
|
||||
) {
|
||||
super(label);
|
||||
try {
|
||||
this._keytar = require(`${vscode.env.appRoot}/node_modules/keytar`);
|
||||
} catch (e) {
|
||||
// unable to find keytar
|
||||
}
|
||||
|
||||
if (this.eventEmitter && this.contextValue === 'azureRegistryRootNode') {
|
||||
|
||||
azureAccount.onFiltersChanged((e) => {
|
||||
this.eventEmitter.fire(this);
|
||||
});
|
||||
azureAccount.onStatusChanged((e) => {
|
||||
this.eventEmitter.fire(this);
|
||||
});
|
||||
azureAccount.onSessionsChanged((e) => {
|
||||
this.eventEmitter.fire(this);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getTreeItem(): vscode.TreeItem {
|
||||
return {
|
||||
label: this.label,
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
|
||||
contextValue: this.contextValue,
|
||||
}
|
||||
}
|
||||
|
||||
async getChildren(element: RegistryRootNode): Promise<NodeBase[]> {
|
||||
if (element.contextValue === 'azureRegistryRootNode') {
|
||||
return this.getAzureRegistries();
|
||||
} else if (element.contextValue === 'dockerHubRootNode') {
|
||||
return this.getDockerHubOrgs();
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async getDockerHubOrgs(): Promise<DockerHubOrgNode[]> {
|
||||
const orgNodes: DockerHubOrgNode[] = [];
|
||||
|
||||
let id: { username: string, password: string, token: string } = { username: null, password: null, token: null };
|
||||
|
||||
if (this._keytar) {
|
||||
id.token = await this._keytar.getPassword('vscode-docker', 'dockerhub.token');
|
||||
id.username = await this._keytar.getPassword('vscode-docker', 'dockerhub.username');
|
||||
id.password = await this._keytar.getPassword('vscode-docker', 'dockerhub.password');
|
||||
}
|
||||
|
||||
if (!id.token) {
|
||||
id = await dockerHub.dockerHubLogin();
|
||||
if (id && id.token) {
|
||||
if (this._keytar) {
|
||||
this._keytar.setPassword('vscode-docker', 'dockerhub.token', id.token);
|
||||
this._keytar.setPassword('vscode-docker', 'dockerhub.password', id.password);
|
||||
this._keytar.setPassword('vscode-docker', 'dockerhub.username', id.username);
|
||||
}
|
||||
} else {
|
||||
return orgNodes;
|
||||
}
|
||||
} else {
|
||||
dockerHub.setDockerHubToken(id.token);
|
||||
}
|
||||
|
||||
const user: dockerHub.User = await dockerHub.getUser();
|
||||
const myRepos: dockerHub.Repository[] = await dockerHub.getRepositories(user.username);
|
||||
const namespaces = [...new Set(myRepos.map(item => item.namespace))];
|
||||
namespaces.forEach((namespace) => {
|
||||
let iconPath = {
|
||||
light: path.join(__filename, '..', '..', '..', '..', 'images', 'light', 'Registry_16x.svg'),
|
||||
dark: path.join(__filename, '..', '..', '..', '..', 'images', 'dark', 'Registry_16x.svg')
|
||||
};
|
||||
let node = new DockerHubOrgNode(`${namespace}`, 'dockerHubNamespace', iconPath);
|
||||
node.userName = id.username;
|
||||
node.password = id.password;
|
||||
node.token = id.token;
|
||||
orgNodes.push(node);
|
||||
});
|
||||
|
||||
return orgNodes;
|
||||
}
|
||||
|
||||
private async getAzureRegistries(): Promise<AzureRegistryNode[] | AzureLoadingNode[] | AzureNotSignedInNode[]> {
|
||||
const loggedIntoAzure: boolean = await azureAccount.waitForLogin()
|
||||
const azureRegistryNodes: AzureRegistryNode[] = [];
|
||||
|
||||
if (azureAccount.status === 'Initializing' || azureAccount.status === 'LoggingIn') {
|
||||
return [new AzureLoadingNode()];
|
||||
}
|
||||
|
||||
if (azureAccount.status === 'LoggedOut') {
|
||||
return [new AzureNotSignedInNode()];
|
||||
}
|
||||
|
||||
if (loggedIntoAzure) {
|
||||
|
||||
const subs: SubscriptionModels.Subscription[] = this.getFilteredSubscriptions();
|
||||
|
||||
for (let i = 0; i < subs.length; i++) {
|
||||
|
||||
const client = new ContainerRegistryManagement(this.getCredentialByTenantId(subs[i].tenantId), subs[i].subscriptionId);
|
||||
const registries: ContainerModels.RegistryListResult = await client.registries.list();
|
||||
|
||||
for (let j = 0; j < registries.length; j++) {
|
||||
|
||||
if (registries[j].adminUserEnabled && registries[j].sku.tier.includes('Managed')) {
|
||||
const resourceGroup: string = registries[j].id.slice(registries[j].id.search('resourceGroups/') + 'resourceGroups/'.length, registries[j].id.search('/providers/'));
|
||||
const creds: ContainerModels.RegistryListCredentialsResult = await client.registries.listCredentials(resourceGroup, registries[j].name);
|
||||
|
||||
let iconPath = {
|
||||
light: path.join(__filename, '..', '..', '..', '..', 'images', 'light', 'Registry_16x.svg'),
|
||||
dark: path.join(__filename, '..', '..', '..', '..', 'images', 'dark', 'Registry_16x.svg')
|
||||
};
|
||||
let node = new AzureRegistryNode(registries[j].loginServer, 'registry', iconPath);
|
||||
node.type = RegistryType.Azure;
|
||||
node.password = creds.passwords[0].value;
|
||||
node.userName = creds.username;
|
||||
node.subscription = subs[i];
|
||||
azureRegistryNodes.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return azureRegistryNodes;
|
||||
}
|
||||
|
||||
private getCredentialByTenantId(tenantId: string): ServiceClientCredentials {
|
||||
const session = azureAccount.sessions.find((s, i, array) => s.tenantId.toLowerCase() === tenantId.toLowerCase());
|
||||
|
||||
if (session) {
|
||||
return session.credentials;
|
||||
}
|
||||
|
||||
throw new Error(`Failed to get credentials, tenant ${tenantId} not found.`);
|
||||
}
|
||||
|
||||
private getFilteredSubscriptions(): SubscriptionModels.Subscription[] {
|
||||
return azureAccount.filters.map<SubscriptionModels.Subscription>(filter => {
|
||||
return {
|
||||
id: filter.subscription.id,
|
||||
session: filter.session,
|
||||
subscriptionId: filter.subscription.subscriptionId,
|
||||
tenantId: filter.session.tenantId,
|
||||
displayName: filter.subscription.displayName,
|
||||
state: filter.subscription.state,
|
||||
subscriptionPolicies: filter.subscription.subscriptionPolicies,
|
||||
authorizationSource: filter.subscription.authorizationSource
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
export enum RegistryType {
|
||||
DockerHub,
|
||||
Azure,
|
||||
Unknown
|
||||
}
|
|
@ -0,0 +1,250 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { ContainerNode } from './containerNode';
|
||||
import { docker } from '../../commands/utils/docker-endpoint';
|
||||
import { ImageNode } from './imageNode';
|
||||
import { NodeBase } from './nodeBase';
|
||||
import { RegistryRootNode } from './registryRootNode';
|
||||
|
||||
export class RootNode extends NodeBase {
|
||||
private _imageCache: Docker.ImageDesc[];
|
||||
private _imageDebounceTimer: NodeJS.Timer;
|
||||
private _imagesNode: RootNode;
|
||||
private _containerCache: Docker.ContainerDesc[];
|
||||
private _containerDebounceTimer: NodeJS.Timer;
|
||||
private _containersNode: RootNode;
|
||||
|
||||
constructor(
|
||||
public readonly label: string,
|
||||
public readonly contextValue: string,
|
||||
public eventEmitter: vscode.EventEmitter<NodeBase>
|
||||
) {
|
||||
super(label);
|
||||
if (this.contextValue === 'imagesRootNode') {
|
||||
this._imagesNode = this;
|
||||
} else if (this.contextValue === 'containersRootNode') {
|
||||
this._containersNode = this;
|
||||
}
|
||||
}
|
||||
|
||||
autoRefreshImages(): void {
|
||||
|
||||
const configOptions: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration('docker');
|
||||
const refreshInterval: number = configOptions.get<number>('explorerRefreshInterval', 1000);
|
||||
|
||||
// https://github.com/Microsoft/vscode/issues/30535
|
||||
// if (this._imagesNode.collapsibleState === vscode.TreeItemCollapsibleState.Collapsed) {
|
||||
// clearInterval(this._imageDebounceTimer);
|
||||
// return;
|
||||
// }
|
||||
|
||||
clearInterval(this._imageDebounceTimer);
|
||||
|
||||
if (refreshInterval > 0) {
|
||||
this._imageDebounceTimer = setInterval(async () => {
|
||||
|
||||
const opts = {
|
||||
"filters": {
|
||||
"dangling": ["false"]
|
||||
}
|
||||
};
|
||||
|
||||
let needToRefresh: boolean = false;
|
||||
let found: boolean = false;
|
||||
|
||||
const images: Docker.ImageDesc[] = await docker.getImageDescriptors(opts);
|
||||
|
||||
if (!this._imageCache) {
|
||||
this._imageCache = images;
|
||||
}
|
||||
|
||||
if (this._imageCache.length !== images.length) {
|
||||
needToRefresh = true;
|
||||
} else {
|
||||
for (let i: number = 0; i < this._imageCache.length; i++) {
|
||||
let before: string = JSON.stringify(this._imageCache[i]);
|
||||
for (let j: number = 0; j < images.length; j++) {
|
||||
let after: string = JSON.stringify(images[j]);
|
||||
if (before === after) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
needToRefresh = true;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needToRefresh) {
|
||||
this.eventEmitter.fire(this._imagesNode);
|
||||
this._imageCache = images;
|
||||
}
|
||||
|
||||
}, refreshInterval);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getTreeItem(): vscode.TreeItem {
|
||||
return {
|
||||
label: this.label,
|
||||
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
|
||||
contextValue: this.contextValue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async getChildren(element): Promise<NodeBase[]> {
|
||||
|
||||
if (element.contextValue === 'imagesRootNode') {
|
||||
this.autoRefreshImages();
|
||||
return this.getImages();
|
||||
}
|
||||
if (element.contextValue === 'containersRootNode') {
|
||||
this.autoRefreshContainers();
|
||||
return this.getContainers();
|
||||
}
|
||||
if (element.contextValue === 'registriesRootNode') {
|
||||
return this.getRegistries()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async getImages(): Promise<ImageNode[]> {
|
||||
const imageNodes: ImageNode[] = [];
|
||||
const images: Docker.ImageDesc[] = await docker.getImageDescriptors();
|
||||
|
||||
if (!images || images.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
if (!images[i].RepoTags) {
|
||||
let node = new ImageNode("<none>:<none>", "localImageNode", this.eventEmitter);
|
||||
node.imageDesc = images[i];
|
||||
imageNodes.push(node);
|
||||
} else {
|
||||
for (let j = 0; j < images[i].RepoTags.length; j++) {
|
||||
let node = new ImageNode(images[i].RepoTags[j], "localImageNode", this.eventEmitter);
|
||||
node.imageDesc = images[i];
|
||||
imageNodes.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imageNodes;
|
||||
}
|
||||
|
||||
autoRefreshContainers(): void {
|
||||
const configOptions: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration('docker');
|
||||
const refreshInterval = configOptions.get('explorerRefreshInterval', 1000);
|
||||
|
||||
// https://github.com/Microsoft/vscode/issues/30535
|
||||
// if (this._containersNode.collapsibleState === vscode.TreeItemCollapsibleState.Collapsed) {
|
||||
// clearInterval(this._containerDebounceTimer);
|
||||
// return;
|
||||
// }
|
||||
|
||||
clearInterval(this._containerDebounceTimer);
|
||||
|
||||
if (refreshInterval > 0) {
|
||||
this._containerDebounceTimer = setInterval(async () => {
|
||||
|
||||
const opts = {
|
||||
"filters": {
|
||||
"status": ["created", "restarting", "running", "paused", "exited", "dead"]
|
||||
}
|
||||
};
|
||||
|
||||
let needToRefresh: boolean = false;
|
||||
let found: boolean = false;
|
||||
|
||||
const containers: Docker.ContainerDesc[] = await docker.getContainerDescriptors(opts);
|
||||
|
||||
if (!this._containerCache) {
|
||||
this._containerCache = containers;
|
||||
}
|
||||
|
||||
if (this._containerCache.length !== containers.length) {
|
||||
needToRefresh = true;
|
||||
} else {
|
||||
for (let i = 0; i < this._containerCache.length; i++) {
|
||||
let ctr: Docker.ContainerDesc = this._containerCache[i];
|
||||
for (let j = 0; j < containers.length; j++) {
|
||||
// can't do a full object compare because "Status" keeps changing for running containers
|
||||
if (ctr.Id === containers[j].Id &&
|
||||
ctr.Image === containers[j].Image &&
|
||||
ctr.State === containers[j].State) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
needToRefresh = true;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needToRefresh) {
|
||||
this.eventEmitter.fire(this._containersNode);
|
||||
this._containerCache = containers;
|
||||
}
|
||||
|
||||
}, refreshInterval);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async getContainers(): Promise<ContainerNode[]> {
|
||||
const containerNodes: ContainerNode[] = [];
|
||||
let contextValue: string;
|
||||
let iconPath: any = {};
|
||||
|
||||
const opts = {
|
||||
"filters": {
|
||||
"status": ["created", "restarting", "running", "paused", "exited", "dead"]
|
||||
}
|
||||
};
|
||||
|
||||
const containers: Docker.ContainerDesc[] = await docker.getContainerDescriptors(opts);
|
||||
|
||||
if (!containers || containers.length == 0) {
|
||||
return [];
|
||||
} else {
|
||||
for (let i = 0; i < containers.length; i++) {
|
||||
if (['exited', 'dead'].includes(containers[i].State)) {
|
||||
contextValue = "stoppedLocalContainerNode";
|
||||
iconPath = {
|
||||
light: path.join(__filename, '..', '..', '..', '..', 'images', 'light', 'stoppedContainer.svg'),
|
||||
dark: path.join(__filename, '..', '..', '..', '..', 'images', 'dark', 'stoppedContainer.svg')
|
||||
};
|
||||
} else {
|
||||
contextValue = "runningLocalContainerNode";
|
||||
iconPath = {
|
||||
light: path.join(__filename, '..', '..', '..', '..', 'images', 'light', 'runningContainer.svg'),
|
||||
dark: path.join(__filename, '..', '..', '..', '..', 'images', 'dark', 'runningContainer.svg')
|
||||
};
|
||||
}
|
||||
|
||||
let containerNode: ContainerNode = new ContainerNode(`${containers[i].Image} (${containers[i].Names[0].substring(1)}) [${containers[i].Status}]`, contextValue, iconPath);
|
||||
containerNode.containerDesc = containers[i];
|
||||
containerNodes.push(containerNode);
|
||||
|
||||
}
|
||||
}
|
||||
return containerNodes;
|
||||
}
|
||||
|
||||
private async getRegistries(): Promise<RegistryRootNode[]> {
|
||||
const registryRootNodes: RegistryRootNode[] = [];
|
||||
registryRootNodes.push(new RegistryRootNode('DockerHub', "dockerHubRootNode", null));
|
||||
registryRootNodes.push(new RegistryRootNode('Azure', "azureRegistryRootNode", this.eventEmitter));
|
||||
return registryRootNodes;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#2d2d30}.icon-vs-out{fill:#2d2d30}.icon-vs-bg{fill:#c5c5c5}.icon-vs-fg{fill:#2b282e}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M8 16c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8z" id="outline"/><path class="icon-vs-bg" d="M8 1a7 7 0 0 0-6.158 10.33l4.451-4.451L8.414 9l3-3H10V5h3v3h-1V6.829l-3.586 3.586-2.121-2.122-3.894 3.893A6.984 6.984 0 0 0 8 15 7 7 0 1 0 8 1z" id="iconBg"/><path class="icon-vs-fg" d="M12 8V6.829l-3.586 3.586-2.121-2.122-3.894 3.893a7.062 7.062 0 0 1-.558-.856l4.451-4.451L8.414 9l3-3H10V5h3v3h-1z" id="iconFg"/></svg>
|
После Ширина: | Высота: | Размер: 728 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.st0{opacity:0}.st0,.st1{fill:#f6f6f6}.st2{fill:#424242}</style><g id="outline"><path class="st0" d="M0 0h16v16H0z"/><path class="st1" d="M10 5.57l2.465 2.5L16 4.535 12.465 1 8.93 4.535l.5.465H6V1H1v13h13V9h-4z"/></g><g id="icon_x5F_bg"><path class="st2" d="M2 6h3v3H2zM2 10h3v3H2zM2 2h3v3H2z"/><path transform="rotate(-45.001 12.465 4.535)" class="st2" d="M10.965 3.035h3v3h-3z"/><path class="st2" d="M10 10h3v3h-3zM6 6h3v3H6zM6 10h3v3H6z"/></g></svg>
|
После Ширина: | Высота: | Размер: 519 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-vs-bg{fill:#424242}.icon-vs-fg{fill:#f0eff1}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M15 14H1V1h14v13z" id="outline"/><path class="icon-vs-bg" d="M14 5v8H2V5h4v2h4V5h4zm0-3H2v2h12V2z" id="iconBg"/><path class="icon-vs-fg" d="M14 4v1h-4v2H6V5H2V4h12z" id="iconFg"/></svg>
|
После Ширина: | Высота: | Размер: 486 B |
|
@ -0,0 +1 @@
|
|||
<svg viewBox="1 1 45 45" focusable="false" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svg="http://www.w3.org/2000/svg" class="" role="presentation" id="FxSymbol0-025" width="100%" height="100%"><g><title></title><path class="msportalfx-svg-c15" d="M15 34H9c-4-1-7.085-4.151-7.085-8.33 0-3.459 2.232-6.617 5.263-7.819a10.937 10.937 0 0 1-.055-1.092c0-5.94 4.817-10.753 10.757-10.753a10.74 10.74 0 0 1 8.376 4.019 8.672 8.672 0 0 1 3.924-.927c4.704 0 8.778 4.173 8.944 8.839l-16.063-6.625L15 16.167V34z"></path><path d="M45 38.435l-22.132 4.218.102-15.559L45 31.179v7.256zM22.945 13.348L45 21.649v8.311l-22.017-4.691" fill="#959595"></path><path d="M22.983 25.268l-5.999 2.789v-11.08l5.96-3.63M44 29l-20-4.563V14.5l20 7.375V29zm-11-3.563l2 .548v-6.478l-2-.7v6.63zm-2-7.268l-2-.722v6.926l2 .564v-6.768zm6 8.363l1.972.52.034-6.175-2.006-.67v6.325zm-12-3.187l2 .515v-7.07l-2-.658v7.213zm18-1.101l-2-.7v6.034l2 .548v-5.882zM24 28.75v12.833l20-3.667v-6.083L24 28.75zm1 11.333v-9.875l2 .25v9.292l-2 .333zm6-1.01l-2 .365v-8.73L31 31v8.073zm2-.288v-7.577l2 .25v6.958l-2 .369zm6-1.052l-2 .34v-6.365L39 32v5.733zm4.068-.647L41 37.444v-5.236l1.931.241.137 4.637z" fill="#b3b4b5"></path><path d="M19.009 25.077l-1 .585v-7.88l1-.538v7.833zM21 16.098l-.997.688L20 24.44l1-.547v-7.795z" fill="#959595"></path><path d="M16.984 39.1l-.038-9.094 6.021-2.912L23 42.691" fill="#b3b4b5"></path><path d="M18.887 39.066l-.91-.463v-7.679l.91-.41v8.552zm2.147-9.518l-.993.435-.038 9.69 1.032.535v-10.66z" fill="#959595"></path><path class="msportalfx-svg-c04" d="M42.981 22.892l-17.977-6.031.029-.779 17.948 6.175v.635zm-.05 9.556l-17.899-2.246v.777l17.916 2.119-.017-.65zM21 17.129v-.982l-2.904 1.611-.039.825L21 17.129zm-3.019 14.369l3.049-1.104.006-.847-3.061 1.374.006.577z"></path><path class="msportalfx-svg-c01" d="M45 21.695v16.74L23 42.65l16.669-22.973L45 21.695z" opacity="0.2"></path></g></svg>
|
После Ширина: | Высота: | Размер: 1.8 KiB |
После Ширина: | Высота: | Размер: 70 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#2d2d30}.icon-vs-out{fill:#2d2d30}.icon-vs-bg{fill:#c5c5c5}.icon-vs-fg{fill:#2b282e}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M8 16c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8z" id="outline"/><path class="icon-vs-bg" d="M8 1a7 7 0 0 0-6.158 10.33l4.451-4.451L8.414 9l3-3H10V5h3v3h-1V6.829l-3.586 3.586-2.121-2.122-3.894 3.893A6.984 6.984 0 0 0 8 15 7 7 0 1 0 8 1z" id="iconBg"/><path class="icon-vs-fg" d="M12 8V6.829l-3.586 3.586-2.121-2.122-3.894 3.893a7.062 7.062 0 0 1-.558-.856l4.451-4.451L8.414 9l3-3H10V5h3v3h-1z" id="iconFg"/></svg>
|
После Ширина: | Высота: | Размер: 728 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.st0{opacity:0}.st0,.st1{fill:#f6f6f6}.st2{fill:#424242}</style><g id="outline"><path class="st0" d="M0 0h16v16H0z"/><path class="st1" d="M10 5.57l2.465 2.5L16 4.535 12.465 1 8.93 4.535l.5.465H6V1H1v13h13V9h-4z"/></g><g id="icon_x5F_bg"><path class="st2" d="M2 6h3v3H2zM2 10h3v3H2zM2 2h3v3H2z"/><path transform="rotate(-45.001 12.465 4.535)" class="st2" d="M10.965 3.035h3v3h-3z"/><path class="st2" d="M10 10h3v3h-3zM6 6h3v3H6zM6 10h3v3H6z"/></g></svg>
|
После Ширина: | Высота: | Размер: 519 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-vs-bg{fill:#424242}.icon-vs-fg{fill:#f0eff1}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M15 14H1V1h14v13z" id="outline"/><path class="icon-vs-bg" d="M14 5v8H2V5h4v2h4V5h4zm0-3H2v2h12V2z" id="iconBg"/><path class="icon-vs-fg" d="M14 4v1h-4v2H6V5H2V4h12z" id="iconFg"/></svg>
|
После Ширина: | Высота: | Размер: 486 B |
|
@ -0,0 +1 @@
|
|||
<svg viewBox="1 1 45 45" focusable="false" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svg="http://www.w3.org/2000/svg" class="" role="presentation" id="FxSymbol0-025" width="100%" height="100%"><g><title></title><path class="msportalfx-svg-c15" d="M15 34H9c-4-1-7.085-4.151-7.085-8.33 0-3.459 2.232-6.617 5.263-7.819a10.937 10.937 0 0 1-.055-1.092c0-5.94 4.817-10.753 10.757-10.753a10.74 10.74 0 0 1 8.376 4.019 8.672 8.672 0 0 1 3.924-.927c4.704 0 8.778 4.173 8.944 8.839l-16.063-6.625L15 16.167V34z"></path><path d="M45 38.435l-22.132 4.218.102-15.559L45 31.179v7.256zM22.945 13.348L45 21.649v8.311l-22.017-4.691" fill="#959595"></path><path d="M22.983 25.268l-5.999 2.789v-11.08l5.96-3.63M44 29l-20-4.563V14.5l20 7.375V29zm-11-3.563l2 .548v-6.478l-2-.7v6.63zm-2-7.268l-2-.722v6.926l2 .564v-6.768zm6 8.363l1.972.52.034-6.175-2.006-.67v6.325zm-12-3.187l2 .515v-7.07l-2-.658v7.213zm18-1.101l-2-.7v6.034l2 .548v-5.882zM24 28.75v12.833l20-3.667v-6.083L24 28.75zm1 11.333v-9.875l2 .25v9.292l-2 .333zm6-1.01l-2 .365v-8.73L31 31v8.073zm2-.288v-7.577l2 .25v6.958l-2 .369zm6-1.052l-2 .34v-6.365L39 32v5.733zm4.068-.647L41 37.444v-5.236l1.931.241.137 4.637z" fill="#b3b4b5"></path><path d="M19.009 25.077l-1 .585v-7.88l1-.538v7.833zM21 16.098l-.997.688L20 24.44l1-.547v-7.795z" fill="#959595"></path><path d="M16.984 39.1l-.038-9.094 6.021-2.912L23 42.691" fill="#b3b4b5"></path><path d="M18.887 39.066l-.91-.463v-7.679l.91-.41v8.552zm2.147-9.518l-.993.435-.038 9.69 1.032.535v-10.66z" fill="#959595"></path><path class="msportalfx-svg-c04" d="M42.981 22.892l-17.977-6.031.029-.779 17.948 6.175v.635zm-.05 9.556l-17.899-2.246v.777l17.916 2.119-.017-.65zM21 17.129v-.982l-2.904 1.611-.039.825L21 17.129zm-3.019 14.369l3.049-1.104.006-.847-3.061 1.374.006.577z"></path><path class="msportalfx-svg-c01" d="M45 21.695v16.74L23 42.65l16.669-22.973L45 21.695z" opacity="0.2"></path></g></svg>
|
После Ширина: | Высота: | Размер: 1.8 KiB |
80
package.json
|
@ -42,8 +42,10 @@
|
|||
"onCommand:vscode-docker.compose.up",
|
||||
"onCommand:vscode-docker.compose.down",
|
||||
"onCommand:vscode-docker.configure",
|
||||
"onCommand:vscode-docker.createWebApp",
|
||||
"onCommand:vscode-docker.debug.configureLaunchJson",
|
||||
"onCommand:vscode-docker.system.prune",
|
||||
"onCommand:vscode-docker.dockerHubLogout",
|
||||
"onView:dockerExplorer"
|
||||
],
|
||||
"main": "./out/dockerExtension",
|
||||
|
@ -78,87 +80,99 @@
|
|||
"view/item/context": [
|
||||
{
|
||||
"command": "vscode-docker.container.start",
|
||||
"when": "view == dockerExplorer && viewItem == dockerImage"
|
||||
"when": "view == dockerExplorer && viewItem == localImageNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.container.start",
|
||||
"when": "view == dockerExplorer && viewItem == rootImages"
|
||||
"when": "view == dockerExplorer && viewItem == imagesRootNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.container.start.interactive",
|
||||
"when": "view == dockerExplorer && viewItem == dockerImage"
|
||||
"when": "view == dockerExplorer && viewItem == localImageNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.container.start.interactive",
|
||||
"when": "view == dockerExplorer && viewItem == rootImages"
|
||||
"when": "view == dockerExplorer && viewItem == imagesRootNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.image.push",
|
||||
"when": "view == dockerExplorer && viewItem == dockerImage"
|
||||
"when": "view == dockerExplorer && viewItem == localImageNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.image.push",
|
||||
"when": "view == dockerExplorer && viewItem == rootImages"
|
||||
"when": "view == dockerExplorer && viewItem == imagesRootNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.image.remove",
|
||||
"when": "view == dockerExplorer && viewItem == dockerImage"
|
||||
"when": "view == dockerExplorer && viewItem == localImageNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.image.remove",
|
||||
"when": "view == dockerExplorer && viewItem == rootImages"
|
||||
"when": "view == dockerExplorer && viewItem == imagesRootNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.image.inspect",
|
||||
"when": "view == dockerExplorer && viewItem == dockerImage"
|
||||
"when": "view == dockerExplorer && viewItem == localImageNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.image.inspect",
|
||||
"when": "view == dockerExplorer && viewItem == rootImages"
|
||||
"when": "view == dockerExplorer && viewItem == imagesRootNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.image.tag",
|
||||
"when": "view == dockerExplorer && viewItem == dockerImage"
|
||||
"when": "view == dockerExplorer && viewItem == localImageNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.image.tag",
|
||||
"when": "view == dockerExplorer && viewItem == rootImages"
|
||||
"when": "view == dockerExplorer && viewItem == imagesRootNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.container.stop",
|
||||
"when": "view == dockerExplorer && viewItem == dockerContainerRunning"
|
||||
"when": "view == dockerExplorer && viewItem == runningLocalContainerNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.container.stop",
|
||||
"when": "view == dockerExplorer && viewItem == dockerContainersLabel"
|
||||
"when": "view == dockerExplorer && viewItem == containersRootNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.container.show-logs",
|
||||
"when": "view == dockerExplorer && viewItem == dockerContainerRunning"
|
||||
"when": "view == dockerExplorer && viewItem == runningLocalContainerNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.container.show-logs",
|
||||
"when": "view == dockerExplorer && viewItem == dockerContainersLabel"
|
||||
"when": "view == dockerExplorer && viewItem == containersRootNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.container.open-shell",
|
||||
"when": "view == dockerExplorer && viewItem == dockerContainerRunning"
|
||||
"when": "view == dockerExplorer && viewItem == runningLocalContainerNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.container.open-shell",
|
||||
"when": "view == dockerExplorer && viewItem == dockerContainersLabel"
|
||||
"when": "view == dockerExplorer && viewItem == containersRootNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.container.remove",
|
||||
"when": "view == dockerExplorer && viewItem == dockerContainerStopped"
|
||||
"when": "view == dockerExplorer && viewItem == stoppedLocalContainerNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.container.remove",
|
||||
"when": "view == dockerExplorer && viewItem == dockerContainerRunning"
|
||||
"when": "view == dockerExplorer && viewItem == runningLocalContainerNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.container.remove",
|
||||
"when": "view == dockerExplorer && viewItem == rootContainers"
|
||||
"when": "view == dockerExplorer && viewItem == containersRootNode"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.createWebApp",
|
||||
"when": "view == dockerExplorer && viewItem == azureImageTag"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.createWebApp",
|
||||
"when": "view == dockerExplorer && viewItem == dockerHubImageTag"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.dockerHubLogout",
|
||||
"when": "view == dockerExplorer && viewItem == dockerHubRootNode"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -376,6 +390,16 @@
|
|||
"light": "images/light/refresh.svg",
|
||||
"dark": "images/dark/refresh.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.createWebApp",
|
||||
"title": "Deploy Image to Azure App Service",
|
||||
"category": "Docker"
|
||||
},
|
||||
{
|
||||
"command": "vscode-docker.dockerHubLogout",
|
||||
"title": "DockerHub Logout",
|
||||
"category": "Docker"
|
||||
}
|
||||
],
|
||||
"views": {
|
||||
|
@ -398,17 +422,23 @@
|
|||
},
|
||||
"extensionDependencies": [
|
||||
"vscode.docker",
|
||||
"vscode.yaml"
|
||||
"vscode.yaml",
|
||||
"ms-vscode.azure-account"
|
||||
],
|
||||
"devDependencies": {
|
||||
"vscode": "^1.0.0",
|
||||
"typescript": "^2.1.5",
|
||||
"@types/node": "^6.0.40"
|
||||
"@types/node": "^6.0.40",
|
||||
"@types/keytar": "^4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"dockerfile-language-server-nodejs": "^0.0.7",
|
||||
"dockerode": "^2.5.1",
|
||||
"vscode-extension-telemetry": "^0.0.6",
|
||||
"vscode-languageclient": "^3.1.0"
|
||||
"vscode-languageclient": "^3.1.0",
|
||||
"azure-arm-containerregistry": "^1.0.0-preview",
|
||||
"azure-arm-resource": "^2.0.0-preview",
|
||||
"azure-arm-website": "^1.0.0-preview",
|
||||
"request-promise": "^4.2.2"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -78,3 +78,21 @@ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE A
|
|||
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
5. request-promise version 4.2.2 (https://github.com/request/request-promise)
|
||||
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2017, Nicolai Kamenzky, Ty Abonil, and contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.)
|
|
@ -0,0 +1,39 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vscode';
|
||||
import { ServiceClientCredentials } from 'ms-rest';
|
||||
import { AzureEnvironment } from 'ms-rest-azure';
|
||||
import { SubscriptionModels } from 'azure-arm-resource';
|
||||
|
||||
export type AzureLoginStatus = 'Initializing' | 'LoggingIn' | 'LoggedIn' | 'LoggedOut';
|
||||
|
||||
export interface AzureAccount {
|
||||
readonly status: AzureLoginStatus;
|
||||
readonly onStatusChanged: Event<AzureLoginStatus>;
|
||||
readonly waitForLogin: () => Promise<boolean>;
|
||||
readonly sessions: AzureSession[];
|
||||
readonly onSessionsChanged: Event<void>;
|
||||
readonly filters: AzureResourceFilter[];
|
||||
readonly onFiltersChanged: Event<void>;
|
||||
}
|
||||
|
||||
export interface AzureSession {
|
||||
readonly environment: AzureEnvironment;
|
||||
readonly userId: string;
|
||||
readonly tenantId: string;
|
||||
readonly credentials: ServiceClientCredentials;
|
||||
}
|
||||
|
||||
export interface AzureResourceFilter {
|
||||
readonly session: AzureSession;
|
||||
readonly subscription: SubscriptionModels.Subscription;
|
||||
}
|
||||
|
||||
export interface Credentials {
|
||||
readSecret(service: string, account: string): Thenable<string | undefined>;
|
||||
writeSecret(service: string, account: string, secret: string): Thenable<void>;
|
||||
deleteSecret(service: string, account: string): Thenable<boolean>;
|
||||
}
|