mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-08-19 21:56:12 +00:00
Merge branch 'main' into 5048-CSI-snapshot-timeout-configurable
This commit is contained in:
+28
-21
@@ -20,9 +20,9 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// ResticRepositorySpec is the specification for a ResticRepository.
|
||||
type ResticRepositorySpec struct {
|
||||
// VolumeNamespace is the namespace this restic repository contains
|
||||
// BackupRepositorySpec is the specification for a BackupRepository.
|
||||
type BackupRepositorySpec struct {
|
||||
// VolumeNamespace is the namespace this backup repository contains
|
||||
// pod volume backups for.
|
||||
VolumeNamespace string `json:"volumeNamespace"`
|
||||
|
||||
@@ -30,6 +30,11 @@ type ResticRepositorySpec struct {
|
||||
// that should contain this repository.
|
||||
BackupStorageLocation string `json:"backupStorageLocation"`
|
||||
|
||||
// RepositoryType indicates the type of the backend repository
|
||||
// +kubebuilder:validation:Enum=kopia;restic;""
|
||||
// +optional
|
||||
RepositoryType string `json:"repositoryType"`
|
||||
|
||||
// ResticIdentifier is the full restic-compatible string for identifying
|
||||
// this repository.
|
||||
ResticIdentifier string `json:"resticIdentifier"`
|
||||
@@ -38,23 +43,23 @@ type ResticRepositorySpec struct {
|
||||
MaintenanceFrequency metav1.Duration `json:"maintenanceFrequency"`
|
||||
}
|
||||
|
||||
// ResticRepositoryPhase represents the lifecycle phase of a ResticRepository.
|
||||
// BackupRepositoryPhase represents the lifecycle phase of a BackupRepository.
|
||||
// +kubebuilder:validation:Enum=New;Ready;NotReady
|
||||
type ResticRepositoryPhase string
|
||||
type BackupRepositoryPhase string
|
||||
|
||||
const (
|
||||
ResticRepositoryPhaseNew ResticRepositoryPhase = "New"
|
||||
ResticRepositoryPhaseReady ResticRepositoryPhase = "Ready"
|
||||
ResticRepositoryPhaseNotReady ResticRepositoryPhase = "NotReady"
|
||||
BackupRepositoryPhaseNew BackupRepositoryPhase = "New"
|
||||
BackupRepositoryPhaseReady BackupRepositoryPhase = "Ready"
|
||||
BackupRepositoryPhaseNotReady BackupRepositoryPhase = "NotReady"
|
||||
)
|
||||
|
||||
// ResticRepositoryStatus is the current status of a ResticRepository.
|
||||
type ResticRepositoryStatus struct {
|
||||
// Phase is the current state of the ResticRepository.
|
||||
// BackupRepositoryStatus is the current status of a BackupRepository.
|
||||
type BackupRepositoryStatus struct {
|
||||
// Phase is the current state of the BackupRepository.
|
||||
// +optional
|
||||
Phase ResticRepositoryPhase `json:"phase,omitempty"`
|
||||
Phase BackupRepositoryPhase `json:"phase,omitempty"`
|
||||
|
||||
// Message is a message about the current status of the ResticRepository.
|
||||
// Message is a message about the current status of the BackupRepository.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty"`
|
||||
|
||||
@@ -72,33 +77,35 @@ type ResticRepositoryStatus struct {
|
||||
// +kubebuilder:object:generate=true
|
||||
// +kubebuilder:storageversion
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
|
||||
// +kubebuilder:printcolumn:name="Repository Type",type="string",JSONPath=".spec.repositoryType"
|
||||
//
|
||||
|
||||
type ResticRepository struct {
|
||||
type BackupRepository struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// +optional
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// +optional
|
||||
Spec ResticRepositorySpec `json:"spec,omitempty"`
|
||||
Spec BackupRepositorySpec `json:"spec,omitempty"`
|
||||
|
||||
// +optional
|
||||
Status ResticRepositoryStatus `json:"status,omitempty"`
|
||||
Status BackupRepositoryStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// TODO(2.0) After converting all resources to use the runtime-controller client,
|
||||
// the k8s:deepcopy marker will no longer be needed and should be removed.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:rbac:groups=velero.io,resources=resticrepositories,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=velero.io,resources=resticrepositories/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=velero.io,resources=backuprepositories,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=velero.io,resources=backuprepositories/status,verbs=get;update;patch
|
||||
|
||||
// ResticRepositoryList is a list of ResticRepositories.
|
||||
type ResticRepositoryList struct {
|
||||
// BackupRepositoryList is a list of BackupRepositories.
|
||||
type BackupRepositoryList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// +optional
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []ResticRepository `json:"items"`
|
||||
Items []BackupRepository `json:"items"`
|
||||
}
|
||||
@@ -34,12 +34,17 @@ type PodVolumeBackupSpec struct {
|
||||
Volume string `json:"volume"`
|
||||
|
||||
// BackupStorageLocation is the name of the backup storage location
|
||||
// where the restic repository is stored.
|
||||
// where the backup repository is stored.
|
||||
BackupStorageLocation string `json:"backupStorageLocation"`
|
||||
|
||||
// RepoIdentifier is the restic repository identifier.
|
||||
// RepoIdentifier is the backup repository identifier.
|
||||
RepoIdentifier string `json:"repoIdentifier"`
|
||||
|
||||
// UploaderType is the type of the uploader to handle the data transfer.
|
||||
// +kubebuilder:validation:Enum=kopia;restic;""
|
||||
// +optional
|
||||
UploaderType string `json:"uploaderType"`
|
||||
|
||||
// Tags are a map of key-value pairs that should be applied to the
|
||||
// volume backup as tags.
|
||||
// +optional
|
||||
@@ -107,7 +112,8 @@ type PodVolumeBackupStatus struct {
|
||||
// +kubebuilder:printcolumn:name="Namespace",type="string",JSONPath=".spec.pod.namespace",description="Namespace of the pod containing the volume to be backed up"
|
||||
// +kubebuilder:printcolumn:name="Pod",type="string",JSONPath=".spec.pod.name",description="Name of the pod containing the volume to be backed up"
|
||||
// +kubebuilder:printcolumn:name="Volume",type="string",JSONPath=".spec.volume",description="Name of the volume to be backed up"
|
||||
// +kubebuilder:printcolumn:name="Restic Repo",type="string",JSONPath=".spec.repoIdentifier",description="Restic repository identifier for this backup"
|
||||
// +kubebuilder:printcolumn:name="Repository ID",type="string",JSONPath=".spec.repoIdentifier",description="Backup repository identifier for this backup"
|
||||
// +kubebuilder:printcolumn:name="Uploader Type",type="string",JSONPath=".spec.uploaderType",description="The type of the uploader to handle data transfer"
|
||||
// +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"
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
@@ -30,12 +30,17 @@ type PodVolumeRestoreSpec struct {
|
||||
Volume string `json:"volume"`
|
||||
|
||||
// BackupStorageLocation is the name of the backup storage location
|
||||
// where the restic repository is stored.
|
||||
// where the backup repository is stored.
|
||||
BackupStorageLocation string `json:"backupStorageLocation"`
|
||||
|
||||
// RepoIdentifier is the restic repository identifier.
|
||||
// RepoIdentifier is the backup repository identifier.
|
||||
RepoIdentifier string `json:"repoIdentifier"`
|
||||
|
||||
// UploaderType is the type of the uploader to handle the data transfer.
|
||||
// +kubebuilder:validation:Enum=kopia;restic;""
|
||||
// +optional
|
||||
UploaderType string `json:"uploaderType"`
|
||||
|
||||
// SnapshotID is the ID of the volume snapshot to be restored.
|
||||
SnapshotID string `json:"snapshotID"`
|
||||
}
|
||||
@@ -89,6 +94,7 @@ type PodVolumeRestoreStatus struct {
|
||||
// +kubebuilder:storageversion
|
||||
// +kubebuilder:printcolumn:name="Namespace",type="string",JSONPath=".spec.pod.namespace",description="Namespace of the pod containing the volume to be restored"
|
||||
// +kubebuilder:printcolumn:name="Pod",type="string",JSONPath=".spec.pod.name",description="Name of the pod containing the volume to be restored"
|
||||
// +kubebuilder:printcolumn:name="Uploader Type",type="string",JSONPath=".spec.uploaderType",description="The type of the uploader to handle data transfer"
|
||||
// +kubebuilder:printcolumn:name="Volume",type="string",JSONPath=".spec.volume",description="Name of the volume to be restored"
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase",description="Pod Volume Restore status such as New/InProgress"
|
||||
// +kubebuilder:printcolumn:name="TotalBytes",type="integer",format="int64",JSONPath=".status.progress.totalBytes",description="Pod Volume Restore status such as New/InProgress"
|
||||
|
||||
@@ -52,7 +52,7 @@ func CustomResources() map[string]typeInfo {
|
||||
"DeleteBackupRequest": newTypeInfo("deletebackuprequests", &DeleteBackupRequest{}, &DeleteBackupRequestList{}),
|
||||
"PodVolumeBackup": newTypeInfo("podvolumebackups", &PodVolumeBackup{}, &PodVolumeBackupList{}),
|
||||
"PodVolumeRestore": newTypeInfo("podvolumerestores", &PodVolumeRestore{}, &PodVolumeRestoreList{}),
|
||||
"ResticRepository": newTypeInfo("resticrepositories", &ResticRepository{}, &ResticRepositoryList{}),
|
||||
"BackupRepository": newTypeInfo("backuprepositories", &BackupRepository{}, &BackupRepositoryList{}),
|
||||
"BackupStorageLocation": newTypeInfo("backupstoragelocations", &BackupStorageLocation{}, &BackupStorageLocationList{}),
|
||||
"VolumeSnapshotLocation": newTypeInfo("volumesnapshotlocations", &VolumeSnapshotLocation{}, &VolumeSnapshotLocationList{}),
|
||||
"ServerStatusRequest": newTypeInfo("serverstatusrequests", &ServerStatusRequest{}, &ServerStatusRequestList{}),
|
||||
|
||||
@@ -17,8 +17,8 @@ limitations under the License.
|
||||
package v1
|
||||
|
||||
import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// RestoreSpec defines the specification for a Velero restore.
|
||||
@@ -208,9 +208,10 @@ type ExecRestoreHook struct {
|
||||
// InitRestoreHook is a hook that adds an init container to a PodSpec to run commands before the
|
||||
// workload pod is able to start.
|
||||
type InitRestoreHook struct {
|
||||
// +kubebuilder:pruning:PreserveUnknownFields
|
||||
// InitContainers is list of init containers to be added to a pod during its restore.
|
||||
// +optional
|
||||
InitContainers []v1.Container `json:"initContainers"`
|
||||
InitContainers []runtime.RawExtension `json:"initContainers"`
|
||||
|
||||
// Timeout defines the maximum amount of time Velero should wait for the initContainers to complete.
|
||||
// +optional
|
||||
|
||||
+7
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2018 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,10 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:shortName=vsl
|
||||
// +kubebuilder:object:generate=true
|
||||
// +kubebuilder:storageversion
|
||||
|
||||
// VolumeSnapshotLocation is a location where Velero stores volume snapshots.
|
||||
type VolumeSnapshotLocation struct {
|
||||
@@ -36,6 +40,8 @@ type VolumeSnapshotLocation struct {
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:rbac:groups=velero.io,resources=volumesnapshotlocations,verbs=get;list;watch;create;update;patch;delete
|
||||
|
||||
// VolumeSnapshotLocationList is a list of VolumeSnapshotLocations.
|
||||
type VolumeSnapshotLocationList struct {
|
||||
@@ -107,6 +107,100 @@ func (in *BackupProgress) DeepCopy() *BackupProgress {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BackupRepository) DeepCopyInto(out *BackupRepository) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupRepository.
|
||||
func (in *BackupRepository) DeepCopy() *BackupRepository {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupRepository)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *BackupRepository) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BackupRepositoryList) DeepCopyInto(out *BackupRepositoryList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]BackupRepository, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupRepositoryList.
|
||||
func (in *BackupRepositoryList) DeepCopy() *BackupRepositoryList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupRepositoryList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *BackupRepositoryList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BackupRepositorySpec) DeepCopyInto(out *BackupRepositorySpec) {
|
||||
*out = *in
|
||||
out.MaintenanceFrequency = in.MaintenanceFrequency
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupRepositorySpec.
|
||||
func (in *BackupRepositorySpec) DeepCopy() *BackupRepositorySpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupRepositorySpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BackupRepositoryStatus) DeepCopyInto(out *BackupRepositoryStatus) {
|
||||
*out = *in
|
||||
if in.LastMaintenanceTime != nil {
|
||||
in, out := &in.LastMaintenanceTime, &out.LastMaintenanceTime
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupRepositoryStatus.
|
||||
func (in *BackupRepositoryStatus) DeepCopy() *BackupRepositoryStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupRepositoryStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BackupResourceHook) DeepCopyInto(out *BackupResourceHook) {
|
||||
*out = *in
|
||||
@@ -671,7 +765,7 @@ func (in *InitRestoreHook) DeepCopyInto(out *InitRestoreHook) {
|
||||
*out = *in
|
||||
if in.InitContainers != nil {
|
||||
in, out := &in.InitContainers, &out.InitContainers
|
||||
*out = make([]corev1.Container, len(*in))
|
||||
*out = make([]runtime.RawExtension, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
@@ -966,100 +1060,6 @@ func (in *PodVolumeRestoreStatus) DeepCopy() *PodVolumeRestoreStatus {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ResticRepository) DeepCopyInto(out *ResticRepository) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResticRepository.
|
||||
func (in *ResticRepository) DeepCopy() *ResticRepository {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ResticRepository)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ResticRepository) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ResticRepositoryList) DeepCopyInto(out *ResticRepositoryList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]ResticRepository, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResticRepositoryList.
|
||||
func (in *ResticRepositoryList) DeepCopy() *ResticRepositoryList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ResticRepositoryList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ResticRepositoryList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ResticRepositorySpec) DeepCopyInto(out *ResticRepositorySpec) {
|
||||
*out = *in
|
||||
out.MaintenanceFrequency = in.MaintenanceFrequency
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResticRepositorySpec.
|
||||
func (in *ResticRepositorySpec) DeepCopy() *ResticRepositorySpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ResticRepositorySpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ResticRepositoryStatus) DeepCopyInto(out *ResticRepositoryStatus) {
|
||||
*out = *in
|
||||
if in.LastMaintenanceTime != nil {
|
||||
in, out := &in.LastMaintenanceTime, &out.LastMaintenanceTime
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResticRepositoryStatus.
|
||||
func (in *ResticRepositoryStatus) DeepCopy() *ResticRepositoryStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ResticRepositoryStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Restore) DeepCopyInto(out *Restore) {
|
||||
*out = *in
|
||||
|
||||
@@ -46,7 +46,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/framework"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
"github.com/vmware-tanzu/velero/pkg/podexec"
|
||||
"github.com/vmware-tanzu/velero/pkg/restic"
|
||||
"github.com/vmware-tanzu/velero/pkg/podvolume"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/collections"
|
||||
)
|
||||
@@ -74,7 +74,7 @@ type kubernetesBackupper struct {
|
||||
dynamicFactory client.DynamicFactory
|
||||
discoveryHelper discovery.Helper
|
||||
podCommandExecutor podexec.PodCommandExecutor
|
||||
resticBackupperFactory restic.BackupperFactory
|
||||
resticBackupperFactory podvolume.BackupperFactory
|
||||
resticTimeout time.Duration
|
||||
defaultVolumesToRestic bool
|
||||
clientPageSize int
|
||||
@@ -100,7 +100,7 @@ func NewKubernetesBackupper(
|
||||
discoveryHelper discovery.Helper,
|
||||
dynamicFactory client.DynamicFactory,
|
||||
podCommandExecutor podexec.PodCommandExecutor,
|
||||
resticBackupperFactory restic.BackupperFactory,
|
||||
resticBackupperFactory podvolume.BackupperFactory,
|
||||
resticTimeout time.Duration,
|
||||
defaultVolumesToRestic bool,
|
||||
clientPageSize int,
|
||||
@@ -234,7 +234,7 @@ func (kb *kubernetesBackupper) BackupWithResolvers(log logrus.FieldLogger,
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), podVolumeTimeout)
|
||||
defer cancelFunc()
|
||||
|
||||
var resticBackupper restic.Backupper
|
||||
var resticBackupper podvolume.Backupper
|
||||
if kb.resticBackupperFactory != nil {
|
||||
resticBackupper, err = kb.resticBackupperFactory.NewBackupper(ctx, backupRequest.Backup)
|
||||
if err != nil {
|
||||
|
||||
@@ -47,7 +47,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/discovery"
|
||||
"github.com/vmware-tanzu/velero/pkg/kuberesource"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
"github.com/vmware-tanzu/velero/pkg/restic"
|
||||
"github.com/vmware-tanzu/velero/pkg/podvolume"
|
||||
"github.com/vmware-tanzu/velero/pkg/test"
|
||||
testutil "github.com/vmware-tanzu/velero/pkg/test"
|
||||
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
@@ -2595,7 +2595,7 @@ func TestBackupWithHooks(t *testing.T) {
|
||||
|
||||
type fakeResticBackupperFactory struct{}
|
||||
|
||||
func (f *fakeResticBackupperFactory) NewBackupper(context.Context, *velerov1.Backup) (restic.Backupper, error) {
|
||||
func (f *fakeResticBackupperFactory) NewBackupper(context.Context, *velerov1.Backup) (podvolume.Backupper, error) {
|
||||
return &fakeResticBackupper{}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/features"
|
||||
"github.com/vmware-tanzu/velero/pkg/kuberesource"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
"github.com/vmware-tanzu/velero/pkg/restic"
|
||||
"github.com/vmware-tanzu/velero/pkg/podvolume"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
"github.com/vmware-tanzu/velero/pkg/volume"
|
||||
)
|
||||
@@ -53,7 +53,7 @@ type itemBackupper struct {
|
||||
tarWriter tarWriter
|
||||
dynamicFactory client.DynamicFactory
|
||||
discoveryHelper discovery.Helper
|
||||
resticBackupper restic.Backupper
|
||||
resticBackupper podvolume.Backupper
|
||||
resticSnapshotTracker *pvcSnapshotTracker
|
||||
volumeSnapshotterGetter VolumeSnapshotterGetter
|
||||
|
||||
@@ -61,6 +61,11 @@ type itemBackupper struct {
|
||||
snapshotLocationVolumeSnapshotters map[string]velero.VolumeSnapshotter
|
||||
}
|
||||
|
||||
const (
|
||||
// veleroExcludeFromBackupLabel labeled item should be exclude by velero in backup job.
|
||||
veleroExcludeFromBackupLabel = "velero.io/exclude-from-backup"
|
||||
)
|
||||
|
||||
// backupItem backs up an individual item to tarWriter. The item may be excluded based on the
|
||||
// namespaces IncludesExcludes list.
|
||||
// In addition to the error return, backupItem also returns a bool indicating whether the item
|
||||
@@ -78,8 +83,8 @@ func (ib *itemBackupper) backupItem(logger logrus.FieldLogger, obj runtime.Unstr
|
||||
log = log.WithField("resource", groupResource.String())
|
||||
log = log.WithField("namespace", namespace)
|
||||
|
||||
if metadata.GetLabels()["velero.io/exclude-from-backup"] == "true" {
|
||||
log.Info("Excluding item because it has label velero.io/exclude-from-backup=true")
|
||||
if metadata.GetLabels()[veleroExcludeFromBackupLabel] == "true" {
|
||||
log.Infof("Excluding item because it has label %s=true", veleroExcludeFromBackupLabel)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -144,7 +149,7 @@ func (ib *itemBackupper) backupItem(logger logrus.FieldLogger, obj runtime.Unstr
|
||||
// Get the list of volumes to back up using restic from the pod's annotations. Remove from this list
|
||||
// any volumes that use a PVC that we've already backed up (this would be in a read-write-many scenario,
|
||||
// where it's been backed up from another pod), since we don't need >1 backup per PVC.
|
||||
for _, volume := range restic.GetPodVolumesUsingRestic(pod, boolptr.IsSetToTrue(ib.backupRequest.Spec.DefaultVolumesToRestic)) {
|
||||
for _, volume := range podvolume.GetPodVolumesUsingRestic(pod, boolptr.IsSetToTrue(ib.backupRequest.Spec.DefaultVolumesToRestic)) {
|
||||
if found, pvcName := ib.resticSnapshotTracker.HasPVCForPodVolume(pod, volume); found {
|
||||
log.WithFields(map[string]interface{}{
|
||||
"podVolume": volume,
|
||||
|
||||
@@ -17,9 +17,11 @@ limitations under the License.
|
||||
package builder
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
apimachineryRuntime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// ContainerBuilder builds Container objects
|
||||
@@ -89,6 +91,17 @@ func (b *ContainerBuilder) Result() *corev1api.Container {
|
||||
return b.object
|
||||
}
|
||||
|
||||
// ResultRawExtension returns the Container as runtime.RawExtension.
|
||||
func (b *ContainerBuilder) ResultRawExtension() apimachineryRuntime.RawExtension {
|
||||
result, err := json.Marshal(b.object)
|
||||
if err != nil {
|
||||
return apimachineryRuntime.RawExtension{}
|
||||
}
|
||||
return apimachineryRuntime.RawExtension{
|
||||
Raw: result,
|
||||
}
|
||||
}
|
||||
|
||||
// Args sets the container's Args.
|
||||
func (b *ContainerBuilder) Args(args ...string) *ContainerBuilder {
|
||||
b.object.Args = append(b.object.Args, args...)
|
||||
|
||||
@@ -151,8 +151,8 @@ func findAssociatedBackups(client kbclient.Client, bslName, ns string) (velerov1
|
||||
return backups, err
|
||||
}
|
||||
|
||||
func findAssociatedResticRepos(client kbclient.Client, bslName, ns string) (velerov1api.ResticRepositoryList, error) {
|
||||
var repos velerov1api.ResticRepositoryList
|
||||
func findAssociatedResticRepos(client kbclient.Client, bslName, ns string) (velerov1api.BackupRepositoryList, error) {
|
||||
var repos velerov1api.BackupRepositoryList
|
||||
err := client.List(context.Background(), &repos, &kbclient.ListOptions{
|
||||
Namespace: ns,
|
||||
Raw: &metav1.ListOptions{LabelSelector: bslLabelKey + "=" + bslName},
|
||||
@@ -172,7 +172,7 @@ func deleteBackups(client kbclient.Client, backups velerov1api.BackupList) []err
|
||||
return errs
|
||||
}
|
||||
|
||||
func deleteResticRepos(client kbclient.Client, repos velerov1api.ResticRepositoryList) []error {
|
||||
func deleteResticRepos(client kbclient.Client, repos velerov1api.BackupRepositoryList) []error {
|
||||
var errs []error
|
||||
for _, repo := range repos.Items {
|
||||
if err := client.Delete(context.Background(), &repo, &kbclient.DeleteOptions{}); err != nil {
|
||||
|
||||
@@ -41,16 +41,16 @@ func NewGetCommand(f client.Factory, use string) *cobra.Command {
|
||||
veleroClient, err := f.Client()
|
||||
cmd.CheckError(err)
|
||||
|
||||
var repos *api.ResticRepositoryList
|
||||
var repos *api.BackupRepositoryList
|
||||
if len(args) > 0 {
|
||||
repos = new(api.ResticRepositoryList)
|
||||
repos = new(api.BackupRepositoryList)
|
||||
for _, name := range args {
|
||||
repo, err := veleroClient.VeleroV1().ResticRepositories(f.Namespace()).Get(context.TODO(), name, metav1.GetOptions{})
|
||||
repo, err := veleroClient.VeleroV1().BackupRepositories(f.Namespace()).Get(context.TODO(), name, metav1.GetOptions{})
|
||||
cmd.CheckError(err)
|
||||
repos.Items = append(repos.Items, *repo)
|
||||
}
|
||||
} else {
|
||||
repos, err = veleroClient.VeleroV1().ResticRepositories(f.Namespace()).List(context.TODO(), listOptions)
|
||||
repos, err = veleroClient.VeleroV1().BackupRepositories(f.Namespace()).List(context.TODO(), listOptions)
|
||||
cmd.CheckError(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ type CreateOptions struct {
|
||||
func NewCreateOptions() *CreateOptions {
|
||||
return &CreateOptions{
|
||||
Config: flag.NewMap(),
|
||||
Labels: flag.NewMap(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,11 @@ package plugin
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
apiextensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/features"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/backup"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
velerodiscovery "github.com/vmware-tanzu/velero/pkg/discovery"
|
||||
@@ -36,11 +38,10 @@ func NewCommand(f client.Factory) *cobra.Command {
|
||||
Hidden: true,
|
||||
Short: "INTERNAL COMMAND ONLY - not intended to be run directly by users",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
pluginServer.
|
||||
pluginServer = pluginServer.
|
||||
RegisterBackupItemAction("velero.io/pv", newPVBackupItemAction).
|
||||
RegisterBackupItemAction("velero.io/pod", newPodBackupItemAction).
|
||||
RegisterBackupItemAction("velero.io/service-account", newServiceAccountBackupItemAction(f)).
|
||||
RegisterBackupItemAction("velero.io/crd-remap-version", newRemapCRDVersionAction(f)).
|
||||
RegisterRestoreItemAction("velero.io/job", newJobRestoreItemAction).
|
||||
RegisterRestoreItemAction("velero.io/pod", newPodRestoreItemAction).
|
||||
RegisterRestoreItemAction("velero.io/restic", newResticRestoreItemAction(f)).
|
||||
@@ -55,13 +56,15 @@ func NewCommand(f client.Factory) *cobra.Command {
|
||||
RegisterRestoreItemAction("velero.io/crd-preserve-fields", newCRDV1PreserveUnknownFieldsItemAction).
|
||||
RegisterRestoreItemAction("velero.io/change-pvc-node-selector", newChangePVCNodeSelectorItemAction(f)).
|
||||
RegisterRestoreItemAction("velero.io/apiservice", newAPIServiceRestoreItemAction).
|
||||
RegisterRestoreItemAction("velero.io/admission-webhook-configuration", newAdmissionWebhookConfigurationAction).
|
||||
Serve()
|
||||
RegisterRestoreItemAction("velero.io/admission-webhook-configuration", newAdmissionWebhookConfigurationAction)
|
||||
if !features.IsEnabled(velerov1api.APIGroupVersionsFeatureFlag) {
|
||||
// Do not register crd-remap-version BIA if the API Group feature flag is enabled, so that the v1 CRD can be backed up
|
||||
pluginServer = pluginServer.RegisterBackupItemAction("velero.io/crd-remap-version", newRemapCRDVersionAction(f))
|
||||
}
|
||||
pluginServer.Serve()
|
||||
},
|
||||
}
|
||||
|
||||
pluginServer.BindFlags(c.Flags())
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/internal/storage"
|
||||
"github.com/vmware-tanzu/velero/internal/util/managercontroller"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
repokey "github.com/vmware-tanzu/velero/pkg/repository/keys"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -522,7 +523,7 @@ func (s *server) initRestic() error {
|
||||
}
|
||||
|
||||
// ensure the repo key secret is set up
|
||||
if err := restic.EnsureCommonRepositoryKey(s.kubeClient.CoreV1(), s.namespace); err != nil {
|
||||
if err := repokey.EnsureCommonRepositoryKey(s.kubeClient.CoreV1(), s.namespace); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -530,7 +531,7 @@ func (s *server) initRestic() error {
|
||||
s.ctx,
|
||||
s.namespace,
|
||||
s.veleroClient,
|
||||
s.sharedInformerFactory.Velero().V1().ResticRepositories(),
|
||||
s.sharedInformerFactory.Velero().V1().BackupRepositories(),
|
||||
s.veleroClient.VeleroV1(),
|
||||
s.mgr.GetClient(),
|
||||
s.kubeClient.CoreV1(),
|
||||
|
||||
@@ -177,15 +177,15 @@ func printTable(cmd *cobra.Command, obj runtime.Object) (bool, error) {
|
||||
ColumnDefinitions: scheduleColumns,
|
||||
Rows: printScheduleList(obj.(*velerov1api.ScheduleList)),
|
||||
}
|
||||
case *velerov1api.ResticRepository:
|
||||
case *velerov1api.BackupRepository:
|
||||
table = &metav1.Table{
|
||||
ColumnDefinitions: resticRepoColumns,
|
||||
Rows: printResticRepo(obj.(*velerov1api.ResticRepository)),
|
||||
Rows: printResticRepo(obj.(*velerov1api.BackupRepository)),
|
||||
}
|
||||
case *velerov1api.ResticRepositoryList:
|
||||
case *velerov1api.BackupRepositoryList:
|
||||
table = &metav1.Table{
|
||||
ColumnDefinitions: resticRepoColumns,
|
||||
Rows: printResticRepoList(obj.(*velerov1api.ResticRepositoryList)),
|
||||
Rows: printResticRepoList(obj.(*velerov1api.BackupRepositoryList)),
|
||||
}
|
||||
case *velerov1api.BackupStorageLocation:
|
||||
table = &metav1.Table{
|
||||
|
||||
@@ -33,7 +33,7 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func printResticRepoList(list *v1.ResticRepositoryList) []metav1.TableRow {
|
||||
func printResticRepoList(list *v1.BackupRepositoryList) []metav1.TableRow {
|
||||
rows := make([]metav1.TableRow, 0, len(list.Items))
|
||||
|
||||
for i := range list.Items {
|
||||
@@ -42,14 +42,14 @@ func printResticRepoList(list *v1.ResticRepositoryList) []metav1.TableRow {
|
||||
return rows
|
||||
}
|
||||
|
||||
func printResticRepo(repo *v1.ResticRepository) []metav1.TableRow {
|
||||
func printResticRepo(repo *v1.BackupRepository) []metav1.TableRow {
|
||||
row := metav1.TableRow{
|
||||
Object: runtime.RawExtension{Object: repo},
|
||||
}
|
||||
|
||||
status := repo.Status.Phase
|
||||
if status == "" {
|
||||
status = v1.ResticRepositoryPhaseNew
|
||||
status = v1.BackupRepositoryPhaseNew
|
||||
}
|
||||
|
||||
var lastMaintenance string
|
||||
|
||||
@@ -70,6 +70,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/util/logging"
|
||||
"github.com/vmware-tanzu/velero/pkg/volume"
|
||||
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
@@ -260,7 +261,6 @@ func (c *backupController) processBackup(key string) error {
|
||||
|
||||
log.Debug("Preparing backup request")
|
||||
request := c.prepareBackupRequest(original)
|
||||
|
||||
if len(request.Status.ValidationErrors) > 0 {
|
||||
request.Status.Phase = velerov1api.BackupPhaseFailedValidation
|
||||
} else {
|
||||
@@ -444,6 +444,17 @@ func (c *backupController) prepareBackupRequest(backup *velerov1api.Backup) *pkg
|
||||
request.Annotations[velerov1api.SourceClusterK8sMajorVersionAnnotation] = c.discoveryHelper.ServerVersion().Major
|
||||
request.Annotations[velerov1api.SourceClusterK8sMinorVersionAnnotation] = c.discoveryHelper.ServerVersion().Minor
|
||||
|
||||
// Add namespaces with label velero.io/exclude-from-backup=true into request.Spec.ExcludedNamespaces
|
||||
// Essentially, adding the label velero.io/exclude-from-backup=true to a namespace would be equivalent to setting spec.ExcludedNamespaces
|
||||
namespaces := corev1api.NamespaceList{}
|
||||
if err := c.kbClient.List(context.Background(), &namespaces, kbclient.MatchingLabels{"velero.io/exclude-from-backup": "true"}); err == nil {
|
||||
for _, ns := range namespaces.Items {
|
||||
request.Spec.ExcludedNamespaces = append(request.Spec.ExcludedNamespaces, ns.Name)
|
||||
}
|
||||
} else {
|
||||
request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("error getting namespace list: %v", err))
|
||||
}
|
||||
|
||||
// validate the included/excluded resources
|
||||
for _, err := range collections.ValidateIncludesExcludes(request.Spec.IncludedResources, request.Spec.ExcludedResources) {
|
||||
request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("Invalid included/excluded resource lists: %v", err))
|
||||
|
||||
@@ -198,7 +198,7 @@ func (r *backupDeletionReconciler) Reconcile(ctx context.Context, req ctrl.Reque
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// if the request object has no labels defined, initialise an empty map since
|
||||
// if the request object has no labels defined, initialize an empty map since
|
||||
// we will be updating labels
|
||||
if dbr.Labels == nil {
|
||||
dbr.Labels = map[string]string{}
|
||||
|
||||
@@ -24,12 +24,10 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/storage"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
@@ -39,7 +37,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
backupStorageLocationSyncPeriod = 1 * time.Minute
|
||||
// keep the enqueue period a smaller value to make sure the BSL can be validated as expected.
|
||||
// The BSL validation frequency is 1 minute by default, if we set the enqueue period as 1 minute,
|
||||
// this will cause the actual validation interval for each BSL to be 2 minutes
|
||||
bslValidationEnqueuePeriod = 10 * time.Second
|
||||
)
|
||||
|
||||
// BackupStorageLocationReconciler reconciles a BackupStorageLocation object
|
||||
@@ -185,7 +186,7 @@ func (r *BackupStorageLocationReconciler) SetupWithManager(mgr ctrl.Manager) err
|
||||
r.Log,
|
||||
mgr.GetClient(),
|
||||
&velerov1api.BackupStorageLocationList{},
|
||||
backupStorageLocationSyncPeriod,
|
||||
bslValidationEnqueuePeriod,
|
||||
// Add filter function to enqueue BSL per ValidationFrequency setting.
|
||||
func(object client.Object) bool {
|
||||
location := object.(*velerov1api.BackupStorageLocation)
|
||||
@@ -193,22 +194,8 @@ func (r *BackupStorageLocationReconciler) SetupWithManager(mgr ctrl.Manager) err
|
||||
},
|
||||
)
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&velerov1api.BackupStorageLocation{}).
|
||||
// Handle BSL's creation event and spec update event to let changed BSL got validation immediately.
|
||||
WithEventFilter(predicate.Funcs{
|
||||
CreateFunc: func(ce event.CreateEvent) bool {
|
||||
return true
|
||||
},
|
||||
UpdateFunc: func(ue event.UpdateEvent) bool {
|
||||
return ue.ObjectNew.GetGeneration() != ue.ObjectOld.GetGeneration()
|
||||
},
|
||||
DeleteFunc: func(de event.DeleteEvent) bool {
|
||||
return false
|
||||
},
|
||||
GenericFunc: func(ge event.GenericEvent) bool {
|
||||
return false
|
||||
},
|
||||
}).
|
||||
// As the "status.LastValidationTime" field is always updated, this triggers new reconciling process, skip the update event that include no spec change to avoid the reconcile loop
|
||||
For(&velerov1api.BackupStorageLocation{}, builder.WithPredicates(kube.SpecChangePredicate{})).
|
||||
Watches(g, nil).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,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/metrics"
|
||||
repokey "github.com/vmware-tanzu/velero/pkg/repository/keys"
|
||||
"github.com/vmware-tanzu/velero/pkg/restic"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
@@ -124,7 +125,11 @@ func (r *PodVolumeBackupReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
||||
if err != nil {
|
||||
return r.updateStatusToFailed(ctx, &pvb, err, "building Restic command", log)
|
||||
}
|
||||
defer os.Remove(resticDetails.credsFile)
|
||||
|
||||
defer func() {
|
||||
os.Remove(resticDetails.credsFile)
|
||||
os.Remove(resticDetails.caCertFile)
|
||||
}()
|
||||
|
||||
backupLocation := &velerov1api.BackupStorageLocation{}
|
||||
if err := r.Client.Get(context.Background(), client.ObjectKey{
|
||||
@@ -204,19 +209,6 @@ func (r *PodVolumeBackupReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *PodVolumeBackupReconciler) singlePathMatch(path string) (string, error) {
|
||||
matches, err := r.FileSystem.Glob(path)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(matches) != 1 {
|
||||
return "", errors.Errorf("expected one matching path: %s, got %d", path, len(matches))
|
||||
}
|
||||
|
||||
return matches[0], nil
|
||||
}
|
||||
|
||||
// getParentSnapshot finds the most recent completed PodVolumeBackup for the
|
||||
// specified PVC and returns its Restic snapshot ID. Any errors encountered are
|
||||
// logged but not returned since they do not prevent a backup from proceeding.
|
||||
@@ -237,7 +229,7 @@ func (r *PodVolumeBackupReconciler) getParentSnapshot(ctx context.Context, log l
|
||||
|
||||
// Go through all the podvolumebackups for the PVC and look for the most
|
||||
// recent completed one to use as the parent.
|
||||
var mostRecentPVB *velerov1api.PodVolumeBackup
|
||||
var mostRecentPVB velerov1api.PodVolumeBackup
|
||||
for _, pvb := range pvbList.Items {
|
||||
if pvb.Status.Phase != velerov1api.PodVolumeBackupPhaseCompleted {
|
||||
continue
|
||||
@@ -254,12 +246,12 @@ func (r *PodVolumeBackupReconciler) getParentSnapshot(ctx context.Context, log l
|
||||
continue
|
||||
}
|
||||
|
||||
if mostRecentPVB == nil || pvb.Status.StartTimestamp.After(mostRecentPVB.Status.StartTimestamp.Time) {
|
||||
mostRecentPVB = &pvb
|
||||
if mostRecentPVB.Status == (velerov1api.PodVolumeBackupStatus{}) || pvb.Status.StartTimestamp.After(mostRecentPVB.Status.StartTimestamp.Time) {
|
||||
mostRecentPVB = pvb
|
||||
}
|
||||
}
|
||||
|
||||
if mostRecentPVB == nil {
|
||||
if mostRecentPVB.Status == (velerov1api.PodVolumeBackupStatus{}) {
|
||||
log.Info("No completed PodVolumeBackup found for PVC")
|
||||
return ""
|
||||
}
|
||||
@@ -313,14 +305,14 @@ func (r *PodVolumeBackupReconciler) buildResticCommand(ctx context.Context, log
|
||||
pathGlob := fmt.Sprintf("/host_pods/%s/volumes/*/%s", string(pvb.Spec.Pod.UID), volDir)
|
||||
log.WithField("pathGlob", pathGlob).Debug("Looking for path matching glob")
|
||||
|
||||
path, err := r.singlePathMatch(pathGlob)
|
||||
path, err := kube.SinglePathMatch(pathGlob, r.FileSystem, log)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "identifying unique volume path on host")
|
||||
}
|
||||
log.WithField("path", path).Debugf("Found path matching glob")
|
||||
|
||||
// Temporary credentials.
|
||||
details.credsFile, err = r.CredsFileStore.Path(restic.RepoKeySelector())
|
||||
details.credsFile, err = r.CredsFileStore.Path(repokey.RepoKeySelector())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "creating temporary Restic credentials file")
|
||||
}
|
||||
@@ -344,8 +336,6 @@ func (r *PodVolumeBackupReconciler) buildResticCommand(ctx context.Context, log
|
||||
if err != nil {
|
||||
log.WithError(err).Error("creating temporary caCert file")
|
||||
}
|
||||
defer os.Remove(details.caCertFile)
|
||||
|
||||
}
|
||||
cmd.CACertFile = details.caCertFile
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import (
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/credentials"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
repokey "github.com/vmware-tanzu/velero/pkg/repository/keys"
|
||||
"github.com/vmware-tanzu/velero/pkg/restic"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
@@ -215,19 +216,6 @@ func getResticInitContainerIndex(pod *corev1api.Pod) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func singlePathMatch(path string) (string, error) {
|
||||
matches, err := filepath.Glob(path)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(matches) != 1 {
|
||||
return "", errors.Errorf("expected one matching path: %s, got %d", path, len(matches))
|
||||
}
|
||||
|
||||
return matches[0], nil
|
||||
}
|
||||
|
||||
func (c *PodVolumeRestoreReconciler) processRestore(ctx context.Context, req *velerov1api.PodVolumeRestore, pod *corev1api.Pod, log logrus.FieldLogger) error {
|
||||
volumeDir, err := kube.GetVolumeDirectory(ctx, log, pod, req.Spec.Volume, c.Client)
|
||||
if err != nil {
|
||||
@@ -236,12 +224,14 @@ func (c *PodVolumeRestoreReconciler) processRestore(ctx context.Context, req *ve
|
||||
|
||||
// Get the full path of the new volume's directory as mounted in the daemonset pod, which
|
||||
// will look like: /host_pods/<new-pod-uid>/volumes/<volume-plugin-name>/<volume-dir>
|
||||
volumePath, err := singlePathMatch(fmt.Sprintf("/host_pods/%s/volumes/*/%s", string(req.Spec.Pod.UID), volumeDir))
|
||||
volumePath, err := kube.SinglePathMatch(
|
||||
fmt.Sprintf("/host_pods/%s/volumes/*/%s", string(req.Spec.Pod.UID), volumeDir),
|
||||
c.fileSystem, log)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error identifying path of volume")
|
||||
}
|
||||
|
||||
credsFile, err := c.credentialsFileStore.Path(restic.RepoKeySelector())
|
||||
credsFile, err := c.credentialsFileStore.Path(repokey.RepoKeySelector())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating temp restic credentials file")
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ import (
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/clock"
|
||||
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
repoconfig "github.com/vmware-tanzu/velero/pkg/repository/config"
|
||||
"github.com/vmware-tanzu/velero/pkg/restic"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
)
|
||||
@@ -68,16 +68,16 @@ func NewResticRepoReconciler(namespace string, logger logrus.FieldLogger, client
|
||||
}
|
||||
|
||||
func (r *ResticRepoReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
s := kube.NewPeriodicalEnqueueSource(r.logger, mgr.GetClient(), &velerov1api.ResticRepositoryList{}, repoSyncPeriod)
|
||||
s := kube.NewPeriodicalEnqueueSource(r.logger, mgr.GetClient(), &velerov1api.BackupRepositoryList{}, repoSyncPeriod)
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&velerov1api.ResticRepository{}).
|
||||
For(&velerov1api.BackupRepository{}).
|
||||
Watches(s, nil).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *ResticRepoReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
log := r.logger.WithField("resticRepo", req.String())
|
||||
resticRepo := &velerov1api.ResticRepository{}
|
||||
resticRepo := &velerov1api.BackupRepository{}
|
||||
if err := r.Get(ctx, req.NamespacedName, resticRepo); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
log.Warnf("restic repository %s in namespace %s is not found", req.Name, req.Namespace)
|
||||
@@ -87,7 +87,7 @@ func (r *ResticRepoReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if resticRepo.Status.Phase == "" || resticRepo.Status.Phase == velerov1api.ResticRepositoryPhaseNew {
|
||||
if resticRepo.Status.Phase == "" || resticRepo.Status.Phase == velerov1api.BackupRepositoryPhaseNew {
|
||||
if err := r.initializeRepo(ctx, resticRepo, log); err != nil {
|
||||
log.WithError(err).Error("error initialize repository")
|
||||
return ctrl.Result{}, errors.WithStack(err)
|
||||
@@ -105,16 +105,16 @@ func (r *ResticRepoReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||
}
|
||||
|
||||
switch resticRepo.Status.Phase {
|
||||
case velerov1api.ResticRepositoryPhaseReady:
|
||||
case velerov1api.BackupRepositoryPhaseReady:
|
||||
return ctrl.Result{}, r.runMaintenanceIfDue(ctx, resticRepo, log)
|
||||
case velerov1api.ResticRepositoryPhaseNotReady:
|
||||
case velerov1api.BackupRepositoryPhaseNotReady:
|
||||
return ctrl.Result{}, r.checkNotReadyRepo(ctx, resticRepo, log)
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *ResticRepoReconciler) initializeRepo(ctx context.Context, req *velerov1api.ResticRepository, log logrus.FieldLogger) error {
|
||||
func (r *ResticRepoReconciler) initializeRepo(ctx context.Context, req *velerov1api.BackupRepository, log logrus.FieldLogger) error {
|
||||
log.Info("Initializing restic repository")
|
||||
|
||||
// confirm the repo's BackupStorageLocation is valid
|
||||
@@ -127,11 +127,11 @@ func (r *ResticRepoReconciler) initializeRepo(ctx context.Context, req *velerov1
|
||||
return r.patchResticRepository(ctx, req, repoNotReady(err.Error()))
|
||||
}
|
||||
|
||||
repoIdentifier, err := restic.GetRepoIdentifier(loc, req.Spec.VolumeNamespace)
|
||||
repoIdentifier, err := repoconfig.GetRepoIdentifier(loc, req.Spec.VolumeNamespace)
|
||||
if err != nil {
|
||||
return r.patchResticRepository(ctx, req, func(rr *velerov1api.ResticRepository) {
|
||||
return r.patchResticRepository(ctx, req, func(rr *velerov1api.BackupRepository) {
|
||||
rr.Status.Message = err.Error()
|
||||
rr.Status.Phase = velerov1api.ResticRepositoryPhaseNotReady
|
||||
rr.Status.Phase = velerov1api.BackupRepositoryPhaseNotReady
|
||||
|
||||
if rr.Spec.MaintenanceFrequency.Duration <= 0 {
|
||||
rr.Spec.MaintenanceFrequency = metav1.Duration{Duration: r.defaultMaintenanceFrequency}
|
||||
@@ -140,7 +140,7 @@ func (r *ResticRepoReconciler) initializeRepo(ctx context.Context, req *velerov1
|
||||
}
|
||||
|
||||
// defaulting - if the patch fails, return an error so the item is returned to the queue
|
||||
if err := r.patchResticRepository(ctx, req, func(rr *velerov1api.ResticRepository) {
|
||||
if err := r.patchResticRepository(ctx, req, func(rr *velerov1api.BackupRepository) {
|
||||
rr.Spec.ResticIdentifier = repoIdentifier
|
||||
|
||||
if rr.Spec.MaintenanceFrequency.Duration <= 0 {
|
||||
@@ -154,8 +154,8 @@ func (r *ResticRepoReconciler) initializeRepo(ctx context.Context, req *velerov1
|
||||
return r.patchResticRepository(ctx, req, repoNotReady(err.Error()))
|
||||
}
|
||||
|
||||
return r.patchResticRepository(ctx, req, func(rr *velerov1api.ResticRepository) {
|
||||
rr.Status.Phase = velerov1api.ResticRepositoryPhaseReady
|
||||
return r.patchResticRepository(ctx, req, func(rr *velerov1api.BackupRepository) {
|
||||
rr.Status.Phase = velerov1api.BackupRepositoryPhaseReady
|
||||
rr.Status.LastMaintenanceTime = &metav1.Time{Time: time.Now()}
|
||||
})
|
||||
}
|
||||
@@ -163,7 +163,7 @@ func (r *ResticRepoReconciler) initializeRepo(ctx context.Context, req *velerov1
|
||||
// ensureRepo checks to see if a repository exists, and attempts to initialize it if
|
||||
// it does not exist. An error is returned if the repository can't be connected to
|
||||
// or initialized.
|
||||
func ensureRepo(repo *velerov1api.ResticRepository, repoManager restic.RepositoryManager) error {
|
||||
func ensureRepo(repo *velerov1api.BackupRepository, repoManager restic.RepositoryManager) error {
|
||||
if err := repoManager.ConnectToRepo(repo); err != nil {
|
||||
// If the repository has not yet been initialized, the error message will always include
|
||||
// the following string. This is the only scenario where we should try to initialize it.
|
||||
@@ -179,7 +179,7 @@ func ensureRepo(repo *velerov1api.ResticRepository, repoManager restic.Repositor
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ResticRepoReconciler) runMaintenanceIfDue(ctx context.Context, req *velerov1api.ResticRepository, log logrus.FieldLogger) error {
|
||||
func (r *ResticRepoReconciler) runMaintenanceIfDue(ctx context.Context, req *velerov1api.BackupRepository, log logrus.FieldLogger) error {
|
||||
log.Debug("resticRepositoryController.runMaintenanceIfDue")
|
||||
|
||||
now := r.clock.Now()
|
||||
@@ -196,21 +196,21 @@ func (r *ResticRepoReconciler) runMaintenanceIfDue(ctx context.Context, req *vel
|
||||
log.Debug("Pruning repo")
|
||||
if err := r.repositoryManager.PruneRepo(req); err != nil {
|
||||
log.WithError(err).Warn("error pruning repository")
|
||||
return r.patchResticRepository(ctx, req, func(rr *velerov1api.ResticRepository) {
|
||||
return r.patchResticRepository(ctx, req, func(rr *velerov1api.BackupRepository) {
|
||||
rr.Status.Message = err.Error()
|
||||
})
|
||||
}
|
||||
|
||||
return r.patchResticRepository(ctx, req, func(rr *velerov1api.ResticRepository) {
|
||||
return r.patchResticRepository(ctx, req, func(rr *velerov1api.BackupRepository) {
|
||||
rr.Status.LastMaintenanceTime = &metav1.Time{Time: now}
|
||||
})
|
||||
}
|
||||
|
||||
func dueForMaintenance(req *velerov1api.ResticRepository, now time.Time) bool {
|
||||
func dueForMaintenance(req *velerov1api.BackupRepository, now time.Time) bool {
|
||||
return req.Status.LastMaintenanceTime == nil || req.Status.LastMaintenanceTime.Add(req.Spec.MaintenanceFrequency.Duration).Before(now)
|
||||
}
|
||||
|
||||
func (r *ResticRepoReconciler) checkNotReadyRepo(ctx context.Context, req *velerov1api.ResticRepository, log logrus.FieldLogger) error {
|
||||
func (r *ResticRepoReconciler) checkNotReadyRepo(ctx context.Context, req *velerov1api.BackupRepository, log logrus.FieldLogger) error {
|
||||
// no identifier: can't possibly be ready, so just return
|
||||
if req.Spec.ResticIdentifier == "" {
|
||||
return nil
|
||||
@@ -226,16 +226,16 @@ func (r *ResticRepoReconciler) checkNotReadyRepo(ctx context.Context, req *veler
|
||||
return r.patchResticRepository(ctx, req, repoReady())
|
||||
}
|
||||
|
||||
func repoNotReady(msg string) func(*velerov1api.ResticRepository) {
|
||||
return func(r *velerov1api.ResticRepository) {
|
||||
r.Status.Phase = velerov1api.ResticRepositoryPhaseNotReady
|
||||
func repoNotReady(msg string) func(*velerov1api.BackupRepository) {
|
||||
return func(r *velerov1api.BackupRepository) {
|
||||
r.Status.Phase = velerov1api.BackupRepositoryPhaseNotReady
|
||||
r.Status.Message = msg
|
||||
}
|
||||
}
|
||||
|
||||
func repoReady() func(*velerov1api.ResticRepository) {
|
||||
return func(r *velerov1api.ResticRepository) {
|
||||
r.Status.Phase = velerov1api.ResticRepositoryPhaseReady
|
||||
func repoReady() func(*velerov1api.BackupRepository) {
|
||||
return func(r *velerov1api.BackupRepository) {
|
||||
r.Status.Phase = velerov1api.BackupRepositoryPhaseReady
|
||||
r.Status.Message = ""
|
||||
}
|
||||
}
|
||||
@@ -243,7 +243,7 @@ func repoReady() func(*velerov1api.ResticRepository) {
|
||||
// patchResticRepository mutates req with the provided mutate function, and patches it
|
||||
// through the Kube API. After executing this function, req will be updated with both
|
||||
// the mutation and the results of the Patch() API call.
|
||||
func (r *ResticRepoReconciler) patchResticRepository(ctx context.Context, req *velerov1api.ResticRepository, mutate func(*velerov1api.ResticRepository)) error {
|
||||
func (r *ResticRepoReconciler) patchResticRepository(ctx context.Context, req *velerov1api.BackupRepository, mutate func(*velerov1api.BackupRepository)) error {
|
||||
original := req.DeepCopy()
|
||||
mutate(req)
|
||||
if err := r.Patch(ctx, req, client.MergeFrom(original)); err != nil {
|
||||
|
||||
@@ -30,7 +30,7 @@ import (
|
||||
|
||||
const defaultMaintenanceFrequency = 10 * time.Minute
|
||||
|
||||
func mockResticRepoReconciler(t *testing.T, rr *velerov1api.ResticRepository, mockOn string, arg interface{}, ret interface{}) *ResticRepoReconciler {
|
||||
func mockResticRepoReconciler(t *testing.T, rr *velerov1api.BackupRepository, mockOn string, arg interface{}, ret interface{}) *ResticRepoReconciler {
|
||||
mgr := &resticmokes.RepositoryManager{}
|
||||
if mockOn != "" {
|
||||
mgr.On(mockOn, arg).Return(ret)
|
||||
@@ -44,13 +44,13 @@ func mockResticRepoReconciler(t *testing.T, rr *velerov1api.ResticRepository, mo
|
||||
)
|
||||
}
|
||||
|
||||
func mockResticRepositoryCR() *velerov1api.ResticRepository {
|
||||
return &velerov1api.ResticRepository{
|
||||
func mockResticRepositoryCR() *velerov1api.BackupRepository {
|
||||
return &velerov1api.BackupRepository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
Name: "repo",
|
||||
},
|
||||
Spec: velerov1api.ResticRepositorySpec{
|
||||
Spec: velerov1api.BackupRepositorySpec{
|
||||
MaintenanceFrequency: metav1.Duration{defaultMaintenanceFrequency},
|
||||
},
|
||||
}
|
||||
@@ -64,10 +64,10 @@ func TestPatchResticRepository(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
err = reconciler.patchResticRepository(context.Background(), rr, repoReady())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, rr.Status.Phase, velerov1api.ResticRepositoryPhaseReady)
|
||||
assert.Equal(t, rr.Status.Phase, velerov1api.BackupRepositoryPhaseReady)
|
||||
err = reconciler.patchResticRepository(context.Background(), rr, repoNotReady("not ready"))
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, rr.Status.Phase, velerov1api.ResticRepositoryPhaseReady)
|
||||
assert.NotEqual(t, rr.Status.Phase, velerov1api.BackupRepositoryPhaseReady)
|
||||
}
|
||||
|
||||
func TestCheckNotReadyRepo(t *testing.T) {
|
||||
@@ -77,11 +77,11 @@ func TestCheckNotReadyRepo(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
err = reconciler.checkNotReadyRepo(context.TODO(), rr, reconciler.logger)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, rr.Status.Phase, velerov1api.ResticRepositoryPhase(""))
|
||||
assert.Equal(t, rr.Status.Phase, velerov1api.BackupRepositoryPhase(""))
|
||||
rr.Spec.ResticIdentifier = "s3:test.amazonaws.com/bucket/restic"
|
||||
err = reconciler.checkNotReadyRepo(context.TODO(), rr, reconciler.logger)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, rr.Status.Phase, velerov1api.ResticRepositoryPhaseReady)
|
||||
assert.Equal(t, rr.Status.Phase, velerov1api.BackupRepositoryPhaseReady)
|
||||
}
|
||||
|
||||
func TestRunMaintenanceIfDue(t *testing.T) {
|
||||
@@ -121,23 +121,23 @@ func TestInitializeRepo(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
err = reconciler.initializeRepo(context.TODO(), rr, reconciler.logger)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, rr.Status.Phase, velerov1api.ResticRepositoryPhaseReady)
|
||||
assert.Equal(t, rr.Status.Phase, velerov1api.BackupRepositoryPhaseReady)
|
||||
}
|
||||
|
||||
func TestResticRepoReconcile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
repo *velerov1api.ResticRepository
|
||||
repo *velerov1api.BackupRepository
|
||||
expectNil bool
|
||||
}{
|
||||
{
|
||||
name: "test on api server not found",
|
||||
repo: &velerov1api.ResticRepository{
|
||||
repo: &velerov1api.BackupRepository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
Name: "unknown",
|
||||
},
|
||||
Spec: velerov1api.ResticRepositorySpec{
|
||||
Spec: velerov1api.BackupRepositorySpec{
|
||||
MaintenanceFrequency: metav1.Duration{defaultMaintenanceFrequency},
|
||||
},
|
||||
},
|
||||
@@ -145,12 +145,12 @@ func TestResticRepoReconcile(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "test on initialize repo",
|
||||
repo: &velerov1api.ResticRepository{
|
||||
repo: &velerov1api.BackupRepository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
Name: "repo",
|
||||
},
|
||||
Spec: velerov1api.ResticRepositorySpec{
|
||||
Spec: velerov1api.BackupRepositorySpec{
|
||||
MaintenanceFrequency: metav1.Duration{defaultMaintenanceFrequency},
|
||||
},
|
||||
},
|
||||
@@ -158,16 +158,16 @@ func TestResticRepoReconcile(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "test on repo with new phase",
|
||||
repo: &velerov1api.ResticRepository{
|
||||
repo: &velerov1api.BackupRepository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
Name: "repo",
|
||||
},
|
||||
Spec: velerov1api.ResticRepositorySpec{
|
||||
Spec: velerov1api.BackupRepositorySpec{
|
||||
MaintenanceFrequency: metav1.Duration{defaultMaintenanceFrequency},
|
||||
},
|
||||
Status: velerov1api.ResticRepositoryStatus{
|
||||
Phase: velerov1api.ResticRepositoryPhaseNew,
|
||||
Status: velerov1api.BackupRepositoryStatus{
|
||||
Phase: velerov1api.BackupRepositoryPhaseNew,
|
||||
},
|
||||
},
|
||||
expectNil: true,
|
||||
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
hook "github.com/vmware-tanzu/velero/internal/hook"
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
velerov1client "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/typed/velero/v1"
|
||||
@@ -71,10 +72,14 @@ var nonRestorableResources = []string{
|
||||
// https://github.com/vmware-tanzu/velero/issues/622
|
||||
"restores.velero.io",
|
||||
|
||||
// TODO: Remove this in v1.11 or v1.12
|
||||
// Restic repositories are automatically managed by Velero and will be automatically
|
||||
// created as needed if they don't exist.
|
||||
// https://github.com/vmware-tanzu/velero/issues/1113
|
||||
"resticrepositories.velero.io",
|
||||
|
||||
// Backup repositories were renamed from Restic repositories
|
||||
"backuprepositories.velero.io",
|
||||
}
|
||||
|
||||
type restoreController struct {
|
||||
@@ -324,6 +329,22 @@ func (c *restoreController) validateAndComplete(restore *api.Restore, pluginMana
|
||||
return backupInfo{}
|
||||
}
|
||||
|
||||
// validate Restore Init Hook's InitContainers
|
||||
restoreHooks, err := hook.GetRestoreHooksFromSpec(&restore.Spec.Hooks)
|
||||
if err != nil {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, err.Error())
|
||||
}
|
||||
for _, resource := range restoreHooks {
|
||||
for _, h := range resource.RestoreHooks {
|
||||
for _, container := range h.Init.InitContainers {
|
||||
err = hook.ValidateContainer(container.Raw)
|
||||
if err != nil {
|
||||
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if ScheduleName is specified, fill in BackupName with the most recent successful backup from
|
||||
// the schedule
|
||||
if restore.Spec.ScheduleName != "" {
|
||||
|
||||
+53
-53
@@ -30,46 +30,46 @@ import (
|
||||
rest "k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// ResticRepositoriesGetter has a method to return a ResticRepositoryInterface.
|
||||
// BackupRepositoriesGetter has a method to return a BackupRepositoryInterface.
|
||||
// A group's client should implement this interface.
|
||||
type ResticRepositoriesGetter interface {
|
||||
ResticRepositories(namespace string) ResticRepositoryInterface
|
||||
type BackupRepositoriesGetter interface {
|
||||
BackupRepositories(namespace string) BackupRepositoryInterface
|
||||
}
|
||||
|
||||
// ResticRepositoryInterface has methods to work with ResticRepository resources.
|
||||
type ResticRepositoryInterface interface {
|
||||
Create(ctx context.Context, resticRepository *v1.ResticRepository, opts metav1.CreateOptions) (*v1.ResticRepository, error)
|
||||
Update(ctx context.Context, resticRepository *v1.ResticRepository, opts metav1.UpdateOptions) (*v1.ResticRepository, error)
|
||||
UpdateStatus(ctx context.Context, resticRepository *v1.ResticRepository, opts metav1.UpdateOptions) (*v1.ResticRepository, error)
|
||||
// BackupRepositoryInterface has methods to work with BackupRepository resources.
|
||||
type BackupRepositoryInterface interface {
|
||||
Create(ctx context.Context, backupRepository *v1.BackupRepository, opts metav1.CreateOptions) (*v1.BackupRepository, error)
|
||||
Update(ctx context.Context, backupRepository *v1.BackupRepository, opts metav1.UpdateOptions) (*v1.BackupRepository, error)
|
||||
UpdateStatus(ctx context.Context, backupRepository *v1.BackupRepository, opts metav1.UpdateOptions) (*v1.BackupRepository, error)
|
||||
Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error
|
||||
DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error
|
||||
Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.ResticRepository, error)
|
||||
List(ctx context.Context, opts metav1.ListOptions) (*v1.ResticRepositoryList, error)
|
||||
Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.BackupRepository, error)
|
||||
List(ctx context.Context, opts metav1.ListOptions) (*v1.BackupRepositoryList, error)
|
||||
Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error)
|
||||
Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.ResticRepository, err error)
|
||||
ResticRepositoryExpansion
|
||||
Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.BackupRepository, err error)
|
||||
BackupRepositoryExpansion
|
||||
}
|
||||
|
||||
// resticRepositories implements ResticRepositoryInterface
|
||||
type resticRepositories struct {
|
||||
// backupRepositories implements BackupRepositoryInterface
|
||||
type backupRepositories struct {
|
||||
client rest.Interface
|
||||
ns string
|
||||
}
|
||||
|
||||
// newResticRepositories returns a ResticRepositories
|
||||
func newResticRepositories(c *VeleroV1Client, namespace string) *resticRepositories {
|
||||
return &resticRepositories{
|
||||
// newBackupRepositories returns a BackupRepositories
|
||||
func newBackupRepositories(c *VeleroV1Client, namespace string) *backupRepositories {
|
||||
return &backupRepositories{
|
||||
client: c.RESTClient(),
|
||||
ns: namespace,
|
||||
}
|
||||
}
|
||||
|
||||
// Get takes name of the resticRepository, and returns the corresponding resticRepository object, and an error if there is any.
|
||||
func (c *resticRepositories) Get(ctx context.Context, name string, options metav1.GetOptions) (result *v1.ResticRepository, err error) {
|
||||
result = &v1.ResticRepository{}
|
||||
// Get takes name of the backupRepository, and returns the corresponding backupRepository object, and an error if there is any.
|
||||
func (c *backupRepositories) Get(ctx context.Context, name string, options metav1.GetOptions) (result *v1.BackupRepository, err error) {
|
||||
result = &v1.BackupRepository{}
|
||||
err = c.client.Get().
|
||||
Namespace(c.ns).
|
||||
Resource("resticrepositories").
|
||||
Resource("backuprepositories").
|
||||
Name(name).
|
||||
VersionedParams(&options, scheme.ParameterCodec).
|
||||
Do(ctx).
|
||||
@@ -77,16 +77,16 @@ func (c *resticRepositories) Get(ctx context.Context, name string, options metav
|
||||
return
|
||||
}
|
||||
|
||||
// List takes label and field selectors, and returns the list of ResticRepositories that match those selectors.
|
||||
func (c *resticRepositories) List(ctx context.Context, opts metav1.ListOptions) (result *v1.ResticRepositoryList, err error) {
|
||||
// List takes label and field selectors, and returns the list of BackupRepositories that match those selectors.
|
||||
func (c *backupRepositories) List(ctx context.Context, opts metav1.ListOptions) (result *v1.BackupRepositoryList, err error) {
|
||||
var timeout time.Duration
|
||||
if opts.TimeoutSeconds != nil {
|
||||
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
|
||||
}
|
||||
result = &v1.ResticRepositoryList{}
|
||||
result = &v1.BackupRepositoryList{}
|
||||
err = c.client.Get().
|
||||
Namespace(c.ns).
|
||||
Resource("resticrepositories").
|
||||
Resource("backuprepositories").
|
||||
VersionedParams(&opts, scheme.ParameterCodec).
|
||||
Timeout(timeout).
|
||||
Do(ctx).
|
||||
@@ -94,8 +94,8 @@ func (c *resticRepositories) List(ctx context.Context, opts metav1.ListOptions)
|
||||
return
|
||||
}
|
||||
|
||||
// Watch returns a watch.Interface that watches the requested resticRepositories.
|
||||
func (c *resticRepositories) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
|
||||
// Watch returns a watch.Interface that watches the requested backupRepositories.
|
||||
func (c *backupRepositories) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
|
||||
var timeout time.Duration
|
||||
if opts.TimeoutSeconds != nil {
|
||||
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
|
||||
@@ -103,34 +103,34 @@ func (c *resticRepositories) Watch(ctx context.Context, opts metav1.ListOptions)
|
||||
opts.Watch = true
|
||||
return c.client.Get().
|
||||
Namespace(c.ns).
|
||||
Resource("resticrepositories").
|
||||
Resource("backuprepositories").
|
||||
VersionedParams(&opts, scheme.ParameterCodec).
|
||||
Timeout(timeout).
|
||||
Watch(ctx)
|
||||
}
|
||||
|
||||
// Create takes the representation of a resticRepository and creates it. Returns the server's representation of the resticRepository, and an error, if there is any.
|
||||
func (c *resticRepositories) Create(ctx context.Context, resticRepository *v1.ResticRepository, opts metav1.CreateOptions) (result *v1.ResticRepository, err error) {
|
||||
result = &v1.ResticRepository{}
|
||||
// Create takes the representation of a backupRepository and creates it. Returns the server's representation of the backupRepository, and an error, if there is any.
|
||||
func (c *backupRepositories) Create(ctx context.Context, backupRepository *v1.BackupRepository, opts metav1.CreateOptions) (result *v1.BackupRepository, err error) {
|
||||
result = &v1.BackupRepository{}
|
||||
err = c.client.Post().
|
||||
Namespace(c.ns).
|
||||
Resource("resticrepositories").
|
||||
Resource("backuprepositories").
|
||||
VersionedParams(&opts, scheme.ParameterCodec).
|
||||
Body(resticRepository).
|
||||
Body(backupRepository).
|
||||
Do(ctx).
|
||||
Into(result)
|
||||
return
|
||||
}
|
||||
|
||||
// Update takes the representation of a resticRepository and updates it. Returns the server's representation of the resticRepository, and an error, if there is any.
|
||||
func (c *resticRepositories) Update(ctx context.Context, resticRepository *v1.ResticRepository, opts metav1.UpdateOptions) (result *v1.ResticRepository, err error) {
|
||||
result = &v1.ResticRepository{}
|
||||
// Update takes the representation of a backupRepository and updates it. Returns the server's representation of the backupRepository, and an error, if there is any.
|
||||
func (c *backupRepositories) Update(ctx context.Context, backupRepository *v1.BackupRepository, opts metav1.UpdateOptions) (result *v1.BackupRepository, err error) {
|
||||
result = &v1.BackupRepository{}
|
||||
err = c.client.Put().
|
||||
Namespace(c.ns).
|
||||
Resource("resticrepositories").
|
||||
Name(resticRepository.Name).
|
||||
Resource("backuprepositories").
|
||||
Name(backupRepository.Name).
|
||||
VersionedParams(&opts, scheme.ParameterCodec).
|
||||
Body(resticRepository).
|
||||
Body(backupRepository).
|
||||
Do(ctx).
|
||||
Into(result)
|
||||
return
|
||||
@@ -138,25 +138,25 @@ func (c *resticRepositories) Update(ctx context.Context, resticRepository *v1.Re
|
||||
|
||||
// UpdateStatus was generated because the type contains a Status member.
|
||||
// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
|
||||
func (c *resticRepositories) UpdateStatus(ctx context.Context, resticRepository *v1.ResticRepository, opts metav1.UpdateOptions) (result *v1.ResticRepository, err error) {
|
||||
result = &v1.ResticRepository{}
|
||||
func (c *backupRepositories) UpdateStatus(ctx context.Context, backupRepository *v1.BackupRepository, opts metav1.UpdateOptions) (result *v1.BackupRepository, err error) {
|
||||
result = &v1.BackupRepository{}
|
||||
err = c.client.Put().
|
||||
Namespace(c.ns).
|
||||
Resource("resticrepositories").
|
||||
Name(resticRepository.Name).
|
||||
Resource("backuprepositories").
|
||||
Name(backupRepository.Name).
|
||||
SubResource("status").
|
||||
VersionedParams(&opts, scheme.ParameterCodec).
|
||||
Body(resticRepository).
|
||||
Body(backupRepository).
|
||||
Do(ctx).
|
||||
Into(result)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete takes name of the resticRepository and deletes it. Returns an error if one occurs.
|
||||
func (c *resticRepositories) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
|
||||
// Delete takes name of the backupRepository and deletes it. Returns an error if one occurs.
|
||||
func (c *backupRepositories) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
|
||||
return c.client.Delete().
|
||||
Namespace(c.ns).
|
||||
Resource("resticrepositories").
|
||||
Resource("backuprepositories").
|
||||
Name(name).
|
||||
Body(&opts).
|
||||
Do(ctx).
|
||||
@@ -164,14 +164,14 @@ func (c *resticRepositories) Delete(ctx context.Context, name string, opts metav
|
||||
}
|
||||
|
||||
// DeleteCollection deletes a collection of objects.
|
||||
func (c *resticRepositories) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
|
||||
func (c *backupRepositories) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
|
||||
var timeout time.Duration
|
||||
if listOpts.TimeoutSeconds != nil {
|
||||
timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
|
||||
}
|
||||
return c.client.Delete().
|
||||
Namespace(c.ns).
|
||||
Resource("resticrepositories").
|
||||
Resource("backuprepositories").
|
||||
VersionedParams(&listOpts, scheme.ParameterCodec).
|
||||
Timeout(timeout).
|
||||
Body(&opts).
|
||||
@@ -179,12 +179,12 @@ func (c *resticRepositories) DeleteCollection(ctx context.Context, opts metav1.D
|
||||
Error()
|
||||
}
|
||||
|
||||
// Patch applies the patch and returns the patched resticRepository.
|
||||
func (c *resticRepositories) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.ResticRepository, err error) {
|
||||
result = &v1.ResticRepository{}
|
||||
// Patch applies the patch and returns the patched backupRepository.
|
||||
func (c *backupRepositories) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.BackupRepository, err error) {
|
||||
result = &v1.BackupRepository{}
|
||||
err = c.client.Patch(pt).
|
||||
Namespace(c.ns).
|
||||
Resource("resticrepositories").
|
||||
Resource("backuprepositories").
|
||||
Name(name).
|
||||
SubResource(subresources...).
|
||||
VersionedParams(&opts, scheme.ParameterCodec).
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// Code generated by client-gen. DO NOT EDIT.
|
||||
|
||||
package fake
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
labels "k8s.io/apimachinery/pkg/labels"
|
||||
schema "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
types "k8s.io/apimachinery/pkg/types"
|
||||
watch "k8s.io/apimachinery/pkg/watch"
|
||||
testing "k8s.io/client-go/testing"
|
||||
)
|
||||
|
||||
// FakeBackupRepositories implements BackupRepositoryInterface
|
||||
type FakeBackupRepositories struct {
|
||||
Fake *FakeVeleroV1
|
||||
ns string
|
||||
}
|
||||
|
||||
var backuprepositoriesResource = schema.GroupVersionResource{Group: "velero.io", Version: "v1", Resource: "backuprepositories"}
|
||||
|
||||
var backuprepositoriesKind = schema.GroupVersionKind{Group: "velero.io", Version: "v1", Kind: "BackupRepository"}
|
||||
|
||||
// Get takes name of the backupRepository, and returns the corresponding backupRepository object, and an error if there is any.
|
||||
func (c *FakeBackupRepositories) Get(ctx context.Context, name string, options v1.GetOptions) (result *velerov1.BackupRepository, err error) {
|
||||
obj, err := c.Fake.
|
||||
Invokes(testing.NewGetAction(backuprepositoriesResource, c.ns, name), &velerov1.BackupRepository{})
|
||||
|
||||
if obj == nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.(*velerov1.BackupRepository), err
|
||||
}
|
||||
|
||||
// List takes label and field selectors, and returns the list of BackupRepositories that match those selectors.
|
||||
func (c *FakeBackupRepositories) List(ctx context.Context, opts v1.ListOptions) (result *velerov1.BackupRepositoryList, err error) {
|
||||
obj, err := c.Fake.
|
||||
Invokes(testing.NewListAction(backuprepositoriesResource, backuprepositoriesKind, c.ns, opts), &velerov1.BackupRepositoryList{})
|
||||
|
||||
if obj == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
label, _, _ := testing.ExtractFromListOptions(opts)
|
||||
if label == nil {
|
||||
label = labels.Everything()
|
||||
}
|
||||
list := &velerov1.BackupRepositoryList{ListMeta: obj.(*velerov1.BackupRepositoryList).ListMeta}
|
||||
for _, item := range obj.(*velerov1.BackupRepositoryList).Items {
|
||||
if label.Matches(labels.Set(item.Labels)) {
|
||||
list.Items = append(list.Items, item)
|
||||
}
|
||||
}
|
||||
return list, err
|
||||
}
|
||||
|
||||
// Watch returns a watch.Interface that watches the requested backupRepositories.
|
||||
func (c *FakeBackupRepositories) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
|
||||
return c.Fake.
|
||||
InvokesWatch(testing.NewWatchAction(backuprepositoriesResource, c.ns, opts))
|
||||
|
||||
}
|
||||
|
||||
// Create takes the representation of a backupRepository and creates it. Returns the server's representation of the backupRepository, and an error, if there is any.
|
||||
func (c *FakeBackupRepositories) Create(ctx context.Context, backupRepository *velerov1.BackupRepository, opts v1.CreateOptions) (result *velerov1.BackupRepository, err error) {
|
||||
obj, err := c.Fake.
|
||||
Invokes(testing.NewCreateAction(backuprepositoriesResource, c.ns, backupRepository), &velerov1.BackupRepository{})
|
||||
|
||||
if obj == nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.(*velerov1.BackupRepository), err
|
||||
}
|
||||
|
||||
// Update takes the representation of a backupRepository and updates it. Returns the server's representation of the backupRepository, and an error, if there is any.
|
||||
func (c *FakeBackupRepositories) Update(ctx context.Context, backupRepository *velerov1.BackupRepository, opts v1.UpdateOptions) (result *velerov1.BackupRepository, err error) {
|
||||
obj, err := c.Fake.
|
||||
Invokes(testing.NewUpdateAction(backuprepositoriesResource, c.ns, backupRepository), &velerov1.BackupRepository{})
|
||||
|
||||
if obj == nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.(*velerov1.BackupRepository), err
|
||||
}
|
||||
|
||||
// UpdateStatus was generated because the type contains a Status member.
|
||||
// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
|
||||
func (c *FakeBackupRepositories) UpdateStatus(ctx context.Context, backupRepository *velerov1.BackupRepository, opts v1.UpdateOptions) (*velerov1.BackupRepository, error) {
|
||||
obj, err := c.Fake.
|
||||
Invokes(testing.NewUpdateSubresourceAction(backuprepositoriesResource, "status", c.ns, backupRepository), &velerov1.BackupRepository{})
|
||||
|
||||
if obj == nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.(*velerov1.BackupRepository), err
|
||||
}
|
||||
|
||||
// Delete takes name of the backupRepository and deletes it. Returns an error if one occurs.
|
||||
func (c *FakeBackupRepositories) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
|
||||
_, err := c.Fake.
|
||||
Invokes(testing.NewDeleteAction(backuprepositoriesResource, c.ns, name), &velerov1.BackupRepository{})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteCollection deletes a collection of objects.
|
||||
func (c *FakeBackupRepositories) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
|
||||
action := testing.NewDeleteCollectionAction(backuprepositoriesResource, c.ns, listOpts)
|
||||
|
||||
_, err := c.Fake.Invokes(action, &velerov1.BackupRepositoryList{})
|
||||
return err
|
||||
}
|
||||
|
||||
// Patch applies the patch and returns the patched backupRepository.
|
||||
func (c *FakeBackupRepositories) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *velerov1.BackupRepository, err error) {
|
||||
obj, err := c.Fake.
|
||||
Invokes(testing.NewPatchSubresourceAction(backuprepositoriesResource, c.ns, name, pt, data, subresources...), &velerov1.BackupRepository{})
|
||||
|
||||
if obj == nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.(*velerov1.BackupRepository), err
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
/*
|
||||
Copyright the Velero contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by client-gen. DO NOT EDIT.
|
||||
|
||||
package fake
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
labels "k8s.io/apimachinery/pkg/labels"
|
||||
schema "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
types "k8s.io/apimachinery/pkg/types"
|
||||
watch "k8s.io/apimachinery/pkg/watch"
|
||||
testing "k8s.io/client-go/testing"
|
||||
)
|
||||
|
||||
// FakeResticRepositories implements ResticRepositoryInterface
|
||||
type FakeResticRepositories struct {
|
||||
Fake *FakeVeleroV1
|
||||
ns string
|
||||
}
|
||||
|
||||
var resticrepositoriesResource = schema.GroupVersionResource{Group: "velero.io", Version: "v1", Resource: "resticrepositories"}
|
||||
|
||||
var resticrepositoriesKind = schema.GroupVersionKind{Group: "velero.io", Version: "v1", Kind: "ResticRepository"}
|
||||
|
||||
// Get takes name of the resticRepository, and returns the corresponding resticRepository object, and an error if there is any.
|
||||
func (c *FakeResticRepositories) Get(ctx context.Context, name string, options v1.GetOptions) (result *velerov1.ResticRepository, err error) {
|
||||
obj, err := c.Fake.
|
||||
Invokes(testing.NewGetAction(resticrepositoriesResource, c.ns, name), &velerov1.ResticRepository{})
|
||||
|
||||
if obj == nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.(*velerov1.ResticRepository), err
|
||||
}
|
||||
|
||||
// List takes label and field selectors, and returns the list of ResticRepositories that match those selectors.
|
||||
func (c *FakeResticRepositories) List(ctx context.Context, opts v1.ListOptions) (result *velerov1.ResticRepositoryList, err error) {
|
||||
obj, err := c.Fake.
|
||||
Invokes(testing.NewListAction(resticrepositoriesResource, resticrepositoriesKind, c.ns, opts), &velerov1.ResticRepositoryList{})
|
||||
|
||||
if obj == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
label, _, _ := testing.ExtractFromListOptions(opts)
|
||||
if label == nil {
|
||||
label = labels.Everything()
|
||||
}
|
||||
list := &velerov1.ResticRepositoryList{ListMeta: obj.(*velerov1.ResticRepositoryList).ListMeta}
|
||||
for _, item := range obj.(*velerov1.ResticRepositoryList).Items {
|
||||
if label.Matches(labels.Set(item.Labels)) {
|
||||
list.Items = append(list.Items, item)
|
||||
}
|
||||
}
|
||||
return list, err
|
||||
}
|
||||
|
||||
// Watch returns a watch.Interface that watches the requested resticRepositories.
|
||||
func (c *FakeResticRepositories) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
|
||||
return c.Fake.
|
||||
InvokesWatch(testing.NewWatchAction(resticrepositoriesResource, c.ns, opts))
|
||||
|
||||
}
|
||||
|
||||
// Create takes the representation of a resticRepository and creates it. Returns the server's representation of the resticRepository, and an error, if there is any.
|
||||
func (c *FakeResticRepositories) Create(ctx context.Context, resticRepository *velerov1.ResticRepository, opts v1.CreateOptions) (result *velerov1.ResticRepository, err error) {
|
||||
obj, err := c.Fake.
|
||||
Invokes(testing.NewCreateAction(resticrepositoriesResource, c.ns, resticRepository), &velerov1.ResticRepository{})
|
||||
|
||||
if obj == nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.(*velerov1.ResticRepository), err
|
||||
}
|
||||
|
||||
// Update takes the representation of a resticRepository and updates it. Returns the server's representation of the resticRepository, and an error, if there is any.
|
||||
func (c *FakeResticRepositories) Update(ctx context.Context, resticRepository *velerov1.ResticRepository, opts v1.UpdateOptions) (result *velerov1.ResticRepository, err error) {
|
||||
obj, err := c.Fake.
|
||||
Invokes(testing.NewUpdateAction(resticrepositoriesResource, c.ns, resticRepository), &velerov1.ResticRepository{})
|
||||
|
||||
if obj == nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.(*velerov1.ResticRepository), err
|
||||
}
|
||||
|
||||
// UpdateStatus was generated because the type contains a Status member.
|
||||
// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
|
||||
func (c *FakeResticRepositories) UpdateStatus(ctx context.Context, resticRepository *velerov1.ResticRepository, opts v1.UpdateOptions) (*velerov1.ResticRepository, error) {
|
||||
obj, err := c.Fake.
|
||||
Invokes(testing.NewUpdateSubresourceAction(resticrepositoriesResource, "status", c.ns, resticRepository), &velerov1.ResticRepository{})
|
||||
|
||||
if obj == nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.(*velerov1.ResticRepository), err
|
||||
}
|
||||
|
||||
// Delete takes name of the resticRepository and deletes it. Returns an error if one occurs.
|
||||
func (c *FakeResticRepositories) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
|
||||
_, err := c.Fake.
|
||||
Invokes(testing.NewDeleteAction(resticrepositoriesResource, c.ns, name), &velerov1.ResticRepository{})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteCollection deletes a collection of objects.
|
||||
func (c *FakeResticRepositories) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
|
||||
action := testing.NewDeleteCollectionAction(resticrepositoriesResource, c.ns, listOpts)
|
||||
|
||||
_, err := c.Fake.Invokes(action, &velerov1.ResticRepositoryList{})
|
||||
return err
|
||||
}
|
||||
|
||||
// Patch applies the patch and returns the patched resticRepository.
|
||||
func (c *FakeResticRepositories) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *velerov1.ResticRepository, err error) {
|
||||
obj, err := c.Fake.
|
||||
Invokes(testing.NewPatchSubresourceAction(resticrepositoriesResource, c.ns, name, pt, data, subresources...), &velerov1.ResticRepository{})
|
||||
|
||||
if obj == nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.(*velerov1.ResticRepository), err
|
||||
}
|
||||
@@ -32,6 +32,10 @@ func (c *FakeVeleroV1) Backups(namespace string) v1.BackupInterface {
|
||||
return &FakeBackups{c, namespace}
|
||||
}
|
||||
|
||||
func (c *FakeVeleroV1) BackupRepositories(namespace string) v1.BackupRepositoryInterface {
|
||||
return &FakeBackupRepositories{c, namespace}
|
||||
}
|
||||
|
||||
func (c *FakeVeleroV1) BackupStorageLocations(namespace string) v1.BackupStorageLocationInterface {
|
||||
return &FakeBackupStorageLocations{c, namespace}
|
||||
}
|
||||
@@ -52,10 +56,6 @@ func (c *FakeVeleroV1) PodVolumeRestores(namespace string) v1.PodVolumeRestoreIn
|
||||
return &FakePodVolumeRestores{c, namespace}
|
||||
}
|
||||
|
||||
func (c *FakeVeleroV1) ResticRepositories(namespace string) v1.ResticRepositoryInterface {
|
||||
return &FakeResticRepositories{c, namespace}
|
||||
}
|
||||
|
||||
func (c *FakeVeleroV1) Restores(namespace string) v1.RestoreInterface {
|
||||
return &FakeRestores{c, namespace}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ package v1
|
||||
|
||||
type BackupExpansion interface{}
|
||||
|
||||
type BackupRepositoryExpansion interface{}
|
||||
|
||||
type BackupStorageLocationExpansion interface{}
|
||||
|
||||
type DeleteBackupRequestExpansion interface{}
|
||||
@@ -30,8 +32,6 @@ type PodVolumeBackupExpansion interface{}
|
||||
|
||||
type PodVolumeRestoreExpansion interface{}
|
||||
|
||||
type ResticRepositoryExpansion interface{}
|
||||
|
||||
type RestoreExpansion interface{}
|
||||
|
||||
type ScheduleExpansion interface{}
|
||||
|
||||
@@ -27,12 +27,12 @@ import (
|
||||
type VeleroV1Interface interface {
|
||||
RESTClient() rest.Interface
|
||||
BackupsGetter
|
||||
BackupRepositoriesGetter
|
||||
BackupStorageLocationsGetter
|
||||
DeleteBackupRequestsGetter
|
||||
DownloadRequestsGetter
|
||||
PodVolumeBackupsGetter
|
||||
PodVolumeRestoresGetter
|
||||
ResticRepositoriesGetter
|
||||
RestoresGetter
|
||||
SchedulesGetter
|
||||
ServerStatusRequestsGetter
|
||||
@@ -48,6 +48,10 @@ func (c *VeleroV1Client) Backups(namespace string) BackupInterface {
|
||||
return newBackups(c, namespace)
|
||||
}
|
||||
|
||||
func (c *VeleroV1Client) BackupRepositories(namespace string) BackupRepositoryInterface {
|
||||
return newBackupRepositories(c, namespace)
|
||||
}
|
||||
|
||||
func (c *VeleroV1Client) BackupStorageLocations(namespace string) BackupStorageLocationInterface {
|
||||
return newBackupStorageLocations(c, namespace)
|
||||
}
|
||||
@@ -68,10 +72,6 @@ func (c *VeleroV1Client) PodVolumeRestores(namespace string) PodVolumeRestoreInt
|
||||
return newPodVolumeRestores(c, namespace)
|
||||
}
|
||||
|
||||
func (c *VeleroV1Client) ResticRepositories(namespace string) ResticRepositoryInterface {
|
||||
return newResticRepositories(c, namespace)
|
||||
}
|
||||
|
||||
func (c *VeleroV1Client) Restores(namespace string) RestoreInterface {
|
||||
return newRestores(c, namespace)
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
|
||||
// Group=velero.io, Version=v1
|
||||
case v1.SchemeGroupVersion.WithResource("backups"):
|
||||
return &genericInformer{resource: resource.GroupResource(), informer: f.Velero().V1().Backups().Informer()}, nil
|
||||
case v1.SchemeGroupVersion.WithResource("backuprepositories"):
|
||||
return &genericInformer{resource: resource.GroupResource(), informer: f.Velero().V1().BackupRepositories().Informer()}, nil
|
||||
case v1.SchemeGroupVersion.WithResource("backupstoragelocations"):
|
||||
return &genericInformer{resource: resource.GroupResource(), informer: f.Velero().V1().BackupStorageLocations().Informer()}, nil
|
||||
case v1.SchemeGroupVersion.WithResource("deletebackuprequests"):
|
||||
@@ -65,8 +67,6 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
|
||||
return &genericInformer{resource: resource.GroupResource(), informer: f.Velero().V1().PodVolumeBackups().Informer()}, nil
|
||||
case v1.SchemeGroupVersion.WithResource("podvolumerestores"):
|
||||
return &genericInformer{resource: resource.GroupResource(), informer: f.Velero().V1().PodVolumeRestores().Informer()}, nil
|
||||
case v1.SchemeGroupVersion.WithResource("resticrepositories"):
|
||||
return &genericInformer{resource: resource.GroupResource(), informer: f.Velero().V1().ResticRepositories().Informer()}, nil
|
||||
case v1.SchemeGroupVersion.WithResource("restores"):
|
||||
return &genericInformer{resource: resource.GroupResource(), informer: f.Velero().V1().Restores().Informer()}, nil
|
||||
case v1.SchemeGroupVersion.WithResource("schedules"):
|
||||
|
||||
+19
-19
@@ -32,59 +32,59 @@ import (
|
||||
cache "k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
// ResticRepositoryInformer provides access to a shared informer and lister for
|
||||
// ResticRepositories.
|
||||
type ResticRepositoryInformer interface {
|
||||
// BackupRepositoryInformer provides access to a shared informer and lister for
|
||||
// BackupRepositories.
|
||||
type BackupRepositoryInformer interface {
|
||||
Informer() cache.SharedIndexInformer
|
||||
Lister() v1.ResticRepositoryLister
|
||||
Lister() v1.BackupRepositoryLister
|
||||
}
|
||||
|
||||
type resticRepositoryInformer struct {
|
||||
type backupRepositoryInformer struct {
|
||||
factory internalinterfaces.SharedInformerFactory
|
||||
tweakListOptions internalinterfaces.TweakListOptionsFunc
|
||||
namespace string
|
||||
}
|
||||
|
||||
// NewResticRepositoryInformer constructs a new informer for ResticRepository type.
|
||||
// NewBackupRepositoryInformer constructs a new informer for BackupRepository type.
|
||||
// Always prefer using an informer factory to get a shared informer instead of getting an independent
|
||||
// one. This reduces memory footprint and number of connections to the server.
|
||||
func NewResticRepositoryInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
|
||||
return NewFilteredResticRepositoryInformer(client, namespace, resyncPeriod, indexers, nil)
|
||||
func NewBackupRepositoryInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
|
||||
return NewFilteredBackupRepositoryInformer(client, namespace, resyncPeriod, indexers, nil)
|
||||
}
|
||||
|
||||
// NewFilteredResticRepositoryInformer constructs a new informer for ResticRepository type.
|
||||
// NewFilteredBackupRepositoryInformer constructs a new informer for BackupRepository type.
|
||||
// Always prefer using an informer factory to get a shared informer instead of getting an independent
|
||||
// one. This reduces memory footprint and number of connections to the server.
|
||||
func NewFilteredResticRepositoryInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
|
||||
func NewFilteredBackupRepositoryInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
|
||||
return cache.NewSharedIndexInformer(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
|
||||
if tweakListOptions != nil {
|
||||
tweakListOptions(&options)
|
||||
}
|
||||
return client.VeleroV1().ResticRepositories(namespace).List(context.TODO(), options)
|
||||
return client.VeleroV1().BackupRepositories(namespace).List(context.TODO(), options)
|
||||
},
|
||||
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
|
||||
if tweakListOptions != nil {
|
||||
tweakListOptions(&options)
|
||||
}
|
||||
return client.VeleroV1().ResticRepositories(namespace).Watch(context.TODO(), options)
|
||||
return client.VeleroV1().BackupRepositories(namespace).Watch(context.TODO(), options)
|
||||
},
|
||||
},
|
||||
&velerov1.ResticRepository{},
|
||||
&velerov1.BackupRepository{},
|
||||
resyncPeriod,
|
||||
indexers,
|
||||
)
|
||||
}
|
||||
|
||||
func (f *resticRepositoryInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
|
||||
return NewFilteredResticRepositoryInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
|
||||
func (f *backupRepositoryInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
|
||||
return NewFilteredBackupRepositoryInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
|
||||
}
|
||||
|
||||
func (f *resticRepositoryInformer) Informer() cache.SharedIndexInformer {
|
||||
return f.factory.InformerFor(&velerov1.ResticRepository{}, f.defaultInformer)
|
||||
func (f *backupRepositoryInformer) Informer() cache.SharedIndexInformer {
|
||||
return f.factory.InformerFor(&velerov1.BackupRepository{}, f.defaultInformer)
|
||||
}
|
||||
|
||||
func (f *resticRepositoryInformer) Lister() v1.ResticRepositoryLister {
|
||||
return v1.NewResticRepositoryLister(f.Informer().GetIndexer())
|
||||
func (f *backupRepositoryInformer) Lister() v1.BackupRepositoryLister {
|
||||
return v1.NewBackupRepositoryLister(f.Informer().GetIndexer())
|
||||
}
|
||||
@@ -26,6 +26,8 @@ import (
|
||||
type Interface interface {
|
||||
// Backups returns a BackupInformer.
|
||||
Backups() BackupInformer
|
||||
// BackupRepositories returns a BackupRepositoryInformer.
|
||||
BackupRepositories() BackupRepositoryInformer
|
||||
// BackupStorageLocations returns a BackupStorageLocationInformer.
|
||||
BackupStorageLocations() BackupStorageLocationInformer
|
||||
// DeleteBackupRequests returns a DeleteBackupRequestInformer.
|
||||
@@ -36,8 +38,6 @@ type Interface interface {
|
||||
PodVolumeBackups() PodVolumeBackupInformer
|
||||
// PodVolumeRestores returns a PodVolumeRestoreInformer.
|
||||
PodVolumeRestores() PodVolumeRestoreInformer
|
||||
// ResticRepositories returns a ResticRepositoryInformer.
|
||||
ResticRepositories() ResticRepositoryInformer
|
||||
// Restores returns a RestoreInformer.
|
||||
Restores() RestoreInformer
|
||||
// Schedules returns a ScheduleInformer.
|
||||
@@ -64,6 +64,11 @@ func (v *version) Backups() BackupInformer {
|
||||
return &backupInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
|
||||
}
|
||||
|
||||
// BackupRepositories returns a BackupRepositoryInformer.
|
||||
func (v *version) BackupRepositories() BackupRepositoryInformer {
|
||||
return &backupRepositoryInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
|
||||
}
|
||||
|
||||
// BackupStorageLocations returns a BackupStorageLocationInformer.
|
||||
func (v *version) BackupStorageLocations() BackupStorageLocationInformer {
|
||||
return &backupStorageLocationInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
|
||||
@@ -89,11 +94,6 @@ func (v *version) PodVolumeRestores() PodVolumeRestoreInformer {
|
||||
return &podVolumeRestoreInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
|
||||
}
|
||||
|
||||
// ResticRepositories returns a ResticRepositoryInformer.
|
||||
func (v *version) ResticRepositories() ResticRepositoryInformer {
|
||||
return &resticRepositoryInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
|
||||
}
|
||||
|
||||
// Restores returns a RestoreInformer.
|
||||
func (v *version) Restores() RestoreInformer {
|
||||
return &restoreInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// Code generated by lister-gen. DO NOT EDIT.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
// BackupRepositoryLister helps list BackupRepositories.
|
||||
// All objects returned here must be treated as read-only.
|
||||
type BackupRepositoryLister interface {
|
||||
// List lists all BackupRepositories in the indexer.
|
||||
// Objects returned here must be treated as read-only.
|
||||
List(selector labels.Selector) (ret []*v1.BackupRepository, err error)
|
||||
// BackupRepositories returns an object that can list and get BackupRepositories.
|
||||
BackupRepositories(namespace string) BackupRepositoryNamespaceLister
|
||||
BackupRepositoryListerExpansion
|
||||
}
|
||||
|
||||
// backupRepositoryLister implements the BackupRepositoryLister interface.
|
||||
type backupRepositoryLister struct {
|
||||
indexer cache.Indexer
|
||||
}
|
||||
|
||||
// NewBackupRepositoryLister returns a new BackupRepositoryLister.
|
||||
func NewBackupRepositoryLister(indexer cache.Indexer) BackupRepositoryLister {
|
||||
return &backupRepositoryLister{indexer: indexer}
|
||||
}
|
||||
|
||||
// List lists all BackupRepositories in the indexer.
|
||||
func (s *backupRepositoryLister) List(selector labels.Selector) (ret []*v1.BackupRepository, err error) {
|
||||
err = cache.ListAll(s.indexer, selector, func(m interface{}) {
|
||||
ret = append(ret, m.(*v1.BackupRepository))
|
||||
})
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// BackupRepositories returns an object that can list and get BackupRepositories.
|
||||
func (s *backupRepositoryLister) BackupRepositories(namespace string) BackupRepositoryNamespaceLister {
|
||||
return backupRepositoryNamespaceLister{indexer: s.indexer, namespace: namespace}
|
||||
}
|
||||
|
||||
// BackupRepositoryNamespaceLister helps list and get BackupRepositories.
|
||||
// All objects returned here must be treated as read-only.
|
||||
type BackupRepositoryNamespaceLister interface {
|
||||
// List lists all BackupRepositories in the indexer for a given namespace.
|
||||
// Objects returned here must be treated as read-only.
|
||||
List(selector labels.Selector) (ret []*v1.BackupRepository, err error)
|
||||
// Get retrieves the BackupRepository from the indexer for a given namespace and name.
|
||||
// Objects returned here must be treated as read-only.
|
||||
Get(name string) (*v1.BackupRepository, error)
|
||||
BackupRepositoryNamespaceListerExpansion
|
||||
}
|
||||
|
||||
// backupRepositoryNamespaceLister implements the BackupRepositoryNamespaceLister
|
||||
// interface.
|
||||
type backupRepositoryNamespaceLister struct {
|
||||
indexer cache.Indexer
|
||||
namespace string
|
||||
}
|
||||
|
||||
// List lists all BackupRepositories in the indexer for a given namespace.
|
||||
func (s backupRepositoryNamespaceLister) List(selector labels.Selector) (ret []*v1.BackupRepository, err error) {
|
||||
err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
|
||||
ret = append(ret, m.(*v1.BackupRepository))
|
||||
})
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// Get retrieves the BackupRepository from the indexer for a given namespace and name.
|
||||
func (s backupRepositoryNamespaceLister) Get(name string) (*v1.BackupRepository, error) {
|
||||
obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, errors.NewNotFound(v1.Resource("backuprepository"), name)
|
||||
}
|
||||
return obj.(*v1.BackupRepository), nil
|
||||
}
|
||||
@@ -26,6 +26,14 @@ type BackupListerExpansion interface{}
|
||||
// BackupNamespaceLister.
|
||||
type BackupNamespaceListerExpansion interface{}
|
||||
|
||||
// BackupRepositoryListerExpansion allows custom methods to be added to
|
||||
// BackupRepositoryLister.
|
||||
type BackupRepositoryListerExpansion interface{}
|
||||
|
||||
// BackupRepositoryNamespaceListerExpansion allows custom methods to be added to
|
||||
// BackupRepositoryNamespaceLister.
|
||||
type BackupRepositoryNamespaceListerExpansion interface{}
|
||||
|
||||
// BackupStorageLocationListerExpansion allows custom methods to be added to
|
||||
// BackupStorageLocationLister.
|
||||
type BackupStorageLocationListerExpansion interface{}
|
||||
@@ -66,14 +74,6 @@ type PodVolumeRestoreListerExpansion interface{}
|
||||
// PodVolumeRestoreNamespaceLister.
|
||||
type PodVolumeRestoreNamespaceListerExpansion interface{}
|
||||
|
||||
// ResticRepositoryListerExpansion allows custom methods to be added to
|
||||
// ResticRepositoryLister.
|
||||
type ResticRepositoryListerExpansion interface{}
|
||||
|
||||
// ResticRepositoryNamespaceListerExpansion allows custom methods to be added to
|
||||
// ResticRepositoryNamespaceLister.
|
||||
type ResticRepositoryNamespaceListerExpansion interface{}
|
||||
|
||||
// RestoreListerExpansion allows custom methods to be added to
|
||||
// RestoreLister.
|
||||
type RestoreListerExpansion interface{}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
Copyright the Velero contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by lister-gen. DO NOT EDIT.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
// ResticRepositoryLister helps list ResticRepositories.
|
||||
// All objects returned here must be treated as read-only.
|
||||
type ResticRepositoryLister interface {
|
||||
// List lists all ResticRepositories in the indexer.
|
||||
// Objects returned here must be treated as read-only.
|
||||
List(selector labels.Selector) (ret []*v1.ResticRepository, err error)
|
||||
// ResticRepositories returns an object that can list and get ResticRepositories.
|
||||
ResticRepositories(namespace string) ResticRepositoryNamespaceLister
|
||||
ResticRepositoryListerExpansion
|
||||
}
|
||||
|
||||
// resticRepositoryLister implements the ResticRepositoryLister interface.
|
||||
type resticRepositoryLister struct {
|
||||
indexer cache.Indexer
|
||||
}
|
||||
|
||||
// NewResticRepositoryLister returns a new ResticRepositoryLister.
|
||||
func NewResticRepositoryLister(indexer cache.Indexer) ResticRepositoryLister {
|
||||
return &resticRepositoryLister{indexer: indexer}
|
||||
}
|
||||
|
||||
// List lists all ResticRepositories in the indexer.
|
||||
func (s *resticRepositoryLister) List(selector labels.Selector) (ret []*v1.ResticRepository, err error) {
|
||||
err = cache.ListAll(s.indexer, selector, func(m interface{}) {
|
||||
ret = append(ret, m.(*v1.ResticRepository))
|
||||
})
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// ResticRepositories returns an object that can list and get ResticRepositories.
|
||||
func (s *resticRepositoryLister) ResticRepositories(namespace string) ResticRepositoryNamespaceLister {
|
||||
return resticRepositoryNamespaceLister{indexer: s.indexer, namespace: namespace}
|
||||
}
|
||||
|
||||
// ResticRepositoryNamespaceLister helps list and get ResticRepositories.
|
||||
// All objects returned here must be treated as read-only.
|
||||
type ResticRepositoryNamespaceLister interface {
|
||||
// List lists all ResticRepositories in the indexer for a given namespace.
|
||||
// Objects returned here must be treated as read-only.
|
||||
List(selector labels.Selector) (ret []*v1.ResticRepository, err error)
|
||||
// Get retrieves the ResticRepository from the indexer for a given namespace and name.
|
||||
// Objects returned here must be treated as read-only.
|
||||
Get(name string) (*v1.ResticRepository, error)
|
||||
ResticRepositoryNamespaceListerExpansion
|
||||
}
|
||||
|
||||
// resticRepositoryNamespaceLister implements the ResticRepositoryNamespaceLister
|
||||
// interface.
|
||||
type resticRepositoryNamespaceLister struct {
|
||||
indexer cache.Indexer
|
||||
namespace string
|
||||
}
|
||||
|
||||
// List lists all ResticRepositories in the indexer for a given namespace.
|
||||
func (s resticRepositoryNamespaceLister) List(selector labels.Selector) (ret []*v1.ResticRepository, err error) {
|
||||
err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
|
||||
ret = append(ret, m.(*v1.ResticRepository))
|
||||
})
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// Get retrieves the ResticRepository from the indexer for a given namespace and name.
|
||||
func (s resticRepositoryNamespaceLister) Get(name string) (*v1.ResticRepository, error) {
|
||||
obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, errors.NewNotFound(v1.Resource("resticrepository"), name)
|
||||
}
|
||||
return obj.(*v1.ResticRepository), nil
|
||||
}
|
||||
@@ -131,19 +131,25 @@ func (b *objectBackupStoreGetter) Get(location *velerov1api.BackupStorageLocatio
|
||||
return nil, errors.Errorf("backup storage location's bucket name %q must not contain a '/' (if using a prefix, put it in the 'Prefix' field instead)", location.Spec.ObjectStorage.Bucket)
|
||||
}
|
||||
|
||||
// Pass a new map into the object store rather than modifying the passed-in
|
||||
// location. This prevents Velero controllers from accidentally modifying
|
||||
// the in-cluster BSL with data which doesn't belong in Spec.Config
|
||||
objectStoreConfig := make(map[string]string)
|
||||
if location.Spec.Config != nil {
|
||||
for key, val := range location.Spec.Config {
|
||||
objectStoreConfig[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
// add the bucket name and prefix to the config map so that object stores
|
||||
// can use them when initializing. The AWS object store uses the bucket
|
||||
// name to determine the bucket's region when setting up its client.
|
||||
if location.Spec.Config == nil {
|
||||
location.Spec.Config = make(map[string]string)
|
||||
}
|
||||
|
||||
location.Spec.Config["bucket"] = bucket
|
||||
location.Spec.Config["prefix"] = prefix
|
||||
objectStoreConfig["bucket"] = bucket
|
||||
objectStoreConfig["prefix"] = prefix
|
||||
|
||||
// Only include a CACert if it's specified in order to maintain compatibility with plugins that don't expect it.
|
||||
if location.Spec.ObjectStorage.CACert != nil {
|
||||
location.Spec.Config["caCert"] = string(location.Spec.ObjectStorage.CACert)
|
||||
objectStoreConfig["caCert"] = string(location.Spec.ObjectStorage.CACert)
|
||||
}
|
||||
|
||||
// If the BSL specifies a credential, fetch its path on disk and pass to
|
||||
@@ -154,7 +160,7 @@ func (b *objectBackupStoreGetter) Get(location *velerov1api.BackupStorageLocatio
|
||||
return nil, errors.Wrap(err, "unable to get credentials")
|
||||
}
|
||||
|
||||
location.Spec.Config["credentialsFile"] = credsFile
|
||||
objectStoreConfig["credentialsFile"] = credsFile
|
||||
}
|
||||
|
||||
objectStore, err := objectStoreGetter.GetObjectStore(location.Spec.Provider)
|
||||
@@ -162,7 +168,7 @@ func (b *objectBackupStoreGetter) Get(location *velerov1api.BackupStorageLocatio
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := objectStore.Init(location.Spec.Config); err != nil {
|
||||
if err := objectStore.Init(objectStoreConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ limitations under the License.
|
||||
package framework
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
@@ -38,7 +40,8 @@ func handlePanic(p interface{}) error {
|
||||
if _, ok := panicErr.(stackTracer); ok {
|
||||
err = panicErr
|
||||
} else {
|
||||
err = errors.Wrap(panicErr, "plugin panicked")
|
||||
errWithStacktrace := errors.Errorf("%v, stack trace: %s", panicErr, debug.Stack())
|
||||
err = errors.Wrap(errWithStacktrace, "plugin panicked")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
veleroflag "github.com/vmware-tanzu/velero/pkg/cmd/util/flag"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/logging"
|
||||
)
|
||||
|
||||
@@ -78,6 +77,7 @@ type Server interface {
|
||||
|
||||
// RegisterItemSnapshotters registers multiple Item Snapshotters
|
||||
RegisterItemSnapshotters(map[string]HandlerInitializer) Server
|
||||
|
||||
// Server runs the plugin server.
|
||||
Serve()
|
||||
}
|
||||
@@ -87,7 +87,6 @@ type server struct {
|
||||
log *logrus.Logger
|
||||
logLevelFlag *logging.LevelFlag
|
||||
flagSet *pflag.FlagSet
|
||||
featureSet *veleroflag.StringArray
|
||||
backupItemAction *BackupItemActionPlugin
|
||||
volumeSnapshotter *VolumeSnapshotterPlugin
|
||||
objectStore *ObjectStorePlugin
|
||||
@@ -99,12 +98,10 @@ type server struct {
|
||||
// NewServer returns a new Server
|
||||
func NewServer() Server {
|
||||
log := newLogger()
|
||||
features := veleroflag.NewStringArray()
|
||||
|
||||
return &server{
|
||||
log: log,
|
||||
logLevelFlag: logging.LogLevelFlag(log.Level),
|
||||
featureSet: &features,
|
||||
backupItemAction: NewBackupItemActionPlugin(serverLogger(log)),
|
||||
volumeSnapshotter: NewVolumeSnapshotterPlugin(serverLogger(log)),
|
||||
objectStore: NewObjectStorePlugin(serverLogger(log)),
|
||||
@@ -116,7 +113,6 @@ func NewServer() Server {
|
||||
|
||||
func (s *server) BindFlags(flags *pflag.FlagSet) Server {
|
||||
flags.Var(s.logLevelFlag, "log-level", fmt.Sprintf("The level at which to log. Valid values are %s.", strings.Join(s.logLevelFlag.AllowedValues(), ", ")))
|
||||
flags.Var(s.featureSet, "features", "List of feature flags for this plugin")
|
||||
s.flagSet = flags
|
||||
s.flagSet.ParseErrorsWhitelist.UnknownFlags = true
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
package podvolume
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -30,7 +30,9 @@ import (
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
|
||||
"github.com/vmware-tanzu/velero/pkg/label"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
)
|
||||
|
||||
@@ -41,11 +43,12 @@ type Backupper interface {
|
||||
}
|
||||
|
||||
type backupper struct {
|
||||
ctx context.Context
|
||||
repoManager *repositoryManager
|
||||
repoEnsurer *repositoryEnsurer
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter
|
||||
pvClient corev1client.PersistentVolumesGetter
|
||||
ctx context.Context
|
||||
repoLocker *repository.RepoLocker
|
||||
repoEnsurer *repository.RepositoryEnsurer
|
||||
veleroClient clientset.Interface
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter
|
||||
pvClient corev1client.PersistentVolumesGetter
|
||||
|
||||
results map[string]chan *velerov1api.PodVolumeBackup
|
||||
resultsLock sync.Mutex
|
||||
@@ -53,19 +56,21 @@ type backupper struct {
|
||||
|
||||
func newBackupper(
|
||||
ctx context.Context,
|
||||
repoManager *repositoryManager,
|
||||
repoEnsurer *repositoryEnsurer,
|
||||
repoLocker *repository.RepoLocker,
|
||||
repoEnsurer *repository.RepositoryEnsurer,
|
||||
podVolumeBackupInformer cache.SharedIndexInformer,
|
||||
veleroClient clientset.Interface,
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter,
|
||||
pvClient corev1client.PersistentVolumesGetter,
|
||||
log logrus.FieldLogger,
|
||||
) *backupper {
|
||||
b := &backupper{
|
||||
ctx: ctx,
|
||||
repoManager: repoManager,
|
||||
repoEnsurer: repoEnsurer,
|
||||
pvcClient: pvcClient,
|
||||
pvClient: pvClient,
|
||||
ctx: ctx,
|
||||
repoLocker: repoLocker,
|
||||
repoEnsurer: repoEnsurer,
|
||||
veleroClient: veleroClient,
|
||||
pvcClient: pvcClient,
|
||||
pvClient: pvClient,
|
||||
|
||||
results: make(map[string]chan *velerov1api.PodVolumeBackup),
|
||||
}
|
||||
@@ -109,8 +114,8 @@ func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.
|
||||
|
||||
// get a single non-exclusive lock since we'll wait for all individual
|
||||
// backups to be complete before releasing it.
|
||||
b.repoManager.repoLocker.Lock(repo.Name)
|
||||
defer b.repoManager.repoLocker.Unlock(repo.Name)
|
||||
b.repoLocker.Lock(repo.Name)
|
||||
defer b.repoLocker.Unlock(repo.Name)
|
||||
|
||||
resultsChan := make(chan *velerov1api.PodVolumeBackup)
|
||||
|
||||
@@ -177,8 +182,9 @@ func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.
|
||||
continue
|
||||
}
|
||||
|
||||
volumeBackup := newPodVolumeBackup(backup, pod, volume, repo.Spec.ResticIdentifier, pvc)
|
||||
if volumeBackup, err = b.repoManager.veleroClient.VeleroV1().PodVolumeBackups(volumeBackup.Namespace).Create(context.TODO(), volumeBackup, metav1.CreateOptions{}); err != nil {
|
||||
// TODO: Remove the hard-coded uploader type before v1.10 FC
|
||||
volumeBackup := newPodVolumeBackup(backup, pod, volume, repo.Spec.ResticIdentifier, "restic", pvc)
|
||||
if volumeBackup, err = b.veleroClient.VeleroV1().PodVolumeBackups(volumeBackup.Namespace).Create(context.TODO(), volumeBackup, metav1.CreateOptions{}); err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
@@ -236,7 +242,7 @@ func isHostPathVolume(volume *corev1api.Volume, pvc *corev1api.PersistentVolumeC
|
||||
return pv.Spec.HostPath != nil, nil
|
||||
}
|
||||
|
||||
func newPodVolumeBackup(backup *velerov1api.Backup, pod *corev1api.Pod, volume corev1api.Volume, repoIdentifier string, pvc *corev1api.PersistentVolumeClaim) *velerov1api.PodVolumeBackup {
|
||||
func newPodVolumeBackup(backup *velerov1api.Backup, pod *corev1api.Pod, volume corev1api.Volume, repoIdentifier, uploaderType string, pvc *corev1api.PersistentVolumeClaim) *velerov1api.PodVolumeBackup {
|
||||
pvb := &velerov1api.PodVolumeBackup{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: backup.Namespace,
|
||||
@@ -274,6 +280,7 @@ func newPodVolumeBackup(backup *velerov1api.Backup, pod *corev1api.Pod, volume c
|
||||
},
|
||||
BackupStorageLocation: backup.Spec.StorageLocation,
|
||||
RepoIdentifier: repoIdentifier,
|
||||
UploaderType: uploaderType,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
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 podvolume
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
|
||||
velerov1informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
)
|
||||
|
||||
// BackupperFactory can construct pod volumes backuppers.
|
||||
type BackupperFactory interface {
|
||||
// NewBackupper returns a pod volumes backupper for use during a single Velero backup.
|
||||
NewBackupper(context.Context, *velerov1api.Backup) (Backupper, error)
|
||||
}
|
||||
|
||||
func NewBackupperFactory(repoLocker *repository.RepoLocker,
|
||||
repoEnsurer *repository.RepositoryEnsurer,
|
||||
veleroClient clientset.Interface,
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter,
|
||||
pvClient corev1client.PersistentVolumesGetter,
|
||||
repoInformerSynced cache.InformerSynced,
|
||||
log logrus.FieldLogger) BackupperFactory {
|
||||
return &backupperFactory{
|
||||
repoLocker: repoLocker,
|
||||
repoEnsurer: repoEnsurer,
|
||||
veleroClient: veleroClient,
|
||||
pvcClient: pvcClient,
|
||||
pvClient: pvClient,
|
||||
repoInformerSynced: repoInformerSynced,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
type backupperFactory struct {
|
||||
repoLocker *repository.RepoLocker
|
||||
repoEnsurer *repository.RepositoryEnsurer
|
||||
veleroClient clientset.Interface
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter
|
||||
pvClient corev1client.PersistentVolumesGetter
|
||||
repoInformerSynced cache.InformerSynced
|
||||
log logrus.FieldLogger
|
||||
}
|
||||
|
||||
func (bf *backupperFactory) NewBackupper(ctx context.Context, backup *velerov1api.Backup) (Backupper, error) {
|
||||
informer := velerov1informers.NewFilteredPodVolumeBackupInformer(
|
||||
bf.veleroClient,
|
||||
backup.Namespace,
|
||||
0,
|
||||
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
|
||||
func(opts *metav1.ListOptions) {
|
||||
opts.LabelSelector = fmt.Sprintf("%s=%s", velerov1api.BackupUIDLabel, backup.UID)
|
||||
},
|
||||
)
|
||||
|
||||
b := newBackupper(ctx, bf.repoLocker, bf.repoEnsurer, informer, bf.veleroClient, bf.pvcClient, bf.pvClient, bf.log)
|
||||
|
||||
go informer.Run(ctx.Done())
|
||||
if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced, bf.repoInformerSynced) {
|
||||
return nil, errors.New("timed out waiting for caches to sync")
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
package podvolume
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
package mocks
|
||||
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import restic "github.com/vmware-tanzu/velero/pkg/restic"
|
||||
import (
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
"github.com/vmware-tanzu/velero/pkg/podvolume"
|
||||
)
|
||||
|
||||
// Restorer is an autogenerated mock type for the Restorer type
|
||||
type Restorer struct {
|
||||
@@ -11,11 +13,11 @@ type Restorer struct {
|
||||
}
|
||||
|
||||
// RestorePodVolumes provides a mock function with given fields: _a0
|
||||
func (_m *Restorer) RestorePodVolumes(_a0 restic.RestoreData) []error {
|
||||
func (_m *Restorer) RestorePodVolumes(_a0 podvolume.RestoreData) []error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 []error
|
||||
if rf, ok := ret.Get(0).(func(restic.RestoreData) []error); ok {
|
||||
if rf, ok := ret.Get(0).(func(podvolume.RestoreData) []error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
package podvolume
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -28,7 +28,9 @@ import (
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
|
||||
"github.com/vmware-tanzu/velero/pkg/label"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
)
|
||||
|
||||
@@ -46,10 +48,11 @@ type Restorer interface {
|
||||
}
|
||||
|
||||
type restorer struct {
|
||||
ctx context.Context
|
||||
repoManager *repositoryManager
|
||||
repoEnsurer *repositoryEnsurer
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter
|
||||
ctx context.Context
|
||||
repoLocker *repository.RepoLocker
|
||||
repoEnsurer *repository.RepositoryEnsurer
|
||||
veleroClient clientset.Interface
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter
|
||||
|
||||
resultsLock sync.Mutex
|
||||
results map[string]chan *velerov1api.PodVolumeRestore
|
||||
@@ -57,17 +60,19 @@ type restorer struct {
|
||||
|
||||
func newRestorer(
|
||||
ctx context.Context,
|
||||
rm *repositoryManager,
|
||||
repoEnsurer *repositoryEnsurer,
|
||||
repoLocker *repository.RepoLocker,
|
||||
repoEnsurer *repository.RepositoryEnsurer,
|
||||
podVolumeRestoreInformer cache.SharedIndexInformer,
|
||||
veleroClient clientset.Interface,
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter,
|
||||
log logrus.FieldLogger,
|
||||
) *restorer {
|
||||
r := &restorer{
|
||||
ctx: ctx,
|
||||
repoManager: rm,
|
||||
repoEnsurer: repoEnsurer,
|
||||
pvcClient: pvcClient,
|
||||
ctx: ctx,
|
||||
repoLocker: repoLocker,
|
||||
repoEnsurer: repoEnsurer,
|
||||
veleroClient: veleroClient,
|
||||
pvcClient: pvcClient,
|
||||
|
||||
results: make(map[string]chan *velerov1api.PodVolumeRestore),
|
||||
}
|
||||
@@ -108,8 +113,8 @@ func (r *restorer) RestorePodVolumes(data RestoreData) []error {
|
||||
|
||||
// get a single non-exclusive lock since we'll wait for all individual
|
||||
// restores to be complete before releasing it.
|
||||
r.repoManager.repoLocker.Lock(repo.Name)
|
||||
defer r.repoManager.repoLocker.Unlock(repo.Name)
|
||||
r.repoLocker.Lock(repo.Name)
|
||||
defer r.repoLocker.Unlock(repo.Name)
|
||||
|
||||
resultsChan := make(chan *velerov1api.PodVolumeRestore)
|
||||
|
||||
@@ -139,10 +144,10 @@ func (r *restorer) RestorePodVolumes(data RestoreData) []error {
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Remove the hard-coded uploader type before v1.10 FC
|
||||
volumeRestore := newPodVolumeRestore(data.Restore, data.Pod, data.BackupLocation, volume, snapshot, repo.Spec.ResticIdentifier, "restic", pvc)
|
||||
|
||||
volumeRestore := newPodVolumeRestore(data.Restore, data.Pod, data.BackupLocation, volume, snapshot, repo.Spec.ResticIdentifier, pvc)
|
||||
|
||||
if err := errorOnly(r.repoManager.veleroClient.VeleroV1().PodVolumeRestores(volumeRestore.Namespace).Create(context.TODO(), volumeRestore, metav1.CreateOptions{})); err != nil {
|
||||
if err := errorOnly(r.veleroClient.VeleroV1().PodVolumeRestores(volumeRestore.Namespace).Create(context.TODO(), volumeRestore, metav1.CreateOptions{})); err != nil {
|
||||
errs = append(errs, errors.WithStack(err))
|
||||
continue
|
||||
}
|
||||
@@ -169,7 +174,7 @@ ForEachVolume:
|
||||
return errs
|
||||
}
|
||||
|
||||
func newPodVolumeRestore(restore *velerov1api.Restore, pod *corev1api.Pod, backupLocation, volume, snapshot, repoIdentifier string, pvc *corev1api.PersistentVolumeClaim) *velerov1api.PodVolumeRestore {
|
||||
func newPodVolumeRestore(restore *velerov1api.Restore, pod *corev1api.Pod, backupLocation, volume, snapshot, repoIdentifier, uploaderType string, pvc *corev1api.PersistentVolumeClaim) *velerov1api.PodVolumeRestore {
|
||||
pvr := &velerov1api.PodVolumeRestore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: restore.Namespace,
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
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 podvolume
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
|
||||
velerov1informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
)
|
||||
|
||||
// RestorerFactory can construct pod volumes restorers.
|
||||
type RestorerFactory interface {
|
||||
// NewRestorer returns a pod volumes restorer for use during a single Velero restore.
|
||||
NewRestorer(context.Context, *velerov1api.Restore) (Restorer, error)
|
||||
}
|
||||
|
||||
func NewRestorerFactory(repoLocker *repository.RepoLocker,
|
||||
repoEnsurer *repository.RepositoryEnsurer,
|
||||
veleroClient clientset.Interface,
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter,
|
||||
repoInformerSynced cache.InformerSynced,
|
||||
log logrus.FieldLogger) RestorerFactory {
|
||||
return &restorerFactory{
|
||||
repoLocker: repoLocker,
|
||||
repoEnsurer: repoEnsurer,
|
||||
veleroClient: veleroClient,
|
||||
pvcClient: pvcClient,
|
||||
repoInformerSynced: repoInformerSynced,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
type restorerFactory struct {
|
||||
repoLocker *repository.RepoLocker
|
||||
repoEnsurer *repository.RepositoryEnsurer
|
||||
veleroClient clientset.Interface
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter
|
||||
repoInformerSynced cache.InformerSynced
|
||||
log logrus.FieldLogger
|
||||
}
|
||||
|
||||
func (rf *restorerFactory) NewRestorer(ctx context.Context, restore *velerov1api.Restore) (Restorer, error) {
|
||||
informer := velerov1informers.NewFilteredPodVolumeRestoreInformer(
|
||||
rf.veleroClient,
|
||||
restore.Namespace,
|
||||
0,
|
||||
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
|
||||
func(opts *metav1.ListOptions) {
|
||||
opts.LabelSelector = fmt.Sprintf("%s=%s", velerov1api.RestoreUIDLabel, restore.UID)
|
||||
},
|
||||
)
|
||||
|
||||
r := newRestorer(ctx, rf.repoLocker, rf.repoEnsurer, informer, rf.veleroClient, rf.pvcClient, rf.log)
|
||||
|
||||
go informer.Run(ctx.Done())
|
||||
if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced, rf.repoInformerSynced) {
|
||||
return nil, errors.New("timed out waiting for cache to sync")
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
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 podvolume
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
// PVCNameAnnotation is the key for the annotation added to
|
||||
// pod volume backups when they're for a PVC.
|
||||
PVCNameAnnotation = "velero.io/pvc-name"
|
||||
|
||||
// Deprecated.
|
||||
//
|
||||
// TODO(2.0): remove
|
||||
podAnnotationPrefix = "snapshot.velero.io/"
|
||||
|
||||
// VolumesToBackupAnnotation is the annotation on a pod whose mounted volumes
|
||||
// need to be backed up using restic.
|
||||
VolumesToBackupAnnotation = "backup.velero.io/backup-volumes"
|
||||
|
||||
// VolumesToExcludeAnnotation is the annotation on a pod whose mounted volumes
|
||||
// should be excluded from restic backup.
|
||||
VolumesToExcludeAnnotation = "backup.velero.io/backup-volumes-excludes"
|
||||
)
|
||||
|
||||
// GetVolumeBackupsForPod returns a map, of volume name -> snapshot id,
|
||||
// of the PodVolumeBackups that exist for the provided pod.
|
||||
func GetVolumeBackupsForPod(podVolumeBackups []*velerov1api.PodVolumeBackup, pod *corev1api.Pod, sourcePodNs string) map[string]string {
|
||||
volumes := make(map[string]string)
|
||||
|
||||
for _, pvb := range podVolumeBackups {
|
||||
if !isPVBMatchPod(pvb, pod.GetName(), sourcePodNs) {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip PVBs without a snapshot ID since there's nothing
|
||||
// to restore (they could be failed, or for empty volumes).
|
||||
if pvb.Status.SnapshotID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the volume came from a projected or DownwardAPI source, skip its restore.
|
||||
// This allows backups affected by https://github.com/vmware-tanzu/velero/issues/3863
|
||||
// or https://github.com/vmware-tanzu/velero/issues/4053 to be restored successfully.
|
||||
if volumeHasNonRestorableSource(pvb.Spec.Volume, pod.Spec.Volumes) {
|
||||
continue
|
||||
}
|
||||
|
||||
volumes[pvb.Spec.Volume] = pvb.Status.SnapshotID
|
||||
}
|
||||
|
||||
if len(volumes) > 0 {
|
||||
return volumes
|
||||
}
|
||||
|
||||
return getPodSnapshotAnnotations(pod)
|
||||
}
|
||||
|
||||
func isPVBMatchPod(pvb *velerov1api.PodVolumeBackup, podName string, namespace string) bool {
|
||||
return podName == pvb.Spec.Pod.Name && namespace == pvb.Spec.Pod.Namespace
|
||||
}
|
||||
|
||||
// volumeHasNonRestorableSource checks if the given volume exists in the list of podVolumes
|
||||
// and returns true if the volume's source is not restorable. This is true for volumes with
|
||||
// a Projected or DownwardAPI source.
|
||||
func volumeHasNonRestorableSource(volumeName string, podVolumes []corev1api.Volume) bool {
|
||||
var volume corev1api.Volume
|
||||
for _, v := range podVolumes {
|
||||
if v.Name == volumeName {
|
||||
volume = v
|
||||
break
|
||||
}
|
||||
}
|
||||
return volume.Projected != nil || volume.DownwardAPI != nil
|
||||
}
|
||||
|
||||
// getPodSnapshotAnnotations returns a map, of volume name -> snapshot id,
|
||||
// of all snapshots for this pod.
|
||||
// TODO(2.0) to remove
|
||||
// Deprecated: we will stop using pod annotations to record restic snapshot IDs after they're taken,
|
||||
// therefore we won't need to check if these annotations exist.
|
||||
func getPodSnapshotAnnotations(obj metav1.Object) map[string]string {
|
||||
var res map[string]string
|
||||
|
||||
insertSafe := func(k, v string) {
|
||||
if res == nil {
|
||||
res = make(map[string]string)
|
||||
}
|
||||
res[k] = v
|
||||
}
|
||||
|
||||
for k, v := range obj.GetAnnotations() {
|
||||
if strings.HasPrefix(k, podAnnotationPrefix) {
|
||||
insertSafe(k[len(podAnnotationPrefix):], v)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// GetVolumesToBackup returns a list of volume names to backup for
|
||||
// the provided pod.
|
||||
// Deprecated: Use GetPodVolumesUsingRestic instead.
|
||||
func GetVolumesToBackup(obj metav1.Object) []string {
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
backupsValue := annotations[VolumesToBackupAnnotation]
|
||||
if backupsValue == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(backupsValue, ",")
|
||||
}
|
||||
|
||||
func getVolumesToExclude(obj metav1.Object) []string {
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(annotations[VolumesToExcludeAnnotation], ",")
|
||||
}
|
||||
|
||||
func contains(list []string, k string) bool {
|
||||
for _, i := range list {
|
||||
if i == k {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetPodVolumesUsingRestic returns a list of volume names to backup for the provided pod.
|
||||
func GetPodVolumesUsingRestic(pod *corev1api.Pod, defaultVolumesToRestic bool) []string {
|
||||
if !defaultVolumesToRestic {
|
||||
return GetVolumesToBackup(pod)
|
||||
}
|
||||
|
||||
volsToExclude := getVolumesToExclude(pod)
|
||||
podVolumes := []string{}
|
||||
for _, pv := range pod.Spec.Volumes {
|
||||
// cannot backup hostpath volumes as they are not mounted into /var/lib/kubelet/pods
|
||||
// and therefore not accessible to the restic daemon set.
|
||||
if pv.HostPath != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup volumes mounting secrets. Secrets will be backed up separately.
|
||||
if pv.Secret != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup volumes mounting config maps. Config maps will be backed up separately.
|
||||
if pv.ConfigMap != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup volumes mounted as projected volumes, all data in those come from kube state.
|
||||
if pv.Projected != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup DownwardAPI volumes, all data in those come from kube state.
|
||||
if pv.DownwardAPI != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup volumes that are included in the exclude list.
|
||||
if contains(volsToExclude, pv.Name) {
|
||||
continue
|
||||
}
|
||||
// don't include volumes that mount the default service account token.
|
||||
if strings.HasPrefix(pv.Name, "default-token") {
|
||||
continue
|
||||
}
|
||||
podVolumes = append(podVolumes, pv.Name)
|
||||
}
|
||||
return podVolumes
|
||||
}
|
||||
@@ -0,0 +1,563 @@
|
||||
/*
|
||||
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 podvolume
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
)
|
||||
|
||||
func TestGetVolumeBackupsForPod(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
podVolumeBackups []*velerov1api.PodVolumeBackup
|
||||
podVolumes []corev1api.Volume
|
||||
podAnnotations map[string]string
|
||||
podName string
|
||||
sourcePodNs string
|
||||
expected map[string]string
|
||||
}{
|
||||
{
|
||||
name: "nil annotations results in no volume backups returned",
|
||||
podAnnotations: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "empty annotations results in no volume backups returned",
|
||||
podAnnotations: make(map[string]string),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "pod annotations with no snapshot annotation prefix results in no volume backups returned",
|
||||
podAnnotations: map[string]string{"foo": "bar"},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "pod annotation with only snapshot annotation prefix, results in volume backup with empty volume key",
|
||||
podAnnotations: map[string]string{podAnnotationPrefix: "snapshotID"},
|
||||
expected: map[string]string{"": "snapshotID"},
|
||||
},
|
||||
{
|
||||
name: "pod annotation with snapshot annotation prefix results in volume backup with volume name and snapshot ID",
|
||||
podAnnotations: map[string]string{podAnnotationPrefix + "volume": "snapshotID"},
|
||||
expected: map[string]string{"volume": "snapshotID"},
|
||||
},
|
||||
{
|
||||
name: "only pod annotations with snapshot annotation prefix are considered",
|
||||
podAnnotations: map[string]string{"x": "y", podAnnotationPrefix + "volume1": "snapshot1", podAnnotationPrefix + "volume2": "snapshot2"},
|
||||
expected: map[string]string{"volume1": "snapshot1", "volume2": "snapshot2"},
|
||||
},
|
||||
{
|
||||
name: "pod annotations are not considered if PVBs are provided",
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(),
|
||||
},
|
||||
podName: "TestPod",
|
||||
sourcePodNs: "TestNS",
|
||||
podAnnotations: map[string]string{"x": "y", podAnnotationPrefix + "foo": "bar", podAnnotationPrefix + "abc": "123"},
|
||||
expected: map[string]string{"pvbtest1-foo": "snapshot1", "pvbtest2-abc": "snapshot2"},
|
||||
},
|
||||
{
|
||||
name: "volume backups are returned even if no pod annotations are present",
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(),
|
||||
},
|
||||
podName: "TestPod",
|
||||
sourcePodNs: "TestNS",
|
||||
expected: map[string]string{"pvbtest1-foo": "snapshot1", "pvbtest2-abc": "snapshot2"},
|
||||
},
|
||||
{
|
||||
name: "only volumes from PVBs with snapshot IDs are returned",
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-3").PodName("TestPod").PodNamespace("TestNS").Volume("pvbtest3-foo").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-4").PodName("TestPod").PodNamespace("TestNS").Volume("pvbtest4-abc").Result(),
|
||||
},
|
||||
podName: "TestPod",
|
||||
sourcePodNs: "TestNS",
|
||||
expected: map[string]string{"pvbtest1-foo": "snapshot1", "pvbtest2-abc": "snapshot2"},
|
||||
},
|
||||
{
|
||||
name: "only volumes from PVBs for the given pod are returned",
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-3").PodName("TestAnotherPod").SnapshotID("snapshot3").Volume("pvbtest3-xyz").Result(),
|
||||
},
|
||||
podName: "TestPod",
|
||||
sourcePodNs: "TestNS",
|
||||
expected: map[string]string{"pvbtest1-foo": "snapshot1", "pvbtest2-abc": "snapshot2"},
|
||||
},
|
||||
{
|
||||
name: "only volumes from PVBs which match the pod name and source pod namespace are returned",
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestAnotherPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-3").PodName("TestPod").PodNamespace("TestAnotherNS").SnapshotID("snapshot3").Volume("pvbtest3-xyz").Result(),
|
||||
},
|
||||
podName: "TestPod",
|
||||
sourcePodNs: "TestNS",
|
||||
expected: map[string]string{"pvbtest1-foo": "snapshot1"},
|
||||
},
|
||||
{
|
||||
name: "volumes from PVBs that correspond to a pod volume from a projected source are not returned",
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvb-non-projected").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvb-projected").Result(),
|
||||
},
|
||||
podVolumes: []corev1api.Volume{
|
||||
{
|
||||
Name: "pvb-non-projected",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "pvb-projected",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
Projected: &corev1api.ProjectedVolumeSource{},
|
||||
},
|
||||
},
|
||||
},
|
||||
podName: "TestPod",
|
||||
sourcePodNs: "TestNS",
|
||||
expected: map[string]string{"pvb-non-projected": "snapshot1"},
|
||||
},
|
||||
{
|
||||
name: "volumes from PVBs that correspond to a pod volume from a DownwardAPI source are not returned",
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvb-non-downwardapi").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvb-downwardapi").Result(),
|
||||
},
|
||||
podVolumes: []corev1api.Volume{
|
||||
{
|
||||
Name: "pvb-non-downwardapi",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "pvb-downwardapi",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
DownwardAPI: &corev1api.DownwardAPIVolumeSource{},
|
||||
},
|
||||
},
|
||||
},
|
||||
podName: "TestPod",
|
||||
sourcePodNs: "TestNS",
|
||||
expected: map[string]string{"pvb-non-downwardapi": "snapshot1"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
pod := &corev1api.Pod{}
|
||||
pod.Annotations = test.podAnnotations
|
||||
pod.Name = test.podName
|
||||
pod.Spec.Volumes = test.podVolumes
|
||||
|
||||
res := GetVolumeBackupsForPod(test.podVolumeBackups, pod, test.sourcePodNs)
|
||||
assert.Equal(t, test.expected, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolumeHasNonRestorableSource(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
volumeName string
|
||||
podVolumes []corev1api.Volume
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "volume name not in list of volumes",
|
||||
volumeName: "missing-volume",
|
||||
podVolumes: []corev1api.Volume{
|
||||
{
|
||||
Name: "restorable",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "projected",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
Projected: &corev1api.ProjectedVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "downwardapi",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
DownwardAPI: &corev1api.DownwardAPIVolumeSource{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "volume name in list of volumes but not projected or DownwardAPI",
|
||||
volumeName: "restorable",
|
||||
podVolumes: []corev1api.Volume{
|
||||
{
|
||||
Name: "restorable",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "projected",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
Projected: &corev1api.ProjectedVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "downwardapi",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
DownwardAPI: &corev1api.DownwardAPIVolumeSource{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "volume name in list of volumes and projected",
|
||||
volumeName: "projected",
|
||||
podVolumes: []corev1api.Volume{
|
||||
{
|
||||
Name: "restorable",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "projected",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
Projected: &corev1api.ProjectedVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "downwardapi",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
DownwardAPI: &corev1api.DownwardAPIVolumeSource{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "volume name in list of volumes and is a DownwardAPI volume",
|
||||
volumeName: "downwardapi",
|
||||
podVolumes: []corev1api.Volume{
|
||||
{
|
||||
Name: "restorable",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "projected",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
Projected: &corev1api.ProjectedVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "downwardapi",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
DownwardAPI: &corev1api.DownwardAPIVolumeSource{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := volumeHasNonRestorableSource(tc.volumeName, tc.podVolumes)
|
||||
assert.Equal(t, tc.expected, actual)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVolumesToBackup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
annotations map[string]string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "nil annotations",
|
||||
annotations: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "no volumes to backup",
|
||||
annotations: map[string]string{"foo": "bar"},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "one volume to backup",
|
||||
annotations: map[string]string{"foo": "bar", VolumesToBackupAnnotation: "volume-1"},
|
||||
expected: []string{"volume-1"},
|
||||
},
|
||||
{
|
||||
name: "multiple volumes to backup",
|
||||
annotations: map[string]string{"foo": "bar", VolumesToBackupAnnotation: "volume-1,volume-2,volume-3"},
|
||||
expected: []string{"volume-1", "volume-2", "volume-3"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
pod := &corev1api.Pod{}
|
||||
pod.Annotations = test.annotations
|
||||
|
||||
res := GetVolumesToBackup(pod)
|
||||
|
||||
// sort to ensure good compare of slices
|
||||
sort.Strings(test.expected)
|
||||
sort.Strings(res)
|
||||
|
||||
assert.Equal(t, test.expected, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPodVolumesUsingRestic(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
pod *corev1api.Pod
|
||||
expected []string
|
||||
defaultVolumesToRestic bool
|
||||
}{
|
||||
{
|
||||
name: "should get PVs from VolumesToBackupAnnotation when defaultVolumesToRestic is false",
|
||||
defaultVolumesToRestic: false,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToBackupAnnotation: "resticPV1,resticPV2,resticPV3",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
{
|
||||
name: "should get all pod volumes when defaultVolumesToRestic is true and no PVs are excluded",
|
||||
defaultVolumesToRestic: true,
|
||||
pod: &corev1api.Pod{
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// Restic Volumes
|
||||
{Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
{
|
||||
name: "should get all pod volumes except ones excluded when defaultVolumesToRestic is true",
|
||||
defaultVolumesToRestic: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// Restic Volumes
|
||||
{Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"},
|
||||
/// Excluded from restic through annotation
|
||||
{Name: "nonResticPV1"}, {Name: "nonResticPV2"}, {Name: "nonResticPV3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
{
|
||||
name: "should exclude default service account token from restic backup",
|
||||
defaultVolumesToRestic: true,
|
||||
pod: &corev1api.Pod{
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// Restic Volumes
|
||||
{Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"},
|
||||
/// Excluded from restic because colume mounting default service account token
|
||||
{Name: "default-token-5xq45"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
{
|
||||
name: "should exclude host path volumes from restic backups",
|
||||
defaultVolumesToRestic: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// Restic Volumes
|
||||
{Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"},
|
||||
/// Excluded from restic through annotation
|
||||
{Name: "nonResticPV1"}, {Name: "nonResticPV2"}, {Name: "nonResticPV3"},
|
||||
// Excluded from restic because hostpath
|
||||
{Name: "hostPath1", VolumeSource: corev1api.VolumeSource{HostPath: &corev1api.HostPathVolumeSource{Path: "/hostpathVol"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
{
|
||||
name: "should exclude volumes mounting secrets",
|
||||
defaultVolumesToRestic: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// Restic Volumes
|
||||
{Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"},
|
||||
/// Excluded from restic through annotation
|
||||
{Name: "nonResticPV1"}, {Name: "nonResticPV2"}, {Name: "nonResticPV3"},
|
||||
// Excluded from restic because hostpath
|
||||
{Name: "superSecret", VolumeSource: corev1api.VolumeSource{Secret: &corev1api.SecretVolumeSource{SecretName: "super-secret"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
{
|
||||
name: "should exclude volumes mounting config maps",
|
||||
defaultVolumesToRestic: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// Restic Volumes
|
||||
{Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"},
|
||||
/// Excluded from restic through annotation
|
||||
{Name: "nonResticPV1"}, {Name: "nonResticPV2"}, {Name: "nonResticPV3"},
|
||||
// Excluded from restic because hostpath
|
||||
{Name: "appCOnfig", VolumeSource: corev1api.VolumeSource{ConfigMap: &corev1api.ConfigMapVolumeSource{LocalObjectReference: corev1api.LocalObjectReference{Name: "app-config"}}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
{
|
||||
name: "should exclude projected volumes",
|
||||
defaultVolumesToRestic: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
{Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"},
|
||||
{
|
||||
Name: "projected",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
Projected: &corev1api.ProjectedVolumeSource{
|
||||
Sources: []corev1api.VolumeProjection{{
|
||||
Secret: &corev1api.SecretProjection{
|
||||
LocalObjectReference: corev1api.LocalObjectReference{},
|
||||
Items: nil,
|
||||
Optional: nil,
|
||||
},
|
||||
DownwardAPI: nil,
|
||||
ConfigMap: nil,
|
||||
ServiceAccountToken: nil,
|
||||
}},
|
||||
DefaultMode: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
{
|
||||
name: "should exclude DownwardAPI volumes",
|
||||
defaultVolumesToRestic: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
{Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"},
|
||||
{
|
||||
Name: "downwardAPI",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
DownwardAPI: &corev1api.DownwardAPIVolumeSource{
|
||||
Items: []corev1api.DownwardAPIVolumeFile{
|
||||
{
|
||||
Path: "labels",
|
||||
FieldRef: &corev1api.ObjectFieldSelector{
|
||||
APIVersion: "v1",
|
||||
FieldPath: "metadata.labels",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := GetPodVolumesUsingRestic(tc.pod, tc.defaultVolumesToRestic)
|
||||
|
||||
sort.Strings(tc.expected)
|
||||
sort.Strings(actual)
|
||||
assert.Equal(t, tc.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
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 config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/endpoints"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// AWS specific environment variable
|
||||
awsProfileEnvVar = "AWS_PROFILE"
|
||||
awsProfileKey = "profile"
|
||||
awsCredentialsFileEnvVar = "AWS_SHARED_CREDENTIALS_FILE"
|
||||
)
|
||||
|
||||
// GetS3ResticEnvVars gets the environment variables that restic
|
||||
// relies on (AWS_PROFILE) based on info in the provided object
|
||||
// storage location config map.
|
||||
func GetS3ResticEnvVars(config map[string]string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
if credentialsFile, ok := config[CredentialsFileKey]; ok {
|
||||
result[awsCredentialsFileEnvVar] = credentialsFile
|
||||
}
|
||||
|
||||
if profile, ok := config[awsProfileKey]; ok {
|
||||
result[awsProfileEnvVar] = profile
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetS3Credentials gets the S3 credential values according to the information
|
||||
// of the provided config or the system's environment variables
|
||||
func GetS3Credentials(config map[string]string) (credentials.Value, error) {
|
||||
credentialsFile := config[CredentialsFileKey]
|
||||
if credentialsFile == "" {
|
||||
credentialsFile = os.Getenv("AWS_SHARED_CREDENTIALS_FILE")
|
||||
}
|
||||
|
||||
if credentialsFile == "" {
|
||||
return credentials.Value{}, errors.New("missing credential file")
|
||||
}
|
||||
|
||||
creds := credentials.NewSharedCredentials(credentialsFile, "")
|
||||
credValue, err := creds.Get()
|
||||
if err != nil {
|
||||
return credValue, err
|
||||
}
|
||||
|
||||
return credValue, nil
|
||||
}
|
||||
|
||||
// GetAWSBucketRegion returns the AWS region that a bucket is in, or an error
|
||||
// if the region cannot be determined.
|
||||
func GetAWSBucketRegion(bucket string) (string, error) {
|
||||
var region string
|
||||
|
||||
sess, err := session.NewSession()
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, partition := range endpoints.DefaultPartitions() {
|
||||
for regionHint := range partition.Regions() {
|
||||
region, _ = s3manager.GetBucketRegion(context.Background(), sess, bucket, regionHint)
|
||||
|
||||
// we only need to try a single region hint per partition, so break after the first
|
||||
break
|
||||
}
|
||||
|
||||
if region != "" {
|
||||
return region, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("unable to determine bucket's region")
|
||||
}
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -55,7 +55,7 @@ func TestGetS3ResticEnvVars(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual, err := getS3ResticEnvVars(tc.config)
|
||||
actual, err := GetS3ResticEnvVars(tc.config)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -37,6 +37,7 @@ const (
|
||||
storageAccountConfigKey = "storageAccount"
|
||||
storageAccountKeyEnvVarConfigKey = "storageAccountKeyEnvVar"
|
||||
subscriptionIDConfigKey = "subscriptionId"
|
||||
storageDomainConfigKey = "storageDomain"
|
||||
)
|
||||
|
||||
// getSubscriptionID gets the subscription ID from the 'config' map if it contains
|
||||
@@ -131,10 +132,10 @@ func mapLookup(data map[string]string) func(string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// getAzureResticEnvVars gets the environment variables that restic
|
||||
// GetAzureResticEnvVars gets the environment variables that restic
|
||||
// relies on (AZURE_ACCOUNT_NAME and AZURE_ACCOUNT_KEY) based
|
||||
// on info in the provided object storage location config map.
|
||||
func getAzureResticEnvVars(config map[string]string) (map[string]string, error) {
|
||||
func GetAzureResticEnvVars(config map[string]string) (map[string]string, error) {
|
||||
storageAccountKey, _, err := getStorageAccountKey(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -158,7 +159,7 @@ func credentialsFileFromEnv() string {
|
||||
// selectCredentialsFile selects the Azure credentials file to use, retrieving it
|
||||
// from the given config or falling back to retrieving it from the environment.
|
||||
func selectCredentialsFile(config map[string]string) string {
|
||||
if credentialsFile, ok := config[credentialsFileKey]; ok {
|
||||
if credentialsFile, ok := config[CredentialsFileKey]; ok {
|
||||
return credentialsFile
|
||||
}
|
||||
|
||||
@@ -208,3 +209,22 @@ func getRequiredValues(getValue func(string) string, keys ...string) (map[string
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetAzureStorageDomain gets the Azure storage domain required by a Azure blob connection,
|
||||
// if the provided config doean't have the value, get it from system's environment variables
|
||||
func GetAzureStorageDomain(config map[string]string) string {
|
||||
if domain, exist := config[storageDomainConfigKey]; exist {
|
||||
return domain
|
||||
} else {
|
||||
return os.Getenv(cloudNameEnvVar)
|
||||
}
|
||||
}
|
||||
|
||||
func GetAzureCredentials(config map[string]string) (string, string, error) {
|
||||
storageAccountKey, _, err := getStorageAccountKey(config)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return config[storageAccountConfigKey], storageAccountKey, nil
|
||||
}
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -14,17 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/endpoints"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
@@ -37,11 +33,16 @@ const (
|
||||
AWSBackend BackendType = "velero.io/aws"
|
||||
AzureBackend BackendType = "velero.io/azure"
|
||||
GCPBackend BackendType = "velero.io/gcp"
|
||||
FSBackend BackendType = "velero.io/fs"
|
||||
|
||||
// CredentialsFileKey is the key within a BSL config that is checked to see if
|
||||
// the BSL is using its own credentials, rather than those in the environment
|
||||
CredentialsFileKey = "credentialsFile"
|
||||
)
|
||||
|
||||
// this func is assigned to a package-level variable so it can be
|
||||
// replaced when unit-testing
|
||||
var getAWSBucketRegion = getBucketRegion
|
||||
var getAWSBucketRegion = GetAWSBucketRegion
|
||||
|
||||
// getRepoPrefix returns the prefix of the value of the --repo flag for
|
||||
// restic commands, i.e. everything except the "/<repo-name>".
|
||||
@@ -55,7 +56,7 @@ func getRepoPrefix(location *velerov1api.BackupStorageLocation) (string, error)
|
||||
prefix = layout.GetResticDir()
|
||||
}
|
||||
|
||||
backendType := getBackendType(location.Spec.Provider)
|
||||
backendType := GetBackendType(location.Spec.Provider)
|
||||
|
||||
if repoPrefix := location.Spec.Config["resticRepoPrefix"]; repoPrefix != "" {
|
||||
return repoPrefix, nil
|
||||
@@ -89,7 +90,7 @@ func getRepoPrefix(location *velerov1api.BackupStorageLocation) (string, error)
|
||||
return "", errors.New("restic repository prefix (resticRepoPrefix) not specified in backup storage location's config")
|
||||
}
|
||||
|
||||
func getBackendType(provider string) BackendType {
|
||||
func GetBackendType(provider string) BackendType {
|
||||
if !strings.Contains(provider, "/") {
|
||||
provider = "velero.io/" + provider
|
||||
}
|
||||
@@ -97,6 +98,10 @@ func getBackendType(provider string) BackendType {
|
||||
return BackendType(provider)
|
||||
}
|
||||
|
||||
func IsBackendTypeValid(backendType BackendType) bool {
|
||||
return (backendType == AWSBackend || backendType == AzureBackend || backendType == GCPBackend || backendType == FSBackend)
|
||||
}
|
||||
|
||||
// GetRepoIdentifier returns the string to be used as the value of the --repo flag in
|
||||
// restic commands for the given repository.
|
||||
func GetRepoIdentifier(location *velerov1api.BackupStorageLocation, name string) (string, error) {
|
||||
@@ -107,29 +112,3 @@ func GetRepoIdentifier(location *velerov1api.BackupStorageLocation, name string)
|
||||
|
||||
return fmt.Sprintf("%s/%s", strings.TrimSuffix(prefix, "/"), name), nil
|
||||
}
|
||||
|
||||
// getBucketRegion returns the AWS region that a bucket is in, or an error
|
||||
// if the region cannot be determined.
|
||||
func getBucketRegion(bucket string) (string, error) {
|
||||
var region string
|
||||
|
||||
sess, err := session.NewSession()
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, partition := range endpoints.DefaultPartitions() {
|
||||
for regionHint := range partition.Regions() {
|
||||
region, _ = s3manager.GetBucketRegion(context.Background(), sess, bucket, regionHint)
|
||||
|
||||
// we only need to try a single region hint per partition, so break after the first
|
||||
break
|
||||
}
|
||||
|
||||
if region != "" {
|
||||
return region, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("unable to determine bucket's region")
|
||||
}
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -14,21 +14,33 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
const (
|
||||
// GCP specific environment variable
|
||||
gcpCredentialsFileEnvVar = "GOOGLE_APPLICATION_CREDENTIALS"
|
||||
)
|
||||
|
||||
// getGCPResticEnvVars gets the environment variables that restic relies
|
||||
// GetGCPResticEnvVars gets the environment variables that restic relies
|
||||
// on based on info in the provided object storage location config map.
|
||||
func getGCPResticEnvVars(config map[string]string) (map[string]string, error) {
|
||||
func GetGCPResticEnvVars(config map[string]string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
if credentialsFile, ok := config[credentialsFileKey]; ok {
|
||||
if credentialsFile, ok := config[CredentialsFileKey]; ok {
|
||||
result[gcpCredentialsFileEnvVar] = credentialsFile
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetGCPCredentials gets the credential file required by a GCP bucket connection,
|
||||
// if the provided config doean't have the value, get it from system's environment variables
|
||||
func GetGCPCredentials(config map[string]string) string {
|
||||
if credentialsFile, ok := config[CredentialsFileKey]; ok {
|
||||
return credentialsFile
|
||||
} else {
|
||||
return os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -46,7 +46,7 @@ func TestGetGCPResticEnvVars(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual, err := getGCPResticEnvVars(tc.config)
|
||||
actual, err := GetGCPResticEnvVars(tc.config)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -35,14 +35,14 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/label"
|
||||
)
|
||||
|
||||
// repositoryEnsurer ensures that Velero restic repositories are created and ready.
|
||||
type repositoryEnsurer struct {
|
||||
// RepositoryEnsurer ensures that backup repositories are created and ready.
|
||||
type RepositoryEnsurer struct {
|
||||
log logrus.FieldLogger
|
||||
repoLister velerov1listers.ResticRepositoryLister
|
||||
repoClient velerov1client.ResticRepositoriesGetter
|
||||
repoLister velerov1listers.BackupRepositoryLister
|
||||
repoClient velerov1client.BackupRepositoriesGetter
|
||||
|
||||
repoChansLock sync.Mutex
|
||||
repoChans map[string]chan *velerov1api.ResticRepository
|
||||
repoChans map[string]chan *velerov1api.BackupRepository
|
||||
|
||||
// repoLocksMu synchronizes reads/writes to the repoLocks map itself
|
||||
// since maps are not threadsafe.
|
||||
@@ -55,20 +55,20 @@ type repoKey struct {
|
||||
backupLocation string
|
||||
}
|
||||
|
||||
func newRepositoryEnsurer(repoInformer velerov1informers.ResticRepositoryInformer, repoClient velerov1client.ResticRepositoriesGetter, log logrus.FieldLogger) *repositoryEnsurer {
|
||||
r := &repositoryEnsurer{
|
||||
func NewRepositoryEnsurer(repoInformer velerov1informers.BackupRepositoryInformer, repoClient velerov1client.BackupRepositoriesGetter, log logrus.FieldLogger) *RepositoryEnsurer {
|
||||
r := &RepositoryEnsurer{
|
||||
log: log,
|
||||
repoLister: repoInformer.Lister(),
|
||||
repoClient: repoClient,
|
||||
repoChans: make(map[string]chan *velerov1api.ResticRepository),
|
||||
repoChans: make(map[string]chan *velerov1api.BackupRepository),
|
||||
repoLocks: make(map[repoKey]*sync.Mutex),
|
||||
}
|
||||
|
||||
repoInformer.Informer().AddEventHandler(
|
||||
cache.ResourceEventHandlerFuncs{
|
||||
UpdateFunc: func(old, upd interface{}) {
|
||||
oldObj := old.(*velerov1api.ResticRepository)
|
||||
newObj := upd.(*velerov1api.ResticRepository)
|
||||
oldObj := old.(*velerov1api.BackupRepository)
|
||||
newObj := upd.(*velerov1api.BackupRepository)
|
||||
|
||||
// we're only interested in phase-changing updates
|
||||
if oldObj.Status.Phase == newObj.Status.Phase {
|
||||
@@ -76,7 +76,7 @@ func newRepositoryEnsurer(repoInformer velerov1informers.ResticRepositoryInforme
|
||||
}
|
||||
|
||||
// we're only interested in updates where the updated object is either Ready or NotReady
|
||||
if newObj.Status.Phase != velerov1api.ResticRepositoryPhaseReady && newObj.Status.Phase != velerov1api.ResticRepositoryPhaseNotReady {
|
||||
if newObj.Status.Phase != velerov1api.BackupRepositoryPhaseReady && newObj.Status.Phase != velerov1api.BackupRepositoryPhaseNotReady {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ func repoLabels(volumeNamespace, backupLocation string) labels.Set {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *repositoryEnsurer) EnsureRepo(ctx context.Context, namespace, volumeNamespace, backupLocation string) (*velerov1api.ResticRepository, error) {
|
||||
func (r *RepositoryEnsurer) EnsureRepo(ctx context.Context, namespace, volumeNamespace, backupLocation string) (*velerov1api.BackupRepository, error) {
|
||||
log := r.log.WithField("volumeNamespace", volumeNamespace).WithField("backupLocation", backupLocation)
|
||||
|
||||
// It's only safe to have one instance of this method executing concurrently for a
|
||||
@@ -132,7 +132,7 @@ func (r *repositoryEnsurer) EnsureRepo(ctx context.Context, namespace, volumeNam
|
||||
|
||||
selector := labels.SelectorFromSet(repoLabels(volumeNamespace, backupLocation))
|
||||
|
||||
repos, err := r.repoLister.ResticRepositories(namespace).List(selector)
|
||||
repos, err := r.repoLister.BackupRepositories(namespace).List(selector)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
@@ -140,7 +140,7 @@ func (r *repositoryEnsurer) EnsureRepo(ctx context.Context, namespace, volumeNam
|
||||
return nil, errors.Errorf("more than one ResticRepository found for workload namespace %q, backup storage location %q", volumeNamespace, backupLocation)
|
||||
}
|
||||
if len(repos) == 1 {
|
||||
if repos[0].Status.Phase != velerov1api.ResticRepositoryPhaseReady {
|
||||
if repos[0].Status.Phase != velerov1api.BackupRepositoryPhaseReady {
|
||||
return nil, errors.Errorf("restic repository is not ready: %s", repos[0].Status.Message)
|
||||
}
|
||||
|
||||
@@ -151,13 +151,13 @@ func (r *repositoryEnsurer) EnsureRepo(ctx context.Context, namespace, volumeNam
|
||||
log.Debug("No repository found, creating one")
|
||||
|
||||
// no repo found: create one and wait for it to be ready
|
||||
repo := &velerov1api.ResticRepository{
|
||||
repo := &velerov1api.BackupRepository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
GenerateName: fmt.Sprintf("%s-%s-", volumeNamespace, backupLocation),
|
||||
Labels: repoLabels(volumeNamespace, backupLocation),
|
||||
},
|
||||
Spec: velerov1api.ResticRepositorySpec{
|
||||
Spec: velerov1api.BackupRepositorySpec{
|
||||
VolumeNamespace: volumeNamespace,
|
||||
BackupStorageLocation: backupLocation,
|
||||
},
|
||||
@@ -169,7 +169,7 @@ func (r *repositoryEnsurer) EnsureRepo(ctx context.Context, namespace, volumeNam
|
||||
close(repoChan)
|
||||
}()
|
||||
|
||||
if _, err := r.repoClient.ResticRepositories(namespace).Create(context.TODO(), repo, metav1.CreateOptions{}); err != nil {
|
||||
if _, err := r.repoClient.BackupRepositories(namespace).Create(context.TODO(), repo, metav1.CreateOptions{}); err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to create restic repository resource")
|
||||
}
|
||||
|
||||
@@ -181,7 +181,8 @@ func (r *repositoryEnsurer) EnsureRepo(ctx context.Context, namespace, volumeNam
|
||||
case <-ctx.Done():
|
||||
return nil, errors.New("timed out waiting for restic repository to become ready")
|
||||
case res := <-repoChan:
|
||||
if res.Status.Phase == velerov1api.ResticRepositoryPhaseNotReady {
|
||||
|
||||
if res.Status.Phase == velerov1api.BackupRepositoryPhaseNotReady {
|
||||
return nil, errors.Errorf("restic repository is not ready: %s", res.Status.Message)
|
||||
}
|
||||
|
||||
@@ -189,15 +190,15 @@ func (r *repositoryEnsurer) EnsureRepo(ctx context.Context, namespace, volumeNam
|
||||
}
|
||||
}
|
||||
|
||||
func (r *repositoryEnsurer) getRepoChan(name string) chan *velerov1api.ResticRepository {
|
||||
func (r *RepositoryEnsurer) getRepoChan(name string) chan *velerov1api.BackupRepository {
|
||||
r.repoChansLock.Lock()
|
||||
defer r.repoChansLock.Unlock()
|
||||
|
||||
r.repoChans[name] = make(chan *velerov1api.ResticRepository)
|
||||
r.repoChans[name] = make(chan *velerov1api.BackupRepository)
|
||||
return r.repoChans[name]
|
||||
}
|
||||
|
||||
func (r *repositoryEnsurer) repoLock(volumeNamespace, backupLocation string) *sync.Mutex {
|
||||
func (r *RepositoryEnsurer) repoLock(volumeNamespace, backupLocation string) *sync.Mutex {
|
||||
r.repoLocksMu.Lock()
|
||||
defer r.repoLocksMu.Unlock()
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
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 keys
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
)
|
||||
|
||||
const (
|
||||
credentialsSecretName = "velero-restic-credentials"
|
||||
credentialsKey = "repository-password"
|
||||
|
||||
encryptionKey = "static-passw0rd"
|
||||
)
|
||||
|
||||
func EnsureCommonRepositoryKey(secretClient corev1client.SecretsGetter, namespace string) error {
|
||||
_, err := secretClient.Secrets(namespace).Get(context.TODO(), credentialsSecretName, metav1.GetOptions{})
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if we got here, we got an IsNotFound error, so we need to create the key
|
||||
|
||||
secret := &corev1api.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
Name: credentialsSecretName,
|
||||
},
|
||||
Type: corev1api.SecretTypeOpaque,
|
||||
Data: map[string][]byte{
|
||||
credentialsKey: []byte(encryptionKey),
|
||||
},
|
||||
}
|
||||
|
||||
if _, err = secretClient.Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{}); err != nil {
|
||||
return errors.Wrapf(err, "error creating %s secret", credentialsSecretName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RepoKeySelector returns the SecretKeySelector which can be used to fetch
|
||||
// the restic repository key.
|
||||
func RepoKeySelector() *corev1api.SecretKeySelector {
|
||||
// For now, all restic repos share the same key so we don't need the repoName to fetch it.
|
||||
// When we move to full-backup encryption, we'll likely have a separate key per restic repo
|
||||
// (all within the Velero server's namespace) so RepoKeySelector will need to select the key
|
||||
// for that repo.
|
||||
return builder.ForSecretKeySelector(credentialsSecretName, credentialsKey).Result()
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
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 keys
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRepoKeySelector(t *testing.T) {
|
||||
selector := RepoKeySelector()
|
||||
|
||||
require.Equal(t, credentialsSecretName, selector.Name)
|
||||
require.Equal(t, credentialsKey, selector.Key)
|
||||
}
|
||||
@@ -13,23 +13,24 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package restic
|
||||
|
||||
package repository
|
||||
|
||||
import "sync"
|
||||
|
||||
// repoLocker manages exclusive/non-exclusive locks for
|
||||
// RepoLocker manages exclusive/non-exclusive locks for
|
||||
// operations against restic repositories. The semantics
|
||||
// of exclusive/non-exclusive locks are the same as for
|
||||
// a sync.RWMutex, where a non-exclusive lock is equivalent
|
||||
// to a read lock, and an exclusive lock is equivalent to
|
||||
// a write lock.
|
||||
type repoLocker struct {
|
||||
type RepoLocker struct {
|
||||
mu sync.Mutex
|
||||
locks map[string]*sync.RWMutex
|
||||
}
|
||||
|
||||
func newRepoLocker() *repoLocker {
|
||||
return &repoLocker{
|
||||
func NewRepoLocker() *RepoLocker {
|
||||
return &RepoLocker{
|
||||
locks: make(map[string]*sync.RWMutex),
|
||||
}
|
||||
}
|
||||
@@ -37,28 +38,28 @@ func newRepoLocker() *repoLocker {
|
||||
// LockExclusive acquires an exclusive lock for the specified
|
||||
// repository. This function blocks until no other locks exist
|
||||
// for the repo.
|
||||
func (rl *repoLocker) LockExclusive(name string) {
|
||||
func (rl *RepoLocker) LockExclusive(name string) {
|
||||
rl.ensureLock(name).Lock()
|
||||
}
|
||||
|
||||
// Lock acquires a non-exclusive lock for the specified
|
||||
// repository. This function blocks until no exclusive
|
||||
// locks exist for the repo.
|
||||
func (rl *repoLocker) Lock(name string) {
|
||||
func (rl *RepoLocker) Lock(name string) {
|
||||
rl.ensureLock(name).RLock()
|
||||
}
|
||||
|
||||
// UnlockExclusive releases an exclusive lock for the repo.
|
||||
func (rl *repoLocker) UnlockExclusive(name string) {
|
||||
func (rl *RepoLocker) UnlockExclusive(name string) {
|
||||
rl.ensureLock(name).Unlock()
|
||||
}
|
||||
|
||||
// Unlock releases a non-exclusive lock for the repo.
|
||||
func (rl *repoLocker) Unlock(name string) {
|
||||
func (rl *RepoLocker) Unlock(name string) {
|
||||
rl.ensureLock(name).RUnlock()
|
||||
}
|
||||
|
||||
func (rl *repoLocker) ensureLock(name string) *sync.RWMutex {
|
||||
func (rl *RepoLocker) ensureLock(name string) *sync.RWMutex {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
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 provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
)
|
||||
|
||||
// RepoParam includes the parameters to manipulate a backup repository
|
||||
// SubDir is used to generate the path in the backup storage
|
||||
type RepoParam struct {
|
||||
SubDir string
|
||||
BackupLocation *velerov1api.BackupStorageLocation
|
||||
}
|
||||
|
||||
type Provider interface {
|
||||
//InitRepo is to initialize a repository from a new storage place
|
||||
InitRepo(ctx context.Context, param RepoParam) error
|
||||
|
||||
//ConnectToRepo is to establish the connection to a
|
||||
//storage place that a repository is already initialized
|
||||
ConnectToRepo(ctx context.Context, param RepoParam) error
|
||||
|
||||
//PrepareRepo is a combination of InitRepo and ConnectToRepo,
|
||||
//it may do initializing + connecting, connecting only if the repository
|
||||
//is already initialized, or do nothing if the repository is already connected
|
||||
PrepareRepo(ctx context.Context, param RepoParam) error
|
||||
|
||||
//PruneRepo does a full prune/maintenance of the repository
|
||||
PruneRepo(ctx context.Context, param RepoParam) error
|
||||
|
||||
//PruneRepoQuick does a quick prune/maintenance of the repository if available
|
||||
PruneRepoQuick(ctx context.Context, param RepoParam) error
|
||||
|
||||
//EnsureUnlockRepo esures to remove any stale file locks in the storage
|
||||
EnsureUnlockRepo(ctx context.Context, param RepoParam) error
|
||||
|
||||
//Forget is to delete a snapshot from the repository
|
||||
Forget(ctx context.Context, snapshotID string, param RepoParam) error
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
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 provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/credentials"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
repoconfig "github.com/vmware-tanzu/velero/pkg/repository/config"
|
||||
repokey "github.com/vmware-tanzu/velero/pkg/repository/keys"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/ownership"
|
||||
)
|
||||
|
||||
type unifiedRepoProvider struct {
|
||||
credentialGetter credentials.CredentialGetter
|
||||
workPath string
|
||||
repoService udmrepo.BackupRepoService
|
||||
log logrus.FieldLogger
|
||||
}
|
||||
|
||||
// this func is assigned to a package-level variable so it can be
|
||||
// replaced when unit-testing
|
||||
var getAzureCredentials = repoconfig.GetAzureCredentials
|
||||
var getS3Credentials = repoconfig.GetS3Credentials
|
||||
var getGCPCredentials = repoconfig.GetGCPCredentials
|
||||
var getS3BucketRegion = repoconfig.GetAWSBucketRegion
|
||||
var getAzureStorageDomain = repoconfig.GetAzureStorageDomain
|
||||
|
||||
type localFuncTable struct {
|
||||
getRepoPassword func(credentials.SecretStore, RepoParam) (string, error)
|
||||
getStorageVariables func(*velerov1api.BackupStorageLocation, string) (map[string]string, error)
|
||||
getStorageCredentials func(*velerov1api.BackupStorageLocation, credentials.FileStore) (map[string]string, error)
|
||||
}
|
||||
|
||||
var funcTable = localFuncTable{
|
||||
getRepoPassword: getRepoPassword,
|
||||
getStorageVariables: getStorageVariables,
|
||||
getStorageCredentials: getStorageCredentials,
|
||||
}
|
||||
|
||||
// NewUnifiedRepoProvider creates the service provider for Unified Repo
|
||||
// workPath is the path for Unified Repo to store some local information
|
||||
// workPath could be empty, if so, the default path will be used
|
||||
func NewUnifiedRepoProvider(
|
||||
credentialGetter credentials.CredentialGetter,
|
||||
workPath string,
|
||||
log logrus.FieldLogger,
|
||||
) (Provider, error) {
|
||||
repo := unifiedRepoProvider{
|
||||
credentialGetter: credentialGetter,
|
||||
workPath: workPath,
|
||||
log: log,
|
||||
}
|
||||
|
||||
repo.repoService = createRepoService(log)
|
||||
|
||||
log.Debug("Finished create unified repo service")
|
||||
|
||||
return &repo, nil
|
||||
}
|
||||
|
||||
func (urp *unifiedRepoProvider) InitRepo(ctx context.Context, param RepoParam) error {
|
||||
log := urp.log.WithFields(logrus.Fields{
|
||||
"BSL name": param.BackupLocation.Name,
|
||||
"BSL UID": param.BackupLocation.UID,
|
||||
})
|
||||
|
||||
log.Debug("Start to init repo")
|
||||
|
||||
repoOption, err := urp.getRepoOption(param)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error to get repo options")
|
||||
}
|
||||
|
||||
err = urp.repoService.Init(ctx, repoOption, true)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error to init backup repo")
|
||||
}
|
||||
|
||||
log.Debug("Init repo complete")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (urp *unifiedRepoProvider) ConnectToRepo(ctx context.Context, param RepoParam) error {
|
||||
///TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
func (urp *unifiedRepoProvider) PrepareRepo(ctx context.Context, param RepoParam) error {
|
||||
///TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
func (urp *unifiedRepoProvider) PruneRepo(ctx context.Context, param RepoParam) error {
|
||||
///TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
func (urp *unifiedRepoProvider) PruneRepoQuick(ctx context.Context, param RepoParam) error {
|
||||
///TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
func (urp *unifiedRepoProvider) EnsureUnlockRepo(ctx context.Context, param RepoParam) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (urp *unifiedRepoProvider) Forget(ctx context.Context, snapshotID string, param RepoParam) error {
|
||||
///TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRepoPassword(secretStore credentials.SecretStore, param RepoParam) (string, error) {
|
||||
if secretStore == nil {
|
||||
return "", errors.New("invalid credentials interface")
|
||||
}
|
||||
|
||||
buf, err := secretStore.Get(repokey.RepoKeySelector())
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error to get password buffer")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(buf)), nil
|
||||
}
|
||||
|
||||
func (urp *unifiedRepoProvider) getRepoOption(param RepoParam) (udmrepo.RepoOptions, error) {
|
||||
repoPassword, err := funcTable.getRepoPassword(urp.credentialGetter.FromSecret, param)
|
||||
if err != nil {
|
||||
return udmrepo.RepoOptions{}, errors.Wrap(err, "error to get repo password")
|
||||
}
|
||||
|
||||
storeVar, err := funcTable.getStorageVariables(param.BackupLocation, param.SubDir)
|
||||
if err != nil {
|
||||
return udmrepo.RepoOptions{}, errors.Wrap(err, "error to get storage variables")
|
||||
}
|
||||
|
||||
storeCred, err := funcTable.getStorageCredentials(param.BackupLocation, urp.credentialGetter.FromFile)
|
||||
if err != nil {
|
||||
return udmrepo.RepoOptions{}, errors.Wrap(err, "error to get repo credentials")
|
||||
}
|
||||
|
||||
repoOption := udmrepo.RepoOptions{
|
||||
StorageType: getStorageType(param.BackupLocation),
|
||||
RepoPassword: repoPassword,
|
||||
ConfigFilePath: getRepoConfigFile(urp.workPath, string(param.BackupLocation.UID)),
|
||||
Ownership: udmrepo.OwnershipOptions{
|
||||
Username: ownership.GetRepositoryOwner().Username,
|
||||
DomainName: ownership.GetRepositoryOwner().DomainName,
|
||||
},
|
||||
StorageOptions: make(map[string]string),
|
||||
GeneralOptions: make(map[string]string),
|
||||
}
|
||||
|
||||
for k, v := range storeVar {
|
||||
repoOption.StorageOptions[k] = v
|
||||
}
|
||||
|
||||
for k, v := range storeCred {
|
||||
repoOption.StorageOptions[k] = v
|
||||
}
|
||||
|
||||
return repoOption, nil
|
||||
}
|
||||
|
||||
func getStorageType(backupLocation *velerov1api.BackupStorageLocation) string {
|
||||
backendType := repoconfig.GetBackendType(backupLocation.Spec.Provider)
|
||||
|
||||
switch backendType {
|
||||
case repoconfig.AWSBackend:
|
||||
return udmrepo.StorageTypeS3
|
||||
case repoconfig.AzureBackend:
|
||||
return udmrepo.StorageTypeAzure
|
||||
case repoconfig.GCPBackend:
|
||||
return udmrepo.StorageTypeGcs
|
||||
case repoconfig.FSBackend:
|
||||
return udmrepo.StorageTypeFs
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func getStorageCredentials(backupLocation *velerov1api.BackupStorageLocation, credentialsFileStore credentials.FileStore) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
var err error
|
||||
|
||||
if credentialsFileStore == nil {
|
||||
return map[string]string{}, errors.New("invalid credentials interface")
|
||||
}
|
||||
|
||||
backendType := repoconfig.GetBackendType(backupLocation.Spec.Provider)
|
||||
if !repoconfig.IsBackendTypeValid(backendType) {
|
||||
return map[string]string{}, errors.New("invalid storage provider")
|
||||
}
|
||||
|
||||
config := backupLocation.Spec.Config
|
||||
if config == nil {
|
||||
config = map[string]string{}
|
||||
}
|
||||
|
||||
if backupLocation.Spec.Credential != nil {
|
||||
config[repoconfig.CredentialsFileKey], err = credentialsFileStore.Path(backupLocation.Spec.Credential)
|
||||
if err != nil {
|
||||
return map[string]string{}, errors.Wrap(err, "error get credential file in bsl")
|
||||
}
|
||||
}
|
||||
|
||||
switch backendType {
|
||||
case repoconfig.AWSBackend:
|
||||
credValue, err := getS3Credentials(config)
|
||||
if err != nil {
|
||||
return map[string]string{}, errors.Wrap(err, "error get s3 credentials")
|
||||
}
|
||||
result[udmrepo.StoreOptionS3KeyId] = credValue.AccessKeyID
|
||||
result[udmrepo.StoreOptionS3Provider] = credValue.ProviderName
|
||||
result[udmrepo.StoreOptionS3SecretKey] = credValue.SecretAccessKey
|
||||
result[udmrepo.StoreOptionS3Token] = credValue.SessionToken
|
||||
|
||||
case repoconfig.AzureBackend:
|
||||
storageAccount, accountKey, err := getAzureCredentials(config)
|
||||
if err != nil {
|
||||
return map[string]string{}, errors.Wrap(err, "error get azure credentials")
|
||||
}
|
||||
result[udmrepo.StoreOptionAzureStorageAccount] = storageAccount
|
||||
result[udmrepo.StoreOptionAzureKey] = accountKey
|
||||
|
||||
case repoconfig.GCPBackend:
|
||||
result[udmrepo.StoreOptionCredentialFile] = getGCPCredentials(config)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getStorageVariables(backupLocation *velerov1api.BackupStorageLocation, repoName string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
backendType := repoconfig.GetBackendType(backupLocation.Spec.Provider)
|
||||
if !repoconfig.IsBackendTypeValid(backendType) {
|
||||
return map[string]string{}, errors.New("invalid storage provider")
|
||||
}
|
||||
|
||||
config := backupLocation.Spec.Config
|
||||
if config == nil {
|
||||
config = map[string]string{}
|
||||
}
|
||||
|
||||
bucket := strings.Trim(config["bucket"], "/")
|
||||
prefix := strings.Trim(config["prefix"], "/")
|
||||
if backupLocation.Spec.ObjectStorage != nil {
|
||||
bucket = strings.Trim(backupLocation.Spec.ObjectStorage.Bucket, "/")
|
||||
prefix = strings.Trim(backupLocation.Spec.ObjectStorage.Prefix, "/")
|
||||
}
|
||||
|
||||
prefix = path.Join(prefix, udmrepo.StoreOptionPrefixName, repoName) + "/"
|
||||
|
||||
region := config["region"]
|
||||
|
||||
if backendType == repoconfig.AWSBackend {
|
||||
s3Url := config["s3Url"]
|
||||
|
||||
var err error
|
||||
if s3Url == "" {
|
||||
region, err = getS3BucketRegion(bucket)
|
||||
if err != nil {
|
||||
return map[string]string{}, errors.Wrap(err, "error get s3 bucket region")
|
||||
}
|
||||
|
||||
s3Url = fmt.Sprintf("s3-%s.amazonaws.com", region)
|
||||
}
|
||||
|
||||
result[udmrepo.StoreOptionS3Endpoint] = strings.Trim(s3Url, "/")
|
||||
result[udmrepo.StoreOptionS3DisableTlsVerify] = config["insecureSkipTLSVerify"]
|
||||
} else if backendType == repoconfig.AzureBackend {
|
||||
result[udmrepo.StoreOptionAzureDomain] = getAzureStorageDomain(config)
|
||||
}
|
||||
|
||||
result[udmrepo.StoreOptionOssBucket] = bucket
|
||||
result[udmrepo.StoreOptionPrefix] = prefix
|
||||
result[udmrepo.StoreOptionOssRegion] = strings.Trim(region, "/")
|
||||
result[udmrepo.StoreOptionFsPath] = config["fspath"]
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getRepoConfigFile(workPath string, repoID string) string {
|
||||
///TODO: call udmrepo to get config file
|
||||
return ""
|
||||
}
|
||||
|
||||
func createRepoService(log logrus.FieldLogger) udmrepo.BackupRepoService {
|
||||
///TODO: call udmrepo create repo service
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
/*
|
||||
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 provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
awscredentials "github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
|
||||
velerocredentials "github.com/vmware-tanzu/velero/internal/credentials"
|
||||
credmock "github.com/vmware-tanzu/velero/internal/credentials/mocks"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
)
|
||||
|
||||
func TestGetStorageCredentials(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
backupLocation velerov1api.BackupStorageLocation
|
||||
credFileStore *credmock.FileStore
|
||||
credStoreError error
|
||||
credStorePath string
|
||||
getAzureCredentials func(map[string]string) (string, string, error)
|
||||
getS3Credentials func(map[string]string) (awscredentials.Value, error)
|
||||
getGCPCredentials func(map[string]string) string
|
||||
expected map[string]string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "invalid credentials file store interface",
|
||||
expected: map[string]string{},
|
||||
expectedErr: "invalid credentials interface",
|
||||
},
|
||||
{
|
||||
name: "invalid provider",
|
||||
backupLocation: velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "invalid-provider",
|
||||
},
|
||||
},
|
||||
credFileStore: new(credmock.FileStore),
|
||||
expected: map[string]string{},
|
||||
expectedErr: "invalid storage provider",
|
||||
},
|
||||
{
|
||||
name: "credential section exists in BSL, file store fail",
|
||||
backupLocation: velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "aws",
|
||||
Credential: &corev1api.SecretKeySelector{},
|
||||
},
|
||||
},
|
||||
credFileStore: new(credmock.FileStore),
|
||||
credStoreError: errors.New("fake error"),
|
||||
expected: map[string]string{},
|
||||
expectedErr: "error get credential file in bsl: fake error",
|
||||
},
|
||||
{
|
||||
name: "aws, Credential section not exists in BSL",
|
||||
backupLocation: velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "velero.io/aws",
|
||||
Config: map[string]string{
|
||||
"credentialsFile": "credentials-from-config-map",
|
||||
},
|
||||
},
|
||||
},
|
||||
getS3Credentials: func(config map[string]string) (awscredentials.Value, error) {
|
||||
return awscredentials.Value{
|
||||
AccessKeyID: "from: " + config["credentialsFile"],
|
||||
}, nil
|
||||
},
|
||||
credFileStore: new(credmock.FileStore),
|
||||
expected: map[string]string{
|
||||
"accessKeyID": "from: credentials-from-config-map",
|
||||
"providerName": "",
|
||||
"secretAccessKey": "",
|
||||
"sessionToken": "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aws, Credential section exists in BSL",
|
||||
backupLocation: velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "velero.io/aws",
|
||||
Config: map[string]string{
|
||||
"credentialsFile": "credentials-from-config-map",
|
||||
},
|
||||
Credential: &corev1api.SecretKeySelector{},
|
||||
},
|
||||
},
|
||||
credFileStore: new(credmock.FileStore),
|
||||
credStorePath: "credentials-from-credential-key",
|
||||
getS3Credentials: func(config map[string]string) (awscredentials.Value, error) {
|
||||
return awscredentials.Value{
|
||||
AccessKeyID: "from: " + config["credentialsFile"],
|
||||
}, nil
|
||||
},
|
||||
|
||||
expected: map[string]string{
|
||||
"accessKeyID": "from: credentials-from-credential-key",
|
||||
"providerName": "",
|
||||
"secretAccessKey": "",
|
||||
"sessionToken": "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aws, get credentials fail",
|
||||
backupLocation: velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "velero.io/aws",
|
||||
Config: map[string]string{
|
||||
"credentialsFile": "credentials-from-config-map",
|
||||
},
|
||||
},
|
||||
},
|
||||
getS3Credentials: func(config map[string]string) (awscredentials.Value, error) {
|
||||
return awscredentials.Value{}, errors.New("fake error")
|
||||
},
|
||||
credFileStore: new(credmock.FileStore),
|
||||
expected: map[string]string{},
|
||||
expectedErr: "error get s3 credentials: fake error",
|
||||
},
|
||||
{
|
||||
name: "azure, Credential section exists in BSL",
|
||||
backupLocation: velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "velero.io/azure",
|
||||
Config: map[string]string{
|
||||
"credentialsFile": "credentials-from-config-map",
|
||||
},
|
||||
Credential: &corev1api.SecretKeySelector{},
|
||||
},
|
||||
},
|
||||
credFileStore: new(credmock.FileStore),
|
||||
credStorePath: "credentials-from-credential-key",
|
||||
getAzureCredentials: func(config map[string]string) (string, string, error) {
|
||||
return "storage account from: " + config["credentialsFile"], "", nil
|
||||
},
|
||||
|
||||
expected: map[string]string{
|
||||
"storageAccount": "storage account from: credentials-from-credential-key",
|
||||
"storageKey": "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "azure, get azure credentials fails",
|
||||
backupLocation: velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "velero.io/azure",
|
||||
Config: map[string]string{
|
||||
"credentialsFile": "credentials-from-config-map",
|
||||
},
|
||||
},
|
||||
},
|
||||
getAzureCredentials: func(config map[string]string) (string, string, error) {
|
||||
return "", "", errors.New("fake error")
|
||||
},
|
||||
credFileStore: new(credmock.FileStore),
|
||||
expected: map[string]string{},
|
||||
expectedErr: "error get azure credentials: fake error",
|
||||
},
|
||||
{
|
||||
name: "gcp, Credential section not exists in BSL",
|
||||
backupLocation: velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "velero.io/gcp",
|
||||
Config: map[string]string{
|
||||
"credentialsFile": "credentials-from-config-map",
|
||||
},
|
||||
},
|
||||
},
|
||||
getGCPCredentials: func(config map[string]string) string {
|
||||
return "credentials-from-config-map"
|
||||
},
|
||||
credFileStore: new(credmock.FileStore),
|
||||
expected: map[string]string{
|
||||
"credFile": "credentials-from-config-map",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
getAzureCredentials = tc.getAzureCredentials
|
||||
getS3Credentials = tc.getS3Credentials
|
||||
getGCPCredentials = tc.getGCPCredentials
|
||||
|
||||
var fileStore velerocredentials.FileStore
|
||||
if tc.credFileStore != nil {
|
||||
tc.credFileStore.On("Path", mock.Anything, mock.Anything).Return(tc.credStorePath, tc.credStoreError)
|
||||
fileStore = tc.credFileStore
|
||||
}
|
||||
|
||||
actual, err := getStorageCredentials(&tc.backupLocation, fileStore)
|
||||
|
||||
require.Equal(t, tc.expected, actual)
|
||||
|
||||
if tc.expectedErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStorageVariables(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
backupLocation velerov1api.BackupStorageLocation
|
||||
repoName string
|
||||
getS3BucketRegion func(string) (string, error)
|
||||
getAzureStorageDomain func(map[string]string) string
|
||||
expected map[string]string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "invalid provider",
|
||||
backupLocation: velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "invalid-provider",
|
||||
},
|
||||
},
|
||||
expected: map[string]string{},
|
||||
expectedErr: "invalid storage provider",
|
||||
},
|
||||
{
|
||||
name: "aws, ObjectStorage section not exists in BSL, s3Url exist",
|
||||
backupLocation: velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "velero.io/aws",
|
||||
Config: map[string]string{
|
||||
"bucket": "fake-bucket",
|
||||
"prefix": "fake-prefix",
|
||||
"region": "fake-region/",
|
||||
"s3Url": "fake-url",
|
||||
"insecureSkipTLSVerify": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"bucket": "fake-bucket",
|
||||
"prefix": "fake-prefix/unified-repo/",
|
||||
"region": "fake-region",
|
||||
"fspath": "",
|
||||
"endpoint": "fake-url",
|
||||
"skipTLSVerify": "true",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aws, ObjectStorage section not exists in BSL, s3Url not exist",
|
||||
backupLocation: velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "velero.io/aws",
|
||||
Config: map[string]string{
|
||||
"bucket": "fake-bucket",
|
||||
"prefix": "fake-prefix",
|
||||
"insecureSkipTLSVerify": "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
getS3BucketRegion: func(bucket string) (string, error) {
|
||||
return "region from bucket: " + bucket, nil
|
||||
},
|
||||
expected: map[string]string{
|
||||
"bucket": "fake-bucket",
|
||||
"prefix": "fake-prefix/unified-repo/",
|
||||
"region": "region from bucket: fake-bucket",
|
||||
"fspath": "",
|
||||
"endpoint": "s3-region from bucket: fake-bucket.amazonaws.com",
|
||||
"skipTLSVerify": "false",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aws, ObjectStorage section not exists in BSL, s3Url not exist, get region fail",
|
||||
backupLocation: velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "velero.io/aws",
|
||||
Config: map[string]string{},
|
||||
},
|
||||
},
|
||||
getS3BucketRegion: func(bucket string) (string, error) {
|
||||
return "", errors.New("fake error")
|
||||
},
|
||||
expected: map[string]string{},
|
||||
expectedErr: "error get s3 bucket region: fake error",
|
||||
},
|
||||
{
|
||||
name: "aws, ObjectStorage section exists in BSL, s3Url exist",
|
||||
backupLocation: velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "velero.io/aws",
|
||||
Config: map[string]string{
|
||||
"bucket": "fake-bucket-config",
|
||||
"prefix": "fake-prefix-config",
|
||||
"region": "fake-region",
|
||||
"s3Url": "fake-url",
|
||||
"insecureSkipTLSVerify": "false",
|
||||
},
|
||||
StorageType: velerov1api.StorageType{
|
||||
ObjectStorage: &velerov1api.ObjectStorageLocation{
|
||||
Bucket: "fake-bucket-object-store",
|
||||
Prefix: "fake-prefix-object-store",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getS3BucketRegion: func(bucket string) (string, error) {
|
||||
return "region from bucket: " + bucket, nil
|
||||
},
|
||||
expected: map[string]string{
|
||||
"bucket": "fake-bucket-object-store",
|
||||
"prefix": "fake-prefix-object-store/unified-repo/",
|
||||
"region": "fake-region",
|
||||
"fspath": "",
|
||||
"endpoint": "fake-url",
|
||||
"skipTLSVerify": "false",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "azure, ObjectStorage section exists in BSL",
|
||||
backupLocation: velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "velero.io/azure",
|
||||
Config: map[string]string{
|
||||
"bucket": "fake-bucket-config",
|
||||
"prefix": "fake-prefix-config",
|
||||
"region": "fake-region",
|
||||
"fspath": "",
|
||||
"storageDomain": "fake-domain",
|
||||
},
|
||||
StorageType: velerov1api.StorageType{
|
||||
ObjectStorage: &velerov1api.ObjectStorageLocation{
|
||||
Bucket: "fake-bucket-object-store",
|
||||
Prefix: "fake-prefix-object-store",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getAzureStorageDomain: func(config map[string]string) string {
|
||||
return config["storageDomain"]
|
||||
},
|
||||
expected: map[string]string{
|
||||
"bucket": "fake-bucket-object-store",
|
||||
"prefix": "fake-prefix-object-store/unified-repo/",
|
||||
"region": "fake-region",
|
||||
"fspath": "",
|
||||
"storageDomain": "fake-domain",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "azure, ObjectStorage section not exists in BSL, repo name exists",
|
||||
backupLocation: velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "velero.io/azure",
|
||||
Config: map[string]string{
|
||||
"bucket": "fake-bucket",
|
||||
"prefix": "fake-prefix",
|
||||
"region": "fake-region",
|
||||
"fspath": "",
|
||||
"storageDomain": "fake-domain",
|
||||
},
|
||||
},
|
||||
},
|
||||
repoName: "//fake-name//",
|
||||
getAzureStorageDomain: func(config map[string]string) string {
|
||||
return config["storageDomain"]
|
||||
},
|
||||
expected: map[string]string{
|
||||
"bucket": "fake-bucket",
|
||||
"prefix": "fake-prefix/unified-repo/fake-name/",
|
||||
"region": "fake-region",
|
||||
"fspath": "",
|
||||
"storageDomain": "fake-domain",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fs",
|
||||
backupLocation: velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "velero.io/fs",
|
||||
Config: map[string]string{
|
||||
"fspath": "fake-path",
|
||||
"prefix": "fake-prefix",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"fspath": "fake-path",
|
||||
"bucket": "",
|
||||
"prefix": "fake-prefix/unified-repo/",
|
||||
"region": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
getS3BucketRegion = tc.getS3BucketRegion
|
||||
getAzureStorageDomain = tc.getAzureStorageDomain
|
||||
|
||||
actual, err := getStorageVariables(&tc.backupLocation, tc.repoName)
|
||||
|
||||
require.Equal(t, tc.expected, actual)
|
||||
|
||||
if tc.expectedErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRepoPassword(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
getter *credmock.SecretStore
|
||||
credStoreReturn string
|
||||
credStoreError error
|
||||
cached string
|
||||
expected string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "invalid secret interface",
|
||||
expectedErr: "invalid credentials interface",
|
||||
},
|
||||
{
|
||||
name: "error from secret interface",
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreError: errors.New("fake error"),
|
||||
expectedErr: "error to get password buffer: fake error",
|
||||
},
|
||||
{
|
||||
name: "secret with whitespace",
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: " fake-passwor d ",
|
||||
expected: "fake-passwor d",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var secretStore velerocredentials.SecretStore
|
||||
if tc.getter != nil {
|
||||
tc.getter.On("Get", mock.Anything, mock.Anything).Return(tc.credStoreReturn, tc.credStoreError)
|
||||
secretStore = tc.getter
|
||||
}
|
||||
|
||||
urp := unifiedRepoProvider{
|
||||
credentialGetter: velerocredentials.CredentialGetter{
|
||||
FromSecret: secretStore,
|
||||
},
|
||||
}
|
||||
|
||||
password, err := getRepoPassword(urp.credentialGetter.FromSecret, RepoParam{})
|
||||
|
||||
require.Equal(t, tc.expected, password)
|
||||
|
||||
if tc.expectedErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRepoOption(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
funcTable localFuncTable
|
||||
getRepoPassword func(velerocredentials.SecretStore, RepoParam) (string, error)
|
||||
getStorageCredentials func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error)
|
||||
getStorageVariables func(*velerov1api.BackupStorageLocation, string) (map[string]string, error)
|
||||
expected udmrepo.RepoOptions
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "get repo password fail",
|
||||
funcTable: localFuncTable{
|
||||
getRepoPassword: func(velerocredentials.SecretStore, RepoParam) (string, error) {
|
||||
return "", errors.New("fake-error-1")
|
||||
},
|
||||
},
|
||||
expectedErr: "error to get repo password: fake-error-1",
|
||||
},
|
||||
{
|
||||
name: "get storage variable fail",
|
||||
funcTable: localFuncTable{
|
||||
getRepoPassword: func(velerocredentials.SecretStore, RepoParam) (string, error) {
|
||||
return "fake-password", nil
|
||||
},
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string) (map[string]string, error) {
|
||||
return map[string]string{}, errors.New("fake-error-2")
|
||||
},
|
||||
},
|
||||
expectedErr: "error to get storage variables: fake-error-2",
|
||||
},
|
||||
{
|
||||
name: "get storage credentials fail",
|
||||
funcTable: localFuncTable{
|
||||
getRepoPassword: func(velerocredentials.SecretStore, RepoParam) (string, error) {
|
||||
return "fake-password", nil
|
||||
},
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, errors.New("fake-error-3")
|
||||
},
|
||||
},
|
||||
expectedErr: "error to get repo credentials: fake-error-3",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
funcTable = tc.funcTable
|
||||
urp := unifiedRepoProvider{}
|
||||
|
||||
password, err := urp.getRepoOption(RepoParam{})
|
||||
|
||||
require.Equal(t, tc.expected, password)
|
||||
|
||||
if tc.expectedErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
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 udmrepo
|
||||
|
||||
const (
|
||||
StorageTypeS3 = "s3"
|
||||
StorageTypeAzure = "azure"
|
||||
StorageTypeFs = "filesystem"
|
||||
StorageTypeGcs = "gcs"
|
||||
|
||||
GenOptionMaintainMode = "mode"
|
||||
GenOptionMaintainFull = "full"
|
||||
GenOptionMaintainQuick = "quick"
|
||||
|
||||
StoreOptionS3KeyId = "accessKeyID"
|
||||
StoreOptionS3Provider = "providerName"
|
||||
StoreOptionS3SecretKey = "secretAccessKey"
|
||||
StoreOptionS3Token = "sessionToken"
|
||||
StoreOptionS3Endpoint = "endpoint"
|
||||
StoreOptionS3DisableTls = "doNotUseTLS"
|
||||
StoreOptionS3DisableTlsVerify = "skipTLSVerify"
|
||||
|
||||
StoreOptionAzureKey = "storageKey"
|
||||
StoreOptionAzureDomain = "storageDomain"
|
||||
StoreOptionAzureStorageAccount = "storageAccount"
|
||||
StoreOptionAzureToken = "sasToken"
|
||||
|
||||
StoreOptionFsPath = "fspath"
|
||||
|
||||
StoreOptionGcsReadonly = "readonly"
|
||||
|
||||
StoreOptionOssBucket = "bucket"
|
||||
StoreOptionOssRegion = "region"
|
||||
|
||||
StoreOptionCredentialFile = "credFile"
|
||||
StoreOptionPrefix = "prefix"
|
||||
StoreOptionPrefixName = "unified-repo"
|
||||
|
||||
ThrottleOptionReadOps = "readOPS"
|
||||
ThrottleOptionWriteOps = "writeOPS"
|
||||
ThrottleOptionListOps = "listOPS"
|
||||
ThrottleOptionUploadBytes = "uploadBytes"
|
||||
ThrottleOptionDownloadBytes = "downloadBytes"
|
||||
)
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
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 udmrepo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ID string
|
||||
|
||||
// ManifestEntryMetadata is the metadata describing one manifest data
|
||||
type ManifestEntryMetadata struct {
|
||||
ID ID // The ID of the manifest data
|
||||
Length int32 // The data size of the manifest data
|
||||
Labels map[string]string // Labels saved together with the manifest data
|
||||
ModTime time.Time // Modified time of the manifest data
|
||||
}
|
||||
|
||||
type RepoManifest struct {
|
||||
Payload interface{} // The user data of manifest
|
||||
Metadata *ManifestEntryMetadata // The metadata data of manifest
|
||||
}
|
||||
|
||||
type ManifestFilter struct {
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
const (
|
||||
// Below consts descrbe the data type of one object.
|
||||
// Metadata: This type describes how the data is organized.
|
||||
// For a file system backup, the Metadata describes a Dir or File.
|
||||
// For a block backup, the Metadata describes a Disk and its incremental link.
|
||||
ObjectDataTypeUnknown int = 0
|
||||
ObjectDataTypeMetadata int = 1
|
||||
ObjectDataTypeData int = 2
|
||||
|
||||
// Below consts defines the access mode when creating an object for write
|
||||
ObjectDataAccessModeUnknown int = 0
|
||||
ObjectDataAccessModeFile int = 1
|
||||
ObjectDataAccessModeBlock int = 2
|
||||
|
||||
ObjectDataBackupModeUnknown int = 0
|
||||
ObjectDataBackupModeFull int = 1
|
||||
ObjectDataBackupModeInc int = 2
|
||||
)
|
||||
|
||||
// ObjectWriteOptions defines the options when creating an object for write
|
||||
type ObjectWriteOptions struct {
|
||||
FullPath string // Full logical path of the object
|
||||
DataType int // OBJECT_DATA_TYPE_*
|
||||
Description string // A description of the object, could be empty
|
||||
Prefix ID // A prefix of the name used to save the object
|
||||
AccessMode int // OBJECT_DATA_ACCESS_*
|
||||
BackupMode int // OBJECT_DATA_BACKUP_*
|
||||
}
|
||||
|
||||
// OwnershipOptions is used to add some access control to the unified repository.
|
||||
// For example, some privileged operations of the unified repository can be done by the
|
||||
// repository owner only; the data of a backup may be manipulated by the backup owner
|
||||
// who created it only. It is optional for a backup repository to support this ownership control.
|
||||
type OwnershipOptions struct {
|
||||
Username string
|
||||
DomainName string
|
||||
FullQualified string
|
||||
}
|
||||
|
||||
type RepoOptions struct {
|
||||
// A repository specific string to identify a backup storage, i.e., "s3", "filesystem"
|
||||
StorageType string
|
||||
// Backup repository password, if any
|
||||
RepoPassword string
|
||||
// A custom path to save the repository's configuration, if any
|
||||
ConfigFilePath string
|
||||
// The ownership for the current repository operation
|
||||
Ownership OwnershipOptions
|
||||
// Other repository specific options
|
||||
GeneralOptions map[string]string
|
||||
// Storage specific options
|
||||
StorageOptions map[string]string
|
||||
|
||||
// Description of the backup repository
|
||||
Description string
|
||||
}
|
||||
|
||||
// BackupRepoService is used to initialize, open or maintain a backup repository
|
||||
type BackupRepoService interface {
|
||||
// Create a backup repository or connect to an existing backup repository.
|
||||
// repoOption: option to the backup repository and the underlying backup storage.
|
||||
// createNew: indicates whether to create a new or connect to an existing backup repository.
|
||||
Init(ctx context.Context, repoOption RepoOptions, createNew bool) error
|
||||
|
||||
// Open an backup repository that has been created/connected.
|
||||
// repoOption: options to open the backup repository and the underlying storage.
|
||||
Open(ctx context.Context, repoOption RepoOptions) (BackupRepo, error)
|
||||
|
||||
// Periodically called to maintain the backup repository to eliminate redundant data and improve performance.
|
||||
// repoOption: options to maintain the backup repository.
|
||||
Maintain(ctx context.Context, repoOption RepoOptions) error
|
||||
}
|
||||
|
||||
// BackupRepo provides the access to the backup repository
|
||||
type BackupRepo interface {
|
||||
// Open an existing object for read.
|
||||
// id: the object's unified identifier.
|
||||
OpenObject(ctx context.Context, id ID) (ObjectReader, error)
|
||||
|
||||
// Get a manifest data.
|
||||
GetManifest(ctx context.Context, id ID, mani *RepoManifest) error
|
||||
|
||||
// Get one or more manifest data that match the given labels
|
||||
FindManifests(ctx context.Context, filter ManifestFilter) ([]*ManifestEntryMetadata, error)
|
||||
|
||||
// Create a new object and return the object's writer interface.
|
||||
// return: A unified identifier of the object on success.
|
||||
NewObjectWriter(ctx context.Context, opt ObjectWriteOptions) ObjectWriter
|
||||
|
||||
// Save a manifest object
|
||||
PutManifest(ctx context.Context, mani RepoManifest) (ID, error)
|
||||
|
||||
// Delete a manifest object
|
||||
DeleteManifest(ctx context.Context, id ID) error
|
||||
|
||||
// Flush all the backup repository data
|
||||
Flush(ctx context.Context) error
|
||||
|
||||
// Get the local time of the backup repository. It may be different from the time of the caller
|
||||
Time() time.Time
|
||||
|
||||
// Close the backup repository
|
||||
Close(ctx context.Context) error
|
||||
}
|
||||
|
||||
type ObjectReader interface {
|
||||
io.ReadCloser
|
||||
io.Seeker
|
||||
|
||||
// Length returns the logical size of the object
|
||||
Length() int64
|
||||
}
|
||||
|
||||
type ObjectWriter interface {
|
||||
io.WriteCloser
|
||||
|
||||
// For some cases, i.e. block incremental, the object is not written sequentially
|
||||
io.Seeker
|
||||
|
||||
// Periodically called to preserve the state of data written to the repo so far.
|
||||
// Return a unified identifier that represent the current state.
|
||||
// An empty ID could be returned on success if the backup repository doesn't support this.
|
||||
Checkpoint() (ID, error)
|
||||
|
||||
// Wait for the completion of the object write.
|
||||
// Result returns the object's unified identifier after the write completes.
|
||||
Result() (ID, error)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
Copyright the Velero contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
|
||||
const (
|
||||
// AWS specific environment variable
|
||||
awsProfileEnvVar = "AWS_PROFILE"
|
||||
awsProfileKey = "profile"
|
||||
awsCredentialsFileEnvVar = "AWS_SHARED_CREDENTIALS_FILE"
|
||||
)
|
||||
|
||||
// getS3ResticEnvVars gets the environment variables that restic
|
||||
// relies on (AWS_PROFILE) based on info in the provided object
|
||||
// storage location config map.
|
||||
func getS3ResticEnvVars(config map[string]string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
if credentialsFile, ok := config[credentialsFileKey]; ok {
|
||||
result[awsCredentialsFileEnvVar] = credentialsFile
|
||||
}
|
||||
|
||||
if profile, ok := config[awsProfileKey]; ok {
|
||||
result[awsProfileEnvVar] = profile
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
+9
-184
@@ -20,11 +20,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -32,6 +30,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/label"
|
||||
repoconfig "github.com/vmware-tanzu/velero/pkg/repository/config"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
)
|
||||
|
||||
@@ -50,182 +49,8 @@ const (
|
||||
// DefaultVolumesToRestic specifies whether restic should be used, by default, to
|
||||
// take backup of all pod volumes.
|
||||
DefaultVolumesToRestic = false
|
||||
|
||||
// PVCNameAnnotation is the key for the annotation added to
|
||||
// pod volume backups when they're for a PVC.
|
||||
PVCNameAnnotation = "velero.io/pvc-name"
|
||||
|
||||
// VolumesToBackupAnnotation is the annotation on a pod whose mounted volumes
|
||||
// need to be backed up using restic.
|
||||
VolumesToBackupAnnotation = "backup.velero.io/backup-volumes"
|
||||
|
||||
// VolumesToExcludeAnnotation is the annotation on a pod whose mounted volumes
|
||||
// should be excluded from restic backup.
|
||||
VolumesToExcludeAnnotation = "backup.velero.io/backup-volumes-excludes"
|
||||
|
||||
// credentialsFileKey is the key within a BSL config that is checked to see if
|
||||
// the BSL is using its own credentials, rather than those in the environment
|
||||
credentialsFileKey = "credentialsFile"
|
||||
|
||||
// Deprecated.
|
||||
//
|
||||
// TODO(2.0): remove
|
||||
podAnnotationPrefix = "snapshot.velero.io/"
|
||||
)
|
||||
|
||||
// getPodSnapshotAnnotations returns a map, of volume name -> snapshot id,
|
||||
// of all restic snapshots for this pod.
|
||||
// TODO(2.0) to remove
|
||||
// Deprecated: we will stop using pod annotations to record restic snapshot IDs after they're taken,
|
||||
// therefore we won't need to check if these annotations exist.
|
||||
func getPodSnapshotAnnotations(obj metav1.Object) map[string]string {
|
||||
var res map[string]string
|
||||
|
||||
insertSafe := func(k, v string) {
|
||||
if res == nil {
|
||||
res = make(map[string]string)
|
||||
}
|
||||
res[k] = v
|
||||
}
|
||||
|
||||
for k, v := range obj.GetAnnotations() {
|
||||
if strings.HasPrefix(k, podAnnotationPrefix) {
|
||||
insertSafe(k[len(podAnnotationPrefix):], v)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func isPVBMatchPod(pvb *velerov1api.PodVolumeBackup, podName string, namespace string) bool {
|
||||
return podName == pvb.Spec.Pod.Name && namespace == pvb.Spec.Pod.Namespace
|
||||
}
|
||||
|
||||
// volumeHasNonRestorableSource checks if the given volume exists in the list of podVolumes
|
||||
// and returns true if the volume's source is not restorable. This is true for volumes with
|
||||
// a Projected or DownwardAPI source.
|
||||
func volumeHasNonRestorableSource(volumeName string, podVolumes []corev1api.Volume) bool {
|
||||
var volume corev1api.Volume
|
||||
for _, v := range podVolumes {
|
||||
if v.Name == volumeName {
|
||||
volume = v
|
||||
break
|
||||
}
|
||||
}
|
||||
return volume.Projected != nil || volume.DownwardAPI != nil
|
||||
}
|
||||
|
||||
// GetVolumeBackupsForPod returns a map, of volume name -> snapshot id,
|
||||
// of the PodVolumeBackups that exist for the provided pod.
|
||||
func GetVolumeBackupsForPod(podVolumeBackups []*velerov1api.PodVolumeBackup, pod *corev1api.Pod, sourcePodNs string) map[string]string {
|
||||
volumes := make(map[string]string)
|
||||
|
||||
for _, pvb := range podVolumeBackups {
|
||||
if !isPVBMatchPod(pvb, pod.GetName(), sourcePodNs) {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip PVBs without a snapshot ID since there's nothing
|
||||
// to restore (they could be failed, or for empty volumes).
|
||||
if pvb.Status.SnapshotID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the volume came from a projected or DownwardAPI source, skip its restore.
|
||||
// This allows backups affected by https://github.com/vmware-tanzu/velero/issues/3863
|
||||
// or https://github.com/vmware-tanzu/velero/issues/4053 to be restored successfully.
|
||||
if volumeHasNonRestorableSource(pvb.Spec.Volume, pod.Spec.Volumes) {
|
||||
continue
|
||||
}
|
||||
|
||||
volumes[pvb.Spec.Volume] = pvb.Status.SnapshotID
|
||||
}
|
||||
|
||||
if len(volumes) > 0 {
|
||||
return volumes
|
||||
}
|
||||
|
||||
return getPodSnapshotAnnotations(pod)
|
||||
}
|
||||
|
||||
// GetVolumesToBackup returns a list of volume names to backup for
|
||||
// the provided pod.
|
||||
// Deprecated: Use GetPodVolumesUsingRestic instead.
|
||||
func GetVolumesToBackup(obj metav1.Object) []string {
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
backupsValue := annotations[VolumesToBackupAnnotation]
|
||||
if backupsValue == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(backupsValue, ",")
|
||||
}
|
||||
|
||||
func getVolumesToExclude(obj metav1.Object) []string {
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(annotations[VolumesToExcludeAnnotation], ",")
|
||||
}
|
||||
|
||||
func contains(list []string, k string) bool {
|
||||
for _, i := range list {
|
||||
if i == k {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetPodVolumesUsingRestic returns a list of volume names to backup for the provided pod.
|
||||
func GetPodVolumesUsingRestic(pod *corev1api.Pod, defaultVolumesToRestic bool) []string {
|
||||
if !defaultVolumesToRestic {
|
||||
return GetVolumesToBackup(pod)
|
||||
}
|
||||
|
||||
volsToExclude := getVolumesToExclude(pod)
|
||||
podVolumes := []string{}
|
||||
for _, pv := range pod.Spec.Volumes {
|
||||
// cannot backup hostpath volumes as they are not mounted into /var/lib/kubelet/pods
|
||||
// and therefore not accessible to the restic daemon set.
|
||||
if pv.HostPath != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup volumes mounting secrets. Secrets will be backed up separately.
|
||||
if pv.Secret != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup volumes mounting config maps. Config maps will be backed up separately.
|
||||
if pv.ConfigMap != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup volumes mounted as projected volumes, all data in those come from kube state.
|
||||
if pv.Projected != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup DownwardAPI volumes, all data in those come from kube state.
|
||||
if pv.DownwardAPI != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup volumes that are included in the exclude list.
|
||||
if contains(volsToExclude, pv.Name) {
|
||||
continue
|
||||
}
|
||||
// don't include volumes that mount the default service account token.
|
||||
if strings.HasPrefix(pv.Name, "default-token") {
|
||||
continue
|
||||
}
|
||||
podVolumes = append(podVolumes, pv.Name)
|
||||
}
|
||||
return podVolumes
|
||||
}
|
||||
|
||||
// SnapshotIdentifier uniquely identifies a restic snapshot
|
||||
// taken by Velero.
|
||||
type SnapshotIdentifier struct {
|
||||
@@ -322,24 +147,24 @@ func CmdEnv(backupLocation *velerov1api.BackupStorageLocation, credentialFileSto
|
||||
if err != nil {
|
||||
return []string{}, errors.WithStack(err)
|
||||
}
|
||||
config[credentialsFileKey] = credsFile
|
||||
config[repoconfig.CredentialsFileKey] = credsFile
|
||||
}
|
||||
|
||||
backendType := getBackendType(backupLocation.Spec.Provider)
|
||||
backendType := repoconfig.GetBackendType(backupLocation.Spec.Provider)
|
||||
|
||||
switch backendType {
|
||||
case AWSBackend:
|
||||
customEnv, err = getS3ResticEnvVars(config)
|
||||
case repoconfig.AWSBackend:
|
||||
customEnv, err = repoconfig.GetS3ResticEnvVars(config)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
case AzureBackend:
|
||||
customEnv, err = getAzureResticEnvVars(config)
|
||||
case repoconfig.AzureBackend:
|
||||
customEnv, err = repoconfig.GetAzureResticEnvVars(config)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
case GCPBackend:
|
||||
customEnv, err = getGCPResticEnvVars(config)
|
||||
case repoconfig.GCPBackend:
|
||||
customEnv, err = repoconfig.GetGCPResticEnvVars(config)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
@@ -28,212 +28,9 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
)
|
||||
|
||||
func TestGetVolumeBackupsForPod(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
podVolumeBackups []*velerov1api.PodVolumeBackup
|
||||
podVolumes []corev1api.Volume
|
||||
podAnnotations map[string]string
|
||||
podName string
|
||||
sourcePodNs string
|
||||
expected map[string]string
|
||||
}{
|
||||
{
|
||||
name: "nil annotations results in no volume backups returned",
|
||||
podAnnotations: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "empty annotations results in no volume backups returned",
|
||||
podAnnotations: make(map[string]string),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "pod annotations with no snapshot annotation prefix results in no volume backups returned",
|
||||
podAnnotations: map[string]string{"foo": "bar"},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "pod annotation with only snapshot annotation prefix, results in volume backup with empty volume key",
|
||||
podAnnotations: map[string]string{podAnnotationPrefix: "snapshotID"},
|
||||
expected: map[string]string{"": "snapshotID"},
|
||||
},
|
||||
{
|
||||
name: "pod annotation with snapshot annotation prefix results in volume backup with volume name and snapshot ID",
|
||||
podAnnotations: map[string]string{podAnnotationPrefix + "volume": "snapshotID"},
|
||||
expected: map[string]string{"volume": "snapshotID"},
|
||||
},
|
||||
{
|
||||
name: "only pod annotations with snapshot annotation prefix are considered",
|
||||
podAnnotations: map[string]string{"x": "y", podAnnotationPrefix + "volume1": "snapshot1", podAnnotationPrefix + "volume2": "snapshot2"},
|
||||
expected: map[string]string{"volume1": "snapshot1", "volume2": "snapshot2"},
|
||||
},
|
||||
{
|
||||
name: "pod annotations are not considered if PVBs are provided",
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(),
|
||||
},
|
||||
podName: "TestPod",
|
||||
sourcePodNs: "TestNS",
|
||||
podAnnotations: map[string]string{"x": "y", podAnnotationPrefix + "foo": "bar", podAnnotationPrefix + "abc": "123"},
|
||||
expected: map[string]string{"pvbtest1-foo": "snapshot1", "pvbtest2-abc": "snapshot2"},
|
||||
},
|
||||
{
|
||||
name: "volume backups are returned even if no pod annotations are present",
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(),
|
||||
},
|
||||
podName: "TestPod",
|
||||
sourcePodNs: "TestNS",
|
||||
expected: map[string]string{"pvbtest1-foo": "snapshot1", "pvbtest2-abc": "snapshot2"},
|
||||
},
|
||||
{
|
||||
name: "only volumes from PVBs with snapshot IDs are returned",
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-3").PodName("TestPod").PodNamespace("TestNS").Volume("pvbtest3-foo").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-4").PodName("TestPod").PodNamespace("TestNS").Volume("pvbtest4-abc").Result(),
|
||||
},
|
||||
podName: "TestPod",
|
||||
sourcePodNs: "TestNS",
|
||||
expected: map[string]string{"pvbtest1-foo": "snapshot1", "pvbtest2-abc": "snapshot2"},
|
||||
},
|
||||
{
|
||||
name: "only volumes from PVBs for the given pod are returned",
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-3").PodName("TestAnotherPod").SnapshotID("snapshot3").Volume("pvbtest3-xyz").Result(),
|
||||
},
|
||||
podName: "TestPod",
|
||||
sourcePodNs: "TestNS",
|
||||
expected: map[string]string{"pvbtest1-foo": "snapshot1", "pvbtest2-abc": "snapshot2"},
|
||||
},
|
||||
{
|
||||
name: "only volumes from PVBs which match the pod name and source pod namespace are returned",
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestAnotherPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-3").PodName("TestPod").PodNamespace("TestAnotherNS").SnapshotID("snapshot3").Volume("pvbtest3-xyz").Result(),
|
||||
},
|
||||
podName: "TestPod",
|
||||
sourcePodNs: "TestNS",
|
||||
expected: map[string]string{"pvbtest1-foo": "snapshot1"},
|
||||
},
|
||||
{
|
||||
name: "volumes from PVBs that correspond to a pod volume from a projected source are not returned",
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvb-non-projected").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvb-projected").Result(),
|
||||
},
|
||||
podVolumes: []corev1api.Volume{
|
||||
{
|
||||
Name: "pvb-non-projected",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "pvb-projected",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
Projected: &corev1api.ProjectedVolumeSource{},
|
||||
},
|
||||
},
|
||||
},
|
||||
podName: "TestPod",
|
||||
sourcePodNs: "TestNS",
|
||||
expected: map[string]string{"pvb-non-projected": "snapshot1"},
|
||||
},
|
||||
{
|
||||
name: "volumes from PVBs that correspond to a pod volume from a DownwardAPI source are not returned",
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvb-non-downwardapi").Result(),
|
||||
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvb-downwardapi").Result(),
|
||||
},
|
||||
podVolumes: []corev1api.Volume{
|
||||
{
|
||||
Name: "pvb-non-downwardapi",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "pvb-downwardapi",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
DownwardAPI: &corev1api.DownwardAPIVolumeSource{},
|
||||
},
|
||||
},
|
||||
},
|
||||
podName: "TestPod",
|
||||
sourcePodNs: "TestNS",
|
||||
expected: map[string]string{"pvb-non-downwardapi": "snapshot1"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
pod := &corev1api.Pod{}
|
||||
pod.Annotations = test.podAnnotations
|
||||
pod.Name = test.podName
|
||||
pod.Spec.Volumes = test.podVolumes
|
||||
|
||||
res := GetVolumeBackupsForPod(test.podVolumeBackups, pod, test.sourcePodNs)
|
||||
assert.Equal(t, test.expected, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVolumesToBackup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
annotations map[string]string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "nil annotations",
|
||||
annotations: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "no volumes to backup",
|
||||
annotations: map[string]string{"foo": "bar"},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "one volume to backup",
|
||||
annotations: map[string]string{"foo": "bar", VolumesToBackupAnnotation: "volume-1"},
|
||||
expected: []string{"volume-1"},
|
||||
},
|
||||
{
|
||||
name: "multiple volumes to backup",
|
||||
annotations: map[string]string{"foo": "bar", VolumesToBackupAnnotation: "volume-1,volume-2,volume-3"},
|
||||
expected: []string{"volume-1", "volume-2", "volume-3"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
pod := &corev1api.Pod{}
|
||||
pod.Annotations = test.annotations
|
||||
|
||||
res := GetVolumesToBackup(pod)
|
||||
|
||||
// sort to ensure good compare of slices
|
||||
sort.Strings(test.expected)
|
||||
sort.Strings(res)
|
||||
|
||||
assert.Equal(t, test.expected, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSnapshotsInBackup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -419,410 +216,3 @@ func TestTempCACertFile(t *testing.T) {
|
||||
|
||||
os.Remove(fileName)
|
||||
}
|
||||
|
||||
func TestGetPodVolumesUsingRestic(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
pod *corev1api.Pod
|
||||
expected []string
|
||||
defaultVolumesToRestic bool
|
||||
}{
|
||||
{
|
||||
name: "should get PVs from VolumesToBackupAnnotation when defaultVolumesToRestic is false",
|
||||
defaultVolumesToRestic: false,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToBackupAnnotation: "resticPV1,resticPV2,resticPV3",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
{
|
||||
name: "should get all pod volumes when defaultVolumesToRestic is true and no PVs are excluded",
|
||||
defaultVolumesToRestic: true,
|
||||
pod: &corev1api.Pod{
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// Restic Volumes
|
||||
{Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
{
|
||||
name: "should get all pod volumes except ones excluded when defaultVolumesToRestic is true",
|
||||
defaultVolumesToRestic: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// Restic Volumes
|
||||
{Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"},
|
||||
/// Excluded from restic through annotation
|
||||
{Name: "nonResticPV1"}, {Name: "nonResticPV2"}, {Name: "nonResticPV3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
{
|
||||
name: "should exclude default service account token from restic backup",
|
||||
defaultVolumesToRestic: true,
|
||||
pod: &corev1api.Pod{
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// Restic Volumes
|
||||
{Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"},
|
||||
/// Excluded from restic because colume mounting default service account token
|
||||
{Name: "default-token-5xq45"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
{
|
||||
name: "should exclude host path volumes from restic backups",
|
||||
defaultVolumesToRestic: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// Restic Volumes
|
||||
{Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"},
|
||||
/// Excluded from restic through annotation
|
||||
{Name: "nonResticPV1"}, {Name: "nonResticPV2"}, {Name: "nonResticPV3"},
|
||||
// Excluded from restic because hostpath
|
||||
{Name: "hostPath1", VolumeSource: corev1api.VolumeSource{HostPath: &corev1api.HostPathVolumeSource{Path: "/hostpathVol"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
{
|
||||
name: "should exclude volumes mounting secrets",
|
||||
defaultVolumesToRestic: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// Restic Volumes
|
||||
{Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"},
|
||||
/// Excluded from restic through annotation
|
||||
{Name: "nonResticPV1"}, {Name: "nonResticPV2"}, {Name: "nonResticPV3"},
|
||||
// Excluded from restic because hostpath
|
||||
{Name: "superSecret", VolumeSource: corev1api.VolumeSource{Secret: &corev1api.SecretVolumeSource{SecretName: "super-secret"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
{
|
||||
name: "should exclude volumes mounting config maps",
|
||||
defaultVolumesToRestic: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// Restic Volumes
|
||||
{Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"},
|
||||
/// Excluded from restic through annotation
|
||||
{Name: "nonResticPV1"}, {Name: "nonResticPV2"}, {Name: "nonResticPV3"},
|
||||
// Excluded from restic because hostpath
|
||||
{Name: "appCOnfig", VolumeSource: corev1api.VolumeSource{ConfigMap: &corev1api.ConfigMapVolumeSource{LocalObjectReference: corev1api.LocalObjectReference{Name: "app-config"}}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
{
|
||||
name: "should exclude projected volumes",
|
||||
defaultVolumesToRestic: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
{Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"},
|
||||
{
|
||||
Name: "projected",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
Projected: &corev1api.ProjectedVolumeSource{
|
||||
Sources: []corev1api.VolumeProjection{{
|
||||
Secret: &corev1api.SecretProjection{
|
||||
LocalObjectReference: corev1api.LocalObjectReference{},
|
||||
Items: nil,
|
||||
Optional: nil,
|
||||
},
|
||||
DownwardAPI: nil,
|
||||
ConfigMap: nil,
|
||||
ServiceAccountToken: nil,
|
||||
}},
|
||||
DefaultMode: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
{
|
||||
name: "should exclude DownwardAPI volumes",
|
||||
defaultVolumesToRestic: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
{Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"},
|
||||
{
|
||||
Name: "downwardAPI",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
DownwardAPI: &corev1api.DownwardAPIVolumeSource{
|
||||
Items: []corev1api.DownwardAPIVolumeFile{
|
||||
{
|
||||
Path: "labels",
|
||||
FieldRef: &corev1api.ObjectFieldSelector{
|
||||
APIVersion: "v1",
|
||||
FieldPath: "metadata.labels",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"resticPV1", "resticPV2", "resticPV3"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := GetPodVolumesUsingRestic(tc.pod, tc.defaultVolumesToRestic)
|
||||
|
||||
sort.Strings(tc.expected)
|
||||
sort.Strings(actual)
|
||||
assert.Equal(t, tc.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPVBMatchPod(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
pvb velerov1api.PodVolumeBackup
|
||||
podName string
|
||||
sourcePodNs string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "should match PVB and pod",
|
||||
pvb: velerov1api.PodVolumeBackup{
|
||||
Spec: velerov1api.PodVolumeBackupSpec{
|
||||
Pod: corev1api.ObjectReference{
|
||||
Name: "matching-pod",
|
||||
Namespace: "matching-namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
podName: "matching-pod",
|
||||
sourcePodNs: "matching-namespace",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "should not match PVB and pod, pod name mismatch",
|
||||
pvb: velerov1api.PodVolumeBackup{
|
||||
Spec: velerov1api.PodVolumeBackupSpec{
|
||||
Pod: corev1api.ObjectReference{
|
||||
Name: "matching-pod",
|
||||
Namespace: "matching-namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
podName: "not-matching-pod",
|
||||
sourcePodNs: "matching-namespace",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "should not match PVB and pod, pod namespace mismatch",
|
||||
pvb: velerov1api.PodVolumeBackup{
|
||||
Spec: velerov1api.PodVolumeBackupSpec{
|
||||
Pod: corev1api.ObjectReference{
|
||||
Name: "matching-pod",
|
||||
Namespace: "matching-namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
podName: "matching-pod",
|
||||
sourcePodNs: "not-matching-namespace",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "should not match PVB and pod, pod name and namespace mismatch",
|
||||
pvb: velerov1api.PodVolumeBackup{
|
||||
Spec: velerov1api.PodVolumeBackupSpec{
|
||||
Pod: corev1api.ObjectReference{
|
||||
Name: "matching-pod",
|
||||
Namespace: "matching-namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
podName: "not-matching-pod",
|
||||
sourcePodNs: "not-matching-namespace",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := isPVBMatchPod(&tc.pvb, tc.podName, tc.sourcePodNs)
|
||||
assert.Equal(t, tc.expected, actual)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolumeHasNonRestorableSource(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
volumeName string
|
||||
podVolumes []corev1api.Volume
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "volume name not in list of volumes",
|
||||
volumeName: "missing-volume",
|
||||
podVolumes: []corev1api.Volume{
|
||||
{
|
||||
Name: "restorable",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "projected",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
Projected: &corev1api.ProjectedVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "downwardapi",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
DownwardAPI: &corev1api.DownwardAPIVolumeSource{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "volume name in list of volumes but not projected or DownwardAPI",
|
||||
volumeName: "restorable",
|
||||
podVolumes: []corev1api.Volume{
|
||||
{
|
||||
Name: "restorable",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "projected",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
Projected: &corev1api.ProjectedVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "downwardapi",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
DownwardAPI: &corev1api.DownwardAPIVolumeSource{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "volume name in list of volumes and projected",
|
||||
volumeName: "projected",
|
||||
podVolumes: []corev1api.Volume{
|
||||
{
|
||||
Name: "restorable",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "projected",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
Projected: &corev1api.ProjectedVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "downwardapi",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
DownwardAPI: &corev1api.DownwardAPIVolumeSource{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "volume name in list of volumes and is a DownwardAPI volume",
|
||||
volumeName: "downwardapi",
|
||||
podVolumes: []corev1api.Volume{
|
||||
{
|
||||
Name: "restorable",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "projected",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
Projected: &corev1api.ProjectedVolumeSource{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "downwardapi",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
DownwardAPI: &corev1api.DownwardAPIVolumeSource{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := volumeHasNonRestorableSource(tc.volumeName, tc.podVolumes)
|
||||
assert.Equal(t, tc.expected, actual)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
restic "github.com/vmware-tanzu/velero/pkg/restic"
|
||||
|
||||
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/podvolume"
|
||||
)
|
||||
|
||||
// RepositoryManager is an autogenerated mock type for the RepositoryManager type
|
||||
@@ -31,11 +32,11 @@ type RepositoryManager struct {
|
||||
}
|
||||
|
||||
// ConnectToRepo provides a mock function with given fields: repo
|
||||
func (_m *RepositoryManager) ConnectToRepo(repo *v1.ResticRepository) error {
|
||||
func (_m *RepositoryManager) ConnectToRepo(repo *v1.BackupRepository) error {
|
||||
ret := _m.Called(repo)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*v1.ResticRepository) error); ok {
|
||||
if rf, ok := ret.Get(0).(func(*v1.BackupRepository) error); ok {
|
||||
r0 = rf(repo)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
@@ -59,11 +60,11 @@ func (_m *RepositoryManager) Forget(_a0 context.Context, _a1 restic.SnapshotIden
|
||||
}
|
||||
|
||||
// InitRepo provides a mock function with given fields: repo
|
||||
func (_m *RepositoryManager) InitRepo(repo *v1.ResticRepository) error {
|
||||
func (_m *RepositoryManager) InitRepo(repo *v1.BackupRepository) error {
|
||||
ret := _m.Called(repo)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*v1.ResticRepository) error); ok {
|
||||
if rf, ok := ret.Get(0).(func(repository *v1.BackupRepository) error); ok {
|
||||
r0 = rf(repo)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
@@ -73,15 +74,15 @@ func (_m *RepositoryManager) InitRepo(repo *v1.ResticRepository) error {
|
||||
}
|
||||
|
||||
// NewBackupper provides a mock function with given fields: _a0, _a1
|
||||
func (_m *RepositoryManager) NewBackupper(_a0 context.Context, _a1 *v1.Backup) (restic.Backupper, error) {
|
||||
func (_m *RepositoryManager) NewBackupper(_a0 context.Context, _a1 *v1.Backup) (podvolume.Backupper, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
var r0 restic.Backupper
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v1.Backup) restic.Backupper); ok {
|
||||
var r0 podvolume.Backupper
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v1.Backup) podvolume.Backupper); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(restic.Backupper)
|
||||
r0 = ret.Get(0).(podvolume.Backupper)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,15 +97,15 @@ func (_m *RepositoryManager) NewBackupper(_a0 context.Context, _a1 *v1.Backup) (
|
||||
}
|
||||
|
||||
// NewRestorer provides a mock function with given fields: _a0, _a1
|
||||
func (_m *RepositoryManager) NewRestorer(_a0 context.Context, _a1 *v1.Restore) (restic.Restorer, error) {
|
||||
func (_m *RepositoryManager) NewRestorer(_a0 context.Context, _a1 *v1.Restore) (podvolume.Restorer, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
var r0 restic.Restorer
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v1.Restore) restic.Restorer); ok {
|
||||
var r0 podvolume.Restorer
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v1.Restore) podvolume.Restorer); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(restic.Restorer)
|
||||
r0 = ret.Get(0).(podvolume.Restorer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,11 +120,11 @@ func (_m *RepositoryManager) NewRestorer(_a0 context.Context, _a1 *v1.Restore) (
|
||||
}
|
||||
|
||||
// PruneRepo provides a mock function with given fields: repo
|
||||
func (_m *RepositoryManager) PruneRepo(repo *v1.ResticRepository) error {
|
||||
func (_m *RepositoryManager) PruneRepo(repo *v1.BackupRepository) error {
|
||||
ret := _m.Called(repo)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*v1.ResticRepository) error); ok {
|
||||
if rf, ok := ret.Get(0).(func(repository *v1.BackupRepository) error); ok {
|
||||
r0 = rf(repo)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
@@ -133,11 +134,11 @@ func (_m *RepositoryManager) PruneRepo(repo *v1.ResticRepository) error {
|
||||
}
|
||||
|
||||
// UnlockRepo provides a mock function with given fields: repo
|
||||
func (_m *RepositoryManager) UnlockRepo(repo *v1.ResticRepository) error {
|
||||
func (_m *RepositoryManager) UnlockRepo(repo *v1.BackupRepository) error {
|
||||
ret := _m.Called(repo)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*v1.ResticRepository) error); ok {
|
||||
if rf, ok := ret.Get(0).(func(repository *v1.BackupRepository) error); ok {
|
||||
r0 = rf(repo)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
|
||||
@@ -18,16 +18,13 @@ package restic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/credentials"
|
||||
@@ -36,6 +33,9 @@ import (
|
||||
velerov1client "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/typed/velero/v1"
|
||||
velerov1informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions/velero/v1"
|
||||
velerov1listers "github.com/vmware-tanzu/velero/pkg/generated/listers/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/podvolume"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
repokey "github.com/vmware-tanzu/velero/pkg/repository/keys"
|
||||
veleroexec "github.com/vmware-tanzu/velero/pkg/util/exec"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
)
|
||||
@@ -43,57 +43,45 @@ import (
|
||||
// RepositoryManager executes commands against restic repositories.
|
||||
type RepositoryManager interface {
|
||||
// InitRepo initializes a repo with the specified name and identifier.
|
||||
InitRepo(repo *velerov1api.ResticRepository) error
|
||||
InitRepo(repo *velerov1api.BackupRepository) error
|
||||
|
||||
// ConnectToRepo runs the 'restic snapshots' command against the
|
||||
// specified repo, and returns an error if it fails. This is
|
||||
// intended to be used to ensure that the repo exists/can be
|
||||
// authenticated to.
|
||||
ConnectToRepo(repo *velerov1api.ResticRepository) error
|
||||
ConnectToRepo(repo *velerov1api.BackupRepository) error
|
||||
|
||||
// PruneRepo deletes unused data from a repo.
|
||||
PruneRepo(repo *velerov1api.ResticRepository) error
|
||||
PruneRepo(repo *velerov1api.BackupRepository) error
|
||||
|
||||
// UnlockRepo removes stale locks from a repo.
|
||||
UnlockRepo(repo *velerov1api.ResticRepository) error
|
||||
UnlockRepo(repo *velerov1api.BackupRepository) error
|
||||
|
||||
// Forget removes a snapshot from the list of
|
||||
// available snapshots in a repo.
|
||||
Forget(context.Context, SnapshotIdentifier) error
|
||||
|
||||
BackupperFactory
|
||||
podvolume.BackupperFactory
|
||||
|
||||
RestorerFactory
|
||||
}
|
||||
|
||||
// BackupperFactory can construct restic backuppers.
|
||||
type BackupperFactory interface {
|
||||
// NewBackupper returns a restic backupper for use during a single
|
||||
// Velero backup.
|
||||
NewBackupper(context.Context, *velerov1api.Backup) (Backupper, error)
|
||||
}
|
||||
|
||||
// RestorerFactory can construct restic restorers.
|
||||
type RestorerFactory interface {
|
||||
// NewRestorer returns a restic restorer for use during a single
|
||||
// Velero restore.
|
||||
NewRestorer(context.Context, *velerov1api.Restore) (Restorer, error)
|
||||
podvolume.RestorerFactory
|
||||
}
|
||||
|
||||
type repositoryManager struct {
|
||||
namespace string
|
||||
veleroClient clientset.Interface
|
||||
repoLister velerov1listers.ResticRepositoryLister
|
||||
repoLister velerov1listers.BackupRepositoryLister
|
||||
repoInformerSynced cache.InformerSynced
|
||||
kbClient kbclient.Client
|
||||
log logrus.FieldLogger
|
||||
repoLocker *repoLocker
|
||||
repoEnsurer *repositoryEnsurer
|
||||
repoLocker *repository.RepoLocker
|
||||
repoEnsurer *repository.RepositoryEnsurer
|
||||
fileSystem filesystem.Interface
|
||||
ctx context.Context
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter
|
||||
pvClient corev1client.PersistentVolumesGetter
|
||||
credentialsFileStore credentials.FileStore
|
||||
podvolume.BackupperFactory
|
||||
podvolume.RestorerFactory
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -111,8 +99,8 @@ func NewRepositoryManager(
|
||||
ctx context.Context,
|
||||
namespace string,
|
||||
veleroClient clientset.Interface,
|
||||
repoInformer velerov1informers.ResticRepositoryInformer,
|
||||
repoClient velerov1client.ResticRepositoriesGetter,
|
||||
repoInformer velerov1informers.BackupRepositoryInformer,
|
||||
repoClient velerov1client.BackupRepositoriesGetter,
|
||||
kbClient kbclient.Client,
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter,
|
||||
pvClient corev1client.PersistentVolumesGetter,
|
||||
@@ -131,57 +119,19 @@ func NewRepositoryManager(
|
||||
log: log,
|
||||
ctx: ctx,
|
||||
|
||||
repoLocker: newRepoLocker(),
|
||||
repoEnsurer: newRepositoryEnsurer(repoInformer, repoClient, log),
|
||||
repoLocker: repository.NewRepoLocker(),
|
||||
repoEnsurer: repository.NewRepositoryEnsurer(repoInformer, repoClient, log),
|
||||
fileSystem: filesystem.NewFileSystem(),
|
||||
}
|
||||
rm.BackupperFactory = podvolume.NewBackupperFactory(rm.repoLocker, rm.repoEnsurer, rm.veleroClient, rm.pvcClient,
|
||||
rm.pvClient, rm.repoInformerSynced, rm.log)
|
||||
rm.RestorerFactory = podvolume.NewRestorerFactory(rm.repoLocker, rm.repoEnsurer, rm.veleroClient, rm.pvcClient,
|
||||
rm.repoInformerSynced, rm.log)
|
||||
|
||||
return rm, nil
|
||||
}
|
||||
|
||||
func (rm *repositoryManager) NewBackupper(ctx context.Context, backup *velerov1api.Backup) (Backupper, error) {
|
||||
informer := velerov1informers.NewFilteredPodVolumeBackupInformer(
|
||||
rm.veleroClient,
|
||||
backup.Namespace,
|
||||
0,
|
||||
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
|
||||
func(opts *metav1.ListOptions) {
|
||||
opts.LabelSelector = fmt.Sprintf("%s=%s", velerov1api.BackupUIDLabel, backup.UID)
|
||||
},
|
||||
)
|
||||
|
||||
b := newBackupper(ctx, rm, rm.repoEnsurer, informer, rm.pvcClient, rm.pvClient, rm.log)
|
||||
|
||||
go informer.Run(ctx.Done())
|
||||
if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced, rm.repoInformerSynced) {
|
||||
return nil, errors.New("timed out waiting for caches to sync")
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (rm *repositoryManager) NewRestorer(ctx context.Context, restore *velerov1api.Restore) (Restorer, error) {
|
||||
informer := velerov1informers.NewFilteredPodVolumeRestoreInformer(
|
||||
rm.veleroClient,
|
||||
restore.Namespace,
|
||||
0,
|
||||
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
|
||||
func(opts *metav1.ListOptions) {
|
||||
opts.LabelSelector = fmt.Sprintf("%s=%s", velerov1api.RestoreUIDLabel, restore.UID)
|
||||
},
|
||||
)
|
||||
|
||||
r := newRestorer(ctx, rm, rm.repoEnsurer, informer, rm.pvcClient, rm.log)
|
||||
|
||||
go informer.Run(ctx.Done())
|
||||
if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced, rm.repoInformerSynced) {
|
||||
return nil, errors.New("timed out waiting for cache to sync")
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (rm *repositoryManager) InitRepo(repo *velerov1api.ResticRepository) error {
|
||||
func (rm *repositoryManager) InitRepo(repo *velerov1api.BackupRepository) error {
|
||||
// restic init requires an exclusive lock
|
||||
rm.repoLocker.LockExclusive(repo.Name)
|
||||
defer rm.repoLocker.UnlockExclusive(repo.Name)
|
||||
@@ -189,7 +139,7 @@ func (rm *repositoryManager) InitRepo(repo *velerov1api.ResticRepository) error
|
||||
return rm.exec(InitCommand(repo.Spec.ResticIdentifier), repo.Spec.BackupStorageLocation)
|
||||
}
|
||||
|
||||
func (rm *repositoryManager) ConnectToRepo(repo *velerov1api.ResticRepository) error {
|
||||
func (rm *repositoryManager) ConnectToRepo(repo *velerov1api.BackupRepository) error {
|
||||
// restic snapshots requires a non-exclusive lock
|
||||
rm.repoLocker.Lock(repo.Name)
|
||||
defer rm.repoLocker.Unlock(repo.Name)
|
||||
@@ -204,7 +154,7 @@ func (rm *repositoryManager) ConnectToRepo(repo *velerov1api.ResticRepository) e
|
||||
return rm.exec(snapshotsCmd, repo.Spec.BackupStorageLocation)
|
||||
}
|
||||
|
||||
func (rm *repositoryManager) PruneRepo(repo *velerov1api.ResticRepository) error {
|
||||
func (rm *repositoryManager) PruneRepo(repo *velerov1api.BackupRepository) error {
|
||||
// restic prune requires an exclusive lock
|
||||
rm.repoLocker.LockExclusive(repo.Name)
|
||||
defer rm.repoLocker.UnlockExclusive(repo.Name)
|
||||
@@ -212,7 +162,7 @@ func (rm *repositoryManager) PruneRepo(repo *velerov1api.ResticRepository) error
|
||||
return rm.exec(PruneCommand(repo.Spec.ResticIdentifier), repo.Spec.BackupStorageLocation)
|
||||
}
|
||||
|
||||
func (rm *repositoryManager) UnlockRepo(repo *velerov1api.ResticRepository) error {
|
||||
func (rm *repositoryManager) UnlockRepo(repo *velerov1api.BackupRepository) error {
|
||||
// restic unlock requires a non-exclusive lock
|
||||
rm.repoLocker.Lock(repo.Name)
|
||||
defer rm.repoLocker.Unlock(repo.Name)
|
||||
@@ -242,7 +192,7 @@ func (rm *repositoryManager) Forget(ctx context.Context, snapshot SnapshotIdenti
|
||||
}
|
||||
|
||||
func (rm *repositoryManager) exec(cmd *Command, backupLocation string) error {
|
||||
file, err := rm.credentialsFileStore.Path(RepoKeySelector())
|
||||
file, err := rm.credentialsFileStore.Path(repokey.RepoKeySelector())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -90,11 +90,11 @@ func TestInitContainerRestoreHookPodActionExecute(t *testing.T) {
|
||||
PostHooks: []velerov1api.RestoreResourceHook{
|
||||
{
|
||||
Init: &velerov1api.InitRestoreHook{
|
||||
InitContainers: []corev1api.Container{
|
||||
*builder.ForContainer("restore-init1", "busy-box").
|
||||
Command([]string{"foobarbaz"}).Result(),
|
||||
*builder.ForContainer("restore-init2", "busy-box").
|
||||
Command([]string{"foobarbaz"}).Result(),
|
||||
InitContainers: []runtime.RawExtension{
|
||||
builder.ForContainer("restore-init1", "busy-box").
|
||||
Command([]string{"foobarbaz"}).ResultRawExtension(),
|
||||
builder.ForContainer("restore-init2", "busy-box").
|
||||
Command([]string{"foobarbaz"}).ResultRawExtension(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/label"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/framework"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
"github.com/vmware-tanzu/velero/pkg/podvolume"
|
||||
"github.com/vmware-tanzu/velero/pkg/restic"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
)
|
||||
@@ -96,7 +97,7 @@ func (a *ResticRestoreAction) Execute(input *velero.RestoreItemActionExecuteInpu
|
||||
for i := range podVolumeBackupList.Items {
|
||||
podVolumeBackups = append(podVolumeBackups, &podVolumeBackupList.Items[i])
|
||||
}
|
||||
volumeSnapshots := restic.GetVolumeBackupsForPod(podVolumeBackups, &pod, podFromBackup.Namespace)
|
||||
volumeSnapshots := podvolume.GetVolumeBackupsForPod(podVolumeBackups, &pod, podFromBackup.Namespace)
|
||||
if len(volumeSnapshots) == 0 {
|
||||
log.Debug("No restic backups found for pod")
|
||||
return velero.NewRestoreItemActionExecuteOutput(input.Item), nil
|
||||
|
||||
@@ -59,7 +59,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/framework"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
"github.com/vmware-tanzu/velero/pkg/podexec"
|
||||
"github.com/vmware-tanzu/velero/pkg/restic"
|
||||
"github.com/vmware-tanzu/velero/pkg/podvolume"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/collections"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
@@ -104,7 +104,7 @@ type kubernetesRestorer struct {
|
||||
discoveryHelper discovery.Helper
|
||||
dynamicFactory client.DynamicFactory
|
||||
namespaceClient corev1.NamespaceInterface
|
||||
resticRestorerFactory restic.RestorerFactory
|
||||
resticRestorerFactory podvolume.RestorerFactory
|
||||
resticTimeout time.Duration
|
||||
resourceTerminatingTimeout time.Duration
|
||||
resourcePriorities []string
|
||||
@@ -122,7 +122,7 @@ func NewKubernetesRestorer(
|
||||
dynamicFactory client.DynamicFactory,
|
||||
resourcePriorities []string,
|
||||
namespaceClient corev1.NamespaceInterface,
|
||||
resticRestorerFactory restic.RestorerFactory,
|
||||
resticRestorerFactory podvolume.RestorerFactory,
|
||||
resticTimeout time.Duration,
|
||||
resourceTerminatingTimeout time.Duration,
|
||||
logger logrus.FieldLogger,
|
||||
@@ -248,7 +248,7 @@ func (kr *kubernetesRestorer) RestoreWithResolvers(
|
||||
ctx, cancelFunc := go_context.WithTimeout(go_context.Background(), podVolumeTimeout)
|
||||
defer cancelFunc()
|
||||
|
||||
var resticRestorer restic.Restorer
|
||||
var resticRestorer podvolume.Restorer
|
||||
if kr.resticRestorerFactory != nil {
|
||||
resticRestorer, err = kr.resticRestorerFactory.NewRestorer(ctx, req.Restore)
|
||||
if err != nil {
|
||||
@@ -338,7 +338,7 @@ type restoreContext struct {
|
||||
restoreItemActions []framework.RestoreItemResolvedAction
|
||||
itemSnapshotterActions []framework.ItemSnapshotterResolvedAction
|
||||
volumeSnapshotterGetter VolumeSnapshotterGetter
|
||||
resticRestorer restic.Restorer
|
||||
resticRestorer podvolume.Restorer
|
||||
resticWaitGroup sync.WaitGroup
|
||||
resticErrs chan error
|
||||
pvsToProvision sets.String
|
||||
@@ -1394,7 +1394,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
|
||||
// Do not create podvolumerestore when current restore excludes pv/pvc
|
||||
if ctx.resourceIncludesExcludes.ShouldInclude(kuberesource.PersistentVolumeClaims.String()) &&
|
||||
ctx.resourceIncludesExcludes.ShouldInclude(kuberesource.PersistentVolumes.String()) &&
|
||||
len(restic.GetVolumeBackupsForPod(ctx.podVolumeBackups, pod, originalNamespace)) > 0 {
|
||||
len(podvolume.GetVolumeBackupsForPod(ctx.podVolumeBackups, pod, originalNamespace)) > 0 {
|
||||
restorePodVolumeBackups(ctx, createdObj, originalNamespace)
|
||||
}
|
||||
}
|
||||
@@ -1549,7 +1549,7 @@ func restorePodVolumeBackups(ctx *restoreContext, createdObj *unstructured.Unstr
|
||||
return
|
||||
}
|
||||
|
||||
data := restic.RestoreData{
|
||||
data := podvolume.RestoreData{
|
||||
Restore: ctx.restore,
|
||||
Pod: pod,
|
||||
PodVolumeBackups: ctx.podVolumeBackups,
|
||||
@@ -1631,7 +1631,7 @@ func hasResticBackup(unstructuredPV *unstructured.Unstructured, ctx *restoreCont
|
||||
|
||||
var found bool
|
||||
for _, pvb := range ctx.podVolumeBackups {
|
||||
if pvb.Spec.Pod.Namespace == pv.Spec.ClaimRef.Namespace && pvb.GetAnnotations()[restic.PVCNameAnnotation] == pv.Spec.ClaimRef.Name {
|
||||
if pvb.Spec.Pod.Namespace == pv.Spec.ClaimRef.Namespace && pvb.GetAnnotations()[podvolume.PVCNameAnnotation] == pv.Spec.ClaimRef.Name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ import (
|
||||
velerov1informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions"
|
||||
"github.com/vmware-tanzu/velero/pkg/kuberesource"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
"github.com/vmware-tanzu/velero/pkg/restic"
|
||||
resticmocks "github.com/vmware-tanzu/velero/pkg/restic/mocks"
|
||||
"github.com/vmware-tanzu/velero/pkg/podvolume"
|
||||
uploadermocks "github.com/vmware-tanzu/velero/pkg/podvolume/mocks"
|
||||
"github.com/vmware-tanzu/velero/pkg/test"
|
||||
testutil "github.com/vmware-tanzu/velero/pkg/test"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
@@ -2681,10 +2681,10 @@ func TestRestorePersistentVolumes(t *testing.T) {
|
||||
}
|
||||
|
||||
type fakeResticRestorerFactory struct {
|
||||
restorer *resticmocks.Restorer
|
||||
restorer *uploadermocks.Restorer
|
||||
}
|
||||
|
||||
func (f *fakeResticRestorerFactory) NewRestorer(context.Context, *velerov1api.Restore) (restic.Restorer, error) {
|
||||
func (f *fakeResticRestorerFactory) NewRestorer(context.Context, *velerov1api.Restore) (podvolume.Restorer, error) {
|
||||
return f.restorer, nil
|
||||
}
|
||||
|
||||
@@ -2749,7 +2749,7 @@ func TestRestoreWithRestic(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
restorer := new(resticmocks.Restorer)
|
||||
restorer := new(uploadermocks.Restorer)
|
||||
defer restorer.AssertExpectations(t)
|
||||
h.restorer.resticRestorerFactory = &fakeResticRestorerFactory{
|
||||
restorer: restorer,
|
||||
@@ -2773,7 +2773,7 @@ func TestRestoreWithRestic(t *testing.T) {
|
||||
|
||||
// the restore process adds these labels before restoring, so we must add them here too otherwise they won't match
|
||||
pod.Labels = map[string]string{"velero.io/backup-name": tc.backup.Name, "velero.io/restore-name": tc.restore.Name}
|
||||
expectedArgs := restic.RestoreData{
|
||||
expectedArgs := podvolume.RestoreData{
|
||||
Restore: tc.restore,
|
||||
Pod: pod,
|
||||
PodVolumeBackups: tc.podVolumeBackups,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
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 kube
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
)
|
||||
|
||||
// SpecChangePredicate implements a default update predicate function on Spec change
|
||||
// As Velero doesn't enable subresource in CRDs, we cannot use the object's metadata.generation field to check the spec change
|
||||
// More details about the generation field refer to https://github.com/kubernetes-sigs/controller-runtime/blob/v0.12.2/pkg/predicate/predicate.go#L156
|
||||
type SpecChangePredicate struct {
|
||||
predicate.Funcs
|
||||
}
|
||||
|
||||
func (SpecChangePredicate) Update(e event.UpdateEvent) bool {
|
||||
if e.ObjectOld == nil {
|
||||
return false
|
||||
}
|
||||
if e.ObjectNew == nil {
|
||||
return false
|
||||
}
|
||||
oldSpec := reflect.ValueOf(e.ObjectOld).Elem().FieldByName("Spec")
|
||||
// contains no field named "Spec", return false directly
|
||||
if oldSpec.IsZero() {
|
||||
return false
|
||||
}
|
||||
newSpec := reflect.ValueOf(e.ObjectNew).Elem().FieldByName("Spec")
|
||||
return !reflect.DeepEqual(oldSpec.Interface(), newSpec.Interface())
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
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 kube
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
|
||||
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
)
|
||||
|
||||
func TestSpecChangePredicate(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
oldObj client.Object
|
||||
newObj client.Object
|
||||
changed bool
|
||||
}{
|
||||
{
|
||||
name: "Contains no spec field",
|
||||
oldObj: &velerov1.BackupStorageLocation{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bsl01",
|
||||
},
|
||||
},
|
||||
newObj: &velerov1.BackupStorageLocation{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bsl01",
|
||||
},
|
||||
},
|
||||
changed: false,
|
||||
},
|
||||
{
|
||||
name: "ObjectMetas are different, Specs are same",
|
||||
oldObj: &velerov1.BackupStorageLocation{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bsl01",
|
||||
Annotations: map[string]string{"key1": "value1"},
|
||||
},
|
||||
Spec: velerov1.BackupStorageLocationSpec{
|
||||
Provider: "azure",
|
||||
},
|
||||
},
|
||||
newObj: &velerov1.BackupStorageLocation{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bsl01",
|
||||
Annotations: map[string]string{"key2": "value2"},
|
||||
},
|
||||
Spec: velerov1.BackupStorageLocationSpec{
|
||||
Provider: "azure",
|
||||
},
|
||||
},
|
||||
changed: false,
|
||||
},
|
||||
{
|
||||
name: "Statuses are different, Specs are same",
|
||||
oldObj: &velerov1.BackupStorageLocation{
|
||||
Spec: velerov1.BackupStorageLocationSpec{
|
||||
Provider: "azure",
|
||||
},
|
||||
Status: velerov1.BackupStorageLocationStatus{
|
||||
Phase: velerov1.BackupStorageLocationPhaseAvailable,
|
||||
},
|
||||
},
|
||||
newObj: &velerov1.BackupStorageLocation{
|
||||
Spec: velerov1.BackupStorageLocationSpec{
|
||||
Provider: "azure",
|
||||
},
|
||||
Status: velerov1.BackupStorageLocationStatus{
|
||||
Phase: velerov1.BackupStorageLocationPhaseUnavailable,
|
||||
},
|
||||
},
|
||||
changed: false,
|
||||
},
|
||||
{
|
||||
name: "Specs are different",
|
||||
oldObj: &velerov1.BackupStorageLocation{
|
||||
Spec: velerov1.BackupStorageLocationSpec{
|
||||
Provider: "azure",
|
||||
},
|
||||
},
|
||||
newObj: &velerov1.BackupStorageLocation{
|
||||
Spec: velerov1.BackupStorageLocationSpec{
|
||||
Provider: "aws",
|
||||
},
|
||||
},
|
||||
changed: true,
|
||||
},
|
||||
{
|
||||
name: "Specs are same",
|
||||
oldObj: &velerov1.BackupStorageLocation{
|
||||
Spec: velerov1.BackupStorageLocationSpec{
|
||||
Provider: "azure",
|
||||
Config: map[string]string{"key": "value"},
|
||||
Credential: &corev1api.SecretKeySelector{
|
||||
LocalObjectReference: corev1api.LocalObjectReference{
|
||||
Name: "secret",
|
||||
},
|
||||
Key: "credential",
|
||||
},
|
||||
StorageType: velerov1.StorageType{
|
||||
ObjectStorage: &velerov1.ObjectStorageLocation{
|
||||
Bucket: "bucket1",
|
||||
Prefix: "prefix",
|
||||
CACert: []byte{'a'},
|
||||
},
|
||||
},
|
||||
Default: true,
|
||||
AccessMode: velerov1.BackupStorageLocationAccessModeReadWrite,
|
||||
BackupSyncPeriod: &metav1.Duration{
|
||||
Duration: 1 * time.Minute,
|
||||
},
|
||||
ValidationFrequency: &metav1.Duration{
|
||||
Duration: 1 * time.Minute,
|
||||
},
|
||||
},
|
||||
},
|
||||
newObj: &velerov1.BackupStorageLocation{
|
||||
Spec: velerov1.BackupStorageLocationSpec{
|
||||
Provider: "azure",
|
||||
Config: map[string]string{"key": "value"},
|
||||
Credential: &corev1api.SecretKeySelector{
|
||||
LocalObjectReference: corev1api.LocalObjectReference{
|
||||
Name: "secret",
|
||||
},
|
||||
Key: "credential",
|
||||
},
|
||||
StorageType: velerov1.StorageType{
|
||||
ObjectStorage: &velerov1.ObjectStorageLocation{
|
||||
Bucket: "bucket1",
|
||||
Prefix: "prefix",
|
||||
CACert: []byte{'a'},
|
||||
},
|
||||
},
|
||||
Default: true,
|
||||
AccessMode: velerov1.BackupStorageLocationAccessModeReadWrite,
|
||||
BackupSyncPeriod: &metav1.Duration{
|
||||
Duration: 1 * time.Minute,
|
||||
},
|
||||
ValidationFrequency: &metav1.Duration{
|
||||
Duration: 1 * time.Minute,
|
||||
},
|
||||
},
|
||||
},
|
||||
changed: false,
|
||||
},
|
||||
}
|
||||
|
||||
predicate := SpecChangePredicate{}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
changed := predicate.Update(event.UpdateEvent{
|
||||
ObjectOld: c.oldObj,
|
||||
ObjectNew: c.newObj,
|
||||
})
|
||||
assert.Equal(t, c.changed, changed)
|
||||
})
|
||||
}
|
||||
}
|
||||
+30
-6
@@ -34,15 +34,20 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
)
|
||||
|
||||
// These annotations are taken from the Kubernetes persistent volume/persistent volume claim controller.
|
||||
// They cannot be directly importing because they are part of the kubernetes/kubernetes package, and importing that package is unsupported.
|
||||
// Their values are well-known and slow changing. They're duplicated here as constants to provide compile-time checking.
|
||||
// Originals can be found in kubernetes/kubernetes/pkg/controller/volume/persistentvolume/util/util.go.
|
||||
const KubeAnnBindCompleted = "pv.kubernetes.io/bind-completed"
|
||||
const KubeAnnBoundByController = "pv.kubernetes.io/bound-by-controller"
|
||||
const KubeAnnDynamicallyProvisioned = "pv.kubernetes.io/provisioned-by"
|
||||
const (
|
||||
KubeAnnBindCompleted = "pv.kubernetes.io/bind-completed"
|
||||
KubeAnnBoundByController = "pv.kubernetes.io/bound-by-controller"
|
||||
KubeAnnDynamicallyProvisioned = "pv.kubernetes.io/provisioned-by"
|
||||
KubeAnnMigratedTo = "pv.kubernetes.io/migrated-to"
|
||||
)
|
||||
|
||||
// NamespaceAndName returns a string in the format <namespace>/<name>
|
||||
func NamespaceAndName(objMeta metav1.Object) string {
|
||||
@@ -163,6 +168,9 @@ func GetVolumeDirectory(ctx context.Context, log logrus.FieldLogger, pod *corev1
|
||||
return pvc.Spec.VolumeName, nil
|
||||
}
|
||||
|
||||
// isProvisionedByCSI function checks whether this is a CSI PV by annotation.
|
||||
// Either "pv.kubernetes.io/provisioned-by" or "pv.kubernetes.io/migrated-to" indicates
|
||||
// PV is provisioned by CSI.
|
||||
func isProvisionedByCSI(log logrus.FieldLogger, pv *corev1api.PersistentVolume, kbClient client.Client) (bool, error) {
|
||||
if pv.Spec.CSI != nil {
|
||||
return true, nil
|
||||
@@ -171,14 +179,15 @@ func isProvisionedByCSI(log logrus.FieldLogger, pv *corev1api.PersistentVolume,
|
||||
// Refer to https://github.com/vmware-tanzu/velero/issues/4496 for more details
|
||||
if pv.Annotations != nil {
|
||||
driverName := pv.Annotations[KubeAnnDynamicallyProvisioned]
|
||||
if len(driverName) > 0 {
|
||||
migratedDriver := pv.Annotations[KubeAnnMigratedTo]
|
||||
if len(driverName) > 0 || len(migratedDriver) > 0 {
|
||||
list := &storagev1api.CSIDriverList{}
|
||||
if err := kbClient.List(context.TODO(), list); err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, driver := range list.Items {
|
||||
if driverName == driver.Name {
|
||||
log.Debugf("the annotation %s=%s indicates the volume is provisioned by a CSI driver", KubeAnnDynamicallyProvisioned, driverName)
|
||||
if driverName == driver.Name || migratedDriver == driver.Name {
|
||||
log.Debugf("the annotation %s or %s equals to %s indicates the volume is provisioned by a CSI driver", KubeAnnDynamicallyProvisioned, KubeAnnMigratedTo, driverName)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
@@ -187,6 +196,21 @@ func isProvisionedByCSI(log logrus.FieldLogger, pv *corev1api.PersistentVolume,
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// SinglePathMatch function will be called by PVB and PVR controller to check whether pass-in volume path is valid.
|
||||
// Check whether there is only one match by the path's pattern (/host_pods/%s/volumes/*/volume_name/[mount|]).
|
||||
func SinglePathMatch(path string, fs filesystem.Interface, log logrus.FieldLogger) (string, error) {
|
||||
matches, err := fs.Glob(path)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
if len(matches) != 1 {
|
||||
return "", errors.Errorf("expected one matching path: %s, got %d", path, len(matches))
|
||||
}
|
||||
|
||||
log.Debugf("This is a valid volume path: %s.", matches[0])
|
||||
return matches[0], nil
|
||||
}
|
||||
|
||||
// IsV1CRDReady checks a v1 CRD to see if it's ready, with both the Established and NamesAccepted conditions.
|
||||
func IsV1CRDReady(crd *apiextv1.CustomResourceDefinition) bool {
|
||||
var isEstablished, namesAccepted bool
|
||||
|
||||
@@ -197,6 +197,13 @@ func TestGetVolumeDirectorySuccess(t *testing.T) {
|
||||
pv: builder.ForPersistentVolume("a-pv").ObjectMeta(builder.WithAnnotations(KubeAnnDynamicallyProvisioned, "csi.test.com")).Result(),
|
||||
want: "a-pv/mount",
|
||||
},
|
||||
{
|
||||
name: "Volume with CSI annotation 'pv.kubernetes.io/migrated-to' appends '/mount' to the volume name",
|
||||
pod: builder.ForPod("ns-1", "my-pod").Volumes(builder.ForVolume("my-vol").PersistentVolumeClaimSource("my-pvc").Result()).Result(),
|
||||
pvc: builder.ForPersistentVolumeClaim("ns-1", "my-pvc").VolumeName("a-pv").Result(),
|
||||
pv: builder.ForPersistentVolume("a-pv").ObjectMeta(builder.WithAnnotations(KubeAnnMigratedTo, "csi.test.com")).Result(),
|
||||
want: "a-pv/mount",
|
||||
},
|
||||
}
|
||||
|
||||
csiDriver := storagev1api.CSIDriver{
|
||||
@@ -425,3 +432,13 @@ func TestIsCRDReady(t *testing.T) {
|
||||
_, err = IsCRDReady(obj)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestSinglePathMatch(t *testing.T) {
|
||||
fakeFS := velerotest.NewFakeFileSystem()
|
||||
fakeFS.MkdirAll("testDir1/subpath", 0755)
|
||||
fakeFS.MkdirAll("testDir2/subpath", 0755)
|
||||
|
||||
_, err := SinglePathMatch("./*/subpath", fakeFS, logrus.StandardLogger())
|
||||
assert.NotNil(t, err)
|
||||
assert.Contains(t, err.Error(), "expected one matching path")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
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 ownership
|
||||
|
||||
import "github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
|
||||
const (
|
||||
defaultOwnerUsername = "default"
|
||||
defaultOwnerDomain = "default"
|
||||
)
|
||||
|
||||
// GetBackupOwner returns the owner used by uploaders when saving a snapshot or
|
||||
// opening the unified repository. At present, use the default owner only
|
||||
func GetBackupOwner() udmrepo.OwnershipOptions {
|
||||
return udmrepo.OwnershipOptions{
|
||||
Username: defaultOwnerUsername,
|
||||
DomainName: defaultOwnerDomain,
|
||||
}
|
||||
}
|
||||
|
||||
// GetBackupOwner returns the owner used to create/connect the unified repository.
|
||||
//At present, use the default owner only
|
||||
func GetRepositoryOwner() udmrepo.OwnershipOptions {
|
||||
return udmrepo.OwnershipOptions{
|
||||
Username: defaultOwnerUsername,
|
||||
DomainName: defaultOwnerDomain,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user