Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jk/cumulus 3701 fix null rules (#3641) #3645

Merged
merged 1 commit into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).

## Unreleased

### Fixed

- **CUMULUS-3701**
- Updated `@cumulus/api` to no longer improperly pass PATCH/PUT null values to Eventbridge rules

## [v18.2.0] 2023-02-02

### Migration Notes
Expand Down
10 changes: 8 additions & 2 deletions packages/api/endpoints/rules.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//@ts-check

'use strict';

const router = require('express-promise-router')();
Expand Down Expand Up @@ -33,6 +35,10 @@ const schemas = require('../lib/schemas.js');

const log = new Logger({ sender: '@cumulus/api/rules' });

/**
* @typedef {import('@cumulus/types/api/rules').RuleRecord} RuleRecord
*/

/**
* List all rules.
*
Expand Down Expand Up @@ -133,7 +139,7 @@ async function post(req, res) {
*
* @param {object} params - params object
* @param {object} params.res - express response object
* @param {object} params.oldApiRule - API 'rule' to update
* @param {RuleRecord} params.oldApiRule - API 'rule' to update
* @param {object} params.apiRule - updated API rule
* @param {object} params.rulePgModel - @cumulus/db compatible rule module instance
* @param {object} params.knex - Knex object
Expand All @@ -158,7 +164,7 @@ async function patchRule(params) {
return invokeRerun(oldApiRule).then(() => res.send(oldApiRule));
}

const apiRuleWithTrigger = await updateRuleTrigger(oldApiRule, apiRule, knex);
const apiRuleWithTrigger = await updateRuleTrigger(oldApiRule, apiRule);
const apiPgRule = await translateApiRuleToPostgresRuleRaw(apiRuleWithTrigger, knex);
log.debug(`rules.patchRule apiRuleWithTrigger: ${JSON.stringify(apiRuleWithTrigger)}`);

Expand Down
28 changes: 15 additions & 13 deletions packages/api/lib/rulesHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -571,8 +571,9 @@ async function addRule(item, payload) {
/**
* Checks if record is valid
*
* @param {RuleRecord} rule - Rule to check validation
* @returns {void} - Returns if record is valid, throws error otherwise
* @param {any} rule - Object to validate as a Rule Record validation
* @param {any} rule - Object to validate as a Rule Record validation
* @returns {RuleRecord} - Returns if record is valid, throws error otherwise
*/
function validateRecord(rule) {
const error = new Error('The record has validation errors. ');
Expand All @@ -590,12 +591,13 @@ function validateRecord(rule) {
throw error;
}

recordIsValid(omitDeepBy(rule, isNil), ruleSchema, false);
recordIsValid(rule, ruleSchema, false);

if (rule.rule.type !== 'onetime' && !rule.rule.value) {
error.message += `Rule value is undefined for ${rule.rule.type} rule`;
throw error;
}
return rule;
}

/**
Expand All @@ -616,15 +618,14 @@ async function invokeRerun(rule) {
}

/**
* Updates rule trigger for rule
* Updates rule trigger for rule object
*
* @param {RuleRecord} original - Rule to update trigger for
* @param {RuleRecord} updated - Updated rule for rule trigger
* @param {Object} updated - Updated rule for rule trigger
* @returns {Promise<RuleRecord>} - Returns new rule object
*/
async function updateRuleTrigger(original, updated) {
let resultRule = cloneDeep(updated);
validateRecord(resultRule);
let resultRule = validateRecord(omitDeepBy(updated, isNil));

const stateChanged = updated.state && updated.state !== original.state;
const valueUpdated = updated.rule && updated.rule.value !== original.rule.value;
Expand Down Expand Up @@ -672,18 +673,19 @@ async function updateRuleTrigger(original, updated) {
/**
* Creates rule trigger for rule
*
* @param {RuleRecord} ruleItem - Rule to create trigger for
* @param {Object} ruleItem - Rule to create trigger for
* @param {Object} testParams - Function to determine to use actual invoke or testInvoke
* @returns {Promise<RuleRecord>} - Returns new rule object
*/
async function createRuleTrigger(ruleItem, testParams = {}) {
let newRuleItem = cloneDeep(ruleItem);
const candidateRuleItem = omitDeepBy(ruleItem, isNil);

// the default value for enabled is true
if (newRuleItem.state === undefined) {
newRuleItem.state = 'ENABLED';
if (candidateRuleItem.state === undefined) {
candidateRuleItem.state = 'ENABLED';
}

const enabled = newRuleItem.state === 'ENABLED';
const enabled = candidateRuleItem.state === 'ENABLED';
const invokeMethod = testParams.invokeMethod || invoke;
// make sure the name only has word characters
const re = /\W/;
Expand All @@ -692,7 +694,7 @@ async function createRuleTrigger(ruleItem, testParams = {}) {
}

// Validate rule before kicking off workflows or adding event source mappings
validateRecord(newRuleItem);
let newRuleItem = validateRecord(candidateRuleItem);

const payload = await buildPayload(newRuleItem);
switch (newRuleItem.rule.type) {
Expand Down
84 changes: 83 additions & 1 deletion packages/api/tests/lib/rules/test-rulesHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
const test = require('ava');
const sinon = require('sinon');
const omit = require('lodash/omit');

const isObject = require('lodash/isObject');
const cloneDeep = require('lodash/cloneDeep');
const proxyquire = require('proxyquire');
const fs = require('fs-extra');

Expand Down Expand Up @@ -1763,6 +1764,46 @@ test.serial('Creating a rule trigger for a scheduled rule succeeds', async (t) =
});
});

test.serial('Creating a rule trigger for a scheduled rule with null values in the rule results in putRule being called with an object missing null values', async (t) => {
const rule = fakeRuleFactoryV2({
workflow,
rule: {
type: 'scheduled',
value: 'rate(1 min)',
},
state: 'ENABLED',
meta: {
visibilityTimeout: 100,
retries: 4,
someOtherValue: null,
},
queueUrl: null,
});

const cloudwatchStub = sinon.stub(awsServices, 'cloudwatchevents')
.returns({
putRule: () => ({
promise: () => Promise.resolve(),
}),
putTargets: (params) => {
const valueFunc = (obj) =>
((obj && isObject(obj))
? Object.values(obj).map(valueFunc).flat()
: [obj]);
const paramValues = params.Targets.map((target) => JSON.parse(target.Input)).map((x) => valueFunc(x)).flat();
t.false(paramValues.includes(null));
return {
promise: () => Promise.resolve(),
};
},
});
await createRuleTrigger(rule);
t.true(cloudwatchStub.called);
t.teardown(() => {
cloudwatchStub.restore();
});
});

test('buildPayload builds a lambda payload from the rule', async (t) => {
const collectionPgModel = new CollectionPgModel();
const providerPgModel = new ProviderPgModel();
Expand Down Expand Up @@ -1830,6 +1871,47 @@ test('buildPayload throws error if workflow file does not exist', async (t) => {
);
});

test.serial('Updating a rule trigger for a scheduled rule with null values in the rule results in putRule being called with an object missing null values', async (t) => {
const rule = fakeRuleFactoryV2({
workflow,
rule: {
type: 'scheduled',
value: 'rate(1 min)',
},
state: 'ENABLED',
meta: {
visibilityTimeout: 100,
retries: 4,
},
});

const updatedRule = cloneDeep(rule);
updatedRule.queueUrl = null;
updatedRule.meta.someOtherValue = null;
const cloudwatchStub = sinon.stub(awsServices, 'cloudwatchevents')
.returns({
putRule: () => ({
promise: () => Promise.resolve(),
}),
putTargets: (params) => {
const valueFunc = (obj) =>
((obj && isObject(obj))
? Object.values(obj).map(valueFunc).flat()
: [obj]);
const paramValues = params.Targets.map((target) => JSON.parse(target.Input)).map((x) => valueFunc(x)).flat();
t.false(paramValues.includes(null));
return {
promise: () => Promise.resolve(),
};
},
});
await updateRuleTrigger(rule, updatedRule);
t.true(cloudwatchStub.called);
t.teardown(() => {
cloudwatchStub.restore();
});
});

test.serial('Updating a rule trigger with an "onetime" rule type returns updated rule', async (t) => {
const fakeRule = fakeRuleFactoryV2({
workflow,
Expand Down