UNPKG

ask-cli

Version:

Alexa Skills Kit (ASK) Command Line Interfaces

337 lines (336 loc) 16.1 kB
"use strict"; const R = require("ramda"); const path = require("path"); const SkillMetadataController = require("../skill-metadata-controller"); const ResourcesConfig = require("../../model/resources-config"); const Manifest = require("../../model/manifest"); const MultiTasksView = require("../../view/multi-tasks-view"); const Messenger = require("../../view/messenger"); const hashUtils = require("../../utils/hash-utils"); const stringUtils = require("../../utils/string-utils"); const profileHelper = require("../../utils/profile-helper"); const CONSTANTS = require("../../utils/constants"); const { isAcSkill, syncManifest } = require("../../utils/ac-util"); const acdl = require("@alexa/acdl"); const DeployDelegate = require("./deploy-delegate"); const defaultAlexaAwsRegionMap = { default: "us-east-1", NA: "us-east-1", EU: "eu-west-1", FE: "us-west-2", }; module.exports = class SkillInfrastructureController { constructor(configuration) { const { profile, doDebug, ignoreHash } = configuration; this.profile = profile; this.doDebug = doDebug; this.ignoreHash = ignoreHash; this.skillMetaController = new SkillMetadataController({ profile, doDebug }); } /** * Bootstrap a skill project with deploy delegate's custom logic, to initiate the project to be ready for deployment. * * @param {String} workspacePath The path to the new skill's workspace * @param {Function} callback (error) */ bootstrapInfrastructures(workspacePath, callback) { const infraType = ResourcesConfig.getInstance().getSkillInfraType(this.profile); if (!stringUtils.isNonBlankString(infraType)) { return process.nextTick(() => { callback('[Error]: Please set the "type" field for your skill infrastructures.'); }); } // 1.Prepare the loading of deploy delegate DeployDelegate.load(infraType, (loadErr, deployDelegate) => { if (loadErr) { return callback(loadErr); } // 2.Call bootstrap method from deploy delegate const bootstrapOptions = { profile: this.profile, userConfig: ResourcesConfig.getInstance().getSkillInfraUserConfig(this.profile), workspacePath, }; deployDelegate.bootstrap(bootstrapOptions, (bootsErr, bootstrapResult) => { if (bootsErr) { return callback(bootsErr); } const { userConfig } = bootstrapResult; ResourcesConfig.getInstance().setSkillInfraUserConfig(this.profile, userConfig); callback(); }); }); } /** * Entry method for skill infrastructure deployment based on the deploy delegate type. * * @param {Function} callback */ deployInfrastructure(callback) { const infraType = ResourcesConfig.getInstance().getSkillInfraType(this.profile); // 1.Prepare the loading of deploy delegate DeployDelegate.load(infraType, (loadErr, deployDelegate) => { if (loadErr) { return callback(loadErr); } // 2.Trigger regional deployment using deploy delegate this.deployInfraToAllRegions(deployDelegate, (deployErr, deployResult) => { if (deployErr) { return callback(deployErr); } // 3.Post deploy skill manifest update this.updateSkillManifestWithDeployResult(deployResult, (postUpdateErr) => { if (postUpdateErr) { return callback(postUpdateErr); } callback(); }); }); }); } /** * Deploy skill infrastructure to all the Alexa regions. * * @param {Object} dd DeployDelegate instance * @param {Function} callback (error, invokeResult) * invokeResult: { $region: { endpoint: { url }, lastDeployHash, deployState } } */ deployInfraToAllRegions(dd, callback) { const skillName = stringUtils.filterNonAlphanumeric(Manifest.getInstance().getSkillName()) || stringUtils.filterNonAlphanumeric(path.basename(process.cwd())); if (!stringUtils.isNonBlankString(skillName)) { return callback("[Error]: Failed to parse the skill name used to decide the CloudFormation stack name. " + "Please make sure your skill name or skill project folder basename contains alphanumeric characters."); } const regionsList = ResourcesConfig.getInstance().getCodeRegions(this.profile); if (!regionsList || regionsList.length === 0) { return callback('[Warn]: Skip the infrastructure deployment, as the "code" field has not been set in the resources config file.'); } const userConfig = ResourcesConfig.getInstance().getSkillInfraUserConfig(this.profile); const deployRegions = this._getAlexaDeployRegions(regionsList, userConfig); // 1.instantiate MultiTasksView const taskConfig = { concurrent: true, exitOnError: false, }; const multiTasksView = new MultiTasksView(taskConfig); // 2.register each regional task into MultiTasksView regionsList.forEach((region) => { const taskTitle = `Deploy Alexa skill infrastructure for region "${region}"`; const taskHandle = (reporter, taskCallback) => { this._deployInfraByRegion(reporter, dd, region, skillName, deployRegions, taskCallback); }; multiTasksView.loadTask(taskHandle, taskTitle, region); }); // 3.start multi-tasks and validate task response multiTasksView.start((taskErr, taskResult) => { const { error, partialResult } = taskErr || {}; const result = partialResult || taskResult; // update skipped deployment task with deploy region result if (result) { R.keys(result) .filter((alexaRegion) => result[alexaRegion].isDeploySkipped) .forEach((alexaRegion) => { const { deployRegion } = result[alexaRegion]; result[alexaRegion] = result[deployRegion]; }); } if (error) { // update partial successful deploy results to resources config if (result && !R.isEmpty(R.keys(result))) { this._updateResourcesConfig(regionsList, result, error); } return callback(error); } // 4.validate response and update states based on the results try { dd.validateDeployDelegateResponse(result); } catch (responseInvalidErr) { return callback(responseInvalidErr); } this._updateResourcesConfig(regionsList, result); callback(null, result); }); } /** * Update the skill manifest based on the skill infrastructure deployment result. * * @param {Object} rawDeployResult deploy result from invoke: { $region: { endpoint: { url }, lastDeployHash, deployState } } * @param {Function} callback (error) */ updateSkillManifestWithDeployResult(rawDeployResult, callback) { const targetEndpoints = ResourcesConfig.getInstance().getTargetEndpoints(this.profile); // for backward compatibility, defaulting to api from skill manifest if targetEndpoints is not defined const domains = targetEndpoints.length ? targetEndpoints : Object.keys(Manifest.getInstance().getApis()); // 1.update local skill.json file: // update the "uri" in all target api endpoints for each region domains.forEach((domain) => { R.keys(rawDeployResult).forEach((region) => { Manifest.getInstance().setApisEndpointByDomainRegion(domain, region, rawDeployResult[region].endpoint); }); }); // add skill events if defined in resources config const events = ResourcesConfig.getInstance().getSkillEvents(this.profile); if (events) { R.keys(rawDeployResult).forEach((region) => { Manifest.getInstance().setEventsEndpointByRegion(region, rawDeployResult[region].endpoint); }); if (events.publications) { const publications = events.publications.map((eventName) => ({ eventName })); Manifest.getInstance().setEventsPublications(publications); } if (events.subscriptions) { const subscriptions = events.subscriptions.map((eventName) => ({ eventName })); Manifest.getInstance().setEventsSubscriptions(subscriptions); } } Manifest.getInstance().write(); let skillMetaSrc = ResourcesConfig.getInstance().getSkillMetaSrc(this.profile); // If it's a AC skill, update build/skill-package/skill.json as well const isACSkill = isAcSkill(this.profile); if (isACSkill) { const projectConfig = acdl.loadProjectConfigSync(); const outDirPath = path.join(projectConfig.rootDir, projectConfig.outDir); const skillPackageSrc = path.join(outDirPath, CONSTANTS.COMPILER.TARGETDIR); skillMetaSrc = skillPackageSrc; const manifestPath = path.join(skillPackageSrc, CONSTANTS.COMPILER.MANIFEST); syncManifest(manifestPath); } // 2.compare with current hash result to decide if skill.json file need to be updated // (the only possible change in skillMetaSrc during the infra deployment is the skill.json's uri change) hashUtils.getHash(skillMetaSrc, (hashErr, currentHash) => { if (hashErr) { return callback(hashErr); } if (currentHash === ResourcesConfig.getInstance().getSkillMetaLastDeployHash(this.profile)) { return callback(); } // 3.re-upload skill package this._ensureSkillManifestGotUpdated((manifestUpdateErr) => { if (manifestUpdateErr) { return callback(manifestUpdateErr); } ResourcesConfig.getInstance().setSkillMetaLastDeployHash(this.profile, currentHash); Messenger.getInstance().info("\nThe api endpoints of skill.json have been updated from the skill infrastructure deploy results."); callback(); }); }); } /** * Deploy skill infrastructure by calling invoke function from deploy delegate. * @param {Object} reporter upstream CLI status reporter * @param {Object} dd injected deploy delegate instance * @param {String} alexaRegion * @param {String} skillName * @param {Object} deployRegions * @param {Function} callback (error, invokeResult) * callback.error can be a String or { message, context } Object which passes back the partial deploy result */ _deployInfraByRegion(reporter, dd, alexaRegion, skillName, deployRegions, callback) { const regionConfig = { profile: this.profile, doDebug: this.doDebug, ignoreHash: this.ignoreHash, alexaRegion, skillId: ResourcesConfig.getInstance().getSkillId(this.profile), skillName, code: { codeBuild: ResourcesConfig.getInstance().getCodeBuildByRegion(this.profile, alexaRegion).file, isCodeModified: null, }, userConfig: ResourcesConfig.getInstance().getSkillInfraUserConfig(this.profile), deployState: ResourcesConfig.getInstance().getSkillInfraDeployState(this.profile), deployRegions, }; // 1.calculate the lastDeployHash for current code folder and compare with the one in record const lastDeployHash = ResourcesConfig.getInstance().getCodeLastDeployHashByRegion(this.profile, regionConfig.alexaRegion); hashUtils.getHash(regionConfig.code.codeBuild, (hashErr, currentHash) => { if (hashErr) { return callback(hashErr); } regionConfig.code.isCodeModified = currentHash !== lastDeployHash; // 2.trigger the invoke function from deploy delegate dd.invoke(reporter, regionConfig, (invokeErr, invokeResult) => { if (invokeErr) { return callback(invokeErr); } const { isAllStepSuccess, isCodeDeployed, isDeploySkipped, resultMessage } = invokeResult; // track the current hash if isCodeDeployed if (isCodeDeployed) { invokeResult.lastDeployHash = currentHash; } // skip task if isDeploySkipped if (isDeploySkipped) { reporter.skipTask(resultMessage); } // pass result message as error message if deploy not success and not skipped if (!isAllStepSuccess && !isDeploySkipped) { callback({ message: resultMessage, context: invokeResult }); } else { callback(null, invokeResult); } }); }); } /** * Update the the ask resources config and the deploy state. * * @param {Object} rawDeployResult deploy result from invoke: { $region: deploy-delegate's response } */ _updateResourcesConfig(regionsList, rawDeployResult, error) { const curDeployState = ResourcesConfig.getInstance().getSkillInfraDeployState(this.profile) || {}; const newDeployState = {}; regionsList.forEach((alexaRegion) => { const { deployState, lastDeployHash } = rawDeployResult[alexaRegion] || {}; newDeployState[alexaRegion] = deployState || curDeployState[alexaRegion]; if (lastDeployHash) { ResourcesConfig.getInstance().setCodeLastDeployHashByRegion(this.profile, alexaRegion, lastDeployHash); } }); ResourcesConfig.getInstance().setSkillInfraDeployState(this.profile, newDeployState); if (!error) { ResourcesConfig.getInstance().setCodeLastDeployTimestamp(this.profile, `${(new Date()).toISOString()}`); } ResourcesConfig.getInstance().write(); } /** * Make sure the skill manifest is updated successfully * @param {Function} callback */ _ensureSkillManifestGotUpdated(callback) { let vendorId; try { vendorId = profileHelper.resolveVendorId(this.profile); } catch (err) { return callback(err); } // deploy skill package if the skill manifest has icon file uri, otherwise update the skill manifest if (Manifest.getInstance().hasIconFileUri()) { this.skillMetaController.deploySkillPackage(vendorId, this.ignoreHash, (err) => { callback(err); }); } else { this.skillMetaController.updateSkillManifest((err) => { callback(err); }); } } /** * Return deploy regions map based on configured alexa code regions * @param {Array} regionsList list of configured alexa regions * @param {Object} userConfig * @return {Object} */ _getAlexaDeployRegions(regionsList, userConfig) { const deployRegions = {}; regionsList.forEach((alexaRegion) => { const awsRegion = alexaRegion === "default" ? userConfig.awsRegion : R.path(["regionalOverrides", alexaRegion, "awsRegion"], userConfig); deployRegions[alexaRegion] = awsRegion || defaultAlexaAwsRegionMap[alexaRegion]; }); return deployRegions; } };