Skip to content
Merged
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -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.

Expand Down
4 changes: 4 additions & 0 deletions charts/volume-replicator/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
6 changes: 6 additions & 0 deletions charts/volume-replicator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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()

Expand Down
21 changes: 21 additions & 0 deletions internal/replicator/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package replicator
import (
"context"
"fmt"
"regexp"

"github.com/skalanetworks/volume-replicator/internal/constants"
"github.com/skalanetworks/volume-replicator/internal/k8s"
Expand All @@ -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())
Expand Down Expand Up @@ -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
}
59 changes: 59 additions & 0 deletions internal/replicator/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
6 changes: 6 additions & 0 deletions internal/replicator/vrc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down
21 changes: 21 additions & 0 deletions internal/replicator/vrc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down