Skip to content

Commit

Permalink
feat: implement Lua EnvoyExtensionPolicy
Browse files Browse the repository at this point in the history
Signed-off-by: Rudrakh Panigrahi <[email protected]>
  • Loading branch information
rudrakhp committed Jan 30, 2025
1 parent 57d4aa8 commit 195ee8f
Show file tree
Hide file tree
Showing 25 changed files with 2,566 additions and 12 deletions.
2 changes: 1 addition & 1 deletion api/v1alpha1/lua_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type Lua struct {
// The value of key `lua` in the ConfigMap will be used.
// If the key is not found, the first value in the ConfigMap will be used.
//
// +kubebuilder:validation:XValidation:rule="self.kind == 'ConfigMap' && (!has(self.group) || self.group == '')",message="Only a reference to an object of kind ConfigMap belonging to default core API group is supported."
// +kubebuilder:validation:XValidation:rule="self.kind == 'ConfigMap' && (!has(self.group) || self.group == 'v1')",message="Only a reference to an object of kind ConfigMap belonging to default v1 API group is supported."
// +optional
// +unionMember
ValueRef *gwapiv1.LocalObjectReference `json:"valueRef,omitempty"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1059,9 +1059,9 @@ spec:
type: object
x-kubernetes-validations:
- message: Only a reference to an object of kind ConfigMap belonging
to default core API group is supported.
to default v1 API group is supported.
rule: self.kind == 'ConfigMap' && (!has(self.group) || self.group
== '')
== 'v1')
required:
- type
type: object
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ require (
github.com/telepresenceio/watchable v0.0.0-20220726211108-9bb86f92afa7
github.com/tetratelabs/func-e v1.1.5-0.20240822223546-c85a098d5bf0
github.com/tsaarni/certyaml v0.10.0
github.com/yuin/gopher-lua v1.1.1
go.opentelemetry.io/otel v1.34.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,8 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
Expand Down
88 changes: 88 additions & 0 deletions internal/gatewayapi/envoyextensionpolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"

egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/envoyproxy/gateway/internal/gatewayapi/luavalidator"
"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
"github.com/envoyproxy/gateway/internal/gatewayapi/status"
"github.com/envoyproxy/gateway/internal/ir"
Expand Down Expand Up @@ -293,6 +295,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForRoute(
) error {
var (
wasms []ir.Wasm
luas []ir.Lua
err, errs error
)

Expand All @@ -301,6 +304,11 @@ func (t *Translator) translateEnvoyExtensionPolicyForRoute(
errs = errors.Join(errs, err)
}

if luas, err = t.buildLuas(policy, resources); err != nil {
err = perr.WithMessage(err, "Lua")
errs = errors.Join(errs, err)
}

// Apply IR to all relevant routes
prefix := irRoutePrefix(route)
parentRefs := GetParentReferences(route)
Expand Down Expand Up @@ -332,6 +340,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForRoute(
r.EnvoyExtensions = &ir.EnvoyExtensionFeatures{
ExtProcs: extProcs,
Wasms: wasms,
Luas: luas,
}
}
}
Expand All @@ -352,6 +361,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForGateway(
var (
extProcs []ir.ExtProc
wasms []ir.Wasm
luas []ir.Lua
err, errs error
)

Expand All @@ -363,6 +373,10 @@ func (t *Translator) translateEnvoyExtensionPolicyForGateway(
err = perr.WithMessage(err, "Wasm")
errs = errors.Join(errs, err)
}
if luas, err = t.buildLuas(policy, resources); err != nil {
err = perr.WithMessage(err, "Lua")
errs = errors.Join(errs, err)
}

Check warning on line 379 in internal/gatewayapi/envoyextensionpolicy.go

View check run for this annotation

Codecov / codecov/patch

internal/gatewayapi/envoyextensionpolicy.go#L377-L379

Added lines #L377 - L379 were not covered by tests

irKey := t.getIRKey(gateway.Gateway)
// Should exist since we've validated this
Expand Down Expand Up @@ -395,13 +409,80 @@ func (t *Translator) translateEnvoyExtensionPolicyForGateway(
r.EnvoyExtensions = &ir.EnvoyExtensionFeatures{
ExtProcs: extProcs,
Wasms: wasms,
Luas: luas,
}
}
}

return errs
}

func (t *Translator) buildLuas(policy *egv1a1.EnvoyExtensionPolicy, resources *resource.Resources) ([]ir.Lua, error) {
var luaIRList []ir.Lua

if policy == nil {
return nil, nil
}

Check warning on line 425 in internal/gatewayapi/envoyextensionpolicy.go

View check run for this annotation

Codecov / codecov/patch

internal/gatewayapi/envoyextensionpolicy.go#L424-L425

Added lines #L424 - L425 were not covered by tests

for idx, ep := range policy.Spec.Lua {
name := irConfigNameForLua(policy, idx)
luaIR, err := t.buildLua(name, policy, ep, resources)
if err != nil {
return nil, err
}
luaIRList = append(luaIRList, *luaIR)
}
return luaIRList, nil
}

func (t *Translator) buildLua(
name string,
policy *egv1a1.EnvoyExtensionPolicy,
lua egv1a1.Lua,
resources *resource.Resources,
) (*ir.Lua, error) {
var luaBody *string
var err error
if lua.Type == egv1a1.LuaValueTypeValueRef {
luaBody, err = getLuaBodyFromLocalObjectReference(lua.ValueRef, resources, policy.Namespace)
} else {
luaBody = lua.Inline
}
if err != nil {
return nil, err
}

Check warning on line 453 in internal/gatewayapi/envoyextensionpolicy.go

View check run for this annotation

Codecov / codecov/patch

internal/gatewayapi/envoyextensionpolicy.go#L452-L453

Added lines #L452 - L453 were not covered by tests
if err = luavalidator.NewLuaValidator(*luaBody).Validate(); err != nil {
return nil, fmt.Errorf("validation failed for lua body in policy with name %v: %w", name, err)
}
return &ir.Lua{
Name: name,
Body: luaBody,
}, nil
}

// getLuaBodyFromLocalObjectReference assumes the local object reference points to a Kubernetes ConfigMap
func getLuaBodyFromLocalObjectReference(valueRef *gwapiv1.LocalObjectReference, resources *resource.Resources, policyNs string) (*string, error) {
cm := resources.GetConfigMap(policyNs, string(valueRef.Name))
if cm != nil {
b, dataOk := cm.Data["lua"]
switch {
case dataOk:
return &b, nil
case len(cm.Data) > 0: // Fallback to the first key if lua is not found
for _, value := range cm.Data {
b = value
break

Check warning on line 474 in internal/gatewayapi/envoyextensionpolicy.go

View check run for this annotation

Codecov / codecov/patch

internal/gatewayapi/envoyextensionpolicy.go#L471-L474

Added lines #L471 - L474 were not covered by tests
}
return &b, nil
default:
return nil, fmt.Errorf("can't find the key lua in the referenced configmap %s", valueRef.Name)

Check warning on line 478 in internal/gatewayapi/envoyextensionpolicy.go

View check run for this annotation

Codecov / codecov/patch

internal/gatewayapi/envoyextensionpolicy.go#L476-L478

Added lines #L476 - L478 were not covered by tests
}

} else {
return nil, fmt.Errorf("can't find the referenced configmap %s", valueRef.Name)
}

Check warning on line 483 in internal/gatewayapi/envoyextensionpolicy.go

View check run for this annotation

Codecov / codecov/patch

internal/gatewayapi/envoyextensionpolicy.go#L481-L483

Added lines #L481 - L483 were not covered by tests
}

func (t *Translator) buildExtProcs(policy *egv1a1.EnvoyExtensionPolicy, resources *resource.Resources, envoyProxy *egv1a1.EnvoyProxy) ([]ir.ExtProc, error) {
var extProcIRList []ir.ExtProc

Expand Down Expand Up @@ -522,6 +603,13 @@ func irConfigNameForExtProc(policy *egv1a1.EnvoyExtensionPolicy, index int) stri
strconv.Itoa(index))
}

func irConfigNameForLua(policy *egv1a1.EnvoyExtensionPolicy, index int) string {
return fmt.Sprintf(
"%s/lua/%s",
irConfigName(policy),
strconv.Itoa(index))
}

func (t *Translator) buildWasms(
policy *egv1a1.EnvoyExtensionPolicy,
resources *resource.Resources,
Expand Down
60 changes: 60 additions & 0 deletions internal/gatewayapi/luavalidator/lua_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright Envoy Gateway Authors
// SPDX-License-Identifier: Apache-2.0
// The full text of the Apache license is available in the LICENSE file at
// the root of the repo.

package luavalidator

import (
_ "embed"
"fmt"
"strings"

lua "github.com/yuin/gopher-lua"
)

// mockData contains mocks of Envoy supported APIs for Lua filters.
// Refer: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/lua_filter#stream-handle-api
//
//go:embed mocks.lua
var mockData []byte

// LuaValidator validates user provided Lua for compatibility with Envoy supported Lua HTTP filter
type LuaValidator struct {
body string
}

// NewLuaValidator returns a LuaValidator for user provided Lua body
func NewLuaValidator(body string) *LuaValidator {
return &LuaValidator{
body: body,
}
}

// Validate runs all validations for the LuaValidator
func (l *LuaValidator) Validate() error {
if !strings.Contains(l.body, "envoy_on_request") && !strings.Contains(l.body, "envoy_on_response") {
return fmt.Errorf("expected one of envoy_on_request() or envoy_on_response() to be defined")
}
if strings.Contains(l.body, "envoy_on_request") {
if err := l.runLua(string(mockData) + "\n" + l.body + "\nenvoy_on_request(StreamHandle)"); err != nil {
return fmt.Errorf("failed to mock run envoy_on_request: %w", err)
}
}
if strings.Contains(l.body, "envoy_on_response") {
if err := l.runLua(string(mockData) + "\n" + l.body + "\nenvoy_on_response(StreamHandle)"); err != nil {
return fmt.Errorf("failed to mock run envoy_on_response: %w", err)
}

Check warning on line 47 in internal/gatewayapi/luavalidator/lua_validator.go

View check run for this annotation

Codecov / codecov/patch

internal/gatewayapi/luavalidator/lua_validator.go#L46-L47

Added lines #L46 - L47 were not covered by tests
}
return nil
}

// runLua interprets and runs the provided Lua body in runtime
func (l *LuaValidator) runLua(body string) error {
L := lua.NewState()
defer L.Close()
if err := L.DoString(body); err != nil {
return err
}
return nil
}
146 changes: 146 additions & 0 deletions internal/gatewayapi/luavalidator/lua_validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright Envoy Gateway Authors
// SPDX-License-Identifier: Apache-2.0
// The full text of the Apache license is available in the LICENSE file at
// the root of the repo.

package luavalidator

import (
"strings"
"testing"
)

func Test_Validate(t *testing.T) {
type args struct {
name string
body string
expectedErrSubstring string
}
tests := []args{
{
name: "empty body",
body: "",
expectedErrSubstring: "expected one of envoy_on_request() or envoy_on_response() to be defined",
},
{
name: "logInfo: envoy_on_response",
body: `function envoy_on_response(response_handle)
response_handle:logInfo("Goodbye.")
end`,
expectedErrSubstring: "",
},
{
name: "logInfo: envoy_on_request",
body: `function envoy_on_request(request_handle)
request_handle:logInfo("Goodbye.")
end`,
expectedErrSubstring: "",
},
{
name: "stream:headers:Get",
body: `function envoy_on_request(request_handle)
request_handle:headers():get("foo")
end`,
expectedErrSubstring: "",
},
{
name: "stream:connection:ssl:expirationPeerCertificate",
body: `function envoy_on_request(request_handle)
request_handle:connection():ssl():expirationPeerCertificate()
end`,
expectedErrSubstring: "",
},
{
name: "stream:metadata:pairs",
body: `function envoy_on_request(request_handle)
for key, value in pairs(request_handle:metadata()) do
print(key, value)
end
end`,
expectedErrSubstring: "",
},
{
name: "stream:httpCall",
body: `function envoy_on_request(request_handle)
-- Make an HTTP call.
local headers, body = request_handle:httpCall(
"lua_cluster",
{
[":method"] = "POST",
[":path"] = "/",
[":authority"] = "lua_cluster",
["set-cookie"] = { "lang=lua; Path=/", "type=binding; Path=/" }
},
"hello world",
5000)
-- Response directly and set a header from the HTTP call. No further filter iteration
-- occurs.
request_handle:respond(
{[":status"] = "403",
["upstream_foo"] = headers["foo"]},
"nope")
end`,
expectedErrSubstring: "",
},
{
name: "stream:httpPostCall unsupported api",
body: `function envoy_on_request(request_handle)
-- Make an HTTP call.
local headers, body = request_handle:httpPostCall(
"lua_cluster",
{
[":method"] = "POST",
[":path"] = "/",
[":authority"] = "lua_cluster",
["set-cookie"] = { "lang=lua; Path=/", "type=binding; Path=/" }
},
"hello world",
5000)
-- Response directly and set a header from the HTTP call. No further filter iteration
-- occurs.
request_handle:respond(
{[":status"] = "403",
["upstream_foo"] = headers["foo"]},
"nope")
end`,
expectedErrSubstring: "attempt to call a non-function object",
},
{
name: "stream:bodyChunks",
body: `function envoy_on_response(response_handle)
-- Sets the content-type.
response_handle:headers():replace("content-type", "text/html")
local last
for chunk in response_handle:bodyChunks() do
-- Clears each received chunk.
chunk:setBytes("")
last = chunk
end
last:setBytes("<html><b>Not Found<b></html>")
end`,
expectedErrSubstring: "",
},
{
name: "unsupported api",
body: `function envoy_on_request(request_handle)
request_handle:unknownApi()
end`,
expectedErrSubstring: "attempt to call a non-function object",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := NewLuaValidator(tt.body)
if err := l.Validate(); err != nil && tt.expectedErrSubstring == "" {
t.Errorf("Unexpected error: %v", err)
} else if err != nil && !strings.Contains(err.Error(), tt.expectedErrSubstring) {
t.Errorf("Expected substring in error: %v, got error: %v", tt.expectedErrSubstring, err)
} else if err == nil && tt.expectedErrSubstring != "" {
t.Errorf("Expected error with substring: %v", tt.expectedErrSubstring)
}
})
}
}
Loading

0 comments on commit 195ee8f

Please sign in to comment.