Skip to content

Commit 1e7cae5

Browse files
committed
feat(filtering): filter pvc by their name and a regex to avoid replication
Signed-off-by: SkalaNetworks <contact@skala.network>
1 parent e892344 commit 1e7cae5

6 files changed

Lines changed: 126 additions & 2 deletions

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ the actual replication itself.
1010

1111
- **Automated Lifecycle**: Automatically creates `VolumeReplication` objects for PVCs with the appropriate annotation.
1212
- **Inheritance**: Can inherit the `VolumeReplicationClass` from the PVC or from the PVC's namespace (if not specified on the PVC).
13+
- **Exclusion by Name**: Supports excluding PVCs from replication using a global regular expression.
1314
- **VRC Selector**: Supports selecting a `VolumeReplicationClass` using a selector, allowing for more dynamic configuration based on `StorageClass` groups.
1415
- **Cleanup**: Automatically deletes `VolumeReplication` resources when their parent PVC is deleted or when the replication annotation is removed.
1516
- **Leader Election**: Supports high availability with leader election to ensure only one instance is active at a time.
@@ -147,6 +148,19 @@ If the annotation is modified, the `VolumeReplication` is updated accordingly wi
147148

148149
If the annotation is deleted on both the PVC and the namespace, the VolumeReplication is deleted.
149150

151+
### Excluding PVCs from replication
152+
153+
It is possible to exclude some PVCs from being replicated, even if they have the correct annotations (or their namespace has them).
154+
This is done by providing a regular expression to the controller using the `--exclusion-regex` flag or the `EXCLUSION_REGEX` environment variable.
155+
156+
Any PVC whose name matches the regular expression will be ignored by the controller.
157+
For example, if you set `EXCLUSION_REGEX` to `^prime-.*`, all PVCs starting with `prime-` will be excluded from replication.
158+
159+
This feature is useful to avoid unnecessary replications of temporary PVCs (for example, for "prime" PVCs created by the [Container Data Importer](https://github.com/kubevirt/containerized-data-importer)).
160+
161+
> [!NOTE]
162+
> If the regular expression is empty, no PVC will be excluded (unless it doesn't have the appropriate annotations).
163+
150164
## Configuration
151165

152166
The controller can be configured using command-line flags or environment variables:
@@ -155,6 +169,7 @@ The controller can be configured using command-line flags or environment variabl
155169
|------|----------------------|---------|-------------|
156170
| `--kubeconfig` | - | - | Path to a kubeconfig file. If not provided, it assumes in-cluster configuration. |
157171
| `--namespace` | `NAMESPACE` | - | **Required**. The namespace where the controller is deployed (used for leader election). |
172+
| `--exclusion-regex` | `EXCLUSION_REGEX` | - | Optional regular expression to exclude PVCs from replication by name. |
158173

159174
Standard `klog` flags are also supported for logging configuration.
160175

cmd/main.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ package main
33
import (
44
"context"
55
"flag"
6+
"os"
7+
"os/signal"
8+
69
"github.com/skalanetworks/volume-replicator/internal/k8s"
710
"github.com/skalanetworks/volume-replicator/internal/replicator"
811
"k8s.io/client-go/tools/leaderelection"
912
"k8s.io/klog/v2"
10-
"os"
11-
"os/signal"
1213
)
1314

1415
func main() {
@@ -18,6 +19,7 @@ func main() {
1819
var kubeconfig, namespace string
1920
flag.StringVar(&kubeconfig, "kubeconfig", "", "path to kubeconfig file")
2021
flag.StringVar(&namespace, "namespace", os.Getenv("NAMESPACE"), "deployment namespace")
22+
flag.StringVar(&replicator.ExclusionRegex, "exclusion-regex", os.Getenv("EXCLUSION_REGEX"), "regex to exclude PVCs from replication")
2123
klog.InitFlags(nil)
2224
flag.Parse()
2325

internal/replicator/utils.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package replicator
33
import (
44
"context"
55
"fmt"
6+
"regexp"
67

78
"github.com/skalanetworks/volume-replicator/internal/constants"
89
"github.com/skalanetworks/volume-replicator/internal/k8s"
@@ -13,6 +14,8 @@ import (
1314
"k8s.io/klog/v2"
1415
)
1516

17+
var ExclusionRegex string
18+
1619
// isVolumeReplicationCorrect verifies if the definition of a VolumeReplication conforms to its originating PVC
1720
func isVolumeReplicationCorrect(pvc *corev1.PersistentVolumeClaim, vr *unstructured.Unstructured) bool {
1821
key := fmt.Sprintf("%s/%s", vr.GetNamespace(), vr.GetName())
@@ -168,3 +171,21 @@ func getPvcProvisioner(pvc *corev1.PersistentVolumeClaim) string {
168171
// Fallback to the deprecated annotation
169172
return pvc.Annotations[constants.DeprecatedStorageProvisionerAnnotation]
170173
}
174+
175+
// pvcNameMatchesExclusion returns whether a PVC has a name matching the exclusion regex
176+
func pvcNameMatchesExclusion(pvc *corev1.PersistentVolumeClaim) bool {
177+
// If no regex is provided, return that it doesn't match
178+
// This is to avoid Go matching "" as "everything matches"
179+
if ExclusionRegex == "" {
180+
return false
181+
}
182+
183+
// Match the user-provided regex
184+
match, err := regexp.MatchString(ExclusionRegex, pvc.Name)
185+
if err != nil {
186+
klog.Errorf("failed to parse exclusion regex: %s", err.Error())
187+
return false
188+
}
189+
190+
return match
191+
}

internal/replicator/utils_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,3 +662,62 @@ func TestGetPvcProvisioner(t *testing.T) {
662662
})
663663
}
664664
}
665+
666+
func TestPvcNameMatchesExclusion(t *testing.T) {
667+
tests := []struct {
668+
name string
669+
exclusionRegex string
670+
pvcName string
671+
expected bool
672+
}{
673+
{
674+
name: "Empty regex matches nothing",
675+
exclusionRegex: "",
676+
pvcName: "any-pvc",
677+
expected: false,
678+
},
679+
{
680+
name: "Exact match",
681+
exclusionRegex: "^test-pvc$",
682+
pvcName: "test-pvc",
683+
expected: true,
684+
},
685+
{
686+
name: "Prefix match",
687+
exclusionRegex: "^skip-.*",
688+
pvcName: "skip-this-pvc",
689+
expected: true,
690+
},
691+
{
692+
name: "No match",
693+
exclusionRegex: "^skip-.*",
694+
pvcName: "dont-skip-this",
695+
expected: false,
696+
},
697+
{
698+
name: "Partial match (regexp.MatchString behavior)",
699+
exclusionRegex: "exclude",
700+
pvcName: "pvc-to-exclude-here",
701+
expected: true,
702+
},
703+
{
704+
name: "Invalid regex",
705+
exclusionRegex: "[invalid",
706+
pvcName: "any-pvc",
707+
expected: false,
708+
},
709+
}
710+
711+
for _, tt := range tests {
712+
t.Run(tt.name, func(t *testing.T) {
713+
ExclusionRegex = tt.exclusionRegex
714+
pvc := &corev1.PersistentVolumeClaim{
715+
ObjectMeta: metav1.ObjectMeta{
716+
Name: tt.pvcName,
717+
},
718+
}
719+
result := pvcNameMatchesExclusion(pvc)
720+
require.Equal(t, tt.expected, result)
721+
})
722+
}
723+
}

internal/replicator/vrc.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ import (
1515
// The VRC can be provided through annotations as a value or as a selector.
1616
// The annotations can be placed on the PVC or on its namespace.
1717
func getVolumeReplicationClass(pvc *corev1.PersistentVolumeClaim) string {
18+
// If the PVC is to be excluded, return an empty replication class
19+
if pvcNameMatchesExclusion(pvc) {
20+
klog.Infof("PVC %s/%s matches exclusion pattern, no replication class to apply", pvc.Namespace, pvc.Name)
21+
return ""
22+
}
23+
1824
// Retrieve the literal VRC provided on the PVC
1925
value := getVolumeReplicationClassValue(pvc)
2026
if value != "" {

internal/replicator/vrc_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,33 @@ func TestGetVolumeReplicationClass(t *testing.T) {
191191
},
192192
expectedResult: "",
193193
},
194+
{
195+
name: "Excluded by name regex",
196+
pvc: &corev1.PersistentVolumeClaim{
197+
ObjectMeta: metav1.ObjectMeta{
198+
Name: "exclude-this-pvc",
199+
Namespace: nsName,
200+
Annotations: map[string]string{
201+
constants.VrcValueAnnotation: vrcName,
202+
},
203+
},
204+
},
205+
expectedResult: "",
206+
},
194207
}
195208

196209
for _, tt := range tests {
197210
t.Run(tt.name, func(t *testing.T) {
198211
clearNamespaceIndexer(t)
199212

213+
// Set ExclusionRegex specifically for each test case
214+
if tt.name == "Excluded by name regex" {
215+
ExclusionRegex = "exclude-.*"
216+
} else {
217+
ExclusionRegex = ""
218+
}
219+
defer func() { ExclusionRegex = "" }()
220+
200221
if tt.namespace != nil {
201222
err := NamespaceInformer.Informer().GetIndexer().Add(tt.namespace)
202223
require.NoError(t, err)

0 commit comments

Comments
 (0)