docs/content/master/guides/implementing-safestart.md

20 KiB

title weight description
Implementing SafeStart in Providers 160 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:

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" >}}

// 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" >}}

// 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:

// 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:

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:

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" >}}

# 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" >}}

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:

# 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:

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:

#!/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 - <<EOF
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-example
spec:
  package: registry.example.com/provider-example:latest
  capabilities:
  - name: SafeStart
EOF

# Wait for provider installation
kubectl wait --for=condition=Healthy provider/provider-example --timeout=300s

# Verify MRDs created but inactive
echo "Checking MRD states..."
MRD_COUNT=$(kubectl get mrds --no-headers | wc -l)
INACTIVE_COUNT=$(kubectl get mrds -o jsonpath='{.items[*].spec.state}' | grep -o "Inactive" | wc -l)

if [ "$MRD_COUNT" -eq "$INACTIVE_COUNT" ]; then
    echo "✓ All MRDs are inactive as expected"
else
    echo "✗ Some MRDs are unexpectedly active"
    exit 1
fi

# Test activation policy
kubectl apply -f - <<EOF  
apiVersion: apiextensions.crossplane.io/v1alpha1
kind: ManagedResourceActivationPolicy
metadata:
  name: test-policy
spec:
  activations:
  - "databases.rds.aws.example.io"
EOF

# Wait for activation
sleep 10

# Verify activation worked
ACTIVE_COUNT=$(kubectl get mrd databases.rds.aws.example.io -o jsonpath='{.spec.state}' | grep -o "Active" | wc -l)
if [ "$ACTIVE_COUNT" -eq "1" ]; then
    echo "✓ MRD activation successful"
else
    echo "✗ MRD activation failed"
    exit 1
fi

# Test resource creation
kubectl apply -f - <<EOF
apiVersion: rds.aws.example.io/v1alpha1
kind: Database
metadata:
  name: test-db
  namespace: default
spec:
  forProvider:
    engine: postgres
    region: us-east-1
EOF

# Verify resource creation
kubectl wait --for=condition=Ready database/test-db --timeout=300s --namespace default

echo "✓ SafeStart integration test passed"

Migration considerations

For existing users

When you add SafeStart to an existing provider:

Breaking change considerations:

  • Existing installations will continue working (backward compatibility)
  • New installations will have inactive MRDs by default
  • Users need activation policies for new installations

Migration strategy:

# Provide migration documentation like:
# For users upgrading to SafeStart-enabled provider v2.0:

# 1. Existing resources continue working unchanged
# 2. For new installations, create activation policy:
apiVersion: apiextensions.crossplane.io/v1alpha1
kind: ManagedResourceActivationPolicy
metadata:
  name: legacy-compatibility
spec:
  activations:
  - "*.aws.example.io"  # Activate all resources (legacy behavior)

Version compatibility matrix

Document version compatibility clearly:

Provider Version Crossplane Version SafeStart Support Notes
v1.x v1.x - v2.x No Legacy CRD-only mode
v2.0 v2.0+ Yes Full SafeStart support
v2.1 v2.0+ Yes Enhanced MRD features

Documentation requirements

Update your provider documentation to include:

README updates

# Provider Example

## SafeStart Support

This provider supports SafeStart capability, which provides:
- Selective resource activation
- Improved performance for large providers  
- Connection details documentation

### Quick Start with SafeStart

1. Install the provider:
```yaml
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-example
spec:
  package: registry.example.com/provider-example:v2.0.0
  1. Create activation policy:
apiVersion: apiextensions.crossplane.io/v1alpha1  
kind: ManagedResourceActivationPolicy
metadata:
  name: example-resources
spec:
  activations:
  - "databases.rds.aws.example.io"
  - "*.s3.aws.example.io"
  1. Create resources normally - only activated resources work.

### Connection details documentation

Document what connection details each resource provides:

```markdown
## Connection Details Reference

### Database (`databases.rds.aws.example.io`)
- `endpoint` (string): RDS instance connection endpoint
- `port` (integer): Database connection port  
- `username` (string): Master database username
- `password` (string): Master database password
- `ca_certificate` (string): CA certificate for SSL connections

### Storage Bucket (`buckets.s3.aws.example.io`) 
- `bucket_name` (string): The S3 bucket name
- `region` (string): AWS region where bucket is located
- `arn` (string): Full ARN of the S3 bucket

Troubleshooting

Common issues

MRDs not activating:

# Check activation policy exists and matches
kubectl get mrap
kubectl describe mrap my-policy

# Verify MRD exists  
kubectl get mrd my-resource.provider.example.io
kubectl describe mrd my-resource.provider.example.io

CRDs not created:

# Check MRD controller logs
kubectl logs -n crossplane-system deployment/provider-example

# Verify RBAC permissions
kubectl auth can-i create customresourcedefinitions --as=system:serviceaccount:crossplane-system:provider-example

Resource creation fails:

# Verify MRD is active
kubectl get mrd my-resource.provider.example.io -o jsonpath='{.spec.state}'

# Check if CRD exists
kubectl get crd my-resource.provider.example.io

# Look for controller errors
kubectl describe my-resource my-instance

Best practices

Performance optimization

  • Start with inactive MRDs for providers with >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.