Integration tests never exercise the filtered Secret lister, so they cannot catch regressions in the default configuration

Author: wallrjCreated Sep 13, 2026Updated Sep 13, 2026
Labelsarea/testing

The integration tests always build the unfiltered Secret informer, so none of them exercises the Secret lister that a default install actually uses.

framework.NewClients hardcodes the basic factory:

https://github.com/cert-manager/cert-manager/blob/9313ef502c74abcf53cb21ea9641e5a141d7ba6c/test/integration/framework/helpers.go#L62

Production chooses between two listers on the SecretsFilteredCaching feature gate:

https://github.com/cert-manager/cert-manager/blob/9313ef502c74abcf53cb21ea9641e5a141d7ba6c/pkg/controller/context.go#L338

That gate is Beta and on by default:

https://github.com/cert-manager/cert-manager/blob/9313ef502c74abcf53cb21ea9641e5a141d7ba6c/internal/controller/feature/features.go#L222

So every integration test runs the lister that only non-default installs use.

Why this matters

We hit this on #9224. A commit there widened the keymanager's Secret LIST selector to labels.Everything(). Under the filtered lister that becomes one live GET against the API server per unrelated Secret in the namespace, on every reconcile of every Certificate. No Certificate reached Ready.

TestGeneratesNewPrivateKeyPerRequest passed on that commit. It starts the real keymanager, requestmanager, trigger, readiness and issuing controllers against a live apiserver and requires a Certificate to become Ready, which is precisely the behaviour that had broken. It passed because it used the basic lister. Both end-to-end jobs failed, twice, on two different nodes.

We therefore have integration tests that look like they cover the issuance chain, but cannot see a whole class of regression in the default configuration. The end-to-end jobs are the only thing standing between us and that class of bug, and they take twenty minutes.

How the filtered lister makes a LIST expensive

The filtered lister serves a LIST from two caches. The typed cache holds cert-manager's own Secrets, which carry the controller.cert-manager.io/fao label. A metadata-only cache holds every other Secret in the namespace, and partialMetadataRemoveAll strips its labels to nil:

https://github.com/cert-manager/cert-manager/blob/9313ef502c74abcf53cb21ea9641e5a141d7ba6c/internal/informers/transformers.go#L38

Every Secret the metadata cache returns costs one live GET:

https://github.com/cert-manager/cert-manager/blob/9313ef502c74abcf53cb21ea9641e5a141d7ba6c/internal/informers/core_filteredsecrets.go#L327

A selector requiring a label matches nothing in that cache. labels.Everything() matches all of it.

Suggestion

Let a test choose the filtered factory. Roughly in order of cost:

  • framework.NewClients honours the SecretsFilteredCaching gate the way production does, and tests set it with featuregatetesting.SetFeatureGateDuringTest, which we already use elsewhere in the repo.
  • Add a variant of NewClients that returns the filtered factory, and run at least the issuance-chain tests against both.
  • Run the whole integration suite twice, once per lister.

The first is closest to production and the smallest change.

[Claude Opus 5]

Source: cert-manager/cert-manager