UNPKG

@zowe/imperative

Version:
906 lines 61.3 kB
"use strict"; /* * 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. * */ Object.defineProperty(exports, "__esModule", { value: true }); exports.PluginManagementFacility = void 0; const UpdateImpConfig_1 = require("../UpdateImpConfig"); const path_1 = require("path"); const utilities_1 = require("../../../utilities"); const logger_1 = require("../../../logger"); const fs_1 = require("fs"); const PMFConstants_1 = require("./utilities/PMFConstants"); const jsonfile_1 = require("jsonfile"); const PluginIssues_1 = require("./utilities/PluginIssues"); const ConfigurationValidator_1 = require("../ConfigurationValidator"); const ConfigurationLoader_1 = require("../ConfigurationLoader"); const DefinitionTreeResolver_1 = require("../DefinitionTreeResolver"); const settings_1 = require("../../../settings"); const io_1 = require("../../../io"); const security_1 = require("../../../security"); /** * This class is the main engine for the Plugin Management Facility. The * underlying class should be treated as a singleton and should be accessed * via PluginManagmentFacility.instance. */ class PluginManagementFacility { constructor() { /** * Internal reference to the set of configuration properties for all loaded plugins. */ this.mAllPluginCfgProps = []; /** * Internal reference to the overrides provided by plugins. */ this.mPluginOverrides = {}; /** * Used as a short-name access to PMF constants. */ this.pmfConst = PMFConstants_1.PMFConstants.instance; /** * The CLI command tree with module globs already resolved. * * @private * @type {ICommandDefinition} */ this.resolvedCliCmdTree = null; /** * The property name within package.json that holds the * Imperative configuration object. * * @private * @type {string} */ this.impConfigPropNm = "imperative"; /** * Used for internal imperative logging. * * @private * @type {Logger} */ this.impLogger = logger_1.Logger.getImperativeLogger(); /** * A class with recorded issues for each plugin for which problems were detected. * * @private * @type {IPluginIssues} */ this.pluginIssues = PluginIssues_1.PluginIssues.instance; /** * A set of Zowe dependencies used by plugins. Each item in the * set contains the dependency's property name, and the the version * of that dependency. * * @type {Object} */ this.npmPkgNmProp = "name"; this.noPeerDependency = "-1"; /** * The semantic versioning module (which does not have the * typing to do an 'import'). */ this.semver = require("semver"); /** * Tracker to ensure that [init]{@link PluginManagementFacility#init} was * called. Most methods cannot be used unless init was called first. * * @private * @type {boolean} */ this.wasInitCalled = false; } /** * Gets a single instance of the PMF. On the first call of * PluginManagementFacility.instance, a new PMF is initialized and returned. * Every subsequent call will use the one that was first created. * * @returns {PluginManagementFacility} - The newly initialized PMF object. */ static get instance() { if (this.mInstance == null) { this.mInstance = new PluginManagementFacility(); } return this.mInstance; } /** * Get the set of configuration properties for all loaded plugins. */ get allPluginCfgProps() { return this.mAllPluginCfgProps; } /** * Object that defines what overrides will be provided by all plugins. */ get pluginOverrides() { return this.mPluginOverrides; } // __________________________________________________________________________ /** * Initialize the PMF. Must be called to enable the various commands provided * by the facility. */ init() { this.impLogger.debug("PluginManagementFacility.init() - Start"); // Load lib after the fact to save on speed when plugins not enabled const { PluginRequireProvider } = require("./PluginRequireProvider"); // Create the hook for imperative and the application cli PluginRequireProvider.createPluginHooks([ PMFConstants_1.PMFConstants.instance.IMPERATIVE_PKG_NAME, PMFConstants_1.PMFConstants.instance.CLI_CORE_PKG_NAME ]); // Add the plugin group and related commands. UpdateImpConfig_1.UpdateImpConfig.addCmdGrp({ name: "plugins", type: "group", summary: "Install and manage plug-ins", description: "Install and manage plug-ins.", children: [ // Done dynamically so that PMFConstants can be initialized require("./cmd/install/install.definition").installDefinition, require("./cmd/list/list.definition").listDefinition, require("./cmd/uninstall/uninstall.definition").uninstallDefinition, require("./cmd/update/update.definition").updateDefinition, require("./cmd/validate/validate.definition").validateDefinition, require("./cmd/showfirststeps/showfirststeps.definition").firststepsDefinition ] }); // When everything is done set this variable to true indicating successful // initialization. this.wasInitCalled = true; this.impLogger.debug("PluginManagementFacility.init() - Success"); } // __________________________________________________________________________ /** * Add all installed plugins' commands and profiles into the host CLI's command tree. * * @param resolvedCliCmdTree - The CLI command tree with * module globs already resolved. */ addAllPluginsToHostCli(resolvedCliCmdTree) { // Store the host CLI command tree. Later functions will use it. this.resolvedCliCmdTree = resolvedCliCmdTree; // Loop through each plugin and add it to the CLI command tree for (const nextPluginCfgProps of this.mAllPluginCfgProps) { this.addPluginToHostCli(nextPluginCfgProps); // log the issue list for this plugin const issueListForPlugin = this.pluginIssues.getIssueListForPlugin(nextPluginCfgProps.pluginName); if (issueListForPlugin.length > 0) { this.impLogger.warn("addAllPluginsToHostCli: Issues for plugin = '" + nextPluginCfgProps.pluginName + "':\n" + JSON.stringify(issueListForPlugin, null, 2)); } else { this.impLogger.info("addAllPluginsToHostCli: Plugin = '" + nextPluginCfgProps.pluginName + "' was successfully validated with no issues."); } } } // __________________________________________________________________________ /** * Loads the configuration properties of each plugin. The configuration * information is used when overriding a piece of the imperative * infrastructure with a plugin's capability, when validating each plugin, * and when adding each plugin's commands to the CLI command tree. * Errors are recorded in PluginIssues. */ loadAllPluginCfgProps() { // Initialize the plugin.json file if needed if (!(0, fs_1.existsSync)(this.pmfConst.PLUGIN_JSON)) { if (!(0, fs_1.existsSync)(this.pmfConst.PMF_ROOT)) { this.impLogger.debug("Creating PMF_ROOT directory"); io_1.IO.createDirSync(this.pmfConst.PMF_ROOT); } this.impLogger.debug("Creating PLUGIN_JSON file"); (0, jsonfile_1.writeFileSync)(this.pmfConst.PLUGIN_JSON, {}); } const loadedOverrides = {}; // iterate through all of our installed plugins for (const nextPluginNm of Object.keys(this.pluginIssues.getInstalledPlugins())) { const nextPluginCfgProps = this.loadPluginCfgProps(nextPluginNm); if (nextPluginCfgProps) { this.mAllPluginCfgProps.push(nextPluginCfgProps); // Keep the overrides in a temporary object indexed by plugin name loadedOverrides[nextPluginNm] = nextPluginCfgProps.impConfig.overrides; this.impLogger.trace("Next plugin's configuration properties:\n" + JSON.stringify(nextPluginCfgProps, null, 2)); } else { this.impLogger.error("loadAllPluginCfgProps: Unable to load the configuration for the plug-in named '" + nextPluginNm + "' The plug-in was not added to the host CLI."); } } // Loop through each overrides settings specified by all plugins. // This was designed to handle different types of overrides, // but we currently only process CredentialManager overrides. let overrideDispNm; let overridePluginNm; for (const [settingNm, settingVal] of Object.entries(settings_1.AppSettings.instance.getNamespace("overrides"))) { overrideDispNm = settingVal; overridePluginNm = settingVal; let credMgrIsUnknown = false; if (settingVal !== false && settingVal !== utilities_1.ImperativeConfig.instance.hostPackageName) { // A setting has been specified to override a built-in capability this.impLogger.debug(`Attempting to replace "${settingNm}" with an override named "${overridePluginNm}"`); if (settingNm === security_1.CredentialManagerOverride.CRED_MGR_SETTING_NAME) { /* For credMgr override, the value of the setting is the override display name. * We must use the display name to find this override within our loadedOverrides. * We must find the plugin name from within our known credMgr overrides. */ const credMgrInfo = security_1.CredentialManagerOverride.getCredMgrInfoByDisplayName(overrideDispNm); if (credMgrInfo === null) { credMgrIsUnknown = true; } else { // record the known plugin name that we found for this display name overridePluginNm = credMgrInfo.credMgrPluginName; } } if (!Object.prototype.hasOwnProperty.call(loadedOverrides, overridePluginNm)) { // The overrideName specified in our settings is not available from any plugin. this.useOverrideThatFails(settingNm, overrideDispNm, overridePluginNm, `No plugin has been installed that overrides '${settingNm}' with '${overrideDispNm}'.` + "\nPlugins that provide overrides are:\n" + JSON.stringify(loadedOverrides, null, 2)); continue; } if (credMgrIsUnknown) { // We found the plugin specified for a credMgr setting, but the plugin is unknown const unknownCredMgrMsg = `Your configured '${settingNm}' setting specified a ` + `plugin named '${overridePluginNm}' that is not a known credential manager. ` + `You should only use this plugin for testing until it is added to ` + `${utilities_1.ImperativeConfig.instance.rootCommandName}'s list of known credential managers. ` + `If that plugin does not implement a credential manager override class, the built-in ` + `${utilities_1.ImperativeConfig.instance.rootCommandName} credential manager will be used.`; this.impLogger.warn(unknownCredMgrMsg); // We also want the warning displayed to the user logger_1.Logger.getConsoleLogger().warn(unknownCredMgrMsg); } // Like the cli the overrides can be the actual class or the string path let loadedSetting = loadedOverrides[overridePluginNm][settingNm]; // If the overrides loaded is a string path, just resolve it here since it would be much // to do so in the overrides loader. if (typeof loadedSetting === "string") { let pathToPluginOverride = loadedSetting; try { if (!(0, path_1.isAbsolute)(pathToPluginOverride)) { this.impLogger.trace(`PluginOverride: Resolving ${pathToPluginOverride} in ${overridePluginNm}`); // This is actually kind of disgusting. What is happening is that we are getting the // entry file of the plugin using require.resolve since the modules loaded are different // when using node or ts-node. This require gets us the index.js/index.ts file that // the plugin defines. So we then cd up a directory and resolve the path relative // to the plugin entry file. pathToPluginOverride = (0, path_1.join)(require.resolve(this.formPluginRuntimePath(overridePluginNm)), "../", pathToPluginOverride); } loadedSetting = require(pathToPluginOverride); this.impLogger.info(`PluginOverride: Overrode "${settingNm}" ` + `with "${pathToPluginOverride}" from plugin "${overridePluginNm}"`); } catch (requireError) { this.useOverrideThatFails(settingNm, overrideDispNm, overridePluginNm, `Unable to load class from '${pathToPluginOverride}'. ${requireError.message}`); continue; } } // Save the setting in the mPluginsOverrides object that was stored previously in // the loadedOverrides object as the plugin name. this.mPluginOverrides[settingNm] = loadedSetting; } // end overriding was specified } // end for loop of settings this.impLogger.info("All plugin configurations have been loaded. Details at trace level of logging."); } // __________________________________________________________________________ /** * Produces a function that requires a module from a plugin using a relative * path name from the plugin's root to the module. Used as a callback function * from the ConfigurationLoader to load configuration handlers. * * @param {string} pluginName - The name of the plugin/module to load. * * @returns {function} - The method responsible for requiring the module */ requirePluginModuleCallback(pluginName) { return (relativePath) => { const pluginModuleRuntimePath = this.formPluginRuntimePath(pluginName, relativePath); try { return require(pluginModuleRuntimePath); } catch (requireError) { PluginIssues_1.PluginIssues.instance.recordIssue(pluginName, PluginIssues_1.IssueSeverity.CMD_ERROR, "Unable to load the following module for plug-in '" + pluginName + "' :\n" + pluginModuleRuntimePath + "\n" + "Reason = " + requireError.message); return {}; } }; } // __________________________________________________________________________ /** * Add the specified plugin to the imperative command tree. * * @param {IPluginCfgProps} pluginCfgProps - The configuration properties for this plugin */ addPluginToHostCli(pluginCfgProps) { /* Form a top-level command group for this plugin. * Resolve all means of command definition into the pluginCmdGroup.children */ let pluginCmdGroup = null; try { pluginCmdGroup = { name: pluginCfgProps.impConfig.name, description: pluginCfgProps.impConfig.rootCommandDescription, type: "group", children: DefinitionTreeResolver_1.DefinitionTreeResolver.combineAllCmdDefs(this.formPluginRuntimePath(pluginCfgProps.pluginName, "./lib"), pluginCfgProps.impConfig.definitions, pluginCfgProps.impConfig.commandModuleGlobs, utilities_1.ImperativeConfig.instance.loadedConfig.baseProfile != null) }; /** * Fill in the optional aliases and summary fields, * if specified. */ if (pluginCfgProps.impConfig.pluginSummary != null) { this.impLogger.debug("Adding summary from pluginSummary field of configuration"); pluginCmdGroup.summary = pluginCfgProps.impConfig.pluginSummary; } if (pluginCfgProps.impConfig.pluginAliases != null) { this.impLogger.debug("Adding aliases from pluginAliases field of configuration"); pluginCmdGroup.aliases = pluginCfgProps.impConfig.pluginAliases; } } catch (impErr) { const errMsg = "Failed to combine command definitions. Reason = " + impErr.message; this.impLogger.error("addPluginToHostCli: DefinitionTreeResolver.combineAllCmdDefs: " + errMsg); this.pluginIssues.recordIssue(pluginCfgProps.pluginName, PluginIssues_1.IssueSeverity.CMD_ERROR, errMsg); return; } // validate the plugin's configuration if (this.validatePlugin(pluginCfgProps, pluginCmdGroup) === false) { this.impLogger.error("addPluginToHostCli: The plug-in named '" + pluginCfgProps.pluginName + "' failed validation and was not added to the host CLI app."); return; } if (pluginCmdGroup.children.length <= 0) { this.impLogger.info("addPluginToHostCli: The plugin '" + pluginCfgProps.pluginName + "' has no commands, so no new commands will be added to the host CLI app."); } else { // add the new plugin group into the imperative command tree this.impLogger.info("addPluginToHostCli: Adding commands for plug-in '" + pluginCfgProps.pluginName + "' to CLI command tree. Plugin command details at trace level of logging."); this.impLogger.trace("addPluginToHostCli: Commands for plugin = '" + pluginCfgProps.pluginName + "':\n" + JSON.stringify(pluginCmdGroup, null, 2)); if (!this.addCmdGrpToResolvedCliCmdTree(pluginCfgProps.pluginName, pluginCmdGroup)) { return; } } // add the profiles for this plugin to our imperative config object if (pluginCfgProps.impConfig.profiles && pluginCfgProps.impConfig.profiles.length > 0) { this.impLogger.trace("addPluginToHostCli: Adding these profiles for plug-in = '" + pluginCfgProps.pluginName + "':\n" + JSON.stringify(pluginCfgProps.impConfig.profiles, null, 2)); try { UpdateImpConfig_1.UpdateImpConfig.addProfiles(pluginCfgProps.impConfig.profiles); } catch (impErr) { const errMsg = "Failed to add profiles for the plug-in = '" + pluginCfgProps.pluginName + "'.\nReason = " + impErr.message + "\nBecause of profile error, removing commands for this plug-in"; this.impLogger.error("addPluginToHostCli: " + errMsg); this.pluginIssues.recordIssue(pluginCfgProps.pluginName, PluginIssues_1.IssueSeverity.CMD_ERROR, errMsg); this.removeCmdGrpFromResolvedCliCmdTree(pluginCmdGroup); } } } // __________________________________________________________________________ /** * Add a new command group into the host CLI's resolved command tree. * We had to wait until the host CLI was resolved, so that we could check for * name conflicts. So each plugin's commands are added to the host CLI * command tree after both have been resolved. * * @param {string} pluginName - the name of the plugin to initialize * * @param {ICommandDefinition} cmdDefToAdd - command definition group to to be added. * * @returns True upon success. False upon error, and errors are recorded in pluginIssues. */ addCmdGrpToResolvedCliCmdTree(pluginName, cmdDefToAdd) { if (this.resolvedCliCmdTree == null) { const errMsg = "The resolved command tree was null. " + "Imperative should have created an empty command definition array."; this.impLogger.error("addCmdGrpToResolvedCliCmdTree: While adding plugin = '" + pluginName + "', " + errMsg); this.pluginIssues.recordIssue(pluginName, PluginIssues_1.IssueSeverity.CMD_ERROR, errMsg); return false; } if (this.resolvedCliCmdTree.children == null) { const errMsg = "The resolved command tree children was null. " + "Imperative should have created an empty children array."; this.impLogger.error("addCmdGrpToResolvedCliCmdTree: While adding plugin = '" + pluginName + "', " + errMsg); this.pluginIssues.recordIssue(pluginName, PluginIssues_1.IssueSeverity.CMD_ERROR, errMsg); return false; } const cmdDefInx = this.resolvedCliCmdTree.children.findIndex((existingCmdDef) => { return existingCmdDef.name === cmdDefToAdd.name; }); if (cmdDefInx > -1) { const errMsg = "The command group = '" + cmdDefToAdd.name + "' already exists. Plugin management should have already rejected this plugin."; this.impLogger.error("addCmdGrpToResolvedCliCmdTree: " + errMsg); this.pluginIssues.recordIssue(pluginName, PluginIssues_1.IssueSeverity.CMD_ERROR, errMsg); return false; } this.impLogger.debug("Adding definition = '" + cmdDefToAdd.name + "' to the resolved command tree."); this.resolvedCliCmdTree.children.push(cmdDefToAdd); return true; } // __________________________________________________________________________ /** * Compare the version of a plugin version property with a version property * of its base CLI. * * If the versions do not satisfy the semver rules, then a PluginIssue is recorded. * * @param pluginName - The name of the plugin. * @param pluginVerPropNm - The name of the plugin property containing a version. * @param pluginVerRange - value of the plugin's version. * @param cliVerPropNm - The name of the base CLI property containing a version. * @param cliVerValue - value of the base CLI's version. */ comparePluginVersionToCli(pluginName, pluginVerPropNm, pluginVerRange, cliVerPropNm, cliVerValue) { const cliCmdName = utilities_1.ImperativeConfig.instance.rootCommandName; try { if (!this.semver.satisfies(cliVerValue, pluginVerRange)) { this.pluginIssues.recordIssue(pluginName, PluginIssues_1.IssueSeverity.WARNING, "The version value (" + pluginVerRange + ") of the plugin's '" + pluginVerPropNm + "' property is incompatible with the version value (" + cliVerValue + ") of the " + cliCmdName + " command's '" + cliVerPropNm + "' property."); } } catch (semverExcept) { PluginIssues_1.PluginIssues.instance.recordIssue(pluginName, PluginIssues_1.IssueSeverity.WARNING, "Failed to compare the version value (" + pluginVerRange + ") of the plugin's '" + pluginVerPropNm + "' property with the version value (" + cliVerValue + ") of the " + cliCmdName + " command's '" + cliVerPropNm + "' property.\n" + "This can occur when one of the specified values is not a valid version string.\n" + "Reported reason = " + semverExcept.message); } } // __________________________________________________________________________ /** * Get the package name of our base CLI. * * @returns The CLI package name contained in the package.json 'name' property. */ getCliPkgName() { const cliPackageJson = utilities_1.ImperativeConfig.instance.callerPackageJson; if (!Object.prototype.hasOwnProperty.call(cliPackageJson, this.npmPkgNmProp)) { return "NoNameInCliPkgJson"; } return cliPackageJson[this.npmPkgNmProp]; } // __________________________________________________________________________ /** * Remove a command group that was previously added. * We remove a command group if we discover errors after * adding the command group. * * @param {ICommandDefinition} cmdDefToRemove - command definition to be removed. */ removeCmdGrpFromResolvedCliCmdTree(cmdDefToRemove) { if (this.resolvedCliCmdTree && this.resolvedCliCmdTree.children && this.resolvedCliCmdTree.children.length > 0) { const cmdDefInx = this.resolvedCliCmdTree.children.findIndex((existingCmdDef) => { return existingCmdDef.name === cmdDefToRemove.name; }); if (cmdDefInx > -1) { this.impLogger.debug("Removing definition = '" + cmdDefToRemove.name + "'"); this.resolvedCliCmdTree.children.splice(cmdDefInx, 1); } } } // __________________________________________________________________________ /** * Does the supplied pluginGroupNm match an existing top-level * name or alias in the imperative command tree? * If a conflict occurs, plugIssues.doesPluginHaveError() will return true. * * @param {string} pluginName - The name of the plugin that we are checking. * * @param {ICommandDefinition} pluginGroupDefinition - A plugin's command group definition.. * * @param {ICommandDefinition} cmdTreeDef - A top-level command tree * definition against which we compare the supplied * pluginGroupNm. It is typically the imperative command tree. * * @returns {[boolean, string]} - {hasConflict, message} - hasConflict: True when we found a conflict. * False when find no conflicts. * message: the message describing the conflict */ conflictingNameOrAlias(pluginName, pluginGroupDefinition, cmdTreeDef) { const pluginGroupNm = pluginGroupDefinition.name; /* Confirm that pluginGroupNm is not an existing top-level * group or command in the imperative command tree * and confirm that none of the plugin aliases match any command names */ if (pluginGroupNm.toLowerCase() === cmdTreeDef.name.toLowerCase()) { const conflictMessage = this.impLogger.error("The plugin named '%s' attempted to add a group of commands" + " with the name '%s'. Your base application already contains a group with the name '%s'.", pluginGroupNm, pluginGroupDefinition.name, cmdTreeDef.name); return { hasConflict: true, message: conflictMessage }; } if (pluginGroupDefinition.aliases != null) { for (const pluginAlias of pluginGroupDefinition.aliases) { if (pluginAlias.toLowerCase() === cmdTreeDef.name.toLowerCase()) { const conflictMessage = this.impLogger.error("The plugin named '%s' attempted to add a group of commands" + " with the alias '%s' . Your base application already contains a group with the name '%s'.", pluginGroupNm, pluginAlias, cmdTreeDef.name); return { hasConflict: true, message: conflictMessage }; } } } /* Confirm that pluginGroupNm is not an existing top-level * alias in the command tree definition. */ if (Object.prototype.hasOwnProperty.call(cmdTreeDef, "aliases")) { for (const nextAliasToTest of cmdTreeDef.aliases) { // if the plugin name matches an alias of the definition tree if (pluginGroupNm.toLowerCase() === nextAliasToTest.toLowerCase()) { const conflictMessage = this.impLogger.error("The plugin attempted to add a group of commands with the name '%s' " + ". Your base application already contains a group with an alias '%s'.", pluginGroupNm, nextAliasToTest, cmdTreeDef.name); return { hasConflict: true, message: conflictMessage }; } if (pluginGroupDefinition.aliases != null) { for (const pluginAlias of pluginGroupDefinition.aliases) { // if an alias of the plugin matches an alias of hte definition tree if (pluginAlias.toLowerCase() === nextAliasToTest.toLowerCase()) { const conflictMessage = this.impLogger.error("The plugin named '%s' attempted to add a " + "group of command with the alias '%s', which conflicts with another alias of the same name for group '%s'.", pluginGroupDefinition.name, pluginAlias, cmdTreeDef.name); return { hasConflict: true, message: conflictMessage }; } } } } } // no conflict if we got this far return { hasConflict: false, message: undefined }; } // __________________________________________________________________________ /** * Form the absolute path to a runtime file for a plugin from a path name * that is relative to the plugin's root directory (where its package.json lives). * * @param {string} pluginName - The name of the plugin. * * @param {string} relativePath - A relative path from plugin's root. * Typically supplied as ./lib/blah/blah/blah. * If not supplied, (or supplied as an an empty string, * the result will be a path to * <The_PLUGIN_NODE_MODULE_LOCATION_ForTheBaseCLI>/<pluginName>. * If an absolute path is supplied, it is returned exactly as supplied. * * @returns {string} - The absolute path to the file. */ formPluginRuntimePath(pluginName, relativePath = "") { // Attempt to find the node_modules that contains the plugin let pluginRuntimeDir = null; for (const location of this.pmfConst.PLUGIN_NODE_MODULE_LOCATION) { pluginRuntimeDir = (0, path_1.join)(location, pluginName); if ((0, fs_1.existsSync)(pluginRuntimeDir)) { break; } } if (relativePath.length === 0) { return pluginRuntimeDir; } /* If the relative path is already absolute, do not place our * plugin's runtime location in front of the supplied path. */ if ((0, path_1.isAbsolute)(relativePath)) { return relativePath; } return (0, path_1.join)(pluginRuntimeDir, relativePath); } // __________________________________________________________________________ /** * Read a plugin's configuration properties. The properties are obtained * from the plugins package.json file, including it's imperative property. * * @param {string} pluginName - the name of the plugin * * @returns {IPluginCfgProps} - The plugin's configuration properties * or null if the plugin's configuration cannot be retrieved. * Errors are recorded in PluginIssues. */ loadPluginCfgProps(pluginName) { const pluginCfgProps = { pluginName, npmPackageName: "PluginHasNoNpmPkgName", impConfig: {}, cliDependency: { peerDepName: this.pmfConst.CLI_CORE_PKG_NAME, peerDepVer: this.noPeerDependency }, impDependency: { peerDepName: this.pmfConst.IMPERATIVE_PKG_NAME, peerDepVer: this.noPeerDependency } }; this.impLogger.trace("loadPluginCfgProps: Reading configuration for plugin = '" + pluginName + "' from its package.json file."); // this is the starting point for reporting plugin issues, so clear old ones this.pluginIssues.removeIssuesForPlugin(pluginName); // confirm that we can find the path to the plugin node_module const pluginRunTimeRootPath = this.formPluginRuntimePath(pluginName); if (!(0, fs_1.existsSync)(pluginRunTimeRootPath)) { this.pluginIssues.recordIssue(pluginName, PluginIssues_1.IssueSeverity.CFG_ERROR, "The path to the plugin does not exist: " + pluginRunTimeRootPath); return null; } // confirm that we can find the path to the plugin's package.json const pluginPkgJsonPathNm = (0, path_1.join)(pluginRunTimeRootPath, "package.json"); if (!(0, fs_1.existsSync)(pluginPkgJsonPathNm)) { this.pluginIssues.recordIssue(pluginName, PluginIssues_1.IssueSeverity.CFG_ERROR, "Configuration file does not exist: '" + pluginPkgJsonPathNm + "'"); return null; } // read package.json let pkgJsonData = null; try { pkgJsonData = (0, jsonfile_1.readFileSync)(pluginPkgJsonPathNm); } catch (ioErr) { this.pluginIssues.recordIssue(pluginName, PluginIssues_1.IssueSeverity.CFG_ERROR, "Cannot read '" + pluginPkgJsonPathNm + "' Reason = " + ioErr.message); return null; } // extract the plugin npm package name property for later use in class if (Object.prototype.hasOwnProperty.call(pkgJsonData, this.npmPkgNmProp)) { pluginCfgProps.npmPackageName = pkgJsonData[this.npmPkgNmProp]; } // use the CLI's package name as a peer dependency in the plugin const cliPkgName = this.getCliPkgName(); const cliCmdName = utilities_1.ImperativeConfig.instance.rootCommandName; if (cliPkgName === "NoNameInCliPkgJson") { this.pluginIssues.recordIssue(pluginName, PluginIssues_1.IssueSeverity.WARNING, "The property '" + this.npmPkgNmProp + "' does not exist in the package.json file of the '" + cliCmdName + "' project. Defaulting to " + "'" + pluginCfgProps.cliDependency.peerDepName + "',"); } else { pluginCfgProps.cliDependency.peerDepName = cliPkgName; } // confirm that the peerDependencies property exists in plugin's package.json const peerDepPropNm = "peerDependencies"; if (Object.prototype.hasOwnProperty.call(pkgJsonData, peerDepPropNm)) { // get the version of the host CLI dependency for this plugin if (Object.prototype.hasOwnProperty.call(pkgJsonData[peerDepPropNm], pluginCfgProps.cliDependency.peerDepName)) { pluginCfgProps.cliDependency.peerDepVer = pkgJsonData[peerDepPropNm][pluginCfgProps.cliDependency.peerDepName]; } // get the version of the imperative dependency for this plugin if (Object.prototype.hasOwnProperty.call(pkgJsonData[peerDepPropNm], pluginCfgProps.impDependency.peerDepName)) { pluginCfgProps.impDependency.peerDepVer = pkgJsonData[peerDepPropNm][pluginCfgProps.impDependency.peerDepName]; } else { this.pluginIssues.recordIssue(pluginName, PluginIssues_1.IssueSeverity.WARNING, "The property '" + pluginCfgProps.impDependency.peerDepName + "' does not exist within the '" + peerDepPropNm + "' property in the file '" + pluginPkgJsonPathNm + "'."); } } else { this.pluginIssues.recordIssue(pluginName, PluginIssues_1.IssueSeverity.WARNING, "Your '" + this.pmfConst.NPM_NAMESPACE + "' dependencies must be contained within a '" + peerDepPropNm + "' property. That property does not exist in the file '" + pluginPkgJsonPathNm + "'."); } // extract the imperative property if (!Object.prototype.hasOwnProperty.call(pkgJsonData, this.impConfigPropNm)) { this.pluginIssues.recordIssue(pluginName, PluginIssues_1.IssueSeverity.CFG_ERROR, "The required property '" + this.impConfigPropNm + "' does not exist in file '" + pluginPkgJsonPathNm + "'."); return null; } // use the core imperative loader because it will load config modules let pluginConfig; try { pluginConfig = ConfigurationLoader_1.ConfigurationLoader.load(null, pkgJsonData, this.requirePluginModuleCallback(pluginName)); } catch (impError) { this.pluginIssues.recordIssue(pluginName, PluginIssues_1.IssueSeverity.CFG_ERROR, "Failed to load the plugin's configuration from:\n" + pluginPkgJsonPathNm + "\nReason = " + impError.message); return null; } pluginCfgProps.impConfig = pluginConfig; return pluginCfgProps; } // __________________________________________________________________________ /** * Due to configuration errors, we use an override that purposely fails. * * @param {string} settingNm - The name of the setting being processed. * @param {string} overrideDispNm - The display name of override being processed. * @param {string} overridePluginNm - The name of plugin supplying the override. * @param {string} reasonText - The text describing the reason for the error. */ useOverrideThatFails(settingNm, overrideDispNm, overridePluginNm, reasonText) { let overrideErrMsg = `Unable to override "${settingNm}" with "${overrideDispNm}" ` + `from plugin "${overridePluginNm}"\nReason = ${reasonText}\n` + `We will use a "${settingNm}" that purposely fails until you reconfigure.\n` + `You can edit the file $ZOWE_CLI_HOME/settings/imperative.json ` + `and enter a value for the "${settingNm}" property`; if (settingNm === security_1.CredentialManagerOverride.CRED_MGR_SETTING_NAME) { overrideErrMsg += `\nFor the "${security_1.CredentialManagerOverride.CRED_MGR_SETTING_NAME}" ` + `property you can specify "${security_1.CredentialManagerOverride.DEFAULT_CRED_MGR_NAME}" ` + `or you can install a plugin from the list below:\n\n`; /* Add all known credMgr override display names to the error message. * This code assumes that the default Zowe credMgr name is first in our knownCredMgrs. */ const knownCredMgrs = security_1.CredentialManagerOverride.getKnownCredMgrs(); overrideErrMsg += `"${settingNm}": "${security_1.CredentialManagerOverride.DEFAULT_CRED_MGR_NAME}" (default)`; for (let credMgrInx = 1; credMgrInx < knownCredMgrs.length; credMgrInx++) { overrideErrMsg += `\n"${settingNm}": "${knownCredMgrs[credMgrInx].credMgrDisplayName}" `; if (typeof knownCredMgrs[credMgrInx].credMgrPluginName !== "undefined") { overrideErrMsg += `(supplied in CLI plugin ${knownCredMgrs[credMgrInx].credMgrPluginName}`; } if (typeof knownCredMgrs[credMgrInx].credMgrZEName !== "undefined") { const punctuation = 8; overrideErrMsg += "\n"; const indentLength = settingNm.length + `${knownCredMgrs[credMgrInx].credMgrDisplayName}`.length + punctuation; for (let indent = 0; indent < indentLength; indent++) { overrideErrMsg += " "; } overrideErrMsg += `and in ZE extension ${knownCredMgrs[credMgrInx].credMgrZEName}`; } overrideErrMsg += `)`; } } // log our error message and create a failing override class that throws the same error. this.impLogger.error(overrideErrMsg); /* We need to assign a class into the current override setting. * We cannot create a new object from a class and pass the error into its * constructor, because the CredentialManagerFactory takes a class and * it calls the constructor of our supplied class. Thus we need an * anonymous class so that we can access our 'overrideErrMsg' variable. * Our trick is that we simply throw an error in the constructor * of our anonymous class. The CredentialManagerFactory catches * our error, and places it into its InvalidCredentialManager, which * in turn shows our error every time the CLI tries to use credentials. */ this.mPluginOverrides[settingNm] = class { constructor() { throw overrideErrMsg; } }; } // __________________________________________________________________________ /** * Validates that the semver range strings specified by the plugin for * versions of the imperative framework and host CLI program are compatible * with those specified in the host CLI. * * Both range strings come from the package.json files of the plugin and the * hosting CLI. We consider the version ranges to be compatible if they satisfy * the CLI version range. This should allow npm to download one common version * of core and of imperative to be owned by the base CLI and shared by the plugin. * * Any errors are recorded in PluginIssues. * * @param {IPluginCfgProps} pluginCfgProps - The configuration properties for this plugin */ validatePeerDepVersions(pluginCfgProps) { // get the name of the base CLI for error messages const cliCmdName = utilities_1.ImperativeConfig.instance.rootCommandName; const cliPackageJson = utilities_1.ImperativeConfig.instance.callerPackageJson; let cliVerPropName = "version"; // compare the plugin's requested CLI version with the CLI's actual version if (pluginCfgProps.cliDependency.peerDepVer !== this.noPeerDependency) { if (Object.prototype.hasOwnProperty.call(cliPackageJson, cliVerPropName)) { this.comparePluginVersionToCli(pluginCfgProps.pluginName, pluginCfgProps.cliDependency.peerDepName, pluginCfgProps.cliDependency.peerDepVer, cliVerPropName, cliPackageJson[cliVerPropName]); } else { this.pluginIssues.recordIssue(pluginCfgProps.pluginName, PluginIssues_1.IssueSeverity.CFG_ERROR, "The property '" + cliVerPropName + "' does not exist within the package.json file of the '" + cliCmdName + "' project."); } } // compare the plugin's requested imperative version with the CLI's actual version if (pluginCfgProps.impDependency.peerDepVer !== this.noPeerDependency) { /* The CLI's imperative version is within its dependencies property * under the same property name as the plugin uses. */ const cliDepPropName = "dependencies"; cliVerPropName = pluginCfgProps.impDependency.peerDepName; if (Object.prototype.hasOwnProperty.call(cliPackageJson, cliDepPropName)) { if (Object.prototype.hasOwnProperty.call(cliPackageJson[cliDepPropName], cliVerPropName)) { this.comparePluginVersionToCli(pluginCfgProps.pluginName, pluginCfgProps.impDependency.peerDepName, pluginCfgProps.impDependency.peerDepVer, cliVerPropName, cliPackageJson[cliDepPropName][cliVerPropName]); } else { this.pluginIssues.recordIssue(pluginCfgProps.pluginName, PluginIssues_1.IssueSeverity.CFG_ERROR, "The property '" + cliVerPropName + "' does not exist within the '" + cliDepPropName + "' property in the package.json file of the '" + cliCmdName + "' project."); } } else { this.pluginIssues.recordIssue(pluginCfgProps.pluginName, PluginIssues_1.IssueSeverity.CFG_ERROR, "The property '" + cliDepPropName + "' does not exist in the package.json file of the '" + cliCmdName + "' project."); } } } // __________________________________________________________________________ /** * Validate the plugin. * * @param {IPluginCfgProps} pluginCfgProps - The configuration properties for this plugin * * @param {ICommandDefinition} pluginCmdGroup - The command group to be added * for this plugin, with all commands resolved into its children property. * * @returns {boolean} - True if valid. False otherwise. * PluginIssues contains the set of issues. */ validatePlugin(pluginCfgProps, pluginCmdGroup) { if (utilities_1.JsUtils.isObjEmpty(pluginCfgProps.impConfig)) { // without a config object, we can do no further validation this.pluginIssues.recordIssue(pluginCfgProps.pluginName, PluginIssues_1.IssueSeverity.CFG_ERROR, "The plugin's configuration is empty."); return false; } this.impLogger.info("validatePlugin: Validating plugin '" + pluginCfgProps.pluginName + "'. Plugin config details at trace level of logging."); this.impLogger.trace("validatePlugin: Config for plugin '" + pluginCfgProps.pluginName + "':\n" + JSON.stringify(pluginCfgProps.impConfig, null, 2)); // is there an imperative.name property? if (!Object.prototype.hasOwnProperty.call(pluginCfgProps.impConfig, "name")) { // can we default to the npm package name? if (pluginCfgProps.npmPackageName === "PluginHasNoNpmPkgName" || pluginCfgProps.npmPackageName.length === 0) { this.pluginIssues.recordIssue(pluginCfgProps.pluginName, PluginIssues_1.IssueSeverity.CFG_ERROR, "The plugin's configuration does not contain an '" + this.impConfigPropNm + ".name' property, or an npm package 'name' property in package.json."); } else { pluginCfgProps.impConfig.name = pluginCfgProps.npmPackageName; } } /* Confirm that the plugin group name does not conflict with another * top-level item in the imperative command tree. */ if (Object.prototype.hasOwnProperty.call(pluginCfgProps.impConfig, "name")) { for (const nextImpCmdDef of this.resolvedCliCmdTree.children) { const conflictAndMessage = this.conflictingNameOrAlias(pluginCfgProps.pluginName, pluginCmdGroup, nextImpCmdDef); if (conflictAndMessage.hasConflict) { this.pluginIssues.recordIssue(pluginCfgProps.pluginName, PluginIssues_1.IssueSeverity.CMD_ERROR, conflictAndMessage.message); break; } } } if (!Object.prototype.hasOwnProperty.call(pluginCfgProps.impConfig, "rootCommandDescription")) { this.pluginIssues.recordIssue(pluginCfgProps.pluginName, PluginIssues_1.IssueSeverity.CMD_ERROR, "The plugin's configuration does not contain an '" + this.impConfigPropNm + ".rootCommandDescription' property."); } /* Validate that versions of the imperative framework and * host CLI program are compatible with those of the host CLI. */ this.validatePeerDepVersions(pluginCfgProps); /* If a plugin does neither of the following actions, we reject it: * - define commands * - override an infrastructure component */ if ((!pluginCmdGroup.children || pluginCmdGroup.children.length <= 0) && (!pluginCfgProps.impConfig.overrides || Object.keys(pluginCfgProps.impConfig.overrides).length <= 0)) { this.pluginIssues.recordIssue(pluginCfgProps.pluginName, PluginIssues_1.IssueSeverity.CFG_ERROR, "The plugin defines no commands and overrides no framework components."); } else { // recursively validate the plugin's command definitions this.validatePluginCmdDefs(pluginCfgProps.pluginName, pluginCmdGroup.children); } /* Plugins are not required to have profiles. * So, if they do not exist, just move on. */ if (pluginCfgProps.impConfig.profiles) { this.validatePluginProfiles(pluginCfgProps.pluginName, pluginCfgProps.impConfig.profiles); } /* N