diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go index 8b9c83e17e3..a423d3fb2ea 100644 --- a/api/v1alpha1/envoyproxy_types.go +++ b/api/v1alpha1/envoyproxy_types.go @@ -114,6 +114,8 @@ type EnvoyProxySpec struct { // // - envoy.filters.http.stateful_session // + // - envoy.filters.http.lua + // // - envoy.filters.http.ext_proc // // - envoy.filters.http.wasm @@ -195,7 +197,7 @@ type FilterPosition struct { } // EnvoyFilter defines the type of Envoy HTTP filter. -// +kubebuilder:validation:Enum=envoy.filters.http.health_check;envoy.filters.http.fault;envoy.filters.http.cors;envoy.filters.http.ext_authz;envoy.filters.http.api_key_auth;envoy.filters.http.basic_auth;envoy.filters.http.oauth2;envoy.filters.http.jwt_authn;envoy.filters.http.stateful_session;envoy.filters.http.ext_proc;envoy.filters.http.wasm;envoy.filters.http.rbac;envoy.filters.http.local_ratelimit;envoy.filters.http.ratelimit;envoy.filters.http.custom_response;envoy.filters.http.compressor +// +kubebuilder:validation:Enum=envoy.filters.http.health_check;envoy.filters.http.fault;envoy.filters.http.cors;envoy.filters.http.ext_authz;envoy.filters.http.api_key_auth;envoy.filters.http.basic_auth;envoy.filters.http.oauth2;envoy.filters.http.jwt_authn;envoy.filters.http.stateful_session;envoy.filters.http.lua;envoy.filters.http.ext_proc;envoy.filters.http.wasm;envoy.filters.http.rbac;envoy.filters.http.local_ratelimit;envoy.filters.http.ratelimit;envoy.filters.http.custom_response;envoy.filters.http.compressor type EnvoyFilter string const ( @@ -233,6 +235,9 @@ const ( // EnvoyFilterWasm defines the Envoy HTTP WebAssembly filter. EnvoyFilterWasm EnvoyFilter = "envoy.filters.http.wasm" + // EnvoyFilterLua defines the Envoy HTTP Lua filter. + EnvoyFilterLua EnvoyFilter = "envoy.filters.http.lua" + // EnvoyFilterRBAC defines the Envoy RBAC filter. EnvoyFilterRBAC EnvoyFilter = "envoy.filters.http.rbac" diff --git a/api/v1alpha1/lua_types.go b/api/v1alpha1/lua_types.go index 40e1bd9667d..9c938800619 100644 --- a/api/v1alpha1/lua_types.go +++ b/api/v1alpha1/lua_types.go @@ -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' && (self.group == 'v1' || self.group == '')",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"` diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml index 6036db8264d..08fb470c03c 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml @@ -1069,8 +1069,8 @@ spec: type: object x-kubernetes-validations: - message: Only a reference to an object of kind ConfigMap belonging - to default core API group is supported. - rule: self.kind == 'ConfigMap' && (!has(self.group) || self.group + to default v1 API group is supported. + rule: self.kind == 'ConfigMap' && (self.group == 'v1' || self.group == '') required: - type diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml index 391fb3147a7..5e2bfab7df1 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -300,6 +300,8 @@ spec: - envoy.filters.http.stateful_session + - envoy.filters.http.lua + - envoy.filters.http.ext_proc - envoy.filters.http.wasm @@ -333,6 +335,7 @@ spec: - envoy.filters.http.oauth2 - envoy.filters.http.jwt_authn - envoy.filters.http.stateful_session + - envoy.filters.http.lua - envoy.filters.http.ext_proc - envoy.filters.http.wasm - envoy.filters.http.rbac @@ -355,6 +358,7 @@ spec: - envoy.filters.http.oauth2 - envoy.filters.http.jwt_authn - envoy.filters.http.stateful_session + - envoy.filters.http.lua - envoy.filters.http.ext_proc - envoy.filters.http.wasm - envoy.filters.http.rbac @@ -375,6 +379,7 @@ spec: - envoy.filters.http.oauth2 - envoy.filters.http.jwt_authn - envoy.filters.http.stateful_session + - envoy.filters.http.lua - envoy.filters.http.ext_proc - envoy.filters.http.wasm - envoy.filters.http.rbac diff --git a/go.mod b/go.mod index 1e7bccefe71..7ad150ece56 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 14b901efb7b..61079830771 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..320d54120cd 100644 --- a/internal/gatewayapi/envoyextensionpolicy.go +++ b/internal/gatewayapi/envoyextensionpolicy.go @@ -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" @@ -293,6 +295,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForRoute( ) error { var ( wasms []ir.Wasm + luas []ir.Lua err, errs error ) @@ -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) @@ -332,6 +340,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForRoute( r.EnvoyExtensions = &ir.EnvoyExtensionFeatures{ ExtProcs: extProcs, Wasms: wasms, + Luas: luas, } } } @@ -352,6 +361,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForGateway( var ( extProcs []ir.ExtProc wasms []ir.Wasm + luas []ir.Lua err, errs error ) @@ -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) + } irKey := t.getIRKey(gateway.Gateway) // Should exist since we've validated this @@ -395,6 +409,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForGateway( r.EnvoyExtensions = &ir.EnvoyExtensionFeatures{ ExtProcs: extProcs, Wasms: wasms, + Luas: luas, } } } @@ -402,6 +417,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 luaCode *string + var err error + if lua.Type == egv1a1.LuaValueTypeValueRef { + luaCode, err = getLuaBodyFromLocalObjectReference(lua.ValueRef, resources, policy.Namespace) + } else { + luaCode = lua.Inline + } + if err != nil { + return nil, err + } + if err = luavalidator.NewLuaValidator(*luaCode).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, + Code: luaCode, + }, 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 in namespace %s", valueRef.Name, policyNs) + } +} + func (t *Translator) buildExtProcs(policy *egv1a1.EnvoyExtensionPolicy, resources *resource.Resources, envoyProxy *egv1a1.EnvoyProxy) ([]ir.ExtProc, error) { var extProcIRList []ir.ExtProc @@ -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, diff --git a/internal/gatewayapi/luavalidator/lua_validator.go b/internal/gatewayapi/luavalidator/lua_validator.go new file mode 100644 index 00000000000..11694fbbe59 --- /dev/null +++ b/internal/gatewayapi/luavalidator/lua_validator.go @@ -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 { + code string +} + +// NewLuaValidator returns a LuaValidator for user provided Lua code +func NewLuaValidator(code string) *LuaValidator { + return &LuaValidator{ + code: code, + } +} + +// Validate runs all validations for the LuaValidator +func (l *LuaValidator) Validate() error { + if !strings.Contains(l.code, "envoy_on_request") && !strings.Contains(l.code, "envoy_on_response") { + return fmt.Errorf("expected one of envoy_on_request() or envoy_on_response() to be defined") + } + if strings.Contains(l.code, "envoy_on_request") { + if err := l.runLua(string(mockData) + "\n" + l.code + "\nenvoy_on_request(StreamHandle)"); err != nil { + return fmt.Errorf("failed to mock run envoy_on_request: %w", err) + } + } + if strings.Contains(l.code, "envoy_on_response") { + if err := l.runLua(string(mockData) + "\n" + l.code + "\nenvoy_on_response(StreamHandle)"); err != nil { + return fmt.Errorf("failed to mock run envoy_on_response: %w", err) + } + } + return nil +} + +// runLua interprets and runs the provided Lua code in runtime +func (l *LuaValidator) runLua(code string) error { + L := lua.NewState() + defer L.Close() + if err := L.DoString(code); 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..cb3ba754892 --- /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 + code string + expectedErrSubstring string + } + tests := []args{ + { + name: "empty body", + code: "", + expectedErrSubstring: "expected one of envoy_on_request() or envoy_on_response() to be defined", + }, + { + name: "logInfo: envoy_on_response", + code: `function envoy_on_response(response_handle) + response_handle:logInfo("Goodbye.") + end`, + expectedErrSubstring: "", + }, + { + name: "logInfo: envoy_on_request", + code: `function envoy_on_request(request_handle) + request_handle:logInfo("Goodbye.") + end`, + expectedErrSubstring: "", + }, + { + name: "stream:headers:Get", + code: `function envoy_on_request(request_handle) + request_handle:headers():get("foo") + end`, + expectedErrSubstring: "", + }, + { + name: "stream:connection:ssl:expirationPeerCertificate", + code: `function envoy_on_request(request_handle) + request_handle:connection():ssl():expirationPeerCertificate() + end`, + expectedErrSubstring: "", + }, + { + name: "stream:metadata:pairs", + code: `function envoy_on_request(request_handle) + for key, value in pairs(request_handle:metadata()) do + print(key, value) + end + end`, + expectedErrSubstring: "", + }, + { + name: "stream:httpCall", + code: `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", + code: `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", + code: `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", + code: `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.code) + 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..cfad596e793 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.out.yaml @@ -0,0 +1,281 @@ +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: + - Code: 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: + - Code: 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 + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 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..c2825cd1194 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-configmap.in.yaml @@ -0,0 +1,229 @@ +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 +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm-policy-for-http-route-no-key + namespace: default + data: + key: | + function envoy_on_response(response_handle) + response_handle:logWarn('Goodbye.') + end +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm-policy-for-http-route-empty + namespace: default + data: {} +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 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-3 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/bar3" + backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-4 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/bar4" + backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-5 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/bar5" + 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 + group: v1 +- 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 + group: v1 +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: default + name: policy-for-http-route-4 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-3 + lua: + - type: ValueRef + valueRef: + name: cm-policy-for-http-route-no-exist + kind: ConfigMap + group: v1 +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: default + name: policy-for-http-route-4 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-4 + lua: + - type: ValueRef + valueRef: + name: cm-policy-for-http-route-no-key + kind: ConfigMap + group: v1 +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: default + name: policy-for-http-route-5 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-5 + lua: + - type: ValueRef + valueRef: + name: cm-policy-for-http-route-empty + kind: ConfigMap + group: v1 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..6ef4cb93a7b --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-configmap.out.yaml @@ -0,0 +1,551 @@ +envoyExtensionPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + creationTimestamp: null + name: policy-for-http-route + namespace: default + spec: + lua: + - type: ValueRef + valueRef: + group: v1 + 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-http-route-4 + namespace: default + spec: + lua: + - type: ValueRef + valueRef: + group: v1 + kind: ConfigMap + name: cm-policy-for-http-route-no-exist + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-3 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: 'Lua: can''t find the referenced configmap cm-policy-for-http-route-no-exist + in namespace default.' + 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-http-route-5 + namespace: default + spec: + lua: + - type: ValueRef + valueRef: + group: v1 + kind: ConfigMap + name: cm-policy-for-http-route-empty + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-5 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: 'Lua: can''t find the key lua in the referenced configmap cm-policy-for-http-route-empty.' + 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: + - type: ValueRef + valueRef: + group: v1 + 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 default/httproute-3 default/httproute-4 + default/httproute-5]' + 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: 5 + 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 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-3 + 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: /bar3 + 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-4 + 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: /bar4 + 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-5 + 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: /bar5 + 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-3/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + directResponse: + statusCode: 500 + envoyExtensions: + luas: + - Code: | + 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-3 + namespace: default + name: httproute/default/httproute-3/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /bar3 + - destination: + name: httproute/default/httproute-4/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + directResponse: + statusCode: 500 + envoyExtensions: + luas: + - Code: | + 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-4 + namespace: default + name: httproute/default/httproute-4/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /bar4 + - destination: + name: httproute/default/httproute-5/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + directResponse: + statusCode: 500 + envoyExtensions: + luas: + - Code: | + 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-5 + namespace: default + name: httproute/default/httproute-5/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /bar5 + - 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: + - Code: | + 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: + - Code: | + 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 + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 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..d4b53944db1 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-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: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: + - Code: 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: + - Code: 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 + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/ir/xds.go b/internal/ir/xds.go index f653fbf32fe..9f20aed7691 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 @@ -2822,6 +2824,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 + // Code is the Lua source code + Code *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 a5be0b4b176..75b371f5f2c 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. @@ -2137,6 +2144,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.Code != nil { + in, out := &in.Code, &out.Code + *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/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go index bc7b5ba8c53..9ed0445fe31 100644 --- a/internal/provider/kubernetes/controller.go +++ b/internal/provider/kubernetes/controller.go @@ -2102,6 +2102,7 @@ func (r *gatewayAPIReconciler) processExtensionServerPolicies( // to the resourceTree // - BackendRefs for ExtProcs // - SecretRefs for Wasms +// - ValueRefs for Luas func (r *gatewayAPIReconciler) processEnvoyExtensionPolicyObjectRefs( ctx context.Context, resourceTree *resource.Resources, resourceMap *resourceMappings, ) { @@ -2173,5 +2174,30 @@ func (r *gatewayAPIReconciler) processEnvoyExtensionPolicyObjectRefs( } } } + + // Add referenced ConfigMaps in Lua EnvoyExtensionPolicies to the resource tree + for _, lua := range policy.Spec.Lua { + if lua.Type == egv1a1.LuaValueTypeValueRef { + if lua.ValueRef != nil && string(lua.ValueRef.Kind) == resource.KindConfigMap { + configMap := new(corev1.ConfigMap) + err := r.client.Get(ctx, + types.NamespacedName{Namespace: policy.Namespace, Name: string(lua.ValueRef.Name)}, + configMap, + ) + if err != nil { + r.log.Error(err, + "failed to process Lua ValueRef for EnvoyExtensionPolicy", + "policy", policy, "ValueRef", lua.ValueRef.Name) + } + + resourceMap.allAssociatedNamespaces.Insert(policy.Namespace) + if !resourceMap.allAssociatedConfigMaps.Has(utils.NamespacedName(configMap).String()) { + resourceMap.allAssociatedConfigMaps.Insert(utils.NamespacedName(configMap).String()) + resourceTree.ConfigMaps = append(resourceTree.ConfigMaps, configMap) + r.log.Info("processing ConfigMap", "namespace", policy.Namespace, "name", string(lua.ValueRef.Name)) + } + } + } + } } } diff --git a/internal/xds/translator/httpfilters.go b/internal/xds/translator/httpfilters.go index 7fcb1e0b785..e291a3b461a 100644 --- a/internal/xds/translator/httpfilters.go +++ b/internal/xds/translator/httpfilters.go @@ -116,22 +116,24 @@ func newOrderedHTTPFilter(filter *hcmv3.HttpFilter) *OrderedHTTPFilter { order = 7 case isFilterType(filter, egv1a1.EnvoyFilterSessionPersistence): order = 8 - case isFilterType(filter, egv1a1.EnvoyFilterExtProc): + case isFilterType(filter, egv1a1.EnvoyFilterLua): order = 9 + mustGetFilterIndex(filter.Name) - case isFilterType(filter, egv1a1.EnvoyFilterWasm): + case isFilterType(filter, egv1a1.EnvoyFilterExtProc): order = 100 + mustGetFilterIndex(filter.Name) + case isFilterType(filter, egv1a1.EnvoyFilterWasm): + order = 200 + mustGetFilterIndex(filter.Name) case isFilterType(filter, egv1a1.EnvoyFilterRBAC): - order = 201 + order = 301 case isFilterType(filter, egv1a1.EnvoyFilterLocalRateLimit): - order = 202 + order = 302 case isFilterType(filter, egv1a1.EnvoyFilterRateLimit): - order = 203 + order = 303 case isFilterType(filter, egv1a1.EnvoyFilterCustomResponse): - order = 204 + order = 304 case isFilterType(filter, egv1a1.EnvoyFilterCompressor): - order = 205 + order = 305 case isFilterType(filter, egv1a1.EnvoyFilterRouter): - order = 206 + order = 306 } return &OrderedHTTPFilter{ diff --git a/internal/xds/translator/lua.go b/internal/xds/translator/lua.go new file mode 100644 index 00000000000..f4444a97dcf --- /dev/null +++ b/internal/xds/translator/lua.go @@ -0,0 +1,128 @@ +// 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" + + 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" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/xds/types" +) + +func init() { + registerHTTPFilter(&lua{}) +} + +type lua struct{} + +var _ httpFilter = &lua{} + +// patchHCM builds and appends the lua Filters to the HTTP Connection Manager +// Lua filters are created in disabled mode. +func (*lua) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error { + if mgr == nil { + return errors.New("hcm is nil") + } + if irListener == nil { + return errors.New("ir listener is nil") + } + + var errs error + for _, route := range irListener.Routes { + if !routeContainsLua(route) { + continue + } + for _, ep := range route.EnvoyExtensions.Luas { + if hcmContainsFilter(mgr, luaFilterName(ep)) { + continue + } + filter, err := buildHCMLuaFilter(ep) + if err != nil { + errs = errors.Join(errs, err) + continue + } + mgr.HttpFilters = append(mgr.HttpFilters, filter) + } + } + + return errs +} + +// buildHCMLuaFilter returns a Lua filter for HCM. +func buildHCMLuaFilter(lua ir.Lua) (*hcmv3.HttpFilter, error) { + var ( + luaProto *luafilterv3.Lua + luaAny *anypb.Any + err error + ) + luaProto = &luafilterv3.Lua{ + DefaultSourceCode: &corev3.DataSource{ + Specifier: &corev3.DataSource_InlineString{ + InlineString: *lua.Code, + }, + }, + } + 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), + Disabled: true, + ConfigType: &hcmv3.HttpFilter_TypedConfig{ + TypedConfig: luaAny, + }, + }, nil +} + +func luaFilterName(lua ir.Lua) string { + return perRouteFilterName(egv1a1.EnvoyFilterLua, lua.Name) +} + +// 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 so Lua filters are enabled if applicable. +func (*lua) patchRoute(route *routev3.Route, irRoute *ir.HTTPRoute) error { + if route == nil { + return errors.New("xds route is nil") + } + if irRoute == nil { + return errors.New("ir route is nil") + } + if irRoute.EnvoyExtensions == nil { + return nil + } + + for _, ep := range irRoute.EnvoyExtensions.Luas { + filterName := luaFilterName(ep) + if err := enableFilterOnRoute(route, filterName); err != nil { + return err + } + } + 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..e72cb05ec88 --- /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: + - code: 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: + - code: function envoy_on_response(response_handle) + response_handle:logWarn('Goodbye.') + end + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/0 + - code: 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..d28bf2fe254 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/lua.listeners.yaml @@ -0,0 +1,55 @@ +- 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: + - disabled: true + name: envoy.filters.http.lua/envoyextensionpolicy/default/policy-for-http-route/lua/0 + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + defaultSourceCode: + inlineString: function envoy_on_request(request_handle) request_handle:logInfo('Goodbye.') + end + - disabled: true + name: envoy.filters.http.lua/envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/0 + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + defaultSourceCode: + inlineString: function envoy_on_response(response_handle) response_handle:logWarn('Goodbye.') + end + - disabled: true + name: envoy.filters.http.lua/envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/1 + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + defaultSourceCode: + 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..307616b16e6 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/lua.routes.yaml @@ -0,0 +1,32 @@ +- 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 + typedPerFilterConfig: + envoy.filters.http.lua/envoyextensionpolicy/default/policy-for-http-route/lua/0: + '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig + config: {} + - 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 + typedPerFilterConfig: + envoy.filters.http.lua/envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/0: + '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig + config: {} + envoy.filters.http.lua/envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/1: + '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig + config: {} diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 49a929a4f62..bbdfefe849e 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -1043,6 +1043,7 @@ _Appears in:_ | `envoy.filters.http.stateful_session` | EnvoyFilterSessionPersistence defines the Envoy HTTP session persistence filter.
| | `envoy.filters.http.ext_proc` | EnvoyFilterExtProc defines the Envoy HTTP external process filter.
| | `envoy.filters.http.wasm` | EnvoyFilterWasm defines the Envoy HTTP WebAssembly filter.
| +| `envoy.filters.http.lua` | EnvoyFilterLua defines the Envoy HTTP Lua filter.
| | `envoy.filters.http.rbac` | EnvoyFilterRBAC defines the Envoy RBAC filter.
| | `envoy.filters.http.local_ratelimit` | EnvoyFilterLocalRateLimit defines the Envoy HTTP local rate limit filter.
| | `envoy.filters.http.ratelimit` | EnvoyFilterRateLimit defines the Envoy HTTP rate limit filter.
| @@ -1483,7 +1484,7 @@ _Appears in:_ | `extraArgs` | _string array_ | false | | ExtraArgs defines additional command line options that are provided to Envoy.
More info: https://www.envoyproxy.io/docs/envoy/latest/operations/cli#command-line-options
Note: some command line options are used internally(e.g. --log-level) so they cannot be provided here. | | `mergeGateways` | _boolean_ | false | | MergeGateways defines if Gateway resources should be merged onto the same Envoy Proxy Infrastructure.
Setting this field to true would merge all Gateway Listeners under the parent Gateway Class.
This means that the port, protocol and hostname tuple must be unique for every listener.
If a duplicate listener is detected, the newer listener (based on timestamp) will be rejected and its status will be updated with a "Accepted=False" condition. | | `shutdown` | _[ShutdownConfig](#shutdownconfig)_ | false | | Shutdown defines configuration for graceful envoy shutdown process. | -| `filterOrder` | _[FilterPosition](#filterposition) array_ | false | | FilterOrder defines the order of filters in the Envoy proxy's HTTP filter chain.
The FilterPosition in the list will be applied in the order they are defined.
If unspecified, the default filter order is applied.
Default filter order is:

- envoy.filters.http.health_check

- envoy.filters.http.fault

- envoy.filters.http.cors

- envoy.filters.http.ext_authz

- envoy.filters.http.basic_auth

- envoy.filters.http.oauth2

- envoy.filters.http.jwt_authn

- envoy.filters.http.stateful_session

- envoy.filters.http.ext_proc

- envoy.filters.http.wasm

- envoy.filters.http.rbac

- envoy.filters.http.local_ratelimit

- envoy.filters.http.ratelimit

- envoy.filters.http.custom_response

- envoy.filters.http.router

Note: "envoy.filters.http.router" cannot be reordered, it's always the last filter in the chain. | +| `filterOrder` | _[FilterPosition](#filterposition) array_ | false | | FilterOrder defines the order of filters in the Envoy proxy's HTTP filter chain.
The FilterPosition in the list will be applied in the order they are defined.
If unspecified, the default filter order is applied.
Default filter order is:

- envoy.filters.http.health_check

- envoy.filters.http.fault

- envoy.filters.http.cors

- envoy.filters.http.ext_authz

- envoy.filters.http.basic_auth

- envoy.filters.http.oauth2

- envoy.filters.http.jwt_authn

- envoy.filters.http.stateful_session

- envoy.filters.http.lua

- envoy.filters.http.ext_proc

- envoy.filters.http.wasm

- envoy.filters.http.rbac

- envoy.filters.http.local_ratelimit

- envoy.filters.http.ratelimit

- envoy.filters.http.custom_response

- envoy.filters.http.router

Note: "envoy.filters.http.router" cannot be reordered, it's always the last filter in the chain. | | `backendTLS` | _[BackendTLSConfig](#backendtlsconfig)_ | false | | BackendTLS is the TLS configuration for the Envoy proxy to use when connecting to backends.
These settings are applied on backends for which TLS policies are specified. | | `ipFamily` | _[IPFamily](#ipfamily)_ | false | | IPFamily specifies the IP family for the EnvoyProxy fleet.
This setting only affects the Gateway listener port and does not impact
other aspects of the Envoy proxy configuration.
If not specified, the system will operate as follows:
- It defaults to IPv4 only.
- IPv6 and dual-stack environments are not supported in this default configuration.
Note: To enable IPv6 or dual-stack functionality, explicit configuration is required. | | `preserveRouteOrder` | _boolean_ | false | | PreserveRouteOrder determines if the order of matching for HTTPRoutes is determined by Gateway-API
specification (https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteRule)
or preserves the order defined by users in the HTTPRoute's HTTPRouteRule list.
Default: False | diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md index 49a929a4f62..bbdfefe849e 100644 --- a/site/content/zh/latest/api/extension_types.md +++ b/site/content/zh/latest/api/extension_types.md @@ -1043,6 +1043,7 @@ _Appears in:_ | `envoy.filters.http.stateful_session` | EnvoyFilterSessionPersistence defines the Envoy HTTP session persistence filter.
| | `envoy.filters.http.ext_proc` | EnvoyFilterExtProc defines the Envoy HTTP external process filter.
| | `envoy.filters.http.wasm` | EnvoyFilterWasm defines the Envoy HTTP WebAssembly filter.
| +| `envoy.filters.http.lua` | EnvoyFilterLua defines the Envoy HTTP Lua filter.
| | `envoy.filters.http.rbac` | EnvoyFilterRBAC defines the Envoy RBAC filter.
| | `envoy.filters.http.local_ratelimit` | EnvoyFilterLocalRateLimit defines the Envoy HTTP local rate limit filter.
| | `envoy.filters.http.ratelimit` | EnvoyFilterRateLimit defines the Envoy HTTP rate limit filter.
| @@ -1483,7 +1484,7 @@ _Appears in:_ | `extraArgs` | _string array_ | false | | ExtraArgs defines additional command line options that are provided to Envoy.
More info: https://www.envoyproxy.io/docs/envoy/latest/operations/cli#command-line-options
Note: some command line options are used internally(e.g. --log-level) so they cannot be provided here. | | `mergeGateways` | _boolean_ | false | | MergeGateways defines if Gateway resources should be merged onto the same Envoy Proxy Infrastructure.
Setting this field to true would merge all Gateway Listeners under the parent Gateway Class.
This means that the port, protocol and hostname tuple must be unique for every listener.
If a duplicate listener is detected, the newer listener (based on timestamp) will be rejected and its status will be updated with a "Accepted=False" condition. | | `shutdown` | _[ShutdownConfig](#shutdownconfig)_ | false | | Shutdown defines configuration for graceful envoy shutdown process. | -| `filterOrder` | _[FilterPosition](#filterposition) array_ | false | | FilterOrder defines the order of filters in the Envoy proxy's HTTP filter chain.
The FilterPosition in the list will be applied in the order they are defined.
If unspecified, the default filter order is applied.
Default filter order is:

- envoy.filters.http.health_check

- envoy.filters.http.fault

- envoy.filters.http.cors

- envoy.filters.http.ext_authz

- envoy.filters.http.basic_auth

- envoy.filters.http.oauth2

- envoy.filters.http.jwt_authn

- envoy.filters.http.stateful_session

- envoy.filters.http.ext_proc

- envoy.filters.http.wasm

- envoy.filters.http.rbac

- envoy.filters.http.local_ratelimit

- envoy.filters.http.ratelimit

- envoy.filters.http.custom_response

- envoy.filters.http.router

Note: "envoy.filters.http.router" cannot be reordered, it's always the last filter in the chain. | +| `filterOrder` | _[FilterPosition](#filterposition) array_ | false | | FilterOrder defines the order of filters in the Envoy proxy's HTTP filter chain.
The FilterPosition in the list will be applied in the order they are defined.
If unspecified, the default filter order is applied.
Default filter order is:

- envoy.filters.http.health_check

- envoy.filters.http.fault

- envoy.filters.http.cors

- envoy.filters.http.ext_authz

- envoy.filters.http.basic_auth

- envoy.filters.http.oauth2

- envoy.filters.http.jwt_authn

- envoy.filters.http.stateful_session

- envoy.filters.http.lua

- envoy.filters.http.ext_proc

- envoy.filters.http.wasm

- envoy.filters.http.rbac

- envoy.filters.http.local_ratelimit

- envoy.filters.http.ratelimit

- envoy.filters.http.custom_response

- envoy.filters.http.router

Note: "envoy.filters.http.router" cannot be reordered, it's always the last filter in the chain. | | `backendTLS` | _[BackendTLSConfig](#backendtlsconfig)_ | false | | BackendTLS is the TLS configuration for the Envoy proxy to use when connecting to backends.
These settings are applied on backends for which TLS policies are specified. | | `ipFamily` | _[IPFamily](#ipfamily)_ | false | | IPFamily specifies the IP family for the EnvoyProxy fleet.
This setting only affects the Gateway listener port and does not impact
other aspects of the Envoy proxy configuration.
If not specified, the system will operate as follows:
- It defaults to IPv4 only.
- IPv6 and dual-stack environments are not supported in this default configuration.
Note: To enable IPv6 or dual-stack functionality, explicit configuration is required. | | `preserveRouteOrder` | _boolean_ | false | | PreserveRouteOrder determines if the order of matching for HTTPRoutes is determined by Gateway-API
specification (https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteRule)
or preserves the order defined by users in the HTTPRoute's HTTPRouteRule list.
Default: False | diff --git a/test/cel-validation/envoyextensionpolicy_test.go b/test/cel-validation/envoyextensionpolicy_test.go index ec401f2c188..c8f3fe776af 100644 --- a/test/cel-validation/envoyextensionpolicy_test.go +++ b/test/cel-validation/envoyextensionpolicy_test.go @@ -445,8 +445,9 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { { Type: egv1a1.LuaValueTypeValueRef, ValueRef: &gwapiv1a2.LocalObjectReference{ - Kind: gwapiv1a2.Kind("ConfigMap"), - Name: gwapiv1a2.ObjectName("eg"), + Kind: gwapiv1a2.Kind("ConfigMap"), + Name: gwapiv1a2.ObjectName("eg"), + Group: gwapiv1a2.Group("v1"), }, }, }, @@ -471,8 +472,9 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { { Type: egv1a1.LuaValueTypeInline, ValueRef: &gwapiv1a2.LocalObjectReference{ - Kind: gwapiv1a2.Kind("ConfigMap"), - Name: gwapiv1a2.ObjectName("eg"), + Kind: gwapiv1a2.Kind("ConfigMap"), + Name: gwapiv1a2.ObjectName("eg"), + Group: gwapiv1a2.Group("v1"), }, }, }, @@ -524,8 +526,9 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { { Type: egv1a1.LuaValueTypeValueRef, ValueRef: &gwapiv1a2.LocalObjectReference{ - Kind: gwapiv1a2.Kind("NotConfigMap"), - Name: gwapiv1a2.ObjectName("eg"), + Kind: gwapiv1a2.Kind("NotConfigMap"), + Name: gwapiv1a2.ObjectName("eg"), + Group: gwapiv1a2.Group("v1"), }, }, }, @@ -541,7 +544,36 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { } }, wantErrors: []string{ - "spec.lua[0].valueRef: Invalid value: \"object\": Only a reference to an object of kind ConfigMap belonging to default core API group is supported.", + "spec.lua[0].valueRef: Invalid value: \"object\": Only a reference to an object of kind ConfigMap belonging to default v1 API group is supported.", + }, + }, + { + desc: "Invalid Lua filter (source object group not default)", + mutate: func(sp *egv1a1.EnvoyExtensionPolicy) { + sp.Spec = egv1a1.EnvoyExtensionPolicySpec{ + Lua: []egv1a1.Lua{ + { + Type: egv1a1.LuaValueTypeValueRef, + ValueRef: &gwapiv1a2.LocalObjectReference{ + Kind: gwapiv1a2.Kind("ConfigMap"), + Name: gwapiv1a2.ObjectName("eg"), + Group: gwapiv1a2.Group(gwapiv1a2.GroupName), + }, + }, + }, + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "eg", + }, + }, + }, + } + }, + wantErrors: []string{ + "spec.lua[0].valueRef: Invalid value: \"object\": Only a reference to an object of kind ConfigMap belonging to default v1 API group is supported.", }, }, { @@ -553,8 +585,9 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { Type: egv1a1.LuaValueTypeInline, Inline: ptr.To("function envoy_on_response(response_handle) -- Do something -- end"), ValueRef: &gwapiv1a2.LocalObjectReference{ - Kind: gwapiv1a2.Kind("ConfigMap"), - Name: gwapiv1a2.ObjectName("eg"), + Kind: gwapiv1a2.Kind("ConfigMap"), + Name: gwapiv1a2.ObjectName("eg"), + Group: gwapiv1a2.Group("v1"), }, }, }, diff --git a/test/e2e/testdata/lua-http.yaml b/test/e2e/testdata/lua-http.yaml new file mode 100644 index 00000000000..37e4252146a --- /dev/null +++ b/test/e2e/testdata/lua-http.yaml @@ -0,0 +1,100 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: example-route-1-with-lua + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + hostnames: ["www.example.com"] + rules: + - matches: + - path: + type: PathPrefix + value: /route1 + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: example-route-2-with-lua + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + hostnames: ["www.example.com"] + rules: + - matches: + - path: + type: PathPrefix + value: /route2 + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: example-route-3-without-lua + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + hostnames: ["www.example.com"] + rules: + - matches: + - path: + type: PathPrefix + value: /route3 + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm-example-lua + namespace: gateway-conformance-infra +data: + lua: | + function envoy_on_response(response_handle) + response_handle:headers():add("X-Custom-Lua-Header", "lua_value_1") + end +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyExtensionPolicy +metadata: + name: example-lua-1 + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: example-route-1-with-lua + lua: + - type: ValueRef + valueRef: + name: cm-example-lua + kind: ConfigMap + group: v1 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyExtensionPolicy +metadata: + name: example-lua-2 + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: example-route-2-with-lua + lua: + - type: Inline + inline: | + function envoy_on_response(response_handle) + response_handle:headers():add("X-Custom-Lua-Header", "lua_value_2") + response_handle:headers():add("X-Custom-Lua-Another-Header", "lua_another_value") + end diff --git a/test/e2e/tests/lua_http.go b/test/e2e/tests/lua_http.go new file mode 100644 index 00000000000..40df80973f5 --- /dev/null +++ b/test/e2e/tests/lua_http.go @@ -0,0 +1,154 @@ +// 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: "LuaHTTP", + Description: "Test Lua extension that adds response headers", + Manifests: []string{"testdata/lua-http.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("http route with lua filter 1", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "example-route-1-with-lua", 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: "example-lua-1", Namespace: ns}, suite.ControllerName, ancestorRef) + EnvoyExtensionPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "example-lua-2", Namespace: ns}, suite.ControllerName, ancestorRef) + + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Host: "www.example.com", + Path: "/route1", + }, + Response: http.Response{ + StatusCode: 200, + Headers: map[string]string{ + "X-Custom-Lua-Header": "lua_value_1", + }, + AbsentHeaders: []string{"X-Custom-Lua-Another-Header"}, + }, + Namespace: ns, + } + + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + }) + + t.Run("http route with lua filter 2", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "example-route-2-with-lua", 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: "example-lua-1", Namespace: ns}, suite.ControllerName, ancestorRef) + EnvoyExtensionPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "example-lua-2", Namespace: ns}, suite.ControllerName, ancestorRef) + + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Host: "www.example.com", + Path: "/route2", + }, + Response: http.Response{ + StatusCode: 200, + Headers: map[string]string{ + "X-Custom-Lua-Header": "lua_value_2", + "X-Custom-Lua-Another-Header": "lua_another_value", + }, + }, + Namespace: ns, + } + + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + }) + + t.Run("http route without lua filter", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "example-route-3-without-lua", 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: "example-lua-1", Namespace: ns}, suite.ControllerName, ancestorRef) + EnvoyExtensionPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "example-lua-2", Namespace: ns}, suite.ControllerName, ancestorRef) + + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Host: "www.example.com", + Path: "/route3", + }, + Response: http.Response{ + StatusCode: 200, + AbsentHeaders: []string{"X-Custom-Lua-Header", "X-Custom-Lua-Another-Header"}, + }, + Namespace: ns, + } + + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + }) + }, +} diff --git a/test/e2e/tests/utils.go b/test/e2e/tests/utils.go index a674e5fc69f..786cc7fcdaa 100644 --- a/test/e2e/tests/utils.go +++ b/test/e2e/tests/utils.go @@ -330,11 +330,11 @@ func EnvoyExtensionPolicyMustBeAccepted(t *testing.T, client client.Client, poli } if policyAcceptedByAncestor(policy.Status.Ancestors, controllerName, ancestorRef) { - tlog.Logf(t, "EnvoyExtensionPolicy has been accepted: %v", policy) + tlog.Logf(t, "EnvoyExtensionPolicy has been accepted: %+v", policy) return true, nil } - tlog.Logf(t, "EnvoyExtensionPolicy not yet accepted: %v", policy) + tlog.Logf(t, "EnvoyExtensionPolicy not yet accepted: %+v", policy) return false, nil })