Repository: open-telemetry/opentelemetry-operator
Pull request #3976 (2025-05-09 01:16:41)

[targetallocator] collector namespace overridden by env var


**Description:**
do not override the config file value of collector namespace if env var is not set



Patch:
diff --git a/.chloggen/keep_going_if_collector_crd_is_not_present.yaml b/.chloggen/keep_going_if_collector_crd_is_not_present.yaml
new file mode 100755
index 00000000..1e4776f9
--- /dev/null
+++ b/.chloggen/keep_going_if_collector_crd_is_not_present.yaml
@@ -0,0 +1,17 @@
+# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
+change_type: enhancement
+
+# 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: webhook
+
+# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Allow to run the operator without the OpenTelemetry CRDs present
+
+# One or more tracking issues related to the change
+issues: [3568]
+
+# (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: |
+  Skip registering the webhook and keep the operator working in case the OpenTelemetryCollector CRDs are not deployed.
diff --git a/.chloggen/override_with_env.yaml b/.chloggen/override_with_env.yaml
new file mode 100755
index 00000000..6f616f27
--- /dev/null
+++ b/.chloggen/override_with_env.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: targetallocator
+
+# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Do not override the collector namespace from the config file if the environment variable is not set.
+
+# One or more tracking issues related to the change
+issues: [3976]
+
+# (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/cmd/otel-allocator/internal/config/config.go b/cmd/otel-allocator/internal/config/config.go
index bc51a1d6..37099662 100644
--- a/cmd/otel-allocator/internal/config/config.go
+++ b/cmd/otel-allocator/internal/config/config.go
@@ -301,7 +301,9 @@ func LoadFromCLI(target *Config, flagSet *pflag.FlagSet) error {
 
 // LoadFromEnv loads configuration from environment variables.
 func LoadFromEnv(target *Config) error {
-	target.CollectorNamespace = os.Getenv("OTELCOL_NAMESPACE")
+	if ns, ok := os.LookupEnv("OTELCOL_NAMESPACE"); ok {
+		target.CollectorNamespace = ns
+	}
 	return nil
 }
 
diff --git a/internal/autodetect/collector/operator.go b/internal/autodetect/collector/operator.go
new file mode 100644
index 00000000..1a2b7bb4
--- /dev/null
+++ b/internal/autodetect/collector/operator.go
@@ -0,0 +1,19 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package collector
+
+// Availability represents that the OpenTelemetryCollector CR is available in the cluster.
+type Availability int
+
+const (
+	// NotAvailable OpenTelemetryCollector CR is not available in the cluster.
+	NotAvailable Availability = iota
+
+	// Available OpenTelemetryCollector CR is available in the cluster.
+	Available
+)
+
+func (p Availability) String() string {
+	return [...]string{"NotAvailable", "Available"}[p]
+}
diff --git a/internal/autodetect/main.go b/internal/autodetect/main.go
index adc9770b..89b9c120 100644
--- a/internal/autodetect/main.go
+++ b/internal/autodetect/main.go
@@ -14,6 +14,7 @@ import (
 	"k8s.io/client-go/rest"
 
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager"
+	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/collector"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/fips"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/openshift"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/prometheus"
@@ -31,6 +32,7 @@ type AutoDetect interface {
 	RBACPermissions(ctx context.Context) (autoRBAC.Availability, error)
 	CertManagerAvailability(ctx context.Context) (certmanager.Availability, error)
 	TargetAllocatorAvailability() (targetallocator.Availability, error)
+	CollectorAvailability() (collector.Availability, error)
 	FIPSEnabled(ctx context.Context) bool
 }
 
@@ -182,6 +184,38 @@ func (a *autoDetect) TargetAllocatorAvailability() (targetallocator.Availability
 	return targetallocator.NotAvailable, nil
 }
 
+// CollectorAvailability checks if OpenTelemetryCollector CR are available.
+func (a *autoDetect) CollectorAvailability() (collector.Availability, error) {
+	apiList, err := a.dcl.ServerGroups()
+	if err != nil {
+		return collector.NotAvailable, err
+	}
+
+	apiGroups := apiList.Groups
+	otelGroupIndex := slices.IndexFunc(apiGroups, func(group metav1.APIGroup) bool {
+		return group.Name == "opentelemetry.io"
+	})
+	if otelGroupIndex == -1 {
+		return collector.NotAvailable, nil
+	}
+
+	otelGroup := apiGroups[otelGroupIndex]
+	for _, groupVersion := range otelGroup.Versions {
+		resourceList, err := a.dcl.ServerResourcesForGroupVersion(groupVersion.GroupVersion)
+		if err != nil {
+			return collector.NotAvailable, err
+		}
+		collectorIndex := slices.IndexFunc(resourceList.APIResources, func(group metav1.APIResource) bool {
+			return group.Kind == "OpenTelemetryCollector"
+		})
+		if collectorIndex >= 0 {
+			return collector.Available, nil
+		}
+	}
+
+	return collector.NotAvailable, nil
+}
+
 func (a *autoDetect) FIPSEnabled(_ context.Context) bool {
 	return fips.IsFipsEnabled()
 }
diff --git a/internal/config/config.go b/internal/config/config.go
index 6428bbef..438b76fb 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -12,6 +12,7 @@ import (
 
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager"
+	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/collector"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/openshift"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/prometheus"
 	autoRBAC "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/rbac"
@@ -56,6 +57,8 @@ type Config struct {
 	prometheusCRAvailability    prometheus.Availability
 	certManagerAvailability     certmanager.Availability
 	targetAllocatorAvailability targetallocator.Availability
+	collectorAvailability       collector.Availability
+	ignoreMissingCollectorCRDs  bool
 	labelsFilter                []string
 	annotationsFilter           []string
 }
@@ -69,6 +72,7 @@ func New(opts ...Option) Config {
 		createRBACPermissions:             autoRBAC.NotAvailable,
 		certManagerAvailability:           certmanager.NotAvailable,
 		targetAllocatorAvailability:       targetallocator.NotAvailable,
+		collectorAvailability:             collector.NotAvailable,
 		collectorConfigMapEntry:           defaultCollectorConfigMapEntry,
 		targetAllocatorConfigMapEntry:     defaultTargetAllocatorConfigMapEntry,
 		operatorOpAMPBridgeConfigMapEntry: defaultOperatorOpAMPBridgeConfigMapEntry,
@@ -103,6 +107,8 @@ func New(opts ...Option) Config {
 		prometheusCRAvailability:            o.prometheusCRAvailability,
 		certManagerAvailability:             o.certManagerAvailability,
 		targetAllocatorAvailability:         o.targetAllocatorAvailability,
+		collectorAvailability:               o.collectorAvailability,
+		ignoreMissingCollectorCRDs:          o.ignoreMissingCollectorCRDs,
 		autoInstrumentationJavaImage:        o.autoInstrumentationJavaImage,
 		autoInstrumentationNodeJSImage:      o.autoInstrumentationNodeJSImage,
 		autoInstrumentationPythonImage:      o.autoInstrumentationPythonImage,
@@ -155,6 +161,13 @@ func (c *Config) AutoDetect() error {
 	c.targetAllocatorAvailability = taAvl
 	c.logger.V(2).Info("determined TargetAllocator CRD availability", "availability", cmAvl)
 
+	coAvl, err := c.autoDetect.CollectorAvailability()
+	if err != nil {
+		return err
+	}
+	c.collectorAvailability = coAvl
+	c.logger.V(2).Info("determined Collector CRD availability", "availability", coAvl)
+
 	return nil
 }
 
@@ -253,6 +266,16 @@ func (c *Config) TargetAllocatorAvailability() targetallocator.Availability {
 	return c.targetAllocatorAvailability
 }
 
+// CollectorAvailability represents the availability of the OpenTelemetryCollector CRD.
+func (c *Config) CollectorAvailability() collector.Availability {
+	return c.collectorAvailability
+}
+
+// IgnoreMissingCollectorCRDs is true if the operator can ignore missing OpenTelemetryCollector CRDs.
+func (c *Config) IgnoreMissingCollectorCRDs() bool {
+	return c.ignoreMissingCollectorCRDs
+}
+
 // AutoInstrumentationJavaImage returns OpenTelemetry Java auto-instrumentation container image.
 func (c *Config) AutoInstrumentationJavaImage() string {
 	return c.autoInstrumentationJavaImage
diff --git a/internal/config/options.go b/internal/config/options.go
index 58eb7092..9fc42a36 100644
--- a/internal/config/options.go
+++ b/internal/config/options.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager"
+	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/collector"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/openshift"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/prometheus"
 	autoRBAC "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/rbac"
@@ -49,6 +50,8 @@ type options struct {
 	prometheusCRAvailability            prometheus.Availability
 	certManagerAvailability             certmanager.Availability
 	targetAllocatorAvailability         targetallocator.Availability
+	collectorAvailability               collector.Availability
+	ignoreMissingCollectorCRDs          bool
 	labelsFilter                        []string
 	annotationsFilter                   []string
 }
@@ -227,3 +230,9 @@ func WithEncodeLevelFormat(s string) zapcore.LevelEncoder {
 		return zapcore.CapitalLevelEncoder
 	}
 }
+
+func WithIgnoreMissingCollectorCRDs(b bool) Option {
+	return func(o *options) {
+		o.ignoreMissingCollectorCRDs = b
+	}
+}
diff --git a/main.go b/main.go
index 87befab1..7b8d61ca 100644
--- a/main.go
+++ b/main.go
@@ -6,6 +6,7 @@ package main
 import (
 	"context"
 	"crypto/tls"
+	"errors"
 	"flag"
 	"fmt"
 	"os"
@@ -39,6 +40,7 @@ import (
 	otelv1beta1 "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager"
+	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/collector"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/openshift"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/prometheus"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/targetallocator"
@@ -109,6 +111,7 @@ func main() {
 		enableJavaInstrumentation        bool
 		enableCRMetrics                  bool
 		createSMOperatorMetrics          bool
+		ignoreMissingCollectorCRDs       bool
 		collectorImage                   string
 		targetAllocatorImage             string
 		operatorOpAMPBridgeImage         string
@@ -148,6 +151,7 @@ func main() {
 	pflag.BoolVar(&enableJavaInstrumentation, constants.FlagJava, true, "Controls whether the operator supports java auto-instrumentation")
 	pflag.BoolVar(&enableCRMetrics, constants.FlagCRMetrics, false, "Controls whether exposing the CR metrics is enabled")
 	pflag.BoolVar(&createSMOperatorMetrics, "create-sm-operator-metrics", false, "Create a ServiceMonitor for the operator metrics")
+	pflag.BoolVar(&ignoreMissingCollectorCRDs, "ignore-missing-collector-crds", false, "Ignore missing OpenTelemetryCollector CRDs presence in the cluster")
 
 	stringFlagOrEnv(&collectorImage, "collector-image", "RELATED_IMAGE_COLLECTOR", fmt.Sprintf("ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector:%s", v.OpenTelemetryCollector), "The default OpenTelemetry collector image. This image is used when no image is specified in the CustomResource.")
 	stringFlagOrEnv(&targetAllocatorImage, "target-allocator-image", "RELATED_IMAGE_TARGET_ALLOCATOR", fmt.Sprintf("ghcr.io/open-telemetry/opentelemetry-operator/target-allocator:%s", v.TargetAllocator), "The default OpenTelemetry target allocator image. This image is used when no image is specified in the CustomResource.")
@@ -186,6 +190,7 @@ func main() {
 		"opentelemetry-collector", collectorImage,
 		"opentelemetry-targetallocator", targetAllocatorImage,
 		"operator-opamp-bridge", operatorOpAMPBridgeImage,
+		"ignore-missing-collector-crds", ignoreMissingCollectorCRDs,
 		"auto-instrumentation-java", autoInstrumentationJava,
 		"auto-instrumentation-nodejs", autoInstrumentationNodeJS,
 		"auto-instrumentation-python", autoInstrumentationPython,
@@ -319,6 +324,7 @@ func main() {
 		config.WithAutoDetect(ad),
 		config.WithLabelFilters(labelsFilter),
 		config.WithAnnotationFilters(annotationsFilter),
+		config.WithIgnoreMissingCollectorCRDs(ignoreMissingCollectorCRDs),
 	)
 	err = cfg.AutoDetect()
 	if err != nil {
@@ -347,6 +353,15 @@ func main() {
 	} else {
 		setupLog.Info("Cert-Manager is not available to the operator, skipping adding to scheme.")
 	}
+	if cfg.CollectorAvailability() == collector.Available {
+		setupLog.Info("OpenTelemetryCollectorCRDSs are available to the operator")
+	} else {
+		setupLog.Info("OpenTelemetryCollectorCRDSs are not available to the operator")
+		if !cfg.IgnoreMissingCollectorCRDs() {
+			setupLog.Error(errors.New("missing OpenTelemetryCollector CRDs"), "The OpenTelemetryCollector CRDs are not present in the cluster. Set ignore_missing_collector_crds to true or install the CRDs in the cluster.")
+			os.Exit(1)
+		}
+	}
 	if cfg.AnnotationsFilter() != nil {
 		for _, basePattern := range cfg.AnnotationsFilter() {
 			_, compileErr := regexp.Compile(basePattern)
@@ -371,19 +386,22 @@ func main() {
 		os.Exit(1)
 	}
 
-	collectorReconciler := controllers.NewReconciler(controllers.Params{
-		Client:   mgr.GetClient(),
-		Log:      ctrl.Log.WithName("controllers").WithName("OpenTelemetryCollector"),
-		Scheme:   mgr.GetScheme(),
-		Config:   cfg,
-		Recorder: mgr.GetEventRecorderFor("opentelemetry-operator"),
-		Reviewer: reviewer,
-		Version:  v,
-	})
+	var collectorReconciler *controllers.OpenTelemetryCollectorReconciler
+	if cfg.CollectorAvailability() == collector.Available {
+		collectorReconciler = controllers.NewReconciler(controllers.Params{
+			Client:   mgr.GetClient(),
+			Log:      ctrl.Log.WithName("controllers").WithName("OpenTelemetryCollector"),
+			Scheme:   mgr.GetScheme(),
+			Config:   cfg,
+			Recorder: mgr.GetEventRecorderFor("opentelemetry-operator"),
+			Reviewer: reviewer,
+			Version:  v,
+		})
 
-	if err = collectorReconciler.SetupWithManager(mgr); err != nil {
-		setupLog.Error(err, "unable to create controller", "controller", "OpenTelemetryCollector")
-		os.Exit(1)
+		if err = collectorReconciler.SetupWithManager(mgr); err != nil {
+			setupLog.Error(err, "unable to create controller", "controller", "OpenTelemetryCollector")
+			os.Exit(1)
+		}
 	}
 
 	if cfg.TargetAllocatorAvailability() == targetallocator.Available {
@@ -436,32 +454,34 @@ func main() {
 			}
 		}
 
-		bv := func(ctx context.Context, collector otelv1beta1.OpenTelemetryCollector) admission.Warnings {
-			var warnings admission.Warnings
-			params, newErr := collectorReconciler.GetParams(ctx, collector)
-			if err != nil {
-				warnings = append(warnings, newErr.Error())
+		if cfg.CollectorAvailability() == collector.Available {
+			bv := func(ctx context.Context, collector otelv1beta1.OpenTelemetryCollector) admission.Warnings {
+				var warnings admission.Warnings
+				params, newErr := collectorReconciler.GetParams(ctx, collector)
+				if err != nil {
+					warnings = append(warnings, newErr.Error())
+					return warnings
+				}
+
+				params.ErrorAsWarning = true
+				_, newErr = collectorManifests.Build(params)
+				if newErr != nil {
+					warnings = append(warnings, newErr.Error())
+					return warnings
+				}
 				return warnings
 			}
 
-			params.ErrorAsWarning = true
-			_, newErr = collectorManifests.Build(params)
-			if newErr != nil {
-				warnings = append(warnings, newErr.Error())
-				return warnings
+			var fipsCheck fips.FIPSCheck
+			if ad.FIPSEnabled(ctx) {
+				receivers, exporters, processors, extensions := parseFipsFlag(fipsDisabledComponents)
+				logger.Info("Fips disabled components", "receivers", receivers, "exporters", exporters, "processors", processors, "extensions", extensions)
+				fipsCheck = fips.NewFipsCheck(receivers, exporters, processors, extensions)
+			}
+			if err = otelv1beta1.SetupCollectorWebhook(mgr, cfg, reviewer, crdMetrics, bv, fipsCheck); err != nil {
+				setupLog.Error(err, "unable to create webhook", "webhook", "OpenTelemetryCollector")
+				os.Exit(1)
 			}
-			return warnings
-		}
-
-		var fipsCheck fips.FIPSCheck
-		if ad.FIPSEnabled(ctx) {
-			receivers, exporters, processors, extensions := parseFipsFlag(fipsDisabledComponents)
-			logger.Info("Fips disabled components", "receivers", receivers, "exporters", exporters, "processors", processors, "extensions", extensions)
-			fipsCheck = fips.NewFipsCheck(receivers, exporters, processors, extensions)
-		}
-		if err = otelv1beta1.SetupCollectorWebhook(mgr, cfg, reviewer, crdMetrics, bv, fipsCheck); err != nil {
-			setupLog.Error(err, "unable to create webhook", "webhook", "OpenTelemetryCollector")
-			os.Exit(1)
 		}
 		if cfg.TargetAllocatorAvailability() == targetallocator.Available {
 			if err = otelv1alpha1.SetupTargetAllocatorWebhook(mgr, cfg, reviewer); err != nil {


Test patch:
diff --git a/cmd/otel-allocator/internal/config/config_test.go b/cmd/otel-allocator/internal/config/config_test.go
index 7593681e..a736d8f3 100644
--- a/cmd/otel-allocator/internal/config/config_test.go
+++ b/cmd/otel-allocator/internal/config/config_test.go
@@ -501,13 +501,8 @@ func TestLoadFromFile(t *testing.T) {
 }
 
 func TestLoadFromEnv(t *testing.T) {
-	current := os.Getenv("OTELCOL_NAMESPACE")
-	t.Cleanup(func() {
-		err := os.Setenv("OTELCOL_NAMESPACE", current)
-		assert.NoError(t, err)
-	})
 	namespace := "default"
-	os.Setenv("OTELCOL_NAMESPACE", namespace)
+	t.Setenv("OTELCOL_NAMESPACE", namespace)
 	cfg := &Config{}
 	err := LoadFromEnv(cfg)
 	require.NoError(t, err)
@@ -767,6 +762,7 @@ kube_config_file_path: "/config/kube.config"
 		kubeConfigPath := createDummyKubeConfig(t, tempDir)
 
 		configContent := `
+collector_namespace: config-file-namespace
 listen_addr: "` + configListenAddr + `"
 prometheus_cr:
   enabled: true
@@ -794,6 +790,7 @@ kube_config_file_path: "` + kubeConfigPath + `"
 		assert.True(t, config.HTTPS.Enabled, "Config file should override defaults for HTTPS enabled")
 		assert.Equal(t, ":7443", config.HTTPS.ListenAddr, "Config file should override defaults for HTTPS listen address")
 		assert.Equal(t, kubeConfigPath, config.KubeConfigFilePath, "Config file should set kube config path")
+		assert.Equal(t, "config-file-namespace", config.CollectorNamespace, "Config file should set collector namespace")
 	})
 
 	t.Run("environment variables are applied", func(t *testing.T) {
@@ -805,17 +802,9 @@ kube_config_file_path: "` + kubeConfigPath + `"
 
 		kubeConfigPath := createDummyKubeConfig(t, tempDir)
 
-		// Save original env var value and restore after test
-		originalNamespace := os.Getenv("OTELCOL_NAMESPACE")
-		defer func() {
-			envErr := os.Setenv("OTELCOL_NAMESPACE", originalNamespace)
-			assert.NoError(t, envErr)
-		}()
-
 		// Set environment variable
 		testNamespace := "test-namespace"
-		err = os.Setenv("OTELCOL_NAMESPACE", testNamespace)
-		require.NoError(t, err)
+		t.Setenv("OTELCOL_NAMESPACE", testNamespace)
 
 		// Prepare args for Load function
 		args := []string{
@@ -848,17 +837,9 @@ kube_config_file_path: "` + kubeConfigPath + `"
 		err := os.WriteFile(configPath, []byte(configContent), 0600)
 		require.NoError(t, err)
 
-		// Save original env var value and restore after test
-		originalNamespace := os.Getenv("OTELCOL_NAMESPACE")
-		defer func() {
-			envErr := os.Setenv("OTELCOL_NAMESPACE", originalNamespace)
-			assert.NoError(t, envErr)
-		}()
-
 		// Environment variable sets value
 		testNamespace := "env-var-namespace"
-		err = os.Setenv("OTELCOL_NAMESPACE", testNamespace)
-		require.NoError(t, err)
+		t.Setenv("OTELCOL_NAMESPACE", testNamespace)
 
 		// Prepare args for Load function with CLI values
 		cliListenAddr := ":8888"
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 2da957d7..43ebe045 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -12,6 +12,7 @@ import (
 
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager"
+	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/collector"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/openshift"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/prometheus"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/rbac"
@@ -53,6 +54,9 @@ func TestConfigChangesOnAutoDetect(t *testing.T) {
 		TargetAllocatorAvailabilityFunc: func() (targetallocator.Availability, error) {
 			return targetallocator.Available, nil
 		},
+		CollectorAvailabilityFunc: func() (collector.Availability, error) {
+			return collector.Available, nil
+		},
 	}
 	cfg := config.New(
 		config.WithAutoDetect(mock),
@@ -64,6 +68,7 @@ func TestConfigChangesOnAutoDetect(t *testing.T) {
 	require.Equal(t, rbac.NotAvailable, cfg.CreateRBACPermissions())
 	require.Equal(t, certmanager.NotAvailable, cfg.CertManagerAvailability())
 	require.Equal(t, targetallocator.NotAvailable, cfg.TargetAllocatorAvailability())
+	require.Equal(t, collector.NotAvailable, cfg.CollectorAvailability())
 
 	// test
 	err := cfg.AutoDetect()
@@ -85,6 +90,7 @@ type mockAutoDetect struct {
 	RBACPermissionsFunc             func(ctx context.Context) (rbac.Availability, error)
 	CertManagerAvailabilityFunc     func(ctx context.Context) (certmanager.Availability, error)
 	TargetAllocatorAvailabilityFunc func() (targetallocator.Availability, error)
+	CollectorAvailabilityFunc       func() (collector.Availability, error)
 }
 
 func (m *mockAutoDetect) FIPSEnabled(_ context.Context) bool {
@@ -125,3 +131,10 @@ func (m *mockAutoDetect) TargetAllocatorAvailability() (targetallocator.Availabi
 	}
 	return targetallocator.NotAvailable, nil
 }
+
+func (m *mockAutoDetect) CollectorAvailability() (collector.Availability, error) {
+	if m.CollectorAvailabilityFunc != nil {
+		return m.CollectorAvailabilityFunc()
+	}
+	return collector.NotAvailable, nil
+}
diff --git a/internal/controllers/reconcile_test.go b/internal/controllers/reconcile_test.go
index 7251b9a6..e06638e3 100644
--- a/internal/controllers/reconcile_test.go
+++ b/internal/controllers/reconcile_test.go
@@ -38,6 +38,7 @@ import (
 
 	"github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1"
 	"github.com/open-telemetry/opentelemetry-operator/apis/v1beta1"
+	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/collector"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/openshift"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/prometheus"
 	autoRBAC "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/rbac"
@@ -794,6 +795,9 @@ func TestOpenTelemetryCollectorReconciler_RemoveDisabled(t *testing.T) {
 	testCtx, cancel := context.WithCancel(context.Background())
 	t.Cleanup(cancel)
 	reconciler := createTestReconciler(t, testCtx, config.New(
+		config.WithAutoDetect(&mockAutoDetect{CollectorCRDAvailabilityFunc: func() (collector.Availability, error) {
+			return collector.Available, nil
+		}}),
 		config.WithCollectorImage("default-collector"),
 		config.WithTargetAllocatorImage("default-ta-allocator"),
 		config.WithOpenShiftRoutesAvailability(openshift.RoutesAvailable),
diff --git a/internal/controllers/suite_test.go b/internal/controllers/suite_test.go
index 6c4dade2..2157830a 100644
--- a/internal/controllers/suite_test.go
+++ b/internal/controllers/suite_test.go
@@ -46,6 +46,7 @@ import (
 	"github.com/open-telemetry/opentelemetry-operator/apis/v1beta1"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager"
+	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/collector"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/openshift"
 	"github.com/open-telemetry/opentelemetry-operator/internal/autodetect/prometheus"
 	autoRBAC "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/rbac"
@@ -92,6 +93,7 @@ type mockAutoDetect struct {
 	RBACPermissionsFunc             func(ctx context.Context) (autoRBAC.Availability, error)
 	CertManagerAvailabilityFunc     func(ctx context.Context) (certmanager.Availability, error)
 	TargetAllocatorAvailabilityFunc func() (targetallocator.Availability, error)
+	CollectorCRDAvailabilityFunc    func() (collector.Availability, error)
 }
 
 func (m *mockAutoDetect) FIPSEnabled(_ context.Context) bool {
@@ -133,6 +135,13 @@ func (m *mockAutoDetect) TargetAllocatorAvailability() (targetallocator.Availabi
 	return targetallocator.NotAvailable, nil
 }
 
+func (m *mockAutoDetect) CollectorAvailability() (collector.Availability, error) {
+	if m.CollectorCRDAvailabilityFunc != nil {
+		return m.CollectorCRDAvailabilityFunc()
+	}
+	return collector.Available, nil
+}
+
 func TestMain(m *testing.M) {
 	var err error
 	ctx, cancel = context.WithCancel(context.TODO())