UNPKG

salesforce-alm

Version:

This package contains tools, and APIs, for an improved salesforce.com developer experience.

325 lines (323 loc) 16.3 kB
"use strict"; /* * Copyright (c) 2020, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ /** * Contains config details and operations meant for package subscriber orgs. * This could potentially include test orgs used by CI process for testing packages, * and target subscriber orgs. **/ const util = require("util"); const BBPromise = require("bluebird"); const _ = require("lodash"); // New messages (move to this) const core_1 = require("@salesforce/core"); core_1.Messages.importMessagesDirectory(__dirname); const packagingMessages = core_1.Messages.loadMessages('salesforce-alm', 'packaging'); // Old style messages const MessagesLocal = require("../messages"); const messages = MessagesLocal(); const logger = require("../core/logApi"); const pkgUtils = require("../package/packageUtils"); const DEFAULT_POLL_INTERVAL_MILLIS = 5000; const REPLICATION_POLLING_INTERVAL_MILLIS = 10000; const DEFAULT_MAX_RETRIES = 0; const RETRY_MINUTES_IN_MILLIS = 60000; const DEFAULT_PUBLISH_WAIT_MIN = 0; const SECURITY_TYPE_KEY_ALLUSERS = 'AllUsers'; const SECURITY_TYPE_KEY_ADMINSONLY = 'AdminsOnly'; const SECURITY_TYPE_VALUE_ALLUSERS = 'full'; const SECURITY_TYPE_VALUE_ADMINSONLY = 'none'; const SECURITY_TYPE_MAP = new Map(); SECURITY_TYPE_MAP.set(SECURITY_TYPE_KEY_ALLUSERS, SECURITY_TYPE_VALUE_ALLUSERS); SECURITY_TYPE_MAP.set(SECURITY_TYPE_KEY_ADMINSONLY, SECURITY_TYPE_VALUE_ADMINSONLY); const UPGRADE_TYPE_KEY_DELETE = 'Delete'; const UPGRADE_TYPE_KEY_DEPRECATE_ONLY = 'DeprecateOnly'; const UPGRADE_TYPE_KEY_MIXED = 'Mixed'; const UPGRADE_TYPE_VALUE_DELETE = 'delete-only'; const UPGRADE_TYPE_VALUE_DEPRECATE_ONLY = 'deprecate-only'; const UPGRADE_TYPE_VALUE_MIXED = 'mixed-mode'; const UPGRADE_TYPE_MAP = new Map(); UPGRADE_TYPE_MAP.set(UPGRADE_TYPE_KEY_DELETE, UPGRADE_TYPE_VALUE_DELETE); UPGRADE_TYPE_MAP.set(UPGRADE_TYPE_KEY_DEPRECATE_ONLY, UPGRADE_TYPE_VALUE_DEPRECATE_ONLY); UPGRADE_TYPE_MAP.set(UPGRADE_TYPE_KEY_MIXED, UPGRADE_TYPE_VALUE_MIXED); const APEX_COMPILE_KEY_ALL = 'all'; const APEX_COMPILE_KEY_PACKAGE = 'package'; const APEX_COMPILE_VALUE_ALL = 'all'; const APEX_COMPILE_VALUE_PACKAGE = 'package'; const APEX_COMPILE_MAP = new Map(); APEX_COMPILE_MAP.set(APEX_COMPILE_KEY_ALL, APEX_COMPILE_VALUE_ALL); APEX_COMPILE_MAP.set(APEX_COMPILE_KEY_PACKAGE, APEX_COMPILE_VALUE_PACKAGE); /** * Private utility to parse out errors from PackageInstallRequest as a user-readable string. */ const readInstallErrorsAsString = function (request) { if (request.Errors && request.Errors.errors) { const errorsArray = request.Errors.errors; const len = errorsArray.length; if (len > 0) { let errorMessage = 'Installation errors: '; for (let i = 0; i < len; i++) { errorMessage += `\n${i + 1}) ${errorsArray[i].message}`; } return errorMessage; } } return '<empty>'; }; class PackageInstallCommand { constructor(stdinPrompt) { this.pollIntervalMillis = DEFAULT_POLL_INTERVAL_MILLIS; this.replicationPollIntervalMillis = REPLICATION_POLLING_INTERVAL_MILLIS; this.maxRetries = DEFAULT_MAX_RETRIES; this.allPackageVersionId = null; this.installationKey = null; this.publishwait = DEFAULT_PUBLISH_WAIT_MIN; this.logger = logger.child('PackageInstallCommand'); this.stdinPrompt = stdinPrompt; this.packageInstallRequest = {}; } poll(context, id, retries) { this.org = context.org; this.configApi = this.org.config; this.force = this.org.force; return this.force.toolingRetrieve(this.org, 'PackageInstallRequest', id).then((request) => { switch (request.Status) { case 'SUCCESS': return request; case 'ERROR': { const err = readInstallErrorsAsString(request); this.logger.error('Encountered errors installing the package!', err); throw new Error(err); } default: if (retries > 0) { // Request still in progress. Just log a message and move on. Server will be polled again. this.logger.log(messages.getMessage('installStatus', request.Status, 'packaging')); return BBPromise.delay(this.pollIntervalMillis).then(() => this.poll(context, id, retries - 1)); } else { // timed out } } return request; }); } async _waitForApvReplication(remainingRetries) { const QUERY_NO_KEY = 'SELECT Id, SubscriberPackageId, InstallValidationStatus FROM SubscriberPackageVersion' + ` WHERE Id ='${this.allPackageVersionId}'`; const escapedInstallationKey = this.installationKey != null ? this.installationKey.replace(/\\/g, '\\\\').replace(/\'/g, "\\'") : null; const QUERY_W_KEY = 'SELECT Id, SubscriberPackageId, InstallValidationStatus FROM SubscriberPackageVersion' + ` WHERE Id ='${this.allPackageVersionId}' AND InstallationKey ='${escapedInstallationKey}'`; let queryResult = null; try { queryResult = await this.force.toolingQuery(this.org, util.format(QUERY_W_KEY)); } catch (e) { // Check first for Implementation Restriction error that is enforced in 214, before it was possible to query // against InstallationKey, otherwise surface the error. if (pkgUtils.isErrorFromSPVQueryRestriction(e)) { queryResult = await this.force.toolingQuery(this.org, util.format(QUERY_NO_KEY)); } else { if (!pkgUtils.isErrorPackageNotAvailable(e)) { throw e; } } } // Continue retrying if there is no record // or for an InstallValidationStatus of PACKAGE_UNAVAILABLE (replication to the subscriber's instance has not completed) // or for an InstallValidationStatus of UNINSTALL_IN_PROGRESS let installValidationStatus = null; if (queryResult && queryResult.records && queryResult.records.length > 0) { installValidationStatus = queryResult.records[0].InstallValidationStatus; if (installValidationStatus != 'PACKAGE_UNAVAILABLE' && installValidationStatus != 'UNINSTALL_IN_PROGRESS') { return queryResult.records[0]; } } if (remainingRetries <= 0) { // There are no more reties. Throw an error indicating the package is unavailable. // For UNINSTALL_IN_PROGRESS, though, allow install to proceed which will result in an appropriate UninstallInProgressProblem // error message being displayed. if (installValidationStatus == 'UNINSTALL_IN_PROGRESS') { return null; } else { throw new Error(messages.getMessage('errorApvIdNotPublished', [], 'package_install')); } } return BBPromise.delay(this.replicationPollIntervalMillis).then(() => { this.logger.log(messages.getMessage('publishWaitProgress', [], 'package_install')); return this._waitForApvReplication(remainingRetries - 1); }); } /** * This installs a package version into a target org. * * @param context: heroku context * @returns {*|promise} */ async execute(context) { this.org = context.org; this.configApi = this.org.config; this.force = this.org.force; // either of the id or package flag is required, not both at the same time if ((!context.flags.id && !context.flags.package) || (context.flags.id && context.flags.package)) { const idFlag = context.command.flags.find((x) => x.name === 'id'); const packageFlag = context.command.flags.find((x) => x.name === 'package'); throw new Error(messages.getMessage('errorRequiredFlags', [`--${idFlag.name} (-${idFlag.char})`, `--${packageFlag.name}`], 'package_install')); } let apvId; if (context.flags.id) { apvId = context.flags.id; } else if (context.flags.package) { // look up the alias only when it's not a 04t apvId = context.flags.package.startsWith(pkgUtils.BY_LABEL.SUBSCRIBER_PACKAGE_VERSION_ID.prefix) ? context.flags.package : pkgUtils.getPackageIdFromAlias(context.flags.package, this.force); } // validate whatever is set as the apvId, even if that might be a bunk alias try { pkgUtils.validateId(pkgUtils.BY_LABEL.SUBSCRIBER_PACKAGE_VERSION_ID, apvId); } catch (err) { throw new Error(messages.getMessage('invalidIdOrPackage', apvId, 'package_install')); } this.allPackageVersionId = apvId; this.maxRetries = _.isNil(context.flags.wait) ? this.maxRetries : (RETRY_MINUTES_IN_MILLIS / this.pollIntervalMillis) * context.flags.wait; // Be careful with the fact that cmd line flags are NOT camel cased: flags.installationkey this.installationKey = _.isNil(context.flags.installationkey) ? this.installationKey : context.flags.installationkey; this.publishwait = _.isNil(context.flags.publishwait) ? this.publishwait : context.flags.publishwait; const apiVersion = this.configApi.getApiVersion(); if (apiVersion < 36) { throw new Error('This command is supported only on API versions 36.0 and higher'); } const publishWaitRetries = Math.ceil((parseInt(this.publishwait) * 60 * 1000) / parseInt(this.replicationPollIntervalMillis)); await this._waitForApvReplication(publishWaitRetries); const pkgType = await pkgUtils.getPackage2TypeBy04t(apvId, this.force, this.org, this.installationKey); // If the user has specified --upgradetype Delete, then prompt for confirmation // unless the noprompt option has been included if (context.flags.upgradetype == UPGRADE_TYPE_KEY_DELETE && pkgType === 'Unlocked') { // don't prompt if we're going to ignore anyway const accepted = await this._prompt(context.flags.noprompt, messages.getMessage('promptUpgradeType', [], 'package_install')); if (!accepted) { throw new Error(messages.getMessage('promptUpgradeTypeDeny', [], 'package_install')); } } // If the user is installing an unlocked package with external sites (RSS/CSP) then // inform and prompt the user of these sites for acknowledgement. const externalSiteData = await this._getExternalSites(this.allPackageVersionId); let enableExternalSites = false; if (externalSiteData.trustedSites && externalSiteData.trustedSites.length > 0) { const accepted = await this._prompt(context.flags.noprompt, messages.getMessage('promptRss', externalSiteData.trustedSites.join('\n'), 'package_install')); if (accepted) { enableExternalSites = true; } } // Construct PackageInstallRequest sobject used to trigger package version install. this.packageInstallRequest.subscriberPackageVersionKey = this.allPackageVersionId; this.packageInstallRequest.password = this.installationKey; // W-3980736 in the future we hope to change "Password" to "InstallationKey" on the server if (context.flags.upgradetype !== UPGRADE_TYPE_KEY_MIXED) { if (pkgType === 'Unlocked') { // include upgradetype if it's not the default value 'mixed' this.packageInstallRequest.upgradeType = UPGRADE_TYPE_MAP.get(context.flags.upgradetype); } else { this.logger.log(packagingMessages.getMessage('install.warningUpgradeTypeOnlyForUnlocked')); } } if (context.flags.apexcompile !== APEX_COMPILE_KEY_ALL) { if (pkgType === 'Unlocked') { // include apexcompile if it's not the default value 'all' this.packageInstallRequest.apexCompileType = APEX_COMPILE_MAP.get(context.flags.apexcompile); } else { this.logger.log(packagingMessages.getMessage('install.warningApexCompileOnlyForUnlocked')); } } // Add default parameters to input object. this.packageInstallRequest.securityType = SECURITY_TYPE_MAP.get(context.flags.securitytype); this.packageInstallRequest.nameConflictResolution = 'Block'; this.packageInstallRequest.packageInstallSource = 'U'; this.packageInstallRequest.enableRss = enableExternalSites; const result = await this.force.toolingCreate(this.org, 'PackageInstallRequest', this.packageInstallRequest); const packageInstallRequestId = result.id; if (_.isNil(packageInstallRequestId)) { throw new Error(`Failed to create PackageInstallRequest for: ${this.allPackageVersionId}`); } return this.poll(context, packageInstallRequestId, this.maxRetries); } async _prompt(noninteractive, message) { const answer = noninteractive ? 'YES' : await this.stdinPrompt(message); // print a line of white space after the prompt is entered for separation this.logger.log(''); return answer.toUpperCase() === 'YES' || answer.toUpperCase() === 'Y'; } /** * Returns all RSS/CSP external third party websites * * @param allPackageVersionId * @returns {object} * * { * unlocked: boolean, * trustedSites: [string] * } */ async _getExternalSites(allPackageVersionId) { const subscriberPackageValues = {}; const query_no_key = 'SELECT RemoteSiteSettings, CspTrustedSites ' + 'FROM SubscriberPackageVersion ' + `WHERE Id ='${allPackageVersionId}'`; const escapedInstallationKey = this.installationKey != null ? this.installationKey.replace(/\\/g, '\\\\').replace(/\'/g, "\\'") : null; const query_w_key = 'SELECT RemoteSiteSettings, CspTrustedSites ' + 'FROM SubscriberPackageVersion ' + `WHERE Id ='${allPackageVersionId}' AND InstallationKey ='${escapedInstallationKey}'`; let queryResult = null; try { queryResult = await this.force.toolingQuery(this.org, query_w_key); } catch (e) { // Check first for Implementation Restriction error that is enforced in 214, before it was possible to query // against InstallationKey, otherwise surface the error. if (pkgUtils.isErrorFromSPVQueryRestriction(e)) { queryResult = await this.force.toolingQuery(this.org, query_no_key); } else { throw e; } } if (queryResult.records && queryResult.records.length > 0) { const record = queryResult.records[0]; const rssUrls = record.RemoteSiteSettings.settings.map((rss) => rss.url); const cspUrls = record.CspTrustedSites.settings.map((csp) => csp.endpointUrl); subscriberPackageValues.trustedSites = rssUrls.concat(cspUrls); } return subscriberPackageValues; } /** * returns a human readable message for a cli output * * @returns {string} */ getHumanSuccessMessage(result) { switch (result.Status) { case 'SUCCESS': return messages.getMessage(result.Status, [result.SubscriberPackageVersionKey], 'package_install_report'); case 'TERMINATED': return ''; default: return messages.getMessage(result.Status, [result.Id, this.org.name], 'package_install_report'); } } } module.exports = PackageInstallCommand; //# sourceMappingURL=packageInstallCommand.js.map