diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js
index ac633fea72..c2c1893ee2 100644
--- a/build/gulpfile.hygiene.js
+++ b/build/gulpfile.hygiene.js
@@ -95,7 +95,8 @@ const indentationFilter = [
'!**/*.{xlf,docx,sql,vsix}',
'!extensions/mssql/sqltoolsservice/**',
'!extensions/import/flatfileimportservice/**',
- '!extensions/admin-tool-ext-win/ssmsmin/**'
+ '!extensions/admin-tool-ext-win/ssmsmin/**',
+ '!extensions/resource-deployment/notebooks/**'
];
const copyrightFilter = [
@@ -257,7 +258,7 @@ function hygiene(some) {
const lines = file.__lines;
// Only take the first 10 lines to reduce false positives- the compiler will throw an error if it's not the first non-comment line in a file
// (10 is used to account for copyright and extraneous newlines)
- lines.slice(0,10).forEach((line, i) => {
+ lines.slice(0, 10).forEach((line, i) => {
if (/\s*'use\s*strict\s*'/.test(line)) {
console.error(file.relative + '(' + (i + 1) + ',1): Unnecessary \'use strict\' - this is already added by the compiler');
errorCount++;
diff --git a/extensions/resource-deployment/.vscodeignore b/extensions/resource-deployment/.vscodeignore
index 7f47d852aa..1142d2b348 100644
--- a/extensions/resource-deployment/.vscodeignore
+++ b/extensions/resource-deployment/.vscodeignore
@@ -1,2 +1,4 @@
+coverage/**
+out/test/**
src/**
-tsconfig.json
+coverageConfig.json
\ No newline at end of file
diff --git a/extensions/resource-deployment/coverageConfig.json b/extensions/resource-deployment/coverageConfig.json
new file mode 100644
index 0000000000..9b87f7e033
--- /dev/null
+++ b/extensions/resource-deployment/coverageConfig.json
@@ -0,0 +1,17 @@
+{
+ "enabled": true,
+ "relativeSourcePath": "..",
+ "relativeCoverageDir": "../../coverage",
+ "ignorePatterns": [
+ "**/node_modules/**"
+ ],
+ "includePid": false,
+ "reports": [
+ "cobertura"
+ ],
+ "verbose": false,
+ "remapOptions": {
+ "basePath": ".",
+ "useAbsolutePaths": true
+ }
+}
\ No newline at end of file
diff --git a/extensions/resource-deployment/images/aks.svg b/extensions/resource-deployment/images/aks.svg
deleted file mode 100644
index 1df4bc8835..0000000000
--- a/extensions/resource-deployment/images/aks.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/extensions/resource-deployment/images/kubernetes.svg b/extensions/resource-deployment/images/kubernetes.svg
deleted file mode 100644
index 0b17a4013a..0000000000
--- a/extensions/resource-deployment/images/kubernetes.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/extensions/resource-deployment/images/new.svg b/extensions/resource-deployment/images/new.svg
deleted file mode 100644
index f76813b363..0000000000
--- a/extensions/resource-deployment/images/new.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/extensions/resource-deployment/images/new_inverse.svg b/extensions/resource-deployment/images/new_inverse.svg
deleted file mode 100644
index 787edf63bd..0000000000
--- a/extensions/resource-deployment/images/new_inverse.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/extensions/resource-deployment/images/sql_bdc.svg b/extensions/resource-deployment/images/sql_bdc.svg
new file mode 100644
index 0000000000..fcf1133186
--- /dev/null
+++ b/extensions/resource-deployment/images/sql_bdc.svg
@@ -0,0 +1,45 @@
+
+
diff --git a/extensions/resource-deployment/images/sql_bdc_inverse.svg b/extensions/resource-deployment/images/sql_bdc_inverse.svg
new file mode 100644
index 0000000000..dbc823e9f0
--- /dev/null
+++ b/extensions/resource-deployment/images/sql_bdc_inverse.svg
@@ -0,0 +1,46 @@
+
+
diff --git a/extensions/resource-deployment/images/sql_server.svg b/extensions/resource-deployment/images/sql_server.svg
new file mode 100644
index 0000000000..03cff4d932
--- /dev/null
+++ b/extensions/resource-deployment/images/sql_server.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/extensions/resource-deployment/images/sql_server_inverse.svg b/extensions/resource-deployment/images/sql_server_inverse.svg
new file mode 100644
index 0000000000..1ce3f48b39
--- /dev/null
+++ b/extensions/resource-deployment/images/sql_server_inverse.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/extensions/resource-deployment/notebooks/sql2017-image-docker.ipynb b/extensions/resource-deployment/notebooks/sql2017-image-docker.ipynb
new file mode 100644
index 0000000000..6a915e14ef
--- /dev/null
+++ b/extensions/resource-deployment/notebooks/sql2017-image-docker.ipynb
@@ -0,0 +1,72 @@
+{
+ "metadata": {
+ "kernelspec": {
+ "name": "python3",
+ "display_name": "Python 3"
+ },
+ "language_info": {
+ "name": "python",
+ "version": "3.6.6",
+ "mimetype": "text/x-python",
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "pygments_lexer": "ipython3",
+ "nbconvert_exporter": "python",
+ "file_extension": ".py"
+ }
+ },
+ "nbformat_minor": 2,
+ "nbformat": 4,
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "source": "First, pull the Microsoft SQL Server 2017 Image",
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "source": "cmd = f'docker pull mcr.microsoft.com/mssql/server:2017-latest'\r\n!{cmd}",
+ "metadata": {},
+ "outputs": [],
+ "execution_count": 49
+ },
+ {
+ "cell_type": "markdown",
+ "source": "List all the containers",
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "source": "cmd = f'docker ps -a'\r\n!{cmd}",
+ "metadata": {},
+ "outputs": [],
+ "execution_count": 50
+ },
+ {
+ "cell_type": "markdown",
+ "source": "Start a new container with SQL Server 2017",
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "source": "import time\r\n\r\ncontainer_name = 'sql2017-' + time.strftime(\"%Y%m%d%H%M%S\", time.localtime())\r\nprint('New container name: ' + container_name)\r\n\r\nsql_port = 1433\r\nsa_password = \"\"\r\n\r\ncmd = f'docker run -e ACCEPT_EULA=Y -e \"SA_PASSWORD={sa_password}\" -p {sql_port}:{sql_port} --name {container_name} -d mcr.microsoft.com/mssql/server:2017-latest'\r\nprint(cmd)\r\n!{cmd}",
+ "metadata": {},
+ "outputs": [],
+ "execution_count": 51
+ },
+ {
+ "cell_type": "markdown",
+ "source": "List all the containers",
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "source": "cmd = f'docker ps -a'\r\n!{cmd}",
+ "metadata": {},
+ "outputs": [],
+ "execution_count": 48
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions/resource-deployment/notebooks/sql2019-ctp25-bdc-aks.ipynb b/extensions/resource-deployment/notebooks/sql2019-ctp25-bdc-aks.ipynb
new file mode 100644
index 0000000000..5ae0b48f5c
--- /dev/null
+++ b/extensions/resource-deployment/notebooks/sql2019-ctp25-bdc-aks.ipynb
@@ -0,0 +1,29 @@
+{
+ "metadata": {
+ "kernelspec": {
+ "name": "python3",
+ "display_name": "Python 3"
+ },
+ "language_info": {
+ "name": "python",
+ "version": "3.6.6",
+ "mimetype": "text/x-python",
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "pygments_lexer": "ipython3",
+ "nbconvert_exporter": "python",
+ "file_extension": ".py"
+ }
+ },
+ "nbformat_minor": 2,
+ "nbformat": 4,
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "source": "TODO",
+ "metadata": {}
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions/resource-deployment/notebooks/sql2019-ctp25-bdc-existing-cluster.ipynb b/extensions/resource-deployment/notebooks/sql2019-ctp25-bdc-existing-cluster.ipynb
new file mode 100644
index 0000000000..5ae0b48f5c
--- /dev/null
+++ b/extensions/resource-deployment/notebooks/sql2019-ctp25-bdc-existing-cluster.ipynb
@@ -0,0 +1,29 @@
+{
+ "metadata": {
+ "kernelspec": {
+ "name": "python3",
+ "display_name": "Python 3"
+ },
+ "language_info": {
+ "name": "python",
+ "version": "3.6.6",
+ "mimetype": "text/x-python",
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "pygments_lexer": "ipython3",
+ "nbconvert_exporter": "python",
+ "file_extension": ".py"
+ }
+ },
+ "nbformat_minor": 2,
+ "nbformat": 4,
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "source": "TODO",
+ "metadata": {}
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions/resource-deployment/notebooks/sql2019-ctp30-bdc-aks.ipynb b/extensions/resource-deployment/notebooks/sql2019-ctp30-bdc-aks.ipynb
new file mode 100644
index 0000000000..5ae0b48f5c
--- /dev/null
+++ b/extensions/resource-deployment/notebooks/sql2019-ctp30-bdc-aks.ipynb
@@ -0,0 +1,29 @@
+{
+ "metadata": {
+ "kernelspec": {
+ "name": "python3",
+ "display_name": "Python 3"
+ },
+ "language_info": {
+ "name": "python",
+ "version": "3.6.6",
+ "mimetype": "text/x-python",
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "pygments_lexer": "ipython3",
+ "nbconvert_exporter": "python",
+ "file_extension": ".py"
+ }
+ },
+ "nbformat_minor": 2,
+ "nbformat": 4,
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "source": "TODO",
+ "metadata": {}
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions/resource-deployment/notebooks/sql2019-ctp30-bdc-existing-cluster.ipynb b/extensions/resource-deployment/notebooks/sql2019-ctp30-bdc-existing-cluster.ipynb
new file mode 100644
index 0000000000..5ae0b48f5c
--- /dev/null
+++ b/extensions/resource-deployment/notebooks/sql2019-ctp30-bdc-existing-cluster.ipynb
@@ -0,0 +1,29 @@
+{
+ "metadata": {
+ "kernelspec": {
+ "name": "python3",
+ "display_name": "Python 3"
+ },
+ "language_info": {
+ "name": "python",
+ "version": "3.6.6",
+ "mimetype": "text/x-python",
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "pygments_lexer": "ipython3",
+ "nbconvert_exporter": "python",
+ "file_extension": ".py"
+ }
+ },
+ "nbformat_minor": 2,
+ "nbformat": 4,
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "source": "TODO",
+ "metadata": {}
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions/resource-deployment/notebooks/sql2019-image-docker.ipynb b/extensions/resource-deployment/notebooks/sql2019-image-docker.ipynb
new file mode 100644
index 0000000000..b7cbf977e6
--- /dev/null
+++ b/extensions/resource-deployment/notebooks/sql2019-image-docker.ipynb
@@ -0,0 +1,72 @@
+{
+ "metadata": {
+ "kernelspec": {
+ "name": "python3",
+ "display_name": "Python 3"
+ },
+ "language_info": {
+ "name": "python",
+ "version": "3.6.6",
+ "mimetype": "text/x-python",
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "pygments_lexer": "ipython3",
+ "nbconvert_exporter": "python",
+ "file_extension": ".py"
+ }
+ },
+ "nbformat_minor": 2,
+ "nbformat": 4,
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "source": "First, pull the Microsoft SQL Server 2019 Image",
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "source": "cmd = f'docker pull mcr.microsoft.com/mssql/server:2019-latest'\r\n!{cmd}",
+ "metadata": {},
+ "outputs": [],
+ "execution_count": 49
+ },
+ {
+ "cell_type": "markdown",
+ "source": "List all the containers",
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "source": "cmd = f'docker ps -a'\r\n!{cmd}",
+ "metadata": {},
+ "outputs": [],
+ "execution_count": 50
+ },
+ {
+ "cell_type": "markdown",
+ "source": "Start a new container with SQL Server 2019",
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "source": "import time\r\n\r\ncontainer_name = 'sql2019-' + time.strftime(\"%Y%m%d%H%M%S\", time.localtime())\r\nprint('New container name: ' + container_name)\r\n\r\nsql_port = 1433\r\nsa_password = \"\"\r\n\r\ncmd = f'docker run -e ACCEPT_EULA=Y -e \"SA_PASSWORD={sa_password}\" -p {sql_port}:{sql_port} --name {container_name} -d mcr.microsoft.com/mssql/server:2019-latest'\r\nprint(cmd)\r\n!{cmd}",
+ "metadata": {},
+ "outputs": [],
+ "execution_count": 51
+ },
+ {
+ "cell_type": "markdown",
+ "source": "List all the containers",
+ "metadata": {}
+ },
+ {
+ "cell_type": "code",
+ "source": "cmd = f'docker ps -a'\r\n!{cmd}",
+ "metadata": {},
+ "outputs": [],
+ "execution_count": 48
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions/resource-deployment/package.json b/extensions/resource-deployment/package.json
index 8d68d81e62..61a4c555fd 100644
--- a/extensions/resource-deployment/package.json
+++ b/extensions/resource-deployment/package.json
@@ -50,7 +50,163 @@
]
}
},
+ "resourceTypes": [
+ {
+ "name": "sql-image",
+ "displayName": "%resource-type-sql-image-display-name%",
+ "description": "%resource-type-sql-image-description%",
+ "platforms": [
+ "darwin",
+ "win32",
+ "linux"
+ ],
+ "icon": {
+ "light": "./images/sql_server.svg",
+ "dark": "./images/sql_server_inverse.svg"
+ },
+ "options": [
+ {
+ "name": "version",
+ "displayName": "%version-display-name%",
+ "values": [
+ {
+ "name": "sql2017",
+ "displayName": "%sql-2017-display-name%"
+ },
+ {
+ "name": "sql2019",
+ "displayName": "%sql-2019-display-name%"
+ }
+ ]
+ }
+ ],
+ "providers": [
+ {
+ "notebook": "%sql-2017-docker-notebook%",
+ "requiredTools": [
+ {
+ "name": "docker"
+ }
+ ],
+ "when": "version=sql2017"
+ },
+ {
+ "notebook": "%sql-2019-docker-notebook%",
+ "requiredTools": [
+ {
+ "name": "docker"
+ }
+ ],
+ "when": "version=sql2019"
+ }
+ ]
+ },
+ {
+ "name": "sql-bdc",
+ "displayName": "%resource-type-sql-bdc-display-name%",
+ "description": "%resource-type-sql-bdc-description%",
+ "platforms": [
+ "darwin",
+ "linux",
+ "win32"
+ ],
+ "icon": {
+ "light": "./images/sql_bdc.svg",
+ "dark": "./images/sql_bdc_inverse.svg"
+ },
+ "options": [
+ {
+ "name": "version",
+ "displayName": "%version-display-name%",
+ "values": [
+ {
+ "name": "ctp2.5",
+ "displayName": "%sql-2019ctp25-display-name%"
+ },
+ {
+ "name": "ctp3.0",
+ "displayName": "%sql-2019ctp30-display-name%"
+ }
+ ]
+ },
+ {
+ "name": "target",
+ "displayName": "%bdc-deployment-target%",
+ "values": [
+ {
+ "name": "aks",
+ "displayName": "%bdc-deployment-target-aks%"
+ },
+ {
+ "name": "existingCluster",
+ "displayName": "%bdc-deployment-target-existing-cluster%"
+ }
+ ]
+ }
+ ],
+ "providers": [
+ {
+ "notebook": "%bdc-ctp25-aks-notebook%",
+ "requiredTools": [
+ {
+ "name": "kubectl"
+ },
+ {
+ "name": "azcli"
+ },
+ {
+ "name": "mssqlctl"
+ }
+ ],
+ "when": "target=aks&&version=ctp2.5"
+ },
+ {
+ "notebook": "%bdc-ctp25-existing-cluster-notebook%",
+ "requiredTools": [
+ {
+ "name": "kubectl"
+ },
+ {
+ "name": "mssqlctl"
+ }
+ ],
+ "when": "target=existingCluster&&version=ctp2.5"
+ },
+ {
+ "notebook": "%bdc-ctp30-aks-notebook%",
+ "requiredTools": [
+ {
+ "name": "kubectl"
+ },
+ {
+ "name": "azcli"
+ },
+ {
+ "name": "mssqlctl"
+ }
+ ],
+ "when": "target=aks&&version=ctp3.0"
+ },
+ {
+ "notebook": "%bdc-ctp30-existing-cluster-notebook%",
+ "requiredTools": [
+ {
+ "name": "kubectl"
+ },
+ {
+ "name": "mssqlctl"
+ }
+ ],
+ "when": "target=existingCluster&&version=ctp3.0"
+ }
+ ]
+ }
+ ],
"dependencies": {
"vscode-nls": "^3.2.1"
+ },
+ "devDependencies": {
+ "typemoq": "^2.1.0",
+ "vscode": "^1.1.26"
}
}
\ No newline at end of file
diff --git a/extensions/resource-deployment/package.nls.json b/extensions/resource-deployment/package.nls.json
index 46d7e7bcbb..8bdd24ffd3 100644
--- a/extensions/resource-deployment/package.nls.json
+++ b/extensions/resource-deployment/package.nls.json
@@ -1,7 +1,25 @@
{
- "extension-displayName":"Resource Deployment extension for Azure Data Studio",
- "extension-description":"Provides a notebook-based experience to deploy SQL Server and other Azure Data Services",
+ "extension-displayName": "Resource Deployment extension for Azure Data Studio",
+ "extension-description": "Provides a notebook-based experience to deploy SQL Server and other Azure Data Services",
"deploy-sql-image-command-name": "Deploy SQL Server on Docker…",
"deploy-sql-bdc-command-name": "Deploy SQL Server Big Data Cluster…",
- "deploy-resource-command-category": "Resource Deployment"
+ "deploy-resource-command-category": "Resource Deployment",
+ "resource-type-sql-image-display-name": "Container Image",
+ "resource-type-sql-image-description": "SQL Server container image on Docker",
+ "resource-type-sql-bdc-display-name": "Big Data Cluster",
+ "resource-type-sql-bdc-description": "SQL Server Big Data Cluster",
+ "version-display-name": "Version",
+ "sql-2017-display-name": "SQL Server 2017",
+ "sql-2019-display-name": "SQL Server 2019",
+ "sql-2017-docker-notebook": "./notebooks/sql2017-image-docker.ipynb",
+ "sql-2019-docker-notebook": "./notebooks/sql2019-image-docker.ipynb",
+ "sql-2019ctp25-display-name": "SQL Server 2019 Big Data Cluster CTP 2.5",
+ "sql-2019ctp30-display-name": "SQL Server 2019 Big Data Cluster CTP 3.0",
+ "bdc-deployment-target": "Deployment target",
+ "bdc-deployment-target-aks": "New Azure Kubernetes Service Cluster",
+ "bdc-deployment-target-existing-cluster": "Existing Kubernetes Cluster",
+ "bdc-ctp25-aks-notebook": "./notebooks/sql2019-ctp25-bdc-aks.ipynb",
+ "bdc-ctp25-existing-cluster-notebook": "./notebooks/sql2019-ctp25-bdc-existing-cluster.ipynb",
+ "bdc-ctp30-aks-notebook": "./notebooks/sql2019-ctp30-bdc-aks.ipynb",
+ "bdc-ctp30-existing-cluster-notebook": "./notebooks/sql2019-ctp30-bdc-existing-cluster.ipynb"
}
\ No newline at end of file
diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts
index b7dec93b31..ad2f3eb536 100644
--- a/extensions/resource-deployment/src/interfaces.ts
+++ b/extensions/resource-deployment/src/interfaces.ts
@@ -3,3 +3,77 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
+
+export interface ResourceType {
+ name: string;
+ displayName: string;
+ description: string;
+ platforms: string[];
+ icon: { light: string; dark: string };
+ options: ResourceTypeOption[];
+ providers: DeploymentProvider[];
+ getProvider(selectedOptions: { option: string, value: string }[]): DeploymentProvider | undefined;
+}
+
+export interface ResourceTypeOption {
+ name: string;
+ displayName: string;
+ values: ResourceTypeOptionValue[];
+}
+
+export interface ResourceTypeOptionValue {
+ name: string;
+ displayName: string;
+}
+
+export interface DeploymentProvider {
+ notebook: string | NotebookInfo;
+ requiredTools: ToolRequirementInfo[];
+ when: string;
+}
+
+export interface NotebookInfo {
+ win32: string;
+ darwin: string;
+ linux: string;
+}
+
+export interface ToolRequirementInfo {
+ name: string;
+ version: string;
+}
+
+export enum ToolType {
+ Unknown,
+ AzCli,
+ KubeCtl,
+ Docker,
+ Python,
+ MSSQLCtl
+}
+
+export interface ToolStatusInfo {
+ type: ToolType;
+ name: string;
+ description: string;
+ version: string;
+ status: ToolInstallationStatus;
+}
+
+export interface ITool {
+ readonly name: string;
+ readonly displayName: string;
+ readonly description: string;
+ readonly type: ToolType;
+ readonly supportAutoInstall: boolean;
+
+ getInstallationStatus(versionExpression: string): Thenable;
+ install(version: string): Thenable;
+}
+
+export enum ToolInstallationStatus {
+ NotInstalled,
+ Installed,
+ Installing,
+ FailedToInstall
+}
\ No newline at end of file
diff --git a/extensions/resource-deployment/src/main.ts b/extensions/resource-deployment/src/main.ts
index 97f4fe99e2..2e0fe06c91 100644
--- a/extensions/resource-deployment/src/main.ts
+++ b/extensions/resource-deployment/src/main.ts
@@ -6,15 +6,45 @@
import vscode = require('vscode');
import { ResourceDeploymentDialog } from './ui/resourceDeploymentDialog';
+import { ToolsService } from './services/toolsService';
+import { NotebookService } from './services/notebookService';
+import { ResourceTypeService } from './services/resourceTypeService';
+import { PlatformService } from './services/platformService';
+import * as nls from 'vscode-nls';
+
+const localize = nls.loadMessageBundle();
export function activate(context: vscode.ExtensionContext) {
+ const platformService = new PlatformService();
+ const toolsService = new ToolsService();
+ const notebookService = new NotebookService(platformService);
+ const resourceTypeService = new ResourceTypeService(platformService, toolsService);
+
+ const resourceTypes = resourceTypeService.getResourceTypes();
+ const validationFailures = resourceTypeService.validateResourceTypes(resourceTypes);
+ if (validationFailures.length !== 0) {
+ const errorMessage = localize('resourceDeployment.FailedToLoadExtension', 'Failed to load extension: {0}, Error detected in the resource type definition in package.json, check debug console for details.', context.extensionPath);
+ vscode.window.showErrorMessage(errorMessage);
+ validationFailures.forEach(message => console.error(message));
+ return;
+ }
+
+ const openDialog = (resourceTypeName: string) => {
+ const filtered = resourceTypes.filter(resourceType => resourceType.name === resourceTypeName);
+ if (filtered.length !== 1) {
+ vscode.window.showErrorMessage(localize('resourceDeployment.UnknownResourceType', 'The resource type: {0} is not defined', resourceTypeName));
+ }
+ else {
+ let dialog = new ResourceDeploymentDialog(context, notebookService, toolsService, resourceTypeService, filtered[0]);
+ dialog.open();
+ }
+ };
+
vscode.commands.registerCommand('azdata.resource.sql-image.deploy', () => {
- let dialog = new ResourceDeploymentDialog();
- dialog.open();
+ openDialog('sql-image');
});
vscode.commands.registerCommand('azdata.resource.sql-bdc.deploy', () => {
- let dialog = new ResourceDeploymentDialog();
- dialog.open();
+ openDialog('sql-bdc');
});
}
diff --git a/extensions/resource-deployment/src/services/notebookService.ts b/extensions/resource-deployment/src/services/notebookService.ts
new file mode 100644
index 0000000000..90542556be
--- /dev/null
+++ b/extensions/resource-deployment/src/services/notebookService.ts
@@ -0,0 +1,79 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+'use strict';
+
+import { NotebookInfo } from '../interfaces';
+import { isString } from 'util';
+import * as os from 'os';
+import * as path from 'path';
+import * as nls from 'vscode-nls';
+import { IPlatformService } from './platformService';
+const localize = nls.loadMessageBundle();
+
+export interface INotebookService {
+ launchNotebook(notebook: string | NotebookInfo): void;
+}
+
+export class NotebookService implements INotebookService {
+
+ constructor(private platformService: IPlatformService) { }
+
+ /**
+ * Copy the notebook to the user's home directory and launch the notebook from there.
+ * @param notebook the path of the notebook
+ */
+ launchNotebook(notebook: string | NotebookInfo): void {
+ const notebookRelativePath = this.getNotebook(notebook);
+ const notebookFullPath = path.join(__dirname, '../../', notebookRelativePath);
+ if (notebookRelativePath && this.platformService.fileExists(notebookFullPath)) {
+ const targetFileName = this.getTargetNotebookFileName(notebookFullPath, os.homedir());
+ this.platformService.copyFile(notebookFullPath, targetFileName);
+ this.platformService.openFile(targetFileName);
+ }
+ else {
+ this.platformService.showErrorMessage(localize('resourceDeployment.notebookNotFound', 'The notebook {0} does not exist', notebookFullPath));
+ }
+ }
+
+ /**
+ * get the notebook path for current platform
+ * @param notebook the notebook path
+ */
+ getNotebook(notebook: string | NotebookInfo): string {
+ let notebookPath;
+ if (notebook && !isString(notebook)) {
+ const platform = this.platformService.platform();
+ if (platform === 'win32') {
+ notebookPath = notebook.win32;
+ } else if (platform === 'darwin') {
+ notebookPath = notebook.darwin;
+ } else {
+ notebookPath = notebook.linux;
+ }
+ } else {
+ notebookPath = notebook;
+ }
+ return notebookPath;
+ }
+
+ /**
+ * Get a file name that is not already used in the target directory
+ * @param notebook source notebook file name
+ * @param targetDirectory target directory
+ */
+ getTargetNotebookFileName(notebook: string, targetDirectory: string): string {
+ const notebookFileExtension = '.ipynb';
+ const baseName = path.basename(notebook, notebookFileExtension);
+ let targetFileName;
+ let idx = 0;
+ do {
+ const suffix = idx === 0 ? '' : `-${idx}`;
+ targetFileName = path.join(targetDirectory, `${baseName}${suffix}${notebookFileExtension}`);
+ idx++;
+ } while (this.platformService.fileExists(targetFileName));
+
+ return targetFileName;
+ }
+}
\ No newline at end of file
diff --git a/extensions/resource-deployment/src/services/platformService.ts b/extensions/resource-deployment/src/services/platformService.ts
new file mode 100644
index 0000000000..5f442df282
--- /dev/null
+++ b/extensions/resource-deployment/src/services/platformService.ts
@@ -0,0 +1,41 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+'use strict';
+
+import * as fs from 'fs';
+import * as vscode from 'vscode';
+
+/**
+ * Abstract of platform dependencies
+ */
+export interface IPlatformService {
+ platform(): string;
+ copyFile(source: string, target: string): void;
+ fileExists(file: string): boolean;
+ openFile(filePath: string): void;
+ showErrorMessage(message: string): void;
+}
+
+export class PlatformService implements IPlatformService {
+ platform(): string {
+ return process.platform;
+ }
+
+ copyFile(source: string, target: string): void {
+ fs.copyFileSync(source, target);
+ }
+
+ fileExists(file: string): boolean {
+ return fs.existsSync(file);
+ }
+
+ openFile(filePath: string): void {
+ vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath));
+ }
+
+ showErrorMessage(message: string): void {
+ vscode.window.showErrorMessage(message);
+ }
+}
\ No newline at end of file
diff --git a/extensions/resource-deployment/src/services/resourceTypeService.ts b/extensions/resource-deployment/src/services/resourceTypeService.ts
new file mode 100644
index 0000000000..038e3ee18b
--- /dev/null
+++ b/extensions/resource-deployment/src/services/resourceTypeService.ts
@@ -0,0 +1,174 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+'use strict';
+
+import { ResourceType, ResourceTypeOption, DeploymentProvider } from '../interfaces';
+import { IToolsService } from './toolsService';
+import * as vscode from 'vscode';
+import { IPlatformService } from './platformService';
+
+export interface IResourceTypeService {
+ getResourceTypes(filterByPlatform?: boolean): ResourceType[];
+ validateResourceTypes(resourceTypes: ResourceType[]): string[];
+}
+
+export class ResourceTypeService implements IResourceTypeService {
+ private _resourceTypes: ResourceType[] = [];
+
+ constructor(private platformService: IPlatformService, private toolsService: IToolsService) { }
+
+ /**
+ * Get the supported resource types
+ * @param filterByPlatform indicates whether to return the resource types supported on current platform.
+ */
+ getResourceTypes(filterByPlatform: boolean = true): ResourceType[] {
+ if (this._resourceTypes.length === 0) {
+ // If we load package.json directly using require(path) the contents won't be localized
+ this._resourceTypes = vscode.extensions.getExtension('microsoft.resource-deployment')!.packageJSON.resourceTypes as ResourceType[];
+ this._resourceTypes.forEach(resourceType => {
+ resourceType.getProvider = (selectedOptions) => { return this.getProvider(resourceType, selectedOptions); };
+ });
+ }
+
+ let resourceTypes = this._resourceTypes;
+ if (filterByPlatform) {
+ resourceTypes = resourceTypes.filter(resourceType => resourceType.platforms.includes(this.platformService.platform()));
+ }
+
+ return resourceTypes;
+ }
+
+ /**
+ * Validate the resource types and returns validation error messages if any.
+ * @param resourceTypes resource types to be validated
+ */
+ validateResourceTypes(resourceTypes: ResourceType[]): string[] {
+ // NOTE: The validation error messages do not need to be localized as it is only meant for the developer's use.
+ const errorMessages: string[] = [];
+ if (!resourceTypes || resourceTypes.length === 0) {
+ errorMessages.push('Resource type list is empty');
+ } else {
+ let resourceTypeIndex = 1;
+ resourceTypes.forEach(resourceType => {
+ this.validateResourceType(resourceType, `resource type index: ${resourceTypeIndex}`, errorMessages);
+ resourceTypeIndex++;
+ });
+ }
+
+ return errorMessages;
+ }
+
+ private validateResourceType(resourceType: ResourceType, positionInfo: string, errorMessages: string[]): void {
+ this.validateNameDisplayName(resourceType, 'resource type', positionInfo, errorMessages);
+ if (!resourceType.icon || !resourceType.icon.dark || !resourceType.icon.light) {
+ errorMessages.push(`Icon for resource type is not specified properly. ${positionInfo} `);
+ }
+
+ if (resourceType.options && resourceType.options.length > 0) {
+ let optionIndex = 1;
+ resourceType.options.forEach(option => {
+ const optionInfo = `${positionInfo}, option index: ${optionIndex} `;
+ this.validateResourceTypeOption(option, optionInfo, errorMessages);
+ optionIndex++;
+ });
+ }
+
+ this.validateProviders(resourceType, positionInfo, errorMessages);
+ }
+
+ private validateResourceTypeOption(option: ResourceTypeOption, positionInfo: string, errorMessages: string[]): void {
+ this.validateNameDisplayName(option, 'option', positionInfo, errorMessages);
+ if (!option.values || option.values.length === 0) {
+ errorMessages.push(`Option contains no values.${positionInfo} `);
+ } else {
+ let optionValueIndex = 1;
+ option.values.forEach(optionValue => {
+ const optionValueInfo = `${positionInfo}, option value index: ${optionValueIndex} `;
+ this.validateNameDisplayName(optionValue, 'option value', optionValueInfo, errorMessages);
+ optionValueIndex++;
+ });
+
+ // Make sure the values are unique
+ for (let i = 0; i < option.values.length; i++) {
+ if (option.values[i].name && option.values[i].displayName) {
+ let dupePositions = [];
+ for (let j = i + 1; j < option.values.length; j++) {
+ if (option.values[i].name === option.values[j].name
+ || option.values[i].displayName === option.values[j].displayName) {
+ // +1 to make the position 1 based.
+ dupePositions.push(j + 1);
+ }
+ }
+
+ if (dupePositions.length !== 0) {
+ errorMessages.push(`Option values with same name or display name are found at the following positions: ${i + 1}, ${dupePositions.join(',')}.${positionInfo} `);
+ }
+ }
+ }
+ }
+ }
+
+ private validateProviders(resourceType: ResourceType, positionInfo: string, errorMessages: string[]): void {
+ if (!resourceType.providers || resourceType.providers.length === 0) {
+ errorMessages.push(`No providers defined for resource type, ${positionInfo}`);
+ } else {
+ let providerIndex = 1;
+ resourceType.providers.forEach(provider => {
+ const providerPositionInfo = `${positionInfo}, provider index: ${providerIndex} `;
+ if (!provider.notebook) {
+ errorMessages.push(`Notebook is not specified for the provider, ${providerPositionInfo}`);
+ }
+
+ if (provider.requiredTools && provider.requiredTools.length > 0) {
+ provider.requiredTools.forEach(tool => {
+ if (!this.toolsService.getToolByName(tool.name)) {
+ errorMessages.push(`The tool is not supported: ${tool.name}, ${providerPositionInfo} `);
+ }
+ });
+ }
+ providerIndex++;
+ });
+ }
+ }
+
+ private validateNameDisplayName(obj: { name: string; displayName: string }, type: string, positionInfo: string, errorMessages: string[]): void {
+ if (!obj.name) {
+ errorMessages.push(`Name of the ${type} is empty.${positionInfo} `);
+ }
+ if (!obj.displayName) {
+ errorMessages.push(`Display name of the ${type} is empty.${positionInfo} `);
+ }
+ }
+
+ /**
+ * Get the provider based on the selected options
+ */
+ private getProvider(resourceType: ResourceType, selectedOptions: { option: string, value: string }[]): DeploymentProvider | undefined {
+ for (let i = 0; i < resourceType.providers.length; i++) {
+ const provider = resourceType.providers[i];
+
+ const expected = provider.when.replace(' ', '').split('&&').sort();
+ let actual: string[] = [];
+ selectedOptions.forEach(option => {
+ actual.push(`${option.option}=${option.value}`);
+ });
+ actual = actual.sort();
+
+ if (actual.length === expected.length) {
+ let matches = true;
+ for (let j = 0; j < actual.length; j++) {
+ if (actual[j] !== expected[j]) {
+ matches = false;
+ break;
+ }
+ }
+ if (matches) {
+ return provider;
+ }
+ }
+ }
+ return undefined;
+ }
+}
diff --git a/extensions/resource-deployment/src/services/tools/azCLITool.ts b/extensions/resource-deployment/src/services/tools/azCLITool.ts
new file mode 100644
index 0000000000..99cc4383c3
--- /dev/null
+++ b/extensions/resource-deployment/src/services/tools/azCLITool.ts
@@ -0,0 +1,43 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+'use strict';
+import { ToolType, ITool, ToolInstallationStatus } from '../../interfaces';
+import * as nls from 'vscode-nls';
+const localize = nls.loadMessageBundle();
+
+export class AzCliTool implements ITool {
+ get name(): string {
+ return 'azcli';
+ }
+
+ get description(): string {
+ return localize('resourceDeployment.AzCLIDescription', 'Tool used for managing Azure services');
+ }
+
+ get type(): ToolType {
+ return ToolType.AzCli;
+ }
+
+ get displayName(): string {
+ return localize('resourceDeployment.AzCLIDisplayName', 'Azure CLI');
+ }
+
+ get supportAutoInstall(): boolean {
+ return true;
+ }
+
+ install(version: string): Thenable {
+ throw new Error('Method not implemented.');
+ }
+
+ getInstallationStatus(versionExpression: string): Thenable {
+ let promise = new Promise(resolve => {
+ setTimeout(() => {
+ resolve(ToolInstallationStatus.Installed);
+ }, 500);
+ });
+ return promise;
+ }
+}
\ No newline at end of file
diff --git a/extensions/resource-deployment/src/services/tools/dockerTool.ts b/extensions/resource-deployment/src/services/tools/dockerTool.ts
new file mode 100644
index 0000000000..d9000920aa
--- /dev/null
+++ b/extensions/resource-deployment/src/services/tools/dockerTool.ts
@@ -0,0 +1,44 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+'use strict';
+import { ToolType, ITool, ToolInstallationStatus } from '../../interfaces';
+import * as nls from 'vscode-nls';
+const localize = nls.loadMessageBundle();
+
+
+export class DockerTool implements ITool {
+ get name(): string {
+ return 'docker';
+ }
+
+ get description(): string {
+ return localize('resourceDeployment.DockerDescription', 'Manages the containers');
+ }
+
+ get type(): ToolType {
+ return ToolType.Docker;
+ }
+
+ get displayName(): string {
+ return localize('resourceDeployment.DockerDisplayName', 'Docker');
+ }
+
+ get supportAutoInstall(): boolean {
+ return true;
+ }
+
+ install(version: string): Thenable {
+ throw new Error('Method not implemented.');
+ }
+
+ getInstallationStatus(versionExpression: string): Thenable {
+ let promise = new Promise(resolve => {
+ setTimeout(() => {
+ resolve(ToolInstallationStatus.Installed);
+ }, 500);
+ });
+ return promise;
+ }
+}
\ No newline at end of file
diff --git a/extensions/resource-deployment/src/services/tools/kubectlTool.ts b/extensions/resource-deployment/src/services/tools/kubectlTool.ts
new file mode 100644
index 0000000000..35b333f45c
--- /dev/null
+++ b/extensions/resource-deployment/src/services/tools/kubectlTool.ts
@@ -0,0 +1,43 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+'use strict';
+import { ToolType, ITool, ToolInstallationStatus } from '../../interfaces';
+import * as nls from 'vscode-nls';
+const localize = nls.loadMessageBundle();
+
+export class KubeCtlTool implements ITool {
+ get name(): string {
+ return 'kubectl';
+ }
+
+ get description(): string {
+ return localize('resourceDeployment.KUBECTLDescription', 'Tool used for managing the Kubernetes cluster');
+ }
+
+ get type(): ToolType {
+ return ToolType.KubeCtl;
+ }
+
+ get displayName(): string {
+ return localize('resourceDeployment.KUBECTLDisplayName', 'kubectl');
+ }
+
+ get supportAutoInstall(): boolean {
+ return true;
+ }
+
+ install(version: string): Thenable {
+ throw new Error('Method not implemented.');
+ }
+
+ getInstallationStatus(versionExpression: string): Thenable {
+ let promise = new Promise(resolve => {
+ setTimeout(() => {
+ resolve(ToolInstallationStatus.Installed);
+ }, 500);
+ });
+ return promise;
+ }
+}
\ No newline at end of file
diff --git a/extensions/resource-deployment/src/services/tools/mssqlctlTool.ts b/extensions/resource-deployment/src/services/tools/mssqlctlTool.ts
new file mode 100644
index 0000000000..6fb45289cf
--- /dev/null
+++ b/extensions/resource-deployment/src/services/tools/mssqlctlTool.ts
@@ -0,0 +1,52 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+'use strict';
+import { ToolType, ITool, ToolInstallationStatus } from '../../interfaces';
+import * as nls from 'vscode-nls';
+const localize = nls.loadMessageBundle();
+
+export class MSSQLCtlTool implements ITool {
+ get name(): string {
+ return 'mssqlctl';
+ }
+
+ get description(): string {
+ return localize('resourceDeployment.MSSQLCTLDescription', 'Command-line tool for installing and managing the SQL Server big data cluster');
+ }
+
+ get type(): ToolType {
+ return ToolType.MSSQLCtl;
+ }
+
+ get displayName(): string {
+ return localize('resourceDeployment.MSSQLCTLDisplayName', 'mssqlctl');
+ }
+
+ isInstalled(versionExpression: string): Thenable {
+ let promise = new Promise(resolve => {
+ setTimeout(() => {
+ resolve(true);
+ }, 500);
+ });
+ return promise;
+ }
+
+ get supportAutoInstall(): boolean {
+ return true;
+ }
+
+ install(version: string): Thenable {
+ throw new Error('Method not implemented.');
+ }
+
+ getInstallationStatus(versionExpression: string): Thenable {
+ let promise = new Promise(resolve => {
+ setTimeout(() => {
+ resolve(ToolInstallationStatus.Installed);
+ }, 500);
+ });
+ return promise;
+ }
+}
\ No newline at end of file
diff --git a/extensions/resource-deployment/src/services/tools/pythonTool.ts b/extensions/resource-deployment/src/services/tools/pythonTool.ts
new file mode 100644
index 0000000000..4ff698f839
--- /dev/null
+++ b/extensions/resource-deployment/src/services/tools/pythonTool.ts
@@ -0,0 +1,44 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+'use strict';
+import { ToolType, ITool, ToolInstallationStatus } from '../../interfaces';
+import * as nls from 'vscode-nls';
+const localize = nls.loadMessageBundle();
+
+
+export class PythonTool implements ITool {
+ get name(): string {
+ return 'python';
+ }
+
+ get description(): string {
+ return localize('resourceDeployment.PythonDescription', 'Required by notebook feature');
+ }
+
+ get type(): ToolType {
+ return ToolType.Python;
+ }
+
+ get displayName(): string {
+ return localize('resourceDeployment.PythonDisplayName', 'Python');
+ }
+
+ get supportAutoInstall(): boolean {
+ return true;
+ }
+
+ install(version: string): Thenable {
+ throw new Error('Method not implemented.');
+ }
+
+ getInstallationStatus(versionExpression: string): Thenable {
+ let promise = new Promise(resolve => {
+ setTimeout(() => {
+ resolve(ToolInstallationStatus.Installed);
+ }, 500);
+ });
+ return promise;
+ }
+}
\ No newline at end of file
diff --git a/extensions/resource-deployment/src/services/toolsService.ts b/extensions/resource-deployment/src/services/toolsService.ts
new file mode 100644
index 0000000000..c440893154
--- /dev/null
+++ b/extensions/resource-deployment/src/services/toolsService.ts
@@ -0,0 +1,52 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+'use strict';
+import { ToolRequirementInfo, ToolStatusInfo, ITool } from '../interfaces';
+import { PythonTool } from './tools/pythonTool';
+import { DockerTool } from './tools/dockerTool';
+import { AzCliTool } from './tools/azCliTool';
+import { MSSQLCtlTool } from './tools/mssqlCtlTool';
+import { KubeCtlTool } from './tools/kubeCtlTool';
+
+export interface IToolsService {
+ getToolStatus(toolRequirements: ToolRequirementInfo[]): Thenable;
+ getToolByName(toolName: string): ITool | undefined;
+}
+
+export class ToolsService implements IToolsService {
+ private static readonly SupportedTools: ITool[] = [new PythonTool(), new DockerTool(), new AzCliTool(), new MSSQLCtlTool(), new KubeCtlTool()];
+
+ getToolStatus(toolRequirements: ToolRequirementInfo[]): Thenable {
+ const toolStatusList: ToolStatusInfo[] = [];
+ let promises = [];
+ for (let i = 0; i < toolRequirements.length; i++) {
+ const toolRequirement = toolRequirements[i];
+ const tool = this.getToolByName(toolRequirement.name);
+ if (tool !== undefined) {
+ promises.push(tool.getInstallationStatus(toolRequirement.version).then(installStatus => {
+ toolStatusList.push({
+ name: tool.displayName,
+ description: tool.description,
+ status: installStatus,
+ version: toolRequirement.version
+ });
+ }));
+ }
+ }
+ return Promise.all(promises).then(() => { return toolStatusList; });
+ }
+
+ getToolByName(toolName: string): ITool | undefined {
+ if (toolName) {
+ for (let i = 0; i < ToolsService.SupportedTools.length; i++) {
+ if (toolName === ToolsService.SupportedTools[i].name) {
+ return ToolsService.SupportedTools[i];
+ }
+ }
+ }
+ return undefined;
+ }
+
+}
\ No newline at end of file
diff --git a/extensions/resource-deployment/src/test/index.ts b/extensions/resource-deployment/src/test/index.ts
new file mode 100644
index 0000000000..f525a8cf52
--- /dev/null
+++ b/extensions/resource-deployment/src/test/index.ts
@@ -0,0 +1,30 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+const path = require('path');
+const testRunner = require('vscode/lib/testrunner');
+
+const suite = 'Resource Deployment Unit Tests';
+
+const testOptions: any = {
+ ui: 'tdd',
+ useColors: true,
+ timeout: 60000
+};
+
+if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
+ testOptions.reporter = 'mocha-multi-reporters';
+ testOptions.reporterOptions = {
+ reporterEnabled: 'spec, mocha-junit-reporter',
+ mochaJunitReporterReporterOptions: {
+ testsuitesTitle: `${suite} ${process.platform}`,
+ mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
+ }
+ };
+}
+
+testRunner.configure(testOptions);
+
+export = testRunner;
diff --git a/extensions/resource-deployment/src/test/notebookService.test.ts b/extensions/resource-deployment/src/test/notebookService.test.ts
new file mode 100644
index 0000000000..c5afc79274
--- /dev/null
+++ b/extensions/resource-deployment/src/test/notebookService.test.ts
@@ -0,0 +1,130 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+'use strict';
+
+import * as TypeMoq from 'typemoq';
+import 'mocha';
+import * as path from 'path';
+import * as os from 'os';
+import { NotebookService } from '../services/NotebookService';
+import assert = require('assert');
+import { NotebookInfo } from '../interfaces';
+import { IPlatformService } from '../services/platformService';
+
+suite('Notebook Service Tests', function (): void {
+
+ test('getNotebook with string parameter', () => {
+ const mockPlatformService = TypeMoq.Mock.ofType();
+ const notebookService = new NotebookService(mockPlatformService.object);
+ const notebookInput = 'test-notebook.ipynb';
+ mockPlatformService.setup((service) => service.platform()).returns(() => { return 'win32'; });
+ let returnValue = notebookService.getNotebook(notebookInput);
+ assert.equal(returnValue, notebookInput, 'returned notebook name does not match expected value');
+ mockPlatformService.verify((service) => service.platform(), TypeMoq.Times.never());
+
+ mockPlatformService.reset();
+ mockPlatformService.setup((service) => service.platform()).returns(() => { return 'win32'; });
+ returnValue = notebookService.getNotebook('');
+ assert.equal(returnValue, '', 'returned notebook name does not match expected value is not an empty string');
+ mockPlatformService.verify((service) => service.platform(), TypeMoq.Times.never());
+ });
+
+ test('getNotebook with NotebookInfo parameter', () => {
+ const mockPlatformService = TypeMoq.Mock.ofType();
+ const notebookService = new NotebookService(mockPlatformService.object);
+ const notebookWin32 = 'test-notebook-win32.ipynb';
+ const notebookDarwin = 'test-notebook-darwin.ipynb';
+ const notebookLinux = 'test-notebook-linux.ipynb';
+
+ const notebookInput: NotebookInfo = {
+ darwin: notebookDarwin,
+ win32: notebookWin32,
+ linux: notebookLinux
+ };
+ mockPlatformService.setup((service) => service.platform()).returns(() => { return 'win32'; });
+ let returnValue = notebookService.getNotebook(notebookInput);
+ assert.equal(returnValue, notebookWin32, 'returned notebook name does not match expected value for win32 platform');
+ mockPlatformService.verify((service) => service.platform(), TypeMoq.Times.once());
+
+ mockPlatformService.reset();
+ mockPlatformService.setup((service) => service.platform()).returns(() => { return 'darwin'; });
+ returnValue = notebookService.getNotebook(notebookInput);
+ assert.equal(returnValue, notebookDarwin, 'returned notebook name does not match expected value for darwin platform');
+ mockPlatformService.verify((service) => service.platform(), TypeMoq.Times.once());
+
+ mockPlatformService.reset();
+ mockPlatformService.setup((service) => service.platform()).returns(() => { return 'linux'; });
+ returnValue = notebookService.getNotebook(notebookInput);
+ assert.equal(returnValue, notebookLinux, 'returned notebook name does not match expected value for linux platform');
+ mockPlatformService.verify((service) => service.platform(), TypeMoq.Times.once());
+ });
+
+ test('launchNotebook', () => {
+ const mockPlatformService = TypeMoq.Mock.ofType();
+ const notebookService = new NotebookService(mockPlatformService.object);
+ const notebookFileName = 'mynotebook.ipynb';
+ const notebookPath = `./notebooks/${notebookFileName}`;
+
+ let actualSourceFile;
+ const expectedSourceFile = path.join(__dirname, '../../', notebookPath);
+ let actualTargetFile;
+ const expectedTargetFile = path.join(os.homedir(), notebookFileName);
+ mockPlatformService.setup((service) => service.platform()).returns(() => { return 'win32'; });
+ mockPlatformService.setup((service) => service.openFile(TypeMoq.It.isAnyString()));
+ mockPlatformService.setup((service) => service.fileExists(TypeMoq.It.isAnyString()))
+ .returns((path) => {
+ if (path === expectedSourceFile) {
+ return true;
+ }
+ return false;
+ });
+ mockPlatformService.setup((service) => service.copyFile(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString()))
+ .returns((source, target) => { actualSourceFile = source; actualTargetFile = target; });
+ notebookService.launchNotebook(notebookPath);
+ mockPlatformService.verify((service) => service.copyFile(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString()), TypeMoq.Times.once());
+ mockPlatformService.verify((service) => service.openFile(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
+ assert.equal(actualSourceFile, expectedSourceFile, 'source file is not correct');
+ assert.equal(actualTargetFile, expectedTargetFile, 'target file is not correct');
+ });
+
+ test('getTargetNotebookFileName with no name conflict', () => {
+ const mockPlatformService = TypeMoq.Mock.ofType();
+ const notebookService = new NotebookService(mockPlatformService.object);
+ const notebookFileName = 'mynotebook.ipynb';
+ const sourceNotebookPath = `./notebooks/${notebookFileName}`;
+
+ const expectedTargetFile = path.join(os.homedir(), notebookFileName);
+ mockPlatformService.setup((service) => service.fileExists(TypeMoq.It.isAnyString()))
+ .returns((path) => { return false; });
+ const actualFileName = notebookService.getTargetNotebookFileName(sourceNotebookPath, os.homedir());
+ mockPlatformService.verify((service) => service.fileExists(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
+ assert.equal(actualFileName, expectedTargetFile, 'target file name is not correct');
+ });
+
+ test('getTargetNotebookFileName with name conflicts', () => {
+ const mockPlatformService = TypeMoq.Mock.ofType();
+ const notebookService = new NotebookService(mockPlatformService.object);
+ const notebookFileName = 'mynotebook.ipynb';
+ const sourceNotebookPath = `./notebooks/${notebookFileName}`;
+ const expectedFileName = 'mynotebook-2.ipynb';
+
+ const expected1stAttemptTargetFile = path.join(os.homedir(), notebookFileName);
+ const expected2ndAttemptTargetFile = path.join(os.homedir(), 'mynotebook-1.ipynb');
+ const expectedTargetFile = path.join(os.homedir(), expectedFileName);
+ mockPlatformService.setup((service) => service.fileExists(TypeMoq.It.isAnyString()))
+ .returns((path) => {
+ // list all the possible values here and handle them
+ // if we only handle the expected value and return true for anything else, the test might run forever until times out
+ if (path === expected1stAttemptTargetFile || path === expected2ndAttemptTargetFile) {
+ return true;
+ }
+ return false;
+ });
+ const actualFileName = notebookService.getTargetNotebookFileName(sourceNotebookPath, os.homedir());
+ mockPlatformService.verify((service) => service.fileExists(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(3));
+ assert.equal(actualFileName, expectedTargetFile, 'target file name is not correct');
+ });
+});
\ No newline at end of file
diff --git a/extensions/resource-deployment/src/ui/resourceDeploymentDialog.ts b/extensions/resource-deployment/src/ui/resourceDeploymentDialog.ts
index d880ec31e0..282ae9b040 100644
--- a/extensions/resource-deployment/src/ui/resourceDeploymentDialog.ts
+++ b/extensions/resource-deployment/src/ui/resourceDeploymentDialog.ts
@@ -6,26 +6,210 @@
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
+import { IResourceTypeService } from '../services/resourceTypeService';
+import * as vscode from 'vscode';
+import { ResourceType, DeploymentProvider, ToolInstallationStatus } from '../interfaces';
+import { IToolsService } from '../services/toolsService';
+import { INotebookService } from '../services/notebookService';
+
const localize = nls.loadMessageBundle();
export class ResourceDeploymentDialog {
- private dialogObject: azdata.window.Dialog;
+ private _selectedResourceType: ResourceType;
+ private _toDispose: vscode.Disposable[] = [];
+ private _dialogObject: azdata.window.Dialog;
+ private _resourceTypeCards: azdata.CardComponent[] = [];
+ private _view!: azdata.ModelView;
+ private _resourceDescriptionLabel!: azdata.TextComponent;
+ private _optionsContainer!: azdata.FlexContainer;
+ private _toolsTable!: azdata.TableComponent;
+ private _cardResourceTypeMap: Map = new Map();
+ private _optionDropDownMap: Map = new Map();
- constructor() {
- this.dialogObject = azdata.window.createModelViewDialog(localize('deploymentDialog.title', 'Install SQL Server'), 'resourceDeploymentDialog', true);
+ constructor(private context: vscode.ExtensionContext,
+ private notebookService: INotebookService,
+ private toolsService: IToolsService,
+ private resourceTypeService: IResourceTypeService,
+ resourceType: ResourceType) {
+ this._selectedResourceType = resourceType;
+ this._dialogObject = azdata.window.createModelViewDialog(localize('deploymentDialog.title', 'Select a configuration'), 'resourceDeploymentDialog', true);
+ this._dialogObject.cancelButton.onClick(() => this.onCancel());
+ this._dialogObject.okButton.label = localize('deploymentDialog.OKButtonText', 'Select');
+ this._dialogObject.okButton.onClick(() => this.onComplete());
}
private initializeDialog() {
let tab = azdata.window.createTab('');
tab.registerContent((view: azdata.ModelView) => {
- let text = view.modelBuilder.text().withProperties({ value: 'place holder' }).component();
- return view.initializeModel(text);
+ this._view = view;
+ this.resourceTypeService.getResourceTypes().forEach(resourceType => this.addCard(resourceType));
+ const cardsContainer = view.modelBuilder.flexContainer().withItems(this._resourceTypeCards, { flex: '0 0 auto', CSSStyles: { 'margin-bottom': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'left' }).component();
+ this._resourceDescriptionLabel = view.modelBuilder.text().withProperties({ value: this._selectedResourceType ? this._selectedResourceType.description : undefined }).component();
+ this._optionsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
+
+ const toolColumn: azdata.TableColumn = {
+ value: localize('deploymentDialog.toolNameColumnHeader', 'Tool'),
+ width: 100
+ };
+ const descriptionColumn: azdata.TableColumn = {
+ value: localize('deploymentDialog.toolDescriptionColumnHeader', 'Description'),
+ width: 500
+ };
+ const versionColumn: azdata.TableColumn = {
+ value: localize('deploymentDialog.toolVersionColumnHeader', 'Version'),
+ width: 200
+ };
+ const statusColumn: azdata.TableColumn = {
+ value: localize('deploymentDialog.toolStatusColumnHeader', 'Status'),
+ width: 200
+ };
+
+ this._toolsTable = view.modelBuilder.table().withProperties({
+ height: 150,
+ data: [],
+ columns: [toolColumn, descriptionColumn, versionColumn, statusColumn],
+ width: 1000
+ }).component();
+
+ const formBuilder = view.modelBuilder.formContainer().withFormItems(
+ [
+ {
+ component: cardsContainer,
+ title: ''
+ }, {
+ component: this._resourceDescriptionLabel,
+ title: ''
+ }, {
+ component: this._optionsContainer,
+ title: localize('deploymentDialog.OptionsTitle', 'Options')
+ }, {
+ component: this._toolsTable,
+ title: localize('deploymentDialog.RequiredToolsTitle', 'Required tools')
+ }
+ ],
+ {
+ horizontal: false
+ }
+ );
+
+ const form = formBuilder.withLayout({ width: '100%' }).component();
+
+ if (this._selectedResourceType) {
+ this.selectResourceType(this._selectedResourceType);
+ }
+
+ return view.initializeModel(form);
});
- this.dialogObject.content = [tab];
+ this._dialogObject.content = [tab];
}
public open(): void {
this.initializeDialog();
- azdata.window.openDialog(this.dialogObject);
+ azdata.window.openDialog(this._dialogObject);
+ }
+
+ private addCard(resourceType: ResourceType): void {
+ const card = this._view.modelBuilder.card().withProperties({
+ cardType: azdata.CardType.VerticalButton,
+ iconPath: {
+ dark: this.context.asAbsolutePath(resourceType.icon.dark),
+ light: this.context.asAbsolutePath(resourceType.icon.light)
+ },
+ label: resourceType.displayName,
+ selected: (this._selectedResourceType && this._selectedResourceType.name === resourceType.name)
+ }).component();
+
+ this._resourceTypeCards.push(card);
+ this._cardResourceTypeMap.set(resourceType.name, card);
+ this._toDispose.push(card.onCardSelectedChanged(() => this.selectResourceType(resourceType)));
+ }
+
+ private selectResourceType(resourceType: ResourceType): void {
+ this._selectedResourceType = resourceType;
+ const card = this._cardResourceTypeMap.get(this._selectedResourceType.name)!;
+ if (card.selected) {
+ // clear the selected state of the previously selected card
+ this._resourceTypeCards.forEach(c => {
+ if (c !== card) {
+ c.selected = false;
+ }
+ });
+ } else {
+ // keep the selected state if no other card is selected
+ if (this._resourceTypeCards.filter(c => { return c !== card && c.selected; }).length === 0) {
+ card.selected = true;
+ }
+ }
+
+ this._resourceDescriptionLabel.value = resourceType.description;
+ this._optionsContainer.clearItems();
+ this._optionDropDownMap.clear();
+ resourceType.options.forEach(option => {
+ const optionLabel = this._view.modelBuilder.text().withProperties({
+ value: option.displayName
+ }).component();
+ optionLabel.width = '150px';
+
+ const optionSelectBox = this._view.modelBuilder.dropDown().withProperties({
+ values: option.values,
+ value: option.values[0],
+ width: '300px'
+ }).component();
+
+ this._toDispose.push(optionSelectBox.onValueChanged(() => { this.updateTools(); }));
+ this._optionDropDownMap.set(option.name, optionSelectBox);
+ const row = this._view.modelBuilder.flexContainer().withItems([optionLabel, optionSelectBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '20px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
+ this._optionsContainer.addItem(row);
+ });
+ this.updateTools();
+ }
+
+ private updateTools(): void {
+ this.toolsService.getToolStatus(this.getCurrentProvider().requiredTools).then(toolStatus => {
+ let tableData = toolStatus.map(tool => {
+ return [tool.name, tool.description, tool.version, this.getToolStatusText(tool.status)];
+ });
+ this._toolsTable.data = tableData;
+ });
+ }
+
+ private getToolStatusText(status: ToolInstallationStatus): string {
+ switch (status) {
+ case ToolInstallationStatus.Installed:
+ return '✔️ ' + localize('deploymentDialog.InstalledText', 'Installed');
+ case ToolInstallationStatus.NotInstalled:
+ return '❌ ' + localize('deploymentDialog.NotInstalledText', 'Not Installed');
+ case ToolInstallationStatus.Installing:
+ return '⌛ ' + localize('deploymentDialog.InstallingText', 'Installing…');
+ case ToolInstallationStatus.FailedToInstall:
+ return '❌ ' + localize('deploymentDialog.FailedToInstallText', 'Install Failed');
+ default:
+ return 'unknown status';
+ }
+ }
+
+ private getCurrentProvider(): DeploymentProvider {
+ const options: { option: string, value: string }[] = [];
+
+ this._optionDropDownMap.forEach((selectBox, option) => {
+ let selectedValue: azdata.CategoryValue = selectBox.value as azdata.CategoryValue;
+ options.push({ option: option, value: selectedValue.name });
+ });
+
+ return this._selectedResourceType.getProvider(options)!;
+ }
+
+ private onCancel(): void {
+ this.dispose();
+ }
+
+ private onComplete(): void {
+ const provider = this.getCurrentProvider();
+ this.notebookService.launchNotebook(provider.notebook);
+ this.dispose();
+ }
+
+ private dispose(): void {
+ this._toDispose.forEach(disposable => disposable.dispose());
}
}
diff --git a/extensions/resource-deployment/yarn.lock b/extensions/resource-deployment/yarn.lock
index 45f8b9278d..f25cf91f7e 100644
--- a/extensions/resource-deployment/yarn.lock
+++ b/extensions/resource-deployment/yarn.lock
@@ -2,7 +2,629 @@
# yarn lockfile v1
+agent-base@4, agent-base@^4.1.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
+ integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==
+ dependencies:
+ es6-promisify "^5.0.0"
+
+ajv@^6.5.5:
+ version "6.10.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
+ integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==
+ dependencies:
+ fast-deep-equal "^2.0.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+asn1@~0.2.3:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+ integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
+ dependencies:
+ safer-buffer "~2.1.0"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+ integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+ integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+
+aws-sign2@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+ integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
+
+aws4@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
+ integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+ integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
+ integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
+ dependencies:
+ tweetnacl "^0.14.3"
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+browser-stdout@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f"
+ integrity sha1-81HTKWnTL6XXpVZxVCY9korjvR8=
+
+buffer-from@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+ integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+ integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
+
+circular-json@^0.3.1:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
+ integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==
+
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+ integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commander@2.11.0:
+ version "2.11.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
+ integrity sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+core-util-is@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+ integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
+ dependencies:
+ assert-plus "^1.0.0"
+
+debug@3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+ integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
+ dependencies:
+ ms "2.0.0"
+
+debug@^3.1.0:
+ version "3.2.6"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
+ integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+ dependencies:
+ ms "^2.1.1"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+ integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+
+diff@3.3.1:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75"
+ integrity sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==
+
+ecc-jsbn@~0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+ integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
+ dependencies:
+ jsbn "~0.1.0"
+ safer-buffer "^2.1.0"
+
+es6-promise@^4.0.3:
+ version "4.2.6"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f"
+ integrity sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==
+
+es6-promisify@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
+ integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
+ dependencies:
+ es6-promise "^4.0.3"
+
+escape-string-regexp@1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+ integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+extend@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+ integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+extsprintf@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+ integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
+
+extsprintf@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+ integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+
+fast-deep-equal@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
+ integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
+ integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I=
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+ integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+
+form-data@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+ integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.6"
+ mime-types "^2.1.12"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+ dependencies:
+ assert-plus "^1.0.0"
+
+glob@7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+ integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@^7.1.2:
+ version "7.1.4"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
+ integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+growl@1.10.3:
+ version "1.10.3"
+ resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f"
+ integrity sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==
+
+har-schema@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+ integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
+
+har-validator@~5.1.0:
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+ integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+ dependencies:
+ ajv "^6.5.5"
+ har-schema "^2.0.0"
+
+has-flag@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
+ integrity sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=
+
+he@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
+ integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0=
+
+http-proxy-agent@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405"
+ integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==
+ dependencies:
+ agent-base "4"
+ debug "3.1.0"
+
+http-signature@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+ integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+ dependencies:
+ assert-plus "^1.0.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+https-proxy-agent@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
+ integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==
+ dependencies:
+ agent-base "^4.1.0"
+ debug "^3.1.0"
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+ integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+ integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+ integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+ integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+ integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+ integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
+ json-schema "0.2.3"
+ verror "1.10.0"
+
+lodash@^4.17.4:
+ version "4.17.11"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
+ integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
+
+mime-db@1.40.0:
+ version "1.40.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
+ integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==
+
+mime-types@^2.1.12, mime-types@~2.1.19:
+ version "2.1.24"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81"
+ integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==
+ dependencies:
+ mime-db "1.40.0"
+
+minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+ integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
+
+mkdirp@0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
+ dependencies:
+ minimist "0.0.8"
+
+mocha@^4.0.1:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.1.0.tgz#7d86cfbcf35cb829e2754c32e17355ec05338794"
+ integrity sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==
+ dependencies:
+ browser-stdout "1.3.0"
+ commander "2.11.0"
+ debug "3.1.0"
+ diff "3.3.1"
+ escape-string-regexp "1.0.5"
+ glob "7.1.2"
+ growl "1.10.3"
+ he "1.1.1"
+ mkdirp "0.5.1"
+ supports-color "4.4.0"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+ integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+ integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
+
+oauth-sign@~0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+ integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
+
+once@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+ dependencies:
+ wrappy "1"
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+ integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+ integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+
+postinstall-build@^5.0.1:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7"
+ integrity sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg==
+
+psl@^1.1.24:
+ version "1.1.31"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
+ integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==
+
+punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+ integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
+
+punycode@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+ integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+qs@~6.5.2:
+ version "6.5.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+ integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+
+querystringify@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e"
+ integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==
+
+request@^2.88.0:
+ version "2.88.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
+ integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.8.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.6"
+ extend "~3.0.2"
+ forever-agent "~0.6.1"
+ form-data "~2.3.2"
+ har-validator "~5.1.0"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.19"
+ oauth-sign "~0.9.0"
+ performance-now "^2.1.0"
+ qs "~6.5.2"
+ safe-buffer "^5.1.2"
+ tough-cookie "~2.4.3"
+ tunnel-agent "^0.6.0"
+ uuid "^3.3.2"
+
+requires-port@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+ integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+semver@^5.4.1:
+ version "5.7.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
+ integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==
+
+source-map-support@^0.5.0:
+ version "0.5.12"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599"
+ integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map@^0.6.0:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+sshpk@^1.7.0:
+ version "1.16.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
+ integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ bcrypt-pbkdf "^1.0.0"
+ dashdash "^1.12.0"
+ ecc-jsbn "~0.1.1"
+ getpass "^0.1.1"
+ jsbn "~0.1.0"
+ safer-buffer "^2.0.2"
+ tweetnacl "~0.14.0"
+
+supports-color@4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e"
+ integrity sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==
+ dependencies:
+ has-flag "^2.0.0"
+
+tough-cookie@~2.4.3:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
+ integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==
+ dependencies:
+ psl "^1.1.24"
+ punycode "^1.4.1"
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+ integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
+
+typemoq@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8"
+ integrity sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==
+ dependencies:
+ circular-json "^0.3.1"
+ lodash "^4.17.4"
+ postinstall-build "^5.0.1"
+
+uri-js@^4.2.2:
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+ integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
+ dependencies:
+ punycode "^2.1.0"
+
+url-parse@^1.4.4:
+ version "1.4.7"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278"
+ integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==
+ dependencies:
+ querystringify "^2.1.1"
+ requires-port "^1.0.0"
+
+uuid@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
+ integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
+
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
+ dependencies:
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
+
vscode-nls@^3.2.1:
version "3.2.5"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4"
integrity sha512-ITtoh3V4AkWXMmp3TB97vsMaHRgHhsSFPsUdzlueSL+dRZbSNTZeOmdQv60kjCV306ghPxhDeoNUEm3+EZMuyw==
+
+vscode-test@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/vscode-test/-/vscode-test-0.4.1.tgz#5e2387dbc303544c932092469e6bbf42204bfab3"
+ integrity sha512-uIi/07uG/gmCbD9Y9bFpNzmk4el82xiclijEdL426A3jOFfvwdqgfmtuWYfxEGo0w6JY9EqVDTGQCXwuInXVTQ==
+ dependencies:
+ http-proxy-agent "^2.1.0"
+ https-proxy-agent "^2.2.1"
+
+vscode@^1.1.26:
+ version "1.1.34"
+ resolved "https://registry.yarnpkg.com/vscode/-/vscode-1.1.34.tgz#3aba5d2f3a9d43f4e798f6933339fe5fcfb782c6"
+ integrity sha512-GuT3tCT2N5Qp26VG4C+iGmWMgg/MuqtY5G5TSOT3U/X6pgjM9LFulJEeqpyf6gdzpI4VyU3ZN/lWPo54UFPuQg==
+ dependencies:
+ glob "^7.1.2"
+ mocha "^4.0.1"
+ request "^2.88.0"
+ semver "^5.4.1"
+ source-map-support "^0.5.0"
+ url-parse "^1.4.4"
+ vscode-test "^0.4.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
diff --git a/scripts/test-extensions-unit.bat b/scripts/test-extensions-unit.bat
index 2571a33b4e..8952929e0d 100644
--- a/scripts/test-extensions-unit.bat
+++ b/scripts/test-extensions-unit.bat
@@ -15,6 +15,7 @@ call .\scripts\code.bat --extensionDevelopmentPath=%~dp0\..\extensions\agent --e
call .\scripts\code.bat --extensionDevelopmentPath=%~dp0\..\extensions\cms --extensionTestsPath=%~dp0\..\extensions\cms\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222
call .\scripts\code.bat --extensionDevelopmentPath=%~dp0\..\extensions\dacpac --extensionTestsPath=%~dp0\..\extensions\dacpac\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222
call .\scripts\code.bat --extensionDevelopmentPath=%~dp0\..\extensions\notebook --extensionTestsPath=%~dp0\..\extensions\notebook\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222
+call .\scripts\code.bat --extensionDevelopmentPath=%~dp0\..\extensions\resource-deployment --extensionTestsPath=%~dp0\..\extensions\resource-deployment\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222
if %errorlevel% neq 0 exit /b %errorlevel%
diff --git a/scripts/test-extensions-unit.sh b/scripts/test-extensions-unit.sh
index e3f01e8ece..b080f05b9c 100644
--- a/scripts/test-extensions-unit.sh
+++ b/scripts/test-extensions-unit.sh
@@ -23,6 +23,7 @@ echo $VSCODEEXTDIR
./scripts/code.sh --extensionDevelopmentPath=$ROOT/extensions/cms --extensionTestsPath=$ROOT/extensions/cms/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR
./scripts/code.sh --extensionDevelopmentPath=$ROOT/extensions/dacpac --extensionTestsPath=$ROOT/extensions/dacpac/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR
./scripts/code.sh --extensionDevelopmentPath=$ROOT/extensions/notebook --extensionTestsPath=$ROOT/extensions/notebook/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR
+./scripts/code.sh --extensionDevelopmentPath=$ROOT/extensions/resource-deployment --extensionTestsPath=$ROOT/extensions/resource-deployment/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR
rm -r $VSCODEUSERDATADIR
rm -r $VSCODEEXTDIR