Repository: Dynatrace/dynatrace-operator
Pull request #4763: Prevent to run `helm upgrade` with wrong/missing CRD versions
Author: login: waodim
Labels: name: core
description: Changes to core functionality of the Operator
Created: 2025-04-15T08:20:37Z | Merged: 2025-04-24T08:58:23Z

<!--

Thanks for opening a pull request! Here are some tips to get everything merged smoothly:

1. Read our contributor guidelines: https://github.com/Dynatrace/dynatrace-operator/blob/main/CONTRIBUTING.md

2. If the PR is unfinished, raise it as a draft or prefix the title with "WIP:" so it's clear to everyone.

3. Be sure to allow edits from maintainers, so it's easier for us to help: https://help.github.com/en/articles/allowing-changes-to-a-pull-request-branch-created-from-a-fork

-->

## Description

[JIRA](https://dt-rnd.atlassian.net/browse/DAQ-5399)

With this PR we prevent the successful usage of helm upgrade, if no CRD version is available on the cluster and the installCRD flag is set to false.

<!--

Please include the following:
- The motivation for the change
    - Link to the Github issue or Jira ticket, if exists.
- The summary of the change

-->

## How can this be tested?

- run helm upgrade with installCRD set to false. Operator pod should error.

<!--

Please include some guiding steps on how to test this change during a review.

- What environment is necessary for the change to be noticeable ?
- What needs to be enabled/applied for the change to be noticeable ?

-->


Diff:
diff --git a/cmd/operator/certs.go b/cmd/operator/certs.go
index 5488694662..fabbde2e96 100644
--- a/cmd/operator/certs.go
+++ b/cmd/operator/certs.go
@@ -19,6 +19,11 @@ func runCertInit(cfg *rest.Config, namespace string) error {
 		return err
 	}
 
+	err = checkCRDs(certInitManager)
+	if err != nil {
+		return err
+	}
+
 	ctx, cancelFn := context.WithCancel(context.Background())
 
 	err = certificates.AddInit(certInitManager, namespace, cancelFn)
diff --git a/cmd/operator/cmd.go b/cmd/operator/cmd.go
index f371252dd6..c94caa2475 100644
--- a/cmd/operator/cmd.go
+++ b/cmd/operator/cmd.go
@@ -3,7 +3,12 @@ package operator
 import (
 	"context"
 	"os"
+	"reflect"
 
+	"github.com/Dynatrace/dynatrace-operator/pkg/api/v1alpha2"
+	"github.com/Dynatrace/dynatrace-operator/pkg/api/v1alpha2/edgeconnect"
+	"github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4"
+	"github.com/Dynatrace/dynatrace-operator/pkg/api/v1beta4/dynakube"
 	"github.com/Dynatrace/dynatrace-operator/pkg/logd"
 	"github.com/Dynatrace/dynatrace-operator/pkg/util/installconfig"
 	"github.com/Dynatrace/dynatrace-operator/pkg/util/kubeobjects/env"
@@ -12,10 +17,12 @@ import (
 	"github.com/Dynatrace/dynatrace-operator/pkg/version"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/client-go/rest"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/config"
+	"sigs.k8s.io/controller-runtime/pkg/manager"
 )
 
 const (
@@ -66,6 +73,7 @@ func runInPod(kubeCfg *rest.Config) error {
 	}
 
 	isOLM := kubesystem.IsDeployedViaOlm(*operatorPod)
+
 	if !isOLM {
 		err = runCertInit(kubeCfg, namespace)
 		if err != nil {
@@ -73,7 +81,23 @@ func runInPod(kubeCfg *rest.Config) error {
 		}
 	}
 
-	return runOperator(kubeCfg, namespace, isOLM)
+	operatorManager, err := createOperatorManager(kubeCfg, namespace, isOLM)
+	if err != nil {
+		return err
+	}
+
+	if isOLM {
+		// in most cases checkCRDs happen in the runCertInit,
+		// the reason for that is we run a manager to create the certs
+		// this manager uses the same ports for livez
+		// the controller-runtime will error with "port already in use" even if we didn't .Start the manager
+		err = checkCRDs(operatorManager)
+		if err != nil {
+			return err
+		}
+	}
+
+	return errors.WithStack(operatorManager.Start(ctrl.SetupSignalHandler()))
 }
 
 func runLocally(kubeCfg *rest.Config) error {
@@ -84,17 +108,40 @@ func runLocally(kubeCfg *rest.Config) error {
 		return err
 	}
 
-	return runOperator(kubeCfg, namespace, false)
+	operatorManager, err := createOperatorManager(kubeCfg, namespace, false)
+	if err != nil {
+		return err
+	}
+
+	return errors.WithStack(operatorManager.Start(ctrl.SetupSignalHandler()))
 }
 
-func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error {
-	operatorManager, err := createOperatorManager(kubeCfg, namespace, isOLM)
+func checkCRDs(operatorManager manager.Manager) error {
+	groupKind := schema.GroupKind{
+		Group: v1beta4.GroupVersion.Group,
+		Kind:  reflect.TypeOf(dynakube.DynaKube{}).Name(),
+	}
+
+	_, err := operatorManager.GetRESTMapper().RESTMapping(groupKind, v1beta4.GroupVersion.Version)
 	if err != nil {
+		log.Info("missing expected CRD version for DynaKube", "version", v1beta4.GroupVersion.Version)
+
 		return err
 	}
 
-	ctx := ctrl.SetupSignalHandler()
-	err = operatorManager.Start(ctx)
+	if installconfig.GetModules().EdgeConnect {
+		groupKind = schema.GroupKind{
+			Group: v1alpha2.GroupVersion.Group,
+			Kind:  reflect.TypeOf(edgeconnect.EdgeConnect{}).Name(),
+		}
+
+		_, err = operatorManager.GetRESTMapper().RESTMapping(groupKind, v1alpha2.GroupVersion.Version)
+		if err != nil {
+			log.Info("missing expected CRD version for EdgeConnect", "version", v1alpha2.GroupVersion.Version)
+
+			return err
+		}
+	}
 
-	return errors.WithStack(err)
+	return nil
 }


Reviews:
user: login: 0sewa0
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T08:40:38Z
user: login: aorcholski
body: I performed the following steps
- make deploy
- delete  dynakube CRD
- delete operator pod
No "DynaKube kind not found in group version" error message in the operator log after it is re-started..
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T08:48:51Z
user: login: andriisoldatenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:49:52Z
user: login: andriisoldatenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:50:01Z
user: login: andriisoldatenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:55:00Z
user: login: andriisoldatenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:58:01Z
user: login: waodim
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T14:40:40Z
user: login: 0sewa0
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T14:43:43Z
user: login: waodim
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T14:44:47Z
user: login: 0sewa0
body: tested it without applying any CRD, I don't know what is the expected outcome

because [this](https://github.com/Dynatrace/dynatrace-operator/blob/df7c2e68316e25c511e64209b50605e08b29b410/pkg/controllers/certificates/webhook_cert_controller.go#L83) will not allow us to reach the code

which causes the operator to be `Running` but with a bunch of errors
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T14:53:02Z
user: login: 0sewa0
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T09:32:10Z
user: login: 0sewa0
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T09:35:16Z
user: login: waodim
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T10:14:10Z
user: login: aorcholski
body: ```
helm upgrade dynatrace-operator config/helm/chart/default \
                --install \
                --namespace dynatrace \
                --create-namespace \
                --atomic \
                --set installCRD=false \
                ...
```

```
│ {"level":"info","ts":"2025-04-22T12:34:29.579Z","logger":"operator-command","msg":"missing expected CRD version for DynaKube","version":"v1beta4"}
│ {"level":"info","ts":"2025-04-22T12:34:29.579Z","logger":"main","msg":"no matches for kind \"DynaKube\" in version \"dynatrace.com/v1beta4\""} 
```
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T12:36:55Z
user: login: aorcholski
body: make deploy
```
│ {"level":"info","ts":"2025-04-22T12:49:07.459Z","logger":"install-config","msg":"envvar content read and set","envvar":"modules.json","value":"{\n  \"csiDriver\": true,\n  \"activeGate\": true,\n  \"oneAgent\": true,\n  \"extensions\": true,\n  \"logMonitoring\": true,\n  \"edgeConnect\": true,\n  \"supportability\": true,\n  \"kspm\": tr │
│ {"level":"info","ts":"2025-04-22T12:49:07.459Z","logger":"version","msg":"dynatrace-operator","version":"snapshot-feature-err-no-expected-crd-vers","gitCommit":"f001b0d3a6c2edb8cd59780c116867067e5ace56","buildDate":"2025-04-18T07:10:39+00:00","goVersion":"go1.24.2","platform":"linux/amd64"}                                                  │
│ {"level":"info","ts":"2025-04-22T12:49:07.459Z","msg":"logging level","logLevel":"info"}                                                                                                                                                                                                                                                             │
│ {"level":"info","ts":"2025-04-22T12:49:07.864Z","logger":"main","msg":"error listening on :10080: listen tcp :10080: bind: address already in use"}                                                                                                                                                                                                  │
│ Error: error listening on :10080: listen tcp :10080: bind: address already in use 
```
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T13:20:43Z
user: login: 0sewa0
body: tested with an upgrade
1.0.0 -> this branch + `installCRDs: false` == failed upgrade
1.0.0 -> this branch + `installCRDs: true` == successful upgrade

(used an old version so I don't have to think about which CRD version was introduced when 😄 )
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T07:36:46Z
user: login: andriisoldatenko
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T08:06:07Z
user: login: aorcholski
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T08:24:32Z

Review comments:
diff_hunk: @@ -88,6 +91,35 @@ func runLocally(kubeCfg *rest.Config) error {
 }
 
 func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error {
+	discoveryClient, err := discovery.NewDiscoveryClientForConfig(kubeCfg)
+	if err != nil {
+		return err
+	}
+
+	groupVersion := schema.GroupVersion{
+		Group:   "dynatrace.com",
+		Version: expectedVersion,
+	}
+
+	resourceList, err := discoveryClient.ServerResourcesForGroupVersion(groupVersion.String())
+	if err != nil {
+		return errors.WithMessagef(err, "required DynaKube CRD version %s not found", groupVersion.String())
+	}
+
+	var versionFound bool
+
+	for _, resource := range resourceList.APIResources {
+		if resource.Kind == "DynaKube" {
+			versionFound = true
+
+			break
+		}
+	}
+
+	if !versionFound {
+		return errors.New("DynaKube kind not found in group version")
+	}
+
path: cmd/operator/cmd.go
user: login: 0sewa0
body: ```suggestion
	groupKind := schema.GroupKind{
		Group: v1beta4.GroupVersion.Group,
		Kind: "DynaKube", // maybe use reflect to get the name of the DynaKube stuct, shrug
	}

	_, err = operatorManager.GetRESTMapper().RESTMapping(groupKind, v1beta4.GroupVersion.Version)
	if err != nil {
		log.Info("missing CRD")
		return err
	}
```

didn't test it, also this only considers the `DynaKube`

you could also do similar with `EdgeConnect` (if it's not out of scope, )
created_at: 2025-04-16T08:40:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,6 +91,35 @@ func runLocally(kubeCfg *rest.Config) error {
 }
 
 func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error {
+	discoveryClient, err := discovery.NewDiscoveryClientForConfig(kubeCfg)
+	if err != nil {
+		return err
+	}
+
+	groupVersion := schema.GroupVersion{
+		Group:   "dynatrace.com",
+		Version: expectedVersion,
+	}
+
+	resourceList, err := discoveryClient.ServerResourcesForGroupVersion(groupVersion.String())
+	if err != nil {
+		return errors.WithMessagef(err, "required DynaKube CRD version %s not found", groupVersion.String())
+	}
+
+	var versionFound bool
+
+	for _, resource := range resourceList.APIResources {
+		if resource.Kind == "DynaKube" {
+			versionFound = true
+
+			break
+		}
+	}
+
+	if !versionFound {
+		return errors.New("DynaKube kind not found in group version")
+	}
+
path: cmd/operator/cmd.go
user: login: andriisoldatenko
body: i'm about to propose same, i like this idea, bc when we bump version we will update this file easy, rather than change string.

Also you can revisit what we do here https://github.com/dynatrace/dynatrace-operator/blob/02ee3154eb669436c4df8f49aaa84486d6f8ed1f/cmd/support_archive/resources.go#L142-L165
created_at: 2025-04-16T09:49:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,6 +91,35 @@ func runLocally(kubeCfg *rest.Config) error {
 }
 
 func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error {
+	discoveryClient, err := discovery.NewDiscoveryClientForConfig(kubeCfg)
+	if err != nil {
+		return err
+	}
+
+	groupVersion := schema.GroupVersion{
+		Group:   "dynatrace.com",
+		Version: expectedVersion,
+	}
+
+	resourceList, err := discoveryClient.ServerResourcesForGroupVersion(groupVersion.String())
+	if err != nil {
+		return errors.WithMessagef(err, "required DynaKube CRD version %s not found", groupVersion.String())
+	}
+
+	var versionFound bool
+
+	for _, resource := range resourceList.APIResources {
+		if resource.Kind == "DynaKube" {
+			versionFound = true
+
+			break
+		}
+	}
+
+	if !versionFound {
+		return errors.New("DynaKube kind not found in group version")
+	}
+
path: cmd/operator/cmd.go
user: login: andriisoldatenko
body: +1 for EdgeConnect
created_at: 2025-04-16T09:50:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,6 +91,35 @@ func runLocally(kubeCfg *rest.Config) error {
 }
 
 func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error {
+	discoveryClient, err := discovery.NewDiscoveryClientForConfig(kubeCfg)
+	if err != nil {
+		return err
+	}
+
+	groupVersion := schema.GroupVersion{
+		Group:   "dynatrace.com",
+		Version: expectedVersion,
+	}
+
+	resourceList, err := discoveryClient.ServerResourcesForGroupVersion(groupVersion.String())
+	if err != nil {
+		return errors.WithMessagef(err, "required DynaKube CRD version %s not found", groupVersion.String())
+	}
+
+	var versionFound bool
+
+	for _, resource := range resourceList.APIResources {
+		if resource.Kind == "DynaKube" {
+			versionFound = true
+
+			break
+		}
+	}
+
+	if !versionFound {
+		return errors.New("DynaKube kind not found in group version")
+	}
+
path: cmd/operator/cmd.go
user: login: andriisoldatenko
body: should be:

```
	groupVersion := schema.GroupVersion{
		Group:   v1beta4.GroupVersion.Group,
		Version: v1beta4.GroupVersion.Version,
	}
```
created_at: 2025-04-16T09:55:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,6 +91,35 @@ func runLocally(kubeCfg *rest.Config) error {
 }
 
 func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error {
+	discoveryClient, err := discovery.NewDiscoveryClientForConfig(kubeCfg)
+	if err != nil {
+		return err
+	}
+
+	groupVersion := schema.GroupVersion{
+		Group:   "dynatrace.com",
+		Version: expectedVersion,
+	}
+
+	resourceList, err := discoveryClient.ServerResourcesForGroupVersion(groupVersion.String())
+	if err != nil {
+		return errors.WithMessagef(err, "required DynaKube CRD version %s not found", groupVersion.String())
+	}
+
+	var versionFound bool
+
+	for _, resource := range resourceList.APIResources {
+		if resource.Kind == "DynaKube" {
+			versionFound = true
+
+			break
+		}
+	}
+
+	if !versionFound {
+		return errors.New("DynaKube kind not found in group version")
+	}
+
path: cmd/operator/cmd.go
user: login: andriisoldatenko
body: regarding name +1 https://pkg.go.dev/reflect#TypeOf
created_at: 2025-04-16T09:58:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,6 +91,35 @@ func runLocally(kubeCfg *rest.Config) error {
 }
 
 func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error {
+	discoveryClient, err := discovery.NewDiscoveryClientForConfig(kubeCfg)
+	if err != nil {
+		return err
+	}
+
+	groupVersion := schema.GroupVersion{
+		Group:   "dynatrace.com",
+		Version: expectedVersion,
+	}
+
+	resourceList, err := discoveryClient.ServerResourcesForGroupVersion(groupVersion.String())
+	if err != nil {
+		return errors.WithMessagef(err, "required DynaKube CRD version %s not found", groupVersion.String())
+	}
+
+	var versionFound bool
+
+	for _, resource := range resourceList.APIResources {
+		if resource.Kind == "DynaKube" {
+			versionFound = true
+
+			break
+		}
+	}
+
+	if !versionFound {
+		return errors.New("DynaKube kind not found in group version")
+	}
+
path: cmd/operator/cmd.go
user: login: waodim
body: Done https://github.com/Dynatrace/dynatrace-operator/pull/4763/commits/15e868540889ad0ee13ac834d9dd72d662226b7c
created_at: 2025-04-16T14:40:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,14 +17,17 @@ import (
 	"github.com/Dynatrace/dynatrace-operator/pkg/version"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/client-go/rest"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/config"
+	"sigs.k8s.io/controller-runtime/pkg/manager"
 )
 
 const (
-	use = "operator"
+	use             = "operator"
+	expectedVersion = "v1beta4"
path: cmd/operator/cmd.go
user: login: 0sewa0
body: no longer necessary
created_at: 2025-04-16T14:41:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,8 +101,39 @@ func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error {
 		return err
 	}
 
+	err = checkCRDs(operatorManager)
+	if err != nil {
+		return err
+	}
+
 	ctx := ctrl.SetupSignalHandler()
 	err = operatorManager.Start(ctx)
 
 	return errors.WithStack(err)
 }
+
+func checkCRDs(operatorManager manager.Manager) error {
+	groupKind := schema.GroupKind{
+		Group: v1beta4.GroupVersion.Group,
+		Kind:  reflect.TypeOf(dynakube.DynaKube{}).Name(),
+	}
+
+	_, err := operatorManager.GetRESTMapper().RESTMapping(groupKind, v1beta4.GroupVersion.Version)
+	if err != nil {
+		log.Info("missing CRD for DynaKube")
path: cmd/operator/cmd.go
user: login: 0sewa0
body: ```suggestion
		log.Info("missing expected CRD version for DynaKube", "version", v1beta4.GroupVersion.Version)
```
created_at: 2025-04-16T14:42:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,8 +101,39 @@ func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error {
 		return err
 	}
 
+	err = checkCRDs(operatorManager)
+	if err != nil {
+		return err
+	}
+
 	ctx := ctrl.SetupSignalHandler()
 	err = operatorManager.Start(ctx)
 
 	return errors.WithStack(err)
 }
+
+func checkCRDs(operatorManager manager.Manager) error {
+	groupKind := schema.GroupKind{
+		Group: v1beta4.GroupVersion.Group,
+		Kind:  reflect.TypeOf(dynakube.DynaKube{}).Name(),
+	}
+
+	_, err := operatorManager.GetRESTMapper().RESTMapping(groupKind, v1beta4.GroupVersion.Version)
+	if err != nil {
+		log.Info("missing CRD for DynaKube")
+		return err
+	}
+
+	groupKind = schema.GroupKind{
+		Group: v1alpha2.GroupVersion.Group,
+		Kind:  reflect.TypeOf(edgeconnect.EdgeConnect{}).Name(),
+	}
+
+	_, err = operatorManager.GetRESTMapper().RESTMapping(groupKind, v1alpha2.GroupVersion.Version)
+	if err != nil {
+		log.Info("missing CRD for EdgeConnect")
path: cmd/operator/cmd.go
user: login: 0sewa0
body: ```suggestion
		log.Info("missing expected CRD version for EdgeConnect", "version", v1alpha2.GroupVersion.Version)
```
created_at: 2025-04-16T14:43:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,14 +17,17 @@ import (
 	"github.com/Dynatrace/dynatrace-operator/pkg/version"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/client-go/rest"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/config"
+	"sigs.k8s.io/controller-runtime/pkg/manager"
 )
 
 const (
-	use = "operator"
+	use             = "operator"
+	expectedVersion = "v1beta4"
path: cmd/operator/cmd.go
user: login: waodim
body: done https://github.com/Dynatrace/dynatrace-operator/pull/4763/commits/327a4450af6b6b09e14af1f36d36846561090a13
created_at: 2025-04-16T14:44:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -84,17 +102,43 @@ func runLocally(kubeCfg *rest.Config) error {
 		return err
 	}
 
-	return runOperator(kubeCfg, namespace, false)
+	operatorManager, err := createOperatorManager(kubeCfg, namespace, false)
+	if err != nil {
+		return err
+	}
+
+	err = checkCRDs(operatorManager)
+	if err != nil {
+		return err
+	}
+
+	return errors.WithStack(operatorManager.Start(ctrl.SetupSignalHandler()))
 }
 
-func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error {
-	operatorManager, err := createOperatorManager(kubeCfg, namespace, isOLM)
+func checkCRDs(operatorManager manager.Manager) error {
+	groupKind := schema.GroupKind{
+		Group: v1beta4.GroupVersion.Group,
+		Kind:  reflect.TypeOf(dynakube.DynaKube{}).Name(),
+	}
+
+	_, err := operatorManager.GetRESTMapper().RESTMapping(groupKind, v1beta4.GroupVersion.Version)
 	if err != nil {
+		log.Info("missing expected CRD version for DynaKube", "version", v1beta4.GroupVersion.Version)
+
 		return err
 	}
 
-	ctx := ctrl.SetupSignalHandler()
-	err = operatorManager.Start(ctx)
+	groupKind = schema.GroupKind{
path: cmd/operator/cmd.go
user: login: 0sewa0
body: small request, `edgeConnect` can be turned off in helm, while this doesn't affect the CRDs, I would still feel safer adding a check here
```diff
diff --git a/cmd/operator/cmd.go b/cmd/operator/cmd.go
index be366bdf..bdab22a1 100644
--- a/cmd/operator/cmd.go
+++ b/cmd/operator/cmd.go
@@ -128,16 +128,18 @@ func checkCRDs(operatorManager manager.Manager) error {
 		return err
 	}
 
-	groupKind = schema.GroupKind{
-		Group: v1alpha2.GroupVersion.Group,
-		Kind:  reflect.TypeOf(edgeconnect.EdgeConnect{}).Name(),
-	}
+	if installconfig.GetModules().EdgeConnect {
+		groupKind = schema.GroupKind{
+			Group: v1alpha2.GroupVersion.Group,
+			Kind:  reflect.TypeOf(edgeconnect.EdgeConnect{}).Name(),
+		}
 
-	_, err = operatorManager.GetRESTMapper().RESTMapping(groupKind, v1alpha2.GroupVersion.Version)
-	if err != nil {
-		log.Info("missing expected CRD version for EdgeConnect", "version", v1alpha2.GroupVersion.Version)
+		_, err = operatorManager.GetRESTMapper().RESTMapping(groupKind, v1alpha2.GroupVersion.Version)
+		if err != nil {
+			log.Info("missing expected CRD version for EdgeConnect", "version", v1alpha2.GroupVersion.Version)
 
-		return err
+			return err
+		}
 	}
 
 	return nil
```
created_at: 2025-04-17T09:32:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -84,17 +102,43 @@ func runLocally(kubeCfg *rest.Config) error {
 		return err
 	}
 
-	return runOperator(kubeCfg, namespace, false)
+	operatorManager, err := createOperatorManager(kubeCfg, namespace, false)
+	if err != nil {
+		return err
+	}
+
+	err = checkCRDs(operatorManager)
+	if err != nil {
+		return err
+	}
+
+	return errors.WithStack(operatorManager.Start(ctrl.SetupSignalHandler()))
 }
 
-func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error {
-	operatorManager, err := createOperatorManager(kubeCfg, namespace, isOLM)
+func checkCRDs(operatorManager manager.Manager) error {
+	groupKind := schema.GroupKind{
+		Group: v1beta4.GroupVersion.Group,
+		Kind:  reflect.TypeOf(dynakube.DynaKube{}).Name(),
+	}
+
+	_, err := operatorManager.GetRESTMapper().RESTMapping(groupKind, v1beta4.GroupVersion.Version)
 	if err != nil {
+		log.Info("missing expected CRD version for DynaKube", "version", v1beta4.GroupVersion.Version)
+
 		return err
 	}
 
-	ctx := ctrl.SetupSignalHandler()
-	err = operatorManager.Start(ctx)
+	groupKind = schema.GroupKind{
path: cmd/operator/cmd.go
user: login: 0sewa0
body: for `edgeConnect` to be fully "turn-off-able", (CRD and all) we would need a few changes elsewhere, which is out of scope, however, to avoid potential future confusion I think this change is more complete with the check
created_at: 2025-04-17T09:35:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -84,17 +102,43 @@ func runLocally(kubeCfg *rest.Config) error {
 		return err
 	}
 
-	return runOperator(kubeCfg, namespace, false)
+	operatorManager, err := createOperatorManager(kubeCfg, namespace, false)
+	if err != nil {
+		return err
+	}
+
+	err = checkCRDs(operatorManager)
+	if err != nil {
+		return err
+	}
+
+	return errors.WithStack(operatorManager.Start(ctrl.SetupSignalHandler()))
 }
 
-func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error {
-	operatorManager, err := createOperatorManager(kubeCfg, namespace, isOLM)
+func checkCRDs(operatorManager manager.Manager) error {
+	groupKind := schema.GroupKind{
+		Group: v1beta4.GroupVersion.Group,
+		Kind:  reflect.TypeOf(dynakube.DynaKube{}).Name(),
+	}
+
+	_, err := operatorManager.GetRESTMapper().RESTMapping(groupKind, v1beta4.GroupVersion.Version)
 	if err != nil {
+		log.Info("missing expected CRD version for DynaKube", "version", v1beta4.GroupVersion.Version)
+
 		return err
 	}
 
-	ctx := ctrl.SetupSignalHandler()
-	err = operatorManager.Start(ctx)
+	groupKind = schema.GroupKind{
path: cmd/operator/cmd.go
user: login: waodim
body: good catch.

done https://github.com/Dynatrace/dynatrace-operator/pull/4763/commits/75ecfc57d207573ad5fc4f5076d610c8d7c7e0ea
created_at: 2025-04-17T10:14:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: andriisoldatenko
created_at: 2025-04-16T10:07:55Z
body: Also I prefet to move this check and fail somewhere https://github.com/dynatrace/dynatrace-operator/blob/ad4b9942f142321a4cf40939383216bcded73923/cmd/operator/cmd.go#L48-L47 so we can check it before both cases runInPod and run locally as well.

or even as first step before we even run sub-command inside main.go for example. 
author_association: CONTRIBUTOR
user: login: waodim
created_at: 2025-04-16T14:41:30Z
body: > Also I prefet to move this check and fail somewhere https://github.com/dynatrace/dynatrace-operator/blob/ad4b9942f142321a4cf40939383216bcded73923/cmd/operator/cmd.go#L48-L47 so we can check it before both cases runInPod and run locally as well.
> 
> or even as first step before we even run sub-command inside main.go for example.

I am unsure on how to do that to be honest. And as I error the Pod before starting at all in case of no matching crd version I think it works as we want it. Or do you have a strong opinion there?
author_association: CONTRIBUTOR
user: login: 0sewa0
created_at: 2025-04-16T15:04:10Z
body: > > Also I prefet to move this check and fail somewhere [dynatrace/dynatrace-operator@`ad4b994`/cmd/operator/cmd.go#L48-L47](https://github.com/dynatrace/dynatrace-operator/blob/ad4b9942f142321a4cf40939383216bcded73923/cmd/operator/cmd.go#L48-L47) so we can check it before both cases runInPod and run locally as well.
> > or even as first step before we even run sub-command inside main.go for example.
> 
> I am unsure on how to do that to be honest. And as I error the Pod before starting at all in case of no matching crd version I think it works as we want it. Or do you have a strong opinion there?

it is an entrypoint, bit of simple repetition is fine, and [considering my comment](https://github.com/Dynatrace/dynatrace-operator/pull/4763#pullrequestreview-2772841747) you have to move the check either way

```diff
diff --git a/cmd/operator/cmd.go b/cmd/operator/cmd.go
index 14746f48..be366bdf 100644
--- a/cmd/operator/cmd.go
+++ b/cmd/operator/cmd.go
@@ -73,6 +73,17 @@ func runInPod(kubeCfg *rest.Config) error {
 	}
 
 	isOLM := kubesystem.IsDeployedViaOlm(*operatorPod)
+
+	operatorManager, err := createOperatorManager(kubeCfg, namespace, isOLM)
+	if err != nil {
+		return err
+	}
+
+	err = checkCRDs(operatorManager)
+	if err != nil {
+		return err
+	}
+
 	if !isOLM {
 		err = runCertInit(kubeCfg, namespace)
 		if err != nil {
@@ -80,7 +91,7 @@ func runInPod(kubeCfg *rest.Config) error {
 		}
 	}
 
-	return runOperator(kubeCfg, namespace, isOLM)
+	return errors.WithStack(operatorManager.Start(ctrl.SetupSignalHandler()))
 }
 
 func runLocally(kubeCfg *rest.Config) error {
@@ -91,11 +102,7 @@ func runLocally(kubeCfg *rest.Config) error {
 		return err
 	}
 
-	return runOperator(kubeCfg, namespace, false)
-}
-
-func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error {
-	operatorManager, err := createOperatorManager(kubeCfg, namespace, isOLM)
+	operatorManager, err := createOperatorManager(kubeCfg, namespace, false)
 	if err != nil {
 		return err
 	}
@@ -105,10 +112,7 @@ func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error {
 		return err
 	}
 
-	ctx := ctrl.SetupSignalHandler()
-	err = operatorManager.Start(ctx)
-
-	return errors.WithStack(err)
+	return errors.WithStack(operatorManager.Start(ctrl.SetupSignalHandler()))
 }
 
 func checkCRDs(operatorManager manager.Manager) error {

```
author_association: CONTRIBUTOR
user: login: waodim
created_at: 2025-04-17T06:59:32Z
body: > > > Also I prefet to move this check and fail somewhere [dynatrace/dynatrace-operator@`ad4b994`/cmd/operator/cmd.go#L48-L47](https://github.com/dynatrace/dynatrace-operator/blob/ad4b9942f142321a4cf40939383216bcded73923/cmd/operator/cmd.go#L48-L47) so we can check it before both cases runInPod and run locally as well.
> > > or even as first step before we even run sub-command inside main.go for example.
> > 
> > 
> > I am unsure on how to do that to be honest. And as I error the Pod before starting at all in case of no matching crd version I think it works as we want it. Or do you have a strong opinion there?
> 
> it is an entrypoint, bit of simple repetition is fine, and [considering my comment](https://github.com/Dynatrace/dynatrace-operator/pull/4763#pullrequestreview-2772841747) you have to move the check either way
> 
> ```diff
> diff --git a/cmd/operator/cmd.go b/cmd/operator/cmd.go
> index 14746f48..be366bdf 100644
> --- a/cmd/operator/cmd.go
> +++ b/cmd/operator/cmd.go
> @@ -73,6 +73,17 @@ func runInPod(kubeCfg *rest.Config) error {
>  	}
>  
>  	isOLM := kubesystem.IsDeployedViaOlm(*operatorPod)
> +
> +	operatorManager, err := createOperatorManager(kubeCfg, namespace, isOLM)
> +	if err != nil {
> +		return err
> +	}
> +
> +	err = checkCRDs(operatorManager)
> +	if err != nil {
> +		return err
> +	}
> +
>  	if !isOLM {
>  		err = runCertInit(kubeCfg, namespace)
>  		if err != nil {
> @@ -80,7 +91,7 @@ func runInPod(kubeCfg *rest.Config) error {
>  		}
>  	}
>  
> -	return runOperator(kubeCfg, namespace, isOLM)
> +	return errors.WithStack(operatorManager.Start(ctrl.SetupSignalHandler()))
>  }
>  
>  func runLocally(kubeCfg *rest.Config) error {
> @@ -91,11 +102,7 @@ func runLocally(kubeCfg *rest.Config) error {
>  		return err
>  	}
>  
> -	return runOperator(kubeCfg, namespace, false)
> -}
> -
> -func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error {
> -	operatorManager, err := createOperatorManager(kubeCfg, namespace, isOLM)
> +	operatorManager, err := createOperatorManager(kubeCfg, namespace, false)
>  	if err != nil {
>  		return err
>  	}
> @@ -105,10 +112,7 @@ func runOperator(kubeCfg *rest.Config, namespace string, isOLM bool) error {
>  		return err
>  	}
>  
> -	ctx := ctrl.SetupSignalHandler()
> -	err = operatorManager.Start(ctx)
> -
> -	return errors.WithStack(err)
> +	return errors.WithStack(operatorManager.Start(ctrl.SetupSignalHandler()))
>  }
>  
>  func checkCRDs(operatorManager manager.Manager) error {
> ```

applied your diff and now if you install the operator without a CRD the pod errors immediately.
author_association: CONTRIBUTOR
user: login: waodim
created_at: 2025-04-22T14:24:54Z
body: > make deploy
> 
> ```
> │ {"level":"info","ts":"2025-04-22T12:49:07.459Z","logger":"install-config","msg":"envvar content read and set","envvar":"modules.json","value":"{\n  \"csiDriver\": true,\n  \"activeGate\": true,\n  \"oneAgent\": true,\n  \"extensions\": true,\n  \"logMonitoring\": true,\n  \"edgeConnect\": true,\n  \"supportability\": true,\n  \"kspm\": tr │
> │ {"level":"info","ts":"2025-04-22T12:49:07.459Z","logger":"version","msg":"dynatrace-operator","version":"snapshot-feature-err-no-expected-crd-vers","gitCommit":"f001b0d3a6c2edb8cd59780c116867067e5ace56","buildDate":"2025-04-18T07:10:39+00:00","goVersion":"go1.24.2","platform":"linux/amd64"}                                                  │
> │ {"level":"info","ts":"2025-04-22T12:49:07.459Z","msg":"logging level","logLevel":"info"}                                                                                                                                                                                                                                                             │
> │ {"level":"info","ts":"2025-04-22T12:49:07.864Z","logger":"main","msg":"error listening on :10080: listen tcp :10080: bind: address already in use"}                                                                                                                                                                                                  │
> │ Error: error listening on :10080: listen tcp :10080: bind: address already in use 
> ```

Thanks to @0sewa0 this should be now fixed. I ran `make deploy` and encountered no problems.
author_association: CONTRIBUTOR