Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions api/v1alpha1/dynamic_module_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const (
//
// +kubebuilder:validation:XValidation:rule="self.type == 'Remote' ? has(self.remote) : !has(self.remote)",message="If type is Remote, remote field needs to be set."
// +kubebuilder:validation:XValidation:rule="self.type != 'Local' || has(self.local)",message="If type is Local, local field needs to be set."
// +kubebuilder:validation:XValidation:rule="self.type == 'Remote' ? !has(self.local) : true",message="If type is Remote, local field must not be set."
type DynamicModuleSource struct {
// Type is the type of the source of the dynamic module code.
// Defaults to Local.
Expand All @@ -45,7 +46,6 @@ type DynamicModuleSource struct {
// The module binary is downloaded and cached by Envoy.
//
// +optional
// +notImplementedHide
Remote *RemoteDynamicModuleSource `json:"remote,omitempty"`
}

Expand All @@ -58,10 +58,20 @@ type LocalDynamicModuleSource struct {
Path string `json:"path"`
}

// RemoteDynamicModuleSource defines a dynamic module fetched from a remote source.
//
// +notImplementedHide
type RemoteDynamicModuleSource struct{}
// RemoteDynamicModuleSource defines a dynamic module fetched from a remote HTTP source.
type RemoteDynamicModuleSource struct {
// URL is the HTTP or HTTPS URL of the dynamic module shared library (.so file).
//
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=4096
// +kubebuilder:validation:Pattern=`^https?://[^/?#]+(?:[/?#].*)?$`
URL string `json:"url"`

// SHA256 checksum that Envoy will use to verify the downloaded module binary.
//
// +kubebuilder:validation:Pattern=`^[a-f0-9]{64}$`
SHA256 string `json:"sha256"`
}

// DynamicModuleEntry defines a dynamic module that is registered and allowed
// for use by EnvoyExtensionPolicy resources.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,22 @@ spec:
description: |-
Remote specifies a module fetched from a remote source.
The module binary is downloaded and cached by Envoy.
properties:
sha256:
description: SHA256 checksum that Envoy will use to
verify the downloaded module binary.
pattern: ^[a-f0-9]{64}$
type: string
url:
description: URL is the HTTP or HTTPS URL of the dynamic
module shared library (.so file).
maxLength: 4096
minLength: 1
pattern: ^https?://[^/?#]+(?:[/?#].*)?$
type: string
required:
- sha256
- url
type: object
type:
default: Local
Expand All @@ -368,6 +384,8 @@ spec:
rule: 'self.type == ''Remote'' ? has(self.remote) : !has(self.remote)'
- message: If type is Local, local field needs to be set.
rule: self.type != 'Local' || has(self.local)
- message: If type is Remote, local field must not be set.
rule: 'self.type == ''Remote'' ? !has(self.local) : true'
required:
- name
- source
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,22 @@ spec:
description: |-
Remote specifies a module fetched from a remote source.
The module binary is downloaded and cached by Envoy.
properties:
sha256:
description: SHA256 checksum that Envoy will use to
verify the downloaded module binary.
pattern: ^[a-f0-9]{64}$
type: string
url:
description: URL is the HTTP or HTTPS URL of the dynamic
module shared library (.so file).
maxLength: 4096
minLength: 1
pattern: ^https?://[^/?#]+(?:[/?#].*)?$
type: string
required:
- sha256
- url
type: object
type:
default: Local
Expand All @@ -367,6 +383,8 @@ spec:
rule: 'self.type == ''Remote'' ? has(self.remote) : !has(self.remote)'
- message: If type is Local, local field needs to be set.
rule: self.type != 'Local' || has(self.local)
- message: If type is Remote, local field must not be set.
rule: 'self.type == ''Remote'' ? !has(self.local) : true'
required:
- name
- source
Expand Down
77 changes: 61 additions & 16 deletions internal/gatewayapi/envoyextensionpolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package gatewayapi
import (
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -46,6 +47,31 @@ func deprecatedFieldsUsedInEnvoyExtensionPolicy(policy *egv1a1.EnvoyExtensionPol
return deprecatedFields
}

func validateDynamicModuleRemoteURL(rawURL string) error {
parsedURL, err := url.ParseRequestURI(rawURL)
if err != nil {
return err
}

switch parsedURL.Scheme {
case "http", "https":
default:
return fmt.Errorf("unsupported URL scheme %q", parsedURL.Scheme)
}

if parsedURL.Hostname() == "" {
return errors.New("URL must include a hostname")
}

if port := parsedURL.Port(); port != "" {
if _, err := strconv.Atoi(port); err != nil {
return fmt.Errorf("invalid URL port %q: %w", port, err)
}
}

return nil
}

func (t *Translator) ProcessEnvoyExtensionPolicies(
envoyExtensionPolicies []*egv1a1.EnvoyExtensionPolicy,
gateways []*GatewayContext,
Expand Down Expand Up @@ -1190,31 +1216,50 @@ func (t *Translator) buildDynamicModules(
continue
}

// Resolve module path from source
if entry.Source.Type != nil && *entry.Source.Type == egv1a1.RemoteDynamicModuleSourceType {
errs = errors.Join(errs, fmt.Errorf("dynamic module %q uses remote source which is not yet implemented", dm.Name))
continue
}
if entry.Source.Local == nil {
errs = errors.Join(errs, fmt.Errorf("dynamic module %q has no local source configured", dm.Name))
continue
}
path := entry.Source.Local.Path

filterName := ""
if dm.FilterName != nil {
filterName = *dm.FilterName
}
filterName := ptr.Deref(dm.FilterName, "")

dmIR := ir.DynamicModule{
Name: name,
Path: path,
FilterName: filterName,
Config: dm.Config,
DoNotClose: ptr.Deref(entry.DoNotClose, false),
LoadGlobally: ptr.Deref(entry.LoadGlobally, false),
TerminalFilter: ptr.Deref(dm.TerminalFilter, false),
}

switch sourceType := ptr.Deref(entry.Source.Type, egv1a1.LocalDynamicModuleSourceType); sourceType {
case egv1a1.RemoteDynamicModuleSourceType:
if entry.Source.Remote == nil {
errs = errors.Join(errs, fmt.Errorf("dynamic module %q has no remote source configured", dm.Name))
continue
}
if entry.Source.Remote.URL == "" {
errs = errors.Join(errs, fmt.Errorf("dynamic module %q has no remote source URL configured", dm.Name))
continue
}
if entry.Source.Remote.SHA256 == "" {
errs = errors.Join(errs, fmt.Errorf("dynamic module %q has no remote source SHA256 configured", dm.Name))
continue
}
if err := validateDynamicModuleRemoteURL(entry.Source.Remote.URL); err != nil {
errs = errors.Join(errs, fmt.Errorf("dynamic module %q has invalid remote source URL %q: %w", dm.Name, entry.Source.Remote.URL, err))
continue
}
dmIR.Remote = &ir.RemoteDynamicModuleSource{
URL: entry.Source.Remote.URL,
SHA256: entry.Source.Remote.SHA256,
}
case egv1a1.LocalDynamicModuleSourceType:
if entry.Source.Local == nil {
errs = errors.Join(errs, fmt.Errorf("dynamic module %q has no local source configured", dm.Name))
continue
}
dmIR.Path = entry.Source.Local.Path
default:
errs = errors.Join(errs, fmt.Errorf("dynamic module %q has unsupported source type %q", dm.Name, sourceType))
continue
}

dmIRList = append(dmIRList, dmIR)
}

Expand Down
41 changes: 41 additions & 0 deletions internal/gatewayapi/envoyextensionpolicy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,44 @@ func Test_hasTag(t *testing.T) {
})
}
}

func TestValidateDynamicModuleRemoteURL(t *testing.T) {
tests := []struct {
name string
rawURL string
wantErr string
}{
{
name: "valid https URL",
rawURL: "https://modules.example.com/libremote_auth.so",
},
{
name: "valid http URL with port",
rawURL: "http://modules.example.com:8443/libremote_auth.so",
},
{
name: "missing hostname",
rawURL: "https:///libremote_auth.so",
wantErr: "hostname",
},
{
name: "unsupported scheme",
rawURL: "ftp://modules.example.com/libremote_auth.so",
wantErr: "unsupported URL scheme",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateDynamicModuleRemoteURL(tt.rawURL)
if tt.wantErr == "" {
assert.NoError(t, err)
return
}

if assert.Error(t, err) {
assert.ErrorContains(t, err, tt.wantErr)
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ envoyProxiesForGateways:
type: Local
local:
path: /usr/lib/envoy/modules/my_auth.so
- name: remote-auth-module
source:
type: Remote
remote:
url: https://modules.example.com/libremote_auth.so
sha256: abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789
- name: ai-gateway-filter
source:
local:
Expand Down Expand Up @@ -97,6 +103,10 @@ envoyextensionpolicies:
filterName: auth-check
config:
authEndpoint: https://auth.example.com
- name: remote-auth-module
filterName: remote-auth-check
config:
authEndpoint: https://remote-auth.example.com
- apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyExtensionPolicy
metadata:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ envoyExtensionPolicies:
authEndpoint: https://auth.example.com
filterName: auth-check
name: my-auth-module
- config:
authEndpoint: https://remote-auth.example.com
filterName: remote-auth-check
name: remote-auth-module
targetRef:
group: gateway.networking.k8s.io
kind: Gateway
Expand Down Expand Up @@ -212,6 +216,12 @@ infraIR:
local:
path: /usr/lib/envoy/modules/my_auth.so
type: Local
- name: remote-auth-module
source:
remote:
sha256: abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789
url: https://modules.example.com/libremote_auth.so
type: Remote
- doNotClose: true
loadGlobally: true
name: ai-gateway-filter
Expand Down Expand Up @@ -358,6 +368,16 @@ xdsIR:
name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/dynamic-module/0
path: /usr/lib/envoy/modules/my_auth.so
terminalFilter: false
- config:
authEndpoint: https://remote-auth.example.com
doNotClose: false
filterName: remote-auth-check
loadGlobally: false
name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/dynamic-module/1
remote:
sha256: abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789
url: https://modules.example.com/libremote_auth.so
terminalFilter: false
hostname: www.example.com
isHTTP2: false
metadata:
Expand Down
15 changes: 14 additions & 1 deletion internal/ir/xds.go
Original file line number Diff line number Diff line change
Expand Up @@ -3565,7 +3565,10 @@ type DynamicModule struct {
Name string `json:"name"`

// Path is the absolute filesystem path to the dynamic module shared library.
Path string `json:"path"`
Path string `json:"path,omitempty"`

// Remote is the remote source of the dynamic module shared library.
Remote *RemoteDynamicModuleSource `json:"remote,omitempty"`

// FilterName identifies the filter implementation within the module.
FilterName string `json:"filterName,omitempty"`
Expand All @@ -3583,6 +3586,16 @@ type DynamicModule struct {
TerminalFilter bool `json:"terminalFilter"`
}

// RemoteDynamicModuleSource holds the remote source information for a dynamic module.
// +k8s:deepcopy-gen=true
type RemoteDynamicModuleSource struct {
// URL is the HTTP(S) URL of the dynamic module shared library.
URL string `json:"url"`

// SHA256 is the checksum used by Envoy to verify the downloaded module.
SHA256 string `json:"sha256"`
}

// DestinationFilters contains HTTP filters that will be used with the DestinationSetting.
// +k8s:deepcopy-gen=true
type DestinationFilters struct {
Expand Down
20 changes: 20 additions & 0 deletions internal/ir/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading