Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d392a13
controller_offline.go: register extension-server policy kinds in clie…
daum3ns Oct 21, 2025
e7ed24c
controller_offline.go: add comments about possible missing extension …
daum3ns Oct 21, 2025
59f8ae0
load.go: temporary treat all unknown kinds as extension server policy
daum3ns Oct 21, 2025
762ffe2
runner.go: pass extension policy GVKs in translator
daum3ns Oct 21, 2025
ddea828
file provider: persist ExtensionServerPolicies to store
daum3ns Oct 21, 2025
db3c6e8
test: add offline controller test for custom extension resources
daum3ns Oct 22, 2025
e9263de
config loader test: add testcase for standalone mode with extension
daum3ns Oct 22, 2025
61c416a
test(config/decoder): Add a new test case to that verifies decoding o…
daum3ns Oct 22, 2025
c6f855f
set default namespace in custom resources if no namespace is provided
daum3ns Oct 23, 2025
7302313
test/controller_offline: extend test to verify the CR can be loaded from
daum3ns Oct 23, 2025
26a0312
add missin yaml for test
daum3ns Oct 24, 2025
3c1e2b9
configloader test: fix race condition
daum3ns Oct 24, 2025
21cc5f8
add configuration to provider struct, pass it along so that the loader
daum3ns Oct 24, 2025
d66478e
add missing yaml for decoder test
daum3ns Oct 24, 2025
0b63ad4
fix linter warnings
daum3ns Oct 30, 2025
f996f5a
controller_offline_test: extend test to show the issue also affects
daum3ns Oct 30, 2025
6e480ac
fix testcase, register all extensions im scheme
daum3ns Oct 31, 2025
ed62242
load.go: store route filter resources and custom backend resources in…
daum3ns Nov 4, 2025
63e49df
file provider: persist ExtensionRefFilters to store
daum3ns Nov 4, 2025
5d8fdb6
controller_offline_test: adapt test expectation
daum3ns Nov 4, 2025
b423cdf
typo
daum3ns Nov 4, 2025
de1eae3
adapt YAML file for controller_offline.test
daum3ns Nov 4, 2025
af6d1d5
extend decoder test with extensionmanager.resources and
daum3ns Nov 4, 2025
150c9e6
revises from review: rename loop variable, use envoyGateway directly
daum3ns Nov 5, 2025
0ef8f43
fix linter errors
daum3ns Nov 5, 2025
fe0585d
Merge branch 'main' into support-custom-CRDs-for-ExtensionServer-in-S…
shawnh2 Nov 6, 2025
568f6fc
Refactor loadKubernetesYAMLToResources to get rid of the default label
daum3ns Nov 6, 2025
0de3be1
controller_offline_test: add testcase that shows problem with
daum3ns Nov 7, 2025
fa2a671
controller_offline: register missing indices
daum3ns Nov 7, 2025
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
2 changes: 1 addition & 1 deletion internal/cmd/egctl/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ func translate(w io.Writer, inFile, inType string, outTypes []string, output, re

if inType == gatewayAPIType {
// Unmarshal input
resources, err := resource.LoadResourcesFromYAMLBytes(inBytes, addMissingResources)
resources, err := resource.LoadResourcesFromYAMLBytes(inBytes, addMissingResources, nil)
if err != nil {
return fmt.Errorf("unable to unmarshal input: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/egctl/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func runValidate(w io.Writer, inFile string) error {
noErr := true
_ = resource.IterYAMLBytes(inBytes, func(yamlByte []byte) error {
// Passing each resource as YAML string and get all their errors from local validator.
_, err = resource.LoadResourcesFromYAMLBytes(yamlByte, false)
_, err = resource.LoadResourcesFromYAMLBytes(yamlByte, false, nil)
if err != nil {
noErr = false
yamlRows := bytes.Split(yamlByte, []byte("\n"))
Expand Down
82 changes: 82 additions & 0 deletions internal/envoygateway/config/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,88 @@ func TestDecode(t *testing.T) {
},
expect: true,
},
{
in: inPath + "standalone-extension-server.yaml",
out: &egv1a1.EnvoyGateway{
TypeMeta: metav1.TypeMeta{
Kind: egv1a1.KindEnvoyGateway,
APIVersion: egv1a1.GroupVersion.String(),
},
EnvoyGatewaySpec: egv1a1.EnvoyGatewaySpec{
Gateway: egv1a1.DefaultGateway(),
Provider: &egv1a1.EnvoyGatewayProvider{
Type: egv1a1.ProviderTypeCustom,
Custom: &egv1a1.EnvoyGatewayCustomProvider{
Resource: egv1a1.EnvoyGatewayResourceProvider{
Type: egv1a1.ResourceProviderTypeFile,
File: &egv1a1.EnvoyGatewayFileResourceProvider{
Paths: []string{
"/tmp/envoy-gateway-test",
},
},
},
Infrastructure: &egv1a1.EnvoyGatewayInfrastructureProvider{
Type: egv1a1.InfrastructureProviderTypeHost,
Host: &egv1a1.EnvoyGatewayHostInfrastructureProvider{},
},
},
},
Logging: egv1a1.DefaultEnvoyGatewayLogging(),
ExtensionManager: &egv1a1.ExtensionManager{
Resources: []egv1a1.GroupVersionKind{
{
Group: "gateway.example.io",
Version: "v1alpha1",
Kind: "CustomRouteFilterResource",
},
},
BackendResources: []egv1a1.GroupVersionKind{
{
Group: "storage.example.io",
Version: "v1",
Kind: "S3Bucket",
},
},
PolicyResources: []egv1a1.GroupVersionKind{
{
Group: "gateway.example.io",
Version: "v1alpha1",
Kind: "ExampleExtPolicy",
},
},
Hooks: &egv1a1.ExtensionHooks{
XDSTranslator: &egv1a1.XDSTranslatorHooks{
Post: []egv1a1.XDSTranslatorHook{
egv1a1.XDSHTTPListener,
egv1a1.XDSRoute,
egv1a1.XDSVirtualHost,
egv1a1.XDSCluster,
egv1a1.XDSTranslation,
},
},
},
Service: &egv1a1.ExtensionService{
BackendEndpoint: egv1a1.BackendEndpoint{
FQDN: &egv1a1.FQDNEndpoint{
Hostname: "127.0.0.1",
Port: 5005,
},
},
},
},
ExtensionAPIs: &egv1a1.ExtensionAPISettings{
EnableBackend: true,
EnableEnvoyPatchPolicy: false,
},
RuntimeFlags: &egv1a1.RuntimeFlags{
Enabled: []egv1a1.RuntimeFlag{
"XDSNameSchemeV2",
},
},
},
},
expect: true,
},
}

for _, tc := range testCases {
Expand Down
61 changes: 60 additions & 1 deletion internal/envoygateway/config/loader/configloader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import (
"context"
_ "embed"
"os"
"sync/atomic"
"testing"

"github.com/stretchr/testify/require"

egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/envoyproxy/gateway/internal/envoygateway/config"
)

Expand All @@ -21,6 +23,10 @@ var (
defaultConfig string
//go:embed testdata/enable-redis.yaml
redisConfig string
//go:embed testdata/standalone.yaml
standaloneConfig string
//go:embed testdata/standalone-enable-extension-server.yaml
standaloneConfigWithExtensionServer string
)

func TestConfigLoader(t *testing.T) {
Expand All @@ -35,7 +41,7 @@ func TestConfigLoader(t *testing.T) {
s, err := config.New(os.Stdout, os.Stderr)
require.NoError(t, err)

ctx, cancel := context.WithCancel(context.TODO())
ctx, cancel := context.WithCancel(context.Background())
defer func() {
cancel()
}()
Expand All @@ -57,3 +63,56 @@ func TestConfigLoader(t *testing.T) {

<-ctx.Done()
}

func TestConfigLoaderStandaloneExtensionServerAndCustomResource(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "envoy-gateway-configloader-test")
require.NoError(t, err)
defer func(path string) {
_ = os.RemoveAll(path)
}(tmpDir)

cfgPath := tmpDir + "/config.yaml"
require.NoError(t, os.WriteFile(cfgPath, []byte(standaloneConfig), 0o600))
s, err := config.New(os.Stdout, os.Stderr)
require.NoError(t, err)

ctx, cancel := context.WithCancel(context.Background())
defer func() {
cancel()
}()

type testResult struct {
changed int32
extMgr *egv1a1.ExtensionManager
}
resultChannel := make(chan testResult, 1)

var changed int32
loader := New(cfgPath, s, func(_ context.Context, cfg *config.Server) error {
c := atomic.AddInt32(&changed, 1)
t.Logf("config changed %d times", c)
if c > 1 {
resultChannel <- testResult{changed: c, extMgr: cfg.EnvoyGateway.ExtensionManager}
cancel()
}
return nil
})

require.NoError(t, loader.Start(ctx, os.Stdout))
require.NotNil(t, loader.cfg.EnvoyGateway)
require.Nil(t, loader.cfg.EnvoyGateway.ExtensionManager)

go func() {
_ = os.WriteFile(cfgPath, []byte(standaloneConfigWithExtensionServer), 0o600)
}()

<-ctx.Done()
res := <-resultChannel // wait until second reload
require.Equal(t, int32(2), res.changed)
require.NotNil(t, res.extMgr)
require.NotNil(t, res.extMgr.PolicyResources)
require.Len(t, res.extMgr.PolicyResources, 1)
require.Equal(t, "gateway.example.io", res.extMgr.PolicyResources[0].Group)
require.Equal(t, "v1alpha1", res.extMgr.PolicyResources[0].Version)
require.Equal(t, "ExampleExtPolicy", res.extMgr.PolicyResources[0].Kind)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyGateway
gateway:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
logging:
level:
default: info
provider:
type: Custom
custom:
resource:
type: File
file:
paths: ["/tmp/envoy-gateway-test"]
infrastructure:
type: Host
host: {}
extensionApis:
enableBackend: true
extensionManager:
policyResources:
- group: gateway.example.io
version: v1alpha1
kind: ExampleExtPolicy
hooks:
xdsTranslator:
post:
- HTTPListener
- Route
- VirtualHost
- Cluster
- Translation
service:
fqdn:
hostname: 127.0.0.1
port: 5005
runtimeFlags:
enabled:
- XDSNameSchemeV2
22 changes: 22 additions & 0 deletions internal/envoygateway/config/loader/testdata/standalone.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyGateway
gateway:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
logging:
level:
default: info
provider:
type: Custom
custom:
resource:
type: File
file:
paths: ["/tmp/envoy-gateway-test"]
infrastructure:
type: Host
host: {}
extensionApis:
enableBackend: true
runtimeFlags:
enabled:
- XDSNameSchemeV2
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyGateway
gateway:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
logging:
level:
default: info
provider:
type: Custom
custom:
resource:
type: File
file:
paths: ["/tmp/envoy-gateway-test"]
infrastructure:
type: Host
host: {}
extensionApis:
enableBackend: true
extensionManager:
resources:
- group: gateway.example.io
version: v1alpha1
kind: CustomRouteFilterResource
backendResources:
- group: storage.example.io
version: v1
kind: S3Bucket
policyResources:
- group: gateway.example.io
version: v1alpha1
kind: ExampleExtPolicy
hooks:
xdsTranslator:
post:
- HTTPListener
- Route
- VirtualHost
- Cluster
- Translation
service:
fqdn:
hostname: 127.0.0.1
port: 5005
runtimeFlags:
enabled:
- XDSNameSchemeV2
45 changes: 42 additions & 3 deletions internal/gatewayapi/resource/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ const dummyClusterIP = "1.2.3.4"

// LoadResourcesFromYAMLBytes will load Resources from given Kubernetes YAML string.
// TODO: This function should be able to process arbitrary number of resources, tracked by https://github.com/envoyproxy/gateway/issues/3207.
func LoadResourcesFromYAMLBytes(yamlBytes []byte, addMissingResources bool) (*Resources, error) {
r, err := loadKubernetesYAMLToResources(yamlBytes, addMissingResources)
func LoadResourcesFromYAMLBytes(yamlBytes []byte, addMissingResources bool, envoyGateway *egv1a1.EnvoyGateway) (*Resources, error) {
r, err := loadKubernetesYAMLToResources(yamlBytes, addMissingResources, envoyGateway)
if err != nil {
return nil, err
}
Expand All @@ -53,7 +53,7 @@ func LoadResourcesFromYAMLBytes(yamlBytes []byte, addMissingResources bool) (*Re
}

// loadKubernetesYAMLToResources converts a Kubernetes YAML string into GatewayAPI Resources.
func loadKubernetesYAMLToResources(input []byte, addMissingResources bool) (*Resources, error) {
func loadKubernetesYAMLToResources(input []byte, addMissingResources bool, envoyGateway *egv1a1.EnvoyGateway) (*Resources, error) {
resources := NewResources()
var useDefaultNamespace bool
providedNamespaceMap := sets.New[string]()
Expand All @@ -62,6 +62,30 @@ func loadKubernetesYAMLToResources(input []byte, addMissingResources bool) (*Res
defaulter := GetGatewaySchemaDefaulter()
validator := GetDefaultValidator()

// Build a map of extension-managed resources (by Group/Version/Kind)
type extCategory int
const (
extNone extCategory = iota
extFilter
extPolicy
extBackend
)
extGVKMap := map[string]extCategory{}
if envoyGateway != nil && envoyGateway.ExtensionManager != nil {
for _, gvk := range envoyGateway.ExtensionManager.Resources {
key := fmt.Sprintf("%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind)
extGVKMap[key] = extFilter
}
for _, gvk := range envoyGateway.ExtensionManager.PolicyResources {
key := fmt.Sprintf("%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind)
extGVKMap[key] = extPolicy
}
for _, gvk := range envoyGateway.ExtensionManager.BackendResources {
key := fmt.Sprintf("%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind)
extGVKMap[key] = extBackend
}
}

if err := IterYAMLBytes(input, func(yamlByte []byte) error {
var obj map[string]interface{}
err := yaml.Unmarshal(yamlByte, &obj)
Expand Down Expand Up @@ -117,6 +141,21 @@ func loadKubernetesYAMLToResources(input []byte, addMissingResources bool) (*Res
data := kobjVal.FieldByName("Data")
stringData := kobjVal.FieldByName("StringData")

// Check if this resource is managed by the ExtensionManager and if so, classify it
if len(extGVKMap) > 0 {
key := fmt.Sprintf("%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind)
if category, ok := extGVKMap[key]; ok {
un.SetNamespace(namespace)
switch category {
case extFilter, extBackend:
resources.ExtensionRefFilters = append(resources.ExtensionRefFilters, *un)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this logic right of adding the ext backend into ext ref flters ?
cc @Xunzhuo

case extPolicy:
resources.ExtensionServerPolicies = append(resources.ExtensionServerPolicies, *un)
}
return nil
}
}

switch gvk.Kind {
case KindEnvoyProxy:
typedSpec := spec.Interface()
Expand Down
2 changes: 1 addition & 1 deletion internal/gatewayapi/resource/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestLoadAllSupportedResourcesFromYAMLBytes(t *testing.T) {
t.Parallel() // this's used for race detection
data, err := os.ReadFile(inFile)
require.NoError(t, err)
got, err := LoadResourcesFromYAMLBytes(data, true)
got, err := LoadResourcesFromYAMLBytes(data, true, nil)
require.NoError(t, err)

outputFile := strings.Replace(inFile, ".in.yaml", ".out.yaml", 1)
Expand Down
Loading
Loading