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 015d52a
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 85 deletions.
49 changes: 42 additions & 7 deletions packages/@aws-cdk/aws-s3objectlambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,54 @@

<!--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
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:

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:

<!--END CFNONLY DISCLAIMER-->
```ts nofixture
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"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
]
},
"Handler": "index.handler",
"Runtime": "nodejs10.x"
"Runtime": "nodejs14.x"
},
"DependsOn": [
"MyFunction1ServiceRoleDefaultPolicy39556460",
Expand Down Expand Up @@ -142,7 +142,7 @@
]
},
"Handler": "index.handler",
"Runtime": "nodejs10.x"
"Runtime": "nodejs14.x"
},
"DependsOn": [
"MyFunction2ServiceRoleDefaultPolicyA79C693E",
Expand All @@ -169,7 +169,7 @@
"CloudWatchMetricsEnabled": true,
"SupportingAccessPoint": {
"Fn::GetAtt": [
"MyObjectLambda1AccessPointD5812646",
"MyObjectLambda1SupportingAccessPoint223B719B",
"Arn"
]
},
Expand All @@ -185,8 +185,7 @@
"MyFunction12A744C2E",
"Arn"
]
},
"FunctionPayload": ""
}
}
}
}
Expand Down Expand Up @@ -216,7 +215,7 @@
],
"SupportingAccessPoint": {
"Fn::GetAtt": [
"MyObjectLambda2AccessPoint76FB5ACF",
"MyObjectLambda2SupportingAccessPoint6C54778F",
"Arn"
]
},
Expand All @@ -233,7 +232,7 @@
"Arn"
]
},
"FunctionPayload": "{foo: 10}"
"FunctionPayload": "{\"foo\":10}"
}
}
}
Expand Down
Loading

0 comments on commit 015d52a

Please sign in to comment.