Skip to content

feat(source): migrate gateway sources to indexer-based filtering#6545

Open
ivankatliarchuk wants to merge 1 commit into
kubernetes-sigs:masterfrom
gofogo:feat/gateway-source-indexer-filtering
Open

feat(source): migrate gateway sources to indexer-based filtering#6545
ivankatliarchuk wants to merge 1 commit into
kubernetes-sigs:masterfrom
gofogo:feat/gateway-source-indexer-filtering

Conversation

@ivankatliarchuk

@ivankatliarchuk ivankatliarchuk commented Jul 6, 2026

Copy link
Copy Markdown
Member

What does it do ?

  • Label selector, IsControllerMatch filtering, and (for routes/ListenerSets) annotation filtering are now enforced at informer cache population time via MustAddIndexers, eliminating runtime filtering in Endpoints()

Motivation

  • Gateway sources previously applied namespace, label, annotation, and controller-mismatch filters at query time inside Endpoints(), after objects were already loaded into the informer cache — wasting memory and CPU on every sync cycle
  • Aligns gateway sources with the indexer filtering pattern already used by ingress, service, and other sources, where filters are baked into the cache at population time

More

  • Yes, this PR title follows Conventional Commits
  • Yes, I added unit tests
  • Yes, I updated end user documentation accordingly

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
@kubernetes-prow
kubernetes-prow Bot requested review from mloiseleur and u-kai July 6, 2026 15:40
@kubernetes-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign raffo for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubernetes-prow kubernetes-prow Bot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Jul 6, 2026
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 28803821713

Coverage increased (+0.5%) to 84.031%

Details

  • Coverage increased (+0.5%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 2 coverage regressions across 1 file.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

2 previously-covered lines in 1 file lost coverage.

File Lines Losing Coverage Coverage
gateway.go 2 98.53%

Coverage Stats

Coverage Status
Relevant Lines: 21986
Covered Lines: 18475
Line Coverage: 84.03%
Coverage Strength: 1417.46 hits per line

💛 - Coveralls

@mloiseleur

mloiseleur commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

When reviewing this PR, I discovered two related issues.

One directly on indexers, following #6445, on "/", confirmed with this test into source/informers/indexers_test.go:

func TestIndexerWithOptions_ClusterScopedLabelKeyLeadingSlash(t *testing.T) {
  node := &corev1.Node{} // Node is cluster-scoped: GetNamespace() == ""
  node.SetName("my-node")
  node.SetLabels(map[string]string{"parent": "owner-node"})

  // by-name branch: fixed in #6445 -> bare name, matches MetaNamespaceKeyFunc.
  byName := IndexerWithOptions[*corev1.Node]()
  keys, err := byName[IndexWithSelectors](node)
  require.NoError(t, err)
  assert.Equal(t, []string{"my-node"}, keys, "by-name branch correctly emits a bare name")

  // by-label branch: NOT fixed -> leading slash from NamespacedName{Namespace:""}.String().
  byLabel := IndexerWithOptions[*corev1.Node](IndexSelectorWithLabelKey("parent"))
  keys, err = byLabel[IndexWithSelectors](node)
  require.NoError(t, err)
  assert.Equal(t, []string{"/owner-node"}, keys,
    "by-label branch still prepends a slash for cluster-scoped objects; "+
      "a bare \"owner-node\" (consistent with the by-name fix) is expected")
}

The second is regression on filters, also following #6445, where invalid filters are silently discarded, confirmed with this test into source/ingress_test.go:

func TestIngressSource_InvalidAnnotationFilterSilentlyMatchesAll(t *testing.T) {
	t.Parallel()

	// The invalid filter really is invalid: ParseFilter reports an error...
	invalidFilter := "tier in (x y)" // set-based selector missing the comma
	sel, err := annotations.ParseFilter(invalidFilter)
	require.Error(t, err, "expected an invalid annotation filter to error at parse time")
	require.Nil(t, sel, "ParseFilter returns a nil selector on invalid input")

	// ...but store.go discards that error, so the source is built with a nil selector.
	// Reproduce exactly what BuildWithConfig hands to NewIngressSource.
	client := fake.NewClientset()
	for i, ing := range createTestIngresses(3) {
		maps.Copy(ing.Annotations, map[string]string{
			annotations.HostnameKey: fmt.Sprintf("ing-%d.example.org", i),
			annotations.TargetKey:   fmt.Sprintf("target-%d.example.com", i),
		})
		_, err := client.NetworkingV1().Ingresses(ing.Namespace).Create(t.Context(), ing, metav1.CreateOptions{})
		require.NoError(t, err)
	}

	src, err := NewIngressSource(t.Context(), client, &Config{
		AnnotationFilter: sel, // nil, as produced by the discarded-error path in store.go
		LabelFilter:      labels.Everything(),
		TemplateEngine:   templatetest.MustEngine(t, "", "", "", false),
	})
	require.NoError(t, err)

	endpoints, err := src.Endpoints(t.Context())
	// Regression: the invalid filter neither errors nor filters — all 3 ingresses pass.
	require.NoError(t, err, "invalid annotation filter no longer surfaces an error")
	assert.Len(t, endpoints, 3, "invalid annotation filter silently matched every ingress")
}

@ivankatliarchuk

Copy link
Copy Markdown
Member Author

I'm not too sure that I fully follow what needs to be done

@ivankatliarchuk

Copy link
Copy Markdown
Member Author

I don't understand that test TestIngressSource_InvalidAnnotationFilterSilentlyMatchesAll. sel, err := annotations.ParseFilter(invalidFilter) -> it throws an error, but you let the test proceed.

Not clear how behaviour is changed, it looks exactly same to me -> wrong annotation filter will not reach the service layer. If you passing no annotations filter, what is your expectation then? Nil annotation filter means there is no filter. You could not provide invalid annotation in production setup, not because of

annotationSelector, _ := annotations.ParseFilter(cfg.AnnotationFilter)
but because this guard
if _, err := metav1.ParseToLabelSelector(cfg.AnnotationFilter); err != nil {

The label selector has exactly same behaviour https://github.com/kubernetes-sigs/external-dns/pull/5222/changes#diff-2873f79a86c0d8b3335cd7731b0ecf7dd4301eb19a82ef7a1cba7589b5252261

Screenshot 2026-07-10 at 11 24 07

Do you mean, you want instead

labelSelector, _ := labels.Parse(cfg.LabelFilter)
annotationSelector, _ := annotations.ParseFilter(cfg.AnnotationFilter)

Something like

labelSelector, err := labels.Parse(cfg.LabelFilter)
annotationSelector, err := annotations.ParseFilter(cfg.AnnotationFilter)

if err != nil {
    return nil, err
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. source

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants