Skip to content

Commit

Permalink
feat: add skill events property to ask-resources.json (#410)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsetton authored Aug 4, 2022
1 parent bd4c2fc commit ccf7972
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 46 deletions.
72 changes: 43 additions & 29 deletions docs/concepts/Alexa-Skill-Project-Definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
}
}
Expand All @@ -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}",
Expand All @@ -121,8 +136,7 @@ Below shows the example how CLI tracks user's config and the deployment states,
"{supportedRegion}": { ... }
}
}
},

}
}
}
}
Expand Down
24 changes: 18 additions & 6 deletions lib/controllers/skill-infrastructure-controller/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions lib/model/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
4 changes: 4 additions & 0 deletions lib/model/resources-config/ask-resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
4 changes: 4 additions & 0 deletions lib/model/resources-config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
64 changes: 54 additions & 10 deletions test/unit/controller/skill-infrastructure-controller-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -387,41 +387,82 @@ 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');
done();
});
});

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();
});
Expand All @@ -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');
Expand Down
1 change: 0 additions & 1 deletion test/unit/fixture/model/regular-proj/ask-resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"template": "./awsStacks/skill-infra.yaml",
"targetEndpoint": [
"custom",
"events",
"smartHome"
],
"regionOverrides": {
Expand Down

0 comments on commit ccf7972

Please sign in to comment.