Repository: crossplane/crossplane

Issue:
[v2] Convert provider CRDs to MRDs at package install time
<!--
Thank you for helping to improve Crossplane!

Please be sure to search for open issues before raising a new one. We use issues
for bug reports and feature requests. Please find us at https://slack.crossplane.io
for questions, support, and discussion.
-->

### What problem are you facing?
<!--
Please tell us a little about your use case - it's okay if it's hypothetical!
Leading with this context helps frame the feature request so we can ensure we
implement it sensibly.
--->

MRD design: https://github.com/crossplane/crossplane/blob/f5f6aac/design/one-pager-mrds.md

### How could Crossplane help solve your problem?
<!--
Let us know how you think Crossplane could help with your use case. 
-->

Per the design:

* Providers will keep packaging CRDs `package.yaml` stream for now
* Provider package metadata should support an array of string `spec.capabilities`
* Providers that can support the late-start MR capability must have the `late-mr-activation` capability
* Crossplane must convert CRDs to MRDs at install time (during revision establish)
* Crossplane must activate MRDs at install time if the provider doesn't have the `late-mr-activation` capability
* Crossplane mustn't activate MRDs at install time if the provider does have the `late-mr-activation` capability

Hints from issue comments:

Solution patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 143b20ea9..2cd723b25 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -257,9 +257,13 @@ jobs:
          - test-area: pkg
            test-suite: package-signature-verification
 
-        # Operations feature flags
+         # Operations feature flags
          - test-area: ops
-           test-suite: ops 
+           test-suite: ops
+
+         # Managed Resource Activation Policy (defaults disabled)
+         - test-area: mrap
+           test-suite: mrap
 
     steps:
       - name: Checkout
diff --git a/apis/pkg/meta/v1/capabilities.go b/apis/pkg/meta/v1/capabilities.go
index 7a328b7f9..428afeec7 100644
--- a/apis/pkg/meta/v1/capabilities.go
+++ b/apis/pkg/meta/v1/capabilities.go
@@ -16,13 +16,42 @@ limitations under the License.
 
 package v1
 
+import (
+	"regexp"
+	"strings"
+)
+
 // Known function capabilities. This shouldn't be treated as an exhaustive list.
 // A package's capabilities array may contain arbitrary entries that aren't
 // meaningful to Crossplane but might be meaningful to some other consumer.
 const (
-	// A function that can be used in a composition.
+	// FunctionCapabilityComposition is a capability key for a function that
+	// can be used in a composition.
 	FunctionCapabilityComposition = "composition"
 
-	// A function that can be used in an operation.
+	// FunctionCapabilityOperation is a capability key for function that can be
+	// used in an operation.
 	FunctionCapabilityOperation = "operation"
+
+	// ProviderCapabilitySafeStart is a capability key for a provider that
+	// supports "safe" starting of its controller gated on the existence of
+	// dependent kinds in the cluster.
+	ProviderCapabilitySafeStart = "safe-start"
 )
+
+// CapabilitiesContainFuzzyMatch will look in the list of capabilities and fuzzy match it to the given key.
+// Fuzzy match is defined as stripping all non-alphanumeric characters and ignoring case.
+func CapabilitiesContainFuzzyMatch(capabilities []string, key string) bool {
+	// fuz will match only alphanumeric characters.
+	fuz := regexp.MustCompile("[^a-zA-Z0-9]+")
+	key = fuz.ReplaceAllString(key, "")
+	key = strings.ToLower(key)
+	for _, c := range capabilities {
+		c = fuz.ReplaceAllString(c, "")
+		c = strings.ToLower(c)
+		if c == key {
+			return true
+		}
+	}
+	return false
+}
diff --git a/apis/pkg/meta/v1/meta.go b/apis/pkg/meta/v1/meta.go
index 624925681..c8e57e156 100644
--- a/apis/pkg/meta/v1/meta.go
+++ b/apis/pkg/meta/v1/meta.go
@@ -52,7 +52,7 @@ type Dependency struct {
 
 	// Package OCI reference of the dependency. Only used when apiVersion and
 	// kind are set.
-	// Musth be a fully qualified image name, including the registry,
+	// Must be a fully qualified image name, including the registry,
 	// repository, and tag. For example, "registry.example.com/repo/package:tag".
 	// +optional
 	Package *string `json:"package,omitempty"`
diff --git a/apis/pkg/meta/v1alpha1/zz_generated.meta.go b/apis/pkg/meta/v1alpha1/zz_generated.meta.go
index 396f28e8e..793a5c2fc 100644
--- a/apis/pkg/meta/v1alpha1/zz_generated.meta.go
+++ b/apis/pkg/meta/v1alpha1/zz_generated.meta.go
@@ -54,7 +54,7 @@ type Dependency struct {
 
 	// Package OCI reference of the dependency. Only used when apiVersion and
 	// kind are set.
-	// Musth be a fully qualified image name, including the registry,
+	// Must be a fully qualified image name, including the registry,
 	// repository, and tag. For example, "registry.example.com/repo/package:tag".
 	// +optional
 	Package *string `json:"package,omitempty"`
diff --git a/apis/pkg/meta/v1beta1/zz_generated.meta.go b/apis/pkg/meta/v1beta1/zz_generated.meta.go
index 593c871b3..3a4026e00 100644
--- a/apis/pkg/meta/v1beta1/zz_generated.meta.go
+++ b/apis/pkg/meta/v1beta1/zz_generated.meta.go
@@ -54,7 +54,7 @@ type Dependency struct {
 
 	// Package OCI reference of the dependency. Only used when apiVersion and
 	// kind are set.
-	// Musth be a fully qualified image name, including the registry,
+	// Must be a fully qualified image name, including the registry,
 	// repository, and tag. For example, "registry.example.com/repo/package:tag".
 	// +optional
 	Package *string `json:"package,omitempty"`
diff --git a/cluster/charts/crossplane/README.md b/cluster/charts/crossplane/README.md
index 6393ce5ab..985602a13 100644
--- a/cluster/charts/crossplane/README.md
+++ b/cluster/charts/crossplane/README.md
@@ -99,6 +99,7 @@ and their default values.
 | `podSecurityContextCrossplane` | Add a custom `securityContext` to the Crossplane pod. | `{}` |
 | `podSecurityContextRBACManager` | Add a custom `securityContext` to the RBAC Manager pod. | `{}` |
 | `priorityClassName` | The PriorityClass name to apply to the Crossplane and RBAC Manager pods. | `""` |
+| `provider.defaultActivations` | Define entries for the default managed resource activation policy. If defined, a default MRAP will contain these activations. | `["*"]` |
 | `provider.packages` | A list of Provider packages to install. | `[]` |
 | `rbacManager.affinity` | Add `affinities` to the RBAC Manager pod deployment. | `{}` |
 | `rbacManager.args` | Add custom arguments to the RBAC Manager pod. | `[]` |
diff --git a/cluster/charts/crossplane/templates/deployment.yaml b/cluster/charts/crossplane/templates/deployment.yaml
index c66f09357..b527952b4 100644
--- a/cluster/charts/crossplane/templates/deployment.yaml
+++ b/cluster/charts/crossplane/templates/deployment.yaml
@@ -79,6 +79,10 @@ spec:
           - --function
           - "{{ $arg }}"
           {{- end }}
+          {{- range $arg := .Values.provider.defaultActivations }}
+          - --activation
+          - "{{ $arg }}"
+          {{- end }}
           resources:
             {{- toYaml .Values.resourcesCrossplane | nindent 12 }}
           {{- with .Values.securityContextCrossplane }}
@@ -147,7 +151,6 @@ spec:
         - {{ $arg }}
         {{- end }}
         imagePullPolicy: {{ .Values.image.pullPolicy }}
-        name: {{ .Chart.Name }}
         resources:
           {{- toYaml .Values.resourcesCrossplane | nindent 12 }}
         startupProbe:
diff --git a/cluster/charts/crossplane/values.yaml b/cluster/charts/crossplane/values.yaml
index e1632f99e..2f7afb915 100755
--- a/cluster/charts/crossplane/values.yaml
+++ b/cluster/charts/crossplane/values.yaml
@@ -57,6 +57,8 @@ args: []
 provider:
   # -- A list of Provider packages to install.
   packages: []
+  # -- Define entries for the default managed resource activation policy. If defined, a default MRAP will contain these activations.
+  defaultActivations: ["*"]
 
 configuration:
   # -- A list of Configuration packages to install.
diff --git a/cluster/meta/meta.pkg.crossplane.io_configurations.yaml b/cluster/meta/meta.pkg.crossplane.io_configurations.yaml
index 9dec57f83..e55837155 100644
--- a/cluster/meta/meta.pkg.crossplane.io_configurations.yaml
+++ b/cluster/meta/meta.pkg.crossplane.io_configurations.yaml
@@ -90,7 +90,7 @@ spec:
                       description: |-
                         Package OCI reference of the dependency. Only used when apiVersion and
                         kind are set.
-                        Musth be a fully qualified image name, including the registry,
+                        Must be a fully qualified image name, including the registry,
                         repository, and tag. For example, "registry.example.com/repo/package:tag".
                       type: string
                     provider:
@@ -189,7 +189,7 @@ spec:
                       description: |-
                         Package OCI reference of the dependency. Only used when apiVersion and
                         kind are set.
-                        Musth be a fully qualified image name, including the registry,
+                        Must be a fully qualified image name, including the registry,
                         repository, and tag. For example, "registry.example.com/repo/package:tag".
                       type: string
                     provider:
diff --git a/cluster/meta/meta.pkg.crossplane.io_functions.yaml b/cluster/meta/meta.pkg.crossplane.io_functions.yaml
index 79df94bbe..c34d218eb 100644
--- a/cluster/meta/meta.pkg.crossplane.io_functions.yaml
+++ b/cluster/meta/meta.pkg.crossplane.io_functions.yaml
@@ -89,7 +89,7 @@ spec:
                       description: |-
                         Package OCI reference of the dependency. Only used when apiVersion and
                         kind are set.
-                        Musth be a fully qualified image name, including the registry,
+                        Must be a fully qualified image name, including the registry,
                         repository, and tag. For example, "registry.example.com/repo/package:tag".
                       type: string
                     provider:
@@ -187,7 +187,7 @@ spec:
                       description: |-
                         Package OCI reference of the dependency. Only used when apiVersion and
                         kind are set.
-                        Musth be a fully qualified image name, including the registry,
+                        Must be a fully qualified image name, including the registry,
                         repository, and tag. For example, "registry.example.com/repo/package:tag".
                       type: string
                     provider:
diff --git a/cluster/meta/meta.pkg.crossplane.io_providers.yaml b/cluster/meta/meta.pkg.crossplane.io_providers.yaml
index d469563c4..77f8d5c4b 100644
--- a/cluster/meta/meta.pkg.crossplane.io_providers.yaml
+++ b/cluster/meta/meta.pkg.crossplane.io_providers.yaml
@@ -89,7 +89,7 @@ spec:
                       description: |-
                         Package OCI reference of the dependency. Only used when apiVersion and
                         kind are set.
-                        Musth be a fully qualified image name, including the registry,
+                        Must be a fully qualified image name, including the registry,
                         repository, and tag. For example, "registry.example.com/repo/package:tag".
                       type: string
                     provider:
@@ -187,7 +187,7 @@ spec:
                       description: |-
                         Package OCI reference of the dependency. Only used when apiVersion and
                         kind are set.
-                        Musth be a fully qualified image name, including the registry,
+                        Must be a fully qualified image name, including the registry,
                         repository, and tag. For example, "registry.example.com/repo/package:tag".
                       type: string
                     provider:
diff --git a/cmd/crossplane/core/core.go b/cmd/crossplane/core/core.go
index ebc85ff6a..ba2b7fc4e 100644
--- a/cmd/crossplane/core/core.go
+++ b/cmd/crossplane/core/core.go
@@ -126,10 +126,11 @@ type startCommand struct {
 	XfnCacheDir    string        `default:"/cache/xfn" env:"XFN_CACHE_DIR"     group:"Alpha Features:" help:"Directory used for caching function responses. Requires --enable-function-response-cache."`
 	XfnCacheMaxTTL time.Duration `default:"24h"        env:"XFN_CACHE_MAX_TTL" group:"Alpha Features:" help:"Maximum TTL for cached function responses. Set to 0 to disable. Requires --enable-function-response-cache."`
 
-	EnableDeploymentRuntimeConfigs bool `default:"true" group:"Beta Features:" help:"Enable support for Deployment Runtime Configs."`
-	EnableUsages                   bool `default:"true" group:"Beta Features:" help:"Enable support for deletion ordering and resource protection with Usages."`
-	EnableSSAClaims                bool `default:"true" group:"Beta Features:" help:"Enable support for using Kubernetes server-side apply to sync claims with composite resources (XRs)."`
-	EnableRealtimeCompositions     bool `default:"true" group:"Beta Features:" help:"Enable support for realtime compositions, i.e. watching composed resources and reconciling compositions immediately when any of the composed resources is updated."`
+	EnableDeploymentRuntimeConfigs          bool `default:"true" group:"Beta Features:" help:"Enable support for Deployment Runtime Configs."`
+	EnableUsages                            bool `default:"true" group:"Beta Features:" help:"Enable support for deletion ordering and resource protection with Usages."`
+	EnableSSAClaims                         bool `default:"true" group:"Beta Features:" help:"Enable support for using Kubernetes server-side apply to sync claims with composite resources (XRs)."`
+	EnableRealtimeCompositions              bool `default:"true" group:"Beta Features:" help:"Enable support for realtime compositions, i.e. watching composed resources and reconciling compositions immediately when any of the composed resources is updated."`
+	EnableCustomToManagedResourceConversion bool `default:"true" group:"Beta Features:" help:"Enable support CRD to MRD conversion when installing a package."`
 
 	// These are features that we've removed support for. Crossplane returns an
 	// error when you enable them. This ensures you'll see an explicit and
@@ -287,6 +288,11 @@ func (c *startCommand) Run(s *runtime.Scheme, log logging.Logger) error { //noli
 		log.Info("Beta feature enabled", "flag", features.EnableBetaRealtimeCompositions)
 	}
 
+	if c.EnableCustomToManagedResourceConversion {
+		o.Features.Enable(features.EnableBetaCustomToManagedResourceConversion)
+		log.Info("Beta feature enabled", "flag", features.EnableBetaCustomToManagedResourceConversion)
+	}
+
 	if c.EnableDeploymentRuntimeConfigs {
 		o.Features.Enable(features.EnableBetaDeploymentRuntimeConfigs)
 		log.Info("Beta feature enabled", "flag", features.EnableBetaDeploymentRuntimeConfigs)
diff --git a/cmd/crossplane/core/init.go b/cmd/crossplane/core/init.go
index c820363fb..73d53a2c3 100644
--- a/cmd/crossplane/core/init.go
+++ b/cmd/crossplane/core/init.go
@@ -29,18 +29,21 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/logging"
 
+	"github.com/crossplane/crossplane/apis/apiextensions/v2alpha1"
 	"github.com/crossplane/crossplane/internal/initializer"
 )
 
 // initCommand configuration for the initialization of core Crossplane controllers.
 type initCommand struct {
-	Providers                 []string `help:"Pre-install a Provider by giving its image URI. This argument can be repeated."      name:"provider"`
-	Configurations            []string `help:"Pre-install a Configuration by giving its image URI. This argument can be repeated." name:"configuration"`
-	Functions                 []string `help:"Pre-install a Function by giving its image URI. This argument can be repeated."      name:"function"`
-	Namespace                 string   `default:"crossplane-system"                                                                env:"POD_NAMESPACE"        help:"Namespace used to set as default scope in default secret store config." short:"n"`
-	ServiceAccount            string   `default:"crossplane"                                                                       env:"POD_SERVICE_ACCOUNT"  help:"Name of the Crossplane Service Account."`
-	CRDsPath                  string   `default:"/crds"                                                                            env:"CRDS_PATH"            help:"Path of Crossplane core Custom Resource Definitions."`
-	WebhookConfigurationsPath string   `default:"/webhookconfigurations"                                                           env:"WEBHOOK_CONFIGS_PATH" help:"Path of Crossplane core Webhook Configurations."`
+	Providers      []string                    `help:"Pre-install a Provider by giving its image URI. This argument can be repeated."                                            name:"provider"`
+	Configurations []string                    `help:"Pre-install a Configuration by giving its image URI. This argument can be repeated."                                       name:"configuration"`
+	Functions      []string                    `help:"Pre-install a Function by giving its image URI. This argument can be repeated."                                            name:"function"`
+	Activations    []v2alpha1.ActivationPolicy `help:"Pre-install a default managed resource activation policy by providing activations entries. This argument can be repeated." name:"activation"`
+
+	Namespace                 string `default:"crossplane-system"      env:"POD_NAMESPACE"        help:"Namespace used to set as default scope in default secret store config." short:"n"`
+	ServiceAccount            string `default:"crossplane"             env:"POD_SERVICE_ACCOUNT"  help:"Name of the Crossplane Service Account."`
+	CRDsPath                  string `default:"/crds"                  env:"CRDS_PATH"            help:"Path of Crossplane core Custom Resource Definitions."`
+	WebhookConfigurationsPath string `default:"/webhookconfigurations" env:"WEBHOOK_CONFIGS_PATH" help:"Path of Crossplane core Webhook Configurations."`
 
 	WebhookEnabled          bool   `default:"true"                   env:"WEBHOOK_ENABLED"                                                                         help:"Enable webhook configuration."`
 	WebhookServiceName      string `env:"WEBHOOK_SERVICE_NAME"       help:"The name of the Service object that the webhook service will be run."`
@@ -123,6 +126,7 @@ func (c *initCommand) Run(s *runtime.Scheme, log logging.Logger) error {
 	steps = append(steps, initializer.NewLockObject(),
 		initializer.NewPackageInstaller(c.Providers, c.Configurations, c.Functions),
 		initializer.StepFunc(initializer.DefaultDeploymentRuntimeConfig),
+		initializer.DefaultManagedResourceActivationPolicy(c.Activations...),
 	)
 
 	if err := initializer.New(cl, log, steps...).Init(context.TODO()); err != nil {
diff --git a/internal/controller/apiextensions/managed/reconciler.go b/internal/controller/apiextensions/managed/reconciler.go
index 984ec300f..2beddd383 100644
--- a/internal/controller/apiextensions/managed/reconciler.go
+++ b/internal/controller/apiextensions/managed/reconciler.go
@@ -19,6 +19,7 @@ package managed
 
 import (
 	"context"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"time"
 
 	extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -159,6 +160,13 @@ func (r *Reconciler) reconcileCustomResourceDefinition(ctx context.Context, log
 		return nil, errors.New("crd was deleted")
 	}
 
+	// Own the CRD.
+	meta.AddOwnerReference(want, meta.AsOwner(meta.TypedReferenceTo(mrd, v2alpha1.ManagedResourceDefinitionGroupVersionKind)))
+	// But also propagate our controller as the controller of the CRD.
+	if owner := metav1.GetControllerOf(mrd); owner != nil {
+		meta.AddOwnerReference(want, *owner)
+	}
+
 	// Stage changes.
 	if err := resources.MergeCustomResourceDefinitionInto(mrd, want); err != nil {
 		return nil, errors.Wrap(err, "cannot merge CustomResourceDefinition")
diff --git a/internal/controller/pkg/revision/reconciler.go b/internal/controller/pkg/revision/reconciler.go
index bd3e9958d..2a84a7824 100644
--- a/internal/controller/pkg/revision/reconciler.go
+++ b/internal/controller/pkg/revision/reconciler.go
@@ -48,6 +48,7 @@ import (
 	v1 "github.com/crossplane/crossplane/apis/pkg/v1"
 	"github.com/crossplane/crossplane/apis/pkg/v1beta1"
 	"github.com/crossplane/crossplane/internal/controller/pkg/controller"
+	"github.com/crossplane/crossplane/internal/converter"
 	"github.com/crossplane/crossplane/internal/dag"
 	"github.com/crossplane/crossplane/internal/features"
 	"github.com/crossplane/crossplane/internal/version"
@@ -109,6 +110,7 @@ const (
 	reasonParse        event.Reason = "ParsePackage"
 	reasonLint         event.Reason = "LintPackage"
 	reasonDependencies event.Reason = "ResolveDependencies"
+	reasonConvertCRD   event.Reason = "ConvertCRDToMRD"
 	reasonSync         event.Reason = "SyncPackage"
 	reasonDeactivate   event.Reason = "DeactivateRevision"
 	reasonPaused       event.Reason = "ReconciliationPaused"
@@ -874,8 +876,23 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
 		}
 	}
 
+	objects := pkg.GetObjects()
+	// The CustomToManagedResourceConversion feature converts CRDs to MRDs from
+	// a provider package.
+	if _, ok := pkgMeta.(*pkgmetav1.Provider); ok && r.features.Enabled(features.EnableBetaCustomToManagedResourceConversion) {
+		// Convert CRDs to MRDs
+		// If SafeStart is not in capabilities, we default mrd state to Active.
+		activationState := !pkgmetav1.CapabilitiesContainFuzzyMatch(pr.GetCapabilities(), pkgmetav1.ProviderCapabilitySafeStart)
+		if mrdObjs, err := converter.CustomToManagedResourceDefinitions(activationState, objects...); err != nil {
+			log.Debug("failed to convert CRDs to MRDs for provider, skipping conversion", "error", err)
+			r.record.Event(pr, event.Warning(reasonConvertCRD, err))
+		} else {
+			objects = mrdObjs
+		}
+	}
+
 	// Establish control or ownership of objects.
-	refs, err := r.objects.Establish(ctx, pkg.GetObjects(), pr, pr.GetDesiredState() == v1.PackageRevisionActive)
+	refs, err := r.objects.Establish(ctx, objects, pr, pr.GetDesiredState() == v1.PackageRevisionActive)
 	if err != nil {
 		if kerrors.IsConflict(err) {
 			return reconcile.Result{Requeue: true}, nil
diff --git a/internal/controller/rbac/provider/roles/reconciler.go b/internal/controller/rbac/provider/roles/reconciler.go
index 7a2619859..3c525b1b5 100644
--- a/internal/controller/rbac/provider/roles/reconciler.go
+++ b/internal/controller/rbac/provider/roles/reconciler.go
@@ -43,6 +43,7 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/ratelimiter"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 
+	"github.com/crossplane/crossplane/apis/apiextensions/v2alpha1"
 	v1 "github.com/crossplane/crossplane/apis/pkg/v1"
 	"github.com/crossplane/crossplane/internal/controller/rbac/controller"
 )
@@ -330,8 +331,14 @@ func DefinedResources(refs []xpv1.TypedReference) []Resource {
 		// just skip this resource since it can't be a CRD.
 		gv, _ := schema.ParseGroupVersion(ref.APIVersion)
 
-		// We're only concerned with CRDs.
-		if gv.Group != apiextensions.GroupName || ref.Kind != "CustomResourceDefinition" {
+		// We're only concerned with CRDs or MRDs.
+		switch {
+		case gv.Group == apiextensions.GroupName && ref.Kind == "CustomResourceDefinition":
+		// Do the work!
+		case gv.Group == v2alpha1.Group && ref.Kind == v2alpha1.ManagedResourceDefinitionKind:
+		// Do the work!
+		default:
+			// Filter out the non CRD or MRD.
 			continue
 		}
 
diff --git a/internal/controller/rbac/provider/roles/roles.go b/internal/controller/rbac/provider/roles/roles.go
index c092419a5..0d56d26da 100644
--- a/internal/controller/rbac/provider/roles/roles.go
+++ b/internal/controller/rbac/provider/roles/roles.go
@@ -25,6 +25,7 @@ import (
 
 	"github.com/crossplane/crossplane-runtime/pkg/meta"
 
+	pkgmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1"
 	v1 "github.com/crossplane/crossplane/apis/pkg/v1"
 )
 
@@ -154,6 +155,19 @@ func RenderClusterRoles(pr *v1.ProviderRevision, rs []Resource) []rbacv1.Cluster
 		Rules: withVerbs(rules, verbsEdit),
 	}
 
+	var viewRules []rbacv1.PolicyRule
+	if pkgmetav1.CapabilitiesContainFuzzyMatch(pr.GetCapabilities(), pkgmetav1.ProviderCapabilitySafeStart) {
+		viewRules = make([]rbacv1.PolicyRule, len(rules)+1)
+		copy(viewRules, rules)
+		viewRules[len(rules)] = rbacv1.PolicyRule{
+			APIGroups: []string{"apiextensions.k8s.io"},
+			Resources: []string{"customresourcedefinitions"},
+		}
+	} else {
+		// Default to the original rules.
+		viewRules = rules
+	}
+
 	view := &rbacv1.ClusterRole{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: namePrefix + pr.GetName() + nameSuffixView,
@@ -161,7 +175,7 @@ func RenderClusterRoles(pr *v1.ProviderRevision, rs []Resource) []rbacv1.Cluster
 				keyAggregateToView: valTrue,
 			},
 		},
-		Rules: withVerbs(rules, verbsView),
+		Rules: withVerbs(viewRules, verbsView),
 	}
 
 	// The 'system' RBAC role does not aggregate; it is intended to be bound
diff --git a/internal/converter/custom_to_managed.go b/internal/converter/custom_to_managed.go
new file mode 100644
index 000000000..2cafa88f0
--- /dev/null
+++ b/internal/converter/custom_to_managed.go
@@ -0,0 +1,103 @@
+/*
+Copyright 2025 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package converter holds resource converter helpers.
+package converter
+
+import (
+	"encoding/json"
+	"reflect"
+
+	extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
+	"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
+
+	"github.com/crossplane/crossplane/apis/apiextensions/v2alpha1"
+)
+
+// CustomResourceDefinition type metadata.
+//
+//nolint:gochecknoglobals // These should be globals.
+var (
+	customResourceDefinition     = reflect.TypeOf(extv1.CustomResourceDefinition{}).Name()
+	customResourceDefinitionKind = extv1.SchemeGroupVersion.WithKind(customResourceDefinition)
+)
+
+// CustomToManagedResourceDefinitions in place converts and returns any
+// CustomResourceDefinition runtime object into a ManagedResourceDefinition.
+func CustomToManagedResourceDefinitions(defaultActive bool, objects ...runtime.Object) ([]runtime.Object, error) {
+	var errs []error
+
+	extv1.Kind(customResourceDefinition)
+	for i, obj := range objects {
+		if obj.GetObjectKind().GroupVersionKind() == customResourceDefinitionKind {
+			// Don't mutate the original object just in-case it is used elsewhere.
+			obj := obj.DeepCopyObject()
+			// The object has to be either an unstructured.Unstructured object or a CustomResourceDefinition
+			switch o := obj.(type) {
+			// to covert, all we need to worry about is the metadata and spec.state.
+			case *unstructured.Unstructured:
+				mrdObject, err := convertCRDToMRD(defaultActive, o.Object)
+				if err != nil {
+					errs = append(errs, err)
+					continue
+				}
+				o.Object = mrdObject
+				objects[i] = o
+			default:
+				b, err := json.Marshal(o)
+				if err != nil {
+					errs = append(errs, err)
+					continue
+				}
+				u := &unstructured.Unstructured{}
+				if err := json.Unmarshal(b, u); err != nil {
+					errs = append(errs, err)
+					continue
+				}
+				mrdObject, err := convertCRDToMRD(defaultActive, u.Object)
+				if err != nil {
+					errs = append(errs, err)
+					continue
+				}
+				u.Object = mrdObject
+				objects[i] = u
+			}
+		}
+	}
+	return objects, errors.Join(errs...)
+}
+
+func convertCRDToMRD(defaultActive bool, in map[string]any) (map[string]any, error) {
+	paved := fieldpath.Pave(in)
+	if err := paved.SetValue("apiVersion", v2alpha1.SchemeGroupVersion.String()); err != nil {
+		return in, err
+	}
+	if err := paved.SetValue("kind", v2alpha1.ManagedResourceDefinitionKind); err != nil {
+		return in, err
+	}
+	// We don't have to set spec.state directly when Inactive.
+	// We will use the default or existing resource to get this value.
+	if defaultActive {
+		if err := paved.SetValue("spec.state", v2alpha1.ManagedResourceDefinitionActive); err != nil {
+			return in, err
+		}
+	}
+	return paved.UnstructuredContent(), nil
+}
diff --git a/internal/features/features.go b/internal/features/features.go
index 017ea1999..7e27dd61f 100644
--- a/internal/features/features.go
+++ b/internal/features/features.go
@@ -65,4 +65,9 @@ const (
 	// compositions, i.e. watching composed resources and reconciling
 	// compositions immediately when any composed resource is updated.
 	EnableBetaRealtimeCompositions feature.Flag = "EnableBetaRealtimeCompositions"
+
+	// EnableBetaCustomToManagedResourceConversion enables beta support for
+	// custom resource definition to managed resource definition conversion.
+	// Conversion happens at provider install time.
+	EnableBetaCustomToManagedResourceConversion feature.Flag = "EnableBetaCustomToManagedResourceConversion"
 )
diff --git a/internal/initializer/initializer.go b/internal/initializer/initializer.go
index dbea4c611..bd4410de9 100644
--- a/internal/initializer/initializer.go
+++ b/internal/initializer/initializer.go
@@ -42,6 +42,9 @@ type StepFunc func(ctx context.Context, kube client.Client) error
 
 // Run calls the step function.
 func (f StepFunc) Run(ctx context.Context, kube client.Client) error {
+	if f == nil {
+		return nil
+	}
 	return f(ctx, kube)
 }
 
diff --git a/internal/initializer/managed_resource_activation_policy.go b/internal/initializer/managed_resource_activation_policy.go
new file mode 100644
index 000000000..b157a4a9a
--- /dev/null
+++ b/internal/initializer/managed_resource_activation_policy.go
@@ -0,0 +1,50 @@
+/*
+Copyright 2025 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package initializer
+
+import (
+	"context"
+
+	kerrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
+	"github.com/crossplane/crossplane-runtime/pkg/resource"
+
+	"github.com/crossplane/crossplane/apis/apiextensions/v2alpha1"
+)
+
+// DefaultManagedResourceActivationPolicy creates a "default" ManagedResourceActivationPolicy
+// object. It is a no-op if the object already exists.
+func DefaultManagedResourceActivationPolicy(activations ...v2alpha1.ActivationPolicy) StepFunc {
+	if len(activations) == 0 {
+		return nil
+	}
+	return func(ctx context.Context, kube client.Client) error {
+		rc := &v2alpha1.ManagedResourceActivationPolicy{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "default",
+			},
+			Spec: v2alpha1.ManagedResourceActivationPolicySpec{
+				Activations: activations,
+			},
+		}
+
+		return errors.Wrap(resource.Ignore(kerrors.IsAlreadyExists, kube.Create(ctx, rc)), "cannot create ManagedResourceActivationPolicy \"default\"")
+	}
+}



Test patch:
diff --git a/apis/pkg/meta/v1/capabilities_test.go b/apis/pkg/meta/v1/capabilities_test.go
new file mode 100644
index 000000000..b41b129f2
--- /dev/null
+++ b/apis/pkg/meta/v1/capabilities_test.go
@@ -0,0 +1,180 @@
+/*
+Copyright 2025 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestCapabilitiesContainFuzzy(t *testing.T) {
+	type args struct {
+		capabilities []string
+		key          string
+	}
+	type want struct {
+		result bool
+	}
+
+	cases := map[string]struct {
+		reason string
+		args   args
+		want   want
+	}{
+		"ExactMatch": {
+			reason: "Should return true for exact match",
+			args: args{
+				capabilities: []string{"composition", "operation"},
+				key:          "composition",
+			},
+			want: want{
+				result: true,
+			},
+		},
+		"CaseInsensitiveMatch": {
+			reason: "Should return true for case insensitive match",
+			args: args{
+				capabilities: []string{"Composition", "OPERATION"},
+				key:          "composition",
+			},
+			want: want{
+				result: true,
+			},
+		},
+		"HyphenatedMatch": {
+			reason: "Should return true when matching hyphenated capability",
+			args: args{
+				capabilities: []string{"safe-start", "composition"},
+				key:          "safe-start",
+			},
+			want: want{
+				result: true,
+			},
+		},
+		"FuzzyMatchWithSpecialChars": {
+			reason: "Should return true when key has special characters that are stripped",
+			args: args{
+				capabilities: []string{"safestart", "composition"},
+				key:          "safe-start",
+			},
+			want: want{
+				result: true,
+			},
+		},
+		"FuzzyMatchDifferentSeparators": {
+			reason: "Should return true when capability and key use different separators",
+			args: args{
+				capabilities: []string{"safe_start", "composition"},
+				key:          "safe-start",
+			},
+			want: want{
+				result: true,
+			},
+		},
+		"NoMatch": {
+			reason: "Should return false when no capability matches",
+			args: args{
+				capabilities: []string{"composition", "operation"},
+				key:          "nonexistent",
+			},
+			want: want{
+				result: false,
+			},
+		},
+		"EmptyCapabilities": {
+			reason: "Should return false when capabilities list is empty",
+			args: args{
+				capabilities: []string{},
+				key:          "composition",
+			},
+			want: want{
+				result: false,
+			},
+		},
+		"EmptyKey": {
+			reason: "Should return true when key is empty and empty capability exists",
+			args: args{
+				capabilities: []string{"", "composition"},
+				key:          "",
+			},
+			want: want{
+				result: true,
+			},
+		},
+		"EmptyKeyNoMatch": {
+			reason: "Should return false when key is empty and no empty capability exists",
+			args: args{
+				capabilities: []string{"composition", "operation"},
+				key:          "",
+			},
+			want: want{
+				result: false,
+			},
+		},
+		"SpecialCharactersOnly": {
+			reason: "Should return true when both capability and key reduce to empty after stripping special chars",
+			args: args{
+				capabilities: []string{"---", "composition"},
+				key:          "___",
+			},
+			want: want{
+				result: true,
+			},
+		},
+		"NumbersInCapability": {
+			reason: "Should return true when capability contains numbers",
+			args: args{
+				capabilities: []string{"v1-composition", "operation"},
+				key:          "v1composition",
+			},
+			want: want{
+				result: true,
+			},
+		},
+		"UnicodeCharacters": {
+			reason: "Should return true when matching with unicode characters stripped",
+			args: args{
+				capabilities: []string{"safe→start", "composition"},
+				key:          "safestart",
+			},
+			want: want{
+				result: true,
+			},
+		},
+		"MixedCaseAndSpecialChars": {
+			reason: "Should return true when matching mixed case with special characters",
+			args: args{
+				capabilities: []string{"Safe-Start", "Composition"},
+				key:          "SAFE_START",
+			},
+			want: want{
+				result: true,
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			got := CapabilitiesContainFuzzyMatch(tc.args.capabilities, tc.args.key)
+
+			if diff := cmp.Diff(tc.want.result, got); diff != "" {
+				t.Errorf("\n%s\nCapabilitiesContainFuzzy(...): -want, +got:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
diff --git a/internal/controller/rbac/provider/roles/roles_test.go b/internal/controller/rbac/provider/roles/roles_test.go
index bc8386447..98519487a 100644
--- a/internal/controller/rbac/provider/roles/roles_test.go
+++ b/internal/controller/rbac/provider/roles/roles_test.go
@@ -24,6 +24,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 
+	pkgmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1"
 	v1 "github.com/crossplane/crossplane/apis/pkg/v1"
 )
 
@@ -160,6 +161,85 @@ func TestRenderClusterRoles(t *testing.T) {
 				},
 			},
 		},
+		"SafeStart": {
+			reason: "A ProviderRevision with safe-start capability should add CRD permissions to view role.",
+			args: args{
+				pr: &v1.ProviderRevision{
+					ObjectMeta: metav1.ObjectMeta{Name: prName, UID: prUID},
+					Status: v1.ProviderRevisionStatus{
+						PackageRevisionStatus: v1.PackageRevisionStatus{
+							Capabilities: []string{pkgmetav1.ProviderCapabilitySafeStart},
+						},
+					},
+				},
+				resources: []Resource{
+					{
+						Group:  groupA,
+						Plural: pluralA,
+					},
+				},
+			},
+			want: []rbacv1.ClusterRole{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:            nameEdit,
+						OwnerReferences: []metav1.OwnerReference{crCtrlr},
+						Labels: map[string]string{
+							keyAggregateToCrossplane: valTrue,
+							keyAggregateToAdmin:      valTrue,
+							keyAggregateToEdit:       valTrue,
+						},
+					},
+					Rules: []rbacv1.PolicyRule{
+						{
+							APIGroups: []string{groupA},
+							Resources: []string{pluralA, pluralA + suffixStatus},
+							Verbs:     verbsEdit,
+						},
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:            nameView,
+						OwnerReferences: []metav1.OwnerReference{crCtrlr},
+						Labels: map[string]string{
+							keyAggregateToView: valTrue,
+						},
+					},
+					Rules: []rbacv1.PolicyRule{
+						{
+							APIGroups: []string{groupA},
+							Resources: []string{pluralA, pluralA + suffixStatus},
+							Verbs:     verbsView,
+						},
+						{
+							APIGroups: []string{"apiextensions.k8s.io"},
+							Resources: []string{"customresourcedefinitions"},
+							Verbs:     verbsView,
+						},
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:            nameSystem,
+						Labels:          map[string]string{keyProviderName: prName},
+						OwnerReferences: []metav1.OwnerReference{crCtrlr},
+					},
+					Rules: append([]rbacv1.PolicyRule{
+						{
+							APIGroups: []string{groupA},
+							Resources: []string{pluralA, pluralA + suffixStatus},
+							Verbs:     verbsSystem,
+						},
+						{
+							APIGroups: []string{groupA},
+							Resources: []string{rbacv1.ResourceAll + suffixFinalizers},
+							Verbs:     verbsUpdate,
+						},
+					}, rulesSystemExtra...),
+				},
+			},
+		},
 	}
 
 	for name, tc := range cases {
diff --git a/internal/converter/custom_to_managed_test.go b/internal/converter/custom_to_managed_test.go
new file mode 100644
index 000000000..b1961b6f4
--- /dev/null
+++ b/internal/converter/custom_to_managed_test.go
@@ -0,0 +1,691 @@
+/*
+Copyright 2025 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package converter
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
+	"github.com/crossplane/crossplane-runtime/pkg/test"
+
+	"github.com/crossplane/crossplane/apis/apiextensions/v2alpha1"
+)
+
+// invalidJSONObject is a runtime.Object that fails to marshal to JSON.
+type invalidJSONObject struct {
+	metav1.TypeMeta
+	metav1.ObjectMeta
+	Channel chan struct{} `json:"channel"` // channels cannot be marshaled to JSON
+}
+
+func (i *invalidJSONObject) DeepCopyObject() runtime.Object {
+	return &invalidJSONObject{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: customResourceDefinitionKind.GroupVersion().String(),
+			Kind:       customResourceDefinitionKind.Kind,
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "invalid-json-object",
+		},
+		Channel: make(chan struct{}),
+	}
+}
+
+func (i *invalidJSONObject) GetObjectKind() schema.ObjectKind {
+	return i
+}
+
+func (i *invalidJSONObject) GroupVersionKind() schema.GroupVersionKind {
+	return customResourceDefinitionKind
+}
+
+func (i *invalidJSONObject) SetGroupVersionKind(gvk schema.GroupVersionKind) {
+	i.APIVersion = gvk.GroupVersion().String()
+	i.Kind = gvk.Kind
+}
+
+func TestCustomToManagedResourceDefinitions(t *testing.T) {
+	// Test CustomResourceDefinition
+	testCRD := &extv1.CustomResourceDefinition{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: extv1.SchemeGroupVersion.String(),
+			Kind:       customResourceDefinition,
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "buckets.example.org",
+			Namespace: "test-namespace",
+		},
+		Spec: extv1.CustomResourceDefinitionSpec{
+			Group: "example.org",
+			Names: extv1.CustomResourceDefinitionNames{
+				Kind:     "Bucket",
+				Plural:   "buckets",
+				Singular: "bucket",
+			},
+			Scope: extv1.NamespaceScoped,
+			Versions: []extv1.CustomResourceDefinitionVersion{
+				{
+					Name:    "v1",
+					Served:  true,
+					Storage: true,
+				},
+			},
+		},
+	}
+
+	// Test unstructured CRD
+	testUnstructuredCRD := &unstructured.Unstructured{
+		Object: map[string]any{
+			"apiVersion": extv1.SchemeGroupVersion.String(),
+			"kind":       customResourceDefinition,
+			"metadata": map[string]any{
+				"name":      "instances.example.org",
+				"namespace": "test-namespace",
+			},
+			"spec": map[string]any{
+				"group": "example.org",
+				"names": map[string]any{
+					"kind":     "Instance",
+					"plural":   "instances",
+					"singular": "instance",
+				},
+				"scope": "Namespaced",
+				"versions": []any{
+					map[string]any{
+						"name":    "v1",
+						"served":  true,
+						"storage": true,
+					},
+				},
+			},
+		},
+	}
+
+	// Test non-CRD object
+	testNonCRD := &unstructured.Unstructured{
+		Object: map[string]any{
+			"apiVersion": "apps/v1",
+			"kind":       "Deployment",
+			"metadata": map[string]any{
+				"name":      "test-deployment",
+				"namespace": "test-namespace",
+			},
+		},
+	}
+
+	// Expected MRD from structured CRD (inactive)
+	expectedMRDFromCRDInactive := &unstructured.Unstructured{
+		Object: map[string]any{
+			"apiVersion": v2alpha1.SchemeGroupVersion.String(),
+			"kind":       v2alpha1.ManagedResourceDefinitionKind,
+			"metadata": map[string]any{
+				"creationTimestamp": nil,
+				"name":              "buckets.example.org",
+				"namespace":         "test-namespace",
+			},
+			"spec": map[string]any{
+				"group": "example.org",
+				"names": map[string]any{
+					"kind":     "Bucket",
+					"plural":   "buckets",
+					"singular": "bucket",
+				},
+				"scope": "Namespaced",
+				"versions": []any{
+					map[string]any{
+						"name":    "v1",
+						"served":  true,
+						"storage": true,
+					},
+				},
+			},
+			"status": map[string]any{
+				"acceptedNames": map[string]any{
+					"kind":   "",
+					"plural": "",
+				},
+				"conditions":     nil,
+				"storedVersions": nil,
+			},
+		},
+	}
+
+	// Expected MRD from structured CRD (active)
+	expectedMRDFromCRDActive := &unstructured.Unstructured{
+		Object: map[string]any{
+			"apiVersion": v2alpha1.SchemeGroupVersion.String(),
+			"kind":       v2alpha1.ManagedResourceDefinitionKind,
+			"metadata": map[string]any{
+				"creationTimestamp": nil,
+				"name":              "buckets.example.org",
+				"namespace":         "test-namespace",
+			},
+			"spec": map[string]any{
+				"group": "example.org",
+				"names": map[string]any{
+					"kind":     "Bucket",
+					"plural":   "buckets",
+					"singular": "bucket",
+				},
+				"scope": "Namespaced",
+				"state": string(v2alpha1.ManagedResourceDefinitionActive),
+				"versions": []any{
+					map[string]any{
+						"name":    "v1",
+						"served":  true,
+						"storage": true,
+					},
+				},
+			},
+			"status": map[string]any{
+				"acceptedNames": map[string]any{
+					"kind":   "",
+					"plural": "",
+				},
+				"conditions":     nil,
+				"storedVersions": nil,
+			},
+		},
+	}
+
+	// Expected MRD from unstructured CRD (inactive)
+	expectedMRDFromUnstructuredInactive := &unstructured.Unstructured{
+		Object: map[string]any{
+			"apiVersion": v2alpha1.SchemeGroupVersion.String(),
+			"kind":       v2alpha1.ManagedResourceDefinitionKind,
+			"metadata": map[string]any{
+				"name":      "instances.example.org",
+				"namespace": "test-namespace",
+			},
+			"spec": map[string]any{
+				"group": "example.org",
+				"names": map[string]any{
+					"kind":     "Instance",
+					"plural":   "instances",
+					"singular": "instance",
+				},
+				"scope": "Namespaced",
+				"versions": []any{
+					map[string]any{
+						"name":    "v1",
+						"served":  true,
+						"storage": true,
+					},
+				},
+			},
+		},
+	}
+
+	// Expected MRD from unstructured CRD (active)
+	expectedMRDFromUnstructuredActive := &unstructured.Unstructured{
+		Object: map[string]any{
+			"apiVersion": v2alpha1.SchemeGroupVersion.String(),
+			"kind":       v2alpha1.ManagedResourceDefinitionKind,
+			"metadata": map[string]any{
+				"name":      "instances.example.org",
+				"namespace": "test-namespace",
+			},
+			"spec": map[string]any{
+				"group": "example.org",
+				"names": map[string]any{
+					"kind":     "Instance",
+					"plural":   "instances",
+					"singular": "instance",
+				},
+				"scope": "Namespaced",
+				"state": string(v2alpha1.ManagedResourceDefinitionActive),
+				"versions": []any{
+					map[string]any{
+						"name":    "v1",
+						"served":  true,
+						"storage": true,
+					},
+				},
+			},
+		},
+	}
+
+	type args struct {
+		defaultActive bool
+		objects       []runtime.Object
+	}
+	type want struct {
+		objects []runtime.Object
+		err     error
+	}
+
+	cases := map[string]struct {
+		reason string
+		args   args
+		want   want
+	}{
+		"EmptyInput": {
+			reason: "Should return empty slice for empty input",
+			args: args{
+				defaultActive: false,
+				objects:       []runtime.Object{},
+			},
+			want: want{
+				objects: []runtime.Object{},
+			},
+		},
+		"NonCRDObject": {
+			reason: "Should leave non-CRD objects unchanged",
+			args: args{
+				defaultActive: false,
+				objects:       []runtime.Object{testNonCRD},
+			},
+			want: want{
+				objects: []runtime.Object{testNonCRD},
+			},
+		},
+		"SingleStructuredCRDInactive": {
+			reason: "Should convert single structured CRD to MRD (inactive)",
+			args: args{
+				defaultActive: false,
+				objects:       []runtime.Object{testCRD},
+			},
+			want: want{
+				objects: []runtime.Object{expectedMRDFromCRDInactive},
+			},
+		},
+		"SingleStructuredCRDActive": {
+			reason: "Should convert single structured CRD to MRD (active)",
+			args: args{
+				defaultActive: true,
+				objects:       []runtime.Object{testCRD},
+			},
+			want: want{
+				objects: []runtime.Object{expectedMRDFromCRDActive},
+			},
+		},
+		"SingleUnstructuredCRDInactive": {
+			reason: "Should convert single unstructured CRD to MRD (inactive)",
+			args: args{
+				defaultActive: false,
+				objects:       []runtime.Object{testUnstructuredCRD},
+			},
+			want: want{
+				objects: []runtime.Object{expectedMRDFromUnstructuredInactive},
+			},
+		},
+		"SingleUnstructuredCRDActive": {
+			reason: "Should convert single unstructured CRD to MRD (active)",
+			args: args{
+				defaultActive: true,
+				objects:       []runtime.Object{testUnstructuredCRD},
+			},
+			want: want{
+				objects: []runtime.Object{expectedMRDFromUnstructuredActive},
+			},
+		},
+		"MixedObjectsInactive": {
+			reason: "Should convert only CRD objects, leaving others unchanged (inactive)",
+			args: args{
+				defaultActive: false,
+				objects:       []runtime.Object{testCRD, testNonCRD, testUnstructuredCRD},
+			},
+			want: want{
+				objects: []runtime.Object{expectedMRDFromCRDInactive, testNonCRD, expectedMRDFromUnstructuredInactive},
+			},
+		},
+		"MixedObjectsActive": {
+			reason: "Should convert only CRD objects, leaving others unchanged (active)",
+			args: args{
+				defaultActive: true,
+				objects:       []runtime.Object{testCRD, testNonCRD, testUnstructuredCRD},
+			},
+			want: want{
+				objects: []runtime.Object{expectedMRDFromCRDActive, testNonCRD, expectedMRDFromUnstructuredActive},
+			},
+		},
+		"MultipleCRDsInactive": {
+			reason: "Should convert multiple CRDs to MRDs (inactive)",
+			args: args{
+				defaultActive: false,
+				objects:       []runtime.Object{testCRD, testUnstructuredCRD},
+			},
+			want: want{
+				objects: []runtime.Object{expectedMRDFromCRDInactive, expectedMRDFromUnstructuredInactive},
+			},
+		},
+		"MultipleCRDsActive": {
+			reason: "Should convert multiple CRDs to MRDs (active)",
+			args: args{
+				defaultActive: true,
+				objects:       []runtime.Object{testCRD, testUnstructuredCRD},
+			},
+			want: want{
+				objects: []runtime.Object{expectedMRDFromCRDActive, expectedMRDFromUnstructuredActive},
+			},
+		},
+		"InvalidJSON": {
+			reason: "Should return error when JSON marshaling fails",
+			args: args{
+				defaultActive: false,
+				objects:       []runtime.Object{&invalidJSONObject{}},
+			},
+			want: want{
+				objects: []runtime.Object{&invalidJSONObject{}},
+				// We expect an error but don't care about the exact type since it's wrapped
+				err: errors.New("some error"),
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			gotObjects, err := CustomToManagedResourceDefinitions(tc.args.defaultActive, tc.args.objects...)
+
+			if tc.want.err != nil {
+				// Just check that we got an error when we expected one
+				if err == nil {
+					t.Errorf("\n%s\nCustomToManagedResourceDefinitions(...): expected error but got none", tc.reason)
+				}
+			} else {
+				if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+					t.Errorf("\n%s\nCustomToManagedResourceDefinitions(...): -want error, +got error:\n%s", tc.reason, diff)
+				}
+			}
+			if diff := cmp.Diff(tc.want.objects, gotObjects); diff != "" {
+				t.Errorf("\n%s\nCustomToManagedResourceDefinitions(...): -want objects, +got objects:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
+
+func TestConvertCRDToMRD(t *testing.T) {
+	type args struct {
+		defaultActive bool
+		in            map[string]any
+	}
+	type want struct {
+		out map[string]any
+		err error
+	}
+
+	cases := map[string]struct {
+		reason string
+		args   args
+		want   want
+	}{
+		"BasicCRDInactive": {
+			reason: "Should convert basic CRD to MRD format (inactive)",
+			args: args{
+				defaultActive: false,
+				in: map[string]any{
+					"apiVersion": extv1.SchemeGroupVersion.String(),
+					"kind":       customResourceDefinition,
+					"metadata": map[string]any{
+						"name": "buckets.example.org",
+					},
+					"spec": map[string]any{
+						"group": "example.org",
+						"names": map[string]any{
+							"kind":   "Bucket",
+							"plural": "buckets",
+						},
+					},
+				},
+			},
+			want: want{
+				out: map[string]any{
+					"apiVersion": v2alpha1.SchemeGroupVersion.String(),
+					"kind":       v2alpha1.ManagedResourceDefinitionKind,
+					"metadata": map[string]any{
+						"name": "buckets.example.org",
+					},
+					"spec": map[string]any{
+						"group": "example.org",
+						"names": map[string]any{
+							"kind":   "Bucket",
+							"plural": "buckets",
+						},
+					},
+				},
+			},
+		},
+		"BasicCRDActive": {
+			reason: "Should convert basic CRD to MRD format (active)",
+			args: args{
+				defaultActive: true,
+				in: map[string]any{
+					"apiVersion": extv1.SchemeGroupVersion.String(),
+					"kind":       customResourceDefinition,
+					"metadata": map[string]any{
+						"name": "buckets.example.org",
+					},
+					"spec": map[string]any{
+						"group": "example.org",
+						"names": map[string]any{
+							"kind":   "Bucket",
+							"plural": "buckets",
+						},
+					},
+				},
+			},
+			want: want{
+				out: map[string]any{
+					"apiVersion": v2alpha1.SchemeGroupVersion.String(),
+					"kind":       v2alpha1.ManagedResourceDefinitionKind,
+					"metadata": map[string]any{
+						"name": "buckets.example.org",
+					},
+					"spec": map[string]any{
+						"group": "example.org",
+						"names": map[string]any{
+							"kind":   "Bucket",
+							"plural": "buckets",
+						},
+						"state": string(v2alpha1.ManagedResourceDefinitionActive),
+					},
+				},
+			},
+		},
+		"CRDWithComplexSpecActive": {
+			reason: "Should convert CRD with complex spec to MRD, preserving all fields (active)",
+			args: args{
+				defaultActive: true,
+				in: map[string]any{
+					"apiVersion": extv1.SchemeGroupVersion.String(),
+					"kind":       customResourceDefinition,
+					"metadata": map[string]any{
+						"name":      "databases.example.org",
+						"namespace": "test-namespace",
+						"labels": map[string]any{
+							"app": "test",
+						},
+					},
+					"spec": map[string]any{
+						"group": "example.org",
+						"names": map[string]any{
+							"kind":     "Database",
+							"plural":   "databases",
+							"singular": "database",
+						},
+						"scope": "Namespaced",
+						"versions": []any{
+							map[string]any{
+								"name":    "v1",
+								"served":  true,
+								"storage": true,
+								"schema": map[string]any{
+									"openAPIV3Schema": map[string]any{
+										"type": "object",
+										"properties": map[string]any{
+											"spec": map[string]any{
+												"type": "object",
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				out: map[string]any{
+					"apiVersion": v2alpha1.SchemeGroupVersion.String(),
+					"kind":       v2alpha1.ManagedResourceDefinitionKind,
+					"metadata": map[string]any{
+						"name":      "databases.example.org",
+						"namespace": "test-namespace",
+						"labels": map[string]any{
+							"app": "test",
+						},
+					},
+					"spec": map[string]any{
+						"group": "example.org",
+						"names": map[string]any{
+							"kind":     "Database",
+							"plural":   "databases",
+							"singular": "database",
+						},
+						"scope": "Namespaced",
+						"state": string(v2alpha1.ManagedResourceDefinitionActive),
+						"versions": []any{
+							map[string]any{
+								"name":    "v1",
+								"served":  true,
+								"storage": true,
+								"schema": map[string]any{
+									"openAPIV3Schema": map[string]any{
+										"type": "object",
+										"properties": map[string]any{
+											"spec": map[string]any{
+												"type": "object",
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		"MinimalCRDInactive": {
+			reason: "Should convert minimal CRD to MRD (inactive)",
+			args: args{
+				defaultActive: false,
+				in: map[string]any{
+					"apiVersion": extv1.SchemeGroupVersion.String(),
+					"kind":       customResourceDefinition,
+					"metadata": map[string]any{
+						"name": "minimal.example.org",
+					},
+					"spec": map[string]any{},
+				},
+			},
+			want: want{
+				out: map[string]any{
+					"apiVersion": v2alpha1.SchemeGroupVersion.String(),
+					"kind":       v2alpha1.ManagedResourceDefinitionKind,
+					"metadata": map[string]any{
+						"name": "minimal.example.org",
+					},
+					"spec": map[string]any{},
+				},
+			},
+		},
+		"MinimalCRDActive": {
+			reason: "Should convert minimal CRD to MRD (active)",
+			args: args{
+				defaultActive: true,
+				in: map[string]any{
+					"apiVersion": extv1.SchemeGroupVersion.String(),
+					"kind":       customResourceDefinition,
+					"metadata": map[string]any{
+						"name": "minimal.example.org",
+					},
+					"spec": map[string]any{},
+				},
+			},
+			want: want{
+				out: map[string]any{
+					"apiVersion": v2alpha1.SchemeGroupVersion.String(),
+					"kind":       v2alpha1.ManagedResourceDefinitionKind,
+					"metadata": map[string]any{
+						"name": "minimal.example.org",
+					},
+					"spec": map[string]any{
+						"state": string(v2alpha1.ManagedResourceDefinitionActive),
+					},
+				},
+			},
+		},
+		"InvalidInputStructure": {
+			reason: "Should handle invalid input structure gracefully",
+			args: args{
+				defaultActive: false,
+				in: map[string]any{
+					"apiVersion": 123, // invalid type
+					"kind":       customResourceDefinition,
+					"metadata": map[string]any{
+						"name": "test.example.org",
+					},
+				},
+			},
+			want: want{
+				out: map[string]any{
+					"apiVersion": v2alpha1.SchemeGroupVersion.String(),
+					"kind":       v2alpha1.ManagedResourceDefinitionKind,
+					"metadata": map[string]any{
+						"name": "test.example.org",
+					},
+				},
+			},
+		},
+		"EmptyInputActive": {
+			reason: "Should handle empty input gracefully (active)",
+			args: args{
+				defaultActive: true,
+				in:            map[string]any{},
+			},
+			want: want{
+				out: map[string]any{
+					"apiVersion": v2alpha1.SchemeGroupVersion.String(),
+					"kind":       v2alpha1.ManagedResourceDefinitionKind,
+					"spec": map[string]any{
+						"state": string(v2alpha1.ManagedResourceDefinitionActive),
+					},
+				},
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			got, err := convertCRDToMRD(tc.args.defaultActive, tc.args.in)
+
+			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+				t.Errorf("\n%s\nconvertCRDToMRD(...): -want error, +got error:\n%s", tc.reason, diff)
+			}
+			if diff := cmp.Diff(tc.want.out, got); diff != "" {
+				t.Errorf("\n%s\nconvertCRDToMRD(...): -want, +got:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
diff --git a/internal/initializer/managed_resource_activation_policy_test.go b/internal/initializer/managed_resource_activation_policy_test.go
new file mode 100644
index 000000000..52caa9fce
--- /dev/null
+++ b/internal/initializer/managed_resource_activation_policy_test.go
@@ -0,0 +1,122 @@
+/*
+Copyright 2021 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package initializer
+
+import (
+	"context"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	kerrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
+	"github.com/crossplane/crossplane-runtime/pkg/test"
+
+	"github.com/crossplane/crossplane/apis/apiextensions/v2alpha1"
+)
+
+func TestDefaultManagedResourceActivationPolicy(t *testing.T) {
+	type args struct {
+		activations []v2alpha1.ActivationPolicy
+		kube        client.Client
+	}
+
+	type want struct {
+		err   error
+		nilFn bool
+	}
+
+	cases := map[string]struct {
+		args
+		want
+	}{
+		"NoActivation": {
+			args: args{},
+			want: want{
+				nilFn: true,
+			},
+		},
+		"FailedToCreate": {
+			args: args{
+				activations: []v2alpha1.ActivationPolicy{"*"},
+				kube: &test.MockClient{
+					MockCreate: func(_ context.Context, obj client.Object, _ ...client.CreateOption) error {
+						mrap, ok := obj.(*v2alpha1.ManagedResourceActivationPolicy)
+						if !ok {
+							t.Errorf("Expected ManagedResourceActivationPolicy, got %T", obj)
+							return nil
+						}
+						expectedActivations := []v2alpha1.ActivationPolicy{"*"}
+						if diff := cmp.Diff(expectedActivations, mrap.Spec.Activations); diff != "" {
+							t.Errorf("Activations mismatch (-want +got):\n%s", diff)
+						}
+						return errBoom
+					},
+				},
+			},
+			want: want{
+				err: errors.Wrap(errBoom, "cannot create ManagedResourceActivationPolicy \"default\""),
+			},
+		},
+		"SuccessCreated": {
+			args: args{
+				activations: []v2alpha1.ActivationPolicy{"policy1", "policy2", "*.example.com"},
+				kube: &test.MockClient{
+					MockCreate: func(_ context.Context, obj client.Object, _ ...client.CreateOption) error {
+						mrap, ok := obj.(*v2alpha1.ManagedResourceActivationPolicy)
+						if !ok {
+							t.Errorf("Expected ManagedResourceActivationPolicy, got %T", obj)
+							return nil
+						}
+						expectedActivations := []v2alpha1.ActivationPolicy{"policy1", "policy2", "*.example.com"}
+						if diff := cmp.Diff(expectedActivations, mrap.Spec.Activations); diff != "" {
+							t.Errorf("Activations mismatch (-want +got):\n%s", diff)
+						}
+						return nil
+					},
+				},
+			},
+		},
+		"SuccessAlreadyExists": {
+			args: args{
+				activations: []v2alpha1.ActivationPolicy{"*"},
+				kube: &test.MockClient{
+					MockCreate: func(_ context.Context, _ client.Object, _ ...client.CreateOption) error {
+						return kerrors.NewAlreadyExists(schema.GroupResource{}, "default")
+					},
+				},
+			},
+		},
+	}
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			fn := DefaultManagedResourceActivationPolicy(tc.activations...)
+			if fn == nil {
+				if !tc.nilFn {
+					t.Errorf("\n%s\nUnexpected nil function", name)
+				}
+				return
+			}
+			err := fn(context.TODO(), tc.kube)
+			if diff := cmp.Diff(tc.err, err, test.EquateErrors()); diff != "" {
+				t.Errorf("\n%s\nRun(...): -want err, +got err:\n%s", name, diff)
+			}
+		})
+	}
+}
diff --git a/test/e2e/apiextensions_activation_policy_test.go b/test/e2e/apiextensions_activation_policy_test.go
index 0ceb59457..0d3b244ec 100644
--- a/test/e2e/apiextensions_activation_policy_test.go
+++ b/test/e2e/apiextensions_activation_policy_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package e2e
 
 import (
+	"sigs.k8s.io/e2e-framework/third_party/helm"
 	"testing"
 	"time"
 
@@ -27,14 +28,31 @@ import (
 	"github.com/crossplane/crossplane/test/e2e/funcs"
 )
 
+// LabelAreaMRAP is applied to all features pertaining to MRAP testing.
+const LabelAreaMRAP = "mrap"
+
+// Tests that should be part of the test suite for the MRAP feature.
+const SuiteMRAP = "mrap"
+
+func init() {
+	environment.AddTestSuite(SuiteMRAP,
+		config.WithHelmInstallOpts(
+			helm.WithArgs("--set provider.defaultActivations=null"),
+		),
+		config.WithLabelsToSelect(features.Labels{
+			config.LabelTestSuite: []string{SuiteMRAP, config.TestSuiteDefault},
+		}),
+	)
+}
+
 func TestMRAPActivatesSingleMRD(t *testing.T) {
 	manifests := "test/e2e/manifests/apiextensions/activation-policy/single-activation"
 
 	environment.Test(t,
 		features.NewWithDescription(t.Name(),
 			"Tests that ManagedResourceActivationPolicy can activate a single ManagedResourceDefinition.").
-			WithLabel(LabelArea, LabelAreaAPIExtensions).
-			WithLabel(LabelStage, LabelStageAlpha).
+			WithLabel(LabelArea, LabelAreaMRAP).
+			WithLabel(LabelStage, LabelStageBeta).
 			WithLabel(LabelSize, LabelSizeSmall).
 			WithLabel(config.LabelTestSuite, config.TestSuiteDefault).
 
@@ -97,8 +115,8 @@ func TestMRAPWildcardActivation(t *testing.T) {
 	environment.Test(t,
 		features.NewWithDescription(t.Name(),
 			"Tests that ManagedResourceActivationPolicy can activate multiple ManagedResourceDefinitions using wildcard patterns.").
-			WithLabel(LabelArea, LabelAreaAPIExtensions).
-			WithLabel(LabelStage, LabelStageAlpha).
+			WithLabel(LabelArea, LabelAreaMRAP).
+			WithLabel(LabelStage, LabelStageBeta).
 			WithLabel(LabelSize, LabelSizeSmall).
 			WithLabel(config.LabelTestSuite, config.TestSuiteDefault).
 
@@ -176,8 +194,8 @@ func TestMultipleMRAPsOneMRD(t *testing.T) {
 	environment.Test(t,
 		features.NewWithDescription(t.Name(),
 			"Tests that multiple ManagedResourceActivationPolicies can manage the same ManagedResourceDefinition.").
-			WithLabel(LabelArea, LabelAreaAPIExtensions).
-			WithLabel(LabelStage, LabelStageAlpha).
+			WithLabel(LabelArea, LabelAreaMRAP).
+			WithLabel(LabelStage, LabelStageBeta).
 			WithLabel(LabelSize, LabelSizeSmall).
 			WithLabel(config.LabelTestSuite, config.TestSuiteDefault).
 
diff --git a/test/e2e/apiextensions_mrds_test.go b/test/e2e/apiextensions_mrds_test.go
index 54ba72389..b4ebf12c4 100644
--- a/test/e2e/apiextensions_mrds_test.go
+++ b/test/e2e/apiextensions_mrds_test.go
@@ -78,8 +78,8 @@ func TestMRDValidation(t *testing.T) {
 	}
 	environment.Test(t,
 		cases.Build(t.Name()).
-			WithLabel(LabelStage, LabelStageAlpha).
-			WithLabel(LabelArea, LabelAreaAPIExtensions).
+			WithLabel(LabelArea, LabelAreaMRAP).
+			WithLabel(LabelStage, LabelStageBeta).
 			WithLabel(LabelSize, LabelSizeSmall).
 			WithLabel(config.LabelTestSuite, config.TestSuiteDefault).
 			WithTeardown("DeleteValidMRD", funcs.AllOf(
diff --git a/test/e2e/funcs/env.go b/test/e2e/funcs/env.go
index 9c92c0c5f..621ea384a 100644
--- a/test/e2e/funcs/env.go
+++ b/test/e2e/funcs/env.go
@@ -37,6 +37,7 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/errors"
 
 	apiextensionsv1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
+	apiextensionsv2alpha1 "github.com/crossplane/crossplane/apis/apiextensions/v2alpha1"
 	opsv1alpha1 "github.com/crossplane/crossplane/apis/ops/v1alpha1"
 	pkgv1 "github.com/crossplane/crossplane/apis/pkg/v1"
 )
@@ -97,6 +98,7 @@ func AddCrossplaneTypesToScheme() env.Func {
 	return func(ctx context.Context, c *envconf.Config) (context.Context, error) {
 		_ = apiextensionsv1.AddToScheme(c.Client().Resources().GetScheme())
 		_ = opsv1alpha1.AddToScheme(c.Client().Resources().GetScheme())
+		_ = apiextensionsv2alpha1.AddToScheme(c.Client().Resources().GetScheme())
 		_ = pkgv1.AddToScheme(c.Client().Resources().GetScheme())
 
 		return ctx, nil
diff --git a/test/e2e/install_test.go b/test/e2e/install_test.go
index f4f80d2e3..d9dbcaa51 100644
--- a/test/e2e/install_test.go
+++ b/test/e2e/install_test.go
@@ -18,6 +18,7 @@ package e2e
 
 import (
 	"context"
+	"strings"
 	"testing"
 	"time"
 
@@ -156,14 +157,18 @@ func TestCrossplaneLifecycle(t *testing.T) {
 			//  Tracking issue https://github.com/crossplane/crossplane/issues/6506
 			Assess("DeleteProviderNopDeployment", func(ctx context.Context, t *testing.T, e *envconf.Config) context.Context {
 				t.Helper()
-				err := e.Client().Resources("crossplane-system").Delete(ctx, &appsv1.Deployment{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      "provider-nop-lifecycle-upgrade-37f3300ebfa7",
-						Namespace: "crossplane-system",
-					},
-				})
+				ds := &appsv1.DeploymentList{}
+				err := e.Client().Resources("crossplane-system").List(ctx, ds)
 				if client.IgnoreNotFound(err) != nil {
-					t.Errorf("Failed to delete provider-nop-lifecycle-upgrade-37f3300ebfa7 deployment: %v", err)
+					t.Errorf("Failed to list deployments: %v", err)
+				}
+				// Look for anything with the prefix "provider-nop-lifecycle-upgrade" and delete it.
+				for _, d := range ds.Items {
+					if strings.HasPrefix(d.Name, "provider-nop-lifecycle-upgrade") {
+						if err := e.Client().Resources("crossplane-system").Delete(ctx, &d); client.IgnoreNotFound(err) != nil {
+							t.Errorf("Failed to delete %s deployment: %v", d.Name, err)
+						}
+					}
 				}
 				return ctx
 			}).
diff --git a/test/e2e/manifests/apiextensions/activation-policy/multiple-mraps/setup/mrd.yaml b/test/e2e/manifests/apiextensions/activation-policy/multiple-mraps/setup/mrd.yaml
index 87f3fc553..44dc3ace2 100644
--- a/test/e2e/manifests/apiextensions/activation-policy/multiple-mraps/setup/mrd.yaml
+++ b/test/e2e/manifests/apiextensions/activation-policy/multiple-mraps/setup/mrd.yaml
@@ -4,6 +4,7 @@ metadata:
   name: storages.multiple.activation-e2e.crossplane.io
 spec:
   group: multiple.activation-e2e.crossplane.io
+  scope: Namespaced
   names:
     kind: Storage
     plural: storages
diff --git a/test/e2e/manifests/apiextensions/activation-policy/single-activation/setup/mrd.yaml b/test/e2e/manifests/apiextensions/activation-policy/single-activation/setup/mrd.yaml
index f2b4c90be..dbee2f50b 100644
--- a/test/e2e/manifests/apiextensions/activation-policy/single-activation/setup/mrd.yaml
+++ b/test/e2e/manifests/apiextensions/activation-policy/single-activation/setup/mrd.yaml
@@ -4,9 +4,11 @@ metadata:
   name: buckets.single.activation-e2e.crossplane.io
 spec:
   group: single.activation-e2e.crossplane.io
+  scope: Namespaced
   names:
-    kind: Bucket
     plural: buckets
+    singular: bucket
+    kind: Bucket
   versions:
   - name: v1alpha1
     served: true
diff --git a/test/e2e/manifests/apiextensions/activation-policy/wildcard-activation/setup/mrd-bucket.yaml b/test/e2e/manifests/apiextensions/activation-policy/wildcard-activation/setup/mrd-bucket.yaml
index 3be6389de..e0264f67f 100644
--- a/test/e2e/manifests/apiextensions/activation-policy/wildcard-activation/setup/mrd-bucket.yaml
+++ b/test/e2e/manifests/apiextensions/activation-policy/wildcard-activation/setup/mrd-bucket.yaml
@@ -3,6 +3,7 @@ kind: ManagedResourceDefinition
 metadata:
   name: buckets.wildcard.activation-e2e.crossplane.io
 spec:
+  scope: Namespaced
   group: wildcard.activation-e2e.crossplane.io
   names:
     kind: Bucket
diff --git a/test/e2e/manifests/apiextensions/activation-policy/wildcard-activation/setup/mrd-database.yaml b/test/e2e/manifests/apiextensions/activation-policy/wildcard-activation/setup/mrd-database.yaml
index 89b64f104..3cf4cc648 100644
--- a/test/e2e/manifests/apiextensions/activation-policy/wildcard-activation/setup/mrd-database.yaml
+++ b/test/e2e/manifests/apiextensions/activation-policy/wildcard-activation/setup/mrd-database.yaml
@@ -3,6 +3,7 @@ kind: ManagedResourceDefinition
 metadata:
   name: databases.wildcard.activation-e2e.crossplane.io
 spec:
+  scope: Namespaced
   group: wildcard.activation-e2e.crossplane.io
   names:
     kind: Database
diff --git a/test/e2e/manifests/apiextensions/activation-policy/wildcard-activation/setup/mrd-instance.yaml b/test/e2e/manifests/apiextensions/activation-policy/wildcard-activation/setup/mrd-instance.yaml
index 039503bf1..8ff61130f 100644
--- a/test/e2e/manifests/apiextensions/activation-policy/wildcard-activation/setup/mrd-instance.yaml
+++ b/test/e2e/manifests/apiextensions/activation-policy/wildcard-activation/setup/mrd-instance.yaml
@@ -3,6 +3,7 @@ kind: ManagedResourceDefinition
 metadata:
   name: instances.wildcard.activation-e2e.crossplane.io
 spec:
+  scope: Namespaced
   group: wildcard.activation-e2e.crossplane.io
   names:
     kind: Instance