@zowe/imperative
Version:
framework for building configurable CLIs
265 lines • 15.1 kB
JavaScript
;
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.onlyForTesting = exports.updateExtendersJson = void 0;
exports.install = install;
const PMFConstants_1 = require("../PMFConstants");
const path = require("path");
const fs = require("fs");
const jsonfile_1 = require("jsonfile");
const logger_1 = require("../../../../../logger");
const error_1 = require("../../../../../error");
const NpmFunctions_1 = require("../NpmFunctions");
const ConfigSchema_1 = require("../../../../../config/src/ConfigSchema");
const PluginManagementFacility_1 = require("../../PluginManagementFacility");
const ConfigurationLoader_1 = require("../../../ConfigurationLoader");
const UpdateImpConfig_1 = require("../../../UpdateImpConfig");
const security_1 = require("../../../../../security");
const semver = require("semver");
const config_1 = require("../../../../../config");
// Helper function to update extenders.json object during plugin install.
// Returns true if the object was updated, and false otherwise
const updateExtendersJson = (extendersJson, packageInfo, profile) => {
if (!(profile.type in extendersJson.profileTypes)) {
// If the type doesn't exist, add it to extenders.json and return
extendersJson.profileTypes[profile.type] = {
from: [packageInfo.name],
version: profile.schema.version
};
return true;
}
// Otherwise, only update extenders.json if the schema version is newer
const existingTypeInfo = extendersJson.profileTypes[profile.type];
if (semver.valid(existingTypeInfo.version)) {
if (profile.schema.version && semver.lt(profile.schema.version, existingTypeInfo.version)) {
return false;
}
}
extendersJson.profileTypes[profile.type] = {
from: [packageInfo.name],
version: profile.schema.version
};
return true;
};
exports.updateExtendersJson = updateExtendersJson;
/**
* Common function that abstracts the install process. This function should be called for each
* package that needs to be installed. (ex: `sample-cli plugin install a b c d` -> calls install 4
* times)
*
* @TODO work needs to be done to support proper sharing of the plugins.json. As of now local plugins can only be reinstalled on the same machine.
* (due to how the conversion to an absolute URI happens)
*
* @param {string} packageLocation A package name or location. This value can be a valid npm package
* name or the location of an npm tar file or project folder. Also,
* git URLs are also acceptable here (basically anything that `npm
* install` supports). If this parameter is a relative path, it will
* be converted to an absolute path prior to being passed to the
* `npm install` command.
*
* @param {INpmRegistryInfo} registryInfo The npm registry to use.
*
* @param {boolean} [installFromFile=false] If installing from a file, the package location is
* automatically interpreted as an absolute location.
* It is assumed that the plugin.json file was previously
* generated by this function which always ensures an
* absolute path. Also, if this is true, we will not update
* the plugins.json file since we are not adding/modifying
* it.
* @returns {string} The name of the plugin.
*/
function install(packageLocation_1, registryInfo_1) {
return __awaiter(this, arguments, void 0, function* (packageLocation, registryInfo, installFromFile = false, verbose = false) {
const iConsole = logger_1.Logger.getImperativeLogger();
let npmPackage = packageLocation;
iConsole.debug(`Installing package: ${packageLocation}`);
// Do some parsing on the package location in the case it isn't an absolute location
// If
// we are not installing from a file
// and the location is not absolute.
// Then
// we will try to convert the URI (which is a file path by the above criteria)
// to an absolute file path. If we can't resolve it locally, we'll leave it up to npm
// to do what's best.
if (!installFromFile &&
!path.isAbsolute(packageLocation)) {
const tempLocation = path.resolve(npmPackage);
iConsole.debug(`Package is not absolute, let's see if this is a local file: ${tempLocation}`);
// Now that we have made the location absolute...does it actually exist
if (fs.existsSync(tempLocation)) {
npmPackage = tempLocation;
iConsole.info(`Installing local package: ${npmPackage}`);
}
}
try {
iConsole.debug(`Installing from registry ${registryInfo.location}`);
// Perform the npm install.
iConsole.info("Installing packages...this may take some time.");
(0, NpmFunctions_1.installPackages)(npmPackage, Object.assign({ prefix: PMFConstants_1.PMFConstants.instance.PLUGIN_INSTALL_LOCATION }, registryInfo.npmArgs), verbose);
// We fetch the package name and version of newly installed plugin
const packageInfo = (0, NpmFunctions_1.getPackageInfo)(npmPackage);
const packageName = packageInfo.name;
let packageVersion = packageInfo.version;
iConsole.debug("Reading in the current configuration.");
const installedPlugins = (0, jsonfile_1.readFileSync)(PMFConstants_1.PMFConstants.instance.PLUGIN_JSON);
// Set the correct name and version by checking if package is an npm package, this is done
// by searching for a / or \ as those are not valid characters for an npm package, but they
// would be for a url or local file.
if (packageLocation.search(/(\\|\/)/) === -1) {
// Getting here means that the package installed was an npm package. So the package property
// of the json file should be the same as the package name.
npmPackage = packageName;
const passedVersionIdx = packageLocation.indexOf("@");
if (passedVersionIdx !== -1) {
packageVersion = packageLocation.substring(passedVersionIdx + 1);
}
}
iConsole.debug(`Package version: ${packageVersion}`);
const newPlugin = {
package: npmPackage,
location: registryInfo.location,
version: packageVersion
};
iConsole.debug("Updating the current configuration with new plugin:\n" +
JSON.stringify(newPlugin, null, 2));
installedPlugins[packageName] = newPlugin;
iConsole.debug("Updating configuration file = " + PMFConstants_1.PMFConstants.instance.PLUGIN_JSON);
(0, jsonfile_1.writeFileSync)(PMFConstants_1.PMFConstants.instance.PLUGIN_JSON, installedPlugins, {
spaces: 2
});
// get the plugin's Imperative config definition
const requirerFunction = PluginManagementFacility_1.PluginManagementFacility.instance.requirePluginModuleCallback(packageName);
const pluginImpConfig = ConfigurationLoader_1.ConfigurationLoader.load(null, packageInfo, requirerFunction);
iConsole.debug(`Checking for global Zowe client configuration files to update.`);
if (PMFConstants_1.PMFConstants.instance.PLUGIN_USING_CONFIG) {
// Update the Imperative Configuration to add the profiles introduced by the recently installed plugin
// This might be needed outside of PLUGIN_USING_CONFIG scenarios, but we haven't had issues with other APIs before
const globalLayer = PMFConstants_1.PMFConstants.instance.PLUGIN_CONFIG.layers.find((layer) => layer.global && layer.exists);
if (globalLayer && Array.isArray(pluginImpConfig.profiles)) {
UpdateImpConfig_1.UpdateImpConfig.addProfiles(pluginImpConfig.profiles);
const schemaInfo = PMFConstants_1.PMFConstants.instance.PLUGIN_CONFIG.getSchemaInfo();
if (schemaInfo.local && fs.existsSync(schemaInfo.resolved)) {
let loadedSchema;
try {
// load schema from disk to prevent removal of profile types from other applications
loadedSchema = ConfigSchema_1.ConfigSchema.loadSchema((0, jsonfile_1.readFileSync)(schemaInfo.resolved));
}
catch (err) {
iConsole.error("Error when adding new profile type for plugin %s: failed to parse schema", newPlugin.package);
}
// Only update global schema if we were able to load it from disk
if (loadedSchema != null) {
const extendersJson = config_1.ConfigUtils.readExtendersJson();
// Determine new profile types to add to schema
let shouldUpdate = false;
for (const profile of pluginImpConfig.profiles) {
const existingType = loadedSchema.find((obj) => obj.type === profile.type);
if (existingType == null) {
loadedSchema.push(profile);
}
else {
if (semver.valid(existingType.schema.version)) {
if (semver.valid(profile.schema.version) && semver.gt(profile.schema.version, existingType.schema.version)) {
existingType.schema = profile.schema;
existingType.schema.version = profile.schema.version;
}
}
else {
existingType.schema = profile.schema;
existingType.schema.version = profile.schema.version;
}
}
shouldUpdate = (0, exports.updateExtendersJson)(extendersJson, packageInfo, profile) || shouldUpdate;
}
if (shouldUpdate) {
// Update extenders.json (if necessary) after installing the plugin
config_1.ConfigUtils.writeExtendersJson(extendersJson);
}
const schema = ConfigSchema_1.ConfigSchema.buildSchema(loadedSchema);
ConfigSchema_1.ConfigSchema.updateSchema({ layer: "global", schema });
}
}
}
}
// call the plugin's postInstall function
yield callPluginPostInstall(packageName, pluginImpConfig);
iConsole.info("Plugin '" + packageName + "' successfully installed.");
return packageName;
}
catch (e) {
throw new error_1.ImperativeError({
msg: e.message,
causeErrors: e
});
}
});
}
/**
* Call a plugin's lifecycle hook to enable a plugin to take some action
* after the plugin has been installed.
*
* @param pluginPackageNm The package name of the plugin being installed.
* @param pluginImpConfig The imperative configuration for this plugin.
*
* @throws ImperativeError.
*/
function callPluginPostInstall(pluginPackageNm, pluginImpConfig) {
return __awaiter(this, void 0, void 0, function* () {
const impLogger = logger_1.Logger.getImperativeLogger();
if (pluginImpConfig.pluginLifeCycle === undefined) {
// pluginPostInstall was not defined by the plugin
const credMgrInfo = security_1.CredentialManagerOverride.getCredMgrInfoByPlugin(pluginPackageNm);
if (credMgrInfo !== null) {
// this plugin is a known cred mgr override
throw new error_1.ImperativeError({
msg: `The plugin '${pluginPackageNm}' attempted to override the CLI ` +
`Credential Manager without providing a 'pluginLifeCycle' class. ` +
`The previous Credential Manager remains in place.`
});
}
return;
}
// call the plugin's postInstall operation
try {
impLogger.debug(`Calling the postInstall function for plugin '${pluginPackageNm}'`);
const requirerFun = PluginManagementFacility_1.PluginManagementFacility.instance.requirePluginModuleCallback(pluginPackageNm);
const lifeCycleClass = requirerFun(pluginImpConfig.pluginLifeCycle);
const lifeCycleInstance = new lifeCycleClass();
yield lifeCycleInstance.postInstall();
}
catch (err) {
throw new error_1.ImperativeError({
msg: `Unable to perform the post-install action for plugin '${pluginPackageNm}'.` +
`\nReason: ${err.message}`
});
}
});
}
/* The following functions are private to this module. Breaking changes
* might be made at any time to any of the following functions.
* Make no attempt to to call them externally.
* They are only exported here to enable automated testing.
* Only test programs should access 'onlyForTesting'.
*/
exports.onlyForTesting = {
callPluginPostInstall: callPluginPostInstall
};
//# sourceMappingURL=install.js.map