Update from built-in extension

This commit is contained in:
Christof Marti 2017-08-31 21:31:02 -07:00
Родитель cb55905fcd
Коммит b30f88503d
14 изменённых файлов: 530 добавлений и 913 удалений

Просмотреть файл

@ -1,5 +1,5 @@
# Change Log # Change Log
All notable changes to the "vscode-azurelogin" extension will be documented in this file. All notable changes to the "ms-vscode.azure-account" extension will be documented in this file.
## [0.0.1] ## [0.1.0]
- Initial release - Initial release

Просмотреть файл

@ -1,36 +1,6 @@
# Azure Login README # Azure Login
## Features Base extension supplying login and subscription filtering functionality for Azure extension.
### Login
Calling the login method will result in an "authentication code" being automatically copied to the clipboard, and a browser being launched, which allows you to interactively authenticate with Azure. Once the login is complete, an Azure "service principal" is auto-created and persisted to disk, so that subsequent calls to login won't require re-authenticating. This allows your own apps to behave similarly to tools such as the Az CLI, without too much effort.
If you'd like to specify an exact Azure identity you can set the following environment variables (or [extension settings](Extension Settings)), which provide interop with other Azure management tools such as Serverless and Terraform:
* `azureSubId` / `ARM_SUBSRIPTION_ID`: The ID of the Azure subscription that you'd like to manage resources within
* `azureServicePrincipalClientId` / `ARM_CLIENT_ID`: The name of the service principal
* `azureServicePrincipalPassword` / `ARM_CLIENT_SECRET`: The password of the service principal
* `azureServicePrincipalTenantId` / `ARM_TENANT_ID`: The ID of the tenant that the service principal was created in
### Logout
### Show Subscriptions
### Use Subscription
## Requirements
## Extension Settings
This extension contributes the following settings:
* `azureLogin.azureSubId`: The ID of the Azure subscription that you'd like to manage resources within
* `azureLogin.azureServicePrincipalClientId`: The name of the service principal
* `azureLogin.azureServicePrincipalPassword`: The password of the service principal
* `azureLogin.azureServicePrincipalTenantId`: The ID of the tenant that the service principal
## Known Issues
## Contributing ## Contributing

Просмотреть файл

@ -1,6 +1,6 @@
{ {
"name": "vscode-azurelogin", "name": "azure-account",
"displayName": "Azure Login", "displayName": "Azure Account",
"description": "A common Login and Subscription management extension for VS Code.", "description": "A common Login and Subscription management extension for VS Code.",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"icon": "images/azurelogin.png", "icon": "images/azurelogin.png",
@ -12,11 +12,10 @@
"color": "#1289B9", "color": "#1289B9",
"theme": "dark" "theme": "dark"
}, },
"aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "version": "0.1.0",
"version": "0.0.1", "publisher": "ms-vscode",
"publisher": "chrisdias",
"engines": { "engines": {
"vscode": "^1.15.0" "vscode": "^1.16.0"
}, },
"categories": [ "categories": [
"Other" "Other"
@ -26,45 +25,39 @@
], ],
"enableProposedApi": true, "enableProposedApi": true,
"activationEvents": [ "activationEvents": [
"*" "onCommand:azure-account.login",
"onCommand:azure-account.logout",
"onCommand:azure-account.addFilter",
"onCommand:azure-account.removeFilter",
"onCommand:azure-account.createAccount"
], ],
"main": "./out/src/extension", "main": "./out/src/extension",
"contributes": { "contributes": {
"commands": [ "commands": [
{ {
"command": "vscode-azurelogin.login", "command": "azure-account.login",
"title": "Login", "title": "%azure-account.commands.login%",
"category": "Azure" "category": "%azure-account.commands.azure%"
}, },
{ {
"command": "vscode-azurelogin.logout", "command": "azure-account.logout",
"title": "Logout", "title": "%azure-account.commands.logout%",
"category": "Azure" "category": "%azure-account.commands.azure%"
}, },
{ {
"command": "vscode-azurelogin.addFilter", "command": "azure-account.addFilter",
"title": "Add Resource Filter", "title": "%azure-account.commands.addResourceFilter%",
"category": "Azure" "category": "%azure-account.commands.azure%"
}, },
{ {
"command": "vscode-azurelogin.removeFilter", "command": "azure-account.removeFilter",
"title": "Remove Resource Filter", "title": "%azure-account.commands.removeResourceFilter%",
"category": "Azure" "category": "%azure-account.commands.azure%"
}, },
{ {
"command": "vscode-azurelogin.createAccount", "command": "azure-account.createAccount",
"title": "Create an Account", "title": "%azure-account.commands.createAccount%",
"category": "Azure" "category": "%azure-account.commands.azure%"
},
{
"command": "vscode-azurelogin.showSubscriptions",
"title": "Subscriptions",
"category": "Azure Example"
},
{
"command": "vscode-azurelogin.showAppServices",
"title": "AppServices",
"category": "Azure Example"
} }
], ],
"configuration": { "configuration": {
@ -74,7 +67,7 @@
"azure.resourceFilter": { "azure.resourceFilter": {
"type": "array", "type": "array",
"default": null, "default": null,
"description": "The resource filter, each element is either a subscription id or a subscription id and a resource group name separated by a slash." "description": "The resource filter, each element is a tenant id and a subscription id separated by a slash."
} }
} }
} }
@ -82,25 +75,20 @@
"scripts": { "scripts": {
"vscode:prepublish": "tsc -p ./", "vscode:prepublish": "tsc -p ./",
"compile": "tsc -watch -p ./", "compile": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install", "postinstall": "node ./node_modules/vscode/bin/install"
"test": "node ./node_modules/vscode/bin/test"
}, },
"devDependencies": { "devDependencies": {
"@types/copy-paste": "^1.1.30", "@types/copy-paste": "1.1.30",
"@types/mocha": "^2.2.32", "@types/node": "6.0.40",
"@types/node": "^6.0.40",
"@types/opn": "^3.0.28",
"mocha": "^2.3.3",
"typescript": "^2.0.3",
"vscode": "^1.0.0", "vscode": "^1.0.0",
"vscode-extension-telemetry": "^0.0.6" "@types/opn": "3.0.28"
}, },
"dependencies": { "dependencies": {
"adal-node": "^0.1.22", "adal-node": "^0.1.22",
"azure-arm-resource": "^2.0.0-preview", "azure-arm-resource": "^2.0.0-preview",
"azure-arm-website": "^1.0.0-preview",
"copy-paste": "^1.3.0", "copy-paste": "^1.3.0",
"ms-rest-azure": "^2.2.3", "ms-rest-azure": "^2.2.3",
"vscode-nls": "^2.0.2",
"opn": "^5.1.0" "opn": "^5.1.0"
} }
} }

8
package.nls.json Normal file
Просмотреть файл

@ -0,0 +1,8 @@
{
"azure-account.commands.azure": "Azure",
"azure-account.commands.login": "Login",
"azure-account.commands.logout": "Logout",
"azure-account.commands.addResourceFilter": "Add Resource Filter",
"azure-account.commands.removeResourceFilter": "Remove Resource Filter",
"azure-account.commands.createAccount": "Create an Account"
}

Просмотреть файл

@ -6,17 +6,18 @@
import { Event } from 'vscode'; import { Event } from 'vscode';
import { ServiceClientCredentials } from 'ms-rest'; import { ServiceClientCredentials } from 'ms-rest';
import { AzureEnvironment } from 'ms-rest-azure'; import { AzureEnvironment } from 'ms-rest-azure';
import { SubscriptionModels, ResourceModels } from 'azure-arm-resource'; import { SubscriptionModels } from 'azure-arm-resource';
export type AzureLoginStatus = 'Initializing' | 'LoggingIn' | 'LoggedIn' | 'LoggedOut'; export type AzureLoginStatus = 'Initializing' | 'LoggingIn' | 'LoggedIn' | 'LoggedOut';
export interface AzureLogin { export interface AzureAccount {
readonly status: AzureLoginStatus; readonly status: AzureLoginStatus;
readonly onStatusChanged: Event<AzureLoginStatus>; readonly onStatusChanged: Event<AzureLoginStatus>;
readonly sessions: AzureSession[]; readonly sessions: AzureSession[];
readonly onSessionsChanged: Event<void>; readonly onSessionsChanged: Event<void>;
readonly filters: AzureResourceFilter[]; readonly filters: AzureResourceFilter[];
readonly onFiltersChanged: Event<void>; readonly onFiltersChanged: Event<void>;
readonly credentials: Credentials;
} }
export interface AzureSession { export interface AzureSession {
@ -29,6 +30,10 @@ export interface AzureSession {
export interface AzureResourceFilter { export interface AzureResourceFilter {
readonly session: AzureSession; readonly session: AzureSession;
readonly subscription: SubscriptionModels.Subscription; readonly subscription: SubscriptionModels.Subscription;
readonly allResourceGroups: boolean; }
readonly resourceGroups: ResourceModels.ResourceGroup[];
} 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>;
}

429
src/azure-account.ts Normal file
Просмотреть файл

@ -0,0 +1,429 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const adal = require('adal-node');
const MemoryCache = adal.MemoryCache;
const AuthenticationContext = adal.AuthenticationContext;
const CacheDriver = require('adal-node/lib/cache-driver');
const createLogContext = require('adal-node/lib/log').createLogContext;
import { DeviceTokenCredentials, AzureEnvironment } from 'ms-rest-azure';
import { SubscriptionClient, SubscriptionModels } from 'azure-arm-resource';
import * as opn from 'opn';
import * as copypaste from 'copy-paste';
import * as nls from 'vscode-nls';
import { window, commands, credentials, EventEmitter, MessageItem, ExtensionContext, workspace, ConfigurationTarget, WorkspaceConfiguration } from 'vscode';
import { AzureAccount, AzureSession, AzureLoginStatus, AzureResourceFilter } from './azure-account.api';
const localize = nls.loadMessageBundle();
const defaultEnvironment = (<any>AzureEnvironment).Azure;
const commonTenantId = 'common';
const authorityHostUrl = defaultEnvironment.activeDirectoryEndpointUrl;
const clientId = '04b07795-8ddb-461a-bbee-02f9e1bf7b46';
const authorityUrl = `${authorityHostUrl}${commonTenantId}`;
const resource = defaultEnvironment.activeDirectoryResourceId;
const credentialsService = 'VSCode Public Azure';
const credentialsAccount = 'Refresh Token';
interface DeviceLogin {
userCode: string;
deviceCode: string;
verificationUrl: string;
expiresIn: number;
interval: number;
message: string;
}
interface TokenResponse {
tokenType: string;
expiresIn: number;
expiresOn: string;
resource: string;
accessToken: string;
refreshToken: string;
userId: string;
isUserIdDisplayable: boolean;
familyName: string;
givenName: string;
oid: string;
tenantId: string;
isMRRT: boolean;
_clientId: string;
_authority: string;
}
interface AzureAccountWriteable extends AzureAccount {
status: AzureLoginStatus;
}
class AzureLoginError extends Error {
constructor(message: string, public _reason: any) {
super(message);
}
}
export class AzureLoginHelper {
private onStatusChanged = new EventEmitter<AzureLoginStatus>();
private onSessionsChanged = new EventEmitter<void>();
private onFiltersChanged = new EventEmitter<void>();
private tokenCache = new MemoryCache();
private oldResourceFilter: string;
constructor(context: ExtensionContext) {
const subscriptions = context.subscriptions;
subscriptions.push(commands.registerCommand('azure-account.login', () => this.login().catch(console.error)));
subscriptions.push(commands.registerCommand('azure-account.logout', () => this.logout().catch(console.error)));
subscriptions.push(commands.registerCommand('azure-account.askForLogin', () => this.askForLogin().catch(console.error)));
subscriptions.push(commands.registerCommand('azure-account.addFilter', () => this.addFilter().catch(console.error)));
subscriptions.push(commands.registerCommand('azure-account.removeFilter', () => this.removeFilter().catch(console.error)));
subscriptions.push(this.api.onSessionsChanged(() => this.updateFilters().catch(console.error)));
subscriptions.push(workspace.onDidChangeConfiguration(() => this.updateFilters(true).catch(console.error)));
this.initialize()
.catch(console.error);
}
api: AzureAccount = {
status: 'Initializing',
onStatusChanged: this.onStatusChanged.event,
sessions: [],
onSessionsChanged: this.onSessionsChanged.event,
filters: [],
onFiltersChanged: this.onFiltersChanged.event,
credentials
};
async login() {
try {
this.beginLoggingIn();
const deviceLogin = await deviceLogin1();
const copyAndOpen: MessageItem = { title: localize('azure-account.copyAndOpen', "Copy & Open") };
const close: MessageItem = { title: localize('azure-account.close', "Close"), isCloseAffordance: true };
const response = await window.showInformationMessage(deviceLogin.message, copyAndOpen, close);
if (response === copyAndOpen) {
copypaste.copy(deviceLogin.userCode);
opn(deviceLogin.verificationUrl);
}
const tokenResponse = await deviceLogin2(deviceLogin);
const refreshToken = tokenResponse.refreshToken;
const tokenResponses = await tokensFromToken(tokenResponse);
await credentials.writeSecret(credentialsService, credentialsAccount, refreshToken);
await this.updateSessions(tokenResponses);
} finally {
this.updateStatus();
}
}
async logout() {
await credentials.deleteSecret(credentialsService, credentialsAccount);
await this.updateSessions([]);
this.updateStatus();
}
private async initialize() {
try {
const refreshToken = await credentials.readSecret(credentialsService, credentialsAccount);
if (refreshToken) {
this.beginLoggingIn();
const tokenResponse = await tokenFromRefreshToken(refreshToken);
const tokenResponses = await tokensFromToken(tokenResponse);
await this.updateSessions(tokenResponses);
}
} catch (err) {
if (!(err instanceof AzureLoginError)) {
throw err;
}
} finally {
this.updateStatus();
}
}
private beginLoggingIn() {
if (this.api.status !== 'LoggedIn') {
(<AzureAccountWriteable>this.api).status = 'LoggingIn';
this.onStatusChanged.fire(this.api.status);
}
}
private updateStatus() {
const status = this.api.sessions.length ? 'LoggedIn' : 'LoggedOut';
if (this.api.status !== status) {
(<AzureAccountWriteable>this.api).status = status;
this.onStatusChanged.fire(this.api.status);
}
}
private async updateSessions(tokenResponses: TokenResponse[]) {
await clearTokenCache(this.tokenCache);
for (const tokenResponse of tokenResponses) {
await addTokenToCache(this.tokenCache, tokenResponse);
}
const sessions = this.api.sessions;
sessions.splice(0, sessions.length, ...tokenResponses.map<AzureSession>(tokenResponse => ({
environment: defaultEnvironment,
userId: tokenResponse.userId,
tenantId: tokenResponse.tenantId,
credentials: new DeviceTokenCredentials({ username: tokenResponse.userId, clientId, tokenCache: this.tokenCache, domain: tokenResponse.tenantId })
})));
this.onSessionsChanged.fire();
}
private async askForLogin() {
if (this.api.status === 'LoggedIn') {
return;
}
const login = { title: localize('azure-account.login', "Login") };
const cancel = { title: 'Cancel', isCloseAffordance: true };
const result = await window.showInformationMessage(localize('azure-account.loginFirst', "Not logged in, log in first."), login, cancel);
return result === login && commands.executeCommand('azure-account.login');
}
private async addFilter() {
if (!(await this.waitForLogin())) {
return commands.executeCommand('azure-account.askForLogin');
}
const azureConfig = workspace.getConfiguration('azure');
const resourceFilter = azureConfig.get<string[]>('resourceFilter') || [];
const subscriptionItems = this.loadSubscriptionItems(resourceFilter);
const subscriptionResult = await window.showQuickPick(subscriptionItems);
if (!subscriptionResult) {
return;
}
const { session, subscription } = subscriptionResult.subscription;
resourceFilter.push(`${session.tenantId}/${subscription.subscriptionId}`);
await this.updateConfiguration(azureConfig, resourceFilter);
}
private async removeFilter() {
if (!(await this.waitForLogin())) {
return commands.executeCommand('azure-account.askForLogin');
}
const azureConfig = workspace.getConfiguration('azure');
let resourceFilter = azureConfig.get<string[]>('resourceFilter') || [];
const subscriptionItems = this.loadSubscriptionItems(resourceFilter, false);
const subscriptionResult = await window.showQuickPick(subscriptionItems);
if (!subscriptionResult) {
return;
}
const { session, subscription } = subscriptionResult.subscription;
const remove = `${session.tenantId}/${subscription.subscriptionId}`;
resourceFilter = resourceFilter.filter(e => e !== remove);
await this.updateConfiguration(azureConfig, resourceFilter);
}
private async loadSubscriptionItems(resourceFilter: string[], exclude = true) {
if (!resourceFilter.length && !exclude) {
return [];
}
const subscriptionItems: { session: AzureSession; subscription: SubscriptionModels.Subscription }[] = [];
for (const session of this.api.sessions) {
const credentials = session.credentials;
const client = new SubscriptionClient(credentials);
const subscriptions = await listAll(client.subscriptions, client.subscriptions.list());
const items = subscriptions.filter(subscription => exclude !== (resourceFilter.indexOf(`${session.tenantId}/${subscription.subscriptionId}`) !== -1))
.map(subscription => ({
session,
subscription
}));
subscriptionItems.push(...items);
}
subscriptionItems.sort((a, b) => a.subscription.displayName!.localeCompare(b.subscription.displayName!));
return subscriptionItems.map(subscription => ({
label: subscription.subscription.displayName!,
description: subscription.subscription.subscriptionId!,
subscription
}));
}
private async updateConfiguration(azureConfig: WorkspaceConfiguration, resourceFilter: string[]) {
const resourceFilterConfig = azureConfig.inspect<string[]>('resourceFilter');
let target = ConfigurationTarget.Global;
if (resourceFilterConfig) {
if (resourceFilterConfig.workspaceFolderValue) {
target = ConfigurationTarget.WorkspaceFolder;
} else if (resourceFilterConfig.workspaceValue) {
target = ConfigurationTarget.Workspace;
} else if (resourceFilterConfig.globalValue) {
target = ConfigurationTarget.Global;
}
}
await azureConfig.update('resourceFilter', resourceFilter.length ? resourceFilter : undefined, target);
}
private async updateFilters(configChange = false) {
const azureConfig = workspace.getConfiguration('azure');
let resourceFilter = azureConfig.get<string[]>('resourceFilter');
if (configChange && JSON.stringify(resourceFilter) === this.oldResourceFilter) {
return;
}
this.oldResourceFilter = JSON.stringify(resourceFilter);
if (resourceFilter && !Array.isArray(resourceFilter)) {
resourceFilter = [];
}
const filters = resourceFilter && resourceFilter.map(s => typeof s === 'string' ? s.split('/') : [])
.filter(s => s.length === 2)
.map(([tenantId, subscriptionId]) => ({ tenantId, subscriptionId }));
const tenantIds = filters && filters.reduce<Record<string, Record<string, boolean>>>((result, filter) => {
const tenant = result[filter.tenantId] || (result[filter.tenantId] = {});
tenant[filter.subscriptionId] = true;
return result;
}, {});
const newFilters: AzureResourceFilter[] = [];
const sessions = tenantIds ? this.api.sessions.filter(session => tenantIds[session.tenantId]) : this.api.sessions;
for (const session of sessions) {
const client = new SubscriptionClient(session.credentials);
const subscriptionIds = tenantIds && tenantIds[session.tenantId];
const subscriptions = await listAll(client.subscriptions, client.subscriptions.list());
const filteredSubscriptions = subscriptionIds ? subscriptions.filter(subscription => subscriptionIds[subscription.subscriptionId!]) : subscriptions;
for (const subscription of filteredSubscriptions) {
newFilters.push({ session, subscription });
}
}
this.api.filters.splice(0, this.api.filters.length, ...newFilters);
this.onFiltersChanged.fire();
}
private async waitForLogin() {
switch (this.api.status) {
case 'LoggedIn':
return true;
case 'LoggedOut':
return false;
case 'Initializing':
case 'LoggingIn':
return new Promise<boolean>(resolve => {
const subscription = this.api.onStatusChanged(() => {
subscription.dispose();
resolve(this.waitForLogin());
});
});
default:
const status: never = this.api.status;
throw new Error(`Unexpected status '${status}'`);
}
}
}
async function deviceLogin1(): Promise<DeviceLogin> {
return new Promise<DeviceLogin>((resolve, reject) => {
const cache = new MemoryCache();
const context = new AuthenticationContext(authorityUrl, null, cache);
context.acquireUserCode(resource, clientId, 'en-us', function (err: any, response: any) {
if (err) {
reject(new AzureLoginError(localize('azure-account.userCodeFailed', "Aquiring user code failed"), err));
} else {
resolve(response);
}
});
});
}
async function deviceLogin2(deviceLogin: DeviceLogin) {
return new Promise<TokenResponse>((resolve, reject) => {
const tokenCache = new MemoryCache();
const context = new AuthenticationContext(authorityUrl, null, tokenCache);
context.acquireTokenWithDeviceCode(resource, clientId, deviceLogin, function (err: any, tokenResponse: TokenResponse) {
if (err) {
reject(new AzureLoginError(localize('azure-account.tokenFailed', "Aquiring token with device code"), err));
} else {
resolve(tokenResponse);
}
});
});
}
async function tokenFromRefreshToken(refreshToken: string, tenantId = commonTenantId) {
return new Promise<TokenResponse>((resolve, reject) => {
const tokenCache = new MemoryCache();
const context = new AuthenticationContext(`${authorityHostUrl}${tenantId}`, null, tokenCache);
context.acquireTokenWithRefreshToken(refreshToken, clientId, null, function (err: any, tokenResponse: TokenResponse) {
if (err) {
reject(new AzureLoginError(localize('azure-account.tokenFromRefreshTokenFailed', "Aquiring token with refresh token"), err));
} else {
resolve(tokenResponse);
}
});
});
}
async function tokensFromToken(firstTokenResponse: TokenResponse) {
const tokenResponses = [firstTokenResponse];
const tokenCache = new MemoryCache();
await addTokenToCache(tokenCache, firstTokenResponse);
const credentials = new DeviceTokenCredentials({ username: firstTokenResponse.userId, clientId, tokenCache });
const client = new SubscriptionClient(credentials);
const tenants = await listAll(client.tenants, client.tenants.list());
for (const tenant of tenants) {
if (tenant.tenantId !== firstTokenResponse.tenantId) {
const tokenResponse = await tokenFromRefreshToken(firstTokenResponse.refreshToken, tenant.tenantId);
tokenResponses.push(tokenResponse);
}
}
return tokenResponses;
}
async function addTokenToCache(tokenCache: any, tokenResponse: TokenResponse) {
return new Promise<any>((resolve, reject) => {
const driver = new CacheDriver(
{ _logContext: createLogContext('') },
`${authorityHostUrl}${tokenResponse.tenantId}`,
tokenResponse.resource,
clientId,
tokenCache,
(entry: any, resource: any, callback: (err: any, response: any) => {}) => {
callback(null, entry);
}
);
driver.add(tokenResponse, function (err: any) {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
async function clearTokenCache(tokenCache: any) {
await new Promise<void>((resolve, reject) => {
tokenCache.find({}, (err: any, entries: any[]) => {
if (err) {
reject(err);
} else {
tokenCache.remove(entries, (err: any) => {
if (err) {
reject(err);
} else {
resolve();
}
});
}
});
});
}
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;
}

Просмотреть файл

@ -1,463 +0,0 @@
const adal = require('adal-node');
const MemoryCache = adal.MemoryCache;
const AuthenticationContext = adal.AuthenticationContext;
const CacheDriver = require('adal-node/lib/cache-driver');
const createLogContext = require('adal-node/lib/log').createLogContext;
import { DeviceTokenCredentials, AzureEnvironment } from 'ms-rest-azure';
import { SubscriptionClient, ResourceManagementClient, SubscriptionModels } from 'azure-arm-resource';
import * as opn from 'opn';
import * as copypaste from 'copy-paste';
import { window, commands, credentials, EventEmitter, MessageItem, ExtensionContext, workspace, ConfigurationTarget } from 'vscode';
import { AzureLogin, AzureSession, AzureLoginStatus, AzureResourceFilter } from './azurelogin.api';
const defaultEnvironment = (<any>AzureEnvironment).Azure;
const commonTenantId = 'common';
const authorityHostUrl = defaultEnvironment.activeDirectoryEndpointUrl;
const clientId = '818dee8b-8777-4f45-afc3-f7cc977caae2';
const authorityUrl = `${authorityHostUrl}${commonTenantId}`;
const resource = defaultEnvironment.activeDirectoryResourceId;
const credentialsService = 'VSCode Public Azure';
const credentialsAccount = 'Refresh Token';
interface DeviceLogin {
userCode: string;
deviceCode: string;
verificationUrl: string;
expiresIn: number;
interval: number;
message: string;
}
interface TokenResponse {
tokenType: string;
expiresIn: number;
expiresOn: string;
resource: string;
accessToken: string;
refreshToken: string;
userId: string;
isUserIdDisplayable: boolean;
familyName: string;
givenName: string;
oid: string;
tenantId: string;
isMRRT: boolean;
_clientId: string;
_authority: string;
}
interface AzureLoginWriteable extends AzureLogin {
status: AzureLoginStatus;
}
class AzureLoginError extends Error {
constructor(message: string, public _reason: any) {
super(message);
}
}
export class AzureLoginHelper {
private onStatusChanged = new EventEmitter<AzureLoginStatus>();
private onSessionsChanged = new EventEmitter<void>();
private onFiltersChanged = new EventEmitter<void>();
private tokenCache = new MemoryCache();
private oldResourceFilter: string;
constructor(context: ExtensionContext) {
const subscriptions = context.subscriptions;
subscriptions.push(commands.registerCommand('vscode-azurelogin.login', () => this.login().catch(console.error)));
subscriptions.push(commands.registerCommand('vscode-azurelogin.logout', () => this.logout().catch(console.error)));
subscriptions.push(commands.registerCommand('vscode-azurelogin.askForLogin', () => this.askForLogin().catch(console.error)));
subscriptions.push(commands.registerCommand('vscode-azurelogin.addFilter', () => this.addFilter().catch(console.error)));
subscriptions.push(commands.registerCommand('vscode-azurelogin.removeFilter', () => this.removeFilter().catch(console.error)));
subscriptions.push(this.api.onSessionsChanged(() => this.updateFilters().catch(console.error)));
subscriptions.push(workspace.onDidChangeConfiguration(() => this.updateFilters(true).catch(console.error)));
this.initialize()
.catch(console.error);
}
api: AzureLoginWriteable = {
status: 'Initializing',
onStatusChanged: this.onStatusChanged.event,
sessions: [],
onSessionsChanged: this.onSessionsChanged.event,
filters: [],
onFiltersChanged: this.onFiltersChanged.event
};
async login() {
try {
this.beginLoggingIn();
const deviceLogin = await deviceLogin1();
const copyAndOpen: MessageItem = { title: 'Copy & Open' };
const close: MessageItem = { title: 'Close', isCloseAffordance: true };
const response = await window.showInformationMessage(deviceLogin.message, copyAndOpen, close);
if (response === copyAndOpen) {
copypaste.copy(deviceLogin.userCode);
opn(deviceLogin.verificationUrl);
}
const tokenResponse = await deviceLogin2(deviceLogin);
const refreshToken = tokenResponse.refreshToken;
const tokenResponses = await tokensFromToken(tokenResponse);
await credentials.writeSecret(credentialsService, credentialsAccount, refreshToken);
await this.updateSessions(tokenResponses);
} finally {
this.updateStatus();
}
}
async logout() {
await credentials.deleteSecret(credentialsService, credentialsAccount);
await this.updateSessions([]);
this.updateStatus();
}
private async initialize() {
try {
const refreshToken = await credentials.readSecret(credentialsService, credentialsAccount);
if (refreshToken) {
this.beginLoggingIn();
const tokenResponse = await tokenFromRefreshToken(refreshToken);
const tokenResponses = await tokensFromToken(tokenResponse);
await this.updateSessions(tokenResponses);
}
} catch (err) {
if (!(err instanceof AzureLoginError)) {
throw err;
}
} finally {
this.updateStatus();
}
}
private beginLoggingIn() {
if (this.api.status !== 'LoggedIn') {
this.api.status = 'LoggingIn';
this.onStatusChanged.fire(this.api.status);
}
}
private updateStatus() {
const status = this.api.sessions.length ? 'LoggedIn' : 'LoggedOut';
if (this.api.status !== status) {
this.api.status = status;
this.onStatusChanged.fire(this.api.status);
}
}
private async updateSessions(tokenResponses: TokenResponse[]) {
await clearTokenCache(this.tokenCache);
for (const tokenResponse of tokenResponses) {
await addTokenToCache(this.tokenCache, tokenResponse);
}
const sessions = this.api.sessions;
sessions.splice(0, sessions.length, ...tokenResponses.map<AzureSession>(tokenResponse => ({
environment: defaultEnvironment,
userId: tokenResponse.userId,
tenantId: tokenResponse.tenantId,
credentials: new DeviceTokenCredentials({ username: tokenResponse.userId, clientId, tokenCache: this.tokenCache, domain: tokenResponse.tenantId })
})));
this.onSessionsChanged.fire();
}
private async askForLogin() {
if (this.api.status === 'LoggedIn') {
return;
}
const login = { title: 'Login' };
const cancel = { title: 'Cancel', isCloseAffordance: true };
const result = await window.showInformationMessage('Not logged in, log in first.', login, cancel);
return result === login && commands.executeCommand('vscode-azurelogin.login');
}
private async addFilter() {
if (this.api.status !== 'LoggedIn') {
return commands.executeCommand('vscode-azurelogin.askForLogin');
}
const azureConfig = workspace.getConfiguration('azure');
const resourceFilter = azureConfig.get<string[]>('resourceFilter') || [];
const subscriptionItems: { session: AzureSession; subscription: SubscriptionModels.Subscription }[] = [];
for (const session of this.api.sessions) {
const credentials = session.credentials;
const client = new SubscriptionClient(credentials);
const subscriptions = await listAll(client.subscriptions, client.subscriptions.list());
subscriptionItems.push(...subscriptions.filter(subscription => resourceFilter.indexOf(`${session.tenantId}/${subscription.subscriptionId}`) === -1)
.map(subscription => ({
session,
subscription
})));
}
subscriptionItems.sort((a, b) => a.subscription.displayName!.localeCompare(b.subscription.displayName!));
const subscriptionResult = await window.showQuickPick(subscriptionItems.map(subscription => ({
label: subscription.subscription.displayName!,
description: subscription.subscription.subscriptionId!,
subscription
})));
if (!subscriptionResult) {
return;
}
const { session, subscription } = subscriptionResult.subscription;
const client = new ResourceManagementClient(session.credentials, subscription.subscriptionId!);
const resourceGroups = await listAll(client.resourceGroups, client.resourceGroups.list());
const resourceGroupFilters: AzureResourceFilter[] = [
{
...subscriptionResult.subscription,
allResourceGroups: true,
resourceGroups
}
];
resourceGroupFilters.push(...resourceGroups.filter(resourceGroup => resourceFilter.indexOf(`${session.tenantId}/${subscription.subscriptionId}/${resourceGroup.name}`) === -1)
.map(resourceGroup => ({
session,
subscription,
allResourceGroups: false,
resourceGroups: [resourceGroup]
})));
resourceGroupFilters.sort((a, b) => (!a.allResourceGroups ? a.resourceGroups[0].name! : '').localeCompare(!b.allResourceGroups ? b.resourceGroups[0].name! : ''));
const resourceGroupResult = await window.showQuickPick(resourceGroupFilters.map(resourceGroup => (!resourceGroup.allResourceGroups ? {
label: resourceGroup.resourceGroups[0].name!,
description: resourceGroup.resourceGroups[0].location,
resourceGroup
} : {
label: 'Entire Subscription',
description: '',
resourceGroup
})));
if (!resourceGroupResult) {
return;
}
const resourceGroup = resourceGroupResult.resourceGroup;
if (!resourceGroup.allResourceGroups) {
resourceFilter.push(`${resourceGroup.session.tenantId}/${resourceGroup.subscription.subscriptionId}/${resourceGroup.resourceGroups[0].name}`);
} else {
resourceFilter.splice(0, resourceFilter.length, ...resourceFilter.filter(c => !c.startsWith(`${resourceGroup.session.tenantId}/${resourceGroup.subscription.subscriptionId}/`)));
resourceFilter.push(`${resourceGroup.session.tenantId}/${resourceGroup.subscription.subscriptionId}`);
}
const resourceFilterConfig = azureConfig.inspect<string[]>('resourceFilter');
let target = workspace.workspaceFolders ? ConfigurationTarget.Workspace : ConfigurationTarget.Global;
if (resourceFilterConfig) {
if (resourceFilterConfig.workspaceFolderValue) {
target = ConfigurationTarget.WorkspaceFolder;
} else if (resourceFilterConfig.workspaceValue) {
target = ConfigurationTarget.Workspace;
} else if (resourceFilterConfig.globalValue) {
target = ConfigurationTarget.Global;
}
}
await azureConfig.update('resourceFilter', resourceFilter, target);
}
private async removeFilter() {
if (this.api.status !== 'LoggedIn') {
return commands.executeCommand('vscode-azurelogin.askForLogin');
}
const azureConfig = workspace.getConfiguration('azure');
let resourceFilter = azureConfig.get<string[]>('resourceFilter') || [];
const filters = resourceFilter.length ? this.api.filters.reduce((list, filter) => {
if (filter.allResourceGroups) {
list.push(filter);
} else {
list.push(...filter.resourceGroups.map(resourceGroup => ({
...filter,
resourceGroups: [resourceGroup]
})));
}
return list;
}, <AzureResourceFilter[]>[]) : [];
filters.sort((a, b) => (!a.allResourceGroups ? a.resourceGroups[0].name! : `/${a.subscription.displayName}`).localeCompare(!b.allResourceGroups ? b.resourceGroups[0].name! : `/${b.subscription.displayName}`));
const filterResult = await window.showQuickPick(filters.map(filter => (!filter.allResourceGroups ? {
label: filter.resourceGroups[0].name!,
description: filter.subscription.displayName!,
filter
} : {
label: filter.subscription.displayName!,
description: filter.subscription.subscriptionId!,
filter
})));
if (!filterResult) {
return;
}
const filter = filterResult.filter;
const remove = !filter.allResourceGroups ?
`${filter.session.tenantId}/${filter.subscription.subscriptionId}/${filter.resourceGroups[0].name}` :
`${filter.session.tenantId}/${filter.subscription.subscriptionId}`;
resourceFilter = resourceFilter.filter(e => e !== remove);
const resourceFilterConfig = azureConfig.inspect<string[]>('resourceFilter');
let target = workspace.workspaceFolders ? ConfigurationTarget.Workspace : ConfigurationTarget.Global;
if (resourceFilterConfig) {
if (resourceFilterConfig.workspaceFolderValue) {
target = ConfigurationTarget.WorkspaceFolder;
} else if (resourceFilterConfig.workspaceValue) {
target = ConfigurationTarget.Workspace;
} else if (resourceFilterConfig.globalValue) {
target = ConfigurationTarget.Global;
}
}
await azureConfig.update('resourceFilter', resourceFilter.length ? resourceFilter : undefined, target);
}
private async updateFilters(configChange = false) {
const azureConfig = workspace.getConfiguration('azure');
let resourceFilter = azureConfig.get<string[]>('resourceFilter');
if (configChange && JSON.stringify(resourceFilter) === this.oldResourceFilter) {
return;
}
this.oldResourceFilter = JSON.stringify(resourceFilter);
if (resourceFilter && !Array.isArray(resourceFilter)) {
resourceFilter = [];
}
const filters = resourceFilter && resourceFilter.map(s => typeof s === 'string' ? s.split('/') : [])
.filter(s => s.length === 2 || s.length === 3)
.map(([tenantId, subscriptionId, resourceGroup]) => ({ tenantId, subscriptionId, resourceGroup }));
const tenantIds = filters && filters.reduce<Record<string, Record<string, Record<string, boolean> | boolean>>>((result, filter) => {
const tenant = result[filter.tenantId] || (result[filter.tenantId] = {});
const resourceGroups = tenant[filter.subscriptionId] || (tenant[filter.subscriptionId] = (filter.resourceGroup ? {} : true));
if (typeof resourceGroups === 'object' && filter.resourceGroup) {
resourceGroups[filter.resourceGroup] = true;
}
return result;
}, {});
const newFilters: AzureResourceFilter[] = [];
const sessions = tenantIds ? this.api.sessions.filter(session => tenantIds[session.tenantId]) : this.api.sessions;
for (const session of sessions) {
const client = new SubscriptionClient(session.credentials);
const subscriptionIds = tenantIds && tenantIds[session.tenantId];
const subscriptions = await listAll(client.subscriptions, client.subscriptions.list());
const filteredSubscriptions = subscriptionIds ? subscriptions.filter(subscription => subscriptionIds[subscription.subscriptionId!]) : subscriptions;
for (const subscription of filteredSubscriptions) {
const client = new ResourceManagementClient(session.credentials, subscription.subscriptionId!);
const resourceGroupNames = subscriptionIds && subscriptionIds[subscription.subscriptionId!];
const allResourceGroups = !(resourceGroupNames && typeof resourceGroupNames === 'object');
const unfilteredResourceGroups = await listAll(client.resourceGroups, client.resourceGroups.list());
const resourceGroups = allResourceGroups ? unfilteredResourceGroups : unfilteredResourceGroups.filter(resourceGroup => (<Record<string, boolean>>resourceGroupNames!)[resourceGroup.name!]);
newFilters.push({ session, subscription, allResourceGroups, resourceGroups });
}
}
this.api.filters.splice(0, this.api.filters.length, ...newFilters);
this.onFiltersChanged.fire();
}
}
async function deviceLogin1(): Promise<DeviceLogin> {
return new Promise<DeviceLogin>((resolve, reject) => {
const cache = new MemoryCache();
const context = new AuthenticationContext(authorityUrl, null, cache);
context.acquireUserCode(resource, clientId, 'en-us', function (err: any, response: any) {
if (err) {
reject(new AzureLoginError('Aquiring user code failed', err));
} else {
resolve(response);
}
});
});
}
async function deviceLogin2(deviceLogin: DeviceLogin) {
return new Promise<TokenResponse>((resolve, reject) => {
const tokenCache = new MemoryCache();
const context = new AuthenticationContext(authorityUrl, null, tokenCache);
context.acquireTokenWithDeviceCode(resource, clientId, deviceLogin, function (err: any, tokenResponse: TokenResponse) {
if (err) {
reject(new AzureLoginError('Aquiring token with device code', err));
} else {
resolve(tokenResponse);
}
});
});
}
async function tokenFromRefreshToken(refreshToken: string, tenantId = commonTenantId) {
return new Promise<TokenResponse>((resolve, reject) => {
const tokenCache = new MemoryCache();
const context = new AuthenticationContext(`${authorityHostUrl}${tenantId}`, null, tokenCache);
context.acquireTokenWithRefreshToken(refreshToken, clientId, null, function (err: any, tokenResponse: TokenResponse) {
if (err) {
reject(new AzureLoginError('Aquiring token with refresh token', err));
} else {
resolve(tokenResponse);
}
});
});
}
async function tokensFromToken(firstTokenResponse: TokenResponse) {
const tokenResponses = [firstTokenResponse];
const tokenCache = new MemoryCache();
await addTokenToCache(tokenCache, firstTokenResponse);
const credentials = new DeviceTokenCredentials({ username: firstTokenResponse.userId, clientId, tokenCache });
const client = new SubscriptionClient(credentials);
const tenants = await listAll(client.tenants, client.tenants.list());
for (const tenant of tenants) {
if (tenant.tenantId !== firstTokenResponse.tenantId) {
const tokenResponse = await tokenFromRefreshToken(firstTokenResponse.refreshToken, tenant.tenantId);
tokenResponses.push(tokenResponse);
}
}
return tokenResponses;
}
async function addTokenToCache(tokenCache: any, tokenResponse: TokenResponse) {
return new Promise<any>((resolve, reject) => {
const driver = new CacheDriver(
{ _logContext: createLogContext('') },
`${authorityHostUrl}${tokenResponse.tenantId}`,
tokenResponse.resource,
clientId,
tokenCache,
(entry: any, resource: any, callback: (err: any, response: any) => {}) => {
callback(null, entry);
}
);
driver.add(tokenResponse, function (err: any) {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
async function clearTokenCache(tokenCache: any) {
await new Promise<void>((resolve, reject) => {
tokenCache.find({}, (err: any, entries: any[]) => {
if (err) {
reject(err);
} else {
tokenCache.remove(entries, (err: any) => {
if (err) {
reject(err);
} else {
resolve();
}
});
}
});
});
}
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;
}

Просмотреть файл

@ -1,126 +1,54 @@
import { window, ExtensionContext, commands, credentials, QuickPickItem } from 'vscode'; /*---------------------------------------------------------------------------------------------
import { AzureLoginHelper, listAll } from './azurelogin'; * Copyright (c) Microsoft Corporation. All rights reserved.
import { AzureLogin, AzureSession } from './azurelogin.api'; * Licensed under the MIT License. See License.txt in the project root for license information.
import { SubscriptionClient, ResourceManagementClient, SubscriptionModels } from 'azure-arm-resource'; *--------------------------------------------------------------------------------------------*/
import { window, ExtensionContext, commands, credentials } from 'vscode';
import { AzureLoginHelper } from './azure-account';
import { AzureAccount } from './azure-account.api';
import * as opn from 'opn'; import * as opn from 'opn';
import WebSiteManagementClient = require('azure-arm-website'); import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export function activate(context: ExtensionContext) { export function activate(context: ExtensionContext) {
if (!credentials) { if (!credentials) {
return; // Proposed API not available. return; // Proposed API not available.
} }
const azureLogin = new AzureLoginHelper(context); const azureLogin = new AzureLoginHelper(context);
const subscriptions = context.subscriptions; const subscriptions = context.subscriptions;
subscriptions.push(createStatusBarItem(azureLogin.api)); subscriptions.push(createStatusBarItem(azureLogin.api));
subscriptions.push(commands.registerCommand('vscode-azurelogin.createAccount', createAccount)); subscriptions.push(commands.registerCommand('azure-account.createAccount', createAccount));
subscriptions.push(commands.registerCommand('vscode-azurelogin.showSubscriptions', showSubscriptions(azureLogin.api))); return azureLogin.api;
subscriptions.push(commands.registerCommand('vscode-azurelogin.showAppServices', showAppServices(azureLogin.api)));
return azureLogin.api;
} }
function createAccount() { function createAccount() {
opn("https://azure.microsoft.com/en-us/free"); opn('https://azure.microsoft.com/en-us/free/?utm_source=campaign&utm_campaign=vscode-azure-account&mktingSource=vscode-azure-account');
} }
function createStatusBarItem(api: AzureLogin) { function createStatusBarItem(api: AzureAccount) {
const statusBarItem = window.createStatusBarItem(); const statusBarItem = window.createStatusBarItem();
function updateStatusBar() { function updateStatusBar() {
switch (api.status) { switch (api.status) {
case 'LoggingIn': case 'LoggingIn':
statusBarItem.text = 'Azure: Logging in...'; statusBarItem.text = localize('azure-account.loggingIn', "Azure: Logging in...");
statusBarItem.show(); statusBarItem.show();
break; break;
case 'LoggedIn': case 'LoggedIn':
statusBarItem.text = `Azure: ${api.sessions[0].userId}`; if (api.sessions.length) {
statusBarItem.show(); statusBarItem.text = localize('azure-account.loggedIn', "Azure: {0}", api.sessions[0].userId);
break; statusBarItem.show();
case 'LoggedOut': }
statusBarItem.text = 'Azure: Logged out'; break;
statusBarItem.show(); default:
break; statusBarItem.hide();
} break;
} }
api.onStatusChanged(updateStatusBar); }
api.onSessionsChanged(updateStatusBar); api.onStatusChanged(updateStatusBar);
updateStatusBar(); api.onSessionsChanged(updateStatusBar);
return statusBarItem; updateStatusBar();
} return statusBarItem;
interface SubscriptionItem {
label: string;
description: string;
session: AzureSession;
subscription: SubscriptionModels.Subscription;
}
function showSubscriptions(api: AzureLogin) {
return async () => {
if (api.status !== 'LoggedIn') {
return commands.executeCommand('vscode-azurelogin.askForLogin');
}
const subscriptionItems: SubscriptionItem[] = [];
for (const session of api.sessions) {
const credentials = session.credentials;
const subscriptionClient = new SubscriptionClient(credentials);
const subscriptions = await listAll(subscriptionClient.subscriptions, subscriptionClient.subscriptions.list());
subscriptionItems.push(...subscriptions.map(subscription => ({
label: subscription.displayName || '',
description: subscription.subscriptionId || '',
session,
subscription
})));
}
subscriptionItems.sort((a, b) => a.label.localeCompare(b.label));
const result = await window.showQuickPick(subscriptionItems);
if (result) {
const { session, subscription } = result;
if (subscription.subscriptionId) {
const resources = new ResourceManagementClient(session.credentials, subscription.subscriptionId);
const resourceGroups = await listAll(resources.resourceGroups, resources.resourceGroups.list());
resourceGroups.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
await window.showQuickPick(resourceGroups.map(resourceGroup => ({
label: resourceGroup.name || '',
description: resourceGroup.location,
resourceGroup
})));
}
}
};
}
function showAppServices(api: AzureLogin) {
return async () => {
if (api.status !== 'LoggedIn') {
return commands.executeCommand('vscode-azurelogin.askForLogin');
}
const webAppsPromises: Promise<QuickPickItem[]>[] = [];
for (const filter of api.filters) {
const client = new WebSiteManagementClient(filter.session.credentials, filter.subscription.subscriptionId!);
if (!filter.allResourceGroups) {
for (const resourceGroup of filter.resourceGroups) {
webAppsPromises.push(listAll(client.webApps, client.webApps.listByResourceGroup(resourceGroup.name!))
.then(webApps => webApps.map(webApp => ({
label: webApp.name || '',
description: `${filter.subscription.displayName} > ${resourceGroup.name}`,
webApp
}))));
}
} else {
webAppsPromises.push(listAll(client.webApps, client.webApps.list())
.then(webApps => webApps.map(webApp => {
const resourceGroup = filter.resourceGroups.find(resourceGroup => webApp.id!.startsWith(resourceGroup.id!));
return {
label: webApp.name || '',
description: `${filter.subscription.displayName} > ${resourceGroup ? resourceGroup.name : 'New Resource Group'}`,
webApp
};
})));
}
}
const webApps = (<QuickPickItem[]>[]).concat(...(await Promise.all(webAppsPromises)));
webApps.sort((a, b) => a.label.localeCompare(b.label));
await window.showQuickPick(webApps);
}
} }
export function deactivate() { export function deactivate() {

Просмотреть файл

@ -1,34 +0,0 @@
import TelemetryReporter from 'vscode-extension-telemetry';
import vscode = require('vscode');
export var reporter: TelemetryReporter | undefined;
export class Reporter extends vscode.Disposable {
constructor(ctx: vscode.ExtensionContext) {
super(() => reporter && reporter.dispose());
let packageInfo = getPackageInfo(ctx);
reporter = packageInfo && new TelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
}
}
interface IPackageInfo {
name: string;
version: string;
aiKey: string;
}
function getPackageInfo(context: vscode.ExtensionContext): IPackageInfo | undefined {
let extensionPackage = require(context.asAbsolutePath('./package.json'));
if (extensionPackage) {
return {
name: extensionPackage.name,
version: extensionPackage.version,
aiKey: extensionPackage.aiKey
};
}
return;
}

139
src/vscode.proposed.d.ts поставляемый
Просмотреть файл

@ -7,145 +7,6 @@
declare module 'vscode' { declare module 'vscode' {
export interface WorkspaceConfiguration2 extends WorkspaceConfiguration {
inspect<T>(section: string): { key: string; defaultValue?: T; globalValue?: T; workspaceValue?: T, folderValue?: T } | undefined;
}
// todo@joh discover files etc
export interface FileSystemProvider {
// todo@joh -> added, deleted, renamed, changed
onDidChange: Event<Uri>;
resolveContents(resource: Uri): string | Thenable<string>;
writeContents(resource: Uri, contents: string): void | Thenable<void>;
}
export namespace workspace {
export function registerFileSystemProvider(authority: string, provider: FileSystemProvider): Disposable;
/**
* Get a configuration object.
*
* When a section-identifier is provided only that part of the configuration
* is returned. Dots in the section-identifier are interpreted as child-access,
* like `{ myExt: { setting: { doIt: true }}}` and `getConfiguration('myExt.setting').get('doIt') === true`.
*
* When a resource is provided, only configuration scoped to that resource
* is returned.
*
* If editor is opened with `no folders` then returns the global configuration.
*
* If editor is opened with `folders` then returns the configuration from the folder in which the resource belongs to.
*
* If resource does not belongs to any opened folders, then returns the workspace configuration.
*
* @param section A dot-separated identifier.
* @param resource A resource for which configuration is asked
* @return The full workspace configuration or a subset.
*/
export function getConfiguration2(section?: string, resource?: Uri): WorkspaceConfiguration2;
}
/**
* Represents the workspace configuration.
*
* The workspace configuration is a merged view of
*
* - Default configuration
* - Global configuration
* - Workspace configuration (if available)
* - Folder configuration of the [resource](#workspace.getConfiguration2) (if requested and available)
*
* **Global configuration** comes from User Settings and shadows Defaults.
*
* **Workspace configuration** comes from the `.vscode` folder under first [workspace folders](#workspace.workspaceFolders)
* and shadows Globals configuration.
*
* **Folder configurations** comes from `.vscode` folder under [workspace folders](#workspace.workspaceFolders). Each [workspace folder](#workspace.workspaceFolders)
* has a configuration and the requested resource determines which folder configuration to pick. Folder configuration shodows Workspace configuration.
*
* *Note:* Workspace and Folder configurations contains settings from `launch.json` and `tasks.json` files. Their basename will be
* part of the section identifier. The following snippets shows how to retrieve all configurations
* from `launch.json`:
*
* ```ts
* // launch.json configuration
* const config = workspace.getConfiguration('launch', workspace.workspaceFolders[1]);
*
* // retrieve values
* const values = config.get('configurations');
* ```
*/
export interface WorkspaceConfiguration2 extends WorkspaceConfiguration {
/**
* Retrieve all information about a configuration setting. A configuration value
* often consists of a *default* value, a global or installation-wide value,
* a workspace-specific value and a folder-specific value.
*
* The *effective* value (returned by [`get`](#WorkspaceConfiguration.get))
* is computed like this: `defaultValue` overwritten by `globalValue`,
* `globalValue` overwritten by `workspaceValue`. `workspaceValue` overwritten by `folderValue`.
*
* *Note:* The configuration name must denote a leaf in the configuration tree
* (`editor.fontSize` vs `editor`) otherwise no result is returned.
*
* @param section Configuration name, supports _dotted_ names.
* @return Information about a configuration setting or `undefined`.
*/
inspect<T>(section: string): { key: string; defaultValue?: T; globalValue?: T; workspaceValue?: T, folderValue?: T } | undefined;
}
export namespace window {
export function sampleFunction(): Thenable<any>;
}
/**
* The contiguous set of modified lines in a diff.
*/
export interface LineChange {
readonly originalStartLineNumber: number;
readonly originalEndLineNumber: number;
readonly modifiedStartLineNumber: number;
readonly modifiedEndLineNumber: number;
}
export namespace commands {
/**
* Registers a diff information command that can be invoked via a keyboard shortcut,
* a menu item, an action, or directly.
*
* Diff information commands are different from ordinary [commands](#commands.registerCommand) as
* they only execute when there is an active diff editor when the command is called, and the diff
* information has been computed. Also, the command handler of an editor command has access to
* the diff information.
*
* @param command A unique identifier for the command.
* @param callback A command handler function with access to the [diff information](#LineChange).
* @param thisArg The `this` context used when invoking the handler function.
* @return Disposable which unregisters this command on disposal.
*/
export function registerDiffInformationCommand(command: string, callback: (diff: LineChange[], ...args: any[]) => any, thisArg?: any): Disposable;
}
export namespace debug {
/**
* Start debugging by using either a named launch or named compound configuration,
* or by directly passing a DebugConfiguration.
* Before debugging starts, all unsaved files are saved and the launch configurations are up-to-date.
* @param nameOrConfiguration Either the name of a debug or compound configuration or a DebugConfiguration object.
* @return A thenable that resolves when debugging could be successfully started.
*/
export function startDebugging(nameOrConfiguration: string | DebugConfiguration): Thenable<boolean>;
}
/** /**
* Namespace for handling credentials. * Namespace for handling credentials.
*/ */

Просмотреть файл

@ -1,22 +0,0 @@
//
// Note: This example test is leveraging the Mocha test framework.
// Please refer to their documentation on https://mochajs.org/ for help.
//
// The module 'assert' provides assertion methods from node
import * as assert from 'assert';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
// import * as vscode from 'vscode';
// import * as myExtension from '../src/extension';
// Defines a Mocha test suite to group tests of similar kind together
suite("Extension Tests", () => {
// Defines a Mocha unit test
test("Something 1", () => {
assert.equal(-1, [1, 2, 3].indexOf(5));
assert.equal(-1, [1, 2, 3].indexOf(0));
});
});

Просмотреть файл

@ -1,22 +0,0 @@
//
// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING
//
// This file is providing the test runner to use when running extension tests.
// By default the test runner in use is Mocha based.
//
// You can provide your own test runner if you want to override it by exporting
// a function run(testRoot: string, clb: (error:Error) => void) that the extension
// host can call to run the tests. The test runner is expected to use console.log
// to report the results back to the caller. When the tests are finished, return
// a possible error to the callback or null if none.
var testRunner = require('vscode/lib/testrunner');
// You can directly control Mocha options by uncommenting the following lines
// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info
testRunner.configure({
ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.)
useColors: true // colored output from test results
});
module.exports = testRunner;

Просмотреть файл

@ -4,7 +4,7 @@
"target": "es6", "target": "es6",
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"outDir": "out", "outDir": "./out",
"lib": [ "lib": [
"es6" "es6"
], ],
@ -12,7 +12,9 @@
"rootDir": "." "rootDir": "."
}, },
"exclude": [ "exclude": [
"node_modules", "node_modules"
".vscode-test" ],
"include": [
"src/**/*"
] ]
} }

Просмотреть файл

@ -1,33 +0,0 @@
# Welcome to your first VS Code Extension
## What's in the folder
* This folder contains all of the files necessary for your extension
* `package.json` - this is the manifest file in which you declare your extension and command.
The sample plugin registers a command and defines its title and command name. With this information
VS Code can show the command in the command palette. It doesnt yet need to load the plugin.
* `src/extension.ts` - this is the main file where you will provide the implementation of your command.
The file exports one function, `activate`, which is called the very first time your extension is
activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
We pass the function containing the implementation of the command as the second parameter to
`registerCommand`.
## Get up and running straight away
* press `F5` to open a new window with your extension loaded
* run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`
* set breakpoints in your code inside `src/extension.ts` to debug your extension
* find output from your extension in the debug console
## Make changes
* you can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`
* you can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes
## Explore the API
* you can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts`
## Run tests
* open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Launch Tests`
* press `F5` to run the tests in a new window with your extension loaded
* see the output of the test result in the debug console
* make changes to `test/extension.test.ts` or create new test files inside the `test` folder
* by convention, the test runner will only consider files matching the name pattern `**.test.ts`
* you can create folders inside the `test` folder to structure your tests any way you want