From c08a29c0c9dfa6cfbf4ce7e3eddb47ce082b55d5 Mon Sep 17 00:00:00 2001 From: Michael Kaiser Date: Thu, 9 Jan 2025 10:29:17 -0600 Subject: [PATCH] Add comments explaining the code --- .../java/com/myorg/S3ObjectLambdaApp.java | 6 ++ .../java/com/myorg/S3ObjectLambdaStack.java | 59 ++++++++++++--- .../com/myorg/S3ObjectLambdaTransformer.java | 74 ++++++++++++++++--- .../com/myorg/model/TransformedObject.java | 8 ++ 4 files changed, 126 insertions(+), 21 deletions(-) diff --git a/java/s3-object-lambda/infra/src/main/java/com/myorg/S3ObjectLambdaApp.java b/java/s3-object-lambda/infra/src/main/java/com/myorg/S3ObjectLambdaApp.java index 33cc6613f..9dbe76a49 100644 --- a/java/s3-object-lambda/infra/src/main/java/com/myorg/S3ObjectLambdaApp.java +++ b/java/s3-object-lambda/infra/src/main/java/com/myorg/S3ObjectLambdaApp.java @@ -3,6 +3,9 @@ import software.amazon.awscdk.App; import software.amazon.awscdk.StackProps; +/** + * Main CDK application class that serves as the entry point for deploying the S3 Object Lambda infrastructure. + */ public class S3ObjectLambdaApp extends App { public static void main(final String... args) { @@ -12,6 +15,9 @@ public static void main(final String... args) { app.synth(); } + /** + * Creates a new instance of the S3ObjectLambdaStack with the specified properties. + */ public S3ObjectLambdaStack createStack(StackProps stackProps) { return new S3ObjectLambdaStack(this, "S3ObjectLambdaStack", stackProps); } diff --git a/java/s3-object-lambda/infra/src/main/java/com/myorg/S3ObjectLambdaStack.java b/java/s3-object-lambda/infra/src/main/java/com/myorg/S3ObjectLambdaStack.java index e50d9af2b..eb2a9c1a8 100644 --- a/java/s3-object-lambda/infra/src/main/java/com/myorg/S3ObjectLambdaStack.java +++ b/java/s3-object-lambda/infra/src/main/java/com/myorg/S3ObjectLambdaStack.java @@ -23,13 +23,19 @@ import static software.amazon.awscdk.services.s3objectlambda.CfnAccessPoint.*; public class S3ObjectLambdaStack extends Stack { - private static final String S3_ACCESS_POINT_NAME = "s3-access-point"; private static final String OBJECT_LAMBDA_ACCESS_POINT_NAME = "object-lambda-access-point"; + /** + * Constructs a new S3ObjectLambdaStack. + */ public S3ObjectLambdaStack(final Construct scope, final String id, final StackProps props) { - super(scope, id, props); + super(scope, id, props); + + // Construct the access point ARN using the region, account ID and access point name var accessPoint = "arn:aws:s3:" + Aws.REGION + ":" + Aws.ACCOUNT_ID + ":accesspoint/" + S3_ACCESS_POINT_NAME; + + // Create a new S3 bucket with secure configuration including: var s3ObjectLambdaBucket = Bucket.Builder.create(this, "S3ObjectLambdaBucket") .removalPolicy(RemovalPolicy.RETAIN) .autoDeleteObjects(false) @@ -37,6 +43,8 @@ public S3ObjectLambdaStack(final Construct scope, final String id, final StackPr .encryption(BucketEncryption.S3_MANAGED) .blockPublicAccess(BlockPublicAccess.BLOCK_ALL) .build(); + + // Create bucket policy statement allowing access through access points var s3ObjectLambdaBucketPolicyStatement = PolicyStatement.Builder.create() .actions(List.of("*")) .principals(List.of(new AnyPrincipal())) @@ -52,20 +60,30 @@ public S3ObjectLambdaStack(final Construct scope, final String id, final StackPr ) ) .build(); + + // Attach the policy to the bucket s3ObjectLambdaBucket.addToResourcePolicy(s3ObjectLambdaBucketPolicyStatement); + + // Create the Lambda function that will transform objects var s3ObjectLambdaFunction = createS3ObjectLambdaFunction(); + + // Add permission for Lambda to write GetObject responses for s3 object var s3ObjectLambdaFunctionPolicyStatement = PolicyStatement.Builder.create() .effect(Effect.ALLOW) .resources(List.of("*")) .actions(List.of("s3-object-lambda:WriteGetObjectResponse")) .build(); s3ObjectLambdaFunction.addToRolePolicy(s3ObjectLambdaFunctionPolicyStatement); + + // Add permission for the account root to invoke the Lambda function var s3ObjectLambdaFunctionPermission = Permission.builder() .action("lambda:InvokeFunction") .principal(new AccountRootPrincipal()) .sourceAccount(Aws.ACCOUNT_ID) .build(); s3ObjectLambdaFunction.addPermission("S3ObjectLambdaPermission", s3ObjectLambdaFunctionPermission); + + // Create policy allowing Lambda function to get objects through the access point var s3ObjectLambdaAccessPointPolicyStatement = PolicyStatement.Builder.create() .sid("S3ObjectLambdaAccessPointPolicyStatement") .effect(Effect.ALLOW) @@ -76,16 +94,22 @@ public S3ObjectLambdaStack(final Construct scope, final String id, final StackPr ) .resources(List.of(accessPoint + "/object/*")) .build(); + + // Create policy document containing the access point policy var s3ObjectLambdaAccessPointPolicyDocument = PolicyDocument.Builder.create() .statements(List.of( s3ObjectLambdaAccessPointPolicyStatement )) .build(); + + // Create the S3 access point for direct bucket access software.amazon.awscdk.services.s3.CfnAccessPoint.Builder.create(this, "S3ObjectLambdaS3AccessPoint") .bucket(s3ObjectLambdaBucket.getBucketName()) .name(S3_ACCESS_POINT_NAME) .policy(s3ObjectLambdaAccessPointPolicyDocument) .build(); + + // Create the Object Lambda access point that will transform objects var s3ObjectLambdaAccessPoint = CfnAccessPoint.Builder.create(this, "S3ObjectLambdaAccessPoint") .name(OBJECT_LAMBDA_ACCESS_POINT_NAME) .objectLambdaConfiguration(ObjectLambdaConfigurationProperty.builder() @@ -107,28 +131,39 @@ public S3ObjectLambdaStack(final Construct scope, final String id, final StackPr ) .build(); CfnOutput.Builder.create(this, "s3ObjectLambdaBucketArn") - .value(s3ObjectLambdaBucket.getBucketArn()) + .value(s3ObjectLambdaBucket.getBucketArn()) // Export bucket ARN .build(); CfnOutput.Builder.create(this, "s3ObjectLambdaFunctionArn") - .value(s3ObjectLambdaFunction.getFunctionArn()) + .value(s3ObjectLambdaFunction.getFunctionArn()) // Export Lambda function ARN .build(); CfnOutput.Builder.create(this, "s3ObjectLambdaAccessPointArn") - .value(s3ObjectLambdaAccessPoint.getAttrArn()) + .value(s3ObjectLambdaAccessPoint.getAttrArn()) // Export access point ARN .build(); + + // Create output with Console URL for easy access to the Lambda access point CfnOutput.Builder.create(this, "s3ObjectLambdaAccessPointUrl") .value("https://console.aws.amazon.com/s3/olap/" + Aws.ACCOUNT_ID + "/" + OBJECT_LAMBDA_ACCESS_POINT_NAME + "?region=" + Aws.REGION) .build(); } + /** + * Creates the Lambda function that will process S3 Object Lambda requests. + * This method configures the function's runtime, code, and build process. + * + * @return A Lambda Function construct configured for S3 Object Lambda processing + */ private Function createS3ObjectLambdaFunction() { + // Define Maven packaging commands to build the Lambda function List packagingInstructions = List.of( "/bin/sh", "-c", + // Build the project and copy the JAR to the asset output directory "mvn -e -q clean package && cp /asset-input/target/lambda-1.0-SNAPSHOT.jar /asset-output/" ); + // Configure the bundling options for packaging the Lambda function var builderOptions = BundlingOptions.builder() - .command(packagingInstructions) - .image(Runtime.JAVA_17.getBundlingImage()) + .command(packagingInstructions) // Set the Maven build commands + .image(Runtime.JAVA_17.getBundlingImage()) // Use Java 17 runtime image .volumes( singletonList( DockerVolume.builder() @@ -139,10 +174,12 @@ private Function createS3ObjectLambdaFunction() { .user("root") .outputType(ARCHIVED) .build(); + + // Create the Lambda function with specified configuration return Function.Builder.create(this, "S3ObjectLambdaFunction") - .runtime(Runtime.JAVA_17) - .functionName("S3ObjectLambdaFunction") - .memorySize(2048) + .runtime(Runtime.JAVA_17) // Set Java 17 runtime + .functionName("S3ObjectLambdaFunction") // Set function name + .memorySize(2048) // Allocate 2GB memory .code( Code.fromAsset( "../lambda/", @@ -152,4 +189,4 @@ private Function createS3ObjectLambdaFunction() { .handler("com.myorg.S3ObjectLambdaTransformer::handleRequest") .build(); } -} +} \ No newline at end of file diff --git a/java/s3-object-lambda/lambda/src/main/java/com/myorg/S3ObjectLambdaTransformer.java b/java/s3-object-lambda/lambda/src/main/java/com/myorg/S3ObjectLambdaTransformer.java index d22f2af50..40109e9cb 100644 --- a/java/s3-object-lambda/lambda/src/main/java/com/myorg/S3ObjectLambdaTransformer.java +++ b/java/s3-object-lambda/lambda/src/main/java/com/myorg/S3ObjectLambdaTransformer.java @@ -21,14 +21,33 @@ import static java.net.http.HttpResponse.BodyHandlers.ofInputStream; +/** + * AWS Lambda function handler that processes S3 Object Lambda requests. + * This class implements the transformation logic for S3 objects accessed + * through + * the Object Lambda Access Point. + */ public class S3ObjectLambdaTransformer { + /** + * Main handler method that processes S3 Object Lambda events. + * This method retrieves the original object from S3, applies transformations, + * and returns the modified object data. + * + * @param event The S3 Object Lambda event containing request details + * @param context The Lambda execution context + */ public void handleRequest(S3ObjectLambdaEvent event, Context context) { try (var s3Client = S3Client.create()) { + // Create JSON mapper and log the incoming event var objectMapper = createObjectMapper(); log(context, "event: " + writeValue(objectMapper, event)); + + // Extract the pre-signed URL from the event context var objectContext = event.getGetObjectContext(); var s3Url = objectContext.getInputS3Url(); + + // Create HTTP client and fetch the original object var uri = URI.create(s3Url); var httpClient = HttpClient.newBuilder().build(); var httpRequest = HttpRequest.newBuilder(uri).GET().build(); @@ -39,48 +58,83 @@ public void handleRequest(S3ObjectLambdaEvent event, Context context) { .requestRoute(event.outputRoute()) .requestToken(event.outputToken()) .statusCode(response.statusCode()); + // Process successful responses (HTTP 200) if (response.statusCode() == 200) { + // Build metadata object with content length and hash values var metadata = TransformedObject.Metadata.builder() - .withLength((long) responseBodyBytes.length) - .withMD5(DigestUtils.md5Hex(responseBodyBytes)) - .withSHA1(DigestUtils.sha1Hex(responseBodyBytes)) - .withSHA256(DigestUtils.sha256Hex(responseBodyBytes)) + .withLength((long) responseBodyBytes.length) // Set content length + .withMD5(DigestUtils.md5Hex(responseBodyBytes)) // Calculate MD5 hash + .withSHA1(DigestUtils.sha1Hex(responseBodyBytes)) // Calculate SHA1 hash + .withSHA256(DigestUtils.sha256Hex(responseBodyBytes)) // Calculate SHA256 hash .build(); + // Create transformed object containing the metadata var transformedObject = TransformedObject.builder() .withMetadata(metadata) .build(); + // Log the transformed object for debugging log(context, "transformedObject: " + writeValue(objectMapper, transformedObject)); requestBody = RequestBody.fromString(writeValue(objectMapper, transformedObject)); - } else { + } else { + // Handle non-200 HTTP responses by setting the error message writeGetObjectResponseRequestBuilder - .errorMessage(new String(responseBodyBytes)); + .errorMessage(new String(responseBodyBytes)); // Convert error response body to string and set as error message } + // Write the final response back to S3 Object Lambda with either transformed object or error details s3Client.writeGetObjectResponse(writeGetObjectResponseRequestBuilder.build(), requestBody); } catch (IOException | InterruptedException e) { - throw new RuntimeException("Error while handling request: " + e.getMessage(), e); - } + // Wrap and rethrow any IO or threading exceptions that occur during processing + throw new RuntimeException("Error while handling request: " + e.getMessage(), e) } } + /** + * Reads all bytes from the input stream of an HTTP response. + * Converts the response stream into a byte array for processing. + * + * @param response HTTP response containing the input stream to read + * @return byte array containing all the data from the input stream + * @throws IOException if an I/O error occurs while reading the stream + */ private static byte[] readBytes(HttpResponse response) throws IOException { try (var inputStream = response.body()) { return inputStream.readAllBytes(); } } + /** + * Logs a message to CloudWatch using the Lambda context logger. + * Prefixes the message with the request ID for tracing purposes. + * + * @param context Lambda execution context containing the logger + * @param message Message to be logged + */ private static void log(Context context, String message) { if (message != null) { Optional.ofNullable(context) - .map(Context::getLogger) - .ifPresent(lambdaLogger -> lambdaLogger.log(message)); + .map(Context::getLogger) + .ifPresent(lambdaLogger -> lambdaLogger.log(message)); } } + /** + * Creates and configures a Jackson ObjectMapper for JSON serialization. + * Enables pretty printing and disables failing on empty beans. + * + * @return Configured ObjectMapper instance + */ private static ObjectMapper createObjectMapper() { var objectMapper = new ObjectMapper(); objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); return objectMapper; } + /** + * Serializes an object to a JSON string using the provided ObjectMapper. + * Handles JsonProcessingException by returning an error message. + * + * @param objectMapper The ObjectMapper to use for serialization + * @param object The object to serialize + * @return JSON string representation of the object or error message + */ private static String writeValue(ObjectMapper objectMapper, Object object) { try { return objectMapper.writeValueAsString(object); diff --git a/java/s3-object-lambda/lambda/src/main/java/com/myorg/model/TransformedObject.java b/java/s3-object-lambda/lambda/src/main/java/com/myorg/model/TransformedObject.java index a62bb814d..fc6019cda 100644 --- a/java/s3-object-lambda/lambda/src/main/java/com/myorg/model/TransformedObject.java +++ b/java/s3-object-lambda/lambda/src/main/java/com/myorg/model/TransformedObject.java @@ -2,11 +2,19 @@ import lombok.*; +/** + * Represents a transformed S3 object with its metadata. + * This class uses Lombok annotations for builder pattern, toString, and data methods generation. + */ @Builder(setterPrefix = "with", builderClassName = "TransformedObjectBuilder") @ToString @Data public class TransformedObject { + /** + * Record class representing the metadata of a transformed object. + * Contains various hash values and the object length for verification purposes. + */ @Builder(setterPrefix = "with", builderClassName = "MetadataBuilder") public record Metadata(Long length, String MD5, String SHA1, String SHA256) {