Skip to content

add unmanged crd detection to remove duplicated openapi path#348

Open
krishbajaj1609 wants to merge 1 commit into
kyverno:mainfrom
krishbajaj1609:fix/crd-detection-openapi-spec
Open

add unmanged crd detection to remove duplicated openapi path#348
krishbajaj1609 wants to merge 1 commit into
kyverno:mainfrom
krishbajaj1609:fix/crd-detection-openapi-spec

Conversation

@krishbajaj1609
Copy link
Copy Markdown

@krishbajaj1609 krishbajaj1609 commented May 11, 2026

Summary

  • Add automatic detection of third-party CRDs (e.g., Red Hat ACM) that conflict with reports-server's APIService registrations for the same API group
  • When a conflicting CRD is detected, reports-server suppresses its own OpenAPI paths for that group via IgnorePrefixes,
    eliminating duplicate-path errors while keeping APIService request routing fully functional
  • A periodic checker (every 2 min) detects CRDs installed after startup and triggers a graceful restart to apply suppression

Context

When both a CRD and an APIService exist for the same API group/version (e.g., wgpolicyk8s.io/v1alpha2), the kube-apiserver's OpenAPI aggregator can produce duplicate-path errors (K8s 1.28+), breaking ArgoCD, kubectl apply, and Helm validation. Actual API request handling is unaffected — the problem is only in the OpenAPI documentation/discovery layer.

Related: kubernetes/kubernetes#122668, kubernetes/kubernetes#129499

Evidence

(kind, K8s 1.32)

Periodic detection and restart:
When a conflicting CRD (policyreports.wgpolicyk8s.io) was installed while reports-server was running, the periodic checker detected it within 2 minutes and triggered a graceful restart detect.go:59 "detected third-party CRD for API group served by reports-server, suppressing OpenAPI paths to avoid duplicate-path errors" group="wgpolicyk8s.io"
periodic.go:31 "new conflicting CRD detected since startup, restarting to apply OpenAPI suppression" prefix="/apis/wgpolicyk8s.io/"

Startup detection after restart:
On restart, the CRD was detected at startup and IgnorePrefixes was set before Complete():
detect.go:59 "detected third-party CRD for API group served by reports-server suppressing OpenAPI paths to avoid duplicate-path errors" group="wgpolicyk8s.io"

OpenAPI suppression verified (per-group OpenAPI v3):

API Group Source Paths Note
wgpolicyk8s.io (with fix) reports-server 0 Suppressed — CRD provides schema
wgpolicyk8s.io (without fix) reports-server 11 Dual source — conflict risk
openreports.io reports-server 11 No CRD conflict
reports.kyverno.io reports-server 11 No CRD conflict

Functional verification (with fix + CRDs present):

  • kubectl apply PolicyReport: works, stored in reports-server (served-by: reports-server annotation)
  • kubectl get/delete: works
  • APIService v1alpha2.wgpolicyk8s.io: Available=True
  • OpenAPI v2 endpoint: 200 OK, no errors

Test plan

  • Deploy to cluster with no third-party CRDs — verify no change in behavior (IgnorePrefixes stays empty)
  • Deploy to cluster with conflicting CRD pre-installed — verify startup detection and OpenAPI suppression
  • Install conflicting CRD after startup — verify periodic checker triggers restart
  • Verify kubectl apply, get, delete for PolicyReport/ClusterPolicyReport still work
  • Verify APIService remains Available=True throughout

closes #344

// the corresponding OpenAPI path prefix is returned so it can be added to
// IgnorePrefixes — suppressing reports-server's OpenAPI paths for that group
// while keeping the APIService request routing fully functional.
func DetectConflictingCRDs(restConfig *rest.Config) []string {
Copy link
Copy Markdown
Member

@aerosouund aerosouund May 12, 2026

Choose a reason for hiding this comment

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

its better if this function returns an error and the error is ignored at the caller level. Also it doesn't have to be public


for _, crd := range crds {
group := crd.Spec.Group
if !conflictGroups[group] || seen[group] {
Copy link
Copy Markdown
Member

@aerosouund aerosouund May 12, 2026

Choose a reason for hiding this comment

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

can you make this a variable that gets supplied to the function rather than a global ? it's fine if the caller references the global. but its ok for the caller to pass the global variable. it makes it easier to guess about the function's intent and what it does.

Comment thread pkg/app/policyserver.go

ctx, cancel := context.WithCancel(context.Background())
go func() {
<-stopCh
Copy link
Copy Markdown
Member

@aerosouund aerosouund May 12, 2026

Choose a reason for hiding this comment

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

There are a few things i wanna comment on in this bit:
First, if the PeriodicallyCheckForNewConflicts detect a conflict, it will cancel the context. and RunUntil will exit because the done channel became readable. This means that cleanups we are running on events received through the stopCh won't run.

Second, I think its added complexity to create a context that you cancel when you detect a conflict, run a goroutine that checks if stopCh became readable (something else triggered a server shutdown signal) and cancel the periodic check goroutine.
Why not just pass the stopCh as an argument to the function, run a for select on it becoming readable (something else cancelled) and when a conflict is detected you close this channel yourself ?

Lastly, i am a bit against making this whole thing periodic in the first place. I prefer if its only once on startup. If the admin installed a conflicting CRD, it should be on them to restart the server (to make sure they are doing it in line with keeping their services available).
We can help them out by logging a message that nudges them to do so. Something like there's a CRD installed that conflicts with the CRDs the reports server serves, please ensure to restart the server once after CRD installation to avoid serving duplicate prefixes. Or something less verbose?

"k8s.io/klog/v2"
)

var conflictGroups = map[string]bool{
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

a map of string to empty struct is better here. an empty struct is 0 bytes.


var conflictGroups = map[string]bool{
"wgpolicyk8s.io": true,
"openreports.io": true,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why do we only have these and not the other crds the reports server serves ?

return nil
}

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why not pass the context given to PeriodicallyCheckForNewConflicts ?

return detectFromCRDList(crdList.Items)
}

func detectFromCRDList(crds []apiextensionsv1.CustomResourceDefinition) []string {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i think its best to drop this helper entirely and move its logic to DetectConflictingCrds

case <-ticker.C:
current := DetectConflictingCRDs(restConfig)
for _, p := range current {
if !known[p] {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

here, you are checking if we detected new conflicting CRDs other than the ones in config.OpenAPIIgnorePrefixes but this field won't persist across startups anyways, and the user doesn't have a way to supply their own ignore prefixes. so why not just skip passing and comparing against the initial prefixes array and report if we detect any of the api groups that can cause problems in a CRD ?

// If a new conflicting CRD appears that was not present at startup, the cancel
// function is called to trigger a graceful restart.
func PeriodicallyCheckForNewConflicts(ctx context.Context, cancel context.CancelFunc, restConfig *rest.Config, initialPrefixes []string) {
known := make(map[string]bool, len(initialPrefixes))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

map[string]struct{}

@aerosouund
Copy link
Copy Markdown
Member

aerosouund commented May 12, 2026

@krishbajaj1609
please sign off your commits to pass DCO.
IMHO, i would push for a simpler approach to this problem. which is on startup, check if there are any crds installed from the conflicting groups. If there is.. log the message that a restart is preferred in case it didn't already take place. And add those to the ignored prefixes before serving the API in the first place.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

reports-server should handle coexistence with third-party CRDs for policy reports

2 participants