Skip to content

Commit

Permalink
feat(aws-lambda): Support ApiGatewayV1 and ALB integration in AWS lam…
Browse files Browse the repository at this point in the history
…bda. (#2497)

* feat: Manage multiple handlers for AWS Lambda

* feat: Support all type of AWS Lambda usage

* doc: Add lambda adapter doc

* Renaming to follow lint guidelines
  • Loading branch information
thomaspoignant authored Oct 11, 2024
1 parent 1087c3a commit ed39a6e
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 143 deletions.
43 changes: 33 additions & 10 deletions cmd/relayproxy/api/lambda_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,54 @@ package api

import (
"context"
"strings"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
echoadapter "github.com/awslabs/aws-lambda-go-api-proxy/echo"
"github.com/labstack/echo/v4"
)

// newAwsLambdaHandler is creating a new awsLambdaHandler struct with the echoadapter
// newAwsLambdaHandlerManager is creating a new awsLambdaHandler struct with the echoadapter
// to proxy all lambda event to echo.
func newAwsLambdaHandler(echoInstance *echo.Echo) awsLambdaHandler {
func newAwsLambdaHandlerManager(echoInstance *echo.Echo) awsLambdaHandler {
return awsLambdaHandler{
adapter: echoadapter.NewV2(echoInstance),
adapterAPIGtwV2: echoadapter.NewV2(echoInstance),
adapterALB: echoadapter.NewALB(echoInstance),
adapterAPIGtwV1: echoadapter.New(echoInstance),
}
}

type awsLambdaHandler struct {
adapter *echoadapter.EchoLambdaV2
adapterAPIGtwV2 *echoadapter.EchoLambdaV2
adapterAPIGtwV1 *echoadapter.EchoLambda
adapterALB *echoadapter.EchoLambdaALB
}

func (h *awsLambdaHandler) Start() {
lambda.Start(h.Handler)
func (h *awsLambdaHandler) GetAdapter(mode string) interface{} {
switch strings.ToUpper(mode) {
case "APIGATEWAYV1":
return h.HandlerAPIGatewayV1
case "ALB":
return h.HandlerALB
default:
return h.HandlerAPIGatewayV2
}
}

// Handler is the function that proxy the lambda events to echo calls.
func (h *awsLambdaHandler) Handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (
// HandlerAPIGatewayV2 is the function that proxy the lambda events to echo calls for API Gateway V2.
func (h *awsLambdaHandler) HandlerAPIGatewayV2(ctx context.Context, req events.APIGatewayV2HTTPRequest) (
events.APIGatewayV2HTTPResponse, error) {
return h.adapter.ProxyWithContext(ctx, req)
return h.adapterAPIGtwV2.ProxyWithContext(ctx, req)
}

// HandlerAPIGatewayV1 is the function that proxy the lambda events to echo calls for API Gateway V1.
func (h *awsLambdaHandler) HandlerAPIGatewayV1(ctx context.Context, req events.APIGatewayProxyRequest) (
events.APIGatewayProxyResponse, error) {
return h.adapterAPIGtwV1.ProxyWithContext(ctx, req)
}

// HandlerALB is the function that proxy the lambda events to echo calls for API Gateway V1.
func (h *awsLambdaHandler) HandlerALB(ctx context.Context, req events.ALBTargetGroupRequest) (
events.ALBTargetGroupResponse, error) {
return h.adapterALB.ProxyWithContext(ctx, req)
}
125 changes: 125 additions & 0 deletions cmd/relayproxy/api/lambda_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package api

import (
"context"
"encoding/json"
"strings"
"testing"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/stretchr/testify/require"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/metric"
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/service"
"github.com/thomaspoignant/go-feature-flag/notifier"
"go.uber.org/zap"
)

func TestAwsLambdaHandler_GetAdapter(t *testing.T) {
type test struct {
name string
mode string
request interface{}
}

tests := []test{
{
name: "APIGatewayV2 event handler",
mode: "APIGatewayV2",
request: events.APIGatewayV2HTTPRequest{
RequestContext: events.APIGatewayV2HTTPRequestContext{
HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{
Method: "GET",
Path: "/health",
},
},
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: "",
},
},
{
name: "APIGatewayV1 event handler",
mode: "APIGatewayV1",
request: events.APIGatewayProxyRequest{
HTTPMethod: "GET",
Path: "/health",
RequestContext: events.APIGatewayProxyRequestContext{
Path: "/health",
HTTPMethod: "GET",
},
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: "",
},
},
{
name: "ALB event handler",
mode: "ALB",
request: events.ALBTargetGroupRequest{
HTTPMethod: "GET",
Path: "/health",
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: "",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
z, err := zap.NewProduction()
require.NoError(t, err)
c := &config.Config{
StartAsAwsLambda: true,
AwsLambdaAdapter: tt.mode,
Retriever: &config.RetrieverConf{
Kind: "file",
Path: "../../../testdata/flag-config.yaml",
},
}
goff, err := service.NewGoFeatureFlagClient(c, z, []notifier.Notifier{})
require.NoError(t, err)
apiServer := New(c, service.Services{
MonitoringService: service.NewMonitoring(goff),
WebsocketService: service.NewWebsocketService(),
GOFeatureFlagService: goff,
Metrics: metric.Metrics{},
}, z)

reqJSON, err := json.Marshal(tt.request)
require.NoError(t, err)

// Create a Lambda handler
handler := lambda.NewHandler(apiServer.getLambdaHandler())

// Invoke the handler with the mock event
response, err := handler.Invoke(context.Background(), reqJSON)
require.NoError(t, err)

switch strings.ToLower(tt.mode) {
case "apigatewayv2":
var res events.APIGatewayV2HTTPResponse
err = json.Unmarshal(response, &res)
require.NoError(t, err)
require.Equal(t, 200, res.StatusCode)
case "apigatewayv1":
var res events.APIGatewayProxyResponse
err = json.Unmarshal(response, &res)
require.NoError(t, err)
require.Equal(t, 200, res.StatusCode)
case "alb":
var res events.ALBTargetGroupResponse
err = json.Unmarshal(response, &res)
require.NoError(t, err)
require.Equal(t, 200, res.StatusCode)
default:
require.Fail(t, "not implemented")
}
})
}
}
9 changes: 7 additions & 2 deletions cmd/relayproxy/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"time"

"github.com/aws/aws-lambda-go/lambda"
"github.com/labstack/echo-contrib/echoprometheus"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
Expand Down Expand Up @@ -143,8 +144,12 @@ func (s *Server) Start() {

// StartAwsLambda is starting the relay proxy as an AWS Lambda
func (s *Server) StartAwsLambda() {
adapter := newAwsLambdaHandler(s.apiEcho)
adapter.Start()
lambda.Start(s.getLambdaHandler())
}

func (s *Server) getLambdaHandler() interface{} {
handlerMngr := newAwsLambdaHandlerManager(s.apiEcho)
return handlerMngr.GetAdapter(s.config.AwsLambdaAdapter)
}

// Stop shutdown the API server
Expand Down
5 changes: 5 additions & 0 deletions cmd/relayproxy/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ type Config struct {
// StartAsAwsLambda (optional) if true, the relay proxy will start ready to be launched as AWS Lambda
StartAsAwsLambda bool `mapstructure:"startAsAwsLambda" koanf:"startasawslambda"`

// AwsLambdaAdapter (optional) is the adapter to use when the relay proxy is started as an AWS Lambda.
// Possible values are "APIGatewayV1", "APIGatewayV2" and "ALB"
// Default: "APIGatewayV2"
AwsLambdaAdapter string `mapstructure:"awsLambdaAdapter" koanf:"awslambdaadapter"`

// EvaluationContextEnrichment (optional) will be merged with the evaluation context sent during the evaluation.
// It is useful to add common attributes to all the evaluations, such as a server version, environment, ...
//
Expand Down
Loading

0 comments on commit ed39a6e

Please sign in to comment.