Update from built-in extension
This commit is contained in:
Родитель
cb55905fcd
Коммит
b30f88503d
|
@ -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
|
34
README.md
34
README.md
|
@ -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
|
||||||
|
|
||||||
|
|
74
package.json
74
package.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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>;
|
||||||
|
}
|
|
@ -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';
|
* 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;
|
|
||||||
}
|
|
|
@ -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 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
|
|
Загрузка…
Ссылка в новой задаче