mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-08-16 04:06:06 +00:00
Merge branch 'main' into pvr-restorer-could-run-concurrently
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
module github.com/vmware-tanzu/velero/pkg/apis
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
k8s.io/api v0.36.0
|
||||
k8s.io/apimachinery v0.36.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
k8s.io/klog/v2 v2.140.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80=
|
||||
k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34=
|
||||
k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
|
||||
k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc=
|
||||
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
Copyright The Velero Contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package shared
|
||||
|
||||
const (
|
||||
DataUploadParentSnapshotNone = "none"
|
||||
DataUploadParentSnapshotAuto = "auto"
|
||||
)
|
||||
@@ -118,7 +118,7 @@ type BackupRepositoryMaintenanceStatus struct {
|
||||
// +kubebuilder:storageversion
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
|
||||
// +kubebuilder:printcolumn:name="Repository Type",type="string",JSONPath=".spec.repositoryType"
|
||||
//
|
||||
// +kubebuilder:resource:shortName=repo
|
||||
|
||||
type BackupRepository struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
@@ -23,6 +23,9 @@ import (
|
||||
|
||||
type Metadata struct {
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
// +optional
|
||||
// +nullable
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
// BackupSpec defines the specification for a Velero backup.
|
||||
@@ -176,7 +179,7 @@ type BackupSpec struct {
|
||||
SnapshotMoveData *bool `json:"snapshotMoveData,omitempty"`
|
||||
|
||||
// DataMover specifies the data mover to be used by the backup.
|
||||
// If DataMover is "" or "velero", the built-in data mover will be used.
|
||||
// If DataMover is "" or "velero", the default built-in data mover will be used.
|
||||
// +optional
|
||||
DataMover string `json:"datamover,omitempty"`
|
||||
|
||||
@@ -184,6 +187,10 @@ type BackupSpec struct {
|
||||
// +optional
|
||||
// +nullable
|
||||
UploaderConfig *UploaderConfigForBackup `json:"uploaderConfig,omitempty"`
|
||||
|
||||
// BackupType specifies how volume data is backed up, with possible values including Full and Incremental.
|
||||
// +optional
|
||||
BackupType BackupType `json:"backupType,omitempty"`
|
||||
}
|
||||
|
||||
// UploaderConfigForBackup defines the configuration for the uploader when doing backup.
|
||||
@@ -357,6 +364,15 @@ const (
|
||||
BackupPhaseDeleting BackupPhase = "Deleting"
|
||||
)
|
||||
|
||||
// BackupType specifies how volume data is backed up, with possible values including Full and Incremental.
|
||||
// +kubebuilder:validation:Enum=Full;Incremental
|
||||
type BackupType string
|
||||
|
||||
const (
|
||||
BackupTypeFull BackupType = "Full"
|
||||
BackupTypeIncremental BackupType = "Incremental"
|
||||
)
|
||||
|
||||
// BackupStatus captures the current status of a Velero backup.
|
||||
type BackupStatus struct {
|
||||
// Version is the backup format major version.
|
||||
@@ -503,6 +519,12 @@ type HookStatus struct {
|
||||
// +kubebuilder:storageversion
|
||||
// +kubebuilder:rbac:groups=velero.io,resources=backups,verbs=create;delete;get;list;patch;update;watch
|
||||
// +kubebuilder:rbac:groups=velero.io,resources=backups/status,verbs=get;update;patch
|
||||
// +kubebuilder:resource:shortName=bak
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase",description="Backup status such as New/InProgress"
|
||||
// +kubebuilder:printcolumn:name="Errors",type="integer",JSONPath=".status.errors",description="Total number of errors logged during the backup"
|
||||
// +kubebuilder:printcolumn:name="Warnings",type="integer",JSONPath=".status.warnings",description="Total number of warnings logged during the backup"
|
||||
// +kubebuilder:printcolumn:name="Started",type="date",JSONPath=".status.startTimestamp",description="The time the backup was started"
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
|
||||
|
||||
// Backup is a Velero resource that represents the capture of Kubernetes
|
||||
// cluster state at a point in time (API objects and associated volume state).
|
||||
|
||||
@@ -58,6 +58,7 @@ type DeleteBackupRequestStatus struct {
|
||||
// +kubebuilder:storageversion
|
||||
// +kubebuilder:printcolumn:name="BackupName",type="string",JSONPath=".spec.backupName",description="The name of the backup to be deleted"
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase",description="The status of the deletion request"
|
||||
// +kubebuilder:resource:shortName=dbr
|
||||
|
||||
// DeleteBackupRequest is a request to delete one or more backups.
|
||||
type DeleteBackupRequest struct {
|
||||
|
||||
@@ -92,6 +92,7 @@ type DownloadRequestStatus struct {
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:object:generate=true
|
||||
// +kubebuilder:storageversion
|
||||
// +kubebuilder:resource:shortName=dreq
|
||||
|
||||
// DownloadRequest is a request to download an artifact from backup object storage, such as a backup
|
||||
// log file.
|
||||
|
||||
@@ -166,6 +166,14 @@ const (
|
||||
|
||||
// Velero checks this annotation to determine whether to skip resource excluding check.
|
||||
MustIncludeAdditionalItemAnnotation = "backup.velero.io/must-include-additional-items"
|
||||
// MustIncludeAdditionalItemRestoreAnnotation is set by RestoreItemActions on the UpdatedItem
|
||||
// to tell Velero to bypass global resource/namespace exclusion checks (and IncludeClusterResources=false)
|
||||
// for that action's AdditionalItems. Value must be "true" to enable the bypass. The annotation is
|
||||
// always stripped before the item is applied to the cluster when present, including non-"true" values.
|
||||
//
|
||||
// Notice: SkipRestore on the Execute output takes precedence. If SkipRestore is true, the
|
||||
// annotation is never inspected and AdditionalItems are not processed.
|
||||
MustIncludeAdditionalItemRestoreAnnotation = "restore.velero.io/must-include-additional-items"
|
||||
// SkippedNoCSIPVAnnotation - Velero checks this annotation on processed PVC to
|
||||
// find out if the snapshot was skipped b/c the PV is not provisioned via CSI
|
||||
SkippedNoCSIPVAnnotation = "backup.velero.io/skipped-no-csi-pv"
|
||||
|
||||
@@ -145,6 +145,7 @@ type PodVolumeBackupStatus struct {
|
||||
// +kubebuilder:printcolumn:name="Uploader",type="string",JSONPath=".spec.uploaderType",description="The type of the uploader to handle data transfer"
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:object:generate=true
|
||||
// +kubebuilder:resource:shortName=pvb
|
||||
|
||||
type PodVolumeBackup struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
@@ -133,6 +133,7 @@ type PodVolumeRestoreStatus struct {
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since this PodVolumeRestore was created"
|
||||
// +kubebuilder:printcolumn:name="Node",type="string",JSONPath=".status.node",description="Name of the node where the PodVolumeRestore is processed"
|
||||
// +kubebuilder:printcolumn:name="Uploader Type",type="string",JSONPath=".spec.uploaderType",description="The type of the uploader to handle data transfer"
|
||||
// +kubebuilder:resource:shortName=pvr
|
||||
|
||||
type PodVolumeRestore struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
@@ -135,6 +135,14 @@ type RestoreSpec struct {
|
||||
// +nullable
|
||||
ResourcePolicy *corev1api.TypedLocalObjectReference `json:"resourcePolicy,omitempty"`
|
||||
|
||||
// SkipDefaultResourceModifier controls whether the server-configured default
|
||||
// resource modifier is applied to this restore.
|
||||
// When true, the default modifier is skipped even if configured on the server.
|
||||
// Has no effect when a per-restore ResourceModifier is specified.
|
||||
// +optional
|
||||
// +nullable
|
||||
SkipDefaultResourceModifier *bool `json:"skipDefaultResourceModifier,omitempty"`
|
||||
|
||||
// UploaderConfig specifies the configuration for the restore.
|
||||
// +optional
|
||||
// +nullable
|
||||
@@ -411,6 +419,12 @@ type RestoreProgress struct {
|
||||
// +kubebuilder:storageversion
|
||||
// +kubebuilder:rbac:groups=velero.io,resources=restores,verbs=create;delete;get;list;patch;update;watch
|
||||
// +kubebuilder:rbac:groups=velero.io,resources=restores/status,verbs=get;update;patch
|
||||
// +kubebuilder:resource:shortName=rst
|
||||
// +kubebuilder:printcolumn:name="Backup",type="string",JSONPath=".spec.backupName",description="The name of the backup this restore is from"
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase",description="Restore status such as New/InProgress"
|
||||
// +kubebuilder:printcolumn:name="Errors",type="integer",JSONPath=".status.errors",description="Total number of errors logged during the restore"
|
||||
// +kubebuilder:printcolumn:name="Warnings",type="integer",JSONPath=".status.warnings",description="Total number of warnings logged during the restore"
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
|
||||
|
||||
// Restore is a Velero resource that represents the application of
|
||||
// resources from a Velero backup to a target Kubernetes cluster.
|
||||
|
||||
@@ -104,6 +104,7 @@ type ScheduleStatus struct {
|
||||
// +kubebuilder:printcolumn:name="LastBackup",type="date",JSONPath=".status.lastBackup",description="The last time a Backup was run for this schedule"
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
|
||||
// +kubebuilder:printcolumn:name="Paused",type="boolean",JSONPath=".spec.paused"
|
||||
// +kubebuilder:resource:shortName=sched
|
||||
|
||||
// Schedule is a Velero resource that represents a pre-scheduled or
|
||||
// periodic Backup that should be run.
|
||||
|
||||
@@ -27,6 +27,9 @@ import (
|
||||
// +kubebuilder:resource:shortName=vsl
|
||||
// +kubebuilder:object:generate=true
|
||||
// +kubebuilder:storageversion
|
||||
// +kubebuilder:printcolumn:name="Provider",type="string",JSONPath=".spec.provider",description="Provider is the provider of the volume storage"
|
||||
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="Volume Snapshot Location status such as Available/Unavailable"
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
|
||||
|
||||
// VolumeSnapshotLocation is a location where Velero stores volume snapshots.
|
||||
type VolumeSnapshotLocation struct {
|
||||
|
||||
@@ -895,6 +895,13 @@ func (in *Metadata) DeepCopyInto(out *Metadata) {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Annotations != nil {
|
||||
in, out := &in.Annotations, &out.Annotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metadata.
|
||||
@@ -1420,6 +1427,11 @@ func (in *RestoreSpec) DeepCopyInto(out *RestoreSpec) {
|
||||
*out = new(corev1.TypedLocalObjectReference)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.SkipDefaultResourceModifier != nil {
|
||||
in, out := &in.SkipDefaultResourceModifier, &out.SkipDefaultResourceModifier
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.UploaderConfig != nil {
|
||||
in, out := &in.UploaderConfig, &out.UploaderConfig
|
||||
*out = new(UploaderConfigForRestore)
|
||||
|
||||
@@ -32,7 +32,7 @@ type DataDownloadSpec struct {
|
||||
BackupStorageLocation string `json:"backupStorageLocation"`
|
||||
|
||||
// DataMover specifies the data mover to be used by the backup.
|
||||
// If DataMover is "" or "velero", the built-in data mover will be used.
|
||||
// If DataMover is "" or "velero", the built-in fs data mover will be used.
|
||||
// +optional
|
||||
DataMover string `json:"datamover,omitempty"`
|
||||
|
||||
@@ -152,6 +152,7 @@ type DataDownloadStatus struct {
|
||||
// +kubebuilder:printcolumn:name="Storage Location",type="string",JSONPath=".spec.backupStorageLocation",description="Name of the Backup Storage Location where the backup data is stored"
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since this DataDownload was created"
|
||||
// +kubebuilder:printcolumn:name="Node",type="string",JSONPath=".status.node",description="Name of the node where the DataDownload is processed"
|
||||
// +kubebuilder:resource:shortName=dd
|
||||
|
||||
// DataDownload acts as the protocol between data mover plugins and data mover controller for the datamover restore operation
|
||||
type DataDownload struct {
|
||||
|
||||
@@ -36,7 +36,7 @@ type DataUploadSpec struct {
|
||||
SourcePVC string `json:"sourcePVC"`
|
||||
|
||||
// DataMover specifies the data mover to be used by the backup.
|
||||
// If DataMover is "" or "velero", the built-in data mover will be used.
|
||||
// If DataMover is "" or "velero", the built-in fs data mover will be used.
|
||||
// +optional
|
||||
DataMover string `json:"datamover,omitempty"`
|
||||
|
||||
@@ -64,6 +64,12 @@ type DataUploadSpec struct {
|
||||
// SourceFSType is the file system type of the source volume.
|
||||
// +optional
|
||||
SourceFSType string `json:"sourceFSType,omitempty"`
|
||||
|
||||
// ParentSnapshot specifies the parent snapshot that current backup is based on.
|
||||
// If its value is "" or "auto", the data mover finds the recent backup of the same volume as parent.
|
||||
// If its value is "none", the data mover will do a full backup
|
||||
// If its value is a specific snapshotID, the data mover finds the specific snapshot as parent.
|
||||
ParentSnapshot string `json:"parentSnapshot,omitempty"`
|
||||
}
|
||||
|
||||
type SnapshotType string
|
||||
@@ -197,6 +203,7 @@ type DataUploadStatus struct {
|
||||
// +kubebuilder:printcolumn:name="Storage Location",type="string",JSONPath=".spec.backupStorageLocation",description="Name of the Backup Storage Location where this backup should be stored"
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since this DataUpload was created"
|
||||
// +kubebuilder:printcolumn:name="Node",type="string",JSONPath=".status.node",description="Name of the node where the DataUpload is processed"
|
||||
// +kubebuilder:resource:shortName=du
|
||||
|
||||
// DataUpload acts as the protocol between data mover plugins and data mover controller for the datamover backup operation
|
||||
type DataUpload struct {
|
||||
@@ -261,4 +268,8 @@ type DataUploadResult struct {
|
||||
// FSType is the file system type of the volume.
|
||||
// +optional
|
||||
FSType string `json:"fsType,omitempty"`
|
||||
|
||||
// SnapshotClass is the name of the snapshot class that the volume snapshot is created with
|
||||
// +optional
|
||||
SnapshotClass string `json:"snapshotClass,omitempty"`
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ package archive
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
@@ -27,13 +29,37 @@ import (
|
||||
)
|
||||
|
||||
// GetItemFilePath returns an item's file path once extracted from a Velero backup archive.
|
||||
func GetItemFilePath(rootDir, groupResource, namespace, name string) string {
|
||||
func GetItemFilePath(rootDir, groupResource, namespace, name string) (string, error) {
|
||||
return GetVersionedItemFilePath(rootDir, groupResource, namespace, name, "")
|
||||
}
|
||||
|
||||
// GetVersionedItemFilePath returns an item's file path once extracted from a Velero backup archive, with version included.
|
||||
func GetVersionedItemFilePath(rootDir, groupResource, namespace, name, versionPath string) string {
|
||||
return filepath.Join(rootDir, velerov1api.ResourcesDir, groupResource, versionPath, GetScopeDir(namespace), namespace, name+".json")
|
||||
//
|
||||
// The namespace and name components can originate from backup contents - for example the
|
||||
// additional items a RestoreItemAction returns are built from annotations on a backed up
|
||||
// object - so the joined path is verified to stay within rootDir. Without that check a
|
||||
// component containing ".." escapes the extracted backup directory and addresses an
|
||||
// arbitrary file on the Velero pod's filesystem.
|
||||
func GetVersionedItemFilePath(rootDir, groupResource, namespace, name, versionPath string) (string, error) {
|
||||
path := filepath.Join(rootDir, velerov1api.ResourcesDir, groupResource, versionPath, GetScopeDir(namespace), namespace, name+".json")
|
||||
|
||||
// rootDir is empty when building the path of an entry inside the backup tarball rather
|
||||
// than of an extracted file on disk; "." is the containment base for that relative form.
|
||||
base := rootDir
|
||||
if base == "" {
|
||||
base = "."
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(base, path)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "error resolving item path for %q/%q", namespace, name)
|
||||
}
|
||||
|
||||
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||
return "", errors.Errorf("invalid item path for %q/%q: escapes the backup directory", namespace, name)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// GetScopeDir returns NamespaceScopedDir if namespace is present, or ClusterScopedDir if empty
|
||||
|
||||
@@ -27,31 +27,104 @@ import (
|
||||
)
|
||||
|
||||
func TestGetItemFilePath(t *testing.T) {
|
||||
res := GetItemFilePath("root", "resource", "", "item")
|
||||
res, err := GetItemFilePath("root", "resource", "", "item")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "root/resources/resource/cluster/item.json", res)
|
||||
|
||||
res = GetItemFilePath("root", "resource", "namespace", "item")
|
||||
res, err = GetItemFilePath("root", "resource", "namespace", "item")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "root/resources/resource/namespaces/namespace/item.json", res)
|
||||
|
||||
res = GetItemFilePath("", "resource", "", "item")
|
||||
res, err = GetItemFilePath("", "resource", "", "item")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "resources/resource/cluster/item.json", res)
|
||||
|
||||
res = GetVersionedItemFilePath("root", "resource", "", "item", "")
|
||||
res, err = GetVersionedItemFilePath("root", "resource", "", "item", "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "root/resources/resource/cluster/item.json", res)
|
||||
|
||||
res = GetVersionedItemFilePath("root", "resource", "namespace", "item", "")
|
||||
res, err = GetVersionedItemFilePath("root", "resource", "namespace", "item", "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "root/resources/resource/namespaces/namespace/item.json", res)
|
||||
|
||||
res = GetVersionedItemFilePath("root", "resource", "namespace", "item", "v1")
|
||||
res, err = GetVersionedItemFilePath("root", "resource", "namespace", "item", "v1")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "root/resources/resource/v1/namespaces/namespace/item.json", res)
|
||||
|
||||
res = GetVersionedItemFilePath("root", "resource", "", "item", "v1")
|
||||
res, err = GetVersionedItemFilePath("root", "resource", "", "item", "v1")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "root/resources/resource/v1/cluster/item.json", res)
|
||||
|
||||
res = GetVersionedItemFilePath("", "resource", "", "item", "")
|
||||
res, err = GetVersionedItemFilePath("", "resource", "", "item", "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "resources/resource/cluster/item.json", res)
|
||||
}
|
||||
|
||||
// TestGetItemFilePathRejectsPathTraversal verifies that a name or namespace containing
|
||||
// ".." cannot address a file outside the extracted backup directory. These components can
|
||||
// come from backup contents, for example the additional items a RestoreItemAction builds
|
||||
// from annotations on a backed up object.
|
||||
func TestGetItemFilePathRejectsPathTraversal(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rootDir string
|
||||
groupResource string
|
||||
namespace string
|
||||
itemName string
|
||||
}{
|
||||
{
|
||||
name: "traversal in name escapes root",
|
||||
rootDir: "/tmp/restore-dir",
|
||||
groupResource: "secrets",
|
||||
namespace: "x",
|
||||
itemName: "../../../../../../root/.docker/config",
|
||||
},
|
||||
{
|
||||
name: "traversal in namespace escapes root",
|
||||
rootDir: "/tmp/restore-dir",
|
||||
groupResource: "secrets",
|
||||
namespace: "../../../../../../etc",
|
||||
itemName: "passwd",
|
||||
},
|
||||
{
|
||||
name: "traversal in group resource escapes root",
|
||||
rootDir: "/tmp/restore-dir",
|
||||
groupResource: "../../../../../../etc",
|
||||
namespace: "",
|
||||
itemName: "passwd",
|
||||
},
|
||||
{
|
||||
name: "traversal escapes archive-relative root",
|
||||
rootDir: "",
|
||||
groupResource: "secrets",
|
||||
namespace: "x",
|
||||
itemName: "../../../../../../escape",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res, err := GetItemFilePath(tc.rootDir, tc.groupResource, tc.namespace, tc.itemName)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "escapes the backup directory")
|
||||
assert.Empty(t, res)
|
||||
|
||||
res, err = GetVersionedItemFilePath(tc.rootDir, tc.groupResource, tc.namespace, tc.itemName, "v1")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "escapes the backup directory")
|
||||
assert.Empty(t, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetItemFilePathAllowsInnerDotDot verifies the containment check does not reject a
|
||||
// path whose ".." segments resolve back inside the root directory.
|
||||
func TestGetItemFilePathAllowsInnerDotDot(t *testing.T) {
|
||||
res, err := GetItemFilePath("root", "resource", "namespaces/..", "item")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "root/resources/resource/namespaces/item.json", res)
|
||||
}
|
||||
|
||||
func TestGetScopeDir(t *testing.T) {
|
||||
res := GetScopeDir("")
|
||||
assert.Equal(t, velerov1api.ClusterScopedDir, res)
|
||||
|
||||
@@ -22,8 +22,6 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"k8s.io/client-go/util/retry"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2"
|
||||
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
|
||||
@@ -31,6 +29,7 @@ import (
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
storagev1api "k8s.io/api/storage/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
@@ -39,16 +38,17 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||
"k8s.io/client-go/util/retry"
|
||||
crclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
|
||||
veleroshared "github.com/vmware-tanzu/velero/pkg/apis/velero/shared"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1"
|
||||
veleroclient "github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/kuberesource"
|
||||
"github.com/vmware-tanzu/velero/pkg/label"
|
||||
"github.com/vmware-tanzu/velero/pkg/nodeagent"
|
||||
plugincommon "github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/utils/volumehelper"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
@@ -56,6 +56,7 @@ import (
|
||||
uploaderUtil "github.com/vmware-tanzu/velero/pkg/uploader/util"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/csi"
|
||||
datamover "github.com/vmware-tanzu/velero/pkg/util/datamover"
|
||||
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
podvolumeutil "github.com/vmware-tanzu/velero/pkg/util/podvolume"
|
||||
vhutil "github.com/vmware-tanzu/velero/pkg/util/volumehelper"
|
||||
@@ -160,7 +161,7 @@ func (p *pvcBackupItemAction) getOrCreateVolumeHelper(backup *velerov1api.Backup
|
||||
return p.getVolumeHelperWithCache(backup)
|
||||
}
|
||||
|
||||
func (p *pvcBackupItemAction) validatePVCandPV(
|
||||
func (p *pvcBackupItemAction) validatePVCAndPV(
|
||||
pvc corev1api.PersistentVolumeClaim,
|
||||
item runtime.Unstructured,
|
||||
) (
|
||||
@@ -213,6 +214,7 @@ func (p *pvcBackupItemAction) validatePVCandPV(
|
||||
func (p *pvcBackupItemAction) createVolumeSnapshot(
|
||||
pvc corev1api.PersistentVolumeClaim,
|
||||
backup *velerov1api.Backup,
|
||||
policySnapshotClass string,
|
||||
) (
|
||||
vs *snapshotv1api.VolumeSnapshot,
|
||||
err error,
|
||||
@@ -233,6 +235,7 @@ func (p *pvcBackupItemAction) createVolumeSnapshot(
|
||||
&pvc,
|
||||
p.log,
|
||||
p.crClient,
|
||||
policySnapshotClass,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(
|
||||
@@ -304,7 +307,7 @@ func (p *pvcBackupItemAction) Execute(
|
||||
return nil, nil, "", nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
valid, item, fsType, err := p.validatePVCandPV(
|
||||
valid, item, fsType, err := p.validatePVCAndPV(
|
||||
pvc,
|
||||
item,
|
||||
)
|
||||
@@ -339,7 +342,25 @@ func (p *pvcBackupItemAction) Execute(
|
||||
return nil, nil, "", nil, err
|
||||
}
|
||||
|
||||
vs, err := p.getVolumeSnapshotReference(context.TODO(), pvc, backup)
|
||||
// validate that the node-agent daemonset is ready when snapshot data movement with
|
||||
// the built-in data mover is requested. Without this, the DataUpload CR will be
|
||||
// created but never processed (the DataUpload controller runs inside node-agent),
|
||||
// causing the backup to hang until itemOperationTimeout expires.
|
||||
if boolptr.IsSetToTrue(backup.Spec.SnapshotMoveData) && datamover.IsBuiltInDataMover(backup.Spec.DataMover) {
|
||||
if err := nodeagent.IsReady(context.TODO(), backup.Namespace, p.crClient, p.log); err != nil {
|
||||
p.log.WithError(err).Error("cannot perform snapshot data movement without running node-agent pods")
|
||||
return nil, nil, "", nil, errors.Wrap(err, "CSI PVC BIA cannot proceed: node-agent is not ready for snapshot data movement")
|
||||
}
|
||||
}
|
||||
|
||||
policySnapshotClass, scErr := vh.GetSnapshotClass(item, kuberesource.PersistentVolumeClaims)
|
||||
if scErr != nil {
|
||||
p.log.WithError(scErr).Warn("failed to get snapshotClass from volume policy, proceeding without it")
|
||||
} else if policySnapshotClass != "" {
|
||||
p.log.Infof("Volume policy specifies snapshotClass=%s for PVC %s/%s", policySnapshotClass, pvc.Namespace, pvc.Name)
|
||||
}
|
||||
|
||||
vs, err := p.getVolumeSnapshotReference(context.TODO(), pvc, backup, policySnapshotClass)
|
||||
if err != nil {
|
||||
return nil, nil, "", nil, err
|
||||
}
|
||||
@@ -537,6 +558,12 @@ func newDataUpload(
|
||||
vsc *snapshotv1api.VolumeSnapshotContent,
|
||||
fsType string,
|
||||
) *velerov2alpha1.DataUpload {
|
||||
parentSnapshot := ""
|
||||
|
||||
if backup.Spec.BackupType == velerov1api.BackupTypeFull {
|
||||
parentSnapshot = veleroshared.DataUploadParentSnapshotNone
|
||||
}
|
||||
|
||||
dataUpload := &velerov2alpha1.DataUpload{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: velerov2alpha1.SchemeGroupVersion.String(),
|
||||
@@ -574,6 +601,7 @@ func newDataUpload(
|
||||
SourceNamespace: pvc.Namespace,
|
||||
OperationTimeout: backup.Spec.CSISnapshotTimeout,
|
||||
SourceFSType: fsType,
|
||||
ParentSnapshot: parentSnapshot,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -672,6 +700,7 @@ func (p *pvcBackupItemAction) getVolumeSnapshotReference(
|
||||
ctx context.Context,
|
||||
pvc corev1api.PersistentVolumeClaim,
|
||||
backup *velerov1api.Backup,
|
||||
policySnapshotClass string,
|
||||
) (*snapshotv1api.VolumeSnapshot, error) {
|
||||
vgsLabelKey := backup.Spec.VolumeGroupSnapshotLabelKey
|
||||
group, hasLabel := pvc.Labels[vgsLabelKey]
|
||||
@@ -802,7 +831,7 @@ func (p *pvcBackupItemAction) getVolumeSnapshotReference(
|
||||
}
|
||||
|
||||
// Legacy fallback: create individual VS
|
||||
return p.createVolumeSnapshot(pvc, backup)
|
||||
return p.createVolumeSnapshot(pvc, backup, policySnapshotClass)
|
||||
}
|
||||
|
||||
func (p *pvcBackupItemAction) findExistingVSForBackup(
|
||||
|
||||
@@ -23,31 +23,26 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/kuberesource"
|
||||
|
||||
volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/label"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2"
|
||||
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
appsv1api "k8s.io/api/apps/v1"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
storagev1api "k8s.io/api/storage/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/utils/ptr"
|
||||
crclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/apis/velero/shared"
|
||||
@@ -55,8 +50,12 @@ import (
|
||||
velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1"
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
factorymocks "github.com/vmware-tanzu/velero/pkg/client/mocks"
|
||||
"github.com/vmware-tanzu/velero/pkg/kuberesource"
|
||||
"github.com/vmware-tanzu/velero/pkg/label"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
uploaderUtil "github.com/vmware-tanzu/velero/pkg/uploader/util"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
)
|
||||
|
||||
const testDriver = "csi.example.com"
|
||||
@@ -81,21 +80,23 @@ func (c *errorInjectingClient) Create(ctx context.Context, obj crclient.Object,
|
||||
func TestExecute(t *testing.T) {
|
||||
boolTrue := true
|
||||
tests := []struct {
|
||||
name string
|
||||
backup *velerov1api.Backup
|
||||
pvc *corev1api.PersistentVolumeClaim
|
||||
pv *corev1api.PersistentVolume
|
||||
sc *storagev1api.StorageClass
|
||||
vsClass *snapshotv1api.VolumeSnapshotClass
|
||||
operationID string
|
||||
expectedErr error
|
||||
expectErr bool // Use bool for cases where we just need to check for any error
|
||||
expectedBackup *velerov1api.Backup
|
||||
expectedDataUpload *velerov2alpha1.DataUpload
|
||||
expectedPVC *corev1api.PersistentVolumeClaim
|
||||
resourcePolicy *corev1api.ConfigMap
|
||||
failVSCreate bool
|
||||
skipVSReadyUpdate bool // New flag to control VS readiness
|
||||
name string
|
||||
backup *velerov1api.Backup
|
||||
pvc *corev1api.PersistentVolumeClaim
|
||||
pv *corev1api.PersistentVolume
|
||||
sc *storagev1api.StorageClass
|
||||
vsClass *snapshotv1api.VolumeSnapshotClass
|
||||
operationID string
|
||||
expectedErr error
|
||||
expectErr bool // Use bool for cases where we just need to check for any error
|
||||
expectedBackup *velerov1api.Backup
|
||||
expectedDataUpload *velerov2alpha1.DataUpload
|
||||
expectedPVC *corev1api.PersistentVolumeClaim
|
||||
resourcePolicy *corev1api.ConfigMap
|
||||
extraObjects []runtime.Object
|
||||
failVSCreate bool
|
||||
skipVSReadyUpdate bool // New flag to control VS readiness
|
||||
expectedVSClassName string
|
||||
}{
|
||||
{
|
||||
name: "Skip PVC BIA when backup is in finalizing phase",
|
||||
@@ -122,12 +123,21 @@ func TestExecute(t *testing.T) {
|
||||
expectErr: true, // Expect an error, but the exact message can vary
|
||||
},
|
||||
{
|
||||
name: "Test SnapshotMoveData",
|
||||
backup: builder.ForBackup("velero", "test").SnapshotMoveData(true).CSISnapshotTimeout(1 * time.Minute).Result(),
|
||||
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(),
|
||||
pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(),
|
||||
sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(),
|
||||
vsClass: builder.ForVolumeSnapshotClass("testVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(),
|
||||
name: "Test SnapshotMoveData",
|
||||
backup: builder.ForBackup("velero", "test").SnapshotMoveData(true).CSISnapshotTimeout(1 * time.Minute).Result(),
|
||||
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(),
|
||||
pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(),
|
||||
sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(),
|
||||
vsClass: builder.ForVolumeSnapshotClass("testVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(),
|
||||
extraObjects: []runtime.Object{
|
||||
&corev1api.Node{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "linux-node", Labels: map[string]string{"kubernetes.io/os": "linux"}},
|
||||
},
|
||||
&appsv1api.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "velero", Name: "node-agent"},
|
||||
Status: appsv1api.DaemonSetStatus{NumberReady: 3},
|
||||
},
|
||||
},
|
||||
operationID: ".",
|
||||
expectedDataUpload: &velerov2alpha1.DataUpload{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
@@ -163,22 +173,42 @@ func TestExecute(t *testing.T) {
|
||||
SourcePVC: "testPVC",
|
||||
SourceNamespace: "velero",
|
||||
OperationTimeout: metav1.Duration{Duration: 1 * time.Minute},
|
||||
ParentSnapshot: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Verify PVC is modified as expected",
|
||||
backup: builder.ForBackup("velero", "test").SnapshotMoveData(true).CSISnapshotTimeout(1 * time.Minute).Result(),
|
||||
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(),
|
||||
pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(),
|
||||
sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(),
|
||||
vsClass: builder.ForVolumeSnapshotClass("tescVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(),
|
||||
name: "Verify PVC is modified as expected",
|
||||
backup: builder.ForBackup("velero", "test").SnapshotMoveData(true).CSISnapshotTimeout(1 * time.Minute).Result(),
|
||||
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(),
|
||||
pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(),
|
||||
sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(),
|
||||
vsClass: builder.ForVolumeSnapshotClass("tescVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(),
|
||||
extraObjects: []runtime.Object{
|
||||
&corev1api.Node{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "linux-node", Labels: map[string]string{"kubernetes.io/os": "linux"}},
|
||||
},
|
||||
&appsv1api.DaemonSet{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "velero", Name: "node-agent"},
|
||||
Status: appsv1api.DaemonSetStatus{NumberReady: 3},
|
||||
},
|
||||
},
|
||||
operationID: ".",
|
||||
expectedPVC: builder.ForPersistentVolumeClaim("velero", "testPVC").
|
||||
ObjectMeta(builder.WithAnnotations(velerov1api.MustIncludeAdditionalItemAnnotation, "true", velerov1api.DataUploadNameAnnotation, "velero/"),
|
||||
builder.WithLabels(velerov1api.BackupNameLabel, "test")).
|
||||
VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(),
|
||||
},
|
||||
{
|
||||
name: "Test SnapshotMoveData without node-agent",
|
||||
backup: builder.ForBackup("velero", "test").SnapshotMoveData(true).CSISnapshotTimeout(1 * time.Minute).Result(),
|
||||
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(),
|
||||
pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(),
|
||||
sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(),
|
||||
vsClass: builder.ForVolumeSnapshotClass("testVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(),
|
||||
expectErr: true,
|
||||
skipVSReadyUpdate: true,
|
||||
},
|
||||
{
|
||||
name: "Test ResourcePolicy",
|
||||
backup: builder.ForBackup("velero", "test").ResourcePolicies("resourcePolicy").SnapshotVolumes(false).CSISnapshotTimeout(time.Duration(3600) * time.Second).Result(),
|
||||
@@ -188,6 +218,16 @@ func TestExecute(t *testing.T) {
|
||||
sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(),
|
||||
vsClass: builder.ForVolumeSnapshotClass("tescVSClass").Driver("hostpath").ObjectMeta(builder.WithLabels(velerov1api.VolumeSnapshotClassSelectorLabel, "")).Result(),
|
||||
},
|
||||
{
|
||||
name: "Volume policy with snapshotClass selects correct VolumeSnapshotClass",
|
||||
backup: builder.ForBackup("velero", "test").ResourcePolicies("resourcePolicy").CSISnapshotTimeout(time.Duration(3600) * time.Second).Result(),
|
||||
resourcePolicy: builder.ForConfigMap("velero", "resourcePolicy").Data("policy", `{"version":"v1","volumePolicies":[{"conditions":{"csi":{}},"action":{"type":"snapshot","parameters":{"snapshotClass":"policy-selected-vsclass"}}}]}`).Result(),
|
||||
pvc: builder.ForPersistentVolumeClaim("velero", "testPVC").VolumeName("testPV").StorageClass("testSC").Phase(corev1api.ClaimBound).Result(),
|
||||
pv: builder.ForPersistentVolume("testPV").CSI("hostpath", "testVolume").Result(),
|
||||
sc: builder.ForStorageClass("testSC").Provisioner("hostpath").Result(),
|
||||
vsClass: builder.ForVolumeSnapshotClass("policy-selected-vsclass").Driver("hostpath").Result(),
|
||||
expectedVSClassName: "policy-selected-vsclass",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
@@ -210,6 +250,7 @@ func TestExecute(t *testing.T) {
|
||||
if tc.resourcePolicy != nil {
|
||||
objects = append(objects, tc.resourcePolicy)
|
||||
}
|
||||
objects = append(objects, tc.extraObjects...)
|
||||
|
||||
var crClient crclient.Client
|
||||
if tc.failVSCreate {
|
||||
@@ -301,6 +342,15 @@ func TestExecute(t *testing.T) {
|
||||
runtime.DefaultUnstructuredConverter.FromUnstructured(resultUnstructed.UnstructuredContent(), resultPVC)
|
||||
require.True(t, cmp.Equal(tc.expectedPVC, resultPVC, cmpopts.IgnoreFields(corev1api.PersistentVolumeClaim{}, "ResourceVersion", "Annotations", "Labels")))
|
||||
}
|
||||
|
||||
if tc.expectedVSClassName != "" {
|
||||
vsList := new(snapshotv1api.VolumeSnapshotList)
|
||||
require.NoError(t, crClient.List(t.Context(), vsList, &crclient.ListOptions{Namespace: tc.pvc.Namespace}))
|
||||
require.NotEmpty(t, vsList.Items, "expected VolumeSnapshot to be created")
|
||||
require.NotNil(t, vsList.Items[0].Spec.VolumeSnapshotClassName)
|
||||
assert.Equal(t, tc.expectedVSClassName, *vsList.Items[0].Spec.VolumeSnapshotClassName,
|
||||
"VolumeSnapshot should use the VolumeSnapshotClass specified by volume policy")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2176,3 +2226,131 @@ func TestGetOrCreateVolumeHelper(t *testing.T) {
|
||||
// The pvcPodCache should be the same instance
|
||||
require.Same(t, cache1, action.pvcPodCache, "Expected same pvcPodCache instance on repeated calls")
|
||||
}
|
||||
|
||||
func TestNewDataUpload(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
backupType velerov1api.BackupType
|
||||
vsClassName *string
|
||||
uploaderConfig *velerov1api.UploaderConfigForBackup
|
||||
expectedParentSnap string
|
||||
expectedDataMoverCfg map[string]string
|
||||
}{
|
||||
{
|
||||
name: "Full backup type, no uploader config, no vs class name",
|
||||
backupType: velerov1api.BackupTypeFull,
|
||||
vsClassName: nil,
|
||||
uploaderConfig: nil,
|
||||
expectedParentSnap: "none",
|
||||
expectedDataMoverCfg: nil,
|
||||
},
|
||||
{
|
||||
name: "Incremental backup type, with uploader config, with vs class name",
|
||||
backupType: velerov1api.BackupTypeIncremental,
|
||||
vsClassName: ptr.To("test-vs-class"),
|
||||
uploaderConfig: &velerov1api.UploaderConfigForBackup{ParallelFilesUpload: 10},
|
||||
expectedParentSnap: "",
|
||||
expectedDataMoverCfg: map[string]string{
|
||||
uploaderUtil.ParallelFilesUpload: "10",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Default backup type, uploader config with 0 parallel files",
|
||||
backupType: "",
|
||||
vsClassName: ptr.To("test-vs-class"),
|
||||
uploaderConfig: &velerov1api.UploaderConfigForBackup{ParallelFilesUpload: 0},
|
||||
expectedParentSnap: "",
|
||||
expectedDataMoverCfg: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
backup := &velerov1api.Backup{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-backup",
|
||||
Namespace: "velero",
|
||||
UID: types.UID("backup-uid"),
|
||||
},
|
||||
Spec: velerov1api.BackupSpec{
|
||||
BackupType: tc.backupType,
|
||||
DataMover: "velero",
|
||||
StorageLocation: "default",
|
||||
CSISnapshotTimeout: metav1.Duration{Duration: 10 * time.Minute},
|
||||
UploaderConfig: tc.uploaderConfig,
|
||||
},
|
||||
}
|
||||
|
||||
vs := &snapshotv1api.VolumeSnapshot{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-vs",
|
||||
},
|
||||
Spec: snapshotv1api.VolumeSnapshotSpec{
|
||||
VolumeSnapshotClassName: tc.vsClassName,
|
||||
},
|
||||
}
|
||||
|
||||
pvc := &corev1api.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pvc",
|
||||
Namespace: "test-ns",
|
||||
UID: types.UID("pvc-uid"),
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeClaimSpec{
|
||||
StorageClassName: ptr.To("test-storage-class"),
|
||||
},
|
||||
}
|
||||
|
||||
vsc := &snapshotv1api.VolumeSnapshotContent{
|
||||
Spec: snapshotv1api.VolumeSnapshotContentSpec{
|
||||
Driver: "test-driver",
|
||||
},
|
||||
}
|
||||
|
||||
operationID := "test-op-id"
|
||||
fsType := "ext4"
|
||||
|
||||
du := newDataUpload(backup, vs, pvc, operationID, vsc, fsType)
|
||||
|
||||
require.NotNil(t, du)
|
||||
assert.Equal(t, velerov2alpha1.SchemeGroupVersion.String(), du.APIVersion)
|
||||
assert.Equal(t, "DataUpload", du.Kind)
|
||||
assert.Equal(t, backup.Namespace, du.Namespace)
|
||||
assert.Equal(t, backup.Name+"-", du.GenerateName)
|
||||
|
||||
require.Len(t, du.OwnerReferences, 1)
|
||||
assert.Equal(t, velerov1api.SchemeGroupVersion.String(), du.OwnerReferences[0].APIVersion)
|
||||
assert.Equal(t, "Backup", du.OwnerReferences[0].Kind)
|
||||
assert.Equal(t, backup.Name, du.OwnerReferences[0].Name)
|
||||
assert.Equal(t, backup.UID, du.OwnerReferences[0].UID)
|
||||
assert.Equal(t, boolptr.True(), du.OwnerReferences[0].Controller)
|
||||
|
||||
expectedLabels := map[string]string{
|
||||
velerov1api.BackupNameLabel: label.GetValidName(backup.Name),
|
||||
velerov1api.BackupUIDLabel: string(backup.UID),
|
||||
velerov1api.PVCUIDLabel: string(pvc.UID),
|
||||
velerov1api.AsyncOperationIDLabel: operationID,
|
||||
}
|
||||
assert.Equal(t, expectedLabels, du.Labels)
|
||||
|
||||
assert.Equal(t, velerov2alpha1.SnapshotTypeCSI, du.Spec.SnapshotType)
|
||||
assert.Equal(t, vs.Name, du.Spec.CSISnapshot.VolumeSnapshot)
|
||||
assert.Equal(t, *pvc.Spec.StorageClassName, du.Spec.CSISnapshot.StorageClass)
|
||||
assert.Equal(t, vsc.Spec.Driver, du.Spec.CSISnapshot.Driver)
|
||||
if tc.vsClassName != nil {
|
||||
assert.Equal(t, *tc.vsClassName, du.Spec.CSISnapshot.SnapshotClass)
|
||||
} else {
|
||||
assert.Empty(t, du.Spec.CSISnapshot.SnapshotClass)
|
||||
}
|
||||
|
||||
assert.Equal(t, pvc.Name, du.Spec.SourcePVC)
|
||||
assert.Equal(t, backup.Spec.DataMover, du.Spec.DataMover)
|
||||
assert.Equal(t, backup.Spec.StorageLocation, du.Spec.BackupStorageLocation)
|
||||
assert.Equal(t, pvc.Namespace, du.Spec.SourceNamespace)
|
||||
assert.Equal(t, backup.Spec.CSISnapshotTimeout, du.Spec.OperationTimeout)
|
||||
assert.Equal(t, fsType, du.Spec.SourceFSType)
|
||||
assert.Equal(t, tc.expectedParentSnap, du.Spec.ParentSnapshot)
|
||||
assert.Equal(t, tc.expectedDataMoverCfg, du.Spec.DataMoverConfig)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1428,22 +1428,20 @@ func resolveClusterScopedFilterPolicy(
|
||||
}
|
||||
|
||||
func resolveResourceFilter(rf resourcepolicies.ResourceFilter) (*ResolvedResourceFilter, error) {
|
||||
var selector labels.Selector
|
||||
if len(rf.LabelSelector) > 0 {
|
||||
var err error
|
||||
selector, err = labels.ValidatedSelectorFromSet(labels.Set(rf.LabelSelector))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid label selector in resource filter: %w", err)
|
||||
}
|
||||
selector, err := resourcepolicies.SelectorFromPolicyLabelSelector(rf.LabelSelector)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid label selector in resource filter: %w", err)
|
||||
}
|
||||
|
||||
var orSelectors []labels.Selector
|
||||
for _, ols := range rf.OrLabelSelectors {
|
||||
s, err := labels.ValidatedSelectorFromSet(labels.Set(ols))
|
||||
s, err := resourcepolicies.SelectorFromPolicyLabelSelector(ols)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid OR label selector in resource filter: %w", err)
|
||||
}
|
||||
orSelectors = append(orSelectors, s)
|
||||
if s != nil {
|
||||
orSelectors = append(orSelectors, s)
|
||||
}
|
||||
}
|
||||
|
||||
var nameIE *collections.IncludesExcludes
|
||||
|
||||
+100
-15
@@ -5429,6 +5429,29 @@ func TestBackupNamespaces(t *testing.T) {
|
||||
"resources/namespaces/v1-preferredversion/cluster/ns-3.json",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Wildcard star with excluded namespaces test",
|
||||
backup: defaultBackup().IncludedNamespaces("*").ExcludedNamespaces("ns-2").Result(),
|
||||
apiResources: []*test.APIResource{
|
||||
test.Namespaces(
|
||||
builder.ForNamespace("ns-1").Phase(corev1api.NamespaceActive).Result(),
|
||||
builder.ForNamespace("ns-2").Phase(corev1api.NamespaceActive).Result(),
|
||||
builder.ForNamespace("ns-3").Phase(corev1api.NamespaceActive).Result(),
|
||||
),
|
||||
test.Deployments(
|
||||
builder.ForDeployment("ns-1", "deploy-1").Result(),
|
||||
builder.ForDeployment("ns-2", "deploy-2").Result(),
|
||||
),
|
||||
},
|
||||
want: []string{
|
||||
"resources/namespaces/cluster/ns-1.json",
|
||||
"resources/namespaces/v1-preferredversion/cluster/ns-1.json",
|
||||
"resources/namespaces/cluster/ns-3.json",
|
||||
"resources/namespaces/v1-preferredversion/cluster/ns-3.json",
|
||||
"resources/deployments.apps/namespaces/ns-1/deploy-1.json",
|
||||
"resources/deployments.apps/v1-preferredversion/namespaces/ns-1/deploy-1.json",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Empty namespace test",
|
||||
backup: defaultBackup().IncludedNamespaces("invalid*").Result(),
|
||||
@@ -5741,7 +5764,7 @@ func TestResolveResourceFilter(t *testing.T) {
|
||||
{
|
||||
name: "valid label selector",
|
||||
rf: resourcepolicies.ResourceFilter{
|
||||
LabelSelector: map[string]string{"app": "foo"},
|
||||
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"app": "foo"}},
|
||||
},
|
||||
expectErr: false,
|
||||
checkResult: func(t *testing.T, r *ResolvedResourceFilter) {
|
||||
@@ -5754,16 +5777,16 @@ func TestResolveResourceFilter(t *testing.T) {
|
||||
{
|
||||
name: "invalid label selector",
|
||||
rf: resourcepolicies.ResourceFilter{
|
||||
LabelSelector: map[string]string{"invalid/label/key": "value"},
|
||||
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"invalid/label/key": "value"}},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid or label selectors",
|
||||
rf: resourcepolicies.ResourceFilter{
|
||||
OrLabelSelectors: []map[string]string{
|
||||
{"app": "foo"},
|
||||
{"app": "bar"},
|
||||
OrLabelSelectors: []*resourcepolicies.PolicyLabelSelector{
|
||||
{MatchLabels: map[string]string{"app": "foo"}},
|
||||
{MatchLabels: map[string]string{"app": "bar"}},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
@@ -5776,8 +5799,8 @@ func TestResolveResourceFilter(t *testing.T) {
|
||||
{
|
||||
name: "invalid or label selectors",
|
||||
rf: resourcepolicies.ResourceFilter{
|
||||
OrLabelSelectors: []map[string]string{
|
||||
{"invalid/label/key": "value"},
|
||||
OrLabelSelectors: []*resourcepolicies.PolicyLabelSelector{
|
||||
{MatchLabels: map[string]string{"invalid/label/key": "value"}},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
@@ -5797,6 +5820,68 @@ func TestResolveResourceFilter(t *testing.T) {
|
||||
assert.False(t, r.NameIE.ShouldInclude("exc1"))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty labelSelector is no filter",
|
||||
rf: resourcepolicies.ResourceFilter{
|
||||
LabelSelector: &resourcepolicies.PolicyLabelSelector{},
|
||||
},
|
||||
expectErr: false,
|
||||
checkResult: func(t *testing.T, r *ResolvedResourceFilter) {
|
||||
t.Helper()
|
||||
require.NotNil(t, r)
|
||||
assert.Nil(t, r.LabelSelector)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set-based In and DoesNotExist",
|
||||
rf: resourcepolicies.ResourceFilter{
|
||||
LabelSelector: &resourcepolicies.PolicyLabelSelector{
|
||||
MatchExpressions: []resourcepolicies.PolicyLabelSelectorRequirement{
|
||||
{Key: "environment", Operator: "In", Values: []string{"prod", "staging"}},
|
||||
{Key: "do-not-backup", Operator: "DoesNotExist"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
checkResult: func(t *testing.T, r *ResolvedResourceFilter) {
|
||||
t.Helper()
|
||||
require.NotNil(t, r.LabelSelector)
|
||||
assert.True(t, r.LabelSelector.Matches(labels.Set{"environment": "prod"}))
|
||||
assert.True(t, r.LabelSelector.Matches(labels.Set{"environment": "staging"}))
|
||||
assert.False(t, r.LabelSelector.Matches(labels.Set{"environment": "dev"}))
|
||||
assert.False(t, r.LabelSelector.Matches(labels.Set{"environment": "prod", "do-not-backup": "true"}))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set-based NotIn and Exists",
|
||||
rf: resourcepolicies.ResourceFilter{
|
||||
LabelSelector: &resourcepolicies.PolicyLabelSelector{
|
||||
MatchExpressions: []resourcepolicies.PolicyLabelSelectorRequirement{
|
||||
{Key: "tier", Operator: "NotIn", Values: []string{"debug"}},
|
||||
{Key: "app", Operator: "Exists"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
checkResult: func(t *testing.T, r *ResolvedResourceFilter) {
|
||||
t.Helper()
|
||||
require.NotNil(t, r.LabelSelector)
|
||||
assert.True(t, r.LabelSelector.Matches(labels.Set{"app": "web", "tier": "frontend"}))
|
||||
assert.False(t, r.LabelSelector.Matches(labels.Set{"app": "web", "tier": "debug"}))
|
||||
assert.False(t, r.LabelSelector.Matches(labels.Set{"tier": "frontend"}))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid operator",
|
||||
rf: resourcepolicies.ResourceFilter{
|
||||
LabelSelector: &resourcepolicies.PolicyLabelSelector{
|
||||
MatchExpressions: []resourcepolicies.PolicyLabelSelectorRequirement{
|
||||
{Key: "env", Operator: "Equals", Values: []string{"prod"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
@@ -5834,11 +5919,11 @@ func TestResolveClusterScopedFilterPolicy(t *testing.T) {
|
||||
ResourceFilters: []resourcepolicies.ResourceFilter{
|
||||
{
|
||||
Kinds: []string{"pods", "secrets"},
|
||||
LabelSelector: map[string]string{"app": "foo"},
|
||||
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"app": "foo"}},
|
||||
},
|
||||
{
|
||||
Kinds: []string{"invalid-kind"},
|
||||
LabelSelector: map[string]string{"invalid/label/key": "value"},
|
||||
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"invalid/label/key": "value"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -5852,7 +5937,7 @@ func TestResolveClusterScopedFilterPolicy(t *testing.T) {
|
||||
ResourceFilters: []resourcepolicies.ResourceFilter{
|
||||
{
|
||||
Kinds: []string{"pods", "secrets"},
|
||||
LabelSelector: map[string]string{"app": "foo"},
|
||||
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"app": "foo"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -5900,11 +5985,11 @@ func TestResolveNamespacedFilterPolicies(t *testing.T) {
|
||||
ResourceFilters: []resourcepolicies.ResourceFilter{
|
||||
{
|
||||
Kinds: []string{"pods"},
|
||||
LabelSelector: map[string]string{"app": "foo"},
|
||||
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"app": "foo"}},
|
||||
},
|
||||
{
|
||||
Kinds: []string{"*"},
|
||||
LabelSelector: map[string]string{"catch": "all"},
|
||||
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"catch": "all"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -5932,7 +6017,7 @@ func TestResolveNamespacedFilterPolicies(t *testing.T) {
|
||||
ResourceFilters: []resourcepolicies.ResourceFilter{
|
||||
{
|
||||
Kinds: []string{"pods"},
|
||||
LabelSelector: map[string]string{"invalid/label/key": "value"},
|
||||
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"invalid/label/key": "value"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -6016,7 +6101,7 @@ func TestBackupWithResPoliciesLogs(t *testing.T) {
|
||||
ResourceFilters: []resourcepolicies.ResourceFilter{
|
||||
{
|
||||
Kinds: []string{"pods"},
|
||||
LabelSelector: map[string]string{"invalid/label/key": "value"},
|
||||
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"invalid/label/key": "value"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -6035,7 +6120,7 @@ func TestBackupWithResPoliciesLogs(t *testing.T) {
|
||||
ResourceFilters: []resourcepolicies.ResourceFilter{
|
||||
{
|
||||
Kinds: []string{"pods"},
|
||||
LabelSelector: map[string]string{"invalid/label/key": "value"},
|
||||
LabelSelector: &resourcepolicies.PolicyLabelSelector{MatchLabels: map[string]string{"invalid/label/key": "value"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -351,16 +351,28 @@ func (ib *itemBackupper) backupItemInternal(logger logrus.FieldLogger, obj runti
|
||||
if versionPath == preferredGVR.Version {
|
||||
// backing up preferred version backup without API Group version - for backward compatibility
|
||||
log.Debugf("Resource %s/%s, version= %s, preferredVersion=%s", groupResource.String(), name, versionPath, preferredGVR.Version)
|
||||
itemFiles = append(itemFiles, getFileForArchive(namespace, name, groupResource.String(), "", itemBytes))
|
||||
fileForArchive, err := getFileForArchive(namespace, name, groupResource.String(), "", itemBytes)
|
||||
if err != nil {
|
||||
return false, itemFiles, err
|
||||
}
|
||||
itemFiles = append(itemFiles, fileForArchive)
|
||||
versionPath = versionPath + velerov1api.PreferredVersionDir
|
||||
}
|
||||
|
||||
itemFiles = append(itemFiles, getFileForArchive(namespace, name, groupResource.String(), versionPath, itemBytes))
|
||||
fileForArchive, err := getFileForArchive(namespace, name, groupResource.String(), versionPath, itemBytes)
|
||||
if err != nil {
|
||||
return false, itemFiles, err
|
||||
}
|
||||
itemFiles = append(itemFiles, fileForArchive)
|
||||
return true, itemFiles, nil
|
||||
}
|
||||
|
||||
func getFileForArchive(namespace, name, groupResource, versionPath string, itemBytes []byte) FileForArchive {
|
||||
filePath := archive.GetVersionedItemFilePath("", groupResource, namespace, name, versionPath)
|
||||
func getFileForArchive(namespace, name, groupResource, versionPath string, itemBytes []byte) (FileForArchive, error) {
|
||||
filePath, err := archive.GetVersionedItemFilePath("", groupResource, namespace, name, versionPath)
|
||||
if err != nil {
|
||||
return FileForArchive{}, err
|
||||
}
|
||||
|
||||
hdr := &tar.Header{
|
||||
Name: filePath,
|
||||
Size: int64(len(itemBytes)),
|
||||
@@ -368,7 +380,7 @@ func getFileForArchive(namespace, name, groupResource, versionPath string, itemB
|
||||
Mode: 0755,
|
||||
ModTime: time.Now(),
|
||||
}
|
||||
return FileForArchive{FilePath: filePath, Header: hdr, FileBytes: itemBytes}
|
||||
return FileForArchive{FilePath: filePath, Header: hdr, FileBytes: itemBytes}, nil
|
||||
}
|
||||
|
||||
// backupPodVolumes triggers pod volume backups of the specified pod volumes, and returns a list of PodVolumeBackups
|
||||
|
||||
@@ -508,7 +508,8 @@ func (r *itemCollector) getResourceItems(
|
||||
kind: resource.Kind,
|
||||
})
|
||||
|
||||
if item.GetNamespace() != "" {
|
||||
if item.GetNamespace() != "" &&
|
||||
r.backupRequest.NamespaceIncludesExcludes.ShouldInclude(item.GetNamespace()) {
|
||||
log.Debugf("Track namespace %s in nsTracker", item.GetNamespace())
|
||||
r.nsTracker.track(item.GetNamespace())
|
||||
}
|
||||
|
||||
@@ -109,8 +109,23 @@ func (b *BackupBuilder) FromSchedule(schedule *velerov1api.Schedule) *BackupBuil
|
||||
b.object.Spec = schedule.Spec.Template
|
||||
b.ObjectMeta(WithLabelsMap(labels))
|
||||
|
||||
if schedule.Annotations != nil {
|
||||
b.ObjectMeta(WithAnnotationsMap(schedule.Annotations))
|
||||
var annotations map[string]string
|
||||
|
||||
// Check if there's explicit Annotations defined in the Schedule object template
|
||||
// and if present then copy it to the backup object.
|
||||
if schedule.Spec.Template.Metadata.Annotations != nil {
|
||||
logger := logging.DefaultLogger(logging.LogLevelFlag(logrus.InfoLevel).Parse(), logging.NewFormatFlag().Parse())
|
||||
annotations = schedule.Spec.Template.Metadata.Annotations
|
||||
logger.WithFields(logrus.Fields{
|
||||
"backup": fmt.Sprintf("%s/%s", b.object.GetNamespace(), b.object.GetName()),
|
||||
"annotations": schedule.Spec.Template.Metadata.Annotations,
|
||||
}).Info("Schedule.template.metadata.annotations set - using those annotations instead of schedule.annotations for backup object")
|
||||
} else {
|
||||
annotations = schedule.Annotations
|
||||
}
|
||||
|
||||
if annotations != nil {
|
||||
b.ObjectMeta(WithAnnotationsMap(annotations))
|
||||
}
|
||||
|
||||
if boolptr.IsSetToTrue(schedule.Spec.UseOwnerReferencesInBackup) {
|
||||
@@ -321,6 +336,11 @@ func (b *BackupBuilder) ParallelFilesUpload(parallel int) *BackupBuilder {
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *BackupBuilder) BackupType(backupType velerov1api.BackupType) *BackupBuilder {
|
||||
b.object.Spec.BackupType = backupType
|
||||
return b
|
||||
}
|
||||
|
||||
// WithStatus sets the Backup's status.
|
||||
func (b *BackupBuilder) WithStatus(status velerov1api.BackupStatus) *BackupBuilder {
|
||||
b.object.Status = status
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
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 builder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
)
|
||||
|
||||
func TestBackupFromSchedule(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schedule *velerov1api.Schedule
|
||||
expectedLabels map[string]string
|
||||
expectedAnnotations map[string]string
|
||||
}{
|
||||
{
|
||||
name: "no schedule labels/annotations and no template overrides",
|
||||
schedule: ForSchedule("velero", "test").
|
||||
Result(),
|
||||
expectedLabels: map[string]string{velerov1api.ScheduleNameLabel: "test"},
|
||||
expectedAnnotations: nil,
|
||||
},
|
||||
{
|
||||
name: "schedule labels/annotations are copied when no template override is set",
|
||||
schedule: ForSchedule("velero", "test").
|
||||
ObjectMeta(
|
||||
WithLabels("schedule-label", "schedule-value"),
|
||||
WithAnnotations("schedule-annotation", "schedule-value"),
|
||||
).
|
||||
Result(),
|
||||
expectedLabels: map[string]string{
|
||||
"schedule-label": "schedule-value",
|
||||
velerov1api.ScheduleNameLabel: "test",
|
||||
},
|
||||
expectedAnnotations: map[string]string{"schedule-annotation": "schedule-value"},
|
||||
},
|
||||
{
|
||||
name: "template.metadata.labels/annotations override schedule labels/annotations",
|
||||
schedule: ForSchedule("velero", "test").
|
||||
ObjectMeta(
|
||||
WithLabels("schedule-label", "schedule-value"),
|
||||
WithAnnotations("schedule-annotation", "schedule-value"),
|
||||
).
|
||||
Template(velerov1api.BackupSpec{
|
||||
Metadata: velerov1api.Metadata{
|
||||
Labels: map[string]string{"template-label": "template-value"},
|
||||
Annotations: map[string]string{"template-annotation": "template-value"},
|
||||
},
|
||||
}).
|
||||
Result(),
|
||||
expectedLabels: map[string]string{
|
||||
"template-label": "template-value",
|
||||
velerov1api.ScheduleNameLabel: "test",
|
||||
},
|
||||
expectedAnnotations: map[string]string{"template-annotation": "template-value"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
backup := ForBackup("velero", "test-backup").FromSchedule(test.schedule).Result()
|
||||
assert.Equal(t, test.expectedLabels, backup.GetLabels())
|
||||
assert.Equal(t, test.expectedAnnotations, backup.GetAnnotations())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -181,3 +181,9 @@ func (b *RestoreBuilder) ResourcePoliciesConfigmap(name string) *RestoreBuilder
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// SkipDefaultResourceModifier sets whether to skip the server default resource modifier.
|
||||
func (b *RestoreBuilder) SkipDefaultResourceModifier(val bool) *RestoreBuilder {
|
||||
b.object.Spec.SkipDefaultResourceModifier = &val
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -86,6 +86,12 @@ func (s *ServiceImpl) GetAllocatedBlocks(ctx context.Context, snapshot string, r
|
||||
return err
|
||||
}
|
||||
|
||||
saNamespace := ""
|
||||
if s.SAName != "" {
|
||||
// The SA is created in the same namespace as Velero server. vsNamespace is the namespace of Velero server.
|
||||
saNamespace = s.vsNamespace
|
||||
}
|
||||
|
||||
args := iterator.Args{
|
||||
SnapshotName: snapshot,
|
||||
Emitter: &emitterImpl{
|
||||
@@ -95,7 +101,7 @@ func (s *ServiceImpl) GetAllocatedBlocks(ctx context.Context, snapshot string, r
|
||||
|
||||
Clients: clients,
|
||||
Namespace: s.vsNamespace, // DataUpload is created in the same namespace as Velero server. vsNamespace is the namespace of the Velero server.
|
||||
SANamespace: s.vsNamespace, // The SA is created in the same namespace as Velero server. vsNamespace is the namespace of Velero server.
|
||||
SANamespace: saNamespace,
|
||||
SAName: s.SAName,
|
||||
TokenExpirySecs: iterator.DefaultTokenExpirySeconds,
|
||||
MaxResults: 0, // If 0 then the CSI driver decides the value.
|
||||
@@ -110,9 +116,15 @@ func (s *ServiceImpl) GetChangedBlocks(ctx context.Context, snapshot string, cha
|
||||
return err
|
||||
}
|
||||
|
||||
saNamespace := ""
|
||||
if s.SAName != "" {
|
||||
// The SA is created in the same namespace as Velero server. vsNamespace is the namespace of Velero server.
|
||||
saNamespace = s.vsNamespace
|
||||
}
|
||||
|
||||
args := iterator.Args{
|
||||
SnapshotName: snapshot,
|
||||
PrevSnapshotName: changeID,
|
||||
SnapshotName: snapshot,
|
||||
PrevSnapshotID: changeID,
|
||||
Emitter: &emitterImpl{
|
||||
logger: s.logger,
|
||||
recordCallBack: record,
|
||||
@@ -120,7 +132,7 @@ func (s *ServiceImpl) GetChangedBlocks(ctx context.Context, snapshot string, cha
|
||||
|
||||
Clients: clients,
|
||||
Namespace: s.vsNamespace,
|
||||
SANamespace: s.vsNamespace,
|
||||
SANamespace: saNamespace,
|
||||
SAName: s.SAName,
|
||||
TokenExpirySecs: iterator.DefaultTokenExpirySeconds,
|
||||
MaxResults: 0, // If 0 then the CSI driver decides the value.
|
||||
|
||||
@@ -234,7 +234,7 @@ func TestServiceImplGetChangedBlocks(t *testing.T) {
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "snap-2", capturedArgs.SnapshotName)
|
||||
assert.Equal(t, "snap-1", capturedArgs.PrevSnapshotName)
|
||||
assert.Equal(t, "snap-1", capturedArgs.PrevSnapshotID)
|
||||
assert.Equal(t, "velero-ns", capturedArgs.Namespace)
|
||||
assert.Equal(t, iterator.DefaultTokenExpirySeconds, capturedArgs.TokenExpirySecs)
|
||||
assert.Zero(t, capturedArgs.MaxResults)
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/flag"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/collections"
|
||||
@@ -75,6 +76,10 @@ func NewCreateCommand(f client.Factory, use string) *cobra.Command {
|
||||
output.BindFlags(c.Flags())
|
||||
output.ClearOutputFlagDefault(c)
|
||||
|
||||
_ = c.RegisterFlagCompletionFunc("from-schedule", cli.CompleteScheduleNames(f))
|
||||
_ = c.RegisterFlagCompletionFunc("storage-location", cli.CompleteBackupStorageLocationNames(f))
|
||||
_ = c.RegisterFlagCompletionFunc("volume-snapshot-locations", cli.CompleteVolumeSnapshotLocationNames(f))
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -108,6 +113,7 @@ type CreateOptions struct {
|
||||
ResPoliciesConfigmap string
|
||||
client kbclient.WithWatch
|
||||
ParallelFilesUpload int
|
||||
BackupType string
|
||||
}
|
||||
|
||||
func NewCreateOptions() *CreateOptions {
|
||||
@@ -156,6 +162,7 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
|
||||
flags.StringVar(&o.ResPoliciesConfigmap, "resource-policies-configmap", "", "Reference to the resource policies configmap that backup should use")
|
||||
flags.StringVar(&o.DataMover, "data-mover", "", "Specify the data mover to be used by the backup. If the parameter is not set or set as 'velero', the built-in data mover will be used")
|
||||
flags.IntVar(&o.ParallelFilesUpload, "parallel-files-upload", 0, "Number of files uploads simultaneously when running a backup. This is only applicable for the kopia uploader")
|
||||
flags.StringVar(&o.BackupType, "backup-type", "", "Specify how volume data is backed up, with possible values including Full and Incremental.")
|
||||
}
|
||||
|
||||
// BindWait binds the wait flag separately so it is not called by other create
|
||||
@@ -217,6 +224,10 @@ func (o *CreateOptions) Validate(c *cobra.Command, args []string, f client.Facto
|
||||
}
|
||||
}
|
||||
|
||||
if err := o.validateBackupType(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -231,6 +242,24 @@ func (o *CreateOptions) validateFromScheduleFlag(c *cobra.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateBackupType check the backupType value and return the valid value.
|
||||
func (o *CreateOptions) validateBackupType() error {
|
||||
// Allow full, and incremental from the CLI, and ignore case of the input string's case.
|
||||
backupType := strings.ToLower(strings.TrimSpace(o.BackupType))
|
||||
|
||||
switch backupType {
|
||||
case "":
|
||||
case "incremental":
|
||||
o.BackupType = string(velerov1api.BackupTypeIncremental)
|
||||
case "full":
|
||||
o.BackupType = string(velerov1api.BackupTypeFull)
|
||||
default:
|
||||
return fmt.Errorf("invalid backup type %s - valid values are 'Incremental', and 'Full'", backupType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *CreateOptions) Complete(args []string, f client.Factory) error {
|
||||
// If an explicit name is specified, use that name
|
||||
if len(args) > 0 {
|
||||
@@ -393,7 +422,8 @@ func (o *CreateOptions) BuildBackup(namespace string) (*velerov1api.Backup, erro
|
||||
VolumeSnapshotLocations(o.SnapshotLocations...).
|
||||
CSISnapshotTimeout(o.CSISnapshotTimeout).
|
||||
ItemOperationTimeout(o.ItemOperationTimeout).
|
||||
DataMover(o.DataMover)
|
||||
DataMover(o.DataMover).
|
||||
BackupType(velerov1api.BackupType(o.BackupType))
|
||||
if len(o.OrderedResources) > 0 {
|
||||
orders, err := ParseOrderedResources(o.OrderedResources)
|
||||
if err != nil {
|
||||
|
||||
@@ -122,6 +122,46 @@ func TestCreateOptions_ValidateFromScheduleFlag(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateOptions_ValidateBackupType(t *testing.T) {
|
||||
t.Run("valid backup types", func(t *testing.T) {
|
||||
o := NewCreateOptions()
|
||||
|
||||
o.BackupType = ""
|
||||
err := o.validateBackupType()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, o.BackupType)
|
||||
|
||||
o.BackupType = "Incremental"
|
||||
err = o.validateBackupType()
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, velerov1api.BackupTypeIncremental, o.BackupType)
|
||||
|
||||
o.BackupType = "Full"
|
||||
err = o.validateBackupType()
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, velerov1api.BackupTypeFull, o.BackupType)
|
||||
|
||||
o.BackupType = " Incremental "
|
||||
err = o.validateBackupType()
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, velerov1api.BackupTypeIncremental, o.BackupType)
|
||||
|
||||
o.BackupType = "iNcReMeNtAl"
|
||||
err = o.validateBackupType()
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, velerov1api.BackupTypeIncremental, o.BackupType)
|
||||
})
|
||||
|
||||
t.Run("invalid backup type", func(t *testing.T) {
|
||||
o := NewCreateOptions()
|
||||
|
||||
o.BackupType = "invalid"
|
||||
err := o.validateBackupType()
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "invalid backup type invalid - valid values are 'Incremental', and 'Full'", err.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateOptions_BuildBackupFromSchedule(t *testing.T) {
|
||||
o := NewCreateOptions()
|
||||
o.FromSchedule = "test"
|
||||
@@ -231,6 +271,7 @@ func TestCreateCommand(t *testing.T) {
|
||||
resPoliciesConfigmap := "cm-name-2"
|
||||
dataMover := "velero"
|
||||
parallelFilesUpload := 10
|
||||
backupType := "Incremental"
|
||||
flags := new(flag.FlagSet)
|
||||
o := NewCreateOptions()
|
||||
o.BindFlags(flags)
|
||||
@@ -260,6 +301,7 @@ func TestCreateCommand(t *testing.T) {
|
||||
flags.Parse([]string{"--resource-policies-configmap", resPoliciesConfigmap})
|
||||
flags.Parse([]string{"--data-mover", dataMover})
|
||||
flags.Parse([]string{"--parallel-files-upload", strconv.Itoa(parallelFilesUpload)})
|
||||
flags.Parse([]string{"--backup-type", backupType})
|
||||
//flags.Parse([]string{"--wait"})
|
||||
|
||||
client := velerotest.NewFakeControllerRuntimeClient(t).(kbclient.WithWatch)
|
||||
@@ -310,6 +352,7 @@ func TestCreateCommand(t *testing.T) {
|
||||
require.Equal(t, resPoliciesConfigmap, o.ResPoliciesConfigmap)
|
||||
require.Equal(t, dataMover, o.DataMover)
|
||||
require.Equal(t, parallelFilesUpload, o.ParallelFilesUpload)
|
||||
require.Equal(t, backupType, o.BackupType)
|
||||
//assert.Equal(t, true, o.Wait)
|
||||
|
||||
// verify oldAndNewFilterParametersUsedTogether
|
||||
|
||||
@@ -64,6 +64,7 @@ func NewDeleteCommand(f client.Factory, use string) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteBackupNames(f)
|
||||
o.BindFlags(c.Flags())
|
||||
|
||||
return c
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
|
||||
"github.com/vmware-tanzu/velero/pkg/label"
|
||||
)
|
||||
@@ -112,6 +113,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteBackupNames(f)
|
||||
c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector.")
|
||||
c.Flags().BoolVar(&details, "details", details, "Display additional detail in the command output.")
|
||||
c.Flags().BoolVar(&insecureSkipTLSVerify, "insecure-skip-tls-verify", insecureSkipTLSVerify, "If true, the object store's TLS certificate will not be checked for validity. This is insecure and susceptible to man-in-the-middle attacks. Not recommended for production.")
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/cacert"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest"
|
||||
)
|
||||
@@ -55,6 +56,7 @@ func NewDownloadCommand(f client.Factory) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteBackupNames(f)
|
||||
o.BindFlags(c.Flags())
|
||||
|
||||
return c
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
|
||||
)
|
||||
|
||||
@@ -66,6 +67,7 @@ func NewGetCommand(f client.Factory, use string) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteBackupNames(f)
|
||||
c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector")
|
||||
|
||||
output.BindFlags(c.Flags())
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/cacert"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest"
|
||||
)
|
||||
@@ -119,6 +120,7 @@ func NewLogsCommand(f client.Factory) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteBackupNames(f)
|
||||
l.BindFlags(c.Flags())
|
||||
|
||||
return c
|
||||
|
||||
@@ -62,6 +62,7 @@ func NewDeleteCommand(f client.Factory, use string) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteBackupStorageLocationNames(f)
|
||||
o.BindFlags(c.Flags())
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
|
||||
)
|
||||
|
||||
@@ -89,6 +90,7 @@ func NewGetCommand(f client.Factory, use string) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteBackupStorageLocationNames(f)
|
||||
c.Flags().BoolVar(&showDefaultOnly, "default", false, "Displays the current default backup storage location.")
|
||||
c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector.")
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/flag"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
)
|
||||
@@ -51,6 +52,7 @@ func NewSetCommand(f client.Factory, use string) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteBackupStorageLocationNames(f)
|
||||
o.BindFlags(c.Flags())
|
||||
|
||||
return c
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
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 cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
)
|
||||
|
||||
// completionFunc is the function signature for cobra's ValidArgsFunction.
|
||||
type completionFunc = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
|
||||
|
||||
// completeNames builds a completion function for any Velero list type.
|
||||
// It extracts resource names via apimachinery's meta helpers.
|
||||
func completeNames(f client.Factory, list kbclient.ObjectList) completionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
kbClient, err := f.KubebuilderClient()
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
parentCtx := context.Background()
|
||||
if cmd != nil && cmd.Context() != nil {
|
||||
parentCtx = cmd.Context()
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(parentCtx, 3*time.Second)
|
||||
defer cancel()
|
||||
freshObject := list.DeepCopyObject()
|
||||
freshList, ok := freshObject.(kbclient.ObjectList)
|
||||
if !ok {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
if err := kbClient.List(ctx, freshList, &kbclient.ListOptions{Namespace: f.Namespace()}); err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
items, err := meta.ExtractList(freshList)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
seen := make(map[string]bool, len(args))
|
||||
for _, a := range args {
|
||||
seen[a] = true
|
||||
}
|
||||
var filtered []string
|
||||
for _, item := range items {
|
||||
accessor, err := meta.Accessor(item)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
name := accessor.GetName()
|
||||
if seen[name] {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(name, toComplete) {
|
||||
filtered = append(filtered, name)
|
||||
}
|
||||
}
|
||||
return filtered, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
func CompleteBackupNames(f client.Factory) completionFunc {
|
||||
return completeNames(f, &velerov1api.BackupList{})
|
||||
}
|
||||
|
||||
func CompleteRestoreNames(f client.Factory) completionFunc {
|
||||
return completeNames(f, &velerov1api.RestoreList{})
|
||||
}
|
||||
|
||||
func CompleteScheduleNames(f client.Factory) completionFunc {
|
||||
return completeNames(f, &velerov1api.ScheduleList{})
|
||||
}
|
||||
|
||||
func CompleteBackupStorageLocationNames(f client.Factory) completionFunc {
|
||||
return completeNames(f, &velerov1api.BackupStorageLocationList{})
|
||||
}
|
||||
|
||||
func CompleteVolumeSnapshotLocationNames(f client.Factory) completionFunc {
|
||||
return completeNames(f, &velerov1api.VolumeSnapshotLocationList{})
|
||||
}
|
||||
|
||||
func CompleteBackupRepositoryNames(f client.Factory) completionFunc {
|
||||
return completeNames(f, &velerov1api.BackupRepositoryList{})
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
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 cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
factorymocks "github.com/vmware-tanzu/velero/pkg/client/mocks"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
)
|
||||
|
||||
// TestCompleteNames exercises the core completeNames helper with various list
|
||||
// types, prefix filters, and edge cases (empty cluster, no match).
|
||||
func TestCompleteNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
objects []runtime.Object
|
||||
list kbclient.ObjectList
|
||||
args []string
|
||||
toComplete string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "no resources returns nil",
|
||||
objects: nil,
|
||||
list: &velerov1api.BackupList{},
|
||||
toComplete: "",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "returns all matching names",
|
||||
objects: []runtime.Object{
|
||||
&velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "daily", Namespace: "velero"}},
|
||||
&velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "weekly", Namespace: "velero"}},
|
||||
},
|
||||
list: &velerov1api.BackupList{},
|
||||
toComplete: "",
|
||||
want: []string{"daily", "weekly"},
|
||||
},
|
||||
{
|
||||
name: "filters by prefix",
|
||||
objects: []runtime.Object{
|
||||
&velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "daily", Namespace: "velero"}},
|
||||
&velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "weekly", Namespace: "velero"}},
|
||||
&velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "daily-full", Namespace: "velero"}},
|
||||
},
|
||||
list: &velerov1api.BackupList{},
|
||||
toComplete: "dai",
|
||||
want: []string{"daily", "daily-full"},
|
||||
},
|
||||
{
|
||||
name: "no prefix match returns nil",
|
||||
objects: []runtime.Object{
|
||||
&velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "daily", Namespace: "velero"}},
|
||||
},
|
||||
list: &velerov1api.BackupList{},
|
||||
toComplete: "xyz",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "works with RestoreList",
|
||||
objects: []runtime.Object{
|
||||
&velerov1api.Restore{ObjectMeta: metav1.ObjectMeta{Name: "restore-1", Namespace: "velero"}},
|
||||
&velerov1api.Restore{ObjectMeta: metav1.ObjectMeta{Name: "restore-2", Namespace: "velero"}},
|
||||
},
|
||||
list: &velerov1api.RestoreList{},
|
||||
toComplete: "restore-",
|
||||
want: []string{"restore-1", "restore-2"},
|
||||
},
|
||||
{
|
||||
name: "works with ScheduleList",
|
||||
objects: []runtime.Object{
|
||||
&velerov1api.Schedule{ObjectMeta: metav1.ObjectMeta{Name: "nightly", Namespace: "velero"}},
|
||||
},
|
||||
list: &velerov1api.ScheduleList{},
|
||||
toComplete: "",
|
||||
want: []string{"nightly"},
|
||||
},
|
||||
{
|
||||
name: "works with BackupStorageLocationList",
|
||||
objects: []runtime.Object{
|
||||
&velerov1api.BackupStorageLocation{ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "velero"}},
|
||||
&velerov1api.BackupStorageLocation{ObjectMeta: metav1.ObjectMeta{Name: "secondary", Namespace: "velero"}},
|
||||
},
|
||||
list: &velerov1api.BackupStorageLocationList{},
|
||||
toComplete: "s",
|
||||
want: []string{"secondary"},
|
||||
},
|
||||
{
|
||||
name: "works with VolumeSnapshotLocationList",
|
||||
objects: []runtime.Object{
|
||||
&velerov1api.VolumeSnapshotLocation{ObjectMeta: metav1.ObjectMeta{Name: "aws-snap", Namespace: "velero"}},
|
||||
},
|
||||
list: &velerov1api.VolumeSnapshotLocationList{},
|
||||
toComplete: "",
|
||||
want: []string{"aws-snap"},
|
||||
},
|
||||
{
|
||||
name: "works with BackupRepositoryList",
|
||||
objects: []runtime.Object{
|
||||
&velerov1api.BackupRepository{ObjectMeta: metav1.ObjectMeta{Name: "repo-1", Namespace: "velero"}},
|
||||
},
|
||||
list: &velerov1api.BackupRepositoryList{},
|
||||
toComplete: "",
|
||||
want: []string{"repo-1"},
|
||||
},
|
||||
{
|
||||
name: "excludes already-typed args",
|
||||
objects: []runtime.Object{
|
||||
&velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "daily", Namespace: "velero"}},
|
||||
&velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "weekly", Namespace: "velero"}},
|
||||
&velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "monthly", Namespace: "velero"}},
|
||||
},
|
||||
list: &velerov1api.BackupList{},
|
||||
args: []string{"daily", "monthly"},
|
||||
toComplete: "",
|
||||
want: []string{"weekly"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
kbClient := velerotest.NewFakeControllerRuntimeClient(t, tc.objects...)
|
||||
|
||||
f := new(factorymocks.Factory)
|
||||
f.On("KubebuilderClient").Return(kbClient, nil)
|
||||
f.On("Namespace").Return("velero")
|
||||
|
||||
completionFn := completeNames(f, tc.list)
|
||||
got, directive := completionFn(&cobra.Command{}, tc.args, tc.toComplete)
|
||||
|
||||
assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive)
|
||||
assert.ElementsMatch(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCompleteNames_KubebuilderClientError verifies that a factory error
|
||||
// (e.g. no kubeconfig) returns nil completions instead of panicking.
|
||||
func TestCompleteNames_KubebuilderClientError(t *testing.T) {
|
||||
f := new(factorymocks.Factory)
|
||||
f.On("KubebuilderClient").Return(nil, fmt.Errorf("connection refused"))
|
||||
|
||||
completionFn := completeNames(f, &velerov1api.BackupList{})
|
||||
got, directive := completionFn(&cobra.Command{}, nil, "")
|
||||
|
||||
assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive)
|
||||
assert.Nil(t, got)
|
||||
}
|
||||
|
||||
// TestCompleteWrappers verifies each exported Complete*Names wrapper returns
|
||||
// only its own resource type. A single fake client holds one object of every
|
||||
// type, so each wrapper must filter correctly and not leak other kinds.
|
||||
func TestCompleteWrappers(t *testing.T) {
|
||||
objects := []runtime.Object{
|
||||
&velerov1api.Backup{ObjectMeta: metav1.ObjectMeta{Name: "b1", Namespace: "velero"}},
|
||||
&velerov1api.Restore{ObjectMeta: metav1.ObjectMeta{Name: "r1", Namespace: "velero"}},
|
||||
&velerov1api.Schedule{ObjectMeta: metav1.ObjectMeta{Name: "s1", Namespace: "velero"}},
|
||||
&velerov1api.BackupStorageLocation{ObjectMeta: metav1.ObjectMeta{Name: "bsl1", Namespace: "velero"}},
|
||||
&velerov1api.VolumeSnapshotLocation{ObjectMeta: metav1.ObjectMeta{Name: "vsl1", Namespace: "velero"}},
|
||||
&velerov1api.BackupRepository{ObjectMeta: metav1.ObjectMeta{Name: "br1", Namespace: "velero"}},
|
||||
}
|
||||
kbClient := velerotest.NewFakeControllerRuntimeClient(t, objects...)
|
||||
|
||||
f := new(factorymocks.Factory)
|
||||
f.On("KubebuilderClient").Return(kbClient, nil)
|
||||
f.On("Namespace").Return("velero")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fn completionFunc
|
||||
expected []string
|
||||
}{
|
||||
{"CompleteBackupNames", CompleteBackupNames(f), []string{"b1"}},
|
||||
{"CompleteRestoreNames", CompleteRestoreNames(f), []string{"r1"}},
|
||||
{"CompleteScheduleNames", CompleteScheduleNames(f), []string{"s1"}},
|
||||
{"CompleteBackupStorageLocationNames", CompleteBackupStorageLocationNames(f), []string{"bsl1"}},
|
||||
{"CompleteVolumeSnapshotLocationNames", CompleteVolumeSnapshotLocationNames(f), []string{"vsl1"}},
|
||||
{"CompleteBackupRepositoryNames", CompleteBackupRepositoryNames(f), []string{"br1"}},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, directive := tc.fn(&cobra.Command{}, nil, "")
|
||||
require.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive)
|
||||
assert.Equal(t, tc.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ package datamover
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/fips140"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -58,6 +59,9 @@ type dataMoverBackupConfig struct {
|
||||
duName string
|
||||
resourceTimeout time.Duration
|
||||
cbtSAName string
|
||||
changeID string
|
||||
volumeID string
|
||||
snapshotID string
|
||||
}
|
||||
|
||||
func NewBackupCommand(f client.Factory) *cobra.Command {
|
||||
@@ -79,12 +83,15 @@ func NewBackupCommand(f client.Factory) *cobra.Command {
|
||||
logger.Infof("Starting Velero data-mover backup %s (%s)", buildinfo.Version, buildinfo.FormattedGitSHA())
|
||||
|
||||
f.SetBasename(fmt.Sprintf("%s-%s", c.Parent().Name(), c.Name()))
|
||||
s, err := newdataMoverBackup(logger, f, config)
|
||||
s, err := newDataMoverBackup(logger, f, config)
|
||||
if err != nil {
|
||||
kube.ExitPodWithMessage(logger, false, "Failed to create data mover backup, %v", err)
|
||||
}
|
||||
|
||||
s.run()
|
||||
// Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet.
|
||||
fips140.WithoutEnforcement(func() {
|
||||
s.run()
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -95,6 +102,9 @@ func NewBackupCommand(f client.Factory) *cobra.Command {
|
||||
command.Flags().StringVar(&config.duName, "data-upload", config.duName, "The data upload name")
|
||||
command.Flags().DurationVar(&config.resourceTimeout, "resource-timeout", config.resourceTimeout, "How long to wait for resource processes which are not covered by other specific timeout parameters.")
|
||||
command.Flags().StringVar(&config.cbtSAName, "cbt-sa-name", config.cbtSAName, "The name of the service account used by CSI's CBT service")
|
||||
command.Flags().StringVar(&config.changeID, "change-id", config.changeID, "The change ID of the snapshot")
|
||||
command.Flags().StringVar(&config.volumeID, "volume-id", config.volumeID, "The volume ID of the snapshot")
|
||||
command.Flags().StringVar(&config.snapshotID, "snapshot-id", config.snapshotID, "The ID of the snapshot")
|
||||
|
||||
_ = command.MarkFlagRequired("volume-path")
|
||||
_ = command.MarkFlagRequired("volume-mode")
|
||||
@@ -118,7 +128,7 @@ type dataMoverBackup struct {
|
||||
cbtService cbtservice.Service
|
||||
}
|
||||
|
||||
func newdataMoverBackup(logger logrus.FieldLogger, factory client.Factory, config dataMoverBackupConfig) (*dataMoverBackup, error) {
|
||||
func newDataMoverBackup(logger logrus.FieldLogger, factory client.Factory, config dataMoverBackupConfig) (*dataMoverBackup, error) {
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
|
||||
clientConfig, err := factory.ClientConfig()
|
||||
@@ -303,8 +313,25 @@ func (s *dataMoverBackup) createDataPathService() (dataPathService, error) {
|
||||
|
||||
repoEnsurer := repository.NewEnsurer(s.client, s.logger, s.config.resourceTimeout)
|
||||
|
||||
return datamover.NewBackupMicroService(s.ctx, s.client, s.kubeClient, s.config.duName, s.namespace, s.nodeName, datapath.AccessPoint{
|
||||
ByPath: s.config.volumePath,
|
||||
VolMode: uploader.PersistentVolumeMode(s.config.volumeMode),
|
||||
}, s.dataPathMgr, repoEnsurer, credGetter, duInformer, s.logger), nil
|
||||
return datamover.NewBackupMicroService(
|
||||
s.ctx,
|
||||
s.client,
|
||||
s.kubeClient,
|
||||
s.config.duName,
|
||||
s.namespace,
|
||||
s.nodeName,
|
||||
datapath.AccessPoint{
|
||||
ByPath: s.config.volumePath,
|
||||
VolMode: uploader.PersistentVolumeMode(s.config.volumeMode),
|
||||
},
|
||||
s.dataPathMgr,
|
||||
repoEnsurer,
|
||||
credGetter,
|
||||
duInformer,
|
||||
s.config.changeID,
|
||||
s.config.volumeID,
|
||||
s.config.snapshotID,
|
||||
s.cbtService,
|
||||
s.logger,
|
||||
), nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ package datamover
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/fips140"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -81,7 +82,10 @@ func NewRestoreCommand(f client.Factory) *cobra.Command {
|
||||
kube.ExitPodWithMessage(logger, false, "Failed to create data mover restore, %v", err)
|
||||
}
|
||||
|
||||
s.run()
|
||||
// Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet.
|
||||
fips140.WithoutEnforcement(func() {
|
||||
s.run()
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
)
|
||||
|
||||
//go:embed cshd-scripts/velero.cshd
|
||||
@@ -171,6 +172,10 @@ specs of resources created by velero server, and optionally the logs of backup a
|
||||
},
|
||||
}
|
||||
o.bindFlags(c.Flags())
|
||||
|
||||
_ = c.RegisterFlagCompletionFunc("backup", cli.CompleteBackupNames(f))
|
||||
_ = c.RegisterFlagCompletionFunc("restore", cli.CompleteRestoreNames(f))
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
|
||||
+117
-105
@@ -42,59 +42,61 @@ import (
|
||||
|
||||
// Options collects all the options for installing Velero into a Kubernetes cluster.
|
||||
type Options struct {
|
||||
Namespace string
|
||||
Image string
|
||||
BucketName string
|
||||
Prefix string
|
||||
ProviderName string
|
||||
PodAnnotations flag.Map
|
||||
PodLabels flag.Map
|
||||
ServiceAccountAnnotations flag.Map
|
||||
ServiceAccountName string
|
||||
VeleroPodCPURequest string
|
||||
VeleroPodMemRequest string
|
||||
VeleroPodCPULimit string
|
||||
VeleroPodMemLimit string
|
||||
NodeAgentPodCPURequest string
|
||||
NodeAgentPodMemRequest string
|
||||
NodeAgentPodCPULimit string
|
||||
NodeAgentPodMemLimit string
|
||||
RestoreOnly bool
|
||||
SecretFile string
|
||||
NoSecret bool
|
||||
DryRun bool
|
||||
BackupStorageConfig flag.Map
|
||||
VolumeSnapshotConfig flag.Map
|
||||
UseNodeAgent bool
|
||||
UseNodeAgentWindows bool
|
||||
PrivilegedNodeAgent bool
|
||||
Wait bool
|
||||
UseVolumeSnapshots bool
|
||||
DefaultRepoMaintenanceFrequency time.Duration
|
||||
GarbageCollectionFrequency time.Duration
|
||||
PodVolumeOperationTimeout time.Duration
|
||||
Plugins flag.StringArray
|
||||
NoDefaultBackupLocation bool
|
||||
CRDsOnly bool
|
||||
CACertFile string
|
||||
Features string
|
||||
DefaultVolumesToFsBackup bool
|
||||
UploaderType string
|
||||
DefaultSnapshotMoveData bool
|
||||
DisableInformerCache bool
|
||||
ScheduleSkipImmediately bool
|
||||
PodResources kubeutil.PodResources
|
||||
KeepLatestMaintenanceJobs int
|
||||
BackupRepoConfigMap string
|
||||
RepoMaintenanceJobConfigMap string
|
||||
NodeAgentConfigMap string
|
||||
ItemBlockWorkerCount int
|
||||
ConcurrentBackups int
|
||||
NodeAgentDisableHostPath bool
|
||||
kubeletRootDir string
|
||||
Apply bool
|
||||
ServerPriorityClassName string
|
||||
NodeAgentPriorityClassName string
|
||||
Namespace string
|
||||
Image string
|
||||
BucketName string
|
||||
Prefix string
|
||||
ProviderName string
|
||||
PodAnnotations flag.Map
|
||||
PodLabels flag.Map
|
||||
ServiceAccountAnnotations flag.Map
|
||||
ServiceAccountName string
|
||||
VeleroPodCPURequest string
|
||||
VeleroPodMemRequest string
|
||||
VeleroPodCPULimit string
|
||||
VeleroPodMemLimit string
|
||||
NodeAgentPodCPURequest string
|
||||
NodeAgentPodMemRequest string
|
||||
NodeAgentPodCPULimit string
|
||||
NodeAgentPodMemLimit string
|
||||
RestoreOnly bool
|
||||
SecretFile string
|
||||
NoSecret bool
|
||||
DryRun bool
|
||||
BackupStorageConfig flag.Map
|
||||
VolumeSnapshotConfig flag.Map
|
||||
UseNodeAgent bool
|
||||
UseNodeAgentWindows bool
|
||||
PrivilegedNodeAgent bool
|
||||
Wait bool
|
||||
UseVolumeSnapshots bool
|
||||
DefaultRepoMaintenanceFrequency time.Duration
|
||||
GarbageCollectionFrequency time.Duration
|
||||
PodVolumeOperationTimeout time.Duration
|
||||
Plugins flag.StringArray
|
||||
NoDefaultBackupLocation bool
|
||||
CRDsOnly bool
|
||||
CACertFile string
|
||||
Features string
|
||||
DefaultVolumesToFsBackup bool
|
||||
UploaderType string
|
||||
DefaultSnapshotMoveData bool
|
||||
CSISnapshotEarlyFrequentPolling bool
|
||||
DisableInformerCache bool
|
||||
ScheduleSkipImmediately bool
|
||||
PodResources kubeutil.PodResources
|
||||
KeepLatestMaintenanceJobs int
|
||||
BackupRepoConfigMap string
|
||||
RepoMaintenanceJobConfigMap string
|
||||
DefaultResourceModifierConfigMap string
|
||||
NodeAgentConfigMap string
|
||||
ItemBlockWorkerCount int
|
||||
ConcurrentBackups int
|
||||
NodeAgentDisableHostPath bool
|
||||
kubeletRootDir string
|
||||
Apply bool
|
||||
ServerPriorityClassName string
|
||||
NodeAgentPriorityClassName string
|
||||
}
|
||||
|
||||
// BindFlags adds command line values to the options struct.
|
||||
@@ -141,6 +143,7 @@ func (o *Options) BindFlags(flags *pflag.FlagSet) {
|
||||
flags.BoolVar(&o.DefaultVolumesToFsBackup, "default-volumes-to-fs-backup", o.DefaultVolumesToFsBackup, "Bool flag to configure Velero server to use pod volume file system backup by default for all volumes on all backups. Optional.")
|
||||
flags.StringVar(&o.UploaderType, "uploader-type", o.UploaderType, fmt.Sprintf("The type of uploader to transfer the data of pod volumes, supported value: '%s'", uploader.KopiaType))
|
||||
flags.BoolVar(&o.DefaultSnapshotMoveData, "default-snapshot-move-data", o.DefaultSnapshotMoveData, "Bool flag to configure Velero server to move data by default for all snapshots supporting data movement. Optional.")
|
||||
flags.BoolVar(&o.CSISnapshotEarlyFrequentPolling, "csi-snapshot-early-frequent-polling", o.CSISnapshotEarlyFrequentPolling, "Bool flag to configure Velero server to use early frequent polling by default for all CSI snapshots. Optional.")
|
||||
flags.BoolVar(&o.DisableInformerCache, "disable-informer-cache", o.DisableInformerCache, "Disable informer cache for Get calls on restore. With this enabled, it will speed up restore in cases where there are backup resources which already exist in the cluster, but for very large clusters this will increase velero memory usage. Default is false (don't disable). Optional.")
|
||||
flags.BoolVar(&o.ScheduleSkipImmediately, "schedule-skip-immediately", o.ScheduleSkipImmediately, "Skip the first scheduled backup immediately after creating a schedule. Default is false (don't skip).")
|
||||
flags.BoolVar(&o.NodeAgentDisableHostPath, "node-agent-disable-host-path", o.NodeAgentDisableHostPath, "Don't mount the pod volume host path to node-agent. Optional. Pod volume host path mount is required by fs-backup but could be disabled for other backup methods.")
|
||||
@@ -187,6 +190,12 @@ func (o *Options) BindFlags(flags *pflag.FlagSet) {
|
||||
o.RepoMaintenanceJobConfigMap,
|
||||
"The name of ConfigMap containing repository maintenance Job configurations.",
|
||||
)
|
||||
flags.StringVar(
|
||||
&o.DefaultResourceModifierConfigMap,
|
||||
"default-resource-modifier-configmap",
|
||||
o.DefaultResourceModifierConfigMap,
|
||||
"The name of a ConfigMap in the Velero namespace containing default resource modifier rules applied to all restores.",
|
||||
)
|
||||
flags.StringVar(
|
||||
&o.NodeAgentConfigMap,
|
||||
"node-agent-configmap",
|
||||
@@ -238,16 +247,17 @@ func NewInstallOptions() *Options {
|
||||
NodeAgentPodCPULimit: install.DefaultNodeAgentPodCPULimit,
|
||||
NodeAgentPodMemLimit: install.DefaultNodeAgentPodMemLimit,
|
||||
// Default to creating a VSL unless we're told otherwise
|
||||
UseVolumeSnapshots: true,
|
||||
NoDefaultBackupLocation: false,
|
||||
CRDsOnly: false,
|
||||
DefaultVolumesToFsBackup: false,
|
||||
UploaderType: uploader.KopiaType,
|
||||
DefaultSnapshotMoveData: false,
|
||||
DisableInformerCache: false,
|
||||
ScheduleSkipImmediately: false,
|
||||
kubeletRootDir: install.DefaultKubeletRootDir,
|
||||
NodeAgentDisableHostPath: false,
|
||||
UseVolumeSnapshots: true,
|
||||
NoDefaultBackupLocation: false,
|
||||
CRDsOnly: false,
|
||||
DefaultVolumesToFsBackup: false,
|
||||
UploaderType: uploader.KopiaType,
|
||||
DefaultSnapshotMoveData: false,
|
||||
CSISnapshotEarlyFrequentPolling: false,
|
||||
DisableInformerCache: false,
|
||||
ScheduleSkipImmediately: false,
|
||||
kubeletRootDir: install.DefaultKubeletRootDir,
|
||||
NodeAgentDisableHostPath: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,48 +305,50 @@ func (o *Options) AsVeleroOptions() (*install.VeleroOptions, error) {
|
||||
}
|
||||
|
||||
return &install.VeleroOptions{
|
||||
Namespace: o.Namespace,
|
||||
Image: o.Image,
|
||||
ProviderName: o.ProviderName,
|
||||
Bucket: o.BucketName,
|
||||
Prefix: o.Prefix,
|
||||
PodAnnotations: o.PodAnnotations.Data(),
|
||||
PodLabels: o.PodLabels.Data(),
|
||||
ServiceAccountAnnotations: o.ServiceAccountAnnotations.Data(),
|
||||
ServiceAccountName: o.ServiceAccountName,
|
||||
VeleroPodResources: veleroPodResources,
|
||||
NodeAgentPodResources: nodeAgentPodResources,
|
||||
SecretData: secretData,
|
||||
RestoreOnly: o.RestoreOnly,
|
||||
UseNodeAgent: o.UseNodeAgent,
|
||||
UseNodeAgentWindows: o.UseNodeAgentWindows,
|
||||
PrivilegedNodeAgent: o.PrivilegedNodeAgent,
|
||||
UseVolumeSnapshots: o.UseVolumeSnapshots,
|
||||
BSLConfig: o.BackupStorageConfig.Data(),
|
||||
VSLConfig: o.VolumeSnapshotConfig.Data(),
|
||||
DefaultRepoMaintenanceFrequency: o.DefaultRepoMaintenanceFrequency,
|
||||
GarbageCollectionFrequency: o.GarbageCollectionFrequency,
|
||||
PodVolumeOperationTimeout: o.PodVolumeOperationTimeout,
|
||||
Plugins: o.Plugins,
|
||||
NoDefaultBackupLocation: o.NoDefaultBackupLocation,
|
||||
CACertData: caCertData,
|
||||
Features: strings.Split(o.Features, ","),
|
||||
DefaultVolumesToFsBackup: o.DefaultVolumesToFsBackup,
|
||||
UploaderType: o.UploaderType,
|
||||
DefaultSnapshotMoveData: o.DefaultSnapshotMoveData,
|
||||
DisableInformerCache: o.DisableInformerCache,
|
||||
ScheduleSkipImmediately: o.ScheduleSkipImmediately,
|
||||
PodResources: o.PodResources,
|
||||
KeepLatestMaintenanceJobs: o.KeepLatestMaintenanceJobs,
|
||||
BackupRepoConfigMap: o.BackupRepoConfigMap,
|
||||
RepoMaintenanceJobConfigMap: o.RepoMaintenanceJobConfigMap,
|
||||
NodeAgentConfigMap: o.NodeAgentConfigMap,
|
||||
ItemBlockWorkerCount: o.ItemBlockWorkerCount,
|
||||
ConcurrentBackups: o.ConcurrentBackups,
|
||||
KubeletRootDir: o.kubeletRootDir,
|
||||
NodeAgentDisableHostPath: o.NodeAgentDisableHostPath,
|
||||
ServerPriorityClassName: o.ServerPriorityClassName,
|
||||
NodeAgentPriorityClassName: o.NodeAgentPriorityClassName,
|
||||
Namespace: o.Namespace,
|
||||
Image: o.Image,
|
||||
ProviderName: o.ProviderName,
|
||||
Bucket: o.BucketName,
|
||||
Prefix: o.Prefix,
|
||||
PodAnnotations: o.PodAnnotations.Data(),
|
||||
PodLabels: o.PodLabels.Data(),
|
||||
ServiceAccountAnnotations: o.ServiceAccountAnnotations.Data(),
|
||||
ServiceAccountName: o.ServiceAccountName,
|
||||
VeleroPodResources: veleroPodResources,
|
||||
NodeAgentPodResources: nodeAgentPodResources,
|
||||
SecretData: secretData,
|
||||
RestoreOnly: o.RestoreOnly,
|
||||
UseNodeAgent: o.UseNodeAgent,
|
||||
UseNodeAgentWindows: o.UseNodeAgentWindows,
|
||||
PrivilegedNodeAgent: o.PrivilegedNodeAgent,
|
||||
UseVolumeSnapshots: o.UseVolumeSnapshots,
|
||||
BSLConfig: o.BackupStorageConfig.Data(),
|
||||
VSLConfig: o.VolumeSnapshotConfig.Data(),
|
||||
DefaultRepoMaintenanceFrequency: o.DefaultRepoMaintenanceFrequency,
|
||||
GarbageCollectionFrequency: o.GarbageCollectionFrequency,
|
||||
PodVolumeOperationTimeout: o.PodVolumeOperationTimeout,
|
||||
Plugins: o.Plugins,
|
||||
NoDefaultBackupLocation: o.NoDefaultBackupLocation,
|
||||
CACertData: caCertData,
|
||||
Features: strings.Split(o.Features, ","),
|
||||
DefaultVolumesToFsBackup: o.DefaultVolumesToFsBackup,
|
||||
UploaderType: o.UploaderType,
|
||||
DefaultSnapshotMoveData: o.DefaultSnapshotMoveData,
|
||||
CSISnapshotEarlyFrequentPolling: o.CSISnapshotEarlyFrequentPolling,
|
||||
DisableInformerCache: o.DisableInformerCache,
|
||||
ScheduleSkipImmediately: o.ScheduleSkipImmediately,
|
||||
PodResources: o.PodResources,
|
||||
KeepLatestMaintenanceJobs: o.KeepLatestMaintenanceJobs,
|
||||
BackupRepoConfigMap: o.BackupRepoConfigMap,
|
||||
RepoMaintenanceJobConfigMap: o.RepoMaintenanceJobConfigMap,
|
||||
DefaultResourceModifierConfigMap: o.DefaultResourceModifierConfigMap,
|
||||
NodeAgentConfigMap: o.NodeAgentConfigMap,
|
||||
ItemBlockWorkerCount: o.ItemBlockWorkerCount,
|
||||
ConcurrentBackups: o.ConcurrentBackups,
|
||||
KubeletRootDir: o.kubeletRootDir,
|
||||
NodeAgentDisableHostPath: o.NodeAgentDisableHostPath,
|
||||
ServerPriorityClassName: o.ServerPriorityClassName,
|
||||
NodeAgentPriorityClassName: o.NodeAgentPriorityClassName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ package podvolume
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/fips140"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -80,7 +81,10 @@ func NewBackupCommand(f client.Factory) *cobra.Command {
|
||||
kube.ExitPodWithMessage(logger, false, "Failed to create pod volume backup, %v", err)
|
||||
}
|
||||
|
||||
s.run()
|
||||
// Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet.
|
||||
fips140.WithoutEnforcement(func() {
|
||||
s.run()
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ package podvolume
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/fips140"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -79,7 +80,10 @@ func NewRestoreCommand(f client.Factory) *cobra.Command {
|
||||
kube.ExitPodWithMessage(logger, false, "Failed to create pod volume restore, %v", err)
|
||||
}
|
||||
|
||||
s.run()
|
||||
// Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet.
|
||||
fips140.WithoutEnforcement(func() {
|
||||
s.run()
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
|
||||
)
|
||||
|
||||
@@ -66,6 +67,7 @@ func NewGetCommand(f client.Factory, use string) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteBackupRepositoryNames(f)
|
||||
c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector.")
|
||||
|
||||
output.BindFlags(c.Flags())
|
||||
|
||||
@@ -2,6 +2,7 @@ package repomantenance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/fips140"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -57,7 +58,10 @@ func NewCommand(f velerocli.Factory) *cobra.Command {
|
||||
Hidden: true,
|
||||
Short: "VELERO INTERNAL COMMAND ONLY - not intended to be run directly by users",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
o.Run(f)
|
||||
// Disable FIPS-140 compliance check, because Kopia doesn't support FIPS-140 yet.
|
||||
fips140.WithoutEnforcement(func() {
|
||||
o.Run(f)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,11 @@ import (
|
||||
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/resourcemodifiers"
|
||||
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/flag"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
@@ -61,7 +63,13 @@ func NewCreateCommand(f client.Factory, use string) *cobra.Command {
|
||||
velero restore create --from-schedule schedule-1 --allow-partially-failed
|
||||
|
||||
# Create a restore for only persistentvolumeclaims and persistentvolumes within a backup.
|
||||
velero restore create --from-backup backup-2 --include-resources persistentvolumeclaims,persistentvolumes`,
|
||||
velero restore create --from-backup backup-2 --include-resources persistentvolumeclaims,persistentvolumes
|
||||
|
||||
Notes:
|
||||
- Global filters (--include-resources, --selector, etc.) apply to all included namespaces
|
||||
- Namespace-scoped filters defined in --resource-policies-configmap refine global filters for matching namespaces (globally excluded kinds cannot be re-included)
|
||||
- Fine-grained global filter policies defined in --resource-policies-configmap refine global filters for cluster-scoped resources
|
||||
- Use 'velero restore describe' to view the referenced resource policies ConfigMap after restore creation`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
cmd.CheckError(o.Complete(args, f))
|
||||
@@ -74,35 +82,40 @@ func NewCreateCommand(f client.Factory, use string) *cobra.Command {
|
||||
output.BindFlags(c.Flags())
|
||||
output.ClearOutputFlagDefault(c)
|
||||
|
||||
_ = c.RegisterFlagCompletionFunc("from-backup", cli.CompleteBackupNames(f))
|
||||
_ = c.RegisterFlagCompletionFunc("from-schedule", cli.CompleteScheduleNames(f))
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
type CreateOptions struct {
|
||||
BackupName string
|
||||
ScheduleName string
|
||||
RestoreName string
|
||||
RestoreVolumes flag.OptionalBool
|
||||
PreserveNodePorts flag.OptionalBool
|
||||
Labels flag.Map
|
||||
Annotations flag.Map
|
||||
IncludeNamespaces flag.StringArray
|
||||
ExcludeNamespaces flag.StringArray
|
||||
ExistingResourcePolicy string
|
||||
IncludeResources flag.StringArray
|
||||
ExcludeResources flag.StringArray
|
||||
StatusIncludeResources flag.StringArray
|
||||
StatusExcludeResources flag.StringArray
|
||||
NamespaceMappings flag.Map
|
||||
Selector flag.LabelSelector
|
||||
OrSelector flag.OrLabelSelector
|
||||
IncludeClusterResources flag.OptionalBool
|
||||
Wait bool
|
||||
AllowPartiallyFailed flag.OptionalBool
|
||||
ItemOperationTimeout time.Duration
|
||||
ResourceModifierConfigMap string
|
||||
WriteSparseFiles flag.OptionalBool
|
||||
ParallelFilesDownload int
|
||||
client kbclient.WithWatch
|
||||
BackupName string
|
||||
ScheduleName string
|
||||
RestoreName string
|
||||
RestoreVolumes flag.OptionalBool
|
||||
PreserveNodePorts flag.OptionalBool
|
||||
Labels flag.Map
|
||||
Annotations flag.Map
|
||||
IncludeNamespaces flag.StringArray
|
||||
ExcludeNamespaces flag.StringArray
|
||||
ExistingResourcePolicy string
|
||||
IncludeResources flag.StringArray
|
||||
ExcludeResources flag.StringArray
|
||||
StatusIncludeResources flag.StringArray
|
||||
StatusExcludeResources flag.StringArray
|
||||
NamespaceMappings flag.Map
|
||||
Selector flag.LabelSelector
|
||||
OrSelector flag.OrLabelSelector
|
||||
IncludeClusterResources flag.OptionalBool
|
||||
Wait bool
|
||||
AllowPartiallyFailed flag.OptionalBool
|
||||
ItemOperationTimeout time.Duration
|
||||
ResourceModifierConfigMap string
|
||||
ResourcePoliciesConfigMap string
|
||||
SkipDefaultResourceModifier bool
|
||||
WriteSparseFiles flag.OptionalBool
|
||||
ParallelFilesDownload int
|
||||
client kbclient.WithWatch
|
||||
}
|
||||
|
||||
func NewCreateOptions() *CreateOptions {
|
||||
@@ -154,6 +167,10 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
|
||||
|
||||
flags.StringVar(&o.ResourceModifierConfigMap, "resource-modifier-configmap", "", "Reference to the resource modifier configmap that restore will use")
|
||||
|
||||
flags.StringVar(&o.ResourcePoliciesConfigMap, "resource-policies-configmap", "", "Reference to the ConfigMap containing restore resource filter policies")
|
||||
|
||||
flags.BoolVar(&o.SkipDefaultResourceModifier, "skip-default-resource-modifier", false, "Skip applying the server-configured default resource modifier for this restore")
|
||||
|
||||
f = flags.VarPF(&o.WriteSparseFiles, "write-sparse-files", "", "Whether to write sparse files during restoring volumes")
|
||||
f.NoOptDefVal = cmd.TRUE
|
||||
|
||||
@@ -310,6 +327,15 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
|
||||
}
|
||||
}
|
||||
|
||||
var resPolicies *corev1api.TypedLocalObjectReference
|
||||
|
||||
if o.ResourcePoliciesConfigMap != "" {
|
||||
resPolicies = &corev1api.TypedLocalObjectReference{
|
||||
Kind: resourcepolicies.ConfigmapRefType,
|
||||
Name: o.ResourcePoliciesConfigMap,
|
||||
}
|
||||
}
|
||||
|
||||
restore := &api.Restore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: f.Namespace(),
|
||||
@@ -332,6 +358,7 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
|
||||
PreserveNodePorts: o.PreserveNodePorts.Value,
|
||||
IncludeClusterResources: o.IncludeClusterResources.Value,
|
||||
ResourceModifier: resModifiers,
|
||||
ResourcePolicy: resPolicies,
|
||||
ItemOperationTimeout: metav1.Duration{
|
||||
Duration: o.ItemOperationTimeout,
|
||||
},
|
||||
@@ -342,6 +369,10 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
|
||||
},
|
||||
}
|
||||
|
||||
if o.SkipDefaultResourceModifier {
|
||||
restore.Spec.SkipDefaultResourceModifier = boolptr.True()
|
||||
}
|
||||
|
||||
if len([]string(o.StatusIncludeResources)) > 0 {
|
||||
restore.Spec.RestoreStatus = &api.RestoreStatusSpec{
|
||||
IncludedResources: o.StatusIncludeResources,
|
||||
|
||||
@@ -77,6 +77,8 @@ func TestCreateCommand(t *testing.T) {
|
||||
includeClusterResources := "true"
|
||||
allowPartiallyFailed := "true"
|
||||
itemOperationTimeout := "10m0s"
|
||||
resourceModifierConfigMap := "modifier-cm"
|
||||
ResourcePoliciesConfigMap := "policies-cm"
|
||||
writeSparseFiles := "true"
|
||||
parallel := 2
|
||||
flags := new(pflag.FlagSet)
|
||||
@@ -101,6 +103,9 @@ func TestCreateCommand(t *testing.T) {
|
||||
flags.Parse([]string{"--include-cluster-resources", includeClusterResources})
|
||||
flags.Parse([]string{"--allow-partially-failed", allowPartiallyFailed})
|
||||
flags.Parse([]string{"--item-operation-timeout", itemOperationTimeout})
|
||||
flags.Parse([]string{"--resource-modifier-configmap", resourceModifierConfigMap})
|
||||
flags.Parse([]string{"--resource-policies-configmap", ResourcePoliciesConfigMap})
|
||||
flags.Parse([]string{"--skip-default-resource-modifier"})
|
||||
flags.Parse([]string{"--write-sparse-files", writeSparseFiles})
|
||||
flags.Parse([]string{"--parallel-files-download", "2"})
|
||||
client := velerotest.NewFakeControllerRuntimeClient(t).(kbclient.WithWatch)
|
||||
@@ -139,6 +144,9 @@ func TestCreateCommand(t *testing.T) {
|
||||
require.Equal(t, includeClusterResources, o.IncludeClusterResources.String())
|
||||
require.Equal(t, allowPartiallyFailed, o.AllowPartiallyFailed.String())
|
||||
require.Equal(t, itemOperationTimeout, o.ItemOperationTimeout.String())
|
||||
require.Equal(t, resourceModifierConfigMap, o.ResourceModifierConfigMap)
|
||||
require.Equal(t, ResourcePoliciesConfigMap, o.ResourcePoliciesConfigMap)
|
||||
require.True(t, o.SkipDefaultResourceModifier)
|
||||
require.Equal(t, writeSparseFiles, o.WriteSparseFiles.String())
|
||||
require.Equal(t, parallel, o.ParallelFilesDownload)
|
||||
})
|
||||
@@ -189,4 +197,37 @@ func TestCreateCommand(t *testing.T) {
|
||||
err := o.Validate(c, []string{}, f)
|
||||
require.Equal(t, "backups.velero.io \"not-exist\" not found", err.Error())
|
||||
})
|
||||
|
||||
t.Run("create a restore with resource policies configmap", func(t *testing.T) {
|
||||
f := &factorymocks.Factory{}
|
||||
c := NewCreateCommand(f, "")
|
||||
require.Equal(t, "Create a restore", c.Short)
|
||||
flags := new(pflag.FlagSet)
|
||||
o := NewCreateOptions()
|
||||
o.BindFlags(flags)
|
||||
|
||||
backupName := "backup-with-policies"
|
||||
ResourcePoliciesConfigMap := "test-policies-cm"
|
||||
flags.Parse([]string{"--from-backup", backupName})
|
||||
flags.Parse([]string{"--resource-policies-configmap", ResourcePoliciesConfigMap})
|
||||
|
||||
kbclient := velerotest.NewFakeControllerRuntimeClient(t).(kbclient.WithWatch)
|
||||
backup := builder.ForBackup(cmdtest.VeleroNameSpace, backupName).Phase(velerov1api.BackupPhaseCompleted).Result()
|
||||
require.NoError(t, kbclient.Create(t.Context(), backup, &controllerclient.CreateOptions{}))
|
||||
|
||||
f.On("Namespace").Return(cmdtest.VeleroNameSpace)
|
||||
f.On("KubebuilderWatchClient").Return(kbclient, nil)
|
||||
|
||||
require.NoError(t, o.Complete(args, f))
|
||||
require.NoError(t, o.Validate(c, []string{}, f))
|
||||
require.NoError(t, o.Run(c, f))
|
||||
|
||||
// Verify the created restore object
|
||||
createdRestore := &velerov1api.Restore{}
|
||||
err := kbclient.Get(t.Context(), controllerclient.ObjectKey{Namespace: cmdtest.VeleroNameSpace, Name: name}, createdRestore)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, createdRestore.Spec.ResourcePolicy)
|
||||
require.Equal(t, "configmap", createdRestore.Spec.ResourcePolicy.Kind)
|
||||
require.Equal(t, ResourcePoliciesConfigMap, createdRestore.Spec.ResourcePolicy.Name)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ func NewDeleteCommand(f client.Factory, use string) *cobra.Command {
|
||||
cmd.CheckError(Run(o))
|
||||
},
|
||||
}
|
||||
c.ValidArgsFunction = cli.CompleteRestoreNames(f)
|
||||
o.BindFlags(c.Flags())
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
|
||||
"github.com/vmware-tanzu/velero/pkg/label"
|
||||
)
|
||||
@@ -92,6 +93,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteRestoreNames(f)
|
||||
c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector.")
|
||||
c.Flags().BoolVar(&details, "details", details, "Display additional detail in the command output.")
|
||||
c.Flags().BoolVar(&insecureSkipTLSVerify, "insecure-skip-tls-verify", insecureSkipTLSVerify, "If true, the object store's TLS certificate will not be checked for validity. This is insecure and susceptible to man-in-the-middle attacks. Not recommended for production.")
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
|
||||
)
|
||||
|
||||
@@ -76,6 +77,7 @@ func NewGetCommand(f client.Factory, use string) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteRestoreNames(f)
|
||||
c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector.")
|
||||
|
||||
output.BindFlags(c.Flags())
|
||||
|
||||
+74
-36
@@ -23,68 +23,106 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/cacert"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest"
|
||||
)
|
||||
|
||||
func NewLogsCommand(f client.Factory) *cobra.Command {
|
||||
// LogsOptions holds the state for the restore logs command, mirroring
|
||||
// pkg/cmd/cli/backup.LogsOptions so both commands are shaped the same way.
|
||||
type LogsOptions struct {
|
||||
Timeout time.Duration
|
||||
InsecureSkipTLSVerify bool
|
||||
CaCertFile string
|
||||
Client kbclient.Client
|
||||
RestoreName string
|
||||
}
|
||||
|
||||
func NewLogsOptions() LogsOptions {
|
||||
config, err := client.LoadConfig()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARNING: Error reading config file: %v\n", err)
|
||||
}
|
||||
|
||||
timeout := time.Minute
|
||||
insecureSkipTLSVerify := false
|
||||
caCertFile := config.CACertFile()
|
||||
return LogsOptions{
|
||||
Timeout: time.Minute,
|
||||
InsecureSkipTLSVerify: false,
|
||||
CaCertFile: config.CACertFile(),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LogsOptions) BindFlags(flags *pflag.FlagSet) {
|
||||
flags.DurationVar(&l.Timeout, "timeout", l.Timeout, "How long to wait to receive logs.")
|
||||
flags.BoolVar(&l.InsecureSkipTLSVerify, "insecure-skip-tls-verify", l.InsecureSkipTLSVerify, "If true, the object store's TLS certificate will not be checked for validity. This is insecure and susceptible to man-in-the-middle attacks. Not recommended for production.")
|
||||
flags.StringVar(&l.CaCertFile, "cacert", l.CaCertFile, "Path to a certificate bundle to use when verifying TLS connections. If not specified, the CA certificate from the BackupStorageLocation will be used if available.")
|
||||
}
|
||||
|
||||
func (l *LogsOptions) Run(c *cobra.Command, f client.Factory) error {
|
||||
restore := new(velerov1api.Restore)
|
||||
err := l.Client.Get(context.Background(), kbclient.ObjectKey{Namespace: f.Namespace(), Name: l.RestoreName}, restore)
|
||||
if apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("restore %q does not exist", l.RestoreName)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error checking for restore %q: %v", l.RestoreName, err)
|
||||
}
|
||||
|
||||
switch restore.Status.Phase {
|
||||
case velerov1api.RestorePhaseCompleted, velerov1api.RestorePhaseFailed, velerov1api.RestorePhasePartiallyFailed, velerov1api.RestorePhaseWaitingForPluginOperations, velerov1api.RestorePhaseWaitingForPluginOperationsPartiallyFailed:
|
||||
// terminal and waiting for plugin operations phases, do nothing.
|
||||
default:
|
||||
return fmt.Errorf("logs for restore %q are not available until it's finished processing, please wait "+
|
||||
"until the restore has a phase of Completed or Failed and try again", l.RestoreName)
|
||||
}
|
||||
|
||||
// Get BSL cacert if available
|
||||
bslCACert, err := cacert.GetCACertFromRestore(context.Background(), l.Client, f.Namespace(), restore)
|
||||
if err != nil {
|
||||
// Log the error but don't fail - we can still try to download without the BSL cacert
|
||||
fmt.Fprintf(os.Stderr, "WARNING: Error getting cacert from BSL: %v\n", err)
|
||||
bslCACert = ""
|
||||
}
|
||||
|
||||
return downloadrequest.StreamWithBSLCACert(context.Background(), l.Client, f.Namespace(), l.RestoreName, velerov1api.DownloadTargetKindRestoreLog, os.Stdout, l.Timeout, l.InsecureSkipTLSVerify, l.CaCertFile, bslCACert)
|
||||
}
|
||||
|
||||
func (l *LogsOptions) Complete(args []string, f client.Factory) error {
|
||||
if len(args) > 0 {
|
||||
l.RestoreName = args[0]
|
||||
}
|
||||
|
||||
kbClient, err := f.KubebuilderClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.Client = kbClient
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewLogsCommand(f client.Factory) *cobra.Command {
|
||||
l := NewLogsOptions()
|
||||
|
||||
c := &cobra.Command{
|
||||
Use: "logs RESTORE",
|
||||
Short: "Get restore logs",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
restoreName := args[0]
|
||||
|
||||
kbClient, err := f.KubebuilderClient()
|
||||
err := l.Complete(args, f)
|
||||
cmd.CheckError(err)
|
||||
|
||||
restore := new(velerov1api.Restore)
|
||||
err = kbClient.Get(context.Background(), ctrlclient.ObjectKey{Namespace: f.Namespace(), Name: restoreName}, restore)
|
||||
if apierrors.IsNotFound(err) {
|
||||
cmd.Exit("Restore %q does not exist.", restoreName)
|
||||
} else if err != nil {
|
||||
cmd.Exit("Error checking for restore %q: %v", restoreName, err)
|
||||
}
|
||||
|
||||
switch restore.Status.Phase {
|
||||
case velerov1api.RestorePhaseCompleted, velerov1api.RestorePhaseFailed, velerov1api.RestorePhasePartiallyFailed, velerov1api.RestorePhaseWaitingForPluginOperations, velerov1api.RestorePhaseWaitingForPluginOperationsPartiallyFailed:
|
||||
// terminal and waiting for plugin operations phases, don't exit.
|
||||
default:
|
||||
cmd.Exit("Logs for restore %q are not available until it's finished processing. Please wait "+
|
||||
"until the restore has a phase of Completed or Failed and try again.", restoreName)
|
||||
}
|
||||
|
||||
// Get BSL cacert if available
|
||||
bslCACert, err := cacert.GetCACertFromRestore(context.Background(), kbClient, f.Namespace(), restore)
|
||||
if err != nil {
|
||||
// Log the error but don't fail - we can still try to download without the BSL cacert
|
||||
fmt.Fprintf(os.Stderr, "WARNING: Error getting cacert from BSL: %v\n", err)
|
||||
bslCACert = ""
|
||||
}
|
||||
|
||||
err = downloadrequest.StreamWithBSLCACert(context.Background(), kbClient, f.Namespace(), restoreName, velerov1api.DownloadTargetKindRestoreLog, os.Stdout, timeout, insecureSkipTLSVerify, caCertFile, bslCACert)
|
||||
err = l.Run(c, f)
|
||||
cmd.CheckError(err)
|
||||
},
|
||||
}
|
||||
|
||||
c.Flags().DurationVar(&timeout, "timeout", timeout, "How long to wait to receive logs.")
|
||||
c.Flags().BoolVar(&insecureSkipTLSVerify, "insecure-skip-tls-verify", insecureSkipTLSVerify, "If true, the object store's TLS certificate will not be checked for validity. This is insecure and susceptible to man-in-the-middle attacks. Not recommended for production.")
|
||||
c.Flags().StringVar(&caCertFile, "cacert", caCertFile, "Path to a certificate bundle to use when verifying TLS connections. If not specified, the CA certificate from the BackupStorageLocation will be used if available.")
|
||||
c.ValidArgsFunction = cli.CompleteRestoreNames(f)
|
||||
l.BindFlags(c.Flags())
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -17,10 +17,12 @@ limitations under the License.
|
||||
package restore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
flag "github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -77,13 +79,20 @@ func TestNewLogsCommand(t *testing.T) {
|
||||
c := NewLogsCommand(f)
|
||||
assert.Equal(t, "Get restore logs", c.Short)
|
||||
|
||||
// The restore command exits with an error message when restore is not complete
|
||||
// We can't easily test this since it calls cmd.Exit, which exits the process
|
||||
// So we'll skip this test case
|
||||
t.Skip("Cannot test restore not complete case due to cmd.Exit() call")
|
||||
l := NewLogsOptions()
|
||||
flags := new(flag.FlagSet)
|
||||
l.BindFlags(flags)
|
||||
err = l.Complete([]string{restoreName}, f)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = l.Run(c, f)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, fmt.Sprintf("logs for restore %q are not available until it's finished processing", restoreName))
|
||||
})
|
||||
|
||||
t.Run("Restore not exist test", func(t *testing.T) {
|
||||
restoreName := "not-exist"
|
||||
|
||||
// create a factory
|
||||
f := &factorymocks.Factory{}
|
||||
|
||||
@@ -95,10 +104,15 @@ func TestNewLogsCommand(t *testing.T) {
|
||||
c := NewLogsCommand(f)
|
||||
assert.Equal(t, "Get restore logs", c.Short)
|
||||
|
||||
// The restore command exits with an error message when restore doesn't exist
|
||||
// We can't easily test this since it calls cmd.Exit, which exits the process
|
||||
// So we'll skip this test case
|
||||
t.Skip("Cannot test restore not exist case due to cmd.Exit() call")
|
||||
l := NewLogsOptions()
|
||||
flags := new(flag.FlagSet)
|
||||
l.BindFlags(flags)
|
||||
err := l.Complete([]string{restoreName}, f)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = l.Run(c, f)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, fmt.Sprintf("restore %q does not exist", restoreName), err.Error())
|
||||
})
|
||||
|
||||
t.Run("Restore with BSL cacert test", func(t *testing.T) {
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli/backup"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
|
||||
)
|
||||
@@ -77,6 +78,9 @@ example: "@every 2h30m".`,
|
||||
output.BindFlags(c.Flags())
|
||||
output.ClearOutputFlagDefault(c)
|
||||
|
||||
_ = c.RegisterFlagCompletionFunc("storage-location", cli.CompleteBackupStorageLocationNames(f))
|
||||
_ = c.RegisterFlagCompletionFunc("volume-snapshot-locations", cli.CompleteVolumeSnapshotLocationNames(f))
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ func NewDeleteCommand(f client.Factory, use string) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteScheduleNames(f)
|
||||
o.BindFlags(c.Flags())
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
|
||||
)
|
||||
|
||||
@@ -73,6 +74,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteScheduleNames(f)
|
||||
c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector.")
|
||||
|
||||
return c
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
|
||||
)
|
||||
|
||||
@@ -71,6 +72,7 @@ func NewGetCommand(f client.Factory, use string) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteScheduleNames(f)
|
||||
c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector.")
|
||||
|
||||
output.BindFlags(c.Flags())
|
||||
|
||||
@@ -60,6 +60,7 @@ func NewPauseCommand(f client.Factory, use string) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteScheduleNames(f)
|
||||
o.BindFlags(c.Flags())
|
||||
pauseOpts.BindFlags(c.Flags())
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ func NewUnpauseCommand(f client.Factory, use string) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteScheduleNames(f)
|
||||
o.BindFlags(c.Flags())
|
||||
pauseOpts.BindFlags(c.Flags())
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
|
||||
)
|
||||
|
||||
@@ -56,6 +57,7 @@ func NewGetCommand(f client.Factory, use string) *cobra.Command {
|
||||
cmd.CheckError(err)
|
||||
},
|
||||
}
|
||||
c.ValidArgsFunction = cli.CompleteVolumeSnapshotLocationNames(f)
|
||||
c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "Only show items matching this label selector")
|
||||
output.BindFlags(c.Flags())
|
||||
return c
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/cli"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/flag"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd/util/output"
|
||||
)
|
||||
@@ -48,6 +49,7 @@ func NewSetCommand(f client.Factory, use string) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
c.ValidArgsFunction = cli.CompleteVolumeSnapshotLocationNames(f)
|
||||
o.BindFlags(c.Flags())
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -182,6 +182,7 @@ type Config struct {
|
||||
ItemBlockWorkerCount int
|
||||
ConcurrentBackups int
|
||||
GlobalBackupVolumePoliciesConfigMap string
|
||||
DefaultResourceModifierConfigMap string
|
||||
}
|
||||
|
||||
func GetDefaultConfig() *Config {
|
||||
@@ -282,4 +283,10 @@ func (c *Config) BindFlags(flags *pflag.FlagSet) {
|
||||
c.GlobalBackupVolumePoliciesConfigMap,
|
||||
"The name of a ConfigMap in the Velero install namespace holding global backup volume policies that are merged into every backup. Optional.",
|
||||
)
|
||||
flags.StringVar(
|
||||
&c.DefaultResourceModifierConfigMap,
|
||||
"default-resource-modifier-configmap",
|
||||
c.DefaultResourceModifierConfigMap,
|
||||
"The name of a ConfigMap in the Velero namespace containing default resource modifier rules applied to all restores. Ignored when a per-restore resource modifier is specified.",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -881,6 +881,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
|
||||
s.config.DisableInformerCache,
|
||||
s.crClient,
|
||||
s.config.ResourceTimeout,
|
||||
s.config.DefaultResourceModifierConfigMap,
|
||||
)
|
||||
|
||||
if err = r.SetupWithManager(s.mgr); err != nil {
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -31,7 +30,6 @@ import (
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/fatih/color"
|
||||
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -94,9 +92,6 @@ func DescribeBackup(
|
||||
if backup.Spec.ResourcePolicy != nil {
|
||||
d.Println()
|
||||
DescribeResourcePolicies(d, backup.Spec.ResourcePolicy)
|
||||
|
||||
// Display fine-grained filter policies if they exist
|
||||
DescribeFineGrainedFilterPolicies(ctx, kbClient, d, backup)
|
||||
}
|
||||
|
||||
DescribeGlobalVolumePolicy(d, backup)
|
||||
@@ -151,119 +146,6 @@ func DescribeGlobalVolumePolicy(d *Describer, backup *velerov1api.Backup) {
|
||||
d.Printf("\tName:\t%s\n", name)
|
||||
}
|
||||
|
||||
// DescribeFineGrainedFilterPolicies describes cluster-scoped and namespace-scoped filter policies if present
|
||||
func DescribeFineGrainedFilterPolicies(ctx context.Context, kbClient kbclient.Client, d *Describer, backup *velerov1api.Backup) {
|
||||
if backup.Spec.ResourcePolicy == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Create a discard logger for the resource policies function since this is CLI output context
|
||||
discardLogger := logrus.New()
|
||||
discardLogger.Out = io.Discard
|
||||
|
||||
resourcePolicies, err := resourcepolicies.GetResourcePoliciesFromBackup(*backup, kbClient, discardLogger)
|
||||
if err != nil {
|
||||
// Don't fail the describe if we can't read policies, just skip
|
||||
return
|
||||
}
|
||||
|
||||
if resourcePolicies == nil {
|
||||
return
|
||||
}
|
||||
|
||||
clusterScopedFilterPolicy := resourcePolicies.GetClusterScopedFilterPolicy()
|
||||
if clusterScopedFilterPolicy != nil {
|
||||
d.Printf("\nCluster Scoped Filter Policy:\n")
|
||||
d.Printf(" Resource Filters:\n")
|
||||
for _, rf := range clusterScopedFilterPolicy.ResourceFilters {
|
||||
kindsStr := strings.Join(rf.Kinds, ", ")
|
||||
d.Printf(" %s:\n", kindsStr)
|
||||
|
||||
// Label selector
|
||||
if len(rf.LabelSelector) > 0 {
|
||||
selectorStr := formatLabelMap(rf.LabelSelector)
|
||||
d.Printf(" Label selector: %s\n", selectorStr)
|
||||
} else if len(rf.OrLabelSelectors) > 0 {
|
||||
var orStrs []string
|
||||
for _, ols := range rf.OrLabelSelectors {
|
||||
orStrs = append(orStrs, formatLabelMap(ols))
|
||||
}
|
||||
d.Printf(" OR label selectors: [%s]\n", strings.Join(orStrs, ", "))
|
||||
} else {
|
||||
d.Printf(" Label selector: <none>\n")
|
||||
}
|
||||
|
||||
// Name patterns
|
||||
if len(rf.Names) > 0 {
|
||||
d.Printf(" Included names: [%s]\n", strings.Join(rf.Names, ", "))
|
||||
} else {
|
||||
d.Printf(" Included names: <none>\n")
|
||||
}
|
||||
|
||||
if len(rf.ExcludedNames) > 0 {
|
||||
d.Printf(" Excluded names: [%s]\n", strings.Join(rf.ExcludedNames, ", "))
|
||||
} else {
|
||||
d.Printf(" Excluded names: <none>\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nfPolicies := resourcePolicies.GetNamespacedFilterPolicies()
|
||||
if len(nfPolicies) > 0 {
|
||||
d.Printf("\nNamespace-Scoped Filter Policies:\n")
|
||||
for _, policy := range nfPolicies {
|
||||
for _, ns := range policy.Namespaces {
|
||||
d.Printf(" %s:\n", ns)
|
||||
d.Printf(" Resource Filters:\n")
|
||||
for _, rf := range policy.ResourceFilters {
|
||||
var kindsStr string
|
||||
if rf.IsCatchAll() {
|
||||
kindsStr = "<catch-all> (all other kinds)"
|
||||
} else {
|
||||
kindsStr = strings.Join(rf.Kinds, ", ")
|
||||
}
|
||||
d.Printf(" %s:\n", kindsStr)
|
||||
|
||||
// Label selector
|
||||
if len(rf.LabelSelector) > 0 {
|
||||
selectorStr := formatLabelMap(rf.LabelSelector)
|
||||
d.Printf(" Label selector: %s\n", selectorStr)
|
||||
} else if len(rf.OrLabelSelectors) > 0 {
|
||||
var orStrs []string
|
||||
for _, ols := range rf.OrLabelSelectors {
|
||||
orStrs = append(orStrs, formatLabelMap(ols))
|
||||
}
|
||||
d.Printf(" OR label selectors: [%s]\n", strings.Join(orStrs, ", "))
|
||||
} else {
|
||||
d.Printf(" Label selector: <none>\n")
|
||||
}
|
||||
|
||||
// Name patterns
|
||||
if len(rf.Names) > 0 {
|
||||
d.Printf(" Included names: [%s]\n", strings.Join(rf.Names, ", "))
|
||||
} else {
|
||||
d.Printf(" Included names: <none>\n")
|
||||
}
|
||||
|
||||
if len(rf.ExcludedNames) > 0 {
|
||||
d.Printf(" Excluded names: [%s]\n", strings.Join(rf.ExcludedNames, ", "))
|
||||
} else {
|
||||
d.Printf(" Excluded names: <none>\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func formatLabelMap(labelMap map[string]string) string {
|
||||
var pairs []string
|
||||
for k, v := range labelMap {
|
||||
pairs = append(pairs, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
return strings.Join(pairs, ",")
|
||||
}
|
||||
|
||||
// DescribeUploaderConfigForBackup describes uploader config in human-readable format
|
||||
func DescribeUploaderConfigForBackup(d *Describer, spec velerov1api.BackupSpec) {
|
||||
d.Printf("Uploader config:\n")
|
||||
|
||||
@@ -18,7 +18,6 @@ package output
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
@@ -26,8 +25,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/volume"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
@@ -897,85 +894,3 @@ func TestDescribeBackupItemOperation(t *testing.T) {
|
||||
d.out.Flush()
|
||||
assert.Equal(t, expected, d.buf.String())
|
||||
}
|
||||
|
||||
func TestDescribeFineGrainedFilterPolicies(t *testing.T) {
|
||||
yamlData := `
|
||||
version: v1
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters:
|
||||
- kinds: ["StorageClass"]
|
||||
labelSelector: {"app": "velero"}
|
||||
- kinds: ["ClusterRole"]
|
||||
orLabelSelectors:
|
||||
- {"app": "velero"}
|
||||
- {"app": "test"}
|
||||
names: ["role1"]
|
||||
excludedNames: ["role2"]
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["ns1", "ns2"]
|
||||
resourceFilters:
|
||||
- kinds: ["Pod", "ConfigMap"]
|
||||
labelSelector: {"app": "velero"}
|
||||
- kinds: ["*"]
|
||||
`
|
||||
cm := &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-policy",
|
||||
Namespace: "velero",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"policy.yaml": yamlData,
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewClientBuilder().WithRuntimeObjects(cm).Build()
|
||||
|
||||
backup := builder.ForBackup("velero", "test-backup").
|
||||
ResourcePolicies("test-policy").Result()
|
||||
|
||||
d := &Describer{
|
||||
Prefix: "",
|
||||
out: &tabwriter.Writer{},
|
||||
buf: &bytes.Buffer{},
|
||||
}
|
||||
d.out.Init(d.buf, 0, 8, 2, ' ', 0)
|
||||
|
||||
DescribeFineGrainedFilterPolicies(context.Background(), client, d, backup)
|
||||
d.out.Flush()
|
||||
|
||||
expected := `
|
||||
Cluster Scoped Filter Policy:
|
||||
Resource Filters:
|
||||
StorageClass:
|
||||
Label selector: app=velero
|
||||
Included names: <none>
|
||||
Excluded names: <none>
|
||||
ClusterRole:
|
||||
OR label selectors: [app=velero, app=test]
|
||||
Included names: [role1]
|
||||
Excluded names: [role2]
|
||||
|
||||
Namespace-Scoped Filter Policies:
|
||||
ns1:
|
||||
Resource Filters:
|
||||
Pod, ConfigMap:
|
||||
Label selector: app=velero
|
||||
Included names: <none>
|
||||
Excluded names: <none>
|
||||
<catch-all> (all other kinds):
|
||||
Label selector: <none>
|
||||
Included names: <none>
|
||||
Excluded names: <none>
|
||||
ns2:
|
||||
Resource Filters:
|
||||
Pod, ConfigMap:
|
||||
Label selector: app=velero
|
||||
Included names: <none>
|
||||
Excluded names: <none>
|
||||
<catch-all> (all other kinds):
|
||||
Label selector: <none>
|
||||
Included names: <none>
|
||||
Excluded names: <none>
|
||||
`
|
||||
assert.Equal(t, expected, d.buf.String())
|
||||
}
|
||||
|
||||
@@ -21,10 +21,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
@@ -57,7 +55,6 @@ func DescribeBackupInSF(
|
||||
|
||||
if backup.Spec.ResourcePolicy != nil {
|
||||
DescribeResourcePoliciesInSF(d, backup.Spec.ResourcePolicy)
|
||||
DescribeFineGrainedFilterPoliciesInSF(ctx, kbClient, d, backup)
|
||||
}
|
||||
|
||||
DescribeGlobalVolumePolicyInSF(d, backup)
|
||||
@@ -228,88 +225,6 @@ func DescribeBackupSpecInSF(d *StructuredDescriber, spec velerov1api.BackupSpec)
|
||||
d.Describe("spec", backupSpecInfo)
|
||||
}
|
||||
|
||||
// DescribeFineGrainedFilterPoliciesInSF adds the clusterScopedFilterPolicy
|
||||
// and namespacedFilterPolicies sections to the structured describer output when present
|
||||
// in the ResourcePolicy ConfigMap referenced by the backup.
|
||||
func DescribeFineGrainedFilterPoliciesInSF(ctx context.Context, kbClient kbclient.Client, d *StructuredDescriber, backup *velerov1api.Backup) {
|
||||
if backup.Spec.ResourcePolicy == nil {
|
||||
return
|
||||
}
|
||||
|
||||
discardLogger := logrus.New()
|
||||
discardLogger.Out = io.Discard
|
||||
|
||||
resPolicies, err := resourcepolicies.GetResourcePoliciesFromBackup(*backup, kbClient, discardLogger)
|
||||
if err != nil || resPolicies == nil {
|
||||
return
|
||||
}
|
||||
|
||||
clusterScopedFilterPolicy := resPolicies.GetClusterScopedFilterPolicy()
|
||||
if clusterScopedFilterPolicy != nil {
|
||||
var clusterScopedFilters []map[string]any
|
||||
for _, rf := range clusterScopedFilterPolicy.ResourceFilters {
|
||||
entry := map[string]any{
|
||||
"kinds": rf.Kinds,
|
||||
}
|
||||
if len(rf.LabelSelector) > 0 {
|
||||
entry["labelSelector"] = rf.LabelSelector
|
||||
}
|
||||
if len(rf.OrLabelSelectors) > 0 {
|
||||
entry["orLabelSelectors"] = rf.OrLabelSelectors
|
||||
}
|
||||
if len(rf.Names) > 0 {
|
||||
entry["names"] = rf.Names
|
||||
}
|
||||
if len(rf.ExcludedNames) > 0 {
|
||||
entry["excludedNames"] = rf.ExcludedNames
|
||||
}
|
||||
clusterScopedFilters = append(clusterScopedFilters, entry)
|
||||
}
|
||||
d.Describe("clusterScopedFilterPolicy", map[string]any{
|
||||
"resourceFilters": clusterScopedFilters,
|
||||
})
|
||||
}
|
||||
|
||||
nfPolicies := resPolicies.GetNamespacedFilterPolicies()
|
||||
if len(nfPolicies) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var structuredPolicies []map[string]any
|
||||
for _, policy := range nfPolicies {
|
||||
for _, ns := range policy.Namespaces {
|
||||
var rfEntries []map[string]any
|
||||
for _, rf := range policy.ResourceFilters {
|
||||
entry := map[string]any{}
|
||||
if rf.IsCatchAll() {
|
||||
entry["kinds"] = []string{}
|
||||
entry["isCatchAll"] = true
|
||||
} else {
|
||||
entry["kinds"] = rf.Kinds
|
||||
}
|
||||
if len(rf.LabelSelector) > 0 {
|
||||
entry["labelSelector"] = rf.LabelSelector
|
||||
}
|
||||
if len(rf.OrLabelSelectors) > 0 {
|
||||
entry["orLabelSelectors"] = rf.OrLabelSelectors
|
||||
}
|
||||
if len(rf.Names) > 0 {
|
||||
entry["names"] = rf.Names
|
||||
}
|
||||
if len(rf.ExcludedNames) > 0 {
|
||||
entry["excludedNames"] = rf.ExcludedNames
|
||||
}
|
||||
rfEntries = append(rfEntries, entry)
|
||||
}
|
||||
structuredPolicies = append(structuredPolicies, map[string]any{
|
||||
"namespace": ns,
|
||||
"resourceFilters": rfEntries,
|
||||
})
|
||||
}
|
||||
}
|
||||
d.Describe("namespacedFilterPolicies", structuredPolicies)
|
||||
}
|
||||
|
||||
// DescribeBackupStatusInSF describes a backup status in structured format.
|
||||
func DescribeBackupStatusInSF(ctx context.Context, kbClient kbclient.Client, d *StructuredDescriber, backup *velerov1api.Backup, details bool,
|
||||
insecureSkipTLSVerify bool, caCertPath string, podVolumeBackups []velerov1api.PodVolumeBackup) {
|
||||
|
||||
@@ -17,7 +17,6 @@ limitations under the License.
|
||||
package output
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -25,8 +24,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/volume"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
@@ -731,96 +728,3 @@ func TestDescribeDeleteBackupRequestsInSF(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescribeFineGrainedFilterPoliciesInSF(t *testing.T) {
|
||||
yamlData := `
|
||||
version: v1
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters:
|
||||
- kinds: ["StorageClass"]
|
||||
labelSelector: {"app": "velero"}
|
||||
- kinds: ["ClusterRole"]
|
||||
orLabelSelectors:
|
||||
- {"app": "velero"}
|
||||
- {"app": "test"}
|
||||
names: ["role1"]
|
||||
excludedNames: ["role2"]
|
||||
namespacedFilterPolicies:
|
||||
- namespaces: ["ns1", "ns2"]
|
||||
resourceFilters:
|
||||
- kinds: ["Pod", "ConfigMap"]
|
||||
labelSelector: {"app": "velero"}
|
||||
- kinds: ["*"]
|
||||
`
|
||||
cm := &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-policy",
|
||||
Namespace: "velero",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"policy.yaml": yamlData,
|
||||
},
|
||||
}
|
||||
|
||||
client := fake.NewClientBuilder().WithRuntimeObjects(cm).Build()
|
||||
|
||||
backup := builder.ForBackup("velero", "test-backup").
|
||||
ResourcePolicies("test-policy").Result()
|
||||
|
||||
sd := &StructuredDescriber{
|
||||
output: make(map[string]any),
|
||||
format: "",
|
||||
}
|
||||
|
||||
DescribeFineGrainedFilterPoliciesInSF(context.Background(), client, sd, backup)
|
||||
|
||||
expect := map[string]any{
|
||||
"clusterScopedFilterPolicy": map[string]any{
|
||||
"resourceFilters": []map[string]any{
|
||||
{
|
||||
"kinds": []string{"StorageClass"},
|
||||
"labelSelector": map[string]string{"app": "velero"},
|
||||
},
|
||||
{
|
||||
"kinds": []string{"ClusterRole"},
|
||||
"orLabelSelectors": []map[string]string{
|
||||
{"app": "velero"},
|
||||
{"app": "test"},
|
||||
},
|
||||
"names": []string{"role1"},
|
||||
"excludedNames": []string{"role2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"namespacedFilterPolicies": []map[string]any{
|
||||
{
|
||||
"namespace": "ns1",
|
||||
"resourceFilters": []map[string]any{
|
||||
{
|
||||
"kinds": []string{"Pod", "ConfigMap"},
|
||||
"labelSelector": map[string]string{"app": "velero"},
|
||||
},
|
||||
{
|
||||
"kinds": []string{},
|
||||
"isCatchAll": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"namespace": "ns2",
|
||||
"resourceFilters": []map[string]any{
|
||||
{
|
||||
"kinds": []string{"Pod", "ConfigMap"},
|
||||
"labelSelector": map[string]string{"app": "velero"},
|
||||
},
|
||||
{
|
||||
"kinds": []string{},
|
||||
"isCatchAll": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.True(t, reflect.DeepEqual(sd.output, expect))
|
||||
}
|
||||
|
||||
@@ -219,6 +219,15 @@ func DescribeRestore(
|
||||
DescribeResourceModifier(d, restore.Spec.ResourceModifier)
|
||||
}
|
||||
|
||||
if boolptr.IsSetToTrue(restore.Spec.SkipDefaultResourceModifier) {
|
||||
d.Printf("Skip Default Resource Modifier:\ttrue\n")
|
||||
}
|
||||
|
||||
if restore.Spec.ResourcePolicy != nil {
|
||||
d.Println()
|
||||
DescribeResourcePolicies(d, restore.Spec.ResourcePolicy)
|
||||
}
|
||||
|
||||
describeUploaderConfigForRestore(d, restore.Spec)
|
||||
|
||||
d.Println()
|
||||
|
||||
@@ -58,6 +58,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/framework"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/collections"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/datamover"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/encode"
|
||||
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/logging"
|
||||
@@ -84,33 +85,34 @@ var autoExcludeClusterScopedResources = []string{
|
||||
}
|
||||
|
||||
type backupReconciler struct {
|
||||
ctx context.Context
|
||||
logger logrus.FieldLogger
|
||||
discoveryHelper discovery.Helper
|
||||
backupper pkgbackup.Backupper
|
||||
kbClient kbclient.Client
|
||||
clock clock.WithTickerAndDelayedExecution
|
||||
backupLogLevel logrus.Level
|
||||
newPluginManager func(logrus.FieldLogger) clientmgmt.Manager
|
||||
backupTracker BackupTracker
|
||||
defaultBackupLocation string
|
||||
defaultVolumesToFsBackup bool
|
||||
defaultBackupTTL time.Duration
|
||||
defaultVGSLabelKey string
|
||||
defaultCSISnapshotTimeout time.Duration
|
||||
resourceTimeout time.Duration
|
||||
defaultItemOperationTimeout time.Duration
|
||||
defaultSnapshotLocations map[string]string
|
||||
metrics *metrics.ServerMetrics
|
||||
backupStoreGetter persistence.ObjectBackupStoreGetter
|
||||
formatFlag logging.Format
|
||||
credentialFileStore credentials.FileStore
|
||||
maxConcurrentK8SConnections int
|
||||
defaultSnapshotMoveData bool
|
||||
globalCRClient kbclient.Client
|
||||
itemBlockWorkerCount int
|
||||
concurrentBackups int
|
||||
globalVolumePoliciesConfigMap string
|
||||
ctx context.Context
|
||||
logger logrus.FieldLogger
|
||||
discoveryHelper discovery.Helper
|
||||
backupper pkgbackup.Backupper
|
||||
kbClient kbclient.Client
|
||||
clock clock.WithTickerAndDelayedExecution
|
||||
backupLogLevel logrus.Level
|
||||
newPluginManager func(logrus.FieldLogger) clientmgmt.Manager
|
||||
backupTracker BackupTracker
|
||||
defaultBackupLocation string
|
||||
defaultVolumesToFsBackup bool
|
||||
defaultBackupTTL time.Duration
|
||||
defaultVGSLabelKey string
|
||||
defaultCSISnapshotTimeout time.Duration
|
||||
resourceTimeout time.Duration
|
||||
defaultItemOperationTimeout time.Duration
|
||||
defaultSnapshotLocations map[string]string
|
||||
metrics *metrics.ServerMetrics
|
||||
backupStoreGetter persistence.ObjectBackupStoreGetter
|
||||
formatFlag logging.Format
|
||||
credentialFileStore credentials.FileStore
|
||||
maxConcurrentK8SConnections int
|
||||
defaultSnapshotMoveData bool
|
||||
globalCRClient kbclient.Client
|
||||
itemBlockWorkerCount int
|
||||
concurrentBackups int
|
||||
globalVolumePoliciesConfigMap string
|
||||
knownSchedulesWithSuccessfulBackup sets.Set[string]
|
||||
}
|
||||
|
||||
func NewBackupReconciler(
|
||||
@@ -204,28 +206,43 @@ func (b *backupReconciler) updateTotalBackupMetric() {
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
wait.Until(
|
||||
func() {
|
||||
// recompute backup_total metric
|
||||
backups := &velerov1api.BackupList{}
|
||||
err := b.kbClient.List(context.Background(), backups, &kbclient.ListOptions{LabelSelector: labels.Everything()})
|
||||
if err != nil {
|
||||
b.logger.Error(err, "Error computing backup_total metric")
|
||||
} else {
|
||||
b.metrics.SetBackupTotal(int64(len(backups.Items)))
|
||||
}
|
||||
|
||||
// recompute backup_last_successful_timestamp metric for each
|
||||
// schedule (including the empty schedule, i.e. ad-hoc backups)
|
||||
for schedule, timestamp := range getLastSuccessBySchedule(backups.Items) {
|
||||
b.metrics.SetBackupLastSuccessfulTimestamp(schedule, timestamp)
|
||||
}
|
||||
},
|
||||
b.resyncBackupMetrics,
|
||||
backupResyncPeriod,
|
||||
b.ctx.Done(),
|
||||
)
|
||||
}()
|
||||
}
|
||||
|
||||
func (b *backupReconciler) resyncBackupMetrics() {
|
||||
backups := &velerov1api.BackupList{}
|
||||
err := b.kbClient.List(context.Background(), backups, &kbclient.ListOptions{LabelSelector: labels.Everything()})
|
||||
if err != nil {
|
||||
b.logger.Error(err, "Error computing backup_total metric")
|
||||
return
|
||||
}
|
||||
|
||||
b.metrics.SetBackupTotal(int64(len(backups.Items)))
|
||||
|
||||
currentSchedules := getLastSuccessBySchedule(backups.Items)
|
||||
for schedule, timestamp := range currentSchedules {
|
||||
b.metrics.SetBackupLastSuccessfulTimestamp(schedule, timestamp)
|
||||
}
|
||||
|
||||
// Remove metrics for schedules that no longer have successful backups
|
||||
if b.knownSchedulesWithSuccessfulBackup != nil {
|
||||
for schedule := range b.knownSchedulesWithSuccessfulBackup {
|
||||
if _, exists := currentSchedules[schedule]; !exists {
|
||||
b.metrics.DeleteBackupLastSuccessfulTimestamp(schedule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.knownSchedulesWithSuccessfulBackup = sets.New[string]()
|
||||
for schedule := range currentSchedules {
|
||||
b.knownSchedulesWithSuccessfulBackup.Insert(schedule)
|
||||
}
|
||||
}
|
||||
|
||||
// getLastSuccessBySchedule finds the most recent completed backup for each schedule
|
||||
// and returns a map of schedule name -> completion time of the most recent completed
|
||||
// backup. This map includes an entry for ad-hoc/non-scheduled backups, where the key
|
||||
@@ -410,6 +427,15 @@ func (b *backupReconciler) prepareBackupRequest(ctx context.Context, backup *vel
|
||||
request.Spec.ItemOperationTimeout.Duration = b.defaultItemOperationTimeout
|
||||
}
|
||||
|
||||
if len(request.Spec.BackupType) == 0 {
|
||||
// default backup type to incremental if not specified
|
||||
request.Spec.BackupType = velerov1api.BackupTypeIncremental
|
||||
}
|
||||
|
||||
if len(request.Spec.DataMover) == 0 || request.Spec.DataMover == datamover.DataMoverTypeVelero {
|
||||
request.Spec.DataMover = datamover.GetDefaultBuiltInDataMover()
|
||||
}
|
||||
|
||||
// calculate expiration
|
||||
request.Status.Expiration = &metav1.Time{Time: b.clock.Now().Add(request.Spec.TTL.Duration)}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
@@ -63,6 +64,7 @@ import (
|
||||
ibav1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/itemblockaction/v1"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/datamover"
|
||||
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/logging"
|
||||
)
|
||||
@@ -524,6 +526,63 @@ func TestDefaultBackupTTL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareBackupRequest_SetBackupType(t *testing.T) {
|
||||
now, err := time.Parse(time.RFC1123Z, time.RFC1123Z)
|
||||
require.NoError(t, err)
|
||||
now = now.Local()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
backup *velerov1api.Backup
|
||||
expectedBackupType velerov1api.BackupType
|
||||
}{
|
||||
{
|
||||
name: "default backup type is Incremental",
|
||||
backup: defaultBackup().Result(),
|
||||
expectedBackupType: velerov1api.BackupTypeIncremental,
|
||||
},
|
||||
{
|
||||
name: "backup type is set to Full",
|
||||
backup: defaultBackup().BackupType(velerov1api.BackupTypeFull).Result(),
|
||||
expectedBackupType: velerov1api.BackupTypeFull,
|
||||
},
|
||||
{
|
||||
name: "backup type is set to Incremental",
|
||||
backup: defaultBackup().BackupType(velerov1api.BackupTypeIncremental).Result(),
|
||||
expectedBackupType: velerov1api.BackupTypeIncremental,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
formatFlag := logging.FormatText
|
||||
var (
|
||||
fakeClient kbclient.Client
|
||||
logger = logging.DefaultLogger(logrus.DebugLevel, formatFlag)
|
||||
)
|
||||
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
apiServer := velerotest.NewAPIServer(t)
|
||||
discoveryHelper, err := discovery.NewHelper(apiServer.DiscoveryClient, logger)
|
||||
require.NoError(t, err)
|
||||
// add the test's backup storage location if it's different than the default
|
||||
fakeClient = velerotest.NewFakeControllerRuntimeClient(t)
|
||||
c := &backupReconciler{
|
||||
logger: logger,
|
||||
discoveryHelper: discoveryHelper,
|
||||
kbClient: fakeClient,
|
||||
formatFlag: formatFlag,
|
||||
clock: testclocks.NewFakeClock(now),
|
||||
}
|
||||
|
||||
res := c.prepareBackupRequest(ctx, test.backup, logger)
|
||||
defer res.WorkerPool.Stop()
|
||||
assert.NotNil(t, res)
|
||||
|
||||
assert.Equal(t, test.expectedBackupType, res.Spec.BackupType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareBackupRequest_SetsVGSLabelKey(t *testing.T) {
|
||||
now, err := time.Parse(time.RFC1123Z, time.RFC1123Z)
|
||||
require.NoError(t, err)
|
||||
@@ -746,6 +805,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.False(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -786,6 +847,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.False(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -830,6 +893,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.False(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -871,6 +936,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.False(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -912,6 +979,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.False(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -954,6 +1023,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.False(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -996,6 +1067,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.False(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -1038,6 +1111,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.False(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -1080,6 +1155,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.False(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -1123,6 +1200,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.False(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFailed,
|
||||
@@ -1166,6 +1245,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.False(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFailed,
|
||||
@@ -1209,6 +1290,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.True(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -1253,6 +1336,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.False(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -1297,6 +1382,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.False(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -1341,6 +1428,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.True(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -1386,6 +1475,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.False(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -1430,6 +1521,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
SnapshotMoveData: boolptr.True(),
|
||||
ExcludedClusterScopedResources: autoExcludeClusterScopedResources,
|
||||
ExcludedNamespaceScopedResources: autoExcludeNamespaceScopedResources,
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -1480,6 +1573,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
ExcludedClusterScopedResources: append([]string{"clusterroles"}, autoExcludeClusterScopedResources...),
|
||||
IncludedNamespaceScopedResources: []string{"pods"},
|
||||
ExcludedNamespaceScopedResources: append([]string{"secrets"}, autoExcludeNamespaceScopedResources...),
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -1530,6 +1625,8 @@ func TestProcessBackupCompletions(t *testing.T) {
|
||||
ExcludedClusterScopedResources: append([]string{"clusterroles"}, autoExcludeClusterScopedResources...),
|
||||
IncludedNamespaceScopedResources: []string{"pods"},
|
||||
ExcludedNamespaceScopedResources: append([]string{"secrets"}, autoExcludeNamespaceScopedResources...),
|
||||
BackupType: velerov1api.BackupTypeIncremental,
|
||||
DataMover: datamover.GetDefaultBuiltInDataMover(),
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseFinalizing,
|
||||
@@ -1965,6 +2062,48 @@ func Test_getLastSuccessBySchedule(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test_resyncBackupMetrics_prunesStaleTimestamps verifies that resyncBackupMetrics
|
||||
// removes backupLastSuccessfulTimestamp entries for schedules that no longer have
|
||||
// any completed backups (e.g. after the schedule and its backups are deleted).
|
||||
func Test_resyncBackupMetrics_prunesStaleTimestamps(t *testing.T) {
|
||||
baseTime, err := time.Parse(time.RFC1123, time.RFC1123)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := metrics.NewServerMetrics()
|
||||
gauge := m.Metrics()["backup_last_successful_timestamp"]
|
||||
|
||||
activeBackup := builder.ForBackup("velero", "b1").
|
||||
ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "active-schedule")).
|
||||
Phase(velerov1api.BackupPhaseCompleted).
|
||||
CompletionTimestamp(baseTime).
|
||||
Result()
|
||||
|
||||
deletedBackup := builder.ForBackup("velero", "b2").
|
||||
ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "deleted-schedule")).
|
||||
Phase(velerov1api.BackupPhaseCompleted).
|
||||
CompletionTimestamp(baseTime).
|
||||
Result()
|
||||
|
||||
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, activeBackup, deletedBackup)
|
||||
|
||||
c := &backupReconciler{
|
||||
kbClient: fakeClient,
|
||||
logger: logrus.StandardLogger(),
|
||||
metrics: m,
|
||||
}
|
||||
|
||||
// First resync: sets metrics for both schedules
|
||||
c.resyncBackupMetrics()
|
||||
assert.Equal(t, 2, testutil.CollectAndCount(gauge))
|
||||
|
||||
// Simulate schedule deletion: remove the backup for "deleted-schedule"
|
||||
require.NoError(t, fakeClient.Delete(t.Context(), deletedBackup))
|
||||
|
||||
// Second resync: prunes "deleted-schedule" metric, keeps "active-schedule"
|
||||
c.resyncBackupMetrics()
|
||||
assert.Equal(t, 1, testutil.CollectAndCount(gauge))
|
||||
}
|
||||
|
||||
// Unit tests to make sure that the backup's status is updated correctly during reconcile.
|
||||
// To clear up confusion whether status can be updated with Patch alone without status writer and not kbClient.Status().Patch()
|
||||
func TestPatchResourceWorksWithStatus(t *testing.T) {
|
||||
|
||||
@@ -295,7 +295,7 @@ func (r *backupDeletionReconciler) Reconcile(ctx context.Context, req ctrl.Reque
|
||||
err = delete.InvokeDeleteActions(deleteCtx)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Error invoking delete item actions")
|
||||
err2 := r.patchDeleteBackupRequestWithError(ctx, dbr, errors.New("error invoking delete item actions"))
|
||||
err2 := r.patchDeleteBackupRequestWithError(ctx, dbr, errors.Wrap(err, "error invoking delete item actions"))
|
||||
return ctrl.Result{}, err2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ import (
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
velerov2alpha1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1"
|
||||
"github.com/vmware-tanzu/velero/pkg/constant"
|
||||
datamover "github.com/vmware-tanzu/velero/pkg/datamover"
|
||||
"github.com/vmware-tanzu/velero/pkg/datapath"
|
||||
"github.com/vmware-tanzu/velero/pkg/exposer"
|
||||
"github.com/vmware-tanzu/velero/pkg/metrics"
|
||||
@@ -53,6 +52,7 @@ import (
|
||||
velerotypes "github.com/vmware-tanzu/velero/pkg/types"
|
||||
"github.com/vmware-tanzu/velero/pkg/uploader"
|
||||
"github.com/vmware-tanzu/velero/pkg/util"
|
||||
datamover "github.com/vmware-tanzu/velero/pkg/util/datamover"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
)
|
||||
|
||||
@@ -454,7 +454,7 @@ func (r *DataDownloadReconciler) startCancelableDataPath(asyncBR datapath.AsyncB
|
||||
|
||||
if err := asyncBR.StartRestore(dd.Spec.SnapshotID, datapath.AccessPoint{
|
||||
ByPath: res.ByPod.VolumeName,
|
||||
}, dd.Spec.DataMoverConfig); err != nil {
|
||||
}, dd.Spec.DataMoverConfig, nil); err != nil {
|
||||
return errors.Wrapf(err, "error starting async restore for pod %s, volume %s", res.ByPod.HostingPod.Name, res.ByPod.VolumeName)
|
||||
}
|
||||
|
||||
@@ -466,7 +466,7 @@ func (r *DataDownloadReconciler) OnDataDownloadCompleted(ctx context.Context, na
|
||||
defer r.dataPathMgr.RemoveAsyncBR(ddName)
|
||||
|
||||
log := r.logger.WithField("datadownload", ddName)
|
||||
log.Info("Async fs restore data path completed")
|
||||
log.Info("Async restore data path completed")
|
||||
|
||||
var dd velerov2alpha1api.DataDownload
|
||||
if err := r.client.Get(ctx, types.NamespacedName{Name: ddName, Namespace: namespace}, &dd); err != nil {
|
||||
@@ -513,7 +513,7 @@ func (r *DataDownloadReconciler) OnDataDownloadFailed(ctx context.Context, names
|
||||
|
||||
log := r.logger.WithField("datadownload", ddName)
|
||||
|
||||
log.WithError(err).Error("Async fs restore data path failed")
|
||||
log.WithError(err).Error("Async restore data path failed")
|
||||
|
||||
var dd velerov2alpha1api.DataDownload
|
||||
if getErr := r.client.Get(ctx, types.NamespacedName{Name: ddName, Namespace: namespace}, &dd); getErr != nil {
|
||||
@@ -528,7 +528,7 @@ func (r *DataDownloadReconciler) OnDataDownloadCancelled(ctx context.Context, na
|
||||
|
||||
log := r.logger.WithField("datadownload", ddName)
|
||||
|
||||
log.Warn("Async fs backup data path canceled")
|
||||
log.Warn("Async restore data path canceled")
|
||||
|
||||
var dd velerov2alpha1api.DataDownload
|
||||
if getErr := r.client.Get(ctx, types.NamespacedName{Name: ddName, Namespace: namespace}, &dd); getErr != nil {
|
||||
@@ -693,11 +693,11 @@ func (r *DataDownloadReconciler) findSnapshotRestoreForPod(ctx context.Context,
|
||||
r.prepareDataDownload(dd)
|
||||
return true
|
||||
}); err != nil {
|
||||
log.WithError(err).Warn("failed to update dataudownload, prepare will halt for this dataudownload")
|
||||
log.WithError(err).Warn("failed to update datadownload, prepare will halt for this datadownload")
|
||||
return []reconcile.Request{}
|
||||
}
|
||||
} else if unrecoverable, reason := kube.IsPodUnrecoverable(pod, log); unrecoverable {
|
||||
err := UpdateDataDownloadWithRetry(context.Background(), r.client, types.NamespacedName{Namespace: dd.Namespace, Name: dd.Name}, r.logger.WithField("datadownlad", dd.Name),
|
||||
err := UpdateDataDownloadWithRetry(context.Background(), r.client, types.NamespacedName{Namespace: dd.Namespace, Name: dd.Name}, r.logger.WithField("datadownload", dd.Name),
|
||||
func(dataDownload *velerov2alpha1api.DataDownload) bool {
|
||||
if dataDownload.Spec.Cancel {
|
||||
return false
|
||||
@@ -1096,7 +1096,7 @@ func (r *DataDownloadReconciler) resumeCancellableDataPath(ctx context.Context,
|
||||
|
||||
if err := asyncBR.StartRestore(dd.Spec.SnapshotID, datapath.AccessPoint{
|
||||
ByPath: res.ByPod.VolumeName,
|
||||
}, nil); err != nil {
|
||||
}, nil, nil); err != nil {
|
||||
return errors.Wrapf(err, "error to resume asyncBR watcher for dd %s", dd.Name)
|
||||
}
|
||||
|
||||
|
||||
@@ -529,7 +529,7 @@ func TestDataDownloadReconcile(t *testing.T) {
|
||||
}
|
||||
|
||||
if test.mockStart {
|
||||
asyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.mockStartErr)
|
||||
asyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.mockStartErr)
|
||||
}
|
||||
|
||||
if test.mockCancel {
|
||||
@@ -1288,7 +1288,7 @@ func TestResumeCancellableRestore(t *testing.T) {
|
||||
}
|
||||
|
||||
if test.mockStart {
|
||||
mockAsyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.startWatcherErr)
|
||||
mockAsyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.startWatcherErr)
|
||||
}
|
||||
|
||||
if test.mockClose {
|
||||
|
||||
@@ -45,7 +45,6 @@ import (
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
velerov2alpha1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1"
|
||||
"github.com/vmware-tanzu/velero/pkg/constant"
|
||||
"github.com/vmware-tanzu/velero/pkg/datamover"
|
||||
"github.com/vmware-tanzu/velero/pkg/datapath"
|
||||
"github.com/vmware-tanzu/velero/pkg/exposer"
|
||||
"github.com/vmware-tanzu/velero/pkg/metrics"
|
||||
@@ -53,6 +52,7 @@ import (
|
||||
velerotypes "github.com/vmware-tanzu/velero/pkg/types"
|
||||
"github.com/vmware-tanzu/velero/pkg/uploader"
|
||||
"github.com/vmware-tanzu/velero/pkg/util"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/datamover"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
)
|
||||
|
||||
@@ -463,9 +463,13 @@ func (r *DataUploadReconciler) initCancelableDataPath(ctx context.Context, async
|
||||
func (r *DataUploadReconciler) startCancelableDataPath(asyncBR datapath.AsyncBR, du *velerov2alpha1api.DataUpload, res *exposer.ExposeResult, log logrus.FieldLogger) error {
|
||||
log.Info("Start cancelable dataUpload")
|
||||
|
||||
if err := asyncBR.StartBackup(datapath.AccessPoint{
|
||||
ByPath: res.ByPod.VolumeName,
|
||||
}, du.Spec.DataMoverConfig, nil); err != nil {
|
||||
if err := asyncBR.StartBackup(
|
||||
datapath.AccessPoint{
|
||||
ByPath: res.ByPod.VolumeName,
|
||||
},
|
||||
du.Spec.DataMoverConfig,
|
||||
nil,
|
||||
); err != nil {
|
||||
return errors.Wrapf(err, "error starting async backup for pod %s, volume %s", res.ByPod.HostingPod.Name, res.ByPod.VolumeName)
|
||||
}
|
||||
|
||||
@@ -478,7 +482,7 @@ func (r *DataUploadReconciler) OnDataUploadCompleted(ctx context.Context, namesp
|
||||
|
||||
log := r.logger.WithField("dataupload", duName)
|
||||
|
||||
log.Info("Async fs backup data path completed")
|
||||
log.Info("Async backup data path completed")
|
||||
|
||||
var du velerov2alpha1api.DataUpload
|
||||
if err := r.client.Get(ctx, types.NamespacedName{Name: duName, Namespace: namespace}, &du); err != nil {
|
||||
@@ -530,7 +534,7 @@ func (r *DataUploadReconciler) OnDataUploadFailed(ctx context.Context, namespace
|
||||
|
||||
log := r.logger.WithField("dataupload", duName)
|
||||
|
||||
log.WithError(err).Error("Async fs backup data path failed")
|
||||
log.WithError(err).Error("Async backup data path failed")
|
||||
|
||||
var du velerov2alpha1api.DataUpload
|
||||
if getErr := r.client.Get(ctx, types.NamespacedName{Name: duName, Namespace: namespace}, &du); getErr != nil {
|
||||
@@ -545,7 +549,7 @@ func (r *DataUploadReconciler) OnDataUploadCancelled(ctx context.Context, namesp
|
||||
|
||||
log := r.logger.WithField("dataupload", duName)
|
||||
|
||||
log.Warn("Async fs backup data path canceled")
|
||||
log.Warn("Async backup data path canceled")
|
||||
|
||||
du := &velerov2alpha1api.DataUpload{}
|
||||
if getErr := r.client.Get(ctx, types.NamespacedName{Name: duName, Namespace: namespace}, du); getErr != nil {
|
||||
|
||||
@@ -72,6 +72,7 @@ type FakeClient struct {
|
||||
patchError error
|
||||
updateConflict error
|
||||
listError error
|
||||
getErrorMap map[string]error // key: object kind or name
|
||||
}
|
||||
|
||||
func (c *FakeClient) Get(ctx context.Context, key kbclient.ObjectKey, obj kbclient.Object, opts ...kbclient.GetOption) error {
|
||||
@@ -79,6 +80,19 @@ func (c *FakeClient) Get(ctx context.Context, key kbclient.ObjectKey, obj kbclie
|
||||
return c.getError
|
||||
}
|
||||
|
||||
// Check if there's a specific error for this object type
|
||||
if c.getErrorMap != nil {
|
||||
objType := fmt.Sprintf("%T", obj)
|
||||
if err, ok := c.getErrorMap[objType]; ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if there's a specific error for this object name
|
||||
if err, ok := c.getErrorMap[key.Name]; ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.Client.Get(ctx, key, obj)
|
||||
}
|
||||
|
||||
@@ -209,9 +223,13 @@ func initDataUploaderReconcilerWithError(needError ...error) (*DataUploadReconci
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = snapshotv1api.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fakeClient := &FakeClient{
|
||||
Client: fake.NewClientBuilder().WithScheme(scheme).Build(),
|
||||
Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(vsObject, node).Build(),
|
||||
}
|
||||
|
||||
for k := range needError {
|
||||
@@ -330,7 +348,7 @@ func (f *fakeFSBR) StartBackup(source datapath.AccessPoint, uploaderConfigs map[
|
||||
return f.startErr
|
||||
}
|
||||
|
||||
func (f *fakeFSBR) StartRestore(snapshotID string, target datapath.AccessPoint, uploaderConfigs map[string]string) error {
|
||||
func (f *fakeFSBR) StartRestore(snapshotID string, target datapath.AccessPoint, uploaderConfigs map[string]string, param any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -505,7 +523,7 @@ func TestReconcile(t *testing.T) {
|
||||
{
|
||||
name: "du succeeds for accepted",
|
||||
du: dataUploadBuilder().Finalizers([]string{DataUploadDownloadFinalizer}).SnapshotType(fakeSnapshotType).Result(),
|
||||
pvc: builder.ForPersistentVolumeClaim("fake-ns", "test-pvc").Result(),
|
||||
pvc: builder.ForPersistentVolumeClaim("fake-ns", "test-pvc").VolumeName("test-pv").Result(),
|
||||
expected: dataUploadBuilder().Finalizers([]string{DataUploadDownloadFinalizer}).Phase(velerov2alpha1api.DataUploadPhaseAccepted).Result(),
|
||||
},
|
||||
{
|
||||
@@ -636,6 +654,15 @@ func TestReconcile(t *testing.T) {
|
||||
if test.pvc != nil {
|
||||
err = r.client.Create(ctx, test.pvc)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create the corresponding PV if PVC references one
|
||||
if test.pvc.Spec.VolumeName != "" {
|
||||
pv := builder.ForPersistentVolume(test.pvc.Spec.VolumeName).
|
||||
CSI("csi.driver", "test-volume-id").
|
||||
ClaimRef(test.pvc.Namespace, test.pvc.Name).Result()
|
||||
err = r.client.Create(ctx, pv)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
if test.dataMgr != nil {
|
||||
|
||||
@@ -156,6 +156,10 @@ func (c *gcReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Re
|
||||
|
||||
if !veleroutil.BSLIsAvailable(*loc) {
|
||||
log.Infof("BSL %s is unavailable, cannot gc backup", loc.Name)
|
||||
backup.Labels[garbageCollectionFailure] = gcFailureBSLUnavailable
|
||||
if err := c.Update(ctx, backup); err != nil {
|
||||
log.WithError(err).Error("error updating backup labels")
|
||||
}
|
||||
return ctrl.Result{}, fmt.Errorf("bsl %s is unavailable, cannot gc backup", loc.Name)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
@@ -48,11 +49,12 @@ func TestGCReconcile(t *testing.T) {
|
||||
defaultBackupLocation := builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "default").Phase(velerov1api.BackupStorageLocationPhaseAvailable).Result()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
backup *velerov1api.Backup
|
||||
deleteBackupRequests []*velerov1api.DeleteBackupRequest
|
||||
backupLocation *velerov1api.BackupStorageLocation
|
||||
expectError bool
|
||||
name string
|
||||
backup *velerov1api.Backup
|
||||
deleteBackupRequests []*velerov1api.DeleteBackupRequest
|
||||
backupLocation *velerov1api.BackupStorageLocation
|
||||
expectError bool
|
||||
expectedGCFailureLabel string
|
||||
}{
|
||||
{
|
||||
name: "can't find backup - no error",
|
||||
@@ -118,10 +120,11 @@ func TestGCReconcile(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "BSL is unavailable",
|
||||
backup: defaultBackup().Expiration(fakeClock.Now().Add(-time.Second)).StorageLocation("default").Result(),
|
||||
backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "default").Phase(velerov1api.BackupStorageLocationPhaseUnavailable).Result(),
|
||||
expectError: true,
|
||||
name: "BSL is unavailable",
|
||||
backup: defaultBackup().Expiration(fakeClock.Now().Add(-time.Second)).StorageLocation("default").Result(),
|
||||
backupLocation: builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "default").Phase(velerov1api.BackupStorageLocationPhaseUnavailable).Result(),
|
||||
expectError: true,
|
||||
expectedGCFailureLabel: gcFailureBSLUnavailable,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -147,6 +150,12 @@ func TestGCReconcile(t *testing.T) {
|
||||
_, err := reconciler.Reconcile(t.Context(), ctrl.Request{NamespacedName: types.NamespacedName{Namespace: test.backup.Namespace, Name: test.backup.Name}})
|
||||
gotErr := err != nil
|
||||
assert.Equal(t, test.expectError, gotErr)
|
||||
|
||||
if test.expectedGCFailureLabel != "" {
|
||||
updatedBackup := &velerov1api.Backup{}
|
||||
require.NoError(t, fakeClient.Get(t.Context(), types.NamespacedName{Namespace: test.backup.Namespace, Name: test.backup.Name}, updatedBackup))
|
||||
assert.Equal(t, test.expectedGCFailureLabel, updatedBackup.Labels[garbageCollectionFailure])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,9 +236,9 @@ func (r *PodVolumeRestoreReconciler) Reconcile(ctx context.Context, req ctrl.Req
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
shouldProcess, pod, err := shouldProcess(ctx, r.client, log, pvr)
|
||||
shouldProcess, pod, err := shouldProcess(ctx, r.client, log, pvr, r.resourceTimeout)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
return r.errorOut(ctx, pvr, err, "Pod for this PVR is not ready", log)
|
||||
}
|
||||
if !shouldProcess {
|
||||
return ctrl.Result{}, nil
|
||||
@@ -528,7 +528,7 @@ func (r *PodVolumeRestoreReconciler) startCancelableDataPath(asyncBR datapath.As
|
||||
|
||||
if err := asyncBR.StartRestore(pvr.Spec.SnapshotID, datapath.AccessPoint{
|
||||
ByPath: res.ByPod.VolumeName,
|
||||
}, pvr.Spec.UploaderSettings); err != nil {
|
||||
}, pvr.Spec.UploaderSettings, nil); err != nil {
|
||||
return errors.Wrapf(err, "error starting async restore for pod %s, volume %s", res.ByPod.HostingPod.Name, res.ByPod.VolumeName)
|
||||
}
|
||||
|
||||
@@ -565,7 +565,7 @@ func UpdatePVRStatusToFailed(ctx context.Context, c client.Client, pvr *velerov1
|
||||
return err
|
||||
}
|
||||
|
||||
func shouldProcess(ctx context.Context, client client.Client, log logrus.FieldLogger, pvr *velerov1api.PodVolumeRestore) (bool, *corev1api.Pod, error) {
|
||||
func shouldProcess(ctx context.Context, client client.Client, log logrus.FieldLogger, pvr *velerov1api.PodVolumeRestore, timeout time.Duration) (bool, *corev1api.Pod, error) {
|
||||
if !isPVRNew(pvr) {
|
||||
log.Debug("PVR is not new, skip")
|
||||
return false, nil, nil
|
||||
@@ -573,22 +573,63 @@ func shouldProcess(ctx context.Context, client client.Client, log logrus.FieldLo
|
||||
|
||||
// we filter the pods during the initialization of cache, if we can get a pod here, the pod must be in the same node with the controller
|
||||
// so we don't need to compare the node anymore
|
||||
pod := &corev1api.Pod{}
|
||||
if err := client.Get(ctx, types.NamespacedName{Namespace: pvr.Spec.Pod.Namespace, Name: pvr.Spec.Pod.Name}, pod); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
log.WithError(err).Debug("Pod not found on this node, skip")
|
||||
return false, nil, nil
|
||||
var targetPod *corev1api.Pod
|
||||
err := wait.PollUntilContextTimeout(ctx, time.Millisecond*100, timeout, true, func(ctx context.Context) (bool, error) {
|
||||
updated := &corev1api.Pod{}
|
||||
if err := client.Get(ctx, types.NamespacedName{Namespace: pvr.Spec.Pod.Namespace, Name: pvr.Spec.Pod.Name}, updated); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
targetPod = updated
|
||||
|
||||
return true, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return false, nil, errors.Errorf("timeout to wait for pod %s/%s", pvr.Spec.Pod.Namespace, pvr.Spec.Pod.Name)
|
||||
} else {
|
||||
return false, nil, errors.Wrapf(err, "error waiting for pod %s/%s", pvr.Spec.Pod.Namespace, pvr.Spec.Pod.Name)
|
||||
}
|
||||
log.WithError(err).Error("Unable to get pod")
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
if !isInitContainerRunning(pod) {
|
||||
if targetPod.Status.Phase == corev1api.PodFailed || targetPod.Status.Phase == corev1api.PodUnknown {
|
||||
return false, nil, errors.Errorf("unexpected state for pod %s/%s", targetPod.Namespace, targetPod.Name)
|
||||
}
|
||||
|
||||
idx := getInitContainerIndex(targetPod)
|
||||
if idx < 0 {
|
||||
return false, nil, errors.Errorf("no restore-wait init container in pod %s/%s", targetPod.Namespace, targetPod.Name)
|
||||
}
|
||||
|
||||
if len(targetPod.Status.InitContainerStatuses) <= idx {
|
||||
log.Debug("Pod init container statuses are not fully populated yet, skip")
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
containerStatus := targetPod.Status.InitContainerStatuses[idx]
|
||||
|
||||
if containerStatus.State.Terminated != nil {
|
||||
return false, nil, errors.Errorf("restore-wait init container has already completed in pod %s/%s", targetPod.Namespace, targetPod.Name)
|
||||
}
|
||||
|
||||
if containerStatus.State.Waiting != nil {
|
||||
reason := containerStatus.State.Waiting.Reason
|
||||
if reason == "ImagePullBackOff" || reason == "ErrImageNeverPull" || reason == "CreateContainerConfigError" || reason == "CreateContainerError" || reason == "InvalidImageName" || reason == "ErrImagePull" {
|
||||
return false, nil, errors.Errorf("restore-wait init container in pod %s/%s is in unrecoverable waiting state with reason %s", targetPod.Namespace, targetPod.Name, reason)
|
||||
}
|
||||
}
|
||||
|
||||
if containerStatus.State.Running == nil {
|
||||
log.Debug("Pod is not running restore-wait init container, skip")
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
return true, pod, nil
|
||||
return true, targetPod, nil
|
||||
}
|
||||
|
||||
func (r *PodVolumeRestoreReconciler) closeDataPath(ctx context.Context, pvrName string) {
|
||||
@@ -770,14 +811,6 @@ func isPVRNew(pvr *velerov1api.PodVolumeRestore) bool {
|
||||
return pvr.Status.Phase == "" || pvr.Status.Phase == velerov1api.PodVolumeRestorePhaseNew
|
||||
}
|
||||
|
||||
func isInitContainerRunning(pod *corev1api.Pod) bool {
|
||||
// Pod volume wait container can be anywhere in the list of init containers, but must be running.
|
||||
i := getInitContainerIndex(pod)
|
||||
return i >= 0 &&
|
||||
len(pod.Status.InitContainerStatuses)-1 >= i &&
|
||||
pod.Status.InitContainerStatuses[i].State.Running != nil
|
||||
}
|
||||
|
||||
func getInitContainerIndex(pod *corev1api.Pod) int {
|
||||
// Pod volume wait container can be anywhere in the list of init containers so locate it.
|
||||
for i, initContainer := range pod.Spec.InitContainers {
|
||||
@@ -1113,7 +1146,7 @@ func (r *PodVolumeRestoreReconciler) resumeCancellableDataPath(ctx context.Conte
|
||||
|
||||
if err := asyncBR.StartRestore(pvr.Spec.SnapshotID, datapath.AccessPoint{
|
||||
ByPath: res.ByPod.VolumeName,
|
||||
}, pvr.Spec.UploaderSettings); err != nil {
|
||||
}, pvr.Spec.UploaderSettings, nil); err != nil {
|
||||
return errors.Wrapf(err, "error to resume asyncBR watcher for PVR %s", pvr.Name)
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,8 @@ func TestShouldProcess(t *testing.T) {
|
||||
obj *velerov1api.PodVolumeRestore
|
||||
pod *corev1api.Pod
|
||||
shouldProcessed bool
|
||||
expectError bool
|
||||
errString string
|
||||
}{
|
||||
{
|
||||
name: "InProgress phase pvr should not be processed",
|
||||
@@ -115,6 +117,8 @@ func TestShouldProcess(t *testing.T) {
|
||||
},
|
||||
},
|
||||
shouldProcessed: false,
|
||||
expectError: true,
|
||||
errString: "timeout to wait for pod ns-1/pod-1",
|
||||
},
|
||||
{
|
||||
name: "Empty phase pvr with pod on node not running init container should not be processed",
|
||||
@@ -200,6 +204,268 @@ func TestShouldProcess(t *testing.T) {
|
||||
},
|
||||
shouldProcessed: true,
|
||||
},
|
||||
{
|
||||
name: "pod is in failed phase should return error",
|
||||
obj: &velerov1api.PodVolumeRestore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "velero",
|
||||
Name: "pvr-1",
|
||||
},
|
||||
Spec: velerov1api.PodVolumeRestoreSpec{
|
||||
Pod: corev1api.ObjectReference{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
},
|
||||
Status: velerov1api.PodVolumeRestoreStatus{
|
||||
Phase: "",
|
||||
},
|
||||
},
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
Status: corev1api.PodStatus{
|
||||
Phase: corev1api.PodFailed,
|
||||
},
|
||||
},
|
||||
shouldProcessed: false,
|
||||
expectError: true,
|
||||
errString: "unexpected state for pod",
|
||||
},
|
||||
{
|
||||
name: "pod is in unknown phase should return error",
|
||||
obj: &velerov1api.PodVolumeRestore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "velero",
|
||||
Name: "pvr-1",
|
||||
},
|
||||
Spec: velerov1api.PodVolumeRestoreSpec{
|
||||
Pod: corev1api.ObjectReference{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
},
|
||||
Status: velerov1api.PodVolumeRestoreStatus{
|
||||
Phase: "",
|
||||
},
|
||||
},
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
Status: corev1api.PodStatus{
|
||||
Phase: corev1api.PodUnknown,
|
||||
},
|
||||
},
|
||||
shouldProcessed: false,
|
||||
expectError: true,
|
||||
errString: "unexpected state for pod",
|
||||
},
|
||||
{
|
||||
name: "pod with no init containers should return error",
|
||||
obj: &velerov1api.PodVolumeRestore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "velero",
|
||||
Name: "pvr-1",
|
||||
},
|
||||
Spec: velerov1api.PodVolumeRestoreSpec{
|
||||
Pod: corev1api.ObjectReference{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
},
|
||||
Status: velerov1api.PodVolumeRestoreStatus{
|
||||
Phase: "",
|
||||
},
|
||||
},
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
NodeName: controllerNode,
|
||||
},
|
||||
},
|
||||
shouldProcessed: false,
|
||||
expectError: true,
|
||||
errString: "no restore-wait init container",
|
||||
},
|
||||
{
|
||||
name: "pod init container statuses are not fully populated yet should skip",
|
||||
obj: &velerov1api.PodVolumeRestore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "velero",
|
||||
Name: "pvr-1",
|
||||
},
|
||||
Spec: velerov1api.PodVolumeRestoreSpec{
|
||||
Pod: corev1api.ObjectReference{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
},
|
||||
Status: velerov1api.PodVolumeRestoreStatus{
|
||||
Phase: "",
|
||||
},
|
||||
},
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
NodeName: controllerNode,
|
||||
InitContainers: []corev1api.Container{
|
||||
{
|
||||
Name: restorehelper.WaitInitContainer,
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1api.PodStatus{
|
||||
InitContainerStatuses: []corev1api.ContainerStatus{},
|
||||
},
|
||||
},
|
||||
shouldProcessed: false,
|
||||
},
|
||||
{
|
||||
name: "restore-wait init container has already completed should return error",
|
||||
obj: &velerov1api.PodVolumeRestore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "velero",
|
||||
Name: "pvr-1",
|
||||
},
|
||||
Spec: velerov1api.PodVolumeRestoreSpec{
|
||||
Pod: corev1api.ObjectReference{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
},
|
||||
Status: velerov1api.PodVolumeRestoreStatus{
|
||||
Phase: "",
|
||||
},
|
||||
},
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
NodeName: controllerNode,
|
||||
InitContainers: []corev1api.Container{
|
||||
{
|
||||
Name: restorehelper.WaitInitContainer,
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1api.PodStatus{
|
||||
InitContainerStatuses: []corev1api.ContainerStatus{
|
||||
{
|
||||
State: corev1api.ContainerState{
|
||||
Terminated: &corev1api.ContainerStateTerminated{
|
||||
ExitCode: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldProcessed: false,
|
||||
expectError: true,
|
||||
errString: "restore-wait init container has already completed",
|
||||
},
|
||||
{
|
||||
name: "restore-wait init container is in unrecoverable waiting state should return error",
|
||||
obj: &velerov1api.PodVolumeRestore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "velero",
|
||||
Name: "pvr-1",
|
||||
},
|
||||
Spec: velerov1api.PodVolumeRestoreSpec{
|
||||
Pod: corev1api.ObjectReference{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
},
|
||||
Status: velerov1api.PodVolumeRestoreStatus{
|
||||
Phase: "",
|
||||
},
|
||||
},
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
NodeName: controllerNode,
|
||||
InitContainers: []corev1api.Container{
|
||||
{
|
||||
Name: restorehelper.WaitInitContainer,
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1api.PodStatus{
|
||||
InitContainerStatuses: []corev1api.ContainerStatus{
|
||||
{
|
||||
State: corev1api.ContainerState{
|
||||
Waiting: &corev1api.ContainerStateWaiting{
|
||||
Reason: "ImagePullBackOff",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldProcessed: false,
|
||||
expectError: true,
|
||||
errString: "is in unrecoverable waiting state with reason ImagePullBackOff",
|
||||
},
|
||||
{
|
||||
name: "restore-wait init container is in normal waiting state should skip",
|
||||
obj: &velerov1api.PodVolumeRestore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "velero",
|
||||
Name: "pvr-1",
|
||||
},
|
||||
Spec: velerov1api.PodVolumeRestoreSpec{
|
||||
Pod: corev1api.ObjectReference{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
},
|
||||
Status: velerov1api.PodVolumeRestoreStatus{
|
||||
Phase: "",
|
||||
},
|
||||
},
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
NodeName: controllerNode,
|
||||
InitContainers: []corev1api.Container{
|
||||
{
|
||||
Name: restorehelper.WaitInitContainer,
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1api.PodStatus{
|
||||
InitContainerStatuses: []corev1api.ContainerStatus{
|
||||
{
|
||||
State: corev1api.ContainerState{
|
||||
Waiting: &corev1api.ContainerStateWaiting{
|
||||
Reason: "ContainerCreating",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldProcessed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, ts := range tests {
|
||||
@@ -221,179 +487,16 @@ func TestShouldProcess(t *testing.T) {
|
||||
clock: &clocks.RealClock{},
|
||||
}
|
||||
|
||||
shouldProcess, _, _ := shouldProcess(ctx, c.client, c.logger, ts.obj)
|
||||
shouldProcess, _, err := shouldProcess(ctx, c.client, c.logger, ts.obj, time.Second)
|
||||
require.Equal(t, ts.shouldProcessed, shouldProcess)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsInitContainerRunning(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pod *corev1api.Pod
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "pod with no init containers should return false",
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "pod with running init container that's not restore init should return false",
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
InitContainers: []corev1api.Container{
|
||||
{
|
||||
Name: "non-restore-init",
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1api.PodStatus{
|
||||
InitContainerStatuses: []corev1api.ContainerStatus{
|
||||
{
|
||||
State: corev1api.ContainerState{
|
||||
Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "pod with running init container that's not first should still work",
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
InitContainers: []corev1api.Container{
|
||||
{
|
||||
Name: "non-restore-init",
|
||||
},
|
||||
{
|
||||
Name: restorehelper.WaitInitContainer,
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1api.PodStatus{
|
||||
InitContainerStatuses: []corev1api.ContainerStatus{
|
||||
{
|
||||
State: corev1api.ContainerState{
|
||||
Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}},
|
||||
},
|
||||
},
|
||||
{
|
||||
State: corev1api.ContainerState{
|
||||
Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "pod with init container as first initContainer that's not running should return false",
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
InitContainers: []corev1api.Container{
|
||||
{
|
||||
Name: restorehelper.WaitInitContainer,
|
||||
},
|
||||
{
|
||||
Name: "non-restore-init",
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1api.PodStatus{
|
||||
InitContainerStatuses: []corev1api.ContainerStatus{
|
||||
{
|
||||
State: corev1api.ContainerState{},
|
||||
},
|
||||
{
|
||||
State: corev1api.ContainerState{
|
||||
Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "pod with running init container as first initContainer should return true",
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
InitContainers: []corev1api.Container{
|
||||
{
|
||||
Name: restorehelper.WaitInitContainer,
|
||||
},
|
||||
{
|
||||
Name: "non-restore-init",
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1api.PodStatus{
|
||||
InitContainerStatuses: []corev1api.ContainerStatus{
|
||||
{
|
||||
State: corev1api.ContainerState{
|
||||
Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}},
|
||||
},
|
||||
},
|
||||
{
|
||||
State: corev1api.ContainerState{
|
||||
Running: &corev1api.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "pod with init container with empty InitContainerStatuses should return 0",
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
InitContainers: []corev1api.Container{
|
||||
{
|
||||
Name: restorehelper.WaitInitContainer,
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: corev1api.PodStatus{
|
||||
InitContainerStatuses: []corev1api.ContainerStatus{},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, isInitContainerRunning(test.pod))
|
||||
if ts.expectError {
|
||||
require.Error(t, err)
|
||||
if ts.errString != "" {
|
||||
assert.Contains(t, err.Error(), ts.errString)
|
||||
}
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -996,7 +1099,7 @@ func TestPodVolumeRestoreReconcile(t *testing.T) {
|
||||
}
|
||||
|
||||
if test.mockStart {
|
||||
asyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.mockStartErr)
|
||||
asyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.mockStartErr)
|
||||
}
|
||||
|
||||
if test.mockCancel {
|
||||
@@ -1798,7 +1901,7 @@ func TestResumeCancellablePodVolumeRestore(t *testing.T) {
|
||||
}
|
||||
|
||||
if test.mockStart {
|
||||
mockAsyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.startWatcherErr)
|
||||
mockAsyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.startWatcherErr)
|
||||
}
|
||||
|
||||
if test.mockClose {
|
||||
|
||||
@@ -44,6 +44,7 @@ import (
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/hook"
|
||||
"github.com/vmware-tanzu/velero/internal/resourcemodifiers"
|
||||
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
|
||||
"github.com/vmware-tanzu/velero/internal/volume"
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/constant"
|
||||
@@ -54,6 +55,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/framework"
|
||||
pkgrestore "github.com/vmware-tanzu/velero/pkg/restore"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/collections"
|
||||
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/logging"
|
||||
@@ -108,10 +110,11 @@ type restoreReconciler struct {
|
||||
defaultItemOperationTimeout time.Duration
|
||||
disableInformerCache bool
|
||||
|
||||
newPluginManager func(logger logrus.FieldLogger) clientmgmt.Manager
|
||||
backupStoreGetter persistence.ObjectBackupStoreGetter
|
||||
globalCrClient client.Client
|
||||
resourceTimeout time.Duration
|
||||
newPluginManager func(logger logrus.FieldLogger) clientmgmt.Manager
|
||||
backupStoreGetter persistence.ObjectBackupStoreGetter
|
||||
globalCrClient client.Client
|
||||
resourceTimeout time.Duration
|
||||
defaultResourceModifierConfigMap string
|
||||
}
|
||||
|
||||
type backupInfo struct {
|
||||
@@ -134,6 +137,7 @@ func NewRestoreReconciler(
|
||||
disableInformerCache bool,
|
||||
globalCrClient client.Client,
|
||||
resourceTimeout time.Duration,
|
||||
defaultResourceModifierConfigMap string,
|
||||
) *restoreReconciler {
|
||||
r := &restoreReconciler{
|
||||
ctx: ctx,
|
||||
@@ -153,8 +157,9 @@ func NewRestoreReconciler(
|
||||
newPluginManager: newPluginManager,
|
||||
backupStoreGetter: backupStoreGetter,
|
||||
|
||||
globalCrClient: globalCrClient,
|
||||
resourceTimeout: resourceTimeout,
|
||||
globalCrClient: globalCrClient,
|
||||
resourceTimeout: resourceTimeout,
|
||||
defaultResourceModifierConfigMap: defaultResourceModifierConfigMap,
|
||||
}
|
||||
|
||||
// Move the periodical backup and restore metrics computing logic from controllers to here.
|
||||
@@ -232,7 +237,7 @@ func (r *restoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
||||
original := restore.DeepCopy()
|
||||
|
||||
// Validate the restore and fetch the backup
|
||||
info, resourceModifiers := r.validateAndComplete(restore)
|
||||
info, resourceModifiers, restoreResPolicies := r.validateAndComplete(ctx, restore)
|
||||
|
||||
// Register attempts after validation so we don't have to fetch the backup multiple times
|
||||
backupScheduleName := restore.Spec.ScheduleName
|
||||
@@ -267,7 +272,7 @@ func (r *restoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if err := r.runValidatedRestore(restore, info, resourceModifiers); err != nil {
|
||||
if err := r.runValidatedRestore(restore, info, resourceModifiers, restoreResPolicies); err != nil {
|
||||
log.WithError(err).Debug("Restore failed")
|
||||
restore.Status.Phase = api.RestorePhaseFailed
|
||||
restore.Status.FailureReason = err.Error()
|
||||
@@ -303,7 +308,7 @@ func (r *restoreReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInfo, *resourcemodifiers.ResourceModifiers) {
|
||||
func (r *restoreReconciler) validateAndComplete(ctx context.Context, restore *api.Restore) (backupInfo, *resourcemodifiers.ResourceModifiers, *resourcepolicies.Policies) {
|
||||
// add non-restorable resources to restore's excluded resources
|
||||
excludedResources := sets.NewString(restore.Spec.ExcludedResources...)
|
||||
for _, nonrestorable := range nonRestorableResources {
|
||||
@@ -338,7 +343,7 @@ func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInf
|
||||
// validate that exactly one of BackupName and ScheduleName have been specified
|
||||
if !backupXorScheduleProvided(restore) {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, "Either a backup or schedule must be specified as a source for the restore, but not both")
|
||||
return backupInfo{}, nil
|
||||
return backupInfo{}, nil, nil
|
||||
}
|
||||
|
||||
// validate Restore Init Hook's InitContainers
|
||||
@@ -372,9 +377,9 @@ func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInf
|
||||
}))
|
||||
|
||||
backupList := &api.BackupList{}
|
||||
if err := r.kbClient.List(context.Background(), backupList, &client.ListOptions{LabelSelector: selector}); err != nil {
|
||||
if err := r.kbClient.List(ctx, backupList, &client.ListOptions{LabelSelector: selector}); err != nil {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, "Unable to list backups for schedule")
|
||||
return backupInfo{}, nil
|
||||
return backupInfo{}, nil, nil
|
||||
}
|
||||
if len(backupList.Items) == 0 {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, "No backups found for schedule")
|
||||
@@ -384,19 +389,19 @@ func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInf
|
||||
restore.Spec.BackupName = backup.Name
|
||||
} else {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, "No completed backups found for schedule")
|
||||
return backupInfo{}, nil
|
||||
return backupInfo{}, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
info, err := r.fetchBackupInfo(restore.Spec.BackupName)
|
||||
if err != nil {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, fmt.Sprintf("Error retrieving backup: %v", err))
|
||||
return backupInfo{}, nil
|
||||
return backupInfo{}, nil, nil
|
||||
}
|
||||
|
||||
if !veleroutil.BSLIsAvailable(*info.location) {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, fmt.Sprintf("The BSL %s is unavailable, cannot retrieve the backup", info.location.Name))
|
||||
return backupInfo{}, nil
|
||||
return backupInfo{}, nil, nil
|
||||
}
|
||||
|
||||
// reject restores from backups that are not in a usable phase
|
||||
@@ -407,7 +412,7 @@ func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInf
|
||||
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
|
||||
return backupInfo{}, nil, nil
|
||||
}
|
||||
|
||||
// Fill in the ScheduleName so it's easier to consume for metrics.
|
||||
@@ -415,26 +420,85 @@ func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInf
|
||||
restore.Spec.ScheduleName = info.backup.GetLabels()[api.ScheduleNameLabel]
|
||||
}
|
||||
|
||||
var resourceModifiers *resourcemodifiers.ResourceModifiers
|
||||
if restore.Spec.ResourceModifier != nil && strings.EqualFold(restore.Spec.ResourceModifier.Kind, resourcemodifiers.ConfigmapRefType) {
|
||||
ResourceModifierConfigMap := &corev1api.ConfigMap{}
|
||||
err := r.kbClient.Get(context.Background(), client.ObjectKey{Namespace: restore.Namespace, Name: restore.Spec.ResourceModifier.Name}, ResourceModifierConfigMap)
|
||||
var restoreResPolicies *resourcepolicies.Policies
|
||||
if restore.Spec.ResourcePolicy != nil {
|
||||
var err error
|
||||
restoreResPolicies, err = resourcepolicies.GetResourcePoliciesFromRestore(
|
||||
ctx, restore, r.kbClient, r.logger,
|
||||
)
|
||||
if err != nil {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, fmt.Sprintf("failed to get resource modifiers configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name))
|
||||
return backupInfo{}, nil
|
||||
restore.Status.ValidationErrors = append(
|
||||
restore.Status.ValidationErrors, err.Error(),
|
||||
)
|
||||
return backupInfo{}, nil, nil
|
||||
}
|
||||
resourceModifiers, err = resourcemodifiers.GetResourceModifiersFromConfig(ResourceModifierConfigMap)
|
||||
if err != nil {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, errors.Wrapf(err, "Error in parsing resource modifiers provided in configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name).Error())
|
||||
return backupInfo{}, nil
|
||||
} else if err = resourceModifiers.Validate(); err != nil {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, errors.Wrapf(err, "Validation error in resource modifiers provided in configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name).Error())
|
||||
return backupInfo{}, nil
|
||||
}
|
||||
r.logger.Infof("Retrieved Resource modifiers provided in configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name)
|
||||
}
|
||||
|
||||
return info, resourceModifiers
|
||||
var resourceModifiers *resourcemodifiers.ResourceModifiers
|
||||
if restore.Spec.ResourceModifier != nil {
|
||||
if strings.EqualFold(restore.Spec.ResourceModifier.Kind, resourcemodifiers.ConfigmapRefType) {
|
||||
resourceModifiers = r.loadResourceModifierConfigMap(ctx, restore, restore.Spec.ResourceModifier.Name, false)
|
||||
if resourceModifiers == nil && len(restore.Status.ValidationErrors) > 0 {
|
||||
return backupInfo{}, nil, nil
|
||||
}
|
||||
} else {
|
||||
r.logger.Warnf("Unsupported resource modifier kind %q, only %q is supported", restore.Spec.ResourceModifier.Kind, resourcemodifiers.ConfigmapRefType)
|
||||
}
|
||||
} else if r.defaultResourceModifierConfigMap != "" {
|
||||
if boolptr.IsSetToTrue(restore.Spec.SkipDefaultResourceModifier) {
|
||||
r.logger.Infof("Skipping default resource modifier configmap %s/%s as SkipDefaultResourceModifier is set", restore.Namespace, r.defaultResourceModifierConfigMap)
|
||||
} else {
|
||||
resourceModifiers = r.loadResourceModifierConfigMap(ctx, restore, r.defaultResourceModifierConfigMap, true)
|
||||
}
|
||||
}
|
||||
|
||||
return info, resourceModifiers, restoreResPolicies
|
||||
}
|
||||
|
||||
// loadResourceModifierConfigMap loads and validates a resource modifier ConfigMap.
|
||||
// When isDefault is true, errors are non-fatal (logged as warnings, returns nil).
|
||||
// When isDefault is false, errors are added to restore.Status.ValidationErrors.
|
||||
func (r *restoreReconciler) loadResourceModifierConfigMap(
|
||||
ctx context.Context, restore *api.Restore, cmName string, isDefault bool,
|
||||
) *resourcemodifiers.ResourceModifiers {
|
||||
cm := &corev1api.ConfigMap{}
|
||||
if err := r.kbClient.Get(ctx, client.ObjectKey{Namespace: restore.Namespace, Name: cmName}, cm); err != nil {
|
||||
if isDefault {
|
||||
r.logger.WithError(err).Warnf("Failed to retrieve default resource modifier configmap %s/%s, skipping", restore.Namespace, cmName)
|
||||
return nil
|
||||
}
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors,
|
||||
fmt.Sprintf("failed to get resource modifiers configmap %s/%s: %v", restore.Namespace, cmName, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
modifiers, err := resourcemodifiers.GetResourceModifiersFromConfig(cm)
|
||||
if err != nil {
|
||||
if isDefault {
|
||||
r.logger.WithError(err).Warnf("Error parsing default resource modifier configmap %s/%s, skipping", restore.Namespace, cmName)
|
||||
return nil
|
||||
}
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors,
|
||||
errors.Wrapf(err, "Error in parsing resource modifiers provided in configmap %s/%s", restore.Namespace, cmName).Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = modifiers.Validate(); err != nil {
|
||||
if isDefault {
|
||||
r.logger.WithError(err).Warnf("Validation error in default resource modifier configmap %s/%s, skipping", restore.Namespace, cmName)
|
||||
return nil
|
||||
}
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors,
|
||||
errors.Wrapf(err, "Validation error in resource modifiers provided in configmap %s/%s", restore.Namespace, cmName).Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
source := "per-restore"
|
||||
if isDefault {
|
||||
source = "default"
|
||||
}
|
||||
r.logger.Infof("Retrieved %s resource modifiers from configmap %s/%s", source, restore.Namespace, cmName)
|
||||
return modifiers
|
||||
}
|
||||
|
||||
// backupXorScheduleProvided returns true if exactly one of BackupName and
|
||||
@@ -507,7 +571,7 @@ func fetchBackupInfoInternal(kbClient client.Client, namespace, backupName strin
|
||||
// The log and results files are uploaded to backup storage. Any error returned from this function
|
||||
// means that the restore failed. This function updates the restore API object with warning and error
|
||||
// counts, but *does not* update its phase or patch it via the API.
|
||||
func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backupInfo, resourceModifiers *resourcemodifiers.ResourceModifiers) error {
|
||||
func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backupInfo, resourceModifiers *resourcemodifiers.ResourceModifiers, restoreResPolicies *resourcepolicies.Policies) error {
|
||||
// instantiate the per-restore logger that will output both to a temp file
|
||||
// (for upload to object storage) and to stdout.
|
||||
restoreLog, err := logging.NewTempFileLogger(r.restoreLogLevel, r.logFormat, nil, logrus.Fields{"restore": kubeutil.NamespaceAndName(restore)})
|
||||
@@ -586,6 +650,7 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu
|
||||
VolumeSnapshots: volumeSnapshots,
|
||||
BackupReader: backupFile,
|
||||
ResourceModifiers: resourceModifiers,
|
||||
ResPolicies: restoreResPolicies,
|
||||
DisableInformerCache: r.disableInformerCache,
|
||||
CSIVolumeSnapshots: csiVolumeSnapshots,
|
||||
BackupVolumeInfoMap: backupVolumeInfoMap,
|
||||
|
||||
@@ -116,6 +116,7 @@ func TestFetchBackupInfo(t *testing.T) {
|
||||
false,
|
||||
fakeGlobalClient,
|
||||
10*time.Minute,
|
||||
"",
|
||||
)
|
||||
|
||||
if test.backupStoreError == nil {
|
||||
@@ -197,6 +198,7 @@ func TestProcessQueueItemSkips(t *testing.T) {
|
||||
false,
|
||||
fakeGlobalClient,
|
||||
10*time.Minute,
|
||||
"",
|
||||
)
|
||||
|
||||
_, err := r.Reconcile(t.Context(), ctrl.Request{NamespacedName: types.NamespacedName{
|
||||
@@ -579,6 +581,7 @@ func TestRestoreReconcile(t *testing.T) {
|
||||
false,
|
||||
fakeGlobalClient,
|
||||
10*time.Minute,
|
||||
"",
|
||||
)
|
||||
|
||||
r.clock = clocktesting.NewFakeClock(now)
|
||||
@@ -767,6 +770,7 @@ func TestValidateAndCompleteWhenScheduleNameSpecified(t *testing.T) {
|
||||
false,
|
||||
fakeGlobalClient,
|
||||
10*time.Minute,
|
||||
"",
|
||||
)
|
||||
|
||||
restore := &velerov1api.Restore{
|
||||
@@ -785,7 +789,7 @@ func TestValidateAndCompleteWhenScheduleNameSpecified(t *testing.T) {
|
||||
Phase(velerov1api.BackupPhaseCompleted).
|
||||
Result()))
|
||||
|
||||
r.validateAndComplete(restore)
|
||||
r.validateAndComplete(t.Context(), restore)
|
||||
assert.Contains(t, restore.Status.ValidationErrors, "No backups found for schedule")
|
||||
assert.Empty(t, restore.Spec.BackupName)
|
||||
|
||||
@@ -801,7 +805,7 @@ func TestValidateAndCompleteWhenScheduleNameSpecified(t *testing.T) {
|
||||
Result(),
|
||||
))
|
||||
|
||||
r.validateAndComplete(restore)
|
||||
r.validateAndComplete(t.Context(), restore)
|
||||
assert.Contains(t, restore.Status.ValidationErrors, "No completed backups found for schedule")
|
||||
assert.Empty(t, restore.Spec.BackupName)
|
||||
|
||||
@@ -832,11 +836,141 @@ func TestValidateAndCompleteWhenScheduleNameSpecified(t *testing.T) {
|
||||
ScheduleName: "schedule-1",
|
||||
},
|
||||
}
|
||||
r.validateAndComplete(restore)
|
||||
r.validateAndComplete(t.Context(), restore)
|
||||
assert.Nil(t, restore.Status.ValidationErrors)
|
||||
assert.Equal(t, "foo", restore.Spec.BackupName)
|
||||
}
|
||||
|
||||
func TestValidateAndCompleteWithResourcePolicySpecified(t *testing.T) {
|
||||
formatFlag := logging.FormatText
|
||||
|
||||
var (
|
||||
logger = velerotest.NewLogger()
|
||||
pluginManager = &pluginmocks.Manager{}
|
||||
fakeClient = velerotest.NewFakeControllerRuntimeClient(t)
|
||||
fakeGlobalClient = velerotest.NewFakeControllerRuntimeClient(t)
|
||||
backupStore = &persistencemocks.BackupStore{}
|
||||
)
|
||||
|
||||
r := NewRestoreReconciler(
|
||||
t.Context(),
|
||||
velerov1api.DefaultNamespace,
|
||||
nil,
|
||||
fakeClient,
|
||||
logger,
|
||||
logrus.DebugLevel,
|
||||
func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager },
|
||||
NewFakeSingleObjectBackupStoreGetter(backupStore),
|
||||
metrics.NewServerMetrics(),
|
||||
formatFlag,
|
||||
60*time.Minute,
|
||||
false,
|
||||
fakeGlobalClient,
|
||||
10*time.Minute,
|
||||
"",
|
||||
)
|
||||
|
||||
restore := &velerov1api.Restore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
Name: "restore-1",
|
||||
},
|
||||
Spec: velerov1api.RestoreSpec{
|
||||
BackupName: "backup-1",
|
||||
ResourcePolicy: &corev1api.TypedLocalObjectReference{
|
||||
Kind: "configmap",
|
||||
Name: "test-configmap",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
location := builder.ForBackupStorageLocation("velero", "default").Provider("myCloud").Bucket("bucket").Phase(velerov1api.BackupStorageLocationPhaseAvailable).Result()
|
||||
require.NoError(t, r.kbClient.Create(t.Context(), location))
|
||||
|
||||
require.NoError(t, r.kbClient.Create(
|
||||
t.Context(),
|
||||
defaultBackup().
|
||||
ObjectMeta(
|
||||
builder.WithName("backup-1"),
|
||||
).StorageLocation("default").
|
||||
Phase(velerov1api.BackupPhaseCompleted).
|
||||
Result(),
|
||||
))
|
||||
|
||||
r.validateAndComplete(t.Context(), restore)
|
||||
assert.Contains(t, restore.Status.ValidationErrors[0], "fail to get ResourcePolicies velero/test-configmap ConfigMap")
|
||||
|
||||
restore1 := &velerov1api.Restore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
Name: "restore-1",
|
||||
},
|
||||
Spec: velerov1api.RestoreSpec{
|
||||
BackupName: "backup-1",
|
||||
ResourcePolicy: &corev1api.TypedLocalObjectReference{
|
||||
Kind: "configmap",
|
||||
Name: "test-configmap",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cm1 := &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap",
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"policy.yaml": `version: v1
|
||||
clusterScopedFilterPolicy:
|
||||
resourceFilters:
|
||||
- kinds:
|
||||
- pods
|
||||
`,
|
||||
},
|
||||
}
|
||||
require.NoError(t, r.kbClient.Create(t.Context(), cm1))
|
||||
|
||||
r.validateAndComplete(t.Context(), restore1)
|
||||
assert.Nil(t, restore1.Status.ValidationErrors)
|
||||
|
||||
restore2 := &velerov1api.Restore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
Name: "restore-1",
|
||||
},
|
||||
Spec: velerov1api.RestoreSpec{
|
||||
BackupName: "backup-1",
|
||||
ResourcePolicy: &corev1api.TypedLocalObjectReference{
|
||||
// intentional to ensure case insensitivity works as expected
|
||||
Kind: "confIGMaP",
|
||||
Name: "test-configmap-invalid",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cm2 := &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap-invalid",
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"policy.yaml": `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
capacity: '0,10Gi'
|
||||
csi:
|
||||
driver: disks.csi.driver
|
||||
action:
|
||||
type: invalid_action
|
||||
`,
|
||||
},
|
||||
}
|
||||
require.NoError(t, r.kbClient.Create(t.Context(), cm2))
|
||||
|
||||
r.validateAndComplete(t.Context(), restore2)
|
||||
assert.Contains(t, restore2.Status.ValidationErrors[0], "fail to validate ResourcePolicies in ConfigMap velero/test-configmap-invalid")
|
||||
}
|
||||
|
||||
func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) {
|
||||
formatFlag := logging.FormatText
|
||||
|
||||
@@ -863,6 +997,7 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) {
|
||||
false,
|
||||
fakeGlobalClient,
|
||||
10*time.Minute,
|
||||
"",
|
||||
)
|
||||
|
||||
restore := &velerov1api.Restore{
|
||||
@@ -892,7 +1027,7 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) {
|
||||
Result(),
|
||||
))
|
||||
|
||||
r.validateAndComplete(restore)
|
||||
r.validateAndComplete(t.Context(), restore)
|
||||
assert.Contains(t, restore.Status.ValidationErrors[0], "failed to get resource modifiers configmap")
|
||||
|
||||
restore1 := &velerov1api.Restore{
|
||||
@@ -920,7 +1055,7 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, r.kbClient.Create(t.Context(), cm1))
|
||||
|
||||
r.validateAndComplete(restore1)
|
||||
r.validateAndComplete(t.Context(), restore1)
|
||||
assert.Nil(t, restore1.Status.ValidationErrors)
|
||||
|
||||
restore2 := &velerov1api.Restore{
|
||||
@@ -949,7 +1084,7 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, r.kbClient.Create(t.Context(), invalidVersionCm))
|
||||
|
||||
r.validateAndComplete(restore2)
|
||||
r.validateAndComplete(t.Context(), restore2)
|
||||
assert.Contains(t, restore2.Status.ValidationErrors[0], "Error in parsing resource modifiers provided in configmap")
|
||||
|
||||
restore3 := &velerov1api.Restore{
|
||||
@@ -977,10 +1112,188 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, r.kbClient.Create(t.Context(), invalidOperatorCm))
|
||||
|
||||
r.validateAndComplete(restore3)
|
||||
r.validateAndComplete(t.Context(), restore3)
|
||||
assert.Contains(t, restore3.Status.ValidationErrors[0], "Validation error in resource modifiers provided in configmap")
|
||||
}
|
||||
|
||||
func TestValidateAndCompleteWithDefaultResourceModifier(t *testing.T) {
|
||||
formatFlag := logging.FormatText
|
||||
|
||||
validCMData := map[string]string{
|
||||
"modifiers.yaml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: pods\n mergePatches:\n - patchData: |\n metadata:\n annotations:\n k8s.ovn.org/pod-networks: null\n",
|
||||
}
|
||||
|
||||
setupReconciler := func(t *testing.T, defaultCM string) *restoreReconciler {
|
||||
t.Helper()
|
||||
fakeClient := velerotest.NewFakeControllerRuntimeClient(t)
|
||||
fakeGlobalClient := velerotest.NewFakeControllerRuntimeClient(t)
|
||||
pluginManager := &pluginmocks.Manager{}
|
||||
backupStore := &persistencemocks.BackupStore{}
|
||||
|
||||
r := NewRestoreReconciler(
|
||||
t.Context(),
|
||||
velerov1api.DefaultNamespace,
|
||||
nil,
|
||||
fakeClient,
|
||||
velerotest.NewLogger(),
|
||||
logrus.DebugLevel,
|
||||
func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager },
|
||||
NewFakeSingleObjectBackupStoreGetter(backupStore),
|
||||
metrics.NewServerMetrics(),
|
||||
formatFlag,
|
||||
60*time.Minute,
|
||||
false,
|
||||
fakeGlobalClient,
|
||||
10*time.Minute,
|
||||
defaultCM,
|
||||
)
|
||||
|
||||
location := builder.ForBackupStorageLocation("velero", "default").Provider("myCloud").Bucket("bucket").Phase(velerov1api.BackupStorageLocationPhaseAvailable).Result()
|
||||
require.NoError(t, r.kbClient.Create(t.Context(), location))
|
||||
require.NoError(t, r.kbClient.Create(t.Context(),
|
||||
defaultBackup().ObjectMeta(builder.WithName("backup-1")).StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
))
|
||||
return r
|
||||
}
|
||||
|
||||
newRestore := func(perRestoreCM string, skip *bool) *velerov1api.Restore {
|
||||
restore := &velerov1api.Restore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
Name: "restore-1",
|
||||
},
|
||||
Spec: velerov1api.RestoreSpec{
|
||||
BackupName: "backup-1",
|
||||
SkipDefaultResourceModifier: skip,
|
||||
},
|
||||
}
|
||||
if perRestoreCM != "" {
|
||||
restore.Spec.ResourceModifier = &corev1api.TypedLocalObjectReference{
|
||||
Kind: resourcemodifiers.ConfigmapRefType,
|
||||
Name: perRestoreCM,
|
||||
}
|
||||
}
|
||||
return restore
|
||||
}
|
||||
|
||||
t.Run("default modifier applied when no per-restore modifier", func(t *testing.T) {
|
||||
r := setupReconciler(t, "default-rm")
|
||||
require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "default-rm", Namespace: velerov1api.DefaultNamespace},
|
||||
Data: validCMData,
|
||||
}))
|
||||
|
||||
restore := newRestore("", nil)
|
||||
_, rm, _ := r.validateAndComplete(t.Context(), restore)
|
||||
assert.NotNil(t, rm)
|
||||
assert.Empty(t, restore.Status.ValidationErrors)
|
||||
})
|
||||
|
||||
t.Run("per-restore modifier takes exclusive precedence over default", func(t *testing.T) {
|
||||
// Default ConfigMap does NOT exist, but per-restore does.
|
||||
// If default were applied, it would fail. Per-restore should succeed.
|
||||
r := setupReconciler(t, "nonexistent-default")
|
||||
require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "per-restore-rm", Namespace: velerov1api.DefaultNamespace},
|
||||
Data: validCMData,
|
||||
}))
|
||||
|
||||
restore := newRestore("per-restore-rm", nil)
|
||||
_, rm, _ := r.validateAndComplete(t.Context(), restore)
|
||||
assert.NotNil(t, rm)
|
||||
assert.Empty(t, restore.Status.ValidationErrors)
|
||||
})
|
||||
|
||||
t.Run("skip default modifier when SkipDefaultResourceModifier is true", func(t *testing.T) {
|
||||
r := setupReconciler(t, "default-rm")
|
||||
require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "default-rm", Namespace: velerov1api.DefaultNamespace},
|
||||
Data: validCMData,
|
||||
}))
|
||||
|
||||
skipTrue := true
|
||||
restore := newRestore("", &skipTrue)
|
||||
_, rm, _ := r.validateAndComplete(t.Context(), restore)
|
||||
assert.Nil(t, rm)
|
||||
assert.Empty(t, restore.Status.ValidationErrors)
|
||||
})
|
||||
|
||||
t.Run("default modifier with invalid data is non-fatal", func(t *testing.T) {
|
||||
r := setupReconciler(t, "invalid-default")
|
||||
require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "invalid-default", Namespace: velerov1api.DefaultNamespace},
|
||||
Data: map[string]string{
|
||||
"modifiers.yaml": "not-valid-yaml: [",
|
||||
},
|
||||
}))
|
||||
|
||||
restore := newRestore("", nil)
|
||||
_, rm, _ := r.validateAndComplete(t.Context(), restore)
|
||||
assert.Nil(t, rm)
|
||||
assert.Empty(t, restore.Status.ValidationErrors)
|
||||
})
|
||||
|
||||
t.Run("default modifier missing is non-fatal", func(t *testing.T) {
|
||||
r := setupReconciler(t, "nonexistent-cm")
|
||||
|
||||
restore := newRestore("", nil)
|
||||
_, rm, _ := r.validateAndComplete(t.Context(), restore)
|
||||
assert.Nil(t, rm)
|
||||
assert.Empty(t, restore.Status.ValidationErrors)
|
||||
})
|
||||
|
||||
t.Run("per-restore modifier missing is fatal", func(t *testing.T) {
|
||||
r := setupReconciler(t, "")
|
||||
|
||||
restore := newRestore("nonexistent-cm", nil)
|
||||
_, rm, _ := r.validateAndComplete(t.Context(), restore)
|
||||
assert.Nil(t, rm)
|
||||
assert.NotEmpty(t, restore.Status.ValidationErrors)
|
||||
assert.Contains(t, restore.Status.ValidationErrors[0], "failed to get resource modifiers configmap")
|
||||
})
|
||||
|
||||
t.Run("no default configured and no per-restore modifier", func(t *testing.T) {
|
||||
r := setupReconciler(t, "")
|
||||
|
||||
restore := newRestore("", nil)
|
||||
_, rm, _ := r.validateAndComplete(t.Context(), restore)
|
||||
assert.Nil(t, rm)
|
||||
assert.Empty(t, restore.Status.ValidationErrors)
|
||||
})
|
||||
|
||||
t.Run("unsupported resource modifier kind does not apply default", func(t *testing.T) {
|
||||
r := setupReconciler(t, "default-rm")
|
||||
require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "default-rm", Namespace: velerov1api.DefaultNamespace},
|
||||
Data: validCMData,
|
||||
}))
|
||||
|
||||
restore := newRestore("", nil)
|
||||
restore.Spec.ResourceModifier = &corev1api.TypedLocalObjectReference{
|
||||
Kind: "Secret",
|
||||
Name: "some-secret",
|
||||
}
|
||||
_, rm, _ := r.validateAndComplete(t.Context(), restore)
|
||||
assert.Nil(t, rm)
|
||||
assert.Empty(t, restore.Status.ValidationErrors)
|
||||
})
|
||||
|
||||
t.Run("default modifier validation failure is non-fatal", func(t *testing.T) {
|
||||
r := setupReconciler(t, "invalid-validation")
|
||||
require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "invalid-validation", Namespace: velerov1api.DefaultNamespace},
|
||||
Data: map[string]string{
|
||||
"modifiers.yaml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: pods\n patches:\n - operation: invalid\n path: \"/spec\"\n value: \"test\"\n",
|
||||
},
|
||||
}))
|
||||
|
||||
restore := newRestore("", nil)
|
||||
_, rm, _ := r.validateAndComplete(t.Context(), restore)
|
||||
assert.Nil(t, rm)
|
||||
assert.Empty(t, restore.Status.ValidationErrors)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBackupXorScheduleProvided(t *testing.T) {
|
||||
r := &velerov1api.Restore{}
|
||||
assert.False(t, backupXorScheduleProvided(r))
|
||||
|
||||
@@ -23,23 +23,23 @@ import (
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
cachetool "k8s.io/client-go/tools/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/credentials"
|
||||
veleroshared "github.com/vmware-tanzu/velero/pkg/apis/velero/shared"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
velerov2alpha1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1"
|
||||
"github.com/vmware-tanzu/velero/pkg/cbtservice"
|
||||
"github.com/vmware-tanzu/velero/pkg/datapath"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
"github.com/vmware-tanzu/velero/pkg/uploader"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -67,6 +67,11 @@ type BackupMicroService struct {
|
||||
duInformer cache.Informer
|
||||
duHandler cachetool.ResourceEventHandlerRegistration
|
||||
nodeName string
|
||||
|
||||
changeID string
|
||||
volumeID string
|
||||
snapshotID string
|
||||
cbtService cbtservice.Service
|
||||
}
|
||||
|
||||
type dataPathResult struct {
|
||||
@@ -76,7 +81,7 @@ type dataPathResult struct {
|
||||
|
||||
func NewBackupMicroService(ctx context.Context, client client.Client, kubeClient kubernetes.Interface, dataUploadName string, namespace string, nodeName string,
|
||||
sourceTargetPath datapath.AccessPoint, dataPathMgr *datapath.Manager, repoEnsurer *repository.Ensurer, cred *credentials.CredentialGetter,
|
||||
duInformer cache.Informer, log logrus.FieldLogger) *BackupMicroService {
|
||||
duInformer cache.Informer, changeID string, volumeID string, snapshotID string, cbtService cbtservice.Service, log logrus.FieldLogger) *BackupMicroService {
|
||||
return &BackupMicroService{
|
||||
ctx: ctx,
|
||||
client: client,
|
||||
@@ -91,6 +96,10 @@ func NewBackupMicroService(ctx context.Context, client client.Client, kubeClient
|
||||
nodeName: nodeName,
|
||||
resultSignal: make(chan dataPathResult),
|
||||
duInformer: duInformer,
|
||||
changeID: changeID,
|
||||
volumeID: volumeID,
|
||||
snapshotID: snapshotID,
|
||||
cbtService: cbtService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,16 +204,28 @@ func (r *BackupMicroService) RunCancelableDataPath(ctx context.Context) (string,
|
||||
velerov1api.AsyncOperationIDLabel: du.Labels[velerov1api.AsyncOperationIDLabel],
|
||||
}
|
||||
|
||||
// Modify the ParentSnapshot to "" and ForceFull to true when ParentSnapshot is "none".
|
||||
parentSnapshot := du.Spec.ParentSnapshot
|
||||
forceFull := false
|
||||
if du.Spec.ParentSnapshot == veleroshared.DataUploadParentSnapshotNone {
|
||||
parentSnapshot = ""
|
||||
forceFull = true
|
||||
}
|
||||
|
||||
if err := dp.StartBackup(r.sourceTargetPath, du.Spec.DataMoverConfig, &datapath.BackupStartParam{
|
||||
RealSource: GetRealSource(du.Spec.SourceNamespace, du.Spec.SourcePVC),
|
||||
ParentSnapshot: "",
|
||||
ForceFull: false,
|
||||
ParentSnapshot: parentSnapshot,
|
||||
ForceFull: forceFull,
|
||||
Tags: tags,
|
||||
VolumeID: r.volumeID,
|
||||
ChangeID: r.changeID,
|
||||
SnapshotID: r.snapshotID,
|
||||
CBTService: r.cbtService,
|
||||
}); err != nil {
|
||||
return "", errors.Wrap(err, "error starting data path backup")
|
||||
}
|
||||
|
||||
log.Info("Async fs backup data path started")
|
||||
log.Info("Async backup data path started")
|
||||
r.eventRecorder.Event(du, false, datapath.EventReasonStarted, "Data path for %s started", du.Name)
|
||||
|
||||
result := ""
|
||||
@@ -219,7 +240,7 @@ func (r *BackupMicroService) RunCancelableDataPath(ctx context.Context) (string,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Async fs backup was not completed")
|
||||
log.WithError(err).Error("Async backup was not completed")
|
||||
}
|
||||
|
||||
r.eventRecorder.EndingEvent(du, false, datapath.EventReasonStopped, "Data path for %s stopped", du.Name)
|
||||
@@ -256,12 +277,12 @@ func (r *BackupMicroService) OnDataUploadCompleted(ctx context.Context, namespac
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Async fs backup completed")
|
||||
log.Info("Async backup completed")
|
||||
}
|
||||
|
||||
func (r *BackupMicroService) OnDataUploadFailed(ctx context.Context, namespace string, duName string, err error) {
|
||||
log := r.logger.WithField("dataupload", duName)
|
||||
log.WithError(err).Error("Async fs backup data path failed")
|
||||
log.WithError(err).Error("Async backup data path failed")
|
||||
|
||||
r.eventRecorder.Event(r.dataUpload, false, datapath.EventReasonFailed, "Data path for data upload %s failed, error %v", r.dataUploadName, err)
|
||||
r.resultSignal <- dataPathResult{
|
||||
@@ -271,7 +292,7 @@ func (r *BackupMicroService) OnDataUploadFailed(ctx context.Context, namespace s
|
||||
|
||||
func (r *BackupMicroService) OnDataUploadCancelled(ctx context.Context, namespace string, duName string) {
|
||||
log := r.logger.WithField("dataupload", duName)
|
||||
log.Warn("Async fs backup data path canceled")
|
||||
log.Warn("Async backup data path canceled")
|
||||
|
||||
r.eventRecorder.Event(r.dataUpload, false, datapath.EventReasonCancelled, "Data path for data upload %s canceled", duName)
|
||||
r.resultSignal <- dataPathResult{
|
||||
|
||||
@@ -29,21 +29,16 @@ import (
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
"github.com/vmware-tanzu/velero/pkg/datapath"
|
||||
"github.com/vmware-tanzu/velero/pkg/uploader"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
|
||||
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
clientFake "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
velerov2alpha1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
|
||||
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
"github.com/vmware-tanzu/velero/pkg/datapath"
|
||||
datapathmockes "github.com/vmware-tanzu/velero/pkg/datapath/mocks"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
"github.com/vmware-tanzu/velero/pkg/uploader"
|
||||
)
|
||||
|
||||
type backupMsTestHelper struct {
|
||||
@@ -294,7 +289,10 @@ func TestCancelDataUpload(t *testing.T) {
|
||||
func TestRunCancelableDataPath(t *testing.T) {
|
||||
dataUploadName := "fake-data-upload"
|
||||
du := builder.ForDataUpload(velerov1api.DefaultNamespace, dataUploadName).Phase(velerov2alpha1api.DataUploadPhaseNew).Result()
|
||||
duInProgress := builder.ForDataUpload(velerov1api.DefaultNamespace, dataUploadName).Phase(velerov2alpha1api.DataUploadPhaseInProgress).Result()
|
||||
duInProgress := builder.ForDataUpload(velerov1api.DefaultNamespace, dataUploadName).Phase(velerov2alpha1api.DataUploadPhaseInProgress).CSISnapshot(
|
||||
&velerov2alpha1api.CSISnapshotSpec{
|
||||
VolumeSnapshot: "fake-snapshot",
|
||||
}).Result()
|
||||
ctxTimeout, cancel := context.WithTimeout(t.Context(), time.Second)
|
||||
|
||||
tests := []struct {
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/label"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
repotypes "github.com/vmware-tanzu/velero/pkg/repository/types"
|
||||
datamoverutil "github.com/vmware-tanzu/velero/pkg/util/datamover"
|
||||
)
|
||||
|
||||
type DataUploadDeleteAction struct {
|
||||
@@ -88,7 +89,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 !IsBuiltInDataMover(du.Spec.DataMover) || du.Status.SnapshotID == "" {
|
||||
if !datamoverutil.IsBuiltInDataMover(du.Spec.DataMover) || du.Status.SnapshotID == "" {
|
||||
return nil
|
||||
}
|
||||
snapshot := repotypes.SnapshotIdentifier{
|
||||
|
||||
@@ -180,11 +180,11 @@ func (r *RestoreMicroService) RunCancelableDataPath(ctx context.Context) (string
|
||||
}
|
||||
log.Info("fs init")
|
||||
|
||||
if err := dp.StartRestore(dd.Spec.SnapshotID, r.sourceTargetPath, dd.Spec.DataMoverConfig); err != nil {
|
||||
if err := dp.StartRestore(dd.Spec.SnapshotID, r.sourceTargetPath, dd.Spec.DataMoverConfig, &datapath.RestoreStartParam{}); err != nil {
|
||||
return "", errors.Wrap(err, "error starting data path restore")
|
||||
}
|
||||
|
||||
log.Info("Async fs restore data path started")
|
||||
log.Info("Async restore data path started")
|
||||
r.eventRecorder.Event(dd, false, datapath.EventReasonStarted, "Data path for %s started", dd.Name)
|
||||
|
||||
result := ""
|
||||
@@ -234,12 +234,12 @@ func (r *RestoreMicroService) OnDataDownloadCompleted(ctx context.Context, names
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Async fs restore data path completed")
|
||||
log.Info("Async restore data path completed")
|
||||
}
|
||||
|
||||
func (r *RestoreMicroService) OnDataDownloadFailed(ctx context.Context, namespace string, ddName string, err error) {
|
||||
log := r.logger.WithField("datadownload", ddName)
|
||||
log.WithError(err).Error("Async fs restore data path failed")
|
||||
log.WithError(err).Error("Async restore data path failed")
|
||||
|
||||
r.eventRecorder.Event(r.dataDownload, false, datapath.EventReasonFailed, "Data path for data download %s failed, error %v", r.dataDownloadName, err)
|
||||
r.resultSignal <- dataPathResult{
|
||||
@@ -249,7 +249,7 @@ func (r *RestoreMicroService) OnDataDownloadFailed(ctx context.Context, namespac
|
||||
|
||||
func (r *RestoreMicroService) OnDataDownloadCancelled(ctx context.Context, namespace string, ddName string) {
|
||||
log := r.logger.WithField("datadownload", ddName)
|
||||
log.Warn("Async fs restore data path canceled")
|
||||
log.Warn("Async restore data path canceled")
|
||||
|
||||
r.eventRecorder.Event(r.dataDownload, false, datapath.EventReasonCancelled, "Data path for data download %s canceled", ddName)
|
||||
r.resultSignal <- dataPathResult{
|
||||
|
||||
@@ -355,12 +355,12 @@ func TestRunCancelableRestore(t *testing.T) {
|
||||
|
||||
if test.startErr != nil {
|
||||
fsBR.On("Init", mock.Anything, mock.Anything).Return(nil)
|
||||
fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.startErr)
|
||||
fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.startErr)
|
||||
}
|
||||
|
||||
if test.dataPathStarted {
|
||||
fsBR.On("Init", mock.Anything, mock.Anything).Return(nil)
|
||||
fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
}
|
||||
|
||||
return fsBR
|
||||
|
||||
+8
-10
@@ -16,25 +16,23 @@ limitations under the License.
|
||||
|
||||
package datamover
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
const (
|
||||
DataMoverTypeVeleroFs string = "velero-fs"
|
||||
DataMoverTypeVeleroBlock string = "velero-block"
|
||||
"github.com/vmware-tanzu/velero/pkg/uploader"
|
||||
datamoverutil "github.com/vmware-tanzu/velero/pkg/util/datamover"
|
||||
)
|
||||
|
||||
func GetUploaderType(dataMover string) string {
|
||||
if dataMover == "" || dataMover == "velero" {
|
||||
return "kopia"
|
||||
if datamoverutil.IsVeleroFSDataMover(dataMover) {
|
||||
return uploader.KopiaType
|
||||
} else if datamoverutil.IsVeleroBlockDataMover(dataMover) {
|
||||
return uploader.BlockType
|
||||
} else {
|
||||
return dataMover
|
||||
}
|
||||
}
|
||||
|
||||
func IsBuiltInDataMover(dataMover string) bool {
|
||||
return dataMover == "" || dataMover == "velero"
|
||||
}
|
||||
|
||||
func GetRealSource(sourceNamespace string, pvcName string) string {
|
||||
return fmt.Sprintf("%s/%s", sourceNamespace, pvcName)
|
||||
}
|
||||
|
||||
+10
-29
@@ -6,35 +6,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsBuiltInUploader(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
dataMover string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty dataMover is builtin",
|
||||
dataMover: "",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "velero dataMover is builtin",
|
||||
dataMover: "velero",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "kopia dataMover is not builtin",
|
||||
dataMover: "kopia",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(tt *testing.T) {
|
||||
assert.Equal(tt, tc.want, IsBuiltInDataMover(tc.dataMover))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUploaderType(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
@@ -51,6 +22,16 @@ func TestGetUploaderType(t *testing.T) {
|
||||
input: "velero",
|
||||
want: "kopia",
|
||||
},
|
||||
{
|
||||
name: "velero-fs dataMover is kopia",
|
||||
input: "velero-fs",
|
||||
want: "kopia",
|
||||
},
|
||||
{
|
||||
name: "velero-block dataMover is velero-block",
|
||||
input: "velero-block",
|
||||
want: "velero-block",
|
||||
},
|
||||
{
|
||||
name: "kopia dataMover is kopia",
|
||||
input: "kopia",
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/credentials"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/cbtservice"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
repokey "github.com/vmware-tanzu/velero/pkg/repository/keys"
|
||||
repoProvider "github.com/vmware-tanzu/velero/pkg/repository/provider"
|
||||
@@ -53,6 +54,14 @@ type BackupStartParam struct {
|
||||
ParentSnapshot string
|
||||
ForceFull bool
|
||||
Tags map[string]string
|
||||
VolumeID string
|
||||
ChangeID string
|
||||
SnapshotID string
|
||||
CBTService cbtservice.Service
|
||||
}
|
||||
|
||||
// RestoreStartParam define the input param for restore start
|
||||
type RestoreStartParam struct {
|
||||
}
|
||||
|
||||
type generalDataPath struct {
|
||||
@@ -182,8 +191,25 @@ func (dp *generalDataPath) StartBackup(source AccessPoint, uploaderConfig map[st
|
||||
dp.wgDataPath.Done()
|
||||
}()
|
||||
|
||||
snapshotID, emptySnapshot, totalBytes, incrementalBytes, err := dp.uploaderProv.RunBackup(dp.ctx, source.ByPath, backupParam.RealSource, backupParam.Tags, backupParam.ForceFull,
|
||||
backupParam.ParentSnapshot, provider.CBTParam{}, source.VolMode, uploaderConfig, dp)
|
||||
snapshotID, emptySnapshot, totalBytes, incrementalBytes, err := dp.uploaderProv.RunBackup(
|
||||
dp.ctx,
|
||||
source.ByPath,
|
||||
backupParam.RealSource,
|
||||
backupParam.Tags,
|
||||
backupParam.ForceFull,
|
||||
backupParam.ParentSnapshot,
|
||||
provider.CBTParam{
|
||||
Source: cbtservice.SourceInfo{
|
||||
Snapshot: backupParam.SnapshotID,
|
||||
VolumeID: backupParam.VolumeID,
|
||||
ChangeID: backupParam.ChangeID,
|
||||
},
|
||||
Service: backupParam.CBTService,
|
||||
},
|
||||
source.VolMode,
|
||||
uploaderConfig,
|
||||
dp,
|
||||
)
|
||||
|
||||
if err == provider.ErrorCanceled {
|
||||
dp.callbacks.OnCancelled(context.Background(), dp.namespace, dp.jobName)
|
||||
@@ -201,7 +227,7 @@ func (dp *generalDataPath) StartBackup(source AccessPoint, uploaderConfig map[st
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dp *generalDataPath) StartRestore(snapshotID string, target AccessPoint, uploaderConfigs map[string]string) error {
|
||||
func (dp *generalDataPath) StartRestore(snapshotID string, target AccessPoint, uploaderConfigs map[string]string, param any) error {
|
||||
if !dp.initialized {
|
||||
return errors.New("data path is not initialized")
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ func TestAsyncRestore(t *testing.T) {
|
||||
dp.initialized = true
|
||||
dp.callbacks = test.callbacks
|
||||
|
||||
err := dp.StartRestore(test.snapshot, AccessPoint{ByPath: test.path}, map[string]string{})
|
||||
err := dp.StartRestore(test.snapshot, AccessPoint{ByPath: test.path}, map[string]string{}, &RestoreStartParam{})
|
||||
require.NoError(t, err)
|
||||
|
||||
<-finish
|
||||
|
||||
@@ -221,7 +221,7 @@ func (ms *microServiceBRWatcher) StartBackup(source AccessPoint, uploaderConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ms *microServiceBRWatcher) StartRestore(snapshotID string, target AccessPoint, uploaderConfigs map[string]string) error {
|
||||
func (ms *microServiceBRWatcher) StartRestore(snapshotID string, target AccessPoint, uploaderConfigs map[string]string, param any) error {
|
||||
ms.log.Infof("Start watching restore ms to target %s, from snapshot %s", target.ByPath, snapshotID)
|
||||
|
||||
ms.startWatch()
|
||||
|
||||
@@ -60,17 +60,17 @@ func (_m *AsyncBR) StartBackup(source datapath.AccessPoint, dataMoverConfig map[
|
||||
return r0
|
||||
}
|
||||
|
||||
// StartRestore provides a mock function with given fields: snapshotID, target, dataMoverConfig
|
||||
func (_m *AsyncBR) StartRestore(snapshotID string, target datapath.AccessPoint, dataMoverConfig map[string]string) error {
|
||||
ret := _m.Called(snapshotID, target, dataMoverConfig)
|
||||
// StartRestore provides a mock function with given fields: snapshotID, target, dataMoverConfig, param
|
||||
func (_m *AsyncBR) StartRestore(snapshotID string, target datapath.AccessPoint, dataMoverConfig map[string]string, param interface{}) error {
|
||||
ret := _m.Called(snapshotID, target, dataMoverConfig, param)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for StartRestore")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, datapath.AccessPoint, map[string]string) error); ok {
|
||||
r0 = rf(snapshotID, target, dataMoverConfig)
|
||||
if rf, ok := ret.Get(0).(func(string, datapath.AccessPoint, map[string]string, interface{}) error); ok {
|
||||
r0 = rf(snapshotID, target, dataMoverConfig, param)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ type AsyncBR interface {
|
||||
StartBackup(source AccessPoint, dataMoverConfig map[string]string, param any) error
|
||||
|
||||
// StartRestore starts an asynchronous data path instance for restore
|
||||
StartRestore(snapshotID string, target AccessPoint, dataMoverConfig map[string]string) error
|
||||
StartRestore(snapshotID string, target AccessPoint, dataMoverConfig map[string]string, param any) error
|
||||
|
||||
// Cancel cancels an asynchronous data path instance
|
||||
Cancel()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright The Velero Contributors.
|
||||
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.
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
@@ -34,12 +35,12 @@ 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"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/csi"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/datamover"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
)
|
||||
|
||||
@@ -110,6 +111,12 @@ type CSISnapshotExposeWaitParam struct {
|
||||
NodeName string
|
||||
}
|
||||
|
||||
type cbtInfo struct {
|
||||
changeID string
|
||||
volumeID string
|
||||
snapshotID string
|
||||
}
|
||||
|
||||
// NewCSISnapshotExposer create a new instance of CSI snapshot exposer
|
||||
func NewCSISnapshotExposer(kubeClient kubernetes.Interface, csiSnapshotClient snapshotter.SnapshotV1Interface, log logrus.FieldLogger) SnapshotExposer {
|
||||
return &csiSnapshotExposer{
|
||||
@@ -256,6 +263,14 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1api.O
|
||||
|
||||
affinity := kube.GetLoadAffinityByStorageClass(csiExposeParam.Affinity, backupPVCStorageClass, curLog)
|
||||
|
||||
var cbtInfo cbtInfo
|
||||
if csiExposeParam.DataMover == datamover.DataMoverTypeVeleroBlock {
|
||||
cbtInfo, err = e.getCBTInfo(ctx, backupVS, backupVSC, csiExposeParam.SourcePVName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error to get CBT info")
|
||||
}
|
||||
}
|
||||
|
||||
backupPod, err := e.createBackupPod(
|
||||
ctx,
|
||||
ownerObject,
|
||||
@@ -273,6 +288,7 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1api.O
|
||||
intoleratableNodes,
|
||||
volumeTopology,
|
||||
csiExposeParam.SnapshotMetadataServiceConfigs,
|
||||
&cbtInfo,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error to create backup pod")
|
||||
@@ -289,6 +305,49 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1api.O
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *csiSnapshotExposer) getCBTInfo(ctx context.Context, vs *snapshotv1api.VolumeSnapshot, vsc *snapshotv1api.VolumeSnapshotContent, sourcePVName string) (cbtInfo, error) {
|
||||
cbtInfo := cbtInfo{}
|
||||
if vs == nil || vsc == nil {
|
||||
return cbtInfo, errors.New("vs or vsc is nil")
|
||||
}
|
||||
|
||||
cbtInfo.snapshotID = vs.Name
|
||||
|
||||
if vs.Annotations != nil &&
|
||||
(vs.Annotations[util.VSphereCNSChangeIDAnno] != "" ||
|
||||
vs.Annotations[util.VSphereCNSSnapshotAnno] != "") {
|
||||
cbtInfo.changeID = vs.Annotations[util.VSphereCNSChangeIDAnno]
|
||||
|
||||
splitSnapshotAnno := strings.Split(vs.Annotations[util.VSphereCNSSnapshotAnno], "+")
|
||||
if len(splitSnapshotAnno) >= 2 {
|
||||
cbtInfo.volumeID = splitSnapshotAnno[0]
|
||||
}
|
||||
|
||||
e.log.Debugf("volumeID %s and changeID %s are read from VKS annotations.", cbtInfo.volumeID, cbtInfo.changeID)
|
||||
} else {
|
||||
pv, err := e.kubeClient.CoreV1().PersistentVolumes().Get(ctx, sourcePVName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return cbtInfo, fmt.Errorf("failed to get pv %s: %w", sourcePVName, err)
|
||||
}
|
||||
|
||||
if vsc.Status != nil && vsc.Status.SnapshotHandle != nil {
|
||||
cbtInfo.changeID = *vsc.Status.SnapshotHandle
|
||||
}
|
||||
|
||||
if pv.Spec.CSI != nil && pv.Spec.CSI.VolumeHandle != "" {
|
||||
cbtInfo.volumeID = pv.Spec.CSI.VolumeHandle
|
||||
}
|
||||
|
||||
e.log.Debugf("volumeID %s and changeID %s are read from PV and VS's handles.", cbtInfo.volumeID, cbtInfo.changeID)
|
||||
}
|
||||
|
||||
if cbtInfo.volumeID == "" {
|
||||
return cbtInfo, fmt.Errorf("volumeID must not be empty for CBT")
|
||||
}
|
||||
|
||||
return cbtInfo, nil
|
||||
}
|
||||
|
||||
func (e *csiSnapshotExposer) GetExposed(ctx context.Context, ownerObject corev1api.ObjectReference, timeout time.Duration, param any) (*ExposeResult, error) {
|
||||
exposeWaitParam := param.(*CSISnapshotExposeWaitParam)
|
||||
|
||||
@@ -618,13 +677,16 @@ func (e *csiSnapshotExposer) createBackupPod(
|
||||
intoleratableNodes []string,
|
||||
volumeTopology *corev1api.NodeSelector,
|
||||
csiSnapshotMetadataServiceConfigs *velerotypes.CSISnapshotMetadataService,
|
||||
cbtInfo *cbtInfo,
|
||||
) (*corev1api.Pod, error) {
|
||||
podName := ownerObject.Name
|
||||
|
||||
containerName := string(ownerObject.UID)
|
||||
volumeName := string(ownerObject.UID)
|
||||
|
||||
podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, nodeOS)
|
||||
// The backup pod reads the data through the backup PVC only, so the node-agent's host
|
||||
// path volumes to the kubelet root directory are not inherited.
|
||||
podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, nodeOS, excludeHostPathVolumes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error to get inherited pod info from node-agent")
|
||||
}
|
||||
@@ -670,6 +732,12 @@ func (e *csiSnapshotExposer) createBackupPod(
|
||||
fmt.Sprintf("--resource-timeout=%s", operationTimeout.String()),
|
||||
}
|
||||
|
||||
if cbtInfo != nil {
|
||||
args = append(args, fmt.Sprintf("--change-id=%s", cbtInfo.changeID))
|
||||
args = append(args, fmt.Sprintf("--volume-id=%s", cbtInfo.volumeID))
|
||||
args = append(args, fmt.Sprintf("--snapshot-id=%s", cbtInfo.snapshotID))
|
||||
}
|
||||
|
||||
args = append(args, podInfo.logFormatArgs...)
|
||||
args = append(args, podInfo.logLevelArgs...)
|
||||
|
||||
|
||||
@@ -156,6 +156,7 @@ func TestCreateBackupPodWithPriorityClass(t *testing.T) {
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
require.NoError(t, err, tc.description)
|
||||
@@ -243,6 +244,7 @@ func TestCreateBackupPodWithMissingConfigMap(t *testing.T) {
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
// Should succeed even when config map is missing
|
||||
|
||||
@@ -17,22 +17,27 @@ limitations under the License.
|
||||
package exposer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
|
||||
snapshotFake "github.com/kubernetes-csi/external-snapshotter/client/v8/clientset/versioned/fake"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
appsv1api "k8s.io/api/apps/v1"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
storagev1api "k8s.io/api/storage/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
kubefake "k8s.io/client-go/kubernetes/fake"
|
||||
clientTesting "k8s.io/client-go/testing"
|
||||
"k8s.io/utils/ptr"
|
||||
clientFake "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
@@ -42,9 +47,8 @@ import (
|
||||
velerotypes "github.com/vmware-tanzu/velero/pkg/types"
|
||||
"github.com/vmware-tanzu/velero/pkg/util"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/datamover"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
|
||||
storagev1api "k8s.io/api/storage/v1"
|
||||
)
|
||||
|
||||
type reactor struct {
|
||||
@@ -191,6 +195,19 @@ func TestExpose(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
sourcePV := &corev1api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "fake-pv",
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeSpec{
|
||||
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
||||
CSI: &corev1api.CSIPersistentVolumeSource{
|
||||
VolumeHandle: "csi-volume-handle",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
snapshotClientObj []runtime.Object
|
||||
@@ -1015,6 +1032,46 @@ func TestExpose(t *testing.T) {
|
||||
},
|
||||
expectedPVCAnnotation: map[string]string{util.VSphereCNSFastCloneAnno: "true"},
|
||||
},
|
||||
{
|
||||
name: "block data mover success",
|
||||
ownerBackup: backup,
|
||||
exposeParam: CSISnapshotExposeParam{
|
||||
SnapshotName: "fake-vs",
|
||||
SourceNamespace: "fake-ns",
|
||||
AccessMode: AccessModeFileSystem,
|
||||
OperationTimeout: time.Millisecond,
|
||||
ExposeTimeout: time.Millisecond,
|
||||
StorageClass: "fake-sc",
|
||||
SourcePVName: "fake-pv",
|
||||
DataMover: datamover.DataMoverTypeVeleroBlock,
|
||||
},
|
||||
snapshotClientObj: []runtime.Object{
|
||||
vsObject,
|
||||
vscObj,
|
||||
},
|
||||
kubeClientObj: []runtime.Object{
|
||||
daemonSet,
|
||||
scObj,
|
||||
sourcePV,
|
||||
},
|
||||
expectedAffinity: &corev1api.Affinity{
|
||||
NodeAffinity: &corev1api.NodeAffinity{
|
||||
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
|
||||
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
|
||||
{
|
||||
MatchExpressions: []corev1api.NodeSelectorRequirement{
|
||||
{
|
||||
Key: "kubernetes.io/os",
|
||||
Operator: corev1api.NodeSelectorOpNotIn,
|
||||
Values: []string{"windows"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -1994,3 +2051,150 @@ end diagnose CSI exposer`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCBTInfo(t *testing.T) {
|
||||
handle := "snapshot-handle-1"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vs *snapshotv1api.VolumeSnapshot
|
||||
vsc *snapshotv1api.VolumeSnapshotContent
|
||||
pv *corev1api.PersistentVolume
|
||||
sourcePVName string
|
||||
want cbtInfo
|
||||
wantErrSubstr string
|
||||
}{
|
||||
{
|
||||
name: "return error when vs is nil",
|
||||
vs: nil,
|
||||
vsc: &snapshotv1api.VolumeSnapshotContent{},
|
||||
sourcePVName: "pv-1",
|
||||
wantErrSubstr: "vs or vsc is nil",
|
||||
},
|
||||
{
|
||||
name: "use annotations when change-id and snapshot annotation exist",
|
||||
vs: &snapshotv1api.VolumeSnapshot{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "vs-anno",
|
||||
Annotations: map[string]string{
|
||||
util.VSphereCNSChangeIDAnno: "change-id-1",
|
||||
util.VSphereCNSSnapshotAnno: "volume-id-1+snapshot-id-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
vsc: &snapshotv1api.VolumeSnapshotContent{},
|
||||
sourcePVName: "pv-ignored",
|
||||
want: cbtInfo{
|
||||
changeID: "change-id-1",
|
||||
volumeID: "volume-id-1",
|
||||
snapshotID: "vs-anno",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fallback to pv and vsc snapshot handle",
|
||||
vs: &snapshotv1api.VolumeSnapshot{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "vs-fallback"},
|
||||
},
|
||||
vsc: &snapshotv1api.VolumeSnapshotContent{
|
||||
Status: &snapshotv1api.VolumeSnapshotContentStatus{
|
||||
SnapshotHandle: &handle,
|
||||
},
|
||||
},
|
||||
pv: &corev1api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "pv-1"},
|
||||
Spec: corev1api.PersistentVolumeSpec{
|
||||
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
||||
CSI: &corev1api.CSIPersistentVolumeSource{
|
||||
VolumeHandle: "csi-volume-handle-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourcePVName: "pv-1",
|
||||
want: cbtInfo{
|
||||
changeID: "snapshot-handle-1",
|
||||
volumeID: "csi-volume-handle-1",
|
||||
snapshotID: "vs-fallback",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "return error when pv not found in fallback path",
|
||||
vs: &snapshotv1api.VolumeSnapshot{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "vs-no-pv"},
|
||||
},
|
||||
vsc: &snapshotv1api.VolumeSnapshotContent{},
|
||||
sourcePVName: "pv-not-found",
|
||||
wantErrSubstr: "failed to get pv pv-not-found",
|
||||
},
|
||||
{
|
||||
name: "return error when pv has no csi volume handle",
|
||||
vs: &snapshotv1api.VolumeSnapshot{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "vs-no-volume-handle"},
|
||||
},
|
||||
vsc: &snapshotv1api.VolumeSnapshotContent{},
|
||||
pv: &corev1api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "pv-no-handle"},
|
||||
Spec: corev1api.PersistentVolumeSpec{},
|
||||
},
|
||||
sourcePVName: "pv-no-handle",
|
||||
wantErrSubstr: "volumeID must not be empty for CBT",
|
||||
},
|
||||
{
|
||||
name: "return error when snapshot annotation is invalid",
|
||||
vs: &snapshotv1api.VolumeSnapshot{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "vs-no-volume-handle",
|
||||
Annotations: map[string]string{
|
||||
util.VSphereCNSChangeIDAnno: "change-id-1",
|
||||
util.VSphereCNSSnapshotAnno: "volume-id-1:snapshot-id-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
vsc: &snapshotv1api.VolumeSnapshotContent{},
|
||||
pv: &corev1api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "pv-1"},
|
||||
Spec: corev1api.PersistentVolumeSpec{
|
||||
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
||||
CSI: &corev1api.CSIPersistentVolumeSource{
|
||||
VolumeHandle: "csi-volume-handle-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourcePVName: "pv-1",
|
||||
wantErrSubstr: "volumeID must not be empty for CBT",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var objs []runtime.Object
|
||||
if tc.pv != nil {
|
||||
objs = append(objs, tc.pv)
|
||||
}
|
||||
exposer := &csiSnapshotExposer{
|
||||
kubeClient: kubefake.NewSimpleClientset(objs...),
|
||||
log: logrus.StandardLogger(),
|
||||
}
|
||||
|
||||
got, err := exposer.getCBTInfo(context.Background(), tc.vs, tc.vsc, tc.sourcePVName)
|
||||
|
||||
if tc.wantErrSubstr != "" {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error containing %q, got nil", tc.wantErrSubstr)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantErrSubstr) {
|
||||
t.Fatalf("expected error containing %q, got %q", tc.wantErrSubstr, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got.changeID != tc.want.changeID || got.volumeID != tc.want.volumeID || got.snapshotID != tc.want.snapshotID {
|
||||
t.Fatalf("unexpected cbtInfo, want %+v, got %+v", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright The Velero Contributors.
|
||||
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.
|
||||
@@ -31,10 +31,10 @@ 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/boolptr"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/datamover"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
)
|
||||
|
||||
@@ -628,7 +628,9 @@ func (e *genericRestoreExposer) createRestorePod(
|
||||
affinity = &kube.LoadAffinity{}
|
||||
}
|
||||
|
||||
podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, nodeOS)
|
||||
// The restore pod writes the data through the restore PVC only, so the node-agent's host
|
||||
// path volumes to the kubelet root directory are not inherited.
|
||||
podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, nodeOS, excludeHostPathVolumes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error to get inherited pod info from node-agent")
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user