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

feat(pipes-targets): add API destination #30756

Merged
merged 21 commits into from
Sep 27, 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
34 changes: 34 additions & 0 deletions packages/@aws-cdk/aws-pipes-targets-alpha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ The following targets are supported:
1. `targets.SqsTarget`: [Send event source to a Queue](#amazon-sqs)
2. `targets.SfnStateMachine`: [Invoke a State Machine from an event source](#aws-step-functions-state-machine)
3. `targets.LambdaFunction`: [Send event source to a Lambda Function](#aws-lambda-function)
4. `targets.ApiDestinationTarget`: [Send event source to an EventBridge API Destination](#amazon-eventbridge-api-destination)

### Amazon SQS

Expand Down Expand Up @@ -171,3 +172,36 @@ const pipe = new pipes.Pipe(this, 'Pipe', {
target: pipeTarget
});
```

### Amazon EventBridge API Destination
msambol marked this conversation as resolved.
Show resolved Hide resolved

An EventBridge API destination can be used as a target for a pipe.
The API destination will receive the (enriched/filtered) source payload.

```ts
declare const sourceQueue: sqs.Queue;
declare const dest: events.ApiDestination;

const apiTarget = new targets.ApiDestinationTarget(dest);

const pipe = new pipes.Pipe(this, 'Pipe', {
source: new SqsSource(sourceQueue),
target: apiTarget,
});
```

The input to the target API destination can be transformed:

```ts
declare const sourceQueue: sqs.Queue;
declare const dest: events.ApiDestination;

const apiTarget = new targets.ApiDestinationTarget(dest, {
inputTransformation: pipes.InputTransformation.fromObject({ body: "👀" }),
});

const pipe = new pipes.Pipe(this, 'Pipe', {
source: new SqsSource(sourceQueue),
target: apiTarget,
});
```
80 changes: 80 additions & 0 deletions packages/@aws-cdk/aws-pipes-targets-alpha/lib/api-destination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { IInputTransformation, IPipe, ITarget, TargetConfig } from '@aws-cdk/aws-pipes-alpha';
import { IApiDestination } from 'aws-cdk-lib/aws-events';
import { IRole, PolicyStatement } from 'aws-cdk-lib/aws-iam';

/**
* EventBridge API destination target properties.
*/
export interface ApiDestinationTargetParameters {
/**
* The input transformation to apply to the message before sending it to the target.
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-pipes-pipe-pipetargetparameters.html#cfn-pipes-pipe-pipetargetparameters-inputtemplate
* @default - none
*/
readonly inputTransformation?: IInputTransformation;

/**
* The headers to send as part of the request invoking the EventBridge API destination.
*
* The headers are merged with the headers from the API destination.
* If there are conflicts, the headers from the API destination take precedence.
Copy link
Contributor Author

@msambol msambol Jul 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nmussy I added this note about precedence. From the logs we can see that x-api-key is abc123, not apiKeyFromHeaderParams.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether this is enough to indicate to users which value will take precedence. Is there somewhere we can print a warning when there are two identical headers provided, which explicitly lets the user know that the value is being set twice, and which of those values it's using?

Copy link
Contributor Author

@msambol msambol Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@redwheeler3 jfyi. I think it's worth mentioning in Docs and/or Console about precedence regarding conflicting headers.

*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-pipes-pipe-pipetargethttpparameters.html#cfn-pipes-pipe-pipetargethttpparameters-headerparameters
* @default - none
*/
readonly headerParameters?: { [key: string]: string };

/**
* The path parameter values used to populate the EventBridge API destination path wildcards ("*").
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-pipes-pipe-pipetargethttpparameters.html#cfn-pipes-pipe-pipetargethttpparameters-pathparametervalues
* @default - none
*/
readonly pathParameterValues?: string[];

/**
* The query string keys/values that need to be sent as part of request invoking the EventBridge API destination.
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-pipes-pipe-pipetargethttpparameters.html#cfn-pipes-pipe-pipetargethttpparameters-querystringparameters
* @default - none
*/
readonly queryStringParameters?: Record<string, string>;
}

/**
* A EventBridge Pipes target that sends messages to an EventBridge API destination.
*/
export class ApiDestinationTarget implements ITarget {
private destination: IApiDestination;
private apiParameters?: ApiDestinationTargetParameters;
public readonly targetArn: string;

constructor(destination: IApiDestination, parameters?: ApiDestinationTargetParameters) {
this.destination = destination;
this.apiParameters = parameters;
this.targetArn = destination.apiDestinationArn;
}

grantPush(grantee: IRole): void {
grantee.addToPrincipalPolicy(new PolicyStatement({
resources: [this.destination.apiDestinationArn],
actions: ['events:InvokeApiDestination'],
}));
}

bind(pipe: IPipe): TargetConfig {
if (!this.apiParameters) {
return {
targetParameters: {},
};
}

return {
targetParameters: {
inputTemplate: this.apiParameters.inputTransformation?.bind(pipe).inputTemplate,
httpParameters: this.apiParameters,
},
};
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-pipes-targets-alpha/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './api-destination';
export * from './lambda';
export * from './sqs';
export * from './stepfunctions';
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Fixture with packages imported, but nothing else
import * as cdk from 'aws-cdk-lib';
import * as events from 'aws-cdk-lib/aws-events';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as sfn from 'aws-cdk-lib/aws-stepfunctions';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
import * as pipes from '@aws-cdk/aws-pipes-alpha';
import { SqsSource } from '@aws-cdk/aws-pipes-sources-alpha';
import * as targets from '@aws-cdk/aws-pipes-targets-alpha';

class SomeSource implements pipes.ISource {
Expand Down
172 changes: 172 additions & 0 deletions packages/@aws-cdk/aws-pipes-targets-alpha/test/api-destination.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { InputTransformation, Pipe } from '@aws-cdk/aws-pipes-alpha';
import { App, Stack, SecretValue } from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import * as events from 'aws-cdk-lib/aws-events';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
import { TestSource } from './test-classes';
import { ApiDestinationTarget } from '../lib';

describe('API destination', () => {
let app: App;
let stack: Stack;
let secret: Secret;
let connection: events.Connection;

beforeEach(() => {
app = new App();
stack = new Stack(app, 'TestStack');
secret = new Secret(stack, 'MySecret', {
secretStringValue: SecretValue.unsafePlainText('abc123'),
});
connection = new events.Connection(stack, 'MyConnection', {
authorization: events.Authorization.apiKey('x-api-key', secret.secretValue),
description: 'Connection with API Key x-api-key',
connectionName: 'MyConnection',
});
});

it('should have only target arn', () => {
// ARRANGE
const destination = new events.ApiDestination(stack, 'MyApiDestination', {
connection,
endpoint: 'https://httpbin.org/headers',
httpMethod: events.HttpMethod.GET,
apiDestinationName: 'MyDestination',
rateLimitPerSecond: 1,
description: 'Calling example.com with API key x-api-key',
});
const target = new ApiDestinationTarget(destination);

new Pipe(stack, 'MyPipe', {
source: new TestSource(),
target,
});

// ACT
const template = Template.fromStack(stack);

// ASSERT
template.hasResourceProperties('AWS::Pipes::Pipe', {
Target: {
'Fn::GetAtt': [
'MyApiDestination07E6A8F9',
'Arn',
],
},
TargetParameters: {},
});
});

it('should have target parameters', () => {
// ARRANGE
const destination = new events.ApiDestination(stack, 'MyApiDestination', {
connection,
endpoint: 'https://httpbin.org/headers',
httpMethod: events.HttpMethod.GET,
apiDestinationName: 'MyDestination',
rateLimitPerSecond: 1,
description: 'Calling example.com with API key x-api-key',
});
const target = new ApiDestinationTarget(destination, {
headerParameters: { headerName: 'headerValue' },
pathParameterValues: ['pathValue'],
queryStringParameters: { queryName: 'queryValue' },
});

new Pipe(stack, 'MyPipe', {
source: new TestSource(),
target,
});

// ACT
const template = Template.fromStack(stack);

// ASSERT
template.hasResourceProperties('AWS::Pipes::Pipe', {
TargetParameters: {
HttpParameters: {
HeaderParameters: {
headerName: 'headerValue',
},
PathParameterValues: ['pathValue'],
QueryStringParameters: {
queryName: 'queryValue',
},
},
},
});
});

it('should have input transformation', () => {
// ARRANGE
const destination = new events.ApiDestination(stack, 'MyApiDestination', {
connection,
endpoint: 'https://httpbin.org/headers',
httpMethod: events.HttpMethod.GET,
apiDestinationName: 'MyDestination',
rateLimitPerSecond: 1,
description: 'Calling example.com with API key x-api-key',
});

const inputTransformation = InputTransformation.fromObject({
key: 'value',
});

const target = new ApiDestinationTarget(destination, {
inputTransformation,
});

new Pipe(stack, 'MyPipe', {
source: new TestSource(),
target,
});

// ACT
const template = Template.fromStack(stack);

// ASSERT
template.hasResourceProperties('AWS::Pipes::Pipe', {
TargetParameters: {
InputTemplate: '{"key":"value"}',
},
});
});

it('should grant pipe role push access', () => {
// ARRANGE
const destination = new events.ApiDestination(stack, 'MyApiDestination', {
connection,
endpoint: 'https://httpbin.org/headers',
httpMethod: events.HttpMethod.GET,
apiDestinationName: 'MyDestination',
rateLimitPerSecond: 1,
description: 'Calling example.com with API key x-api-key',
});
const target = new ApiDestinationTarget(destination);

new Pipe(stack, 'MyPipe', {
source: new TestSource(),
target,
});

// ACT
const template = Template.fromStack(stack);

// ASSERT
template.hasResource('AWS::IAM::Policy', {
Properties: {
Roles: [{
Ref: 'MyPipeRoleCBC8E9AB',
}],
PolicyDocument: {
Statement: [{
Action: 'events:InvokeApiDestination',
Resource: {
'Fn::GetAtt': ['MyApiDestination07E6A8F9', 'Arn'],
},
}],
},
},
});
});
});

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

Loading