From d1b4410b2d43bb82c2040987fe86feb8b791fa42 Mon Sep 17 00:00:00 2001 From: Rudrakh Panigrahi Date: Wed, 29 Jan 2025 00:27:06 +0530 Subject: [PATCH] feat: implement Lua EnvoyExtensionPolicy Signed-off-by: Rudrakh Panigrahi --- api/v1alpha1/envoyproxy_types.go | 3 + api/v1alpha1/lua_types.go | 4 +- api/v1alpha1/zz_generated.deepcopy.go | 4 +- ....envoyproxy.io_envoyextensionpolicies.yaml | 24 +- go.mod | 1 + go.sum | 2 + internal/gatewayapi/envoyextensionpolicy.go | 96 +++ .../gatewayapi/luavalidator/lua_validator.go | 60 ++ .../luavalidator/lua_validator_test.go | 146 +++++ internal/gatewayapi/luavalidator/mocks.lua | 613 ++++++++++++++++++ ...oyextensionpolicy-with-invalid-lua.in.yaml | 85 +++ ...yextensionpolicy-with-invalid-lua.out.yaml | 276 ++++++++ ...extensionpolicy-with-lua-configmap.in.yaml | 108 +++ ...xtensionpolicy-with-lua-configmap.out.yaml | 279 ++++++++ .../envoyextensionpolicy-with-lua.in.yaml | 85 +++ .../envoyextensionpolicy-with-lua.out.yaml | 271 ++++++++ internal/ir/xds.go | 12 + internal/ir/zz_generated.deepcopy.go | 27 + internal/provider/kubernetes/controller.go | 19 + internal/xds/translator/lua.go | 108 +++ .../translator/testdata/in/xds-ir/lua.yaml | 59 ++ .../testdata/out/xds-ir/lua.clusters.yaml | 34 + .../testdata/out/xds-ir/lua.endpoints.yaml | 24 + .../testdata/out/xds-ir/lua.listeners.yaml | 34 + .../testdata/out/xds-ir/lua.routes.yaml | 38 ++ internal/xds/translator/utils.go | 9 + site/content/en/latest/api/extension_types.md | 3 +- site/content/zh/latest/api/extension_types.md | 3 +- .../envoyextensionpolicy_test.go | 47 +- test/e2e/testdata/lua-http.yaml | 45 ++ test/e2e/tests/lua_http.go | 75 +++ test/e2e/tests/utils.go | 5 +- 32 files changed, 2577 insertions(+), 22 deletions(-) create mode 100644 internal/gatewayapi/luavalidator/lua_validator.go create mode 100644 internal/gatewayapi/luavalidator/lua_validator_test.go create mode 100644 internal/gatewayapi/luavalidator/mocks.lua create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.in.yaml create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.out.yaml create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-configmap.in.yaml create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-configmap.out.yaml create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-lua.in.yaml create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-lua.out.yaml create mode 100644 internal/xds/translator/lua.go create mode 100644 internal/xds/translator/testdata/in/xds-ir/lua.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/lua.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/lua.endpoints.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/lua.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/lua.routes.yaml create mode 100644 test/e2e/testdata/lua-http.yaml create mode 100644 test/e2e/tests/lua_http.go diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go index 8b9c83e17e3..b942e056fb9 100644 --- a/api/v1alpha1/envoyproxy_types.go +++ b/api/v1alpha1/envoyproxy_types.go @@ -233,6 +233,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..7f43ff3972e 100644 --- a/api/v1alpha1/lua_types.go +++ b/api/v1alpha1/lua_types.go @@ -41,8 +41,8 @@ type Lua struct { // The value of key `lua` in the ConfigMap will be used. // If the key is not found, the first value in the ConfigMap will be used. // - // +kubebuilder:validation:XValidation:rule="self.kind == 'ConfigMap' && (!has(self.group) || self.group == '')",message="Only a reference to an object of kind ConfigMap belonging to default core API group is supported." + // +kubebuilder:validation:XValidation:rule="self.kind == 'ConfigMap' && (!has(self.group) || self.group == '')",message="Only a reference to an object of kind ConfigMap belonging to default API group is supported." // +optional // +unionMember - ValueRef *gwapiv1.LocalObjectReference `json:"valueRef,omitempty"` + ValueRef *gwapiv1.SecretObjectReference `json:"valueRef,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index de1bb429588..03526e55db6 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -4000,8 +4000,8 @@ func (in *Lua) DeepCopyInto(out *Lua) { } if in.ValueRef != nil { in, out := &in.ValueRef, &out.ValueRef - *out = new(v1.LocalObjectReference) - **out = **in + *out = new(v1.SecretObjectReference) + (*in).DeepCopyInto(*out) } } 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 fde62179020..de714d5d8fd 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml @@ -1044,6 +1044,7 @@ spec: If the key is not found, the first value in the ConfigMap will be used. properties: group: + default: "" description: |- Group is the group of the referent. For example, "gateway.networking.k8s.io". When unspecified or empty string, core API group is inferred. @@ -1051,8 +1052,8 @@ spec: pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string kind: - description: Kind is kind of the referent. For example "HTTPRoute" - or "Service". + default: Secret + description: Kind is kind of the referent. For example "Secret". maxLength: 63 minLength: 1 pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ @@ -1062,14 +1063,27 @@ spec: maxLength: 253 minLength: 1 type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string required: - - group - - kind - name type: object x-kubernetes-validations: - message: Only a reference to an object of kind ConfigMap belonging - to default core API group is supported. + to default API group is supported. rule: self.kind == 'ConfigMap' && (!has(self.group) || self.group == '') required: diff --git a/go.mod b/go.mod index 1993b7d139a..9df5746fc7c 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/telepresenceio/watchable v0.0.0-20220726211108-9bb86f92afa7 github.com/tetratelabs/func-e v1.1.5-0.20240822223546-c85a098d5bf0 github.com/tsaarni/certyaml v0.10.0 + github.com/yuin/gopher-lua v1.1.1 go.opentelemetry.io/otel v1.34.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0 diff --git a/go.sum b/go.sum index 35e6872946c..54cb952f496 100644 --- a/go.sum +++ b/go.sum @@ -865,6 +865,8 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= diff --git a/internal/gatewayapi/envoyextensionpolicy.go b/internal/gatewayapi/envoyextensionpolicy.go index 145662e5480..74be501de9e 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,80 @@ func (t *Translator) translateEnvoyExtensionPolicyForGateway( return errs } +func (t *Translator) buildLuas(policy *egv1a1.EnvoyExtensionPolicy, resources *resource.Resources) ([]ir.Lua, error) { + var luaIRList []ir.Lua + + if policy == nil { + return nil, nil + } + + for idx, ep := range policy.Spec.Lua { + name := irConfigNameForLua(policy, idx) + luaIR, err := t.buildLua(name, policy, ep, resources) + if err != nil { + return nil, err + } + luaIRList = append(luaIRList, *luaIR) + } + return luaIRList, nil +} + +func (t *Translator) buildLua( + name string, + policy *egv1a1.EnvoyExtensionPolicy, + lua egv1a1.Lua, + resources *resource.Resources, +) (*ir.Lua, error) { + var luaBody *string + var err error + if lua.Type == egv1a1.LuaValueTypeValueRef { + luaBody, err = getLuaBodyFromLocalObjectReference(lua.ValueRef, resources, policy.Namespace) + } else { + luaBody = lua.Inline + } + if err != nil { + return nil, err + } + if err = luavalidator.NewLuaValidator(*luaBody).Validate(); err != nil { + return nil, fmt.Errorf("validation failed for lua body in policy with name %v: %w", name, err) + } + return &ir.Lua{ + Name: name, + Body: luaBody, + }, nil +} + +// getLuaBodyFromLocalObjectReference assumes the local object reference points to a Kubernetes ConfigMap +func getLuaBodyFromLocalObjectReference(valueRef *gwapiv1.SecretObjectReference, resources *resource.Resources, policyNs string) (*string, error) { + cm := resources.GetConfigMap(string(ptr.Deref(valueRef.Namespace, gwapiv1.Namespace(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/%s among %v", policyNs, valueRef.Name, configMapListToString(resources.ConfigMaps)) + } +} + +func configMapListToString(cms []*corev1.ConfigMap) string { + s := "" + for _, cm := range cms { + s += fmt.Sprintf("[%s/%s]", cm.Namespace, cm.Name) + } + return s +} + func (t *Translator) buildExtProcs(policy *egv1a1.EnvoyExtensionPolicy, resources *resource.Resources, envoyProxy *egv1a1.EnvoyProxy) ([]ir.ExtProc, error) { var extProcIRList []ir.ExtProc @@ -522,6 +611,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..8a02e7c9975 --- /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 { + body string +} + +// NewLuaValidator returns a LuaValidator for user provided Lua body +func NewLuaValidator(body string) *LuaValidator { + return &LuaValidator{ + body: body, + } +} + +// Validate runs all validations for the LuaValidator +func (l *LuaValidator) Validate() error { + if !strings.Contains(l.body, "envoy_on_request") && !strings.Contains(l.body, "envoy_on_response") { + return fmt.Errorf("expected one of envoy_on_request() or envoy_on_response() to be defined") + } + if strings.Contains(l.body, "envoy_on_request") { + if err := l.runLua(string(mockData) + "\n" + l.body + "\nenvoy_on_request(StreamHandle)"); err != nil { + return fmt.Errorf("failed to mock run envoy_on_request: %w", err) + } + } + if strings.Contains(l.body, "envoy_on_response") { + if err := l.runLua(string(mockData) + "\n" + l.body + "\nenvoy_on_response(StreamHandle)"); err != nil { + return fmt.Errorf("failed to mock run envoy_on_response: %w", err) + } + } + return nil +} + +// runLua interprets and runs the provided Lua body in runtime +func (l *LuaValidator) runLua(body string) error { + L := lua.NewState() + defer L.Close() + if err := L.DoString(body); err != nil { + return err + } + return nil +} diff --git a/internal/gatewayapi/luavalidator/lua_validator_test.go b/internal/gatewayapi/luavalidator/lua_validator_test.go new file mode 100644 index 00000000000..3e3c1ffc7bd --- /dev/null +++ b/internal/gatewayapi/luavalidator/lua_validator_test.go @@ -0,0 +1,146 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package luavalidator + +import ( + "strings" + "testing" +) + +func Test_Validate(t *testing.T) { + type args struct { + name string + body string + expectedErrSubstring string + } + tests := []args{ + { + name: "empty body", + body: "", + expectedErrSubstring: "expected one of envoy_on_request() or envoy_on_response() to be defined", + }, + { + name: "logInfo: envoy_on_response", + body: `function envoy_on_response(response_handle) + response_handle:logInfo("Goodbye.") + end`, + expectedErrSubstring: "", + }, + { + name: "logInfo: envoy_on_request", + body: `function envoy_on_request(request_handle) + request_handle:logInfo("Goodbye.") + end`, + expectedErrSubstring: "", + }, + { + name: "stream:headers:Get", + body: `function envoy_on_request(request_handle) + request_handle:headers():get("foo") + end`, + expectedErrSubstring: "", + }, + { + name: "stream:connection:ssl:expirationPeerCertificate", + body: `function envoy_on_request(request_handle) + request_handle:connection():ssl():expirationPeerCertificate() + end`, + expectedErrSubstring: "", + }, + { + name: "stream:metadata:pairs", + body: `function envoy_on_request(request_handle) + for key, value in pairs(request_handle:metadata()) do + print(key, value) + end + end`, + expectedErrSubstring: "", + }, + { + name: "stream:httpCall", + body: `function envoy_on_request(request_handle) + -- Make an HTTP call. + local headers, body = request_handle:httpCall( + "lua_cluster", + { + [":method"] = "POST", + [":path"] = "/", + [":authority"] = "lua_cluster", + ["set-cookie"] = { "lang=lua; Path=/", "type=binding; Path=/" } + }, + "hello world", + 5000) + + -- Response directly and set a header from the HTTP call. No further filter iteration + -- occurs. + request_handle:respond( + {[":status"] = "403", + ["upstream_foo"] = headers["foo"]}, + "nope") + end`, + expectedErrSubstring: "", + }, + { + name: "stream:httpPostCall unsupported api", + body: `function envoy_on_request(request_handle) + -- Make an HTTP call. + local headers, body = request_handle:httpPostCall( + "lua_cluster", + { + [":method"] = "POST", + [":path"] = "/", + [":authority"] = "lua_cluster", + ["set-cookie"] = { "lang=lua; Path=/", "type=binding; Path=/" } + }, + "hello world", + 5000) + + -- Response directly and set a header from the HTTP call. No further filter iteration + -- occurs. + request_handle:respond( + {[":status"] = "403", + ["upstream_foo"] = headers["foo"]}, + "nope") + end`, + expectedErrSubstring: "attempt to call a non-function object", + }, + { + name: "stream:bodyChunks", + body: `function envoy_on_response(response_handle) + -- Sets the content-type. + response_handle:headers():replace("content-type", "text/html") + local last + for chunk in response_handle:bodyChunks() do + -- Clears each received chunk. + chunk:setBytes("") + last = chunk + end + + last:setBytes("Not Found") + end`, + expectedErrSubstring: "", + }, + { + name: "unsupported api", + body: `function envoy_on_request(request_handle) + request_handle:unknownApi() + end`, + expectedErrSubstring: "attempt to call a non-function object", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := NewLuaValidator(tt.body) + if err := l.Validate(); err != nil && tt.expectedErrSubstring == "" { + t.Errorf("Unexpected error: %v", err) + } else if err != nil && !strings.Contains(err.Error(), tt.expectedErrSubstring) { + t.Errorf("Expected substring in error: %v, got error: %v", tt.expectedErrSubstring, err) + } else if err == nil && tt.expectedErrSubstring != "" { + t.Errorf("Expected error with substring: %v", tt.expectedErrSubstring) + } + }) + } +} diff --git a/internal/gatewayapi/luavalidator/mocks.lua b/internal/gatewayapi/luavalidator/mocks.lua new file mode 100644 index 00000000000..4cb13632238 --- /dev/null +++ b/internal/gatewayapi/luavalidator/mocks.lua @@ -0,0 +1,613 @@ +-- ParsedName object. +local ParsedName = {} + +-- Logging methods. +function ParsedName:logTrace(message) + print("[TRACE]: " .. message) +end + +function ParsedName:logDebug(message) + print("[DEBUG]: " .. message) +end + +function ParsedName:logInfo(message) + print("[INFO]: " .. message) +end + +function ParsedName:logWarn(message) + print("[WARN]: " .. message) +end + +function ParsedName:logErr(message) + print("[ERROR]: " .. message) +end + +function ParsedName:logCritical(message) + print("[CRITICAL]: " .. message) +end + +-- Method to return the common name. +function ParsedName:commonName() + return "common_name" +end + +-- Method to return the organization name. +function ParsedName:organizationName() + return "organization_name" +end + +local SSLConnectionObject = {} + +-- Logging methods +function SSLConnectionObject:logTrace(message) + print("[TRACE]: " .. message) +end + +function SSLConnectionObject:logDebug(message) + print("[DEBUG]: " .. message) +end + +function SSLConnectionObject:logInfo(message) + print("[INFO]: " .. message) +end + +function SSLConnectionObject:logWarn(message) + print("[WARN]: " .. message) +end + +function SSLConnectionObject:logErr(message) + print("[ERROR]: " .. message) +end + +function SSLConnectionObject:logCritical(message) + print("[CRITICAL]: " .. message) +end + +-- SSL-related methods +function SSLConnectionObject:peerCertificatePresented() + return true -- Simulate that the peer certificate is always presented +end + +function SSLConnectionObject:peerCertificateValidated() + return false -- Simulate no validation for TLS session resumption +end + +function SSLConnectionObject:uriSanLocalCertificate() + local t = {} + for i = 1, 1000 do + t[i] = "san" + end + return t +end + +function SSLConnectionObject:sha256PeerCertificateDigest() + return "abcdef1234567890" -- Simulated SHA256 digest +end + +function SSLConnectionObject:serialNumberPeerCertificate() + return "123456789" -- Simulated serial number +end + +function SSLConnectionObject:issuerPeerCertificate() + return "CN=Mock Issuer, O=Example Org" -- Simulated issuer +end + +function SSLConnectionObject:subjectPeerCertificate() + return "CN=Mock Subject, O=Example Org" -- Simulated subject +end + +function SSLConnectionObject:parsedSubjectPeerCertificate() + return { + commonName = function() return "Mock Subject" end, + organizationName = function() return "Example Org" end, + } +end + +function SSLConnectionObject:uriSanPeerCertificate() + return {"uri1", "uri2"} -- Example SAN URIs for peer certificate +end + +function SSLConnectionObject:subjectLocalCertificate() + return "CN=Local Cert, O=Example Org" -- Simulated local certificate subject +end + +function SSLConnectionObject:urlEncodedPemEncodedPeerCertificate() + return "PEM_ENCODED_PEER_CERT" -- Simulated PEM encoding +end + +function SSLConnectionObject:urlEncodedPemEncodedPeerCertificateChain() + return "PEM_ENCODED_CERT_CHAIN" -- Simulated PEM chain +end + +function SSLConnectionObject:dnsSansPeerCertificate() + local t = {} + for i = 1, 1000 do + t[i] = "example.com" + end + return t +end + +function SSLConnectionObject:dnsSansLocalCertificate() + local t = {} + for i = 1, 1000 do + t[i] = "local.com" + end + return t +end + +function SSLConnectionObject:oidsPeerCertificate() + local t = {} + for i = 1, 1000 do + t[i] = "1.2.840.113549.1.1.1" + end + return t +end + +function SSLConnectionObject:oidsLocalCertificate() + local t = {} + for i = 1, 1000 do + t[i] = "1.2.840.113549.1.1.1" + end + return t +end + +function SSLConnectionObject:validFromPeerCertificate() + return 1672531200 +end + +function SSLConnectionObject:expirationPeerCertificate() + return 2724608000 +end + +function SSLConnectionObject:sessionId() + return "abcdef1234567890abcdef1234567890" +end + +function SSLConnectionObject:ciphersuiteId() + return "0x1301" +end + +function SSLConnectionObject:ciphersuiteString() + return "TLS_AES_128_GCM_SHA256" +end + +function SSLConnectionObject:tlsVersion() + return "TLSv1.3" +end + +-- Connection object +local Connection = {} + +-- Logging methods for Connection +function Connection:logTrace(message) + print("[TRACE]: " .. message) +end + +function Connection:logDebug(message) + print("[DEBUG]: " .. message) +end + +function Connection:logInfo(message) + print("[INFO]: " .. message) +end + +function Connection:logWarn(message) + print("[WARN]: " .. message) +end + +function Connection:logErr(message) + print("[ERROR]: " .. message) +end + +function Connection:logCritical(message) + print("[CRITICAL]: " .. message) +end + +function Connection:ssl() + return SSLConnectionObject +end + +local DynamicMetadata = { + data = { + key1 = "value1", + key2 = "value2" + } +} + +setmetatable(DynamicMetadata, { + __pairs = function(self) + return pairs(self.data) + end, + __index = function(self, key) + return self.data[key] + end +}) + +-- Logging methods +function DynamicMetadata:logTrace(message) + print("[TRACE]: " .. message) +end + +function DynamicMetadata:logDebug(message) + print("[DEBUG]: " .. message) +end + +function DynamicMetadata:logInfo(message) + print("[INFO]: " .. message) +end + +function DynamicMetadata:logWarn(message) + print("[WARN]: " .. message) +end + +function DynamicMetadata:logErr(message) + print("[ERROR]: " .. message) +end + +function DynamicMetadata:logCritical(message) + print("[CRITICAL]: " .. message) +end + +function DynamicMetadata:get(filterName) + return "get_result" +end + +function DynamicMetadata:set(filterName, key, value) +end + +-- ConnectionStreamInfo Object +local ConnectionStreamInfo = {} + +-- Logging methods for ConnectionStreamInfo +function ConnectionStreamInfo:logTrace(message) + print("[TRACE]: " .. message) +end + +function ConnectionStreamInfo:logDebug(message) + print("[DEBUG]: " .. message) +end + +function ConnectionStreamInfo:logInfo(message) + print("[INFO]: " .. message) +end + +function ConnectionStreamInfo:logWarn(message) + print("[WARN]: " .. message) +end + +function ConnectionStreamInfo:logErr(message) + print("[ERROR]: " .. message) +end + +function ConnectionStreamInfo:logCritical(message) + print("[CRITICAL]: " .. message) +end + +function ConnectionStreamInfo:dynamicMetadata() + return DynamicMetadata +end + +-- StreamInfo Object +local StreamInfo = {} + +function StreamInfo:logTrace(message) + print("[TRACE]: " .. message) +end + +function StreamInfo:logDebug(message) + print("[DEBUG]: " .. message) +end + +function StreamInfo:logInfo(message) + print("[INFO]: " .. message) +end + +function StreamInfo:logWarn(message) + print("[WARN]: " .. message) +end + +function StreamInfo:logErr(message) + print("[ERROR]: " .. message) +end + +function StreamInfo:logCritical(message) + print("[CRITICAL]: " .. message) +end + +function StreamInfo:protocol() + return "HTTP/2" +end + +function StreamInfo:routeName() + return "example-route" +end + +function StreamInfo:virtualClusterName() + return "example-virtual-cluster" +end + +function StreamInfo:downstreamDirectLocalAddress() + return "127.0.0.1:8080" +end + +function StreamInfo:downstreamLocalAddress() + return "192.168.1.100:8080" +end + +function StreamInfo:downstreamDirectRemoteAddress() + return "192.168.1.50:5050" +end + +function StreamInfo:downstreamRemoteAddress() + return "10.0.0.1:5050" +end + +function StreamInfo:dynamicMetadata() + return DynamicMetadata +end + +function StreamInfo:downstreamSslConnection() + return SSLConnectionObject +end + +function StreamInfo:requestedServerName() + return "example-server" +end + +-- Metadata Object +local Metadata = { + data = { + key1 = "value1", + key2 = "value2" + } +} + +setmetatable(Metadata, { + __pairs = function(self) + return pairs(self.data) + end, + __index = function(self, key) + return self.data[key] + end +}) + +-- Logging Methods +function Metadata:logTrace(message) + print("[TRACE]: " .. message) +end + +function Metadata:logDebug(message) + print("[DEBUG]: " .. message) +end + +function Metadata:logInfo(message) + print("[INFO]: " .. message) +end + +function Metadata:logWarn(message) + print("[WARN]: " .. message) +end + +function Metadata:logErr(message) + print("[ERROR]: " .. message) +end + +function Metadata:logCritical(message) + print("[CRITICAL]: " .. message) +end + +function Metadata:get(key) + return "get_example" +end + +-- Buffer Object +local Buffer = {} + +-- Logging Methods +function Buffer:logTrace(message) + print("[TRACE]: " .. message) +end + +function Buffer:logDebug(message) + print("[DEBUG]: " .. message) +end + +function Buffer:logInfo(message) + print("[INFO]: " .. message) +end + +function Buffer:logWarn(message) + print("[WARN]: " .. message) +end + +function Buffer:logErr(message) + print("[ERROR]: " .. message) +end + +function Buffer:logCritical(message) + print("[CRITICAL]: " .. message) +end + +function Buffer:length() + return 10 +end + +function Buffer:getBytes(index, length) + return "example_bytes" +end + +function Buffer:setBytes(setTo) +end + +-- Headers Object +local Headers = { + data = { + key1 = "value1", + key2 = "value2" + } +} + +setmetatable(Headers, { + __pairs = function(self) + return pairs(self.data) + end, + __index = function(self, key) + return self.data[key] + end +}) + +-- Logging Methods +function Headers:logTrace(message) + print("[TRACE]: " .. message) +end + +function Headers:logDebug(message) + print("[DEBUG]: " .. message) +end + +function Headers:logInfo(message) + print("[INFO]: " .. message) +end + +function Headers:logWarn(message) + print("[WARN]: " .. message) +end + +function Headers:logErr(message) + print("[ERROR]: " .. message) +end + +function Headers:logCritical(message) + print("[CRITICAL]: " .. message) +end + +function Headers:add(key, value) +end + +function Headers:get(key) + return "example_value" +end + +function Headers:getAtIndex(key, index) + return "example_value" +end + +function Headers:getNumValues(key) + return 1 +end + +function Headers:remove(key) +end + +function Headers:replace(key, value) +end + +function Headers:setHttp1ReasonPhrase(reasonPhrase) +end + +function Headers:getHttp1ReasonPhrase() + return "reason" +end + +-- StreamHandle Object +local StreamHandle = { +data = {Buffer} +} + +-- Logging Methods (with internal logging mechanism) +function StreamHandle:logTrace(message) + print("[TRACE]: " .. message) +end + +function StreamHandle:logDebug(message) + print("[DEBUG]: " .. message) +end + +function StreamHandle:logInfo(message) + print("[INFO]: " .. message) +end + +function StreamHandle:logWarn(message) + print("[WARN]: " .. message) +end + +function StreamHandle:logErr(message) + print("[ERROR]: " .. message) +end + +function StreamHandle:logCritical(message) + print("[CRITICAL]: " .. message) +end + +function StreamHandle:headers() + return Headers +end + +function StreamHandle:body(always_wrap_body) + return "body" +end + +function StreamHandle:bodyChunks() + local index = 0 + local chunks = self.data or {} + + return function() + index = index + 1 + local chunk = chunks[index] + if chunk then + return chunk + end + end +end + +function StreamHandle:trailers() + return Headers +end + +function StreamHandle:httpCall(cluster, headers, body, timeout_ms, asynchronous) + self:logInfo("Making HTTP call to cluster: " .. cluster) + return headers, body +end + +function StreamHandle:respond(headers, body) + self:logInfo("Responding with status: " .. headers[":status"]) +end + +function StreamHandle:metadata() + return Metadata +end + +function StreamHandle:streamInfo() + return StreamInfo +end + +function StreamHandle:connection() + return Connection +end + +function StreamHandle:connectionStreamInfo() + return ConnectionStreamInfo +end + +function StreamHandle:setUpstreamOverrideHost(host, strict) +end + +function StreamHandle:importPublicKey(keyder, keyderLength) + return "mocked_public_key" +end + +function StreamHandle:verifySignature(hashFunction, pubkey, signature, signatureLength, data, dataLength) + return true, "" +end + +function StreamHandle:base64Escape(inputString) + return inputString +end + +function StreamHandle:timestamp(format) + return os.time() * 1000 +end + +function StreamHandle:timestampString(resolution) + return os.date("!%Y-%m-%dT%H:%M:%SZ", os.time()) +end \ No newline at end of file diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.in.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.in.yaml new file mode 100644 index 00000000000..e47c317a3cd --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.in.yaml @@ -0,0 +1,85 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/foo" + backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-2 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/bar" + backendRefs: + - name: service-1 + port: 8080 +envoyextensionpolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway # This policy should attach httproute-2 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + lua: + - type: Inline + inline: "function envoy_on_request(request_handle) + request_handle:logInfo('Goodbye.') + end" +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: default + name: policy-for-http-route # Invalid Lua + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + lua: + - type: Inline + inline: "function envoy_on_response(response_handle) + response_handle:UnknownApi() + end" diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.out.yaml new file mode 100644 index 00000000000..6fbb154308e --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua.out.yaml @@ -0,0 +1,276 @@ +envoyExtensionPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + creationTimestamp: null + name: policy-for-http-route + namespace: default + spec: + lua: + - inline: function envoy_on_response(response_handle) response_handle:UnknownApi() + end + type: Inline + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: "Lua: validation failed for lua body in policy with name envoyextensionpolicy/default/policy-for-http-route/lua/0: + failed to mock run envoy_on_response: :614: attempt to call a non-function + object\nstack traceback:\n\t:614: in function 'envoy_on_response'\n\t:615: + in main chunk\n\t[G]: ?." + reason: Invalid + status: "False" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + spec: + lua: + - inline: function envoy_on_request(request_handle) request_handle:logInfo('Goodbye.') + end + type: Inline + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: 'This policy is being overridden by other envoyExtensionPolicies + for these routes: [default/httproute-1]' + reason: Overridden + status: "True" + type: Overridden + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 2 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /foo + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-2 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /bar + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + directResponse: + statusCode: 500 + envoyExtensions: + luas: + - Body: function envoy_on_request(request_handle) request_handle:logInfo('Goodbye.') + end + Name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/0 + hostname: www.example.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /foo + - destination: + name: httproute/default/httproute-2/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + envoyExtensions: + luas: + - Body: function envoy_on_request(request_handle) request_handle:logInfo('Goodbye.') + end + Name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/0 + hostname: www.example.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-2 + namespace: default + name: httproute/default/httproute-2/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /bar diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-configmap.in.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-configmap.in.yaml new file mode 100644 index 00000000000..2f3e2487fcc --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-configmap.in.yaml @@ -0,0 +1,108 @@ +configmaps: +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm-policy-for-gateway + namespace: envoy-gateway + data: + lua: | + function envoy_on_request(request_handle) + request_handle:logInfo('Goodbye.') + end +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm-policy-for-http-route + namespace: default + data: + lua: | + function envoy_on_response(response_handle) + response_handle:logWarn('Goodbye.') + end +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/foo" + backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-2 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/bar" + backendRefs: + - name: service-1 + port: 8080 +envoyextensionpolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway # This policy should attach httproute-2 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + lua: + - type: ValueRef + valueRef: + name: cm-policy-for-gateway + kind: ConfigMap + 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 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..68e01d6c26f --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-configmap.out.yaml @@ -0,0 +1,279 @@ +envoyExtensionPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + creationTimestamp: null + name: policy-for-http-route + namespace: default + spec: + lua: + - type: ValueRef + valueRef: + group: 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-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]' + reason: Overridden + status: "True" + type: Overridden + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 2 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /foo + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-2 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /bar + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + envoyExtensions: + luas: + - Body: | + function envoy_on_response(response_handle) + response_handle:logWarn('Goodbye.') + end + Name: envoyextensionpolicy/default/policy-for-http-route/lua/0 + hostname: www.example.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /foo + - destination: + name: httproute/default/httproute-2/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + envoyExtensions: + luas: + - Body: | + function envoy_on_request(request_handle) + request_handle:logInfo('Goodbye.') + end + Name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/0 + hostname: www.example.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-2 + namespace: default + name: httproute/default/httproute-2/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /bar diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua.in.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua.in.yaml new file mode 100644 index 00000000000..65dd36d9f1a --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua.in.yaml @@ -0,0 +1,85 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/foo" + backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-2 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/bar" + backendRefs: + - name: service-1 + port: 8080 +envoyextensionpolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway # This policy should attach httproute-2 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + lua: + - type: Inline + inline: "function envoy_on_request(request_handle) + request_handle:logInfo('Goodbye.') + end" +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: default + name: policy-for-http-route # This policy should attach httproute-1 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + lua: + - type: Inline + inline: "function envoy_on_response(response_handle) + response_handle:logWarn('Goodbye.') + end" diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua.out.yaml new file mode 100644 index 00000000000..6f22ad9811b --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua.out.yaml @@ -0,0 +1,271 @@ +envoyExtensionPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + creationTimestamp: null + name: policy-for-http-route + namespace: default + spec: + lua: + - inline: function envoy_on_response(response_handle) response_handle:logWarn('Goodbye.') + end + type: Inline + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + spec: + lua: + - inline: function envoy_on_request(request_handle) request_handle:logInfo('Goodbye.') + end + type: Inline + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: 'This policy is being overridden by other envoyExtensionPolicies + for these routes: [default/httproute-1]' + reason: Overridden + status: "True" + type: Overridden + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 2 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /foo + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-2 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /bar + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + envoyExtensions: + luas: + - Body: function envoy_on_response(response_handle) response_handle:logWarn('Goodbye.') + end + Name: envoyextensionpolicy/default/policy-for-http-route/lua/0 + hostname: www.example.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /foo + - destination: + name: httproute/default/httproute-2/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + envoyExtensions: + luas: + - Body: function envoy_on_request(request_handle) request_handle:logInfo('Goodbye.') + end + Name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/0 + hostname: www.example.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-2 + namespace: default + name: httproute/default/httproute-2/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /bar diff --git a/internal/ir/xds.go b/internal/ir/xds.go index bbec9abdf5b..5c727a5a1cb 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -863,6 +863,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 @@ -2801,6 +2803,16 @@ type ExtProc struct { AllowModeOverride bool `json:"allowModeOverride,omitempty" yaml:"allowModeOverride,omitempty"` } +// Lua holds the information associated with Lua extensions +// +k8s:deepcopy-gen=true +type Lua struct { + // Name is a unique name for the LUa configuration. + // The xds translator only generates one Lua filter for each unique name + Name string + // Body is the Lua source body + Body *string +} + // Wasm holds the information associated with the Wasm extensions. // +k8s:deepcopy-gen=true type Wasm struct { diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 1856e1da8ae..0c5712c69fe 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.Body != nil { + in, out := &in.Body, &out.Body + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Lua. +func (in *Lua) DeepCopy() *Lua { + if in == nil { + return nil + } + out := new(Lua) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Metrics) DeepCopyInto(out *Metrics) { *out = *in diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go index bc7b5ba8c53..5c1dd9ac4fc 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,23 @@ 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 err := r.processConfigMapRef( + ctx, + resourceMap, + resourceTree, + resource.KindConfigMap, + policy.GetNamespace(), + policy.GetName(), + *lua.ValueRef); err != nil { + r.log.Error(err, + "failed to process Lua ValueRef for EnvoyExtensionPolicy", + "policy", policy, "valueRef", lua.ValueRef) + } + } + } } } diff --git a/internal/xds/translator/lua.go b/internal/xds/translator/lua.go new file mode 100644 index 00000000000..1fd8a1a67a9 --- /dev/null +++ b/internal/xds/translator/lua.go @@ -0,0 +1,108 @@ +// 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 +// TODO: create GW level Lua EEPs +func (*lua) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error { + return nil +} + +// buildLuaPerRouteFilterConfig returns a lua filter override for a route. +func buildLuaPerRouteFilterConfig(lua ir.Lua) (*anypb.Any, error) { + var ( + luaProto *luafilterv3.LuaPerRoute + luaAny *anypb.Any + err error + ) + luaProto = &luafilterv3.LuaPerRoute{ + Override: &luafilterv3.LuaPerRoute_SourceCode{ + SourceCode: &corev3.DataSource{ + Specifier: &corev3.DataSource_InlineString{ + InlineString: *lua.Body, + }, + }, + }, + } + if err = luaProto.ValidateAll(); err != nil { + return nil, err + } + if luaAny, err = anypb.New(luaProto); err != nil { + return nil, err + } + + return 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 with the lua config 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 + } + if route.TypedPerFilterConfig == nil { + route.TypedPerFilterConfig = make(map[string]*anypb.Any) + } + + var errs error + for _, ep := range irRoute.EnvoyExtensions.Luas { + if routeContainsFilter(route, luaFilterName(ep)) { + continue + } + filterConfig, err := buildLuaPerRouteFilterConfig(ep) + if err != nil { + errs = errors.Join(errs, err) + continue + } + route.TypedPerFilterConfig[luaFilterName(ep)] = filterConfig + } + + return errs +} diff --git a/internal/xds/translator/testdata/in/xds-ir/lua.yaml b/internal/xds/translator/testdata/in/xds-ir/lua.yaml new file mode 100644 index 00000000000..9e057b90b3f --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/lua.yaml @@ -0,0 +1,59 @@ +http: +- address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: www.example.com + isHTTP2: false + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /foo + envoyExtensions: + luas: + - body: function envoy_on_request(request_handle) + request_handle:logInfo('Goodbye.') + end + name: envoyextensionpolicy/default/policy-for-http-route/lua/0 + - destination: + name: httproute/default/httproute-2/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: www.example.com + isHTTP2: false + name: httproute/default/httproute-2/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /bar + envoyExtensions: + luas: + - body: function envoy_on_response(response_handle) + response_handle:logWarn('Goodbye.') + end + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/0 + - body: function envoy_on_response(response_handle) + response_handle:logError('Hello.') + end + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/1 diff --git a/internal/xds/translator/testdata/out/xds-ir/lua.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/lua.clusters.yaml new file mode 100644 index 00000000000..ba27dfd9d28 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/lua.clusters.yaml @@ -0,0 +1,34 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/default/httproute-1/rule/0 + ignoreHealthOnHostRemoval: true + lbPolicy: LEAST_REQUEST + name: httproute/default/httproute-1/rule/0 + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/default/httproute-2/rule/0 + ignoreHealthOnHostRemoval: true + lbPolicy: LEAST_REQUEST + name: httproute/default/httproute-2/rule/0 + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/lua.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/lua.endpoints.yaml new file mode 100644 index 00000000000..05442a9a15b --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/lua.endpoints.yaml @@ -0,0 +1,24 @@ +- clusterName: httproute/default/httproute-1/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 7.7.7.7 + portValue: 8080 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: httproute/default/httproute-1/rule/0/backend/0 +- clusterName: httproute/default/httproute-2/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 7.7.7.7 + portValue: 8080 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: httproute/default/httproute-2/rule/0/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/lua.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/lua.listeners.yaml new file mode 100644 index 00000000000..09426a31773 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/lua.listeners.yaml @@ -0,0 +1,34 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.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..0d6fe97f239 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/lua.routes.yaml @@ -0,0 +1,38 @@ +- 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.extensions.filters.http.lua.v3.LuaPerRoute + sourceCode: + inlineString: function envoy_on_request(request_handle) request_handle:logInfo('Goodbye.') + end + - 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.extensions.filters.http.lua.v3.LuaPerRoute + sourceCode: + inlineString: function envoy_on_response(response_handle) response_handle:logWarn('Goodbye.') + end + envoy.filters.http.lua/envoyextensionpolicy/envoy-gateway/policy-for-gateway/lua/1: + '@type': type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute + sourceCode: + inlineString: function envoy_on_response(response_handle) response_handle:logError('Hello.') + end diff --git a/internal/xds/translator/utils.go b/internal/xds/translator/utils.go index a4a38be8ebb..ae17b53a596 100644 --- a/internal/xds/translator/utils.go +++ b/internal/xds/translator/utils.go @@ -129,6 +129,15 @@ func hcmContainsFilter(mgr *hcmv3.HttpConnectionManager, filterName string) bool return false } +func routeContainsFilter(route *routev3.Route, filterName string) bool { + for existingFilterName := range route.TypedPerFilterConfig { + if existingFilterName == filterName { + return true + } + } + return false +} + func createExtServiceXDSCluster(rd *ir.RouteDestination, traffic *ir.TrafficFeatures, tCtx *types.ResourceVersionTable) error { var ( endpointType EndpointType diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 12181f7cc3a..27cb36434d3 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.
| @@ -2845,7 +2846,7 @@ _Appears in:_ | --- | --- | --- | --- | --- | | `type` | _[LuaValueType](#luavaluetype)_ | true | Inline | Type is the type of method to use to read the Lua value.
Valid values are Inline and ValueRef, default is Inline. | | `inline` | _string_ | false | | Inline contains the source code as an inline string. | -| `valueRef` | _[LocalObjectReference](#localobjectreference)_ | false | | ValueRef has the source code specified as a local object reference.
Only a reference to ConfigMap is supported.
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. | +| `valueRef` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | false | | ValueRef has the source code specified as a local object reference.
Only a reference to ConfigMap is supported.
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. | #### LuaValueType diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md index 12181f7cc3a..27cb36434d3 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.
| @@ -2845,7 +2846,7 @@ _Appears in:_ | --- | --- | --- | --- | --- | | `type` | _[LuaValueType](#luavaluetype)_ | true | Inline | Type is the type of method to use to read the Lua value.
Valid values are Inline and ValueRef, default is Inline. | | `inline` | _string_ | false | | Inline contains the source code as an inline string. | -| `valueRef` | _[LocalObjectReference](#localobjectreference)_ | false | | ValueRef has the source code specified as a local object reference.
Only a reference to ConfigMap is supported.
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. | +| `valueRef` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | false | | ValueRef has the source code specified as a local object reference.
Only a reference to ConfigMap is supported.
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. | #### LuaValueType diff --git a/test/cel-validation/envoyextensionpolicy_test.go b/test/cel-validation/envoyextensionpolicy_test.go index ec401f2c188..730ac1ad0e8 100644 --- a/test/cel-validation/envoyextensionpolicy_test.go +++ b/test/cel-validation/envoyextensionpolicy_test.go @@ -444,8 +444,8 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { Lua: []egv1a1.Lua{ { Type: egv1a1.LuaValueTypeValueRef, - ValueRef: &gwapiv1a2.LocalObjectReference{ - Kind: gwapiv1a2.Kind("ConfigMap"), + ValueRef: &gwapiv1a2.SecretObjectReference{ + Kind: ptr.To(gwapiv1a2.Kind("ConfigMap")), Name: gwapiv1a2.ObjectName("eg"), }, }, @@ -470,8 +470,8 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { Lua: []egv1a1.Lua{ { Type: egv1a1.LuaValueTypeInline, - ValueRef: &gwapiv1a2.LocalObjectReference{ - Kind: gwapiv1a2.Kind("ConfigMap"), + ValueRef: &gwapiv1a2.SecretObjectReference{ + Kind: ptr.To(gwapiv1a2.Kind("ConfigMap")), Name: gwapiv1a2.ObjectName("eg"), }, }, @@ -523,8 +523,8 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { Lua: []egv1a1.Lua{ { Type: egv1a1.LuaValueTypeValueRef, - ValueRef: &gwapiv1a2.LocalObjectReference{ - Kind: gwapiv1a2.Kind("NotConfigMap"), + ValueRef: &gwapiv1a2.SecretObjectReference{ + Kind: ptr.To(gwapiv1a2.Kind("NotConfigMap")), Name: gwapiv1a2.ObjectName("eg"), }, }, @@ -541,7 +541,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 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.SecretObjectReference{ + Kind: ptr.To(gwapiv1a2.Kind("ConfigMap")), + Name: gwapiv1a2.ObjectName("eg"), + Group: ptr.To(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 API group is supported.", }, }, { @@ -552,8 +581,8 @@ 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"), + ValueRef: &gwapiv1a2.SecretObjectReference{ + Kind: ptr.To(gwapiv1a2.Kind("ConfigMap")), Name: gwapiv1a2.ObjectName("eg"), }, }, diff --git a/test/e2e/testdata/lua-http.yaml b/test/e2e/testdata/lua-http.yaml new file mode 100644 index 00000000000..03ac480c800 --- /dev/null +++ b/test/e2e/testdata/lua-http.yaml @@ -0,0 +1,45 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: example-route + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + hostnames: ["www.example.com"] + rules: + - matches: + - path: + type: PathPrefix + value: / + 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", "processed") + end +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyExtensionPolicy +metadata: + name: example-lua + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: example-route + lua: + - type: ValueRef + valueRef: + name: cm-example-lua + kind: ConfigMap diff --git a/test/e2e/tests/lua_http.go b/test/e2e/tests/lua_http.go new file mode 100644 index 00000000000..9ae71b5610e --- /dev/null +++ b/test/e2e/tests/lua_http.go @@ -0,0 +1,75 @@ +// 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 always allows/denies", + Manifests: []string{"testdata/lua-http.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("http route with always allow lua", func(t *testing.T) { + testLuaHTTP(t, suite, "example-route", "example-lua", "/", 200) + }) + }, +} + +func testLuaHTTP(t *testing.T, suite *suite.ConformanceTestSuite, route, eep, path string, expectedStatus int) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: route, Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + ancestorRef := gwapiv1a2.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(resource.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + } + EnvoyExtensionPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: eep, Namespace: ns}, suite.ControllerName, ancestorRef) + + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Host: "www.example.com", + Path: path, + }, + ExpectedRequest: &http.ExpectedRequest{ + Request: http.Request{ + Host: "www.example.com", + Path: path, + }, + }, + Namespace: "gateway-conformance-infra", + Response: http.Response{ + StatusCode: expectedStatus, + Headers: map[string]string{ + "x-custom-lua": "processed", // response header added by lua + }, + }, + } + + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) +} diff --git a/test/e2e/tests/utils.go b/test/e2e/tests/utils.go index a674e5fc69f..b2d49756f62 100644 --- a/test/e2e/tests/utils.go +++ b/test/e2e/tests/utils.go @@ -330,11 +330,12 @@ 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) + luaJSON, _ := json.Marshal(policy.Spec.Lua) + tlog.Logf(t, "EnvoyExtensionPolicy has been accepted: %+v, lua: %+v", policy, luaJSON) return true, nil } - tlog.Logf(t, "EnvoyExtensionPolicy not yet accepted: %v", policy) + tlog.Logf(t, "EnvoyExtensionPolicy not yet accepted: %+v", policy) return false, nil })