diff --git a/dist/.gitkeep b/dist/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/extension/.vscode/extensions.json b/src/extension/.vscode/extensions.json index ee71911b10..ff1d982644 100644 --- a/src/extension/.vscode/extensions.json +++ b/src/extension/.vscode/extensions.json @@ -1,7 +1,5 @@ { - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format - "recommendations": [ - "eg2.tslint" - ] -} \ No newline at end of file + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": ["eg2.tslint"] +} diff --git a/src/extension/.vscode/launch.json b/src/extension/.vscode/launch.json index 60182029a5..e0b27b75a7 100644 --- a/src/extension/.vscode/launch.json +++ b/src/extension/.vscode/launch.json @@ -3,33 +3,28 @@ // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { - "version": "0.2.0", - "configurations": [{ - "name": "Run Extension", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "npm: watch" - }, - { - "name": "Extension Tests", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test" - ], - "outFiles": [ - "${workspaceFolder}/out/test/**/*.js" - ], - "preLaunchTask": "npm: watch" - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "npm: watch" + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" + ], + "outFiles": ["${workspaceFolder}/out/test/**/*.js"], + "preLaunchTask": "npm: watch" + } + ] } diff --git a/src/extension/.vscode/settings.json b/src/extension/.vscode/settings.json index 30bf8c2d3f..ffeaf91cb1 100644 --- a/src/extension/.vscode/settings.json +++ b/src/extension/.vscode/settings.json @@ -1,11 +1,11 @@ // Place your settings in this file to overwrite default and user settings. { - "files.exclude": { - "out": false // set this to true to hide the "out" folder with the compiled JS files - }, - "search.exclude": { - "out": true // set this to false to include "out" folder in search results - }, - // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off" -} \ No newline at end of file + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off" +} diff --git a/src/extension/.vscode/tasks.json b/src/extension/.vscode/tasks.json index 3b17e53b62..078ff7e01e 100644 --- a/src/extension/.vscode/tasks.json +++ b/src/extension/.vscode/tasks.json @@ -1,20 +1,20 @@ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format { - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "watch", - "problemMatcher": "$tsc-watch", - "isBackground": true, - "presentation": { - "reveal": "never" - }, - "group": { - "kind": "build", - "isDefault": true - } - } - ] + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] } diff --git a/src/extension/src/azure-arm/armFileHelper.ts b/src/extension/src/azure-arm/armFileHelper.ts new file mode 100644 index 0000000000..7a289de159 --- /dev/null +++ b/src/extension/src/azure-arm/armFileHelper.ts @@ -0,0 +1,18 @@ +import * as fsx from "fs-extra"; + +export namespace ARMFileHelper { + export function createOrOverwriteDir(dirPath: string): void { + if (fsx.pathExistsSync(dirPath)) { + fsx.removeSync(dirPath); + } + + fsx.mkdirpSync(dirPath); + } + + export function writeObjectToJsonFile( + filePath: string, + object: object + ): void { + fsx.writeFileSync(filePath, JSON.stringify(object, null, 2), "utf-8"); + } +} diff --git a/src/extension/src/azure-arm/resourceManager.ts b/src/extension/src/azure-arm/resourceManager.ts new file mode 100644 index 0000000000..cc19f99606 --- /dev/null +++ b/src/extension/src/azure-arm/resourceManager.ts @@ -0,0 +1,48 @@ +import ResourceManagementClient from "azure-arm-resource/lib/resource/resourceManagementClient"; +import { SubscriptionItem } from "../azure-auth/azureAuth"; +import { ServiceClientCredentials } from "ms-rest"; +import { SubscriptionError } from "../errors"; + +export class ResourceManager { + private AzureResourceManagementClient: ResourceManagementClient | undefined; + + private setClientState(userSubscriptionItem: SubscriptionItem): void { + if ( + this.AzureResourceManagementClient === undefined || + this.AzureResourceManagementClient.subscriptionId !== + userSubscriptionItem.subscriptionId + ) { + this.AzureResourceManagementClient = this.createResourceManagementClient( + userSubscriptionItem + ); + } + } + + private createResourceManagementClient( + userSubscriptionItem: SubscriptionItem + ): ResourceManagementClient { + let userCredentials: ServiceClientCredentials = + userSubscriptionItem.session.credentials; + if ( + userSubscriptionItem === undefined || + userSubscriptionItem.subscription === undefined || + userSubscriptionItem.subscriptionId === undefined + ) { + throw new SubscriptionError( + "SubscriptionItem cannot have undefined values" + ); + } + return new ResourceManagementClient( + userCredentials, + userSubscriptionItem.subscriptionId, + userSubscriptionItem.session.environment.resourceManagerEndpointUrl + ); + } + + public getResourceManagementClient( + userSubscriptionItem: SubscriptionItem + ): ResourceManagementClient { + this.setClientState(userSubscriptionItem); + return this.AzureResourceManagementClient!; + } +} diff --git a/src/extension/src/azure-cosmosDB/arm-templates/parameters.json b/src/extension/src/azure-cosmosDB/arm-templates/parameters.json new file mode 100644 index 0000000000..0250aa964c --- /dev/null +++ b/src/extension/src/azure-cosmosDB/arm-templates/parameters.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": {} +} diff --git a/src/extension/src/azure-cosmosDB/arm-templates/template.json b/src/extension/src/azure-cosmosDB/arm-templates/template.json new file mode 100644 index 0000000000..3e318bd742 --- /dev/null +++ b/src/extension/src/azure-cosmosDB/arm-templates/template.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "locationName": { + "type": "string" + }, + "defaultExperience": { + "type": "string" + }, + "capabilities": { + "type": "array" + }, + "kind": { + "type": "string" + } + }, + "variables": {}, + "resources": [ + { + "apiVersion": "2015-04-08", + "kind": "[parameters('kind')]", + "type": "Microsoft.DocumentDb/databaseAccounts", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "properties": { + "databaseAccountOfferType": "Standard", + "locations": [ + { + "id": "[concat(parameters('name'), '-', parameters('location'))]", + "failoverPriority": 0, + "locationName": "[parameters('locationName')]" + } + ], + "enableMultipleWriteLocations": true, + "isVirtualNetworkFilterEnabled": false, + "virtualNetworkRules": [], + "ipRangeFilter": "", + "dependsOn": [], + "capabilities": "[parameters('capabilities')]" + }, + "tags": { + "defaultExperience": "[parameters('defaultExperience')]", + "generatedBy": "Web Template Studio" + } + } + ], + "outputs": {} +} diff --git a/src/extension/src/azure-cosmosDB/cosmosDbModule.ts b/src/extension/src/azure-cosmosDB/cosmosDbModule.ts index ce460a95e4..bfbb465856 100644 --- a/src/extension/src/azure-cosmosDB/cosmosDbModule.ts +++ b/src/extension/src/azure-cosmosDB/cosmosDbModule.ts @@ -2,17 +2,25 @@ import { CosmosDBManagementClient } from "azure-arm-cosmosdb"; import { DatabaseAccount } from "azure-arm-cosmosdb/lib/models"; import { ServiceClientCredentials } from "ms-rest"; import { SubscriptionItem, ResourceGroupItem } from "../azure-auth/azureAuth"; +import * as fs from "fs"; +import * as path from "path"; import { SubscriptionError, AuthorizationError, DeploymentError } from "../errors"; +import { + ResourceManagementClient, + ResourceManagementModels +} from "azure-arm-resource/lib/resource/resourceManagementClient"; +import { ResourceManager } from "../azure-arm/resourceManager"; +import * as appRoot from "app-root-path"; +import { ARMFileHelper } from "../azure-arm/armFileHelper"; export interface CosmosDBSelections { cosmosDBResourceName: string; location: string; cosmosAPI: API; - tags: Object; subscriptionItem: SubscriptionItem; resourceGroupItem: ResourceGroupItem; } @@ -25,16 +33,72 @@ export interface DatabaseObject { connectionString: string; } -// Mongo NoSQL | Gremlin | Azure Table | SQL -export type API = "MongoDB" | "Graph" | "Table" | "DocumentDB"; +/* + * Azure Cosmos DB for Mongo API | Gremlin | Azure Table | Core (SQL) | Cassandra + */ +export type API = "MongoDB" | "Graph" | "Table" | "SQL" | "Cassandra"; + +/* + * ARM template definitions for Cosmos APIs + */ +interface APIdefinition { + readonly kind: string; + readonly defaultExperience: string; + readonly capabilities: Object[]; +} export class CosmosDBDeploy { - private SubscriptionItemCosmosClient: - | CosmosDBManagementClient - | undefined = undefined; + /* + * Map of Cosmos API type to its definitions for ARM templates + */ + private APIdefinitionMap = new Map([ + [ + "MongoDB", + { + kind: "MongoDB", + defaultExperience: "Azure Cosmos DB for MongoDB API", + capabilities: [] + } + ], + [ + "Graph", + { + kind: "GlobalDocumentDB", + defaultExperience: "Gremlin (graph)", + capabilities: [{ name: "EnableGremlin" }] + } + ], + [ + "Table", + { + kind: "GlobalDocumentDB", + defaultExperience: "Azure Table", + capabilities: [{ name: "EnableTable" }] + } + ], + [ + "SQL", + { + kind: "GlobalDocumentDB", + defaultExperience: "Core (SQL)", + capabilities: [] + } + ], + [ + "Cassandra", + { + kind: "GlobalDocumentDB", + defaultExperience: "Cassandra", + capabilities: [{ name: "EnableCassandra" }] + } + ] + ]); + + private SubscriptionItemCosmosClient: CosmosDBManagementClient | undefined; public async createCosmosDB( - userCosmosDBSelection: CosmosDBSelections + userCosmosDBSelection: CosmosDBSelections, + genPath: string ): Promise { /* * Create Cosmos Client with users credentials and selected subscription * @@ -48,41 +112,110 @@ export class CosmosDBDeploy { } var resourceGroup = userCosmosDBSelection.resourceGroupItem.name; - var dataBaseName = userCosmosDBSelection.cosmosDBResourceName; + var databaseName = userCosmosDBSelection.cosmosDBResourceName; var location = userCosmosDBSelection.location; var experience = userCosmosDBSelection.cosmosAPI; - var tagObject = userCosmosDBSelection.tags; - - var options = { - location: location, - locations: [{ locationName: location }], - kind: experience, - tag: tagObject, // sample: { defaultExperience: "Azure Cosmos DB for MongoDB API", BuildOrigin : "Project Acorn"}, - capabilities: [] + + let template = JSON.parse( + fs.readFileSync( + path.join( + appRoot.toString(), + "src", + "azure-cosmosDB", + "arm-templates", + "template.json" + ), + "utf8" + ) + ); + + let parameters = JSON.parse( + fs.readFileSync( + path.join( + appRoot.toString(), + "src", + "azure-cosmosDB", + "arm-templates", + "parameters.json" + ), + "utf8" + ) + ); + + let definitions: APIdefinition = this.APIdefinitionMap.get(experience)!; + + parameters.parameters = { + name: { + value: databaseName + }, + location: { + value: location + .split(" ") + .join("") + .toLowerCase() + }, + locationName: { + value: location + }, + defaultExperience: { + value: definitions.defaultExperience + }, + capabilities: { + value: definitions.capabilities + }, + kind: { + value: definitions.kind + } + }; + + let deploymentParams = parameters.parameters; + + var options: ResourceManagementModels.Deployment = { + properties: { + mode: "Incremental", + parameters: deploymentParams, + template: template + } }; try { if (this.SubscriptionItemCosmosClient === undefined) { throw new AuthorizationError("Cosmos Client cannot be undefined."); } + + let azureResourceClient: ResourceManagementClient = new ResourceManager().getResourceManagementClient( + userSubscriptionItem + ); + + ARMFileHelper.createOrOverwriteDir(path.join(genPath, "arm-templates")); + ARMFileHelper.writeObjectToJsonFile( + path.join(genPath, "arm-templates", "cosmos-template.json"), + template + ); + ARMFileHelper.writeObjectToJsonFile( + path.join(genPath, "arm-templates", "cosmos-parameters.json"), + parameters + ); + /* * Cosmos Client to generate a cosmos DB resource using resource group name, database name, and options * */ - var databaseAccount: DatabaseAccount = await this.SubscriptionItemCosmosClient.databaseAccounts.createOrUpdate( + await azureResourceClient.deployments.createOrUpdate( resourceGroup, - dataBaseName, + databaseName, options ); - databaseAccount = await this.SubscriptionItemCosmosClient.databaseAccounts.get( + + var databaseAccount: DatabaseAccount = await this.SubscriptionItemCosmosClient.databaseAccounts.get( resourceGroup, - dataBaseName + databaseName ); var connectionString = await this.getConnectionString( this.SubscriptionItemCosmosClient, resourceGroup, - dataBaseName + databaseName ); } catch (err) { throw new DeploymentError("CosmosDBDeploy: " + err.message); @@ -96,13 +229,10 @@ export class CosmosDBDeploy { } private setClientState(userSubscriptionItem: SubscriptionItem): void { - if (this.SubscriptionItemCosmosClient === undefined) { - this.SubscriptionItemCosmosClient = this.createCosmosClient( - userSubscriptionItem - ); - } else if ( + if ( + this.SubscriptionItemCosmosClient === undefined || this.SubscriptionItemCosmosClient.subscriptionId !== - userSubscriptionItem.subscriptionId + userSubscriptionItem.subscriptionId ) { this.SubscriptionItemCosmosClient = this.createCosmosClient( userSubscriptionItem @@ -175,8 +305,10 @@ export class CosmosDBDeploy { } /* - * Overload on getConnectionString; one for providing creating the Cosmos Client + * Returns Azure Cosmos DB connection string for user's deployed database instance. + * This is what the user will use to connect to the database. * + * Overload on getConnectionString; one for providing creating the Cosmos Client */ public async getConnectionString( userSubscriptionItem: SubscriptionItem, @@ -211,7 +343,6 @@ export class CosmosDBDeploy { resourceGroup, dataBaseName ); - console.log(result!.connectionStrings![0].connectionString!); return result!.connectionStrings![0].connectionString!; } } diff --git a/src/extension/src/controller.ts b/src/extension/src/controller.ts index 260f6af6da..588a93991e 100644 --- a/src/extension/src/controller.ts +++ b/src/extension/src/controller.ts @@ -257,7 +257,10 @@ export abstract class Controller { if (payload.selectedCosmos) { var cosmosPayload: any = payload.cosmos; - await Controller.processCosmosDeploymentSendStatusToClient(cosmosPayload); + await Controller.processCosmosDeploymentAndSendStatusToClient( + cosmosPayload, + enginePayload.path + ); } } @@ -290,7 +293,10 @@ export abstract class Controller { }); } - public static processCosmosDeploymentSendStatusToClient(cosmosPayload: any) { + public static processCosmosDeploymentAndSendStatusToClient( + cosmosPayload: any, + genPath: string + ) { /* * example: * { @@ -304,7 +310,7 @@ export abstract class Controller { * } * } */ - Controller.deployCosmosResource(cosmosPayload) + Controller.deployCosmosResource(cosmosPayload, genPath) .then((dbObject: DatabaseObject) => { Controller.handleValidMessage(ExtensionCommand.DeployCosmos, { databaseObject: dbObject @@ -417,7 +423,8 @@ export abstract class Controller { } public static async deployCosmosResource( - selections: any + selections: any, + genPath: string ): Promise { try { await Controller.validateCosmosAccountName( @@ -436,12 +443,12 @@ export abstract class Controller { selections.resourceGroup, Controller.usersCosmosDBSubscriptionItemCache ), - subscriptionItem: Controller.usersCosmosDBSubscriptionItemCache, - tags: { "Created from": "Web Template Studio" } + subscriptionItem: Controller.usersCosmosDBSubscriptionItemCache }; return await this.AzureCosmosDBProvider.createCosmosDB( - userCosmosDBSelection + userCosmosDBSelection, + genPath ); }