Skip to content

Commit

Permalink
address comments
Browse files Browse the repository at this point in the history
  • Loading branch information
duarten committed Feb 18, 2022
1 parent 6823642 commit 2a517ce
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 100 deletions.
71 changes: 63 additions & 8 deletions packages/@aws-cdk/aws-s3objectlambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,74 @@

<!--END STABILITY BANNER-->

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
```ts
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",
},
});
```

<!--BEGIN CFNONLY DISCLAIMER-->
## 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:

```ts
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',
supportsGetObjectRange: true,
supportsGetObjectPartNumber: true,
});
```

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.
## Pass additional data to Lambda function

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).
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:

(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.)
```ts
import * as lambda from '@aws-cdk/aws-lambda';
import * as s3 from '@aws-cdk/aws-s3';
import * as s3objectlambda from '@aws-cdk/aws-s3objectlambda';

<!--END CFNONLY DISCLAIMER-->
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",
},
});
```
124 changes: 60 additions & 64 deletions packages/@aws-cdk/aws-s3objectlambda/lib/access-point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -43,82 +43,79 @@ 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
* Lambda function on every request.
*
* @default - No data.
*/
readonly payload?: string
readonly payload?: Record<string, unknown>;
}

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('/')) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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.
Expand All @@ -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) {
Expand All @@ -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}`;
}
}
5 changes: 5 additions & 0 deletions packages/@aws-cdk/aws-s3objectlambda/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,5 +115,10 @@
},
"publishConfig": {
"tag": "latest"
},
"awslint": {
"exclude": [
"attribute-tag:@aws-cdk/aws-s3objectlambda.AccessPoint.accessPointName"
]
}
}
Loading

0 comments on commit 2a517ce

Please sign in to comment.