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

fix: event manager cyclic dep #47

Merged
merged 4 commits into from
May 30, 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
18 changes: 16 additions & 2 deletions API.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/control-plane/billing/billing-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,6 @@ export class BillingProvider extends Construct {
}

const { handler, trigger } = this.getFunctionProps(fn, defaultEvent);
eventManager.addTargetToEvent(trigger, new event_targets.LambdaFunction(handler));
eventManager.addTargetToEvent(this, trigger, new event_targets.LambdaFunction(handler));
}
}
2 changes: 2 additions & 0 deletions src/control-plane/control-plane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,13 @@ export class ControlPlane extends Construct {
this.controlPlaneAPIGatewayUrl = controlPlaneAPI.apiUrl;

this.eventManager.addTargetToEvent(
this,
DetailType.PROVISION_SUCCESS,
controlPlaneAPI.tenantUpdateServiceTarget
);

this.eventManager.addTargetToEvent(
this,
DetailType.DEPROVISION_SUCCESS,
controlPlaneAPI.tenantUpdateServiceTarget
);
Expand Down
6 changes: 5 additions & 1 deletion src/core-app-plane/core-app-plane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,11 @@ export class CoreApplicationPlane extends Construct {
bashJobRunner: job,
});

this.eventManager.addTargetToEvent(jobRunnerProps.incomingEvent, jobOrchestrator.eventTarget);
this.eventManager.addTargetToEvent(
this,
jobRunnerProps.incomingEvent,
jobOrchestrator.eventTarget
);
});
}
}
25 changes: 15 additions & 10 deletions src/utils/event-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export interface IEventManager {
* @param eventType The detail type of the event to add a target to.
* @param target The target that will be added to the event.
*/
addTargetToEvent(eventType: DetailType, target: IRuleTarget): void;
addTargetToEvent(scope: Construct, eventType: DetailType, target: IRuleTarget): void;

/**
* Provides grantee the permissions to place events
Expand Down Expand Up @@ -218,8 +218,6 @@ export class EventManager extends Construct implements IEventManager {
*/
public readonly busArn: string;

private readonly connectedRules: Map<DetailType, Rule> = new Map<DetailType, Rule>();

constructor(scope: Construct, id: string, props?: EventManagerProps) {
super(scope, id);
addTemplateTag(this, 'EventManager');
Expand Down Expand Up @@ -275,25 +273,32 @@ export class EventManager extends Construct implements IEventManager {
/**
* Adds an IRuleTarget to an event.
*
* @param scope The scope in which to find (or create) the Rule.
* @param eventType The detail type of the event to add a target to.
* @param target The target that will be added to the event.
*/
public addTargetToEvent(eventType: DetailType, target: IRuleTarget): void {
this.getOrCreateRule(eventType).addTarget(target);
public addTargetToEvent(scope: Construct, eventType: DetailType, target: IRuleTarget): void {
this.getOrCreateRule(scope, eventType).addTarget(target);
}

private getOrCreateRule(eventType: DetailType): Rule {
let rule = this.connectedRules.get(eventType);

/**
* Returns a Rule for the given eventType in the context of a scope.
* A new one is created if the rule is not found in the scope.
*
* @param scope The scope in which to find (or create) the Rule.
* @param eventType The detail type of the event to add a target to.
* @returns A Rule for the given eventType in the provided scope.
*/
private getOrCreateRule(scope: Construct, eventType: DetailType): Rule {
let rule = scope.node.tryFindChild(`${eventType}Rule`) as Rule;
if (!rule) {
rule = new Rule(this, `${eventType}Rule`, {
rule = new Rule(scope, `${eventType}Rule`, {
eventBus: this.eventBus,
eventPattern: {
source: [this.supportedEvents[eventType]],
detailType: [eventType],
},
});
this.connectedRules.set(eventType, rule);
}

return rule;
Expand Down
1 change: 0 additions & 1 deletion test/core-app-plane.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ describe('CoreApplicationPlane', () => {
const template = Template.fromStack(coreApplicationPlaneStack);

template.hasResourceProperties('AWS::CodeBuild::Project', {
// check that codebuild has the MY_TEST_ENV_VAR environment variable defined
Environment: Match.objectLike({
EnvironmentVariables: Match.anyValue(),
}),
Expand Down
195 changes: 195 additions & 0 deletions test/event-manager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import * as cdk from 'aws-cdk-lib';
import { Capture, Template } from 'aws-cdk-lib/assertions';
import { Effect, PolicyDocument, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { AwsSolutionsChecks } from 'cdk-nag';
import { CognitoAuth, ControlPlane } from '../src/control-plane';
import { CoreApplicationPlane } from '../src/core-app-plane';
import { DetailType, EventManager } from '../src/utils';

function testForDuplicateRulesInCoreAppPlaneStack(stack: cdk.Stack) {
const template = Template.fromStack(stack);
const eventRuleCapture = new Capture();

template.allResourcesProperties('AWS::Events::Rule', eventRuleCapture);
const eventRules: { [key: string]: any } = [];
do {
eventRules.push(eventRuleCapture.asObject());
} while (eventRuleCapture.next());

const onboardingRequestEventRules = eventRules.filter(
(eventRule: { EventPattern: { 'detail-type': string[] } }) => {
return eventRule.EventPattern['detail-type'][0] === DetailType.ONBOARDING_REQUEST;
}
);

it('should not have duplicate rules for the same event detail type', () => {
expect(onboardingRequestEventRules).toHaveLength(1);
});

it('should reuse the same rules for targets triggered by the same event detail type', () => {
expect(onboardingRequestEventRules[0].Targets).toHaveLength(2);
});
}

function testForRulesInOtherStack(stack: cdk.Stack) {
const template = Template.fromStack(stack);
const eventRuleCapture = new Capture();

const rules = template.findResources('AWS::Events::Rule', eventRuleCapture);
console.log(rules);
if (Object.keys(rules).length === 0) {
it('should not have rules for triggering runners in stacks that do not define them', () => {
expect(true).toBe(true);
});
// return as there are no rules in this stack
return;
}

// template.allResourcesProperties('AWS::Events::Rule', eventRuleCapture);
const eventRules: { [key: string]: any } = [];
do {
eventRules.push(eventRuleCapture.asObject());
} while (eventRuleCapture.next());

console.log(eventRules);
const onboardingRequestEventRules = eventRules.filter(
(eventRule: { EventPattern?: { 'detail-type': string[] } }) => {
return (
eventRule.EventPattern &&
eventRule.EventPattern['detail-type'][0] === DetailType.ONBOARDING_REQUEST
);
}
);

it('should not have rules for triggering runners in stacks that do not define them', () => {
expect(onboardingRequestEventRules).toHaveLength(0);
});
}

const samplePolicyDocument = new PolicyDocument({
statements: [
new PolicyStatement({
actions: ['cloudformation:CreateStack'],
resources: ['arn:aws:cloudformation:*:*:stack/MyStack/ExampleStack'],
effect: Effect.ALLOW,
}),
],
});

const jobsUsingTheSameIncomingEvent = [
{
name: 'provisioning-1',
outgoingEvent: DetailType.PROVISION_SUCCESS,
incomingEvent: DetailType.ONBOARDING_REQUEST,
permissions: samplePolicyDocument,
script: '',
},
{
name: 'provisioning-2',
outgoingEvent: DetailType.PROVISION_SUCCESS,
incomingEvent: DetailType.ONBOARDING_REQUEST,
permissions: samplePolicyDocument,
script: '',
},
];

describe('EventManager', () => {
describe('using default event-manager created by control plane', () => {
const app = new cdk.App();
const controlPlaneStack = new cdk.Stack(app, 'ControlPlaneStack');

const cognitoAuth = new CognitoAuth(controlPlaneStack, 'CognitoAuth', {
systemAdminEmail: 'test@example.com',
});

const controlPlane = new ControlPlane(controlPlaneStack, 'ControlPlane', {
auth: cognitoAuth,
});

const coreAppPlaneStack = new cdk.Stack(app, 'CoreApplicationPlaneStack');
new CoreApplicationPlane(coreAppPlaneStack, 'CoreApplicationPlane', {
eventManager: controlPlane.eventManager,
jobRunnerPropsList: jobsUsingTheSameIncomingEvent,
});

cdk.Aspects.of(controlPlaneStack).add(new AwsSolutionsChecks({ verbose: true }));
cdk.Aspects.of(coreAppPlaneStack).add(new AwsSolutionsChecks({ verbose: true }));

it('should synth without errors', () => {
const assembly = app.synth();
expect(assembly).toBeTruthy();
});

testForDuplicateRulesInCoreAppPlaneStack(coreAppPlaneStack);
testForRulesInOtherStack(controlPlaneStack);
});

describe('using event-manager created outside of control plane', () => {
const app = new cdk.App();
const controlPlaneStack = new cdk.Stack(app, 'ControlPlaneStack');

const eventManager = new EventManager(controlPlaneStack, 'EventManager');

const cognitoAuth = new CognitoAuth(controlPlaneStack, 'CognitoAuth', {
systemAdminEmail: 'test@example.com',
});

new ControlPlane(controlPlaneStack, 'ControlPlane', {
auth: cognitoAuth,
eventManager: eventManager,
});

const coreAppPlaneStack = new cdk.Stack(app, 'CoreApplicationPlaneStack');
new CoreApplicationPlane(coreAppPlaneStack, 'CoreApplicationPlane', {
eventManager: eventManager,
jobRunnerPropsList: jobsUsingTheSameIncomingEvent,
});

cdk.Aspects.of(controlPlaneStack).add(new AwsSolutionsChecks({ verbose: true }));
cdk.Aspects.of(coreAppPlaneStack).add(new AwsSolutionsChecks({ verbose: true }));

it('should synth without errors', () => {
const assembly = app.synth();
expect(assembly).toBeTruthy();
});

testForDuplicateRulesInCoreAppPlaneStack(coreAppPlaneStack);
testForRulesInOtherStack(controlPlaneStack);
});

describe('using an event-manager created in a separate stack', () => {
const app = new cdk.App();
const eventManagerStack = new cdk.Stack(app, 'EventManagerStack');
const eventManager = new EventManager(eventManagerStack, 'EventManager');

const controlPlaneStack = new cdk.Stack(app, 'ControlPlaneStack');
const cognitoAuth = new CognitoAuth(controlPlaneStack, 'CognitoAuth', {
systemAdminEmail: 'test@example.com',
});

new ControlPlane(controlPlaneStack, 'ControlPlane', {
auth: cognitoAuth,
eventManager: eventManager,
});

const coreAppPlaneStack = new cdk.Stack(app, 'CoreApplicationPlaneStack');
new CoreApplicationPlane(coreAppPlaneStack, 'CoreApplicationPlane', {
eventManager: eventManager,
jobRunnerPropsList: jobsUsingTheSameIncomingEvent,
});
cdk.Aspects.of(controlPlaneStack).add(new AwsSolutionsChecks({ verbose: true }));
cdk.Aspects.of(coreAppPlaneStack).add(new AwsSolutionsChecks({ verbose: true }));

it('should synth without errors', () => {
const assembly = app.synth();
expect(assembly).toBeTruthy();
});

testForDuplicateRulesInCoreAppPlaneStack(coreAppPlaneStack);
testForRulesInOtherStack(controlPlaneStack);
testForRulesInOtherStack(eventManagerStack);
});
});
Loading