Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;

import java.time.Instant;
Expand Down Expand Up @@ -79,6 +81,7 @@ public Response createFunction(@Context HttpHeaders headers, String body) {
@GET
@Path("/functions/{functionName}")
public Response getFunction(@Context HttpHeaders headers,
@Context UriInfo uriInfo,
@PathParam("functionName") String functionName) {
String region = regionResolver.resolveRegion(headers);
LambdaFunction fn = lambdaService.getFunction(region, functionName);
Expand All @@ -91,8 +94,13 @@ public Response getFunction(@Context HttpHeaders headers,
code.put("ImageUri", fn.getImageUri());
code.put("ResolvedImageUri", fn.getImageUri());
} else {
code.put("Location", "https://awslambda-" + region + "-tasks.s3." + region
+ ".amazonaws.com/" + fn.getFunctionName());
// Path-style URL to the package in Floci's own S3, built from the
// request so it targets the same endpoint the client is talking to.
String location = UriBuilder.fromUri(uriInfo.getBaseUri())
.path(LambdaService.tasksBucketName(region))
.path(LambdaService.codeObjectKey(fn))
.build().toString();
code.put("Location", location);
code.put("RepositoryType", "S3");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,15 +314,15 @@ public LambdaFunction createFunction(String region, Map<String, Object> request)
if (zipFileBase64 != null) {
fn.setS3Bucket(null);
fn.setS3Key(null);
extractZipCode(fn, zipFileBase64);
extractZipCode(fn, zipFileBase64, region);
}
String s3Bucket = (String) code.get("S3Bucket");
String s3Key = (String) code.get("S3Key");
if (s3Bucket != null && s3Key != null) {
if ("hot-reload".equals(s3Bucket)) {
applyHotReload(fn, s3Key);
} else {
extractZipCodeFromS3(fn, s3Bucket, s3Key);
extractZipCodeFromS3(fn, s3Bucket, s3Key, region);
}
}
}
Expand Down Expand Up @@ -384,7 +384,7 @@ public LambdaFunction updateFunctionCode(String region, String functionName, Map
if (zipFileBase64 != null) {
fn.setS3Bucket(null);
fn.setS3Key(null);
extractZipCode(fn, zipFileBase64);
extractZipCode(fn, zipFileBase64, region);
}
if (imageUri != null) {
fn.setImageUri(imageUri);
Expand All @@ -393,7 +393,7 @@ public LambdaFunction updateFunctionCode(String region, String functionName, Map
if ("hot-reload".equals(s3Bucket)) {
applyHotReload(fn, s3Key);
} else {
extractZipCodeFromS3(fn, s3Bucket, s3Key);
extractZipCodeFromS3(fn, s3Bucket, s3Key, region);
}
}

Expand Down Expand Up @@ -554,6 +554,14 @@ public void deleteFunction(String region, String functionName) {
}
}
}
// Best-effort: drop the stored deployment package.
if (s3Service != null) {
try {
s3Service.deleteObject(tasksBucketName(region), codeObjectKey(fn));
} catch (Exception ignored) {
// best-effort cleanup
}
}
Comment on lines +558 to +564

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent catch in deleteFunction cleanup violates the project's coding style. AGENTS.md states: "Never leave a catch block empty. If an exception is intentionally tolerated, log it with enough context to diagnose it later." The storeDeploymentPackage equivalent correctly uses LOG.warnv; the delete cleanup should do the same so failures are diagnosable.

Suggested change
if (s3Service != null) {
try {
s3Service.deleteObject(tasksBucketName(region), codeObjectKey(fn));
} catch (Exception ignored) {
// best-effort cleanup
}
}
if (s3Service != null) {
try {
s3Service.deleteObject(tasksBucketName(region), codeObjectKey(fn));
} catch (Exception e) {
// best-effort cleanup
LOG.warnv("Could not delete deployment package for {0}: {1}",
functionName, e.getMessage());
}
}

Context Used: AGENTS.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

LOG.infov("Deleted Lambda function: {0}", functionName);
}

Expand Down Expand Up @@ -1134,11 +1142,23 @@ private String[] toStringArray(Object obj) {
return null;
}

private void extractZipCode(LambdaFunction fn, String zipFileBase64) {
extractZipCodeBytes(fn, Base64.getDecoder().decode(zipFileBase64));
/** Per-region bucket that mirrors AWS's Lambda code bucket naming. */
public static String tasksBucketName(String region) {
String r = (region == null || region.isBlank()) ? "us-east-1" : region;
return "awslambda-" + r + "-tasks";
}

/** Stable, account-scoped S3 key for a function's current deployment package. */
public static String codeObjectKey(LambdaFunction fn) {
String account = fn.getAccountId() != null ? fn.getAccountId() : "000000000000";
return "snapshots/" + account + "/" + fn.getFunctionName();
}

private void extractZipCodeBytes(LambdaFunction fn, byte[] zipBytes) {
private void extractZipCode(LambdaFunction fn, String zipFileBase64, String region) {
extractZipCodeBytes(fn, Base64.getDecoder().decode(zipFileBase64), region);
}

private void extractZipCodeBytes(LambdaFunction fn, byte[] zipBytes, String region) {
Path codePath = codeStore.getCodePath(fn.getFunctionName());
try {
zipExtractor.extractTo(zipBytes, codePath);
Expand Down Expand Up @@ -1172,6 +1192,10 @@ private void extractZipCodeBytes(LambdaFunction fn, byte[] zipBytes) {
"Handler file '" + handlerFile + "' not found in deployment package", 400);
}
}

// Deploy succeeded: keep the exact package so GetFunction can serve
// a real Code.Location.
storeDeploymentPackage(fn, zipBytes, region);
} catch (AwsException e) {
throw e;
} catch (IOException e) {
Expand All @@ -1180,7 +1204,30 @@ private void extractZipCodeBytes(LambdaFunction fn, byte[] zipBytes) {
}
}

private void extractZipCodeFromS3(LambdaFunction fn, String s3Bucket, String s3Key) {
private void storeDeploymentPackage(LambdaFunction fn, byte[] zipBytes, String region) {
if (s3Service == null) {
return;
}
String bucket = tasksBucketName(region);
try {
try {
s3Service.createBucket(bucket, region);
} catch (AwsException e) {
// createBucket is idempotent only in us-east-1; elsewhere it 409s if it exists.
if (!"BucketAlreadyOwnedByYou".equals(e.getErrorCode())) {
throw e;
}
}
s3Service.putObject(bucket, codeObjectKey(fn), zipBytes,
"application/zip", java.util.Map.of());
} catch (Exception e) {
// Never fail a deploy because the convenience copy failed.
LOG.warnv("Could not store deployment package for {0}: {1}",
fn.getFunctionName(), e.getMessage());
}
}

private void extractZipCodeFromS3(LambdaFunction fn, String s3Bucket, String s3Key, String region) {
if (s3Service == null) {
throw new AwsException("ServiceUnavailableException", "S3 service not available", 503);
}
Expand All @@ -1194,7 +1241,7 @@ private void extractZipCodeFromS3(LambdaFunction fn, String s3Bucket, String s3K
throw new AwsException("InvalidParameterValueException",
"Unable to fetch code from s3://" + s3Bucket + "/" + s3Key + ": " + e.getMessage(), 400);
}
extractZipCodeBytes(fn, obj.getData());
extractZipCodeBytes(fn, obj.getData(), region);
}

private String resolveHandlerFilePath(LambdaFunction fn) {
Expand Down Expand Up @@ -1485,7 +1532,7 @@ public void onS3ObjectUpdated(@Observes S3ObjectUpdatedEvent event) {
fn.getFunctionName(), event.bucketName(), event.key());
try {
S3Object obj = s3Service.getObject(event.bucketName(), event.key());
extractZipCodeBytes(fn, obj.getData());
extractZipCodeBytes(fn, obj.getData(), region);
fn.setLastModified(Instant.now().toEpochMilli());
fn.setRevisionId(UUID.randomUUID().toString());
functionStore.save(region, fn);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,4 +452,103 @@ void asyncInvoke_payloadExactly1MB_isNotRejected() {
.then()
.statusCode(not(413));
}

// ── GetFunction Code.Location downloadability ──────────────────────────────

@Test
@Order(40)
void getFunction_codeLocation_isDownloadableAndByteExact() throws Exception {
// Build a real zip with a nodejs handler file (extractZipCode verifies it).
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
zos.putNextEntry(new ZipEntry("index.js"));
zos.write("exports.handler = async () => ({ statusCode: 200 });\n".getBytes());
zos.closeEntry();
}
String zipB64 = Base64.getEncoder().encodeToString(baos.toByteArray());

given()
.contentType("application/json")
.body("""
{
"FunctionName": "code-dl-fn",
"Runtime": "nodejs20.x",
"Role": "arn:aws:iam::000000000000:role/lambda-role",
"Handler": "index.handler",
"Code": {
"ZipFile": "%s"
}
}
""".formatted(zipB64))
.when()
.post(BASE_PATH + "/functions")
.then()
.statusCode(201);

// GetFunction → Code.Location must be an S3-style URL on this endpoint.
String location = given()
.when()
.get(BASE_PATH + "/functions/code-dl-fn")
.then()
.statusCode(200)
.body("Code.RepositoryType", equalTo("S3"))
.extract().path("Code.Location");

String expectedSha256 = given()
.when()
.get(BASE_PATH + "/functions/code-dl-fn/configuration")
.then()
.statusCode(200)
.extract().path("CodeSha256");

// Follow Code.Location against the same test server via its path.
String path = java.net.URI.create(location).getRawPath();
byte[] pkg = given()
.when()
.get(path)
.then()
.statusCode(200)
.extract().asByteArray();

// Valid zip → local file header magic "PK\003\004".
org.junit.jupiter.api.Assertions.assertTrue(pkg.length > 0, "package must not be empty");
org.junit.jupiter.api.Assertions.assertEquals('P', pkg[0]);
org.junit.jupiter.api.Assertions.assertEquals('K', pkg[1]);

// Byte-exact: downloaded package hashes to the stored CodeSha256.
byte[] digest = java.security.MessageDigest.getInstance("SHA-256").digest(pkg);
String downloadedSha256 = Base64.getEncoder().encodeToString(digest);
org.junit.jupiter.api.Assertions.assertEquals(expectedSha256, downloadedSha256,
"downloaded package must be byte-identical to the uploaded zip");

// Redeploy overwrites in place (stable key), no stale package accumulates.
ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(baos2)) {
zos.putNextEntry(new ZipEntry("index.js"));
zos.write("exports.handler = async () => ({ statusCode: 201 });\n".getBytes());
zos.closeEntry();
}
given()
.contentType("application/json")
.body("{ \"ZipFile\": \"%s\" }".formatted(Base64.getEncoder().encodeToString(baos2.toByteArray())))
.when()
.put(BASE_PATH + "/functions/code-dl-fn/code")
.then()
.statusCode(200);
byte[] pkg2 = given().when().get(path).then().statusCode(200).extract().asByteArray();
org.junit.jupiter.api.Assertions.assertFalse(java.util.Arrays.equals(pkg, pkg2),
"redeploy must replace the stored package");

// Deleting the function removes the stored package (Code.Location 404s).
given()
.when()
.delete(BASE_PATH + "/functions/code-dl-fn")
.then()
.statusCode(anyOf(is(200), is(204)));
given()
.when()
.get(path)
.then()
.statusCode(404);
}
}