From 007a455223031f5dcedc9220932b0b00ce13298e Mon Sep 17 00:00:00 2001 From: Rudrakh Panigrahi Date: Wed, 29 Jan 2025 00:27:06 +0530 Subject: [PATCH] feat: implement Lua EnvoyExtensionPolicy Signed-off-by: Rudrakh Panigrahi --- go.mod | 1 + go.sum | 2 + internal/gatewayapi/envoyextensionpolicy.go | 101 ++- .../gatewayapi/luavalidator/lua_validator.go | 59 ++ .../luavalidator/lua_validator_test.go | 146 +++++ internal/gatewayapi/luavalidator/mocks.lua | 613 ++++++++++++++++++ ...oyextensionpolicy-with-invalid-lua.in.yaml | 85 +++ ...yextensionpolicy-with-invalid-lua.out.yaml | 276 ++++++++ ...extensionpolicy-with-lua-configmap.in.yaml | 106 +++ ...xtensionpolicy-with-lua-configmap.out.yaml | 279 ++++++++ .../envoyextensionpolicy-with-lua.in.yaml | 85 +++ .../envoyextensionpolicy-with-lua.out.yaml | 271 ++++++++ internal/ir/xds.go | 12 + internal/ir/zz_generated.deepcopy.go | 27 + internal/xds/translator/lua.go | 118 ++++ .../translator/testdata/in/xds-ir/lua.yaml | 59 ++ .../testdata/out/xds-ir/lua.clusters.yaml | 34 + .../testdata/out/xds-ir/lua.endpoints.yaml | 24 + .../testdata/out/xds-ir/lua.listeners.yaml | 52 ++ .../testdata/out/xds-ir/lua.routes.yaml | 21 + test/e2e/testdata/lua-http-inline.yaml | 82 +++ test/e2e/tests/lua_http.go | 82 +++ 22 files changed, 2528 insertions(+), 7 deletions(-) create mode 100644 internal/gatewayapi/luavalidator/lua_validator.go create mode 100644 internal/gatewayapi/luavalidator/lua_validator_test.go create mode 100644 internal/gatewayapi/luavalidator/mocks.lua create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.in.yaml create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.out.yaml create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-configmap.in.yaml create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-configmap.out.yaml create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-lua.in.yaml create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-lua.out.yaml create mode 100644 internal/xds/translator/lua.go create mode 100644 internal/xds/translator/testdata/in/xds-ir/lua.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/lua.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/lua.endpoints.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/lua.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/lua.routes.yaml create mode 100644 test/e2e/testdata/lua-http-inline.yaml create mode 100644 test/e2e/tests/lua_http.go diff --git a/go.mod b/go.mod index 1993b7d139a..9df5746fc7c 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 35e6872946c..54cb952f496 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/gatewayapi/envoyextensionpolicy.go b/internal/gatewayapi/envoyextensionpolicy.go index 145662e5480..b3ebb102ffc 100644 --- a/internal/gatewayapi/envoyextensionpolicy.go +++ b/internal/gatewayapi/envoyextensionpolicy.go @@ -13,6 +13,13 @@ import ( "strings" "time" + 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" + "github.com/envoyproxy/gateway/internal/utils" + "github.com/envoyproxy/gateway/internal/wasm" perr "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -20,14 +27,8 @@ 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/resource" - "github.com/envoyproxy/gateway/internal/gatewayapi/status" - "github.com/envoyproxy/gateway/internal/ir" - "github.com/envoyproxy/gateway/internal/utils" - "github.com/envoyproxy/gateway/internal/wasm" ) // oci URL prefix @@ -293,6 +294,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForRoute( ) error { var ( wasms []ir.Wasm + luas []ir.Lua err, errs error ) @@ -301,6 +303,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) @@ -332,6 +339,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForRoute( r.EnvoyExtensions = &ir.EnvoyExtensionFeatures{ ExtProcs: extProcs, Wasms: wasms, + Luas: luas, } } } @@ -352,6 +360,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForGateway( var ( extProcs []ir.ExtProc wasms []ir.Wasm + luas []ir.Lua err, errs error ) @@ -363,6 +372,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) + } irKey := t.getIRKey(gateway.Gateway) // Should exist since we've validated this @@ -395,6 +408,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForGateway( r.EnvoyExtensions = &ir.EnvoyExtensionFeatures{ ExtProcs: extProcs, Wasms: wasms, + Luas: luas, } } } @@ -402,6 +416,72 @@ func (t *Translator) translateEnvoyExtensionPolicyForGateway( 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 + } + + 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 + } + 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 + } + return &b, nil + default: + return nil, fmt.Errorf("can't find the key lua in the referenced configmap %s", valueRef.Name) + } + + } else { + return nil, fmt.Errorf("can't find the referenced configmap %s", valueRef.Name) + } +} + func (t *Translator) buildExtProcs(policy *egv1a1.EnvoyExtensionPolicy, resources *resource.Resources, envoyProxy *egv1a1.EnvoyProxy) ([]ir.ExtProc, error) { var extProcIRList []ir.ExtProc @@ -522,6 +602,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, diff --git a/internal/gatewayapi/luavalidator/lua_validator.go b/internal/gatewayapi/luavalidator/lua_validator.go new file mode 100644 index 00000000000..0ebabd0afa0 --- /dev/null +++ b/internal/gatewayapi/luavalidator/lua_validator.go @@ -0,0 +1,59 @@ +// 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" + lua "github.com/yuin/gopher-lua" + "strings" +) + +// 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.validate(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.validate(string(mockData) + "\n" + l.body + "\nenvoy_on_response(StreamHandle)"); err != nil { + return fmt.Errorf("failed to mock run envoy_on_response: %w", err) + } + } + return nil +} + +// validate interprets and runs the provided Lua body in runtime +func (l *LuaValidator) validate(body string) error { + L := lua.NewState() + defer L.Close() + if err := L.DoString(body); err != nil { + return err + } + return nil +} diff --git a/internal/gatewayapi/luavalidator/lua_validator_test.go b/internal/gatewayapi/luavalidator/lua_validator_test.go new file mode 100644 index 00000000000..3e3c1ffc7bd --- /dev/null +++ b/internal/gatewayapi/luavalidator/lua_validator_test.go @@ -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("Not Found") + 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) + } + }) + } +} diff --git a/internal/gatewayapi/luavalidator/mocks.lua b/internal/gatewayapi/luavalidator/mocks.lua new file mode 100644 index 00000000000..4cb13632238 --- /dev/null +++ b/internal/gatewayapi/luavalidator/mocks.lua @@ -0,0 +1,613 @@ +-- ParsedName object. +local ParsedName = {} + +-- Logging methods. +function ParsedName:logTrace(message) + print("[TRACE]: " .. message) +end + +function ParsedName:logDebug(message) + print("[DEBUG]: " .. message) +end + +function ParsedName:logInfo(message) + print("[INFO]: " .. message) +end + +function ParsedName:logWarn(message) + print("[WARN]: " .. message) +end + +function ParsedName:logErr(message) + print("[ERROR]: " .. message) +end + +function ParsedName:logCritical(message) + print("[CRITICAL]: " .. message) +end + +-- Method to return the common name. +function ParsedName:commonName() + return "common_name" +end + +-- Method to return the organization name. +function ParsedName:organizationName() + return "organization_name" +end + +local SSLConnectionObject = {} + +-- Logging methods +function SSLConnectionObject:logTrace(message) + print("[TRACE]: " .. message) +end + +function SSLConnectionObject:logDebug(message) + print("[DEBUG]: " .. message) +end + +function SSLConnectionObject:logInfo(message) + print("[INFO]: " .. message) +end + +function SSLConnectionObject:logWarn(message) + print("[WARN]: " .. message) +end + +function SSLConnectionObject:logErr(message) + print("[ERROR]: " .. message) +end + +function SSLConnectionObject:logCritical(message) + print("[CRITICAL]: " .. message) +end + +-- SSL-related methods +function SSLConnectionObject:peerCertificatePresented() + return true -- Simulate that the peer certificate is always presented +end + +function SSLConnectionObject:peerCertificateValidated() + return false -- Simulate no validation for TLS session resumption +end + +function SSLConnectionObject:uriSanLocalCertificate() + local t = {} + for i = 1, 1000 do + t[i] = "san" + end + return t +end + +function SSLConnectionObject:sha256PeerCertificateDigest() + return "abcdef1234567890" -- Simulated SHA256 digest +end + +function SSLConnectionObject:serialNumberPeerCertificate() + return "123456789" -- Simulated serial number +end + +function SSLConnectionObject:issuerPeerCertificate() + return "CN=Mock Issuer, O=Example Org" -- Simulated issuer +end + +function SSLConnectionObject:subjectPeerCertificate() + return "CN=Mock Subject, O=Example Org" -- Simulated subject +end + +function SSLConnectionObject:parsedSubjectPeerCertificate() + return { + commonName = function() return "Mock Subject" end, + organizationName = function() return "Example Org" end, + } +end + +function SSLConnectionObject:uriSanPeerCertificate() + return {"uri1", "uri2"} -- Example SAN URIs for peer certificate +end + +function SSLConnectionObject:subjectLocalCertificate() + return "CN=Local Cert, O=Example Org" -- Simulated local certificate subject +end + +function SSLConnectionObject:urlEncodedPemEncodedPeerCertificate() + return "PEM_ENCODED_PEER_CERT" -- Simulated PEM encoding +end + +function SSLConnectionObject:urlEncodedPemEncodedPeerCertificateChain() + return "PEM_ENCODED_CERT_CHAIN" -- Simulated PEM chain +end + +function SSLConnectionObject:dnsSansPeerCertificate() + local t = {} + for i = 1, 1000 do + t[i] = "example.com" + end + return t +end + +function SSLConnectionObject:dnsSansLocalCertificate() + local t = {} + for i = 1, 1000 do + t[i] = "local.com" + end + return t +end + +function SSLConnectionObject:oidsPeerCertificate() + local t = {} + for i = 1, 1000 do + t[i] = "1.2.840.113549.1.1.1" + end + return t +end + +function SSLConnectionObject:oidsLocalCertificate() + local t = {} + for i = 1, 1000 do + t[i] = "1.2.840.113549.1.1.1" + end + return t +end + +function SSLConnectionObject:validFromPeerCertificate() + return 1672531200 +end + +function SSLConnectionObject:expirationPeerCertificate() + return 2724608000 +end + +function SSLConnectionObject:sessionId() + return "abcdef1234567890abcdef1234567890" +end + +function SSLConnectionObject:ciphersuiteId() + return "0x1301" +end + +function SSLConnectionObject:ciphersuiteString() + return "TLS_AES_128_GCM_SHA256" +end + +function SSLConnectionObject:tlsVersion() + return "TLSv1.3" +end + +-- Connection object +local Connection = {} + +-- Logging methods for Connection +function Connection:logTrace(message) + print("[TRACE]: " .. message) +end + +function Connection:logDebug(message) + print("[DEBUG]: " .. message) +end + +function Connection:logInfo(message) + print("[INFO]: " .. message) +end + +function Connection:logWarn(message) + print("[WARN]: " .. message) +end + +function Connection:logErr(message) + print("[ERROR]: " .. message) +end + +function Connection:logCritical(message) + print("[CRITICAL]: " .. message) +end + +function Connection:ssl() + return SSLConnectionObject +end + +local DynamicMetadata = { + data = { + key1 = "value1", + key2 = "value2" + } +} + +setmetatable(DynamicMetadata, { + __pairs = function(self) + return pairs(self.data) + end, + __index = function(self, key) + return self.data[key] + end +}) + +-- Logging methods +function DynamicMetadata:logTrace(message) + print("[TRACE]: " .. message) +end + +function DynamicMetadata:logDebug(message) + print("[DEBUG]: " .. message) +end + +function DynamicMetadata:logInfo(message) + print("[INFO]: " .. message) +end + +function DynamicMetadata:logWarn(message) + print("[WARN]: " .. message) +end + +function DynamicMetadata:logErr(message) + print("[ERROR]: " .. message) +end + +function DynamicMetadata:logCritical(message) + print("[CRITICAL]: " .. message) +end + +function DynamicMetadata:get(filterName) + return "get_result" +end + +function DynamicMetadata:set(filterName, key, value) +end + +-- ConnectionStreamInfo Object +local ConnectionStreamInfo = {} + +-- Logging methods for ConnectionStreamInfo +function ConnectionStreamInfo:logTrace(message) + print("[TRACE]: " .. message) +end + +function ConnectionStreamInfo:logDebug(message) + print("[DEBUG]: " .. message) +end + +function ConnectionStreamInfo:logInfo(message) + print("[INFO]: " .. message) +end + +function ConnectionStreamInfo:logWarn(message) + print("[WARN]: " .. message) +end + +function ConnectionStreamInfo:logErr(message) + print("[ERROR]: " .. message) +end + +function ConnectionStreamInfo:logCritical(message) + print("[CRITICAL]: " .. message) +end + +function ConnectionStreamInfo:dynamicMetadata() + return DynamicMetadata +end + +-- StreamInfo Object +local StreamInfo = {} + +function StreamInfo:logTrace(message) + print("[TRACE]: " .. message) +end + +function StreamInfo:logDebug(message) + print("[DEBUG]: " .. message) +end + +function StreamInfo:logInfo(message) + print("[INFO]: " .. message) +end + +function StreamInfo:logWarn(message) + print("[WARN]: " .. message) +end + +function StreamInfo:logErr(message) + print("[ERROR]: " .. message) +end + +function StreamInfo:logCritical(message) + print("[CRITICAL]: " .. message) +end + +function StreamInfo:protocol() + return "HTTP/2" +end + +function StreamInfo:routeName() + return "example-route" +end + +function StreamInfo:virtualClusterName() + return "example-virtual-cluster" +end + +function StreamInfo:downstreamDirectLocalAddress() + return "127.0.0.1:8080" +end + +function StreamInfo:downstreamLocalAddress() + return "192.168.1.100:8080" +end + +function StreamInfo:downstreamDirectRemoteAddress() + return "192.168.1.50:5050" +end + +function StreamInfo:downstreamRemoteAddress() + return "10.0.0.1:5050" +end + +function StreamInfo:dynamicMetadata() + return DynamicMetadata +end + +function StreamInfo:downstreamSslConnection() + return SSLConnectionObject +end + +function StreamInfo:requestedServerName() + return "example-server" +end + +-- Metadata Object +local Metadata = { + data = { + key1 = "value1", + key2 = "value2" + } +} + +setmetatable(Metadata, { + __pairs = function(self) + return pairs(self.data) + end, + __index = function(self, key) + return self.data[key] + end +}) + +-- Logging Methods +function Metadata:logTrace(message) + print("[TRACE]: " .. message) +end + +function Metadata:logDebug(message) + print("[DEBUG]: " .. message) +end + +function Metadata:logInfo(message) + print("[INFO]: " .. message) +end + +function Metadata:logWarn(message) + print("[WARN]: " .. message) +end + +function Metadata:logErr(message) + print("[ERROR]: " .. message) +end + +function Metadata:logCritical(message) + print("[CRITICAL]: " .. message) +end + +function Metadata:get(key) + return "get_example" +end + +-- Buffer Object +local Buffer = {} + +-- Logging Methods +function Buffer:logTrace(message) + print("[TRACE]: " .. message) +end + +function Buffer:logDebug(message) + print("[DEBUG]: " .. message) +end + +function Buffer:logInfo(message) + print("[INFO]: " .. message) +end + +function Buffer:logWarn(message) + print("[WARN]: " .. message) +end + +function Buffer:logErr(message) + print("[ERROR]: " .. message) +end + +function Buffer:logCritical(message) + print("[CRITICAL]: " .. message) +end + +function Buffer:length() + return 10 +end + +function Buffer:getBytes(index, length) + return "example_bytes" +end + +function Buffer:setBytes(setTo) +end + +-- Headers Object +local Headers = { + data = { + key1 = "value1", + key2 = "value2" + } +} + +setmetatable(Headers, { + __pairs = function(self) + return pairs(self.data) + end, + __index = function(self, key) + return self.data[key] + end +}) + +-- Logging Methods +function Headers:logTrace(message) + print("[TRACE]: " .. message) +end + +function Headers:logDebug(message) + print("[DEBUG]: " .. message) +end + +function Headers:logInfo(message) + print("[INFO]: " .. message) +end + +function Headers:logWarn(message) + print("[WARN]: " .. message) +end + +function Headers:logErr(message) + print("[ERROR]: " .. message) +end + +function Headers:logCritical(message) + print("[CRITICAL]: " .. message) +end + +function Headers:add(key, value) +end + +function Headers:get(key) + return "example_value" +end + +function Headers:getAtIndex(key, index) + return "example_value" +end + +function Headers:getNumValues(key) + return 1 +end + +function Headers:remove(key) +end + +function Headers:replace(key, value) +end + +function Headers:setHttp1ReasonPhrase(reasonPhrase) +end + +function Headers:getHttp1ReasonPhrase() + return "reason" +end + +-- StreamHandle Object +local StreamHandle = { +data = {Buffer} +} + +-- Logging Methods (with internal logging mechanism) +function StreamHandle:logTrace(message) + print("[TRACE]: " .. message) +end + +function StreamHandle:logDebug(message) + print("[DEBUG]: " .. message) +end + +function StreamHandle:logInfo(message) + print("[INFO]: " .. message) +end + +function StreamHandle:logWarn(message) + print("[WARN]: " .. message) +end + +function StreamHandle:logErr(message) + print("[ERROR]: " .. message) +end + +function StreamHandle:logCritical(message) + print("[CRITICAL]: " .. message) +end + +function StreamHandle:headers() + return Headers +end + +function StreamHandle:body(always_wrap_body) + return "body" +end + +function StreamHandle:bodyChunks() + local index = 0 + local chunks = self.data or {} + + return function() + index = index + 1 + local chunk = chunks[index] + if chunk then + return chunk + end + end +end + +function StreamHandle:trailers() + return Headers +end + +function StreamHandle:httpCall(cluster, headers, body, timeout_ms, asynchronous) + self:logInfo("Making HTTP call to cluster: " .. cluster) + return headers, body +end + +function StreamHandle:respond(headers, body) + self:logInfo("Responding with status: " .. headers[":status"]) +end + +function StreamHandle:metadata() + return Metadata +end + +function StreamHandle:streamInfo() + return StreamInfo +end + +function StreamHandle:connection() + return Connection +end + +function StreamHandle:connectionStreamInfo() + return ConnectionStreamInfo +end + +function StreamHandle:setUpstreamOverrideHost(host, strict) +end + +function StreamHandle:importPublicKey(keyder, keyderLength) + return "mocked_public_key" +end + +function StreamHandle:verifySignature(hashFunction, pubkey, signature, signatureLength, data, dataLength) + return true, "" +end + +function StreamHandle:base64Escape(inputString) + return inputString +end + +function StreamHandle:timestamp(format) + return os.time() * 1000 +end + +function StreamHandle:timestampString(resolution) + return os.date("!%Y-%m-%dT%H:%M:%SZ", os.time()) +end \ No newline at end of file diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.in.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.in.yaml new file mode 100644 index 00000000000..e47c317a3cd --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.in.yaml @@ -0,0 +1,85 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/foo" + backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-2 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/bar" + backendRefs: + - name: service-1 + port: 8080 +envoyextensionpolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway # This policy should attach httproute-2 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + lua: + - type: Inline + inline: "function envoy_on_request(request_handle) + request_handle:logInfo('Goodbye.') + end" +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: default + name: policy-for-http-route # Invalid Lua + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + lua: + - type: Inline + inline: "function envoy_on_response(response_handle) + response_handle:UnknownApi() + end" diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.out.yaml new file mode 100644 index 00000000000..6fbb154308e --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.out.yaml @@ -0,0 +1,276 @@ +envoyExtensionPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + creationTimestamp: null + name: policy-for-http-route + namespace: default + spec: + lua: + - inline: function envoy_on_response(response_handle) response_handle:UnknownApi() + end + type: Inline + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: "Lua: validation failed for lua body in policy with name envoyextensionpolicy/default/policy-for-http-route/lua/0: + failed to mock run envoy_on_response: :614: attempt to call a non-function + object\nstack traceback:\n\t:614: in function 'envoy_on_response'\n\t:615: + in main chunk\n\t[G]: ?." + reason: Invalid + status: "False" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + spec: + lua: + - inline: function envoy_on_request(request_handle) request_handle:logInfo('Goodbye.') + end + type: Inline + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: 'This policy is being overridden by other envoyExtensionPolicies + for these routes: [default/httproute-1]' + reason: Overridden + status: "True" + type: Overridden + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 2 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /foo + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-2 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /bar + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + directResponse: + statusCode: 500 + envoyExtensions: + luas: + - Body: function envoy_on_request(request_handle) request_handle:logInfo('Goodbye.') + end + Name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/0 + hostname: www.example.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /foo + - destination: + name: httproute/default/httproute-2/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + envoyExtensions: + luas: + - Body: function envoy_on_request(request_handle) request_handle:logInfo('Goodbye.') + end + Name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/0 + hostname: www.example.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-2 + namespace: default + name: httproute/default/httproute-2/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /bar diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-configmap.in.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-configmap.in.yaml new file mode 100644 index 00000000000..6e3f21e5c3d --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-configmap.in.yaml @@ -0,0 +1,106 @@ +configmaps: +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm-policy-for-gateway + namespace: envoy-gateway + data: + lua: | + function envoy_on_request(request_handle) + request_handle:logInfo('Goodbye.') + end +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm-policy-for-http-route + namespace: default + data: + lua: | + function envoy_on_response(response_handle) + response_handle:logWarn('Goodbye.') + end +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/foo" + backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-2 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/bar" + backendRefs: + - name: service-1 + port: 8080 +envoyextensionpolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway # This policy should attach httproute-2 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + lua: + - type: ValueRef + valueRef: + name: cm-policy-for-gateway + kind: ConfigMap +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: default + name: policy-for-http-route # This policy should attach httproute-1 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + lua: + - type: ValueRef + valueRef: + name: cm-policy-for-http-route + kind: ConfigMap diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-configmap.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-configmap.out.yaml new file mode 100644 index 00000000000..73154370aef --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-configmap.out.yaml @@ -0,0 +1,279 @@ +envoyExtensionPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + creationTimestamp: null + name: policy-for-http-route + namespace: default + spec: + lua: + - type: ValueRef + valueRef: + group: "" + kind: ConfigMap + name: cm-policy-for-http-route + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + spec: + lua: + - type: ValueRef + valueRef: + group: "" + kind: ConfigMap + name: cm-policy-for-gateway + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: 'This policy is being overridden by other envoyExtensionPolicies + for these routes: [default/httproute-1]' + reason: Overridden + status: "True" + type: Overridden + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 2 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /foo + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-2 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /bar + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + envoyExtensions: + luas: + - Body: | + function envoy_on_response(response_handle) + response_handle:logWarn('Goodbye.') + end + Name: envoyextensionpolicy/default/policy-for-http-route/lua/0 + hostname: www.example.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /foo + - destination: + name: httproute/default/httproute-2/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + envoyExtensions: + luas: + - Body: | + function envoy_on_request(request_handle) + request_handle:logInfo('Goodbye.') + end + Name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/0 + hostname: www.example.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-2 + namespace: default + name: httproute/default/httproute-2/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /bar diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua.in.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua.in.yaml new file mode 100644 index 00000000000..65dd36d9f1a --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua.in.yaml @@ -0,0 +1,85 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/foo" + backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-2 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/bar" + backendRefs: + - name: service-1 + port: 8080 +envoyextensionpolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway # This policy should attach httproute-2 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + lua: + - type: Inline + inline: "function envoy_on_request(request_handle) + request_handle:logInfo('Goodbye.') + end" +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: default + name: policy-for-http-route # This policy should attach httproute-1 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + lua: + - type: Inline + inline: "function envoy_on_response(response_handle) + response_handle:logWarn('Goodbye.') + end" diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua.out.yaml new file mode 100644 index 00000000000..6f22ad9811b --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua.out.yaml @@ -0,0 +1,271 @@ +envoyExtensionPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + creationTimestamp: null + name: policy-for-http-route + namespace: default + spec: + lua: + - inline: function envoy_on_response(response_handle) response_handle:logWarn('Goodbye.') + end + type: Inline + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + spec: + lua: + - inline: function envoy_on_request(request_handle) request_handle:logInfo('Goodbye.') + end + type: Inline + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: 'This policy is being overridden by other envoyExtensionPolicies + for these routes: [default/httproute-1]' + reason: Overridden + status: "True" + type: Overridden + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 2 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /foo + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-2 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /bar + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + envoyExtensions: + luas: + - Body: function envoy_on_response(response_handle) response_handle:logWarn('Goodbye.') + end + Name: envoyextensionpolicy/default/policy-for-http-route/lua/0 + hostname: www.example.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /foo + - destination: + name: httproute/default/httproute-2/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + envoyExtensions: + luas: + - Body: function envoy_on_request(request_handle) request_handle:logInfo('Goodbye.') + end + Name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/0 + hostname: www.example.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-2 + namespace: default + name: httproute/default/httproute-2/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /bar diff --git a/internal/ir/xds.go b/internal/ir/xds.go index f428297ace5..ca1f0ebdb66 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -862,6 +862,8 @@ type EnvoyExtensionFeatures struct { ExtProcs []ExtProc `json:"extProcs,omitempty" yaml:"extProcs,omitempty"` // Wasm extensions Wasms []Wasm `json:"wasms,omitempty" yaml:"wasms,omitempty"` + // Lua extensions + Luas []Lua `json:"luas,omitempty" yaml:"luas,omitempty"` } // UnstructuredRef holds unstructured data for an arbitrary k8s resource introduced by an extension @@ -2792,6 +2794,16 @@ type ExtProc struct { AllowModeOverride bool `json:"allowModeOverride,omitempty" yaml:"allowModeOverride,omitempty"` } +// Lua holds the information associated with Lua extensions +// +k8s:deepcopy-gen=true +type Lua struct { + // Name is a unique name for the LUa configuration. + // The xds translator only generates one Lua filter for each unique name + Name string + // Body is the Lua source body + Body *string +} + // Wasm holds the information associated with the Wasm extensions. // +k8s:deepcopy-gen=true type Wasm struct { diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 3c42375daa5..541f28f750b 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -888,6 +888,13 @@ func (in *EnvoyExtensionFeatures) DeepCopyInto(out *EnvoyExtensionFeatures) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Luas != nil { + in, out := &in.Luas, &out.Luas + *out = make([]Lua, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvoyExtensionFeatures. @@ -2132,6 +2139,26 @@ func (in *LocalRateLimit) DeepCopy() *LocalRateLimit { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Lua) DeepCopyInto(out *Lua) { + *out = *in + if in.Body != nil { + in, out := &in.Body, &out.Body + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Lua. +func (in *Lua) DeepCopy() *Lua { + if in == nil { + return nil + } + out := new(Lua) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Metrics) DeepCopyInto(out *Metrics) { *out = *in diff --git a/internal/xds/translator/lua.go b/internal/xds/translator/lua.go new file mode 100644 index 00000000000..78d4ea75a91 --- /dev/null +++ b/internal/xds/translator/lua.go @@ -0,0 +1,118 @@ +// 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 translator + +import ( + "errors" + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/xds/types" + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + luafilterv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/lua/v3" + hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + "google.golang.org/protobuf/types/known/anypb" +) + +func init() { + registerHTTPFilter(&lua{}) +} + +type lua struct{} + +var _ httpFilter = &lua{} + +// patchHCM builds and appends the lua Filters to the HTTP Connection Manager +// if applicable, and it does not already exist. +// Note: this method creates a lua filter for each route that contains a lua config. +func (*lua) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error { + var errs error + + if mgr == nil { + return errors.New("hcm is nil") + } + if irListener == nil { + return errors.New("ir listener is nil") + } + + for _, route := range irListener.Routes { + if !routeContainsLua(route) { + continue + } + for _, ep := range route.EnvoyExtensions.Luas { + if hcmContainsFilter(mgr, luaFilterName(ep)) { + continue + } + filter, err := buildHCMLuaPerRouteFilter(ep) + if err != nil { + errs = errors.Join(errs, err) + continue + } + mgr.HttpFilters = append(mgr.HttpFilters, filter) + } + } + return errs +} + +// buildHCMLuaPerRouteFilter returns a lua HTTP filter from the provided IR HTTPRoute. +func buildHCMLuaPerRouteFilter(lua ir.Lua) (*hcmv3.HttpFilter, error) { + var ( + luaProto *luafilterv3.LuaPerRoute + luaAny *anypb.Any + err error + ) + + if luaProto, err = luaPerRouteConfig(lua); err != nil { + return nil, err + } + if err = luaProto.ValidateAll(); err != nil { + return nil, err + } + if luaAny, err = anypb.New(luaProto); err != nil { + return nil, err + } + + return &hcmv3.HttpFilter{ + Name: luaFilterName(lua), + ConfigType: &hcmv3.HttpFilter_TypedConfig{ + TypedConfig: luaAny, + }, + }, nil +} + +func luaFilterName(lua ir.Lua) string { + return perRouteFilterName(egv1a1.EnvoyFilterWasm, lua.Name) +} + +func luaPerRouteConfig(lua ir.Lua) (*luafilterv3.LuaPerRoute, error) { + return &luafilterv3.LuaPerRoute{ + Override: &luafilterv3.LuaPerRoute_SourceCode{ + SourceCode: &corev3.DataSource{ + Specifier: &corev3.DataSource_InlineString{ + InlineString: *lua.Body, + }, + }, + }, + }, nil +} + +// routeContainsLua returns true if Luas exists for the provided route. +func routeContainsLua(irRoute *ir.HTTPRoute) bool { + if irRoute == nil { + return false + } + return irRoute.EnvoyExtensions != nil && len(irRoute.EnvoyExtensions.Luas) > 0 +} + +// patchResources patches the cluster resources for the http lua code source. +func (*lua) patchResources(_ *types.ResourceVersionTable, _ []*ir.HTTPRoute) error { + return nil +} + +// patchRoute patches the provided route with the lua config if applicable. +func (*lua) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute) error { + return nil +} diff --git a/internal/xds/translator/testdata/in/xds-ir/lua.yaml b/internal/xds/translator/testdata/in/xds-ir/lua.yaml new file mode 100644 index 00000000000..9e057b90b3f --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/lua.yaml @@ -0,0 +1,59 @@ +http: +- address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: www.example.com + isHTTP2: false + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /foo + envoyExtensions: + luas: + - body: function envoy_on_request(request_handle) + request_handle:logInfo('Goodbye.') + end + name: envoyextensionpolicy/default/policy-for-http-route/lua/0 + - destination: + name: httproute/default/httproute-2/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: www.example.com + isHTTP2: false + name: httproute/default/httproute-2/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /bar + envoyExtensions: + luas: + - body: function envoy_on_response(response_handle) + response_handle:logWarn('Goodbye.') + end + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/0 + - body: function envoy_on_response(response_handle) + response_handle:logError('Hello.') + end + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/1 diff --git a/internal/xds/translator/testdata/out/xds-ir/lua.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/lua.clusters.yaml new file mode 100644 index 00000000000..ba27dfd9d28 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/lua.clusters.yaml @@ -0,0 +1,34 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/default/httproute-1/rule/0 + ignoreHealthOnHostRemoval: true + lbPolicy: LEAST_REQUEST + name: httproute/default/httproute-1/rule/0 + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/default/httproute-2/rule/0 + ignoreHealthOnHostRemoval: true + lbPolicy: LEAST_REQUEST + name: httproute/default/httproute-2/rule/0 + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/lua.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/lua.endpoints.yaml new file mode 100644 index 00000000000..05442a9a15b --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/lua.endpoints.yaml @@ -0,0 +1,24 @@ +- clusterName: httproute/default/httproute-1/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 7.7.7.7 + portValue: 8080 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: httproute/default/httproute-1/rule/0/backend/0 +- clusterName: httproute/default/httproute-2/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 7.7.7.7 + portValue: 8080 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: httproute/default/httproute-2/rule/0/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/lua.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/lua.listeners.yaml new file mode 100644 index 00000000000..c7d16568dc3 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/lua.listeners.yaml @@ -0,0 +1,52 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.wasm/envoyextensionpolicy/default/policy-for-http-route/lua/0 + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute + sourceCode: + inlineString: function envoy_on_request(request_handle) request_handle:logInfo('Goodbye.') + end + - name: envoy.filters.http.wasm/envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/0 + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute + sourceCode: + inlineString: function envoy_on_response(response_handle) response_handle:logWarn('Goodbye.') + end + - name: envoy.filters.http.wasm/envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/1 + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute + sourceCode: + inlineString: function envoy_on_response(response_handle) response_handle:logError('Hello.') + end + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: envoy-gateway/gateway-1/http + serverHeaderTransformation: PASS_THROUGH + statPrefix: http-10080 + useRemoteAddress: true + name: envoy-gateway/gateway-1/http + name: envoy-gateway/gateway-1/http + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/lua.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/lua.routes.yaml new file mode 100644 index 00000000000..2787f6819af --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/lua.routes.yaml @@ -0,0 +1,21 @@ +- ignorePortInHostMatching: true + name: envoy-gateway/gateway-1/http + virtualHosts: + - domains: + - www.example.com + name: envoy-gateway/gateway-1/http/www_example_com + routes: + - match: + pathSeparatedPrefix: /foo + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + route: + cluster: httproute/default/httproute-1/rule/0 + upgradeConfigs: + - upgradeType: websocket + - match: + pathSeparatedPrefix: /bar + name: httproute/default/httproute-2/rule/0/match/0/www_example_com + route: + cluster: httproute/default/httproute-2/rule/0 + upgradeConfigs: + - upgradeType: websocket diff --git a/test/e2e/testdata/lua-http-inline.yaml b/test/e2e/testdata/lua-http-inline.yaml new file mode 100644 index 00000000000..9dc3da5f6e4 --- /dev/null +++ b/test/e2e/testdata/lua-http-inline.yaml @@ -0,0 +1,82 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-with-lua-always-deny + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + hostnames: ["www.example.com"] + rules: + - matches: + - path: + type: PathPrefix + value: /always-deny-http + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-with-lua-always-allow + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + hostnames: ["www.example.com"] + rules: + - matches: + - path: + type: PathPrefix + value: /always-allow-http + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-without-lua + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + hostnames: ["www.example.com"] + rules: + - matches: + - path: + type: PathPrefix + value: /no-lua + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyExtensionPolicy +metadata: + name: http-lua-always-deny + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-with-lua-always-deny + lua: + - type: Inline + inline: "function envoy_on_request(request_handle)\r\n request_handle:respond(\r\n {[\":status\"] = \"403\",\r\n [\"x-custom-lua\"] = \"processed\"},\r\n \"nope\")\r\n end" +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyExtensionPolicy +metadata: + name: http-lua-always-allow + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-with-lua-always-allow + lua: + - type: Inline + inline: "function envoy_on_request(request_handle)\r\n request_handle:respond(\r\n {[\":status\"] = \"200\",\r\n [\"x-custom-lua\"] = \"processed\"},\r\n \"yup\")\r\n end" diff --git a/test/e2e/tests/lua_http.go b/test/e2e/tests/lua_http.go new file mode 100644 index 00000000000..a394527d588 --- /dev/null +++ b/test/e2e/tests/lua_http.go @@ -0,0 +1,82 @@ +// 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. + +//go:build e2e + +package tests + +import ( + "testing" + + "k8s.io/apimachinery/pkg/types" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + + "github.com/envoyproxy/gateway/internal/gatewayapi" + "github.com/envoyproxy/gateway/internal/gatewayapi/resource" +) + +func init() { + ConformanceTests = append(ConformanceTests, HTTPLuaTest) +} + +// HTTPLuaTest tests Lua extension for a http route with HTTP Lua configured. +var HTTPLuaTest = suite.ConformanceTest{ + ShortName: "LuaHTTPInlineSource", + Description: "Test Lua extension that always allows/denies", + Manifests: []string{"testdata/lua-http-inline.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("http route with always allow lua", func(t *testing.T) { + testLuaHTTP(t, suite, "http-with-lua-always-allow", "http-lua-always-allow", "/always-allow-http", 200) + }) + + t.Run("http route with always deny lua", func(t *testing.T) { + testLuaHTTP(t, suite, "http-with-lua-always-deny", "http-lua-always-deny", "/always-deny-http", 403) + }) + }, +} + +func testLuaHTTP(t *testing.T, suite *suite.ConformanceTestSuite, route, eep, path string, expectedStatus int) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: route, Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + ancestorRef := gwapiv1a2.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(resource.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + } + EnvoyExtensionPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: eep, Namespace: ns}, suite.ControllerName, ancestorRef) + + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Host: "www.example.com", + Path: path, + }, + + ExpectedRequest: &http.ExpectedRequest{ + Request: http.Request{ + Host: "", + Method: "", + Path: "", + Headers: nil, + }, + }, + Namespace: "", + Response: http.Response{ + StatusCode: expectedStatus, + Headers: map[string]string{ + "x-custom-lua": "processed", // response header added by lua + }, + }, + } + + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) +}