blob: fdc350980ba039e10b45c854cbc624f42ab66bb9 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as vscode from 'vscode';
import * as cp from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import * as yaml from 'js-yaml';
import * as commandExists from 'command-exists';
import { StorageManager } from './storage';
import { updateStatesByLocalWskPropFile } from './entityExplorer';
import { RESOURCE_PATH } from './constant/path';
enum WskDeployCommands {
DEPLOY,
UNDEPLOY,
SYNC,
}
interface AuthMetadata {
apihost: string;
api_key: string;
namespace: string;
endpoint: string;
}
interface CommandExecResult {
stdout: string;
stderr: string;
}
class AuthPick {
public label = '';
public detail = '';
constructor(public readonly auth: AuthMetadata | null) {
if (auth) {
this.label = `${auth.endpoint}/${auth.namespace}`;
this.detail = `API Host: ${auth.apihost}`;
}
}
}
class DefaultAuthPick extends AuthPick {
label = 'Default';
detail = 'Use .wskprops in the home directory';
constructor() {
super(null);
}
}
class CommandError extends Error {
constructor(message?: string) {
super(message);
this.name = 'CommandError';
}
}
class CommandNotFoundError extends Error {
constructor(message?: string) {
super(message);
this.name = 'CommandNotFoundError';
}
}
class WskDeployEntity extends vscode.TreeItem {}
class WskDeployCommand extends WskDeployEntity {
constructor(
public readonly wskDeployCommand: WskDeployCommands,
public readonly label: string,
public readonly manifest: WskDeployManifest
) {
super(label, vscode.TreeItemCollapsibleState.None);
this.iconPath = this.getIconPath(wskDeployCommand);
this.manifest = manifest;
this.contextValue = 'manifestCommand';
}
private getIconPath(wskDeployCommand: WskDeployCommands): { light: string; dark: string } {
if (wskDeployCommand === WskDeployCommands.UNDEPLOY) {
return {
light: path.join(RESOURCE_PATH, 'light', 'undeploy.svg'),
dark: path.join(RESOURCE_PATH, 'dark', 'undeploy.svg'),
};
}
if (wskDeployCommand === WskDeployCommands.SYNC) {
return {
light: path.join(RESOURCE_PATH, 'light', 'sync.svg'),
dark: path.join(RESOURCE_PATH, 'dark', 'sync.svg'),
};
}
return {
light: path.join(RESOURCE_PATH, 'light', 'deploy.svg'),
dark: path.join(RESOURCE_PATH, 'dark', 'deploy.svg'),
};
}
}
class WskDeployManifest extends WskDeployEntity {
constructor(public readonly uri: vscode.Uri, auth?: AuthMetadata) {
super(
path.relative(
(vscode.workspace.workspaceFolders as vscode.WorkspaceFolder[])[0].uri.path,
uri.path
),
vscode.TreeItemCollapsibleState.Expanded
);
if (auth) {
this.auth = auth;
}
super.iconPath = {
light: path.join(RESOURCE_PATH, 'light', 'manifest.svg'),
dark: path.join(RESOURCE_PATH, 'dark', 'manifest.svg'),
};
}
auth: AuthMetadata | null = null;
contextValue = 'manifest';
}
async function showConfirmationModal(message: string, auth: AuthMetadata | null): Promise<void> {
let msg = message;
if (auth) {
msg += `\n\nnamespace: ${auth.namespace}\nAPI host: ${auth.apihost}`;
}
const yes = await vscode.window.showInformationMessage(msg, { modal: true }, 'Yes');
if (yes === 'Yes') {
return Promise.resolve();
} else {
return Promise.reject();
}
}
export class WskDeployManifestProvider implements vscode.TreeDataProvider<WskDeployEntity> {
private _filteredFiles: string[] = [];
private _manifests: WskDeployManifest[] = [];
private storageManager: StorageManager;
private _onDidChangeTreeData: vscode.EventEmitter<
WskDeployManifest | undefined
> = new vscode.EventEmitter<WskDeployManifest | undefined>();
readonly onDidChangeTreeData: vscode.Event<WskDeployManifest | undefined> = this
._onDidChangeTreeData.event;
constructor(public context: vscode.ExtensionContext) {
this.storageManager = new StorageManager(context.globalState);
context.subscriptions.push(
vscode.commands.registerCommand('wskdeploy.openManifest', (uri) =>
this.openManifest(uri)
),
vscode.commands.registerCommand('wskdeploy.refresh', () => this.refresh()),
vscode.commands.registerCommand('wskdeploy.runCommand', async (command) =>
this.runCommand(context, command, false)
),
vscode.commands.registerCommand('wskdeploy.runCommandWithDeployment', async (command) =>
this.runCommand(context, command, true)
)
);
if (vscode.workspace.workspaceFolders) {
const YAMLwatcher = vscode.workspace.createFileSystemWatcher(
new vscode.RelativePattern(vscode.workspace.workspaceFolders[0], '**/*.{yml,yaml}')
);
YAMLwatcher.onDidCreate((_) => this.refresh());
YAMLwatcher.onDidDelete((_) => this.refresh());
YAMLwatcher.onDidChange((_) => this.refresh());
}
}
private async runCommand(
context: vscode.ExtensionContext,
command: WskDeployCommand,
withDeployment: boolean
): Promise<void> {
const authList = [
new DefaultAuthPick(),
...this.getSavedAuthList().map((auth) => new AuthPick(auth)),
];
let deploymentFile;
// select deployment yaml
if (withDeployment) {
const OPEN_FILE = 'Open file...';
const selected = await vscode.window.showQuickPick(
[...this._manifests.map((m) => m.uri.path), OPEN_FILE],
{
placeHolder: 'Select the wskdeploy deployment yaml file',
}
);
if (!selected) {
return;
}
if (selected === OPEN_FILE) {
const uri = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
filters: {
Manifest: ['yaml', 'yml'],
},
});
if (!uri || uri.length === 0) {
return;
}
deploymentFile = uri[0].path;
} else {
deploymentFile = selected;
}
}
// select target namespace and host
const selected = await vscode.window.showQuickPick<AuthPick>(authList, {
placeHolder: `Select the target API host to run ${command.label} command`,
});
if (!selected) {
return;
}
const projectName = await vscode.window.showInputBox({
placeHolder: 'Please enter project name (optional)',
});
// run command
switch (command.wskDeployCommand) {
case WskDeployCommands.DEPLOY:
this.runDeploy(command.manifest, selected.auth, deploymentFile, projectName);
break;
case WskDeployCommands.UNDEPLOY:
this.runUndeploy(command.manifest, selected.auth, deploymentFile, projectName);
break;
case WskDeployCommands.SYNC:
this.runSync(command.manifest, selected.auth, deploymentFile, projectName);
break;
}
}
private async handleCommandNotFoundError(retryCallback: () => void): Promise<void> {
const BROWSE_FOR_FILE = 'Browse for file';
const DOWNLOAD_WSKDEPLOY = 'Download wskdeploy';
const answer = await vscode.window.showInformationMessage(
`The wskdeploy was not found. Would you like to find the binary file yourself?`,
{ modal: true },
BROWSE_FOR_FILE,
DOWNLOAD_WSKDEPLOY
);
if (answer === BROWSE_FOR_FILE) {
const uri = await vscode.window.showOpenDialog({
canSelectFiles: false,
canSelectMany: false,
});
if (uri) {
this.storageManager.setWskdeployPath(uri[0]);
retryCallback();
}
} else if (answer === DOWNLOAD_WSKDEPLOY) {
vscode.env.openExternal(
vscode.Uri.parse('https://github.com/apache/openwhisk-wskdeploy/releases')
);
}
}
private _runCommand(command: { (): Promise<CommandExecResult> }): void {
command()
.then((ret) => vscode.window.showInformationMessage(ret.stdout))
.catch(async (error) => {
if (error instanceof CommandNotFoundError) {
await this.handleCommandNotFoundError(() => {
this._runCommand(command);
});
} else {
vscode.window.showErrorMessage(error.message);
}
});
}
private async runDeploy(
manifest: WskDeployManifest,
auth: AuthMetadata | null,
deployment?: string,
projectName?: string
): Promise<void> {
await showConfirmationModal(
`Are you sure you want to deploy?\n(${manifest.label})`,
auth
).then(() => {
this._runCommand(() => this.deploy(manifest, auth, deployment, projectName));
});
}
private async runUndeploy(
manifest: WskDeployManifest,
auth: AuthMetadata | null,
deployment?: string,
projectName?: string
): Promise<void> {
await showConfirmationModal(
`Are you sure you want to undeploy?\n(${manifest.label})`,
auth
).then(() => {
this._runCommand(() => this.undeploy(manifest, auth, deployment, projectName));
});
}
private async runSync(
manifest: WskDeployManifest,
auth: AuthMetadata | null,
deployment?: string,
projectName?: string
): Promise<void> {
await showConfirmationModal(
`Are you sure you want to sync?\n(${manifest.label})`,
auth
).then(() => {
this._runCommand(() => this.sync(manifest, auth, deployment, projectName));
});
}
refresh(): void {
this._manifests = [];
this._onDidChangeTreeData.fire(undefined);
}
async getChildren(element?: WskDeployEntity): Promise<WskDeployEntity[]> {
updateStatesByLocalWskPropFile(this.storageManager);
if (element instanceof WskDeployManifest) {
return [
new WskDeployCommand(WskDeployCommands.DEPLOY, 'deploy', element),
new WskDeployCommand(WskDeployCommands.UNDEPLOY, 'undeploy', element),
new WskDeployCommand(WskDeployCommands.SYNC, 'sync', element),
];
}
let manifests: vscode.Uri[] = [];
try {
if (vscode.workspace.workspaceFolders) {
manifests = await vscode.workspace.findFiles(
'**/*.{yml,yaml}',
'**/node_modules/**'
);
}
} catch (e) {
return [];
}
this._manifests = manifests
.filter((file) => this.validateManifest(file.fsPath))
.filter((f) => !this._filteredFiles.includes(f.fsPath))
.sort()
.map((uri) => new WskDeployManifest(uri));
return this._manifests;
}
getTreeItem(element: WskDeployEntity): WskDeployEntity {
return element;
}
private getSavedAuthList(): AuthMetadata[] {
const endpoints = this.storageManager.getEndpoints();
// @ts-ignore
return Object.entries(endpoints).flatMap(([endpoint, data]) =>
data.namespaces.map((namespace: { api_key: string; name: string }) =>
Object({
// eslint-disable-next-line @typescript-eslint/camelcase
api_key: namespace.api_key,
namespace: namespace.name,
apihost: endpoint,
endpoint: data.alias,
})
)
);
}
private openManifest(manifest: vscode.Uri): void {
vscode.window.showTextDocument(manifest);
}
private validateManifest(path: string): boolean {
function isEmpty(obj: Record<string, any>): boolean {
return Object.entries(obj).length === 0;
}
try {
const doc = fs.readFileSync(path, { encoding: 'utf8' });
const contents = yaml.safeLoad(doc, { json: true });
if (contents === undefined) {
return false;
}
if (
'project' in contents &&
'packages' in contents.project &&
!isEmpty(contents.project.packages)
) {
return true;
}
if ('packages' in contents && !isEmpty(contents.packages)) {
return true;
}
} catch {
// ignore exception
}
return false;
}
private deploy(
manifest: WskDeployManifest,
auth: AuthMetadata | null,
deploymentFile?: string,
projectName?: string
): Promise<CommandExecResult> {
const command = '';
return this.execCommand(command, manifest, auth, deploymentFile, projectName);
}
private undeploy(
manifest: WskDeployManifest,
auth: AuthMetadata | null,
deploymentFile?: string,
projectName?: string
): Promise<CommandExecResult> {
const command = 'undeploy';
return this.execCommand(command, manifest, auth, deploymentFile, projectName);
}
private sync(
manifest: WskDeployManifest,
auth: AuthMetadata | null,
deploymentFile?: string,
projectName?: string
): Promise<CommandExecResult> {
const command = 'sync';
return this.execCommand(command, manifest, auth, deploymentFile, projectName);
}
private execCommand(
command: string,
manifest: WskDeployManifest,
auth: AuthMetadata | null,
deploymentFile: string | undefined,
projectName?: string
): Promise<CommandExecResult> {
return new Promise<CommandExecResult>((resolve, reject) => {
const wskdeployPath = this.getWskdeployPath();
const cwd = path.dirname(manifest.uri.fsPath);
commandExists(wskdeployPath)
.then(() => {
cp.exec(
this.getCommandWithAuth(
wskdeployPath,
command,
manifest,
auth,
deploymentFile,
projectName
),
{
cwd,
},
(error, stdout, stderr) => {
if (!error) {
resolve({ stdout, stderr });
return;
}
reject(new CommandError(stderr));
}
);
})
.catch(function () {
// command doesn't exist
reject(new CommandNotFoundError());
});
});
}
private getWskdeployPath(): string {
let wskdeploy = 'wskdeploy';
const wskdeployPath = this.storageManager.getWskdeployPath();
if (wskdeployPath) {
wskdeploy = vscode.Uri.file(wskdeployPath.path).fsPath;
}
return wskdeploy;
}
private getCommandWithAuth(
wskdeployPath: string,
command: string,
manifest: WskDeployManifest,
auth: AuthMetadata | null,
deploymentFile?: string,
projectName?: string
): string {
let commandline = `${wskdeployPath} ${command} -m ${manifest.uri.fsPath}`;
if (auth) {
commandline += ` --apihost ${auth.apihost} -u ${auth.api_key}`;
}
if (deploymentFile) {
commandline += ` -d ${deploymentFile}`;
}
if (projectName) {
commandline += ` --projectname ${projectName}`;
}
return commandline;
}
}