diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..02509e2 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "env": { + "browser": false, + "commonjs": true, + "es6": true, + "node": true + }, + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "sourceType": "module" + }, + "rules": { + "no-const-assign": "warn", + "no-this-before-super": "warn", + "no-undef": "warn", + "no-unreachable": "warn", + "no-unused-vars": "warn", + "constructor-super": "warn", + "valid-typeof": "warn" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..0915202 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the extensions.json format + "recommendations": [ + "dbaeumer.vscode-eslint" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2131f2c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +// A launch configuration that launches the extension inside a new window +{ + "version": "0.1.0", + "configurations": [ + { + "name": "Launch Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], + "stopOnEntry": false + }, + { + "name": "Launch Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/test" ], + "stopOnEntry": false + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..902014b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version +} \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..499648c --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,7 @@ +.vscode/** +.vscode-test/** +test/** +.gitignore +jsconfig.json +vsc-extension-quickstart.md +.eslintrc.json diff --git a/README.md b/README.md index 8624b3d..ddf37aa 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,44 @@ -# Contributing +# Azure Node Essentials + +This extension provides tools for NodeJs developers working with Azure SDKs. + +## Feature List + +1. Project and file scaffolding + * `yo azure-node` to create + * Javascript or Typescript project with package.json set up to target Azure SDKs + * Empty .js or .ts files + * pre populated .tsconfig or .jsconfig files + * create a service principal +1. Snippets for some common operations such as authentication, creating a service principal. + * `loginInt` : generate code for interactive login + * `loginPwd` : generate code for logging in with username and password + * `loginSp` : generate code for logging in with a service principal + * `spCreate` : generate code to create a service principal +1. Code generation scenarios + * command `Azure-Node: Generate code for template deployment` : generate code for template deployment + +### Sample workflow + +1. Open VS code with an empty workspace (empty folder) +1. Bring up VS code command palette, invoke `yo` +1. Choose `azure-node` generator and invoke it. +1. Choose `* app` to invoke the main generator (the sub generators for files are listed at the root level) +1. Choose a Javascript project and proceed. +1. This should initialize your project and install npm dependencies +1. Meanwhile, open the folder in VSCode and navigate to index.js +1. Notice that package.json has been set up and index.js has boiler plate code for authentication. +1. Place caret on the line after `// TODO: Write your application logic here.` +1. From VS Code's command palette, invoke `>Azure-Node: Generate code for template deployment` +1. The extension generates code for template deployment in the file currently active in the vscode editor and adds required dependencies to package.json. + +## Dependencies + +The following package/extension dependencies are auto installed when you install this extension. + +1. [vscode yo](https://marketplace.visualstudio.com/items?itemName=samverschueren.yo) +1. [generator-azure-node](https://github.com/Azure/generator-azure-node) + +## Contributing This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..90316b6 --- /dev/null +++ b/changelog.md @@ -0,0 +1,19 @@ +# Change Log for Azure Node Essentials + +## 0.2.3 [2017-02-08] + +1. Bug fixes for Mac +1. updates to readme +1. check for generator package's version and upgrade if not latest + +## 0.2.0 - 0.2.2 [2017-02-07] + +1. No product change, updates to readme and other metadata about the project. + +## 0.1.0 [2017-02-07] + +1. Project and file scaffolding +1. Snippets for some common operations such as authentication, creating a service principal. +1. Code generation + * generate code for template deployment + diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..b7caa7d --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "lib": [ + "es6" + ] + }, + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..752cb8e --- /dev/null +++ b/package.json @@ -0,0 +1,68 @@ +{ + "name": "azurenodeessentials", + "displayName": "Azure Node Essentials", + "description": "Azure Node SDK Essentials for VS Code", + "version": "0.2.3", + "publisher": "azuresdkteam", + "engines": { + "vscode": "^1.8.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/Azure/azure-node-essentials" + }, + "keywords": [ + "azure", + "cloud", + "azure-sdk" + ], + "icon": "resources/azure-icon.png", + "galleryBanner": { + "color": "#00abec", + "theme": "light" + }, + "categories": [ + "Other", + "Snippets" + ], + "activationEvents": [ + "*" + ], + "main": "./src/extension", + "contributes": { + "snippets": [ + { + "language": "javascript", + "path": "./src/snippets/snippets.json" + }, + { + "language": "typescript", + "path": "./src/snippets/snippets.json" + } + ], + "commands": [ + { + "command": "Azure-Node.template-deploy", + "title": "Generate code for template deployment", + "category": "Azure-Node" + } + ] + }, + "scripts": {}, + "dependencies": { + "npm": "*", + "escodegen": "^1.8.1", + "esprima": "^3.1.3" + }, + "devDependencies": { + "typescript": "^2.0.3", + "vscode": "^1.0.0", + "mocha": "^2.3.3", + "eslint": "^3.6.0", + "@types/node": "^6.0.40", + "@types/mocha": "^2.2.32" + }, + "extensionDependencies": [ + "samverschueren.yo" + ] +} diff --git a/resources/azure-icon.png b/resources/azure-icon.png new file mode 100644 index 0000000..9ecb103 Binary files /dev/null and b/resources/azure-icon.png differ diff --git a/src/codegen/codegen.js b/src/codegen/codegen.js new file mode 100644 index 0000000..66a4778 --- /dev/null +++ b/src/codegen/codegen.js @@ -0,0 +1,108 @@ +var esprima = require('esprima'); +var escodegen = require('escodegen'); + +/** + * Generates require('') statements to be inserted. + * + * This scans through the current document and determines the minimal non-duplicated list + * of imports we need to generate. + * + * @param {any} document the current file + * @returns a Set of require statements to be inserted. + */ +exports.generateRequireStatements = function generateRequireStatements(document, requiredModules) { + var foundImportsGroup = false; + var existingModules = new Set(); + var requireStatementMatcher = /var\s*\w+\s+=\s+require\(\'([a-zA-Z0-9_-]*)\'\);*\s*/; + var insertionLine = 0; + + // gather modules that are already imported in the document + for (var index = 0; index < document.lineCount; index++) { + var line = document.lineAt(index); + var matches = line.text.match(requireStatementMatcher); + if (matches) { + if (!foundImportsGroup) { + foundImportsGroup = true; + } + // as long as it matches the require statement, collect the list of imported modules. + existingModules.add(matches[1]); + } else if (foundImportsGroup) { + // we've already discovered a require statement previously and this new line no longer matches the pattern. + // we've gone past the require statement group. Do not loop through entire source text. + // record this line number, as we'll insert our require statements here. then, bail out of reading the source document. + insertionLine = index; + break; + } + } + + // difference between existing modules and ones we require, are the ones we need to insert. + var modulesToInsert = requiredModules.filter(x => !existingModules.has(x)); + + // generate code for importing modules. + var requireStatements = new Set(); + for (var module of modulesToInsert) { + var name = this.getNameAssignmentForModule(module); + var statement = this.generateRequireStatement(name, module) + requireStatements.add(statement); + } + + var result = { + line: insertionLine, + code: requireStatements + }; + + return result; +}; + +/** + * Generates a require('') statement with given information. + * + * This will not perform any additional checks. If you need to generate only + * if module is not already imported, use `generateRequireStatements` + */ +exports.generateRequireStatement = function generateRequireStatement(name, packageName) { + if (!name) { + name = packageName; + } + + var text = `var ${name} = require('${packageName}');\r\n`; + return text; +}; + +// TODO: do this programmatically. Normalize 'name', strip dashes and other illegal chars. use pascal casing. +exports.getNameAssignmentForModule = function getNameAssignmentForModule(module) { + switch (module) { + case 'azure-arm-resource': + return 'ResourceManagement'; + case 'ms-rest': + return 'msRest'; + case 'ms-rest-azure': + return 'msRestAzure'; + default: + return; + } +}; + +exports.generateNewLine = function generateNewLine() { + return this.generateCode('\r\n'); +}; + +// parse the text blob and emit code from the AST. +exports.generateCode = function generateCode(text) { + var ast = esprima.parse(text, { raw: true, tokens: true, range: true, comment: true }); + ast = escodegen.attachComments(ast, ast.comments, ast.tokens); + + var codegenOptions = { + comment: true, + format: { + indent: { + style: ' ' + }, + preserveBlankLines: true, + }, + sourceCode: text + }; + + var code = escodegen.generate(ast, codegenOptions); + return code; +}; diff --git a/src/codegen/codgen.template-deploy.js b/src/codegen/codgen.template-deploy.js new file mode 100644 index 0000000..0011aae --- /dev/null +++ b/src/codegen/codgen.template-deploy.js @@ -0,0 +1,48 @@ +var codegenerator = require('./codegen'); + +const deployTemplateFunctionName = 'deployTemplate'; + +// Generates NodeJs code for arm template deployment. +exports.deployTemplate = function deployTemplate() { + var text = `function ${deployTemplateFunctionName}(credentials, callback){\ + // TODO: initialize these variables + var subscriptionId;\ + var resourceGroupName;\ + var deploymentName;\ + var templateFilePath;\ + var templateParametersFilePath;\ + var template;\ + var templateParameters;\ + var parameters = {\ + template: template,\ + parameters: templateParameters,\ + mode: \'Complete\'\ + };\ + \r\n + try {\ + template = JSON.parse(fs.readFileSync(templateFilePath));\ + templateParameters = JSON.parse(fs.readFileSync(templateParametersFilePath));\ + } catch (error) {\ + console.error('Encountered error parsing template file:', error);\ + }\ + \r\n + var resourceClient = new ResourceManagement.ResourceManagementClient(credentials, subscriptionId);\ + resourceClient.deployments.createOrUpdate(resourceGroupName, deploymentName, parameters, callback);\ + }`; + + return codegenerator.generateCode(text); +}; + +exports.generateRequireStatements = function generateRequireStatements(document) { + const requiredModules = ['fs', 'azure-arm-resource', 'ms-rest', 'ms-rest-azure']; + return codegenerator.generateRequireStatements(document, requiredModules); +}; + +exports.deployTemplateCallSite = function deployTemplateCallSite() { + var text = `${deployTemplateFunctionName}(credentials, function(err, result){ });`; + return codegenerator.generateCode(text); +}; + +exports.getPackageDependencies = function getPackageDependencies() { + return ['azure-arm-resource', 'ms-rest', 'ms-rest-azure']; +} diff --git a/src/codegen/jsoneditor.js b/src/codegen/jsoneditor.js new file mode 100644 index 0000000..3b6ad52 --- /dev/null +++ b/src/codegen/jsoneditor.js @@ -0,0 +1,95 @@ +var fs = require('fs'); +var npm = require('npm'); + +/** + * Removes byte order marker. This catches EF BB BF (the UTF-8 BOM) + * because the buffer-to-string conversion in `fs.readFile()` + * translates it to FEFF, the UTF-16 BOM. + */ +exports.stripBOM = function stripBOM(content) { + if (Buffer.isBuffer(content)) { + content = content.toString(); + } + + if (content.charCodeAt(0) === 0xFEFF || content.charCodeAt(0) === 0xFFFE) { + content = content.slice(1); + } + + return content; +}; + +/** + * Provides a parsed JSON from the given local file path + */ +exports.parseJson = function parseJson(filePath) { + let result = null; + if (!filePath || (filePath && typeof filePath.valueOf() !== 'string')) { + let err = new Error('a local file path to package.json is required and must be of type string.'); + return Promise.reject(err); + } + try { + result = JSON.parse(this.stripBOM(fs.readFileSync(filePath, 'utf8'))); + return Promise.resolve(result); + } catch (err) { + return Promise.reject(err); + } +}; + +/** + * add given list of dependencies to package.json + */ +exports.addDependenciesIfRequired = function addDependenciesIfRequired(filePath, packages) { + this.parseJson(filePath).then(function (jsonObject) { + if (jsonObject) { + var existingPackages = new Set(); + for (var item in jsonObject.dependencies) { + existingPackages.add(item); + } + + var packagesToInsert = packages.filter(x => !existingPackages.has(x)); + var promises = packagesToInsert.map((pkgName) => updateDependency(pkgName)); + + Promise.all(promises).then(function (entries) { + if (!jsonObject.dependencies) { + jsonObject.dependencies = {}; + } + + for (var item in entries) { + var kvp = entries[item].split(':'); + jsonObject.dependencies[kvp[0]] = kvp[1]; + } + + fs.writeFileSync(filePath, JSON.stringify(jsonObject, null, ' ')); + }).catch(function (err) { + return console.error(err); + }); + } + }); +}; + +/** + * Given a package name as string, pings npm about the package and + * returns a well formed package dependency entry that can be inserted in package.json + */ +function updateDependency(pkgName) { + var promise = new Promise(function (resolve, reject) { + npm.load(function (err) { + if (err) { + return reject(err); + } + + npm.commands.view([pkgName, 'name', 'version'], true, function (err, info) { + if (err) { + return reject(err); + } + + var json = JSON.parse(JSON.stringify(info)); + var dependency = json[Object.keys(json)[0]]; + var entry = `${dependency.name}:^${dependency.version}`; + return resolve(entry); + }); + }); + }); + + return promise; +}; diff --git a/src/commands/azure-node.template-deploy.js b/src/commands/azure-node.template-deploy.js new file mode 100644 index 0000000..1facdf3 --- /dev/null +++ b/src/commands/azure-node.template-deploy.js @@ -0,0 +1,66 @@ +var fs = require('fs'); +var path = require('path'); +var vscode = require('vscode'); +var codegen = require('../codegen/codgen.template-deploy'); +var jsonEditor = require('../codegen/jsoneditor'); + +exports.createCommand = function createCommand() { + vscode.commands.registerCommand('Azure-Node.template-deploy', function () { + + if (!vscode.window.activeTextEditor) { + vscode.window.showInformationMessage(`please open a .js file in the editor and then use this code generator command.`); + return; + } + + // update package.json + updatePackageJson(); + + // generate code in current document + return generateCodeInEditor(); + }); +}; + +function updatePackageJson() { + // TODO: search rootDir\package.json + var filePath = path.join(vscode.workspace.rootPath, 'package.json'); + + if (fs.existsSync(filePath)) { + var packages = codegen.getPackageDependencies(); + jsonEditor.addDependenciesIfRequired(filePath, packages); + } +}; + +function generateCodeInEditor() { + // generate code to be inserted. + const document = vscode.window.activeTextEditor.document; + const lineCount = document.lineCount; + var importsAndLineNumber = codegen.generateRequireStatements(document); + var methodBody = codegen.deployTemplate(); + var callsite = codegen.deployTemplateCallSite(); + + vscode.window.activeTextEditor.edit((builder) => { + // insert import statements. + // Insertion point is the line where import group ends. + if (importsAndLineNumber) { + var importPos = new vscode.Position(importsAndLineNumber.line, 0); + var imports = importsAndLineNumber.code; + for (var importStatement of imports) { + builder.insert(importPos, importStatement); + } + } + + // insert code for template deployment. + const range = new vscode.Range(new vscode.Position(lineCount, 0), new vscode.Position(lineCount + 1, 0)); + builder.replace(range, methodBody); + + // fix callsite to invoke the function that was newly generated. + const currentPos = new vscode.Position(vscode.window.activeTextEditor.selection.active.line, 0); + builder.insert(currentPos, callsite); + }); + + // format the entire document. + // the code we inserted was generated as well-formatted but indenting is relative to the existing text + // in the document. Since we didn't examine existing text and are unaware of the indent depth where + // generated code will be inserted, we have to reformat the whole document. If this leads to performance issues, we'll revisit this logic. + return vscode.commands.executeCommand("editor.action.formatDocument"); +}; diff --git a/src/extension.js b/src/extension.js new file mode 100644 index 0000000..3a0c4d8 --- /dev/null +++ b/src/extension.js @@ -0,0 +1,107 @@ +let vscode = require('vscode'); +let path = require('path'); +let fs = require('fs'); +let utils = require('./utils'); + +// TODO: find a way to have the extension auto update the package dependency, instead of having an update to VSCode extension. +// TODO: try to optimize time spent in this method. +// instead of enumerating all the installed npm packages every time on activation. drop a file to this extension's install path +// after installing dependencies. after the first time, just check for the presence of that sentinel file and bail. +// that does not ensure the dependency will always exist though. +// TODO: check for package version updates and re-install if necessary. + +// Called once to activate the extension. +// The only thing this extension needs to do is to ensure that a certain npm package is installed globally. +// Ideally this should be done during install time, but VSCode does not support install time tasks. +// So, we do this on activation. Ideally, this is a one time task. +function activate(context) { + + // Register commands. + var commandFilesPath = path.join(context.extensionPath, 'src', 'commands'); + fs.readdir(commandFilesPath, (err, files) => { + files.forEach((file) => { + context.subscriptions.push( + require('./commands/' + path.basename(file, '.js')).createCommand() + ); + console.log(path.basename(file, '.js') + ' command added'); + }); + }); + + // Install dependencies. + ensureDependenciesAreInstalled(); +} + +function ensureDependenciesAreInstalled() { + // Download and install template generator package. + var extensionName = 'Azure-Node-Essentials'; + var generatorPackageName = 'generator-azure-node'; + var generatorPackageVersion = '0.1.0'; // TODO: query npm and obtain latest version. That would mean updating package would not require an extension update. + + utils.isNodeInstalled().then(function (result) { + if (!result) { + vscode.window.showInformationMessage(`Please install NodeJS and then run ${extensionName} extension.`); + return; + } + + // check the npm cache on this machine and determine if our dependency is present. + // if the dependency is present and is the latest version, there is nothing more to do. + // if it is not present or is not the latest, install it from npm. + utils.npmList().then(function (listOfPackages) { + var generatorPackage = listOfPackages.find(function (item) { + return item.startsWith(generatorPackageName); + }); + + var packageInfo = { present: false, needsUpgrade: false }; + if (generatorPackage) { + packageInfo.present = true; + packageInfo.needsUpgrade = generatorPackage.split('@')[1] !== generatorPackageVersion; + } + + return Promise.resolve(packageInfo); + }).then(function (packageInfo) { + if (!packageInfo.present || packageInfo.needsUpgrade) { + var options = { + global: true + }; + + var actionPerformed; + if (packageInfo.needsUpgrade) { + actionPerformed = 'upgrade'; + } else { + actionPerformed = 'install'; + // indicate to the user that the extension needs to perform some one-time startup tasks + vscode.window.showInformationMessage(`${extensionName} is installing dependencies...`); + } + + var installTask = utils.npmInstall([generatorPackageName], options); + return installTask.then( + function onFulfilled(value) { + return { action: actionPerformed, status: value }; + } + ); + } + else { + var result = { action: 'pre-installed', status: true }; // (pre-installed, nothing to do.) + return Promise.resolve(result); + } + + }).then(function (result) { + if (result && result.status === true) { + if (result.action === 'install') { + vscode.window.showInformationMessage(`${extensionName} successfully installed dependencies and is ready for use.`); + } else if (result.action === 'upgrade') { + vscode.window.setStatusBarMessage(`${extensionName} successfully upgraded dependencies in the background.`); + } + } + }).catch(function (err) { + vscode.window.showInformationMessage(`An error occurred while ${extensionName} was installing dependencies. ${err}`); + }); + }); +} + +exports.activate = activate; + +function deactivate() { +} + +exports.deactivate = deactivate; diff --git a/src/snippets/snippets.json b/src/snippets/snippets.json new file mode 100644 index 0000000..01ebc34 --- /dev/null +++ b/src/snippets/snippets.json @@ -0,0 +1,136 @@ +{ + "Login Interactively": { + "prefix": "loginInt", + "body": "msRestAzure.interactiveLogin(function(${1:err}, ${2:credentials}) {\n\tif (err) {\n\t\tconsole.log(err);\n\t\treturn;\n\t}\n\t$0\n});", + "description": "Interactively login to Azure account" + }, + "Login with Service Principal": { + "prefix": "loginSp", + "body": "msRestAzure.loginWithServicePrincipalSecret(${1:clientId}, ${2:secret}, ${3:domain}, function(${4:err}, ${5:credentials}) {\n\tif (err) {\n\t\tconsole.log(err);\n\t\treturn;\n\t}\n\t$0\n});", + "description": "Login with Service principal into Azure account" + }, + "Login with Password": { + "prefix": "loginPwd", + "body": "msRestAzure.loginWithUsernamePassword(${1:username}, ${2:password}, function(${3:err}, ${4:credentials}) {\n\tif (err) {\n\t\tconsole.log(err);\n\t\treturn;\n\t}\n\t$0\n});", + "description": "Login with username and password into Azure account" + }, + "Create Service Principal": { + "prefix": "spCreate", + "body": ["'use strict';", + "", + // Documentation text @0-indent + "// Steps:", + "// 1. Create AD application", + "// 2. Create SP on top of the AD application", + "// 3. Assigning the Contributor role to the SP", + "", + "function createServicePrincipal() {", + // imports @1-indent + "\tvar msrestazure = require('ms-rest-azure');", + "\tvar graph = require('azure-graph');", + "\tvar authorization = require('azure-arm-authorization');", + "\tvar util = require('util');", + "\tvar moment = require('moment');", + "", + // variable declarations @1-indent + "\t// TODO: Initialize these variables", + "\tvar ${0:tenantId;}", + "\tvar ${1:subscriptionId;}", + "\tvar ${2:passwordForSp;}", + "\tvar ${3:displayName;}", + "", + "\tvar homePage = 'http://' + displayName + ':8080';", + "\tvar identifierUris = [ homePage ];", + "\tvar roleId = 'b24988ac-6180-42a0-ab88-20f7382dd24c'; // contributor role", + "\tvar scope = '/subscriptions/' + subscriptionId;", + "\tvar roleDefinitionId = scope + '/providers/Microsoft.Authorization/roleDefinitions/' + roleId;", + "\tvar loginOptions = {", + "\t\tdomain: tenantId", + "\t};", + "", + "\tmsrestazure.interactiveLogin(loginOptions, function(err, creds) {", + "\t\tif (err) {", + "\t\t\tconsole.log('Error occured in interactive login: \\n' + util.inspect(err, { depth: null }));", + "\t\t\treturn;", + "\t\t}", + "", + // more variable declarations @2-indent + "\t\tvar options = {", + "\t\t\tdomain: tenantId,", + "\t\t\ttokenAudience: 'graph',", + "\t\t\tusername: creds.username,", + "\t\t\ttokenCache: creds.tokenCache,", + "\t\t\tenvironment: creds.environment", + "\t\t};", + "\t\tvar credsForGraph = new msrestazure.DeviceTokenCredentials(options);", + "\t\tvar graphClient = new graph(credsForGraph, tenantId);", + "\t\tvar startDate = new Date(Date.now());", + "\t\tvar endDate = new Date(startDate.toISOString());", + "\t\tvar m = moment(endDate);", + "\t\tm.add(1, 'years');", + "\t\tendDate = new Date(m.toISOString());", + "\t\tvar applicationCreateParameters = {", + "\t\t\tavailableToOtherTenants: false,", + "\t\t\tdisplayName: displayName,", + "\t\t\thomePage: homePage,", + "\t\t\tidentifierUris: identifierUris,", + "\t\t\tpasswordCredentials: [{", + "\t\t\t\tstartDate: startDate,", + "\t\t\t\tendDate: endDate,", + "\t\t\t\tkeyId: msrestazure.generateUuid(),", + "\t\t\t\tvalue: passwordForSp", + "\t\t\t}]", + "\t\t};", + "", + // 1. Create AD application @2-indent + "\t\tgraphClient.applications.create(applicationCreateParameters, function (err, application, req, res) {", + "\t\t\tif (err) {", + "\t\t\t\tconsole.log('Error occured while creating the application: \\n' + util.inspect(err, { depth: null }));", + "\t\t\t\treturn;", + "\t\t\t}", + "", + // 2. Create SP @3-indent + "\t\t\tvar servicePrincipalCreateParameters = {", + "\t\t\t\tappId: application.appId,", + "\t\t\t\taccountEnabled: true", + "\t\t\t};", + "", + "\t\t\tgraphClient.servicePrincipals.create(servicePrincipalCreateParameters, function (err, sp, req, res) {", + "\t\t\t\tif (err) {", + "\t\t\t\t\tconsole.log('Error occured while creating the servicePrincipal: \\n' + util.inspect(err, { depth: null }));", + "\t\t\t\t\treturn;", + "\t\t\t\t}", + "", + // 3. SP role assignment @4-indent + "\t\t\t\tvar authorizationClient = new authorization(creds, subscriptionId, null);", + "\t\t\t\tvar assignmentGuid = msrestazure.generateUuid();", + "\t\t\t\tvar roleCreateParams = {", + "\t\t\t\t\tproperties: {", + "\t\t\t\t\t\tprincipalId: sp.objectId,", + "\t\t\t\t\t\troleDefinitionId: roleDefinitionId,", + "\t\t\t\t\t\tscope: scope", + "\t\t\t\t\t}", + "\t\t\t\t};", + "", + "\t\t\t\tauthorizationClient.roleAssignments.create(scope, assignmentGuid, roleCreateParams, function (err, roleAssignment, req, res) {", + "\t\t\t\t\tif (err) {", + "\t\t\t\t\t\tconsole.log('\\nError occured while creating the roleAssignment: \\n' + util.inspect(err, { depth: null }));", + "\t\t\t\t\t\treturn;", + "\t\t\t\t\t}", + "", + // Print SP information to console + "\t\t\t\t\tconsole.log('>>>>>>>>>>>\\nSuccessfully created the role assignment for the servicePrincipal.\\n');", + "\t\t\t\t\tconsole.log('>>>>>>>>>>>\\nIn future for login you will need the following info:');", + "\t\t\t\t\tconsole.log('ServicePrincipal Id (SPN): ' + sp.appId);", + "\t\t\t\t\tconsole.log('ServicePincipal Password: ' + passwordForSp);", + "\t\t\t\t\tconsole.log('Tenant Id for ServicePrincipal: ' + tenantId);", + "\t\t\t\t\tconsole.log('>>>>>>>>>>>\\n');", + "\t\t\t\t});", // close sp role assignment + "\t\t\t});", // close sp create + "\t\t});", // close AD application create + "\t});", // close interactive login + "}" // close function createServicePrincipal + ], + "description": "Create a service principal" + } +} diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..47b20e0 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,84 @@ +let exec = require('child_process').exec; + +// checks if there exists a valid installation of NodeJs on this machine +exports.isNodeInstalled = function isNodeInstalled() { + var cmdString = "node -v"; + return new Promise(function (resolve, reject) { + exec(cmdString, (error, stdout) => { + if (error) { + return reject(error); + } + if (stdout.startsWith('v')) { + return resolve(true); + } + return resolve(false); + }); + }); +}; + +// lists all globally installed npm packages. +exports.npmList = function npmList(path) { + var global = false; + if (!path) global = true; + var cmdString = "npm ls --depth=0 " + (global ? "-g " : " "); + return new Promise(function (resolve, reject) { + exec(cmdString, { cwd: path ? path : "/" }, (error, stdout) => { + if (error && !stdout) { + return reject(error); + } + + var packages = []; + packages = stdout.split('\n'); + + packages = packages.filter(function (item) { + if (item.match(/^\+--.+/g) != null || item.match(/^├──.+/g) != null) { + return true; + } + if (item.match(/^`--.+/g) != null || item.match(/^└──.+/g) != null) { + return true; + } + return undefined; + }); + + packages = packages.map(function (item) { + // windows + if (item.match(/^\+--.+/g) != null) { + return item.replace(/^\+--\s/g, ""); + } + if (item.match(/^`--.+/g) != null) { + return item.replace(/^`--\s/g, ""); + } + // mac + if (item.match(/^├──.+/g) != null) { + return item.replace(/^├──\s/g, ""); + } + if (item.match(/^└──.+/g) != null) { + return item.replace(/^└──\s/g, ""); + } + }) + resolve(packages); + + }); + }); +}; + +// install given list of npm packages to the global location. +exports.npmInstall = function npmInstall(packages, opts) { + if (packages.length == 0 || !packages || !packages.length) { Promise.reject("No packages found"); } + if (typeof packages == "string") packages = [packages]; + if (!opts) opts = {}; + var cmdString = "npm install " + packages.join(" ") + " " + + (opts.global ? " -g" : "") + + (opts.save ? " --save" : "") + + (opts.saveDev ? " --saveDev" : ""); + + return new Promise(function (resolve, reject) { + exec(cmdString, { cwd: opts.cwd ? opts.cwd : "/" }, (error) => { + if (error) { + reject(error); + } else { + resolve(true); // return success. + } + }); + }); +}; diff --git a/test/extension.test.js b/test/extension.test.js new file mode 100644 index 0000000..c3c1517 --- /dev/null +++ b/test/extension.test.js @@ -0,0 +1,24 @@ +/* global suite, test */ + +// +// 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 +var assert = require('assert'); + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +var vscode = require('vscode'); +var myExtension = require('../extension'); + +// Defines a Mocha test suite to group tests of similar kind together +suite("Extension Tests", function() { + + // Defines a Mocha unit test + test("Something 1", function() { + assert.equal(-1, [1, 2, 3].indexOf(5)); + assert.equal(-1, [1, 2, 3].indexOf(0)); + }); +}); \ No newline at end of file diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..5604517 --- /dev/null +++ b/test/index.js @@ -0,0 +1,22 @@ +// +// 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.js (suite, test, etc.) + useColors: true // colored output from test results +}); + +module.exports = testRunner; \ No newline at end of file diff --git a/vsc-extension-quickstart.md b/vsc-extension-quickstart.md new file mode 100644 index 0000000..21fc8da --- /dev/null +++ b/vsc-extension-quickstart.md @@ -0,0 +1,33 @@ +# 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. +* `extension.js` - 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 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 `extension.js` +* 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.js` or create new test files inside the `test` folder + * by convention, the test runner will only consider files matching the name pattern `**.test.js` + * you can create folders inside the `test` folder to structure your tests any way you want \ No newline at end of file