diff --git a/packages/integration-tests/.eslintrc.json b/packages/integration-tests/.eslintrc.json new file mode 100644 index 00000000000..34f481ae722 --- /dev/null +++ b/packages/integration-tests/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-console": "off" + } +} \ No newline at end of file diff --git a/packages/integration-tests/README.md b/packages/integration-tests/README.md new file mode 100644 index 00000000000..6a8d1330a44 --- /dev/null +++ b/packages/integration-tests/README.md @@ -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 AWS Cloud Formation stack name (default: null) + -b, --bucket-name AWS S3 internal bucket name (default: null) + -w, --workflow Workflow name (default: null) + -i, --input-file 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) diff --git a/packages/integration-tests/bin/cli.js b/packages/integration-tests/bin/cli.js new file mode 100755 index 00000000000..54e38a1d026 --- /dev/null +++ b/packages/integration-tests/bin/cli.js @@ -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} 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 ', 'AWS Cloud Formation stack name', null) + .option('-b, --bucket-name ', 'AWS S3 internal bucket name', null) + .option('-w, --workflow ', 'Workflow name', null) + .option('-i, --input-file ', '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); diff --git a/packages/integration-tests/index.js b/packages/integration-tests/index.js new file mode 100644 index 00000000000..1cc89631642 --- /dev/null +++ b/packages/integration-tests/index.js @@ -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.} - 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.} 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.} - 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.} 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: , 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; diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json new file mode 100644 index 00000000000..7eb522476d3 --- /dev/null +++ b/packages/integration-tests/package.json @@ -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" + } +} diff --git a/packages/integration-tests/webpack.config.js b/packages/integration-tests/webpack.config.js new file mode 100644 index 00000000000..3d163ff19ed --- /dev/null +++ b/packages/integration-tests/webpack.config.js @@ -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' + }] + } +}; \ No newline at end of file