diff --git a/README.md b/README.md index 2b1a0dc..16ea33c 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ This extension requires: - [Terraform](https://www.terraform.io/downloads.html) - [Docker](http://www.docker.io) if you are running the execute test feature locally. - [GraphViz dot](http://www.graphviz.org) if you are using the visualize feature. + - NOTE: On Windows after installing the graphViz msi/zip, you will most likely need to add your PATH env variable `(Ex. c:\Program Files(x86)\GraphViz2.38\bin)` in order to use dot from the command line. ## Supported Environments diff --git a/package.json b/package.json index ae7c975..1fb20ca 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "Other" ], "activationEvents": [ + "workspaceContains:**/*.tf", "onCommand:vscode-terraform-azure.init", "onCommand:vscode-terraform-azure.plan", "onCommand:vscode-terraform-azure.apply", @@ -41,6 +42,11 @@ "default": "**/*.{tf,txt,yml,tfvars,rb}", "description": "Indicates the files that should be synchronized to Azure cloudshell using the glob pattern string, for example: **/*.{tf,txt,yml,tfvars,rb}" }, + "tf-azure.syncEnabled": { + "type": "boolean", + "default": true, + "description": "When terminal is set to `cloudshell`, indicates whether changes to files that match tf-azure.files setting glob pattern should automatically sync to cloudshell." + }, "tf-azure.test-container": { "type": "string", "default": "microsoft/terraform-test", diff --git a/src/cloudShell.ts b/src/cloudShell.ts index fbf36b3..bee0647 100644 --- a/src/cloudShell.ts +++ b/src/cloudShell.ts @@ -4,7 +4,7 @@ import { BaseShell } from "./baseShell"; import { openCloudConsole, OSes } from "./cloudConsole"; import { delay } from "./cloudConsoleLauncher"; import { aciConfig, Constants, exportContainerCmd, exportTestScript } from "./Constants"; -import { azFilePush, escapeFile, TerminalType, TFTerminal } from "./shared"; +import { azFileDelete, azFilePush, escapeFile, TerminalType, TFTerminal } from "./shared"; import { CSTerminal } from "./utilities"; @@ -32,57 +32,38 @@ export class CloudShell extends BaseShell { TerminalType.CloudShell, Constants.TerraformTerminalName); - public async pushFiles(files: vscode.Uri[]) { + public async pushFiles(files: vscode.Uri[], syncAllFiles: boolean) { this.outputChannel.appendLine("Attempting to upload files to CloudShell"); const RETRY_INTERVAL = 500; const RETRY_TIMES = 30; - // Checking if the terminal has been created - if (( this.csTerm.terminal != null) && (this.csTerm.storageAccountKey != null)) { - for (let i = 0; i < RETRY_TIMES; i++ ) { - if (this.csTerm.ws.readyState !== ws.OPEN ) { - // wait for valid ws connection - await delay (RETRY_INTERVAL); - } else { - for (const file of files.map( (a) => a.fsPath)) { - try { - if (await fsExtra.pathExists(file)) { - this.outputChannel.appendLine(`Uploading file ${file} to cloud shell`); - await azFilePush(this.csTerm.storageAccountName, - this.csTerm.storageAccountKey, - this.csTerm.fileShareName, file); - } - } catch (err) { - this.outputChannel.appendLine(err); + // Checking if the terminal has been created and user is logged in. + await this.checkInitTerm(); + + for (let i = 0; i < RETRY_TIMES; i++) { + if (this.csTerm.ws.readyState !== ws.OPEN) { + // wait for valid ws connection + await delay(RETRY_INTERVAL); + } else { + for (const file of files.map((a) => a.fsPath)) { + try { + if (await fsExtra.pathExists(file)) { + this.outputChannel.appendLine(`Uploading file ${file} to cloud shell`); + await azFilePush(this.csTerm.storageAccountName, + this.csTerm.storageAccountKey, + this.csTerm.fileShareName, file); } + } catch (err) { + this.outputChannel.appendLine(err); } + } + + if (syncAllFiles) { vscode.window.showInformationMessage( - "Uploaded all the text files in the current workspace to CloudShell"); - break; + "Synced all matched files in the current workspace to CloudShell"); } + break; } - } else { - const message = "Do you want to open CloudShell?"; - const ok: MessageItem = { title : "Yes" }; - const cancel: MessageItem = { title : "No", isCloseAffordance: true }; - vscode.window.showWarningMessage(message, ok, cancel).then( (response) => { - if ( response === ok ) { - this.startCloudShell().then((terminal) => { - this.csTerm.terminal = terminal[0]; - this.csTerm.ws = terminal[1]; - this.csTerm.storageAccountName = terminal[2]; - this.csTerm.storageAccountKey = terminal[3]; - this.csTerm.fileShareName = terminal[4]; - this.csTerm.ResourceGroup = terminal[5]; - this.outputChannel.appendLine(`Obtained cloudshell terminal, retrying push files.\n`); - this.pushFiles(files); - return; - }); - } else { - console.log("Push to cloud shell cancelled by user."); - } - }); - // TODO THIS LINE SEEMS UNNECESSARY console.log("Cloudshell terminal not opened when trying to transfer files"); } } @@ -91,7 +72,29 @@ export class CloudShell extends BaseShell { } public async deleteFiles(files: vscode.Uri[]) { - return; + const RETRY_INTERVAL = 500; + const RETRY_TIMES = 3; + + // Checking if the terminal has been created and user is logged in. + await this.checkInitTerm(); + + for (let i = 0; i < RETRY_TIMES; i++) { + if (this.csTerm.ws.readyState !== ws.OPEN) { + // wait for valid ws connection + await delay(RETRY_INTERVAL); + } else { + for (const file of files.map((a) => a.fsPath)) { + try { + this.outputChannel.appendLine(`Deleting file ${file} from cloud shell`); + await azFileDelete(this.csTerm.storageAccountName, + this.csTerm.storageAccountKey, + this.csTerm.fileShareName, file); + } catch (err) { + this.outputChannel.appendLine(err); + } + } + } + } } protected runTerraformInternal(TFCommand: string, WorkDir: string) { @@ -286,56 +289,42 @@ export class CloudShell extends BaseShell { } - protected async uploadTFFiles(TFFiles) { - console.log("Uploading files to CloudShell"); - const RETRY_INTERVAL = 500; - const RETRY_TIMES = 30; - - // Checking if the terminal has been created - if ( (this.csTerm.terminal != null) && (this.csTerm.storageAccountKey != null) ) { - for (let i = 0; i < RETRY_TIMES; i++ ) { - if (this.csTerm.ws.readyState !== ws.OPEN ) { - await delay (RETRY_INTERVAL); - } else { - for (const file of TFFiles.map( (a) => a.fsPath)) { - try { - if (fsExtra.existsSync(file)) { - console.log(`Uploading file ${file} to cloud shell`); - azFilePush(this.csTerm.storageAccountName, this.csTerm.storageAccountKey, this.csTerm.fileShareName, file); - } - } catch (err) { - console.log(err); - } - } - vscode.window.showInformationMessage("Uploaded all the text files in the current workspace to CloudShell"); - break; - } - } - } else { - const message = "Do you want to open CloudShell?"; - const ok: MessageItem = { title : "Yes" }; - const cancel: MessageItem = { title : "No", isCloseAffordance: true }; - vscode.window.showWarningMessage(message, ok, cancel).then( (response) => { - if ( response === ok ) { - this.startCloudShell().then((terminal) => { - this.csTerm.terminal = terminal[0]; - this.csTerm.ws = terminal[1]; - this.csTerm.storageAccountName = terminal[2]; - this.csTerm.storageAccountKey = terminal[3]; - this.csTerm.fileShareName = terminal[4]; - this.csTerm.ResourceGroup = terminal[5]; - console.log(`Obtained terminal and fileshare data\n`); - this.uploadTFFiles(TFFiles); - return; - }); - } - }); - console.log("Terminal not opened when trying to transfer files"); - } - } - protected stop(interval: NodeJS.Timer): void { clearInterval(interval); } + private async delayWrap(ms: number) { + await delay(500); + } + + private checkInitTerm(): Promise { + return new Promise ((resolve, reject) => { + if ((this.csTerm.terminal !== null) && (this.csTerm.storageAccountKey !== undefined)) { + resolve(); + } else { + const message = "Do you want to open CloudShell?"; + const ok: MessageItem = { title: "Yes" }; + const cancel: MessageItem = { title: "No", isCloseAffordance: true }; + vscode.window.showWarningMessage(message, ok, cancel).then((response) => { + if (response === ok) { + this.startCloudShell().then((terminal) => { + this.csTerm.terminal = terminal[0]; + this.csTerm.ws = terminal[1]; + this.csTerm.storageAccountName = terminal[2]; + this.csTerm.storageAccountKey = terminal[3]; + this.csTerm.fileShareName = terminal[4]; + this.csTerm.ResourceGroup = terminal[5]; + this.outputChannel.appendLine(`Obtained cloudshell terminal, retrying push files.\n`); + this.delayWrap(500); + resolve(); + }).catch((error) => { + reject(error); + }); + } else { + console.log("Push to cloud shell cancelled by user."); + } + }); + } + }); + } } diff --git a/src/extension.ts b/src/extension.ts index d1582c7..39c165f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,7 +3,7 @@ // Import the module and reference it with the alias vscode in your code below import * as vscode from "vscode"; -import { commands, Disposable, extensions, GlobPattern, window } from "vscode"; +import { commands, Disposable, extensions, GlobPattern, MessageItem, window } from "vscode"; import { AzureServiceClient, BaseResource } from "ms-rest-azure"; import { join } from "path"; @@ -20,6 +20,9 @@ import { isDotInstalled } from "./utils/dotUtils"; let cs: CloudShell; let is: IntegratedShell; let outputChannel: vscode.OutputChannel; +let fileWatcher: vscode.FileSystemWatcher; +let isFirstPush = true; +let _disposable: Disposable; function getShell(): BaseShell { let activeShell = null; @@ -35,6 +38,7 @@ function getShell(): BaseShell { function init(): void { cs = new CloudShell(outputChannel); is = new IntegratedShell(outputChannel); + initFileWatcher(); outputChannel.show(); } @@ -48,18 +52,13 @@ export function activate(ctx: vscode.ExtensionContext) { outputChannel.appendLine("Loading extension"); - const fileWatcher = vscode.workspace.createFileSystemWatcher(filesGlobSetting()); - - // TODO: Implement filewatcher handlers and config to automatically sync changes in workspace to cloudshell. - fileWatcher.onDidDelete((deletedUri) => { - outputChannel.appendLine("Deleted: " + deletedUri.path); - }); - fileWatcher.onDidCreate((createdUri) => { - outputChannel.appendLine("Created: " + createdUri.path); - }); - fileWatcher.onDidChange((changedUri) => { - outputChannel.appendLine("Changed: " + changedUri.path); - }); + ctx.subscriptions.push(vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration("tf-azure.files")) { + // dispose of current file watcher and re-init + fileWatcher.dispose(); + initFileWatcher(); + } + })); ctx.subscriptions.push(vscode.commands.registerCommand("vscode-terraform-azure.init", () => { getShell().runTerraformCmd("terraform init", Constants.clouddrive); @@ -106,7 +105,7 @@ export function activate(ctx: vscode.ExtensionContext) { // Create a function that will sync the files to Cloudshell if (terminalSetToCloudshell()) { vscode.workspace.findFiles(filesGlobSetting()).then((tfFiles) => { - cs.pushFiles(tfFiles); + cs.pushFiles(tfFiles, true); }); } else { vscode.window.showErrorMessage("Push function only available when using cloudshell.") @@ -122,6 +121,10 @@ export function filesGlobSetting(): GlobPattern { return vscode.workspace.getConfiguration("tf-azure").get("files") as GlobPattern; } +export function workspaceSyncEnabled(): boolean { + return vscode.workspace.getConfiguration("tf-azure").get("syncEnabled") as boolean; +} + export async function TFLogin(api: AzureAccount) { outputChannel.appendLine("Attempting - TFLogin"); if (!(await api.waitForLogin())) { @@ -129,3 +132,47 @@ export async function TFLogin(api: AzureAccount) { } outputChannel.appendLine("Succeeded - TFLogin"); } + +function initFileWatcher(): void { + // TODO: Implement filewatcher handlers and config to automatically sync changes in workspace to cloudshell. + const subscriptions: Disposable[] = []; + + fileWatcher = vscode.workspace.createFileSystemWatcher(filesGlobSetting()); + + // enable file watcher to detect file changes. + fileWatcher.onDidDelete((deletedUri) => { + if (terminalSetToCloudshell() && workspaceSyncEnabled()) { + + cs.deleteFiles([deletedUri]); + } + }, this, subscriptions); + fileWatcher.onDidCreate((createdUri) => { + pushHelper(createdUri); + }, this, subscriptions); + fileWatcher.onDidChange((changedUri) => { + pushHelper(changedUri); + }, this, subscriptions); + + this._disposable = Disposable.from(...subscriptions); +} + +function pushHelper(uri: vscode.Uri) { + if (terminalSetToCloudshell() && workspaceSyncEnabled()) { + if (isFirstPush) { + // do initial sync of workspace before enabling file watcher. + vscode.workspace.findFiles(filesGlobSetting()).then((tfFiles) => { + cs.pushFiles(tfFiles, false); + }); + isFirstPush = false; + return; + } + cs.pushFiles([uri], false); + } +} + +export function deactivate(): void { + this._disposable.dispose(); + if (fileWatcher) { + fileWatcher.dispose(); + } +}