Initial job and pool template support (#1)

Authoring, validation and deployment for job and pool templates.
This commit is contained in:
itowlson 2017-06-22 10:16:26 +12:00 коммит произвёл GitHub
Родитель eae453dd06
Коммит 3e748d8212
31 изменённых файлов: 1919 добавлений и 0 удалений

2
.gitignore поставляемый
Просмотреть файл

@ -1,3 +1,5 @@
out
# Logs
logs
*.log

28
.vscode/launch.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,28 @@
// A launch configuration that compiles the extension and then opens it inside a new window
{
"version": "0.1.0",
"configurations": [
{
"name": "Launch Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": [ "${workspaceRoot}/out/src/**/*.js" ],
"preLaunchTask": "npm"
},
{
"name": "Launch Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ],
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": [ "${workspaceRoot}/out/test/**/*.js" ],
"preLaunchTask": "npm"
}
]
}

9
.vscode/settings.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,9 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"out": false // set this to true to hide the "out" folder with the compiled JS files
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
}
}

30
.vscode/tasks.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,30 @@
// Available variables which can be used inside of strings.
// ${workspaceRoot}: the root folder of the team
// ${file}: the current opened file
// ${fileBasename}: the current opened file's basename
// ${fileDirname}: the current opened file's dirname
// ${fileExtname}: the current opened file's extension
// ${cwd}: the current working directory of the spawned process
// A task runner that calls a custom npm script that compiles the extension.
{
"version": "0.1.0",
// we want to run npm
"command": "npm",
// the command is a shell script
"isShellCommand": true,
// show the output window only if unrecognized errors occur.
"showOutput": "silent",
// we run the custom script "compile" as defined in package.json
"args": ["run", "compile", "--loglevel", "silent"],
// The tsc compiler is started in watching mode
"isBackground": true,
// use the standard tsc in watch mode problem matcher to find compile problems in the output.
"problemMatcher": "$tsc-watch"
}

9
.vscodeignore Normal file
Просмотреть файл

@ -0,0 +1,9 @@
.vscode/**
.vscode-test/**
out/test/**
test/**
src/**
**/*.map
.gitignore
tsconfig.json
vsc-extension-quickstart.md

Просмотреть файл

@ -1,3 +1,51 @@
# Azure Batch Tools for Visual Studio Code
A [Visual Studio Code](https://code.visualstudio.com/) extension for working with [Azure Batch](https://azure.microsoft.com/services/batch/).
## Status
This is early-stage work in progress. The happy paths should work okay, but the error paths haven't really been tested, and are likely to produce mediocre error messages at best! And it hasn't yet been tested at all on Mac or Linux. Please do raise issues for anything which is missing (plenty of that!), broken or unpolished.
## Running the Extension
This isn't yet published in the VS Code marketplace. To run it yourself:
* Clone the git repo.
* Run `npm install` in the working copy root.
* Open the folder in VS Code (`code .`).
* Hit F5 to run the extension in the Extension Development Host.
The extension uses VS Code 1.13 (May 2017) features so you will need that version or above.
# Prerequisites
This extension depends on the [Azure CLI 2.0](https://docs.microsoft.com/en-us/cli/azure/overview) and the [Azure Batch CLI extensions](https://github.com/Azure/azure-batch-cli-extensions). Before running this VS Code extension, you must install both of these, *and* must log in to Azure (`az login`) and to a Batch account (`az batch account login`). (At the moment, the VS Code extension doesn't provide any interactive support for installation or login, though you can run the login commands through the VS Code Terminal window. If you install the CLI but not the Batch extensions, you may get weird errors!)
**Important:** The 'convenient' ways of installing Azure CLI 2.0 (e.g. the Windows MSI) _will not allow you to install the Batch Extensions_. You need the `az component update` command to install the extensions. You _should_ be able to use `pip install azure-cli` to install the CLI with component update support (if it doesn't work, please let the Azure CLI folks know).
# Features
* Commands for working with Azure Batch job templates and pool templates (see below)
* Snippets and auto-completion for common template elements and parameters
* Type `batch` in a JSON file to see available snippets
* Template diagnostics
* Warning squiggles when a template references an undeclared parameter (current status: wonky)
* Custom explorer pane displaying Azure Batch jobs and pools
# Commands
All commands are in the 'Azure' category.
* **Azure: Create Batch Job:** Creates an Azure Batch job from the job template or job JSON in the active tab. If the active tab is a template, the command prompts for a value for each template parameter. If there is a parameters file in the same directory (named the same as the job template, but with the extension `.parameters.json`), then it will use any values in this file (but will prompt for any values not given in the file). Refer to [the documentation for the template and parameter file formats](https://github.com/Azure/azure-batch-cli-extensions/blob/master/doc/templates.md).
* **Azure: Create Batch Pool:** Similar to Create Batch Job but creates a pool from a template or JSON.
* **Azure: Create Batch Template from Job:** Prompts for a job in the Azure Batch account, and creates a Batch Extensions template based on that job. You can then use the **Convert to Parameter** command to parameterise aspects of of the job.
* **Azure: Create Batch Template from Pool:** Similar to Create Batch Template from Job but creates a pool template.
* **Azure: Convert to Batch Template Parameter:** Applies to job or pool templates. Converts the selected property to a template parameter - that is, it adds a declaration to the parameters section of the template, and sets the value of the property to be a reference to that newly created parameter. (Of course you can then edit the parameter to change the name, description, etc.)
# Contributing
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

132
package.json Normal file
Просмотреть файл

@ -0,0 +1,132 @@
{
"name": "vscode-azure-batch-tools",
"displayName": "Azure Batch Tools for Visual Studio Code",
"description": "Commands for authoring and deploying Azure Batch resources",
"version": "0.0.1",
"publisher": "itowlson",
"engines": {
"vscode": "^1.13.0"
},
"repository": {
"type": "git",
"url": "https://github.com/Azure/vscode-azure-batch-tools.git"
},
"bugs": {
"url": "https://github.com/Azure/vscode-azure-batch-tools/issues"
},
"keywords": [
"azure",
"batch",
"cloud"
],
"activationEvents": [
"onCommand:azure.batch.createJob",
"onCommand:azure.batch.createPool",
"onCommand:azure.batch.createTemplateFromJob",
"onCommand:azure.batch.createTemplateFromPool",
"onCommand:azure.batch.convertToParameter",
"onView:azure.batch.explorer",
"onLanguage:json"
],
"main": "./out/src/extension",
"contributes": {
"commands": [
{
"command": "azure.batch.createJob",
"title": "Create Batch Job",
"category": "Azure"
},
{
"command": "azure.batch.createPool",
"title": "Create Batch Pool",
"category": "Azure"
},
{
"command": "azure.batch.createTemplateFromJob",
"title": "Create Batch Template from Job",
"category": "Azure"
},
{
"command": "azure.batch.createTemplateFromPool",
"title": "Create Batch Template from Pool",
"category": "Azure"
},
{
"command": "azure.batch.convertToParameter",
"title": "Convert to Batch Template Parameter",
"category": "Azure"
},
{
"command": "azure.batch.get",
"title": "Get"
},
{
"command": "azure.batch.getAsTemplate",
"title": "Get as Template"
},
{
"command": "azure.batch.refresh",
"title": "Refresh"
}
],
"snippets": [
{
"language": "json",
"path": "./snippet/job.json"
},
{
"language": "json",
"path": "./snippet/template.json"
}
],
"views": {
"explorer": [
{
"id": "azure.batch.explorer",
"name": "Azure Batch"
}
]
},
"menus":{
"view/title" : [
{
"command" : "azure.batch.refresh",
"when": "view == azure.batch.explorer",
"group": "navigation"
}
],
"view/item/context": [
{
"command": "azure.batch.get",
"when": "view == azure.batch.explorer && viewItem == azure.batch.resource"
},
{
"command": "azure.batch.getAsTemplate",
"when": "view == azure.batch.explorer && viewItem == azure.batch.resource"
}
]
}
},
"scripts": {
"vscode:prepublish": "tsc -p ./",
"compile": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install",
"test": "node ./node_modules/vscode/bin/test"
},
"devDependencies": {
"@types/mocha": "^2.2.32",
"@types/node": "^6.0.40",
"@types/shelljs": "^0.7.1",
"@types/tmp": "0.0.33",
"@types/moment-duration-format": "^1.3.5",
"mocha": "^2.3.3",
"typescript": "^2.0.3",
"vscode": "^1.0.0"
},
"dependencies": {
"shelljs": "^0.7.7",
"tmp": "^0.0.31",
"moment": "^2.15.2",
"moment-duration-format": "^1.3.0"
}
}

61
snippet/job.json Normal file
Просмотреть файл

@ -0,0 +1,61 @@
{
"Azure Batch Application Template": {
"prefix": "batch.app-template",
"body": [
"{",
"\t\"parameters\": {",
"\t},",
"\t\"onAllTasksComplete\": \"terminateJob\"",
"}"
],
"description": "An application template for Azure Batch jobs"
},
"Azure Batch Task Per File Task Factory": {
"prefix": "batch.task-per-file",
"body": [
"\"taskFactory\": {",
"\t\"type\": \"taskPerFile\",",
"\t\"source\": {",
"\t\t\"fileGroup\": \"${1:fileGroup}\"",
"\t},",
"\t\"repeatTask\": {",
"\t\t\"commandLine\": \"${2:commandLine}\",",
"\t\t\"resourceFiles\": [],",
"\t\t\"outputFiles\": []",
"\t}",
"}"
],
"description": "A task-per-file task factory for Azure Batch jobs"
},
"Azure Batch Parametric Sweep Task Factory": {
"prefix": "batch.parametric-sweep",
"body": [
"\"taskFactory\": {",
"\t\"type\": \"parametricSweep\",",
"\t\"parameterSets\": [ {",
"\t\t\"start\": \"${1:start}\",",
"\t\t\"end\": \"${2:end}\",",
"\t\t\"step\": 1",
"\t} ],",
"\t\"repeatTask\": {",
"\t\t\"commandLine\": \"${3:commandLine}\",",
"\t\t\"resourceFiles\": [",
"\t\t],",
"\t\t\"outputFiles\": []",
"\t}",
"}"
],
"description": "A parametric sweep task factory for Azure Batch jobs"
},
"Azure Batch Task Collection Task Factory": {
"prefix": "batch.task-collection",
"body": [
"\"taskFactory\": {",
"\t\"tasks\": [",
"\t]",
"\t}",
"}"
],
"description": "A task collection task factory for Azure Batch jobs"
}
}

31
snippet/template.json Normal file
Просмотреть файл

@ -0,0 +1,31 @@
{
"Azure Batch Template": {
"prefix": "batch.template",
"body": [
"{",
"\t\"parameters\": {$0",
"\t},",
"\t\"${1:job}\": {",
"\t\t\"type\": \"Microsoft.Batch/batchAccounts/${1:job}s\",",
"\t\t\"apiVersion\": \"2017-05-01\",",
"\t\t\"properties\": {",
"\t\t\t\"id\": \"${2:id}\"",
"\t\t}",
"\t},",
"}"
],
"description": "An Azure Batch resource template"
},
"Azure Batch Template Parameter": {
"prefix": "batch.parameter-declaration",
"body": [
"\"${1:name}\": {",
"\t\"type\": \"${2:string}\",",
"\t\"metadata\": {",
"\t\t\"description\": \"$3\"",
"\t}",
"}"
],
"description": "Declares a parameter for an Azure Batch template"
}
}

97
src/azurebatchtree.ts Normal file
Просмотреть файл

@ -0,0 +1,97 @@
import * as vscode from 'vscode';
import * as batch from './batch';
import * as shell from './shell';
import * as host from './host';
export const UriScheme : string = 'ab';
export class AzureBatchProvider implements vscode.TreeDataProvider<AzureBatchTreeNode>, vscode.TextDocumentContentProvider {
private _onDidChangeTreeData: vscode.EventEmitter<AzureBatchTreeNode | undefined> = new vscode.EventEmitter<AzureBatchTreeNode | undefined>();
readonly onDidChangeTreeData: vscode.Event<AzureBatchTreeNode | undefined> = this._onDidChangeTreeData.event;
getTreeItem(abtn : AzureBatchTreeNode) : vscode.TreeItem {
const collapsibleState = abtn.kind === 'root'
? vscode.TreeItemCollapsibleState.Collapsed
: vscode.TreeItemCollapsibleState.None;
let item = new vscode.TreeItem(abtn.text, collapsibleState);
item.contextValue = 'azure.batch.' + abtn.kind;
if (isResourceNode(abtn)) {
item.command = {
command: 'azure.batch.get',
arguments: [abtn],
title: 'Get'
};
}
return item;
}
async getChildren(abtn? : AzureBatchTreeNode) : Promise<AzureBatchTreeNode[]> {
if (abtn) {
if (isRootNode(abtn)) {
const listResult = await batch.listResources(shell.exec, abtn.resourceType);
if (shell.isCommandError(listResult)) {
host.writeOutput(listResult.error);
return [ new ErrorNode('Error - see output window for details') ];
}
return listResult.map((r) => new ResourceNode(r.id, abtn.resourceType, r));
} else if (isResourceNode(abtn)) {
return [];
}
return [];
}
return [new RootNode("Jobs", 'job'), new RootNode("Pools", 'pool')];
}
provideTextDocumentContent(uri: vscode.Uri, token : vscode.CancellationToken) : vscode.ProviderResult<string> {
const resourceType = <batch.BatchResourceType> uri.authority;
const id : string = uri.path.substring(1, uri.path.length - '.json'.length);
return this.getBatchResourceJson(resourceType, id);
}
private async getBatchResourceJson(resourceType : batch.BatchResourceType, id : string) : Promise<string> {
const getResult = await batch.getResource(shell.exec, resourceType, id);
if (shell.isCommandError(getResult)) {
throw getResult.error;
}
return JSON.stringify(getResult, null, 2);
}
refresh(): void {
this._onDidChangeTreeData.fire();
}
}
function isRootNode(node : AzureBatchTreeNode) : node is RootNode {
return node.kind === 'root';
}
function isResourceNode(node : AzureBatchTreeNode) : node is ResourceNode {
return node.kind === 'resource';
}
export interface AzureBatchTreeNode {
readonly kind : AbtnKind;
readonly text : string;
readonly resourceType? : batch.BatchResourceType;
readonly uri : vscode.Uri;
}
type AbtnKind = 'root' | 'resource' | 'error';
class RootNode implements AzureBatchTreeNode {
constructor(readonly text : string, readonly resourceType : batch.BatchResourceType) { }
readonly kind : AbtnKind = 'root';
readonly uri : vscode.Uri = vscode.Uri.parse(`${UriScheme}://`);
}
class ResourceNode implements AzureBatchTreeNode {
constructor(readonly text : string, readonly resourceType : batch.BatchResourceType, readonly resource : any) { }
readonly kind : AbtnKind = 'resource';
readonly uri : vscode.Uri = vscode.Uri.parse(`${UriScheme}://${this.resourceType}/${this.resource.id}.json`)
}
class ErrorNode implements AzureBatchTreeNode {
constructor(readonly text : string) { }
readonly kind : AbtnKind = 'error';
readonly uri : vscode.Uri = vscode.Uri.parse(`${UriScheme}://`);
}

225
src/batch.ts Normal file
Просмотреть файл

@ -0,0 +1,225 @@
import { IShellExecResult, ICommandError } from './shell';
import * as duration from './duration';
import * as host from './host';
export function parseBatchTemplate(text : string, resourceType : BatchResourceType) : IBatchResource {
try {
const jobject : any = JSON.parse(text);
if (!jobject) {
return { isTemplate: false, templateValidationFailure: 'Unable to parse current document as a JSON file', parameters: [] };
}
if (looksLikeTemplate(jobject, resourceType)) {
return parseTemplateCore(jobject);
}
return { isTemplate: false, templateValidationFailure: `Current document is not a template - expected a ${resourceType} element with type ${templateResourceType(resourceType)}`, parameters: [] };
} catch (SyntaxError) {
return { isTemplate: false, templateValidationFailure: 'Unable to parse current document as a JSON file', parameters: [] };
}
}
function looksLikeTemplate(json : any, resourceType : BatchResourceType) : boolean {
const resourceDecl = json[resourceType];
if (!resourceDecl) {
return false;
}
if (resourceDecl.type && resourceDecl.properties) {
return resourceDecl.type === templateResourceType(resourceType);
}
return false;
}
function templateResourceType(resourceType : BatchResourceType) : string {
return "Microsoft.Batch/batchAccounts/" + plural(resourceType);
}
function plural(resourceType : BatchResourceType) : string {
switch (resourceType) {
case 'job': return 'jobs';
case 'pool': return 'pools';
default: throw `unknown resource type ${resourceType}`;
}
}
function parseTemplateCore(json : any) : IBatchResource {
const parameters : IBatchTemplateParameter[] = [];
for (const p in json.parameters || []) {
const pval : any = json.parameters[p];
parameters.push({
name : p,
dataType : <BatchTemplateParameterDataType>(pval['type']),
defaultValue : pval['defaultValue'],
allowedValues : pval['allowedValues'],
metadata : <IBatchTemplateParameterMetadata>(pval['metadata']),
})
}
return { isTemplate: true, templateValidationFailure: '', parameters: parameters };
}
export function parseParameters(text : string) : IParameterValue[] {
try {
const jobject : any = JSON.parse(text);
if (!jobject) {
return [];
}
return parseParametersCore(jobject);
} catch (SyntaxError) {
host.writeOutput("Can't use candidate parameter file at it's not valid JSON - prompting for parameters instead");
return [];
}
}
function parseParametersCore(json : any) : IParameterValue[] {
const parameters : IParameterValue[] = [];
for (const key in json) {
parameters.push({
name : key,
value : json[key]
})
}
return parameters;
}
export async function listResources(shellExec : (command : string) => Promise<IShellExecResult>, resourceType : BatchResourceType) : Promise<IBatchResourceContent[] | ICommandError> {
//const command = `az batch ${resourceType} list --query [*].{id:id,displayName:displayName}`; // the problem is we are going to want to stringify the resource JSON and that means we need to download it - or do a second call
const command = `az batch ${resourceType} list`;
const result = await shellExec(command);
if (result.exitCode === 0) {
const raw : any[] = JSON.parse(result.output);
const durationised = raw.map((r) => transformProperties(r, durationProperties(resourceType), duration.toISO8601));
return durationised;
}
return { error : result.error };
}
export async function getResource(shellExec : (command : string) => Promise<IShellExecResult>, resourceType : BatchResourceType, id : string) : Promise<IBatchResourceContent | ICommandError> {
const command = `az batch ${resourceType} show --${resourceType}-id ${id}`;
const result = await shellExec(command);
if (result.exitCode === 0) {
const raw = JSON.parse(result.output);
const durationised = transformProperties(raw, durationProperties(resourceType), duration.toISO8601);
return durationised;
}
return { error : result.error };
}
export function makeTemplate(resource : any, resourceType : BatchResourceType) : any {
const filtered = withoutProperties(resource, unsettableProperties(resourceType));
// TODO: strip defaults (particularly nulls or empty objects) - we get null-stripping as a side-effect of transformProperties but shouldn't rely on this!
const templateBody = filtered;
var template : any = {
parameters: { }
};
template[resourceType] = {
type: templateResourceType(resourceType),
apiVersion: '2017-05-01',
properties: templateBody
}
return template;
}
function withoutProperties(resource : any, properties : string[]) : any {
var result : any = {};
for (const property in resource) {
if (properties.indexOf(property) < 0) {
result[property] = resource[property];
}
}
return result;
}
function transformProperties(obj : any, properties: string[], transform : (original : string | undefined) => string | undefined) : any {
var result : any = {};
for (const property in obj) {
const value = obj[property];
if (value instanceof Array) {
result[property] = value.map((e : any) => transformProperties(e, properties, transform));
} else if (value instanceof Object) {
result[property] = transformProperties(value, properties, transform);
} else {
const needsTransform = properties.indexOf(property) >= 0;
const resultProperty = needsTransform ? transform(value) : value;
if (resultProperty !== undefined && resultProperty !== null) {
result[property] = resultProperty;
}
}
}
return result;
}
// This isn't ideal since it doesn't cover property paths, but it will do
function durationProperties(resourceType : BatchResourceType) : string[] {
switch (resourceType) {
case 'job':
return [ 'maxWallClockTime', 'retentionTime' ];
case 'pool':
return [ 'resizeTimeout' ];
default:
throw `unknown resource type ${resourceType}`;
}
}
function unsettableProperties(resourceType : BatchResourceType) : string[] {
// TODO: better plan might be to whitelist properties by using the Swagger spec
// for the 'add' models.
const commonUnsettableProperties = ["odata.metadata", "url", "eTag", "lastModified", "creationTime", "state", "stateTransitionTime", "previousState", "previousStateTransitionTime"];
return commonUnsettableProperties.concat(unsettablePropertiesCore(resourceType));
}
function unsettablePropertiesCore(resourceType : BatchResourceType) : string[] {
// TODO: better plan might be to whitelist properties by using the Swagger spec
// for the 'add' models.
switch (resourceType) {
case 'job':
return ["executionInfo", "stats"];
case 'pool':
return ["allocationState", "allocationStateTransitionTime", "resizeErrors", "currentDedicatedNodes", "currentLowPriorityNodes", "autoScaleRun", "stats"];
default:
throw `unknown resource type ${resourceType}`;
}
}
export interface IBatchResource {
readonly isTemplate : boolean;
readonly templateValidationFailure : string;
readonly parameters : IBatchTemplateParameter[];
}
export interface IBatchTemplateParameter {
readonly name : string;
readonly dataType : BatchTemplateParameterDataType;
readonly defaultValue? : any;
readonly allowedValues? : any[];
readonly metadata? : IBatchTemplateParameterMetadata;
}
export interface IBatchTemplateParameterMetadata {
readonly description : string;
}
export type BatchTemplateParameterDataType = 'int' | 'string' | 'bool';
export interface IParameterValue {
readonly name : string;
readonly value : any;
}
export interface IBatchResourceContent {
readonly id : string;
readonly displayName? : string;
}
export type BatchResourceType = 'job' | 'pool';

26
src/duration.ts Normal file
Просмотреть файл

@ -0,0 +1,26 @@
import * as moment from 'moment';
import * as momentFmt from 'moment-duration-format';
export function toISO8601(duration: string | undefined) : string | undefined {
if (!duration) {
return undefined;
}
// Python CLI format is [ddd days, ] hh:mm:ss.ffffff -
// Moment requires ddd.hh:mm:ss.ffffff
const mfduration = duration.replace(' days, ', '.').replace(' day, ', '.');
const dur = moment.duration(mfduration);
// Handle the 'default surfaced as MaxValue' case
if (dur.asDays() > 10000000) {
return undefined;
}
// Handle the zero case
if (dur.asSeconds() === 0) {
return "PT0S";
}
return dur.toISOString();
}

497
src/extension.ts Normal file
Просмотреть файл

@ -0,0 +1,497 @@
'use strict';
import * as vscode from 'vscode';
import * as shelljs from 'shelljs';
import * as fs from 'fs';
import * as tmp from 'tmp';
import * as path from './path';
import * as batch from './batch';
import * as shell from './shell';
import * as azurebatchtree from './azurebatchtree';
import * as host from './host';
import * as textmodels from './textmodels';
let diagnostics : vscode.DiagnosticCollection;
export function activate(context: vscode.ExtensionContext) {
const azureBatchProvider = new azurebatchtree.AzureBatchProvider();
diagnostics = vscode.languages.createDiagnosticCollection('json');
// TODO: This seems very unwieldy, and our method relies on symbols
// which are only loaded asynchronously so the initial document doesn't
// get checked until it changes.
vscode.workspace.onDidOpenTextDocument(diagnoseTemplateProblems, undefined, context.subscriptions);
vscode.workspace.onDidCloseTextDocument((textDocument) => {
diagnostics.delete(textDocument.uri);
}, null, context.subscriptions);
vscode.workspace.onDidChangeTextDocument((ch) => {
diagnoseTemplateProblems(ch.document);
}, null, context.subscriptions);
vscode.workspace.onDidSaveTextDocument(diagnoseTemplateProblems, undefined, context.subscriptions);
vscode.workspace.textDocuments.forEach(diagnoseTemplateProblems, undefined);
let disposables = [
vscode.commands.registerCommand('azure.batch.createJob', createJob),
vscode.commands.registerCommand('azure.batch.createPool', createPool),
vscode.commands.registerCommand('azure.batch.createTemplateFromJob', createTemplateFromJob),
vscode.commands.registerCommand('azure.batch.createTemplateFromPool', createTemplateFromPool),
vscode.commands.registerCommand('azure.batch.convertToParameter', convertToParameter),
vscode.commands.registerCommand('azure.batch.get', viewnodeGet),
vscode.commands.registerCommand('azure.batch.getAsTemplate', viewnodeGetAsTemplate),
vscode.commands.registerCommand('azure.batch.refresh', () => azureBatchProvider.refresh()),
vscode.window.registerTreeDataProvider('azure.batch.explorer', azureBatchProvider),
vscode.workspace.registerTextDocumentContentProvider(azurebatchtree.UriScheme, azureBatchProvider),
vscode.languages.registerCompletionItemProvider('json', new ParameterReferenceCompletionItemProvider()),
diagnostics
];
disposables.forEach((d) => context.subscriptions.push(d), this);
}
export function deactivate() {
}
function withActiveDocument(action: (doc : vscode.TextDocument) => void) {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
return;
}
const doc = activeEditor.document;
if (!doc) {
return;
}
action(doc);
}
function createJob() {
withActiveDocument((doc) => createResourceImpl(doc, 'job'));
}
function createPool() {
withActiveDocument((doc) => createResourceImpl(doc, 'pool'));
}
async function createResourceImpl(doc : vscode.TextDocument, resourceType : batch.BatchResourceType) {
// https://github.com/Microsoft/vscode/issues/29156
// We don't yet have a workaround for this, so for now, resort to asking the user to
// save the document and then re-run the command. No, it's not great. Sorry.
if (doc.isUntitled) {
await vscode.window.showWarningMessage("Please save the document before running this command.");
return;
}
const templateInfo = batch.parseBatchTemplate(doc.getText(), resourceType);
if (!templateInfo.isTemplate) {
await vscode.window.showErrorMessage(templateInfo.templateValidationFailure);
return;
}
const templateFileName = doc.fileName; // TODO: handle the case where the doc has never been saved
if (doc.isDirty) {
const saved = await doc.save();
if (!saved) {
return;
}
}
// TODO: this results in the unnecessary creation and deletion of a temp file in
// the non-template case - it is harmless and simplifies the code, but it would be
// nice not to have to do it!
const parameterFile = await getParameterFile(templateFileName, resourceType);
const knownParametersText = getParameterJson(parameterFile);
const knownParameters = batch.parseParameters(knownParametersText);
const isKnownParameter = (n : string) => knownParameters.findIndex((p) => p.name === n) >= 0;
const anyUnknownParameters = templateInfo.parameters.findIndex((p) => !isKnownParameter(p.name)) >= 0;
const tempParameterInfo = anyUnknownParameters ? await createTempParameterFile(templateInfo, knownParameters) : undefined;
if (tempParameterInfo && tempParameterInfo.abandoned) {
return;
}
const parameterFilePath = tempParameterInfo ? tempParameterInfo.path : parameterFile.path;
const cleanup = tempParameterInfo ? () => { fs.unlinkSync(tempParameterInfo.path); } : () => { return; };
const commandOptions = templateInfo.isTemplate ?
`--template "${doc.fileName}" --parameters "${parameterFilePath}"` :
`--json-file "${doc.fileName}"`;
host.writeOutput(`Creating Azure Batch ${resourceType}...`);
try {
const execResult = await shell.exec(`az batch ${resourceType} create ${commandOptions}`);
if (execResult.exitCode !== 0 || execResult.error) { // TODO: figure out what to check (if anything) - problem is that the CLI can return exit code 0 on failure... but it writes to stderr on success too (the experimental feature warnings)
host.writeOutput(execResult.error);
} else {
host.writeOutput(execResult.output);
}
host.writeOutput("Done");
} finally {
cleanup();
}
}
async function getParameterFile(templateFileName : string, resourceType : batch.BatchResourceType) : Promise<IParameterFileInfo> {
const templateFileRoot = path.stripExtension(templateFileName);
const templateFileDir = path.directory(templateFileName);
const parameterFileNames = [
templateFileRoot + '.parameters.json',
templateFileDir + `/${resourceType}parameters.json`,
templateFileDir + `/${resourceType}.parameters.json`,
templateFileDir + `/parameters.${resourceType}.json`,
templateFileDir + '/parameters.json'
];
const parameterFileName = parameterFileNames.find(s => fs.existsSync(s));
if (!parameterFileName) {
return {
exists: false,
path: ''
};
}
const parametersDoc = vscode.workspace.textDocuments.find((d) => path.equal(d.fileName, parameterFileName));
if (parametersDoc && parametersDoc.isDirty) {
await parametersDoc.save();
}
return {
exists: true,
path: parameterFileName,
document: parametersDoc
};
}
function getParameterJson(parameterFile : IParameterFileInfo) : string {
if (parameterFile.exists) {
return parameterFile.document ? parameterFile.document.getText() : fs.readFileSync(parameterFile.path, 'utf8');
}
return '{}';
}
async function createTempParameterFile(jobTemplateInfo : batch.IBatchResource, knownParameters : batch.IParameterValue[]) : Promise<ITempFileInfo | undefined> {
let parameterObject : any = {};
for (const p of jobTemplateInfo.parameters) {
const known = knownParameters.find((pv) => pv.name === p.name);
const value = known ? known.value : await promptForParameterValue(p);
if (value) {
parameterObject[p.name] = value;
} else {
return { abandoned: true, path: '' };
}
}
const json = JSON.stringify(parameterObject);
const tempFile = tmp.fileSync();
fs.writeFileSync(tempFile.name, json, { encoding: 'utf8' });
return { abandoned: false, path: tempFile.name };
}
async function promptForParameterValue(parameter : batch.IBatchTemplateParameter) : Promise<any> {
const description = (parameter.metadata && parameter.metadata.description) ? ` | ${parameter.metadata.description}` : '';
if (parameter.allowedValues) {
const allowedValueQuickPicks = parameter.allowedValues.map((v) => quickPickFor(v));
const opts = { placeHolder: `${parameter.name}${description}` };
const selectedValue = await vscode.window.showQuickPick(allowedValueQuickPicks, opts);
return selectedValue ? selectedValue.value : undefined;
} else {
const opts = {
prompt: `${parameter.name}${description} (${parameter.dataType})`,
value: parameter.defaultValue ? String(parameter.defaultValue) : undefined
// TODO: set the validateInput option to do range checking
};
return await vscode.window.showInputBox(opts);
}
}
function quickPickFor(value : any) : AllowedValueQuickPickItem {
return {
label: String(value),
description: '',
value: value
};
}
async function createTemplateFromJob() {
const resourceType : batch.BatchResourceType = 'job';
const resourceTypePlural = 'jobs';
await createTemplateFromResource(resourceType, resourceTypePlural);
}
async function createTemplateFromPool() {
const resourceType : batch.BatchResourceType = 'pool';
const resourceTypePlural = 'pools';
await createTemplateFromResource(resourceType, resourceTypePlural);
}
async function createTemplateFromResource(resourceType : batch.BatchResourceType, resourceTypePlural : string) {
host.writeOutput(`Getting list of ${resourceTypePlural} from account...`);
const resources = await batch.listResources(shell.exec, resourceType);
if (shell.isCommandError(resources)) {
host.writeOutput(`Error getting ${resourceTypePlural} from account.\n\nDetails:\n\n` + resources.error);
return;
}
const quickPicks = resources.map((j) => quickPickForResource(j));
const pick = await vscode.window.showQuickPick(quickPicks);
if (pick) {
const resource = pick.value;
await createTemplateFile(resourceType, resource, resource.id);
}
}
async function createTemplateFromSpecificResource(resourceType: batch.BatchResourceType, id : string) {
const resource = await batch.getResource(shell.exec, resourceType, id);
await createTemplateFile(resourceType, resource, id);
}
async function createTemplateFile(resourceType : batch.BatchResourceType, resource : any, id : string) {
const template = batch.makeTemplate(resource, resourceType);
const filename = id + `.${resourceType}template.json`;
createFile(filename, JSON.stringify(template, null, 2));
}
async function createFile(filename : string, content : string) : Promise<void> {
const filepath = path.join(vscode.workspace.rootPath || process.cwd(), filename);
const doc = await vscode.workspace.openTextDocument(vscode.Uri.parse('untitled:' + filepath));
const start = new vscode.Position(0, 0),
end = new vscode.Position(0, 0),
range = new vscode.Range(start, end),
edit = new vscode.TextEdit(range, content),
wsEdit = new vscode.WorkspaceEdit();
wsEdit.set(doc.uri, [edit]);
await vscode.workspace.applyEdit(wsEdit);
await vscode.window.showTextDocument(doc);
}
function quickPickForResource(resource: batch.IBatchResourceContent) : AllowedValueQuickPickItem {
return {
label: resource.id,
description: resource.displayName || '',
value: resource
};
}
async function convertToParameter() {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
return;
}
const document = activeEditor.document;
if (!document) {
return;
}
const selection = activeEditor.selection;
if (!selection) {
return;
}
const convertResult = await convertToParameterCore(document, selection);
if (isTextEdit(convertResult)) {
activeEditor.revealRange(convertResult.range);
activeEditor.selection = new vscode.Selection(convertResult.range.start, convertResult.range.end);
} else {
vscode.window.showErrorMessage(convertResult);
}
}
function isTextEdit(obj : vscode.TextEdit | string) : obj is vscode.TextEdit {
return (<vscode.TextEdit>obj).range !== undefined;
}
// TODO: any better way to make available for testing?
export async function convertToParameterCore(document: vscode.TextDocument, selection: vscode.Selection) : Promise<vscode.TextEdit | string> {
const jsonSymbols = await getJsonSymbols(document);
if (jsonSymbols.length === 0) {
return 'Active document is not a JSON document';
}
const property = findProperty(jsonSymbols, selection.anchor);
if (!property) {
return 'Selection is not a JSON property';
}
const propertyContainerName = property.containerName;
if (!(propertyContainerName.startsWith('job.properties') || propertyContainerName.startsWith('pool.properties'))) {
return 'Selection is not a resource property';
}
// TODO: we really want to do this only for leaf properties
const propertyLocation = property.location.range;
const propertyText = document.getText(propertyLocation);
const nameBitLength = propertyText.indexOf(':') + 1;
if (nameBitLength <= 0) {
return 'Cannot locate property name';
}
const propertyValueLocation = new vscode.Range(propertyLocation.start.translate(0, nameBitLength), propertyLocation.end);
const propertyValue = JSON.parse(document.getText(propertyValueLocation));
const propertyType = getParameterTypeName(propertyValue); // consider getting this from Swagger?
// TODO: investigate using a smart insert for this (https://github.com/Microsoft/vscode/issues/3210)
// (Currently doesn't seem to be a thing - works for completion items only...?)
const newParameterDefn : any = {
type: propertyType,
defaultValue: propertyValue,
metadata: { description: `Value for ${property.containerName}.${property.name}` }
}
const insertParamEdit = textmodels.getTemplateParameterInsertion(jsonSymbols, property.name, newParameterDefn);
const replaceValueWithParamRef = new vscode.TextEdit(propertyValueLocation, ` "[parameters('${property.name}')]"`);
await applyEdits(document, insertParamEdit, replaceValueWithParamRef);
return insertParamEdit;
}
async function applyEdits(document : vscode.TextDocument, ...edits : vscode.TextEdit[]) : Promise<boolean> {
const wsEdit = new vscode.WorkspaceEdit();
wsEdit.set(document.uri, edits);
return await vscode.workspace.applyEdit(wsEdit);
}
async function getJsonSymbols(document : vscode.TextDocument) : Promise<vscode.SymbolInformation[]> {
const sis : any = await vscode.commands.executeCommand('vscode.executeDocumentSymbolProvider', document.uri);
if (sis && sis.length) {
return sis;
}
return [];
}
function findProperty(symbols: vscode.SymbolInformation[], position: vscode.Position) : vscode.SymbolInformation | null {
const containingSymbols = symbols.filter((s) => s.location.range.contains(position));
if (!containingSymbols || containingSymbols.length === 0) {
return null;
}
const sorted = containingSymbols.sort((a, b) => (a.containerName || '').length - (b.containerName || '').length);
return sorted[sorted.length - 1];
}
function getParameterTypeName(value : any) : string {
return (value instanceof Number || typeof value === 'number') ? 'int' :
(value instanceof Boolean || typeof value === 'boolean') ? 'bool' :
(value instanceof String || typeof value === 'string') ? 'string' :
'object';
}
async function viewnodeGet(node : azurebatchtree.AzureBatchTreeNode) {
const document = await vscode.workspace.openTextDocument(node.uri)
vscode.window.showTextDocument(document);
}
async function viewnodeGetAsTemplate(node : azurebatchtree.AzureBatchTreeNode) {
// TODO: horrible smearing of responsibilities and duplication of code across this
// and the get command - rationalise!
const uri = node.uri;
const resourceType = <batch.BatchResourceType> uri.authority;
const id : string = uri.path.substring(0, uri.path.length - '.json'.length).substr(1);
await createTemplateFromSpecificResource(resourceType, id);
}
interface AllowedValueQuickPickItem extends vscode.QuickPickItem {
value : any
}
interface ITempFileInfo {
readonly abandoned : boolean;
readonly path : string;
}
interface IParameterFileInfo {
readonly exists : boolean;
readonly path : string;
readonly document? : vscode.TextDocument;
}
class ParameterReferenceCompletionItemProvider implements vscode.CompletionItemProvider {
provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) : vscode.ProviderResult<vscode.CompletionItem[]> {
return provideCompletionItemsCore(document, position, token); // Helper method allows us to use async/await
}
}
async function provideCompletionItemsCore(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) : Promise<vscode.CompletionItem[]> {
const sis : vscode.SymbolInformation[] = await getJsonSymbols(document); // We use this rather than JSON.parse because the document is likely to be invalid JSON at the time the user is in the middle of typing the completion trigger
if (sis) {
return sis.filter((si) => si.containerName === 'parameters')
.map((si) => completionItemFor(si));
}
return [];
}
function completionItemFor(si : vscode.SymbolInformation) : vscode.CompletionItem {
let ci = new vscode.CompletionItem(`batch.parameter-ref('${si.name}')`);
ci.insertText = `"[parameters('${si.name}')]"`;
ci.documentation = `A reference to the '${si.name}' template parameter`;
ci.detail = `vscode-azure-batch-tools`;
return ci;
}
interface IParameterRefMatch {
readonly index : number;
readonly name : string;
}
function parameterRefs(text : string) : IParameterRefMatch[] {
let refs : IParameterRefMatch[] = [];
let parameterRegex = /\"\[parameters\('(\w+)'\)\]"/g;
let match : any;
while ((match = parameterRegex.exec(text)) !== null) {
let index : number = match.index + "\"[parameters('".length;
let name : string = match[1];
refs.push({ index: index, name : name });
}
return refs;
}
async function diagnoseTemplateProblems(document : vscode.TextDocument) : Promise<void> {
if (document.languageId !== 'json') {
return;
}
let ds : vscode.Diagnostic[] = [];
const sis : vscode.SymbolInformation[] = await getJsonSymbols(document);
if (sis && sis.length > 0 /* don't report warnings just because the symbols haven't loaded yet */) {
const paramNames = sis.filter((si) => si.containerName === 'parameters')
.map((si) => si.name);
const paramRefs = parameterRefs(document.getText());
if (paramRefs) {
for (const paramRef of paramRefs) {
if (paramNames.indexOf(paramRef.name) < 0) {
const startPos = document.positionAt(paramRef.index);
const endPos = document.positionAt(paramRef.index + paramRef.name.length);
// TODO: perhaps a CodeActionProvider would be better because then
// we could offer to create the parameter?
const d = new vscode.Diagnostic(new vscode.Range(startPos, endPos), `Unknown parameter name ${paramRef.name}`, vscode.DiagnosticSeverity.Warning);
ds.push(d);
}
}
}
}
diagnostics.set(document.uri, ds);
}

11
src/host.ts Normal file
Просмотреть файл

@ -0,0 +1,11 @@
import * as vscode from 'vscode';
const output : vscode.OutputChannel = vscode.window.createOutputChannel('Azure Batch');
let outputShown = false;
export function writeOutput(text : string) {
if (!outputShown) {
output.show();
}
output.appendLine(text);
}

28
src/path.ts Normal file
Просмотреть файл

@ -0,0 +1,28 @@
import * as path from 'path';
export function join(...paths : string[]) : string {
return path.join(...paths);
}
export function stripExtension(filePath : string) : string {
// TODO: this will get it wrong if the file has no extension *and* the
// directory path contains a directory with an extension. This isn't really
// a concern for our use case though would be nice to fix.
const sepIndex = filePath.lastIndexOf('.');
if (sepIndex < 0) {
return filePath;
}
return filePath.substr(0, sepIndex);
}
export function directory(filePath : string) : string {
return path.dirname(filePath);
}
export function equal(filePath1 : string, filePath2 : string) : boolean {
const fwd1 = filePath1.replace(/\\/g, '/');
const fwd2 = filePath2.replace(/\\/g, '/');
if (process.platform === 'win32') {
return fwd1.toLowerCase() === fwd2.toLowerCase();
}
return fwd1 === fwd2;
}

23
src/shell.ts Normal file
Просмотреть файл

@ -0,0 +1,23 @@
import * as shelljs from 'shelljs';
export function exec(command : string) : Promise<IShellExecResult> {
return new Promise<IShellExecResult>((resolve, reject) => {
shelljs.exec(command, { async: true }, (code, stdout, stderr) => {
resolve({ exitCode: code, output: stdout, error: stderr });
});
});
}
export interface IShellExecResult {
readonly exitCode : number;
readonly output : string;
readonly error : string;
}
export interface ICommandError {
readonly error : string;
}
export function isCommandError<T>(obj: T | ICommandError) : obj is ICommandError {
return (<ICommandError>obj).error !== undefined;
}

121
src/textmodels.ts Normal file
Просмотреть файл

@ -0,0 +1,121 @@
import * as vscode from 'vscode';
export function getTemplateParameterInsertion(jsonSymbols : vscode.SymbolInformation[], propertyName : string, newParameterDefn : any) : vscode.TextEdit {
const editProvider = templateParameterInsertionEditProvider(jsonSymbols, propertyName, newParameterDefn);
return editProvider.getEdit();
}
interface IEditProvider {
getEdit() : vscode.TextEdit;
}
function indentOf(symbol : vscode.SymbolInformation) {
return symbol.location.range.start.character;
}
function indentFrom(symbol1 : vscode.SymbolInformation, symbol2 : vscode.SymbolInformation) {
const offset = indentOf(symbol1) - indentOf(symbol2);
return Math.abs(offset);
}
function indentLines(text : string, amount : number) : string {
const indent = ' '.repeat(amount);
const lines = text.split('\n');
const indented = lines.map((l) => indent + l);
return indented.join('\n');
}
function isSingleLine(range: vscode.Range) {
return range.start.line === range.end.line;
}
function lineStart(pos: vscode.Position) {
return new vscode.Position(pos.line, 0);
}
function immediatelyBefore(pos: vscode.Position) {
return pos.translate(0, -1);
}
function templateParameterInsertionEditProvider(jsonSymbols : vscode.SymbolInformation[], propertyName : string, newParameterDefn : any) : IEditProvider {
const parametersElement = jsonSymbols.find((s) => s.name === 'parameters' && !s.containerName);
const parameterSymbols = jsonSymbols.filter((s) => s.containerName === 'parameters');
const lastExistingParameter = parameterSymbols.length > 0 ? parameterSymbols.reverse()[0] : undefined; // not sure what order guarantees the symbol provider makes, but it's not critical if this isn't actually the last one
class InsertAfterExistingParameter implements IEditProvider {
constructor(private readonly parametersElement : vscode.SymbolInformation, private readonly existingParameter : vscode.SymbolInformation) {
}
getEdit() : vscode.TextEdit {
const indentPerLevel = indentFrom(this.existingParameter, this.parametersElement);
const initialIndent = indentPerLevel * 2;
const rawNewParameterDefnText = `"${propertyName}": ${JSON.stringify(newParameterDefn, null, indentPerLevel)}`;
const newParameterDefnText = indentLines(rawNewParameterDefnText, initialIndent);
const insertText = ',\n' + newParameterDefnText;
const insertPos = this.existingParameter.location.range.end;
const edit = vscode.TextEdit.insert(insertPos, insertText);
return edit;
}
}
class InsertIntoEmptyParametersSection implements IEditProvider {
constructor(private readonly parametersElement : vscode.SymbolInformation) {
}
getEdit() : vscode.TextEdit {
const indentPerLevel = this.parametersElement.location.range.start.character;
const initialIndent = indentPerLevel * 2;
const rawNewParameterDefnText = `"${propertyName}": ${JSON.stringify(newParameterDefn, null, indentPerLevel)}`;
const newParameterDefnText = indentLines(rawNewParameterDefnText, initialIndent);
const parametersElementOnOneLine = isSingleLine(this.parametersElement.location.range); // for the "parameters": {} case
// prefix and suffix are for fixing up cases where the parameters element is squashed on one line
const prefix = (parametersElementOnOneLine ? '\n' : '');
const suffix = (parametersElementOnOneLine ? (' '.repeat(indentPerLevel)) : '');
const insertText = prefix + newParameterDefnText + '\n' + suffix; // TODO: line ending
// if the parameters element is squashed then our insert position is to the left of the closing brace (which will push things into the right place)
// otherwise we want to insert at the start of the line containing the closing brace
const closingBracePos = this.parametersElement.location.range.end;
const insertPos = (parametersElementOnOneLine ? immediatelyBefore(closingBracePos) : lineStart(closingBracePos));
const edit = vscode.TextEdit.insert(insertPos, insertText);
return edit;
}
}
class InsertNewParametersSection implements IEditProvider {
getEdit() : vscode.TextEdit {
let parameters : any = {};
parameters[propertyName] = newParameterDefn;
const topLevelElement = jsonSymbols.find((s) => !s.containerName);
const indentPerLevel = topLevelElement ? indentOf(topLevelElement) : 2;
const rawParametersSection = `"parameters": ${JSON.stringify(parameters, null, indentPerLevel)}`;
const parametersSectionText = indentLines(rawParametersSection, indentPerLevel); // going in at top level so indent only once
const topLevelElementSeparator = topLevelElement ? ',\n' : '\n';
const insertText = parametersSectionText + topLevelElementSeparator;
const insertPos = new vscode.Position(1, 0);
const edit = vscode.TextEdit.insert(insertPos, insertText);
return edit;
}
}
if (parametersElement) {
if (lastExistingParameter) {
// We are appending after an existing parameter
return new InsertAfterExistingParameter(parametersElement, lastExistingParameter);
} else {
// We are inserting into an empty parameters section
return new InsertIntoEmptyParametersSection(parametersElement);
}
} else {
// There is no parameters section - we need to create one and insert it at the top of the document
return new InsertNewParametersSection();
}
}

278
test/extension.test.ts Normal file
Просмотреть файл

@ -0,0 +1,278 @@
//
// Note: This example test is leveraging the Mocha test framework.
// Please refer to their documentation on https://mochajs.org/ for help.
//
// The module 'assert' provides assertion methods from node
import * as assert from 'assert';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
import * as batch from '../src/batch';
import * as duration from '../src/duration';
import * as extension from '../src/extension';
import * as path from 'path';
import * as fs from 'fs';
const nonJson = " \
id : 'wonderjob' \
poolInfo \
poolId : 'wonderpool' \
";
const jobJson = ' \
{ \
"id" : "wonderjob", \
"poolInfo" : { \
"poolId" : "wonderpool" \
} \
} \
';
const jobTemplateJson = ' \
{ \
"parameters": { \
"jobId": { \
"type": "string", \
"metadata": { \
"description": "The id of the Batch job" \
} \
}, \
"poolId": { \
"type": "string", \
"metadata": { \
"description": "The id of the Batch pool on which to run the job" \
} \
}, \
"testDefaulted": { \
"type": "string", \
"defaultValue" : "mydef" \
}, \
"testAllowed": { \
"type": "string", \
"allowedValues" : [ "alpha", "bravo", "charlie" ] \
} \
}, \
"job": { \
"type": "Microsoft.Batch/batchAccounts/jobs", \
"apiVersion": "2016-12-01", \
"properties": { \
"id": "[parameters(\'jobId\')]", \
"poolInfo" : { \
"poolId" : "wonderpool" \
} \
} \
} \
} \
';
const jobTemplateJsonNoParams = ' \
{ \
"job": { \
"type": "Microsoft.Batch/batchAccounts/jobs", \
"apiVersion": "2016-12-01", \
"properties": { \
"id" : "wonderjob", \
"poolInfo" : { \
"poolId" : "wonderpool" \
} \
} \
} \
} \
';
const poolTemplateJson = ' \
{ \
"parameters": { \
"vmSize": { \
"type": "string", \
"allowedValues" : [ "STANDARD_A3", "STANDARD_A4" ], \
"metadata": { \
"description": "The size of virtual machine to use" \
} \
} \
}, \
"pool": { \
"type": "Microsoft.Batch/batchAccounts/pools", \
"apiVersion": "2016-12-01", \
"properties": { \
"id": "superduperpool", \
"vmSize": "[parameters(\'vmSize\')]", \
"targetDedicated": 4, \
"cloudServiceConfiguration": { "osFamily": 4 } \
} \
} \
} \
';
function isTextEdit(obj : vscode.TextEdit | string) : obj is vscode.TextEdit {
return (<vscode.TextEdit>obj).range !== undefined;
}
async function waitForSymbols(document : vscode.TextDocument) : Promise<vscode.SymbolInformation[]> {
for (let i = 0; i < 100000; ++i) {
const sis : any = await vscode.commands.executeCommand('vscode.executeDocumentSymbolProvider', document.uri);
if (sis && sis.length > 0) {
return sis;
}
}
throw "symbols not ready after waiting";
}
async function assertParameterConversionTransformsFileTo(sourceFile : string, expectedResultFile : string, cursorPosition : vscode.Position) {
const document = await vscode.workspace.openTextDocument(path.join(__dirname, '../../test/' + sourceFile));
const expected = fs.readFileSync(path.join(__dirname, '../../test/' + expectedResultFile), 'utf8');
await waitForSymbols(document);
const selection = new vscode.Selection(cursorPosition, cursorPosition);
const result = await extension.convertToParameterCore(document, selection);
if (isTextEdit(result)) {
const text = document.getText();
assert.equal(text, expected);
} else {
assert.fail(result, undefined, result, 'tbd');
}
}
suite('Extension Tests', () => {
test('When there is no parameters section, a new parameter is formatted correctly', async () => {
await assertParameterConversionTransformsFileTo(
'jobtemplate_noparams.json',
'jobtemplate_noparams.after_poolid.json',
new vscode.Position(7, 18) // poolId
);
});
test('When there is an empty parameters section, a new parameter is formatted correctly', async () => {
await assertParameterConversionTransformsFileTo(
'jobtemplate_emptyparams.json',
'jobtemplate_emptyparams.after_poolid.json',
new vscode.Position(9, 18) // poolId
);
});
test('When there is an empty parameters section and it is all on one line, a new parameter is formatted correctly', async () => {
await assertParameterConversionTransformsFileTo(
'jobtemplate_emptyparamsoneline.json',
'jobtemplate_emptyparamsoneline.after_poolid.json',
new vscode.Position(8, 18) // poolId
);
});
test('When there are existing parameters, a new parameter is formatted correctly', async () => {
await assertParameterConversionTransformsFileTo(
'jobtemplate_oneparam.json',
'jobtemplate_oneparam.after_poolid.json',
new vscode.Position(15, 18) // poolId
);
});
test('When there are existing parameters, new parameters are added at the end', async () => {
await assertParameterConversionTransformsFileTo(
'jobtemplate_multipleparams.json',
'jobtemplate_multipleparams.after_poolid.json',
new vscode.Position(21, 18) // poolId
);
});
});
suite('Batch Utilities Tests', () => {
test('Parsing a non-JSON document as a job template fails', () => {
const result = batch.parseBatchTemplate(nonJson, 'job');
assert.equal(result.isTemplate, false);
assert.equal(result.parameters.length, 0);
});
test('Parsing job JSON returns a non-template resource', () => {
const result = batch.parseBatchTemplate(jobJson, 'job');
assert.equal(result.isTemplate, false);
assert.equal(result.parameters.length, 0);
});
test('Parsing job template JSON returns a job resource template', () => {
const template = batch.parseBatchTemplate(jobTemplateJson, 'job');
assert.equal(template.isTemplate, true);
});
test('Parsing job template JSON surfaces the parameters', () => {
const template = batch.parseBatchTemplate(jobTemplateJson, 'job');
assert.equal(template.parameters.length, 4);
const jobIdParameter = template.parameters[0];
assert.equal('jobId', jobIdParameter.name);
assert.equal('string', jobIdParameter.dataType);
assert.notEqual(undefined, jobIdParameter.metadata);
if (jobIdParameter.metadata) {
assert.equal('The id of the Batch job', jobIdParameter.metadata.description);
}
});
test('A job template can be parsed even if it has no parameters', () => {
const template = batch.parseBatchTemplate(jobTemplateJsonNoParams, 'job');
assert.equal(template.parameters.length, 0);
});
test('Parsing job template JSON captures default values', () => {
const template = batch.parseBatchTemplate(jobTemplateJson, 'job');
const parameter = template.parameters.find((p) => p.name === 'testDefaulted');
assert.notEqual(undefined, parameter);
if (parameter) {
assert.equal('mydef', parameter.defaultValue);
}
});
test('Parsing job template JSON captures allowed values', () => {
const template = batch.parseBatchTemplate(jobTemplateJson, 'job');
const parameter = template.parameters.find((p) => p.name === 'testAllowed');
assert.notEqual(undefined, parameter);
if (parameter) {
assert.notEqual(undefined, parameter.allowedValues);
if (parameter.allowedValues) {
assert.equal(3, parameter.allowedValues.length);
assert.equal('alpha', parameter.allowedValues[0]);
}
}
});
test('Parsing pool template JSON surfaces the parameters', () => {
const template = batch.parseBatchTemplate(poolTemplateJson, 'pool');
assert.equal(template.parameters.length, 1);
assert.equal('vmSize', template.parameters[0].name);
});
});
suite('Duration Parsing Tests', () => {
test('Parsing a plain time succeeds', () => {
assert.equal('PT5H', duration.toISO8601('5:00:00'));
assert.equal('PT10M', duration.toISO8601('0:10:00'));
assert.equal('PT30S', duration.toISO8601('0:00:30'));
assert.equal('PT45H10M30S', duration.toISO8601('45:10:30'));
assert.equal('PT45H10M30.5S', duration.toISO8601('45:10:30.50'));
});
test('Parsing a time containing days succeeds', () => {
assert.equal('P1DT5H', duration.toISO8601('1 day, 5:00:00'));
assert.equal('P15DT45H10M30.5S', duration.toISO8601('15 days, 45:10:30.50'));
});
test('Parsing a zero time succeeds', () => {
assert.equal('PT0S', duration.toISO8601('0:00:00'));
});
test('Parsing a MaxValue time returns nothing', () => {
assert.equal(undefined, duration.toISO8601('10675199 days, 2:48:05.477581'));
});
});

22
test/index.ts Normal file
Просмотреть файл

@ -0,0 +1,22 @@
//
// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING
//
// This file is providing the test runner to use when running extension tests.
// By default the test runner in use is Mocha based.
//
// You can provide your own test runner if you want to override it by exporting
// a function run(testRoot: string, clb: (error:Error) => void) that the extension
// host can call to run the tests. The test runner is expected to use console.log
// to report the results back to the caller. When the tests are finished, return
// a possible error to the callback or null if none.
var testRunner = require('vscode/lib/testrunner');
// You can directly control Mocha options by uncommenting the following lines
// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info
testRunner.configure({
ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.)
useColors: true // colored output from test results
});
module.exports = testRunner;

Просмотреть файл

@ -0,0 +1,21 @@
{
"parameters": {
"poolId": {
"type": "string",
"defaultValue": "wonderpool",
"metadata": {
"description": "Value for job.properties.poolInfo.poolId"
}
}
},
"job": {
"type": "Microsoft.Batch/batchAccounts/jobs",
"apiVersion": "2016-12-01",
"properties": {
"id" : "wonderjob",
"poolInfo" : {
"poolId" : "[parameters('poolId')]"
}
}
}
}

Просмотреть файл

@ -0,0 +1,14 @@
{
"parameters": {
},
"job": {
"type": "Microsoft.Batch/batchAccounts/jobs",
"apiVersion": "2016-12-01",
"properties": {
"id" : "wonderjob",
"poolInfo" : {
"poolId" : "wonderpool"
}
}
}
}

Просмотреть файл

@ -0,0 +1,21 @@
{
"parameters": {
"poolId": {
"type": "string",
"defaultValue": "wonderpool",
"metadata": {
"description": "Value for job.properties.poolInfo.poolId"
}
}
},
"job": {
"type": "Microsoft.Batch/batchAccounts/jobs",
"apiVersion": "2016-12-01",
"properties": {
"id" : "wonderjob",
"poolInfo" : {
"poolId" : "[parameters('poolId')]"
}
}
}
}

Просмотреть файл

@ -0,0 +1,13 @@
{
"parameters": { },
"job": {
"type": "Microsoft.Batch/batchAccounts/jobs",
"apiVersion": "2016-12-01",
"properties": {
"id" : "wonderjob",
"poolInfo" : {
"poolId" : "wonderpool"
}
}
}
}

Просмотреть файл

@ -0,0 +1,33 @@
{
"parameters": {
"abc" : {
"type" : "string",
"metadata" : {
"description" : "A sequence near the end of the alphabet"
}
},
"xyz" : {
"type" : "string",
"metadata" : {
"description" : "A sequence near the end of the alphabet"
}
},
"poolId": {
"type": "string",
"defaultValue": "wonderpool",
"metadata": {
"description": "Value for job.properties.poolInfo.poolId"
}
}
},
"job": {
"type": "Microsoft.Batch/batchAccounts/jobs",
"apiVersion": "2016-12-01",
"properties": {
"id" : "[parameters('abc')]",
"poolInfo" : {
"poolId" : "[parameters('poolId')]"
}
}
}
}

Просмотреть файл

@ -0,0 +1,26 @@
{
"parameters": {
"abc" : {
"type" : "string",
"metadata" : {
"description" : "A sequence near the end of the alphabet"
}
},
"xyz" : {
"type" : "string",
"metadata" : {
"description" : "A sequence near the end of the alphabet"
}
}
},
"job": {
"type": "Microsoft.Batch/batchAccounts/jobs",
"apiVersion": "2016-12-01",
"properties": {
"id" : "[parameters('abc')]",
"poolInfo" : {
"poolId" : "wonderpool"
}
}
}
}

Просмотреть файл

@ -0,0 +1,21 @@
{
"parameters": {
"poolId": {
"type": "string",
"defaultValue": "wonderpool",
"metadata": {
"description": "Value for job.properties.poolInfo.poolId"
}
}
},
"job": {
"type": "Microsoft.Batch/batchAccounts/jobs",
"apiVersion": "2016-12-01",
"properties": {
"id" : "wonderjob",
"poolInfo" : {
"poolId" : "[parameters('poolId')]"
}
}
}
}

Просмотреть файл

@ -0,0 +1,12 @@
{
"job": {
"type": "Microsoft.Batch/batchAccounts/jobs",
"apiVersion": "2016-12-01",
"properties": {
"id" : "wonderjob",
"poolInfo" : {
"poolId" : "wonderpool"
}
}
}
}

Просмотреть файл

@ -0,0 +1,27 @@
{
"parameters": {
"jobid" : {
"type" : "string",
"metadata" : {
"description" : "A unique identifier for the job"
}
},
"poolId": {
"type": "string",
"defaultValue": "wonderpool",
"metadata": {
"description": "Value for job.properties.poolInfo.poolId"
}
}
},
"job": {
"type": "Microsoft.Batch/batchAccounts/jobs",
"apiVersion": "2016-12-01",
"properties": {
"id" : "[parameters('jobid')]",
"poolInfo" : {
"poolId" : "[parameters('poolId')]"
}
}
}
}

Просмотреть файл

@ -0,0 +1,20 @@
{
"parameters": {
"jobid" : {
"type" : "string",
"metadata" : {
"description" : "A unique identifier for the job"
}
}
},
"job": {
"type": "Microsoft.Batch/batchAccounts/jobs",
"apiVersion": "2016-12-01",
"properties": {
"id" : "[parameters('jobid')]",
"poolInfo" : {
"poolId" : "wonderpool"
}
}
}
}

22
tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "out",
"lib": [
"es6"
],
"sourceMap": true,
"rootDir": ".",
"strictNullChecks": true,
"alwaysStrict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": false,
"strict": true
},
"exclude": [
"node_modules",
".vscode-test"
]
}

11
tslint.json Normal file
Просмотреть файл

@ -0,0 +1,11 @@
{
"rules": {
"no-unused-expression": true,
"no-duplicate-variable": true,
"no-unused-variable": true,
"curly": true,
"class-name": true,
"semicolon": ["always"],
"triple-equals": true
}
}