UNPKG

ask-cli-x

Version:

Alexa Skills Kit (ASK) Command Line Interfaces

239 lines (238 loc) 11.3 kB
"use strict"; const path = require("path"); const fs = require("fs"); const R = require("ramda"); const awsUtil = require("../../../clients/aws-client/aws-util"); const CliCFNDeployerError = require("../../../exceptions/cli-cfn-deployer-error"); const Helper = require("./helper"); const SKILL_STACK_PUBLIC_FILE_NAME = "skill-stack.yaml"; const SKILL_STACK_ASSET_FILE_NAME = "basic-lambda.yaml"; const alexaAwsRegionMap = { default: "us-east-1", NA: "us-east-1", EU: "eu-west-1", FE: "us-west-2", }; module.exports = { bootstrap, invoke, }; /** * Bootstrap ask-cli resources config with initial state and examples. * @param {Object} options * @param {Function} callback */ function bootstrap(options, callback) { const { profile, workspacePath } = options; const userConfig = options.userConfig || {}; const templateLocation = path.join(workspacePath, SKILL_STACK_PUBLIC_FILE_NAME); let updatedUserConfig; try { const templateContent = fs.readFileSync(path.join(__dirname, "assets", SKILL_STACK_ASSET_FILE_NAME), "utf-8"); const awsProfile = awsUtil.getAWSProfile(profile); const awsDefaultRegion = awsUtil.getCLICompatibleDefaultRegion(awsProfile); fs.writeFileSync(templateLocation, templateContent); userConfig.templatePath = `./${path.posix.join("infrastructure", path.basename(workspacePath), SKILL_STACK_PUBLIC_FILE_NAME)}`; updatedUserConfig = R.set(R.lensPath(["awsRegion"]), awsDefaultRegion, userConfig); } catch (e) { return callback(e.message); } callback(null, { userConfig: updatedUserConfig }); } /** * Invoke the actual deploy logic for skill's infrastructures * @param {Object} reporter upstream CLI status reporter * @param {Object} options request object from deploy-delegate which provides user's input and project's info * { profile, alexaRegion, skillId, skillName, code, userConfig, deployState } * @param {Function} callback { errorStr, responseObject } * .errorStr deployDelegate will fail immediately with this errorStr * .responseObject.isAllStepSuccess show if the entire process succeeds * .responseObject.isCodeDeployed true if the code is uploaded sucessfully and no need to upload again * .responseObject.deployState record the state of deployment back to file (including partial success) * .responseObject.endpoint the final result endpoint response * .responseObject.resultMessage the message to summarize current situation */ async function invoke(reporter, options, callback) { const { alexaRegion, deployState = {} } = options; deployState[alexaRegion] = deployState[alexaRegion] || {}; const deployProgress = { isAllStepSuccess: false, isCodeDeployed: false, deployState: deployState[alexaRegion], }; try { await _deploy(reporter, options, deployProgress); deployProgress.resultMessage = _makeSuccessMessage(deployProgress.endpoint.uri, alexaRegion); callback(null, deployProgress); } catch (err) { deployProgress.resultMessage = _makeErrorMessage(err, alexaRegion); callback(null, deployProgress); } } async function _deploy(reporter, options, deployProgress) { const { profile, doDebug, alexaRegion, skillId, skillName, code, userConfig } = options; let { stackId } = deployProgress.deployState; const awsProfile = _getAwsProfile(profile); const awsRegion = _getAwsRegion(alexaRegion, userConfig); const templateBody = _getTemplateBody(alexaRegion, userConfig); const userDefinedParameters = _getUserDefinedParameters(alexaRegion, userConfig); const bucketName = _getS3BucketName(alexaRegion, userConfig, deployProgress.deployState, awsProfile, awsRegion); const bucketKey = _getS3BucketKey(alexaRegion, userConfig, code.codeBuild); const stackName = `ask-${skillName}-${alexaRegion}-skillStack-${Date.now()}`; const helper = new Helper(profile, doDebug, awsProfile, awsRegion, reporter); // skill credentials const skillCredentials = templateBody.includes("SkillClientId") && templateBody.includes("SkillClientSecret") ? await helper.getSkillCredentials(skillId) : {}; // s3 upload await helper.createS3BucketIfNotExists(bucketName); await helper.enableS3BucketVersioningIfNotEnabled(bucketName); const uploadResult = await helper.uploadToS3(bucketName, bucketKey, code.codeBuild); deployProgress.deployState.s3 = { bucket: bucketName, key: bucketKey, }; deployProgress.isCodeDeployed = true; // cf deploy const codeVersion = uploadResult.VersionId; const s3Artifact = { bucketName, bucketKey, codeVersion }; const stackParameters = _mapStackParameters(skillId, skillCredentials, userConfig, s3Artifact, userDefinedParameters); const capabilities = _getCapabilities(alexaRegion, userConfig); const deployRequest = await helper.deployStack(stackId, stackName, templateBody, stackParameters, capabilities); stackId = deployRequest.StackId; const deployResult = await helper.waitForStackDeploy(stackId, reporter); deployProgress.deployState.stackId = stackId; deployProgress.deployState.outputs = deployResult.stackInfo.Outputs; deployProgress.endpoint = { uri: deployResult.endpointUri }; deployProgress.isAllStepSuccess = true; return deployResult; } function _getAwsProfile(profile) { const awsProfile = awsUtil.getAWSProfile(profile); if (!awsProfile) { throw new CliCFNDeployerError(`Profile [${profile}] doesn't have AWS profile linked to it. ` + 'Please run "ask configure" to re-configure your profile.'); } return awsProfile; } function _getAwsRegion(alexaRegion, userConfig) { let awsRegion = alexaRegion === "default" ? userConfig.awsRegion : R.path(["regionalOverrides", alexaRegion, "awsRegion"], userConfig); awsRegion = awsRegion || alexaAwsRegionMap[alexaRegion]; if (!awsRegion) { throw new CliCFNDeployerError(`Unsupported Alexa region: ${alexaRegion}. ` + 'Please check your region name or use "regionalOverrides" to specify AWS region.'); } return awsRegion; } function _getS3BucketName(alexaRegion, userConfig, currentRegionDeployState, awsProfile, awsRegion) { const customValue = R.path(["regionalOverrides", alexaRegion, "artifactsS3", "bucketName"], userConfig) || R.path(["artifactsS3", "bucketName"], userConfig); if (customValue) return customValue; function generateBucketName() { const projectName = path.basename(process.cwd()); const validProjectName = projectName .toLowerCase() .replace(/[^a-z0-9-.]+/g, "") .substring(0, 22); const validProfile = awsProfile .toLowerCase() .replace(/[^a-z0-9-.]+/g, "") .substring(0, 9); const shortRegionName = awsRegion.replace(/-/g, ""); return `ask-${validProjectName}-${validProfile}-${shortRegionName}-${Date.now()}`; } return R.path(["s3", "bucket"], currentRegionDeployState) || generateBucketName(); } function _getS3BucketKey(alexaRegion, userConfig, codeBuild) { const customValue = R.path(["regionalOverrides", alexaRegion, "artifactsS3", "bucketKey"], userConfig) || R.path(["artifactsS3", "bucketKey"], userConfig); if (customValue) return customValue; return `endpoint/${path.basename(codeBuild)}`; } function _getCapabilities(alexaRegion, userConfig) { let capabilities = R.path(["regionalOverrides", alexaRegion, "cfn", "capabilities"], userConfig) || R.path(["cfn", "capabilities"], userConfig); capabilities = new Set(capabilities || []); capabilities.add("CAPABILITY_IAM"); return Array.from(capabilities); } function _getUserDefinedParameters(alexaRegion, userConfig) { const reservedParameters = { SkillId: "Please use a different name.", SkillClientId: "Please use a different name.", SkillClientSecret: "Please use a different name.", LambdaRuntime: "Please specify under skillInfrastructure.userConfig.runtime.", LambdaHandler: "Please specify under skillInfrastructure.userConfig.handler.", CodeBucket: "Please specify under skillInfrastructure.userConfig.artifactsS3.bucketName.", CodeKey: "Please specify under skillInfrastructure.userConfig.artifactsS3.bucketKey.", CodeVersion: "Please use a different name.", }; const reservedParametersKeys = new Set(Object.keys(reservedParameters)); let parameters = R.path(["regionalOverrides", alexaRegion, "cfn", "parameters"], userConfig) || R.path(["cfn", "parameters"], userConfig); parameters = parameters || []; Object.keys(parameters).forEach((key) => { if (reservedParametersKeys.has(key)) { const message = reservedParameters[key]; throw new CliCFNDeployerError(`Cloud Formation parameter "${key}" is reserved. ${message}`); } }); return parameters; } function _getTemplateBody(alexaRegion, userConfig) { const templatePath = R.path(["regionalOverrides", alexaRegion, "templatePath"], userConfig) || userConfig.templatePath; if (!templatePath) { throw new CliCFNDeployerError("The template path in userConfig must be provided."); } return fs.readFileSync(templatePath, "utf-8"); } function _makeSuccessMessage(endpointUri, alexaRegion) { return `The CloudFormation deploy succeeded for Alexa region "${alexaRegion}" with output Lambda ARN: ${endpointUri}.`; } function _makeErrorMessage(error, alexaRegion) { return `The CloudFormation deploy failed for Alexa region "${alexaRegion}": ${error.message}`; } function _mapStackParameters(skillId, skillCredentials, userConfig, s3Artifact, userDefinedParameters) { const parameters = [ { ParameterKey: "SkillId", ParameterValue: skillId, }, { ParameterKey: "LambdaRuntime", ParameterValue: userConfig.runtime, }, { ParameterKey: "LambdaHandler", ParameterValue: userConfig.handler, }, { ParameterKey: "CodeBucket", ParameterValue: s3Artifact.bucketName, }, { ParameterKey: "CodeKey", ParameterValue: s3Artifact.bucketKey, }, { ParameterKey: "CodeVersion", ParameterValue: s3Artifact.codeVersion, }, ]; if (skillCredentials.clientId && skillCredentials.clientSecret) { const clientIdParameter = { ParameterKey: "SkillClientId", ParameterValue: skillCredentials.clientId, }; const clientSecretParameter = { ParameterKey: "SkillClientSecret", ParameterValue: skillCredentials.clientSecret, }; parameters.push(clientIdParameter, clientSecretParameter); } Object.keys(userDefinedParameters).forEach((key) => { const parameter = { ParameterKey: key, ParameterValue: userDefinedParameters[key], }; parameters.push(parameter); }); return parameters; }