Skip to content

Commit d60860a

Browse files
Backport of feature: add LUA script Envoy extension support for API gateway into release/1.20.x (#22333)
backport of commit c130367 Co-authored-by: Vikramarjuna <[email protected]>
1 parent 6b1f6fd commit d60860a

File tree

12 files changed

+814
-16
lines changed

12 files changed

+814
-16
lines changed

agent/envoyextensions/builtin/lua/lua.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,18 @@ package lua
66
import (
77
"errors"
88
"fmt"
9+
910
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
1011

1112
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
1213
envoy_lua_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/lua/v3"
1314
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
1415
envoy_resource_v3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
15-
"github.com/hashicorp/consul/api"
16-
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
1716
"github.com/hashicorp/go-multierror"
1817
"github.com/mitchellh/mapstructure"
18+
19+
"github.com/hashicorp/consul/api"
20+
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
1921
)
2022

2123
var _ extensioncommon.BasicExtension = (*lua)(nil)
@@ -57,7 +59,7 @@ func (l *lua) validate() error {
5759
if l.Script == "" {
5860
resultErr = multierror.Append(resultErr, fmt.Errorf("missing Script value"))
5961
}
60-
if l.ProxyType != string(api.ServiceKindConnectProxy) {
62+
if l.ProxyType != string(api.ServiceKindConnectProxy) && l.ProxyType != string(api.ServiceKindAPIGateway) {
6163
resultErr = multierror.Append(resultErr, fmt.Errorf("unexpected ProxyType %q", l.ProxyType))
6264
}
6365
if l.Listener != "inbound" && l.Listener != "outbound" {

agent/envoyextensions/builtin/lua/lua_test.go

Lines changed: 180 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ package lua
66
import (
77
"testing"
88

9+
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
10+
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
11+
envoy_lua_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/lua/v3"
12+
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
913
"github.com/stretchr/testify/require"
14+
"google.golang.org/protobuf/proto"
15+
"google.golang.org/protobuf/types/known/anypb"
1016

1117
"github.com/hashicorp/consul/api"
1218
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
@@ -17,17 +23,15 @@ func TestConstructor(t *testing.T) {
1723
m := map[string]interface{}{
1824
"ProxyType": "connect-proxy",
1925
"Listener": "inbound",
20-
"Script": "lua-script",
26+
"Script": "function envoy_on_request(request_handle) request_handle:headers():add('test', 'test') end",
2127
}
22-
2328
for k, v := range overrides {
2429
m[k] = v
2530
}
26-
2731
return m
2832
}
2933

30-
cases := map[string]struct {
34+
tests := map[string]struct {
3135
extensionName string
3236
arguments map[string]interface{}
3337
expected lua
@@ -59,7 +63,16 @@ func TestConstructor(t *testing.T) {
5963
expected: lua{
6064
ProxyType: "connect-proxy",
6165
Listener: "inbound",
62-
Script: "lua-script",
66+
Script: "function envoy_on_request(request_handle) request_handle:headers():add('test', 'test') end",
67+
},
68+
ok: true,
69+
},
70+
"api gateway proxy type": {
71+
arguments: makeArguments(map[string]interface{}{"ProxyType": "api-gateway"}),
72+
expected: lua{
73+
ProxyType: "api-gateway",
74+
Listener: "inbound",
75+
Script: "function envoy_on_request(request_handle) request_handle:headers():add('test', 'test') end",
6376
},
6477
ok: true,
6578
},
@@ -68,23 +81,21 @@ func TestConstructor(t *testing.T) {
6881
expected: lua{
6982
ProxyType: "connect-proxy",
7083
Listener: "inbound",
71-
Script: "lua-script",
84+
Script: "function envoy_on_request(request_handle) request_handle:headers():add('test', 'test') end",
7285
},
7386
ok: true,
7487
},
7588
}
7689

77-
for n, tc := range cases {
78-
t.Run(n, func(t *testing.T) {
79-
90+
for name, tc := range tests {
91+
t.Run(name, func(t *testing.T) {
8092
extensionName := api.BuiltinLuaExtension
8193
if tc.extensionName != "" {
8294
extensionName = tc.extensionName
8395
}
8496

85-
svc := api.CompoundServiceName{Name: "svc"}
8697
ext := extensioncommon.RuntimeConfig{
87-
ServiceName: svc,
98+
ServiceName: api.CompoundServiceName{Name: "svc"},
8899
EnvoyExtension: api.EnvoyExtension{
89100
Name: extensionName,
90101
Arguments: tc.arguments,
@@ -102,3 +113,161 @@ func TestConstructor(t *testing.T) {
102113
})
103114
}
104115
}
116+
117+
func TestLuaExtension_PatchFilter(t *testing.T) {
118+
makeFilter := func(filters []*envoy_http_v3.HttpFilter) *envoy_listener_v3.Filter {
119+
hcm := &envoy_http_v3.HttpConnectionManager{
120+
HttpFilters: filters,
121+
}
122+
any, err := anypb.New(hcm)
123+
require.NoError(t, err)
124+
return &envoy_listener_v3.Filter{
125+
Name: "envoy.filters.network.http_connection_manager",
126+
ConfigType: &envoy_listener_v3.Filter_TypedConfig{
127+
TypedConfig: any,
128+
},
129+
}
130+
}
131+
132+
makeLuaFilter := func(script string) *envoy_http_v3.HttpFilter {
133+
luaConfig := &envoy_lua_v3.Lua{
134+
DefaultSourceCode: &envoy_core_v3.DataSource{
135+
Specifier: &envoy_core_v3.DataSource_InlineString{
136+
InlineString: script,
137+
},
138+
},
139+
}
140+
return &envoy_http_v3.HttpFilter{
141+
Name: "envoy.filters.http.lua",
142+
ConfigType: &envoy_http_v3.HttpFilter_TypedConfig{
143+
TypedConfig: mustMarshalAny(luaConfig),
144+
},
145+
}
146+
}
147+
148+
tests := map[string]struct {
149+
extension *lua
150+
filter *envoy_listener_v3.Filter
151+
isInbound bool
152+
expectedFilter *envoy_listener_v3.Filter
153+
expectPatched bool
154+
expectError string
155+
}{
156+
"non-http filter is ignored": {
157+
extension: &lua{
158+
ProxyType: "connect-proxy",
159+
Listener: "inbound",
160+
Script: "function envoy_on_request(request_handle) end",
161+
},
162+
filter: &envoy_listener_v3.Filter{
163+
Name: "envoy.filters.network.tcp_proxy",
164+
},
165+
expectedFilter: &envoy_listener_v3.Filter{
166+
Name: "envoy.filters.network.tcp_proxy",
167+
},
168+
expectPatched: false,
169+
},
170+
"listener direction mismatch": {
171+
extension: &lua{
172+
ProxyType: "connect-proxy",
173+
Listener: "inbound",
174+
Script: "function envoy_on_request(request_handle) end",
175+
},
176+
filter: makeFilter([]*envoy_http_v3.HttpFilter{
177+
{Name: "envoy.filters.http.router"},
178+
}),
179+
isInbound: false,
180+
expectedFilter: makeFilter([]*envoy_http_v3.HttpFilter{{Name: "envoy.filters.http.router"}}),
181+
expectPatched: false,
182+
},
183+
"successful patch with router filter": {
184+
extension: &lua{
185+
ProxyType: "connect-proxy",
186+
Listener: "inbound",
187+
Script: "function envoy_on_request(request_handle) end",
188+
},
189+
filter: makeFilter([]*envoy_http_v3.HttpFilter{
190+
{Name: "envoy.filters.http.router"},
191+
}),
192+
isInbound: true,
193+
expectedFilter: makeFilter([]*envoy_http_v3.HttpFilter{
194+
makeLuaFilter("function envoy_on_request(request_handle) end"),
195+
{Name: "envoy.filters.http.router"},
196+
}),
197+
expectPatched: true,
198+
},
199+
"successful patch with multiple filters": {
200+
extension: &lua{
201+
ProxyType: "connect-proxy",
202+
Listener: "inbound",
203+
Script: "function envoy_on_request(request_handle) end",
204+
},
205+
filter: makeFilter([]*envoy_http_v3.HttpFilter{
206+
{Name: "envoy.filters.http.other1"},
207+
{Name: "envoy.filters.http.router"},
208+
{Name: "envoy.filters.http.other2"},
209+
}),
210+
isInbound: true,
211+
expectedFilter: makeFilter([]*envoy_http_v3.HttpFilter{
212+
{Name: "envoy.filters.http.other1"},
213+
makeLuaFilter("function envoy_on_request(request_handle) end"),
214+
{Name: "envoy.filters.http.router"},
215+
{Name: "envoy.filters.http.other2"},
216+
}),
217+
expectPatched: true,
218+
},
219+
"invalid filter config": {
220+
extension: &lua{
221+
ProxyType: "connect-proxy",
222+
Listener: "inbound",
223+
Script: "function envoy_on_request(request_handle) end",
224+
},
225+
filter: &envoy_listener_v3.Filter{
226+
Name: "envoy.filters.network.http_connection_manager",
227+
ConfigType: &envoy_listener_v3.Filter_TypedConfig{
228+
TypedConfig: &anypb.Any{},
229+
},
230+
},
231+
isInbound: true,
232+
expectedFilter: nil,
233+
expectPatched: false,
234+
expectError: "error unmarshalling filter",
235+
},
236+
}
237+
238+
for name, tc := range tests {
239+
t.Run(name, func(t *testing.T) {
240+
direction := extensioncommon.TrafficDirectionOutbound
241+
if tc.isInbound {
242+
direction = extensioncommon.TrafficDirectionInbound
243+
}
244+
245+
payload := extensioncommon.FilterPayload{
246+
Message: tc.filter,
247+
TrafficDirection: direction,
248+
}
249+
250+
filter, patched, err := tc.extension.PatchFilter(payload)
251+
252+
if tc.expectError != "" {
253+
require.Error(t, err)
254+
require.Contains(t, err.Error(), tc.expectError)
255+
return
256+
}
257+
258+
require.NoError(t, err)
259+
require.Equal(t, tc.expectPatched, patched)
260+
if tc.expectedFilter != nil {
261+
require.Equal(t, tc.expectedFilter, filter)
262+
}
263+
})
264+
}
265+
}
266+
267+
func mustMarshalAny(m proto.Message) *anypb.Any {
268+
a, err := anypb.New(m)
269+
if err != nil {
270+
panic(err)
271+
}
272+
return a
273+
}

agent/xds/extensionruntime/runtime_config.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,29 @@ func GetRuntimeConfigurations(cfgSnap *proxycfg.ConfigSnapshot) map[api.Compound
128128
EnvoyID: envoyID.EnvoyID(),
129129
OutgoingProxyKind: api.ServiceKindTerminatingGateway,
130130
}
131+
}
132+
case structs.ServiceKindAPIGateway:
133+
kind = api.ServiceKindAPIGateway
134+
// For API Gateway, we need to handle the gateway itself
135+
localSvc := api.CompoundServiceName{
136+
Name: cfgSnap.Service,
137+
Namespace: cfgSnap.ProxyID.NamespaceOrDefault(),
138+
Partition: cfgSnap.ProxyID.PartitionOrEmpty(),
139+
}
131140

141+
// Handle extensions for the API Gateway itself
142+
extensionConfigurationsMap[localSvc] = []extensioncommon.RuntimeConfig{}
143+
cfgSnapExts := convertEnvoyExtensions(cfgSnap.Proxy.EnvoyExtensions)
144+
for _, ext := range cfgSnapExts {
145+
extCfg := extensioncommon.RuntimeConfig{
146+
EnvoyExtension: ext,
147+
ServiceName: localSvc,
148+
IsSourcedFromUpstream: false,
149+
Upstreams: upstreamMap,
150+
Kind: kind,
151+
Protocol: proxyConfigProtocol(cfgSnap.Proxy.Config),
152+
}
153+
extensionConfigurationsMap[localSvc] = append(extensionConfigurationsMap[localSvc], extCfg)
132154
}
133155
}
134156

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package extensionruntime
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/hashicorp/consul/agent/proxycfg"
12+
"github.com/hashicorp/consul/agent/structs"
13+
"github.com/hashicorp/consul/api"
14+
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
15+
)
16+
17+
func TestGetRuntimeConfigurations_APIGateway(t *testing.T) {
18+
tests := []struct {
19+
name string
20+
cfgSnap *proxycfg.ConfigSnapshot
21+
expectedConfig map[api.CompoundServiceName][]extensioncommon.RuntimeConfig
22+
}{
23+
{
24+
name: "API Gateway with no extensions",
25+
cfgSnap: &proxycfg.ConfigSnapshot{
26+
Kind: structs.ServiceKindAPIGateway,
27+
Proxy: structs.ConnectProxyConfig{},
28+
Service: "api-gateway",
29+
ProxyID: proxycfg.ProxyID{
30+
ServiceID: structs.ServiceID{
31+
ID: "api-gateway",
32+
},
33+
},
34+
},
35+
expectedConfig: map[api.CompoundServiceName][]extensioncommon.RuntimeConfig{
36+
{
37+
Name: "api-gateway",
38+
Namespace: "default",
39+
}: {},
40+
},
41+
},
42+
{
43+
name: "API Gateway with extensions",
44+
cfgSnap: &proxycfg.ConfigSnapshot{
45+
Kind: structs.ServiceKindAPIGateway,
46+
Proxy: structs.ConnectProxyConfig{
47+
EnvoyExtensions: []structs.EnvoyExtension{
48+
{
49+
Name: "builtin/lua",
50+
Arguments: map[string]interface{}{
51+
"Script": "function envoy_on_response(response_handle) response_handle:headers():add('x-test', 'test') end",
52+
},
53+
},
54+
},
55+
},
56+
Service: "api-gateway",
57+
ProxyID: proxycfg.ProxyID{
58+
ServiceID: structs.ServiceID{
59+
ID: "api-gateway",
60+
},
61+
},
62+
},
63+
expectedConfig: map[api.CompoundServiceName][]extensioncommon.RuntimeConfig{
64+
{
65+
Name: "api-gateway",
66+
Namespace: "default",
67+
}: {
68+
{
69+
EnvoyExtension: api.EnvoyExtension{
70+
Name: "builtin/lua",
71+
Arguments: map[string]interface{}{
72+
"Script": "function envoy_on_response(response_handle) response_handle:headers():add('x-test', 'test') end",
73+
},
74+
},
75+
ServiceName: api.CompoundServiceName{
76+
Name: "api-gateway",
77+
Namespace: "default",
78+
},
79+
Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{},
80+
IsSourcedFromUpstream: false,
81+
Kind: api.ServiceKindAPIGateway,
82+
},
83+
},
84+
},
85+
},
86+
}
87+
88+
for _, tt := range tests {
89+
t.Run(tt.name, func(t *testing.T) {
90+
config := GetRuntimeConfigurations(tt.cfgSnap)
91+
require.Equal(t, tt.expectedConfig, config)
92+
})
93+
}
94+
}

0 commit comments

Comments
 (0)