consul/agent/envoyextensions/builtin/aws-lambda/aws_lambda_test.go

483 lines
14 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package awslambda
import (
"fmt"
"testing"
envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
envoy_lambda_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/aws_lambda/v3"
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
pstruct "google.golang.org/protobuf/types/known/structpb"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
"github.com/hashicorp/consul/proto/private/prototest"
)
func TestConstructor(t *testing.T) {
kind := api.ServiceKindTerminatingGateway
cases := map[string]struct {
extensionName string
arn string
payloadPassthrough bool
region string
expected awsLambda
ok bool
}{
"no arguments": {
ok: false,
},
"a bad name": {
arn: "arn",
region: "blah",
extensionName: "bad",
ok: false,
},
"missing arn": {
region: "blah",
ok: false,
},
"including payload passthrough": {
arn: "arn",
region: "blah",
payloadPassthrough: true,
expected: awsLambda{
ARN: "arn",
PayloadPassthrough: true,
},
ok: true,
},
}
for n, tc := range cases {
t.Run(n, func(t *testing.T) {
extensionName := api.BuiltinAWSLambdaExtension
if tc.extensionName != "" {
extensionName = tc.extensionName
}
svc := api.CompoundServiceName{Name: "svc"}
ext := extensioncommon.RuntimeConfig{
ServiceName: svc,
Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
svc: {OutgoingProxyKind: kind},
},
EnvoyExtension: api.EnvoyExtension{
Name: extensionName,
Arguments: map[string]interface{}{
"ARN": tc.arn,
"PayloadPassthrough": tc.payloadPassthrough,
},
},
}
e, err := Constructor(ext.EnvoyExtension)
if tc.ok {
require.NoError(t, err)
require.Equal(t, &extensioncommon.UpstreamEnvoyExtender{Extension: &tc.expected}, e)
} else {
require.Error(t, err)
}
})
}
}
func TestCanApply(t *testing.T) {
a := awsLambda{}
require.False(t, a.CanApply(&extensioncommon.RuntimeConfig{
Kind: api.ServiceKindConnectProxy,
ServiceName: api.CompoundServiceName{Name: "s1"},
Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
{Name: "s1"}: {
OutgoingProxyKind: api.ServiceKindTerminatingGateway,
},
},
}))
require.True(t, a.CanApply(&extensioncommon.RuntimeConfig{
Kind: api.ServiceKindConnectProxy,
ServiceName: api.CompoundServiceName{Name: "s1"},
Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
{Name: "s1"}: {
OutgoingProxyKind: api.ServiceKindConnectProxy,
},
},
}))
}
func TestPatchCluster(t *testing.T) {
cases := []struct {
name string
lambda awsLambda
input *envoy_cluster_v3.Cluster
expectedRegion string
isErrExpected bool
}{
{
name: "nominal",
input: &envoy_cluster_v3.Cluster{
Name: "test-cluster",
},
lambda: awsLambda{
ARN: "arn:aws:lambda:us-east-1:111111111111:function:lambda-1234",
PayloadPassthrough: true,
InvocationMode: "Asynchronous",
},
expectedRegion: "us-east-1",
},
{
name: "error invalid arn",
input: &envoy_cluster_v3.Cluster{
Name: "test-cluster",
},
lambda: awsLambda{
ARN: "?!@%^SA",
PayloadPassthrough: true,
InvocationMode: "Asynchronous",
},
isErrExpected: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
transportSocket, err := extensioncommon.MakeUpstreamTLSTransportSocket(&envoy_tls_v3.UpstreamTlsContext{
Sni: "*.amazonaws.com",
})
require.NoError(t, err)
expectedCluster := &envoy_cluster_v3.Cluster{
Name: tc.input.Name,
ConnectTimeout: tc.input.ConnectTimeout,
ClusterDiscoveryType: &envoy_cluster_v3.Cluster_Type{Type: envoy_cluster_v3.Cluster_LOGICAL_DNS},
DnsLookupFamily: envoy_cluster_v3.Cluster_V4_ONLY,
LbPolicy: envoy_cluster_v3.Cluster_ROUND_ROBIN,
Metadata: &envoy_core_v3.Metadata{
FilterMetadata: map[string]*pstruct.Struct{
"com.amazonaws.lambda": {
Fields: map[string]*pstruct.Value{
"egress_gateway": {Kind: &pstruct.Value_BoolValue{BoolValue: true}},
},
},
},
},
LoadAssignment: &envoy_endpoint_v3.ClusterLoadAssignment{
ClusterName: tc.input.Name,
Endpoints: []*envoy_endpoint_v3.LocalityLbEndpoints{
{
LbEndpoints: []*envoy_endpoint_v3.LbEndpoint{
{
HostIdentifier: &envoy_endpoint_v3.LbEndpoint_Endpoint{
Endpoint: &envoy_endpoint_v3.Endpoint{
Address: &envoy_core_v3.Address{
Address: &envoy_core_v3.Address_SocketAddress{
SocketAddress: &envoy_core_v3.SocketAddress{
Address: fmt.Sprintf("lambda.%s.amazonaws.com", tc.expectedRegion),
PortSpecifier: &envoy_core_v3.SocketAddress_PortValue{
PortValue: 443,
},
},
},
},
},
},
},
},
},
},
},
TransportSocket: transportSocket,
}
// Test patching the cluster
rc := extensioncommon.RuntimeConfig{}
patchedCluster, patchSuccess, err := tc.lambda.PatchCluster(extensioncommon.ClusterPayload{
RuntimeConfig: &rc,
Message: tc.input,
})
if tc.isErrExpected {
assert.Error(t, err)
assert.False(t, patchSuccess)
} else {
assert.NoError(t, err)
assert.True(t, patchSuccess)
assert.Equal(t, expectedCluster, patchedCluster)
}
})
}
}
func TestPatchRoute(t *testing.T) {
tests := map[string]struct {
conf *extensioncommon.RuntimeConfig
route *envoy_route_v3.RouteConfiguration
expectRoute *envoy_route_v3.RouteConfiguration
expectBool bool
}{
"non terminating gateway unmodified": {
conf: &extensioncommon.RuntimeConfig{
Kind: api.ServiceKindConnectProxy,
},
route: &envoy_route_v3.RouteConfiguration{
VirtualHosts: []*envoy_route_v3.VirtualHost{
{
Routes: []*envoy_route_v3.Route{
{
Action: &envoy_route_v3.Route_Route{
Route: &envoy_route_v3.RouteAction{
HostRewriteSpecifier: &envoy_route_v3.RouteAction_HostRewriteLiteral{},
},
},
},
},
},
},
},
expectRoute: &envoy_route_v3.RouteConfiguration{
VirtualHosts: []*envoy_route_v3.VirtualHost{
{
Routes: []*envoy_route_v3.Route{
{
Action: &envoy_route_v3.Route_Route{
Route: &envoy_route_v3.RouteAction{
HostRewriteSpecifier: &envoy_route_v3.RouteAction_HostRewriteLiteral{},
},
},
},
},
},
},
},
expectBool: false,
},
"terminating gateway modified": {
conf: &extensioncommon.RuntimeConfig{
Kind: api.ServiceKindTerminatingGateway,
},
route: &envoy_route_v3.RouteConfiguration{
VirtualHosts: []*envoy_route_v3.VirtualHost{
{
Routes: []*envoy_route_v3.Route{
// This should be modified.
{
Action: &envoy_route_v3.Route_Route{
Route: &envoy_route_v3.RouteAction{
HostRewriteSpecifier: &envoy_route_v3.RouteAction_HostRewriteLiteral{},
},
},
},
// This should be not be modified.
{
Action: &envoy_route_v3.Route_DirectResponse{},
},
},
},
},
},
expectRoute: &envoy_route_v3.RouteConfiguration{
VirtualHosts: []*envoy_route_v3.VirtualHost{
{
Routes: []*envoy_route_v3.Route{
{
Action: &envoy_route_v3.Route_Route{
Route: &envoy_route_v3.RouteAction{
HostRewriteSpecifier: nil,
},
},
},
{
Action: &envoy_route_v3.Route_DirectResponse{},
},
},
},
},
},
expectBool: true,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
l := awsLambda{}
r, ok, err := l.PatchRoute(extensioncommon.RoutePayload{
RuntimeConfig: tc.conf,
Message: tc.route,
})
require.NoError(t, err)
require.Equal(t, tc.expectRoute, r)
require.Equal(t, tc.expectBool, ok)
})
}
}
func TestPatchFilter(t *testing.T) {
makeAny := func(m proto.Message) *anypb.Any {
v, err := anypb.New(m)
require.NoError(t, err)
return v
}
tests := map[string]struct {
filter *envoy_listener_v3.Filter
isInboundFilter bool
expectFilter *envoy_listener_v3.Filter
expectBool bool
expectErr string
}{
"invalid filter name is ignored": {
filter: &envoy_listener_v3.Filter{Name: "something"},
expectFilter: &envoy_listener_v3.Filter{Name: "something"},
expectBool: false,
},
"error getting typed config": {
filter: &envoy_listener_v3.Filter{Name: "envoy.filters.network.http_connection_manager"},
expectFilter: &envoy_listener_v3.Filter{Name: "envoy.filters.network.http_connection_manager"},
expectBool: false,
expectErr: "error getting typed config for http filter",
},
"error getting http connection manager": {
filter: &envoy_listener_v3.Filter{
Name: "envoy.filters.network.http_connection_manager",
ConfigType: &envoy_listener_v3.Filter_TypedConfig{
TypedConfig: &anypb.Any{},
},
},
expectFilter: &envoy_listener_v3.Filter{
Name: "envoy.filters.network.http_connection_manager",
ConfigType: &envoy_listener_v3.Filter_TypedConfig{
TypedConfig: &anypb.Any{},
},
},
expectBool: false,
expectErr: "error unmarshalling filter",
},
"StripAnyHostPort is set": {
filter: &envoy_listener_v3.Filter{
Name: "envoy.filters.network.http_connection_manager",
ConfigType: &envoy_listener_v3.Filter_TypedConfig{
TypedConfig: makeAny(&envoy_http_v3.HttpConnectionManager{}),
},
},
expectFilter: &envoy_listener_v3.Filter{
Name: "envoy.filters.network.http_connection_manager",
ConfigType: &envoy_listener_v3.Filter_TypedConfig{
TypedConfig: makeAny(&envoy_http_v3.HttpConnectionManager{
StripPortMode: &envoy_http_v3.HttpConnectionManager_StripAnyHostPort{
StripAnyHostPort: true,
},
}),
},
},
expectBool: true,
},
"lambda filter injected correctly": {
filter: &envoy_listener_v3.Filter{
Name: "envoy.filters.network.http_connection_manager",
ConfigType: &envoy_listener_v3.Filter_TypedConfig{
TypedConfig: makeAny(&envoy_http_v3.HttpConnectionManager{
HttpFilters: []*envoy_http_v3.HttpFilter{
{Name: "one"},
{Name: "two"},
{Name: "envoy.filters.http.router"},
{Name: "three"},
},
}),
},
},
expectFilter: &envoy_listener_v3.Filter{
Name: "envoy.filters.network.http_connection_manager",
ConfigType: &envoy_listener_v3.Filter_TypedConfig{
TypedConfig: makeAny(&envoy_http_v3.HttpConnectionManager{
StripPortMode: &envoy_http_v3.HttpConnectionManager_StripAnyHostPort{
StripAnyHostPort: true,
},
HttpFilters: []*envoy_http_v3.HttpFilter{
{Name: "one"},
{Name: "two"},
{
Name: "envoy.filters.http.aws_lambda",
ConfigType: &envoy_http_v3.HttpFilter_TypedConfig{TypedConfig: makeAny(
&envoy_lambda_v3.Config{
Arn: "some-arn",
PayloadPassthrough: true,
InvocationMode: envoy_lambda_v3.Config_ASYNCHRONOUS,
},
)},
},
{Name: "envoy.filters.http.router"},
{Name: "three"},
},
}),
},
},
expectBool: true,
},
"inbound filter ignored": {
filter: &envoy_listener_v3.Filter{
Name: "envoy.filters.network.http_connection_manager",
ConfigType: &envoy_listener_v3.Filter_TypedConfig{
TypedConfig: makeAny(&envoy_http_v3.HttpConnectionManager{
HttpFilters: []*envoy_http_v3.HttpFilter{
{Name: "one"},
{Name: "two"},
{Name: "envoy.filters.http.router"},
{Name: "three"},
},
}),
},
},
expectFilter: &envoy_listener_v3.Filter{
Name: "envoy.filters.network.http_connection_manager",
ConfigType: &envoy_listener_v3.Filter_TypedConfig{
TypedConfig: makeAny(&envoy_http_v3.HttpConnectionManager{
HttpFilters: []*envoy_http_v3.HttpFilter{
{Name: "one"},
{Name: "two"},
{Name: "envoy.filters.http.router"},
{Name: "three"},
},
}),
},
},
isInboundFilter: true,
expectBool: false,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
l := awsLambda{
ARN: "some-arn",
PayloadPassthrough: true,
InvocationMode: "asynchronous",
}
d := extensioncommon.TrafficDirectionOutbound
if tc.isInboundFilter {
d = extensioncommon.TrafficDirectionInbound
}
f, ok, err := l.PatchFilter(extensioncommon.FilterPayload{
Message: tc.filter,
TrafficDirection: d,
})
require.Equal(t, tc.expectBool, ok)
if tc.expectErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tc.expectErr)
}
prototest.AssertDeepEqual(t, tc.expectFilter, f)
})
}
}