Skip to content

Updated the repo to work with the application load balancer. #17

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.13.0")),
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", .upToNextMajor(from: "0.3.0")),
Copy link
Member

Choose a reason for hiding this comment

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

This needs to be changed back

.package(url: "https://github.com/skelpo/swift-aws-lambda-runtime.git", .upToNextMajor(from: "0.4.0")),
.package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "4.0.0")),
.package(url: "https://github.com/swift-extras/swift-extras-base64", .upToNextMajor(from: "0.4.0")),
],
Expand Down
159 changes: 159 additions & 0 deletions Sources/VaporAWSLambdaRuntime/ALB.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//
// File.swift
//
//
// Created by Ralph Küpper on 1/5/21.
//

import AWSLambdaEvents
import AWSLambdaRuntimeCore
import ExtrasBase64
import NIO
import NIOHTTP1
import Vapor

// MARK: - Handler -

struct ALBHandler: EventLoopLambdaHandler {

typealias In = ALB.TargetGroupRequest
typealias Out = ALB.TargetGroupResponse

private let application: Application
private let responder: Responder

init(application: Application, responder: Responder) {
self.application = application
print("responder: ", responder)
self.responder = responder
}

public func handle(context: Lambda.Context, event: ALB.TargetGroupRequest)
-> EventLoopFuture<ALB.TargetGroupResponse>
{
let vaporRequest: Vapor.Request
do {
vaporRequest = try Vapor.Request(req: event, in: context, for: self.application)
} catch {
return context.eventLoop.makeFailedFuture(error)
}

return self.responder.respond(to: vaporRequest).flatMap { ALB.TargetGroupResponse.from(response: $0, in: context) }
}
}

// MARK: - Request -

extension Vapor.Request {
private static let bufferAllocator = ByteBufferAllocator()

convenience init(req: ALB.TargetGroupRequest, in ctx: Lambda.Context, for application: Application) throws {
var buffer: NIO.ByteBuffer?
switch (req.body, req.isBase64Encoded) {
case (let .some(string), true):
let bytes = try string.base64decoded()
buffer = Vapor.Request.bufferAllocator.buffer(capacity: bytes.count)
buffer!.writeBytes(bytes)

case (let .some(string), false):
buffer = Vapor.Request.bufferAllocator.buffer(capacity: string.utf8.count)
buffer!.writeString(string)

case (.none, _):
break
}

var nioHeaders = NIOHTTP1.HTTPHeaders()
req.headers?.forEach { key, value in
nioHeaders.add(name: key, value: value)
}

/*if let cookies = req., cookies.count > 0 {
Copy link
Member

Choose a reason for hiding this comment

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

Lets remove this

nioHeaders.add(name: "Cookie", value: cookies.joined(separator: "; "))
}*/

var url: String = req.path
if req.queryStringParameters.count > 0 {
url += "?"
for key in req.queryStringParameters.keys {
// It leaves an ampersand (&) at the end, but who cares?
url += key + "=" + (req.queryStringParameters[key] ?? "") + "&"
}
}

ctx.logger.debug("The constructed URL is: \(url)")
Copy link
Member

Choose a reason for hiding this comment

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

I'd prefer this be at trace level since it's coming from the library - See the log level PR for what we're trying to move towards


self.init(
application: application,
method: NIOHTTP1.HTTPMethod(rawValue: req.httpMethod.rawValue),
url: Vapor.URI(path: url),
version: HTTPVersion(major: 1, minor: 1),
headers: nioHeaders,
collectedBody: buffer,
remoteAddress: nil,
logger: ctx.logger,
on: ctx.eventLoop
)

storage[ALB.TargetGroupRequest] = req
}
}

extension ALB.TargetGroupRequest: Vapor.StorageKey {
public typealias Value = ALB.TargetGroupRequest
}

// MARK: - Response -

extension ALB.TargetGroupResponse {
static func from(response: Vapor.Response, in context: Lambda.Context) -> EventLoopFuture<ALB.TargetGroupResponse> {
// Create the headers
var headers = [String: String]()
response.headers.forEach { name, value in
if let current = headers[name] {
headers[name] = "\(current),\(value)"
} else {
headers[name] = value
}
}

// Can we access the body right away?
if let string = response.body.string {
return context.eventLoop.makeSucceededFuture(.init(
statusCode: AWSLambdaEvents.HTTPResponseStatus(code: response.status.code),
headers: headers,
body: string,
isBase64Encoded: false
))
} else if let bytes = response.body.data {
return context.eventLoop.makeSucceededFuture(.init(
statusCode: AWSLambdaEvents.HTTPResponseStatus(code: response.status.code),
headers: headers,
body: String(base64Encoding: bytes),
isBase64Encoded: true
))
} else {
// See if it is a stream and try to gather the data
return response.body.collect(on: context.eventLoop).map { (buffer) -> ALB.TargetGroupResponse in
// Was there any content
guard
var buffer = buffer,
let bytes = buffer.readBytes(length: buffer.readableBytes)
else {
return ALB.TargetGroupResponse(
statusCode: AWSLambdaEvents.HTTPResponseStatus(code: response.status.code),
headers: headers
)
}

// Done
return ALB.TargetGroupResponse(
statusCode: AWSLambdaEvents.HTTPResponseStatus(code: response.status.code),
headers: headers,
body: String(base64Encoding: bytes),
isBase64Encoded: true
)
}
}
}
}
10 changes: 6 additions & 4 deletions Sources/VaporAWSLambdaRuntime/LambdaServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ public extension Application.Lambda {
}
}

struct ConfigurationKey: StorageKey {
typealias Value = LambdaServer.Configuration
public struct ConfigurationKey: StorageKey {
public typealias Value = LambdaServer.Configuration
}
}
}
Expand All @@ -79,13 +79,13 @@ public class LambdaServer: Server {
public enum RequestSource {
case apiGateway
case apiGatewayV2
// case applicationLoadBalancer // not in this release
case applicationLoadBalancer
Copy link
Member

Choose a reason for hiding this comment

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

🙌

}

var requestSource: RequestSource
var logger: Logger

init(apiService: RequestSource = .apiGatewayV2, logger: Logger) {
public init(apiService: RequestSource = .apiGatewayV2, logger: Logger) {
self.requestSource = apiService
self.logger = logger
}
Expand Down Expand Up @@ -115,6 +115,8 @@ public class LambdaServer: Server {
handler = APIGatewayHandler(application: application, responder: responder)
case .apiGatewayV2:
handler = APIGatewayV2Handler(application: application, responder: responder)
case .applicationLoadBalancer:
handler = ALBHandler(application: application, responder: responder)
}

self.lambdaLifecycle = Lambda.Lifecycle(
Expand Down
80 changes: 80 additions & 0 deletions Tests/VaporAWSLambdaRuntimeTests/ALB.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import AWSLambdaEvents
@testable import AWSLambdaRuntimeCore
import Logging
import NIO
import Vapor
@testable import VaporAWSLambdaRuntime
import XCTest

final class ALBTests: XCTestCase {
func testALBRequest() throws {
let requestdata = """
{
"requestContext": {
"elb": {
"targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a"
}
},
"httpMethod": "GET",
"path": "/lambda",
"queryStringParameters": {
"query": "1234ABCD"
},
"headers": {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"accept-encoding": "gzip",
"accept-language": "en-US,en;q=0.9",
"connection": "keep-alive",
"host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
"x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476",
"x-forwarded-for": "72.12.164.125",
"x-forwarded-port": "80",
"x-forwarded-proto": "http",
"x-imforwards": "20"
},
"body": "",
"isBase64Encoded": false
}
"""
let decoder = JSONDecoder()
let request = try decoder.decode(ALB.TargetGroupRequest.self, from: requestdata.data(using: .utf8)!)
print("F: ", request)
}

func testCreateALBResponse() {
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) }
let eventLoop = eventLoopGroup.next()
let allocator = ByteBufferAllocator()
let logger = Logger(label: "test")

let body = #"{"hello": "world"}"#
let vaporResponse = Vapor.Response(
status: .ok,
headers: HTTPHeaders([
("Content-Type", "application/json"),
]),
body: .init(string: body)
)

let context = Lambda.Context(
requestID: "abc123",
traceID: AmazonHeaders.generateXRayTraceID(),
invokedFunctionARN: "function-arn",
deadline: .now() + .seconds(3),
logger: logger,
eventLoop: eventLoop,
allocator: allocator
)

var response: ALB.TargetGroupResponse?
XCTAssertNoThrow(response = try ALB.TargetGroupResponse.from(response: vaporResponse, in: context).wait())

XCTAssertEqual(response?.body, body)
XCTAssertEqual(response?.headers?.count, 2)
XCTAssertEqual(response?.headers?["Content-Type"], "application/json")
XCTAssertEqual(response?.headers?["content-length"], String(body.count))
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
Copy link
Member

Choose a reason for hiding this comment

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

Again remove

<Scheme
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Hello"
BuildableName = "Hello"
BlueprintName = "Hello"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Hello"
BuildableName = "Hello"
BlueprintName = "Hello"
ReferencedContainer = "container:">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "LOCAL_LAMBDA_SERVER_ENABLED"
value = "true"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "LOG_LEVEL"
value = "debug"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Hello"
BuildableName = "Hello"
BlueprintName = "Hello"
ReferencedContainer = "container:">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
Loading