Repository: open-telemetry/opentelemetry-operator

Issue:
target-allocator doesn't check if CRDs exist before querying them
### Component(s)

target allocator

### What happened?

## Description

We've got the case, where one of our clients is using a `target-allocator` on the cluster, where not all of `monitoring.coreos.com` CRDs are installed. They want to monitor only `ServiceMonitor` and `PodMonitor` resources, and don't want to monitor `Probe` and `ScrapeConfig` resources, since they didn't install them on the cluster.

What happens is that the target-allocator throws errors, still trying to query non-existent CRDs.

```
{"level":"error","ts":"2025-04-14T05:41:00Z","msg":"Unhandled Error","logger":"UnhandledError","error":"pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:243: Failed to watch *v1alpha1.ScrapeConfig: failed to list *v1alpha1.ScrapeConfig: the server could not find the requested resource (get scrapeconfigs.monitoring.coreos.com)","stacktrace":"k8s.io/client-go/tools/cache.DefaultWatchErrorHandler\n\t/home/runner/go/pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:158\nk8s.io/client-go/tools/cache.(*Reflector).Run.func1\n\t/home/runner/go/pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:308\nk8s.io/apimachinery/pkg/util/wait.BackoffUntil.func1\n\t/home/runner/go/pkg/mod/k8s.io/apimachinery@v0.31.3/pkg/util/wait/backoff.go:226\nk8s.io/apimachinery/pkg/util/wait.BackoffUntil\n\t/home/runner/go/pkg/mod/k8s.io/apimachinery@v0.31.3/pkg/util/wait/backoff.go:227\nk8s.io/client-go/tools/cache.(*Reflector).Run\n\t/home/runner/go/pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:306\nk8s.io/client-go/tools/cache.(*controller).Run.(*Group).StartWithChannel.func2\n\t/home/runner/go/pkg/mod/k8s.io/apimachinery@v0.31.3/pkg/util/wait/wait.go:55\nk8s.io/apimachinery/pkg/util/wait.(*Group).Start.func1\n\t/home/runner/go/pkg/mod/k8s.io/apimachinery@v0.31.3/pkg/util/wait/wait.go:72"}
{"level":"info","ts":"2025-04-14T05:41:38Z","msg":"pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:243: failed to list *v1.Probe: the server could not find the requested resource (get probes.monitoring.coreos.com)"}
{"level":"error","ts":"2025-04-14T05:41:38Z","msg":"Unhandled Error","logger":"UnhandledError","error":"pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:243: Failed to watch *v1.Probe: failed to list *v1.Probe: the server could not find the requested resource (get probes.monitoring.coreos.com)","stacktrace":"k8s.io/client-go/tools/cache.DefaultWatchErrorHandler\n\t/home/runner/go/pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:158\nk8s.io/client-go/tools/cache.(*Reflector).Run.func1\n\t/home/runner/go/pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:308\nk8s.io/apimachinery/pkg/util/wait.BackoffUntil.func1\n\t/home/runner/go/pkg/mod/k8s.io/apimachinery@v0.31.3/pkg/util/wait/backoff.go:226\nk8s.io/apimachinery/pkg/util/wait.BackoffUntil\n\t/home/runner/go/pkg/mod/k8s.io/apimachinery@v0.31.3/pkg/util/wait/backoff.go:227\nk8s.io/client-go/tools/cache.(*Reflector).Run\n\t/home/runner/go/pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:306\nk8s.io/client-go/tools/cache.(*controller).Run.(*Group).StartWithChannel.func2\n\t/home/runner/go/pkg/mod/k8s.io/apimachinery@v0.31.3/pkg/util/wait/wait.go:55\nk8s.io/apimachinery/pkg/util/wait.(*Group).Start.func1\n\t/home/runner/go/pkg/mod/k8s.io/apimachinery@v0.31.3/pkg/util/wait/wait.go:72"}
{"level":"info","ts":"2025-04-14T05:41:49Z","msg":"pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:243: failed to list *v1alpha1.ScrapeConfig: the server could not find the requested resource (get scrapeconfigs.monitoring.coreos.com)"}
```

We're using a static config for target-allocator passed via ConfigMap, we don't use `OpenTelemetryCollector` CRD for it. While I was researching the issue, I've thought: maybe you just can't configure `Probe` and `ScrapeConfig` via YAML config yet – since I haven't seen it in the readme. So what I tried next is:

* creating `Probe` and `ScrapeConfig` CRDs in my test cluster, so they can be monitored by target-allocator
* disabling `PodMonitorSelector` and `ServiceMonitorSelector` that CAN be configured in static config
* deleting `PodMonitor` and `ServiceMonitor` CRDs from the cluster

And I've got the same error like the one I wrote above, this time it was about `PodMonitor` and `ServiceMonitor` CRDs that doesn't exist, even after I've disabled them.

Which means that target-allocator:

* queries all of 4 CRDs, no matter how you configure it
* doesn't check if CRDs exist, blindly relying on the idea that they exist

I don't think this is not how it should work, so I wanted to propose the idea to check if CRDs exist before querying them. If they don't exist, then print a WARN message and gracefully shutdown the monitoring of resources, CRDs of which don't exist.

If this idea gets a positive reaction, I can even try to implement the fix myself.

## Steps to Reproduce

* disabling `PodMonitorSelector` or `ServiceMonitorSelector` or any other selector that can be configured in static config
* deleting `PodMonitor` or `ServiceMonitor` or any other CRD from the cluster

## Expected Result

target-allocator doesn't query CRD of disabled resource selector

## Actual Result

target-allocator queries CRD of disabled resource selector and throws the error if there is no such CRD in the cluster

### Kubernetes Version

1.30.5

### Operator version

ghcr.io/open-telemetry/opentelemetry-operator/target-allocator:v0.124.0

### Collector version

ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib:0.124.1

### Environment information

## Environment

this issue is relevant for all environments


### Log output

```shell
{"level":"error","ts":"2025-04-14T05:41:00Z","msg":"Unhandled Error","logger":"UnhandledError","error":"pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:243: Failed to watch *v1alpha1.ScrapeConfig: failed to list *v1alpha1.ScrapeConfig: the server could not find the requested resource (get scrapeconfigs.monitoring.coreos.com)","stacktrace":"k8s.io/client-go/tools/cache.DefaultWatchErrorHandler\n\t/home/runner/go/pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:158\nk8s.io/client-go/tools/cache.(*Reflector).Run.func1\n\t/home/runner/go/pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:308\nk8s.io/apimachinery/pkg/util/wait.BackoffUntil.func1\n\t/home/runner/go/pkg/mod/k8s.io/apimachinery@v0.31.3/pkg/util/wait/backoff.go:226\nk8s.io/apimachinery/pkg/util/wait.BackoffUntil\n\t/home/runner/go/pkg/mod/k8s.io/apimachinery@v0.31.3/pkg/util/wait/backoff.go:227\nk8s.io/client-go/tools/cache.(*Reflector).Run\n\t/home/runner/go/pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:306\nk8s.io/client-go/tools/cache.(*controller).Run.(*Group).StartWithChannel.func2\n\t/home/runner/go/pkg/mod/k8s.io/apimachinery@v0.31.3/pkg/util/wait/wait.go:55\nk8s.io/apimachinery/pkg/util/wait.(*Group).Start.func1\n\t/home/runner/go/pkg/mod/k8s.io/apimachinery@v0.31.3/pkg/util/wait/wait.go:72"}
{"level":"info","ts":"2025-04-14T05:41:38Z","msg":"pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:243: failed to list *v1.Probe: the server could not find the requested resource (get probes.monitoring.coreos.com)"}
{"level":"error","ts":"2025-04-14T05:41:38Z","msg":"Unhandled Error","logger":"UnhandledError","error":"pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:243: Failed to watch *v1.Probe: failed to list *v1.Probe: the server could not find the requested resource (get probes.monitoring.coreos.com)","stacktrace":"k8s.io/client-go/tools/cache.DefaultWatchErrorHandler\n\t/home/runner/go/pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:158\nk8s.io/client-go/tools/cache.(*Reflector).Run.func1\n\t/home/runner/go/pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:308\nk8s.io/apimachinery/pkg/util/wait.BackoffUntil.func1\n\t/home/runner/go/pkg/mod/k8s.io/apimachinery@v0.31.3/pkg/util/wait/backoff.go:226\nk8s.io/apimachinery/pkg/util/wait.BackoffUntil\n\t/home/runner/go/pkg/mod/k8s.io/apimachinery@v0.31.3/pkg/util/wait/backoff.go:227\nk8s.io/client-go/tools/cache.(*Reflector).Run\n\t/home/runner/go/pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:306\nk8s.io/client-go/tools/cache.(*controller).Run.(*Group).StartWithChannel.func2\n\t/home/runner/go/pkg/mod/k8s.io/apimachinery@v0.31.3/pkg/util/wait/wait.go:55\nk8s.io/apimachinery/pkg/util/wait.(*Group).Start.func1\n\t/home/runner/go/pkg/mod/k8s.io/apimachinery@v0.31.3/pkg/util/wait/wait.go:72"}
{"level":"info","ts":"2025-04-14T05:41:49Z","msg":"pkg/mod/k8s.io/client-go@v0.31.3/tools/cache/reflector.go:243: failed to list *v1alpha1.ScrapeConfig: the server could not find the requested resource (get scrapeconfigs.monitoring.coreos.com)"}
```

### Additional context

_No response_

Discussion:
@swiatekm: That does sound like something we should fix. I recall trying to make it work the way you describe when adding support for Probes and ScrapeConfigs, but then I don't think we have any tests for this. 

For clarity, does the Target Allocator still work, and the problem is just the error logs, or is it completely nonfunctional?

@cazorla19 if you'd like to submit a fix, I'll be happy to review it.@gracewehner: +1 we also are seeing this issue with a very similar use case where the Probe and ScrapeConfig CRDs don't exist.

The issue is just the error logs, which for now we've had to mute (but miss if there are any actual errors). Functionality-wise it all still works@swiatekm: If the CRD isn't present in the cluster, we shouldn't emit any errors about it. I'm not sure how easy this is going to be in practice, as we use prometheus-operator's machinery for generating scrape configs from CRs, and it quite reasonably assumes all of its CRDs are present.@cazorla19: @swiatekm this idea behind prometheus-operator (i.e. assuming CRDs are present instead of putting some extra logic on checking their existence in the cluster) sounds logical – however, I'm still wondering why do we need to query CRDs of those resources that we've disabled. This is one part of this issue – you may disable the scraping of one particular resource, but it still keeps trying to query its CRD.

The use-case I'm trying to tackle is not common, rather a niche one:

1. the user deployed not all prom-operator's CRDs to the cluster, rather the few ones explicitly selected by him
2. this could happen for few reasons, such as local-corporate security guidelines or a simple wish to not deploy CRDs that are not going to be used

I understand this might not be easy to handle such use-case, especially when it's closely tied to the dependency and not to the project itself, so I'm wondering how bad it would be to leave this issue unresolved.@swiatekm: Turns out this is something we do ourselves [here](https://github.com/open-telemetry/opentelemetry-operator/blob/main/cmd/otel-allocator/internal/watcher/promOperator.go#L192). Detecting if a CRD is present is a solved problem because the operator does it [here](https://github.com/open-telemetry/opentelemetry-operator/blob/main/internal/autodetect/main.go#L66). So this shouldn't be too difficult to fix.

Solution patch:
diff --git a/.chloggen/check-crd-availability.yaml b/.chloggen/check-crd-availability.yaml
new file mode 100644
index 00000000..b5ab2948
--- /dev/null
+++ b/.chloggen/check-crd-availability.yaml
@@ -0,0 +1,16 @@
+# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
+change_type: bug_fix
+
+# The name of the component, or a single word describing the area of concern, (e.g. collector, target allocator, auto-instrumentation, opamp, github action)
+component: target allocator
+
+# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: check CRD availability before registering informers
+
+# One or more tracking issues related to the change
+issues: [3987]
+
+# (Optional) One or more lines of additional information to render under the primary note.
+# These lines will be padded with 2 spaces and then inserted directly into the document.
+# Use pipe (|) for multiline entries.
+subtext:
diff --git a/apis/v1beta1/targetallocator_rbac.go b/apis/v1beta1/targetallocator_rbac.go
index e1985164..09a0e782 100644
--- a/apis/v1beta1/targetallocator_rbac.go
+++ b/apis/v1beta1/targetallocator_rbac.go
@@ -39,6 +39,9 @@ var (
 		}, {
 			NonResourceURLs: []string{"/metrics"},
 			Verbs:           []string{"get"},
+		}, {
+			NonResourceURLs: []string{"/api", "/api/*", "/apis", "/apis/*"},
+			Verbs:           []string{"get"},
 		},
 	}
 )
diff --git a/cmd/otel-allocator/internal/watcher/promOperator.go b/cmd/otel-allocator/internal/watcher/promOperator.go
index abf9e283..90058b4e 100644
--- a/cmd/otel-allocator/internal/watcher/promOperator.go
+++ b/cmd/otel-allocator/internal/watcher/promOperator.go
@@ -27,7 +27,10 @@ import (
 	"gopkg.in/yaml.v2"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/discovery"
 	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/cache"
 	"k8s.io/client-go/util/retry"
 
@@ -54,7 +57,7 @@ func NewPrometheusCRWatcher(
 
 	factory := informers.NewMonitoringInformerFactories(allowList, denyList, monitoringclient, allocatorconfig.DefaultResyncTime, nil)
 
-	monitoringInformers, err := getInformers(factory)
+	monitoringInformers, err := getInformers(factory, cfg.ClusterConfig, promLogger)
 	if err != nil {
 		return nil, err
 	}
@@ -185,34 +188,127 @@ func getNamespaceInformer(ctx context.Context, allowList, denyList map[string]st
 
 }
 
+// checkCRDAvailability checks if a specific CRD is available in the cluster.
+func checkCRDAvailability(dcl discovery.DiscoveryInterface, resourceName string) (bool, error) {
+	apiList, err := dcl.ServerGroups()
+	if err != nil {
+		return false, err
+	}
+
+	apiGroups := apiList.Groups
+	for _, group := range apiGroups {
+		if group.Name == "monitoring.coreos.com" {
+			for _, version := range group.Versions {
+				resources, err := dcl.ServerResourcesForGroupVersion(version.GroupVersion)
+				if err != nil {
+					return false, err
+				}
+
+				for _, resource := range resources.APIResources {
+					if resource.Name == resourceName {
+						return true, nil
+					}
+				}
+			}
+		}
+	}
+
+	return false, nil
+}
+
+// createInformerIfAvailable creates an informer for the given resource if the CRD is available,
+// otherwise returns nil. If CRD availability cannot be checked due to permissions or other errors,
+// it fails open and attempts to create the informer anyway.
+func createInformerIfAvailable(
+	factory informers.FactoriesForNamespaces,
+	dcl discovery.DiscoveryInterface,
+	resourceName string,
+	groupVersion schema.GroupVersionResource,
+	logger *slog.Logger,
+) (*informers.ForResource, error) {
+	available, err := checkCRDAvailability(dcl, resourceName)
+	if err != nil {
+		logger.Warn("Failed to check CRD availability, assuming CRD is available", "resource", resourceName, "error", err)
+		// Fail open: if we can't check availability, assume the CRD is available and try to create the informer
+		available = true
+	}
+
+	if !available {
+		logger.Warn("CRD not available, skipping informer", "resource", resourceName)
+		return nil, nil
+	}
+
+	informer, err := informers.NewInformersForResource(factory, groupVersion)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create informer for %s: %w", resourceName, err)
+	}
+
+	return informer, nil
+}
+
 // getInformers returns a map of informers for the given resources.
-func getInformers(factory informers.FactoriesForNamespaces) (map[string]*informers.ForResource, error) {
-	serviceMonitorInformers, err := informers.NewInformersForResource(factory, monitoringv1.SchemeGroupVersion.WithResource(monitoringv1.ServiceMonitorName))
+func getInformers(factory informers.FactoriesForNamespaces, clusterConfig *rest.Config, logger *slog.Logger) (map[string]*informers.ForResource, error) {
+	informersMap := make(map[string]*informers.ForResource)
+
+	// Get the discovery client
+	dcl, err := discovery.NewDiscoveryClientForConfig(clusterConfig)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create discovery client: %w", err)
+	}
+
+	// ServiceMonitor
+	serviceMonitorInformer, err := createInformerIfAvailable(
+		factory, dcl, "servicemonitors",
+		monitoringv1.SchemeGroupVersion.WithResource(monitoringv1.ServiceMonitorName),
+		logger,
+	)
 	if err != nil {
 		return nil, err
 	}
+	if serviceMonitorInformer != nil {
+		informersMap[monitoringv1.ServiceMonitorName] = serviceMonitorInformer
+	}
 
-	podMonitorInformers, err := informers.NewInformersForResource(factory, monitoringv1.SchemeGroupVersion.WithResource(monitoringv1.PodMonitorName))
+	// PodMonitor
+	podMonitorInformer, err := createInformerIfAvailable(
+		factory, dcl, "podmonitors",
+		monitoringv1.SchemeGroupVersion.WithResource(monitoringv1.PodMonitorName),
+		logger,
+	)
 	if err != nil {
 		return nil, err
 	}
+	if podMonitorInformer != nil {
+		informersMap[monitoringv1.PodMonitorName] = podMonitorInformer
+	}
 
-	probeInformers, err := informers.NewInformersForResource(factory, monitoringv1.SchemeGroupVersion.WithResource(monitoringv1.ProbeName))
+	// Probe
+	probeInformer, err := createInformerIfAvailable(
+		factory, dcl, "probes",
+		monitoringv1.SchemeGroupVersion.WithResource(monitoringv1.ProbeName),
+		logger,
+	)
 	if err != nil {
 		return nil, err
 	}
+	if probeInformer != nil {
+		informersMap[monitoringv1.ProbeName] = probeInformer
+	}
 
-	scrapeConfigInformers, err := informers.NewInformersForResource(factory, promv1alpha1.SchemeGroupVersion.WithResource(promv1alpha1.ScrapeConfigName))
+	// ScrapeConfig
+	scrapeConfigInformer, err := createInformerIfAvailable(
+		factory, dcl, "scrapeconfigs",
+		promv1alpha1.SchemeGroupVersion.WithResource(promv1alpha1.ScrapeConfigName),
+		logger,
+	)
 	if err != nil {
 		return nil, err
 	}
+	if scrapeConfigInformer != nil {
+		informersMap[promv1alpha1.ScrapeConfigName] = scrapeConfigInformer
+	}
 
-	return map[string]*informers.ForResource{
-		monitoringv1.ServiceMonitorName: serviceMonitorInformers,
-		monitoringv1.PodMonitorName:     podMonitorInformers,
-		monitoringv1.ProbeName:          probeInformers,
-		promv1alpha1.ScrapeConfigName:   scrapeConfigInformers,
-	}, nil
+	return informersMap, nil
 }
 
 // Watch wrapped informers and wait for an initial sync.
@@ -264,7 +360,13 @@ func (w *PrometheusCRWatcher) Watch(upstreamEvents chan Event, upstreamErrors ch
 		w.logger.Info("Unable to watch namespaces since namespace informer is nil")
 	}
 
+	// Only attempt to sync informers that were actually created
 	for name, resource := range w.informers {
+		if resource == nil {
+			w.logger.Info("Skipping nil informer", "informer", name)
+			continue
+		}
+
 		resource.Start(w.stopChannel)
 
 		if ok := w.WaitForNamedCacheSync(name, resource.HasSynced); !ok {
@@ -348,24 +450,46 @@ func (w *PrometheusCRWatcher) LoadConfig(ctx context.Context) (*promconfig.Confi
 	promCfg := &promconfig.Config{}
 
 	if w.resourceSelector != nil {
-		serviceMonitorInstances, err := w.resourceSelector.SelectServiceMonitors(ctx, w.informers[monitoringv1.ServiceMonitorName].ListAllByNamespace)
-		if err != nil {
-			return nil, err
+		// Initialize empty maps for all resource types
+		serviceMonitorInstances := make(map[string]*monitoringv1.ServiceMonitor)
+		podMonitorInstances := make(map[string]*monitoringv1.PodMonitor)
+		probeInstances := make(map[string]*monitoringv1.Probe)
+		scrapeConfigInstances := make(map[string]*promv1alpha1.ScrapeConfig)
+
+		// Get ServiceMonitors if the informer exists
+		if informer, ok := w.informers[monitoringv1.ServiceMonitorName]; ok {
+			instances, err := w.resourceSelector.SelectServiceMonitors(ctx, informer.ListAllByNamespace)
+			if err != nil {
+				return nil, err
+			}
+			serviceMonitorInstances = instances
 		}
 
-		podMonitorInstances, err := w.resourceSelector.SelectPodMonitors(ctx, w.informers[monitoringv1.PodMonitorName].ListAllByNamespace)
-		if err != nil {
-			return nil, err
+		// Get PodMonitors if the informer exists
+		if informer, ok := w.informers[monitoringv1.PodMonitorName]; ok {
+			instances, err := w.resourceSelector.SelectPodMonitors(ctx, informer.ListAllByNamespace)
+			if err != nil {
+				return nil, err
+			}
+			podMonitorInstances = instances
 		}
 
-		probeInstances, err := w.resourceSelector.SelectProbes(ctx, w.informers[monitoringv1.ProbeName].ListAllByNamespace)
-		if err != nil {
-			return nil, err
+		// Get Probes if the informer exists
+		if informer, ok := w.informers[monitoringv1.ProbeName]; ok {
+			instances, err := w.resourceSelector.SelectProbes(ctx, informer.ListAllByNamespace)
+			if err != nil {
+				return nil, err
+			}
+			probeInstances = instances
 		}
 
-		scrapeConfigInstances, err := w.resourceSelector.SelectScrapeConfigs(ctx, w.informers[promv1alpha1.ScrapeConfigName].ListAllByNamespace)
-		if err != nil {
-			return nil, err
+		// Get ScrapeConfigs if the informer exists
+		if informer, ok := w.informers[promv1alpha1.ScrapeConfigName]; ok {
+			instances, err := w.resourceSelector.SelectScrapeConfigs(ctx, informer.ListAllByNamespace)
+			if err != nil {
+				return nil, err
+			}
+			scrapeConfigInstances = instances
 		}
 
 		generatedConfig, err := w.configGenerator.GenerateServerConfiguration(



Test patch:
diff --git a/apis/v1alpha1/targetallocator_webhook_test.go b/apis/v1alpha1/targetallocator_webhook_test.go
index ffc33a18..c7079b23 100644
--- a/apis/v1alpha1/targetallocator_webhook_test.go
+++ b/apis/v1alpha1/targetallocator_webhook_test.go
@@ -237,6 +237,10 @@ func TestTargetAllocatorValidatingWebhook(t *testing.T) {
 				"missing the following rules for system:serviceaccount:test-ns:test-ta-targetallocator - configmaps: [get]",
 				"missing the following rules for system:serviceaccount:test-ns:test-ta-targetallocator - discovery.k8s.io/endpointslices: [get,list,watch]",
 				"missing the following rules for system:serviceaccount:test-ns:test-ta-targetallocator - nonResourceURL: /metrics: [get]",
+				"missing the following rules for system:serviceaccount:test-ns:test-ta-targetallocator - nonResourceURL: /api: [get]",
+				"missing the following rules for system:serviceaccount:test-ns:test-ta-targetallocator - nonResourceURL: /api/*: [get]",
+				"missing the following rules for system:serviceaccount:test-ns:test-ta-targetallocator - nonResourceURL: /apis: [get]",
+				"missing the following rules for system:serviceaccount:test-ns:test-ta-targetallocator - nonResourceURL: /apis/*: [get]",
 			},
 		},
 		{
diff --git a/apis/v1beta1/collector_webhook_test.go b/apis/v1beta1/collector_webhook_test.go
index e3421460..42598bf1 100644
--- a/apis/v1beta1/collector_webhook_test.go
+++ b/apis/v1beta1/collector_webhook_test.go
@@ -726,6 +726,10 @@ func TestOTELColValidatingWebhook(t *testing.T) {
 				"missing the following rules for system:serviceaccount:test-ns:adm-warning-targetallocator - configmaps: [get]",
 				"missing the following rules for system:serviceaccount:test-ns:adm-warning-targetallocator - discovery.k8s.io/endpointslices: [get,list,watch]",
 				"missing the following rules for system:serviceaccount:test-ns:adm-warning-targetallocator - nonResourceURL: /metrics: [get]",
+				"missing the following rules for system:serviceaccount:test-ns:adm-warning-targetallocator - nonResourceURL: /api: [get]",
+				"missing the following rules for system:serviceaccount:test-ns:adm-warning-targetallocator - nonResourceURL: /api/*: [get]",
+				"missing the following rules for system:serviceaccount:test-ns:adm-warning-targetallocator - nonResourceURL: /apis: [get]",
+				"missing the following rules for system:serviceaccount:test-ns:adm-warning-targetallocator - nonResourceURL: /apis/*: [get]",
 			},
 		},
 		{
diff --git a/cmd/otel-allocator/internal/watcher/promOperator_test.go b/cmd/otel-allocator/internal/watcher/promOperator_test.go
index 7ce879cb..bcd37065 100644
--- a/cmd/otel-allocator/internal/watcher/promOperator_test.go
+++ b/cmd/otel-allocator/internal/watcher/promOperator_test.go
@@ -28,6 +28,7 @@ import (
 	"github.com/stretchr/testify/require"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	fakediscovery "k8s.io/client-go/discovery/fake"
 	"k8s.io/client-go/kubernetes/fake"
 	"k8s.io/client-go/tools/cache"
 	fcache "k8s.io/client-go/tools/cache/testing"
@@ -1220,7 +1221,8 @@ func getTestPrometheusCRWatcher(
 	}
 
 	factory := informers.NewMonitoringInformerFactories(map[string]struct{}{v1.NamespaceAll: {}}, map[string]struct{}{}, mClient, 0, nil)
-	informers, err := getInformers(factory)
+
+	informers, err := getTestInformers(factory)
 	if err != nil {
 		t.Fatal(t, err)
 	}
@@ -1303,3 +1305,119 @@ func sanitizeScrapeConfigsForTest(scs []*promconfig.ScrapeConfig) {
 		sc.MetricRelabelConfigs = nil
 	}
 }
+
+// getTestInformers creates informers for testing without CRD availability checks.
+func getTestInformers(factory informers.FactoriesForNamespaces) (map[string]*informers.ForResource, error) {
+	informersMap := make(map[string]*informers.ForResource)
+
+	// Create ServiceMonitor informers
+	serviceMonitorInformers, err := informers.NewInformersForResource(factory, monitoringv1.SchemeGroupVersion.WithResource(monitoringv1.ServiceMonitorName))
+	if err != nil {
+		return nil, err
+	}
+	informersMap[monitoringv1.ServiceMonitorName] = serviceMonitorInformers
+
+	// Create PodMonitor informers
+	podMonitorInformers, err := informers.NewInformersForResource(factory, monitoringv1.SchemeGroupVersion.WithResource(monitoringv1.PodMonitorName))
+	if err != nil {
+		return nil, err
+	}
+	informersMap[monitoringv1.PodMonitorName] = podMonitorInformers
+
+	// Create Probe informers
+	probeInformers, err := informers.NewInformersForResource(factory, monitoringv1.SchemeGroupVersion.WithResource(monitoringv1.ProbeName))
+	if err != nil {
+		return nil, err
+	}
+	informersMap[monitoringv1.ProbeName] = probeInformers
+
+	// Create ScrapeConfig informers
+	scrapeConfigInformers, err := informers.NewInformersForResource(factory, promv1alpha1.SchemeGroupVersion.WithResource(promv1alpha1.ScrapeConfigName))
+	if err != nil {
+		return nil, err
+	}
+	informersMap[promv1alpha1.ScrapeConfigName] = scrapeConfigInformers
+
+	return informersMap, nil
+}
+
+// TestCRDAvailabilityChecks tests the CRDs' availability.
+func TestCRDAvailabilityChecks(t *testing.T) {
+	tests := []struct {
+		name          string
+		availableCRDs []string
+		expectedCRDs  []string
+	}{
+		{
+			name:          "ServiceMonitor available",
+			availableCRDs: []string{"servicemonitors"},
+			expectedCRDs:  []string{"servicemonitors"},
+		},
+		{
+			name:          "All CRDs available",
+			availableCRDs: []string{"servicemonitors", "podmonitors", "probes", "scrapeconfigs"},
+			expectedCRDs:  []string{"servicemonitors", "podmonitors", "probes", "scrapeconfigs"},
+		},
+		{
+			name:          "No CRDs available",
+			availableCRDs: []string{},
+			expectedCRDs:  []string{},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Create fake discovery client
+			fakeDiscovery := &fakediscovery.FakeDiscovery{
+				Fake: &fake.NewSimpleClientset().Fake,
+			}
+
+			// Set up resources
+			fakeDiscovery.Resources = []*metav1.APIResourceList{}
+
+			// Add v1 resources
+			v1Resources := &metav1.APIResourceList{
+				GroupVersion: "monitoring.coreos.com/v1",
+				APIResources: []metav1.APIResource{},
+			}
+			for _, crd := range tt.availableCRDs {
+				if crd == "servicemonitors" || crd == "podmonitors" || crd == "probes" {
+					v1Resources.APIResources = append(v1Resources.APIResources, metav1.APIResource{
+						Name: crd,
+					})
+				}
+			}
+			fakeDiscovery.Resources = append(fakeDiscovery.Resources, v1Resources)
+
+			// Add v1alpha1 resources
+			v1alpha1Resources := &metav1.APIResourceList{
+				GroupVersion: "monitoring.coreos.com/v1alpha1",
+				APIResources: []metav1.APIResource{},
+			}
+			for _, crd := range tt.availableCRDs {
+				if crd == "scrapeconfigs" {
+					v1alpha1Resources.APIResources = append(v1alpha1Resources.APIResources, metav1.APIResource{
+						Name: crd,
+					})
+				}
+			}
+			fakeDiscovery.Resources = append(fakeDiscovery.Resources, v1alpha1Resources)
+
+			// Test each CRD availability
+			for _, crd := range []string{"servicemonitors", "podmonitors", "probes", "scrapeconfigs"} {
+				available, err := checkCRDAvailability(fakeDiscovery, crd)
+				require.NoError(t, err)
+
+				expected := false
+				for _, expectedCRD := range tt.expectedCRDs {
+					if crd == expectedCRD {
+						expected = true
+						break
+					}
+				}
+
+				assert.Equal(t, expected, available, "CRD %s availability should match expectation", crd)
+			}
+		})
+	}
+}