From 9b8d6614e0d649047d614284b433943b2798260d Mon Sep 17 00:00:00 2001 From: SkalaNetworks Date: Mon, 19 Jan 2026 18:22:58 +0000 Subject: [PATCH] feat(filtering): filter pvc by their name and a regex to avoid replication Signed-off-by: SkalaNetworks --- README.md | 15 +++++ .../templates/deployment.yaml | 4 ++ charts/volume-replicator/values.yaml | 6 ++ cmd/main.go | 6 +- internal/replicator/utils.go | 21 +++++++ internal/replicator/utils_test.go | 59 +++++++++++++++++++ internal/replicator/vrc.go | 6 ++ internal/replicator/vrc_test.go | 21 +++++++ 8 files changed, 136 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c52f8d0..277dd32 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ the actual replication itself. - **Automated Lifecycle**: Automatically creates `VolumeReplication` objects for PVCs with the appropriate annotation. - **Inheritance**: Can inherit the `VolumeReplicationClass` from the PVC or from the PVC's namespace (if not specified on the PVC). +- **Exclusion by Name**: Supports excluding PVCs from replication using a global regular expression. - **VRC Selector**: Supports selecting a `VolumeReplicationClass` using a selector, allowing for more dynamic configuration based on `StorageClass` groups. - **Cleanup**: Automatically deletes `VolumeReplication` resources when their parent PVC is deleted or when the replication annotation is removed. - **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 If the annotation is deleted on both the PVC and the namespace, the VolumeReplication is deleted. +### Excluding PVCs from replication + +It is possible to exclude some PVCs from being replicated, even if they have the correct annotations (or their namespace has them). +This is done by providing a regular expression to the controller using the `--exclusion-regex` flag or the `EXCLUSION_REGEX` environment variable. + +Any PVC whose name matches the regular expression will be ignored by the controller. +For example, if you set `EXCLUSION_REGEX` to `^prime-.*`, all PVCs starting with `prime-` will be excluded from replication. + +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)). + +> [!NOTE] +> If the regular expression is empty, no PVC will be excluded (unless it doesn't have the appropriate annotations). + ## Configuration 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 |------|----------------------|---------|-------------| | `--kubeconfig` | - | - | Path to a kubeconfig file. If not provided, it assumes in-cluster configuration. | | `--namespace` | `NAMESPACE` | - | **Required**. The namespace where the controller is deployed (used for leader election). | +| `--exclusion-regex` | `EXCLUSION_REGEX` | - | Optional regular expression to exclude PVCs from replication by name. | Standard `klog` flags are also supported for logging configuration. diff --git a/charts/volume-replicator/templates/deployment.yaml b/charts/volume-replicator/templates/deployment.yaml index 3fddd1a..bc86b1d 100644 --- a/charts/volume-replicator/templates/deployment.yaml +++ b/charts/volume-replicator/templates/deployment.yaml @@ -45,6 +45,10 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace + {{- with .Values.exclusionRegex }} + - name: EXCLUSION_REGEX + value: {{ . | quote }} + {{- end }} {{- with .Values.resources }} resources: {{- toYaml . | nindent 12 }} diff --git a/charts/volume-replicator/values.yaml b/charts/volume-replicator/values.yaml index 5499ece..0f73e1f 100644 --- a/charts/volume-replicator/values.yaml +++ b/charts/volume-replicator/values.yaml @@ -16,6 +16,12 @@ imagePullSecrets: [] nameOverride: "" fullnameOverride: "" +# Regex to exclude PVCs from being replicated based on their name +# The following regex will exclude all PVCs that start with prime- to prevent them from being replicated +# This is useful for temporary PVCs created by the Container Data Importer of Kubevirt +# exclusionRegex: "^prime-.*$" +exclusionRegex: "" + # This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ serviceAccount: # Specifies whether a service account should be created diff --git a/cmd/main.go b/cmd/main.go index a1498e4..57ece53 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,12 +3,13 @@ package main import ( "context" "flag" + "os" + "os/signal" + "github.com/skalanetworks/volume-replicator/internal/k8s" "github.com/skalanetworks/volume-replicator/internal/replicator" "k8s.io/client-go/tools/leaderelection" "k8s.io/klog/v2" - "os" - "os/signal" ) func main() { @@ -18,6 +19,7 @@ func main() { var kubeconfig, namespace string flag.StringVar(&kubeconfig, "kubeconfig", "", "path to kubeconfig file") flag.StringVar(&namespace, "namespace", os.Getenv("NAMESPACE"), "deployment namespace") + flag.StringVar(&replicator.ExclusionRegex, "exclusion-regex", os.Getenv("EXCLUSION_REGEX"), "regex to exclude PVCs from replication") klog.InitFlags(nil) flag.Parse() diff --git a/internal/replicator/utils.go b/internal/replicator/utils.go index 09bcff7..9f67aff 100644 --- a/internal/replicator/utils.go +++ b/internal/replicator/utils.go @@ -3,6 +3,7 @@ package replicator import ( "context" "fmt" + "regexp" "github.com/skalanetworks/volume-replicator/internal/constants" "github.com/skalanetworks/volume-replicator/internal/k8s" @@ -13,6 +14,8 @@ import ( "k8s.io/klog/v2" ) +var ExclusionRegex string + // isVolumeReplicationCorrect verifies if the definition of a VolumeReplication conforms to its originating PVC func isVolumeReplicationCorrect(pvc *corev1.PersistentVolumeClaim, vr *unstructured.Unstructured) bool { key := fmt.Sprintf("%s/%s", vr.GetNamespace(), vr.GetName()) @@ -168,3 +171,21 @@ func getPvcProvisioner(pvc *corev1.PersistentVolumeClaim) string { // Fallback to the deprecated annotation return pvc.Annotations[constants.DeprecatedStorageProvisionerAnnotation] } + +// pvcNameMatchesExclusion returns whether a PVC has a name matching the exclusion regex +func pvcNameMatchesExclusion(pvc *corev1.PersistentVolumeClaim) bool { + // If no regex is provided, return that it doesn't match + // This is to avoid Go matching "" as "everything matches" + if ExclusionRegex == "" { + return false + } + + // Match the user-provided regex + match, err := regexp.MatchString(ExclusionRegex, pvc.Name) + if err != nil { + klog.Errorf("failed to parse exclusion regex: %s", err.Error()) + return false + } + + return match +} diff --git a/internal/replicator/utils_test.go b/internal/replicator/utils_test.go index 41b3298..374faf2 100644 --- a/internal/replicator/utils_test.go +++ b/internal/replicator/utils_test.go @@ -662,3 +662,62 @@ func TestGetPvcProvisioner(t *testing.T) { }) } } + +func TestPvcNameMatchesExclusion(t *testing.T) { + tests := []struct { + name string + exclusionRegex string + pvcName string + expected bool + }{ + { + name: "Empty regex matches nothing", + exclusionRegex: "", + pvcName: "any-pvc", + expected: false, + }, + { + name: "Exact match", + exclusionRegex: "^test-pvc$", + pvcName: "test-pvc", + expected: true, + }, + { + name: "Prefix match", + exclusionRegex: "^skip-.*", + pvcName: "skip-this-pvc", + expected: true, + }, + { + name: "No match", + exclusionRegex: "^skip-.*", + pvcName: "dont-skip-this", + expected: false, + }, + { + name: "Partial match (regexp.MatchString behavior)", + exclusionRegex: "exclude", + pvcName: "pvc-to-exclude-here", + expected: true, + }, + { + name: "Invalid regex", + exclusionRegex: "[invalid", + pvcName: "any-pvc", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ExclusionRegex = tt.exclusionRegex + pvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: tt.pvcName, + }, + } + result := pvcNameMatchesExclusion(pvc) + require.Equal(t, tt.expected, result) + }) + } +} diff --git a/internal/replicator/vrc.go b/internal/replicator/vrc.go index dc33c4e..8f73e31 100644 --- a/internal/replicator/vrc.go +++ b/internal/replicator/vrc.go @@ -15,6 +15,12 @@ import ( // The VRC can be provided through annotations as a value or as a selector. // The annotations can be placed on the PVC or on its namespace. func getVolumeReplicationClass(pvc *corev1.PersistentVolumeClaim) string { + // If the PVC is to be excluded, return an empty replication class + if pvcNameMatchesExclusion(pvc) { + klog.Infof("PVC %s/%s matches exclusion pattern, no replication class to apply", pvc.Namespace, pvc.Name) + return "" + } + // Retrieve the literal VRC provided on the PVC value := getVolumeReplicationClassValue(pvc) if value != "" { diff --git a/internal/replicator/vrc_test.go b/internal/replicator/vrc_test.go index 1c4041d..f5bf9ac 100644 --- a/internal/replicator/vrc_test.go +++ b/internal/replicator/vrc_test.go @@ -191,12 +191,33 @@ func TestGetVolumeReplicationClass(t *testing.T) { }, expectedResult: "", }, + { + name: "Excluded by name regex", + pvc: &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "exclude-this-pvc", + Namespace: nsName, + Annotations: map[string]string{ + constants.VrcValueAnnotation: vrcName, + }, + }, + }, + expectedResult: "", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { clearNamespaceIndexer(t) + // Set ExclusionRegex specifically for each test case + if tt.name == "Excluded by name regex" { + ExclusionRegex = "exclude-.*" + } else { + ExclusionRegex = "" + } + defer func() { ExclusionRegex = "" }() + if tt.namespace != nil { err := NamespaceInformer.Informer().GetIndexer().Add(tt.namespace) require.NoError(t, err)