diff --git a/docs/concepts/Alexa-Skill-Project-Definition.md b/docs/concepts/Alexa-Skill-Project-Definition.md index 0a73bcb7..64021e8c 100644 --- a/docs/concepts/Alexa-Skill-Project-Definition.md +++ b/docs/concepts/Alexa-Skill-Project-Definition.md @@ -26,51 +26,66 @@ The `SkillInfrastructure` represents the configuration on how to deploy skill's Please check the `skillInfrastructure` field in the example below for its representation in project config. # Project Config For Resources Management -Below shows the example how CLI tracks user's config and the deployment states, using `@ask-cli/cfn-deployer` deployer as an example. +Below shows the example how CLI tracks user's config and the deployment states, using `@ask-cli/cfn-deployer` deployer as an example. ### ask-resources.json (this file is up to configure!) ```jsonc { - // each config is tracked specific to each profile - "profiles": { - "{profileName}": { + "profiles": { + "{profileName}": { // profile name-specific config - // skillMetadata tracks Alexa skill's build-time JSONs - "skillMetadata": { - "src": "./skill-package", // the source folder for skill package (either relative path or absolute path) - "lastDeployHash": "{hashResult}" // CLI internal data to optimize the deploy flow + "skillMetadata": { // Alexa skill metadata to deploy + "src": "./skill-package", // source folder for skill package (either relative path or absolute path) + "lastDeployHash": "{hashResult}" // CLI internal data to optimize the deploy flow }, - // code owns Alexa skill code to be built and hosted - "code": { + "code": { // Alexa skill code to be built and hosted "default": { // region for the codebase - "src": "./code" // the source folder for codebase + "src": "./code" // source folder for codebase }, - "{supportedRegion}": { // the supported regions are always in sync with Alexa, which includes default, NA, EU, FE. + "{supportedRegion}": { // supported regions are always in sync with Alexa, which includes default, NA, EU, FE. "src": "./code" } }, - // skillInfrastructure tracks the settings and states for skill api-endpoint's deployer - "skillInfrastructure": { - "type": "@ask-cli/cfn-deployer", // selected deployer to invoke when deploy - "userConfig": { // tracks settings for the deployer, deployer-specific + "skillInfrastructure": { // Alexa skill infrastructure to deploy + "type": "@ask-cli/cfn-deployer", // deployer type + "userConfig": { // deployer-specific config "awsRegion": "{aws-region}", "runtime": "{lambdaRuntime}", "handler": "{lambdaHandler}", "templatePath": "stack.yaml", - "targetEndpoint": ["alexaForBusiness", "custom", "flashBriefing", "health", "householdList", "music", "smartHome", "video", "events"], // set the targetEndpoint with the value(s) from https://developer.amazon.com/en-US/docs/alexa/smapi/skill-manifest.html#api-enumeration or events to target events endpoint. Defaults to api endpoints from skill manifest if not specified. - "artifactsS3": { // custom s3 configuration to upload skill build artifact (zip, jar,etc) - "bucketName": "{bucketName}", // custom bucket name to store artifacts - "bucketKey": "{bucketKey.zip}" // custom bucket object key + "targetEndpoint": [ // defaults to api names from skill manifest if not specified + "alexaForBusiness", + "custom", + "flashBriefing", + "health", + "householdList", + "music", + "smartHome", + "video" + ], + "skillEvents": { // skill events support for deployer endpoint(s) + "publications": [ // list of proactive event names + "{proactiveEventName1}", + ... + ], + "subscriptions": [ // list of skill/list event names + "{skillOrListEventName1}", + ... + ] + }, + "artifactsS3": { // custom s3 configuration to upload skill build artifact (zip, jar, etc) + "bucketName": "{bucketName}", // custom bucket name to store artifacts + "bucketKey": "{bucketKey.zip}" // custom bucket object key }, "cfn": { - "parameters": { // additional parameters to pass to the CloudFormation + "parameters": { // additional parameters to pass to the CloudFormation "SomeUserParameter1Key": "some value", "SomeUserParameter2Key": "another value" }, - "capabilities": [ // additional capabilities to pass to the CloudFormation. CAPABILITY_IAM capability is always passed by default. - "CAPABILITY_NAMED_IAM" + "capabilities": [ // additional capabilities to pass to the CloudFormation + "CAPABILITY_NAMED_IAM" // CAPABILITY_IAM is always passed by default ] } } @@ -95,21 +110,21 @@ Below shows the example how CLI tracks user's config and the deployment states, "default": { "lastDeployHash": "{hashResult}" // CLI internal data to optimize the deploy flow }, - "{supportedRegion}": { // the supported regions are always in sync with Alexa, which includes default, NA, EU, FE. + "{supportedRegion}": { // supported regions are always in sync with Alexa, which includes default, NA, EU, FE. "lastDeployHash": "{hashResult}" } }, "skillInfrastructure": { - "@ask-cli/cfn-deployer": { // selected deployer to invoke when deploy - "deployState": { // tracks states for the deployer to continuously deploy, deployer-specific + "@ask-cli/cfn-deployer": { // deployer type + "deployState": { // deployer-specific states "default": { "s3": { "bucket": "{bucket}", "key": "{key}", "objectVersion": "{version}" }, - "outputs": [ // outputs from the CloudFormation deploy + "outputs": [ // outputs from the CloudFormation deploy { "OutputKey": "{outputKey}", "OutputValue": "{outputValue}", @@ -121,8 +136,7 @@ Below shows the example how CLI tracks user's config and the deployment states, "{supportedRegion}": { ... } } } - }, - + } } } } diff --git a/lib/controllers/skill-infrastructure-controller/index.js b/lib/controllers/skill-infrastructure-controller/index.js index 55cd21d8..d4834ea8 100644 --- a/lib/controllers/skill-infrastructure-controller/index.js +++ b/lib/controllers/skill-infrastructure-controller/index.js @@ -143,16 +143,28 @@ module.exports = class SkillInfrastructureController { 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 + // 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) => { - if (domain === Manifest.endpointTypes.EVENTS) { - Manifest.getInstance().setEventsEndpointByRegion(region, rawDeployResult[region].endpoint); - } else { - Manifest.getInstance().setApisEndpointByDomainRegion(domain, region, rawDeployResult[region].endpoint); - } + 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(); // 2.compare with current hash result to decide if skill.json file need to be updated diff --git a/lib/model/manifest.js b/lib/model/manifest.js index 55888552..d6808062 100644 --- a/lib/model/manifest.js +++ b/lib/model/manifest.js @@ -112,4 +112,20 @@ module.exports = class Manifest extends ConfigFile { this.setProperty(['manifest', Manifest.endpointTypes.APIS, domain, 'regions', region, 'endpoint'], endpointObj); } } + + getEventsPublications() { + return this.getProperty(['manifest', Manifest.endpointTypes.EVENTS, 'publications']); + } + + setEventsPublications(publications) { + this.setProperty(['manifest', Manifest.endpointTypes.EVENTS, 'publications'], publications); + } + + getEventsSubscriptions() { + return this.getProperty(['manifest', Manifest.endpointTypes.EVENTS, 'subscriptions']); + } + + setEventsSubscriptions(subscriptions) { + this.setProperty(['manifest', Manifest.endpointTypes.EVENTS, 'subscriptions'], subscriptions); + } }; diff --git a/lib/model/resources-config/ask-resources.js b/lib/model/resources-config/ask-resources.js index b0734371..9d57675a 100644 --- a/lib/model/resources-config/ask-resources.js +++ b/lib/model/resources-config/ask-resources.js @@ -93,6 +93,10 @@ module.exports = class AskResources extends ConfigFile { getTargetEndpoint(profile) { return this.getProperty(['profiles', profile, 'skillInfrastructure', 'userConfig', 'targetEndpoint']) || []; } + + getSkillEvents(profile) { + return this.getProperty(['profiles', profile, 'skillInfrastructure', 'userConfig', 'skillEvents']); + } }; module.exports.BASE = BASE; diff --git a/lib/model/resources-config/index.js b/lib/model/resources-config/index.js index bfa33593..aa5ccb2b 100644 --- a/lib/model/resources-config/index.js +++ b/lib/model/resources-config/index.js @@ -117,6 +117,10 @@ module.exports = class ResourcesConfig { return AskResources.getInstance().getTargetEndpoint(profile); } + getSkillEvents(profile) { + return AskResources.getInstance().getSkillEvents(profile); + } + // Group for the "skillInfrastructure" getSkillInfraType(profile) { return AskResources.getInstance().getSkillInfraType(profile); diff --git a/test/unit/controller/skill-infrastructure-controller-test.js b/test/unit/controller/skill-infrastructure-controller-test.js index 1c9bfb39..5b29d740 100644 --- a/test/unit/controller/skill-infrastructure-controller-test.js +++ b/test/unit/controller/skill-infrastructure-controller-test.js @@ -387,16 +387,19 @@ describe('Controller test - skill infrastructure controller test', () => { sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); sinon.stub(hashUtils, 'getHash').callsArgWith(1, null, 'TEST_HASH'); sinon.stub(SkillInfrastructureController.prototype, '_ensureSkillManifestGotUpdated').callsArgWith(0); - const setEventsEndpointByRegionSpy = sinon.spy(Manifest.prototype, 'setEventsEndpointByRegion'); const setApisEndpointByDomainRegionSpy = sinon.spy(Manifest.prototype, 'setApisEndpointByDomainRegion'); - + const setEventsEndpointByRegionSpy = sinon.spy(Manifest.prototype, 'setEventsEndpointByRegion'); + const setEventsPublicationsSpy = sinon.spy(Manifest.prototype, 'setEventsPublications'); + const setEventsSubscriptionsSpy = sinon.spy(Manifest.prototype, 'setEventsSubscriptions'); // call skillInfraController.updateSkillManifestWithDeployResult(TEST_DEPLOY_RESULT, (err, res) => { // verify expect(res).equal(undefined); expect(err).equal(undefined); - expect(setEventsEndpointByRegionSpy.callCount).eq(2); expect(setApisEndpointByDomainRegionSpy.callCount).eq(4); + expect(setEventsEndpointByRegionSpy.callCount).eq(0); + expect(setEventsPublicationsSpy.callCount).eq(0); + expect(setEventsSubscriptionsSpy.callCount).eq(0); expect(Manifest.getInstance().getApisEndpointByDomainRegion('custom', 'default').url).equal('TEST_URL1'); expect(Manifest.getInstance().getApisEndpointByDomainRegion('custom', 'EU').url).equal('TEST_URL2'); expect(ResourcesConfig.getInstance().getSkillMetaLastDeployHash(TEST_PROFILE)).equal('TEST_HASH'); @@ -404,24 +407,62 @@ describe('Controller test - skill infrastructure controller test', () => { }); }); - it('| manifest update correctly without events endpoint, expect success message and new hash set', (done) => { + it('| manifest update correctly with events endpoint only, expect success message and new hash set', (done) => { // setup sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); sinon.stub(hashUtils, 'getHash').callsArgWith(1, null, 'TEST_HASH'); sinon.stub(SkillInfrastructureController.prototype, '_ensureSkillManifestGotUpdated').callsArgWith(0); - sinon.stub(ResourcesConfig.prototype, 'getTargetEndpoints').returns(['custom', 'smartHome']); - const setEventsEndpointByRegionSpy = sinon.spy(Manifest.prototype, 'setEventsEndpointByRegion'); + sinon.stub(ResourcesConfig.prototype, 'getSkillEvents').returns({}); const setApisEndpointByDomainRegionSpy = sinon.spy(Manifest.prototype, 'setApisEndpointByDomainRegion'); + const setEventsEndpointByRegionSpy = sinon.spy(Manifest.prototype, 'setEventsEndpointByRegion'); + const setEventsPublicationsSpy = sinon.spy(Manifest.prototype, 'setEventsPublications'); + const setEventsSubscriptionsSpy = sinon.spy(Manifest.prototype, 'setEventsSubscriptions'); + // call + skillInfraController.updateSkillManifestWithDeployResult(TEST_DEPLOY_RESULT, (err, res) => { + // verify + expect(res).equal(undefined); + expect(err).equal(undefined); + expect(setApisEndpointByDomainRegionSpy.callCount).eq(4); + expect(setEventsEndpointByRegionSpy.callCount).eq(2); + expect(setEventsPublicationsSpy.callCount).eq(0); + expect(setEventsSubscriptionsSpy.callCount).eq(0); + expect(Manifest.getInstance().getApisEndpointByDomainRegion('custom', 'default').url).equal('TEST_URL1'); + expect(Manifest.getInstance().getApisEndpointByDomainRegion('custom', 'EU').url).equal('TEST_URL2'); + expect(Manifest.getInstance().getEventsEndpointByRegion('default').url).equal('TEST_URL1'); + expect(Manifest.getInstance().getEventsEndpointByRegion('EU').url).equal('TEST_URL2'); + expect(ResourcesConfig.getInstance().getSkillMetaLastDeployHash(TEST_PROFILE)).equal('TEST_HASH'); + done(); + }); + }); + it('| manifest update correctly with events properties, expect success message and new hash set', (done) => { + // setup + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); + sinon.stub(hashUtils, 'getHash').callsArgWith(1, null, 'TEST_HASH'); + sinon.stub(SkillInfrastructureController.prototype, '_ensureSkillManifestGotUpdated').callsArgWith(0); + sinon.stub(ResourcesConfig.prototype, 'getSkillEvents').returns({ + publications: ["TEST_PUBLICATION"], + subscriptions: ["TEST_SUBSCRIPTION"] + }); + const setApisEndpointByDomainRegionSpy = sinon.spy(Manifest.prototype, 'setApisEndpointByDomainRegion'); + const setEventsEndpointByRegionSpy = sinon.spy(Manifest.prototype, 'setEventsEndpointByRegion'); + const setEventsPublicationsSpy = sinon.spy(Manifest.prototype, 'setEventsPublications'); + const setEventsSubscriptionsSpy = sinon.spy(Manifest.prototype, 'setEventsSubscriptions'); // call skillInfraController.updateSkillManifestWithDeployResult(TEST_DEPLOY_RESULT, (err, res) => { // verify expect(res).equal(undefined); expect(err).equal(undefined); - expect(setEventsEndpointByRegionSpy.callCount).eq(0); expect(setApisEndpointByDomainRegionSpy.callCount).eq(4); + expect(setEventsEndpointByRegionSpy.callCount).eq(2); + expect(setEventsPublicationsSpy.callCount).eq(1); + expect(setEventsSubscriptionsSpy.callCount).eq(1); expect(Manifest.getInstance().getApisEndpointByDomainRegion('custom', 'default').url).equal('TEST_URL1'); expect(Manifest.getInstance().getApisEndpointByDomainRegion('custom', 'EU').url).equal('TEST_URL2'); + expect(Manifest.getInstance().getEventsEndpointByRegion('default').url).equal('TEST_URL1'); + expect(Manifest.getInstance().getEventsEndpointByRegion('EU').url).equal('TEST_URL2'); + expect(Manifest.getInstance().getEventsPublications()[0].eventName).equal('TEST_PUBLICATION'); + expect(Manifest.getInstance().getEventsSubscriptions()[0].eventName).equal('TEST_SUBSCRIPTION'); expect(ResourcesConfig.getInstance().getSkillMetaLastDeployHash(TEST_PROFILE)).equal('TEST_HASH'); done(); }); @@ -433,16 +474,19 @@ describe('Controller test - skill infrastructure controller test', () => { sinon.stub(hashUtils, 'getHash').callsArgWith(1, null, 'TEST_HASH'); sinon.stub(SkillInfrastructureController.prototype, '_ensureSkillManifestGotUpdated').callsArgWith(0); sinon.stub(ResourcesConfig.prototype, 'getTargetEndpoints').returns([]); - const setEventsEndpointByRegionSpy = sinon.spy(Manifest.prototype, 'setEventsEndpointByRegion'); const setApisEndpointByDomainRegionSpy = sinon.spy(Manifest.prototype, 'setApisEndpointByDomainRegion'); - + const setEventsEndpointByRegionSpy = sinon.spy(Manifest.prototype, 'setEventsEndpointByRegion'); + const setEventsPublicationsSpy = sinon.spy(Manifest.prototype, 'setEventsPublications'); + const setEventsSubscriptionsSpy = sinon.spy(Manifest.prototype, 'setEventsSubscriptions'); // call skillInfraController.updateSkillManifestWithDeployResult(TEST_DEPLOY_RESULT, (err, res) => { // verify expect(res).equal(undefined); expect(err).equal(undefined); - expect(setEventsEndpointByRegionSpy.callCount).eq(0); expect(setApisEndpointByDomainRegionSpy.callCount).eq(2); + expect(setEventsEndpointByRegionSpy.callCount).eq(0); + expect(setEventsPublicationsSpy.callCount).eq(0); + expect(setEventsSubscriptionsSpy.callCount).eq(0); expect(Manifest.getInstance().getApisEndpointByDomainRegion('custom', 'default').url).equal('TEST_URL1'); expect(Manifest.getInstance().getApisEndpointByDomainRegion('custom', 'EU').url).equal('TEST_URL2'); expect(ResourcesConfig.getInstance().getSkillMetaLastDeployHash(TEST_PROFILE)).equal('TEST_HASH'); diff --git a/test/unit/fixture/model/regular-proj/ask-resources.json b/test/unit/fixture/model/regular-proj/ask-resources.json index 7e3b9390..09ef55cf 100644 --- a/test/unit/fixture/model/regular-proj/ask-resources.json +++ b/test/unit/fixture/model/regular-proj/ask-resources.json @@ -24,7 +24,6 @@ "template": "./awsStacks/skill-infra.yaml", "targetEndpoint": [ "custom", - "events", "smartHome" ], "regionOverrides": {