diff --git a/packages/@aws-cdk/aws-s3objectlambda/README.md b/packages/@aws-cdk/aws-s3objectlambda/README.md index 59127cd065606..53b0e42d56a57 100644 --- a/packages/@aws-cdk/aws-s3objectlambda/README.md +++ b/packages/@aws-cdk/aws-s3objectlambda/README.md @@ -21,19 +21,54 @@ -This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +This construct library allows you to define S3 object lambda access points. ```ts nofixture +import * as lambda from '@aws-cdk/aws-lambda'; +import * as s3 from '@aws-cdk/aws-s3'; import * as s3objectlambda from '@aws-cdk/aws-s3objectlambda'; + +const bucket = new s3.Bucket(this, 'MyBucket'); +const handler = new lambda.Function(this, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromAsset('lambda.zip'), +}); +new AccessPoint(this, 'MyObjectLambda', { + bucket, + handler, + accessPointName: 'my-access-point', + payload: { + prop: "value", + }, +}); ``` - +## Handling range and part number requests + +Lambdas are currently limited to only transforming `GetObject` requests. However, they can additionally support `GetObject-Range` and `GetObject-PartNumber` requests, which needs to be specified in the access point configuration: -There are no hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. -However, you can still use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, and use this service exactly as you would using CloudFormation directly. +```ts nofixture +new AccessPoint(this, 'MyObjectLambda', { + bucket, + handler, + accessPointName: 'my-access-point', + supportsGetObjectRange: true, + supportsGetObjectPartNumber: true, +}); +``` -For more information on the resources and properties available for this service, see the [CloudFormation documentation for AWS::S3ObjectLambda](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_S3ObjectLambda.html). +## Pass additional data to Lambda function -(Read the [CDK Contributing Guide](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) if you are interested in contributing to this construct library.) +You can specify an additional object that provides supplemental data to the Lambda function used to transform objects. The data is delivered as a JSON payload to the Lambda: - +```ts nofixture +new AccessPoint(this, 'MyObjectLambda', { + bucket, + handler, + accessPointName: 'my-access-point', + payload: { + prop: "value", + }, +}); +``` diff --git a/packages/@aws-cdk/aws-s3objectlambda/lib/access-point.ts b/packages/@aws-cdk/aws-s3objectlambda/lib/access-point.ts index 0561a7c1ef4a6..e2ab4df1e5abf 100644 --- a/packages/@aws-cdk/aws-s3objectlambda/lib/access-point.ts +++ b/packages/@aws-cdk/aws-s3objectlambda/lib/access-point.ts @@ -13,7 +13,7 @@ export interface IAccessPoint extends core.IResource { * The ARN of the access point. * @attribute */ - readonly accessPointArn: string + readonly accessPointArn: string; /** * The creation data of the access point. @@ -43,48 +43,46 @@ export interface IAccessPoint extends core.IResource { } /** - * Creates an S3 Object Lambda Access Point, which can intercept - * and transform `GetObject` requests. - * - * @param fn The Lambda function - * @param props Configuration for this Access Point + * The S3 object lambda access point configuration. */ export interface AccessPointProps { /** * The bucket to which this access point belongs. */ - readonly bucket: s3.IBucket + readonly bucket: s3.IBucket; /** * The Lambda function used to transform objects. */ - readonly fn: lambda.IFunction + readonly handler: lambda.IFunction; /** - * The name of the access point access point. + * The name of the S3 object lambda access point. + * + * @default a unique name will be generated */ - readonly accessPointName: string + readonly accessPointName?: string; /** * Whether CloudWatch metrics are enabled for the access point. * * @default false */ - readonly cloudWatchMetricsEnabled?: boolean + readonly cloudWatchMetricsEnabled?: boolean; /** * Whether the Lambda function can process `GetObject-Range` requests. * * @default false */ - readonly supportsGetObjectRange?: boolean + readonly supportsGetObjectRange?: boolean; /** * Whether the Lambda function can process `GetObject-PartNumber` requests. * * @default false */ - readonly supportsGetObjectPartNumber?: boolean + readonly supportsGetObjectPartNumber?: boolean; /** * Additional JSON that provides supplemental data passed to the @@ -92,33 +90,32 @@ export interface AccessPointProps { * * @default - No data. */ - readonly payload?: string + readonly payload?: Record; } abstract class AccessPointBase extends core.Resource implements IAccessPoint { - public abstract readonly accessPointArn: string - public abstract readonly accessPointCreationDate: string - - protected abstract readonly name: string; + public abstract readonly accessPointArn: string; + public abstract readonly accessPointCreationDate: string; + public abstract readonly accessPointName: string; /** Implement the {@link IAccessPoint.domainName} field. */ get domainName(): string { const urlSuffix = this.stack.urlSuffix; - return `${this.name}-${this.stack.account}.s3-object-lambda.${urlSuffix}`; + return `${this.accessPointName}-${this.stack.account}.s3-object-lambda.${urlSuffix}`; } /** Implement the {@link IAccessPoint.regionalDomainName} field. */ get regionalDomainName(): string { const urlSuffix = this.stack.urlSuffix; const region = this.stack.region; - return `${this.name}-${this.stack.account}.s3-object-lambda.${region}.${urlSuffix}`; + return `${this.accessPointName}-${this.stack.account}.s3-object-lambda.${region}.${urlSuffix}`; } /** Implement the {@link IAccessPoint.virtualHostedUrlForObject} method. */ public virtualHostedUrlForObject(key?: string, options?: s3.VirtualHostedStyleUrlOptions): string { const domainName = options?.regional ?? true ? this.regionalDomainName : this.domainName; const prefix = `https://${domainName}`; - if (typeof key !== 'string') { + if (!key) { return prefix; } if (key.startsWith('/')) { @@ -147,7 +144,26 @@ export interface AccessPointAttributes { } /** - * An S3 Object Lambda Access Point for intercepting and + * Checks the access point name against the rules in https://docs.aws.amazon.com/AmazonS3/latest/userguide/creating-access-points.html#access-points-names + * @param name The name of the access point + */ +function validateAccessPointName(name: string): void { + if (name.length < 3 || name.length > 50) { + throw new Error('Access point name must be between 3 and 50 characters long'); + } + if (name.endsWith('-s3alias')) { + throw new Error('Access point name cannot end with the suffix -s3alias'); + } + if (name[0] === '-' || name[name.length - 1] === '-') { + throw new Error('Access point name cannot begin or end with a dash'); + } + if (!/^[0-9a-z](.(?![\.A-Z_]))+[0-9a-z]$/.test(name)) { + throw new Error('Access point name must begin with a number or lowercase letter and not contain underscores, uppercase letters, or periods'); + } +} + +/** + * An S3 object lambda access point for intercepting and * transforming `GetObject` requests. */ export class AccessPoint extends AccessPointBase { @@ -163,13 +179,17 @@ export class AccessPoint extends AccessPointBase { class Import extends AccessPointBase { public readonly accessPointArn: string = attrs.accessPointArn; public readonly accessPointCreationDate: string = attrs.accessPointCreationDate; - protected name: string = name; + public readonly accessPointName: string = name; } return new Import(scope, id); } private readonly accessPoint: CfnAccessPoint - protected readonly name: string + + /** + * The ARN of the access point. + */ + public readonly accessPointName: string /** * The ARN of the access point. @@ -184,12 +204,19 @@ export class AccessPoint extends AccessPointBase { public readonly accessPointCreationDate: string constructor(scope: Construct, id: string, props: AccessPointProps) { - super(scope, id); + super(scope, id, { + physicalName: props.accessPointName ?? core.Lazy.string({ + produce: () => core.Names.uniqueId(this).toLowerCase(), + }), + }); - const supporting = new s3.CfnAccessPoint(this, 'AccessPoint', { + if (props.accessPointName) { + validateAccessPointName(props.accessPointName); + } + + const supporting = new s3.CfnAccessPoint(this, 'SupportingAccessPoint', { bucket: props.bucket.bucketName, }); - supporting.addPropertyOverride('Name', `${props.accessPointName}-access-point`); const allowedFeatures = []; if (props.supportsGetObjectPartNumber) { @@ -199,65 +226,34 @@ export class AccessPoint extends AccessPointBase { allowedFeatures.push('GetObject-Range'); } - this.name = props.accessPointName.toLowerCase(); this.accessPoint = new CfnAccessPoint(this, 'LambdaAccessPoint', { - name: this.name, + name: this.physicalName, objectLambdaConfiguration: { allowedFeatures, cloudWatchMetricsEnabled: props.cloudWatchMetricsEnabled, - supportingAccessPoint: supporting.getAtt('Arn').toString(), + supportingAccessPoint: supporting.attrArn, transformationConfigurations: [ { actions: ['GetObject'], contentTransformation: { AwsLambda: { - FunctionArn: props.fn.functionArn, - FunctionPayload: props.payload ?? '', + FunctionArn: props.handler.functionArn, + FunctionPayload: props.payload ? JSON.stringify(props.payload) : undefined, }, }, }, ], }, }); - this.accessPoint.addDependsOn(supporting); - + this.accessPointName = this.accessPoint.ref; this.accessPointArn = this.accessPoint.attrArn; this.accessPointCreationDate = this.accessPoint.attrCreationDate; - props.fn.addToRolePolicy( + props.handler.addToRolePolicy( new iam.PolicyStatement({ actions: ['s3-object-lambda:WriteGetObjectResponse'], resources: ['*'], }), ); } - - /** Implement the {@link IAccessPoint.domainName} field. */ - get domainName(): string { - const urlSuffix = this.stack.urlSuffix; - return `${this.accessPoint.name}-${this.stack.account}.s3-object-lambda.${urlSuffix}`; - } - - /** Implement the {@link IAccessPoint.regionalDomainName} field. */ - get regionalDomainName(): string { - const urlSuffix = this.stack.urlSuffix; - const region = this.stack.region; - return `${this.accessPoint.name}-${this.stack.account}.s3-object-lambda.${region}.${urlSuffix}`; - } - - /** Implement the {@link IAccessPoint.virtualHostedUrlForObject} method. */ - public virtualHostedUrlForObject(key?: string, options?: s3.VirtualHostedStyleUrlOptions): string { - const domainName = options?.regional ?? true ? this.regionalDomainName : this.domainName; - const prefix = `https://${domainName}`; - if (typeof key !== 'string') { - return prefix; - } - if (key.startsWith('/')) { - key = key.slice(1); - } - if (key.endsWith('/')) { - key = key.slice(0, -1); - } - return `${prefix}/${key}`; - } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3objectlambda/package.json b/packages/@aws-cdk/aws-s3objectlambda/package.json index e11ae39a163ec..f89ac1fb489d9 100644 --- a/packages/@aws-cdk/aws-s3objectlambda/package.json +++ b/packages/@aws-cdk/aws-s3objectlambda/package.json @@ -115,5 +115,10 @@ }, "publishConfig": { "tag": "latest" + }, + "awslint": { + "exclude": [ + "attribute-tag:@aws-cdk/aws-s3objectlambda.AccessPoint.accessPointName" + ] } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3objectlambda/test/integ.s3objectlambda.expected.json b/packages/@aws-cdk/aws-s3objectlambda/test/integ.s3objectlambda.expected.json index 38e4f220dd89d..999b4cf29bf1b 100644 --- a/packages/@aws-cdk/aws-s3objectlambda/test/integ.s3objectlambda.expected.json +++ b/packages/@aws-cdk/aws-s3objectlambda/test/integ.s3objectlambda.expected.json @@ -70,7 +70,7 @@ ] }, "Handler": "index.handler", - "Runtime": "nodejs10.x" + "Runtime": "nodejs14.x" }, "DependsOn": [ "MyFunction1ServiceRoleDefaultPolicy39556460", @@ -142,7 +142,7 @@ ] }, "Handler": "index.handler", - "Runtime": "nodejs10.x" + "Runtime": "nodejs14.x" }, "DependsOn": [ "MyFunction2ServiceRoleDefaultPolicyA79C693E", @@ -169,7 +169,7 @@ "CloudWatchMetricsEnabled": true, "SupportingAccessPoint": { "Fn::GetAtt": [ - "MyObjectLambda1AccessPointD5812646", + "MyObjectLambda1SupportingAccessPoint223B719B", "Arn" ] }, @@ -185,8 +185,7 @@ "MyFunction12A744C2E", "Arn" ] - }, - "FunctionPayload": "" + } } } } @@ -216,7 +215,7 @@ ], "SupportingAccessPoint": { "Fn::GetAtt": [ - "MyObjectLambda2AccessPoint76FB5ACF", + "MyObjectLambda2SupportingAccessPoint6C54778F", "Arn" ] }, @@ -233,7 +232,7 @@ "Arn" ] }, - "FunctionPayload": "{foo: 10}" + "FunctionPayload": "{\"foo\":10}" } } } diff --git a/packages/@aws-cdk/aws-s3objectlambda/test/integ.s3objectlambda.ts b/packages/@aws-cdk/aws-s3objectlambda/test/integ.s3objectlambda.ts index b6368f9f1342d..e840de6367479 100644 --- a/packages/@aws-cdk/aws-s3objectlambda/test/integ.s3objectlambda.ts +++ b/packages/@aws-cdk/aws-s3objectlambda/test/integ.s3objectlambda.ts @@ -8,21 +8,21 @@ class TestStack extends cdk.Stack { super(scope, id); const bucket = new s3.Bucket(this, 'MyBucket'); - const fn1 = new lambda.Function(this, 'MyFunction1', { - runtime: lambda.Runtime.NODEJS_10_X, + const handler1 = new lambda.Function(this, 'MyFunction1', { + runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', code: lambda.Code.fromInline('foo'), }); - const fn2 = new lambda.Function(this, 'MyFunction2', { - runtime: lambda.Runtime.NODEJS_10_X, + const handler2 = new lambda.Function(this, 'MyFunction2', { + runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', code: lambda.Code.fromInline('foo'), }); new AccessPoint(this, 'MyObjectLambda1', { bucket, - fn: fn1, + handler: handler1, accessPointName: 'obj-lambda-1', cloudWatchMetricsEnabled: true, supportsGetObjectPartNumber: true, @@ -30,10 +30,10 @@ class TestStack extends cdk.Stack { new AccessPoint(this, 'MyObjectLambda2', { bucket, - fn: fn2, + handler: handler2, accessPointName: 'obj-lambda-1', supportsGetObjectRange: true, - payload: '{foo: 10}', + payload: { foo: 10 }, }); } }