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

Refactor to use cloud assembly #167

Merged
merged 8 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions examples/s3-object-lambda/src/s3-object-lambda-stack.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws-native';
import * as pulumicdk from '@pulumi/cdk';
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3ObjectLambda from 'aws-cdk-lib/aws-s3objectlambda';
import { Construct } from 'constructs';

// configurable variables
const S3_ACCESS_POINT_NAME = 'example-test-ap';
Expand Down Expand Up @@ -74,23 +74,27 @@ export class S3ObjectLambdaStack extends pulumicdk.Stack {
const policyStatement = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:GetObject'],
principals: [new iam.ArnPrincipal(<string>retrieveTransformedObjectLambda.role?.roleArn)],
resources: [`${accessPoint}/object/*`],
principals: [
new iam.ArnPrincipal(this.asOutput(retrieveTransformedObjectLambda.role?.roleArn) as unknown as string),
],
resources: [this.asOutput(`${accessPoint}/object/*`) as unknown as string],
});
policyStatement.sid = 'AllowLambdaToUseAccessPoint';
policyDoc.addStatements(policyStatement);

new s3.CfnAccessPoint(this, 'exampleBucketAP', {
bucket: bucket.bucketName,
const ap = new aws.s3.AccessPoint('exampleBucketAP', {
// CDK property can be passed to a Pulumi resource
bucket: this.asOutput(bucket.bucketName),
name: S3_ACCESS_POINT_NAME,
policy: policyDoc,
policy: policyDoc.toJSON(),
});

// Access point to receive GET request and use lambda to process objects
const objectLambdaAP = new s3ObjectLambda.CfnAccessPoint(this, 's3ObjectLambdaAP', {
name: OBJECT_LAMBDA_ACCESS_POINT_NAME,
objectLambdaConfiguration: {
supportingAccessPoint: accessPoint,
// a pulumi resource property can be passed to a cdk resource
supportingAccessPoint: pulumicdk.asString(ap.arn),
transformationConfigurations: [
{
actions: ['GetObject'],
Expand Down
18 changes: 12 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,40 +21,46 @@
}
},
"resolutions": {
"wrap-ansi": "7.0.0",
"string-width": "4.1.0"
"wrap-ansi": "7.0.0",
"string-width": "4.1.0"
},
"devDependencies": {
"@aws-cdk/aws-apprunner-alpha": "2.20.0-alpha.0",
"@pulumi/aws": "^6.32.0",
"@pulumi/aws-native": "0.121.0",
"@pulumi/docker": "^4.5.0",
"@pulumi/pulumi": "^3.117.0",
"@types/archiver": "^6.0.2",
"@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.2",
"@types/mock-fs": "^4.13.4",
"@types/node": "^20.12.13",
"aws-cdk-lib": "2.149.0",
"constructs": "^10.0.111",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"typescript-eslint": "^7.16.1",
"jest": "^29.5.0",
"jest-junit": "^15",
"mock-fs": "^5.3.0",
"prettier": "^2.6.2",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
"typescript": "^5.4.5",
"typescript-eslint": "^7.16.1"
},
"peerDependencies": {
"@pulumi/aws": "^6.32.0",
"@pulumi/aws-native": "^0.121.0",
"@pulumi/docker": "^4.5.0",
"@pulumi/pulumi": "^3.117.0",
"aws-cdk-lib": "^2.20.0",
"constructs": "^10.0.111"
},
"dependencies": {
"@pulumi/aws-native": "0.121.0",
"@types/glob": "^8.1.0",
"archiver": "^7.0.1"
"archiver": "^7.0.1",
"cdk-assets": "^2.154.8",
"fs-extra": "^11.2.0"
},
"scripts": {
"set-version": "sed -i.bak -e \"s/\\${VERSION}/$(pulumictl get version --language javascript)/g\" package.json && rm package.json.bak",
Expand Down
3 changes: 3 additions & 0 deletions src/assembly/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './stack';
export * from './types';
export * from './manifest';
174 changes: 174 additions & 0 deletions src/assembly/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import * as path from 'path';
import { AssemblyManifest, Manifest, ArtifactType, ArtifactMetadataEntryType } from '@aws-cdk/cloud-assembly-schema';
import * as fs from 'fs-extra';
import { CloudFormationTemplate } from '../cfn';
import { ArtifactManifest, AssetManifestProperties, LogicalIdMetadataEntry } from 'aws-cdk-lib/cloud-assembly-schema';
import { AssetManifest, DockerImageManifestEntry, FileManifestEntry } from 'cdk-assets';
import { StackManifest } from './stack';
import { ConstructTree, StackAsset, StackMetadata } from './types';
import { warn } from '@pulumi/pulumi/log';

/**
* Reads a Cloud Assembly manifest
*/
export class AssemblyManifestReader {
private static readonly DEFAULT_FILENAME = 'manifest.json';

/**
* Reads a Cloud Assembly manifest from a file or a directory
* If the given filePath is a directory then it will look for
* a file within the directory with the DEFAULT_FILENAME
*/
public static fromDirectory(dir: string): AssemblyManifestReader {
const filePath = path.join(dir, AssemblyManifestReader.DEFAULT_FILENAME);
try {
fs.statSync(dir);
const obj = Manifest.loadAssemblyManifest(filePath);
return new AssemblyManifestReader(dir, obj);
} catch (e: any) {
throw new Error(`Cannot read manifest at '${filePath}': ${e}`);
}
}

/**
* The directory where the manifest was found
*/
public readonly directory: string;
corymhall marked this conversation as resolved.
Show resolved Hide resolved

private readonly _stackManifests = new Map<string, StackManifest>();
private readonly tree: ConstructTree;

constructor(directory: string, private readonly manifest: AssemblyManifest) {
this.directory = directory;
try {
const fullTree = fs.readJsonSync(path.resolve(this.directory, 'tree.json'));
if (!fullTree.tree || !fullTree.tree.children) {
throw new Error(`Invalid tree.json found ${JSON.stringify(fullTree)}`);
}
this.tree = fullTree.tree;
this.renderStackManifests();
} catch (e) {
throw new Error(`Could not process CDK Cloud Assembly directory: ${e}`);
}
}

/**
* Renders the StackManifests for all the stacks in the CloudAssembly
* - Finds all CloudFormation stacks in the assembly
* - Reads the stack template files
* - Creates a metadata map of constructPath to logicalId for all resources in the stack
* - Finds all assets that the stack depends on
*/
private renderStackManifests() {
for (const [artifactId, artifact] of Object.entries(this.manifest.artifacts ?? {})) {
if (artifact.type === ArtifactType.AWS_CLOUDFORMATION_STACK) {
if (!artifact.properties || !('templateFile' in artifact.properties)) {
throw new Error('Invalid CloudFormation artifact. Cannot find the template file');
}
const templateFile = artifact.properties.templateFile;

let template: CloudFormationTemplate;
try {
template = fs.readJSONSync(path.resolve(this.directory, templateFile));
} catch (e) {
throw new Error(`Failed to read CloudFormation template at path: ${templateFile}: ${e}`);
}

const metadata = this.getMetadata(artifact);

const assets = this.getAssetsForStack(artifactId);
if (!this.tree.children) {
throw new Error('Invalid tree.json found');
}
const stackTree = this.tree.children[artifactId];
const stackManifest = new StackManifest(
this.directory,
artifactId,
templateFile,
metadata,
stackTree,
template,
assets,
);
this._stackManifests.set(artifactId, stackManifest);
}
}
}

/**
* Creates a metadata map of constructPath to logicalId for all resources in the stack
*
* @param artifact - The manifest containing the stack metadata
* @returns The StackMetadata lookup table
*/
private getMetadata(artifact: ArtifactManifest): StackMetadata {
const metadata: StackMetadata = {};
for (const [metadataId, metadataEntry] of Object.entries(artifact.metadata ?? {})) {
metadataEntry.forEach((meta) => {
if (meta.type === ArtifactMetadataEntryType.LOGICAL_ID) {
// For some reason the metadata entry prefixes the path with a `/`
const path = metadataId.startsWith('/') ? metadataId.substring(1) : metadataId;
metadata[path] = meta.data as LogicalIdMetadataEntry;
}
});
}
return metadata;
}

/**
* Get the stacks from the Cloud Assembly
*
* @returns List of CloudFormationStackArtifacts available in the Cloud Assembly
*/
public get stackManifests(): StackManifest[] {
return Array.from(this._stackManifests.values());
}

/**
* Return a list of assets for a given stack
*
* @param stackId - The artifactId of the stack to find assets for
* @returns a list of `StackAsset` for the given stack
*/
private getAssetsForStack(stackId: string): StackAsset[] {
const assets: (FileManifestEntry | DockerImageManifestEntry)[] = [];
for (const artifact of Object.values(this.manifest.artifacts ?? {})) {
if (
artifact.type === ArtifactType.ASSET_MANIFEST &&
(artifact.properties as AssetManifestProperties)?.file === `${stackId}.assets.json`
) {
assets.push(...this.assetsFromAssetManifest(artifact));
}
}
return assets;
}

/**
* Get a list of assets from the asset manifest.
*
* @param artifact - An ArtifactManifest to extract individual assets from
* @returns a list of file and docker assets found in the manifest
corymhall marked this conversation as resolved.
Show resolved Hide resolved
*/
private assetsFromAssetManifest(artifact: ArtifactManifest): StackAsset[] {
t0yv0 marked this conversation as resolved.
Show resolved Hide resolved
const assets: (FileManifestEntry | DockerImageManifestEntry)[] = [];
const fileName = (artifact.properties as AssetManifestProperties).file;
const assetManifest = AssetManifest.fromFile(path.join(this.directory, fileName));
assetManifest.entries.forEach((entry) => {
if (entry.type === 'file') {
const source = (entry as FileManifestEntry).source;
// This will ignore template assets
if (source.path && source.path.startsWith('asset.')) {
assets.push(entry as FileManifestEntry);
}
} else if (entry.type === 'docker-image') {
const source = (entry as DockerImageManifestEntry).source;
if (source.directory && source.directory.startsWith('asset.')) {
assets.push(entry as DockerImageManifestEntry);
}
} else {
warn(`found unexpected asset type: ${entry.type}`);
}
});
return assets;
}
}
Loading