Skip to content

Commit 75df87e

Browse files
committed
feat(replicationState): add an option to add replication state per ns/pvc
Signed-off-by: SkalaNetworks <contact@skala.network>
1 parent a7e5862 commit 75df87e

7 files changed

Lines changed: 186 additions & 19 deletions

File tree

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,26 @@ metadata:
182182
> [!NOTE]
183183
> While paused, existing `VolumeReplication` objects are frozen in their current state. Unpausing resumes normal reconciliation.
184184

185+
### Replication State
186+
187+
By default, the `replicationState` of the created `VolumeReplication` is set to `primary`.
188+
You can change this using the `replication.superphenix.net/replicationState` annotation on either the PVC or its namespace.
189+
190+
The annotation on the PVC takes precedence over the annotation on the namespace.
191+
192+
Common values for this annotation are `primary` and `secondary`.
193+
194+
Example — setting the entire namespace as `secondary`:
195+
196+
```yaml
197+
apiVersion: v1
198+
kind: Namespace
199+
metadata:
200+
name: my-namespace
201+
annotations:
202+
replication.superphenix.net/replicationState: "secondary"
203+
```
204+
185205
### Excluding PVCs from replication
186206

187207
It is possible to exclude some PVCs from being replicated, even if they have the correct annotations (or their namespace has them).

internal/constants/const.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const (
77
PauseAnnotation = "replication.superphenix.net/pause"
88
ParentLabel = "replication.superphenix.net/parent"
99
StorageClassGroup = "replication.superphenix.net/storageClassGroup"
10+
ReplicationStateAnnotation = "replication.superphenix.net/replicationState"
1011
StorageProvisionerAnnotation = "volume.kubernetes.io/storage-provisioner"
1112
DeprecatedStorageProvisionerAnnotation = "volume.beta.kubernetes.io/storage-provisioner"
1213
)

internal/replicator/replicator_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ func TestReconcileVolumeReplication(t *testing.T) {
4747
},
4848
}
4949

50+
// Add namespace to informer
51+
_ = NamespaceInformer.Informer().GetIndexer().Add(&corev1.Namespace{
52+
ObjectMeta: metav1.ObjectMeta{Name: nsName},
53+
})
54+
5055
vr := &unstructured.Unstructured{}
5156
vr.SetUnstructuredContent(map[string]any{
5257
"apiVersion": fmt.Sprintf("%s/%s", VolumeReplicationResource.Group, VolumeReplicationResource.Version),

internal/replicator/utils.go

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ func isVolumeReplicationCorrect(pvc *corev1.PersistentVolumeClaim, vr *unstructu
2828
return false
2929
}
3030

31+
// Check that the replicationState correspond to the one inherited from the PVC/NS
32+
replicationState, _, _ := unstructured.NestedString(vr.Object, "spec", "replicationState")
33+
if getReplicationState(pvc) != replicationState {
34+
klog.Infof("VolumeReplication %s has a replication state mismatch with its parent (got %s)", key, replicationState)
35+
return false
36+
}
37+
3138
// Check that the dataSource points to the PVC
3239
dataSource, _, _ := unstructured.NestedNullCoercingStringMap(vr.Object, "spec", "dataSource")
3340
if dataSource["apiGroup"] != "v1" || dataSource["kind"] != "PersistentVolumeClaim" || dataSource["name"] != pvc.Name {
@@ -90,7 +97,7 @@ func createVolumeReplication(pvc *corev1.PersistentVolumeClaim) error {
9097
},
9198
"spec": map[string]any{
9299
"volumeReplicationClass": getVolumeReplicationClass(pvc),
93-
"replicationState": "primary",
100+
"replicationState": getReplicationState(pvc),
94101
"dataSource": map[string]any{
95102
"apiGroup": "v1",
96103
"kind": "PersistentVolumeClaim",
@@ -174,27 +181,17 @@ func getPvcProvisioner(pvc *corev1.PersistentVolumeClaim) string {
174181
}
175182

176183
// isPvcPaused returns whether the replication for a PVC is paused
177-
// The PVC-level annotation takes precedence over the namespace-level annotation:
178-
// any explicit value on the PVC (even "false") short-circuits the namespace lookup
179184
func isPvcPaused(pvc *corev1.PersistentVolumeClaim, namespace string) bool {
180-
if pvc != nil {
181-
// If the PVC has the annotation specified, it has priority over the one of the namespace
182-
if value, ok := pvc.Annotations[constants.PauseAnnotation]; ok {
183-
return value == "true"
184-
}
185+
if pvc == nil {
186+
return isNamespacePaused(namespace)
185187
}
186188

187-
return isNamespacePaused(namespace)
189+
return getAnnotationValue(pvc, constants.PauseAnnotation) == "true"
188190
}
189191

190192
// isNamespacePaused returns whether replication is paused at the namespace level
191193
func isNamespacePaused(namespace string) bool {
192-
ns, err := NamespaceInformer.Lister().Get(namespace)
193-
if err != nil {
194-
return false
195-
}
196-
197-
return ns.Annotations[constants.PauseAnnotation] == "true"
194+
return getNamespaceAnnotationValue(namespace, constants.PauseAnnotation) == "true"
198195
}
199196

200197
// pvcNameMatchesExclusion returns whether a PVC has a name matching the exclusion regex

internal/replicator/utils_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ func TestCreateVolumeReplication(t *testing.T) {
4747
},
4848
}
4949

50+
// Add namespace to informer
51+
_ = NamespaceInformer.Informer().GetIndexer().Add(&corev1.Namespace{
52+
ObjectMeta: metav1.ObjectMeta{Name: nsName},
53+
})
54+
5055
t.Run("Successful creation", func(t *testing.T) {
5156
err := createVolumeReplication(pvc)
5257
require.NoError(t, err)
@@ -492,6 +497,11 @@ func TestIsVolumeReplicationCorrect(t *testing.T) {
492497
},
493498
}
494499

500+
// Add namespace to informer
501+
_ = NamespaceInformer.Informer().GetIndexer().Add(&corev1.Namespace{
502+
ObjectMeta: metav1.ObjectMeta{Name: nsName},
503+
})
504+
495505
tests := []struct {
496506
name string
497507
vr *unstructured.Unstructured
@@ -507,6 +517,7 @@ func TestIsVolumeReplicationCorrect(t *testing.T) {
507517
},
508518
"spec": map[string]any{
509519
"volumeReplicationClass": vrcName,
520+
"replicationState": "primary",
510521
"dataSource": map[string]any{
511522
"apiGroup": "v1",
512523
"kind": "PersistentVolumeClaim",
@@ -517,6 +528,27 @@ func TestIsVolumeReplicationCorrect(t *testing.T) {
517528
},
518529
expected: true,
519530
},
531+
{
532+
name: "replicationState mismatch",
533+
vr: &unstructured.Unstructured{
534+
Object: map[string]any{
535+
"metadata": map[string]any{
536+
"name": pvcName,
537+
"namespace": nsName,
538+
},
539+
"spec": map[string]any{
540+
"volumeReplicationClass": vrcName,
541+
"replicationState": "secondary",
542+
"dataSource": map[string]any{
543+
"apiGroup": "v1",
544+
"kind": "PersistentVolumeClaim",
545+
"name": pvcName,
546+
},
547+
},
548+
},
549+
},
550+
expected: false,
551+
},
520552
{
521553
name: "volumeReplicationClass mismatch",
522554
vr: &unstructured.Unstructured{
@@ -833,6 +865,17 @@ func TestIsPvcPaused(t *testing.T) {
833865
namespace: pausedNs,
834866
expected: false, // PVC takes precedence, and invalid is not true
835867
},
868+
{
869+
name: "PVC empty pause value, NS paused",
870+
pvc: &corev1.PersistentVolumeClaim{
871+
ObjectMeta: metav1.ObjectMeta{
872+
Namespace: nsName,
873+
Annotations: map[string]string{constants.PauseAnnotation: ""},
874+
},
875+
},
876+
namespace: pausedNs,
877+
expected: true, // It falls back to NS now
878+
},
836879
}
837880

838881
for _, tt := range tests {

internal/replicator/vrc.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,23 +90,43 @@ func getVolumeReplicationClassSelector(pvc *corev1.PersistentVolumeClaim) string
9090
return getAnnotationValue(pvc, constants.VrcSelectorAnnotation)
9191
}
9292

93+
// getReplicationState returns the replication state to use for a PVC.
94+
// The replication state is specified through an annotation on the PVC or on its namespace.
95+
// The annotation on the PVC has priority over the one of the namespace.
96+
// If no replication state is found on either PVC or namespace, we return "primary" by default.
97+
func getReplicationState(pvc *corev1.PersistentVolumeClaim) string {
98+
state := getAnnotationValue(pvc, constants.ReplicationStateAnnotation)
99+
if state == "" {
100+
return "primary"
101+
}
102+
return state
103+
}
104+
93105
// getAnnotationValue returns the value of an annotation from a PVC or its namespace.
94106
// The annotation on the PVC has priority over the one of the namespace.
95107
func getAnnotationValue(pvc *corev1.PersistentVolumeClaim, annotation string) string {
108+
if pvc == nil {
109+
return ""
110+
}
111+
96112
// If the PVC has the annotation specified, it has priority over the one of the namespace
97113
if value, ok := pvc.Annotations[annotation]; ok && value != "" {
98114
return value
99115
}
100116

101117
// If the PVC doesn't have the annotation specified, fall back to the namespace
102-
namespace, err := NamespaceInformer.Lister().Get(pvc.Namespace)
118+
return getNamespaceAnnotationValue(pvc.Namespace, annotation)
119+
}
120+
121+
// getNamespaceAnnotationValue returns the value of an annotation from a namespace.
122+
func getNamespaceAnnotationValue(namespace string, annotation string) string {
123+
ns, err := NamespaceInformer.Lister().Get(namespace)
103124
if err != nil {
104-
klog.Errorf("failed to retrieve parent namespace for PVC %s/%s: %s", pvc.Namespace, pvc.Name, err.Error())
125+
klog.Errorf("failed to retrieve namespace %s: %s", namespace, err.Error())
105126
return ""
106127
}
107128

108-
// If the namespace doesn't have the annotation, this will return an empty string
109-
return namespace.Annotations[annotation]
129+
return ns.Annotations[annotation]
110130
}
111131

112132
// filterVrcFromSelector returns a VolumeReplicationClass that is in a specific StorageClass Group

internal/replicator/vrc_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,3 +709,84 @@ func TestFilterVrcFromSelector(t *testing.T) {
709709
require.Nil(t, list)
710710
})
711711
}
712+
713+
func TestGetReplicationState(t *testing.T) {
714+
_, _, informerFactory := setupTestEnvironment()
715+
nsName := "test-ns"
716+
717+
tests := []struct {
718+
name string
719+
pvc *corev1.PersistentVolumeClaim
720+
namespace *corev1.Namespace
721+
expectedResult string
722+
}{
723+
{
724+
name: "Default value (primary)",
725+
pvc: &corev1.PersistentVolumeClaim{
726+
ObjectMeta: metav1.ObjectMeta{
727+
Name: "test-pvc",
728+
Namespace: nsName,
729+
},
730+
},
731+
expectedResult: "primary",
732+
},
733+
{
734+
name: "Override from Namespace",
735+
pvc: &corev1.PersistentVolumeClaim{
736+
ObjectMeta: metav1.ObjectMeta{
737+
Name: "test-pvc",
738+
Namespace: nsName,
739+
},
740+
},
741+
namespace: &corev1.Namespace{
742+
ObjectMeta: metav1.ObjectMeta{
743+
Name: nsName,
744+
Annotations: map[string]string{
745+
constants.ReplicationStateAnnotation: "secondary",
746+
},
747+
},
748+
},
749+
expectedResult: "secondary",
750+
},
751+
{
752+
name: "Override from PVC",
753+
pvc: &corev1.PersistentVolumeClaim{
754+
ObjectMeta: metav1.ObjectMeta{
755+
Name: "test-pvc",
756+
Namespace: nsName,
757+
Annotations: map[string]string{
758+
constants.ReplicationStateAnnotation: "primary",
759+
},
760+
},
761+
},
762+
namespace: &corev1.Namespace{
763+
ObjectMeta: metav1.ObjectMeta{
764+
Name: nsName,
765+
Annotations: map[string]string{
766+
constants.ReplicationStateAnnotation: "secondary",
767+
},
768+
},
769+
},
770+
expectedResult: "primary",
771+
},
772+
}
773+
774+
for _, tt := range tests {
775+
t.Run(tt.name, func(t *testing.T) {
776+
clearNamespaceIndexer(t)
777+
if tt.namespace != nil {
778+
err := informerFactory.Core().V1().Namespaces().Informer().GetIndexer().Add(tt.namespace)
779+
require.NoError(t, err)
780+
} else {
781+
// Add a default namespace if not provided
782+
err := informerFactory.Core().V1().Namespaces().Informer().GetIndexer().Add(&corev1.Namespace{
783+
ObjectMeta: metav1.ObjectMeta{Name: nsName},
784+
})
785+
require.NoError(t, err)
786+
}
787+
788+
result := getReplicationState(tt.pvc)
789+
require.Equal(t, tt.expectedResult, result)
790+
})
791+
}
792+
}

0 commit comments

Comments
 (0)