Merge branch 'main' into optimize-sub-object-description

This commit is contained in:
Lyndon-Li
2026-07-28 15:14:15 +08:00
94 changed files with 3779 additions and 1335 deletions
+22
View File
@@ -0,0 +1,22 @@
/*
Copyright The Velero Contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package shared
const (
DataUploadParentSnapshotNone = "none"
DataUploadParentSnapshotAuto = "auto"
)
+8
View File
@@ -166,6 +166,14 @@ const (
// Velero checks this annotation to determine whether to skip resource excluding check.
MustIncludeAdditionalItemAnnotation = "backup.velero.io/must-include-additional-items"
// MustIncludeAdditionalItemRestoreAnnotation is set by RestoreItemActions on the UpdatedItem
// to tell Velero to bypass global resource/namespace exclusion checks (and IncludeClusterResources=false)
// for that action's AdditionalItems. Value must be "true" to enable the bypass. The annotation is
// always stripped before the item is applied to the cluster when present, including non-"true" values.
//
// Notice: SkipRestore on the Execute output takes precedence. If SkipRestore is true, the
// annotation is never inspected and AdditionalItems are not processed.
MustIncludeAdditionalItemRestoreAnnotation = "restore.velero.io/must-include-additional-items"
// SkippedNoCSIPVAnnotation - Velero checks this annotation on processed PVC to
// find out if the snapshot was skipped b/c the PV is not provisioned via CSI
SkippedNoCSIPVAnnotation = "backup.velero.io/skipped-no-csi-pv"
@@ -64,6 +64,12 @@ type DataUploadSpec struct {
// SourceFSType is the file system type of the source volume.
// +optional
SourceFSType string `json:"sourceFSType,omitempty"`
// ParentSnapshot specifies the parent snapshot that current backup is based on.
// If its value is "" or "auto", the data mover finds the recent backup of the same volume as parent.
// If its value is "none", the data mover will do a full backup
// If its value is a specific snapshotID, the data mover finds the specific snapshot as parent.
ParentSnapshot string `json:"parentSnapshot,omitempty"`
}
type SnapshotType string
+12
View File
@@ -42,6 +42,7 @@ import (
crclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
veleroshared "github.com/vmware-tanzu/velero/pkg/apis/velero/shared"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1"
veleroclient "github.com/vmware-tanzu/velero/pkg/client"
@@ -535,6 +536,16 @@ func newDataUpload(
vsc *snapshotv1api.VolumeSnapshotContent,
fsType string,
) *velerov2alpha1.DataUpload {
var parentSnapshot string
switch backup.Spec.BackupType {
case velerov1api.BackupTypeFull:
parentSnapshot = veleroshared.DataUploadParentSnapshotNone
case velerov1api.BackupTypeIncremental:
parentSnapshot = veleroshared.DataUploadParentSnapshotAuto
default:
parentSnapshot = veleroshared.DataUploadParentSnapshotAuto
}
dataUpload := &velerov2alpha1.DataUpload{
TypeMeta: metav1.TypeMeta{
APIVersion: velerov2alpha1.SchemeGroupVersion.String(),
@@ -572,6 +583,7 @@ func newDataUpload(
SourceNamespace: pvc.Namespace,
OperationTimeout: backup.Spec.CSISnapshotTimeout,
SourceFSType: fsType,
ParentSnapshot: parentSnapshot,
},
}
+140 -12
View File
@@ -23,40 +23,39 @@ import (
"testing"
"time"
"github.com/vmware-tanzu/velero/pkg/kuberesource"
volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2"
"github.com/stretchr/testify/assert"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
"github.com/vmware-tanzu/velero/pkg/label"
"github.com/cockroachdb/errors"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
"github.com/cockroachdb/errors"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1api "k8s.io/api/core/v1"
storagev1api "k8s.io/api/storage/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/utils/ptr"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/vmware-tanzu/velero/pkg/apis/velero/shared"
veleroshared "github.com/vmware-tanzu/velero/pkg/apis/velero/shared"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1"
"github.com/vmware-tanzu/velero/pkg/builder"
factorymocks "github.com/vmware-tanzu/velero/pkg/client/mocks"
"github.com/vmware-tanzu/velero/pkg/kuberesource"
"github.com/vmware-tanzu/velero/pkg/label"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
uploaderUtil "github.com/vmware-tanzu/velero/pkg/uploader/util"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
)
const testDriver = "csi.example.com"
@@ -163,6 +162,7 @@ func TestExecute(t *testing.T) {
SourcePVC: "testPVC",
SourceNamespace: "velero",
OperationTimeout: metav1.Duration{Duration: 1 * time.Minute},
ParentSnapshot: veleroshared.DataUploadParentSnapshotAuto,
},
},
},
@@ -2176,3 +2176,131 @@ func TestGetOrCreateVolumeHelper(t *testing.T) {
// The pvcPodCache should be the same instance
require.Same(t, cache1, action.pvcPodCache, "Expected same pvcPodCache instance on repeated calls")
}
func TestNewDataUpload(t *testing.T) {
tests := []struct {
name string
backupType velerov1api.BackupType
vsClassName *string
uploaderConfig *velerov1api.UploaderConfigForBackup
expectedParentSnap string
expectedDataMoverCfg map[string]string
}{
{
name: "Full backup type, no uploader config, no vs class name",
backupType: velerov1api.BackupTypeFull,
vsClassName: nil,
uploaderConfig: nil,
expectedParentSnap: "none",
expectedDataMoverCfg: nil,
},
{
name: "Incremental backup type, with uploader config, with vs class name",
backupType: velerov1api.BackupTypeIncremental,
vsClassName: ptr.To("test-vs-class"),
uploaderConfig: &velerov1api.UploaderConfigForBackup{ParallelFilesUpload: 10},
expectedParentSnap: "auto",
expectedDataMoverCfg: map[string]string{
uploaderUtil.ParallelFilesUpload: "10",
},
},
{
name: "Default backup type, uploader config with 0 parallel files",
backupType: "",
vsClassName: ptr.To("test-vs-class"),
uploaderConfig: &velerov1api.UploaderConfigForBackup{ParallelFilesUpload: 0},
expectedParentSnap: "auto",
expectedDataMoverCfg: nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
backup := &velerov1api.Backup{
ObjectMeta: metav1.ObjectMeta{
Name: "test-backup",
Namespace: "velero",
UID: types.UID("backup-uid"),
},
Spec: velerov1api.BackupSpec{
BackupType: tc.backupType,
DataMover: "velero",
StorageLocation: "default",
CSISnapshotTimeout: metav1.Duration{Duration: 10 * time.Minute},
UploaderConfig: tc.uploaderConfig,
},
}
vs := &snapshotv1api.VolumeSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: "test-vs",
},
Spec: snapshotv1api.VolumeSnapshotSpec{
VolumeSnapshotClassName: tc.vsClassName,
},
}
pvc := &corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pvc",
Namespace: "test-ns",
UID: types.UID("pvc-uid"),
},
Spec: corev1api.PersistentVolumeClaimSpec{
StorageClassName: ptr.To("test-storage-class"),
},
}
vsc := &snapshotv1api.VolumeSnapshotContent{
Spec: snapshotv1api.VolumeSnapshotContentSpec{
Driver: "test-driver",
},
}
operationID := "test-op-id"
fsType := "ext4"
du := newDataUpload(backup, vs, pvc, operationID, vsc, fsType)
require.NotNil(t, du)
assert.Equal(t, velerov2alpha1.SchemeGroupVersion.String(), du.APIVersion)
assert.Equal(t, "DataUpload", du.Kind)
assert.Equal(t, backup.Namespace, du.Namespace)
assert.Equal(t, backup.Name+"-", du.GenerateName)
require.Len(t, du.OwnerReferences, 1)
assert.Equal(t, velerov1api.SchemeGroupVersion.String(), du.OwnerReferences[0].APIVersion)
assert.Equal(t, "Backup", du.OwnerReferences[0].Kind)
assert.Equal(t, backup.Name, du.OwnerReferences[0].Name)
assert.Equal(t, backup.UID, du.OwnerReferences[0].UID)
assert.Equal(t, boolptr.True(), du.OwnerReferences[0].Controller)
expectedLabels := map[string]string{
velerov1api.BackupNameLabel: label.GetValidName(backup.Name),
velerov1api.BackupUIDLabel: string(backup.UID),
velerov1api.PVCUIDLabel: string(pvc.UID),
velerov1api.AsyncOperationIDLabel: operationID,
}
assert.Equal(t, expectedLabels, du.Labels)
assert.Equal(t, velerov2alpha1.SnapshotTypeCSI, du.Spec.SnapshotType)
assert.Equal(t, vs.Name, du.Spec.CSISnapshot.VolumeSnapshot)
assert.Equal(t, *pvc.Spec.StorageClassName, du.Spec.CSISnapshot.StorageClass)
assert.Equal(t, vsc.Spec.Driver, du.Spec.CSISnapshot.Driver)
if tc.vsClassName != nil {
assert.Equal(t, *tc.vsClassName, du.Spec.CSISnapshot.SnapshotClass)
} else {
assert.Empty(t, du.Spec.CSISnapshot.SnapshotClass)
}
assert.Equal(t, pvc.Name, du.Spec.SourcePVC)
assert.Equal(t, backup.Spec.DataMover, du.Spec.DataMover)
assert.Equal(t, backup.Spec.StorageLocation, du.Spec.BackupStorageLocation)
assert.Equal(t, pvc.Namespace, du.Spec.SourceNamespace)
assert.Equal(t, backup.Spec.CSISnapshotTimeout, du.Spec.OperationTimeout)
assert.Equal(t, fsType, du.Spec.SourceFSType)
assert.Equal(t, tc.expectedParentSnap, du.Spec.ParentSnapshot)
assert.Equal(t, tc.expectedDataMoverCfg, du.Spec.DataMoverConfig)
})
}
}
+7 -9
View File
@@ -1428,22 +1428,20 @@ func resolveClusterScopedFilterPolicy(
}
func resolveResourceFilter(rf resourcepolicies.ResourceFilter) (*ResolvedResourceFilter, error) {
var selector labels.Selector
if len(rf.LabelSelector) > 0 {
var err error
selector, err = labels.ValidatedSelectorFromSet(labels.Set(rf.LabelSelector))
if err != nil {
return nil, fmt.Errorf("invalid label selector in resource filter: %w", err)
}
selector, err := resourcepolicies.SelectorFromPolicyLabelSelector(rf.LabelSelector)
if err != nil {
return nil, fmt.Errorf("invalid label selector in resource filter: %w", err)
}
var orSelectors []labels.Selector
for _, ols := range rf.OrLabelSelectors {
s, err := labels.ValidatedSelectorFromSet(labels.Set(ols))
s, err := resourcepolicies.SelectorFromPolicyLabelSelector(ols)
if err != nil {
return nil, fmt.Errorf("invalid OR label selector in resource filter: %w", err)
}
orSelectors = append(orSelectors, s)
if s != nil {
orSelectors = append(orSelectors, s)
}
}
var nameIE *collections.IncludesExcludes
+77 -15
View File
@@ -5741,7 +5741,7 @@ func TestResolveResourceFilter(t *testing.T) {
{
name: "valid label selector",
rf: resourcepolicies.ResourceFilter{
LabelSelector: map[string]string{"app": "foo"},
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"app": "foo"}},
},
expectErr: false,
checkResult: func(t *testing.T, r *ResolvedResourceFilter) {
@@ -5754,16 +5754,16 @@ func TestResolveResourceFilter(t *testing.T) {
{
name: "invalid label selector",
rf: resourcepolicies.ResourceFilter{
LabelSelector: map[string]string{"invalid/label/key": "value"},
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"invalid/label/key": "value"}},
},
expectErr: true,
},
{
name: "valid or label selectors",
rf: resourcepolicies.ResourceFilter{
OrLabelSelectors: []map[string]string{
{"app": "foo"},
{"app": "bar"},
OrLabelSelectors: []*resourcepolicies.PolicyLabelSelector{
{MatchLabels: map[string]string{"app": "foo"}},
{MatchLabels: map[string]string{"app": "bar"}},
},
},
expectErr: false,
@@ -5776,8 +5776,8 @@ func TestResolveResourceFilter(t *testing.T) {
{
name: "invalid or label selectors",
rf: resourcepolicies.ResourceFilter{
OrLabelSelectors: []map[string]string{
{"invalid/label/key": "value"},
OrLabelSelectors: []*resourcepolicies.PolicyLabelSelector{
{MatchLabels: map[string]string{"invalid/label/key": "value"}},
},
},
expectErr: true,
@@ -5797,6 +5797,68 @@ func TestResolveResourceFilter(t *testing.T) {
assert.False(t, r.NameIE.ShouldInclude("exc1"))
},
},
{
name: "empty labelSelector is no filter",
rf: resourcepolicies.ResourceFilter{
LabelSelector: &resourcepolicies.PolicyLabelSelector{},
},
expectErr: false,
checkResult: func(t *testing.T, r *ResolvedResourceFilter) {
t.Helper()
require.NotNil(t, r)
assert.Nil(t, r.LabelSelector)
},
},
{
name: "set-based In and DoesNotExist",
rf: resourcepolicies.ResourceFilter{
LabelSelector: &resourcepolicies.PolicyLabelSelector{
MatchExpressions: []resourcepolicies.PolicyLabelSelectorRequirement{
{Key: "environment", Operator: "In", Values: []string{"prod", "staging"}},
{Key: "do-not-backup", Operator: "DoesNotExist"},
},
},
},
expectErr: false,
checkResult: func(t *testing.T, r *ResolvedResourceFilter) {
t.Helper()
require.NotNil(t, r.LabelSelector)
assert.True(t, r.LabelSelector.Matches(labels.Set{"environment": "prod"}))
assert.True(t, r.LabelSelector.Matches(labels.Set{"environment": "staging"}))
assert.False(t, r.LabelSelector.Matches(labels.Set{"environment": "dev"}))
assert.False(t, r.LabelSelector.Matches(labels.Set{"environment": "prod", "do-not-backup": "true"}))
},
},
{
name: "set-based NotIn and Exists",
rf: resourcepolicies.ResourceFilter{
LabelSelector: &resourcepolicies.PolicyLabelSelector{
MatchExpressions: []resourcepolicies.PolicyLabelSelectorRequirement{
{Key: "tier", Operator: "NotIn", Values: []string{"debug"}},
{Key: "app", Operator: "Exists"},
},
},
},
expectErr: false,
checkResult: func(t *testing.T, r *ResolvedResourceFilter) {
t.Helper()
require.NotNil(t, r.LabelSelector)
assert.True(t, r.LabelSelector.Matches(labels.Set{"app": "web", "tier": "frontend"}))
assert.False(t, r.LabelSelector.Matches(labels.Set{"app": "web", "tier": "debug"}))
assert.False(t, r.LabelSelector.Matches(labels.Set{"tier": "frontend"}))
},
},
{
name: "invalid operator",
rf: resourcepolicies.ResourceFilter{
LabelSelector: &resourcepolicies.PolicyLabelSelector{
MatchExpressions: []resourcepolicies.PolicyLabelSelectorRequirement{
{Key: "env", Operator: "Equals", Values: []string{"prod"}},
},
},
},
expectErr: true,
},
}
for _, tc := range tests {
@@ -5834,11 +5896,11 @@ func TestResolveClusterScopedFilterPolicy(t *testing.T) {
ResourceFilters: []resourcepolicies.ResourceFilter{
{
Kinds: []string{"pods", "secrets"},
LabelSelector: map[string]string{"app": "foo"},
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"app": "foo"}},
},
{
Kinds: []string{"invalid-kind"},
LabelSelector: map[string]string{"invalid/label/key": "value"},
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"invalid/label/key": "value"}},
},
},
}
@@ -5852,7 +5914,7 @@ func TestResolveClusterScopedFilterPolicy(t *testing.T) {
ResourceFilters: []resourcepolicies.ResourceFilter{
{
Kinds: []string{"pods", "secrets"},
LabelSelector: map[string]string{"app": "foo"},
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"app": "foo"}},
},
},
}
@@ -5900,11 +5962,11 @@ func TestResolveNamespacedFilterPolicies(t *testing.T) {
ResourceFilters: []resourcepolicies.ResourceFilter{
{
Kinds: []string{"pods"},
LabelSelector: map[string]string{"app": "foo"},
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"app": "foo"}},
},
{
Kinds: []string{"*"},
LabelSelector: map[string]string{"catch": "all"},
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"catch": "all"}},
},
},
},
@@ -5932,7 +5994,7 @@ func TestResolveNamespacedFilterPolicies(t *testing.T) {
ResourceFilters: []resourcepolicies.ResourceFilter{
{
Kinds: []string{"pods"},
LabelSelector: map[string]string{"invalid/label/key": "value"},
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"invalid/label/key": "value"}},
},
},
},
@@ -6016,7 +6078,7 @@ func TestBackupWithResPoliciesLogs(t *testing.T) {
ResourceFilters: []resourcepolicies.ResourceFilter{
{
Kinds: []string{"pods"},
LabelSelector: map[string]string{"invalid/label/key": "value"},
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"invalid/label/key": "value"}},
},
},
}
@@ -6035,7 +6097,7 @@ func TestBackupWithResPoliciesLogs(t *testing.T) {
ResourceFilters: []resourcepolicies.ResourceFilter{
{
Kinds: []string{"pods"},
LabelSelector: map[string]string{"invalid/label/key": "value"},
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"invalid/label/key": "value"}},
},
},
},
+5 -1
View File
@@ -15,6 +15,7 @@ package datamover
import (
"context"
"crypto/fips140"
"fmt"
"os"
"strings"
@@ -87,7 +88,10 @@ func NewBackupCommand(f client.Factory) *cobra.Command {
kube.ExitPodWithMessage(logger, false, "Failed to create data mover backup, %v", err)
}
s.run()
// Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet.
fips140.WithoutEnforcement(func() {
s.run()
})
},
}
+5 -1
View File
@@ -15,6 +15,7 @@ package datamover
import (
"context"
"crypto/fips140"
"fmt"
"os"
"strings"
@@ -81,7 +82,10 @@ func NewRestoreCommand(f client.Factory) *cobra.Command {
kube.ExitPodWithMessage(logger, false, "Failed to create data mover restore, %v", err)
}
s.run()
// Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet.
fips140.WithoutEnforcement(func() {
s.run()
})
},
}
+5 -1
View File
@@ -15,6 +15,7 @@ package podvolume
import (
"context"
"crypto/fips140"
"fmt"
"os"
"strings"
@@ -80,7 +81,10 @@ func NewBackupCommand(f client.Factory) *cobra.Command {
kube.ExitPodWithMessage(logger, false, "Failed to create pod volume backup, %v", err)
}
s.run()
// Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet.
fips140.WithoutEnforcement(func() {
s.run()
})
},
}
+5 -1
View File
@@ -15,6 +15,7 @@ package podvolume
import (
"context"
"crypto/fips140"
"fmt"
"os"
"strings"
@@ -79,7 +80,10 @@ func NewRestoreCommand(f client.Factory) *cobra.Command {
kube.ExitPodWithMessage(logger, false, "Failed to create pod volume restore, %v", err)
}
s.run()
// Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet.
fips140.WithoutEnforcement(func() {
s.run()
})
},
}
+5 -1
View File
@@ -2,6 +2,7 @@ package repomantenance
import (
"context"
"crypto/fips140"
"fmt"
"os"
"strings"
@@ -57,7 +58,10 @@ func NewCommand(f velerocli.Factory) *cobra.Command {
Hidden: true,
Short: "VELERO INTERNAL COMMAND ONLY - not intended to be run directly by users",
Run: func(c *cobra.Command, args []string) {
o.Run(f)
// Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet.
fips140.WithoutEnforcement(func() {
o.Run(f)
})
},
}
-118
View File
@@ -21,7 +21,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"sort"
"strconv"
"strings"
@@ -31,7 +30,6 @@ import (
"github.com/cockroachdb/errors"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
"github.com/sirupsen/logrus"
"github.com/fatih/color"
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
@@ -94,9 +92,6 @@ func DescribeBackup(
if backup.Spec.ResourcePolicy != nil {
d.Println()
DescribeResourcePolicies(d, backup.Spec.ResourcePolicy)
// Display fine-grained filter policies if they exist
DescribeFineGrainedFilterPolicies(ctx, kbClient, d, backup)
}
DescribeGlobalVolumePolicy(d, backup)
@@ -151,119 +146,6 @@ func DescribeGlobalVolumePolicy(d *Describer, backup *velerov1api.Backup) {
d.Printf("\tName:\t%s\n", name)
}
// DescribeFineGrainedFilterPolicies describes cluster-scoped and namespace-scoped filter policies if present
func DescribeFineGrainedFilterPolicies(ctx context.Context, kbClient kbclient.Client, d *Describer, backup *velerov1api.Backup) {
if backup.Spec.ResourcePolicy == nil {
return
}
// Create a discard logger for the resource policies function since this is CLI output context
discardLogger := logrus.New()
discardLogger.Out = io.Discard
resourcePolicies, err := resourcepolicies.GetResourcePoliciesFromBackup(*backup, kbClient, discardLogger)
if err != nil {
// Don't fail the describe if we can't read policies, just skip
return
}
if resourcePolicies == nil {
return
}
clusterScopedFilterPolicy := resourcePolicies.GetClusterScopedFilterPolicy()
if clusterScopedFilterPolicy != nil {
d.Printf("\nCluster Scoped Filter Policy:\n")
d.Printf(" Resource Filters:\n")
for _, rf := range clusterScopedFilterPolicy.ResourceFilters {
kindsStr := strings.Join(rf.Kinds, ", ")
d.Printf(" %s:\n", kindsStr)
// Label selector
if len(rf.LabelSelector) > 0 {
selectorStr := formatLabelMap(rf.LabelSelector)
d.Printf(" Label selector: %s\n", selectorStr)
} else if len(rf.OrLabelSelectors) > 0 {
var orStrs []string
for _, ols := range rf.OrLabelSelectors {
orStrs = append(orStrs, formatLabelMap(ols))
}
d.Printf(" OR label selectors: [%s]\n", strings.Join(orStrs, ", "))
} else {
d.Printf(" Label selector: <none>\n")
}
// Name patterns
if len(rf.Names) > 0 {
d.Printf(" Included names: [%s]\n", strings.Join(rf.Names, ", "))
} else {
d.Printf(" Included names: <none>\n")
}
if len(rf.ExcludedNames) > 0 {
d.Printf(" Excluded names: [%s]\n", strings.Join(rf.ExcludedNames, ", "))
} else {
d.Printf(" Excluded names: <none>\n")
}
}
}
nfPolicies := resourcePolicies.GetNamespacedFilterPolicies()
if len(nfPolicies) > 0 {
d.Printf("\nNamespace-Scoped Filter Policies:\n")
for _, policy := range nfPolicies {
for _, ns := range policy.Namespaces {
d.Printf(" %s:\n", ns)
d.Printf(" Resource Filters:\n")
for _, rf := range policy.ResourceFilters {
var kindsStr string
if rf.IsCatchAll() {
kindsStr = "<catch-all> (all other kinds)"
} else {
kindsStr = strings.Join(rf.Kinds, ", ")
}
d.Printf(" %s:\n", kindsStr)
// Label selector
if len(rf.LabelSelector) > 0 {
selectorStr := formatLabelMap(rf.LabelSelector)
d.Printf(" Label selector: %s\n", selectorStr)
} else if len(rf.OrLabelSelectors) > 0 {
var orStrs []string
for _, ols := range rf.OrLabelSelectors {
orStrs = append(orStrs, formatLabelMap(ols))
}
d.Printf(" OR label selectors: [%s]\n", strings.Join(orStrs, ", "))
} else {
d.Printf(" Label selector: <none>\n")
}
// Name patterns
if len(rf.Names) > 0 {
d.Printf(" Included names: [%s]\n", strings.Join(rf.Names, ", "))
} else {
d.Printf(" Included names: <none>\n")
}
if len(rf.ExcludedNames) > 0 {
d.Printf(" Excluded names: [%s]\n", strings.Join(rf.ExcludedNames, ", "))
} else {
d.Printf(" Excluded names: <none>\n")
}
}
}
}
}
}
func formatLabelMap(labelMap map[string]string) string {
var pairs []string
for k, v := range labelMap {
pairs = append(pairs, fmt.Sprintf("%s=%s", k, v))
}
return strings.Join(pairs, ",")
}
// DescribeUploaderConfigForBackup describes uploader config in human-readable format
func DescribeUploaderConfigForBackup(d *Describer, spec velerov1api.BackupSpec) {
d.Printf("Uploader config:\n")
@@ -18,7 +18,6 @@ package output
import (
"bytes"
"context"
"testing"
"text/tabwriter"
"time"
@@ -26,8 +25,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/vmware-tanzu/velero/internal/volume"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
@@ -897,85 +894,3 @@ func TestDescribeBackupItemOperation(t *testing.T) {
d.out.Flush()
assert.Equal(t, expected, d.buf.String())
}
func TestDescribeFineGrainedFilterPolicies(t *testing.T) {
yamlData := `
version: v1
clusterScopedFilterPolicy:
resourceFilters:
- kinds: ["StorageClass"]
labelSelector: {"app": "velero"}
- kinds: ["ClusterRole"]
orLabelSelectors:
- {"app": "velero"}
- {"app": "test"}
names: ["role1"]
excludedNames: ["role2"]
namespacedFilterPolicies:
- namespaces: ["ns1", "ns2"]
resourceFilters:
- kinds: ["Pod", "ConfigMap"]
labelSelector: {"app": "velero"}
- kinds: ["*"]
`
cm := &corev1api.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-policy",
Namespace: "velero",
},
Data: map[string]string{
"policy.yaml": yamlData,
},
}
client := fake.NewClientBuilder().WithRuntimeObjects(cm).Build()
backup := builder.ForBackup("velero", "test-backup").
ResourcePolicies("test-policy").Result()
d := &Describer{
Prefix: "",
out: &tabwriter.Writer{},
buf: &bytes.Buffer{},
}
d.out.Init(d.buf, 0, 8, 2, ' ', 0)
DescribeFineGrainedFilterPolicies(context.Background(), client, d, backup)
d.out.Flush()
expected := `
Cluster Scoped Filter Policy:
Resource Filters:
StorageClass:
Label selector: app=velero
Included names: <none>
Excluded names: <none>
ClusterRole:
OR label selectors: [app=velero, app=test]
Included names: [role1]
Excluded names: [role2]
Namespace-Scoped Filter Policies:
ns1:
Resource Filters:
Pod, ConfigMap:
Label selector: app=velero
Included names: <none>
Excluded names: <none>
<catch-all> (all other kinds):
Label selector: <none>
Included names: <none>
Excluded names: <none>
ns2:
Resource Filters:
Pod, ConfigMap:
Label selector: app=velero
Included names: <none>
Excluded names: <none>
<catch-all> (all other kinds):
Label selector: <none>
Included names: <none>
Excluded names: <none>
`
assert.Equal(t, expected, d.buf.String())
}
@@ -21,10 +21,8 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"strings"
"github.com/sirupsen/logrus"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -57,7 +55,6 @@ func DescribeBackupInSF(
if backup.Spec.ResourcePolicy != nil {
DescribeResourcePoliciesInSF(d, backup.Spec.ResourcePolicy)
DescribeFineGrainedFilterPoliciesInSF(ctx, kbClient, d, backup)
}
DescribeGlobalVolumePolicyInSF(d, backup)
@@ -228,88 +225,6 @@ func DescribeBackupSpecInSF(d *StructuredDescriber, spec velerov1api.BackupSpec)
d.Describe("spec", backupSpecInfo)
}
// DescribeFineGrainedFilterPoliciesInSF adds the clusterScopedFilterPolicy
// and namespacedFilterPolicies sections to the structured describer output when present
// in the ResourcePolicy ConfigMap referenced by the backup.
func DescribeFineGrainedFilterPoliciesInSF(ctx context.Context, kbClient kbclient.Client, d *StructuredDescriber, backup *velerov1api.Backup) {
if backup.Spec.ResourcePolicy == nil {
return
}
discardLogger := logrus.New()
discardLogger.Out = io.Discard
resPolicies, err := resourcepolicies.GetResourcePoliciesFromBackup(*backup, kbClient, discardLogger)
if err != nil || resPolicies == nil {
return
}
clusterScopedFilterPolicy := resPolicies.GetClusterScopedFilterPolicy()
if clusterScopedFilterPolicy != nil {
var clusterScopedFilters []map[string]any
for _, rf := range clusterScopedFilterPolicy.ResourceFilters {
entry := map[string]any{
"kinds": rf.Kinds,
}
if len(rf.LabelSelector) > 0 {
entry["labelSelector"] = rf.LabelSelector
}
if len(rf.OrLabelSelectors) > 0 {
entry["orLabelSelectors"] = rf.OrLabelSelectors
}
if len(rf.Names) > 0 {
entry["names"] = rf.Names
}
if len(rf.ExcludedNames) > 0 {
entry["excludedNames"] = rf.ExcludedNames
}
clusterScopedFilters = append(clusterScopedFilters, entry)
}
d.Describe("clusterScopedFilterPolicy", map[string]any{
"resourceFilters": clusterScopedFilters,
})
}
nfPolicies := resPolicies.GetNamespacedFilterPolicies()
if len(nfPolicies) == 0 {
return
}
var structuredPolicies []map[string]any
for _, policy := range nfPolicies {
for _, ns := range policy.Namespaces {
var rfEntries []map[string]any
for _, rf := range policy.ResourceFilters {
entry := map[string]any{}
if rf.IsCatchAll() {
entry["kinds"] = []string{}
entry["isCatchAll"] = true
} else {
entry["kinds"] = rf.Kinds
}
if len(rf.LabelSelector) > 0 {
entry["labelSelector"] = rf.LabelSelector
}
if len(rf.OrLabelSelectors) > 0 {
entry["orLabelSelectors"] = rf.OrLabelSelectors
}
if len(rf.Names) > 0 {
entry["names"] = rf.Names
}
if len(rf.ExcludedNames) > 0 {
entry["excludedNames"] = rf.ExcludedNames
}
rfEntries = append(rfEntries, entry)
}
structuredPolicies = append(structuredPolicies, map[string]any{
"namespace": ns,
"resourceFilters": rfEntries,
})
}
}
d.Describe("namespacedFilterPolicies", structuredPolicies)
}
// DescribeBackupStatusInSF describes a backup status in structured format.
func DescribeBackupStatusInSF(ctx context.Context, kbClient kbclient.Client, d *StructuredDescriber, backup *velerov1api.Backup, details bool,
insecureSkipTLSVerify bool, caCertPath string, podVolumeBackups []velerov1api.PodVolumeBackup) {
@@ -17,7 +17,6 @@ limitations under the License.
package output
import (
"context"
"reflect"
"testing"
"time"
@@ -25,8 +24,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/vmware-tanzu/velero/internal/volume"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
@@ -731,96 +728,3 @@ func TestDescribeDeleteBackupRequestsInSF(t *testing.T) {
})
}
}
func TestDescribeFineGrainedFilterPoliciesInSF(t *testing.T) {
yamlData := `
version: v1
clusterScopedFilterPolicy:
resourceFilters:
- kinds: ["StorageClass"]
labelSelector: {"app": "velero"}
- kinds: ["ClusterRole"]
orLabelSelectors:
- {"app": "velero"}
- {"app": "test"}
names: ["role1"]
excludedNames: ["role2"]
namespacedFilterPolicies:
- namespaces: ["ns1", "ns2"]
resourceFilters:
- kinds: ["Pod", "ConfigMap"]
labelSelector: {"app": "velero"}
- kinds: ["*"]
`
cm := &corev1api.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-policy",
Namespace: "velero",
},
Data: map[string]string{
"policy.yaml": yamlData,
},
}
client := fake.NewClientBuilder().WithRuntimeObjects(cm).Build()
backup := builder.ForBackup("velero", "test-backup").
ResourcePolicies("test-policy").Result()
sd := &StructuredDescriber{
output: make(map[string]any),
format: "",
}
DescribeFineGrainedFilterPoliciesInSF(context.Background(), client, sd, backup)
expect := map[string]any{
"clusterScopedFilterPolicy": map[string]any{
"resourceFilters": []map[string]any{
{
"kinds": []string{"StorageClass"},
"labelSelector": map[string]string{"app": "velero"},
},
{
"kinds": []string{"ClusterRole"},
"orLabelSelectors": []map[string]string{
{"app": "velero"},
{"app": "test"},
},
"names": []string{"role1"},
"excludedNames": []string{"role2"},
},
},
},
"namespacedFilterPolicies": []map[string]any{
{
"namespace": "ns1",
"resourceFilters": []map[string]any{
{
"kinds": []string{"Pod", "ConfigMap"},
"labelSelector": map[string]string{"app": "velero"},
},
{
"kinds": []string{},
"isCatchAll": true,
},
},
},
{
"namespace": "ns2",
"resourceFilters": []map[string]any{
{
"kinds": []string{"Pod", "ConfigMap"},
"labelSelector": map[string]string{"app": "velero"},
},
{
"kinds": []string{},
"isCatchAll": true,
},
},
},
},
}
assert.True(t, reflect.DeepEqual(sd.output, expect))
}
+59 -43
View File
@@ -84,33 +84,34 @@ var autoExcludeClusterScopedResources = []string{
}
type backupReconciler struct {
ctx context.Context
logger logrus.FieldLogger
discoveryHelper discovery.Helper
backupper pkgbackup.Backupper
kbClient kbclient.Client
clock clock.WithTickerAndDelayedExecution
backupLogLevel logrus.Level
newPluginManager func(logrus.FieldLogger) clientmgmt.Manager
backupTracker BackupTracker
defaultBackupLocation string
defaultVolumesToFsBackup bool
defaultBackupTTL time.Duration
defaultVGSLabelKey string
defaultCSISnapshotTimeout time.Duration
resourceTimeout time.Duration
defaultItemOperationTimeout time.Duration
defaultSnapshotLocations map[string]string
metrics *metrics.ServerMetrics
backupStoreGetter persistence.ObjectBackupStoreGetter
formatFlag logging.Format
credentialFileStore credentials.FileStore
maxConcurrentK8SConnections int
defaultSnapshotMoveData bool
globalCRClient kbclient.Client
itemBlockWorkerCount int
concurrentBackups int
globalVolumePoliciesConfigMap string
ctx context.Context
logger logrus.FieldLogger
discoveryHelper discovery.Helper
backupper pkgbackup.Backupper
kbClient kbclient.Client
clock clock.WithTickerAndDelayedExecution
backupLogLevel logrus.Level
newPluginManager func(logrus.FieldLogger) clientmgmt.Manager
backupTracker BackupTracker
defaultBackupLocation string
defaultVolumesToFsBackup bool
defaultBackupTTL time.Duration
defaultVGSLabelKey string
defaultCSISnapshotTimeout time.Duration
resourceTimeout time.Duration
defaultItemOperationTimeout time.Duration
defaultSnapshotLocations map[string]string
metrics *metrics.ServerMetrics
backupStoreGetter persistence.ObjectBackupStoreGetter
formatFlag logging.Format
credentialFileStore credentials.FileStore
maxConcurrentK8SConnections int
defaultSnapshotMoveData bool
globalCRClient kbclient.Client
itemBlockWorkerCount int
concurrentBackups int
globalVolumePoliciesConfigMap string
knownSchedulesWithSuccessfulBackup sets.Set[string]
}
func NewBackupReconciler(
@@ -204,28 +205,43 @@ func (b *backupReconciler) updateTotalBackupMetric() {
time.Sleep(5 * time.Second)
wait.Until(
func() {
// recompute backup_total metric
backups := &velerov1api.BackupList{}
err := b.kbClient.List(context.Background(), backups, &kbclient.ListOptions{LabelSelector: labels.Everything()})
if err != nil {
b.logger.Error(err, "Error computing backup_total metric")
} else {
b.metrics.SetBackupTotal(int64(len(backups.Items)))
}
// recompute backup_last_successful_timestamp metric for each
// schedule (including the empty schedule, i.e. ad-hoc backups)
for schedule, timestamp := range getLastSuccessBySchedule(backups.Items) {
b.metrics.SetBackupLastSuccessfulTimestamp(schedule, timestamp)
}
},
b.resyncBackupMetrics,
backupResyncPeriod,
b.ctx.Done(),
)
}()
}
func (b *backupReconciler) resyncBackupMetrics() {
backups := &velerov1api.BackupList{}
err := b.kbClient.List(context.Background(), backups, &kbclient.ListOptions{LabelSelector: labels.Everything()})
if err != nil {
b.logger.Error(err, "Error computing backup_total metric")
return
}
b.metrics.SetBackupTotal(int64(len(backups.Items)))
currentSchedules := getLastSuccessBySchedule(backups.Items)
for schedule, timestamp := range currentSchedules {
b.metrics.SetBackupLastSuccessfulTimestamp(schedule, timestamp)
}
// Remove metrics for schedules that no longer have successful backups
if b.knownSchedulesWithSuccessfulBackup != nil {
for schedule := range b.knownSchedulesWithSuccessfulBackup {
if _, exists := currentSchedules[schedule]; !exists {
b.metrics.DeleteBackupLastSuccessfulTimestamp(schedule)
}
}
}
b.knownSchedulesWithSuccessfulBackup = sets.New[string]()
for schedule := range currentSchedules {
b.knownSchedulesWithSuccessfulBackup.Insert(schedule)
}
}
// getLastSuccessBySchedule finds the most recent completed backup for each schedule
// and returns a map of schedule name -> completion time of the most recent completed
// backup. This map includes an entry for ad-hoc/non-scheduled backups, where the key
+43
View File
@@ -31,6 +31,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -2041,6 +2042,48 @@ func Test_getLastSuccessBySchedule(t *testing.T) {
}
}
// Test_resyncBackupMetrics_prunesStaleTimestamps verifies that resyncBackupMetrics
// removes backupLastSuccessfulTimestamp entries for schedules that no longer have
// any completed backups (e.g. after the schedule and its backups are deleted).
func Test_resyncBackupMetrics_prunesStaleTimestamps(t *testing.T) {
baseTime, err := time.Parse(time.RFC1123, time.RFC1123)
require.NoError(t, err)
m := metrics.NewServerMetrics()
gauge := m.Metrics()["backup_last_successful_timestamp"]
activeBackup := builder.ForBackup("velero", "b1").
ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "active-schedule")).
Phase(velerov1api.BackupPhaseCompleted).
CompletionTimestamp(baseTime).
Result()
deletedBackup := builder.ForBackup("velero", "b2").
ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "deleted-schedule")).
Phase(velerov1api.BackupPhaseCompleted).
CompletionTimestamp(baseTime).
Result()
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, activeBackup, deletedBackup)
c := &backupReconciler{
kbClient: fakeClient,
logger: logrus.StandardLogger(),
metrics: m,
}
// First resync: sets metrics for both schedules
c.resyncBackupMetrics()
assert.Equal(t, 2, testutil.CollectAndCount(gauge))
// Simulate schedule deletion: remove the backup for "deleted-schedule"
require.NoError(t, fakeClient.Delete(t.Context(), deletedBackup))
// Second resync: prunes "deleted-schedule" metric, keeps "active-schedule"
c.resyncBackupMetrics()
assert.Equal(t, 1, testutil.CollectAndCount(gauge))
}
// Unit tests to make sure that the backup's status is updated correctly during reconcile.
// To clear up confusion whether status can be updated with Patch alone without status writer and not kbClient.Status().Patch()
func TestPatchResourceWorksWithStatus(t *testing.T) {
+2 -2
View File
@@ -454,7 +454,7 @@ func (r *DataDownloadReconciler) startCancelableDataPath(asyncBR datapath.AsyncB
if err := asyncBR.StartRestore(dd.Spec.SnapshotID, datapath.AccessPoint{
ByPath: res.ByPod.VolumeName,
}, dd.Spec.DataMoverConfig); err != nil {
}, dd.Spec.DataMoverConfig, nil); err != nil {
return errors.Wrapf(err, "error starting async restore for pod %s, volume %s", res.ByPod.HostingPod.Name, res.ByPod.VolumeName)
}
@@ -1096,7 +1096,7 @@ func (r *DataDownloadReconciler) resumeCancellableDataPath(ctx context.Context,
if err := asyncBR.StartRestore(dd.Spec.SnapshotID, datapath.AccessPoint{
ByPath: res.ByPod.VolumeName,
}, nil); err != nil {
}, nil, nil); err != nil {
return errors.Wrapf(err, "error to resume asyncBR watcher for dd %s", dd.Name)
}
@@ -529,7 +529,7 @@ func TestDataDownloadReconcile(t *testing.T) {
}
if test.mockStart {
asyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.mockStartErr)
asyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.mockStartErr)
}
if test.mockCancel {
@@ -1288,7 +1288,7 @@ func TestResumeCancellableRestore(t *testing.T) {
}
if test.mockStart {
mockAsyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.startWatcherErr)
mockAsyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.startWatcherErr)
}
if test.mockClose {
@@ -348,7 +348,7 @@ func (f *fakeFSBR) StartBackup(source datapath.AccessPoint, uploaderConfigs map[
return f.startErr
}
func (f *fakeFSBR) StartRestore(snapshotID string, target datapath.AccessPoint, uploaderConfigs map[string]string) error {
func (f *fakeFSBR) StartRestore(snapshotID string, target datapath.AccessPoint, uploaderConfigs map[string]string, param any) error {
return nil
}
+55 -22
View File
@@ -236,9 +236,9 @@ func (r *PodVolumeRestoreReconciler) Reconcile(ctx context.Context, req ctrl.Req
return ctrl.Result{}, nil
}
shouldProcess, pod, err := shouldProcess(ctx, r.client, log, pvr)
shouldProcess, pod, err := shouldProcess(ctx, r.client, log, pvr, r.resourceTimeout)
if err != nil {
return ctrl.Result{}, err
return r.errorOut(ctx, pvr, err, "Pod for this PVR is not ready", log)
}
if !shouldProcess {
return ctrl.Result{}, nil
@@ -528,7 +528,7 @@ func (r *PodVolumeRestoreReconciler) startCancelableDataPath(asyncBR datapath.As
if err := asyncBR.StartRestore(pvr.Spec.SnapshotID, datapath.AccessPoint{
ByPath: res.ByPod.VolumeName,
}, pvr.Spec.UploaderSettings); err != nil {
}, pvr.Spec.UploaderSettings, nil); err != nil {
return errors.Wrapf(err, "error starting async restore for pod %s, volume %s", res.ByPod.HostingPod.Name, res.ByPod.VolumeName)
}
@@ -565,7 +565,7 @@ func UpdatePVRStatusToFailed(ctx context.Context, c client.Client, pvr *velerov1
return err
}
func shouldProcess(ctx context.Context, client client.Client, log logrus.FieldLogger, pvr *velerov1api.PodVolumeRestore) (bool, *corev1api.Pod, error) {
func shouldProcess(ctx context.Context, client client.Client, log logrus.FieldLogger, pvr *velerov1api.PodVolumeRestore, timeout time.Duration) (bool, *corev1api.Pod, error) {
if !isPVRNew(pvr) {
log.Debug("PVR is not new, skip")
return false, nil, nil
@@ -573,22 +573,63 @@ func shouldProcess(ctx context.Context, client client.Client, log logrus.FieldLo
// we filter the pods during the initialization of cache, if we can get a pod here, the pod must be in the same node with the controller
// so we don't need to compare the node anymore
pod := &corev1api.Pod{}
if err := client.Get(ctx, types.NamespacedName{Namespace: pvr.Spec.Pod.Namespace, Name: pvr.Spec.Pod.Name}, pod); err != nil {
if apierrors.IsNotFound(err) {
log.WithError(err).Debug("Pod not found on this node, skip")
return false, nil, nil
var targetPod *corev1api.Pod
err := wait.PollUntilContextTimeout(ctx, time.Millisecond*100, timeout, true, func(ctx context.Context) (bool, error) {
updated := &corev1api.Pod{}
if err := client.Get(ctx, types.NamespacedName{Namespace: pvr.Spec.Pod.Namespace, Name: pvr.Spec.Pod.Name}, updated); err != nil {
if apierrors.IsNotFound(err) {
return false, nil
}
return false, err
}
targetPod = updated
return true, nil
})
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return false, nil, errors.Errorf("timeout to wait for pod %s/%s", pvr.Spec.Pod.Namespace, pvr.Spec.Pod.Name)
} else {
return false, nil, errors.Wrapf(err, "error waiting for pod %s/%s", pvr.Spec.Pod.Namespace, pvr.Spec.Pod.Name)
}
log.WithError(err).Error("Unable to get pod")
return false, nil, err
}
if !isInitContainerRunning(pod) {
if targetPod.Status.Phase == corev1api.PodFailed || targetPod.Status.Phase == corev1api.PodUnknown {
return false, nil, errors.Errorf("unexpected state for pod %s/%s", targetPod.Namespace, targetPod.Name)
}
idx := getInitContainerIndex(targetPod)
if idx < 0 {
return false, nil, errors.Errorf("no restore-wait init container in pod %s/%s", targetPod.Namespace, targetPod.Name)
}
if len(targetPod.Status.InitContainerStatuses) <= idx {
log.Debug("Pod init container statuses are not fully populated yet, skip")
return false, nil, nil
}
containerStatus := targetPod.Status.InitContainerStatuses[idx]
if containerStatus.State.Terminated != nil {
return false, nil, errors.Errorf("restore-wait init container has already completed in pod %s/%s", targetPod.Namespace, targetPod.Name)
}
if containerStatus.State.Waiting != nil {
reason := containerStatus.State.Waiting.Reason
if reason == "ImagePullBackOff" || reason == "ErrImageNeverPull" || reason == "CreateContainerConfigError" || reason == "CreateContainerError" || reason == "InvalidImageName" || reason == "ErrImagePull" {
return false, nil, errors.Errorf("restore-wait init container in pod %s/%s is in unrecoverable waiting state with reason %s", targetPod.Namespace, targetPod.Name, reason)
}
}
if containerStatus.State.Running == nil {
log.Debug("Pod is not running restore-wait init container, skip")
return false, nil, nil
}
return true, pod, nil
return true, targetPod, nil
}
func (r *PodVolumeRestoreReconciler) closeDataPath(ctx context.Context, pvrName string) {
@@ -770,14 +811,6 @@ func isPVRNew(pvr *velerov1api.PodVolumeRestore) bool {
return pvr.Status.Phase == "" || pvr.Status.Phase == velerov1api.PodVolumeRestorePhaseNew
}
func isInitContainerRunning(pod *corev1api.Pod) bool {
// Pod volume wait container can be anywhere in the list of init containers, but must be running.
i := getInitContainerIndex(pod)
return i >= 0 &&
len(pod.Status.InitContainerStatuses)-1 >= i &&
pod.Status.InitContainerStatuses[i].State.Running != nil
}
func getInitContainerIndex(pod *corev1api.Pod) int {
// Pod volume wait container can be anywhere in the list of init containers so locate it.
for i, initContainer := range pod.Spec.InitContainers {
@@ -1113,7 +1146,7 @@ func (r *PodVolumeRestoreReconciler) resumeCancellableDataPath(ctx context.Conte
if err := asyncBR.StartRestore(pvr.Spec.SnapshotID, datapath.AccessPoint{
ByPath: res.ByPod.VolumeName,
}, pvr.Spec.UploaderSettings); err != nil {
}, pvr.Spec.UploaderSettings, nil); err != nil {
return errors.Wrapf(err, "error to resume asyncBR watcher for PVR %s", pvr.Name)
}
@@ -65,6 +65,8 @@ func TestShouldProcess(t *testing.T) {
obj *velerov1api.PodVolumeRestore
pod *corev1api.Pod
shouldProcessed bool
expectError bool
errString string
}{
{
name: "InProgress phase pvr should not be processed",
@@ -115,6 +117,8 @@ func TestShouldProcess(t *testing.T) {
},
},
shouldProcessed: false,
expectError: true,
errString: "timeout to wait for pod ns-1/pod-1",
},
{
name: "Empty phase pvr with pod on node not running init container should not be processed",
@@ -200,6 +204,268 @@ func TestShouldProcess(t *testing.T) {
},
shouldProcessed: true,
},
{
name: "pod is in failed phase should return error",
obj: &velerov1api.PodVolumeRestore{
ObjectMeta: metav1.ObjectMeta{
Namespace: "velero",
Name: "pvr-1",
},
Spec: velerov1api.PodVolumeRestoreSpec{
Pod: corev1api.ObjectReference{
Namespace: "ns-1",
Name: "pod-1",
},
},
Status: velerov1api.PodVolumeRestoreStatus{
Phase: "",
},
},
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "pod-1",
},
Status: corev1api.PodStatus{
Phase: corev1api.PodFailed,
},
},
shouldProcessed: false,
expectError: true,
errString: "unexpected state for pod",
},
{
name: "pod is in unknown phase should return error",
obj: &velerov1api.PodVolumeRestore{
ObjectMeta: metav1.ObjectMeta{
Namespace: "velero",
Name: "pvr-1",
},
Spec: velerov1api.PodVolumeRestoreSpec{
Pod: corev1api.ObjectReference{
Namespace: "ns-1",
Name: "pod-1",
},
},
Status: velerov1api.PodVolumeRestoreStatus{
Phase: "",
},
},
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "pod-1",
},
Status: corev1api.PodStatus{
Phase: corev1api.PodUnknown,
},
},
shouldProcessed: false,
expectError: true,
errString: "unexpected state for pod",
},
{
name: "pod with no init containers should return error",
obj: &velerov1api.PodVolumeRestore{
ObjectMeta: metav1.ObjectMeta{
Namespace: "velero",
Name: "pvr-1",
},
Spec: velerov1api.PodVolumeRestoreSpec{
Pod: corev1api.ObjectReference{
Namespace: "ns-1",
Name: "pod-1",
},
},
Status: velerov1api.PodVolumeRestoreStatus{
Phase: "",
},
},
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "pod-1",
},
Spec: corev1api.PodSpec{
NodeName: controllerNode,
},
},
shouldProcessed: false,
expectError: true,
errString: "no restore-wait init container",
},
{
name: "pod init container statuses are not fully populated yet should skip",
obj: &velerov1api.PodVolumeRestore{
ObjectMeta: metav1.ObjectMeta{
Namespace: "velero",
Name: "pvr-1",
},
Spec: velerov1api.PodVolumeRestoreSpec{
Pod: corev1api.ObjectReference{
Namespace: "ns-1",
Name: "pod-1",
},
},
Status: velerov1api.PodVolumeRestoreStatus{
Phase: "",
},
},
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "pod-1",
},
Spec: corev1api.PodSpec{
NodeName: controllerNode,
InitContainers: []corev1api.Container{
{
Name: restorehelper.WaitInitContainer,
},
},
},
Status: corev1api.PodStatus{
InitContainerStatuses: []corev1api.ContainerStatus{},
},
},
shouldProcessed: false,
},
{
name: "restore-wait init container has already completed should return error",
obj: &velerov1api.PodVolumeRestore{
ObjectMeta: metav1.ObjectMeta{
Namespace: "velero",
Name: "pvr-1",
},
Spec: velerov1api.PodVolumeRestoreSpec{
Pod: corev1api.ObjectReference{
Namespace: "ns-1",
Name: "pod-1",
},
},
Status: velerov1api.PodVolumeRestoreStatus{
Phase: "",
},
},
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "pod-1",
},
Spec: corev1api.PodSpec{
NodeName: controllerNode,
InitContainers: []corev1api.Container{
{
Name: restorehelper.WaitInitContainer,
},
},
},
Status: corev1api.PodStatus{
InitContainerStatuses: []corev1api.ContainerStatus{
{
State: corev1api.ContainerState{
Terminated: &corev1api.ContainerStateTerminated{
ExitCode: 0,
},
},
},
},
},
},
shouldProcessed: false,
expectError: true,
errString: "restore-wait init container has already completed",
},
{
name: "restore-wait init container is in unrecoverable waiting state should return error",
obj: &velerov1api.PodVolumeRestore{
ObjectMeta: metav1.ObjectMeta{
Namespace: "velero",
Name: "pvr-1",
},
Spec: velerov1api.PodVolumeRestoreSpec{
Pod: corev1api.ObjectReference{
Namespace: "ns-1",
Name: "pod-1",
},
},
Status: velerov1api.PodVolumeRestoreStatus{
Phase: "",
},
},
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "pod-1",
},
Spec: corev1api.PodSpec{
NodeName: controllerNode,
InitContainers: []corev1api.Container{
{
Name: restorehelper.WaitInitContainer,
},
},
},
Status: corev1api.PodStatus{
InitContainerStatuses: []corev1api.ContainerStatus{
{
State: corev1api.ContainerState{
Waiting: &corev1api.ContainerStateWaiting{
Reason: "ImagePullBackOff",
},
},
},
},
},
},
shouldProcessed: false,
expectError: true,
errString: "is in unrecoverable waiting state with reason ImagePullBackOff",
},
{
name: "restore-wait init container is in normal waiting state should skip",
obj: &velerov1api.PodVolumeRestore{
ObjectMeta: metav1.ObjectMeta{
Namespace: "velero",
Name: "pvr-1",
},
Spec: velerov1api.PodVolumeRestoreSpec{
Pod: corev1api.ObjectReference{
Namespace: "ns-1",
Name: "pod-1",
},
},
Status: velerov1api.PodVolumeRestoreStatus{
Phase: "",
},
},
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "pod-1",
},
Spec: corev1api.PodSpec{
NodeName: controllerNode,
InitContainers: []corev1api.Container{
{
Name: restorehelper.WaitInitContainer,
},
},
},
Status: corev1api.PodStatus{
InitContainerStatuses: []corev1api.ContainerStatus{
{
State: corev1api.ContainerState{
Waiting: &corev1api.ContainerStateWaiting{
Reason: "ContainerCreating",
},
},
},
},
},
},
shouldProcessed: false,
},
}
for _, ts := range tests {
@@ -221,179 +487,16 @@ func TestShouldProcess(t *testing.T) {
clock: &clocks.RealClock{},
}
shouldProcess, _, _ := shouldProcess(ctx, c.client, c.logger, ts.obj)
shouldProcess, _, err := shouldProcess(ctx, c.client, c.logger, ts.obj, time.Second)
require.Equal(t, ts.shouldProcessed, shouldProcess)
})
}
}
func TestIsInitContainerRunning(t *testing.T) {
tests := []struct {
name string
pod *corev1api.Pod
expected bool
}{
{
name: "pod with no init containers should return false",
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "pod-1",
},
},
expected: false,
},
{
name: "pod with running init container that's not restore init should return false",
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "pod-1",
},
Spec: corev1api.PodSpec{
InitContainers: []corev1api.Container{
{
Name: "non-restore-init",
},
},
},
Status: corev1api.PodStatus{
InitContainerStatuses: []corev1api.ContainerStatus{
{
State: corev1api.ContainerState{
Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}},
},
},
},
},
},
expected: false,
},
{
name: "pod with running init container that's not first should still work",
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "pod-1",
},
Spec: corev1api.PodSpec{
InitContainers: []corev1api.Container{
{
Name: "non-restore-init",
},
{
Name: restorehelper.WaitInitContainer,
},
},
},
Status: corev1api.PodStatus{
InitContainerStatuses: []corev1api.ContainerStatus{
{
State: corev1api.ContainerState{
Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}},
},
},
{
State: corev1api.ContainerState{
Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}},
},
},
},
},
},
expected: true,
},
{
name: "pod with init container as first initContainer that's not running should return false",
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "pod-1",
},
Spec: corev1api.PodSpec{
InitContainers: []corev1api.Container{
{
Name: restorehelper.WaitInitContainer,
},
{
Name: "non-restore-init",
},
},
},
Status: corev1api.PodStatus{
InitContainerStatuses: []corev1api.ContainerStatus{
{
State: corev1api.ContainerState{},
},
{
State: corev1api.ContainerState{
Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}},
},
},
},
},
},
expected: false,
},
{
name: "pod with running init container as first initContainer should return true",
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "pod-1",
},
Spec: corev1api.PodSpec{
InitContainers: []corev1api.Container{
{
Name: restorehelper.WaitInitContainer,
},
{
Name: "non-restore-init",
},
},
},
Status: corev1api.PodStatus{
InitContainerStatuses: []corev1api.ContainerStatus{
{
State: corev1api.ContainerState{
Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}},
},
},
{
State: corev1api.ContainerState{
Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}},
},
},
},
},
},
expected: true,
},
{
name: "pod with init container with empty InitContainerStatuses should return 0",
pod: &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "pod-1",
},
Spec: corev1api.PodSpec{
InitContainers: []corev1api.Container{
{
Name: restorehelper.WaitInitContainer,
},
},
},
Status: corev1api.PodStatus{
InitContainerStatuses: []corev1api.ContainerStatus{},
},
},
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, isInitContainerRunning(test.pod))
if ts.expectError {
require.Error(t, err)
if ts.errString != "" {
assert.Contains(t, err.Error(), ts.errString)
}
} else {
require.NoError(t, err)
}
})
}
}
@@ -996,7 +1099,7 @@ func TestPodVolumeRestoreReconcile(t *testing.T) {
}
if test.mockStart {
asyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.mockStartErr)
asyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.mockStartErr)
}
if test.mockCancel {
@@ -1798,7 +1901,7 @@ func TestResumeCancellablePodVolumeRestore(t *testing.T) {
}
if test.mockStart {
mockAsyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.startWatcherErr)
mockAsyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.startWatcherErr)
}
if test.mockClose {
+1 -1
View File
@@ -204,7 +204,7 @@ func (r *BackupMicroService) RunCancelableDataPath(ctx context.Context) (string,
if err := dp.StartBackup(r.sourceTargetPath, du.Spec.DataMoverConfig, &datapath.BackupStartParam{
RealSource: GetRealSource(du.Spec.SourceNamespace, du.Spec.SourcePVC),
ParentSnapshot: "",
ParentSnapshot: du.Spec.ParentSnapshot,
ForceFull: false,
Tags: tags,
VolumeID: r.volumeID,
+1 -1
View File
@@ -180,7 +180,7 @@ func (r *RestoreMicroService) RunCancelableDataPath(ctx context.Context) (string
}
log.Info("fs init")
if err := dp.StartRestore(dd.Spec.SnapshotID, r.sourceTargetPath, dd.Spec.DataMoverConfig); err != nil {
if err := dp.StartRestore(dd.Spec.SnapshotID, r.sourceTargetPath, dd.Spec.DataMoverConfig, &datapath.RestoreStartParam{}); err != nil {
return "", errors.Wrap(err, "error starting data path restore")
}
+2 -2
View File
@@ -355,12 +355,12 @@ func TestRunCancelableRestore(t *testing.T) {
if test.startErr != nil {
fsBR.On("Init", mock.Anything, mock.Anything).Return(nil)
fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.startErr)
fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.startErr)
}
if test.dataPathStarted {
fsBR.On("Init", mock.Anything, mock.Anything).Return(nil)
fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(nil)
fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
}
return fsBR
+5 -2
View File
@@ -19,12 +19,15 @@ package datamover
import (
"fmt"
"github.com/vmware-tanzu/velero/pkg/uploader"
datamoverutil "github.com/vmware-tanzu/velero/pkg/util/datamover"
)
func GetUploaderType(dataMover string) string {
if datamoverutil.IsBuiltInDataMover(dataMover) {
return "kopia"
if datamoverutil.IsVeleroFSDataMover(dataMover) {
return uploader.KopiaType
} else if datamoverutil.IsVeleroBlockDataMover(dataMover) {
return uploader.BlockType
} else {
return dataMover
}
+10
View File
@@ -22,6 +22,16 @@ func TestGetUploaderType(t *testing.T) {
input: "velero",
want: "kopia",
},
{
name: "velero-fs dataMover is kopia",
input: "velero-fs",
want: "kopia",
},
{
name: "velero-block dataMover is velero-block",
input: "velero-block",
want: "velero-block",
},
{
name: "kopia dataMover is kopia",
input: "kopia",
+5 -1
View File
@@ -59,6 +59,10 @@ type BackupStartParam struct {
SnapshotID string
}
// RestoreStartParam define the input param for restore start
type RestoreStartParam struct {
}
type generalDataPath struct {
ctx context.Context
cancel context.CancelFunc
@@ -221,7 +225,7 @@ func (dp *generalDataPath) StartBackup(source AccessPoint, uploaderConfig map[st
return nil
}
func (dp *generalDataPath) StartRestore(snapshotID string, target AccessPoint, uploaderConfigs map[string]string) error {
func (dp *generalDataPath) StartRestore(snapshotID string, target AccessPoint, uploaderConfigs map[string]string, param any) error {
if !dp.initialized {
return errors.New("data path is not initialized")
}
+1 -1
View File
@@ -190,7 +190,7 @@ func TestAsyncRestore(t *testing.T) {
dp.initialized = true
dp.callbacks = test.callbacks
err := dp.StartRestore(test.snapshot, AccessPoint{ByPath: test.path}, map[string]string{})
err := dp.StartRestore(test.snapshot, AccessPoint{ByPath: test.path}, map[string]string{}, &RestoreStartParam{})
require.NoError(t, err)
<-finish
+1 -1
View File
@@ -221,7 +221,7 @@ func (ms *microServiceBRWatcher) StartBackup(source AccessPoint, uploaderConfig
return nil
}
func (ms *microServiceBRWatcher) StartRestore(snapshotID string, target AccessPoint, uploaderConfigs map[string]string) error {
func (ms *microServiceBRWatcher) StartRestore(snapshotID string, target AccessPoint, uploaderConfigs map[string]string, param any) error {
ms.log.Infof("Start watching restore ms to target %s, from snapshot %s", target.ByPath, snapshotID)
ms.startWatch()
+5 -5
View File
@@ -60,17 +60,17 @@ func (_m *AsyncBR) StartBackup(source datapath.AccessPoint, dataMoverConfig map[
return r0
}
// StartRestore provides a mock function with given fields: snapshotID, target, dataMoverConfig
func (_m *AsyncBR) StartRestore(snapshotID string, target datapath.AccessPoint, dataMoverConfig map[string]string) error {
ret := _m.Called(snapshotID, target, dataMoverConfig)
// StartRestore provides a mock function with given fields: snapshotID, target, dataMoverConfig, param
func (_m *AsyncBR) StartRestore(snapshotID string, target datapath.AccessPoint, dataMoverConfig map[string]string, param interface{}) error {
ret := _m.Called(snapshotID, target, dataMoverConfig, param)
if len(ret) == 0 {
panic("no return value specified for StartRestore")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, datapath.AccessPoint, map[string]string) error); ok {
r0 = rf(snapshotID, target, dataMoverConfig)
if rf, ok := ret.Get(0).(func(string, datapath.AccessPoint, map[string]string, interface{}) error); ok {
r0 = rf(snapshotID, target, dataMoverConfig, param)
} else {
r0 = ret.Error(0)
}
+1 -1
View File
@@ -66,7 +66,7 @@ type AsyncBR interface {
StartBackup(source AccessPoint, dataMoverConfig map[string]string, param any) error
// StartRestore starts an asynchronous data path instance for restore
StartRestore(snapshotID string, target AccessPoint, dataMoverConfig map[string]string) error
StartRestore(snapshotID string, target AccessPoint, dataMoverConfig map[string]string, param any) error
// Cancel cancels an asynchronous data path instance
Cancel()
+4 -1
View File
@@ -139,7 +139,10 @@ func WithPodVolumeOperationTimeout(val time.Duration) podTemplateOption {
func WithPlugins(plugins []string) podTemplateOption {
return func(c *podTemplateConfig) {
c.plugins = plugins
c.plugins = make([]string, 0, len(plugins))
for _, plugin := range plugins {
c.plugins = append(c.plugins, strings.TrimSpace(plugin))
}
}
}
+9
View File
@@ -60,6 +60,15 @@ func TestDeployment(t *testing.T) {
assert.Len(t, deploy.Spec.Template.Spec.Containers[0].Args, 2)
assert.Equal(t, "--features=EnableCSI,foo,bar,baz", deploy.Spec.Template.Spec.Containers[0].Args[1])
deploy = Deployment("velero", WithPlugins([]string{
"harbor-repo.vmware.com/harbor-ci/velero/velero-plugin-for-aws:v1.2.0",
" \n vsphereveleroplugin/velero-plugin-for-vsphere:v1.1.1 ",
}))
assert.Len(t, deploy.Spec.Template.Spec.InitContainers, 2)
assert.Equal(t, "harbor-repo.vmware.com/harbor-ci/velero/velero-plugin-for-aws:v1.2.0", deploy.Spec.Template.Spec.InitContainers[0].Image)
assert.Equal(t, "vsphereveleroplugin/velero-plugin-for-vsphere:v1.1.1", deploy.Spec.Template.Spec.InitContainers[1].Image)
assert.Equal(t, "vsphereveleroplugin-velero-plugin-for-vsphere", deploy.Spec.Template.Spec.InitContainers[1].Name)
deploy = Deployment("velero", WithUploaderType("kopia"))
assert.Len(t, deploy.Spec.Template.Spec.Containers[0].Args, 2)
assert.Equal(t, "--uploader-type=kopia", deploy.Spec.Template.Spec.Containers[0].Args[1])
+8
View File
@@ -758,6 +758,14 @@ func (m *ServerMetrics) RegisterPodVolumeOpLatencyGauge(node, pvbName, opName, b
}
}
// DeleteBackupLastSuccessfulTimestamp removes the backupLastSuccessfulTimestamp
// metric for a single schedule.
func (m *ServerMetrics) DeleteBackupLastSuccessfulTimestamp(scheduleName string) {
if g, ok := m.metrics[backupLastSuccessfulTimestamp].(*prometheus.GaugeVec); ok {
g.DeleteLabelValues(scheduleName)
}
}
// SetBackupTarballSizeBytesGauge records the size, in bytes, of a backup tarball.
func (m *ServerMetrics) SetBackupTarballSizeBytesGauge(backupSchedule string, size int64) {
if g, ok := m.metrics[backupTarballSizeBytesGauge].(*prometheus.GaugeVec); ok {
+27
View File
@@ -21,6 +21,7 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -457,6 +458,32 @@ func getHistogramCount(t *testing.T, vec *prometheus.HistogramVec, scheduleLabel
return 0
}
// TestDeleteBackupLastSuccessfulTimestamp verifies that DeleteBackupLastSuccessfulTimestamp
// removes only the specified schedule's metric.
func TestDeleteBackupLastSuccessfulTimestamp(t *testing.T) {
m := NewServerMetrics()
now := time.Now()
m.SetBackupLastSuccessfulTimestamp("schedule-1", now)
m.SetBackupLastSuccessfulTimestamp("schedule-2", now.Add(-time.Hour))
m.SetBackupLastSuccessfulTimestamp("", now.Add(-2*time.Hour))
g := m.metrics[backupLastSuccessfulTimestamp].(*prometheus.GaugeVec)
assert.Equal(t, 3, testutil.CollectAndCount(g))
m.DeleteBackupLastSuccessfulTimestamp("schedule-1")
assert.Equal(t, 2, testutil.CollectAndCount(g))
assert.Equal(t, float64(now.Add(-time.Hour).Unix()), testutil.ToFloat64(g.WithLabelValues("schedule-2")))
assert.Equal(t, float64(now.Add(-2*time.Hour).Unix()), testutil.ToFloat64(g.WithLabelValues("")))
m.DeleteBackupLastSuccessfulTimestamp("schedule-2")
assert.Equal(t, 1, testutil.CollectAndCount(g))
assert.Equal(t, float64(now.Add(-2*time.Hour).Unix()), testutil.ToFloat64(g.WithLabelValues("")))
m.DeleteBackupLastSuccessfulTimestamp("")
assert.Equal(t, 0, testutil.CollectAndCount(g))
}
// TestRepoMaintenanceMetrics verifies that repo maintenance metrics are properly recorded.
func TestRepoMaintenanceMetrics(t *testing.T) {
tests := []struct {
+48 -1
View File
@@ -20,12 +20,15 @@ import (
"context"
"fmt"
"sync"
"time"
"github.com/cockroachdb/errors"
"github.com/sirupsen/logrus"
corev1api "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/cache"
ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
@@ -181,7 +184,7 @@ func newBackupper(
// the PVB in the indexer is already in final status, no need to call WaitGroup.Done()
if ok && (existPVB.Status.Phase == velerov1api.PodVolumeBackupPhaseCompleted ||
existPVB.Status.Phase == velerov1api.PodVolumeBackupPhaseFailed ||
pvb.Status.Phase == velerov1api.PodVolumeBackupPhaseCanceled) {
existPVB.Status.Phase == velerov1api.PodVolumeBackupPhaseCanceled) {
statusChangedToFinal = false
}
}
@@ -411,6 +414,24 @@ func (b *backupper) WaitAllPodVolumesProcessed(log logrus.FieldLogger) []*velero
select {
case <-b.ctx.Done():
log.Error("timed out waiting for all PodVolumeBackups to complete")
for _, obj := range b.pvbIndexer.List() {
pvb, ok := obj.(*velerov1api.PodVolumeBackup)
if !ok {
log.Errorf("expected PVB, but got %T", obj)
continue
}
if pvb.Status.Phase != velerov1api.PodVolumeBackupPhaseCompleted &&
pvb.Status.Phase != velerov1api.PodVolumeBackupPhaseFailed &&
pvb.Status.Phase != velerov1api.PodVolumeBackupPhaseCanceled {
log.Infof("Setting cancel flag for ongoing PVB %s/%s", pvb.Namespace, pvb.Name)
if err := updatePVBWithRetry(context.Background(), b.crClient, pvb.Namespace, pvb.Name); err != nil {
log.WithError(err).Errorf("Failed to set cancel flag for PVB %s/%s", pvb.Namespace, pvb.Name)
}
}
}
<-done
case <-done:
}
@@ -432,6 +453,32 @@ func (b *backupper) WaitAllPodVolumesProcessed(log logrus.FieldLogger) []*velero
return podVolumeBackups
}
func updatePVBWithRetry(ctx context.Context, client ctrlclient.Client, namespace, name string) error {
return wait.PollUntilContextCancel(ctx, 100*time.Millisecond, true, func(ctx context.Context) (bool, error) {
pvb := &velerov1api.PodVolumeBackup{}
if err := client.Get(ctx, ctrlclient.ObjectKey{Namespace: namespace, Name: name}, pvb); err != nil {
return false, errors.Wrap(err, "getting PVB")
}
if pvb.Spec.Cancel {
return true, nil
}
pvb.Spec.Cancel = true
pvb.Status.Message = "Cancel PVB on pod volume timeout"
err := client.Update(ctx, pvb)
if err != nil {
if apierrors.IsConflict(err) {
return false, nil
}
return false, errors.Wrapf(err, "error updating PVB %s/%s", pvb.Namespace, pvb.Name)
}
return true, nil
})
}
func (b *backupper) GetPodVolumeBackupByPodAndVolume(podNamespace, podName, volume string) (*velerov1api.PodVolumeBackup, error) {
obj, exist, err := b.pvbIndexer.GetByKey(fmt.Sprintf(pvbKeyPattern, podNamespace, podName, volume))
if err != nil {
+37 -5
View File
@@ -733,14 +733,14 @@ func TestListPodVolumeBackupsByPodp(t *testing.T) {
}
type logHook struct {
entry *logrus.Entry
entries []*logrus.Entry
}
func (l *logHook) Levels() []logrus.Level {
return []logrus.Level{logrus.ErrorLevel}
}
func (l *logHook) Fire(entry *logrus.Entry) error {
l.entry = entry
l.entries = append(l.entries, entry)
return nil
}
@@ -808,12 +808,35 @@ func TestWaitAllPodVolumesProcessed(t *testing.T) {
logHook := &logHook{}
logger.Hooks.Add(logHook)
backuper := newBackupper(c.ctx, log, nil, nil, informer, nil, "", &velerov1api.Backup{})
backuper := newBackupper(c.ctx, log, nil, nil, informer, client, "", &velerov1api.Backup{})
if c.pvb != nil {
require.NoError(t, backuper.pvbIndexer.Add(c.pvb))
backuper.wg.Add(1)
}
if c.ctx == timeoutCtx && c.pvb != nil {
// Start a goroutine to simulate the controller's cancellation behavior
go func() {
// Wait a short time for the cancel flag to be set
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
for range ticker.C {
pvb := &velerov1api.PodVolumeBackup{}
err := client.Get(t.Context(), ctrlclient.ObjectKey{Namespace: c.pvb.Namespace, Name: c.pvb.Name}, pvb)
if err == nil && pvb.Spec.Cancel {
oldPVB := pvb.DeepCopy()
pvb.Status.Phase = velerov1api.PodVolumeBackupPhaseCanceled
pvb.Status.Message = "canceled"
_ = client.Update(t.Context(), pvb)
if informer.handler != nil {
informer.handler.OnUpdate(oldPVB, pvb)
}
return
}
}
}()
}
if c.statusToBeUpdated != nil {
pvb := &velerov1api.PodVolumeBackup{}
err := client.Get(t.Context(), ctrlclient.ObjectKey{Namespace: c.pvb.Namespace, Name: c.pvb.Name}, pvb)
@@ -831,9 +854,18 @@ func TestWaitAllPodVolumesProcessed(t *testing.T) {
pvbs := backuper.WaitAllPodVolumesProcessed(logger)
if c.expectedErr != "" {
assert.Equal(t, c.expectedErr, logHook.entry.Message)
found := false
var loggedMsgs []string
for _, entry := range logHook.entries {
loggedMsgs = append(loggedMsgs, entry.Message)
if entry.Message == c.expectedErr {
found = true
break
}
}
assert.True(t, found, "Expected error %q to be logged, but got %v", c.expectedErr, loggedMsgs)
} else {
assert.Nil(t, logHook.entry)
assert.Empty(t, logHook.entries)
}
if c.expectedPVBCount > 0 {
+1 -1
View File
@@ -184,7 +184,7 @@ func (r *RestoreMicroService) RunCancelableDataPath(ctx context.Context) (string
log.Info("Async fs br init")
if err := fsRestore.StartRestore(pvr.Spec.SnapshotID, r.sourceTargetPath, pvr.Spec.UploaderSettings); err != nil {
if err := fsRestore.StartRestore(pvr.Spec.SnapshotID, r.sourceTargetPath, pvr.Spec.UploaderSettings, &datapath.RestoreStartParam{}); err != nil {
return "", errors.Wrap(err, "error starting data path restore")
}
+2 -2
View File
@@ -436,12 +436,12 @@ func TestRunCancelableDataPathRestore(t *testing.T) {
if test.startErr != nil {
fsBR.On("Init", mock.Anything, mock.Anything).Return(nil)
fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.startErr)
fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.startErr)
}
if test.dataPathStarted {
fsBR.On("Init", mock.Anything, mock.Anything).Return(nil)
fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(nil)
fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
}
return fsBR
+19 -3
View File
@@ -18,6 +18,7 @@ package repository
import (
"context"
"crypto/fips140"
"fmt"
"time"
@@ -173,7 +174,13 @@ func (m *manager) PrepareRepo(repo *velerov1api.BackupRepository) error {
if err != nil {
return errors.WithStack(err)
}
return prd.PrepareRepo(context.Background(), param)
// Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet.
var prepareErr error
fips140.WithoutEnforcement(func() {
prepareErr = prd.PrepareRepo(context.Background(), param)
})
return prepareErr
}
func (m *manager) PruneRepo(repo *velerov1api.BackupRepository) error {
@@ -244,11 +251,20 @@ func (m *manager) BatchForget(ctx context.Context, repo *velerov1api.BackupRepos
return []error{errors.WithStack(err)}
}
if err := prd.BoostRepoConnect(context.Background(), param); err != nil {
// Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet.
var connectErr error
fips140.WithoutEnforcement(func() {
connectErr = prd.BoostRepoConnect(context.Background(), param)
})
if connectErr != nil {
return []error{errors.WithStack(err)}
}
return prd.BatchForget(context.Background(), snapshots, param)
forgetErr := make([]error, 0)
fips140.WithoutEnforcement(func() {
forgetErr = prd.BatchForget(context.Background(), snapshots, param)
})
return forgetErr
}
func (m *manager) DefaultMaintenanceFrequency(repo *velerov1api.BackupRepository) (time.Duration, error) {
@@ -1208,6 +1208,10 @@ func TestKopiaObjectWriterEx_MixedWriteAndWriteAt(t *testing.T) {
assert.Equal(t, int64(3072), kow.entries[3].Start)
}
// TestKopiaObjectWriterEx_ConcurrentAsyncErrors verifies the async error contract
// under real scheduling: once an async block write fails, the error either fails a
// subsequent Write call fast or surfaces at Result — it is never lost. Which of the
// two happens first depends on goroutine scheduling, and both are correct.
func TestKopiaObjectWriterEx_ConcurrentAsyncErrors(t *testing.T) {
mockRepoWriter := repomocks.NewMockRepositoryWriter(t)
mockWriter := repomocks.NewWriter(t)
@@ -1231,14 +1235,65 @@ func TestKopiaObjectWriterEx_ConcurrentAsyncErrors(t *testing.T) {
data := make([]byte, 1024)
// Issue multiple writes so they all spawn async goroutines
// First few writes shouldn't fail immediately until getWriteError catches the asynchronous fault
// Issue multiple writes so they all spawn async goroutines. A later Write may
// observe the stored async error and fail fast — that is correct behavior.
for i := 0; i < 10; i++ {
l, err := kow.Write(data)
if err != nil {
assert.Contains(t, err.Error(), "simulated async error")
break
}
assert.Equal(t, 1024, l)
}
// Regardless of whether a Write observed the error first, Result must report it.
id, err := kow.Result()
require.Error(t, err)
assert.Contains(t, err.Error(), "simulated async error")
assert.Equal(t, udmrepo.ID(""), id)
}
// TestKopiaObjectWriterEx_AsyncErrorSurfacesAtResult pins the late-error schedule:
// async writes are held until all writes have been queued, so no Write call observes
// the failure and Result alone must report it.
func TestKopiaObjectWriterEx_AsyncErrorSurfacesAtResult(t *testing.T) {
mockRepoWriter := repomocks.NewMockRepositoryWriter(t)
mockWriter := repomocks.NewWriter(t)
releaseWrites := make(chan struct{})
mockWriter.On("Write", mock.Anything).Run(func(mock.Arguments) {
<-releaseWrites
}).Return(0, errors.New("simulated async error"))
mockWriter.On("Close").Return(nil)
mockRepoWriter.On("NewObjectWriter", mock.Anything, mock.Anything).Return(mockWriter)
sem := make(chan struct{}, 10)
buf := freelist.New(10*1024, 1024)
kow := &kopiaObjectWriterEx{
ctx: context.Background(),
rawRepoWriter: mockRepoWriter,
blockSize: 1024,
asyncWritesSem: sem,
asyncBuffer: buf,
logger: velerotest.NewLogger(),
}
data := make([]byte, 1024)
// All async writes block on releaseWrites, so no error can be stored yet and
// every Write must succeed.
for i := 0; i < 10; i++ {
l, err := kow.Write(data)
require.NoError(t, err)
assert.Equal(t, 1024, l)
}
close(releaseWrites)
// Result waits for the async writers to finish and must report their error.
id, err := kow.Result()
require.Error(t, err)
+65 -38
View File
@@ -478,7 +478,15 @@ func (ctx *restoreContext) getNamespaceFilter(namespace string) *resolvedNamespa
return filter
}
// 2. Walk patterns in definition order (first-match semantics)
// 2. Check for exact match first (O(1) map lookup)
// This ensures exact namespace matches take precedence over globs,
// regardless of where they are listed in the configuration.
if filter, ok := ctx.namespacedFilterMap[namespace]; ok {
ctx.namespaceFilterCache[namespace] = filter
return filter
}
// 3. Walk patterns in definition order using pre-compiled globs
// Note: namespaceFilterCache is mutated below without synchronization. This is safe
// today because resource collection runs sequentially. If the restore loop is
// parallelized in the future, these map writes will need a lock to prevent data races.
@@ -489,14 +497,10 @@ func (ctx *restoreContext) getNamespaceFilter(namespace string) *resolvedNamespa
ctx.namespaceFilterCache[namespace] = filter
return filter
}
} else if p.pattern == namespace {
filter := ctx.namespacedFilterMap[p.pattern]
ctx.namespaceFilterCache[namespace] = filter
return filter
}
}
// 3. Cache the miss so we don't re-evaluate failed matches
// 4. Cache the miss so we don't re-evaluate failed matches
ctx.namespaceFilterCache[namespace] = nil
return nil
}
@@ -634,21 +638,19 @@ func resolveRestoreNamespacedFilterPolicies(
func resolveResourceFilter(
rf resourcepolicies.ResourceFilter,
) (*resolvedResourceFilter, error) {
var selector labels.Selector
if len(rf.LabelSelector) > 0 {
var err error
selector, err = labels.ValidatedSelectorFromSet(labels.Set(rf.LabelSelector))
if err != nil {
return nil, fmt.Errorf("invalid label selector in resource filter: %w", err)
}
selector, err := resourcepolicies.SelectorFromPolicyLabelSelector(rf.LabelSelector)
if err != nil {
return nil, fmt.Errorf("invalid label selector in resource filter: %w", err)
}
var orSelectors []labels.Selector
for _, ols := range rf.OrLabelSelectors {
s, err := labels.ValidatedSelectorFromSet(labels.Set(ols))
s, err := resourcepolicies.SelectorFromPolicyLabelSelector(ols)
if err != nil {
return nil, fmt.Errorf("invalid OR label selector in resource filter: %w", err)
}
orSelectors = append(orSelectors, s)
if s != nil {
orSelectors = append(orSelectors, s)
}
}
var nameIE *collections.IncludesExcludes
if len(rf.Names) > 0 || len(rf.ExcludedNames) > 0 {
@@ -1058,7 +1060,7 @@ func (ctx *restoreContext) processSelectedResource(
continue
}
w, e, _ := ctx.restoreItem(obj, groupResource, targetNS)
w, e, _ := ctx.restoreItem(obj, groupResource, targetNS, false)
warnings.Merge(&w)
errs.Merge(&e)
processedItems++
@@ -1384,7 +1386,7 @@ func (ctx *restoreContext) getResource(groupResource schema.GroupResource, obj *
return u, nil
}
func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupResource schema.GroupResource, namespace string) (results.Result, results.Result, bool) {
func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupResource schema.GroupResource, namespace string, mustInclude bool) (results.Result, results.Result, bool) {
warnings, errs := results.Result{}, results.Result{}
// itemExists bool is used to determine whether to include this item in the "wait for additional items" list
itemExists := false
@@ -1401,27 +1403,41 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
// Check if group/resource should be restored. We need to do this here since
// this method may be getting called for an additional item which is a group/resource
// that's excluded.
if !ctx.resourceIncludesExcludes.ShouldInclude(groupResource.String()) && !ctx.resourceMustHave.Has(groupResource.String()) {
restoreLogger.Info("Not restoring item because resource is excluded")
return warnings, errs, itemExists
}
// Check if namespace/cluster-scoped resource should be restored. We need
// to do this here since this method may be getting called for an additional
// item which is in a namespace that's excluded, or which is cluster-scoped
// and should be excluded. Note that we're checking the object's namespace (
// via obj.GetNamespace()) instead of the namespace parameter, because we want
// to check the *original* namespace, not the remapped one if it's been remapped.
//
// Note: Additional items intentionally bypass fine-grained resource filter policies
// (like per-namespace label/name selectors) to avoid breaking semantic dependencies,
// but they must still pass the global exclusions enforced below.
if namespace != "" {
if !ctx.namespaceIncludesExcludes.ShouldInclude(obj.GetNamespace()) && !ctx.resourceMustHave.Has(groupResource.String()) {
restoreLogger.Info("Not restoring item because namespace is excluded")
// but they must still pass the global exclusions enforced below unless mustInclude is set.
if mustInclude {
restoreLogger.Info("Skipping the resource/namespace exclusion checks because the item is marked as must-include")
} else {
if !ctx.resourceIncludesExcludes.ShouldInclude(groupResource.String()) && !ctx.resourceMustHave.Has(groupResource.String()) {
restoreLogger.Info("Not restoring item because resource is excluded")
return warnings, errs, itemExists
}
// Check if namespace/cluster-scoped resource should be restored. We need
// to do this here since this method may be getting called for an additional
// item which is in a namespace that's excluded, or which is cluster-scoped
// and should be excluded. Note that we're checking the object's namespace (
// via obj.GetNamespace()) instead of the namespace parameter, because we want
// to check the *original* namespace, not the remapped one if it's been remapped.
if namespace != "" {
if !ctx.namespaceIncludesExcludes.ShouldInclude(obj.GetNamespace()) && !ctx.resourceMustHave.Has(groupResource.String()) {
restoreLogger.Info("Not restoring item because namespace is excluded")
return warnings, errs, itemExists
}
} else {
if boolptr.IsSetToFalse(ctx.restore.Spec.IncludeClusterResources) {
restoreLogger.Info("Not restoring item because it's cluster-scoped")
return warnings, errs, itemExists
}
}
}
// Namespace creation runs unconditionally when namespace != "", regardless of
// mustInclude. This ensures target namespaces exist for additional items that
// bypass the namespace-exclusion check above.
if namespace != "" {
// If the namespace scoped resource should be restored, ensure that the
// namespace into which the resource is being restored into exists.
// This is the *remapped* namespace that we are ensuring exists.
@@ -1440,11 +1456,6 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
}
ctx.restoredItems[itemKey] = restoredItemStatus{action: ItemRestoreResultCreated, itemExists: true, createdName: nsToEnsure.Name}
}
} else {
if boolptr.IsSetToFalse(ctx.restore.Spec.IncludeClusterResources) {
restoreLogger.Info("Not restoring item because it's cluster-scoped")
return warnings, errs, itemExists
}
}
// Make a copy of object retrieved from backup to make it available unchanged
@@ -1666,6 +1677,21 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
obj = unstructuredObj
mustIncludeAdditionalItems := false
if annotations := obj.GetAnnotations(); annotations != nil {
if _, present := annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation]; present {
// Only the string value "true" enables the bypass.
if annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] == "true" {
mustIncludeAdditionalItems = true
restoreLogger.Info("RestoreItemAction marked additional items as must-include; bypassing resource/namespace exclusion checks for them")
}
// Always strip the annotation so it never lands on the cluster,
// regardless of whether the value enabled the bypass.
delete(annotations, velerov1api.MustIncludeAdditionalItemRestoreAnnotation)
obj.SetAnnotations(annotations)
}
}
var filteredAdditionalItems []velero.ResourceIdentifier
for _, additionalItem := range executeOutput.AdditionalItems {
itemPath := archive.GetItemFilePath(ctx.restoreDir, additionalItem.GroupResource.String(), additionalItem.Namespace, additionalItem.Name)
@@ -1685,6 +1711,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
additionalObj, err := archive.Unmarshal(ctx.fileSystem, itemPath)
if err != nil {
errs.Add(namespace, errors.Wrapf(err, "error restoring additional item %s", additionalResourceID))
continue
}
additionalItemNamespace := additionalItem.Namespace
@@ -1694,7 +1721,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
}
}
w, e, additionalItemExists := ctx.restoreItem(additionalObj, additionalItem.GroupResource, additionalItemNamespace)
w, e, additionalItemExists := ctx.restoreItem(additionalObj, additionalItem.GroupResource, additionalItemNamespace, mustIncludeAdditionalItems)
if additionalItemExists {
filteredAdditionalItems = append(filteredAdditionalItems, additionalItem)
}
+40 -3
View File
@@ -62,7 +62,7 @@ namespacedFilterPolicies:
},
},
{
name: "namespaced filter policy with glob namespace match and first-match semantics",
name: "namespaced filter policy with exact match priority over glob (glob listed first)",
restore: defaultRestore().Result(),
backup: defaultBackup().Result(),
policyYAML: `version: v1
@@ -94,7 +94,43 @@ namespacedFilterPolicies:
test.Pods(),
},
want: map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-1", "ns-2/pod-1"},
test.Pods(): {"ns-1/pod-2", "ns-2/pod-1"},
},
},
{
name: "namespaced filter policy with exact match priority over glob (exact listed first)",
restore: defaultRestore().Result(),
backup: defaultBackup().Result(),
policyYAML: `version: v1
namespacedFilterPolicies:
- namespaces:
- ns-1
resourceFilters:
- kinds:
- pods
names:
- pod-2
- namespaces:
- ns-*
resourceFilters:
- kinds:
- pods
names:
- pod-1
`,
tarball: test.NewTarWriter(t).
AddItems("pods",
builder.ForPod("ns-1", "pod-1").Result(),
builder.ForPod("ns-1", "pod-2").Result(),
builder.ForPod("ns-2", "pod-1").Result(),
builder.ForPod("ns-2", "pod-2").Result(),
).
Done(),
apiResources: []*test.APIResource{
test.Pods(),
},
want: map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-2", "ns-2/pod-1"},
},
},
{
@@ -134,7 +170,8 @@ namespacedFilterPolicies:
- kinds:
- '*'
labelSelector:
app: test
matchLabels:
app: test
`,
tarball: test.NewTarWriter(t).
AddItems("pods",
+414
View File
@@ -2150,6 +2150,102 @@ func TestRestoreActionAdditionalItems(t *testing.T) {
test.PVs(): nil,
},
},
{
name: "must-include annotation bypasses resource exclusion for additional items",
restore: defaultRestore().IncludedResources("pods").Result(),
backup: defaultBackup().Result(),
tarball: test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
Done(),
apiResources: []*test.APIResource{test.Pods(), test.PVs()},
actions: []riav2.RestoreItemAction{
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
},
}, nil
},
},
},
want: map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-1"},
test.PVs(): {"/pv-1"},
},
},
{
name: "must-include annotation bypasses namespace exclusion for additional items",
restore: defaultRestore().IncludedNamespaces("ns-1").Result(),
backup: defaultBackup().Result(),
tarball: test.NewTarWriter(t).AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()).Done(),
apiResources: []*test.APIResource{test.Pods()},
actions: []riav2.RestoreItemAction{
&pluggableAction{
selector: velero.ResourceSelector{IncludedNamespaces: []string{"ns-1"}},
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.Pods, Namespace: "ns-2", Name: "pod-2"},
},
}, nil
},
},
},
want: map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-1", "ns-2/pod-2"},
},
},
{
name: "must-include annotation bypasses IncludeClusterResources=false for additional items",
restore: defaultRestore().IncludeClusterResources(false).Result(),
backup: defaultBackup().Result(),
tarball: test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
Done(),
apiResources: []*test.APIResource{test.Pods(), test.PVs()},
actions: []riav2.RestoreItemAction{
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
},
}, nil
},
},
},
want: map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-1"},
test.PVs(): {"/pv-1"},
},
},
}
for _, tc := range tests {
@@ -2180,6 +2276,324 @@ func TestRestoreActionAdditionalItems(t *testing.T) {
}
}
// TestRestoreMustIncludeAdditionalItems covers restore must-include edge cases beyond the
// basic filter-bypass cases in TestRestoreActionAdditionalItems.
func TestRestoreMustIncludeAdditionalItems(t *testing.T) {
t.Run("must-include annotation is stripped from the restored item", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.Pods())
data := &Request{
Log: h.log,
Restore: defaultRestore().Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
[]riav2.RestoreItemAction{
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
annotations["keep-me"] = "yes"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{UpdatedItem: item}, nil
},
},
},
nil,
)
assertEmptyResults(t, warnings, errs)
got, err := h.DynamicClient.Resource(test.Pods().GVR()).Namespace("ns-1").Get(t.Context(), "pod-1", metav1.GetOptions{})
require.NoError(t, err)
annotations := got.GetAnnotations()
assert.NotContains(t, annotations, velerov1api.MustIncludeAdditionalItemRestoreAnnotation)
assert.Equal(t, "yes", annotations["keep-me"])
})
t.Run("non-true must-include annotation is stripped without bypassing filters", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.Pods())
h.AddItems(t, test.PVs())
data := &Request{
Log: h.log,
Restore: defaultRestore().IncludedResources("pods").Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
[]riav2.RestoreItemAction{
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "True"
annotations["keep-me"] = "yes"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
},
}, nil
},
},
},
nil,
)
assertEmptyResults(t, warnings, errs)
assertAPIContents(t, h, map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-1"},
test.PVs(): nil,
})
got, err := h.DynamicClient.Resource(test.Pods().GVR()).Namespace("ns-1").Get(t.Context(), "pod-1", metav1.GetOptions{})
require.NoError(t, err)
annotations := got.GetAnnotations()
assert.NotContains(t, annotations, velerov1api.MustIncludeAdditionalItemRestoreAnnotation)
assert.Equal(t, "yes", annotations["keep-me"])
})
t.Run("SkipRestore supersedes must-include annotation and skips additional items", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.Pods())
h.AddItems(t, test.PVs())
data := &Request{
Log: h.log,
Restore: defaultRestore().IncludedResources("pods").Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
[]riav2.RestoreItemAction{
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
SkipRestore: true,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
},
}, nil
},
},
},
nil,
)
assertEmptyResults(t, warnings, errs)
assertAPIContents(t, h, map[*test.APIResource][]string{
test.Pods(): nil,
test.PVs(): nil,
})
})
t.Run("must-include does not restore additional items missing from the backup tarball", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.Pods())
h.AddItems(t, test.PVs())
data := &Request{
Log: h.log,
Restore: defaultRestore().IncludedResources("pods").Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
[]riav2.RestoreItemAction{
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-missing"},
},
}, nil
},
},
},
nil,
)
assertEmptyResults(t, errs)
assertNonEmptyResults(t, "warning", warnings)
assertAPIContents(t, h, map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-1"},
test.PVs(): nil,
})
})
t.Run("transitive must-include requires each RIA level to re-set the annotation", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.Pods())
h.AddItems(t, test.PVs())
h.AddItems(t, test.PVCs())
data := &Request{
Log: h.log,
Restore: defaultRestore().IncludedResources("pods").Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-2", "pvc-1").Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
[]riav2.RestoreItemAction{
// Parent pod RIA force-includes the excluded PV.
&pluggableAction{
selector: velero.ResourceSelector{IncludedResources: []string{"pods"}},
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
},
}, nil
},
},
// Child PV RIA also re-sets the annotation to force-include an excluded PVC.
&pluggableAction{
selector: velero.ResourceSelector{IncludedResources: []string{"persistentvolumes"}},
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumeClaims, Namespace: "ns-2", Name: "pvc-1"},
},
}, nil
},
},
},
nil,
)
assertEmptyResults(t, warnings, errs)
assertAPIContents(t, h, map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-1"},
test.PVs(): {"/pv-1"},
test.PVCs(): {"ns-2/pvc-1"},
})
})
t.Run("without re-annotating, transitive additional items still respect filters", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.Pods())
h.AddItems(t, test.PVs())
h.AddItems(t, test.PVCs())
data := &Request{
Log: h.log,
Restore: defaultRestore().IncludedResources("pods").Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-2", "pvc-1").Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
[]riav2.RestoreItemAction{
&pluggableAction{
selector: velero.ResourceSelector{IncludedResources: []string{"pods"}},
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
},
}, nil
},
},
// Child PV RIA returns an additional PVC but does NOT set must-include.
&pluggableAction{
selector: velero.ResourceSelector{IncludedResources: []string{"persistentvolumes"}},
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
return &velero.RestoreItemActionExecuteOutput{
UpdatedItem: input.Item,
AdditionalItems: []velero.ResourceIdentifier{
{GroupResource: kuberesource.PersistentVolumeClaims, Namespace: "ns-2", Name: "pvc-1"},
},
}, nil
},
},
},
nil,
)
assertEmptyResults(t, warnings, errs)
assertAPIContents(t, h, map[*test.APIResource][]string{
test.Pods(): {"ns-1/pod-1"},
test.PVs(): {"/pv-1"},
test.PVCs(): nil,
})
})
}
// TestShouldRestore runs the ShouldRestore function for various permutations of
// existing/nonexisting/being-deleted PVs, PVCs, and namespaces, and verifies the
// result/error matches expectations.
+15 -2
View File
@@ -121,7 +121,10 @@ func snapshotSource(
return "", 0, errors.Wrapf(err, "Failed to run uploader backup for si %v", source)
}
snap.Tags = make(map[string]string)
if snap.Tags == nil {
snap.Tags = make(map[string]string)
}
snap.Tags[uploader.CBTChangeIDTag] = cbtSource.ChangeID
snap.Tags[uploader.CBTVolumeIDTag] = cbtSource.VolumeID
if snapshotTags != nil {
@@ -222,7 +225,17 @@ func Restore(ctx context.Context, blkUp Uploader, rep udmrepo.BackupRepo, snapsh
defer destDev.Close()
size, err := blkUp.Restore(snapshot, destInfo{dev: destDev, path: destPath}, bitmap.Iterator(), uploaderCfg)
destSize, err := destDev.Seek(0, io.SeekEnd)
if err != nil {
return 0, errors.Wrapf(err, "error getting length of block device %s", dest)
}
_, err = destDev.Seek(0, io.SeekStart)
if err != nil {
return 0, errors.Wrapf(err, "error reset pos of block device %s", dest)
}
size, err := blkUp.Restore(snapshot, destInfo{dev: destDev, path: destPath, size: destSize}, bitmap.Iterator(), uploaderCfg)
if err != nil {
return 0, errors.Wrapf(err, "error restoring to block dev %s", destPath)
}
+244 -4
View File
@@ -17,11 +17,13 @@ limitations under the License.
package block
import (
"bytes"
"context"
"fmt"
"io"
"os"
"runtime"
"strconv"
"strings"
"github.com/cockroachdb/errors"
@@ -36,8 +38,9 @@ import (
var ErrCanceled = errors.New("uploader is canceled")
const (
blockSize = (1 << 20)
bufferSize = 100 << 20
blockSize = (1 << 20)
bufferSize = 100 << 20
bdevSourceSizeTag = "bdev-source-size"
)
type sourceInfo struct {
@@ -49,6 +52,7 @@ type sourceInfo struct {
type destInfo struct {
dev *os.File
path string
size int64
}
type Uploader interface {
@@ -135,12 +139,52 @@ func (blkup *blockUploader) Backup(source sourceInfo, parentObject udmrepo.ID, b
Type: udmrepo.ObjectDataTypeMetadata,
Permissions: 0o777,
},
Tags: map[string]string{
bdevSourceSizeTag: strconv.FormatInt(source.size, 10),
},
}, backupSize, nil
}
// TODO implement in following PRs
func (blkup *blockUploader) Restore(snapshot udmrepo.Snapshot, dest destInfo, bitmap cbt.Iterator, configs map[string]string) (int64, error) {
return 0, errors.New("not implemented")
if bitmap == nil {
return 0, errors.New("bitmap is not available")
}
meta, err := blkup.repoWriter.ReadMetadata(blkup.ctx, snapshot.RootObject.ID)
if err != nil {
return 0, errors.Wrapf(err, "error reading snapshot metadata for %s", snapshot.Description)
}
if len(meta.SubObjects) != 1 {
return 0, errors.Errorf("unexpected number of bdev object (%d) for snapshot %s", len(meta.SubObjects), snapshot.Description)
}
sourceSize, err := getSourceSize(snapshot)
if err != nil {
sourceSize = meta.SubObjects[0].Size
blkup.log.Warnf("Failed to get source size from snapshot %s, use backup size %v", snapshot.Description, sourceSize)
}
if sourceSize > meta.SubObjects[0].Size {
return 0, errors.Wrapf(err, "unexpected size (%v vs. %v) for bdev object %s", meta.SubObjects[0].Size, sourceSize, meta.SubObjects[0].Name)
}
if sourceSize > dest.size {
return 0, errors.Wrapf(err, "dest dev(%s) size is too small (%v vs. %v)", dest.path, dest.size, sourceSize)
}
reader, err := blkup.repoWriter.OpenObject(blkup.ctx, meta.SubObjects[0].ID)
if err != nil {
return 0, errors.Wrapf(err, "error opening bdev object %v", meta.SubObjects[0].Name)
}
defer reader.Close()
size, err := blkup.restoreData(reader, dest.dev, bitmap, sourceSize, dest.path)
if err != nil {
return 0, errors.Wrapf(err, "error restoring bdev object %s to volume %s", meta.SubObjects[0].Name, dest.path)
}
return size, nil
}
func (blkup *blockUploader) backupObject(dev *os.File, dest udmrepo.ObjectWriter, bitmap cbt.Iterator, totalLength int64) (udmrepo.ID, int64, int64, error) {
@@ -319,6 +363,202 @@ func getObjectName(source string) string {
return strings.Trim(s, "-")
}
func (blkup *blockUploader) restoreData(reader io.ReadSeeker, dest *os.File, bitmap cbt.Iterator, totalLength int64, destPath string) (int64, error) {
list := freelist.New(bufferSize, blockSize)
resultChan := make(chan readResult, list.Capacity())
zeroBlock := make([]byte, blockSize)
totalCount := bitmap.Count()
quit := make(chan struct{})
defer close(quit)
go func() {
defer close(resultChan)
offset, valid := bitmap.Next()
var buffer []byte
var nextPos = uint64(0)
for valid {
select {
case <-blkup.ctx.Done():
return
case <-quit:
return
case buffer = <-list.Chunks():
}
var err error
if nextPos != offset {
_, err = reader.Seek(int64(offset), io.SeekStart)
}
if err == nil {
var length int
length, err = io.ReadFull(reader, buffer)
if err == nil && length <= 0 {
err = io.ErrUnexpectedEOF
}
}
r := readResult{
buffer: buffer,
offset: int64(offset),
err: err,
}
if r.err != nil {
r.resetBuffer(list)
}
resultChan <- r
if r.err != nil {
return
}
nextPos = offset + uint64(blockSize)
offset, valid = bitmap.Next()
}
}()
var written int64
var result readResult
var writeErr error
var readerRunning bool
var zeroStart int64 = -1
var zeroLength int64
var curCount int64
for curCount < int64(totalCount) {
select {
case <-blkup.ctx.Done():
writeErr = ErrCanceled
case result, readerRunning = <-resultChan:
if !readerRunning {
if blkup.ctx.Err() != nil {
writeErr = ErrCanceled
} else {
writeErr = io.ErrUnexpectedEOF
}
}
}
if writeErr != nil {
break
}
if result.err != nil {
writeErr = result.err
break
}
length := min(int64(blockSize), totalLength-result.offset)
if bytes.Equal(result.buffer, zeroBlock) {
if zeroStart == -1 {
zeroStart = result.offset
zeroLength = length
} else if result.offset == zeroStart+zeroLength {
zeroLength += length
} else {
if err := blkup.flushZeroBlocks(dest, zeroStart, zeroLength, zeroBlock, destPath); err != nil {
writeErr = errors.Wrapf(err, "error flushing zero blocks from %v, length %v", zeroStart, zeroLength)
break
}
zeroStart = result.offset
zeroLength = length
}
} else {
if zeroStart != -1 {
if err := blkup.flushZeroBlocks(dest, zeroStart, zeroLength, zeroBlock, destPath); err != nil {
writeErr = errors.Wrapf(err, "error flushing zero blocks from %v, length %v", zeroStart, zeroLength)
break
}
zeroStart = -1
zeroLength = 0
}
n, err := dest.WriteAt(result.buffer[:length], result.offset)
if err != nil {
writeErr = err
break
}
if length != int64(n) {
writeErr = io.ErrShortWrite
break
}
}
written += length
curCount++
result.resetBuffer(list)
blkup.progress.UpdateProgress(&uploader.Progress{BytesDone: written, TotalBytes: totalLength})
}
result.resetBuffer(list)
if writeErr != nil {
return written, writeErr
}
if zeroStart != -1 {
if err := blkup.flushZeroBlocks(dest, zeroStart, zeroLength, zeroBlock, destPath); err != nil {
return written, errors.Wrapf(err, "error flushing zero blocks from %v, length %v", zeroStart, zeroLength)
}
}
return written, nil
}
func (blkup *blockUploader) flushZeroBlocks(dest *os.File, start int64, length int64, zeroBlock []byte, destPath string) error {
err := blkZeroOut(dest, start, length)
if err == nil {
return nil
}
blkup.log.WithError(err).Warnf("Failed to call zero out from dev %s, start %v, length %v. Fallback to conservative way", destPath, start, length)
var written int64
for written < length {
writeSize := min(len(zeroBlock), int(length-written))
n, err := dest.WriteAt(zeroBlock[:writeSize], start+written)
if err != nil {
return errors.Wrapf(err, "error writing zero buffer at %v, length %v", start+written, writeSize)
}
if writeSize != n {
return errors.Wrapf(err, "short write zero buffer at %v, length %v", start+written, writeSize)
}
written += int64(writeSize)
}
return nil
}
func getSourceSize(snapshot udmrepo.Snapshot) (int64, error) {
if snapshot.Tags == nil {
return 0, errors.New("source size tag is empty")
}
s, found := snapshot.Tags[bdevSourceSizeTag]
if !found {
return 0, errors.New("source size tag is missing")
}
size, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0, errors.Wrapf(err, "error parsing size from %s", s)
}
return size, nil
}
func loadObjectFromSnapshot(ctx context.Context, rep udmrepo.BackupRepo, snapshot *udmrepo.Snapshot) (udmrepo.ID, error) {
if snapshot == nil {
return "", errors.New("snapshot is empty")
+228
View File
@@ -460,3 +460,231 @@ func TestLoadObjectFromSnapshot(t *testing.T) {
})
}
}
func TestGetSourceSize(t *testing.T) {
testCases := []struct {
name string
snapshot udmrepo.Snapshot
expectErr bool
expected int64
}{
{
name: "nil tags",
snapshot: udmrepo.Snapshot{},
expectErr: true,
},
{
name: "missing tag",
snapshot: udmrepo.Snapshot{
Tags: map[string]string{},
},
expectErr: true,
},
{
name: "invalid tag value",
snapshot: udmrepo.Snapshot{
Tags: map[string]string{
bdevSourceSizeTag: "abc",
},
},
expectErr: true,
},
{
name: "valid tag value",
snapshot: udmrepo.Snapshot{
Tags: map[string]string{
bdevSourceSizeTag: "1048576",
},
},
expectErr: false,
expected: 1048576,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
size, err := getSourceSize(tc.snapshot)
if tc.expectErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expected, size)
}
})
}
}
func TestFlushZeroBlocks(t *testing.T) {
t.Run("success via write fallback", func(t *testing.T) {
f, err := os.CreateTemp(t.TempDir(), "zerotest-*")
require.NoError(t, err)
defer os.Remove(f.Name())
defer f.Close()
require.NoError(t, f.Truncate(2048))
blkup := &blockUploader{
log: logrus.New(),
}
blkup.log.(*logrus.Logger).Out = io.Discard
zeroBlock := make([]byte, 1024)
err = blkup.flushZeroBlocks(f, 0, 2048, zeroBlock, f.Name())
require.NoError(t, err)
data, err := os.ReadFile(f.Name())
require.NoError(t, err)
assert.Equal(t, make([]byte, 2048), data)
})
}
type errReader struct {
err error
}
func (r *errReader) Read(p []byte) (n int, err error) {
return 0, r.err
}
func (r *errReader) Seek(offset int64, whence int) (int64, error) {
return 0, nil
}
func TestRestoreData(t *testing.T) {
t.Run("success", func(t *testing.T) {
ctx := context.Background()
progress := &mockProgressUpdater{}
progress.On("UpdateProgress", mock.Anything).Return()
blkup := &blockUploader{
ctx: ctx,
progress: progress,
log: logrus.New(),
}
f, err := os.CreateTemp(t.TempDir(), "restoretest-*")
require.NoError(t, err)
defer os.Remove(f.Name())
defer f.Close()
data := make([]byte, 1048576)
for i := range data {
data[i] = 1
}
reader := bytes.NewReader(data)
iterMock := cbtmocks.NewIterator(t)
iterMock.On("Count").Return(uint64(1))
iterMock.On("Next").Return(uint64(0), true).Once()
iterMock.On("Next").Return(uint64(0), false)
written, err := blkup.restoreData(reader, f, iterMock, 1048576, f.Name())
require.NoError(t, err)
assert.Equal(t, int64(1048576), written)
f.Seek(0, 0)
writtenData, err := io.ReadAll(f)
require.NoError(t, err)
assert.Equal(t, data, writtenData)
})
t.Run("read err", func(t *testing.T) {
ctx := context.Background()
blkup := &blockUploader{
ctx: ctx,
log: logrus.New(),
}
f, err := os.CreateTemp(t.TempDir(), "restoretest-*")
require.NoError(t, err)
defer os.Remove(f.Name())
defer f.Close()
reader := &errReader{err: errors.New("read error")}
iterMock := cbtmocks.NewIterator(t)
iterMock.On("Count").Return(uint64(1))
iterMock.On("Next").Return(uint64(0), true).Once()
iterMock.On("Next").Return(uint64(0), false)
_, err = blkup.restoreData(reader, f, iterMock, 1048576, f.Name())
require.Error(t, err)
assert.Contains(t, err.Error(), "read error")
})
}
func TestBlockUploaderRestore(t *testing.T) {
t.Run("missing metadata", func(t *testing.T) {
ctx := context.Background()
repoWriter := udmrepomocks.NewBackupRepo(t)
blkup := NewUploader(ctx, repoWriter, nil, logrus.New())
repoWriter.On("ReadMetadata", mock.Anything, udmrepo.ID("root-id")).Return(nil, errors.New("meta not found"))
iterMock := cbtmocks.NewIterator(t)
_, err := blkup.Restore(udmrepo.Snapshot{RootObject: udmrepo.ObjectMetadata{ID: "root-id"}}, destInfo{}, iterMock, nil)
require.Error(t, err)
assert.Contains(t, err.Error(), "meta not found")
})
t.Run("success", func(t *testing.T) {
ctx := context.Background()
repoWriter := udmrepomocks.NewBackupRepo(t)
progress := &mockProgressUpdater{}
progress.On("UpdateProgress", mock.Anything).Return()
blkup := NewUploader(ctx, repoWriter, progress, logrus.New())
f, err := os.CreateTemp(t.TempDir(), "restoretest-*")
require.NoError(t, err)
defer os.Remove(f.Name())
defer f.Close()
meta := &udmrepo.Metadata{
SubObjects: []udmrepo.ObjectMetadata{
{
ID: "data-id",
Name: "bdev",
Size: 1048576,
},
},
}
repoWriter.On("ReadMetadata", mock.Anything, udmrepo.ID("root-id")).Return(meta, nil)
objReader := udmrepomocks.NewObjectReader(t)
objReader.On("Read", mock.Anything).Run(func(args mock.Arguments) {
p := args.Get(0).([]byte)
for i := range p {
p[i] = 1
}
}).Return(1048576, io.EOF).Once()
objReader.On("Read", mock.Anything).Return(0, io.EOF)
objReader.On("Close").Return(nil)
repoWriter.On("OpenObject", mock.Anything, udmrepo.ID("data-id")).Return(objReader, nil)
snap := udmrepo.Snapshot{
Description: "test snapshot",
RootObject: udmrepo.ObjectMetadata{ID: "root-id"},
Tags: map[string]string{
bdevSourceSizeTag: "1048576",
},
}
dest := destInfo{
dev: f,
size: 2048576,
path: f.Name(),
}
iterMock := cbtmocks.NewIterator(t)
iterMock.On("Count").Return(uint64(1))
iterMock.On("Next").Return(uint64(0), true).Once()
iterMock.On("Next").Return(uint64(0), false)
written, err := blkup.Restore(snap, dest, iterMock, nil)
require.NoError(t, err)
assert.Equal(t, int64(1048576), written)
})
}
+12 -1
View File
@@ -32,7 +32,18 @@ const (
// IsBuiltInDataMover reports whether the given data mover value refers to a
// Velero built-in data mover (an empty value or the default "velero" alias).
func IsBuiltInDataMover(dataMover string) bool {
return dataMover == "" || dataMover == DataMoverTypeVelero
return IsVeleroBlockDataMover(dataMover) || IsVeleroFSDataMover(dataMover)
}
func IsVeleroFSDataMover(dataMover string) bool {
if dataMover == "" || dataMover == DataMoverTypeVelero {
dataMover = DataMoverTypeVeleroFs
}
return dataMover == DataMoverTypeVeleroFs
}
func IsVeleroBlockDataMover(dataMover string) bool {
return dataMover == DataMoverTypeVeleroBlock
}
// GetDefaultBuiltInDataMover returns the data mover used when the default
+68
View File
@@ -38,6 +38,16 @@ func TestIsBuiltInDataMover(t *testing.T) {
dataMover: "velero",
want: true,
},
{
name: "velero-fs dataMover is builtin",
dataMover: "velero-fs",
want: true,
},
{
name: "velero-block dataMover is builtin",
dataMover: "velero-block",
want: true,
},
{
name: "kopia dataMover is not builtin",
dataMover: "kopia",
@@ -54,3 +64,61 @@ func TestIsBuiltInDataMover(t *testing.T) {
func TestGetDefaultBuiltInDataMover(t *testing.T) {
assert.Equal(t, DataMoverTypeVeleroFs, GetDefaultBuiltInDataMover())
}
func TestIsFSDataMover(t *testing.T) {
testcases := []struct {
name string
dataMover string
want bool
}{
{
name: "empty dataMover is fs",
dataMover: "",
want: true,
},
{
name: "velero dataMover is fs",
dataMover: "velero",
want: true,
},
{
name: "velero-fs dataMover is fs",
dataMover: "velero-fs",
want: true,
},
{
name: "velero-block dataMover is not fs",
dataMover: "velero-block",
want: false,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(tt *testing.T) {
assert.Equal(tt, tc.want, IsVeleroFSDataMover(tc.dataMover))
})
}
}
func TestIsBlockDataMover(t *testing.T) {
testcases := []struct {
name string
dataMover string
want bool
}{
{
name: "velero-block dataMover is block",
dataMover: "velero-block",
want: true,
},
{
name: "velero-fs dataMover is not block",
dataMover: "velero-fs",
want: false,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(tt *testing.T) {
assert.Equal(tt, tc.want, IsVeleroBlockDataMover(tc.dataMover))
})
}
}