mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-06-10 00:03:10 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 360e7ef707 | |||
| 9331bffa7d | |||
| c602efe054 |
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Make ci
|
||||
run: make ci
|
||||
- name: Upload test coverage
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: coverage.out
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
- name: Test
|
||||
run: make test
|
||||
- name: Upload test coverage
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: coverage.out
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Restores from backups not in a completed or partially failed phase are now rejected.
|
||||
@@ -1 +0,0 @@
|
||||
Fix issue #9813, add validations for ClusterScopedFilterPolicy
|
||||
@@ -1 +0,0 @@
|
||||
Fix issue #9816, add cli support for backup with ClusterScopedFilterPolicy and NamespacedFilterPolicies
|
||||
@@ -1 +0,0 @@
|
||||
Make ToSystemAffinity deterministic by sorting MatchLabels keys to avoid spurious affinity spec diffs and restarts
|
||||
@@ -96,9 +96,7 @@ RUN ARCH=$(go env GOARCH) && \
|
||||
chmod +x /usr/bin/goreleaser
|
||||
|
||||
# get golangci-lint
|
||||
# Use "go install" so the download goes through GOPROXY instead of the GitHub
|
||||
# release API/CDN, which has been returning intermittent/persistent HTTP 504s.
|
||||
RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0
|
||||
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.5.0
|
||||
|
||||
# install kubectl
|
||||
RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/$(go env GOARCH)/kubectl
|
||||
|
||||
@@ -261,10 +261,6 @@ func (p *Policies) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.validateClusterScopedFilterPolicy(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := p.validateNamespacedFilterPolicies(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@@ -418,44 +414,3 @@ func (p *Policies) validateNamespacedFilterPolicies() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Policies) validateClusterScopedFilterPolicy() error {
|
||||
if p.clusterScopedFilterPolicy == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(p.clusterScopedFilterPolicy.ResourceFilters) == 0 {
|
||||
return fmt.Errorf("clusterScopedFilterPolicy: resourceFilters cannot be empty; remove the policy block entirely if it is not needed")
|
||||
}
|
||||
|
||||
seenKinds := make(map[string]int)
|
||||
for j, rf := range p.clusterScopedFilterPolicy.ResourceFilters {
|
||||
if rf.IsCatchAll() {
|
||||
return fmt.Errorf("clusterScopedFilterPolicy.resourceFilters[%d]: kinds must be specified (catch-all is not supported)", j)
|
||||
}
|
||||
|
||||
for _, kind := range rf.Kinds {
|
||||
if prevJ, ok := seenKinds[kind]; ok {
|
||||
return fmt.Errorf("clusterScopedFilterPolicy: kind %q appears in both resourceFilters[%d] and resourceFilters[%d]", kind, prevJ, j)
|
||||
}
|
||||
seenKinds[kind] = j
|
||||
}
|
||||
|
||||
if len(rf.LabelSelector) > 0 && len(rf.OrLabelSelectors) > 0 {
|
||||
return fmt.Errorf("clusterScopedFilterPolicy.resourceFilters[%d]: labelSelector and orLabelSelectors cannot co-exist", j)
|
||||
}
|
||||
|
||||
for k, pattern := range rf.Names {
|
||||
if _, err := glob.Compile(pattern); err != nil {
|
||||
return fmt.Errorf("clusterScopedFilterPolicy.resourceFilters[%d].names[%d]: invalid glob pattern %q: %v", j, k, pattern, err)
|
||||
}
|
||||
}
|
||||
for k, pattern := range rf.ExcludedNames {
|
||||
if _, err := glob.Compile(pattern); err != nil {
|
||||
return fmt.Errorf("clusterScopedFilterPolicy.resourceFilters[%d].excludedNames[%d]: invalid glob pattern %q: %v", j, k, pattern, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1536,147 +1536,3 @@ namespacedFilterPolicies:
|
||||
assert.Equal(t, []string{"team-*", "another-pattern"}, policy2.Namespaces)
|
||||
assert.Equal(t, []string{"Deployment", "Service"}, policy2.ResourceFilters[0].Kinds)
|
||||
}
|
||||
|
||||
func TestClusterScopedFilterPolicies(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
yamlData string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid - single kind with names",
|
||||
yamlData: `version: v1
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters:
|
||||
- kinds: ["ClusterRole"]
|
||||
names: ["my-app-*"]`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid - multi-kind with labelSelector",
|
||||
yamlData: `version: v1
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters:
|
||||
- kinds: ["ClusterRole", "ClusterRoleBinding"]
|
||||
labelSelector:
|
||||
app: my-app`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid - orLabelSelectors",
|
||||
yamlData: `version: v1
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters:
|
||||
- kinds: ["CustomResourceDefinition"]
|
||||
orLabelSelectors:
|
||||
- app: my-app
|
||||
- app: other-app`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid - excludedNames",
|
||||
yamlData: `version: v1
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters:
|
||||
- kinds: ["ClusterRole"]
|
||||
names: ["my-*"]
|
||||
excludedNames: ["my-debug-*"]`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid - empty resourceFilters",
|
||||
yamlData: `version: v1
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters: []`,
|
||||
wantErr: true,
|
||||
errMsg: "resourceFilters cannot be empty; remove the policy block entirely if it is not needed",
|
||||
},
|
||||
{
|
||||
name: "invalid - empty kinds in clusterScopedFilterPolicy",
|
||||
yamlData: `version: v1
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters:
|
||||
- kinds: []
|
||||
names: ["my-app-*"]`,
|
||||
wantErr: true,
|
||||
errMsg: "kinds must be specified",
|
||||
},
|
||||
{
|
||||
name: "invalid - asterisk kinds (explicit catch-all) in clusterScopedFilterPolicy",
|
||||
yamlData: `version: v1
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters:
|
||||
- kinds: ["*"]
|
||||
labelSelector:
|
||||
app: my-app`,
|
||||
wantErr: true,
|
||||
errMsg: "kinds must be specified",
|
||||
},
|
||||
{
|
||||
name: "invalid - duplicate kinds across entries",
|
||||
yamlData: `version: v1
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters:
|
||||
- kinds: ["ClusterRole"]
|
||||
names: ["my-app-*"]
|
||||
- kinds: ["ClusterRole"]
|
||||
labelSelector:
|
||||
app: other`,
|
||||
wantErr: true,
|
||||
errMsg: `kind "ClusterRole" appears in both`,
|
||||
},
|
||||
{
|
||||
name: "invalid - labelSelector and orLabelSelectors co-exist",
|
||||
yamlData: `version: v1
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters:
|
||||
- kinds: ["ClusterRole"]
|
||||
labelSelector:
|
||||
app: my-app
|
||||
orLabelSelectors:
|
||||
- app: other`,
|
||||
wantErr: true,
|
||||
errMsg: "labelSelector and orLabelSelectors cannot co-exist",
|
||||
},
|
||||
{
|
||||
name: "invalid - bad glob in names",
|
||||
yamlData: `version: v1
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters:
|
||||
- kinds: ["ClusterRole"]
|
||||
names: ["[invalid"]`,
|
||||
wantErr: true,
|
||||
errMsg: "invalid glob pattern",
|
||||
},
|
||||
{
|
||||
name: "invalid - bad glob in excludedNames",
|
||||
yamlData: `version: v1
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters:
|
||||
- kinds: ["ClusterRole"]
|
||||
excludedNames: ["[bad"]`,
|
||||
wantErr: true,
|
||||
errMsg: "invalid glob pattern",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
resPolicies, err := unmarshalResourcePolicies(&tc.yamlData)
|
||||
require.NoError(t, err)
|
||||
|
||||
policies := &Policies{}
|
||||
err = policies.BuildPolicy(resPolicies)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = policies.Validate()
|
||||
if tc.wantErr {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.errMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -31,7 +30,6 @@ import (
|
||||
|
||||
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/fatih/color"
|
||||
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -42,7 +40,6 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest"
|
||||
"github.com/vmware-tanzu/velero/pkg/itemoperation"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
|
||||
"github.com/vmware-tanzu/velero/internal/volume"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/collections"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/results"
|
||||
@@ -94,9 +91,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)
|
||||
}
|
||||
|
||||
if backup.Spec.UploaderConfig != nil && backup.Spec.UploaderConfig.ParallelFilesUpload > 0 {
|
||||
@@ -136,119 +130,6 @@ func DescribeResourcePolicies(d *Describer, resPolicies *corev1api.TypedLocalObj
|
||||
d.Printf("\tName:\t%s\n", resPolicies.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"
|
||||
@@ -869,85 +866,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,16 +21,13 @@ 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"
|
||||
|
||||
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
|
||||
"github.com/vmware-tanzu/velero/internal/volume"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/cacert"
|
||||
@@ -57,7 +54,6 @@ func DescribeBackupInSF(
|
||||
|
||||
if backup.Spec.ResourcePolicy != nil {
|
||||
DescribeResourcePoliciesInSF(d, backup.Spec.ResourcePolicy)
|
||||
DescribeFineGrainedFilterPoliciesInSF(ctx, kbClient, d, backup)
|
||||
}
|
||||
|
||||
status := backup.Status
|
||||
@@ -226,88 +222,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"
|
||||
@@ -710,96 +707,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))
|
||||
}
|
||||
|
||||
@@ -399,17 +399,6 @@ func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInf
|
||||
return backupInfo{}, nil
|
||||
}
|
||||
|
||||
// reject restores from backups that are not in a usable phase
|
||||
switch info.backup.Status.Phase {
|
||||
case api.BackupPhaseCompleted, api.BackupPhasePartiallyFailed:
|
||||
// ok
|
||||
default:
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors,
|
||||
fmt.Sprintf("backup %q is in phase %q and cannot be used as a restore source",
|
||||
info.backup.Name, info.backup.Status.Phase))
|
||||
return backupInfo{}, nil
|
||||
}
|
||||
|
||||
// Fill in the ScheduleName so it's easier to consume for metrics.
|
||||
if restore.Spec.ScheduleName == "" {
|
||||
restore.Spec.ScheduleName = info.backup.GetLabels()[api.ScheduleNameLabel]
|
||||
|
||||
@@ -305,7 +305,7 @@ func TestRestoreReconcile(t *testing.T) {
|
||||
name: "restorer throwing an error causes the restore to fail",
|
||||
location: defaultStorageLocation,
|
||||
restore: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Result(),
|
||||
restorerError: errors.New("blarg"),
|
||||
expectedErr: false,
|
||||
expectedPhase: string(velerov1api.RestorePhaseInProgress),
|
||||
@@ -319,7 +319,7 @@ func TestRestoreReconcile(t *testing.T) {
|
||||
name: "valid restore with none existingresourcepolicy gets executed",
|
||||
location: defaultStorageLocation,
|
||||
restore: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).ExistingResourcePolicy("none").Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Result(),
|
||||
expectedErr: false,
|
||||
expectedPhase: string(velerov1api.RestorePhaseInProgress),
|
||||
expectedStartTime: ×tamp,
|
||||
@@ -330,7 +330,7 @@ func TestRestoreReconcile(t *testing.T) {
|
||||
name: "valid restore with update existingresourcepolicy gets executed",
|
||||
location: defaultStorageLocation,
|
||||
restore: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).ExistingResourcePolicy("update").Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Result(),
|
||||
expectedErr: false,
|
||||
expectedPhase: string(velerov1api.RestorePhaseInProgress),
|
||||
expectedStartTime: ×tamp,
|
||||
@@ -352,7 +352,7 @@ func TestRestoreReconcile(t *testing.T) {
|
||||
name: "valid restore gets executed",
|
||||
location: defaultStorageLocation,
|
||||
restore: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Result(),
|
||||
expectedErr: false,
|
||||
expectedPhase: string(velerov1api.RestorePhaseInProgress),
|
||||
expectedStartTime: ×tamp,
|
||||
@@ -363,7 +363,7 @@ func TestRestoreReconcile(t *testing.T) {
|
||||
name: "valid restore gets executed and only includes pod volume backups from restore namespace",
|
||||
location: defaultStorageLocation,
|
||||
restore: NewRestore("foo", "bar2", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Result(),
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
builder.ForPodVolumeBackup("foo", "pvb-1").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(),
|
||||
builder.ForPodVolumeBackup("other-ns", "pvb-2").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(),
|
||||
@@ -444,7 +444,7 @@ func TestRestoreReconcile(t *testing.T) {
|
||||
expectedStartTime: ×tamp,
|
||||
expectedCompletedTime: ×tamp,
|
||||
backupStoreGetBackupContentsErr: errors.New("Couldn't download backup"),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Result(),
|
||||
},
|
||||
{
|
||||
name: "restore attached with an expected finalizer gets cleaned up successfully",
|
||||
@@ -473,7 +473,7 @@ func TestRestoreReconcile(t *testing.T) {
|
||||
name: "valid restore with empty VolumeInfos",
|
||||
location: defaultStorageLocation,
|
||||
restore: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Result(),
|
||||
emptyVolumeInfo: true,
|
||||
expectedErr: false,
|
||||
expectedPhase: string(velerov1api.RestorePhaseInProgress),
|
||||
@@ -497,44 +497,6 @@ func TestRestoreReconcile(t *testing.T) {
|
||||
backup: defaultBackup().StorageLocation("default").Result(),
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "restore from backup in Deleting phase fails validation",
|
||||
location: defaultStorageLocation,
|
||||
restore: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseDeleting).Result(),
|
||||
expectedErr: false,
|
||||
expectedPhase: string(velerov1api.RestorePhaseFailedValidation),
|
||||
expectedValidationErrors: []string{`backup "backup-1" is in phase "Deleting" and cannot be used as a restore source`},
|
||||
},
|
||||
{
|
||||
name: "restore from backup in InProgress phase fails validation",
|
||||
location: defaultStorageLocation,
|
||||
restore: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseInProgress).Result(),
|
||||
expectedErr: false,
|
||||
expectedPhase: string(velerov1api.RestorePhaseFailedValidation),
|
||||
expectedValidationErrors: []string{`backup "backup-1" is in phase "InProgress" and cannot be used as a restore source`},
|
||||
},
|
||||
{
|
||||
name: "restore from backup in PartiallyFailed phase succeeds",
|
||||
location: defaultStorageLocation,
|
||||
restore: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhasePartiallyFailed).Result(),
|
||||
expectedErr: false,
|
||||
expectedPhase: string(velerov1api.RestorePhaseInProgress),
|
||||
expectedStartTime: ×tamp,
|
||||
expectedCompletedTime: ×tamp,
|
||||
expectedRestorerCall: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseInProgress).Result(),
|
||||
},
|
||||
{
|
||||
name: "restore from backup in Failed phase fails validation",
|
||||
location: defaultStorageLocation,
|
||||
restore: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseFailed).Result(),
|
||||
expectedErr: false,
|
||||
expectedPhase: string(velerov1api.RestorePhaseFailedValidation),
|
||||
expectedValidationErrors: []string{`backup "backup-1" is in phase "Failed" and cannot be used as a restore source`},
|
||||
},
|
||||
}
|
||||
|
||||
formatFlag := logging.FormatText
|
||||
|
||||
+1
-15
@@ -20,7 +20,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -237,20 +236,7 @@ func CollectPodLogs(ctx context.Context, podGetter corev1client.CoreV1Interface,
|
||||
func ToSystemAffinity(loadAffinity *LoadAffinity, volumeTopology *corev1api.NodeSelector) *corev1api.Affinity {
|
||||
requirements := []corev1api.NodeSelectorRequirement{}
|
||||
if loadAffinity != nil {
|
||||
// MatchLabels is a map, so its iteration order is not deterministic.
|
||||
// Sort the keys so the generated requirements (and therefore the
|
||||
// resulting affinity) have a stable order. This output may be embedded
|
||||
// into objects that are reconciled continuously (e.g. DaemonSet pod
|
||||
// templates), where an order-only difference would be treated as a spec
|
||||
// change and trigger unnecessary rollouts/restarts.
|
||||
keys := make([]string, 0, len(loadAffinity.NodeSelector.MatchLabels))
|
||||
for k := range loadAffinity.NodeSelector.MatchLabels {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
v := loadAffinity.NodeSelector.MatchLabels[k]
|
||||
for k, v := range loadAffinity.NodeSelector.MatchLabels {
|
||||
requirements = append(requirements, corev1api.NodeSelectorRequirement{
|
||||
Key: k,
|
||||
Values: []string{v},
|
||||
|
||||
@@ -834,45 +834,6 @@ func TestToSystemAffinity(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with multiple match labels are sorted by key",
|
||||
loadAffinity: &LoadAffinity{
|
||||
NodeSelector: metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"key-c": "value-c",
|
||||
"key-a": "value-a",
|
||||
"key-b": "value-b",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &corev1api.Affinity{
|
||||
NodeAffinity: &corev1api.NodeAffinity{
|
||||
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
||||
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
||||
{
|
||||
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
||||
{
|
||||
Key: "key-a",
|
||||
Values: []string{"value-a"},
|
||||
Operator: corev1api.NodeSelectorOpIn,
|
||||
},
|
||||
{
|
||||
Key: "key-b",
|
||||
Values: []string{"value-b"},
|
||||
Operator: corev1api.NodeSelectorOpIn,
|
||||
},
|
||||
{
|
||||
Key: "key-c",
|
||||
Values: []string{"value-c"},
|
||||
Operator: corev1api.NodeSelectorOpIn,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with olume topology",
|
||||
volumeTopology: &corev1api.NodeSelector{
|
||||
|
||||
@@ -18,7 +18,7 @@ Conclusively, you have two ways to add/change/delete configurations of a backup
|
||||
- If the BackupRepository CR for the backup repository is already there, you should modify the `repositoryConfig` field. The new changes will be applied to the backup repository at the due time, it doesn't require Velero server to restart.
|
||||
- Otherwise, you can create the backup repository configMap as a template for the BackupRepository CRs that are going to be created.
|
||||
|
||||
The backup repository configMap is repository type (i.e., kopia, restic) specific, so for one repository type, you only need to create one set of configurations, they will be applied to all BackupRepository CRs of the same type. Whereas, the changes of `repositoryConfig` field apply to the specific BackupRepository CR only, you may need to change every BackupRepository CR of the same type.
|
||||
The backup repository configMap is repository type specific (for example, `kopia`), so for one repository type, you only need to create one set of configurations, they will be applied to all BackupRepository CRs of the same type. Whereas, the changes of `repositoryConfig` field apply to the specific BackupRepository CR only, you may need to change every BackupRepository CR of the same type.
|
||||
|
||||
Below is an example of the BackupRepository configMap with the configurations:
|
||||
```yaml
|
||||
|
||||
@@ -5,7 +5,7 @@ layout: docs
|
||||
|
||||
Velero supports backing up and restoring Kubernetes volumes attached to pods from the file system of the volumes, called
|
||||
File System Backup (FSB shortly) or Pod Volume Backup. The data movement is fulfilled by using modules from free open-source
|
||||
backup tools [restic][1] and [kopia][2]. This support is considered beta quality. Please see the list of [limitations](#limitations)
|
||||
backup tool [kopia][2]. This support is considered beta quality. Please see the list of [limitations](#limitations)
|
||||
to understand if it fits your use case.
|
||||
|
||||
Velero allows you to take snapshots of persistent volumes as part of your backups if you’re using one of
|
||||
@@ -38,7 +38,6 @@ It's important to understand that File System Backup (FSB) and volume snapshots
|
||||
This behavior is automatic and ensures optimal backup performance and storage usage.
|
||||
|
||||
**NOTE:** hostPath volumes are not supported, but the [local volume type][5] is supported.
|
||||
**NOTE:** restic is under the deprecation process by following [Velero Deprecation Policy][17], for more details, see the Restic Deprecation section.
|
||||
|
||||
## Setup File System Backup
|
||||
|
||||
@@ -710,39 +709,6 @@ For Kopia repository, by default, the cache is stored in the data mover pod's ro
|
||||
- configure a limit of the cache size per backup repository, for more details, check [Backup Repository Configuration][18].
|
||||
- configure a dedicated volume for cache data, for more details, check [Data Movement Cache Volume][22].
|
||||
|
||||
## Restic Deprecation
|
||||
|
||||
According to the [Velero Deprecation Policy][17], restic path is being deprecated starting from v1.15, specifically:
|
||||
- For 1.15 and 1.16, if restic path is used by a backup, the backup still creates and succeeds but you will see warnings
|
||||
- For 1.17 and 1.18, backups with restic path are disabled, but you are still allowed to restore from your previous restic backups
|
||||
- From 1.19, both backups and restores with restic path will be disabled, you are not able to use 1.19 or higher to restore your restic backup data
|
||||
|
||||
From 1.17, backup from restic path is not allowed, though you can still restore from the existing backups created by restic path.
|
||||
Velero could automatically identify the legacy backups and switch to restic path without user intervention.
|
||||
|
||||
### How Velero integrates with Restic
|
||||
Velero integrate Restic binary directly, so the operations are done by calling Restic commands:
|
||||
- Run `restic init` command to initialize the [restic repository](https://restic.readthedocs.io/en/latest/100_references.html#terminology)
|
||||
- Run `restic prune` command periodically to prune restic repository
|
||||
- Run `restic restore` commands to restore pod volume data
|
||||
|
||||
For a restore from restic path, restic commands are called by the node-agent itself; whereas, for kopia path backup/restore, the data path runs in the data mover pods.
|
||||
Restore from restic path is handled by the legacy `PodVolumeRestore` controller, so Resume and Cancellation are not supported:
|
||||
- When Velero server is restarted, the legacy `PodVolumeRestore` is left as orphan and contineue running, though the restore has already marked as `Failed`
|
||||
- When node-agent is restarted, the `PodVolumeRestore` is marked as `Failed` directly
|
||||
|
||||
### Restic Repository
|
||||
To support restic repository, the BackupRepository CR should be specially configured:
|
||||
- You need to set the `resticRepoPrefix` value in BackupStorageLocation. For example, on AWS, `resticRepoPrefix` is something like
|
||||
`s3:s3-us-west-2.amazonaws.com/bucket` (note that `resticRepoPrefix` doesn't work for Kopia).
|
||||
|
||||
Velero still effectively manage restic repository, though you cannot write any new backup to it:
|
||||
- When you delete a backup, the restic repository snapshots (if any) could be deleted from restic repository
|
||||
- Velero backup repository controller periodically runs mainteance jobs for BackupRepository CRs representing restic repositories
|
||||
|
||||
|
||||
|
||||
[1]: https://github.com/restic/restic
|
||||
[2]: https://github.com/kopia/kopia
|
||||
[3]: customize-installation.md#enable-file-system-backup
|
||||
[4]: https://github.com/velero-io/velero/releases/
|
||||
@@ -750,7 +716,6 @@ Velero still effectively manage restic repository, though you cannot write any n
|
||||
[6]: https://kubernetes.io/docs/concepts/storage/volumes/#mount-propagation
|
||||
[7]: https://github.com/bitsbeats/velero-pvc-watcher
|
||||
[8]: https://docs.microsoft.com/en-us/azure/aks/azure-files-dynamic-pv
|
||||
[9]: https://github.com/restic/restic/issues/1800
|
||||
[10]: customize-installation.md#default-pod-volume-backup-to-file-system-backup
|
||||
[11]: https://www.vcluster.com/
|
||||
[12]: csi.md
|
||||
@@ -758,7 +723,6 @@ Velero still effectively manage restic repository, though you cannot write any n
|
||||
[14]: https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/
|
||||
[15]: customize-installation.md#customize-resource-requests-and-limits
|
||||
[16]: performance-guidance.md
|
||||
[17]: https://github.com/velero-io/velero/blob/main/GOVERNANCE.md#deprecation-policy
|
||||
[18]: backup-repository-configuration.md
|
||||
[19]: node-agent-concurrency.md
|
||||
[20]: node-agent-prepare-queue-length.md
|
||||
|
||||
@@ -3,9 +3,9 @@ title: "Velero File System Backup Performance Guide"
|
||||
layout: docs
|
||||
---
|
||||
|
||||
When using Velero to do file system backup & restore, Restic uploader or Kopia uploader are both supported now. But the resources used and time consumption are a big difference between them.
|
||||
When using Velero to do file system backup & restore, Kopia uploader performance can vary based on data shape and resource settings.
|
||||
|
||||
We've done series rounds of tests against Restic uploader and Kopia uploader through Velero, which may give you some guidance. But the test results will vary from different infrastructures, and our tests are limited and couldn't cover a variety of data scenarios, **the test results and analysis are for reference only**.
|
||||
We've done several rounds of tests against Kopia uploader through Velero, which may give you some guidance. But the test results will vary from different infrastructures, and our tests are limited and couldn't cover a variety of data scenarios, **the test results and analysis are for reference only**.
|
||||
|
||||
## Infrastructure
|
||||
|
||||
@@ -79,25 +79,21 @@ Server:
|
||||
|
||||
## Test
|
||||
|
||||
Below we've done 6 groups of tests, for each single group of test, we used limited resources (1 core CPU 2 GB memory or 4 cores CPU 4 GB memory) to do Velero file system backup under Restic path and Kopia path, and then compare the results.
|
||||
Below we've done 6 groups of tests. For each single group of test, we used limited resources (1 core CPU 2 GB memory or 4 cores CPU 4 GB memory) to do Velero file system backup under Kopia path.
|
||||
|
||||
Recorded the metrics of time consumption, maximum CPU usage, maximum memory usage, and minio storage usage for node-agent daemonset, and the metrics of Velero deployment are not included since the differences are not obvious by whether using Restic uploader or Kopia uploader.
|
||||
Recorded the metrics of time consumption, maximum CPU usage, maximum memory usage, and minio storage usage for node-agent daemonset. The metrics of Velero deployment are not included.
|
||||
|
||||
Compression is either disabled or not unavailable for both uploader.
|
||||
Compression is disabled for testing purposes.
|
||||
|
||||
### Case 1: 4194304(4M) files, 2396745(2M) directories, 0B per file total 0B content
|
||||
#### result:
|
||||
|Uploader| Resources|Times |Max CPU|Max Memory|Repo Usage|
|
||||
|--------|----------|:----:|------:|:--------:|:--------:|
|
||||
| Kopia | 1c2g |24m54s| 65% |1530 MB |80 MB |
|
||||
| Restic | 1c2g |52m31s| 55% |1708 MB |3.3 GB |
|
||||
| Kopia | 4c4g |24m52s| 63% |2216 MB |80 MB |
|
||||
| Restic | 4c4g |52m28s| 54% |2329 MB |3.3 GB |
|
||||
#### conclusion:
|
||||
- The memory usage is larger than Velero's default memory limit (1GB) for both Kopia and Restic under massive empty files.
|
||||
- For both using Kopia uploader and Restic uploader, there is no significant time reduction by increasing resources from 1c2g to 4c4g.
|
||||
- Restic uploader is one more time slower than Kopia uploader under the same specification resources.
|
||||
- Restic has an **irrational** repository size (3.3GB)
|
||||
- The memory usage is larger than Velero's default memory limit (1GB) for Kopia under massive empty files.
|
||||
- There is no significant time reduction by increasing resources from 1c2g to 4c4g.
|
||||
|
||||
### Case 2: Using the same size (100B) of file and default Velero's resource configuration, the testing quantity of files from 20 thousand to 2 million, these groups of cases mainly test the behavior with the increasing quantity of files.
|
||||
|
||||
@@ -106,58 +102,47 @@ Compression is either disabled or not unavailable for both uploader.
|
||||
| Uploader | Resources|Times |Max CPU|Max Memory|Repo Usage|
|
||||
|-------|----------|:----:|------:|:--------:|:--------:|
|
||||
| Kopia | 1c1g |2m34s | 70% |692 MB |108 MB |
|
||||
| Restic| 1c1g |3m9s | 54% |714 MB |275 MB |
|
||||
|
||||
### Case 2.2 470596(40k) files, 137257 (10k)directories, 100B per file total 44.880MB content
|
||||
#### result:
|
||||
| Uploader | Resources|Times |Max CPU|Max Memory|Repo Usage|
|
||||
|-------|----------|:----:|------:|:--------:|:--------:|
|
||||
| Kopia | 1c1g |3m45s | 68% |831 MB |108 MB |
|
||||
| Restic| 1c1g |4m53s | 57% |788 MB |275 MB |
|
||||
|
||||
### Case 2.3 705894(70k) files, 137257(10k) directories, 100B per file total 67.319MB content
|
||||
#### result:
|
||||
|Uploader| Resources|Times |Max CPU|Max Memory|Repo Usage|
|
||||
|--------|----------|:----:|------:|:--------:|:--------:|
|
||||
| Kopia | 1c1g |5m06s | 71% |861 MB |108 MB |
|
||||
| Restic | 1c1g |6m23s | 56% |810 MB |275 MB |
|
||||
|
||||
### Case 2.4 2097152(2M) files, 2396745(2M) directories, 100B per file total 200.000MB content
|
||||
#### result:
|
||||
|Uploader| Resources|Times |Max CPU|Max Memory|Repo Usage|
|
||||
|--------|----------|:----:|------:|:--------:|:--------:|
|
||||
| Kopia | 1c1g |OOM | 74% |N/A |N/A |
|
||||
| Restic | 1c1g |41m47s| 52% |904 MB |3.2 GB |
|
||||
#### conclusion:
|
||||
- With the increasing number of files, there is no memory abnormal surge, the memory usage for both Kopia uploader and Restic uploader is linear increasing, until exceeds 1GB memory usage in Case 2.4 Kopia uploader OOM happened.
|
||||
- With the increasing number of files, there is no memory abnormal surge, and memory usage is linearly increasing until it exceeds 1GB where Case 2.4 Kopia uploader OOM happened.
|
||||
- Kopia uploader gets increasingly faster along with the increasing number of files.
|
||||
- Restic uploader repository size is still much larger than Kopia uploader repository.
|
||||
|
||||
### Case 3: 10625(10k) files, 781 directories, 1.000MB per file total 10.376GB content
|
||||
#### result:
|
||||
|Uploader| Resources|Times |Max CPU|Max Memory|Repo Usage|
|
||||
|--------|----------|:----:|------:|:--------:|:--------:|
|
||||
| Kopia | 1c2g |1m37s | 75% |251 MB |10 GB |
|
||||
| Restic | 1c2g |5m25s | 100% |153 MB |10 GB |
|
||||
| Kopia | 4c4g |1m35s | 75% |248 MB |10 GB |
|
||||
| Restic | 4c4g |3m17s | 171% |126 MB |10 GB |
|
||||
#### conclusion:
|
||||
- This case involves a relatively large backup size, there is no significant time reduction by increasing resources from 1c2g to 4c4g for Kopia uploader, but for Restic uploader when increasing CPU from 1 core to 4, backup time-consuming was shortened by one-third, which means in this scenario should allocate more CPU resources for Restic uploader.
|
||||
- For the large backup size case, Restic uploader's repository size comes to normal
|
||||
- This case involves a relatively large backup size, and there is no significant time reduction by increasing resources from 1c2g to 4c4g for Kopia uploader.
|
||||
|
||||
### Case 4: 900 files, 1 directory, 1.000GB per file total 900.000GB content
|
||||
#### result:
|
||||
|Uploader| Resources|Times |Max CPU|Max Memory|Repo Usage|
|
||||
|--------|----------|:-----:|------:|:--------:|:--------:|
|
||||
| Kopia | 1c2g |2h30m | 100% |714 MB |900 GB |
|
||||
| Restic | 1c2g |Timeout| 100% |416 MB |N/A |
|
||||
| Kopia | 4c4g |1h42m | 138% |786 MB |900 GB |
|
||||
| Restic | 4c4g |2h15m | 351% |606 MB |900 GB |
|
||||
#### conclusion:
|
||||
- When the target backup data is relatively large, Restic uploader starts to Timeout under 1c2g. So it's better to allocate more memory for Restic uploader when backup large sizes of data.
|
||||
- For backup large amounts of data, Kopia uploader is both less time-consuming and less resource usage.
|
||||
- For backup large amounts of data, allocating more resources can reduce backup time for Kopia uploader.
|
||||
|
||||
## Summary
|
||||
- With the same specification resources, Kopia uploader is less time-consuming when backup.
|
||||
- Performance would be better if choosing Kopia uploader for the scenario in backup large mounts of data or massive small files.
|
||||
- It's better to set one reasonable resource configuration instead of the default depending on your scenario. For default resource configuration, it's easy to be timeout with Restic uploader in backup large amounts of data, and it's easy to be OOM for both Kopia uploader and Restic uploader in backup of massive small files.
|
||||
- Kopia uploader performs well when backing up large amounts of data or massive small files.
|
||||
- It's better to set one reasonable resource configuration instead of the default depending on your scenario. With default configuration, it's easy to hit timeout or OOM in large-scale backups.
|
||||
|
||||
@@ -23,7 +23,7 @@ If there is a key value as `global` in the map, the key's value is applied to al
|
||||
The other keys in the map is the combination of three elements of a BackupRepository, because those three keys can identify a unique BackupRepository:
|
||||
* The namespace in which BackupRepository backs up volume data.
|
||||
* The BackupRepository referenced BackupStorageLocation's name.
|
||||
* The BackupRepository's type. Possible values are `kopia` and `restic`.
|
||||
* The BackupRepository's type. Possible value is `kopia`.
|
||||
|
||||
If there is a key match with BackupRepository, the key's value is applied to the BackupRepository's maintenance jobs.
|
||||
By this way, it's possible to let user configure before the BackupRepository is created.
|
||||
@@ -45,7 +45,6 @@ For example, the following BackupRepository's key should be `test-default-kopia`
|
||||
backupStorageLocation: default
|
||||
maintenanceFrequency: 1h0m0s
|
||||
repositoryType: kopia
|
||||
resticIdentifier: gs:jxun:/restic/test
|
||||
volumeNamespace: test
|
||||
```
|
||||
|
||||
@@ -135,7 +134,7 @@ The frequency of running maintenance jobs could be set by the below command when
|
||||
```bash
|
||||
velero install --default-repo-maintain-frequency <DURATION>
|
||||
```
|
||||
For Kopia the default maintenance frequency is 1 hour, and Restic is 7 * 24 hours.
|
||||
For Kopia the default maintenance frequency is 1 hour.
|
||||
|
||||
### Full Maintenance Interval customization
|
||||
See [backup repository configuration][3]
|
||||
|
||||
@@ -27,7 +27,7 @@ The following is an overview of Velero's restore process that starts after you r
|
||||
|
||||
1. The Velero client makes a call to the Kubernetes API server to create a [`Restore`](api-types/restore.md) object.
|
||||
|
||||
1. The `RestoreController` notices the new Restore object and performs validation. This includes verifying that the referenced backup is in a usable phase. Only backups in `Completed` or `PartiallyFailed` phase are accepted as restore sources.
|
||||
1. The `RestoreController` notices the new Restore object and performs validation.
|
||||
|
||||
1. The `RestoreController` fetches basic information about the backup being restored, like the [BackupStorageLocation](locations.md) (BSL). It also fetches a tarball of the cluster resources in the backup, any volumes that will be restored using File System Backup, and any volume snapshots to be restored.
|
||||
|
||||
@@ -78,41 +78,26 @@ By default, Velero will restore resources in the following order:
|
||||
* VolumeSnapshotClass
|
||||
* VolumeSnapshotContents
|
||||
* VolumeSnapshots
|
||||
* DataUploads
|
||||
* PersistentVolumes
|
||||
* PersistentVolumeClaims
|
||||
* ClusterRoles
|
||||
* Roles
|
||||
* ServiceAccounts
|
||||
* ClusterRoleBindings
|
||||
* RoleBindings
|
||||
* Secrets
|
||||
* ConfigMaps
|
||||
* ServiceAccounts
|
||||
* LimitRanges
|
||||
* PriorityClasses
|
||||
* Pods
|
||||
* ReplicaSets
|
||||
* ClusterClasses
|
||||
* Endpoints
|
||||
* Services
|
||||
* ClusterBootstraps
|
||||
* Clusters
|
||||
* ClusterResourceSets
|
||||
* Apps (apps.kappctrl.k14s.io)
|
||||
* PackageInstalls
|
||||
|
||||
It's recommended that you use the default order for your restores. You are able to customize this order if you need to by setting the `--restore-resource-priorities` flag on the Velero server and specifying a different resource order. This customized order will apply to all future restores. You don't have to specify all resources in the `--restore-resource-priorities` flag. The priority list contains two parts which are split by the `-` element: resources before the `-` element are restored first as high priorities, resources after the `-` element are restored last as low priorities, and any resource not in the list will be restored alphabetically between the high and low priorities.
|
||||
It's recommended that you use the default order for your restores. You are able to customize this order if you need to by setting the `--restore-resource-priorities` flag on the Velero server and specifying a different resource order. This customized order will apply to all future restores. You don't have to specify all resources in the `--restore-resource-priorities` flag. Velero will append resources not listed to the end of your customized list in alphabetical order.
|
||||
|
||||
```shell
|
||||
velero server \
|
||||
--restore-resource-priorities=customresourcedefinitions,namespaces,storageclasses,\
|
||||
volumesnapshotclass.snapshot.storage.k8s.io,volumesnapshotcontents.snapshot.storage.k8s.io,\
|
||||
volumesnapshots.snapshot.storage.k8s.io,datauploads.velero.io,persistentvolumes,\
|
||||
persistentvolumeclaims,clusterroles,roles,serviceaccounts,clusterrolebindings,rolebindings,\
|
||||
secrets,configmaps,limitranges,priorityclasses,pods,replicasets.apps,\
|
||||
clusterclasses.cluster.x-k8s.io,endpoints,services,-,clusterbootstraps.run.tanzu.vmware.com,\
|
||||
clusters.cluster.x-k8s.io,clusterresourcesets.addons.cluster.x-k8s.io,apps.kappctrl.k14s.io,\
|
||||
packageinstalls.packaging.carvel.dev
|
||||
volumesnapshots.snapshot.storage.k8s.io,persistentvolumes,persistentvolumeclaims,secrets,\
|
||||
configmaps,serviceaccounts,limitranges,pods,replicasets.apps,clusters.cluster.x-k8s.io,\
|
||||
clusterresourcesets.addons.cluster.x-k8s.io
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -154,4 +154,4 @@ Velero provides a way for you to skip TLS verification on the object store when
|
||||
|
||||
If true, the object store's TLS certificate will not be checked for validity before Velero or backup repository connects to the object storage. You can permanently skip TLS verification for an object store by setting `Spec.Config.InsecureSkipTLSVerify` to true in the [BackupStorageLocation](api-types/backupstoragelocation.md) CRD.
|
||||
|
||||
Note that Velero's File System Backup uses Restic or Kopia to do data transfer between object store and Kubernetes cluster disks. This means that when you specify `--insecure-skip-tls-verify` in Velero operations that involve File System Backup, Velero will convey this information to Restic or Kopia. For example, for Restic, Velero will add the Restic global command parameter `--insecure-tls` to Restic commands.
|
||||
Note that Velero's File System Backup uses Kopia to do data transfer between object store and Kubernetes cluster disks. This means that when you specify `--insecure-skip-tls-verify` in Velero operations that involve File System Backup, Velero will convey this information to Kopia.
|
||||
|
||||
@@ -78,41 +78,26 @@ By default, Velero will restore resources in the following order:
|
||||
* VolumeSnapshotClass
|
||||
* VolumeSnapshotContents
|
||||
* VolumeSnapshots
|
||||
* DataUploads
|
||||
* PersistentVolumes
|
||||
* PersistentVolumeClaims
|
||||
* ClusterRoles
|
||||
* Roles
|
||||
* ServiceAccounts
|
||||
* ClusterRoleBindings
|
||||
* RoleBindings
|
||||
* Secrets
|
||||
* ConfigMaps
|
||||
* ServiceAccounts
|
||||
* LimitRanges
|
||||
* PriorityClasses
|
||||
* Pods
|
||||
* ReplicaSets
|
||||
* ClusterClasses
|
||||
* Endpoints
|
||||
* Services
|
||||
* ClusterBootstraps
|
||||
* Clusters
|
||||
* ClusterResourceSets
|
||||
* Apps (apps.kappctrl.k14s.io)
|
||||
* PackageInstalls
|
||||
|
||||
It's recommended that you use the default order for your restores. You are able to customize this order if you need to by setting the `--restore-resource-priorities` flag on the Velero server and specifying a different resource order. This customized order will apply to all future restores. You don't have to specify all resources in the `--restore-resource-priorities` flag. The priority list contains two parts which are split by the `-` element: resources before the `-` element are restored first as high priorities, resources after the `-` element are restored last as low priorities, and any resource not in the list will be restored alphabetically between the high and low priorities.
|
||||
It's recommended that you use the default order for your restores. You are able to customize this order if you need to by setting the `--restore-resource-priorities` flag on the Velero server and specifying a different resource order. This customized order will apply to all future restores. You don't have to specify all resources in the `--restore-resource-priorities` flag. Velero will append resources not listed to the end of your customized list in alphabetical order.
|
||||
|
||||
```shell
|
||||
velero server \
|
||||
--restore-resource-priorities=customresourcedefinitions,namespaces,storageclasses,\
|
||||
volumesnapshotclass.snapshot.storage.k8s.io,volumesnapshotcontents.snapshot.storage.k8s.io,\
|
||||
volumesnapshots.snapshot.storage.k8s.io,datauploads.velero.io,persistentvolumes,\
|
||||
persistentvolumeclaims,clusterroles,roles,serviceaccounts,clusterrolebindings,rolebindings,\
|
||||
secrets,configmaps,limitranges,priorityclasses,pods,replicasets.apps,\
|
||||
clusterclasses.cluster.x-k8s.io,endpoints,services,-,clusterbootstraps.run.tanzu.vmware.com,\
|
||||
clusters.cluster.x-k8s.io,clusterresourcesets.addons.cluster.x-k8s.io,apps.kappctrl.k14s.io,\
|
||||
packageinstalls.packaging.carvel.dev
|
||||
volumesnapshots.snapshot.storage.k8s.io,persistentvolumes,persistentvolumeclaims,secrets,\
|
||||
configmaps,serviceaccounts,limitranges,pods,replicasets.apps,clusters.cluster.x-k8s.io,\
|
||||
clusterresourcesets.addons.cluster.x-k8s.io
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -78,41 +78,26 @@ By default, Velero will restore resources in the following order:
|
||||
* VolumeSnapshotClass
|
||||
* VolumeSnapshotContents
|
||||
* VolumeSnapshots
|
||||
* DataUploads
|
||||
* PersistentVolumes
|
||||
* PersistentVolumeClaims
|
||||
* ClusterRoles
|
||||
* Roles
|
||||
* ServiceAccounts
|
||||
* ClusterRoleBindings
|
||||
* RoleBindings
|
||||
* Secrets
|
||||
* ConfigMaps
|
||||
* ServiceAccounts
|
||||
* LimitRanges
|
||||
* PriorityClasses
|
||||
* Pods
|
||||
* ReplicaSets
|
||||
* ClusterClasses
|
||||
* Endpoints
|
||||
* Services
|
||||
* ClusterBootstraps
|
||||
* Clusters
|
||||
* ClusterResourceSets
|
||||
* Apps (apps.kappctrl.k14s.io)
|
||||
* PackageInstalls
|
||||
|
||||
It's recommended that you use the default order for your restores. You are able to customize this order if you need to by setting the `--restore-resource-priorities` flag on the Velero server and specifying a different resource order. This customized order will apply to all future restores. You don't have to specify all resources in the `--restore-resource-priorities` flag. The priority list contains two parts which are split by the `-` element: resources before the `-` element are restored first as high priorities, resources after the `-` element are restored last as low priorities, and any resource not in the list will be restored alphabetically between the high and low priorities.
|
||||
It's recommended that you use the default order for your restores. You are able to customize this order if you need to by setting the `--restore-resource-priorities` flag on the Velero server and specifying a different resource order. This customized order will apply to all future restores. You don't have to specify all resources in the `--restore-resource-priorities` flag. Velero will append resources not listed to the end of your customized list in alphabetical order.
|
||||
|
||||
```shell
|
||||
velero server \
|
||||
--restore-resource-priorities=customresourcedefinitions,namespaces,storageclasses,\
|
||||
volumesnapshotclass.snapshot.storage.k8s.io,volumesnapshotcontents.snapshot.storage.k8s.io,\
|
||||
volumesnapshots.snapshot.storage.k8s.io,datauploads.velero.io,persistentvolumes,\
|
||||
persistentvolumeclaims,clusterroles,roles,serviceaccounts,clusterrolebindings,rolebindings,\
|
||||
secrets,configmaps,limitranges,priorityclasses,pods,replicasets.apps,\
|
||||
clusterclasses.cluster.x-k8s.io,endpoints,services,-,clusterbootstraps.run.tanzu.vmware.com,\
|
||||
clusters.cluster.x-k8s.io,clusterresourcesets.addons.cluster.x-k8s.io,apps.kappctrl.k14s.io,\
|
||||
packageinstalls.packaging.carvel.dev
|
||||
volumesnapshots.snapshot.storage.k8s.io,persistentvolumes,persistentvolumeclaims,secrets,\
|
||||
configmaps,serviceaccounts,limitranges,pods,replicasets.apps,clusters.cluster.x-k8s.io,\
|
||||
clusterresourcesets.addons.cluster.x-k8s.io
|
||||
```
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user