--- title: Implementing SafeStart in Providers weight: 160 description: Guide for provider developers to implement SafeStart capability --- This guide shows provider developers how to implement SafeStart capability in their Crossplane providers. SafeStart enables selective resource activation through Managed Resource Definitions (MRDs), improving performance and resource management. {{< hint "important" >}} SafeStart requires Crossplane v2.0+ and involves significant provider changes. Plan for breaking changes and thorough testing before implementing. {{< /hint >}} ## What SafeStart provides SafeStart transforms how your provider handles resource installation: **Without SafeStart:** - All managed resources become CRDs immediately when provider installs - Users get all ~200 AWS resources even if they need only 5 - Higher memory usage and slower API server responses **With SafeStart:** - All managed resources become inactive MRDs when provider installs - Users activate only needed resources through policies - Lower resource overhead and better performance ## Prerequisites Before implementing SafeStart, ensure you have: * Provider built with Crossplane v2.0+ runtime * Understanding of [MRDs and activation policies]({{< ref "mrd-activation-policies" >}}) * Test environment with Crossplane v2.0+ * CI/CD pipeline that can build and test provider changes ## Implementation steps ### Step 1: Update provider metadata Declare SafeStart capability in your provider package metadata: ```yaml apiVersion: meta.pkg.crossplane.io/v1 kind: Provider metadata: name: provider-example spec: package: registry.example.com/provider-example:v1.0.0 capabilities: - name: SafeStart ``` {{< hint "tip" >}} Crossplane supports flexible capability matching. `SafeStart`, `safestart`, and `safe-start` are all recognized as the same capability. {{< /hint >}} ### Step 2: Enhance MRD generation Update your MRD generation to include connection details documentation: {{< tabs >}} {{< tab "Go Controller Runtime" >}} ```go // In your MRD generation code type ManagedResourceDefinition struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec ManagedResourceDefinitionSpec `json:"spec"` Status ManagedResourceDefinitionStatus `json:"status,omitempty"` } type ManagedResourceDefinitionSpec struct { // Standard CRD fields Group string `json:"group"` Names Names `json:"names"` Scope string `json:"scope"` // SafeStart-specific fields ConnectionDetails []ConnectionDetail `json:"connectionDetails,omitempty"` State ResourceState `json:"state,omitempty"` } type ConnectionDetail struct { Name string `json:"name"` Description string `json:"description"` Type string `json:"type"` FromConnectionSecretKey string `json:"fromConnectionSecretKey,omitempty"` } ``` {{< /tab >}} {{< tab "Terrajet/Upjet Provider" >}} ```go // In your provider configuration func GetProvider() *ujconfig.Provider { pc := ujconfig.NewProvider([]byte(providerSchema), resourcePrefix, modulePath, ujconfig.WithIncludeList(ExternalNameConfigured()), ujconfig.WithDefaultResourceOptions( ExternalNameConfigurations(), SafeStartConfiguration(), // Add SafeStart config )) // Configure SafeStart for specific resources for _, configure := range []func(provider *ujconfig.Provider){ configureConnectionDetails, configureMRDDocumentation, } { configure(pc) } return pc } func configureConnectionDetails(p *ujconfig.Provider) { // Example: RDS Instance connection details p.AddResourceConfigurator("aws_db_instance", func(r *ujconfig.Resource) { r.ConnectionDetails = map[string]ujconfig.ConnectionDetail{ "endpoint": { Description: "The RDS instance endpoint", Type: "string", FromConnectionSecretKey: "endpoint", }, "port": { Description: "The port on which the DB accepts connections", Type: "integer", FromConnectionSecretKey: "port", }, "username": { Description: "The master username for the database", Type: "string", FromConnectionSecretKey: "username", }, "password": { Description: "The master password for the database", Type: "string", FromConnectionSecretKey: "password", }, } }) } ``` {{< /tab >}} {{< /tabs >}} ### Step 3: Handle namespaced resources SafeStart works best with namespaced managed resources. Update your resources to support both cluster and namespaced scopes: ```go // Update resource definitions to support namespacing type Database struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec DatabaseSpec `json:"spec"` Status DatabaseStatus `json:"status,omitempty"` } // Update your CRD generation //+kubebuilder:resource:scope=Namespaced //+kubebuilder:object:root=true //+kubebuilder:subresource:status type Database struct { // ... resource definition } // Optionally create cluster-scoped variants //+kubebuilder:resource:scope=Cluster //+kubebuilder:object:root=true //+kubebuilder:subresource:status type ClusterDatabase struct { // ... same spec but cluster scoped } ``` ### Step 4: Update RBAC permissions SafeStart providers need additional permissions to manage CRDs dynamically: ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: provider-example-system rules: # Existing provider permissions - apiGroups: [""] resources: ["events"] verbs: ["create", "update", "patch"] - apiGroups: ["example.crossplane.io"] resources: ["*"] verbs: ["*"] # Additional SafeStart permissions - apiGroups: ["apiextensions.k8s.io"] resources: ["customresourcedefinitions"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["apiextensions.crossplane.io"] resources: ["managedresourcedefinitions"] verbs: ["get", "list", "watch", "update", "patch"] ``` ### Step 5: Implement MRD controller logic Add controller logic to handle MRD activation and CRD lifecycle: ```go package controller import ( "context" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "sigs.k8s.io/controller-runtime/pkg/reconcile" xpv1alpha1 "github.com/crossplane/crossplane/apis/apiextensions/v1alpha1" ) // MRDReconciler handles MRD activation type MRDReconciler struct { client.Client Scheme *runtime.Scheme } func (r *MRDReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { mrd := &xpv1alpha1.ManagedResourceDefinition{} if err := r.Get(ctx, req.NamespacedName, mrd); err != nil { return reconcile.Result{}, client.IgnoreNotFound(err) } // Check if MRD should be active if mrd.Spec.State != nil && *mrd.Spec.State == xpv1alpha1.ResourceStateActive { return r.ensureCRDExists(ctx, mrd) } // If inactive, ensure CRD is removed return r.ensureCRDRemoved(ctx, mrd) } func (r *MRDReconciler) ensureCRDExists(ctx context.Context, mrd *xpv1alpha1.ManagedResourceDefinition) (reconcile.Result, error) { crd := &apiextv1.CustomResourceDefinition{} crdName := mrd.Spec.Names.Plural + "." + mrd.Spec.Group err := r.Get(ctx, types.NamespacedName{Name: crdName}, crd) if client.IgnoreNotFound(err) != nil { return reconcile.Result{}, err } if err != nil { // CRD doesn't exist return r.createCRD(ctx, mrd) } // CRD exists, ensure it's up to date return r.updateCRD(ctx, mrd, crd) } func (r *MRDReconciler) createCRD(ctx context.Context, mrd *xpv1alpha1.ManagedResourceDefinition) (reconcile.Result, error) { crd := &apiextv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: mrd.Spec.Names.Plural + "." + mrd.Spec.Group, OwnerReferences: []metav1.OwnerReference{{ APIVersion: mrd.APIVersion, Kind: mrd.Kind, Name: mrd.Name, UID: mrd.UID, Controller: pointer.Bool(true), }}, }, Spec: mrd.Spec.CustomResourceDefinitionSpec, } return reconcile.Result{}, r.Create(ctx, crd) } ``` ### Step 6: Update build and CI processes Update your build process to generate MRDs alongside CRDs: {{< tabs >}} {{< tab "Makefile" >}} ```makefile # Update your Makefile to generate both CRDs and MRDs .PHONY: generate generate: controller-gen $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." $(CONTROLLER_GEN) crd:allowDangerousTypes=true paths="./..." output:crd:artifacts:config=package/crds $(CONTROLLER_GEN) mrd:allowDangerousTypes=true paths="./..." output:mrd:artifacts:config=package/mrds # Add MRD generation tool MRD_GEN = $(shell pwd)/bin/mrd-gen .PHONY: mrd-gen mrd-gen: ## Download mrd-gen locally if necessary. $(call go-get-tool,$(MRD_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.13.0) # Update package generation to include MRDs .PHONY: build-package build-package: generate mkdir -p package/ cp package/crds/*.yaml package/ cp package/mrds/*.yaml package/ echo "# Package metadata with SafeStart capability" > package/provider.yaml echo "apiVersion: meta.pkg.crossplane.io/v1" >> package/provider.yaml echo "kind: Provider" >> package/provider.yaml echo "spec:" >> package/provider.yaml echo " capabilities:" >> package/provider.yaml echo " - name: SafeStart" >> package/provider.yaml ``` {{< /tab >}} {{< tab "GitHub Actions" >}} ```yaml name: Build and Test SafeStart Provider on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test-safestart: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v4 with: go-version: '1.21' - name: Run Tests run: make test - name: Generate MRDs run: make generate - name: Verify MRD Generation run: | if [ ! -d "package/mrds" ]; then echo "MRD generation failed" exit 1 fi echo "Generated MRDs:" ls -la package/mrds/ - name: Test SafeStart Integration run: | # Start local cluster make kind-up make install-crossplane-v2 # Install provider with SafeStart make install-provider # Verify MRDs created but inactive kubectl get mrds kubectl get mrds -o jsonpath='{.items[*].spec.state}' | grep -q "Inactive" # Test activation policy kubectl apply -f examples/activation-policy.yaml # Verify resources activate sleep 30 kubectl get mrds -o jsonpath='{.items[*].spec.state}' | grep -q "Active" # Test resource creation kubectl apply -f examples/example-resource.yaml kubectl wait --for=condition=Ready --timeout=300s -f examples/example-resource.yaml ``` {{< /tab >}} {{< /tabs >}} ### Step 7: Add connection details documentation Document connection details in your MRDs to help users understand resource capabilities: ```yaml # Example generated MRD with connection details apiVersion: apiextensions.crossplane.io/v1alpha1 kind: ManagedResourceDefinition metadata: name: databases.rds.aws.example.io spec: group: rds.aws.example.io names: kind: Database plural: databases scope: Namespaced # SafeStart-specific fields connectionDetails: - name: endpoint description: "The RDS instance connection endpoint" type: string fromConnectionSecretKey: endpoint - name: port description: "The port number for database connections" type: integer fromConnectionSecretKey: port - name: username description: "The master username for the database" type: string fromConnectionSecretKey: username - name: password description: "The master password for the database" type: string fromConnectionSecretKey: password - name: ca_certificate description: "The CA certificate for SSL connections" type: string fromConnectionSecretKey: ca_certificate # Standard CRD specification versions: - name: v1alpha1 served: true storage: true # ... rest of CRD spec ``` ## Testing SafeStart implementation ### Unit testing Test your MRD generation and controller logic: ```go func TestMRDGeneration(t *testing.T) { // Test that MRDs are generated with correct connection details mrd := generateMRDForResource("Database") assert.Equal(t, "databases.rds.aws.example.io", mrd.Name) assert.NotEmpty(t, mrd.Spec.ConnectionDetails) // Verify specific connection details endpointDetail := findConnectionDetail(mrd, "endpoint") assert.NotNil(t, endpointDetail) assert.Equal(t, "string", endpointDetail.Type) assert.Contains(t, endpointDetail.Description, "endpoint") } func TestMRDActivation(t *testing.T) { // Test MRD activation creates CRD ctx := context.Background() mrd := &v1alpha1.ManagedResourceDefinition{ Spec: v1alpha1.ManagedResourceDefinitionSpec{ State: &[]v1alpha1.ResourceState{v1alpha1.ResourceStateActive}[0], }, } reconciler := &MRDReconciler{Client: fakeClient} result, err := reconciler.Reconcile(ctx, reconcile.Request{}) assert.NoError(t, err) assert.False(t, result.Requeue) // Verify CRD was created crd := &apiextv1.CustomResourceDefinition{} err = fakeClient.Get(ctx, types.NamespacedName{Name: "databases.rds.aws.example.io"}, crd) assert.NoError(t, err) } ``` ### Integration testing Test SafeStart behavior in a real cluster: ```bash #!/bin/bash set -e echo "Starting SafeStart integration test..." # Install Crossplane v2.0 kubectl create namespace crossplane-system helm install crossplane crossplane-stable/crossplane \ --namespace crossplane-system \ --version v2.0.0 \ --wait # Install provider with SafeStart kubectl apply -f - <20 resources - Document recommended activation patterns for common use cases - Provide environment-specific activation policy examples ### User experience - Include helpful error messages when resources aren't activated - Provide clear migration guides for existing users - Document connection details thoroughly ### Testing strategy - Test both with and without SafeStart in CI - Verify activation/deactivation cycles work correctly - Test resource creation after activation SafeStart provides significant value for large providers and improves the overall Crossplane user experience. Following this guide helps ensure your implementation is robust, well-documented, and user-friendly.