Update from built-in extension
This commit is contained in:
Родитель
cb55905fcd
Коммит
b30f88503d
|
@ -1,5 +1,5 @@
|
|||
# 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
|
34
README.md
34
README.md
|
@ -1,36 +1,6 @@
|
|||
# Azure Login README
|
||||
# Azure Login
|
||||
|
||||
## Features
|
||||
|
||||
### 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
|
||||
Base extension supplying login and subscription filtering functionality for Azure extension.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
74
package.json
74
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "vscode-azurelogin",
|
||||
"displayName": "Azure Login",
|
||||
"name": "azure-account",
|
||||
"displayName": "Azure Account",
|
||||
"description": "A common Login and Subscription management extension for VS Code.",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"icon": "images/azurelogin.png",
|
||||
|
@ -12,11 +12,10 @@
|
|||
"color": "#1289B9",
|
||||
"theme": "dark"
|
||||
},
|
||||
"aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217",
|
||||
"version": "0.0.1",
|
||||
"publisher": "chrisdias",
|
||||
"version": "0.1.0",
|
||||
"publisher": "ms-vscode",
|
||||
"engines": {
|
||||
"vscode": "^1.15.0"
|
||||
"vscode": "^1.16.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
|
@ -26,45 +25,39 @@
|
|||
],
|
||||
"enableProposedApi": true,
|
||||
"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",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "vscode-azurelogin.login",
|
||||
"title": "Login",
|
||||
"category": "Azure"
|
||||
"command": "azure-account.login",
|
||||
"title": "%azure-account.commands.login%",
|
||||
"category": "%azure-account.commands.azure%"
|
||||
},
|
||||
{
|
||||
"command": "vscode-azurelogin.logout",
|
||||
"title": "Logout",
|
||||
"category": "Azure"
|
||||
"command": "azure-account.logout",
|
||||
"title": "%azure-account.commands.logout%",
|
||||
"category": "%azure-account.commands.azure%"
|
||||
},
|
||||
{
|
||||
"command": "vscode-azurelogin.addFilter",
|
||||
"title": "Add Resource Filter",
|
||||
"category": "Azure"
|
||||
"command": "azure-account.addFilter",
|
||||
"title": "%azure-account.commands.addResourceFilter%",
|
||||
"category": "%azure-account.commands.azure%"
|
||||
},
|
||||
{
|
||||
"command": "vscode-azurelogin.removeFilter",
|
||||
"title": "Remove Resource Filter",
|
||||
"category": "Azure"
|
||||
"command": "azure-account.removeFilter",
|
||||
"title": "%azure-account.commands.removeResourceFilter%",
|
||||
"category": "%azure-account.commands.azure%"
|
||||
},
|
||||
{
|
||||
"command": "vscode-azurelogin.createAccount",
|
||||
"title": "Create an Account",
|
||||
"category": "Azure"
|
||||
},
|
||||
{
|
||||
"command": "vscode-azurelogin.showSubscriptions",
|
||||
"title": "Subscriptions",
|
||||
"category": "Azure Example"
|
||||
},
|
||||
{
|
||||
"command": "vscode-azurelogin.showAppServices",
|
||||
"title": "AppServices",
|
||||
"category": "Azure Example"
|
||||
"command": "azure-account.createAccount",
|
||||
"title": "%azure-account.commands.createAccount%",
|
||||
"category": "%azure-account.commands.azure%"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
|
@ -74,7 +67,7 @@
|
|||
"azure.resourceFilter": {
|
||||
"type": "array",
|
||||
"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": {
|
||||
"vscode:prepublish": "tsc -p ./",
|
||||
"compile": "tsc -watch -p ./",
|
||||
"postinstall": "node ./node_modules/vscode/bin/install",
|
||||
"test": "node ./node_modules/vscode/bin/test"
|
||||
"postinstall": "node ./node_modules/vscode/bin/install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/copy-paste": "^1.1.30",
|
||||
"@types/mocha": "^2.2.32",
|
||||
"@types/node": "^6.0.40",
|
||||
"@types/opn": "^3.0.28",
|
||||
"mocha": "^2.3.3",
|
||||
"typescript": "^2.0.3",
|
||||
"@types/copy-paste": "1.1.30",
|
||||
"@types/node": "6.0.40",
|
||||
"vscode": "^1.0.0",
|
||||
"vscode-extension-telemetry": "^0.0.6"
|
||||
"@types/opn": "3.0.28"
|
||||
},
|
||||
"dependencies": {
|
||||
"adal-node": "^0.1.22",
|
||||
"azure-arm-resource": "^2.0.0-preview",
|
||||
"azure-arm-website": "^1.0.0-preview",
|
||||
"copy-paste": "^1.3.0",
|
||||
"ms-rest-azure": "^2.2.3",
|
||||
"vscode-nls": "^2.0.2",
|
||||
"opn": "^5.1.0"
|
||||
}
|
||||
}
|
|
@ -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 { ServiceClientCredentials } from 'ms-rest';
|
||||
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 interface AzureLogin {
|
||||
export interface AzureAccount {
|
||||
readonly status: AzureLoginStatus;
|
||||
readonly onStatusChanged: Event<AzureLoginStatus>;
|
||||
readonly sessions: AzureSession[];
|
||||
readonly onSessionsChanged: Event<void>;
|
||||
readonly filters: AzureResourceFilter[];
|
||||
readonly onFiltersChanged: Event<void>;
|
||||
readonly credentials: Credentials;
|
||||
}
|
||||
|
||||
export interface AzureSession {
|
||||
|
@ -29,6 +30,10 @@ export interface AzureSession {
|
|||
export interface AzureResourceFilter {
|
||||
readonly session: AzureSession;
|
||||
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>;
|
||||
}
|
|
@ -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;
|
||||
}
|
158
src/extension.ts
158
src/extension.ts
|
@ -1,126 +1,54 @@
|
|||
import { window, ExtensionContext, commands, credentials, QuickPickItem } from 'vscode';
|
||||
import { AzureLoginHelper, listAll } from './azurelogin';
|
||||
import { AzureLogin, AzureSession } from './azurelogin.api';
|
||||
import { SubscriptionClient, ResourceManagementClient, SubscriptionModels } from 'azure-arm-resource';
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { window, ExtensionContext, commands, credentials } from 'vscode';
|
||||
import { AzureLoginHelper } from './azure-account';
|
||||
import { AzureAccount } from './azure-account.api';
|
||||
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) {
|
||||
if (!credentials) {
|
||||
return; // Proposed API not available.
|
||||
}
|
||||
const azureLogin = new AzureLoginHelper(context);
|
||||
const subscriptions = context.subscriptions;
|
||||
subscriptions.push(createStatusBarItem(azureLogin.api));
|
||||
subscriptions.push(commands.registerCommand('vscode-azurelogin.createAccount', createAccount));
|
||||
subscriptions.push(commands.registerCommand('vscode-azurelogin.showSubscriptions', showSubscriptions(azureLogin.api)));
|
||||
subscriptions.push(commands.registerCommand('vscode-azurelogin.showAppServices', showAppServices(azureLogin.api)));
|
||||
return azureLogin.api;
|
||||
if (!credentials) {
|
||||
return; // Proposed API not available.
|
||||
}
|
||||
const azureLogin = new AzureLoginHelper(context);
|
||||
const subscriptions = context.subscriptions;
|
||||
subscriptions.push(createStatusBarItem(azureLogin.api));
|
||||
subscriptions.push(commands.registerCommand('azure-account.createAccount', createAccount));
|
||||
return azureLogin.api;
|
||||
}
|
||||
|
||||
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) {
|
||||
const statusBarItem = window.createStatusBarItem();
|
||||
function updateStatusBar() {
|
||||
switch (api.status) {
|
||||
case 'LoggingIn':
|
||||
statusBarItem.text = 'Azure: Logging in...';
|
||||
statusBarItem.show();
|
||||
break;
|
||||
case 'LoggedIn':
|
||||
statusBarItem.text = `Azure: ${api.sessions[0].userId}`;
|
||||
statusBarItem.show();
|
||||
break;
|
||||
case 'LoggedOut':
|
||||
statusBarItem.text = 'Azure: Logged out';
|
||||
statusBarItem.show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
api.onStatusChanged(updateStatusBar);
|
||||
api.onSessionsChanged(updateStatusBar);
|
||||
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);
|
||||
}
|
||||
function createStatusBarItem(api: AzureAccount) {
|
||||
const statusBarItem = window.createStatusBarItem();
|
||||
function updateStatusBar() {
|
||||
switch (api.status) {
|
||||
case 'LoggingIn':
|
||||
statusBarItem.text = localize('azure-account.loggingIn', "Azure: Logging in...");
|
||||
statusBarItem.show();
|
||||
break;
|
||||
case 'LoggedIn':
|
||||
if (api.sessions.length) {
|
||||
statusBarItem.text = localize('azure-account.loggedIn', "Azure: {0}", api.sessions[0].userId);
|
||||
statusBarItem.show();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
statusBarItem.hide();
|
||||
break;
|
||||
}
|
||||
}
|
||||
api.onStatusChanged(updateStatusBar);
|
||||
api.onSessionsChanged(updateStatusBar);
|
||||
updateStatusBar();
|
||||
return statusBarItem;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -7,145 +7,6 @@
|
|||
|
||||
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.
|
||||
*/
|
||||
|
|
|
@ -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",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"outDir": "out",
|
||||
"outDir": "./out",
|
||||
"lib": [
|
||||
"es6"
|
||||
],
|
||||
|
@ -12,7 +12,9 @@
|
|||
"rootDir": "."
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".vscode-test"
|
||||
"node_modules"
|
||||
],
|
||||
"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 doesn’t 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
|
Загрузка…
Ссылка в новой задаче