UNPKG

ask-cli-x

Version:

Alexa Skills Kit (ASK) Command Line Interfaces

267 lines (266 loc) 13.2 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 CONSTANTS = require("../../utils/constants"); const { isAcSkill, syncManifest } = require("../../utils/ac-util"); const acdl = require("@alexa/acdl"); const SpinnerView = require("../../view/spinner-view"); const DeployDelegate = require("./deploy-delegate"); 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.'); } // 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, taskCallback); }; multiTasksView.loadTask(taskHandle, taskTitle, region); }); // 3.start multi-tasks and validate task response multiTasksView.start((taskErr, taskResult) => { if (taskErr) { // update partial successful deploy results to resources config if (taskErr.partialResult && !R.isEmpty(R.keys(taskErr.partialResult))) { this._updateResourcesConfig(taskErr.partialResult); } return callback(taskErr.error); } // 4.validate response and update states based on the results try { dd.validateDeployDelegateResponse(taskResult); } catch (responseInvalidErr) { return callback(responseInvalidErr); } this._updateResourcesConfig(taskResult); callback(null, taskResult); }); } /** * 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 endpoints for each region domains.forEach((domain) => { R.keys(rawDeployResult).forEach((region) => { if (domain === Manifest.endpointTypes.EVENTS) { Manifest.getInstance().setEventsEndpointByRegion(region, rawDeployResult[region].endpoint); } else { Manifest.getInstance().setApisEndpointByDomainRegion(domain, region, rawDeployResult[region].endpoint); } }); }); 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 {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, 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), }; // 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 } = invokeResult; // track the current hash if isCodeDeployed if (isCodeDeployed) { invokeResult.lastDeployHash = currentHash; } // pass back result based on if isAllStepSuccess, pass result as error if not all steps succeed callback(isAllStepSuccess ? null : invokeResult, isAllStepSuccess ? invokeResult : undefined); }); }); } /** * Update the the ask resources config and the deploy state. * @param {Object} rawDeployResult deploy result from invoke: { $region: deploy-delegate's response } */ _updateResourcesConfig(rawDeployResult) { const newDeployState = {}; R.keys(rawDeployResult).forEach((alexaRegion) => { newDeployState[alexaRegion] = rawDeployResult[alexaRegion].deployState; ResourcesConfig.getInstance().setCodeLastDeployHashByRegion(this.profile, alexaRegion, rawDeployResult[alexaRegion].lastDeployHash); }); ResourcesConfig.getInstance().setSkillInfraDeployState(this.profile, newDeployState); ResourcesConfig.getInstance().write(); } /** * Make sure the skill manifest is updated successfully * @param {Function} callback */ _ensureSkillManifestGotUpdated(callback) { let skillMetaController; const spinner = new SpinnerView(); spinner.start("Updating skill package from the skill infrastructure deploy results."); try { skillMetaController = new SkillMetadataController({ profile: this.profile, doDebug: this.doDebug }); } catch (err) { spinner.terminate(); return callback(err); } skillMetaController.updateSkillManifest((err) => { spinner.terminate(); callback(err); }); } };