Skip to content

Commit 0fe85de

Browse files
committed
Per-extension manager filter Resources, BackendResources, and PolicyResources accordingly
Signed-off-by: Marc Navarro Sonnenfeld <marcnavarro@tetrate.io>
1 parent 5fb9677 commit 0fe85de

5 files changed

Lines changed: 263 additions & 37 deletions

File tree

internal/extension/registry/composite_hook_client.go

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
1414
tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
1515
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
16+
"k8s.io/apimachinery/pkg/runtime/schema"
17+
"k8s.io/apimachinery/pkg/util/sets"
1618

1719
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
1820
"github.com/envoyproxy/gateway/internal/extension/types"
@@ -26,8 +28,9 @@ type hookClientEntry struct {
2628
name string
2729
client types.XDSHookClient
2830
failOpen bool
29-
policyGVKSet map[string]struct{} // used for per-extension policy filtering in PostTranslateModifyHook
30-
translationConfig *egv1a1.TranslationConfig // used for per-extension resource-type gating in PostTranslateModifyHook
31+
resourceGVKSet sets.Set[schema.GroupVersionKind] // used for per-extension resource filtering in PostRouteModifyHook, PostClusterModifyHook
32+
policyGVKSet sets.Set[schema.GroupVersionKind] // used for per-extension policy filtering in PostHTTPListenerModifyHook, PostTranslateModifyHook
33+
translationConfig *egv1a1.TranslationConfig // used for per-extension resource-type gating in PostTranslateModifyHook
3134
}
3235

3336
// compositeXDSHookClient chains multiple XDSHookClient calls sequentially.
@@ -39,7 +42,8 @@ type compositeXDSHookClient struct {
3942
func (c *compositeXDSHookClient) PostRouteModifyHook(r *route.Route, routeHostnames []string, extensionResources []*unstructured.Unstructured) (*route.Route, error) {
4043
current := r
4144
for _, entry := range c.entries {
42-
result, err := entry.client.PostRouteModifyHook(current, routeHostnames, extensionResources)
45+
filtered := filterResourcesByGVK(extensionResources, entry.resourceGVKSet)
46+
result, err := entry.client.PostRouteModifyHook(current, routeHostnames, filtered)
4347
if err != nil {
4448
if entry.failOpen {
4549
continue
@@ -69,7 +73,8 @@ func (c *compositeXDSHookClient) PostVirtualHostModifyHook(vh *route.VirtualHost
6973
func (c *compositeXDSHookClient) PostHTTPListenerModifyHook(l *listener.Listener, extensionResources []*unstructured.Unstructured) (*listener.Listener, error) {
7074
current := l
7175
for _, entry := range c.entries {
72-
result, err := entry.client.PostHTTPListenerModifyHook(current, extensionResources)
76+
filtered := filterResourcesByGVK(extensionResources, entry.policyGVKSet)
77+
result, err := entry.client.PostHTTPListenerModifyHook(current, filtered)
7378
if err != nil {
7479
if entry.failOpen {
7580
continue
@@ -84,7 +89,8 @@ func (c *compositeXDSHookClient) PostHTTPListenerModifyHook(l *listener.Listener
8489
func (c *compositeXDSHookClient) PostClusterModifyHook(cl *cluster.Cluster, extensionResources []*unstructured.Unstructured) (*cluster.Cluster, error) {
8590
current := cl
8691
for _, entry := range c.entries {
87-
result, err := entry.client.PostClusterModifyHook(current, extensionResources)
92+
filtered := filterResourcesByGVK(extensionResources, entry.resourceGVKSet)
93+
result, err := entry.client.PostClusterModifyHook(current, filtered)
8894
if err != nil {
8995
if entry.failOpen {
9096
continue
@@ -169,20 +175,36 @@ func (c *compositeXDSHookClient) PostTranslateModifyHook(
169175
return currentClusters, currentSecrets, currentListeners, currentRoutes, nil
170176
}
171177

178+
// filterResourcesByGVK returns only those unstructured resources whose GVK matches the given set.
179+
// If the set is nil or empty, all resources are returned (for backward compatibility).
180+
func filterResourcesByGVK(resources []*unstructured.Unstructured, gvkSet sets.Set[schema.GroupVersionKind]) []*unstructured.Unstructured {
181+
if gvkSet.Len() == 0 {
182+
return resources
183+
}
184+
var filtered []*unstructured.Unstructured
185+
for _, r := range resources {
186+
if r == nil {
187+
continue
188+
}
189+
if gvkSet.Has(r.GroupVersionKind()) {
190+
filtered = append(filtered, r)
191+
}
192+
}
193+
return filtered
194+
}
195+
172196
// filterPoliciesByGVK returns only those policies whose GVK matches the given set.
173197
// If the set is nil or empty, all policies are returned (for backward compatibility).
174-
func filterPoliciesByGVK(policies []*ir.UnstructuredRef, gvkSet map[string]struct{}) []*ir.UnstructuredRef {
175-
if len(gvkSet) == 0 {
198+
func filterPoliciesByGVK(policies []*ir.UnstructuredRef, gvkSet sets.Set[schema.GroupVersionKind]) []*ir.UnstructuredRef {
199+
if gvkSet.Len() == 0 {
176200
return policies
177201
}
178202
var filtered []*ir.UnstructuredRef
179203
for _, p := range policies {
180204
if p == nil || p.Object == nil {
181205
continue
182206
}
183-
gvk := p.Object.GroupVersionKind()
184-
key := fmt.Sprintf("%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind)
185-
if _, ok := gvkSet[key]; ok {
207+
if gvkSet.Has(p.Object.GroupVersionKind()) {
186208
filtered = append(filtered, p)
187209
}
188210
}

internal/extension/registry/composite_hook_client_test.go

Lines changed: 184 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
"github.com/stretchr/testify/assert"
1717
"github.com/stretchr/testify/require"
1818
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
19+
"k8s.io/apimachinery/pkg/runtime/schema"
20+
"k8s.io/apimachinery/pkg/util/sets"
1921
"k8s.io/utils/ptr"
2022

2123
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
@@ -150,6 +152,79 @@ func TestCompositeHookClient_PostRouteModifyHook(t *testing.T) {
150152
require.Equal(t, fmt.Errorf(`extension "ext1": %w`, fmt.Errorf("extension error")), err)
151153
require.False(t, client2Called)
152154
})
155+
156+
t.Run("per-extension resource filtering", func(t *testing.T) {
157+
var ext1Resources, ext2Resources []*unstructured.Unstructured
158+
159+
client1 := &mockXDSHookClient{
160+
postRouteModifyHook: func(r *route.Route, _ []string, resources []*unstructured.Unstructured) (*route.Route, error) {
161+
ext1Resources = resources
162+
return r, nil
163+
},
164+
}
165+
client2 := &mockXDSHookClient{
166+
postRouteModifyHook: func(r *route.Route, _ []string, resources []*unstructured.Unstructured) (*route.Route, error) {
167+
ext2Resources = resources
168+
return r, nil
169+
},
170+
}
171+
172+
fooV1FooFilterGVK := schema.GroupVersionKind{Group: "foo.io", Version: "v1", Kind: "FooFilter"}
173+
barV1BarBackendGVK := schema.GroupVersionKind{Group: "bar.io", Version: "v1", Kind: "BarBackend"}
174+
composite := &compositeXDSHookClient{
175+
entries: []hookClientEntry{
176+
{
177+
name: "ext1",
178+
client: client1,
179+
resourceGVKSet: sets.New(fooV1FooFilterGVK),
180+
},
181+
{
182+
name: "ext2",
183+
client: client2,
184+
resourceGVKSet: sets.New(barV1BarBackendGVK),
185+
},
186+
},
187+
}
188+
189+
allResources := []*unstructured.Unstructured{
190+
{Object: map[string]interface{}{"apiVersion": "foo.io/v1", "kind": "FooFilter"}},
191+
{Object: map[string]interface{}{"apiVersion": "bar.io/v1", "kind": "BarBackend"}},
192+
}
193+
194+
_, err := composite.PostRouteModifyHook(&route.Route{Name: "test"}, nil, allResources)
195+
require.NoError(t, err)
196+
197+
require.Len(t, ext1Resources, 1)
198+
assert.Equal(t, fooV1FooFilterGVK, ext1Resources[0].GetObjectKind().GroupVersionKind())
199+
200+
require.Len(t, ext2Resources, 1)
201+
assert.Equal(t, barV1BarBackendGVK, ext2Resources[0].GetObjectKind().GroupVersionKind())
202+
})
203+
204+
t.Run("no resourceGVKSet passes all resources", func(t *testing.T) {
205+
var receivedResources []*unstructured.Unstructured
206+
client := &mockXDSHookClient{
207+
postRouteModifyHook: func(r *route.Route, _ []string, resources []*unstructured.Unstructured) (*route.Route, error) {
208+
receivedResources = resources
209+
return r, nil
210+
},
211+
}
212+
213+
composite := &compositeXDSHookClient{
214+
entries: []hookClientEntry{
215+
{name: "ext1", client: client},
216+
},
217+
}
218+
219+
allResources := []*unstructured.Unstructured{
220+
{Object: map[string]interface{}{"apiVersion": "foo.io/v1", "kind": "FooFilter"}},
221+
{Object: map[string]interface{}{"apiVersion": "bar.io/v1", "kind": "BarBackend"}},
222+
}
223+
224+
_, err := composite.PostRouteModifyHook(&route.Route{Name: "test"}, nil, allResources)
225+
require.NoError(t, err)
226+
assert.Len(t, receivedResources, 2)
227+
})
153228
}
154229

155230
func TestCompositeHookClient_PostVirtualHostModifyHook(t *testing.T) {
@@ -300,6 +375,54 @@ func TestCompositeHookClient_PostHTTPListenerModifyHook(t *testing.T) {
300375
require.Equal(t, fmt.Errorf(`extension "ext1": %w`, fmt.Errorf("extension error")), err)
301376
require.False(t, client2Called)
302377
})
378+
379+
t.Run("per-extension policy filtering", func(t *testing.T) {
380+
var ext1Resources, ext2Resources []*unstructured.Unstructured
381+
382+
client1 := &mockXDSHookClient{
383+
postHTTPListenerModifyHook: func(l *listener.Listener, resources []*unstructured.Unstructured) (*listener.Listener, error) {
384+
ext1Resources = resources
385+
return l, nil
386+
},
387+
}
388+
client2 := &mockXDSHookClient{
389+
postHTTPListenerModifyHook: func(l *listener.Listener, resources []*unstructured.Unstructured) (*listener.Listener, error) {
390+
ext2Resources = resources
391+
return l, nil
392+
},
393+
}
394+
395+
fooV1FooPolicyGVK := schema.GroupVersionKind{Group: "foo.io", Version: "v1", Kind: "FooPolicy"}
396+
barV1BarPolicyGVK := schema.GroupVersionKind{Group: "bar.io", Version: "v1", Kind: "BarPolicy"}
397+
composite := &compositeXDSHookClient{
398+
entries: []hookClientEntry{
399+
{
400+
name: "ext1",
401+
client: client1,
402+
policyGVKSet: sets.New(fooV1FooPolicyGVK),
403+
},
404+
{
405+
name: "ext2",
406+
client: client2,
407+
policyGVKSet: sets.New(barV1BarPolicyGVK),
408+
},
409+
},
410+
}
411+
412+
allResources := []*unstructured.Unstructured{
413+
{Object: map[string]interface{}{"apiVersion": "foo.io/v1", "kind": "FooPolicy"}},
414+
{Object: map[string]interface{}{"apiVersion": "bar.io/v1", "kind": "BarPolicy"}},
415+
}
416+
417+
_, err := composite.PostHTTPListenerModifyHook(&listener.Listener{Name: "test"}, allResources)
418+
require.NoError(t, err)
419+
420+
require.Len(t, ext1Resources, 1)
421+
assert.Equal(t, fooV1FooPolicyGVK, ext1Resources[0].GetObjectKind().GroupVersionKind())
422+
423+
require.Len(t, ext2Resources, 1)
424+
assert.Equal(t, barV1BarPolicyGVK, ext2Resources[0].GetObjectKind().GroupVersionKind())
425+
})
303426
}
304427

305428
func TestCompositeHookClient_PostClusterModifyHook(t *testing.T) {
@@ -375,6 +498,54 @@ func TestCompositeHookClient_PostClusterModifyHook(t *testing.T) {
375498
require.Error(t, err)
376499
require.False(t, client2Called)
377500
})
501+
502+
t.Run("per-extension resource filtering", func(t *testing.T) {
503+
var ext1Resources, ext2Resources []*unstructured.Unstructured
504+
505+
client1 := &mockXDSHookClient{
506+
postClusterModifyHook: func(c *cluster.Cluster, resources []*unstructured.Unstructured) (*cluster.Cluster, error) {
507+
ext1Resources = resources
508+
return c, nil
509+
},
510+
}
511+
client2 := &mockXDSHookClient{
512+
postClusterModifyHook: func(c *cluster.Cluster, resources []*unstructured.Unstructured) (*cluster.Cluster, error) {
513+
ext2Resources = resources
514+
return c, nil
515+
},
516+
}
517+
518+
fooV1FooBackendGVK := schema.GroupVersionKind{Group: "foo.io", Version: "v1", Kind: "FooBackend"}
519+
barV1BarBackend := schema.GroupVersionKind{Group: "bar.io", Version: "v1", Kind: "BarBackend"}
520+
composite := &compositeXDSHookClient{
521+
entries: []hookClientEntry{
522+
{
523+
name: "ext1",
524+
client: client1,
525+
resourceGVKSet: sets.New(fooV1FooBackendGVK),
526+
},
527+
{
528+
name: "ext2",
529+
client: client2,
530+
resourceGVKSet: sets.New(barV1BarBackend),
531+
},
532+
},
533+
}
534+
535+
allResources := []*unstructured.Unstructured{
536+
{Object: map[string]interface{}{"apiVersion": "foo.io/v1", "kind": "FooBackend"}},
537+
{Object: map[string]interface{}{"apiVersion": "bar.io/v1", "kind": "BarBackend"}},
538+
}
539+
540+
_, err := composite.PostClusterModifyHook(&cluster.Cluster{Name: "test"}, allResources)
541+
require.NoError(t, err)
542+
543+
require.Len(t, ext1Resources, 1)
544+
assert.Equal(t, fooV1FooBackendGVK, ext1Resources[0].GetObjectKind().GroupVersionKind())
545+
546+
require.Len(t, ext2Resources, 1)
547+
assert.Equal(t, barV1BarBackend, ext2Resources[0].GetObjectKind().GroupVersionKind())
548+
})
378549
}
379550

380551
func TestCompositeHookClient_PostTranslateModifyHook(t *testing.T) {
@@ -443,21 +614,19 @@ func TestCompositeHookClient_PostTranslateModifyHook(t *testing.T) {
443614
},
444615
}
445616

617+
fooV1FooPolicyGVK := schema.GroupVersionKind{Group: "foo.io", Version: "v1", Kind: "FooPolicy"}
618+
barV1BarPolicyGVK := schema.GroupVersionKind{Group: "bar.io", Version: "v1", Kind: "BarPolicy"}
446619
composite := &compositeXDSHookClient{
447620
entries: []hookClientEntry{
448621
{
449-
name: "ext1",
450-
client: client1,
451-
policyGVKSet: map[string]struct{}{
452-
"foo.io/v1/FooPolicy": {},
453-
},
622+
name: "ext1",
623+
client: client1,
624+
policyGVKSet: sets.New(fooV1FooPolicyGVK),
454625
},
455626
{
456-
name: "ext2",
457-
client: client2,
458-
policyGVKSet: map[string]struct{}{
459-
"bar.io/v1/BarPolicy": {},
460-
},
627+
name: "ext2",
628+
client: client2,
629+
policyGVKSet: sets.New(barV1BarPolicyGVK),
461630
},
462631
},
463632
}
@@ -485,11 +654,11 @@ func TestCompositeHookClient_PostTranslateModifyHook(t *testing.T) {
485654

486655
// ext1 should only see FooPolicy
487656
require.Len(t, ext1Policies, 1)
488-
assert.Equal(t, "FooPolicy", ext1Policies[0].Object.GetKind())
657+
assert.Equal(t, fooV1FooPolicyGVK, ext1Policies[0].Object.GetObjectKind().GroupVersionKind())
489658

490659
// ext2 should only see BarPolicy
491660
require.Len(t, ext2Policies, 1)
492-
assert.Equal(t, "BarPolicy", ext2Policies[0].Object.GetKind())
661+
assert.Equal(t, barV1BarPolicyGVK, ext2Policies[0].Object.GetObjectKind().GroupVersionKind())
493662
})
494663

495664
t.Run("per-extension resource-type gating", func(t *testing.T) {
@@ -662,9 +831,8 @@ func TestCompositeHookClient_PostTranslateModifyHook(t *testing.T) {
662831
})
663832

664833
t.Run("filterPoliciesByGVK skips nil entries", func(t *testing.T) {
665-
gvkSet := map[string]struct{}{
666-
"foo.io/v1/FooPolicy": {},
667-
}
834+
fooV1FooPolicyGVK := schema.GroupVersionKind{Group: "foo.io", Version: "v1", Kind: "FooPolicy"}
835+
gvkSet := sets.New(fooV1FooPolicyGVK)
668836
policies := []*ir.UnstructuredRef{
669837
nil,
670838
{Object: nil},
@@ -676,7 +844,7 @@ func TestCompositeHookClient_PostTranslateModifyHook(t *testing.T) {
676844

677845
filtered := filterPoliciesByGVK(policies, gvkSet)
678846
require.Len(t, filtered, 1)
679-
assert.Equal(t, "FooPolicy", filtered[0].Object.GetKind())
847+
assert.Equal(t, fooV1FooPolicyGVK, filtered[0].Object.GetObjectKind().GroupVersionKind())
680848
})
681849

682850
t.Run("no policyGVKSet passes all policies", func(t *testing.T) {

0 commit comments

Comments
 (0)