Skip to content

Commit

Permalink
feat: enhance post skillInfrastructure deploy by comparing hash befor…
Browse files Browse the repository at this point in the history
…e calling updateManifest and adding polling to ensure update request completes
  • Loading branch information
RonWang committed Nov 5, 2019
1 parent f5f1f1f commit 10b2e63
Show file tree
Hide file tree
Showing 14 changed files with 352 additions and 71 deletions.
5 changes: 3 additions & 2 deletions .commitlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
"body-leading-blank": [1, "always"],
"footer-leading-blank": [1, "always"],
"subject-empty": [2, "never"],
"subject-full-stop": [2, "never", "."],
"subject-case": [
2,
"always",
["sentence-case", "lower-case", "pascal-case", "camel-case"]
"never",
["sentence-case", "pascal-case", "start-case", "upper-case"]
],
"scope-case": [2, "always", "lower-case"],
"type-case": [2, "always", "lower-case"],
Expand Down
4 changes: 2 additions & 2 deletions lib/commands/api/test/simulate-skill/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ class SimulateSkillCommand extends AbstractCommand {
});
};

const terminateCondition = retryResponse => !retryResponse.status
const shouldRetryCondition = retryResponse => !retryResponse.status
|| retryResponse.status === CONSTANTS.SKILL.SIMULATION_STATUS.SUCCESS
|| retryResponse.status === CONSTANTS.SKILL.SIMULATION_STATUS.FAILURE;

Retry.retry(retryConfig, retryCall, terminateCondition, (err, res) => {
Retry.retry(retryConfig, retryCall, shouldRetryCondition, (err, res) => {
if (err) {
return callback(err);
}
Expand Down
4 changes: 2 additions & 2 deletions lib/commands/api/validation/validate-skill/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ function _keepPollingSkillValidationResult(smapiClient, skillId, validationId, s
});
};

const terminateCondition = retryResponse => !retryResponse.status || retryResponse.status === CONSTANTS.SKILL.VALIDATION_STATUS.SUCCESS
const shouldRetryCondition = retryResponse => !retryResponse.status || retryResponse.status === CONSTANTS.SKILL.VALIDATION_STATUS.SUCCESS
|| retryResponse.status === CONSTANTS.SKILL.VALIDATION_STATUS.FAILURE;

Retry.retry(retryConfig, retryCall, terminateCondition, (err, res) => {
Retry.retry(retryConfig, retryCall, shouldRetryCondition, (err, res) => {
if (err) {
return callback(err);
}
Expand Down
89 changes: 78 additions & 11 deletions lib/controllers/skill-infrastructure-controller/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const Manifest = require('@src/model/manifest');
const jsonView = require('@src/view/json-view');
const MultiTasksView = require('@src/view/multi-tasks-view');
const Messenger = require('@src/view/messenger');
const retryUtils = require('@src/utils/retry-utility');
const hashUtils = require('@src/utils/hash-utils');
const stringUtils = require('@src/utils/string-utils');
const CONSTANTS = require('@src/utils/constants');
Expand Down Expand Up @@ -146,20 +147,25 @@ module.exports = class SkillInfrastructureController {
});
});
Manifest.getInstance().write();
// 2.update skill manifest
const smapiClient = new SmapiClient({ profile: this.profile, doDebug: this.doDebug });
const skillId = ResourcesConfig.getInstance().getSkillId(this.profile);
smapiClient.skill.manifest.updateManifest(skillId, CONSTANTS.SKILL.STAGE.DEVELOPMENT, Manifest.getInstance().content, null,
(updateErr, updateResponse) => {
if (updateErr) {
return callback(updateErr);
}
if (updateResponse.statusCode >= 300) {
return callback(jsonView.toString(updateResponse.body));
// 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(ResourcesConfig.getInstance().getSkillMetaSrc(this.profile), (hashErr, currentHash) => {
if (hashErr) {
return callback(hashErr);
}
if (currentHash === ResourcesConfig.getInstance().getSkillMetaLastDeployHash(this.profile)) {
return callback();
}
// 3.update skill manifest
this._ensureSkillManifestGotUpdated((manifestUpdateErr) => {
if (manifestUpdateErr) {
return callback(manifestUpdateErr);
}
Messenger.getInstance().info(' The api endpoint of skill.json have been updated from the skill infrastructure deploy results.');
ResourcesConfig.getInstance().setSkillMetaLastDeployHash(this.profile, currentHash);
Messenger.getInstance().info(' The api endpoints of skill.json have been updated from the skill infrastructure deploy results.');
callback();
});
});
}

/**
Expand Down Expand Up @@ -228,4 +234,65 @@ module.exports = class SkillInfrastructureController {
ResourcesConfig.getInstance().setSkillInfraDeployState(this.profile, newDeployState);
ResourcesConfig.getInstance().write();
}

/**
* Make sure the skill manifest is updated successfully by submitting the request to SMAPI and keep polling until complete.
* @param {Function} callback
*/
_ensureSkillManifestGotUpdated(callback) {
const smapiClient = new SmapiClient({ profile: this.profile, doDebug: this.doDebug });
const skillId = ResourcesConfig.getInstance().getSkillId(this.profile);
// update manifest
smapiClient.skill.manifest.updateManifest(skillId, CONSTANTS.SKILL.STAGE.DEVELOPMENT, Manifest.getInstance().content, null,
(updateErr, updateResponse) => {
if (updateErr) {
return callback(updateErr);
}
if (updateResponse.statusCode >= 300) {
return callback(jsonView.toString(updateResponse.body));
}
// poll manifest status until finish
this._pollSkillStatus(smapiClient, skillId, (pollErr, pollResponse) => {
if (pollErr) {
return callback(pollErr);
}
const manifestStatus = R.view(R.lensPath(['body', 'manifest', 'lastUpdateRequest', 'status']), pollResponse);
if (!manifestStatus) {
return callback(`[Error]: Failed to extract the manifest result from SMAPI's response.\n${pollResponse}`);
}
if (manifestStatus !== CONSTANTS.SKILL.SKILL_STATUS.SUCCEEDED) {
return callback(`[Error]: Updating skill manifest but received non-success message from SMAPI: ${manifestStatus}`);
}
callback();
});
});
}

/**
* Poll skill's manifest status until the status is not IN_PROGRESS.
* @param {Object} smapiClient
* @param {String} skillId
* @param {Function} callback
*/
_pollSkillStatus(smapiClient, skillId, callback) {
const retryConfig = {
base: 1000,
factor: 1.1,
maxRetry: 50
};
const retryCall = (loopCallback) => {
smapiClient.skill.getSkillStatus(skillId, [CONSTANTS.SKILL.RESOURCES.MANIFEST], (statusErr, statusResponse) => {
if (statusErr) {
return loopCallback(statusErr);
}
if (statusResponse.statusCode >= 300) {
return loopCallback(jsonView.toString(statusResponse.body));
}
loopCallback(null, statusResponse);
});
};
const shouldRetryCondition = retryResponse => R.view(R.lensPath(['body', 'manifest', 'lastUpdateRequest', 'status']), retryResponse)
=== CONSTANTS.SKILL.SKILL_STATUS.IN_PROGRESS;
retryUtils.retry(retryConfig, retryCall, shouldRetryCondition, (err, res) => callback(err, err ? null : res));
}
};
4 changes: 2 additions & 2 deletions lib/controllers/skill-metadata-controller/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ module.exports = class SkillMetadataController {
loopCallback(null, pollResponse);
});
};
const terminateCondition = retryResponse => retryResponse.body.status === CONSTANTS.SKILL.PACKAGE_STATUS.IN_PROGRESS;
retryUtils.retry(retryConfig, retryCall, terminateCondition, (err, res) => callback(err, err ? null : res));
const shouldRetryCondition = retryResponse => retryResponse.body.status === CONSTANTS.SKILL.PACKAGE_STATUS.IN_PROGRESS;
retryUtils.retry(retryConfig, retryCall, shouldRetryCondition, (err, res) => callback(err, err ? null : res));
}
};
9 changes: 7 additions & 2 deletions lib/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ module.exports.SKILL = {
LIVE: 'live',
CERTIFICATION: 'certification'
},
SKILL_STATUS: {
SUCCEEDED: 'SUCCEEDED',
FAILED: 'FAILED',
IN_PROGRESS: 'IN_PROGRESS',
},
PACKAGE_STATUS: {
SUCCEEDED: 'SUCCEEDED',
FAILED: 'FAILED',
Expand Down Expand Up @@ -248,8 +253,8 @@ module.exports.LWA = {
DEFAULT_STATE: 'Ask-SkillModel-ReadWrite',
DEFAULT_SCOPES: `${SCOPES_SKILLS_READWRITE} ${SCOPES_MODELS_READWRITE} ${SCOPES_SKILLS_TEST} ${SCOPES_CATALOG_READ} ${SCOPES_CATALOG_READWRITE}`,
CLI_DEFAULT_CREDENTIALS: {
CLIENT_ID: '',
CLIENT_SECRET: ''
CLIENT_ID: 'amzn1.application-oa2-client.aad322b5faab44b980c8f87f94fbac56',
CLIENT_CONFIRMATION: '1642d8869b829dda3311d6c6539f3ead55192e3fc767b9071c888e60ef151cf9'
}
};

Expand Down
7 changes: 4 additions & 3 deletions lib/utils/lwa.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ module.exports = {
/**
* Use LWA OAuth2 to retrieve access tokens.
* @param needBrowser
* @param lwaOptions contains clientId, clientSecret, scopes and state
* @param lwaOptions contains clientId, clientConfirmation, scopes and state
* @param callback Json object which includes:
* 'access_token', 'refresh_token', 'token_type', 'expires_in', and 'expires_at'
*/
function accessTokenGenerator(needBrowser, lwaOptions, callback) {
const {
clientId,
clientSecret,
clientConfirmation,
scopes = CONSTANTS.LWA.DEFAULT_SCOPES,
state = CONSTANTS.LWA.DEFAULT_STATE
} = lwaOptions;

const OAuth = oauthWrapper.createOAuth(clientId, clientSecret);
const OAuth = oauthWrapper.createOAuth(clientId, clientConfirmation);

if (!needBrowser) {
// prepare url which the user can use to call LWA
Expand Down Expand Up @@ -127,6 +127,7 @@ function _requestTokens(authCode, redirect_uri, OAuth, callback) {
redirect_uri: redirect_uri
};


OAuth.authorizationCode.getToken(tokenConfig, (error, result) => {
if (error) {
callback('Cannot obtain access token. ' + error);
Expand Down
18 changes: 9 additions & 9 deletions lib/utils/oauth-wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ module.exports = {
};

/* If you want to create tools to call SMAPI(Skill Management API),
* please create your own clientId and clientSecret through LWA (login with Amazon).
* please create your own clientId and ClientConfirmation through LWA (login with Amazon).
* https://login.amazon.com/website.
*
* You can find necessary scopes for LWA to call SMAPI here:
* https://developer.amazon.com/docs/smapi/ask-cli-intro.html#smapi-intro
*/

function createOAuth(clientId, clientSecret) {
function createOAuth(inputClientId, inputClientConfirmation) {
// Set default CLI LWA value
let id = clientId || CONSTANTS.LWA.CLI_DEFAULT_CREDENTIALS.CLIENT_ID;
let secret = clientSecret || CONSTANTS.LWA.CLI_DEFAULT_CREDENTIALS.CLIENT_SECRET;
let clientId = inputClientId || CONSTANTS.LWA.CLI_DEFAULT_CREDENTIALS.CLIENT_ID;
let clientConfirmation = inputClientConfirmation || CONSTANTS.LWA.CLI_DEFAULT_CREDENTIALS.CLIENT_CONFIRMATION;
let authorizeHost = 'https://www.amazon.com';
const authorizePath = '/ap/oa';
let tokenHost = 'https://api.amazon.com';
Expand All @@ -33,11 +33,11 @@ function createOAuth(clientId, clientSecret) {
// Overrite LWA options from Environmental Variable
const envVarClientId = process.env.ASK_LWA_CLIENT_ID;
if (stringUtils.isNonBlankString(envVarClientId)) {
id = envVarClientId;
clientId = envVarClientId;
}
const envVarClientSecret = process.env.ASK_LWA_CLIENT_SECRET;
if (stringUtils.isNonBlankString(envVarClientSecret)) {
secret = envVarClientSecret;
const envVarClientConfirmation = process.env.ASK_LWA_CLIENT_CONFIRMATION;
if (stringUtils.isNonBlankString(envVarClientConfirmation)) {
clientConfirmation = envVarClientConfirmation;
}
const envVarAuthorizeHost = process.env.ASK_LWA_AUTHORIZE_HOST;
if (stringUtils.isNonBlankString(envVarAuthorizeHost)) {
Expand All @@ -49,7 +49,7 @@ function createOAuth(clientId, clientSecret) {
}

return oauth2.create({
client: { id, secret },
client: { clientId, clientConfirmation },
auth: { authorizeHost, authorizePath, tokenHost, tokenPath }
});
}
Expand Down
8 changes: 4 additions & 4 deletions lib/utils/retry-utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ module.exports = {
* The function can be used for polling as well as retry scenarios.
* @param retryConfig: Captures the retry configuration - base, factor, pollcount and maxRetry.
* @param retryCall: Calling function defines a retry call that is invoked in each retry attempt.
* @param terminateCondition - The calling function can define a terminateCondition function that
* @param shouldRetryCondition - The calling function can define a shouldRetryCondition function: return true to keep retry and false to stop
* @param callback - The callback function returns either error or error and response from the last retry.
* leads to the termination of the while loop in the retry logic.
*/
function retry(retryConfig, retryCall, terminateCondition, callback) {
function retry(retryConfig, retryCall, shouldRetryCondition, callback) {
let lastResponse;
if (!retryConfig) {
return callback('[Error]: Invalid retry configuration. Retry configuration with values - base, factor and maxRetry needs to be specified');
Expand All @@ -35,7 +35,7 @@ function retry(retryConfig, retryCall, terminateCondition, callback) {
let pollCount = -1;
async.doWhilst(
(loopCallback) => {
let retryInterval = retryConfig.base * Math.pow(retryConfig.factor, pollCount++);
let retryInterval = retryConfig.base * (retryConfig.factor ** pollCount++);
// The very first call is not a retry and hence should not be penalised with a timeout.
if (pollCount === 0) {
retryInterval = 0;
Expand All @@ -52,7 +52,7 @@ function retry(retryConfig, retryCall, terminateCondition, callback) {
},
() => {
if (!retryConfig.maxRetry || retryConfig.maxRetry > pollCount) {
return terminateCondition(lastResponse);
return shouldRetryCondition(lastResponse);
}
return false;
},
Expand Down
9 changes: 0 additions & 9 deletions lib/utils/string-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ module.exports = {
isNonBlankString,
isLambdaFunctionName,
filterNonAlphanumeric,
generateTimeStamp,
splitStringFilterAndMapTo
};

Expand Down Expand Up @@ -42,14 +41,6 @@ function filterNonAlphanumeric(str) {
return str.replace(/[^a-zA-Z0-9-]+/g, '');
}

/**
* Generate ISO string for timestamp, for example: 2019-03-19T22:26:36.002Z -> 20190319222636002
*/
function generateTimeStamp() {
const now = new Date();
return now.toISOString().match(/([0-9])/g).join('');
}

/**
* Applies the sequence of operations split, filter and map on a given string.
*/
Expand Down
Loading

0 comments on commit 10b2e63

Please sign in to comment.