mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-06-10 00:03:10 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 323900adcc | |||
| 317ffd069f | |||
| d6d9e4ee16 | |||
| 2ee99e75cd | |||
| dda779de65 | |||
| 52860f986e | |||
| 283ee24632 | |||
| 50ea4eea74 | |||
| 3b545b506b | |||
| d46bf8a337 | |||
| b34c8436aa | |||
| 0d719f1d8a | |||
| ca0506daa8 | |||
| eb0659f06d | |||
| 5160fb1410 | |||
| 85a98b73a5 | |||
| 4374948830 | |||
| 09bfc69d63 | |||
| 869ec523af | |||
| d435b0509e | |||
| 47822b7ed2 | |||
| 2e1ab5ab62 | |||
| 5cc0398662 | |||
| 3d085de99c | |||
| b7d5d84983 | |||
| 1318d2c5dd | |||
| 74ffe25cbe | |||
| e4ecf26b33 | |||
| 2863f0df48 | |||
| cdf3b9ffaa | |||
| 89be6c01df | |||
| 0a7e5d600b | |||
| 44eaea8faf | |||
| 8575ff031d | |||
| 91e2d93576 | |||
| b278d38f7e | |||
| b29185a62d |
@@ -95,9 +95,9 @@ jobs:
|
||||
\"k8s\":$(wget -q -O - "https://hub.docker.com/v2/namespaces/kindest/repositories/node/tags?page_size=50" | grep -o '"name": *"[^"]*' | grep -o '[^"]*$' | grep -v -E "alpha|beta" | grep -E "v[1-9]\.(2[5-9]|[3-9][0-9])" | awk -F. '{if(!a[$1"."$2]++)print $1"."$2"."$NF}' | sort -r | sed s/v//g | jq -R -c -s 'split("\n")[:-1]'),\
|
||||
\"labels\":[\
|
||||
\"Basic && (ClusterResource || NodePort || StorageClass)\", \
|
||||
\"ResourceFiltering && !Restic\", \
|
||||
\"ResourceFiltering && !FSBackup\", \
|
||||
\"ResourceModifier || (Backups && BackupsSync) || PrivilegesMgmt || OrderedResources\", \
|
||||
\"(NamespaceMapping && Single && Restic) || (NamespaceMapping && Multiple && Restic)\"\
|
||||
\"(NamespaceMapping && Single && FSBackup) || (NamespaceMapping && Multiple && FSBackup)\"\
|
||||
]}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Run E2E test against all Kubernetes versions on kind
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
- uses: engineerd/setup-kind@v0.6.2
|
||||
with:
|
||||
skipClusterLogsExport: true
|
||||
version: "v0.27.0"
|
||||
version: "v0.32.0"
|
||||
image: "kindest/node:v${{ matrix.k8s }}"
|
||||
- name: Fetch built CLI
|
||||
id: cli-cache
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Make ci
|
||||
run: make ci
|
||||
- name: Upload test coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
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@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: coverage.out
|
||||
|
||||
@@ -20,4 +20,4 @@ jobs:
|
||||
days-before-pr-close: -1
|
||||
# Only issues made after Feb 09 2021.
|
||||
start-date: "2021-09-02T00:00:00"
|
||||
exempt-issue-labels: "Epic,Area/CLI,Area/Cloud/AWS,Area/Cloud/Azure,Area/Cloud/GCP,Area/Cloud/vSphere,Area/CSI,Area/Design,Area/Documentation,Area/Plugins,Bug,Enhancement/User,kind/requirement,kind/refactor,kind/tech-debt,limitation,Needs investigation,Needs triage,Needs Product,P0 - Hair on fire,P1 - Important,P2 - Long-term important,P3 - Wouldn't it be nice if...,Product Requirements,Restic - GA,Restic,release-blocker,Security,backlog"
|
||||
exempt-issue-labels: "Epic,Area/CLI,Area/Cloud/AWS,Area/Cloud/Azure,Area/Cloud/GCP,Area/Cloud/vSphere,Area/CSI,Area/Design,Area/Documentation,Area/Plugins,Bug,Enhancement/User,kind/requirement,kind/refactor,kind/tech-debt,limitation,Needs investigation,Needs triage,Needs Product,P0 - Hair on fire,P1 - Important,P2 - Long-term important,P3 - Wouldn't it be nice if...,Product Requirements,release-blocker,Security,backlog"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Restores from backups not in a completed or partially failed phase are now rejected.
|
||||
@@ -0,0 +1 @@
|
||||
Fix issue #9812, validate ClusterScopedFilterPolicy and NamespacedFilterPolicy incompatible with legacy filters
|
||||
@@ -0,0 +1 @@
|
||||
Fix issue #9813, add validations for ClusterScopedFilterPolicy
|
||||
@@ -0,0 +1 @@
|
||||
Fix issue #9814, add validations for NamespacedFilterPolicies
|
||||
@@ -0,0 +1 @@
|
||||
Add the Write implementation for incremental aware object writer
|
||||
@@ -0,0 +1 @@
|
||||
Remove restic command package
|
||||
@@ -0,0 +1 @@
|
||||
Enhance backup exposer for block data mover
|
||||
@@ -0,0 +1 @@
|
||||
Add cbt service parameters to node-agent-config for block data mover
|
||||
@@ -0,0 +1 @@
|
||||
Remove Restic cases and workflow from E2E
|
||||
@@ -0,0 +1 @@
|
||||
Add totalSize to repo snapshot operation; and pin unified repo snapshots to Kopia repository and so pin them with Velero backup lifecycle
|
||||
@@ -0,0 +1 @@
|
||||
Fix issue #9816, add cli support for backup with ClusterScopedFilterPolicy and NamespacedFilterPolicies
|
||||
@@ -0,0 +1 @@
|
||||
Make ToSystemAffinity deterministic by sorting MatchLabels keys to avoid spurious affinity spec diffs and restarts
|
||||
@@ -96,7 +96,9 @@ RUN ARCH=$(go env GOARCH) && \
|
||||
chmod +x /usr/bin/goreleaser
|
||||
|
||||
# get golangci-lint
|
||||
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.5.0
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
@@ -23,12 +23,14 @@ import (
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
crclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/wildcard"
|
||||
)
|
||||
|
||||
type VolumeActionType string
|
||||
@@ -259,6 +261,14 @@ 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)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -335,3 +345,117 @@ func getResourcePoliciesFromConfig(cm *corev1api.ConfigMap) (*Policies, error) {
|
||||
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
func (p *Policies) validateNamespacedFilterPolicies() error {
|
||||
seenPatterns := make(map[string][]int) // pattern -> list of policy indices
|
||||
|
||||
// Rule 1-7: Basic validation rules
|
||||
for i, nfp := range p.namespacedFilterPolicies {
|
||||
if len(nfp.Namespaces) == 0 {
|
||||
return fmt.Errorf("namespacedFilterPolicies[%d]: at least one namespace must be specified", i)
|
||||
}
|
||||
if len(nfp.ResourceFilters) == 0 {
|
||||
return fmt.Errorf("namespacedFilterPolicies[%d]: at least one resourceFilter must be specified", i)
|
||||
}
|
||||
|
||||
// Rule 8 & 9: Validate glob patterns and collect namespace patterns for duplicate check
|
||||
for j, pattern := range nfp.Namespaces {
|
||||
if err := wildcard.ValidateNamespaceName(pattern); err != nil {
|
||||
return fmt.Errorf("namespacedFilterPolicies[%d].namespaces[%d]: %w", i, j, err)
|
||||
}
|
||||
seenPatterns[pattern] = append(seenPatterns[pattern], i)
|
||||
}
|
||||
|
||||
seenKinds := make(map[string]int)
|
||||
hasCatchAll := false
|
||||
for j, rf := range nfp.ResourceFilters {
|
||||
if rf.IsCatchAll() {
|
||||
if hasCatchAll {
|
||||
return fmt.Errorf("namespacedFilterPolicies[%d]: only one catch-all resource filter is allowed", i)
|
||||
}
|
||||
hasCatchAll = true
|
||||
if len(rf.Names) > 0 || len(rf.ExcludedNames) > 0 {
|
||||
return fmt.Errorf("namespacedFilterPolicies[%d].resourceFilters[%d]: names or excludedNames cannot be specified for catch-all filters", i, j)
|
||||
}
|
||||
}
|
||||
|
||||
for _, kind := range rf.Kinds {
|
||||
if kind == "*" {
|
||||
continue // "*" is handled by IsCatchAll, no need to check duplicates against other kinds
|
||||
}
|
||||
if prevJ, ok := seenKinds[kind]; ok {
|
||||
return fmt.Errorf("namespacedFilterPolicies[%d]: kind %q appears in both resourceFilters[%d] and resourceFilters[%d]", i, kind, prevJ, j)
|
||||
}
|
||||
seenKinds[kind] = j
|
||||
}
|
||||
|
||||
if len(rf.LabelSelector) > 0 && len(rf.OrLabelSelectors) > 0 {
|
||||
return fmt.Errorf("namespacedFilterPolicies[%d].resourceFilters[%d]: labelSelector and orLabelSelectors cannot co-exist", i, j)
|
||||
}
|
||||
|
||||
// Validate glob patterns for names and excludedNames using gobwas/glob
|
||||
for k, pattern := range rf.Names {
|
||||
if _, err := glob.Compile(pattern); err != nil {
|
||||
return fmt.Errorf("namespacedFilterPolicies[%d].resourceFilters[%d].names[%d]: invalid glob pattern %q: %v", i, j, k, pattern, err)
|
||||
}
|
||||
}
|
||||
for k, pattern := range rf.ExcludedNames {
|
||||
if _, err := glob.Compile(pattern); err != nil {
|
||||
return fmt.Errorf("namespacedFilterPolicies[%d].resourceFilters[%d].excludedNames[%d]: invalid glob pattern %q: %v", i, j, k, pattern, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rule 8: Report exact duplicates only
|
||||
for pattern, policyIndices := range seenPatterns {
|
||||
if len(policyIndices) > 1 {
|
||||
return fmt.Errorf(
|
||||
"namespacedFilterPolicies: duplicate namespace pattern '%s' found in policies %v",
|
||||
pattern, policyIndices)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1242,3 +1242,441 @@ func TestPVCPhaseMatch(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespacedFilterPolicies(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
yamlData string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid namespacedFilterPolicies with multiple kinds",
|
||||
yamlData: `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["frontend", "backend"]
|
||||
resourceFilters:
|
||||
- kinds: ["Pod", "ConfigMap"]
|
||||
labelSelector:
|
||||
app: web
|
||||
names: ["app-*"]
|
||||
- kinds: ["Secret"]
|
||||
excludedNames: ["temp-*"]`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid namespacedFilterPolicies with glob patterns",
|
||||
yamlData: `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["team-*"]
|
||||
resourceFilters:
|
||||
- kinds: ["Pod"]
|
||||
orLabelSelectors:
|
||||
- env: prod
|
||||
- env: staging`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid - overlapping patterns allowed (first-match semantics)",
|
||||
yamlData: `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["team-frontend-*"]
|
||||
resourceFilters:
|
||||
- kinds: ["Pod", "ConfigMap", "Secret"]
|
||||
- namespaces: ["team-*"]
|
||||
resourceFilters:
|
||||
- kinds: ["Deployment", "Service"]`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid - no namespaces",
|
||||
yamlData: `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: []
|
||||
resourceFilters:
|
||||
- kinds: ["Pod"]`,
|
||||
wantErr: true,
|
||||
errMsg: "at least one namespace must be specified",
|
||||
},
|
||||
{
|
||||
name: "invalid - no resourceFilters",
|
||||
yamlData: `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["test"]
|
||||
resourceFilters: []`,
|
||||
wantErr: true,
|
||||
errMsg: "at least one resourceFilter must be specified",
|
||||
},
|
||||
{
|
||||
name: "valid - asterisk catch-all",
|
||||
yamlData: `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["test"]
|
||||
resourceFilters:
|
||||
- kinds: ["*"]
|
||||
labelSelector:
|
||||
app: web`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid - multiple asterisk kinds",
|
||||
yamlData: `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["test"]
|
||||
resourceFilters:
|
||||
- kinds: ["*"]
|
||||
labelSelector:
|
||||
app: web
|
||||
- kinds: ["*"]
|
||||
labelSelector:
|
||||
app: db`,
|
||||
wantErr: true,
|
||||
errMsg: "only one catch-all resource filter is allowed",
|
||||
},
|
||||
{
|
||||
name: "invalid - empty and asterisk kinds",
|
||||
yamlData: `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["test"]
|
||||
resourceFilters:
|
||||
- kinds: []
|
||||
labelSelector:
|
||||
app: web
|
||||
- kinds: ["*"]
|
||||
labelSelector:
|
||||
app: db`,
|
||||
wantErr: true,
|
||||
errMsg: "only one catch-all resource filter is allowed",
|
||||
},
|
||||
{
|
||||
name: "invalid - multiple empty kinds",
|
||||
yamlData: `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["test"]
|
||||
resourceFilters:
|
||||
- kinds: []
|
||||
labelSelector:
|
||||
app: web
|
||||
- kinds: []
|
||||
labelSelector:
|
||||
app: db`,
|
||||
wantErr: true,
|
||||
errMsg: "only one catch-all resource filter is allowed",
|
||||
},
|
||||
{
|
||||
name: "invalid - names with empty kinds",
|
||||
yamlData: `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["test"]
|
||||
resourceFilters:
|
||||
- kinds: []
|
||||
names: ["app-*"]
|
||||
labelSelector:
|
||||
app: web`,
|
||||
wantErr: true,
|
||||
errMsg: "names or excludedNames cannot be specified for catch-all filters",
|
||||
},
|
||||
{
|
||||
name: "invalid - excludedNames with empty kinds",
|
||||
yamlData: `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["test"]
|
||||
resourceFilters:
|
||||
- kinds: []
|
||||
excludedNames: ["app-*"]
|
||||
labelSelector:
|
||||
app: web`,
|
||||
wantErr: true,
|
||||
errMsg: "names or excludedNames cannot be specified for catch-all filters",
|
||||
},
|
||||
{
|
||||
name: "valid - no label selectors with catch-all",
|
||||
yamlData: `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["test"]
|
||||
resourceFilters:
|
||||
- kinds: ["*"]`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid - duplicate kinds",
|
||||
yamlData: `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["test"]
|
||||
resourceFilters:
|
||||
- kinds: ["Pod"]
|
||||
- kinds: ["Pod", "ConfigMap"]`,
|
||||
wantErr: true,
|
||||
errMsg: "kind \"Pod\" appears in both resourceFilters",
|
||||
},
|
||||
{
|
||||
name: "invalid - both labelSelector and orLabelSelectors",
|
||||
yamlData: `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["test"]
|
||||
resourceFilters:
|
||||
- kinds: ["Pod"]
|
||||
labelSelector:
|
||||
app: web
|
||||
orLabelSelectors:
|
||||
- env: prod`,
|
||||
wantErr: true,
|
||||
errMsg: "labelSelector and orLabelSelectors cannot co-exist",
|
||||
},
|
||||
{
|
||||
name: "invalid - bad glob pattern in names",
|
||||
yamlData: `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["test"]
|
||||
resourceFilters:
|
||||
- kinds: ["Pod"]
|
||||
names: ["[invalid"]`,
|
||||
wantErr: true,
|
||||
errMsg: "invalid glob pattern",
|
||||
},
|
||||
{
|
||||
name: "invalid - duplicate namespace pattern",
|
||||
yamlData: `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["production"]
|
||||
resourceFilters:
|
||||
- kinds: ["Pod"]
|
||||
- namespaces: ["production"]
|
||||
resourceFilters:
|
||||
- kinds: ["ConfigMap"]`,
|
||||
wantErr: true,
|
||||
errMsg: "duplicate namespace pattern",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
resPolicies, err := unmarshalResourcePolicies(&tc.yamlData)
|
||||
require.NoError(t, err) // Unmarshal should always succeed for our test cases
|
||||
|
||||
policies := &Policies{}
|
||||
err = policies.BuildPolicy(resPolicies)
|
||||
require.NoError(t, err) // BuildPolicy should always succeed for our test cases
|
||||
|
||||
err = policies.Validate()
|
||||
if tc.wantErr {
|
||||
require.Error(t, err)
|
||||
if tc.errMsg != "" {
|
||||
assert.Contains(t, err.Error(), tc.errMsg)
|
||||
}
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify that we can retrieve the policies
|
||||
nfPolicies := policies.GetNamespacedFilterPolicies()
|
||||
assert.GreaterOrEqual(t, len(nfPolicies), 1) // Valid test cases have at least 1 policy
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespacedFilterPoliciesAccessor(t *testing.T) {
|
||||
yamlData := `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["frontend"]
|
||||
resourceFilters:
|
||||
- kinds: ["Pod"]
|
||||
labelSelector:
|
||||
app: web`
|
||||
|
||||
resPolicies, err := unmarshalResourcePolicies(&yamlData)
|
||||
require.NoError(t, err)
|
||||
|
||||
policies := &Policies{}
|
||||
err = policies.BuildPolicy(resPolicies)
|
||||
require.NoError(t, err)
|
||||
|
||||
nfPolicies := policies.GetNamespacedFilterPolicies()
|
||||
require.Len(t, nfPolicies, 1)
|
||||
|
||||
policy := nfPolicies[0]
|
||||
assert.Equal(t, []string{"frontend"}, policy.Namespaces)
|
||||
assert.Len(t, policy.ResourceFilters, 1)
|
||||
|
||||
rf := policy.ResourceFilters[0]
|
||||
assert.Equal(t, []string{"Pod"}, rf.Kinds)
|
||||
assert.Equal(t, map[string]string{"app": "web"}, rf.LabelSelector)
|
||||
}
|
||||
|
||||
func TestFirstMatchSemantics(t *testing.T) {
|
||||
yamlData := `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["team-frontend-*", "specific-ns"]
|
||||
resourceFilters:
|
||||
- kinds: ["Pod", "ConfigMap", "Secret"]
|
||||
- namespaces: ["team-*", "another-pattern"]
|
||||
resourceFilters:
|
||||
- kinds: ["Deployment", "Service"]`
|
||||
|
||||
resPolicies, err := unmarshalResourcePolicies(&yamlData)
|
||||
require.NoError(t, err)
|
||||
|
||||
policies := &Policies{}
|
||||
err = policies.BuildPolicy(resPolicies)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = policies.Validate()
|
||||
require.NoError(t, err)
|
||||
|
||||
nfPolicies := policies.GetNamespacedFilterPolicies()
|
||||
require.Len(t, nfPolicies, 2)
|
||||
|
||||
// Verify the first policy has the more specific patterns
|
||||
policy1 := nfPolicies[0]
|
||||
assert.Equal(t, []string{"team-frontend-*", "specific-ns"}, policy1.Namespaces)
|
||||
assert.Equal(t, []string{"Pod", "ConfigMap", "Secret"}, policy1.ResourceFilters[0].Kinds)
|
||||
|
||||
// Verify the second policy has the broader patterns
|
||||
policy2 := nfPolicies[1]
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,6 +385,12 @@ func (s *nodeAgentServer) run() {
|
||||
s.logger.Info("Backup repo config is not provided, using default values for cache volume configs")
|
||||
}
|
||||
|
||||
var csiSnapshotMetadataServiceConfigs *velerotypes.CSISnapshotMetadataService
|
||||
if s.dataPathConfigs != nil && s.dataPathConfigs.CSISnapshotMetadataServiceConfigs != nil {
|
||||
csiSnapshotMetadataServiceConfigs = s.dataPathConfigs.CSISnapshotMetadataServiceConfigs
|
||||
s.logger.Infof("Using CSI snapshot metadata service config %v", s.dataPathConfigs.CSISnapshotMetadataServiceConfigs)
|
||||
}
|
||||
|
||||
pvbReconciler := controller.NewPodVolumeBackupReconciler(
|
||||
s.mgr.GetClient(),
|
||||
s.mgr,
|
||||
@@ -447,6 +453,7 @@ func (s *nodeAgentServer) run() {
|
||||
dataMovePriorityClass,
|
||||
podLabels,
|
||||
podAnnotations,
|
||||
csiSnapshotMetadataServiceConfigs,
|
||||
)
|
||||
if err := dataUploadReconciler.SetupWithManager(s.mgr); err != nil {
|
||||
s.logger.WithError(err).Fatal("Unable to create the data upload controller")
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -30,6 +31,7 @@ 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"
|
||||
@@ -40,6 +42,7 @@ 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"
|
||||
@@ -91,6 +94,9 @@ 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 {
|
||||
@@ -130,6 +136,119 @@ 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,6 +18,7 @@ package output
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
@@ -25,6 +26,8 @@ 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"
|
||||
@@ -866,3 +869,85 @@ 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,13 +21,16 @@ 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"
|
||||
@@ -54,6 +57,7 @@ func DescribeBackupInSF(
|
||||
|
||||
if backup.Spec.ResourcePolicy != nil {
|
||||
DescribeResourcePoliciesInSF(d, backup.Spec.ResourcePolicy)
|
||||
DescribeFineGrainedFilterPoliciesInSF(ctx, kbClient, d, backup)
|
||||
}
|
||||
|
||||
status := backup.Status
|
||||
@@ -222,6 +226,88 @@ 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,6 +17,7 @@ limitations under the License.
|
||||
package output
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -24,6 +25,8 @@ 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"
|
||||
@@ -707,3 +710,96 @@ 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))
|
||||
}
|
||||
|
||||
@@ -595,6 +595,13 @@ func (b *backupReconciler) prepareBackupRequest(ctx context.Context, backup *vel
|
||||
request.Status.ValidationErrors = append(request.Status.ValidationErrors, "include-resources, exclude-resources and include-cluster-resources are old filter parameters.\n"+
|
||||
"They cannot be used with include-exclude policies.")
|
||||
}
|
||||
// namespacedFilterPolicies and clusterScopedFilterPolicy incompatible with old-style filters
|
||||
if resourcePolicies != nil &&
|
||||
(len(resourcePolicies.GetNamespacedFilterPolicies()) > 0 || resourcePolicies.GetClusterScopedFilterPolicy() != nil) &&
|
||||
collections.UseOldResourceFilters(request.Spec) {
|
||||
request.Status.ValidationErrors = append(request.Status.ValidationErrors, "include-resources, exclude-resources and include-cluster-resources are old filter parameters.\n"+
|
||||
"They cannot be used with namespace-scoped or fine-grained global filter policies.")
|
||||
}
|
||||
request.ResPolicies = resourcePolicies
|
||||
return request
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -34,6 +35,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
@@ -2020,3 +2022,237 @@ func TestPatchResourceWorksWithStatus(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPrepareBackupRequest_NamespacedFilterPoliciesIncompatibleWithOldFilters verifies
|
||||
// that a backup referencing a ResourcePolicy ConfigMap with namespacedFilterPolicies
|
||||
// produces a validation error when old-style resource filters are also set on the spec.
|
||||
func TestPrepareBackupRequest_NamespacedFilterPoliciesIncompatibleWithOldFilters(t *testing.T) {
|
||||
formatFlag := logging.FormatText
|
||||
logger := logging.DefaultLogger(logrus.DebugLevel, formatFlag)
|
||||
|
||||
policyYAML := `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["production"]
|
||||
resourceFilters:
|
||||
- kinds: ["Deployment"]
|
||||
names: ["api-server"]
|
||||
`
|
||||
policyConfigMap := &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-filter-policy",
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
},
|
||||
Data: map[string]string{"policy": policyYAML},
|
||||
}
|
||||
|
||||
backup := defaultBackup().IncludedResources("deployments").Result()
|
||||
backup.Spec.ResourcePolicy = &corev1api.TypedLocalObjectReference{
|
||||
Kind: "configmap",
|
||||
Name: "my-filter-policy",
|
||||
}
|
||||
|
||||
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, policyConfigMap)
|
||||
|
||||
apiServer := velerotest.NewAPIServer(t)
|
||||
discoveryHelper, err := discovery.NewHelper(apiServer.DiscoveryClient, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
c := &backupReconciler{
|
||||
logger: logger,
|
||||
discoveryHelper: discoveryHelper,
|
||||
kbClient: fakeClient,
|
||||
clock: &clock.RealClock{},
|
||||
formatFlag: formatFlag,
|
||||
}
|
||||
|
||||
res := c.prepareBackupRequest(ctx, backup, logger)
|
||||
|
||||
require.NotEmpty(t, res.Status.ValidationErrors)
|
||||
|
||||
hasTargetError := slices.ContainsFunc(res.Status.ValidationErrors, func(e string) bool {
|
||||
return strings.Contains(e, "namespace-scoped or fine-grained global filter policies")
|
||||
})
|
||||
|
||||
assert.True(t, hasTargetError, "expected validation error about namespacedFilterPolicies incompatibility with old-style filters, got: %v", res.Status.ValidationErrors)
|
||||
}
|
||||
|
||||
// TestPrepareBackupRequest_ClusterScopedFilterPolicyIncompatibleWithOldFilters verifies
|
||||
// that a backup referencing a ResourcePolicy ConfigMap with clusterScopedFilterPolicy
|
||||
// produces a validation error when old-style resource filters are also set on the spec.
|
||||
func TestPrepareBackupRequest_ClusterScopedFilterPolicyIncompatibleWithOldFilters(t *testing.T) {
|
||||
formatFlag := logging.FormatText
|
||||
logger := logging.DefaultLogger(logrus.DebugLevel, formatFlag)
|
||||
|
||||
policyYAML := `version: v1
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters:
|
||||
- kinds: ["ClusterRole"]
|
||||
names: ["my-app-*"]
|
||||
`
|
||||
policyConfigMap := &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-cluster-filter-policy",
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
},
|
||||
Data: map[string]string{"policy": policyYAML},
|
||||
}
|
||||
|
||||
backup := defaultBackup().IncludedResources("clusterroles").Result()
|
||||
backup.Spec.ResourcePolicy = &corev1api.TypedLocalObjectReference{
|
||||
Kind: "configmap",
|
||||
Name: "my-cluster-filter-policy",
|
||||
}
|
||||
|
||||
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, policyConfigMap)
|
||||
|
||||
apiServer := velerotest.NewAPIServer(t)
|
||||
discoveryHelper, err := discovery.NewHelper(apiServer.DiscoveryClient, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
c := &backupReconciler{
|
||||
logger: logger,
|
||||
discoveryHelper: discoveryHelper,
|
||||
kbClient: fakeClient,
|
||||
clock: &clock.RealClock{},
|
||||
formatFlag: formatFlag,
|
||||
}
|
||||
|
||||
res := c.prepareBackupRequest(ctx, backup, logger)
|
||||
|
||||
require.NotEmpty(t, res.Status.ValidationErrors)
|
||||
|
||||
hasClusterError := slices.ContainsFunc(res.Status.ValidationErrors, func(e string) bool {
|
||||
return strings.Contains(e, "namespace-scoped or fine-grained global filter policies")
|
||||
})
|
||||
|
||||
assert.True(t, hasClusterError, "expected validation error about clusterScopedFilterPolicy incompatibility with old-style filters, got: %v", res.Status.ValidationErrors)
|
||||
}
|
||||
|
||||
const (
|
||||
namespacedFilterPolicyYAML = `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["production"]
|
||||
resourceFilters:
|
||||
- kinds: ["Deployment"]
|
||||
names: ["api-server"]
|
||||
`
|
||||
clusterScopedFilterPolicyYAML = `version: v1
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters:
|
||||
- kinds: ["ClusterRole"]
|
||||
names: ["my-app-*"]
|
||||
`
|
||||
bothFilterPoliciesYAML = `version: v1
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["production"]
|
||||
resourceFilters:
|
||||
- kinds: ["Deployment"]
|
||||
names: ["api-server"]
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters:
|
||||
- kinds: ["ClusterRole"]
|
||||
names: ["my-app-*"]
|
||||
`
|
||||
)
|
||||
|
||||
// TestPrepareBackupRequest_FilterPoliciesWithNewFilters verifies that backups referencing
|
||||
// a ResourcePolicy ConfigMap with namespacedFilterPolicies and/or clusterScopedFilterPolicy
|
||||
// succeed when old-style resource filters are not set on the spec.
|
||||
func TestPrepareBackupRequest_FilterPoliciesWithNewFilters(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
policyYAML string
|
||||
policyConfigMapName string
|
||||
backup *velerov1api.Backup
|
||||
expectNamespacedPolicies int
|
||||
expectClusterScopedPolicy bool
|
||||
}{
|
||||
{
|
||||
name: "namespacedFilterPolicies only",
|
||||
policyYAML: namespacedFilterPolicyYAML,
|
||||
policyConfigMapName: "my-filter-policy",
|
||||
backup: defaultBackup().StorageLocation("loc-1").Result(),
|
||||
expectNamespacedPolicies: 1,
|
||||
},
|
||||
{
|
||||
name: "clusterScopedFilterPolicy only",
|
||||
policyYAML: clusterScopedFilterPolicyYAML,
|
||||
policyConfigMapName: "my-cluster-filter-policy",
|
||||
backup: defaultBackup().StorageLocation("loc-1").Result(),
|
||||
expectClusterScopedPolicy: true,
|
||||
},
|
||||
{
|
||||
name: "both filter policies",
|
||||
policyYAML: bothFilterPoliciesYAML,
|
||||
policyConfigMapName: "my-combined-filter-policy",
|
||||
backup: defaultBackup().StorageLocation("loc-1").Result(),
|
||||
expectNamespacedPolicies: 1,
|
||||
expectClusterScopedPolicy: true,
|
||||
},
|
||||
{
|
||||
name: "with new-style spec filters",
|
||||
policyYAML: bothFilterPoliciesYAML,
|
||||
policyConfigMapName: "my-combined-filter-policy",
|
||||
backup: defaultBackup().
|
||||
StorageLocation("loc-1").
|
||||
IncludedNamespaceScopedResources("deployments").
|
||||
IncludedClusterScopedResources("clusterroles").
|
||||
Result(),
|
||||
expectNamespacedPolicies: 1,
|
||||
expectClusterScopedPolicy: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
formatFlag := logging.FormatText
|
||||
logger := logging.DefaultLogger(logrus.DebugLevel, formatFlag)
|
||||
|
||||
policyConfigMap := &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: test.policyConfigMapName,
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
},
|
||||
Data: map[string]string{"policy": test.policyYAML},
|
||||
}
|
||||
|
||||
test.backup.Spec.ResourcePolicy = &corev1api.TypedLocalObjectReference{
|
||||
Kind: "configmap",
|
||||
Name: test.policyConfigMapName,
|
||||
}
|
||||
|
||||
backupLocation := builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "loc-1").
|
||||
Phase(velerov1api.BackupStorageLocationPhaseAvailable).Result()
|
||||
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, backupLocation, policyConfigMap)
|
||||
|
||||
apiServer := velerotest.NewAPIServer(t)
|
||||
discoveryHelper, err := discovery.NewHelper(apiServer.DiscoveryClient, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
c := &backupReconciler{
|
||||
logger: logger,
|
||||
discoveryHelper: discoveryHelper,
|
||||
kbClient: fakeClient,
|
||||
clock: &clock.RealClock{},
|
||||
formatFlag: formatFlag,
|
||||
}
|
||||
|
||||
res := c.prepareBackupRequest(ctx, test.backup, logger)
|
||||
defer res.WorkerPool.Stop()
|
||||
|
||||
assert.Empty(t, res.Status.ValidationErrors)
|
||||
hasIncompatibilityError := slices.ContainsFunc(res.Status.ValidationErrors, func(e string) bool {
|
||||
return strings.Contains(e, "namespace-scoped or fine-grained global filter policies")
|
||||
})
|
||||
assert.False(t, hasIncompatibilityError)
|
||||
|
||||
require.NotNil(t, res.ResPolicies)
|
||||
assert.Len(t, res.ResPolicies.GetNamespacedFilterPolicies(), test.expectNamespacedPolicies)
|
||||
if test.expectClusterScopedPolicy {
|
||||
assert.NotNil(t, res.ResPolicies.GetClusterScopedFilterPolicy())
|
||||
} else {
|
||||
assert.Nil(t, res.ResPolicies.GetClusterScopedFilterPolicy())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ func (r *DataDownloadReconciler) Reconcile(ctx context.Context, req ctrl.Request
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if !datamover.IsBuiltInUploader(dd.Spec.DataMover) {
|
||||
if !datamover.IsBuiltInDataMover(dd.Spec.DataMover) {
|
||||
log.WithField("data mover", dd.Spec.DataMover).Info("it is not one built-in data mover which is not supported by Velero")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
@@ -66,25 +66,26 @@ const (
|
||||
|
||||
// DataUploadReconciler reconciles a DataUpload object
|
||||
type DataUploadReconciler struct {
|
||||
client client.Client
|
||||
kubeClient kubernetes.Interface
|
||||
csiSnapshotClient snapshotter.SnapshotV1Interface
|
||||
mgr manager.Manager
|
||||
Clock clocks.WithTickerAndDelayedExecution
|
||||
nodeName string
|
||||
logger logrus.FieldLogger
|
||||
snapshotExposerList map[velerov2alpha1api.SnapshotType]exposer.SnapshotExposer
|
||||
dataPathMgr *datapath.Manager
|
||||
vgdpCounter *exposer.VgdpCounter
|
||||
loadAffinity []*kube.LoadAffinity
|
||||
backupPVCConfig map[string]velerotypes.BackupPVC
|
||||
podResources corev1api.ResourceRequirements
|
||||
preparingTimeout time.Duration
|
||||
metrics *metrics.ServerMetrics
|
||||
cancelledDataUpload map[string]time.Time
|
||||
dataMovePriorityClass string
|
||||
podLabels map[string]string
|
||||
podAnnotations map[string]string
|
||||
client client.Client
|
||||
kubeClient kubernetes.Interface
|
||||
csiSnapshotClient snapshotter.SnapshotV1Interface
|
||||
mgr manager.Manager
|
||||
Clock clocks.WithTickerAndDelayedExecution
|
||||
nodeName string
|
||||
logger logrus.FieldLogger
|
||||
snapshotExposerList map[velerov2alpha1api.SnapshotType]exposer.SnapshotExposer
|
||||
dataPathMgr *datapath.Manager
|
||||
vgdpCounter *exposer.VgdpCounter
|
||||
loadAffinity []*kube.LoadAffinity
|
||||
backupPVCConfig map[string]velerotypes.BackupPVC
|
||||
podResources corev1api.ResourceRequirements
|
||||
preparingTimeout time.Duration
|
||||
metrics *metrics.ServerMetrics
|
||||
cancelledDataUpload map[string]time.Time
|
||||
dataMovePriorityClass string
|
||||
podLabels map[string]string
|
||||
podAnnotations map[string]string
|
||||
snapshotMetadataServiceConfigs *velerotypes.CSISnapshotMetadataService
|
||||
}
|
||||
|
||||
func NewDataUploadReconciler(
|
||||
@@ -105,6 +106,7 @@ func NewDataUploadReconciler(
|
||||
dataMovePriorityClass string,
|
||||
podLabels map[string]string,
|
||||
podAnnotations map[string]string,
|
||||
snapshotMetadataServiceConfigs *velerotypes.CSISnapshotMetadataService,
|
||||
) *DataUploadReconciler {
|
||||
return &DataUploadReconciler{
|
||||
client: client,
|
||||
@@ -121,17 +123,18 @@ func NewDataUploadReconciler(
|
||||
log,
|
||||
),
|
||||
},
|
||||
dataPathMgr: dataPathMgr,
|
||||
vgdpCounter: counter,
|
||||
loadAffinity: loadAffinity,
|
||||
backupPVCConfig: backupPVCConfig,
|
||||
podResources: podResources,
|
||||
preparingTimeout: preparingTimeout,
|
||||
metrics: metrics,
|
||||
cancelledDataUpload: make(map[string]time.Time),
|
||||
dataMovePriorityClass: dataMovePriorityClass,
|
||||
podLabels: podLabels,
|
||||
podAnnotations: podAnnotations,
|
||||
dataPathMgr: dataPathMgr,
|
||||
vgdpCounter: counter,
|
||||
loadAffinity: loadAffinity,
|
||||
backupPVCConfig: backupPVCConfig,
|
||||
podResources: podResources,
|
||||
preparingTimeout: preparingTimeout,
|
||||
metrics: metrics,
|
||||
cancelledDataUpload: make(map[string]time.Time),
|
||||
dataMovePriorityClass: dataMovePriorityClass,
|
||||
podLabels: podLabels,
|
||||
podAnnotations: podAnnotations,
|
||||
snapshotMetadataServiceConfigs: snapshotMetadataServiceConfigs,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +159,7 @@ func (r *DataUploadReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||
return ctrl.Result{}, errors.Wrap(err, "getting DataUpload")
|
||||
}
|
||||
|
||||
if !datamover.IsBuiltInUploader(du.Spec.DataMover) {
|
||||
if !datamover.IsBuiltInDataMover(du.Spec.DataMover) {
|
||||
log.WithField("Data mover", du.Spec.DataMover).Debug("it is not one built-in data mover which is not supported by Velero")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
@@ -936,7 +939,12 @@ func (r *DataUploadReconciler) setupExposeParam(du *velerov2alpha1api.DataUpload
|
||||
return nil, errors.Wrapf(err, "failed to get source PV %s", pvc.Spec.VolumeName)
|
||||
}
|
||||
|
||||
nodeOS := kube.GetPVCAttachingNodeOS(pvc, r.kubeClient.CoreV1(), r.kubeClient.StorageV1(), log)
|
||||
nodeOS := ""
|
||||
if du.Spec.DataMover == datamover.DataMoverTypeVeleroBlock {
|
||||
nodeOS = kube.NodeOSLinux
|
||||
} else {
|
||||
nodeOS = kube.GetPVCAttachingNodeOS(pvc, r.kubeClient.CoreV1(), r.kubeClient.StorageV1(), log)
|
||||
}
|
||||
|
||||
if err := kube.HasNodeWithOS(context.Background(), nodeOS, r.kubeClient.CoreV1()); err != nil {
|
||||
return nil, errors.Wrapf(err, "no appropriate node to run data upload for PVC %s/%s", du.Spec.SourceNamespace, du.Spec.SourcePVC)
|
||||
@@ -993,23 +1001,25 @@ func (r *DataUploadReconciler) setupExposeParam(du *velerov2alpha1api.DataUpload
|
||||
}
|
||||
|
||||
return &exposer.CSISnapshotExposeParam{
|
||||
SnapshotName: du.Spec.CSISnapshot.VolumeSnapshot,
|
||||
SourceNamespace: du.Spec.SourceNamespace,
|
||||
SourcePVCName: pvc.Name,
|
||||
SourcePVName: pv.Name,
|
||||
StorageClass: du.Spec.CSISnapshot.StorageClass,
|
||||
HostingPodLabels: hostingPodLabels,
|
||||
HostingPodAnnotations: hostingPodAnnotation,
|
||||
HostingPodTolerations: hostingPodTolerations,
|
||||
AccessMode: accessMode,
|
||||
OperationTimeout: du.Spec.OperationTimeout.Duration,
|
||||
ExposeTimeout: r.preparingTimeout,
|
||||
VolumeSize: pvc.Spec.Resources.Requests[corev1api.ResourceStorage],
|
||||
Affinity: r.loadAffinity,
|
||||
BackupPVCConfig: r.backupPVCConfig,
|
||||
Resources: r.podResources,
|
||||
NodeOS: nodeOS,
|
||||
PriorityClassName: r.dataMovePriorityClass,
|
||||
SnapshotName: du.Spec.CSISnapshot.VolumeSnapshot,
|
||||
SourceNamespace: du.Spec.SourceNamespace,
|
||||
SourcePVCName: pvc.Name,
|
||||
SourcePVName: pv.Name,
|
||||
StorageClass: du.Spec.CSISnapshot.StorageClass,
|
||||
HostingPodLabels: hostingPodLabels,
|
||||
HostingPodAnnotations: hostingPodAnnotation,
|
||||
HostingPodTolerations: hostingPodTolerations,
|
||||
AccessMode: accessMode,
|
||||
OperationTimeout: du.Spec.OperationTimeout.Duration,
|
||||
ExposeTimeout: r.preparingTimeout,
|
||||
VolumeSize: pvc.Spec.Resources.Requests[corev1api.ResourceStorage],
|
||||
Affinity: r.loadAffinity,
|
||||
BackupPVCConfig: r.backupPVCConfig,
|
||||
Resources: r.podResources,
|
||||
NodeOS: nodeOS,
|
||||
PriorityClassName: r.dataMovePriorityClass,
|
||||
DataMover: du.Spec.DataMover,
|
||||
SnapshotMetadataServiceConfigs: r.snapshotMetadataServiceConfigs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -251,6 +251,7 @@ func initDataUploaderReconcilerWithError(needError ...error) (*DataUploadReconci
|
||||
"", // dataMovePriorityClass
|
||||
nil, // podLabels
|
||||
nil, // podAnnotations
|
||||
nil,
|
||||
), nil
|
||||
}
|
||||
|
||||
@@ -1513,6 +1514,7 @@ func TestDataUploadSetupExposeParam(t *testing.T) {
|
||||
"upload-priority",
|
||||
tt.args.customLabels,
|
||||
tt.args.customAnnotations,
|
||||
nil,
|
||||
)
|
||||
|
||||
// Act
|
||||
|
||||
@@ -399,6 +399,17 @@ 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").Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).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").Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).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").Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).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").Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).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").Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).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").Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).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").Result(),
|
||||
backup: defaultBackup().StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
emptyVolumeInfo: true,
|
||||
expectedErr: false,
|
||||
expectedPhase: string(velerov1api.RestorePhaseInProgress),
|
||||
@@ -497,6 +497,44 @@ 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
|
||||
|
||||
@@ -88,7 +88,7 @@ func (d *DataUploadDeleteAction) Execute(input *velero.DeleteItemActionExecuteIn
|
||||
|
||||
// generate the configmap which is to be created and used as a way to communicate the snapshot info to the backup deletion controller
|
||||
func genConfigmap(bak *velerov1.Backup, du velerov2alpha1.DataUpload) *corev1api.ConfigMap {
|
||||
if !IsBuiltInUploader(du.Spec.DataMover) || du.Status.SnapshotID == "" {
|
||||
if !IsBuiltInDataMover(du.Spec.DataMover) || du.Status.SnapshotID == "" {
|
||||
return nil
|
||||
}
|
||||
snapshot := repotypes.SnapshotIdentifier{
|
||||
|
||||
@@ -18,6 +18,11 @@ package datamover
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
DataMoverTypeVeleroFs string = "velero-fs"
|
||||
DataMoverTypeVeleroBlock string = "velero-block"
|
||||
)
|
||||
|
||||
func GetUploaderType(dataMover string) string {
|
||||
if dataMover == "" || dataMover == "velero" {
|
||||
return "kopia"
|
||||
@@ -26,7 +31,7 @@ func GetUploaderType(dataMover string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func IsBuiltInUploader(dataMover string) bool {
|
||||
func IsBuiltInDataMover(dataMover string) bool {
|
||||
return dataMover == "" || dataMover == "velero"
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestIsBuiltInUploader(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(tt *testing.T) {
|
||||
assert.Equal(tt, tc.want, IsBuiltInUploader(tc.dataMover))
|
||||
assert.Equal(tt, tc.want, IsBuiltInDataMover(tc.dataMover))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package exposer
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
|
||||
@@ -33,6 +34,7 @@ import (
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/datamover"
|
||||
"github.com/vmware-tanzu/velero/pkg/nodeagent"
|
||||
velerotypes "github.com/vmware-tanzu/velero/pkg/types"
|
||||
"github.com/vmware-tanzu/velero/pkg/util"
|
||||
@@ -93,6 +95,12 @@ type CSISnapshotExposeParam struct {
|
||||
|
||||
// PriorityClassName is the priority class name for the data mover pod
|
||||
PriorityClassName string
|
||||
|
||||
// DataMover is the data mover type, e.g., velero-fs, velero-block
|
||||
DataMover string
|
||||
|
||||
// SnapshotMetadataServiceConfigs is the config for CSI snapshot metadata service
|
||||
SnapshotMetadataServiceConfigs *velerotypes.CSISnapshotMetadataService
|
||||
}
|
||||
|
||||
// CSISnapshotExposeWaitParam define the input param for WaitExposed of CSI snapshots
|
||||
@@ -234,7 +242,7 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1api.O
|
||||
}
|
||||
}
|
||||
|
||||
backupPVC, err := e.createBackupPVC(ctx, ownerObject, backupVS.Name, backupPVCStorageClass, csiExposeParam.AccessMode, volumeSize, backupPVCReadOnly, backupPVCAnnotations)
|
||||
backupPVC, err := e.createBackupPVC(ctx, ownerObject, backupVS.Name, backupPVCStorageClass, csiExposeParam.AccessMode, volumeSize, backupPVCReadOnly, backupPVCAnnotations, csiExposeParam.DataMover)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error to create backup pvc")
|
||||
}
|
||||
@@ -264,6 +272,7 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1api.O
|
||||
csiExposeParam.PriorityClassName,
|
||||
intoleratableNodes,
|
||||
volumeTopology,
|
||||
csiExposeParam.SnapshotMetadataServiceConfigs,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error to create backup pod")
|
||||
@@ -450,7 +459,11 @@ func (e *csiSnapshotExposer) CleanUp(ctx context.Context, ownerObject corev1api.
|
||||
csi.DeleteVolumeSnapshotIfAny(ctx, e.csiSnapshotClient, vsName, sourceNamespace, e.log)
|
||||
}
|
||||
|
||||
func getVolumeModeByAccessMode(accessMode string) (corev1api.PersistentVolumeMode, error) {
|
||||
func getVolumeModeByAccessMode(accessMode string, dataMover string) (corev1api.PersistentVolumeMode, error) {
|
||||
if dataMover == datamover.DataMoverTypeVeleroBlock {
|
||||
return corev1api.PersistentVolumeBlock, nil
|
||||
}
|
||||
|
||||
switch accessMode {
|
||||
case AccessModeFileSystem:
|
||||
return corev1api.PersistentVolumeFilesystem, nil
|
||||
@@ -488,10 +501,14 @@ func (e *csiSnapshotExposer) createBackupVS(ctx context.Context, ownerObject cor
|
||||
func (e *csiSnapshotExposer) createBackupVSC(ctx context.Context, ownerObject corev1api.ObjectReference, snapshotVSC *snapshotv1api.VolumeSnapshotContent, vs *snapshotv1api.VolumeSnapshot) (*snapshotv1api.VolumeSnapshotContent, error) {
|
||||
backupVSCName := ownerObject.Name
|
||||
|
||||
anno := make(map[string]string)
|
||||
maps.Copy(anno, snapshotVSC.Annotations)
|
||||
anno[kube.KubeAnnAllowVolumeModeChange] = "true"
|
||||
|
||||
vsc := &snapshotv1api.VolumeSnapshotContent{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: backupVSCName,
|
||||
Annotations: snapshotVSC.Annotations,
|
||||
Annotations: anno,
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
Spec: snapshotv1api.VolumeSnapshotContentSpec{
|
||||
@@ -524,10 +541,10 @@ func (e *csiSnapshotExposer) createBackupVSC(ctx context.Context, ownerObject co
|
||||
return e.csiSnapshotClient.VolumeSnapshotContents().Create(ctx, vsc, metav1.CreateOptions{})
|
||||
}
|
||||
|
||||
func (e *csiSnapshotExposer) createBackupPVC(ctx context.Context, ownerObject corev1api.ObjectReference, backupVS, storageClass, accessMode string, resource resource.Quantity, readOnly bool, annotations map[string]string) (*corev1api.PersistentVolumeClaim, error) {
|
||||
func (e *csiSnapshotExposer) createBackupPVC(ctx context.Context, ownerObject corev1api.ObjectReference, backupVS, storageClass, accessMode string, resource resource.Quantity, readOnly bool, annotations map[string]string, dataMover string) (*corev1api.PersistentVolumeClaim, error) {
|
||||
backupPVCName := ownerObject.Name
|
||||
|
||||
volumeMode, err := getVolumeModeByAccessMode(accessMode)
|
||||
volumeMode, err := getVolumeModeByAccessMode(accessMode, dataMover)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -600,6 +617,7 @@ func (e *csiSnapshotExposer) createBackupPod(
|
||||
priorityClassName string,
|
||||
intoleratableNodes []string,
|
||||
volumeTopology *corev1api.NodeSelector,
|
||||
csiSnapshotMetadataServiceConfigs *velerotypes.CSISnapshotMetadataService,
|
||||
) (*corev1api.Pod, error) {
|
||||
podName := ownerObject.Name
|
||||
|
||||
@@ -655,6 +673,12 @@ func (e *csiSnapshotExposer) createBackupPod(
|
||||
args = append(args, podInfo.logFormatArgs...)
|
||||
args = append(args, podInfo.logLevelArgs...)
|
||||
|
||||
if csiSnapshotMetadataServiceConfigs != nil {
|
||||
if csiSnapshotMetadataServiceConfigs.SAName != "" {
|
||||
args = append(args, fmt.Sprintf("--csi-snapshot-metadata-service-sa=%s", csiSnapshotMetadataServiceConfigs.SAName))
|
||||
}
|
||||
}
|
||||
|
||||
if affinity == nil {
|
||||
affinity = &kube.LoadAffinity{}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,7 @@ func TestCreateBackupPodWithPriorityClass(t *testing.T) {
|
||||
tc.expectedPriorityClass,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
require.NoError(t, err, tc.description)
|
||||
@@ -241,6 +242,7 @@ func TestCreateBackupPodWithMissingConfigMap(t *testing.T) {
|
||||
"", // empty priority class since config map is missing
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
// Should succeed even when config map is missing
|
||||
|
||||
@@ -18,6 +18,7 @@ package exposer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -1056,21 +1057,25 @@ func TestExpose(t *testing.T) {
|
||||
backupPVC, err := exposer.kubeClient.CoreV1().PersistentVolumeClaims(ownerObject.Namespace).Get(t.Context(), ownerObject.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedVS, err := exposer.csiSnapshotClient.VolumeSnapshots(ownerObject.Namespace).Get(t.Context(), ownerObject.Name, metav1.GetOptions{})
|
||||
backupVS, err := exposer.csiSnapshotClient.VolumeSnapshots(ownerObject.Namespace).Get(t.Context(), ownerObject.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedVSC, err := exposer.csiSnapshotClient.VolumeSnapshotContents().Get(t.Context(), ownerObject.Name, metav1.GetOptions{})
|
||||
backupVSC, err := exposer.csiSnapshotClient.VolumeSnapshotContents().Get(t.Context(), ownerObject.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expectedVS.Annotations, vsObject.Annotations)
|
||||
assert.Equal(t, *expectedVS.Spec.VolumeSnapshotClassName, *vsObject.Spec.VolumeSnapshotClassName)
|
||||
assert.Equal(t, expectedVSC.Name, *expectedVS.Spec.Source.VolumeSnapshotContentName)
|
||||
assert.Equal(t, vsObject.Annotations, backupVS.Annotations)
|
||||
assert.Equal(t, *vsObject.Spec.VolumeSnapshotClassName, *backupVS.Spec.VolumeSnapshotClassName)
|
||||
assert.Equal(t, *backupVS.Spec.Source.VolumeSnapshotContentName, backupVSC.Name)
|
||||
|
||||
assert.Equal(t, expectedVSC.Annotations, vscObj.Annotations)
|
||||
assert.Equal(t, expectedVSC.Labels, vscObj.Labels)
|
||||
assert.Equal(t, expectedVSC.Spec.DeletionPolicy, vscObj.Spec.DeletionPolicy)
|
||||
assert.Equal(t, expectedVSC.Spec.Driver, vscObj.Spec.Driver)
|
||||
assert.Equal(t, *expectedVSC.Spec.VolumeSnapshotClassName, *vscObj.Spec.VolumeSnapshotClassName)
|
||||
anno := make(map[string]string)
|
||||
maps.Copy(anno, vscObj.Annotations)
|
||||
anno[kube.KubeAnnAllowVolumeModeChange] = "true"
|
||||
|
||||
assert.Equal(t, anno, backupVSC.Annotations)
|
||||
assert.Equal(t, vscObj.Labels, backupVSC.Labels)
|
||||
assert.Equal(t, vscObj.Spec.DeletionPolicy, backupVSC.Spec.DeletionPolicy)
|
||||
assert.Equal(t, vscObj.Spec.Driver, backupVSC.Spec.Driver)
|
||||
assert.Equal(t, *vscObj.Spec.VolumeSnapshotClassName, *backupVSC.Spec.VolumeSnapshotClassName)
|
||||
|
||||
if test.expectedVolumeSize != nil {
|
||||
assert.Equal(t, *test.expectedVolumeSize, backupPVC.Spec.Resources.Requests[corev1api.ResourceStorage])
|
||||
@@ -1514,7 +1519,7 @@ func Test_csiSnapshotExposer_createBackupPVC(t *testing.T) {
|
||||
APIVersion: tt.ownerBackup.APIVersion,
|
||||
}
|
||||
}
|
||||
got, err := e.createBackupPVC(t.Context(), ownerObject, tt.backupVS, tt.storageClass, tt.accessMode, tt.resource, tt.readOnly, map[string]string{})
|
||||
got, err := e.createBackupPVC(t.Context(), ownerObject, tt.backupVS, tt.storageClass, tt.accessMode, tt.resource, tt.readOnly, map[string]string{}, "")
|
||||
if !tt.wantErr(t, err, fmt.Sprintf("createBackupPVC(%v, %v, %v, %v, %v, %v)", ownerObject, tt.backupVS, tt.storageClass, tt.accessMode, tt.resource, tt.readOnly)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
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 freelist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type FreeList struct {
|
||||
chunks chan []byte
|
||||
memory []byte
|
||||
chunkSize int
|
||||
}
|
||||
|
||||
func New(size, chunkSize int) *FreeList {
|
||||
memory := make([]byte, size)
|
||||
numChunks := size / chunkSize
|
||||
chunks := make(chan []byte, numChunks)
|
||||
|
||||
for i := range numChunks {
|
||||
start := i * chunkSize
|
||||
end := start + chunkSize
|
||||
|
||||
chunks <- memory[start:end:end]
|
||||
}
|
||||
|
||||
return &FreeList{
|
||||
chunks: chunks,
|
||||
memory: memory,
|
||||
chunkSize: chunkSize,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FreeList) Chunks() <-chan []byte {
|
||||
return f.chunks
|
||||
}
|
||||
|
||||
func (f *FreeList) Get() []byte {
|
||||
return <-f.chunks
|
||||
}
|
||||
|
||||
func (f *FreeList) Return(chunk []byte) {
|
||||
if cap(chunk) != f.chunkSize {
|
||||
panic(fmt.Sprintf("chunk (%v) is not allocated by me", cap(chunk)))
|
||||
}
|
||||
|
||||
chunk = chunk[:cap(chunk)]
|
||||
f.chunks <- chunk
|
||||
}
|
||||
|
||||
func (f *FreeList) Capacity() int {
|
||||
return len(f.chunks)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
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 freelist
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
size := 1024
|
||||
chunkSize := 256
|
||||
numChunks := size / chunkSize
|
||||
|
||||
fl := New(size, chunkSize)
|
||||
|
||||
assert.NotNil(t, fl)
|
||||
assert.Equal(t, chunkSize, fl.chunkSize)
|
||||
assert.Len(t, fl.memory, size)
|
||||
assert.Equal(t, numChunks, cap(fl.chunks))
|
||||
assert.Len(t, fl.chunks, numChunks)
|
||||
assert.Equal(t, numChunks, fl.Capacity())
|
||||
}
|
||||
|
||||
func TestGetAndReturn(t *testing.T) {
|
||||
size := 1024
|
||||
chunkSize := 256
|
||||
|
||||
fl := New(size, chunkSize)
|
||||
|
||||
chunk := fl.Get()
|
||||
assert.Equal(t, chunkSize, cap(chunk))
|
||||
assert.Len(t, chunk, chunkSize)
|
||||
assert.Equal(t, 3, fl.Capacity())
|
||||
|
||||
fl.Return(chunk)
|
||||
assert.Equal(t, 4, fl.Capacity())
|
||||
}
|
||||
|
||||
func TestReturnPanic(t *testing.T) {
|
||||
fl := New(1024, 256)
|
||||
|
||||
invalidChunk := make([]byte, 128)
|
||||
assert.PanicsWithValue(t, "chunk (128) is not allocated by me", func() {
|
||||
fl.Return(invalidChunk)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChunks(t *testing.T) {
|
||||
fl := New(1024, 256)
|
||||
|
||||
chunks := fl.Chunks()
|
||||
assert.Len(t, chunks, 4)
|
||||
assert.Equal(t, cap(fl.chunks), cap(chunks))
|
||||
}
|
||||
|
||||
func TestCapacity(t *testing.T) {
|
||||
fl := New(1024, 256)
|
||||
|
||||
assert.Equal(t, 4, fl.Capacity())
|
||||
|
||||
fl.Get()
|
||||
assert.Equal(t, 3, fl.Capacity())
|
||||
}
|
||||
|
||||
func TestConcurrentAccess(t *testing.T) {
|
||||
size := 1024 * 10
|
||||
chunkSize := 256
|
||||
numChunks := size / chunkSize
|
||||
|
||||
fl := New(size, chunkSize)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < numChunks; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
chunk := fl.Get()
|
||||
assert.Equal(t, chunkSize, cap(chunk))
|
||||
assert.Len(t, chunk, chunkSize)
|
||||
|
||||
for j := 0; j < len(chunk); j++ {
|
||||
chunk[j] = byte(j % 256)
|
||||
}
|
||||
|
||||
fl.Return(chunk)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
assert.Equal(t, numChunks, fl.Capacity())
|
||||
}
|
||||
@@ -19,9 +19,11 @@ package kopialib
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -42,6 +44,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/kopia"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo/kopialib/backend"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo/kopialib/freelist"
|
||||
)
|
||||
|
||||
type kopiaRepoService struct {
|
||||
@@ -80,14 +83,21 @@ type kopiaObjectWriter struct {
|
||||
}
|
||||
|
||||
type kopiaObjectWriterEx struct {
|
||||
ctx context.Context
|
||||
rawRepoWriter repo.RepositoryWriter
|
||||
parentEntries []object.IndirectObjectEntry
|
||||
blockSize int64
|
||||
description string
|
||||
compressor compression.Name
|
||||
splitter string
|
||||
logger logrus.FieldLogger
|
||||
ctx context.Context
|
||||
rawRepoWriter repo.RepositoryWriter
|
||||
parentEntries []object.IndirectObjectEntry
|
||||
entries []object.IndirectObjectEntry
|
||||
entryLock sync.Mutex
|
||||
blockSize int64
|
||||
description string
|
||||
compressor compression.Name
|
||||
splitter string
|
||||
writeLock sync.Mutex
|
||||
asyncWritesSem chan struct{}
|
||||
asyncWritesGroup sync.WaitGroup
|
||||
asyncBuffer *freelist.FreeList
|
||||
writeError atomic.Value
|
||||
logger logrus.FieldLogger
|
||||
}
|
||||
|
||||
type openOptions struct {
|
||||
@@ -101,6 +111,7 @@ const (
|
||||
overwriteQuickMaintainInterval = time.Duration(0)
|
||||
repoBackend = "kopia"
|
||||
fixedSplitter1M = "FIXED-1M"
|
||||
fixedSplitter128K = "FIXED-128K"
|
||||
fixedBlockSize = 1 << 20
|
||||
)
|
||||
|
||||
@@ -454,15 +465,24 @@ func (kr *kopiaRepository) NewObjectWriter(ctx context.Context, opt udmrepo.Obje
|
||||
kr.logger.Infof("Write object %s in block mode without parent", opt.Description)
|
||||
}
|
||||
|
||||
var asyncWritesSem chan struct{}
|
||||
var asyncBuffer *freelist.FreeList
|
||||
if opt.AsyncWrites > 0 {
|
||||
asyncWritesSem = make(chan struct{}, opt.AsyncWrites)
|
||||
asyncBuffer = freelist.New(opt.AsyncWrites*fixedBlockSize, fixedBlockSize)
|
||||
}
|
||||
|
||||
return &kopiaObjectWriterEx{
|
||||
ctx: ctx,
|
||||
rawRepoWriter: kr.rawWriter,
|
||||
parentEntries: parentEntries,
|
||||
description: opt.Description,
|
||||
compressor: getCompressorForObject(opt),
|
||||
blockSize: fixedBlockSize,
|
||||
splitter: fixedSplitter1M,
|
||||
logger: kr.logger,
|
||||
ctx: ctx,
|
||||
rawRepoWriter: kr.rawWriter,
|
||||
parentEntries: parentEntries,
|
||||
description: opt.Description,
|
||||
compressor: getCompressorForObject(opt),
|
||||
blockSize: fixedBlockSize,
|
||||
splitter: fixedSplitter1M,
|
||||
asyncWritesSem: asyncWritesSem,
|
||||
asyncBuffer: asyncBuffer,
|
||||
logger: kr.logger,
|
||||
}, nil
|
||||
} else {
|
||||
if opt.ParentObject != "" {
|
||||
@@ -606,6 +626,9 @@ func (kr *kopiaRepository) SaveSnapshot(ctx context.Context, snap udmrepo.Snapsh
|
||||
Description: snap.Description,
|
||||
StartTime: fs.UTCTimestampFromTime(snap.StartTime),
|
||||
EndTime: fs.UTCTimestampFromTime(snap.EndTime),
|
||||
Stats: snapshot.Stats{
|
||||
TotalFileSize: snap.TotalSize,
|
||||
},
|
||||
RootEntry: &snapshot.DirEntry{
|
||||
Type: snapshot.EntryTypeDirectory,
|
||||
ObjectID: rootObj,
|
||||
@@ -614,8 +637,12 @@ func (kr *kopiaRepository) SaveSnapshot(ctx context.Context, snap udmrepo.Snapsh
|
||||
FileSize: snap.RootObject.Size,
|
||||
UserID: snap.RootObject.UserID,
|
||||
GroupID: snap.RootObject.GroupID,
|
||||
DirSummary: &fs.DirectorySummary{
|
||||
TotalFileSize: snap.TotalSize,
|
||||
},
|
||||
},
|
||||
Tags: snap.Tags,
|
||||
Pins: []string{"velero-pin"}, // pins are meant to prevent snapshot from automatic expiration/deletion.
|
||||
}
|
||||
|
||||
id, err := snapshot.SaveSnapshot(ctx, kr.rawWriter, &manifest)
|
||||
@@ -642,6 +669,7 @@ func (kr *kopiaRepository) GetSnapshot(ctx context.Context, id udmrepo.ID) (udmr
|
||||
StartTime: snap.StartTime.ToTime(),
|
||||
EndTime: snap.EndTime.ToTime(),
|
||||
Tags: snap.Tags,
|
||||
TotalSize: snap.Stats.TotalFileSize,
|
||||
RootObject: udmrepo.ObjectMetadata{
|
||||
ID: udmrepo.ID(snap.RootEntry.ObjectID.String()),
|
||||
Type: udmrepo.ObjectDataTypeMetadata,
|
||||
@@ -850,9 +878,111 @@ func (kow *kopiaObjectWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO add implementation in following PRs
|
||||
func (kow *kopiaObjectWriterEx) Write(p []byte) (int, error) {
|
||||
return 0, errors.New("not implemented")
|
||||
kow.writeLock.Lock()
|
||||
defer kow.writeLock.Unlock()
|
||||
|
||||
if kow.rawRepoWriter == nil {
|
||||
return 0, errors.New("object writer is closed or not open")
|
||||
}
|
||||
|
||||
if err := kow.getWriteError(); err != nil {
|
||||
return 0, errors.Wrapf(err, "error happened during writing object")
|
||||
}
|
||||
|
||||
length := len(p)
|
||||
if int64(length)%kow.blockSize != 0 {
|
||||
return 0, errors.Errorf("invalid length %v", length)
|
||||
}
|
||||
|
||||
kow.entryLock.Lock()
|
||||
curPos := int64(len(kow.entries)) * kow.blockSize
|
||||
kow.entryLock.Unlock()
|
||||
|
||||
offset := curPos
|
||||
entryID := 0
|
||||
for curPos < offset+int64(length) {
|
||||
kow.entryLock.Lock()
|
||||
entryID = len(kow.entries)
|
||||
kow.entries = append(kow.entries, object.IndirectObjectEntry{
|
||||
Start: curPos,
|
||||
Length: kow.blockSize,
|
||||
})
|
||||
kow.entryLock.Unlock()
|
||||
|
||||
buffOffset := curPos - offset
|
||||
objName := fmt.Sprintf("%s-b%v", kow.description, entryID)
|
||||
kow.writeObjectAsync(objName, entryID, p[buffOffset:buffOffset+kow.blockSize])
|
||||
|
||||
curPos += kow.blockSize
|
||||
}
|
||||
|
||||
return length, nil
|
||||
}
|
||||
|
||||
func (kow *kopiaObjectWriterEx) writeObject(objName string, p []byte) (object.ID, error) {
|
||||
writer := kow.rawRepoWriter.NewObjectWriter(kopia.SetupKopiaLog(kow.ctx, kow.logger), object.WriterOptions{
|
||||
Description: objName,
|
||||
Compressor: kow.compressor,
|
||||
Splitter: kow.splitter,
|
||||
})
|
||||
|
||||
if writer == nil {
|
||||
return object.EmptyID, errors.Errorf("error opening writer for %s", objName)
|
||||
}
|
||||
|
||||
defer writer.Close()
|
||||
|
||||
written, err := writer.Write(p)
|
||||
if err != nil {
|
||||
return object.EmptyID, errors.Wrapf(err, "error writing for %s", objName)
|
||||
}
|
||||
|
||||
if written != len(p) {
|
||||
return object.EmptyID, errors.Errorf("short write for %s", objName)
|
||||
}
|
||||
|
||||
objID, err := writer.Result()
|
||||
if err != nil {
|
||||
return object.EmptyID, errors.Wrapf(err, "error flushing data for %s", objName)
|
||||
}
|
||||
|
||||
return objID, nil
|
||||
}
|
||||
|
||||
func (kow *kopiaObjectWriterEx) writeObjectSync(objName string, entry int, p []byte) error {
|
||||
objID, err := kow.writeObject(objName, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kow.entryLock.Lock()
|
||||
kow.entries[entry].Object = objID
|
||||
kow.entryLock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kow *kopiaObjectWriterEx) writeObjectAsync(objName string, entryID int, p []byte) {
|
||||
if kow.asyncWritesSem == nil {
|
||||
if err := kow.writeObjectSync(objName, entryID, p); err != nil {
|
||||
kow.saveWriteError(errors.Wrapf(err, "error writing object for %s", objName))
|
||||
}
|
||||
} else {
|
||||
kow.asyncWritesSem <- struct{}{}
|
||||
|
||||
buffer := kow.asyncBuffer.Get()
|
||||
copy(buffer, p)
|
||||
|
||||
kow.asyncWritesGroup.Go(func() {
|
||||
if err := kow.writeObjectSync(objName, entryID, buffer); err != nil {
|
||||
kow.saveWriteError(errors.Wrapf(err, "error writing object for %s", objName))
|
||||
}
|
||||
|
||||
kow.asyncBuffer.Return(buffer)
|
||||
<-kow.asyncWritesSem
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO add implementation in following PRs
|
||||
@@ -864,14 +994,85 @@ func (kow *kopiaObjectWriterEx) Checkpoint() (udmrepo.ID, error) {
|
||||
return udmrepo.ID(""), errors.New("not supported")
|
||||
}
|
||||
|
||||
// TODO add implementation in following PRs
|
||||
func (kow *kopiaObjectWriterEx) Result() (udmrepo.ID, error) {
|
||||
return udmrepo.ID(""), errors.New("not implemented")
|
||||
type indirectObject struct {
|
||||
StreamID string `json:"stream"`
|
||||
Entries []object.IndirectObjectEntry `json:"entries"`
|
||||
}
|
||||
|
||||
const kopiaIndirectStreamType = "kopia:indirect"
|
||||
|
||||
func (kow *kopiaObjectWriterEx) writeIndirectObject() (object.ID, error) {
|
||||
if kow.rawRepoWriter == nil {
|
||||
return object.EmptyID, errors.New("object writer is closed or not open")
|
||||
}
|
||||
|
||||
writer := kow.rawRepoWriter.NewObjectWriter(kopia.SetupKopiaLog(kow.ctx, kow.logger), object.WriterOptions{
|
||||
Description: "LIST(" + kow.description + ")",
|
||||
Prefix: "x",
|
||||
Compressor: getMetadataCompressor(),
|
||||
Splitter: fixedSplitter128K,
|
||||
})
|
||||
if writer == nil {
|
||||
return object.EmptyID, errors.New("unable to create writer for indirect object")
|
||||
}
|
||||
|
||||
defer writer.Close()
|
||||
|
||||
ind := indirectObject{
|
||||
StreamID: kopiaIndirectStreamType,
|
||||
Entries: kow.entries,
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(writer).Encode(ind); err != nil {
|
||||
return object.EmptyID, errors.Wrap(err, "unable to write indirect object index")
|
||||
}
|
||||
|
||||
return writer.Result()
|
||||
}
|
||||
|
||||
func (kow *kopiaObjectWriterEx) saveWriteError(err error) {
|
||||
if err != nil {
|
||||
kow.writeError.Store(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (kow *kopiaObjectWriterEx) getWriteError() error {
|
||||
if v := kow.writeError.Load(); v != nil {
|
||||
return v.(error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kow *kopiaObjectWriterEx) Result() (udmrepo.ID, error) {
|
||||
kow.writeLock.Lock()
|
||||
defer kow.writeLock.Unlock()
|
||||
|
||||
kow.asyncWritesGroup.Wait()
|
||||
|
||||
if err := kow.getWriteError(); err != nil {
|
||||
return udmrepo.ID(""), errors.Wrap(err, "error happened during writing object")
|
||||
}
|
||||
|
||||
id, err := kow.writeIndirectObject()
|
||||
if err != nil {
|
||||
return udmrepo.ID(""), errors.Wrap(err, "error to write indirect object")
|
||||
}
|
||||
|
||||
objectID := "I" + udmrepo.ID(id.String())
|
||||
|
||||
return objectID, nil
|
||||
}
|
||||
|
||||
// TODO add implementation in following PRs
|
||||
func (kow *kopiaObjectWriterEx) Close() error {
|
||||
return errors.New("not implemented")
|
||||
kow.writeLock.Lock()
|
||||
defer kow.writeLock.Unlock()
|
||||
|
||||
kow.asyncWritesGroup.Wait()
|
||||
|
||||
kow.rawRepoWriter = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCompressorForObject returns the compressor for an object, at present, we don't support compression
|
||||
|
||||
@@ -2,6 +2,8 @@ package kopialib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/kopia/kopia/repo"
|
||||
@@ -13,6 +15,7 @@ import (
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
repomocks "github.com/vmware-tanzu/velero/pkg/repository/udmrepo/kopialib/backend/mocks"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo/kopialib/freelist"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
)
|
||||
|
||||
@@ -205,3 +208,343 @@ func TestNewObjectWriterEx(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKopiaObjectWriterEx_Write(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
setupWriter func(t *testing.T) *kopiaObjectWriterEx
|
||||
inputData []byte
|
||||
expectedErr string
|
||||
expectedLen int
|
||||
}{
|
||||
{
|
||||
name: "writer is closed",
|
||||
setupWriter: func(t *testing.T) *kopiaObjectWriterEx {
|
||||
t.Helper()
|
||||
return &kopiaObjectWriterEx{
|
||||
rawRepoWriter: nil,
|
||||
}
|
||||
},
|
||||
inputData: make([]byte, 1024),
|
||||
expectedErr: "object writer is closed or not open",
|
||||
},
|
||||
{
|
||||
name: "write error exists",
|
||||
setupWriter: func(t *testing.T) *kopiaObjectWriterEx {
|
||||
t.Helper()
|
||||
kow := &kopiaObjectWriterEx{
|
||||
rawRepoWriter: repomocks.NewMockRepositoryWriter(t),
|
||||
blockSize: 1024,
|
||||
}
|
||||
kow.saveWriteError(errors.New("previous error"))
|
||||
return kow
|
||||
},
|
||||
inputData: make([]byte, 1024),
|
||||
expectedErr: "error happened during writing object: previous error",
|
||||
},
|
||||
{
|
||||
name: "invalid length",
|
||||
setupWriter: func(t *testing.T) *kopiaObjectWriterEx {
|
||||
t.Helper()
|
||||
return &kopiaObjectWriterEx{
|
||||
rawRepoWriter: repomocks.NewMockRepositoryWriter(t),
|
||||
blockSize: 1024,
|
||||
}
|
||||
},
|
||||
inputData: make([]byte, 1023),
|
||||
expectedErr: "invalid length 1023",
|
||||
},
|
||||
{
|
||||
name: "success sync write",
|
||||
setupWriter: func(t *testing.T) *kopiaObjectWriterEx {
|
||||
t.Helper()
|
||||
mockRepoWriter := repomocks.NewMockRepositoryWriter(t)
|
||||
mockWriter := repomocks.NewWriter(t)
|
||||
|
||||
mockWriter.On("Write", mock.Anything).Return(1024, nil)
|
||||
mockWriter.On("Close").Return(nil)
|
||||
|
||||
id, _ := object.ParseID("I12345")
|
||||
mockWriter.On("Result").Return(id, nil)
|
||||
|
||||
mockRepoWriter.On("NewObjectWriter", mock.Anything, mock.Anything).Return(mockWriter)
|
||||
|
||||
return &kopiaObjectWriterEx{
|
||||
ctx: context.Background(),
|
||||
rawRepoWriter: mockRepoWriter,
|
||||
blockSize: 1024,
|
||||
logger: velerotest.NewLogger(),
|
||||
}
|
||||
},
|
||||
inputData: make([]byte, 1024),
|
||||
expectedLen: 1024,
|
||||
},
|
||||
{
|
||||
name: "success async write",
|
||||
setupWriter: func(t *testing.T) *kopiaObjectWriterEx {
|
||||
t.Helper()
|
||||
mockRepoWriter := repomocks.NewMockRepositoryWriter(t)
|
||||
mockWriter := repomocks.NewWriter(t)
|
||||
|
||||
mockWriter.On("Write", mock.Anything).Return(1024, nil)
|
||||
mockWriter.On("Close").Return(nil)
|
||||
|
||||
id, _ := object.ParseID("I12345")
|
||||
mockWriter.On("Result").Return(id, nil)
|
||||
|
||||
mockRepoWriter.On("NewObjectWriter", mock.Anything, mock.Anything).Return(mockWriter)
|
||||
|
||||
sem := make(chan struct{}, 1)
|
||||
buf := freelist.New(1024, 1024)
|
||||
|
||||
return &kopiaObjectWriterEx{
|
||||
ctx: context.Background(),
|
||||
rawRepoWriter: mockRepoWriter,
|
||||
blockSize: 1024,
|
||||
asyncWritesSem: sem,
|
||||
asyncBuffer: buf,
|
||||
logger: velerotest.NewLogger(),
|
||||
}
|
||||
},
|
||||
inputData: make([]byte, 1024),
|
||||
expectedLen: 1024,
|
||||
},
|
||||
{
|
||||
name: "success multiple blocks in one write",
|
||||
setupWriter: func(t *testing.T) *kopiaObjectWriterEx {
|
||||
t.Helper()
|
||||
mockRepoWriter := repomocks.NewMockRepositoryWriter(t)
|
||||
mockWriter := repomocks.NewWriter(t)
|
||||
|
||||
mockWriter.On("Write", mock.Anything).Return(1024, nil)
|
||||
mockWriter.On("Close").Return(nil)
|
||||
|
||||
id, _ := object.ParseID("I12345")
|
||||
mockWriter.On("Result").Return(id, nil)
|
||||
|
||||
mockRepoWriter.On("NewObjectWriter", mock.Anything, mock.Anything).Return(mockWriter)
|
||||
|
||||
return &kopiaObjectWriterEx{
|
||||
ctx: context.Background(),
|
||||
rawRepoWriter: mockRepoWriter,
|
||||
blockSize: 1024,
|
||||
logger: velerotest.NewLogger(),
|
||||
}
|
||||
},
|
||||
inputData: make([]byte, 2048),
|
||||
expectedLen: 2048,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
kow := tc.setupWriter(t)
|
||||
l, err := kow.Write(tc.inputData)
|
||||
|
||||
if kow.asyncWritesSem != nil {
|
||||
kow.asyncWritesGroup.Wait()
|
||||
}
|
||||
|
||||
if tc.expectedErr != "" {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedLen, l)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKopiaObjectWriterEx_Result(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
setupWriter func(t *testing.T) *kopiaObjectWriterEx
|
||||
expectedErr string
|
||||
expectedID udmrepo.ID
|
||||
}{
|
||||
{
|
||||
name: "write error exists",
|
||||
setupWriter: func(t *testing.T) *kopiaObjectWriterEx {
|
||||
t.Helper()
|
||||
kow := &kopiaObjectWriterEx{}
|
||||
kow.saveWriteError(errors.New("async write failed"))
|
||||
return kow
|
||||
},
|
||||
expectedErr: "error happened during writing object: async write failed",
|
||||
},
|
||||
{
|
||||
name: "writer closed",
|
||||
setupWriter: func(t *testing.T) *kopiaObjectWriterEx {
|
||||
t.Helper()
|
||||
kow := &kopiaObjectWriterEx{
|
||||
rawRepoWriter: nil,
|
||||
}
|
||||
return kow
|
||||
},
|
||||
expectedErr: "error to write indirect object: object writer is closed or not open",
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
setupWriter: func(t *testing.T) *kopiaObjectWriterEx {
|
||||
t.Helper()
|
||||
mockRepoWriter := repomocks.NewMockRepositoryWriter(t)
|
||||
mockWriter := repomocks.NewWriter(t)
|
||||
|
||||
mockWriter.On("Write", mock.Anything).Return(100, nil)
|
||||
mockWriter.On("Close").Return(nil)
|
||||
|
||||
id, _ := object.ParseID("Iabcdef")
|
||||
mockWriter.On("Result").Return(id, nil)
|
||||
|
||||
mockRepoWriter.On("NewObjectWriter", mock.Anything, mock.Anything).Return(mockWriter)
|
||||
|
||||
return &kopiaObjectWriterEx{
|
||||
ctx: context.Background(),
|
||||
rawRepoWriter: mockRepoWriter,
|
||||
logger: velerotest.NewLogger(),
|
||||
}
|
||||
},
|
||||
expectedID: udmrepo.ID("IIabcdef"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
kow := tc.setupWriter(t)
|
||||
id, err := kow.Result()
|
||||
|
||||
if tc.expectedErr != "" {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedID, id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKopiaObjectWriterEx_Close(t *testing.T) {
|
||||
kow := &kopiaObjectWriterEx{}
|
||||
err := kow.Close()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestKopiaObjectWriterEx_ConcurrentWrite(t *testing.T) {
|
||||
mockRepoWriter := repomocks.NewMockRepositoryWriter(t)
|
||||
mockWriter := repomocks.NewWriter(t)
|
||||
|
||||
mockWriter.On("Write", mock.Anything).Return(1024, nil)
|
||||
mockWriter.On("Close").Return(nil)
|
||||
|
||||
id, _ := object.ParseID("I12345")
|
||||
mockWriter.On("Result").Return(id, nil)
|
||||
|
||||
mockRepoWriter.On("NewObjectWriter", mock.Anything, mock.Anything).Return(mockWriter)
|
||||
|
||||
kow := &kopiaObjectWriterEx{
|
||||
ctx: context.Background(),
|
||||
rawRepoWriter: mockRepoWriter,
|
||||
blockSize: 1024,
|
||||
logger: velerotest.NewLogger(),
|
||||
}
|
||||
|
||||
numGoroutines := 10
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
data := make([]byte, 1024)
|
||||
l, err := kow.Write(data)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1024, l)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
assert.Len(t, kow.entries, numGoroutines)
|
||||
}
|
||||
|
||||
func TestKopiaObjectWriterEx_ConcurrentAsyncWrite(t *testing.T) {
|
||||
mockRepoWriter := repomocks.NewMockRepositoryWriter(t)
|
||||
mockWriter := repomocks.NewWriter(t)
|
||||
|
||||
mockWriter.On("Write", mock.Anything).Return(1024, nil)
|
||||
mockWriter.On("Close").Return(nil)
|
||||
|
||||
id, _ := object.ParseID("I12345")
|
||||
mockWriter.On("Result").Return(id, nil)
|
||||
|
||||
mockRepoWriter.On("NewObjectWriter", mock.Anything, mock.Anything).Return(mockWriter)
|
||||
|
||||
sem := make(chan struct{}, 5)
|
||||
buf := freelist.New(5*1024, 1024)
|
||||
|
||||
kow := &kopiaObjectWriterEx{
|
||||
ctx: context.Background(),
|
||||
rawRepoWriter: mockRepoWriter,
|
||||
blockSize: 1024,
|
||||
asyncWritesSem: sem,
|
||||
asyncBuffer: buf,
|
||||
logger: velerotest.NewLogger(),
|
||||
}
|
||||
|
||||
numGoroutines := 10
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
data := make([]byte, 1024)
|
||||
l, err := kow.Write(data)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1024, l)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
kow.asyncWritesGroup.Wait()
|
||||
|
||||
assert.Len(t, kow.entries, numGoroutines)
|
||||
}
|
||||
|
||||
func TestKopiaObjectWriterEx_MultipleWrites(t *testing.T) {
|
||||
mockRepoWriter := repomocks.NewMockRepositoryWriter(t)
|
||||
mockWriter := repomocks.NewWriter(t)
|
||||
|
||||
// Since we are writing 3 blocks, Write should be called 3 times and Close 3 times
|
||||
mockWriter.On("Write", mock.Anything).Return(1024, nil)
|
||||
mockWriter.On("Close").Return(nil)
|
||||
|
||||
id, _ := object.ParseID("I12345")
|
||||
mockWriter.On("Result").Return(id, nil)
|
||||
|
||||
mockRepoWriter.On("NewObjectWriter", mock.Anything, mock.Anything).Return(mockWriter)
|
||||
|
||||
kow := &kopiaObjectWriterEx{
|
||||
ctx: context.Background(),
|
||||
rawRepoWriter: mockRepoWriter,
|
||||
blockSize: 1024,
|
||||
logger: velerotest.NewLogger(),
|
||||
}
|
||||
|
||||
// Write 1st block
|
||||
l, err := kow.Write(make([]byte, 1024))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1024, l)
|
||||
|
||||
// Write 2nd and 3rd block
|
||||
l, err = kow.Write(make([]byte, 2048))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2048, l)
|
||||
|
||||
// In the end we expect 3 blocks to be tracked in `kow.entries`
|
||||
assert.Len(t, kow.entries, 3)
|
||||
assert.Equal(t, int64(0), kow.entries[0].Start)
|
||||
assert.Equal(t, int64(1024), kow.entries[1].Start)
|
||||
assert.Equal(t, int64(2048), kow.entries[2].Start)
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ type Snapshot struct {
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Tags map[string]string
|
||||
TotalSize int64
|
||||
RootObject ObjectMetadata
|
||||
}
|
||||
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 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 restic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Command represents a restic command.
|
||||
type Command struct {
|
||||
Command string
|
||||
RepoIdentifier string
|
||||
PasswordFile string
|
||||
CACertFile string
|
||||
Dir string
|
||||
Args []string
|
||||
ExtraFlags []string
|
||||
Env []string
|
||||
}
|
||||
|
||||
func (c *Command) RepoName() string {
|
||||
if c.RepoIdentifier == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return c.RepoIdentifier[strings.LastIndex(c.RepoIdentifier, "/")+1:]
|
||||
}
|
||||
|
||||
// StringSlice returns the command as a slice of strings.
|
||||
func (c *Command) StringSlice() []string {
|
||||
res := []string{"restic"}
|
||||
|
||||
res = append(res, c.Command, repoFlag(c.RepoIdentifier))
|
||||
if c.PasswordFile != "" {
|
||||
res = append(res, passwordFlag(c.PasswordFile))
|
||||
}
|
||||
if c.CACertFile != "" {
|
||||
res = append(res, cacertFlag(c.CACertFile))
|
||||
}
|
||||
|
||||
// If VELERO_SCRATCH_DIR is defined, put the restic cache within it. If not,
|
||||
// allow restic to choose the location. This makes running either in-cluster
|
||||
// or local (dev) work properly.
|
||||
if scratch := os.Getenv("VELERO_SCRATCH_DIR"); scratch != "" {
|
||||
res = append(res, cacheDirFlag(filepath.Join(scratch, ".cache", "restic")))
|
||||
}
|
||||
|
||||
res = append(res, c.Args...)
|
||||
res = append(res, c.ExtraFlags...)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// String returns the command as a string.
|
||||
func (c *Command) String() string {
|
||||
return strings.Join(c.StringSlice(), " ")
|
||||
}
|
||||
|
||||
// Cmd returns an exec.Cmd for the command.
|
||||
func (c *Command) Cmd() *exec.Cmd {
|
||||
parts := c.StringSlice()
|
||||
cmd := exec.Command(parts[0], parts[1:]...) //nolint:gosec,noctx // Internal call. No need to check the parameter. No to add context for deprecated Restic.
|
||||
cmd.Dir = c.Dir
|
||||
|
||||
if len(c.Env) > 0 {
|
||||
cmd.Env = c.Env
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func repoFlag(repoIdentifier string) string {
|
||||
return fmt.Sprintf("--repo=%s", repoIdentifier)
|
||||
}
|
||||
|
||||
func passwordFlag(file string) string {
|
||||
return fmt.Sprintf("--password-file=%s", file)
|
||||
}
|
||||
|
||||
func cacheDirFlag(dir string) string {
|
||||
return fmt.Sprintf("--cache-dir=%s", dir)
|
||||
}
|
||||
|
||||
func cacertFlag(path string) string {
|
||||
return fmt.Sprintf("--cacert=%s", path)
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
/*
|
||||
Copyright 2018, 2019 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 restic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BackupCommand returns a Command for running a restic backup.
|
||||
func BackupCommand(repoIdentifier, passwordFile, path string, tags map[string]string) *Command {
|
||||
// --host flag is provided with a generic value because restic uses the host
|
||||
// to find a parent snapshot, and by default it will be the name of the daemonset pod
|
||||
// where the `restic backup` command is run. If this pod is recreated, we want to continue
|
||||
// taking incremental backups rather than triggering a full one due to a new pod name.
|
||||
|
||||
return &Command{
|
||||
Command: "backup",
|
||||
RepoIdentifier: repoIdentifier,
|
||||
PasswordFile: passwordFile,
|
||||
Dir: path,
|
||||
Args: []string{"."},
|
||||
ExtraFlags: append(backupTagFlags(tags), "--host=velero", "--json"),
|
||||
}
|
||||
}
|
||||
|
||||
func backupTagFlags(tags map[string]string) []string {
|
||||
var flags []string
|
||||
for k, v := range tags {
|
||||
flags = append(flags, fmt.Sprintf("--tag=%s=%s", k, v))
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
// RestoreCommand returns a Command for running a restic restore.
|
||||
func RestoreCommand(repoIdentifier, passwordFile, snapshotID, target string) *Command {
|
||||
return &Command{
|
||||
Command: "restore",
|
||||
RepoIdentifier: repoIdentifier,
|
||||
PasswordFile: passwordFile,
|
||||
Dir: target,
|
||||
Args: []string{snapshotID},
|
||||
ExtraFlags: []string{"--target=."},
|
||||
}
|
||||
}
|
||||
|
||||
// GetSnapshotCommand returns a Command for running a restic (get) snapshots.
|
||||
func GetSnapshotCommand(repoIdentifier, passwordFile string, tags map[string]string) *Command {
|
||||
return &Command{
|
||||
Command: "snapshots",
|
||||
RepoIdentifier: repoIdentifier,
|
||||
PasswordFile: passwordFile,
|
||||
// "--last" is replaced by "--latest=1" in restic v0.12.1
|
||||
ExtraFlags: []string{"--json", "--latest=1", getSnapshotTagFlag(tags)},
|
||||
}
|
||||
}
|
||||
|
||||
func getSnapshotTagFlag(tags map[string]string) string {
|
||||
var tagFilters []string
|
||||
for k, v := range tags {
|
||||
tagFilters = append(tagFilters, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("--tag=%s", strings.Join(tagFilters, ","))
|
||||
}
|
||||
|
||||
func InitCommand(repoIdentifier string) *Command {
|
||||
return &Command{
|
||||
Command: "init",
|
||||
RepoIdentifier: repoIdentifier,
|
||||
}
|
||||
}
|
||||
|
||||
func SnapshotsCommand(repoIdentifier string) *Command {
|
||||
return &Command{
|
||||
Command: "snapshots",
|
||||
RepoIdentifier: repoIdentifier,
|
||||
}
|
||||
}
|
||||
|
||||
func PruneCommand(repoIdentifier string) *Command {
|
||||
return &Command{
|
||||
Command: "prune",
|
||||
RepoIdentifier: repoIdentifier,
|
||||
}
|
||||
}
|
||||
|
||||
func ForgetCommand(repoIdentifier, snapshotID string) *Command {
|
||||
return &Command{
|
||||
Command: "forget",
|
||||
RepoIdentifier: repoIdentifier,
|
||||
Args: []string{snapshotID},
|
||||
}
|
||||
}
|
||||
|
||||
func UnlockCommand(repoIdentifier string) *Command {
|
||||
return &Command{
|
||||
Command: "unlock",
|
||||
RepoIdentifier: repoIdentifier,
|
||||
}
|
||||
}
|
||||
|
||||
func StatsCommand(repoIdentifier, passwordFile, snapshotID string) *Command {
|
||||
return &Command{
|
||||
Command: "stats",
|
||||
RepoIdentifier: repoIdentifier,
|
||||
PasswordFile: passwordFile,
|
||||
Args: []string{snapshotID},
|
||||
ExtraFlags: []string{"--json"},
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
/*
|
||||
Copyright 2018 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 restic
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBackupCommand(t *testing.T) {
|
||||
c := BackupCommand("repo-id", "password-file", "path", map[string]string{"foo": "bar", "c": "d"})
|
||||
|
||||
assert.Equal(t, "backup", c.Command)
|
||||
assert.Equal(t, "repo-id", c.RepoIdentifier)
|
||||
assert.Equal(t, "password-file", c.PasswordFile)
|
||||
assert.Equal(t, "path", c.Dir)
|
||||
assert.Equal(t, []string{"."}, c.Args)
|
||||
|
||||
expected := []string{"--tag=foo=bar", "--tag=c=d", "--host=velero", "--json"}
|
||||
sort.Strings(expected)
|
||||
sort.Strings(c.ExtraFlags)
|
||||
assert.Equal(t, expected, c.ExtraFlags)
|
||||
}
|
||||
|
||||
func TestRestoreCommand(t *testing.T) {
|
||||
c := RestoreCommand("repo-id", "password-file", "snapshot-id", "target")
|
||||
|
||||
assert.Equal(t, "restore", c.Command)
|
||||
assert.Equal(t, "repo-id", c.RepoIdentifier)
|
||||
assert.Equal(t, "password-file", c.PasswordFile)
|
||||
assert.Equal(t, "target", c.Dir)
|
||||
assert.Equal(t, []string{"snapshot-id"}, c.Args)
|
||||
assert.Equal(t, []string{"--target=."}, c.ExtraFlags)
|
||||
}
|
||||
|
||||
func TestGetSnapshotCommand(t *testing.T) {
|
||||
expectedTags := map[string]string{"foo": "bar", "c": "d"}
|
||||
c := GetSnapshotCommand("repo-id", "password-file", expectedTags)
|
||||
|
||||
assert.Equal(t, "snapshots", c.Command)
|
||||
assert.Equal(t, "repo-id", c.RepoIdentifier)
|
||||
assert.Equal(t, "password-file", c.PasswordFile)
|
||||
|
||||
// set up expected flag names
|
||||
expectedFlags := []string{"--json", "--latest=1", "--tag"}
|
||||
// for tracking actual flag names
|
||||
actualFlags := []string{}
|
||||
// for tracking actual --tag values as a map
|
||||
actualTags := make(map[string]string)
|
||||
|
||||
// loop through actual flags
|
||||
for _, flag := range c.ExtraFlags {
|
||||
// split into 2 parts from the first = sign (if any)
|
||||
parts := strings.SplitN(flag, "=", 2)
|
||||
|
||||
// convert --tag data to a map
|
||||
if parts[0] == "--tag" {
|
||||
actualFlags = append(actualFlags, parts[0])
|
||||
|
||||
// split based on ,
|
||||
tags := strings.Split(parts[1], ",")
|
||||
// loop through each key-value tag pair
|
||||
for _, tag := range tags {
|
||||
// split the pair on =
|
||||
kvs := strings.Split(tag, "=")
|
||||
// record actual key & value
|
||||
actualTags[kvs[0]] = kvs[1]
|
||||
}
|
||||
} else {
|
||||
actualFlags = append(actualFlags, flag)
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedFlags, actualFlags)
|
||||
assert.Equal(t, expectedTags, actualTags)
|
||||
}
|
||||
|
||||
func TestInitCommand(t *testing.T) {
|
||||
c := InitCommand("repo-id")
|
||||
|
||||
assert.Equal(t, "init", c.Command)
|
||||
assert.Equal(t, "repo-id", c.RepoIdentifier)
|
||||
}
|
||||
|
||||
func TestSnapshotsCommand(t *testing.T) {
|
||||
c := SnapshotsCommand("repo-id")
|
||||
|
||||
assert.Equal(t, "snapshots", c.Command)
|
||||
assert.Equal(t, "repo-id", c.RepoIdentifier)
|
||||
}
|
||||
|
||||
func TestPruneCommand(t *testing.T) {
|
||||
c := PruneCommand("repo-id")
|
||||
|
||||
assert.Equal(t, "prune", c.Command)
|
||||
assert.Equal(t, "repo-id", c.RepoIdentifier)
|
||||
}
|
||||
|
||||
func TestForgetCommand(t *testing.T) {
|
||||
c := ForgetCommand("repo-id", "snapshot-id")
|
||||
|
||||
assert.Equal(t, "forget", c.Command)
|
||||
assert.Equal(t, "repo-id", c.RepoIdentifier)
|
||||
assert.Equal(t, []string{"snapshot-id"}, c.Args)
|
||||
}
|
||||
|
||||
func TestStatsCommand(t *testing.T) {
|
||||
c := StatsCommand("repo-id", "password-file", "snapshot-id")
|
||||
|
||||
assert.Equal(t, "stats", c.Command)
|
||||
assert.Equal(t, "repo-id", c.RepoIdentifier)
|
||||
assert.Equal(t, "password-file", c.PasswordFile)
|
||||
assert.Equal(t, []string{"snapshot-id"}, c.Args)
|
||||
assert.Equal(t, []string{"--json"}, c.ExtraFlags)
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 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 restic
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRepoName(t *testing.T) {
|
||||
c := &Command{RepoIdentifier: ""}
|
||||
assert.Empty(t, c.RepoName())
|
||||
|
||||
c.RepoIdentifier = "s3:s3.amazonaws.com/bucket/prefix/repo"
|
||||
assert.Equal(t, "repo", c.RepoName())
|
||||
|
||||
c.RepoIdentifier = "azure:bucket:/repo"
|
||||
assert.Equal(t, "repo", c.RepoName())
|
||||
|
||||
c.RepoIdentifier = "gs:bucket:/prefix/repo"
|
||||
assert.Equal(t, "repo", c.RepoName())
|
||||
}
|
||||
|
||||
func TestStringSlice(t *testing.T) {
|
||||
c := &Command{
|
||||
Command: "cmd",
|
||||
RepoIdentifier: "repo-id",
|
||||
PasswordFile: "/path/to/password-file",
|
||||
Dir: "/some/pwd",
|
||||
Args: []string{"arg-1", "arg-2"},
|
||||
ExtraFlags: []string{"--foo=bar"},
|
||||
}
|
||||
|
||||
require.NoError(t, os.Unsetenv("VELERO_SCRATCH_DIR"))
|
||||
assert.Equal(t, []string{
|
||||
"restic",
|
||||
"cmd",
|
||||
"--repo=repo-id",
|
||||
"--password-file=/path/to/password-file",
|
||||
"arg-1",
|
||||
"arg-2",
|
||||
"--foo=bar",
|
||||
}, c.StringSlice())
|
||||
|
||||
os.Setenv("VELERO_SCRATCH_DIR", "/foo")
|
||||
assert.Equal(t, []string{
|
||||
"restic",
|
||||
"cmd",
|
||||
"--repo=repo-id",
|
||||
"--password-file=/path/to/password-file",
|
||||
"--cache-dir=/foo/.cache/restic",
|
||||
"arg-1",
|
||||
"arg-2",
|
||||
"--foo=bar",
|
||||
}, c.StringSlice())
|
||||
|
||||
require.NoError(t, os.Unsetenv("VELERO_SCRATCH_DIR"))
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
c := &Command{
|
||||
Command: "cmd",
|
||||
RepoIdentifier: "repo-id",
|
||||
PasswordFile: "/path/to/password-file",
|
||||
Dir: "/some/pwd",
|
||||
Args: []string{"arg-1", "arg-2"},
|
||||
ExtraFlags: []string{"--foo=bar"},
|
||||
}
|
||||
|
||||
require.NoError(t, os.Unsetenv("VELERO_SCRATCH_DIR"))
|
||||
assert.Equal(t, "restic cmd --repo=repo-id --password-file=/path/to/password-file arg-1 arg-2 --foo=bar", c.String())
|
||||
}
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
c := &Command{
|
||||
Command: "cmd",
|
||||
RepoIdentifier: "repo-id",
|
||||
PasswordFile: "/path/to/password-file",
|
||||
Dir: "/some/pwd",
|
||||
Args: []string{"arg-1", "arg-2"},
|
||||
ExtraFlags: []string{"--foo=bar"},
|
||||
}
|
||||
|
||||
require.NoError(t, os.Unsetenv("VELERO_SCRATCH_DIR"))
|
||||
execCmd := c.Cmd()
|
||||
|
||||
assert.Equal(t, c.StringSlice(), execCmd.Args)
|
||||
assert.Equal(t, c.Dir, execCmd.Dir)
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
/*
|
||||
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 restic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/credentials"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
repoconfig "github.com/vmware-tanzu/velero/pkg/repository/config"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
// DefaultMaintenanceFrequency is the default time interval
|
||||
// at which restic prune is run.
|
||||
DefaultMaintenanceFrequency = 7 * 24 * time.Hour
|
||||
|
||||
// insecureSkipTLSVerifyKey is the flag in BackupStorageLocation's config
|
||||
// to indicate whether to skip TLS verify to setup insecure HTTPS connection.
|
||||
insecureSkipTLSVerifyKey = "insecureSkipTLSVerify"
|
||||
|
||||
// resticInsecureTLSFlag is the flag for Restic command line to indicate
|
||||
// skip TLS verify on https connection.
|
||||
resticInsecureTLSFlag = "--insecure-tls"
|
||||
)
|
||||
|
||||
// TempCACertFile creates a temp file containing a CA bundle
|
||||
// and returns its path. The caller should generally call os.Remove()
|
||||
// to remove the file when done with it.
|
||||
func TempCACertFile(caCert []byte, bsl string, fs filesystem.Interface) (string, error) {
|
||||
file, err := fs.TempFile("", fmt.Sprintf("cacert-%s", bsl))
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if _, err := file.Write(caCert); err != nil {
|
||||
// nothing we can do about an error closing the file here, and we're
|
||||
// already returning an error about the write failing.
|
||||
file.Close()
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
name := file.Name()
|
||||
|
||||
if err := file.Close(); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// environ is a slice of strings representing the environment, in the form "key=value".
|
||||
type environ []string
|
||||
|
||||
// Unset a single environment variable.
|
||||
func (e *environ) Unset(key string) {
|
||||
for i := range *e {
|
||||
if strings.HasPrefix((*e)[i], key+"=") {
|
||||
(*e)[i] = (*e)[len(*e)-1]
|
||||
*e = (*e)[:len(*e)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CmdEnv returns a list of environment variables (in the format var=val) that
|
||||
// should be used when running a restic command for a particular backend provider.
|
||||
// This list is the current environment, plus any provider-specific variables restic needs.
|
||||
func CmdEnv(backupLocation *velerov1api.BackupStorageLocation, credentialFileStore credentials.FileStore) ([]string, error) {
|
||||
var env environ
|
||||
env = os.Environ()
|
||||
customEnv := map[string]string{}
|
||||
var err error
|
||||
|
||||
config := backupLocation.Spec.Config
|
||||
if config == nil {
|
||||
config = map[string]string{}
|
||||
}
|
||||
|
||||
if backupLocation.Spec.Credential != nil {
|
||||
credsFile, err := credentialFileStore.Path(backupLocation.Spec.Credential)
|
||||
if err != nil {
|
||||
return []string{}, errors.WithStack(err)
|
||||
}
|
||||
config[repoconfig.CredentialsFileKey] = credsFile
|
||||
}
|
||||
|
||||
backendType := repoconfig.GetBackendType(backupLocation.Spec.Provider, backupLocation.Spec.Config)
|
||||
|
||||
switch backendType {
|
||||
case repoconfig.AWSBackend:
|
||||
customEnv, err = repoconfig.GetS3ResticEnvVars(config)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
case repoconfig.AzureBackend:
|
||||
customEnv, err = repoconfig.GetAzureResticEnvVars(config)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
case repoconfig.GCPBackend:
|
||||
customEnv, err = repoconfig.GetGCPResticEnvVars(config)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range customEnv {
|
||||
env.Unset(k)
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
env = append(env, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// GetInsecureSkipTLSVerifyFromBSL get insecureSkipTLSVerify flag from BSL configuration,
|
||||
// Then return --insecure-tls flag with boolean value as result.
|
||||
func GetInsecureSkipTLSVerifyFromBSL(backupLocation *velerov1api.BackupStorageLocation, logger logrus.FieldLogger) string {
|
||||
result := ""
|
||||
|
||||
if backupLocation == nil {
|
||||
logger.Info("bsl is nil. return empty.")
|
||||
return result
|
||||
}
|
||||
|
||||
if insecure, _ := strconv.ParseBool(backupLocation.Spec.Config[insecureSkipTLSVerifyKey]); insecure {
|
||||
logger.Debugf("set --insecure-tls=true for Restic command according to BSL %s config", backupLocation.Name)
|
||||
result = resticInsecureTLSFlag + "=true"
|
||||
return result
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
/*
|
||||
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 restic
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
)
|
||||
|
||||
func TestTempCACertFile(t *testing.T) {
|
||||
var (
|
||||
fs = velerotest.NewFakeFileSystem()
|
||||
caCertData = []byte("cacert")
|
||||
)
|
||||
|
||||
fileName, err := TempCACertFile(caCertData, "default", fs)
|
||||
require.NoError(t, err)
|
||||
|
||||
contents, err := fs.ReadFile(fileName)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, string(caCertData), string(contents))
|
||||
|
||||
os.Remove(fileName)
|
||||
}
|
||||
|
||||
func TestGetInsecureSkipTLSVerifyFromBSL(t *testing.T) {
|
||||
log := logrus.StandardLogger()
|
||||
tests := []struct {
|
||||
name string
|
||||
backupLocation *velerov1api.BackupStorageLocation
|
||||
logger logrus.FieldLogger
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"Test with nil BSL. Should return empty string.",
|
||||
nil,
|
||||
log,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Test BSL with no configuration. Should return empty string.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "azure",
|
||||
},
|
||||
},
|
||||
log,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Test with AWS BSL's insecureSkipTLSVerify set to false.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "aws",
|
||||
Config: map[string]string{
|
||||
"insecureSkipTLSVerify": "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
log,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Test with AWS BSL's insecureSkipTLSVerify set to true.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "aws",
|
||||
Config: map[string]string{
|
||||
"insecureSkipTLSVerify": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
log,
|
||||
"--insecure-tls=true",
|
||||
},
|
||||
{
|
||||
"Test with Azure BSL's insecureSkipTLSVerify set to invalid.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "azure",
|
||||
Config: map[string]string{
|
||||
"insecureSkipTLSVerify": "invalid",
|
||||
},
|
||||
},
|
||||
},
|
||||
log,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Test with GCP without insecureSkipTLSVerify.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "gcp",
|
||||
Config: map[string]string{},
|
||||
},
|
||||
},
|
||||
log,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Test with AWS without config.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "aws",
|
||||
},
|
||||
},
|
||||
log,
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res := GetInsecureSkipTLSVerifyFromBSL(test.backupLocation, test.logger)
|
||||
|
||||
assert.Equal(t, test.expected, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
/*
|
||||
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 restic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/uploader"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/exec"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
)
|
||||
|
||||
const restoreProgressCheckInterval = 10 * time.Second
|
||||
const backupProgressCheckInterval = 10 * time.Second
|
||||
|
||||
var fileSystem = filesystem.NewFileSystem()
|
||||
|
||||
type backupStatusLine struct {
|
||||
MessageType string `json:"message_type"`
|
||||
// seen in status lines
|
||||
TotalBytes int64 `json:"total_bytes"`
|
||||
BytesDone int64 `json:"bytes_done"`
|
||||
// seen in summary line at the end
|
||||
TotalBytesProcessed int64 `json:"total_bytes_processed"`
|
||||
}
|
||||
|
||||
// GetSnapshotID runs provided 'restic snapshots' command to get the ID of a snapshot
|
||||
// and an error if a unique snapshot cannot be identified.
|
||||
func GetSnapshotID(snapshotIDCmd *Command) (string, error) {
|
||||
stdout, stderr, err := exec.RunCommand(snapshotIDCmd.Cmd())
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "error running command, stderr=%s", stderr)
|
||||
}
|
||||
|
||||
type snapshotID struct {
|
||||
ShortID string `json:"short_id"`
|
||||
}
|
||||
|
||||
var snapshots []snapshotID
|
||||
if err := json.Unmarshal([]byte(stdout), &snapshots); err != nil {
|
||||
return "", errors.Wrap(err, "error unmarshaling restic snapshots result")
|
||||
}
|
||||
|
||||
if len(snapshots) != 1 {
|
||||
return "", errors.Errorf("expected one matching snapshot by command: %s, got %d", snapshotIDCmd.String(), len(snapshots))
|
||||
}
|
||||
|
||||
return snapshots[0].ShortID, nil
|
||||
}
|
||||
|
||||
// RunBackup runs a `restic backup` command and watches the output to provide
|
||||
// progress updates to the caller.
|
||||
func RunBackup(backupCmd *Command, log logrus.FieldLogger, updater uploader.ProgressUpdater) (string, string, error) {
|
||||
// buffers for copying command stdout/err output into
|
||||
stdoutBuf := new(bytes.Buffer)
|
||||
stderrBuf := new(bytes.Buffer)
|
||||
|
||||
// create a channel to signal when to end the goroutine scanning for progress
|
||||
// updates
|
||||
quit := make(chan struct{})
|
||||
|
||||
cmd := backupCmd.Cmd()
|
||||
cmd.Stdout = stdoutBuf
|
||||
cmd.Stderr = stderrBuf
|
||||
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
exec.LogErrorAsExitCode(err, log)
|
||||
return stdoutBuf.String(), stderrBuf.String(), err
|
||||
}
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(backupProgressCheckInterval)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
lastLine := getLastLine(stdoutBuf.Bytes())
|
||||
if len(lastLine) > 0 {
|
||||
stat, err := decodeBackupStatusLine(lastLine)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("error getting restic backup progress")
|
||||
}
|
||||
|
||||
// if the line contains a non-empty bytes_done field, we can update the
|
||||
// caller with the progress
|
||||
if stat.BytesDone != 0 {
|
||||
updater.UpdateProgress(&uploader.Progress{
|
||||
TotalBytes: stat.TotalBytes,
|
||||
BytesDone: stat.BytesDone,
|
||||
})
|
||||
}
|
||||
}
|
||||
case <-quit:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
exec.LogErrorAsExitCode(err, log)
|
||||
return stdoutBuf.String(), stderrBuf.String(), err
|
||||
}
|
||||
quit <- struct{}{}
|
||||
|
||||
summary, err := getSummaryLine(stdoutBuf.Bytes())
|
||||
if err != nil {
|
||||
return stdoutBuf.String(), stderrBuf.String(), err
|
||||
}
|
||||
stat, err := decodeBackupStatusLine(summary)
|
||||
if err != nil {
|
||||
return stdoutBuf.String(), stderrBuf.String(), err
|
||||
}
|
||||
if stat.MessageType != "summary" {
|
||||
return stdoutBuf.String(), stderrBuf.String(), errors.WithStack(fmt.Errorf("error getting restic backup summary: %s", string(summary)))
|
||||
}
|
||||
|
||||
// update progress to 100%
|
||||
updater.UpdateProgress(&uploader.Progress{
|
||||
TotalBytes: stat.TotalBytesProcessed,
|
||||
BytesDone: stat.TotalBytesProcessed,
|
||||
})
|
||||
|
||||
return string(summary), stderrBuf.String(), nil
|
||||
}
|
||||
|
||||
func decodeBackupStatusLine(lastLine []byte) (backupStatusLine, error) {
|
||||
var stat backupStatusLine
|
||||
if err := json.Unmarshal(lastLine, &stat); err != nil {
|
||||
return stat, errors.Wrapf(err, "unable to decode backup JSON line: %s", string(lastLine))
|
||||
}
|
||||
return stat, nil
|
||||
}
|
||||
|
||||
// getLastLine returns the last line of a byte array. The string is assumed to
|
||||
// have a newline at the end of it, so this returns the substring between the
|
||||
// last two newlines.
|
||||
func getLastLine(b []byte) []byte {
|
||||
if len(b) == 0 {
|
||||
return []byte("")
|
||||
}
|
||||
// subslice the byte array to ignore the newline at the end of the string
|
||||
lastNewLineIdx := bytes.LastIndex(b[:len(b)-1], []byte("\n"))
|
||||
return b[lastNewLineIdx+1 : len(b)-1]
|
||||
}
|
||||
|
||||
// getSummaryLine looks for the summary JSON line
|
||||
// (`{"message_type:"summary",...`) in the restic backup command output. Due to
|
||||
// an issue in Restic, this might not always be the last line
|
||||
// (https://github.com/restic/restic/issues/2389). It returns an error if it
|
||||
// can't be found.
|
||||
func getSummaryLine(b []byte) ([]byte, error) {
|
||||
summaryLineIdx := bytes.LastIndex(b, []byte(`{"message_type":"summary"`))
|
||||
if summaryLineIdx < 0 {
|
||||
return nil, errors.New("unable to find summary in restic backup command output")
|
||||
}
|
||||
// find the end of the summary line
|
||||
newLineIdx := bytes.Index(b[summaryLineIdx:], []byte("\n"))
|
||||
if newLineIdx < 0 {
|
||||
return nil, errors.New("unable to get summary line from restic backup command output")
|
||||
}
|
||||
return b[summaryLineIdx : summaryLineIdx+newLineIdx], nil
|
||||
}
|
||||
|
||||
// RunRestore runs a `restic restore` command and monitors the volume size to
|
||||
// provide progress updates to the caller.
|
||||
func RunRestore(restoreCmd *Command, log logrus.FieldLogger, updater uploader.ProgressUpdater) (string, string, error) {
|
||||
insecureTLSFlag := ""
|
||||
|
||||
for _, extraFlag := range restoreCmd.ExtraFlags {
|
||||
if strings.Contains(extraFlag, resticInsecureTLSFlag) {
|
||||
insecureTLSFlag = extraFlag
|
||||
}
|
||||
}
|
||||
|
||||
snapshotSize, err := getSnapshotSize(restoreCmd.RepoIdentifier, restoreCmd.PasswordFile, restoreCmd.CACertFile, restoreCmd.Args[0], restoreCmd.Env, insecureTLSFlag)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(err, "error getting snapshot size")
|
||||
}
|
||||
|
||||
updater.UpdateProgress(&uploader.Progress{
|
||||
TotalBytes: snapshotSize,
|
||||
})
|
||||
|
||||
// create a channel to signal when to end the goroutine scanning for progress
|
||||
// updates
|
||||
quit := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(restoreProgressCheckInterval)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
volumeSize, err := getVolumeSize(restoreCmd.Dir)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("error getting restic restore progress")
|
||||
}
|
||||
|
||||
if volumeSize != 0 {
|
||||
updater.UpdateProgress(&uploader.Progress{
|
||||
TotalBytes: snapshotSize,
|
||||
BytesDone: volumeSize,
|
||||
})
|
||||
}
|
||||
case <-quit:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
stdout, stderr, err := exec.RunCommandWithLog(restoreCmd.Cmd(), log)
|
||||
quit <- struct{}{}
|
||||
|
||||
// update progress to 100%
|
||||
updater.UpdateProgress(&uploader.Progress{
|
||||
TotalBytes: snapshotSize,
|
||||
BytesDone: snapshotSize,
|
||||
})
|
||||
|
||||
return stdout, stderr, err
|
||||
}
|
||||
|
||||
func getSnapshotSize(repoIdentifier, passwordFile, caCertFile, snapshotID string, env []string, insecureTLS string) (int64, error) {
|
||||
cmd := StatsCommand(repoIdentifier, passwordFile, snapshotID)
|
||||
cmd.Env = env
|
||||
cmd.CACertFile = caCertFile
|
||||
|
||||
if len(insecureTLS) > 0 {
|
||||
cmd.ExtraFlags = append(cmd.ExtraFlags, insecureTLS)
|
||||
}
|
||||
|
||||
stdout, stderr, err := exec.RunCommand(cmd.Cmd())
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "error running command, stderr=%s", stderr)
|
||||
}
|
||||
|
||||
var snapshotStats struct {
|
||||
TotalSize int64 `json:"total_size"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(stdout), &snapshotStats); err != nil {
|
||||
return 0, errors.Wrapf(err, "error unmarshaling restic stats result, stdout=%s", stdout)
|
||||
}
|
||||
|
||||
return snapshotStats.TotalSize, nil
|
||||
}
|
||||
|
||||
func getVolumeSize(path string) (int64, error) {
|
||||
var size int64
|
||||
|
||||
files, err := fileSystem.ReadDir(path)
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "error reading directory %s", path)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
s, err := getVolumeSize(fmt.Sprintf("%s/%s", path, file.Name()))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
size += s
|
||||
} else {
|
||||
size += file.Size()
|
||||
}
|
||||
}
|
||||
|
||||
return size, nil
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
Copyright 2019 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 restic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/test"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
)
|
||||
|
||||
func Test_getSummaryLine(t *testing.T) {
|
||||
summaryLine := `{"message_type":"summary","files_new":0,"files_changed":0,"files_unmodified":3,"dirs_new":0,"dirs_changed":0,"dirs_unmodified":0,"data_blobs":0,"tree_blobs":0,"data_added":0,"total_files_processed":3,"total_bytes_processed":13238272000,"total_duration":0.319265105,"snapshot_id":"38515bb5"}`
|
||||
tests := []struct {
|
||||
name string
|
||||
output string
|
||||
wantErr bool
|
||||
}{
|
||||
{"no summary", `{"message_type":"status","percent_done":0,"total_files":1,"total_bytes":10485760000}
|
||||
{"message_type":"status","percent_done":0,"total_files":3,"files_done":1,"total_bytes":13238272000}
|
||||
`, true},
|
||||
{"no newline after summary", `{"message_type":"status","percent_done":0,"total_files":1,"total_bytes":10485760000}
|
||||
{"message_type":"status","percent_done":0,"total_files":3,"files_done":1,"total_bytes":13238272000}
|
||||
{"message_type":"summary","files_new":0,"files_changed":0,"files_unmodified":3,"dirs_new":0`, true},
|
||||
{"summary at end", `{"message_type":"status","percent_done":0,"total_files":1,"total_bytes":10485760000}
|
||||
{"message_type":"status","percent_done":0,"total_files":3,"files_done":1,"total_bytes":13238272000}
|
||||
{"message_type":"status","percent_done":1,"total_files":3,"files_done":3,"total_bytes":13238272000,"bytes_done":13238272000}
|
||||
{"message_type":"summary","files_new":0,"files_changed":0,"files_unmodified":3,"dirs_new":0,"dirs_changed":0,"dirs_unmodified":0,"data_blobs":0,"tree_blobs":0,"data_added":0,"total_files_processed":3,"total_bytes_processed":13238272000,"total_duration":0.319265105,"snapshot_id":"38515bb5"}
|
||||
`, false},
|
||||
{"summary before status", `{"message_type":"status","percent_done":0,"total_files":1,"total_bytes":10485760000}
|
||||
{"message_type":"status","percent_done":0,"total_files":3,"files_done":1,"total_bytes":13238272000}
|
||||
{"message_type":"summary","files_new":0,"files_changed":0,"files_unmodified":3,"dirs_new":0,"dirs_changed":0,"dirs_unmodified":0,"data_blobs":0,"tree_blobs":0,"data_added":0,"total_files_processed":3,"total_bytes_processed":13238272000,"total_duration":0.319265105,"snapshot_id":"38515bb5"}
|
||||
{"message_type":"status","percent_done":1,"total_files":3,"files_done":3,"total_bytes":13238272000,"bytes_done":13238272000}
|
||||
`, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
summary, err := getSummaryLine([]byte(tt.output))
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, summaryLine, string(summary))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getLastLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
output []byte
|
||||
want string
|
||||
}{
|
||||
{[]byte(`last line
|
||||
`), "last line"},
|
||||
{[]byte(`first line
|
||||
second line
|
||||
third line
|
||||
`), "third line"},
|
||||
{[]byte(""), ""},
|
||||
{nil, ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.want, func(t *testing.T) {
|
||||
assert.Equal(t, []byte(tt.want), getLastLine(tt.output))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getVolumeSize(t *testing.T) {
|
||||
files := map[string][]byte{
|
||||
"/file1.txt": []byte("file1"),
|
||||
"/file2.txt": []byte("file2"),
|
||||
"/file3.txt": []byte("file3"),
|
||||
"/files/file4.txt": []byte("file4"),
|
||||
"/files/nested/file5.txt": []byte("file5"),
|
||||
}
|
||||
fakefs := test.NewFakeFileSystem()
|
||||
|
||||
var expectedSize int64
|
||||
for path, content := range files {
|
||||
fakefs.WithFile(path, content)
|
||||
expectedSize += int64(len(content))
|
||||
}
|
||||
|
||||
fileSystem = fakefs
|
||||
defer func() { fileSystem = filesystem.NewFileSystem() }()
|
||||
|
||||
actualSize, err := getVolumeSize("/")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedSize, actualSize)
|
||||
}
|
||||
@@ -74,6 +74,10 @@ type CachePVC struct {
|
||||
ResidentThresholdInMB int64 `json:"residentThresholdInMB,omitempty"`
|
||||
}
|
||||
|
||||
type CSISnapshotMetadataService struct {
|
||||
SAName string `json:"saName,omitempty"`
|
||||
}
|
||||
|
||||
type NodeAgentConfigs struct {
|
||||
// LoadConcurrency is the config for data path load concurrency per node.
|
||||
LoadConcurrency *LoadConcurrency `json:"loadConcurrency,omitempty"`
|
||||
@@ -104,4 +108,7 @@ type NodeAgentConfigs struct {
|
||||
|
||||
// PodLabels are labels to be added to pods created by node-agent, i.e., data mover pods.
|
||||
PodLabels map[string]string `json:"podLabels,omitempty"`
|
||||
|
||||
// CSISnapshotMetadataServiceConfigs is the config for CSI snapshot metadata service
|
||||
CSISnapshotMetadataServiceConfigs *CSISnapshotMetadataService `json:"csiSnapshotMetadataServiceConfigs,omitempty"`
|
||||
}
|
||||
|
||||
+15
-1
@@ -20,6 +20,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -236,7 +237,20 @@ func CollectPodLogs(ctx context.Context, podGetter corev1client.CoreV1Interface,
|
||||
func ToSystemAffinity(loadAffinity *LoadAffinity, volumeTopology *corev1api.NodeSelector) *corev1api.Affinity {
|
||||
requirements := []corev1api.NodeSelectorRequirement{}
|
||||
if loadAffinity != nil {
|
||||
for k, v := range loadAffinity.NodeSelector.MatchLabels {
|
||||
// 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]
|
||||
requirements = append(requirements, corev1api.NodeSelectorRequirement{
|
||||
Key: k,
|
||||
Values: []string{v},
|
||||
|
||||
@@ -834,6 +834,45 @@ 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{
|
||||
|
||||
@@ -53,6 +53,7 @@ const (
|
||||
KubeAnnDynamicallyProvisioned = "pv.kubernetes.io/provisioned-by"
|
||||
KubeAnnMigratedTo = "pv.kubernetes.io/migrated-to"
|
||||
KubeAnnSelectedNode = "volume.kubernetes.io/selected-node"
|
||||
KubeAnnAllowVolumeModeChange = "snapshot.storage.kubernetes.io/allow-volume-mode-change"
|
||||
)
|
||||
|
||||
// VolumeSnapshotContentManagedByLabel is applied by the snapshot controller
|
||||
|
||||
@@ -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.
|
||||
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` 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,26 +78,41 @@ 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. Velero will append resources not listed to the end of your customized list in alphabetical order.
|
||||
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.
|
||||
|
||||
```shell
|
||||
velero server \
|
||||
--restore-resource-priorities=customresourcedefinitions,namespaces,storageclasses,\
|
||||
volumesnapshotclass.snapshot.storage.k8s.io,volumesnapshotcontents.snapshot.storage.k8s.io,\
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -78,26 +78,41 @@ 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. Velero will append resources not listed to the end of your customized list in alphabetical order.
|
||||
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.
|
||||
|
||||
```shell
|
||||
velero server \
|
||||
--restore-resource-priorities=customresourcedefinitions,namespaces,storageclasses,\
|
||||
volumesnapshotclass.snapshot.storage.k8s.io,volumesnapshotcontents.snapshot.storage.k8s.io,\
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -78,26 +78,41 @@ 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. Velero will append resources not listed to the end of your customized list in alphabetical order.
|
||||
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.
|
||||
|
||||
```shell
|
||||
velero server \
|
||||
--restore-resource-priorities=customresourcedefinitions,namespaces,storageclasses,\
|
||||
volumesnapshotclass.snapshot.storage.k8s.io,volumesnapshotcontents.snapshot.storage.k8s.io,\
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ HAS_VSPHERE_PLUGIN ?= false
|
||||
RESTORE_HELPER_IMAGE ?=
|
||||
|
||||
#Released version only
|
||||
UPGRADE_FROM_VELERO_VERSION ?= v1.16.2,v1.17.2
|
||||
UPGRADE_FROM_VELERO_VERSION ?= v1.15.2,v1.16.2,v1.17.2
|
||||
|
||||
# UPGRADE_FROM_VELERO_CLI can has the same format(a list divided by comma) with UPGRADE_FROM_VELERO_VERSION
|
||||
# Upgrade tests will be executed sequently according to the list by UPGRADE_FROM_VELERO_VERSION
|
||||
|
||||
+6
-8
@@ -293,18 +293,18 @@ E2E tests can be run with specific cases to be included and/or excluded using th
|
||||
|
||||
1. Run Velero tests with specific cases to be included:
|
||||
```bash
|
||||
GINKGO_LABELS="Basic && Restic" \
|
||||
GINKGO_LABELS="Basic && FSBackup" \
|
||||
CLOUD_PROVIDER=aws \
|
||||
BSL_BUCKET=example-bucket \
|
||||
CREDS_FILE=/path/to/aws-creds \
|
||||
make test-e2e \
|
||||
```
|
||||
|
||||
In this example, only case have both `Basic` and `Restic` labels are included.
|
||||
In this example, only case have both `Basic` and `FSBackup` labels are included.
|
||||
|
||||
1. Run Velero tests with specific cases to be excluded:
|
||||
```bash
|
||||
GINKGO_LABELS="!(Scale || Schedule || TTL || (Upgrade && Restic) || (Migration && Restic))" \
|
||||
GINKGO_LABELS="!(Scale || Schedule || TTL || (Upgrade && FSBackup) || (Migration && FSBackup))" \
|
||||
CLOUD_PROVIDER=aws \
|
||||
BSL_BUCKET=example-bucket \
|
||||
CREDS_FILE=/path/to/aws-creds \
|
||||
@@ -315,8 +315,8 @@ In this example, cases are labelled as
|
||||
* `Scale`
|
||||
* `Schedule`
|
||||
* `TTL`
|
||||
* `Upgrade` and `Restic`
|
||||
* `Migration` and `Restic`
|
||||
* `Upgrade` and `FSBackup`
|
||||
* `Migration` and `FSBackup`
|
||||
will be skipped.
|
||||
|
||||
#### VKS environment test
|
||||
@@ -370,9 +370,7 @@ Following pipelines should cover all E2E tests along with proper filters:
|
||||
|
||||
1. **CSI pipeline:** As we can see lots of labels in E2E test code, there're many snapshot-labeled test scripts. To cover CSI scenario, a pipeline with CSI enabled should be a good choice, otherwise, we will double all the snapshot cases for CSI scenario, it's very time-wasting. By providing `FEATURES=EnableCSI` and `PLUGINS=<provider-plugin-images>`, a CSI pipeline is ready for testing.
|
||||
1. **Data mover pipeline:** Data mover scenario is the same scenario with migaration test except the restriction of migaration between different providers, so it better to separated it out from other pipelines. Please refer the example in previous.
|
||||
1. **Restic/Kopia backup path pipelines:**
|
||||
1. **Restic pipeline:** For the same reason of saving time, set `UPLOADER_TYPE` to `restic` for all file system backup test cases;
|
||||
1. **Kopia pipeline:** Set `UPLOADER_TYPE` to `kopia` for all file system backup test cases;
|
||||
1. **File system backup pipeline:** Set `UPLOADER_TYPE` to `kopia` for all file system backup test cases;
|
||||
1. **Long time pipeline:** Long time cases should be group into one pipeline, currently these test cases with labels `Scale`, `Schedule` or `TTL` can be group into a pipeline, and make sure to skip them off in any other pipelines.
|
||||
|
||||
**Note:** please organize filters among proper pipelines for other test cases.
|
||||
|
||||
@@ -43,7 +43,7 @@ func BackupRestoreWithSnapshots() {
|
||||
BackupRestoreTest(config)
|
||||
}
|
||||
|
||||
func BackupRestoreWithRestic() {
|
||||
func BackupRestoreWithFSBackup() {
|
||||
config := BackupRestoreTestConfig{false, "", false}
|
||||
BackupRestoreTest(config)
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func BackupRestoreRetainedPVWithSnapshots() {
|
||||
BackupRestoreTest(config)
|
||||
}
|
||||
|
||||
func BackupRestoreRetainedPVWithRestic() {
|
||||
func BackupRestoreRetainedPVWithFSBackup() {
|
||||
config := BackupRestoreTestConfig{false, "overlays/sc-reclaim-policy/", true}
|
||||
BackupRestoreTest(config)
|
||||
}
|
||||
|
||||
@@ -34,13 +34,11 @@ import (
|
||||
. "github.com/vmware-tanzu/velero/test/util/velero"
|
||||
)
|
||||
|
||||
// Test backup and restore of Kibishii using restic
|
||||
|
||||
func BackupDeletionWithSnapshots() {
|
||||
backup_deletion_test(true)
|
||||
}
|
||||
|
||||
func BackupDeletionWithRestic() {
|
||||
func BackupDeletionWithFSBackup() {
|
||||
backup_deletion_test(false)
|
||||
}
|
||||
func backup_deletion_test(useVolumeSnapshots bool) {
|
||||
|
||||
@@ -21,8 +21,8 @@ type NamespaceMapping struct {
|
||||
|
||||
const NamespaceBaseName string = "ns-mp-"
|
||||
|
||||
var OneNamespaceMappingResticTest func() = TestFunc(&NamespaceMapping{TestCase: TestCase{NamespacesTotal: 1, UseVolumeSnapshots: false}})
|
||||
var MultiNamespacesMappingResticTest func() = TestFunc(&NamespaceMapping{TestCase: TestCase{NamespacesTotal: 2, UseVolumeSnapshots: false}})
|
||||
var OneNamespaceMappingFSBackupTest func() = TestFunc(&NamespaceMapping{TestCase: TestCase{NamespacesTotal: 1, UseVolumeSnapshots: false}})
|
||||
var MultiNamespacesMappingFSBackupTest func() = TestFunc(&NamespaceMapping{TestCase: TestCase{NamespacesTotal: 2, UseVolumeSnapshots: false}})
|
||||
var OneNamespaceMappingSnapshotTest func() = TestFunc(&NamespaceMapping{TestCase: TestCase{NamespacesTotal: 1, UseVolumeSnapshots: true}})
|
||||
var MultiNamespacesMappingSnapshotTest func() = TestFunc(&NamespaceMapping{TestCase: TestCase{NamespacesTotal: 2, UseVolumeSnapshots: true}})
|
||||
|
||||
@@ -37,7 +37,7 @@ func (n *NamespaceMapping) Init() error {
|
||||
if n.VeleroCfg.CloudProvider == "kind" {
|
||||
n.kibishiiData = &KibishiiData{Levels: 0, DirsPerLevel: 0, FilesPerLevel: 0, FileLength: 0, BlockSize: 0, PassNum: 0, ExpectedNodes: 2}
|
||||
}
|
||||
backupType := "restic"
|
||||
backupType := "fs-backup"
|
||||
if n.UseVolumeSnapshots {
|
||||
backupType = "snapshot"
|
||||
}
|
||||
|
||||
@@ -41,13 +41,11 @@ const (
|
||||
bslDeletionTestNs = "bsl-deletion"
|
||||
)
|
||||
|
||||
// Test backup and restore of Kibishii using restic
|
||||
|
||||
func BslDeletionWithSnapshots() {
|
||||
BslDeletionTest(true)
|
||||
}
|
||||
|
||||
func BslDeletionWithRestic() {
|
||||
func BslDeletionWithFSBackup() {
|
||||
BslDeletionTest(false)
|
||||
}
|
||||
func BslDeletionTest(useVolumeSnapshots bool) {
|
||||
@@ -89,7 +87,7 @@ func BslDeletionTest(useVolumeSnapshots bool) {
|
||||
})
|
||||
|
||||
When("kibishii is the sample workload", func() {
|
||||
It("Local backups and restic repos (if Velero was installed with Restic) will be deleted once the corresponding backup storage location is deleted", func() {
|
||||
It("Local backups and backup repos will be deleted once the corresponding backup storage location is deleted", func() {
|
||||
oneHourTimeout, ctxCancel := context.WithTimeout(context.Background(), time.Minute*60)
|
||||
defer ctxCancel()
|
||||
if veleroCfg.AdditionalBSLProvider == "" {
|
||||
@@ -165,7 +163,7 @@ func BslDeletionTest(useVolumeSnapshots bool) {
|
||||
)).To(Succeed())
|
||||
})
|
||||
|
||||
// Restic can not backup PV only, so pod need to be labeled also
|
||||
// FS backup can not backup PV only, so pod need to be labeled also
|
||||
By("Label all 2 worker-pods of Kibishii", func() {
|
||||
Expect(AddLabelToPod(context.Background(), podName1, bslDeletionTestNs, label1)).To(Succeed())
|
||||
Expect(AddLabelToPod(context.Background(), "kibishii-deployment-1", bslDeletionTestNs, label2)).To(Succeed())
|
||||
|
||||
+21
-23
@@ -397,11 +397,10 @@ var _ = Describe(
|
||||
APIExtensionsVersionsTest,
|
||||
)
|
||||
|
||||
// Test backup and restore of Kibishii using restic
|
||||
var _ = Describe(
|
||||
"Velero tests on cluster using the plugin provider for object storage and Restic for volume backups",
|
||||
Label("Basic", "Restic", "AdditionalBSL"),
|
||||
BackupRestoreWithRestic,
|
||||
"Velero tests on cluster using the plugin provider for object storage and file system backup for volumes",
|
||||
Label("Basic", "FSBackup", "AdditionalBSL"),
|
||||
BackupRestoreWithFSBackup,
|
||||
)
|
||||
|
||||
var _ = Describe(
|
||||
@@ -417,9 +416,9 @@ var _ = Describe(
|
||||
)
|
||||
|
||||
var _ = Describe(
|
||||
"Velero tests on cluster using the plugin provider for object storage and snapshots for volume backups",
|
||||
Label("Basic", "Restic", "RetainPV", "AdditionalBSL"),
|
||||
BackupRestoreRetainedPVWithRestic,
|
||||
"Velero tests on cluster using the plugin provider for object storage and file system backup for volumes",
|
||||
Label("Basic", "FSBackup", "RetainPV", "AdditionalBSL"),
|
||||
BackupRestoreRetainedPVWithFSBackup,
|
||||
)
|
||||
|
||||
var _ = Describe(
|
||||
@@ -452,11 +451,10 @@ var _ = Describe(
|
||||
MultiNSBackupRestore,
|
||||
)
|
||||
|
||||
// Upgrade test by Kibishii using Restic
|
||||
var _ = Describe(
|
||||
"Velero upgrade tests on cluster using the plugin provider for object storage and Restic for volume backups",
|
||||
Label("Upgrade", "Restic"),
|
||||
BackupUpgradeRestoreWithRestic,
|
||||
"Velero upgrade tests on cluster using the plugin provider for object storage and file system backup for volumes",
|
||||
Label("Upgrade", "FSBackup"),
|
||||
BackupUpgradeRestoreWithFSBackup,
|
||||
)
|
||||
var _ = Describe(
|
||||
"Velero upgrade tests on cluster using the plugin provider for object storage and snapshots for volume backups",
|
||||
@@ -522,7 +520,7 @@ var _ = Describe(
|
||||
)
|
||||
var _ = Describe(
|
||||
"Velero test on skip backup of volume by resource policies",
|
||||
Label("ResourceFiltering", "ResourcePolicies", "Restic"),
|
||||
Label("ResourceFiltering", "ResourcePolicies", "FSBackup"),
|
||||
ResourcePoliciesTest,
|
||||
)
|
||||
|
||||
@@ -560,9 +558,9 @@ var _ = Describe(
|
||||
)
|
||||
|
||||
var _ = Describe(
|
||||
"Velero tests of Restic backup deletion",
|
||||
Label("Backups", "Deletion", "Restic"),
|
||||
BackupDeletionWithRestic,
|
||||
"Velero tests of file system backup deletion",
|
||||
Label("Backups", "Deletion", "FSBackup"),
|
||||
BackupDeletionWithFSBackup,
|
||||
)
|
||||
var _ = Describe(
|
||||
"Velero tests of snapshot backup deletion",
|
||||
@@ -570,7 +568,7 @@ var _ = Describe(
|
||||
BackupDeletionWithSnapshots,
|
||||
)
|
||||
var _ = Describe(
|
||||
"Local backups and Restic repos will be deleted once the corresponding backup storage location is deleted",
|
||||
"Local backups and backup repos will be deleted once the corresponding backup storage location is deleted",
|
||||
Label("Backups", "TTL", "LongTime", "Snapshot", "SkipVanillaZfs"),
|
||||
TTLTest,
|
||||
)
|
||||
@@ -608,9 +606,9 @@ var _ = Describe(
|
||||
BslDeletionWithSnapshots,
|
||||
)
|
||||
var _ = Describe(
|
||||
"Local backups and Restic repos will be deleted once the corresponding backup storage location is deleted",
|
||||
Label("BSL", "Deletion", "Restic", "AdditionalBSL"),
|
||||
BslDeletionWithRestic,
|
||||
"Local backups and backup repos will be deleted once the corresponding backup storage location is deleted",
|
||||
Label("BSL", "Deletion", "FSBackup", "AdditionalBSL"),
|
||||
BslDeletionWithFSBackup,
|
||||
)
|
||||
|
||||
var _ = Describe(
|
||||
@@ -626,13 +624,13 @@ var _ = Describe(
|
||||
|
||||
var _ = Describe(
|
||||
"Backup resources should follow the specific order in schedule",
|
||||
Label("NamespaceMapping", "Single", "Restic"),
|
||||
OneNamespaceMappingResticTest,
|
||||
Label("NamespaceMapping", "Single", "FSBackup"),
|
||||
OneNamespaceMappingFSBackupTest,
|
||||
)
|
||||
var _ = Describe(
|
||||
"Backup resources should follow the specific order in schedule",
|
||||
Label("NamespaceMapping", "Multiple", "Restic"),
|
||||
MultiNamespacesMappingResticTest,
|
||||
Label("NamespaceMapping", "Multiple", "FSBackup"),
|
||||
MultiNamespacesMappingFSBackupTest,
|
||||
)
|
||||
var _ = Describe(
|
||||
"Backup resources should follow the specific order in schedule",
|
||||
|
||||
@@ -179,10 +179,6 @@ func (t *TestCase) Start() error {
|
||||
Skip("Skip due to issue https://github.com/kubernetes/kubernetes/issues/114384 on AKS")
|
||||
}
|
||||
|
||||
if veleroCfg.UploaderType == UploaderTypeRestic &&
|
||||
strings.Contains(t.GetTestCase().CaseBaseName, "ParallelFiles") {
|
||||
Skip("Skip Parallel Files upload and download test cases for environments using Restic as uploader.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ func BackupUpgradeRestoreWithSnapshots() {
|
||||
}
|
||||
}
|
||||
|
||||
func BackupUpgradeRestoreWithRestic() {
|
||||
func BackupUpgradeRestoreWithFSBackup() {
|
||||
veleroCfg = VeleroCfg
|
||||
for _, upgradeFromVelero := range GetVersionList(veleroCfg.UpgradeFromVeleroCLI, veleroCfg.UpgradeFromVeleroVersion) {
|
||||
BackupUpgradeRestoreTest(false, upgradeFromVelero)
|
||||
@@ -108,8 +108,6 @@ func BackupUpgradeRestoreTest(useVolumeSnapshots bool, veleroCLI2Version VeleroC
|
||||
Expect(err).To(Succeed())
|
||||
oneHourTimeout, ctxCancel := context.WithTimeout(context.Background(), time.Minute*60)
|
||||
defer ctxCancel()
|
||||
supportUploaderType, err := IsSupportUploaderType(veleroCLI2Version.VeleroVersion)
|
||||
Expect(err).To(Succeed())
|
||||
if veleroCLI2Version.VeleroCLI == "" {
|
||||
//Assume tag of velero server image is identical to velero CLI version
|
||||
//Download velero CLI if it's empty according to velero CLI version
|
||||
@@ -182,8 +180,6 @@ func BackupUpgradeRestoreTest(useVolumeSnapshots bool, veleroCLI2Version VeleroC
|
||||
BackupCfg.UseVolumeSnapshots = useVolumeSnapshots
|
||||
BackupCfg.DefaultVolumesToFsBackup = !useVolumeSnapshots
|
||||
BackupCfg.Selector = ""
|
||||
//TODO: pay attention to this param, remove it when restic is not the default backup tool any more.
|
||||
BackupCfg.UseResticIfFSBackup = !supportUploaderType
|
||||
Expect(VeleroBackupNamespace(oneHourTimeout, tmpCfg.UpgradeFromVeleroCLI,
|
||||
tmpCfg.VeleroNamespace, BackupCfg)).To(Succeed(), func() string {
|
||||
RunDebug(context.Background(), tmpCfg.UpgradeFromVeleroCLI, tmpCfg.VeleroNamespace,
|
||||
@@ -247,17 +243,9 @@ func BackupUpgradeRestoreTest(useVolumeSnapshots bool, veleroCLI2Version VeleroC
|
||||
tmpCfg.GCFrequency = ""
|
||||
tmpCfg.UseNodeAgent = !useVolumeSnapshots
|
||||
Expect(err).To(Succeed())
|
||||
if supportUploaderType {
|
||||
Expect(VeleroInstall(context.Background(), &tmpCfg, false)).To(Succeed())
|
||||
Expect(CheckVeleroVersion(context.Background(), tmpCfg.VeleroCLI,
|
||||
tmpCfg.VeleroVersion)).To(Succeed())
|
||||
} else {
|
||||
// For upgrade from v1.9 or other version below v1.9
|
||||
tmpCfg.UploaderType = "restic"
|
||||
Expect(VeleroUpgrade(context.Background(), tmpCfg)).To(Succeed())
|
||||
Expect(CheckVeleroVersion(context.Background(), tmpCfg.VeleroCLI,
|
||||
tmpCfg.VeleroVersion)).To(Succeed())
|
||||
}
|
||||
Expect(VeleroInstall(context.Background(), &tmpCfg, false)).To(Succeed())
|
||||
Expect(CheckVeleroVersion(context.Background(), tmpCfg.VeleroCLI,
|
||||
tmpCfg.VeleroVersion)).To(Succeed())
|
||||
})
|
||||
|
||||
// Wait for 70s to make sure the backups are synced after Velero reinstall
|
||||
|
||||
+1
-5
@@ -44,10 +44,7 @@ const CSI = "csi"
|
||||
const Velero = "velero"
|
||||
const VeleroRestoreHelper = "velero-restore-helper"
|
||||
|
||||
const (
|
||||
UploaderTypeRestic = "restic"
|
||||
UploaderTypeKopia = "kopia"
|
||||
)
|
||||
const UploaderTypeKopia = "kopia"
|
||||
|
||||
const (
|
||||
KubeSystemNamespace = "kube-system"
|
||||
@@ -168,7 +165,6 @@ type BackupConfig struct {
|
||||
ExcludeResources string
|
||||
IncludeClusterResources bool
|
||||
OrderedResources string
|
||||
UseResticIfFSBackup bool
|
||||
DefaultVolumesToFsBackup bool
|
||||
SnapshotMoveData bool
|
||||
}
|
||||
|
||||
@@ -616,10 +616,8 @@ func createVeleroResources(ctx context.Context, cli, namespace string, args []st
|
||||
return errors.Wrapf(err, "failed to run velero install dry run command, stdout=%s, stderr=%s", stdout, stderr)
|
||||
}
|
||||
|
||||
// From v1.15, the Restic uploader is deprecated,
|
||||
// and a warning message is printed for the install CLI.
|
||||
// Need to skip the deprecation of Restic message before the generated JSON.
|
||||
// Redirect to the stdout to the first curly bracket to skip the warning.
|
||||
// The install CLI may print warning messages before the generated JSON.
|
||||
// Skip any text before the first curly bracket.
|
||||
if stdout[0] != '{' {
|
||||
newIndex := strings.Index(stdout, "{")
|
||||
stdout = stdout[newIndex:]
|
||||
@@ -730,7 +728,7 @@ func patchResources(resources *unstructured.UnstructuredList, namespace string,
|
||||
}
|
||||
}
|
||||
|
||||
// customize the restic restore helper image
|
||||
// customize the restore helper image
|
||||
if len(options.RestoreHelperImage) > 0 {
|
||||
restoreActionConfig := corev1api.ConfigMap{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
@@ -755,7 +753,7 @@ func patchResources(resources *unstructured.UnstructuredList, namespace string,
|
||||
return errors.Wrapf(err, "failed to convert restore action config to unstructure")
|
||||
}
|
||||
resources.Items = append(resources.Items, un)
|
||||
fmt.Printf("the restic restore helper image is set by the configmap %q \n", "fs-restore-action-config")
|
||||
fmt.Printf("the restore helper image is set by the configmap %q \n", "fs-restore-action-config")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -41,7 +41,6 @@ import (
|
||||
schedulingv1api "k8s.io/api/scheduling/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
ver "k8s.io/apimachinery/pkg/util/version"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
@@ -240,7 +239,7 @@ func getProviderVeleroInstallOptions(veleroCfg *VeleroConfig,
|
||||
}
|
||||
|
||||
io := cliinstall.NewInstallOptions()
|
||||
// always wait for velero and restic pods to be running.
|
||||
// always wait for velero and node-agent pods to be running.
|
||||
io.Wait = true
|
||||
io.ProviderName = veleroCfg.ObjectStoreProvider
|
||||
|
||||
@@ -471,11 +470,7 @@ func VeleroBackupNamespace(ctx context.Context, veleroCLI, veleroNamespace strin
|
||||
}
|
||||
}
|
||||
if backupCfg.DefaultVolumesToFsBackup {
|
||||
if backupCfg.UseResticIfFSBackup {
|
||||
args = append(args, "--default-volumes-to-restic")
|
||||
} else {
|
||||
args = append(args, "--default-volumes-to-fs-backup")
|
||||
}
|
||||
args = append(args, "--default-volumes-to-fs-backup")
|
||||
|
||||
// To workaround https://github.com/vmware-tanzu/velero-plugin-for-vsphere/issues/347 for vsphere plugin v1.1.1
|
||||
// if the "--snapshot-volumes=false" isn't specified explicitly, the vSphere plugin will always take snapshots
|
||||
@@ -484,20 +479,11 @@ func VeleroBackupNamespace(ctx context.Context, veleroCLI, veleroNamespace strin
|
||||
if backupCfg.ProvideSnapshotsVolumeParam && !backupCfg.UseVolumeSnapshots {
|
||||
args = append(args, "--snapshot-volumes=false")
|
||||
} // if "--snapshot-volumes" is not provide, snapshot should be taken as default behavior.
|
||||
} else { // DefaultVolumesToFsBackup is false
|
||||
} else if backupCfg.UseVolumeSnapshots {
|
||||
// Although DefaultVolumesToFsBackup is false, but probably DefaultVolumesToFsBackup
|
||||
// was set to true in installation CLI in snapshot volume test, so set DefaultVolumesToFsBackup
|
||||
// to false specifically to make sure volume snapshot was taken
|
||||
if backupCfg.UseVolumeSnapshots {
|
||||
if backupCfg.UseResticIfFSBackup {
|
||||
args = append(args, "--default-volumes-to-restic=false")
|
||||
} else {
|
||||
args = append(args, "--default-volumes-to-fs-backup=false")
|
||||
}
|
||||
}
|
||||
// Although DefaultVolumesToFsBackup is false, but probably DefaultVolumesToFsBackup
|
||||
// was set to true in installation CLI in FS volume backup test, so do nothing here, no DefaultVolumesToFsBackup
|
||||
// appear in backup CLI
|
||||
args = append(args, "--default-volumes-to-fs-backup=false")
|
||||
}
|
||||
if backupCfg.BackupLocation != "" {
|
||||
args = append(args, "--storage-location", backupCfg.BackupLocation)
|
||||
@@ -1282,14 +1268,14 @@ func SnapshotCRsCountShouldBe(ctx context.Context, namespace, backupName string,
|
||||
}
|
||||
|
||||
func BackupRepositoriesCountShouldBe(ctx context.Context, veleroNamespace, targetNamespace string, expectedCount int) error {
|
||||
resticArr, err := GetRepositories(ctx, veleroNamespace, targetNamespace)
|
||||
repos, err := GetRepositories(ctx, veleroNamespace, targetNamespace)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Fail to get BackupRepositories")
|
||||
}
|
||||
if len(resticArr) == expectedCount {
|
||||
if len(repos) == expectedCount {
|
||||
return nil
|
||||
} else {
|
||||
return errors.New(fmt.Sprintf("BackupRepositories count %d in namespace %s is not as expected %d", len(resticArr), targetNamespace, expectedCount))
|
||||
return errors.New(fmt.Sprintf("BackupRepositories count %d in namespace %s is not as expected %d", len(repos), targetNamespace, expectedCount))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1429,36 +1415,6 @@ func GetSchedule(ctx context.Context, veleroNamespace, scheduleName string) (str
|
||||
return stdout, err
|
||||
}
|
||||
|
||||
func VeleroUpgrade(ctx context.Context, veleroCfg VeleroConfig) error {
|
||||
crd, err := ApplyCRDs(ctx, veleroCfg.VeleroCLI)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Fail to Apply CRDs")
|
||||
}
|
||||
fmt.Println(crd)
|
||||
deploy, err := UpdateVeleroDeployment(ctx, veleroCfg)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Fail to update Velero deployment")
|
||||
}
|
||||
fmt.Println(deploy)
|
||||
if veleroCfg.UseNodeAgent {
|
||||
dsjson, err := KubectlGetDsJson(veleroCfg.VeleroNamespace)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Fail to update Velero deployment")
|
||||
}
|
||||
|
||||
err = DeleteVeleroDs(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Fail to delete Velero ds")
|
||||
}
|
||||
update, err := UpdateNodeAgent(ctx, veleroCfg, dsjson)
|
||||
fmt.Println(update)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Fail to update node agent")
|
||||
}
|
||||
}
|
||||
return waitVeleroReady(ctx, veleroCfg.VeleroNamespace, veleroCfg.UseNodeAgent, veleroCfg.UseNodeAgentWindows)
|
||||
}
|
||||
|
||||
func ApplyCRDs(ctx context.Context, veleroCLI string) ([]string, error) {
|
||||
cmds := []*common.OsCommandLine{}
|
||||
|
||||
@@ -1476,78 +1432,6 @@ func ApplyCRDs(ctx context.Context, veleroCLI string) ([]string, error) {
|
||||
return common.GetListByCmdPipes(ctx, cmds)
|
||||
}
|
||||
|
||||
func UpdateVeleroDeployment(ctx context.Context, veleroCfg VeleroConfig) ([]string, error) {
|
||||
cmds := []*common.OsCommandLine{}
|
||||
|
||||
cmd := &common.OsCommandLine{
|
||||
Cmd: "kubectl",
|
||||
Args: []string{"get", "deploy", "-n", veleroCfg.VeleroNamespace, "-ojson"},
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
cmd = &common.OsCommandLine{
|
||||
Cmd: "sed",
|
||||
Args: []string{fmt.Sprintf("s#\\\"server\\\",#\\\"server\\\",\\\"--uploader-type=%s\\\",#g", veleroCfg.UploaderType)},
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
cmd = &common.OsCommandLine{
|
||||
Cmd: "sed",
|
||||
Args: []string{"s#default-volumes-to-restic#default-volumes-to-fs-backup#g"},
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
cmd = &common.OsCommandLine{
|
||||
Cmd: "sed",
|
||||
Args: []string{"s#default-restic-prune-frequency#default-repo-maintain-frequency#g"},
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
cmd = &common.OsCommandLine{
|
||||
Cmd: "sed",
|
||||
Args: []string{"s#restic-timeout#fs-backup-timeout#g"},
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
cmd = &common.OsCommandLine{
|
||||
Cmd: "kubectl",
|
||||
Args: []string{"apply", "-f", "-"},
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return common.GetListByCmdPipes(ctx, cmds)
|
||||
}
|
||||
|
||||
func UpdateNodeAgent(ctx context.Context, veleroCfg VeleroConfig, dsjson string) ([]string, error) {
|
||||
cmds := []*common.OsCommandLine{}
|
||||
|
||||
cmd := &common.OsCommandLine{
|
||||
Cmd: "echo",
|
||||
Args: []string{dsjson},
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
cmd = &common.OsCommandLine{
|
||||
Cmd: "sed",
|
||||
Args: []string{"s#\\\"name\\\"\\: \\\"restic\\\"#\\\"name\\\"\\: \\\"node-agent\\\"#g"},
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
cmd = &common.OsCommandLine{
|
||||
Cmd: "sed",
|
||||
Args: []string{"s#\\\"restic\\\",#\\\"node-agent\\\",#g"},
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
cmd = &common.OsCommandLine{
|
||||
Cmd: "kubectl",
|
||||
Args: []string{"create", "-f", "-"},
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return common.GetListByCmdPipes(ctx, cmds)
|
||||
}
|
||||
|
||||
func ListVeleroPods(ctx context.Context, veleroNamespace string) ([]string, error) {
|
||||
cmds := []*common.OsCommandLine{}
|
||||
cmd := &common.OsCommandLine{
|
||||
@@ -1591,22 +1475,6 @@ func RestorePVRNum(ctx context.Context, veleroNamespace, restoreName string) (in
|
||||
return len(outputList), err
|
||||
}
|
||||
|
||||
func IsSupportUploaderType(version string) (bool, error) {
|
||||
verSupportUploaderType, err := ver.ParseSemantic("v1.10.0")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
v, err := ver.ParseSemantic(version)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if v.AtLeast(verSupportUploaderType) {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetVeleroPodName(ctx context.Context) ([]string, error) {
|
||||
// Example:
|
||||
// NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
|
||||
|
||||
Reference in New Issue
Block a user