Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/operator-public-documentation/preview/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,38 @@ The DocumentDB operator uses a sidecar injector plugin to automatically inject t
For detailed information on configuring the sidecar injector plugin, see: [Sidecar Injector Plugin Configuration](../../developer-guides/sidecar-injector-plugin-configuration.md)


### Local High-Availability (HA)

The DocumentDB operator supports local high-availability by deploying multiple DocumentDB instances with automatic failover capabilities. This is achieved by setting `instancesPerNode` to a value greater than 1, which creates a cluster of DocumentDB instances with built-in redundancy.

#### Enable Local HA

To enable local HA, set `instancesPerNode: 3` in your DocumentDB resource:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it has to be 3? what about 2 or 4?


```sh
cat <<EOF | kubectl apply -f -
apiVersion: db.microsoft.com/preview
kind: DocumentDB
metadata:
name: documentdb-ha
namespace: your-namespace
spec:
nodeCount: 1
instancesPerNode: 3 # Creates 3 DocumentDB instances for HA
documentDbCredentialSecret: documentdb-credentials
resource:
storage:
pvcSize: 10Gi
exposeViaService:
serviceType: LoadBalancer
EOF
```

This configuration creates:
- **1 Primary instance**: Handles all write operations
- **2 Replica instances**: Provide read scalability and automatic failover capability


### Multi-Cloud Deployment

The DocumentDB operator supports deployment across multiple cloud environments and Kubernetes distributions. For guidance on multi-cloud deployments, see: [Multi-Cloud Deployment Guide](../../../documentdb-playground/multi-clould-setup/multi-cloud-deployment-guide.md)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,9 @@ spec:
If not specified, defaults to a version that matches the DocumentDB operator version.
type: string
instancesPerNode:
description: InstancesPerNode is the number of DocumentDB instances
per node. Must be 1.
maximum: 1
description: 'InstancesPerNode is the number of DocumentDB instances
per node. Range: 1-3.'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about 4 or more?

maximum: 3
minimum: 1
type: integer
logLevel:
Expand Down
4 changes: 2 additions & 2 deletions operator/src/api/preview/documentdb_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ type DocumentDBSpec struct {
// +kubebuilder:validation:Maximum=1
NodeCount int `json:"nodeCount"`

// InstancesPerNode is the number of DocumentDB instances per node. Must be 1.
// InstancesPerNode is the number of DocumentDB instances per node. Range: 1-3.
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=1
// +kubebuilder:validation:Maximum=3
InstancesPerNode int `json:"instancesPerNode"`

// Resource specifies the storage resources for DocumentDB.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,9 @@ spec:
If not specified, defaults to a version that matches the DocumentDB operator version.
type: string
instancesPerNode:
description: InstancesPerNode is the number of DocumentDB instances
per node. Must be 1.
maximum: 1
description: 'InstancesPerNode is the number of DocumentDB instances
per node. Range: 1-3.'
maximum: 3
minimum: 1
type: integer
logLevel:
Expand Down
5 changes: 2 additions & 3 deletions operator/src/internal/utils/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,8 @@ func GetDocumentDBServiceDefinition(documentdb *dbpreview.DocumentDB, replicatio
}
if replicationContext.EndpointEnabled() {
selector = map[string]string{
LABEL_APP: documentdb.Name,
LABEL_REPLICA_TYPE: "primary", // Service forwards traffic to primary replicas
LABEL_ROLE: "primary",
"cnpg.io/cluster": documentdb.Name,
"cnpg.io/instanceRole": "primary", // Service forwards traffic to CNPG primary instance
}
}

Expand Down
136 changes: 136 additions & 0 deletions operator/src/internal/utils/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ package util

import (
"testing"

dbpreview "github.com/microsoft/documentdb-operator/api/preview"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)

func TestGenerateServiceName(t *testing.T) {
Expand Down Expand Up @@ -82,3 +87,134 @@ func TestGenerateServiceName(t *testing.T) {
})
}
}

func TestGetDocumentDBServiceDefinition_CNPGLabels(t *testing.T) {
tests := []struct {
name string
documentDBName string
endpointEnabled bool
serviceType corev1.ServiceType
expectedSelector map[string]string
description string
}{
{
name: "endpoint disabled - should have disabled selector",
documentDBName: "test-documentdb",
endpointEnabled: false,
serviceType: corev1.ServiceTypeLoadBalancer,
expectedSelector: map[string]string{
"disabled": "true",
},
description: "When endpoint is disabled, service should have disabled selector",
},
{
name: "endpoint enabled with LoadBalancer - should use CNPG labels",
documentDBName: "test-documentdb",
endpointEnabled: true,
serviceType: corev1.ServiceTypeLoadBalancer,
expectedSelector: map[string]string{
"cnpg.io/cluster": "test-documentdb",
"cnpg.io/instanceRole": "primary",
},
description: "When endpoint is enabled, service should use CNPG labels for failover support",
},
{
name: "endpoint enabled with ClusterIP - should use CNPG labels",
documentDBName: "test-documentdb",
endpointEnabled: true,
serviceType: corev1.ServiceTypeClusterIP,
expectedSelector: map[string]string{
"cnpg.io/cluster": "test-documentdb",
"cnpg.io/instanceRole": "primary",
},
description: "Service type should not affect selector labels",
},
{
name: "different documentdb name - should reflect in cluster label",
documentDBName: "my-db-cluster",
endpointEnabled: true,
serviceType: corev1.ServiceTypeLoadBalancer,
expectedSelector: map[string]string{
"cnpg.io/cluster": "my-db-cluster",
"cnpg.io/instanceRole": "primary",
},
description: "Cluster label should match DocumentDB instance name",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a mock DocumentDB instance
documentdb := &dbpreview.DocumentDB{
TypeMeta: metav1.TypeMeta{
APIVersion: "db.microsoft.com/preview",
Kind: "DocumentDB",
},
ObjectMeta: metav1.ObjectMeta{
Name: tt.documentDBName,
Namespace: "test-namespace",
UID: types.UID("test-uid-123"),
},
}

// Create a mock ReplicationContext
replicationContext := &ReplicationContext{
Self: tt.documentDBName,
Environment: "test",
state: NoReplication, // This will make EndpointEnabled() return true
}

// If endpoint should be disabled, set a different state
if !tt.endpointEnabled {
replicationContext.state = Primary
replicationContext.currentLocalPrimary = "different-primary"
replicationContext.targetLocalPrimary = "target-primary"
}

// Generate the service definition
service := GetDocumentDBServiceDefinition(documentdb, replicationContext, "test-namespace", tt.serviceType)

// Verify the selector matches expected values
if len(service.Spec.Selector) != len(tt.expectedSelector) {
t.Errorf("Expected selector to have %d labels, got %d. Expected: %v, Got: %v",
len(tt.expectedSelector), len(service.Spec.Selector), tt.expectedSelector, service.Spec.Selector)
}

for key, expectedValue := range tt.expectedSelector {
if actualValue, exists := service.Spec.Selector[key]; !exists {
t.Errorf("Expected selector to contain key %q, but it was missing. Selector: %v", key, service.Spec.Selector)
} else if actualValue != expectedValue {
t.Errorf("Expected selector[%q] = %q, got %q", key, expectedValue, actualValue)
}
}

// Verify other service properties
if service.Name == "" {
t.Error("Service name should not be empty")
}

if service.Namespace != "test-namespace" {
t.Errorf("Expected service namespace to be 'test-namespace', got %q", service.Namespace)
}

if service.Spec.Type != tt.serviceType {
t.Errorf("Expected service type to be %v, got %v", tt.serviceType, service.Spec.Type)
}

// Verify owner reference is set correctly
if len(service.OwnerReferences) != 1 {
t.Errorf("Expected 1 owner reference, got %d", len(service.OwnerReferences))
} else {
ownerRef := service.OwnerReferences[0]
if ownerRef.Name != tt.documentDBName {
t.Errorf("Expected owner reference name to be %q, got %q", tt.documentDBName, ownerRef.Name)
}
if ownerRef.Kind != "DocumentDB" {
t.Errorf("Expected owner reference kind to be 'DocumentDB', got %q", ownerRef.Kind)
}
}

t.Logf("✅ %s: %s", tt.name, tt.description)
})
}
}
Loading