resource deployment ext implementation -wip (#5508)

* resource types

* implement the dialog

* remove unused method

* fix issues

* formatting

* 5-17

* address comments and more tests
This commit is contained in:
Alan Ren
2019-05-17 20:24:02 -07:00
committed by GitHub
parent a59d1d3c05
commit 586fe10525
36 changed files with 2208 additions and 21 deletions

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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<void> {
throw new Error('Method not implemented.');
}
getInstallationStatus(versionExpression: string): Thenable<ToolInstallationStatus> {
let promise = new Promise<ToolInstallationStatus>(resolve => {
setTimeout(() => {
resolve(ToolInstallationStatus.Installed);
}, 500);
});
return promise;
}
}

View File

@@ -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<void> {
throw new Error('Method not implemented.');
}
getInstallationStatus(versionExpression: string): Thenable<ToolInstallationStatus> {
let promise = new Promise<ToolInstallationStatus>(resolve => {
setTimeout(() => {
resolve(ToolInstallationStatus.Installed);
}, 500);
});
return promise;
}
}

View File

@@ -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<void> {
throw new Error('Method not implemented.');
}
getInstallationStatus(versionExpression: string): Thenable<ToolInstallationStatus> {
let promise = new Promise<ToolInstallationStatus>(resolve => {
setTimeout(() => {
resolve(ToolInstallationStatus.Installed);
}, 500);
});
return promise;
}
}

View File

@@ -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<boolean> {
let promise = new Promise<boolean>(resolve => {
setTimeout(() => {
resolve(true);
}, 500);
});
return promise;
}
get supportAutoInstall(): boolean {
return true;
}
install(version: string): Thenable<void> {
throw new Error('Method not implemented.');
}
getInstallationStatus(versionExpression: string): Thenable<ToolInstallationStatus> {
let promise = new Promise<ToolInstallationStatus>(resolve => {
setTimeout(() => {
resolve(ToolInstallationStatus.Installed);
}, 500);
});
return promise;
}
}

View File

@@ -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<void> {
throw new Error('Method not implemented.');
}
getInstallationStatus(versionExpression: string): Thenable<ToolInstallationStatus> {
let promise = new Promise<ToolInstallationStatus>(resolve => {
setTimeout(() => {
resolve(ToolInstallationStatus.Installed);
}, 500);
});
return promise;
}
}

View File

@@ -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<ToolStatusInfo[]>;
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<ToolStatusInfo[]> {
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(<ToolStatusInfo>{
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;
}
}