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

fix(type-safe-api): create a new asset with PrepareApiSpecOptions props and used it in PrepareApiSpecCustomResourceProperties instead or raw properties in order to avoid large payload that can cause the PrepareSpecCustomResource to fail #773

Closed
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@ export interface S3Location {
/**
* Properties required to prepare the api specification with the given integrations, authorizers, etc
*/
export interface PrepareApiSpecCustomResourceProperties
extends PrepareApiSpecOptions {
export interface PrepareApiSpecCustomResourceProperties {
/**
* The location from which to read the spec to prepare
*/
readonly inputSpecLocation: S3Location;
/**
* The location from which to read the options for the input spec (integrations, cors, authorizers, etc)
*/
readonly inputOptionsLocation: S3Location;
/**
* The location to write the prepared spec. Note that the key is used as a prefix and the output location will
* include a hash.
Expand Down Expand Up @@ -86,14 +89,14 @@ const s3 = new S3Client({
/**
* Prepare the api spec for API Gateway
* @param inputSpecLocation location of the specification to prepare
* @param inputOptionsLocation location of the options for the input spec (integrations, cors, authorizers, etc)
* @param outputSpecLocation location to write the prepared spec to
* @param options integrations, authorizers etc to apply
* @return the output location of the prepared spec
*/
const prepare = async ({
inputSpecLocation,
inputOptionsLocation,
outputSpecLocation,
...options
}: PrepareApiSpecCustomResourceProperties): Promise<S3Location> => {
// Read the spec from the s3 input location
const inputSpec = JSON.parse(
Expand All @@ -107,8 +110,19 @@ const prepare = async ({
).Body!.transformToString("utf-8")
);

const inputOptions: PrepareApiSpecOptions = JSON.parse(
await (
await s3.send(
new GetObjectCommand({
Bucket: inputOptionsLocation.bucket,
Key: inputOptionsLocation.key,
})
)
).Body!.transformToString("utf-8")
);

// Prepare the spec
const preparedSpec = prepareApiSpec(inputSpec, options);
const preparedSpec = prepareApiSpec(inputSpec, inputOptions);
const preparedSpecHash = crypto
.createHash("sha256")
.update(JSON.stringify(preparedSpec))
Expand Down
114 changes: 70 additions & 44 deletions packages/type-safe-api/src/construct/type-safe-rest-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from "aws-cdk-lib/aws-lambda";
import { LogGroup } from "aws-cdk-lib/aws-logs";
import { Asset } from "aws-cdk-lib/aws-s3-assets";
import { BucketDeployment, Source } from "aws-cdk-lib/aws-s3-deployment";
import {
CfnIPSet,
CfnWebACL,
Expand Down Expand Up @@ -120,11 +121,71 @@ export class TypeSafeRestApi extends Construct {
const inputSpecAsset = new Asset(this, "InputSpec", {
path: specPath,
});
// We'll output the prepared spec options in the same asset bucket
const preparedSpecOptionsKeyPrefix = `${inputSpecAsset.s3ObjectKey}-options`;

// We'll output the prepared spec in the same asset bucket
const preparedSpecOutputKeyPrefix = `${inputSpecAsset.s3ObjectKey}-prepared`;

const stack = Stack.of(this);

const serializedCorsOptions: SerializedCorsOptions | undefined =
corsOptions && {
allowHeaders: corsOptions.allowHeaders || [
...Cors.DEFAULT_HEADERS,
"x-amz-content-sha256",
],
allowMethods: corsOptions.allowMethods || Cors.ALL_METHODS,
allowOrigins: corsOptions.allowOrigins,
statusCode: corsOptions.statusCode || 204,
};

const prepareSpecOptions: PrepareApiSpecOptions = {
defaultAuthorizerReference:
serializeAsAuthorizerReference(defaultAuthorizer),
integrations: Object.fromEntries(
Object.entries(integrations).map(([operationId, integration]) => [
operationId,
{
integration: integration.integration.render({
operationId,
scope: this,
...operationLookup[operationId],
corsOptions: serializedCorsOptions,
operationLookup,
}),
methodAuthorizer: serializeAsAuthorizerReference(
integration.authorizer
),
options: integration.options,
},
])
),
securitySchemes: prepareSecuritySchemes(
this,
integrations,
defaultAuthorizer,
options.apiKeyOptions
),
corsOptions: serializedCorsOptions,
operationLookup,
apiKeyOptions: options.apiKeyOptions,
};

// Upload the configuration to s3 as a s3 deployment asset (support deploy-time values)
const inputOptionsDeployment = new BucketDeployment(
this,
"OptionsDeployment",
{
sources: [
Source.jsonData("preparedSpecOptions.json", prepareSpecOptions),
],
destinationBucket: inputSpecAsset.bucket,
destinationKeyPrefix: preparedSpecOptionsKeyPrefix,
prune: false,
}
);

// Lambda name prefix is truncated to 48 characters (16 below the max of 64)
const lambdaNamePrefix = `${PDKNag.getStackPrefix(stack)
.split("/")
Expand Down Expand Up @@ -157,6 +218,11 @@ export class TypeSafeRestApi extends Construct {
actions: ["s3:getObject"],
resources: [
inputSpecAsset.bucket.arnForObjects(inputSpecAsset.s3ObjectKey),
// The options file will include a hash of the prepared spec, which is not known until deploy time since
// tokens must be resolved
inputSpecAsset.bucket.arnForObjects(
`${preparedSpecOptionsKeyPrefix}/*`
),
],
}),
new PolicyStatement({
Expand Down Expand Up @@ -285,49 +351,6 @@ export class TypeSafeRestApi extends Construct {
}
);

const serializedCorsOptions: SerializedCorsOptions | undefined =
corsOptions && {
allowHeaders: corsOptions.allowHeaders || [
...Cors.DEFAULT_HEADERS,
"x-amz-content-sha256",
],
allowMethods: corsOptions.allowMethods || Cors.ALL_METHODS,
allowOrigins: corsOptions.allowOrigins,
statusCode: corsOptions.statusCode || 204,
};

const prepareSpecOptions: PrepareApiSpecOptions = {
defaultAuthorizerReference:
serializeAsAuthorizerReference(defaultAuthorizer),
integrations: Object.fromEntries(
Object.entries(integrations).map(([operationId, integration]) => [
operationId,
{
integration: integration.integration.render({
operationId,
scope: this,
...operationLookup[operationId],
corsOptions: serializedCorsOptions,
operationLookup,
}),
methodAuthorizer: serializeAsAuthorizerReference(
integration.authorizer
),
options: integration.options,
},
])
),
securitySchemes: prepareSecuritySchemes(
this,
integrations,
defaultAuthorizer,
options.apiKeyOptions
),
corsOptions: serializedCorsOptions,
operationLookup,
apiKeyOptions: options.apiKeyOptions,
};

// Spec preparation will happen in a custom resource lambda so that references to lambda integrations etc can be
// resolved. However, we also prepare inline to perform some additional validation at synth time.
const spec = JSON.parse(fs.readFileSync(specPath, "utf-8"));
Expand All @@ -339,11 +362,14 @@ export class TypeSafeRestApi extends Construct {
bucket: inputSpecAsset.bucket.bucketName,
key: inputSpecAsset.s3ObjectKey,
},
inputOptionsLocation: {
bucket: inputOptionsDeployment.deployedBucket.bucketName,
key: `${preparedSpecOptionsKeyPrefix}/preparedSpecOptions.json`,
},
outputSpecLocation: {
bucket: inputSpecAsset.bucket.bucketName,
key: preparedSpecOutputKeyPrefix,
},
...prepareSpecOptions,
};

const prepareSpecCustomResource = new CustomResource(
Expand Down
Loading
Loading