extensions: refactor serverless plugin to use extensions from config entry fields (#15817)

docs: update config entry docs and the Lambda manual registration docs

Co-authored-by: Nitya Dhanushkodi <nitya@hashicorp.com>
Co-authored-by: Eric <eric@haberkorn.co>
This commit is contained in:
Nitya Dhanushkodi 2022-12-19 10:19:37 -10:00 committed by GitHub
parent 87485d05bd
commit 8386bf19bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1557 additions and 978 deletions

3
.changelog/15817.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:breaking-change
extensions: Refactor Lambda integration to get configured with the Envoy extensions field on service-defaults configuration entries.
```

View File

@ -37,6 +37,7 @@ func ComputeResolvedServiceConfig(
thisReply.TransparentProxy = proxyConf.TransparentProxy
thisReply.MeshGateway = proxyConf.MeshGateway
thisReply.Expose = proxyConf.Expose
thisReply.EnvoyExtensions = proxyConf.EnvoyExtensions
// Extract the global protocol from proxyConf for upstream configs.
rawProtocol := proxyConf.Config["protocol"]
@ -102,6 +103,9 @@ func ComputeResolvedServiceConfig(
}
thisReply.Meta = serviceConf.Meta
// Service defaults' envoy extensions are appended to the proxy defaults extensions so that proxy defaults
// extensions are applied first.
thisReply.EnvoyExtensions = append(thisReply.EnvoyExtensions, serviceConf.EnvoyExtensions...)
}
// First collect all upstreams into a set of seen upstreams.

View File

@ -333,6 +333,82 @@ func Test_ComputeResolvedServiceConfig(t *testing.T) {
},
},
},
{
name: "servicedefaults envoy extension",
args: args{
scReq: &structs.ServiceConfigRequest{
Name: "sid",
},
entries: &ResolvedServiceConfigSet{
ServiceDefaults: map[structs.ServiceID]*structs.ServiceConfigEntry{
sid: {
EnvoyExtensions: []structs.EnvoyExtension{
{
Name: "sd-ext",
Required: false,
Arguments: map[string]interface{}{"arg": "val"},
},
},
},
},
},
},
want: &structs.ServiceConfigResponse{
EnvoyExtensions: []structs.EnvoyExtension{
{
Name: "sd-ext",
Required: false,
Arguments: map[string]interface{}{"arg": "val"},
},
},
},
},
{
name: "proxydefaults envoy extension appended to servicedefaults extension",
args: args{
scReq: &structs.ServiceConfigRequest{
Name: "sid",
},
entries: &ResolvedServiceConfigSet{
ProxyDefaults: map[string]*structs.ProxyConfigEntry{
acl.DefaultEnterpriseMeta().PartitionOrDefault(): {
EnvoyExtensions: []structs.EnvoyExtension{
{
Name: "pd-ext",
Required: false,
Arguments: map[string]interface{}{"arg": "val"},
},
},
},
},
ServiceDefaults: map[structs.ServiceID]*structs.ServiceConfigEntry{
sid: {
EnvoyExtensions: []structs.EnvoyExtension{
{
Name: "sd-ext",
Required: false,
Arguments: map[string]interface{}{"arg": "val"},
},
},
},
},
},
},
want: &structs.ServiceConfigResponse{
EnvoyExtensions: []structs.EnvoyExtension{
{
Name: "pd-ext",
Required: false,
Arguments: map[string]interface{}{"arg": "val"},
},
{
Name: "sd-ext",
Required: false,
Arguments: map[string]interface{}{"arg": "val"},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -168,6 +168,12 @@ type compiler struct {
// This is an OUTPUT field.
serviceMeta map[string]string
// envoyExtensions contains the Envoy Extensions configured through service defaults or proxy defaults config
// entries for this discovery chain.
//
// This is an OUTPUT field.
envoyExtensions []structs.EnvoyExtension
// startNode is computed inside of assembleChain()
//
// This is an OUTPUT field.
@ -338,6 +344,7 @@ func (c *compiler) compile() (*structs.CompiledDiscoveryChain, error) {
CustomizationHash: customizationHash,
Protocol: c.protocol,
ServiceMeta: c.serviceMeta,
EnvoyExtensions: c.envoyExtensions,
StartNode: c.startNode,
Nodes: c.nodes,
Targets: c.loadedTargets,
@ -555,9 +562,17 @@ func (c *compiler) assembleChain() error {
sid := structs.NewServiceID(c.serviceName, c.GetEnterpriseMeta())
// Extract the service meta for the service named by this discovery chain.
// Extract extensions from proxy defaults.
proxyDefaults := c.entries.GetProxyDefaults(c.GetEnterpriseMeta().PartitionOrDefault())
if proxyDefaults != nil {
c.envoyExtensions = proxyDefaults.EnvoyExtensions
}
// Extract the service meta for the service named by this discovery chain and add extensions from the service
// defaults.
if serviceDefault := c.entries.GetService(sid); serviceDefault != nil {
c.serviceMeta = serviceDefault.GetMeta()
c.envoyExtensions = append(c.envoyExtensions, serviceDefault.EnvoyExtensions...)
}
// Check for short circuit path.

View File

@ -56,6 +56,7 @@ func TestCompile(t *testing.T) {
"loadbalancer splitter and resolver": testcase_LBSplitterAndResolver(),
"loadbalancer resolver": testcase_LBResolver(),
"service redirect to service with default resolver is not a default chain": testcase_RedirectToDefaultResolverIsNotDefaultChain(),
"extensions": testcase_Extensions(),
"service meta projection": testcase_ServiceMetaProjection(),
"service meta projection with redirect": testcase_ServiceMetaProjectionWithRedirect(),
@ -1671,6 +1672,59 @@ func testcase_DefaultResolver_WithProxyDefaults() compileTestCase {
return compileTestCase{entries: entries, expect: expect}
}
func testcase_Extensions() compileTestCase {
entries := newEntries()
entries.AddProxyDefaults(&structs.ProxyConfigEntry{
Kind: structs.ProxyDefaults,
Name: structs.ProxyConfigGlobal,
EnvoyExtensions: []structs.EnvoyExtension{
{
Name: "ext1",
},
},
})
entries.AddServices(
&structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "main",
EnvoyExtensions: []structs.EnvoyExtension{
{
Name: "ext2",
},
},
},
)
expect := &structs.CompiledDiscoveryChain{
Protocol: "tcp",
Default: true,
EnvoyExtensions: []structs.EnvoyExtension{
{
Name: "ext1",
},
{
Name: "ext2",
},
},
StartNode: "resolver:main.default.default.dc1",
Nodes: map[string]*structs.DiscoveryGraphNode{
"resolver:main.default.default.dc1": {
Type: structs.DiscoveryGraphNodeTypeResolver,
Name: "main.default.default.dc1",
Resolver: &structs.DiscoveryResolver{
Default: true,
ConnectTimeout: 5 * time.Second,
Target: "main.default.default.dc1",
},
},
},
Targets: map[string]*structs.DiscoveryTarget{
"main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil),
},
}
return compileTestCase{entries: entries, expect: expect}
}
func testcase_ServiceMetaProjection() compileTestCase {
entries := newEntries()
entries.AddServices(

View File

@ -950,11 +950,15 @@ func TestConfigSnapshotTerminatingGatewayWithLambdaService(t testing.T, extraUpd
CorrelationID: serviceConfigIDPrefix + web.String(),
Result: &structs.ServiceConfigResponse{
ProxyConfig: map[string]interface{}{"protocol": "http"},
Meta: map[string]string{
"serverless.consul.hashicorp.com/v1alpha1/lambda/enabled": "true",
"serverless.consul.hashicorp.com/v1alpha1/lambda/arn": "lambda-arn",
"serverless.consul.hashicorp.com/v1alpha1/lambda/payload-passthrough": "true",
"serverless.consul.hashicorp.com/v1alpha1/lambda/region": "us-east-1",
EnvoyExtensions: []structs.EnvoyExtension{
{
Name: "builtin/aws/lambda",
Arguments: map[string]interface{}{
"ARN": "lambda-arn",
"PayloadPassthrough": true,
"Region": "us-east-1",
},
},
},
},
})

View File

@ -115,6 +115,7 @@ type ServiceConfigEntry struct {
LocalConnectTimeoutMs int `json:",omitempty" alias:"local_connect_timeout_ms"`
LocalRequestTimeoutMs int `json:",omitempty" alias:"local_request_timeout_ms"`
BalanceInboundConnections string `json:",omitempty" alias:"balance_inbound_connections"`
EnvoyExtensions []EnvoyExtension `json:",omitempty" alias:"envoy_extensions"`
Meta map[string]string `json:",omitempty"`
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
@ -235,6 +236,10 @@ func (e *ServiceConfigEntry) Validate() error {
}
}
if err := validateEnvoyExtensions(e.EnvoyExtensions); err != nil {
validationErr = multierror.Append(validationErr, err)
}
return validationErr
}
@ -282,6 +287,38 @@ func (e *ServiceConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta {
return &e.EnterpriseMeta
}
// EnvoyExtension has configuration for an extension that patches Envoy resources.
type EnvoyExtension struct {
Name string
Required bool
Arguments map[string]interface{}
}
func builtInExtension(name string) bool {
extensions := map[string]struct{}{
"builtin/aws/lambda": {},
}
_, ok := extensions[name]
return ok
}
func validateEnvoyExtensions(extensions []EnvoyExtension) error {
var err error
for i, extension := range extensions {
if extension.Name == "" {
err = multierror.Append(err, fmt.Errorf("invalid EnvoyExtensions[%d]: Name is required", i))
}
if !builtInExtension(extension.Name) {
err = multierror.Append(err, fmt.Errorf("invalid EnvoyExtensions[%d]: Name %q is not a built-in extension", i, extension.Name))
}
}
return err
}
type UpstreamConfiguration struct {
// Overrides is a slice of per-service configuration. The name field is
// required.
@ -343,6 +380,7 @@ type ProxyConfigEntry struct {
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
Expose ExposeConfig `json:",omitempty"`
AccessLogs AccessLogsConfig `json:",omitempty" alias:"access_logs"`
EnvoyExtensions []EnvoyExtension `json:",omitempty" alias:"envoy_extensions"`
Meta map[string]string `json:",omitempty"`
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
@ -427,6 +465,10 @@ func (e *ProxyConfigEntry) Validate() error {
return err
}
if err := validateEnvoyExtensions(e.EnvoyExtensions); err != nil {
return err
}
return e.validateEnterpriseMeta()
}
@ -1127,6 +1169,7 @@ type ServiceConfigResponse struct {
Mode ProxyMode `json:",omitempty"`
Destination DestinationConfig `json:",omitempty"`
Meta map[string]string `json:",omitempty"`
EnvoyExtensions []EnvoyExtension `json:",omitempty"`
QueryMeta
}

View File

@ -2701,6 +2701,42 @@ func TestServiceConfigEntry(t *testing.T) {
},
validateErr: "invalid value for balance_outbound_connections",
},
"validate: invalid extension": {
entry: &ServiceConfigEntry{
Kind: ServiceDefaults,
Name: "external",
Protocol: "http",
EnvoyExtensions: []EnvoyExtension{
{},
},
},
validateErr: "invalid EnvoyExtensions[0]: Name is required",
},
"validate: invalid extension name": {
entry: &ServiceConfigEntry{
Kind: ServiceDefaults,
Name: "external",
Protocol: "http",
EnvoyExtensions: []EnvoyExtension{
{
Name: "not-a-builtin",
},
},
},
validateErr: `invalid EnvoyExtensions[0]: Name "not-a-builtin" is not a built-in extension`,
},
"validate: valid extension name": {
entry: &ServiceConfigEntry{
Kind: ServiceDefaults,
Name: "external",
Protocol: "http",
EnvoyExtensions: []EnvoyExtension{
{
Name: "builtin/aws/lambda",
},
},
},
},
}
testConfigEntryNormalizeAndValidate(t, cases)
}

View File

@ -38,6 +38,9 @@ type CompiledDiscoveryChain struct {
// entry for the service named ServiceName.
ServiceMeta map[string]string `json:",omitempty"`
// EnvoyExtensions has a list of configurations for an extension that patches Envoy resources.
EnvoyExtensions []EnvoyExtension `json:",omitempty"`
// StartNode is the first key into the Nodes map that should be followed
// when walking the discovery chain.
StartNode string `json:",omitempty"`

View File

@ -67,6 +67,18 @@ func (o *CompiledDiscoveryChain) DeepCopy() *CompiledDiscoveryChain {
cp.ServiceMeta[k2] = v2
}
}
if o.EnvoyExtensions != nil {
cp.EnvoyExtensions = make([]EnvoyExtension, len(o.EnvoyExtensions))
copy(cp.EnvoyExtensions, o.EnvoyExtensions)
for i2 := range o.EnvoyExtensions {
if o.EnvoyExtensions[i2].Arguments != nil {
cp.EnvoyExtensions[i2].Arguments = make(map[string]interface{}, len(o.EnvoyExtensions[i2].Arguments))
for k4, v4 := range o.EnvoyExtensions[i2].Arguments {
cp.EnvoyExtensions[i2].Arguments[k4] = v4
}
}
}
}
if o.Nodes != nil {
cp.Nodes = make(map[string]*DiscoveryGraphNode, len(o.Nodes))
for k2, v2 := range o.Nodes {
@ -540,6 +552,18 @@ func (o *ServiceConfigEntry) DeepCopy() *ServiceConfigEntry {
copy(cp.Destination.Addresses, o.Destination.Addresses)
}
}
if o.EnvoyExtensions != nil {
cp.EnvoyExtensions = make([]EnvoyExtension, len(o.EnvoyExtensions))
copy(cp.EnvoyExtensions, o.EnvoyExtensions)
for i2 := range o.EnvoyExtensions {
if o.EnvoyExtensions[i2].Arguments != nil {
cp.EnvoyExtensions[i2].Arguments = make(map[string]interface{}, len(o.EnvoyExtensions[i2].Arguments))
for k4, v4 := range o.EnvoyExtensions[i2].Arguments {
cp.EnvoyExtensions[i2].Arguments[k4] = v4
}
}
}
}
if o.Meta != nil {
cp.Meta = make(map[string]string, len(o.Meta))
for k2, v2 := range o.Meta {
@ -597,6 +621,18 @@ func (o *ServiceConfigResponse) DeepCopy() *ServiceConfigResponse {
cp.Meta[k2] = v2
}
}
if o.EnvoyExtensions != nil {
cp.EnvoyExtensions = make([]EnvoyExtension, len(o.EnvoyExtensions))
copy(cp.EnvoyExtensions, o.EnvoyExtensions)
for i2 := range o.EnvoyExtensions {
if o.EnvoyExtensions[i2].Arguments != nil {
cp.EnvoyExtensions[i2].Arguments = make(map[string]interface{}, len(o.EnvoyExtensions[i2].Arguments))
for k4, v4 := range o.EnvoyExtensions[i2].Arguments {
cp.EnvoyExtensions[i2].Arguments[k4] = v4
}
}
}
}
return &cp
}

View File

@ -29,9 +29,9 @@ func TestServerlessPluginFromSnapshot(t *testing.T) {
// Otherwise payload-passthrough=false and invocation-mode=synchronous.
// This is used to test all the permutations.
makeServiceDefaults := func(opposite bool) *structs.ServiceConfigEntry {
payloadPassthrough := "true"
payloadPassthrough := true
if opposite {
payloadPassthrough = "false"
payloadPassthrough = false
}
invocationMode := "synchronous"
@ -43,12 +43,16 @@ func TestServerlessPluginFromSnapshot(t *testing.T) {
Kind: structs.ServiceDefaults,
Name: "db",
Protocol: "http",
Meta: map[string]string{
"serverless.consul.hashicorp.com/v1alpha1/lambda/enabled": "true",
"serverless.consul.hashicorp.com/v1alpha1/lambda/arn": "lambda-arn",
"serverless.consul.hashicorp.com/v1alpha1/lambda/payload-passthrough": payloadPassthrough,
"serverless.consul.hashicorp.com/v1alpha1/lambda/invocation-mode": invocationMode,
"serverless.consul.hashicorp.com/v1alpha1/lambda/region": "us-east-1",
EnvoyExtensions: []structs.EnvoyExtension{
{
Name: "builtin/aws/lambda",
Arguments: map[string]interface{}{
"ARN": "lambda-arn",
"PayloadPassthrough": payloadPassthrough,
"InvocationMode": invocationMode,
"Region": "us-east-1",
},
},
},
}
}

View File

@ -14,73 +14,69 @@ import (
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"
pstruct "github.com/golang/protobuf/ptypes/struct"
"github.com/mitchellh/mapstructure"
"github.com/hashicorp/consul/agent/xds/xdscommon"
"github.com/hashicorp/consul/api"
)
const (
lambdaPrefix string = "serverless.consul.hashicorp.com/v1alpha1"
lambdaEnabledTag string = lambdaPrefix + "/lambda/enabled"
lambdaArnTag string = lambdaPrefix + "/lambda/arn"
lambdaPayloadPassthroughTag string = lambdaPrefix + "/lambda/payload-passthrough"
lambdaRegionTag string = lambdaPrefix + "/lambda/region"
lambdaInvocationMode string = lambdaPrefix + "/lambda/invocation-mode"
)
type lambdaPatcher struct {
arn string
payloadPassthrough bool
region string
kind api.ServiceKind
invocationMode envoy_lambda_v3.Config_InvocationMode
ARN string `mapstructure:"ARN"`
PayloadPassthrough bool `mapstructure:"PayloadPassthrough"`
Region string `mapstructure:"Region"`
Kind api.ServiceKind
InvocationMode string `mapstructure:"InvocationMode"`
}
var _ patcher = (*lambdaPatcher)(nil)
func makeLambdaPatcher(serviceConfig xdscommon.ServiceConfig) (patcher, bool) {
var patcher lambdaPatcher
if !isStringTrue(serviceConfig.Meta[lambdaEnabledTag]) {
// TODO this is a hack. We should iterate through the extensions outside of here
if len(serviceConfig.EnvoyExtensions) == 0 {
return nil, false
}
// TODO this is a hack. We should iterate through the extensions outside of here
extension := serviceConfig.EnvoyExtensions[0]
if extension.Name != "builtin/aws/lambda" {
return nil, false
}
// TODO this fails when types aren't encode properly. We need to check this earlier in the Validate RPC.
err := mapstructure.Decode(extension.Arguments, &patcher)
if err != nil {
return nil, false
}
if patcher.ARN == "" {
return nil, false
}
if patcher.Region == "" {
return nil, false
}
patcher.Kind = serviceConfig.Kind
return patcher, true
}
arn := serviceConfig.Meta[lambdaArnTag]
if arn == "" {
return patcher, false
func toEnvoyInvocationMode(s string) envoy_lambda_v3.Config_InvocationMode {
m := envoy_lambda_v3.Config_SYNCHRONOUS
if s == "asynchronous" {
m = envoy_lambda_v3.Config_ASYNCHRONOUS
}
region := serviceConfig.Meta[lambdaRegionTag]
if region == "" {
return patcher, false
}
payloadPassthrough := isStringTrue(serviceConfig.Meta[lambdaPayloadPassthroughTag])
invocationModeStr := serviceConfig.Meta[lambdaInvocationMode]
invocationMode := envoy_lambda_v3.Config_SYNCHRONOUS
if invocationModeStr == "asynchronous" {
invocationMode = envoy_lambda_v3.Config_ASYNCHRONOUS
}
return lambdaPatcher{
arn: arn,
payloadPassthrough: payloadPassthrough,
region: region,
kind: serviceConfig.Kind,
invocationMode: invocationMode,
}, true
}
func isStringTrue(v string) bool {
return v == "true"
return m
}
func (p lambdaPatcher) CanPatch(kind api.ServiceKind) bool {
return kind == p.kind
return kind == p.Kind
}
func (p lambdaPatcher) PatchRoute(route *envoy_route_v3.RouteConfiguration) (*envoy_route_v3.RouteConfiguration, bool, error) {
if p.kind != api.ServiceKindTerminatingGateway {
if p.Kind != api.ServiceKindTerminatingGateway {
return route, false, nil
}
@ -136,7 +132,7 @@ func (p lambdaPatcher) PatchCluster(c *envoy_cluster_v3.Cluster) (*envoy_cluster
Address: &envoy_core_v3.Address{
Address: &envoy_core_v3.Address_SocketAddress{
SocketAddress: &envoy_core_v3.SocketAddress{
Address: fmt.Sprintf("lambda.%s.amazonaws.com", p.region),
Address: fmt.Sprintf("lambda.%s.amazonaws.com", p.Region),
PortSpecifier: &envoy_core_v3.SocketAddress_PortValue{
PortValue: 443,
},
@ -170,9 +166,9 @@ func (p lambdaPatcher) PatchFilter(filter *envoy_listener_v3.Filter) (*envoy_lis
lambdaHttpFilter, err := makeEnvoyHTTPFilter(
"envoy.filters.http.aws_lambda",
&envoy_lambda_v3.Config{
Arn: p.arn,
PayloadPassthrough: p.payloadPassthrough,
InvocationMode: p.invocationMode,
Arn: p.ARN,
PayloadPassthrough: p.PayloadPassthrough,
InvocationMode: toEnvoyInvocationMode(p.InvocationMode),
},
)
if err != nil {

View File

@ -1,7 +1,6 @@
package serverlessplugin
import (
"strconv"
"testing"
"github.com/stretchr/testify/require"
@ -14,7 +13,6 @@ func TestMakeLambdaPatcher(t *testing.T) {
kind := api.ServiceKindTerminatingGateway
cases := []struct {
name string
enabled bool
arn string
payloadPassthrough bool
region string
@ -22,37 +20,29 @@ func TestMakeLambdaPatcher(t *testing.T) {
ok bool
}{
{
name: "no meta",
ok: true,
},
{
name: "lambda disabled",
enabled: false,
ok: true,
name: "no extension",
ok: false,
},
{
name: "missing arn",
enabled: true,
region: "blah",
ok: false,
},
{
name: "missing region",
enabled: true,
region: "arn",
arn: "arn",
ok: false,
},
{
name: "including payload passthrough",
enabled: true,
arn: "arn",
region: "blah",
payloadPassthrough: true,
expected: lambdaPatcher{
arn: "arn",
payloadPassthrough: true,
region: "blah",
kind: kind,
ARN: "arn",
PayloadPassthrough: true,
Region: "blah",
Kind: kind,
},
ok: true,
},
@ -62,22 +52,25 @@ func TestMakeLambdaPatcher(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
config := xdscommon.ServiceConfig{
Kind: kind,
Meta: map[string]string{
lambdaEnabledTag: strconv.FormatBool(tc.enabled),
lambdaArnTag: tc.arn,
lambdaRegionTag: tc.region,
EnvoyExtensions: []api.EnvoyExtension{
{
Name: "builtin/aws/lambda",
Arguments: map[string]interface{}{
"ARN": tc.arn,
"Region": tc.region,
"PayloadPassthrough": tc.payloadPassthrough,
},
},
},
}
if tc.payloadPassthrough {
config.Meta[lambdaPayloadPassthroughTag] = strconv.FormatBool(tc.payloadPassthrough)
}
patcher, ok := makeLambdaPatcher(config)
require.Equal(t, tc.ok, ok)
if tc.ok {
require.Equal(t, tc.expected, patcher)
}
})
}
}

View File

@ -36,10 +36,10 @@ func TestGetPatcherBySNI(t *testing.T) {
sni: "lambda-sni",
kind: api.ServiceKindTerminatingGateway,
expected: lambdaPatcher{
arn: "arn",
region: "region",
payloadPassthrough: false,
kind: api.ServiceKindTerminatingGateway,
ARN: "arn",
Region: "region",
PayloadPassthrough: false,
Kind: api.ServiceKindTerminatingGateway,
},
},
}
@ -74,24 +74,27 @@ func sampleConfig() xdscommon.PluginConfiguration {
ServiceConfigs: map[api.CompoundServiceName]xdscommon.ServiceConfig{
lambdaService: {
Kind: api.ServiceKindTerminatingGateway,
Meta: map[string]string{
lambdaEnabledTag: "true",
lambdaArnTag: "arn",
lambdaRegionTag: "region",
EnvoyExtensions: []api.EnvoyExtension{
{
Name: "builtin/aws/lambda",
Arguments: map[string]interface{}{
"ARN": "arn",
"Region": "region",
},
},
},
},
disabledLambdaService: {
Kind: api.ServiceKindTerminatingGateway,
Meta: map[string]string{
lambdaEnabledTag: "false",
lambdaArnTag: "arn",
lambdaRegionTag: "region",
},
// No extension.
},
invalidLambdaService: {
Kind: api.ServiceKindTerminatingGateway,
Meta: map[string]string{
lambdaEnabledTag: "true",
EnvoyExtensions: []api.EnvoyExtension{
{
Name: "builtin/aws/lambda",
Arguments: map[string]interface{}{}, // ARN, etc missing
},
},
},
},

View File

@ -57,7 +57,7 @@ type ServiceConfig struct {
// Kind identifies the final proxy kind that will make the request to the
// destination service.
Kind api.ServiceKind
Meta map[string]string
EnvoyExtensions []api.EnvoyExtension
}
// PluginConfiguration is passed into Envoy plugins. It should depend on the
@ -121,8 +121,8 @@ func MakePluginConfiguration(cfgSnap *proxycfg.ConfigSnapshot) PluginConfigurati
}
serviceConfigs[upstreamIDToCompoundServiceName(uid)] = ServiceConfig{
Meta: dc.ServiceMeta,
Kind: api.ServiceKindConnectProxy,
EnvoyExtensions: convertEnvoyExtensions(dc.EnvoyExtensions),
}
compoundServiceName := upstreamIDToCompoundServiceName(uid)
@ -135,7 +135,7 @@ func MakePluginConfiguration(cfgSnap *proxycfg.ConfigSnapshot) PluginConfigurati
for svc, c := range cfgSnap.TerminatingGateway.ServiceConfigs {
compoundServiceName := serviceNameToCompoundServiceName(svc)
serviceConfigs[compoundServiceName] = ServiceConfig{
Meta: c.Meta,
EnvoyExtensions: convertEnvoyExtensions(c.EnvoyExtensions),
Kind: api.ServiceKindTerminatingGateway,
}
@ -178,3 +178,17 @@ func upstreamIDToCompoundServiceName(uid proxycfg.UpstreamID) api.CompoundServic
Namespace: uid.NamespaceOrDefault(),
}
}
func convertEnvoyExtensions(structExtensions []structs.EnvoyExtension) []api.EnvoyExtension {
var extensions []api.EnvoyExtension
for _, e := range structExtensions {
extensions = append(extensions, api.EnvoyExtension{
Name: e.Name,
Required: e.Required,
Arguments: e.Arguments,
})
}
return extensions
}

View File

@ -42,11 +42,15 @@ func TestMakePluginConfiguration_TerminatingGateway(t *testing.T) {
ServiceConfigs: map[api.CompoundServiceName]ServiceConfig{
webService: {
Kind: api.ServiceKindTerminatingGateway,
Meta: map[string]string{
"serverless.consul.hashicorp.com/v1alpha1/lambda/enabled": "true",
"serverless.consul.hashicorp.com/v1alpha1/lambda/arn": "lambda-arn",
"serverless.consul.hashicorp.com/v1alpha1/lambda/payload-passthrough": "true",
"serverless.consul.hashicorp.com/v1alpha1/lambda/region": "us-east-1",
EnvoyExtensions: []api.EnvoyExtension{
{
Name: "builtin/aws/lambda",
Arguments: map[string]interface{}{
"ARN": "lambda-arn",
"PayloadPassthrough": true,
"Region": "us-east-1",
},
},
},
},
apiService: {
@ -84,17 +88,22 @@ func TestMakePluginConfiguration_ConnectProxy(t *testing.T) {
Partition: "default",
Namespace: "default",
}
lambdaMeta := map[string]string{
"serverless.consul.hashicorp.com/v1alpha1/lambda/enabled": "true",
"serverless.consul.hashicorp.com/v1alpha1/lambda/arn": "lambda-arn",
"serverless.consul.hashicorp.com/v1alpha1/lambda/payload-passthrough": "true",
"serverless.consul.hashicorp.com/v1alpha1/lambda/region": "us-east-1",
envoyExtensions := []structs.EnvoyExtension{
{
Name: "builtin/aws/lambda",
Arguments: map[string]interface{}{
"ARN": "lambda-arn",
"PayloadPassthrough": true,
"Region": "us-east-1",
},
},
}
serviceDefaults := &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: "db",
Protocol: "http",
Meta: lambdaMeta,
EnvoyExtensions: envoyExtensions,
}
snap := proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", nil, nil, serviceDefaults)
@ -103,7 +112,7 @@ func TestMakePluginConfiguration_ConnectProxy(t *testing.T) {
ServiceConfigs: map[api.CompoundServiceName]ServiceConfig{
dbService: {
Kind: api.ServiceKindConnectProxy,
Meta: lambdaMeta,
EnvoyExtensions: convertEnvoyExtensions(envoyExtensions),
},
},
SNIToServiceName: map[string]api.CompoundServiceName{

View File

@ -104,6 +104,13 @@ type ExposeConfig struct {
Paths []ExposePath `json:",omitempty"`
}
// EnvoyExtension has configuration for an extension that patches Envoy resources.
type EnvoyExtension struct {
Name string
Required bool
Arguments map[string]interface{}
}
type ExposePath struct {
// ListenerPort defines the port of the proxy's listener for exposed paths.
ListenerPort int `json:",omitempty" alias:"listener_port"`
@ -272,6 +279,7 @@ type ServiceConfigEntry struct {
LocalConnectTimeoutMs int `json:",omitempty" alias:"local_connect_timeout_ms"`
LocalRequestTimeoutMs int `json:",omitempty" alias:"local_request_timeout_ms"`
BalanceInboundConnections string `json:",omitempty" alias:"balance_inbound_connections"`
EnvoyExtensions []EnvoyExtension `json:",omitempty" alias:"envoy_extensions"`
Meta map[string]string `json:",omitempty"`
CreateIndex uint64
ModifyIndex uint64
@ -296,6 +304,7 @@ type ProxyConfigEntry struct {
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
Expose ExposeConfig `json:",omitempty"`
AccessLogs *AccessLogsConfig `json:",omitempty"`
EnvoyExtensions []EnvoyExtension `json:",omitempty" alias:"envoy_extensions"`
Meta map[string]string `json:",omitempty"`
CreateIndex uint64

View File

@ -34,6 +34,22 @@ func DestinationConfigFromStructs(t *structs.DestinationConfig, s *DestinationCo
s.Addresses = t.Addresses
s.Port = int32(t.Port)
}
func EnvoyExtensionToStructs(s *EnvoyExtension, t *structs.EnvoyExtension) {
if s == nil {
return
}
t.Name = s.Name
t.Required = s.Required
t.Arguments = envoyExtensionArgumentsToStructs(s.Arguments)
}
func EnvoyExtensionFromStructs(t *structs.EnvoyExtension, s *EnvoyExtension) {
if s == nil {
return
}
s.Name = t.Name
s.Required = t.Required
s.Arguments = envoyExtensionArgumentsFromStructs(t.Arguments)
}
func ExposeConfigToStructs(s *ExposeConfig, t *structs.ExposeConfig) {
if s == nil {
return
@ -706,6 +722,14 @@ func ServiceDefaultsToStructs(s *ServiceDefaults, t *structs.ServiceConfigEntry)
t.LocalConnectTimeoutMs = int(s.LocalConnectTimeoutMs)
t.LocalRequestTimeoutMs = int(s.LocalRequestTimeoutMs)
t.BalanceInboundConnections = s.BalanceInboundConnections
{
t.EnvoyExtensions = make([]structs.EnvoyExtension, len(s.EnvoyExtensions))
for i := range s.EnvoyExtensions {
if s.EnvoyExtensions[i] != nil {
EnvoyExtensionToStructs(s.EnvoyExtensions[i], &t.EnvoyExtensions[i])
}
}
}
t.Meta = s.Meta
}
func ServiceDefaultsFromStructs(t *structs.ServiceConfigEntry, s *ServiceDefaults) {
@ -744,6 +768,16 @@ func ServiceDefaultsFromStructs(t *structs.ServiceConfigEntry, s *ServiceDefault
s.LocalConnectTimeoutMs = int32(t.LocalConnectTimeoutMs)
s.LocalRequestTimeoutMs = int32(t.LocalRequestTimeoutMs)
s.BalanceInboundConnections = t.BalanceInboundConnections
{
s.EnvoyExtensions = make([]*EnvoyExtension, len(t.EnvoyExtensions))
for i := range t.EnvoyExtensions {
{
var x EnvoyExtension
EnvoyExtensionFromStructs(&t.EnvoyExtensions[i], &x)
s.EnvoyExtensions[i] = &x
}
}
}
s.Meta = t.Meta
}
func ServiceIntentionsToStructs(s *ServiceIntentions, t *structs.ServiceIntentionsConfigEntry) {

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/golang/protobuf/ptypes/timestamp"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/hashicorp/consul/acl"
@ -266,3 +267,14 @@ func meshGatewayModeToStructs(a MeshGatewayMode) structs.MeshGatewayMode {
return structs.MeshGatewayModeDefault
}
}
func envoyExtensionArgumentsToStructs(args *structpb.Value) map[string]interface{} {
return args.GetStructValue().AsMap()
}
func envoyExtensionArgumentsFromStructs(args map[string]interface{}) *structpb.Value {
if s, err := structpb.NewValue(args); err == nil {
return s
}
return nil
}

View File

@ -327,6 +327,16 @@ func (msg *TransparentProxyConfig) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *EnvoyExtension) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *EnvoyExtension) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *MeshGatewayConfig) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@ syntax = "proto3";
package hashicorp.consul.internal.configentry;
import "google/protobuf/duration.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";
import "proto/pbcommon/common.proto";
@ -433,6 +434,7 @@ message ServiceDefaults {
int32 LocalRequestTimeoutMs = 11;
string BalanceInboundConnections = 12;
map<string, string> Meta = 13;
repeated EnvoyExtension EnvoyExtensions = 14;
}
enum ProxyMode {
@ -452,6 +454,18 @@ message TransparentProxyConfig {
bool DialedDirectly = 2;
}
// mog annotation:
//
// target=github.com/hashicorp/consul/agent/structs.EnvoyExtension
// output=config_entry.gen.go
// name=Structs
message EnvoyExtension {
string Name = 1;
bool Required = 2;
// mog: func-to=envoyExtensionArgumentsToStructs func-from=envoyExtensionArgumentsFromStructs
google.protobuf.Value Arguments = 3;
}
// mog annotation:
//
// target=github.com/hashicorp/consul/agent/structs.MeshGatewayConfig

View File

@ -2,10 +2,14 @@
"Kind": "service-defaults",
"Name": "l1",
"Protocol": "http",
"Meta": {
"serverless.consul.hashicorp.com/v1alpha1/lambda/enabled": "true",
"serverless.consul.hashicorp.com/v1alpha1/lambda/region": "${AWS_LAMBDA_REGION}",
"serverless.consul.hashicorp.com/v1alpha1/lambda/arn": "${AWS_LAMBDA_ARN}",
"serverless.consul.hashicorp.com/v1alpha1/lambda/payload-passthrough": "true"
"EnvoyExtensions": [
{
"Name": "builtin/aws/lambda",
"Arguments": {
"Region": "${AWS_LAMBDA_REGION}",
"ARN": "${AWS_LAMBDA_ARN}",
"PayloadPassthrough": true
}
}
]
}

View File

@ -2,10 +2,14 @@
"Kind": "service-defaults",
"Name": "l2",
"Protocol": "http",
"Meta": {
"serverless.consul.hashicorp.com/v1alpha1/lambda/enabled": "true",
"serverless.consul.hashicorp.com/v1alpha1/lambda/region": "${AWS_LAMBDA_REGION}",
"serverless.consul.hashicorp.com/v1alpha1/lambda/arn": "${AWS_LAMBDA_ARN}",
"serverless.consul.hashicorp.com/v1alpha1/lambda/payload-passthrough": "false"
"EnvoyExtensions": [
{
"Name": "builtin/aws/lambda",
"Arguments": {
"Region": "${AWS_LAMBDA_REGION}",
"ARN": "${AWS_LAMBDA_ARN}",
"PayloadPassthrough": false
}
}
]
}

View File

@ -308,6 +308,30 @@ spec:
<ul><li>[Envoy](/docs/connect/proxies/envoy#proxy-config-options)</li>
<li>[Consul's built-in proxy](/docs/connect/proxies/built-in#proxy-config-key-reference)</li></ul>`,
},
{
name: 'EnvoyExtensions',
type: 'array<EnvoyExtension>: []',
description: `A list of extensions to modify Envoy proxy configuration.`,
children: [
{
name: 'Name',
type: `string: ""`,
description: `Name of the extension.`,
},
{
name: 'Required',
type: `string: ""`,
description: `When \`Required\` is true and the extension does not update any Envoy resources, an error is
returned. Use this parameter to ensure that extensions required for secure communication are not unintentionally
bypassed.`,
},
{
name: 'Arguments',
type: 'map<string|Any>: nil',
description: `Arguments to pass to the extension executable.`,
},
],
},
{
name: 'Mode',
type: `string: ""`,

View File

@ -364,6 +364,30 @@ represents a location outside the Consul cluster. They can be dialed directly wh
[Envoy Connection Balance config](https://cloudnative.to/envoy/api-v3/config/listener/v3/listener.proto.html#config-listener-v3-listener-connectionbalanceconfig)
for details.`
},
{
name: 'EnvoyExtensions',
type: 'array<EnvoyExtension>: []',
description: `A list of extensions to modify Envoy proxy configuration.`,
children: [
{
name: 'Name',
type: `string: ""`,
description: `Name of the extension.`,
},
{
name: 'Required',
type: `string: ""`,
description: `When \`Required\` is true and the extension does not update any Envoy resources, an error is
returned. Use this parameter to ensure that extensions required for secure communication are not unintentionally
bypassed.`,
},
{
name: 'Arguments',
type: 'map<string|Any>: nil',
description: `Arguments to pass to the extension executable.`,
},
],
},
{
name: 'Mode',
type: `string: ""`,
@ -524,10 +548,10 @@ represents a location outside the Consul cluster. They can be dialed directly wh
name: 'EnforcingConsecutive5xx',
type: 'int: 100',
description: {
hcl: `The % chance that a host will be actually ejected
when an outlier status is detected through consecutive 5xx.`,
yaml: `The % chance that a host will be actually ejected
when an outlier status is detected through consecutive 5xx.`,
hcl: `Measured in percent (%), the probability of a host's ejection
after a passive health check detects an outlier status through consecutive 5xx.`,
yaml: `Measured in percent (%), the probability of a host's ejection
after a passive health check detects an outlier status through consecutive 5xx.`,
},
},
],
@ -675,10 +699,10 @@ represents a location outside the Consul cluster. They can be dialed directly wh
name: 'EnforcingConsecutive5xx',
type: 'int: 100',
description: {
hcl: `The % chance that a host will be actually ejected
when an outlier status is detected through consecutive 5xx.`,
yaml: `The % chance that a host will be actually ejected
when an outlier status is detected through consecutive 5xx.`,
hcl: `Measured in percent (%), the probability of a host's ejection
after a passive health check detects an outlier status through consecutive 5xx.`,
yaml: `Measured in percent (%), the probability of a host's ejection
after a passive health check detects an outlier status through consecutive 5xx.`,
},
},
],

View File

@ -46,20 +46,26 @@ You can manually register Lambda functions if you are unable to automate the pro
$ curl --request PUT --data @lambda.json localhost:8500/v1/catalog/register
```
1. Create the `service-defaults` configuration entry and include the AWS tags used to invoke the Lambda function in the `Meta` field (refer to [Supported `Meta` fields](#supported-meta-fields). The following example creates a `service-defaults` configuration entry named `lambda`:
1. Create the `service-defaults` configuration entry and include the AWS tags used to invoke the Lambda function in the `EnvoyExtensions` configuration. Refer to [Supported `EnvoyExtension` arguments](#supported-envoyextension-arguments) for more information.
The following example creates a `service-defaults` configuration entry named `lambda`:
<CodeBlockConfig filename="lambda-service-defaults.hcl">
```hcl
Kind = "service-defaults"
Name = "lambda"
Name = "<SERVICE_NAME>"
Protocol = "http"
Meta = {
"serverless.consul.hashicorp.com/v1alpha1/lambda/enabled" = "true"
"serverless.consul.hashicorp.com/v1alpha1/lambda/arn" = "<INSERT ARN HERE>"
"serverless.consul.hashicorp.com/v1alpha1/lambda/payload-passthrough" = "true"
"serverless.consul.hashicorp.com/v1alpha1/lambda/region" = "us-east-2"
EnvoyExtensions = [
{
"Name": "builtin/aws/lambda",
"Arguments": {
"Region": "us-east-2",
"ARN": "<INSERT ARN HERE>",
"PayloadPassthrough": true
}
}
]
```
</CodeBlockConfig>
@ -69,16 +75,11 @@ You can manually register Lambda functions if you are unable to automate the pro
$ consul config write lambda-service-defaults.hcl
```
### Supported `Meta` fields
### Supported `EnvoyExtension` arguments
The following tags are supported. The path prefix for all tags is `serverless.consul.hashicorp.com/v1alpha1/lambda`. For example, specify the following tag to enable Consul to configure the service as an AWS Lambda function:
The `lambda` Envoy extension supports the following arguments:
`serverless.consul.hashicorp.com/v1alpha1/lambda/enabled`.
| Tag | Description |
| --- | --- |
| `<prefix-path>/enabled` | Determines if Consul configures the service as an AWS Lambda. |
| `<prefix-path>/payload-passthrough` | Determines if the body Envoy receives is converted to JSON or directly passed to Lambda. |
| `<prefix-path>/arn` | Specifies the [AWS ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) for the service's Lambda. |
| `<prefix-path>/invocation-mode` | Determines if Consul configures the Lambda to be invoked using the `synchronous` or `asynchronous` [invocation mode](https://docs.aws.amazon.com/lambda/latest/operatorguide/invocation-modes.html). |
| `<prefix-path>/region` | Specifies the AWS region the Lambda is running in. |
- `ARN` (`string`) - Specifies the [AWS ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) for the service's Lambda. `ARN` must be set to a valid Lambda function ARN.
- `Region` (`string`) - Specifies the AWS region the Lambda is running in. `Region` must be set to a valid AWS region where the Lambda function exists.
- `PayloadPassthrough` (`boolean: false`) - Determines if the body Envoy receives is converted to JSON or directly passed to Lambda.
- `InvocationMode` (`string: synchronous`) - Determines if Consul configures the Lambda to be invoked using the `synchronous` or `asynchronous` [invocation mode](https://docs.aws.amazon.com/lambda/latest/operatorguide/invocation-modes.html).

View File

@ -20,6 +20,47 @@ upgrade flow.
The `connect.enable_serverless_plugin` configuration option was removed. Lambda integration is now enabled by default.
#### Lambda Configuration
Instead of configuring Lambda functions in the `Meta` field of `service-defaults` configuration entries, configure them with the `EnvoyExtensions` field.
Before Consul v1.15:
<CodeBlockConfig filename="lambda-service-defaults.hcl">
```hcl
Kind = "service-defaults"
Name = "<SERVICE_NAME>"
Protocol = "http"
Meta = {
"serverless.consul.hashicorp.com/v1alpha1/lambda/enabled" = "true"
}
```
</CodeBlockConfig>
In Consul v1.15 and higher:
<CodeBlockConfig filename="lambda-service-defaults.hcl">
```hcl
Kind = "service-defaults"
Name = "<SERVICE_NAME>"
Protocol = "http"
EnvoyExtensions = [
{
"Name": "builtin/aws/lambda",
"Arguments": {
"Region": "us-east-2",
"ARN": "<INSERT ARN HERE>",
"PayloadPassthrough": true
}
}
]
```
</CodeBlockConfig>
## Consul 1.14.x
### Service Mesh Compatibility