All files / src/mq-esm event-source-base.ts

99.03% Statements 205/207
100% Branches 8/8
66.66% Functions 2/3
99.03% Lines 205/207

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 2071x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 19x 16x 8x 8x 19x 19x 1x 1x 11x 11x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 11x 1x 1x     1x
/*
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
import { Aws, CustomResource, Duration, Names, withResolved } from 'aws-cdk-lib';
import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { CfnEventSourceMapping, IEventSource, IFunction, SourceAccessConfiguration, SourceAccessConfigurationType } from 'aws-cdk-lib/aws-lambda';
import { ISecret } from 'aws-cdk-lib/aws-secretsmanager';
import { Provider } from 'aws-cdk-lib/custom-resources';
import { EsmDeleterIsCompleteFunction } from './esm-deleter.is-complete-function';
import { EsmDeleterOnEventFunction } from './esm-deleter.on-event-function';
import { IBrokerDeployment } from '../broker-deployment';
 
export interface EventSourceProps {
  /**
   * source at the time of invoking your function. Your function receives an
   * The largest number of records that AWS Lambda will retrieve from your event
   * event with all the retrieved records.
   *
   * Valid Range:
   * * Minimum value of 1
   * * Maximum value of: 10000
   *
   * @default 100
   */
  readonly batchSize?: number;
 
  /**
   * The maximum amount of time to gather records before invoking the function.
   * Maximum of Duration.minutes(5).
   *
   * @see https://docs.aws.amazon.com/lambda/latest/dg/invocation-eventsourcemapping.html#invocation-eventsourcemapping-batching
   *
   * @default - Duration.millis(500) for Amazon MQ.
   */
  readonly maxBatchingWindow?: Duration;
 
  /**
   * If the stream event source mapping should be enabled.
   *
   * @default true
   */
  readonly enabled?: boolean;
 
 
  /**
   * A secret with credentials of the user to use when receiving messages.
   *
   * The credentials in the secret have fields required:
   *  * username
   *  * password
   */
  readonly credentials: ISecret;
 
  /**
   * The name of the queue that the function will receive messages from.
   */
  readonly queueName: string;
 
  /**
   * If the default permissions should be added to the Lambda function's execution role.
   *
   * @default true
   */
  readonly addPermissions?: boolean;
}
 
export interface EventSourceBaseProps extends EventSourceProps {
 
  /**
   * The Amazon MQ broker deployment to receive messages from.
   */
  readonly broker: IBrokerDeployment;
}
 
/**
 * Represents an AWS Lambda Event Source Mapping for RabbitMQ. This event source will add additional permissions to
 * the AWS Lambda function's IAM Role following https://docs.aws.amazon.com/lambda/latest/dg/with-mq.html#events-mq-permissions
 */
export abstract class EventSourceBase implements IEventSource {
  private sourceAccessConfigurations: SourceAccessConfiguration[] = [];
 
  /**
   * Instantiates an AWS Lambda Event Source Mapping for RabbitMQ. This event source will add additional permissions to
   * the AWS Lambda function's IAM Role following https://docs.aws.amazon.com/lambda/latest/dg/with-mq.html#events-mq-permissions
   *
   * @param props properties of the RabbitMQ event source
   */
  constructor(protected readonly props: EventSourceBaseProps, protected readonly mqType: string) {
    this.props.batchSize !== undefined && withResolved(this.props.batchSize, batchSize => {
      if (batchSize < 1 || batchSize > 10000) {
        throw new Error(`Maximum batch size must be between 1 and 10000 inclusive (given ${this.props.batchSize})`);
      }
    });
  }
 
  bind(target: IFunction): void {
 
    if (this.props.addPermissions === undefined || this.props.addPermissions) {
      this.props.credentials.grantRead(target);
 
      target.node.addMetadata('function-mq-permissions', 'Additional permissions following https://docs.aws.amazon.com/lambda/latest/dg/with-mq.html#events-mq-permissions');
 
      if (!target.isBoundToVpc) {
        // INFO: if the target is VPC bound then CDK attaches
        //       managed policy AWSLambdaVPCAccessExecutionRole
        //       which contains the necessary permissions.
        target.addToRolePolicy(new PolicyStatement({
          effect: Effect.ALLOW,
          actions: [
            'ec2:CreateNetworkInterface',
            'ec2:DeleteNetworkInterface',
            'ec2:DescribeNetworkInterfaces',
            'ec2:DescribeSubnets',
          ],
          resources: ['*'],
        }));
      }
 
      target.addToRolePolicy(new PolicyStatement({
        effect: Effect.ALLOW,
        actions: ['mq:DescribeBroker'],
        resources: [this.props.broker.arn],
      }));
 
      target.addToRolePolicy(new PolicyStatement({
        effect: Effect.ALLOW,
        actions: ['ec2:DescribeVpcs', 'ec2:DescribeSecurityGroups'],
        resources: ['*'],
      }));
    }
 
    // if (this.props.virtualHost) {
    //   this.sourceAccessConfigurations.push({
    //     type: SourceAccessConfigurationType.of('VIRTUAL_HOST'),
    //     uri: this.props.virtualHost,
    //   });
    // }
 
    this.sourceAccessConfigurations.push({
      type: SourceAccessConfigurationType.BASIC_AUTH,
      uri: this.props.credentials.secretArn,
    });
 
    const mapping = target.addEventSourceMapping(`RabbitMqEventSource:${Names.nodeUniqueId(this.props.broker.node)}`, {
      batchSize: this.props.batchSize,
      maxBatchingWindow: this.props.maxBatchingWindow,
      enabled: this.props.enabled,
      eventSourceArn: this.props.broker.arn,
      sourceAccessConfigurations: this.sourceAccessConfigurations,
    });
 
    const esMapping = mapping.node.defaultChild as CfnEventSourceMapping;
 
    // INFO: even though the property allows an array of items
    //       there can be no more than one queue
    esMapping.addPropertyOverride('Queues', [this.props.queueName]);
 
    // INFO: This is a (hopefully) temporary workaround due to the fact that ESM notifies CFN too early its deletion
    //       completion and as a result, target's IAM Role is being deleted before ESM is able to assume it to delete the ENIs.
    //       This in turn causes a deletion failure that requires manual ENIs' deletion to recover.
    if (target.role) {
      const provider = new Provider(mapping, `MqEsmDeleter:${Names.nodeUniqueId(mapping.node)}`, {
        onEventHandler: new EsmDeleterOnEventFunction(mapping, 'onevent', {
          initialPolicy: [
            new PolicyStatement({
              actions: [
                'lambda:DeleteEventSourceMapping',
              ],
              effect: Effect.ALLOW,
              resources: [`arn:${Aws.PARTITION}:lambda:${Aws.REGION}:${Aws.ACCOUNT_ID}:event-source-mapping:${mapping.eventSourceMappingId}`],
            }),
          ],
        }),
        isCompleteHandler: new EsmDeleterIsCompleteFunction(mapping, 'iscomplete', {
          initialPolicy: [
            new PolicyStatement({
              actions: [
                'ec2:DescribeNetworkInterfaces',
              ],
              effect: Effect.ALLOW,
              resources: ['*'],
            }),
          ],
        }),
        queryInterval: Duration.minutes(1),
      });
 
      const cr = new CustomResource(mapping, `MqEsmDeleterCR:${Names.nodeUniqueId(mapping.node)}`, {
        serviceToken: provider.serviceToken,
        properties: {
          MqType: this.mqType,
          EsmId: mapping.eventSourceMappingId,
          AccountId: Aws.ACCOUNT_ID,
        },
      });
 
      // INFO: the Amazon MQ service uses this role to provision/deprovision the ESM.
      //       we need it to remain until the ESM is deleted.
      cr.node.addDependency(target.role);
    }
  }
 
  protected addToSourceAccessConfigurations(config: SourceAccessConfiguration) {
    this.sourceAccessConfigurations.push(config);
  }
}