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

246 lines
8.0 KiB
Go
Raw Normal View History

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2023-08-11 13:12:13 +00:00
// SPDX-License-Identifier: BUSL-1.1
package awslambda
import (
"errors"
"fmt"
arn_sdk "github.com/aws/aws-sdk-go/aws/arn"
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"
envoy_resource_v3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
"github.com/mitchellh/mapstructure"
pstruct "google.golang.org/protobuf/types/known/structpb"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
2023-01-06 17:13:40 +00:00
"github.com/hashicorp/go-multierror"
)
var _ extensioncommon.BasicExtension = (*awsLambda)(nil)
type awsLambda struct {
extensioncommon.BasicExtensionAdapter
2023-01-06 17:13:40 +00:00
ARN string
PayloadPassthrough bool
InvocationMode string
}
// Constructor follows a specific function signature required for the extension registration.
func Constructor(ext api.EnvoyExtension) (extensioncommon.EnvoyExtender, error) {
var a awsLambda
if name := ext.Name; name != api.BuiltinAWSLambdaExtension {
return nil, fmt.Errorf("expected extension name %q but got %q", api.BuiltinAWSLambdaExtension, name)
}
if err := a.fromArguments(ext.Arguments); err != nil {
return nil, err
}
return &extensioncommon.UpstreamEnvoyExtender{
Extension: &a,
}, nil
}
func (a *awsLambda) fromArguments(args map[string]interface{}) error {
if err := mapstructure.Decode(args, a); err != nil {
return fmt.Errorf("error decoding extension arguments: %v", err)
}
return a.validate()
}
func (a *awsLambda) validate() error {
var resultErr error
if a.ARN == "" {
resultErr = multierror.Append(resultErr, fmt.Errorf("ARN is required"))
}
return resultErr
}
// CanApply returns true if the kind of the provided ExtensionConfiguration matches
// the kind of the lambda configuration
func (a *awsLambda) CanApply(config *extensioncommon.RuntimeConfig) bool {
return config.Kind == config.UpstreamOutgoingProxyKind()
}
// PatchRoute modifies the routing configuration for a service of kind TerminatingGateway. If the kind is
// not TerminatingGateway, then it can not be modified.
func (a *awsLambda) PatchRoute(p extensioncommon.RoutePayload) (*envoy_route_v3.RouteConfiguration, bool, error) {
cfg := p.RuntimeConfig
if cfg.Kind != api.ServiceKindTerminatingGateway {
return p.Message, false, nil
}
// Only patch outbound routes.
if p.IsInbound() {
return p.Message, false, nil
}
route := p.Message
for _, virtualHost := range route.VirtualHosts {
for _, route := range virtualHost.Routes {
action, ok := route.Action.(*envoy_route_v3.Route_Route)
if !ok {
continue
}
// When auto_host_rewrite is set it conflicts with strip_any_host_port
// on the http_connection_manager filter, which is required to be true to support
// lambda functions. See the patch filter method for more details.
action.Route.HostRewriteSpecifier = nil
}
}
return route, true, nil
}
// PatchCluster patches the provided envoy cluster with data required to support an AWS lambda function
func (a *awsLambda) PatchCluster(p extensioncommon.ClusterPayload) (*envoy_cluster_v3.Cluster, bool, error) {
// Only patch outbound clusters.
if p.IsInbound() {
return p.Message, false, nil
}
2023-04-06 21:12:07 +00:00
transportSocket, err := extensioncommon.MakeUpstreamTLSTransportSocket(&envoy_tls_v3.UpstreamTlsContext{
Sni: "*.amazonaws.com",
})
c := p.Message
if err != nil {
return c, false, fmt.Errorf("failed to make transport socket: %w", err)
}
// Use the aws SDK to parse the ARN so that we can later extract the region
parsedARN, err := arn_sdk.Parse(a.ARN)
if err != nil {
return c, false, err
}
cluster := &envoy_cluster_v3.Cluster{
Name: c.Name,
ConnectTimeout: c.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: c.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", parsedARN.Region),
PortSpecifier: &envoy_core_v3.SocketAddress_PortValue{
PortValue: 443,
},
},
},
},
},
},
},
},
},
},
},
TransportSocket: transportSocket,
}
return cluster, true, nil
}
// PatchFilter patches the provided envoy filter with an inserted lambda filter being careful not to
// overwrite the http filters.
func (a *awsLambda) PatchFilter(p extensioncommon.FilterPayload) (*envoy_listener_v3.Filter, bool, error) {
filter := p.Message
// Only patch outbound filters.
if p.IsInbound() {
return filter, false, nil
}
if filter.Name != "envoy.filters.network.http_connection_manager" {
return filter, false, nil
}
if typedConfig := filter.GetTypedConfig(); typedConfig == nil {
return filter, false, errors.New("error getting typed config for http filter")
}
config := envoy_resource_v3.GetHTTPConnectionManager(filter)
if config == nil {
return filter, false, errors.New("error unmarshalling filter")
}
2023-04-06 21:12:07 +00:00
lambdaHttpFilter, err := extensioncommon.MakeEnvoyHTTPFilter(
"envoy.filters.http.aws_lambda",
&envoy_lambda_v3.Config{
Arn: a.ARN,
PayloadPassthrough: a.PayloadPassthrough,
InvocationMode: toEnvoyInvocationMode(a.InvocationMode),
},
)
if err != nil {
return filter, false, err
}
// We need to be careful about overwriting http filters completely because
// http filters validates intentions with the RBAC filter. This inserts the
// lambda filter before `envoy.filters.http.router` while keeping everything
// else intact.
changedFilters := make([]*envoy_http_v3.HttpFilter, 0, len(config.HttpFilters)+1)
var changed bool
for _, httpFilter := range config.HttpFilters {
if httpFilter.Name == "envoy.filters.http.router" {
changedFilters = append(changedFilters, lambdaHttpFilter)
changed = true
}
changedFilters = append(changedFilters, httpFilter)
}
if changed {
config.HttpFilters = changedFilters
}
// StripPortMode must be set to true since all requests have to be signed using the AWS v4 signature and
// if the port is included in the request, it will be used in the signature calculation causing AWS to reject the
// Lambda HTTP request.
config.StripPortMode = &envoy_http_v3.HttpConnectionManager_StripAnyHostPort{
StripAnyHostPort: true,
}
2023-04-06 21:12:07 +00:00
newFilter, err := extensioncommon.MakeFilter("envoy.filters.network.http_connection_manager", config)
if err != nil {
return filter, false, errors.New("error making new filter")
}
return newFilter, true, nil
}
func toEnvoyInvocationMode(s string) envoy_lambda_v3.Config_InvocationMode {
m := envoy_lambda_v3.Config_SYNCHRONOUS
if s == "asynchronous" {
m = envoy_lambda_v3.Config_ASYNCHRONOUS
}
return m
}