Skip to content

Commit

Permalink
Merge pull request #223 from cumulus-nasa/CUMULUS-200
Browse files Browse the repository at this point in the history
Cumulus 200
  • Loading branch information
laurenfrederick committed Feb 28, 2018
2 parents 9b754b6 + 7e9dff8 commit 8ad2fed
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 0 deletions.
5 changes: 5 additions & 0 deletions packages/integration-tests/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"no-console": "off"
}
}
47 changes: 47 additions & 0 deletions packages/integration-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# @cumulus/integration-tests

[![CircleCI](https://circleci.com/gh/cumulus-nasa/cumulus.svg?style=svg)](https://circleci.com/gh/cumulus-nasa/cumulus)

@cumulus/integration-tests provides a CLI and functions for testing Cumulus workflow executions in a Cumulus deployment.

## What is Cumulus?

Cumulus is a cloud-based data ingest, archive, distribution and management prototype for NASA's future Earth science data streams.

[Cumulus Documentation](https://cumulus-nasa.github.io/)

## Installation

```
npm install @cumulus/integration-tests
```

## Usage

```
Usage: cumulus-test TYPE COMMAND [options]
Options:
-V, --version output the version number
-s, --stack-name <stackName> AWS Cloud Formation stack name (default: null)
-b, --bucket-name <bucketName> AWS S3 internal bucket name (default: null)
-w, --workflow <workflow> Workflow name (default: null)
-i, --input-file <inputFile> Workflow input JSON file (default: null)
-h, --help output usage information
Commands:
workflow Execute a workflow and determine if the workflow completes successfully
```
i.e. to test the HelloWorld workflow:

`cumulus-test workflow --stack-name helloworld-cumulus --bucket-name cumulus-bucket-internal --workflow HelloWorldWorkflow --input-file ./helloWorldInput.json`



## Contributing

See [Cumulus README](https://github.com/cumulus-nasa/cumulus/blob/master/README.md#installing-and-deploying)
57 changes: 57 additions & 0 deletions packages/integration-tests/bin/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env node

'use strict';

const pckg = require('../package.json');
const testRunner = require('../index');
const program = require('commander');

program.version(pckg.version);

/**
* Verify that the given param is not null. Write out an error if null.
*
* @param {Object} paramConfig - param name and value {name: value:}
* @returns {boolean} true if param is not null
*/
function verifyRequiredParameter(paramConfig) {
if (paramConfig.value === null) {
console.log(`Error: ${paramConfig.name} is a required parameter.`);
return false;
}

return true;
}

/**
* Verify required parameters are present
*
* @param {list<Object>} requiredParams - params in the form {name: 'x' value: 'y'}
* @returns {boolean} - true if all params are not null
*/
function verifyWorkflowParameters(requiredParams) {
return requiredParams.map(verifyRequiredParameter).includes(false) === false;
}

program
.usage('TYPE COMMAND [options]')
.option('-s, --stack-name <stackName>', 'AWS Cloud Formation stack name', null)
.option('-b, --bucket-name <bucketName>', 'AWS S3 internal bucket name', null)
.option('-w, --workflow <workflow>', 'Workflow name', null)
.option('-i, --input-file <inputFile>', 'Workflow input JSON file', null);

program
.command('workflow')
.description('Execute a workflow and determine if the workflow completes successfully')
.action(() => {
if (verifyWorkflowParameters([{ name: 'stack-name', value: program.stackName },
{ name: 'bucket-name', value: program.bucketName },
{ name: 'workflow', value: program.workflow },
{ name: 'input-file', value: program.inputFile }])) {
testRunner.testWorkflow(program.stackName, program.bucketName,
program.workflow, program.inputFile);
}
});

program
.parse(process.argv);
161 changes: 161 additions & 0 deletions packages/integration-tests/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
'use strict';

const uuidv4 = require('uuid/v4');
const fs = require('fs-extra');
const { s3, sfn } = require('@cumulus/common/aws');

const executionStatusNumRetries = 20;
const waitPeriodMs = 5000;

/**
* Wait for the defined number of milliseconds
*
* @param {integer} waitPeriod - number of milliseconds to wait
* @returns {Promise.<undefined>} - promise resolves after a given time period
*/
function timeout(waitPeriod) {
return new Promise((resolve) => setTimeout(resolve, waitPeriod));
}

/**
* Get the template JSON from S3 for the workflow
*
* @param {string} stackName - Cloud formation stack name
* @param {string} bucketName - S3 internal bucket name
* @param {string} workflowName - workflow name
* @returns {Promise.<Object>} template as a JSON object
*/
function getWorkflowTemplate(stackName, bucketName, workflowName) {
const key = `${stackName}/workflows/${workflowName}.json`;
return s3().getObject({ Bucket: bucketName, Key: key }).promise()
.then((templateJson) => JSON.parse(templateJson.Body.toString()));
}

/**
* Get the workflow ARN for the given workflow from the
* template stored on S3
*
* @param {string} stackName - Cloud formation stack name
* @param {string} bucketName - S3 internal bucket name
* @param {string} workflowName - workflow name
* @returns {Promise.<string>} - workflow arn
*/
function getWorkflowArn(stackName, bucketName, workflowName) {
return getWorkflowTemplate(stackName, bucketName, workflowName)
.then((template) => template.cumulus_meta.state_machine);
}

/**
* Get the execution status (i.e. running, completed, etc)
* for the given execution
*
* @param {string} executionArn - ARN of the execution
* @returns {string} status
*/
function getExecutionStatus(executionArn) {
return sfn().describeExecution({ executionArn }).promise()
.then((status) => status.status);
}

/**
* Wait for a given execution to complete, then return the status
*
* @param {string} executionArn - ARN of the execution
* @returns {string} status
*/
async function waitForCompletedExecution(executionArn) {
let executionStatus = await getExecutionStatus(executionArn);
let statusCheckCount = 0;

// While execution is running, check status on a time interval
while (executionStatus === 'RUNNING' && statusCheckCount < executionStatusNumRetries) {
await timeout(waitPeriodMs);
executionStatus = await getExecutionStatus(executionArn);
statusCheckCount++;
}

if (executionStatus === 'RUNNING' && statusCheckCount >= executionStatusNumRetries) {
//eslint-disable-next-line max-len
console.log(`Execution status check timed out, exceeded ${executionStatusNumRetries} status checks.`);
}

return executionStatus;
}

/**
* Kick off a workflow execution
*
* @param {string} workflowArn - ARN for the workflow
* @param {string} inputFile - path to input JSON
* @returns {Promise.<Object>} execution details: {executionArn, startDate}
*/
async function startWorkflowExecution(workflowArn, inputFile) {
const rawInput = await fs.readFile(inputFile, 'utf8');

const parsedInput = JSON.parse(rawInput);

// Give this execution a unique name
parsedInput.cumulus_meta.execution_name = uuidv4();
parsedInput.cumulus_meta.workflow_start_time = Date.now();
parsedInput.cumulus_meta.state_machine = workflowArn;

const workflowParams = {
stateMachineArn: workflowArn,
input: JSON.stringify(parsedInput),
name: parsedInput.cumulus_meta.execution_name
};

return sfn().startExecution(workflowParams).promise();
}

/**
* Execute the given workflow.
* Wait for workflow to complete to get the status
* Return the execution arn and the workflow status.
*
* @param {string} stackName - Cloud formation stack name
* @param {string} bucketName - S3 internal bucket name
* @param {string} workflowName - workflow name
* @param {string} inputFile - path to input JSON file
* @returns {Object} - {executionArn: <arn>, status: <status>}
*/
async function executeWorkflow(stackName, bucketName, workflowName, inputFile) {
const workflowArn = await getWorkflowArn(stackName, bucketName, workflowName);
const execution = await startWorkflowExecution(workflowArn, inputFile);
const executionArn = execution.executionArn;

console.log(`Executing workflow: ${workflowName}. Execution ARN ${executionArn}`);

// Wait for the execution to complete to get the status
const status = await waitForCompletedExecution(executionArn);

return { status, executionArn };
}

/**
* Test the given workflow and report whether the workflow failed or succeeded
*
* @param {string} stackName - Cloud formation stack name
* @param {string} bucketName - S3 internal bucket name
* @param {string} workflowName - workflow name
* @param {string} inputFile - path to input JSON file
* @returns {*} undefined
*/
async function testWorkflow(stackName, bucketName, workflowName, inputFile) {
try {
const workflowStatus = await executeWorkflow(stackName, bucketName, workflowName, inputFile);

if (workflowStatus.status === 'SUCCEEDED') {
console.log(`Workflow ${workflowName} execution succeeded.`);
}
else {
console.log(`Workflow ${workflowName} execution failed with state: ${workflowStatus.status}`);
}
}
catch (err) {
console.log(`Error executing workflow ${workflowName}. Error: ${err}`);
}
}

exports.testWorkflow = testWorkflow;
exports.executeWorkflow = executeWorkflow;
41 changes: 41 additions & 0 deletions packages/integration-tests/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "@cumulus/integration-tests",
"version": "1.0.0",
"description": "Integration tests",
"bin": {
"cumulus-test": "./bin/cli.js"
},
"repository": {
"type": "git",
"url": "https://github.com/cumulus-nasa/cumulus"
},
"scripts": {
"build": "webpack --progress",
"watch": "webpack --progress -w"
},
"publishConfig": {
"access": "public"
},
"babel": {
"presets": [
"es2017"
],
"plugins": [
"transform-async-to-generator"
]
},
"author": "Cumulus Authors",
"license": "Apache-2.0",
"dependencies": {
"@cumulus/common": "^1.0.0-beta.19",
"babel-core": "^6.25.0",
"babel-loader": "^6.2.4",
"babel-plugin-transform-async-to-generator": "^6.24.1",
"babel-polyfill": "^6.23.0",
"babel-preset-es2017": "^6.24.1",
"commander": "^2.9.0",
"fs-extra": "^5.0.0",
"uuid": "^3.2.1",
"webpack": "^1.12.13"
}
}
22 changes: 22 additions & 0 deletions packages/integration-tests/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module.exports = {
entry: ['babel-polyfill', './index.js'],
output: {
libraryTarget: 'commonjs2',
filename: 'dist/index.js'
},
externals: [
'electron'
],
target: 'node',
devtool: 'sourcemap',
module: {
loaders: [{
test: /\.js?$/,
exclude: /node_modules(?!\/@cumulus\/)/,
loader: 'babel'
}, {
test: /\.json$/,
loader: 'json'
}]
}
};

0 comments on commit 8ad2fed

Please sign in to comment.