Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Azure REST samples support #178

Merged
merged 31 commits into from
Sep 21, 2018
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- [Run Playbook in Cloud Shell](#run-playbook-in-cloud-shell)
- [Run Playbook Remotely via ssh](#run-playbook-remotely-via-ssh)
- [Auto File Copy on saving](#files-copy-to-remote-on-saving)
- [Samples for azure_rm_resource](#samples-for-azure_rm_resource-preview)
- [Feedback and Questions](#feedback-and-questions)
- [License](#license)
- [Telemetry](#telemetry)
Expand Down Expand Up @@ -139,6 +140,28 @@ Enable syntax highlighting by adding `files.associations` in `settings.json`, to
```
There's notification message at right side of status bar.

## Samples for azure_rm_resource (Preview)

This option provides sample code snippets for [azure_rm_resource](https://docs.ansible.com/ansible/latest/modules/azure_rm_resource_module.html) module, those samples are from Azure Rest API spec.

To use, press **F1** and then select **Ansible: Samples for azure_rm_resource (PREVIEW)** option.
You will be first asked for API group, then required operation. After selecting operation a pickup list will be displayed where you can select particular API version and sample. Extension will create appropriate playbook based on **azure_rm_resource** or **azure_rm_resource_facts** module.

Note that all the samples generated by the extension are based on REST API samples from this repository:

https://github.com/Azure/azure-rest-api-specs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you meet error when using those samples, please file issues at xxxx/issues.


Please also refer to Azure REST API reference here:

https://docs.microsoft.com/en-us/rest/api/?view=Azure

In case of any bugs encountered in samples please fill an issue here:

https://github.com/Azure/azure-rest-api-specs/issues

Note 1: This feature requires Ansible 2.7.
Note 2: When using for the first time it may take a few minutes to retrieve REST API specifications.
zikalino marked this conversation as resolved.
Show resolved Hide resolved


## Feedback and Questions
You can submit bug or feature suggestion via [issues](https://github.com/VSChina/vscode-ansible/issues/new).
Expand Down
7 changes: 6 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"onCommand:vscode-ansible.playbook-in-docker",
"onCommand:vscode-ansible.playbook-in-localansible",
"onCommand:vscode-ansible.ssh",
"onCommand:vscode-ansible.sync-folder-ssh"
"onCommand:vscode-ansible.sync-folder-ssh",
"onCommand:vscode-ansible.resource-module-samples"
],
"main": "./out/extension",
"contributes": {
Expand Down Expand Up @@ -72,6 +73,11 @@
"command": "vscode-ansible.sync-folder-ssh",
"title": "Copy folder to Remote Host",
"category": "Ansible"
},
{
"command": "vscode-ansible.resource-module-samples",
"title": "Samples for azure_rm_resource (PREVIEW)",
"category": "Ansible"
}
],
"menus": {
Expand Down Expand Up @@ -223,6 +229,11 @@
"type": "string",
"default": "microsoft/ansible:latest",
"description": "Custom docker image when running playbook in Docker."
},
"ansible.azureRestSpec": {
"type": "string",
"default": "",
"description": "Location of Azure REST specification."
}
}
}
Expand All @@ -248,6 +259,7 @@
"azure-storage": "^2.10.1",
"fs-extra": "^4.0.2",
"fuzzaldrin-plus": "^0.6.0",
"git-clone": "^0.1.0",
"ms-rest": "^2.3.6",
"ms-rest-azure": "^2.5.7",
"opn": "^5.3.0",
Expand Down
6 changes: 6 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { SSHRunner } from './sshRunner';
import { DeploymentTemplate } from './deploymentTemplate';
import { FolderSyncer } from './folderSyncer';
import { FileSyncer } from './fileSyncer';
import { RestSamples } from './restSamples';

const documentSelector = [
{ language: 'yaml', scheme: 'file' },
Expand Down Expand Up @@ -44,6 +45,7 @@ export function activate(context: vscode.ExtensionContext) {
var deploymentTemplate = new DeploymentTemplate();
var folderSyncer = new FolderSyncer(outputChannel);
var fileSyncer = new FileSyncer(outputChannel);
var restSamples = new RestSamples(outputChannel);

context.subscriptions.push(vscode.commands.registerCommand('vscode-ansible.playbook-in-docker', (playbook) => {
dockerRunner.runPlaybook(playbook ? playbook.fsPath : null);
Expand All @@ -66,6 +68,10 @@ export function activate(context: vscode.ExtensionContext) {
folderSyncer.syncFolder(srcFolder, null, true);
}));

let disposable = vscode.commands.registerCommand('vscode-ansible.resource-module-samples', () => {
restSamples.displayMenu();
});

context.subscriptions.push(vscode.workspace.onDidChangeConfiguration((configChange) => {
if (configChange.affectsConfiguration("ansible.fileCopyConfig")) {
let config = vscode.workspace.getConfiguration('ansible').get('fileCopyConfig');
Expand Down
14 changes: 10 additions & 4 deletions src/playbookManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export class PlaybookManager {
text = vscode.window.activeTextEditor.document.getText();
}

let authorizationTaskPosition: number = text.indexOf("- name: " + name);
return authorizationTaskPosition >= 0;
let authorisationTaskPosition: number = text.indexOf("- name: " + name);
return authorisationTaskPosition >= 0;
}

// just a simple approach for now
Expand All @@ -40,27 +40,33 @@ export class PlaybookManager {
tabSize = vscode.window.activeTextEditor.options.tabSize;
}

let lineCount = 0;
if (vscode.window.activeTextEditor.document.getText() == "") {

vscode.window.activeTextEditor.edit(function (edit) {
let prefix: string = "- hosts: localhost\r" +
" ".repeat(tabSize) + "vars:\r" +
" ".repeat(tabSize * 2) + "resource_group:\r" +
" ".repeat(tabSize) + "tasks:\r";
edit.insert(new vscode.Position(0, 0), prefix);
});

lineCount = 7;
} else {
lineCount = vscode.window.activeTextEditor.document.lineCount;
}

// add spaces below
let lines: string[] = task.split('\r');

for (var i = 0; i < lines.length; i++) {
// XXX - just 2 tabs at the moment, we have to detect exactly
let prefix = " ".repeat(tabSize * 2);
lines[i] = prefix + lines[i];
}

task = '\r' + lines.join('\r');

let insertionPoint = new vscode.Position(vscode.window.activeTextEditor.document.lineCount + 1, 0);
let insertionPoint = new vscode.Position(lineCount - 1, 0);
vscode.window.activeTextEditor.insertSnippet(new vscode.SnippetString(task), insertionPoint);
}
}
Expand Down
206 changes: 206 additions & 0 deletions src/restSamples.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
'use strict';

import * as vscode from 'vscode';
import { Swagger } from './swagger';
import { PlaybookManager } from './playbookManager';
import * as utilities from './utilities';
var path = require("path");
var fs = require('fs');
var pm = new PlaybookManager();

export class RestSamples {
protected _outputChannel: vscode.OutputChannel;

constructor(outputChannel: vscode.OutputChannel) {
this._outputChannel = outputChannel;
}

public async displayMenu() {
let __this = this;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why need private __this?

zikalino marked this conversation as resolved.
Show resolved Hide resolved

let specLocation = await this.getSpecificationLocation();
__this.queryDirectory(specLocation + '/specification', false, "", function (groups) {
if (groups != null) {
vscode.window.showQuickPick(groups).then(selection => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is async function already, pls use await vscode.window.showQuickPick()

if (!selection) return;
__this.selectOperation(specLocation + "/specification/" + selection);
});
}
})
}

public selectOperation(path: string) {
let operations = this.queryAll(path);
let items = [];

if (operations) {
for (var key in operations) {
items.push(operations[key]);
}
}


if (items.length == 0) {
vscode.window.showInformationMessage("No samples available");
return;
}

vscode.window.showQuickPick(items).then(selection => {
// the user canceled the selection
if (!selection) return;

items = [];

for (var f in selection['files']) {
let swagger = require(selection['files'][f]);
if (swagger != null) {
let xpath = selection['files'][f].split('/').slice(0, -1).join('/');
let swaggerHandler = new Swagger(swagger);
let examples: string[] = swaggerHandler.getExampleNames(selection['path'], selection['method']);
let apiVersion = xpath.split('/').slice(-1)[0];

examples.forEach(function(s, i, a) {
items.push({
'label': 'API Version: ' + apiVersion + ' - ' + s.split('/').pop().split('.json')[0],
'file': selection['files'][f],
'example': require(xpath + '/' + s),
'path': selection['path'],
'method': selection['method']
});
})
}
}

vscode.window.showQuickPick(items).then(selection => {
// the user canceled the selection
if (!selection) return;

let swagger = require(selection['file']);
let swaggerHandler = new Swagger(swagger);
let playbook = swaggerHandler.generateRestApiTasks(selection['path'], selection['method'], selection['example']);
pm.insertTask(playbook);
});
});
}

public async getSpecificationLocation(): Promise<string> {
let spec = utilities.getCodeConfiguration('ansible', 'azureRestSpec');
if (spec != "") {
return spec as string;
} else {

this._outputChannel.show();
const progress = utilities.delayedInterval(() => { this._outputChannel.append('.') }, 500);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

progress should at end of below log message, otherwise you'll ...............Getting Azurexxxxx


this._outputChannel.append("Getting Azure REST API specifications.");
let clone = require('git-clone');
let home: string = path.join(process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'], 'azure-rest-api-specs');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

under .vscode to make it hide?

clone("https://github.com/Azure/azure-rest-api-specs.git", home, null, (result) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async function, pls use await instead of callback

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this library only supports callback

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then pls remove async on function declaration

progress.cancel();
if (!result) {
utilities.updateCodeConfiguration('ansible', 'azureRestSpec', home, true);
this._outputChannel.appendLine("");
this._outputChannel.appendLine("REST API feature ready");
} else {
this._outputChannel.appendLine("Failed to acquire REST API specifications");
}
return home;
})
}
}

private queryAll(path: string) {
let operations = {};
let __this = this;
yungezz marked this conversation as resolved.
Show resolved Hide resolved

__this.queryApiGroup(path, function (dirs: string[]) {
dirs.forEach(dir => {
__this.queryDirectory(dir, true, ".json", function(files) {
if (files != null) {
files.forEach(file => {
let swagger = require(dir + '/' + file);
for (var path in swagger.paths) {
for (var method in swagger.paths[path]) {
// add only if there are examples
if (swagger.paths[path][method]['x-ms-examples']) {
let operationId: string = swagger.paths[path][method].operationId;
let description: string = swagger.paths[path][method].description;

if (!operations[operationId]) {
operations[operationId] = { 'label': operationId, 'description': description, 'files': [], 'path': path, 'method': method }
}

operations[operationId]['files'].push(dir + '/' + file);
}
}
}
});
};
});
});
});

return operations;
}

private queryApiGroup(path, cb) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the 2 function names confusing.. why not use queryDirectory and queryApiGroup?

yungezz marked this conversation as resolved.
Show resolved Hide resolved
this.queryApiGroupInternal([ path ], [], cb);
}

private queryApiGroupInternal(dirsToQuery: string[], finalDirs: string[], cb) {

let __this = this;
// if no more dirs to query, just respond via callback
if (dirsToQuery.length == 0) {
cb(finalDirs);
return;
}

// get first dir to query
let nextDir: string = dirsToQuery.pop();

this.queryDirectory(nextDir, false, "", function(dir) {
if (dir == null) {
cb(null);
vscode.window.showErrorMessage("Failed to query: " + nextDir);
return;
} else {
let depth: number = nextDir.split('/specification/')[1].split('/').length;

if (depth < 4) {
for (var i = 0; i < dir.length; i++) dirsToQuery.push(nextDir + '/' + dir[i])
} else {
for (var i = 0; i < dir.length; i++) finalDirs.push(nextDir + '/' + dir[i])
}

__this.queryApiGroupInternal(dirsToQuery, finalDirs, cb);
}
})
}

private queryDirectory(path: string, files: boolean, ext: string, cb) {
// just use filesystem
try {
let dirEntries = fs.readdirSync(path);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check path exists before read?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's why try is here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps there should be a message and we could clear the configuration if not there?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so you'd like the caller handle when path not exists?

let directories = [];

for (var d in dirEntries) {
if (ext != null && ext != "" && dirEntries[d].indexOf(ext) != (dirEntries[d].length - ext.length))
continue;

if (!files) {
if (fs.lstatSync(path + '/' + dirEntries[d]).isDirectory()) {
directories.push(dirEntries[d]);
}
} else {
if (!fs.lstatSync(path + '/' + dirEntries[d]).isDirectory()) {
directories.push(dirEntries[d]);
}
}
}
cb(directories);
} catch (e) {
cb(null);
}
}
}
Loading