diff --git a/extensions/resource-deployment/package.json b/extensions/resource-deployment/package.json index 681d236afc..479ddc9f20 100644 --- a/extensions/resource-deployment/package.json +++ b/extensions/resource-deployment/package.json @@ -40,12 +40,17 @@ "command": "azdata.resource.deploy", "title": "%deploy-resource-command-name%", "category": "%deploy-resource-command-category%" + }, + { + "command": "azdata.openNotebookInputDialog", + "title": "Open notebook input dialog", + "category": "Notebook" } ], "menus": { "commandPalette": [ { - "command": "azdata.resource.deploy", + "command": "azdata.openNotebookInputDialog", "when": "false" } ], @@ -57,6 +62,10 @@ { "command": "azdata.resource.sql-bdc.deploy", "group": "secondary" + }, + { + "command": "azdata.resource.deploy", + "group": "secondary" } ] } @@ -94,6 +103,7 @@ "providers": [ { "dialog": { + "notebook": "%sql-2017-docker-notebook%", "title": "%docker-sql-2017-title%", "name": "docker-sql-2017-dialog", "tabs": [ @@ -135,7 +145,6 @@ } ] }, - "notebook": "%sql-2017-docker-notebook%", "requiredTools": [ { "name": "docker" @@ -145,6 +154,7 @@ }, { "dialog": { + "notebook": "%sql-2019-docker-notebook%", "title": "%docker-sql-2019-title%", "name": "docker-sql-2019-dialog", "tabs": [ @@ -186,7 +196,6 @@ } ] }, - "notebook": "%sql-2019-docker-notebook%", "requiredTools": [ { "name": "docker" @@ -242,6 +251,7 @@ "providers": [ { "dialog": { + "notebook": "%bdc-2019-aks-notebook%", "title": "%bdc-new-aks-dialog-title%", "name": "bdc-new-aks-dialog", "tabs": [ @@ -331,7 +341,6 @@ } ] }, - "notebook": "%bdc-2019-aks-notebook%", "requiredTools": [ { "name": "kubectl" @@ -347,6 +356,7 @@ }, { "dialog": { + "notebook": "%bdc-2019-existing-aks-notebook%", "title": "%bdc-existing-aks-dialog-title%", "name": "bdc-existing-aks-dialog", "tabs": [ @@ -386,7 +396,6 @@ } ] }, - "notebook": "%bdc-2019-existing-aks-notebook%", "requiredTools": [ { "name": "kubectl" @@ -401,6 +410,7 @@ "dialog": { "title": "%bdc-existing-kubeadm-dialog-title%", "name": "bdc-existing-kubeadm-dialog", + "notebook": "%bdc-2019-existing-kubeadm-notebook%", "tabs": [ { "title": "", @@ -461,7 +471,6 @@ } ] }, - "notebook": "%bdc-2019-existing-kubeadm-notebook%", "requiredTools": [ { "name": "kubectl" @@ -473,6 +482,46 @@ "when": "target=existing-kubeadm&&version=bdc2019" } ] + }, + { + "name": "sql-windows-setup", + "displayName": "%resource-type-sql-windows-setup-display-name%", + "description": "%resource-type-sql-windows-setup-description%", + "platforms": [ + "win32" + ], + "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": [ + { + "downloadUrl": "https://go.microsoft.com/fwlink/?linkid=853016", + "requiredTools": [], + "when": "version=sql2017" + }, + { + "webPageUrl": "https://www.microsoft.com/evalcenter/evaluate-sql-server-2019-ctp", + "requiredTools": [], + "when": "version=sql2019" + } + ] } ], "dependencies": { diff --git a/extensions/resource-deployment/package.nls.json b/extensions/resource-deployment/package.nls.json index f40ba304da..4d2e814026 100644 --- a/extensions/resource-deployment/package.nls.json +++ b/extensions/resource-deployment/package.nls.json @@ -3,7 +3,7 @@ "extension-description": "Provides a notebook-based experience to deploy Microsoft SQL Server", "deploy-sql-image-command-name": "Deploy SQL Server on Docker…", "deploy-sql-bdc-command-name": "Deploy SQL Server big data cluster…", - "deploy-resource-command-name": "Deploy SQL Server…", + "deploy-resource-command-name": "Open SQL Server deployment dialog", "deploy-resource-command-category": "Deployment", "resource-type-sql-image-display-name": "SQL Server container image", "resource-type-sql-image-description": "Run SQL Server container image with Docker", @@ -11,10 +11,10 @@ "resource-type-sql-bdc-description": "SQL Server big data cluster allows you to deploy scalable clusters of SQL Server, Spark, and HDFS containers running on Kubernetes", "version-display-name": "Version", "sql-2017-display-name": "SQL Server 2017", - "sql-2019-display-name": "SQL Server 2019 CTP 3.2", + "sql-2019-display-name": "SQL Server 2019 CTP", "sql-2017-docker-notebook": "./notebooks/docker/2017/deploy-sql2017-image.ipynb", "sql-2019-docker-notebook": "./notebooks/docker/2019/deploy-sql2019-image.ipynb", - "bdc-2019-display-name": "SQL Server 2019 CTP 3.2 big data cluster", + "bdc-2019-display-name": "SQL Server 2019 CTP big data cluster", "bdc-deployment-target": "Deployment target", "bdc-deployment-target-aks": "New Azure Kubernetes Service Cluster", "bdc-deployment-target-existing-aks": "Existing Azure Kubernetes Service Cluster", @@ -46,5 +46,7 @@ "bdc-existing-kubeadm-dialog-title": "Deployment target: existing Kubernetes cluster (kubeadm)", "bdc-storage-class-field": "Storage class name", "bdc-data-size-field": "Capacity for data (GB)", - "bdc-log-size-field": "Capacity for logs (GB)" + "bdc-log-size-field": "Capacity for logs (GB)", + "resource-type-sql-windows-setup-display-name": "SQL Server on Windows", + "resource-type-sql-windows-setup-description": "Run SQL Server on Windows, select a version to get started." } diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts index 5a37c40b64..d0fbc58c88 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -30,11 +30,14 @@ export interface DeploymentProvider { title: string; dialog: DialogInfo; notebook: string | NotebookInfo; + downloadUrl: string; + webPageUrl: string; requiredTools: ToolRequirementInfo[]; when: string; } export interface DialogInfo { + notebook: string | NotebookInfo; title: string; name: string; tabs: DialogTabInfo[]; diff --git a/extensions/resource-deployment/src/main.ts b/extensions/resource-deployment/src/main.ts index 70a5a09dc0..36f7f0ff6f 100644 --- a/extensions/resource-deployment/src/main.ts +++ b/extensions/resource-deployment/src/main.ts @@ -4,13 +4,15 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import vscode = require('vscode'); -import { ResourceTypePickerDialog } 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 vscode from 'vscode'; import * as nls from 'vscode-nls'; +import { DialogInfo } from './interfaces'; +import { NotebookService } from './services/notebookService'; +import { PlatformService } from './services/platformService'; +import { ResourceTypeService } from './services/resourceTypeService'; +import { ToolsService } from './services/toolsService'; +import { NotebookInputDialog } from './ui/notebookInputDialog'; +import { ResourceTypePickerDialog } from './ui/resourceTypePickerDialog'; const localize = nls.loadMessageBundle(); @@ -18,8 +20,7 @@ export function activate(context: vscode.ExtensionContext) { const platformService = new PlatformService(); const toolsService = new ToolsService(); const notebookService = new NotebookService(platformService, context.extensionPath); - const resourceTypeService = new ResourceTypeService(platformService, toolsService); - + const resourceTypeService = new ResourceTypeService(platformService, toolsService, notebookService); const resourceTypes = resourceTypeService.getResourceTypes(); const validationFailures = resourceTypeService.validateResourceTypes(resourceTypes); if (validationFailures.length !== 0) { @@ -33,7 +34,7 @@ export function activate(context: vscode.ExtensionContext) { if (filtered.length !== 1) { vscode.window.showErrorMessage(localize('resourceDeployment.UnknownResourceType', 'The resource type: {0} is not defined', resourceTypeName)); } else { - const dialog = new ResourceTypePickerDialog(context, notebookService, toolsService, resourceTypeService, filtered[0]); + const dialog = new ResourceTypePickerDialog(context, toolsService, resourceTypeService, filtered[0]); dialog.open(); } }; @@ -47,6 +48,10 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('azdata.resource.deploy', () => { openDialog('sql-bdc'); }); + vscode.commands.registerCommand('azdata.openNotebookInputDialog', (dialogInfo: DialogInfo) => { + const dialog = new NotebookInputDialog(notebookService, dialogInfo); + dialog.open(); + }); } // this method is called when your extension is deactivated diff --git a/extensions/resource-deployment/src/services/notebookService.ts b/extensions/resource-deployment/src/services/notebookService.ts index 1c8af8c514..246ffda4a9 100644 --- a/extensions/resource-deployment/src/services/notebookService.ts +++ b/extensions/resource-deployment/src/services/notebookService.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import * as vscode from 'vscode'; import * as azdata from 'azdata'; -import { NotebookInfo } from '../interfaces'; -import { isString } from 'util'; import * as path from 'path'; +import { isString } from 'util'; +import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { IPlatformService } from './platformService'; +import { NotebookInfo } from '../interfaces'; const localize = nls.loadMessageBundle(); export interface INotebookService { @@ -26,13 +26,16 @@ export class NotebookService implements INotebookService { * @param notebook the path of the notebook */ launchNotebook(notebook: string | NotebookInfo): void { - const notebookRelativePath = this.getNotebook(notebook); - const notebookFullPath = path.join(this.extensionPath, notebookRelativePath); - if (notebookRelativePath && this.platformService.fileExists(notebookFullPath)) { + const notebookPath = this.getNotebook(notebook); + const notebookFullPath = path.join(this.extensionPath, notebookPath); + if (notebookPath && this.platformService.fileExists(notebookPath)) { + this.showNotebookAsUntitled(notebookPath); + } + else if (notebookPath && this.platformService.fileExists(notebookFullPath)) { this.showNotebookAsUntitled(notebookFullPath); } else { - this.platformService.showErrorMessage(localize('resourceDeployment.notebookNotFound', 'The notebook {0} does not exist', notebookFullPath)); + this.platformService.showErrorMessage(localize('resourceDeployment.notebookNotFound', "The notebook {0} does not exist", notebookPath)); } } diff --git a/extensions/resource-deployment/src/services/resourceTypeService.ts b/extensions/resource-deployment/src/services/resourceTypeService.ts index d0d3d501cf..4c363c6d42 100644 --- a/extensions/resource-deployment/src/services/resourceTypeService.ts +++ b/extensions/resource-deployment/src/services/resourceTypeService.ts @@ -4,22 +4,31 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { ResourceType, ResourceTypeOption, DeploymentProvider } from '../interfaces'; -import { IToolsService } from './toolsService'; +import * as azdata from 'azdata'; +import * as cp from 'child_process'; +import * as fs from 'fs'; +import * as https from 'https'; +import * as os from 'os'; +import * as path from 'path'; import * as vscode from 'vscode'; -import { IPlatformService } from './platformService'; import * as nls from 'vscode-nls'; +import { INotebookService } from './notebookService'; +import { IPlatformService } from './platformService'; +import { IToolsService } from './toolsService'; +import { ResourceType, ResourceTypeOption, DeploymentProvider } from '../interfaces'; +import { NotebookInputDialog } from '../ui/notebookInputDialog'; const localize = nls.loadMessageBundle(); export interface IResourceTypeService { getResourceTypes(filterByPlatform?: boolean): ResourceType[]; validateResourceTypes(resourceTypes: ResourceType[]): string[]; + startDeployment(provider: DeploymentProvider): void; } export class ResourceTypeService implements IResourceTypeService { private _resourceTypes: ResourceType[] = []; - constructor(private platformService: IPlatformService, private toolsService: IToolsService) { } + constructor(private platformService: IPlatformService, private toolsService: IToolsService, private notebookService: INotebookService) { } /** * Get the supported resource types @@ -129,8 +138,8 @@ export class ResourceTypeService implements IResourceTypeService { 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.dialog && !provider.notebook && !provider.downloadUrl && !provider.webPageUrl) { + errorMessages.push(`No deployment method defined for the provider, ${providerPositionInfo}`); } if (provider.requiredTools && provider.requiredTools.length > 0) { @@ -183,4 +192,79 @@ export class ResourceTypeService implements IResourceTypeService { } return undefined; } + + public startDeployment(provider: DeploymentProvider): void { + const self = this; + if (provider.dialog) { + const dialog = new NotebookInputDialog(this.notebookService, provider.dialog); + dialog.open(); + } else if (provider.notebook) { + this.notebookService.launchNotebook(provider.notebook); + } else if (provider.downloadUrl) { + const taskName = localize('resourceDeployment.DownloadAndLaunchTaskName', "Download and launch installer, URL: {0}", provider.downloadUrl); + azdata.tasks.startBackgroundOperation({ + displayName: taskName, + description: taskName, + isCancelable: false, + operation: op => { + op.updateStatus(azdata.TaskStatus.InProgress, localize('resourceDeployment.DownloadingText', "Downloading from: {0}", provider.downloadUrl)); + self.download(provider.downloadUrl).then((downloadedFile) => { + op.updateStatus(azdata.TaskStatus.InProgress, localize('resourceDeployment.DownloadCompleteText', "Successfully downloaded: {0}", downloadedFile)); + op.updateStatus(azdata.TaskStatus.InProgress, localize('resourceDeployment.LaunchingProgramText', "Launching: {0}", downloadedFile)); + cp.exec(downloadedFile); + op.updateStatus(azdata.TaskStatus.Succeeded, localize('resourceDeployment.ProgramLaunchedText', "Successfully launched: {0}", downloadedFile)); + }, (error) => { + op.updateStatus(azdata.TaskStatus.Failed, error); + }); + } + }); + } else if (provider.webPageUrl) { + vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(provider.webPageUrl)); + } + } + + private download(url: string): Promise { + const self = this; + const promise = new Promise((resolve, reject) => { + https.get(url, function (response) { + console.log('Download installer from: ' + url); + if (response.statusCode === 301 || response.statusCode === 302) { + // Redirect and download from new location + console.log('Redirecting the download to: ' + response.headers.location); + self.download(response.headers.location!).then((result) => { + resolve(result); + }, (err) => { + reject(err); + }); + return; + } + if (response.statusCode !== 200) { + reject(localize('downloadError', "Download failed, status code: {0}, message: {1}", response.statusCode, response.statusMessage)); + return; + } + const extension = path.extname(url); + const originalFileName = path.basename(url, extension); + let fileName = originalFileName; + const downloadFolder = os.homedir(); + let cnt = 1; + while (fs.existsSync(path.join(downloadFolder, fileName + extension))) { + fileName = `${originalFileName}-${cnt}`; + cnt++; + } + fileName = path.join(downloadFolder, fileName + extension); + const file = fs.createWriteStream(fileName); + response.pipe(file); + file.on('finish', () => { + file.close(); + resolve(fileName); + }); + file.on('error', (err) => { + fs.unlink(fileName, () => { }); + reject(err.message); + }); + }); + }); + return promise; + } + } diff --git a/extensions/resource-deployment/src/test/resourceTypeService.test.ts b/extensions/resource-deployment/src/test/resourceTypeService.test.ts index b380660754..03c406da55 100644 --- a/extensions/resource-deployment/src/test/resourceTypeService.test.ts +++ b/extensions/resource-deployment/src/test/resourceTypeService.test.ts @@ -12,13 +12,15 @@ import { EOL } from 'os'; import { ResourceTypeService } from '../services/resourceTypeService'; import { IPlatformService } from '../services/platformService'; import { ToolsService } from '../services/toolsService'; +import { NotebookService } from '../services/notebookService'; suite('Resource Type Service Tests', function (): void { test('test resource types', () => { const mockPlatformService = TypeMoq.Mock.ofType(); const toolsService = new ToolsService(); - const resourceTypeService = new ResourceTypeService(mockPlatformService.object, toolsService); + const notebookService = new NotebookService(mockPlatformService.object, ''); + const resourceTypeService = new ResourceTypeService(mockPlatformService.object, toolsService, notebookService); // index 0: platform name, index 1: number of expected resource types const platforms: { platform: string; resourceTypeCount: number }[] = [ { platform: 'win32', resourceTypeCount: 2 }, @@ -39,4 +41,4 @@ suite('Resource Type Service Tests', function (): void { const validationErrors = resourceTypeService.validateResourceTypes(allResourceTypes); assert(validationErrors.length === 0, `Validation errors detected in the package.json: ${validationErrors.join(EOL)}.`); }); -}); \ No newline at end of file +}); diff --git a/extensions/resource-deployment/src/ui/dialogBase.ts b/extensions/resource-deployment/src/ui/dialogBase.ts index ad10a6f790..0a31dd9dc0 100644 --- a/extensions/resource-deployment/src/ui/dialogBase.ts +++ b/extensions/resource-deployment/src/ui/dialogBase.ts @@ -11,7 +11,7 @@ export abstract class DialogBase { protected _toDispose: vscode.Disposable[] = []; protected _dialogObject: azdata.window.Dialog; - constructor(protected extensionContext: vscode.ExtensionContext, dialogTitle: string, dialogName: string, isWide: boolean = false) { + constructor(dialogTitle: string, dialogName: string, isWide: boolean = false) { this._dialogObject = azdata.window.createModelViewDialog(dialogTitle, dialogName, isWide); this._dialogObject.cancelButton.onClick(() => this.onCancel()); } diff --git a/extensions/resource-deployment/src/ui/deploymentDialog.ts b/extensions/resource-deployment/src/ui/notebookInputDialog.ts similarity index 94% rename from extensions/resource-deployment/src/ui/deploymentDialog.ts rename to extensions/resource-deployment/src/ui/notebookInputDialog.ts index 34b3ef5876..3bbf052cb4 100644 --- a/extensions/resource-deployment/src/ui/deploymentDialog.ts +++ b/extensions/resource-deployment/src/ui/notebookInputDialog.ts @@ -6,29 +6,27 @@ import * as azdata from 'azdata'; import * as nls from 'vscode-nls'; -import * as vscode from 'vscode'; import { DialogBase } from './dialogBase'; import { INotebookService } from '../services/notebookService'; -import { DeploymentProvider, DialogFieldInfo, FieldType } from '../interfaces'; +import { DialogFieldInfo, FieldType, DialogInfo } from '../interfaces'; const localize = nls.loadMessageBundle(); -export class DeploymentDialog extends DialogBase { +export class NotebookInputDialog extends DialogBase { private variables: { [s: string]: string | undefined; } = {}; private validators: (() => { valid: boolean, message: string })[] = []; - constructor(context: vscode.ExtensionContext, - private notebookService: INotebookService, - private deploymentProvider: DeploymentProvider) { - super(context, deploymentProvider.dialog.title, deploymentProvider.dialog.name, false); + constructor(private notebookService: INotebookService, + private dialogInfo: DialogInfo) { + super(dialogInfo.title, dialogInfo.name, false); this._dialogObject.okButton.label = localize('deploymentDialog.OKButtonText', 'Open Notebook'); this._dialogObject.okButton.onClick(() => this.onComplete()); } protected initializeDialog() { const tabs: azdata.window.DialogTab[] = []; - this.deploymentProvider.dialog.tabs.forEach(tabInfo => { + this.dialogInfo.tabs.forEach(tabInfo => { const tab = azdata.window.createTab(tabInfo.title); tab.registerContent((view: azdata.ModelView) => { const sections: azdata.FormComponentGroup[] = []; @@ -191,7 +189,7 @@ export class DeploymentDialog extends DialogBase { Object.keys(this.variables).forEach(key => { process.env[key] = this.variables[key]; }); - this.notebookService.launchNotebook(this.deploymentProvider.notebook); + this.notebookService.launchNotebook(this.dialogInfo.notebook); this.dispose(); } diff --git a/extensions/resource-deployment/src/ui/resourceDeploymentDialog.ts b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts similarity index 89% rename from extensions/resource-deployment/src/ui/resourceDeploymentDialog.ts rename to extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts index 575fef2472..63a0a98f00 100644 --- a/extensions/resource-deployment/src/ui/resourceDeploymentDialog.ts +++ b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts @@ -5,14 +5,12 @@ 'use strict'; import * as azdata from 'azdata'; -import * as nls from 'vscode-nls'; -import { IResourceTypeService } from '../services/resourceTypeService'; import * as vscode from 'vscode'; -import { ResourceType, DeploymentProvider } from '../interfaces'; -import { IToolsService } from '../services/toolsService'; -import { INotebookService } from '../services/notebookService'; +import * as nls from 'vscode-nls'; import { DialogBase } from './dialogBase'; -import { DeploymentDialog } from './deploymentDialog'; +import { ResourceType, DeploymentProvider } from '../interfaces'; +import { IResourceTypeService } from '../services/resourceTypeService'; +import { IToolsService } from '../services/toolsService'; const localize = nls.loadMessageBundle(); @@ -26,12 +24,11 @@ export class ResourceTypePickerDialog extends DialogBase { private _cardResourceTypeMap: Map = new Map(); private _optionDropDownMap: Map = new Map(); - constructor(context: vscode.ExtensionContext, - private notebookService: INotebookService, + constructor(private extensionContext: vscode.ExtensionContext, private toolsService: IToolsService, private resourceTypeService: IResourceTypeService, resourceType: ResourceType) { - super(context, localize('resourceTypePickerDialog.title', "Select the deployment options"), 'ResourceTypePickerDialog', true); + super(localize('resourceTypePickerDialog.title', "Select the deployment options"), 'ResourceTypePickerDialog', true); this._selectedResourceType = resourceType; this._dialogObject.okButton.label = localize('deploymentDialog.OKButtonText', 'Select'); this._dialogObject.okButton.onClick(() => this.onComplete()); @@ -156,11 +153,15 @@ export class ResourceTypePickerDialog extends DialogBase { private updateTools(): void { const tools = this.getCurrentProvider().requiredTools; const headerRowHeight = 28; - this._toolsTable.height = 25 * tools.length + headerRowHeight; - this._toolsTable.data = tools.map(toolRef => { - const tool = this.toolsService.getToolByName(toolRef.name)!; - return [tool.displayName, tool.description]; - }); + this._toolsTable.height = 25 * Math.max(tools.length, 1) + headerRowHeight; + if (tools.length === 0) { + this._toolsTable.data = [[localize('deploymentDialog.NoRequiredTool', "No tools required"), '']]; + } else { + this._toolsTable.data = tools.map(toolRef => { + const tool = this.toolsService.getToolByName(toolRef.name)!; + return [tool.displayName, tool.description]; + }); + } } private getCurrentProvider(): DeploymentProvider { @@ -175,13 +176,7 @@ export class ResourceTypePickerDialog extends DialogBase { } private onComplete(): void { - const provider = this.getCurrentProvider(); - if (provider.dialog) { - const dialog = new DeploymentDialog(this.extensionContext, this.notebookService, provider); - dialog.open(); - } else { - this.notebookService.launchNotebook(provider.notebook); - } + this.resourceTypeService.startDeployment(this.getCurrentProvider()); this.dispose(); } }