Skip to content

Commit e8fefff

Browse files
authored
Feat: syntax validation level for Lua EEPs (#6494)
feat: syntax validation level for Lua EEPs Signed-off-by: Rudrakh Panigrahi <rudrakh97@gmail.com>
1 parent 9d2b27d commit e8fefff

11 files changed

+621
-38
lines changed

api/v1alpha1/envoyproxy_types.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,14 @@ const (
173173
// For supported APIs, see: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/lua_filter#stream-handle-api
174174
LuaValidationStrict LuaValidation = "Strict"
175175

176-
// LuaValidationDisabled disables all validation of Lua scripts.
176+
// LuaValidationSyntax checks for syntax errors in the Lua script.
177+
// Note that this is not a full runtime validation and does not check for issues during script execution.
178+
// This is recommended if your scripts use external libraries that are not supported by Lua runtime validation.
179+
LuaValidationSyntax LuaValidation = "Syntax"
180+
181+
// LuaValidationDisabled disables all validations of Lua scripts.
177182
// Scripts will be accepted and executed without any validation checks.
178-
// This is not recommended unless your scripts import libraries that are not supported by Lua runtime validation.
183+
// This is not recommended unless both runtime and syntax validations are failing unexpectedly.
179184
LuaValidationDisabled LuaValidation = "Disabled"
180185
)
181186

internal/gatewayapi/envoyextensionpolicy.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@ func (t *Translator) buildLua(
443443
envoyProxy *egv1a1.EnvoyProxy,
444444
) (*ir.Lua, error) {
445445
var luaCode *string
446+
var luaValidation egv1a1.LuaValidation
446447
var err error
447448
if lua.Type == egv1a1.LuaValueTypeValueRef {
448449
luaCode, err = getLuaBodyFromLocalObjectReference(lua.ValueRef, resources, policy.Namespace)
@@ -452,14 +453,12 @@ func (t *Translator) buildLua(
452453
if err != nil {
453454
return nil, err
454455
}
455-
if envoyProxy != nil && envoyProxy.Spec.LuaValidation != nil &&
456-
*envoyProxy.Spec.LuaValidation == egv1a1.LuaValidationDisabled {
457-
return &ir.Lua{
458-
Name: name,
459-
Code: luaCode,
460-
}, nil
456+
if envoyProxy != nil && envoyProxy.Spec.LuaValidation != nil {
457+
luaValidation = *envoyProxy.Spec.LuaValidation
458+
} else {
459+
luaValidation = egv1a1.LuaValidationStrict
461460
}
462-
if err = luavalidator.NewLuaValidator(*luaCode).Validate(); err != nil {
461+
if err = luavalidator.NewLuaValidator(*luaCode, luaValidation).Validate(); err != nil {
463462
return nil, fmt.Errorf("validation failed for lua body in policy with name %v: %w", name, err)
464463
}
465464
return &ir.Lua{

internal/gatewayapi/luavalidator/lua_validator.go

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ import (
1111
"strings"
1212

1313
lua "github.com/yuin/gopher-lua"
14+
15+
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
16+
)
17+
18+
const (
19+
envoyOnRequestFunctionName = "envoy_on_request"
20+
envoyOnResponseFunctionName = "envoy_on_response"
1421
)
1522

1623
// mockData contains mocks of Envoy supported APIs for Lua filters.
@@ -20,35 +27,50 @@ import (
2027
var mockData []byte
2128

2229
// LuaValidator validates user provided Lua for compatibility with Envoy supported Lua HTTP filter
30+
// Validation strictness is controlled by the validation field
2331
type LuaValidator struct {
24-
code string
32+
code string
33+
validation egv1a1.LuaValidation
2534
}
2635

2736
// NewLuaValidator returns a LuaValidator for user provided Lua code
28-
func NewLuaValidator(code string) *LuaValidator {
37+
func NewLuaValidator(code string, validation egv1a1.LuaValidation) *LuaValidator {
2938
return &LuaValidator{
30-
code: code,
39+
code: code,
40+
validation: validation,
3141
}
3242
}
3343

3444
// Validate runs all validations for the LuaValidator
3545
func (l *LuaValidator) Validate() error {
36-
if !strings.Contains(l.code, "envoy_on_request") && !strings.Contains(l.code, "envoy_on_response") {
37-
return fmt.Errorf("expected one of envoy_on_request() or envoy_on_response() to be defined")
46+
if !strings.Contains(l.code, envoyOnRequestFunctionName) && !strings.Contains(l.code, envoyOnResponseFunctionName) {
47+
return fmt.Errorf("expected one of %s() or %s() to be defined", envoyOnRequestFunctionName, envoyOnResponseFunctionName)
3848
}
39-
if strings.Contains(l.code, "envoy_on_request") {
40-
if err := l.runLua(string(mockData) + "\n" + l.code + "\nenvoy_on_request(StreamHandle)"); err != nil {
41-
return fmt.Errorf("failed to mock run envoy_on_request: %w", err)
49+
if strings.Contains(l.code, envoyOnRequestFunctionName) {
50+
if err := l.validate(string(mockData) + "\n" + l.code + "\n" + envoyOnRequestFunctionName + "(StreamHandle)"); err != nil {
51+
return fmt.Errorf("failed to validate with %s: %w", envoyOnRequestFunctionName, err)
4252
}
4353
}
44-
if strings.Contains(l.code, "envoy_on_response") {
45-
if err := l.runLua(string(mockData) + "\n" + l.code + "\nenvoy_on_response(StreamHandle)"); err != nil {
46-
return fmt.Errorf("failed to mock run envoy_on_response: %w", err)
54+
if strings.Contains(l.code, envoyOnResponseFunctionName) {
55+
if err := l.validate(string(mockData) + "\n" + l.code + "\n" + envoyOnResponseFunctionName + "(StreamHandle)"); err != nil {
56+
return fmt.Errorf("failed to validate with %s: %w", envoyOnResponseFunctionName, err)
4757
}
4858
}
4959
return nil
5060
}
5161

62+
// validate runs the validation on given code
63+
func (l *LuaValidator) validate(code string) error {
64+
switch l.validation {
65+
case egv1a1.LuaValidationSyntax:
66+
return l.loadLua(code)
67+
case egv1a1.LuaValidationDisabled:
68+
return nil
69+
default:
70+
return l.runLua(code)
71+
}
72+
}
73+
5274
// runLua interprets and runs the provided Lua code in runtime using gopher-lua
5375
// Refer: https://github.com/yuin/gopher-lua?tab=readme-ov-file#differences-between-lua-and-gopherlua
5476
func (l *LuaValidator) runLua(code string) error {
@@ -59,3 +81,14 @@ func (l *LuaValidator) runLua(code string) error {
5981
}
6082
return nil
6183
}
84+
85+
// loadLua loads the Lua code into the Lua state, does not run it
86+
// This is used to check for syntax errors in the Lua code
87+
func (l *LuaValidator) loadLua(code string) error {
88+
L := lua.NewState()
89+
defer L.Close()
90+
if _, err := L.LoadString(code); err != nil {
91+
return err
92+
}
93+
return nil
94+
}

internal/gatewayapi/luavalidator/lua_validator_test.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ package luavalidator
88
import (
99
"strings"
1010
"testing"
11+
12+
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
1113
)
1214

1315
func Test_Validate(t *testing.T) {
1416
type args struct {
1517
name string
1618
code string
19+
validation egv1a1.LuaValidation
1720
expectedErrSubstring string
1821
}
1922
tests := []args{
@@ -130,10 +133,52 @@ func Test_Validate(t *testing.T) {
130133
end`,
131134
expectedErrSubstring: "attempt to call a non-function object",
132135
},
136+
{
137+
name: "unsupported api",
138+
code: `function envoy_on_request(request_handle)
139+
request_handle:unknownApi()
140+
end`,
141+
validation: egv1a1.LuaValidationSyntax,
142+
expectedErrSubstring: "",
143+
},
144+
{
145+
name: "unsupported api",
146+
code: `function envoy_on_response(response_handle)
147+
-- Sets the content-type.
148+
response_handle:headers():replace("content-type", "text/html")
149+
local last
150+
for chunk in response_handle:bodyChunks() do
151+
-- Clears each received chunk.
152+
chunk:setBytes("")
153+
last = chunk
154+
-- invalid syntax as there is no end for the for loop
155+
156+
last:setBytes("<html><b>Not Found<b></html>")
157+
end`,
158+
validation: egv1a1.LuaValidationSyntax,
159+
expectedErrSubstring: "<string> at EOF: syntax error",
160+
},
161+
{
162+
name: "unsupported api",
163+
code: `function envoy_on_response(response_handle)
164+
-- Sets the content-type.
165+
response_handle:headers():replace("content-type", "text/html")
166+
local last
167+
for chunk in response_handle:bodyChunks() do
168+
-- Clears each received chunk.
169+
chunk:setBytes("")
170+
last = chunk
171+
-- invalid syntax as there is no end for the for loop
172+
173+
last:setBytes("<html><b>Not Found<b></html>")
174+
end`,
175+
validation: egv1a1.LuaValidationDisabled,
176+
expectedErrSubstring: "",
177+
},
133178
}
134179
for _, tt := range tests {
135180
t.Run(tt.name, func(t *testing.T) {
136-
l := NewLuaValidator(tt.code)
181+
l := NewLuaValidator(tt.code, tt.validation)
137182
if err := l.Validate(); err != nil && tt.expectedErrSubstring == "" {
138183
t.Errorf("Unexpected error: %v", err)
139184
} else if err != nil && !strings.Contains(err.Error(), tt.expectedErrSubstring) {

internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-disabled.in.yaml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,18 @@ envoyextensionpolicies:
4646
kind: EnvoyExtensionPolicy
4747
metadata:
4848
namespace: default
49-
name: policy-for-http-route # Invalid Lua but still gets accepted
49+
name: policy-for-http-route
5050
spec:
5151
targetRef:
5252
group: gateway.networking.k8s.io
5353
kind: HTTPRoute
5454
name: httproute-1
5555
lua:
56-
- type: Inline
57-
inline: "function envoy_on_response(response_handle)
58-
response_handle:UnknownApi()
59-
end"
56+
- type: Inline # Invalid Lua syntax (missing then keyword in if statement) but should be accepted
57+
inline: |
58+
function envoy_on_response(response_handle)
59+
local value = 10
60+
if value > 5
61+
print("Value is greater than 5")
62+
end
63+
end

internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-disabled.out.yaml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ envoyExtensionPolicies:
77
namespace: default
88
spec:
99
lua:
10-
- inline: function envoy_on_response(response_handle) response_handle:UnknownApi()
10+
- inline: |
11+
function envoy_on_response(response_handle)
12+
local value = 10
13+
if value > 5
14+
print("Value is greater than 5")
15+
end
1116
end
1217
type: Inline
1318
targetRef:
@@ -181,7 +186,12 @@ xdsIR:
181186
weight: 1
182187
envoyExtensions:
183188
luas:
184-
- Code: function envoy_on_response(response_handle) response_handle:UnknownApi()
189+
- Code: |
190+
function envoy_on_response(response_handle)
191+
local value = 10
192+
if value > 5
193+
print("Value is greater than 5")
194+
end
185195
end
186196
Name: envoyextensionpolicy/default/policy-for-http-route/lua/0
187197
hostname: www.example.com
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
envoyProxyForGatewayClass:
2+
apiVersion: gateway.envoyproxy.io/v1alpha1
3+
kind: EnvoyProxy
4+
metadata:
5+
namespace: envoy-gateway-system
6+
name: test
7+
spec:
8+
luaValidation: Syntax
9+
gateways:
10+
- apiVersion: gateway.networking.k8s.io/v1
11+
kind: Gateway
12+
metadata:
13+
namespace: envoy-gateway
14+
name: gateway-1
15+
spec:
16+
gatewayClassName: envoy-gateway-class
17+
listeners:
18+
- name: http
19+
protocol: HTTP
20+
port: 80
21+
allowedRoutes:
22+
namespaces:
23+
from: All
24+
httpRoutes:
25+
- apiVersion: gateway.networking.k8s.io/v1
26+
kind: HTTPRoute
27+
metadata:
28+
namespace: default
29+
name: httproute-1
30+
spec:
31+
hostnames:
32+
- www.example.com
33+
parentRefs:
34+
- namespace: envoy-gateway
35+
name: gateway-1
36+
sectionName: http
37+
rules:
38+
- matches:
39+
- path:
40+
value: "/foo"
41+
backendRefs:
42+
- name: service-1
43+
port: 8080
44+
- apiVersion: gateway.networking.k8s.io/v1
45+
kind: HTTPRoute
46+
metadata:
47+
namespace: default
48+
name: httproute-2
49+
spec:
50+
hostnames:
51+
- www.example.com
52+
parentRefs:
53+
- namespace: envoy-gateway
54+
name: gateway-1
55+
sectionName: http
56+
rules:
57+
- matches:
58+
- path:
59+
value: "/foo"
60+
backendRefs:
61+
- name: service-1
62+
port: 8080
63+
envoyextensionpolicies:
64+
- apiVersion: gateway.envoyproxy.io/v1alpha1
65+
kind: EnvoyExtensionPolicy
66+
metadata:
67+
namespace: default
68+
name: policy-for-http-route
69+
spec:
70+
targetRef:
71+
group: gateway.networking.k8s.io
72+
kind: HTTPRoute
73+
name: httproute-1
74+
lua:
75+
- type: Inline # Lua with external library and UnknownApi() call but correct syntax so should be accepted
76+
inline: |
77+
local json = require("json")
78+
function envoy_on_response(response_handle)
79+
local content_type = response_handle:headers():get("content-type")
80+
if content_type and string.find(content_type, "application/json", 1, true) then
81+
response_handle:body():setBytes(0, response_handle:body():length())
82+
response_handle:UnknownApi()
83+
local response_body = response_handle:body():getBytes(0, response_handle:body():length())
84+
if response_body and #response_body > 0 then
85+
local parsed_json = json.decode(response_body)
86+
if type(parsed_json) == "table" then
87+
response_handle:logInfo("Successfully parsed JSON response.")
88+
else
89+
response_handle:logWarn("Parsed JSON is not a table, or unexpected format.")
90+
end
91+
end
92+
end
93+
return envoy.lua.ResponseStatus.Continue
94+
end
95+
- apiVersion: gateway.envoyproxy.io/v1alpha1
96+
kind: EnvoyExtensionPolicy
97+
metadata:
98+
namespace: default
99+
name: policy-for-http-route-2
100+
spec:
101+
targetRef:
102+
group: gateway.networking.k8s.io
103+
kind: HTTPRoute
104+
name: httproute-2
105+
lua:
106+
- type: Inline # Invalid Lua syntax (missing then keyword in if statement) so should be rejected
107+
inline: |
108+
function envoy_on_response(response_handle)
109+
local value = 10
110+
if value > 5
111+
print("Value is greater than 5")
112+
end
113+
end

0 commit comments

Comments
 (0)