mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-04-22 16:50:34 +00:00
Merge branch 'main' of https://github.com/qiuming-best/velero into uploader-kopia
This commit is contained in:
2
.github/workflows/pr-codespell.yml
vendored
2
.github/workflows/pr-codespell.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
uses: codespell-project/actions-codespell@master
|
||||
with:
|
||||
# ignore the config/.../crd.go file as it's generated binary data that is edited elswhere.
|
||||
skip: .git,*.png,*.jpg,*.woff,*.ttf,*.gif,*.ico,./config/crd/v1beta1/crds/crds.go,./config/crd/v1/crds/crds.go,./go.sum
|
||||
skip: .git,*.png,*.jpg,*.woff,*.ttf,*.gif,*.ico,./config/crd/v1beta1/crds/crds.go,./config/crd/v1/crds/crds.go,./go.sum,./LICENSE
|
||||
ignore_words_list: iam,aks,ist,bridget,ue
|
||||
check_filenames: true
|
||||
check_hidden: true
|
||||
|
||||
1
changelogs/unreleased/5231-lyndon
Normal file
1
changelogs/unreleased/5231-lyndon
Normal file
@@ -0,0 +1 @@
|
||||
Add changes for Kopia Integration: Kopia Lib - initialize Kopia repo
|
||||
@@ -429,7 +429,7 @@ Instead, a new method for 'Progress' will be added to interface. Velero server r
|
||||
|
||||
But, this involves good amount of changes and needs a way for backward compatibility.
|
||||
|
||||
As volume plugins are mostly K8s native, its fine to go ahead with current limiation.
|
||||
As volume plugins are mostly K8s native, its fine to go ahead with current limitation.
|
||||
|
||||
### Update Backup CR
|
||||
Instead of creating new CRs, plugins can directly update the status of Backup CR. But, this deviates from current approach of having separate CRs like PodVolumeBackup/PodVolumeRestore to know operations progress.
|
||||
|
||||
9
go.mod
9
go.mod
@@ -57,6 +57,9 @@ require (
|
||||
cloud.google.com/go v0.100.2 // indirect
|
||||
cloud.google.com/go/compute v1.5.0 // indirect
|
||||
cloud.google.com/go/iam v0.1.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.16 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 // indirect
|
||||
@@ -69,6 +72,7 @@ require (
|
||||
github.com/chmduquesne/rollinghash v4.0.0+incompatible // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/go-logr/logr v0.4.0 // indirect
|
||||
github.com/go-logr/zapr v0.4.0 // indirect
|
||||
@@ -93,6 +97,9 @@ require (
|
||||
github.com/mattn/go-ieproxy v0.0.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.23 // indirect
|
||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
@@ -106,6 +113,7 @@ require (
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/rs/xid v1.3.0 // indirect
|
||||
github.com/stretchr/objx v0.2.0 // indirect
|
||||
github.com/vladimirvivien/gexe v0.1.1 // indirect
|
||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||
@@ -126,6 +134,7 @@ require (
|
||||
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
|
||||
10
go.sum
10
go.sum
@@ -63,8 +63,11 @@ github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVt
|
||||
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
|
||||
github.com/Azure/azure-sdk-for-go v61.4.0+incompatible h1:BF2Pm3aQWIa6q9KmxyF1JYKYXtVw67vtvu2Wd54NGuY=
|
||||
github.com/Azure/azure-sdk-for-go v61.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1 h1:qoVeMsc9/fh/yhxVaA0obYjVH/oI/ihrOoMwsLS9KSA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3 h1:E+m3SkZCN0Bf5q7YdTs5lSm2CYY3CK4spn5OmUIiQtk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0 h1:Px2UA+2RvSSvv+RvJNuUB6n7rs5Wsel4dXLe90Um2n4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0/go.mod h1:tPaiy8S5bQ+S5sOiDlINkp7+Ef339+Nz5L5XO+cnOHo=
|
||||
github.com/Azure/azure-storage-blob-go v0.14.0 h1:1BCg74AmVdYwO3dlKwtFU1V0wU2PZdREkXvAmZJRUlM=
|
||||
github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck=
|
||||
@@ -231,10 +234,12 @@ github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQ
|
||||
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
|
||||
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
|
||||
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
@@ -587,9 +592,12 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.23 h1:NleyGQvAn9VQMU+YHVrgV4CX+EPtxPt/78lHOOTncy4=
|
||||
github.com/minio/minio-go/v7 v7.0.23/go.mod h1:ei5JjmxwHaMrgsMrn4U/+Nmg+d8MKS1U2DAn1ou4+Do=
|
||||
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
|
||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
@@ -737,6 +745,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
@@ -1440,6 +1449,7 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI=
|
||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216/go.mod h1:zJ2QpyDCYo1KvLXlmdnFlQAyF/Qfth0fB8239Qg7BIE=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
|
||||
@@ -95,7 +95,7 @@ eval $(go run $DIR/chk_version.go)
|
||||
printf "To clarify, you've provided a version string of $VELERO_VERSION.\n"
|
||||
printf "Based on this, the following assumptions have been made: \n"
|
||||
|
||||
# $VELERO_PATCH gets populated by the chk_version.go scrip that parses and verifies the given version format
|
||||
# $VELERO_PATCH gets populated by the chk_version.go script that parses and verifies the given version format
|
||||
# If we've got a patch release, we assume the tag is on release branch.
|
||||
if [[ "$VELERO_PATCH" != 0 ]]; then
|
||||
printf "*\t This is a patch release.\n"
|
||||
|
||||
@@ -51,6 +51,9 @@ const (
|
||||
BackupRepositoryPhaseNew BackupRepositoryPhase = "New"
|
||||
BackupRepositoryPhaseReady BackupRepositoryPhase = "Ready"
|
||||
BackupRepositoryPhaseNotReady BackupRepositoryPhase = "NotReady"
|
||||
|
||||
BackupRepositoryTypeRestic string = "restic"
|
||||
BackupRepositoryTypeUnified string = "unified"
|
||||
)
|
||||
|
||||
// BackupRepositoryStatus is the current status of a BackupRepository.
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -82,6 +82,8 @@ 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"
|
||||
"github.com/vmware-tanzu/velero/pkg/podvolume"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
repokey "github.com/vmware-tanzu/velero/pkg/repository/keys"
|
||||
)
|
||||
|
||||
@@ -248,7 +250,9 @@ type server struct {
|
||||
logger logrus.FieldLogger
|
||||
logLevel logrus.Level
|
||||
pluginRegistry clientmgmt.Registry
|
||||
resticManager restic.RepositoryManager
|
||||
repoManager repository.Manager
|
||||
repoLocker *repository.RepoLocker
|
||||
repoEnsurer *repository.RepositoryEnsurer
|
||||
metrics *metrics.ServerMetrics
|
||||
config serverConfig
|
||||
mgr manager.Manager
|
||||
@@ -536,22 +540,10 @@ func (s *server) initRestic() error {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := restic.NewRepositoryManager(
|
||||
s.ctx,
|
||||
s.namespace,
|
||||
s.veleroClient,
|
||||
s.sharedInformerFactory.Velero().V1().BackupRepositories(),
|
||||
s.veleroClient.VeleroV1(),
|
||||
s.mgr.GetClient(),
|
||||
s.kubeClient.CoreV1(),
|
||||
s.kubeClient.CoreV1(),
|
||||
s.credentialFileStore,
|
||||
s.logger,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.resticManager = res
|
||||
s.repoLocker = repository.NewRepoLocker()
|
||||
s.repoEnsurer = repository.NewRepositoryEnsurer(s.sharedInformerFactory.Velero().V1().BackupRepositories(), s.veleroClient.VeleroV1(), s.logger)
|
||||
|
||||
s.repoManager = repository.NewManager(s.namespace, s.mgr.GetClient(), s.repoLocker, s.repoEnsurer, s.credentialFileStore, s.logger)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -643,7 +635,8 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
|
||||
s.discoveryHelper,
|
||||
client.NewDynamicFactory(s.dynamicClient),
|
||||
podexec.NewPodCommandExecutor(s.kubeClientConfig, s.kubeClient.CoreV1().RESTClient()),
|
||||
s.resticManager,
|
||||
podvolume.NewBackupperFactory(s.repoLocker, s.repoEnsurer, s.veleroClient, s.kubeClient.CoreV1(),
|
||||
s.kubeClient.CoreV1(), s.sharedInformerFactory.Velero().V1().BackupRepositories().Informer().HasSynced, s.logger),
|
||||
s.config.podVolumeOperationTimeout,
|
||||
s.config.defaultVolumesToRestic,
|
||||
s.config.clientPageSize,
|
||||
@@ -704,7 +697,8 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
|
||||
client.NewDynamicFactory(s.dynamicClient),
|
||||
s.config.restoreResourcePriorities,
|
||||
s.kubeClient.CoreV1().Namespaces(),
|
||||
s.resticManager,
|
||||
podvolume.NewRestorerFactory(s.repoLocker, s.repoEnsurer, s.veleroClient, s.kubeClient.CoreV1(),
|
||||
s.sharedInformerFactory.Velero().V1().BackupRepositories().Informer().HasSynced, s.logger),
|
||||
s.config.podVolumeOperationTimeout,
|
||||
s.config.resourceTerminatingTimeout,
|
||||
s.logger,
|
||||
@@ -812,7 +806,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
|
||||
s.logger.Fatal(err, "unable to create controller", "controller", controller.Schedule)
|
||||
}
|
||||
|
||||
if err := controller.NewResticRepoReconciler(s.namespace, s.logger, s.mgr.GetClient(), s.config.defaultResticMaintenanceFrequency, s.resticManager).SetupWithManager(s.mgr); err != nil {
|
||||
if err := controller.NewResticRepoReconciler(s.namespace, s.logger, s.mgr.GetClient(), s.config.defaultResticMaintenanceFrequency, s.repoManager).SetupWithManager(s.mgr); err != nil {
|
||||
s.logger.Fatal(err, "unable to create controller", "controller", controller.ResticRepo)
|
||||
}
|
||||
|
||||
@@ -820,7 +814,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
|
||||
s.logger,
|
||||
s.mgr.GetClient(),
|
||||
backupTracker,
|
||||
s.resticManager,
|
||||
s.repoManager,
|
||||
s.metrics,
|
||||
s.discoveryHelper,
|
||||
newPluginManager,
|
||||
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/persistence"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
"github.com/vmware-tanzu/velero/pkg/restic"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
@@ -56,7 +57,7 @@ type backupDeletionReconciler struct {
|
||||
client.Client
|
||||
logger logrus.FieldLogger
|
||||
backupTracker BackupTracker
|
||||
resticMgr restic.RepositoryManager
|
||||
repoMgr repository.Manager
|
||||
metrics *metrics.ServerMetrics
|
||||
clock clock.Clock
|
||||
discoveryHelper discovery.Helper
|
||||
@@ -69,7 +70,7 @@ func NewBackupDeletionReconciler(
|
||||
logger logrus.FieldLogger,
|
||||
client client.Client,
|
||||
backupTracker BackupTracker,
|
||||
resticMgr restic.RepositoryManager,
|
||||
repoMgr repository.Manager,
|
||||
metrics *metrics.ServerMetrics,
|
||||
helper discovery.Helper,
|
||||
newPluginManager func(logrus.FieldLogger) clientmgmt.Manager,
|
||||
@@ -79,7 +80,7 @@ func NewBackupDeletionReconciler(
|
||||
Client: client,
|
||||
logger: logger,
|
||||
backupTracker: backupTracker,
|
||||
resticMgr: resticMgr,
|
||||
repoMgr: repoMgr,
|
||||
metrics: metrics,
|
||||
clock: clock.RealClock{},
|
||||
discoveryHelper: helper,
|
||||
@@ -435,7 +436,7 @@ func (r *backupDeletionReconciler) deleteExistingDeletionRequests(ctx context.Co
|
||||
}
|
||||
|
||||
func (r *backupDeletionReconciler) deleteResticSnapshots(ctx context.Context, backup *velerov1api.Backup) []error {
|
||||
if r.resticMgr == nil {
|
||||
if r.repoMgr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -449,7 +450,7 @@ func (r *backupDeletionReconciler) deleteResticSnapshots(ctx context.Context, ba
|
||||
|
||||
var errs []error
|
||||
for _, snapshot := range snapshots {
|
||||
if err := r.resticMgr.Forget(ctx2, snapshot); err != nil {
|
||||
if err := r.repoMgr.Forget(ctx2, snapshot); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
repoconfig "github.com/vmware-tanzu/velero/pkg/repository/config"
|
||||
"github.com/vmware-tanzu/velero/pkg/restic"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
@@ -45,11 +46,11 @@ type ResticRepoReconciler struct {
|
||||
logger logrus.FieldLogger
|
||||
clock clock.Clock
|
||||
defaultMaintenanceFrequency time.Duration
|
||||
repositoryManager restic.RepositoryManager
|
||||
repositoryManager repository.Manager
|
||||
}
|
||||
|
||||
func NewResticRepoReconciler(namespace string, logger logrus.FieldLogger, client client.Client,
|
||||
defaultMaintenanceFrequency time.Duration, repositoryManager restic.RepositoryManager) *ResticRepoReconciler {
|
||||
defaultMaintenanceFrequency time.Duration, repositoryManager repository.Manager) *ResticRepoReconciler {
|
||||
c := &ResticRepoReconciler{
|
||||
client,
|
||||
namespace,
|
||||
@@ -163,7 +164,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.BackupRepository, repoManager restic.RepositoryManager) error {
|
||||
func ensureRepo(repo *velerov1api.BackupRepository, repoManager repository.Manager) 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.
|
||||
|
||||
@@ -24,14 +24,14 @@ import (
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
resticmokes "github.com/vmware-tanzu/velero/pkg/restic/mocks"
|
||||
repomokes "github.com/vmware-tanzu/velero/pkg/repository/mocks"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
)
|
||||
|
||||
const defaultMaintenanceFrequency = 10 * time.Minute
|
||||
|
||||
func mockResticRepoReconciler(t *testing.T, rr *velerov1api.BackupRepository, mockOn string, arg interface{}, ret interface{}) *ResticRepoReconciler {
|
||||
mgr := &resticmokes.RepositoryManager{}
|
||||
mgr := &repomokes.RepositoryManager{}
|
||||
if mockOn != "" {
|
||||
mgr.On(mockOn, arg).Return(ret)
|
||||
}
|
||||
|
||||
188
pkg/repository/manager.go
Normal file
188
pkg/repository/manager.go
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
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 repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/credentials"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/provider"
|
||||
"github.com/vmware-tanzu/velero/pkg/restic"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
)
|
||||
|
||||
// Manager manages backup repositories.
|
||||
type Manager interface {
|
||||
// InitRepo initializes a repo with the specified name and identifier.
|
||||
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.BackupRepository) error
|
||||
|
||||
// PruneRepo deletes unused data from a repo.
|
||||
PruneRepo(repo *velerov1api.BackupRepository) error
|
||||
|
||||
// UnlockRepo removes stale locks from a repo.
|
||||
UnlockRepo(repo *velerov1api.BackupRepository) error
|
||||
|
||||
// Forget removes a snapshot from the list of
|
||||
// available snapshots in a repo.
|
||||
Forget(context.Context, restic.SnapshotIdentifier) error
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
namespace string
|
||||
providers map[string]provider.Provider
|
||||
client client.Client
|
||||
repoLocker *RepoLocker
|
||||
repoEnsurer *RepositoryEnsurer
|
||||
fileSystem filesystem.Interface
|
||||
log logrus.FieldLogger
|
||||
}
|
||||
|
||||
// NewManager create a new repository manager.
|
||||
func NewManager(
|
||||
namespace string,
|
||||
client client.Client,
|
||||
repoLocker *RepoLocker,
|
||||
repoEnsurer *RepositoryEnsurer,
|
||||
credentialFileStore credentials.FileStore,
|
||||
log logrus.FieldLogger,
|
||||
) Manager {
|
||||
mgr := &manager{
|
||||
namespace: namespace,
|
||||
client: client,
|
||||
providers: map[string]provider.Provider{},
|
||||
repoLocker: repoLocker,
|
||||
repoEnsurer: repoEnsurer,
|
||||
fileSystem: filesystem.NewFileSystem(),
|
||||
log: log,
|
||||
}
|
||||
|
||||
mgr.providers[velerov1api.BackupRepositoryTypeRestic] = provider.NewResticRepositoryProvider(credentialFileStore, mgr.fileSystem, mgr.log)
|
||||
|
||||
return mgr
|
||||
}
|
||||
|
||||
func (m *manager) InitRepo(repo *velerov1api.BackupRepository) error {
|
||||
m.repoLocker.LockExclusive(repo.Name)
|
||||
defer m.repoLocker.UnlockExclusive(repo.Name)
|
||||
|
||||
prd, err := m.getRepositoryProvider(repo)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
param, err := m.assembleRepoParam(repo)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
return prd.InitRepo(context.Background(), param)
|
||||
}
|
||||
|
||||
func (m *manager) ConnectToRepo(repo *velerov1api.BackupRepository) error {
|
||||
m.repoLocker.Lock(repo.Name)
|
||||
defer m.repoLocker.Unlock(repo.Name)
|
||||
|
||||
prd, err := m.getRepositoryProvider(repo)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
param, err := m.assembleRepoParam(repo)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
return prd.ConnectToRepo(context.Background(), param)
|
||||
}
|
||||
|
||||
func (m *manager) PruneRepo(repo *velerov1api.BackupRepository) error {
|
||||
m.repoLocker.LockExclusive(repo.Name)
|
||||
defer m.repoLocker.UnlockExclusive(repo.Name)
|
||||
|
||||
prd, err := m.getRepositoryProvider(repo)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
param, err := m.assembleRepoParam(repo)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
return prd.PruneRepo(context.Background(), param)
|
||||
}
|
||||
|
||||
func (m *manager) UnlockRepo(repo *velerov1api.BackupRepository) error {
|
||||
m.repoLocker.Lock(repo.Name)
|
||||
defer m.repoLocker.Unlock(repo.Name)
|
||||
|
||||
prd, err := m.getRepositoryProvider(repo)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
param, err := m.assembleRepoParam(repo)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
return prd.EnsureUnlockRepo(context.Background(), param)
|
||||
}
|
||||
|
||||
func (m *manager) Forget(ctx context.Context, snapshot restic.SnapshotIdentifier) error {
|
||||
repo, err := m.repoEnsurer.EnsureRepo(ctx, m.namespace, snapshot.VolumeNamespace, snapshot.BackupStorageLocation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.repoLocker.LockExclusive(repo.Name)
|
||||
defer m.repoLocker.UnlockExclusive(repo.Name)
|
||||
|
||||
prd, err := m.getRepositoryProvider(repo)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
param, err := m.assembleRepoParam(repo)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
return prd.Forget(context.Background(), snapshot.SnapshotID, param)
|
||||
}
|
||||
|
||||
func (m *manager) getRepositoryProvider(repo *velerov1api.BackupRepository) (provider.Provider, error) {
|
||||
switch repo.Spec.RepositoryType {
|
||||
case "", velerov1api.BackupRepositoryTypeRestic:
|
||||
return m.providers[velerov1api.BackupRepositoryTypeRestic], nil
|
||||
default:
|
||||
return nil, fmt.Errorf("failed to get provider for repository %s", repo.Spec.RepositoryType)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) assembleRepoParam(repo *velerov1api.BackupRepository) (provider.RepoParam, error) {
|
||||
bsl := &velerov1api.BackupStorageLocation{}
|
||||
if err := m.client.Get(context.Background(), client.ObjectKey{m.namespace, repo.Spec.BackupStorageLocation}, bsl); err != nil {
|
||||
return provider.RepoParam{}, errors.WithStack(err)
|
||||
}
|
||||
return provider.RepoParam{
|
||||
BackupLocation: bsl,
|
||||
BackupRepo: repo,
|
||||
}, nil
|
||||
}
|
||||
47
pkg/repository/manager_test.go
Normal file
47
pkg/repository/manager_test.go
Normal file
@@ -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 repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
)
|
||||
|
||||
func TestGetRepositoryProvider(t *testing.T) {
|
||||
mgr := NewManager("", nil, nil, nil, nil, nil).(*manager)
|
||||
repo := &velerov1.BackupRepository{}
|
||||
|
||||
// empty repository type
|
||||
provider, err := mgr.getRepositoryProvider(repo)
|
||||
require.Nil(t, err)
|
||||
assert.NotNil(t, provider)
|
||||
|
||||
// valid repository type
|
||||
repo.Spec.RepositoryType = velerov1.BackupRepositoryTypeRestic
|
||||
provider, err = mgr.getRepositoryProvider(repo)
|
||||
require.Nil(t, err)
|
||||
assert.NotNil(t, provider)
|
||||
|
||||
// invalid repository type
|
||||
repo.Spec.RepositoryType = "unknown"
|
||||
_, err = mgr.getRepositoryProvider(repo)
|
||||
require.NotNil(t, err)
|
||||
}
|
||||
@@ -28,6 +28,7 @@ type RepoParam struct {
|
||||
BackupRepo *velerov1api.BackupRepository
|
||||
}
|
||||
|
||||
// Provider defines the methods to manipulate a backup repository
|
||||
type Provider interface {
|
||||
//InitRepo is to initialize a repository from a new storage place
|
||||
InitRepo(ctx context.Context, param RepoParam) error
|
||||
|
||||
69
pkg/repository/provider/restic.go
Normal file
69
pkg/repository/provider/restic.go
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/credentials"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/restic"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
)
|
||||
|
||||
func NewResticRepositoryProvider(store credentials.FileStore, fs filesystem.Interface, log logrus.FieldLogger) Provider {
|
||||
return &resticRepositoryProvider{
|
||||
svc: restic.NewRepositoryService(store, fs, log),
|
||||
}
|
||||
}
|
||||
|
||||
type resticRepositoryProvider struct {
|
||||
svc *restic.RepositoryService
|
||||
}
|
||||
|
||||
func (r *resticRepositoryProvider) InitRepo(ctx context.Context, param RepoParam) error {
|
||||
return r.svc.InitRepo(param.BackupLocation, param.BackupRepo)
|
||||
}
|
||||
|
||||
func (r *resticRepositoryProvider) ConnectToRepo(ctx context.Context, param RepoParam) error {
|
||||
return r.svc.ConnectToRepo(param.BackupLocation, param.BackupRepo)
|
||||
}
|
||||
|
||||
func (r *resticRepositoryProvider) PrepareRepo(ctx context.Context, param RepoParam) error {
|
||||
if err := r.InitRepo(ctx, param); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.ConnectToRepo(ctx, param)
|
||||
}
|
||||
|
||||
func (r *resticRepositoryProvider) PruneRepo(ctx context.Context, param RepoParam) error {
|
||||
return r.svc.PruneRepo(param.BackupLocation, param.BackupRepo)
|
||||
}
|
||||
|
||||
func (r *resticRepositoryProvider) PruneRepoQuick(ctx context.Context, param RepoParam) error {
|
||||
// restic doesn't support this operation
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *resticRepositoryProvider) EnsureUnlockRepo(ctx context.Context, param RepoParam) error {
|
||||
return r.svc.UnlockRepo(param.BackupLocation, param.BackupRepo)
|
||||
}
|
||||
|
||||
func (r *resticRepositoryProvider) Forget(ctx context.Context, snapshotID string, param RepoParam) error {
|
||||
return r.svc.Forget(param.BackupLocation, param.BackupRepo, snapshotID)
|
||||
}
|
||||
@@ -62,6 +62,8 @@ const (
|
||||
repoOpDescFullMaintain = "full maintenance"
|
||||
repoOpDescQuickMaintain = "quick maintenance"
|
||||
repoOpDescForget = "forget"
|
||||
|
||||
repoConnectDesc = "unfied repo"
|
||||
)
|
||||
|
||||
// NewUnifiedRepoProvider creates the service provider for Unified Repo
|
||||
@@ -92,8 +94,14 @@ func (urp *unifiedRepoProvider) InitRepo(ctx context.Context, param RepoParam) e
|
||||
repoOption, err := udmrepo.NewRepoOptions(
|
||||
udmrepo.WithPassword(urp, param),
|
||||
udmrepo.WithConfigFile(urp.workPath, string(param.BackupLocation.UID)),
|
||||
udmrepo.WithGenOptions(
|
||||
map[string]string{
|
||||
udmrepo.GenOptionOwnerName: udmrepo.GetRepoUser(),
|
||||
udmrepo.GenOptionOwnerDomain: udmrepo.GetRepoDomain(),
|
||||
},
|
||||
),
|
||||
udmrepo.WithStoreOptions(urp, param),
|
||||
udmrepo.WithDescription(repoOpDescFullMaintain),
|
||||
udmrepo.WithDescription(repoConnectDesc),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -121,8 +129,14 @@ func (urp *unifiedRepoProvider) ConnectToRepo(ctx context.Context, param RepoPar
|
||||
repoOption, err := udmrepo.NewRepoOptions(
|
||||
udmrepo.WithPassword(urp, param),
|
||||
udmrepo.WithConfigFile(urp.workPath, string(param.BackupLocation.UID)),
|
||||
udmrepo.WithGenOptions(
|
||||
map[string]string{
|
||||
udmrepo.GenOptionOwnerName: udmrepo.GetRepoUser(),
|
||||
udmrepo.GenOptionOwnerDomain: udmrepo.GetRepoDomain(),
|
||||
},
|
||||
),
|
||||
udmrepo.WithStoreOptions(urp, param),
|
||||
udmrepo.WithDescription(repoOpDescFullMaintain),
|
||||
udmrepo.WithDescription(repoConnectDesc),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -150,8 +164,14 @@ func (urp *unifiedRepoProvider) PrepareRepo(ctx context.Context, param RepoParam
|
||||
repoOption, err := udmrepo.NewRepoOptions(
|
||||
udmrepo.WithPassword(urp, param),
|
||||
udmrepo.WithConfigFile(urp.workPath, string(param.BackupLocation.UID)),
|
||||
udmrepo.WithGenOptions(
|
||||
map[string]string{
|
||||
udmrepo.GenOptionOwnerName: udmrepo.GetRepoUser(),
|
||||
udmrepo.GenOptionOwnerDomain: udmrepo.GetRepoDomain(),
|
||||
},
|
||||
),
|
||||
udmrepo.WithStoreOptions(urp, param),
|
||||
udmrepo.WithDescription(repoOpDescFullMaintain),
|
||||
udmrepo.WithDescription(repoConnectDesc),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -185,7 +205,11 @@ func (urp *unifiedRepoProvider) PruneRepo(ctx context.Context, param RepoParam)
|
||||
repoOption, err := udmrepo.NewRepoOptions(
|
||||
udmrepo.WithPassword(urp, param),
|
||||
udmrepo.WithConfigFile(urp.workPath, string(param.BackupLocation.UID)),
|
||||
udmrepo.WithGenOptions(map[string]string{udmrepo.GenOptionMaintainMode: udmrepo.GenOptionMaintainFull}),
|
||||
udmrepo.WithGenOptions(
|
||||
map[string]string{
|
||||
udmrepo.GenOptionMaintainMode: udmrepo.GenOptionMaintainFull,
|
||||
},
|
||||
),
|
||||
udmrepo.WithDescription(repoOpDescFullMaintain),
|
||||
)
|
||||
|
||||
@@ -214,7 +238,11 @@ func (urp *unifiedRepoProvider) PruneRepoQuick(ctx context.Context, param RepoPa
|
||||
repoOption, err := udmrepo.NewRepoOptions(
|
||||
udmrepo.WithPassword(urp, param),
|
||||
udmrepo.WithConfigFile(urp.workPath, string(param.BackupLocation.UID)),
|
||||
udmrepo.WithGenOptions(map[string]string{udmrepo.GenOptionMaintainMode: udmrepo.GenOptionMaintainQuick}),
|
||||
udmrepo.WithGenOptions(
|
||||
map[string]string{
|
||||
udmrepo.GenOptionMaintainMode: udmrepo.GenOptionMaintainQuick,
|
||||
},
|
||||
),
|
||||
udmrepo.WithDescription(repoOpDescQuickMaintain),
|
||||
)
|
||||
|
||||
@@ -280,7 +308,7 @@ func (urp *unifiedRepoProvider) Forget(ctx context.Context, snapshotID string, p
|
||||
func (urp *unifiedRepoProvider) GetPassword(param interface{}) (string, error) {
|
||||
repoParam, ok := param.(RepoParam)
|
||||
if !ok {
|
||||
return "", errors.New("invalid parameter")
|
||||
return "", errors.Errorf("invalid parameter, expect %T, actual %T", RepoParam{}, param)
|
||||
}
|
||||
|
||||
repoPassword, err := getRepoPassword(urp.credentialGetter.FromSecret, repoParam)
|
||||
@@ -294,7 +322,7 @@ func (urp *unifiedRepoProvider) GetPassword(param interface{}) (string, error) {
|
||||
func (urp *unifiedRepoProvider) GetStoreType(param interface{}) (string, error) {
|
||||
repoParam, ok := param.(RepoParam)
|
||||
if !ok {
|
||||
return "", errors.New("invalid parameter")
|
||||
return "", errors.Errorf("invalid parameter, expect %T, actual %T", RepoParam{}, param)
|
||||
}
|
||||
|
||||
return getStorageType(repoParam.BackupLocation), nil
|
||||
@@ -303,7 +331,7 @@ func (urp *unifiedRepoProvider) GetStoreType(param interface{}) (string, error)
|
||||
func (urp *unifiedRepoProvider) GetStoreOptions(param interface{}) (map[string]string, error) {
|
||||
repoParam, ok := param.(RepoParam)
|
||||
if !ok {
|
||||
return map[string]string{}, errors.New("invalid parameter")
|
||||
return map[string]string{}, errors.Errorf("invalid parameter, expect %T, actual %T", RepoParam{}, param)
|
||||
}
|
||||
|
||||
storeVar, err := funcTable.getStorageVariables(repoParam.BackupLocation, repoParam.BackupRepo.Spec.VolumeNamespace)
|
||||
|
||||
@@ -503,7 +503,7 @@ func TestGetStoreOptions(t *testing.T) {
|
||||
name: "wrong param type",
|
||||
repoParam: struct{}{},
|
||||
expected: map[string]string{},
|
||||
expectedErr: "invalid parameter",
|
||||
expectedErr: "invalid parameter, expect provider.RepoParam, actual struct {}",
|
||||
},
|
||||
{
|
||||
name: "get storage variable fail",
|
||||
|
||||
122
pkg/repository/restic/repository.go
Normal file
122
pkg/repository/restic/repository.go
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
Copyright the Velero contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"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"
|
||||
repokey "github.com/vmware-tanzu/velero/pkg/repository/keys"
|
||||
"github.com/vmware-tanzu/velero/pkg/restic"
|
||||
veleroexec "github.com/vmware-tanzu/velero/pkg/util/exec"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
)
|
||||
|
||||
func NewRepositoryService(store credentials.FileStore, fs filesystem.Interface, log logrus.FieldLogger) *RepositoryService {
|
||||
return &RepositoryService{
|
||||
credentialsFileStore: store,
|
||||
fileSystem: fs,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
type RepositoryService struct {
|
||||
credentialsFileStore credentials.FileStore
|
||||
fileSystem filesystem.Interface
|
||||
log logrus.FieldLogger
|
||||
}
|
||||
|
||||
func (r *RepositoryService) InitRepo(bsl *velerov1api.BackupStorageLocation, repo *velerov1api.BackupRepository) error {
|
||||
return r.exec(restic.InitCommand(repo.Spec.ResticIdentifier), bsl)
|
||||
}
|
||||
|
||||
func (r *RepositoryService) ConnectToRepo(bsl *velerov1api.BackupStorageLocation, repo *velerov1api.BackupRepository) error {
|
||||
snapshotsCmd := restic.SnapshotsCommand(repo.Spec.ResticIdentifier)
|
||||
// use the '--latest=1' flag to minimize the amount of data fetched since
|
||||
// we're just validating that the repo exists and can be authenticated
|
||||
// to.
|
||||
// "--last" is replaced by "--latest=1" in restic v0.12.1
|
||||
snapshotsCmd.ExtraFlags = append(snapshotsCmd.ExtraFlags, "--latest=1")
|
||||
|
||||
return r.exec(snapshotsCmd, bsl)
|
||||
}
|
||||
|
||||
func (r *RepositoryService) PruneRepo(bsl *velerov1api.BackupStorageLocation, repo *velerov1api.BackupRepository) error {
|
||||
return r.exec(restic.PruneCommand(repo.Spec.ResticIdentifier), bsl)
|
||||
}
|
||||
|
||||
func (r *RepositoryService) UnlockRepo(bsl *velerov1api.BackupStorageLocation, repo *velerov1api.BackupRepository) error {
|
||||
return r.exec(restic.UnlockCommand(repo.Spec.ResticIdentifier), bsl)
|
||||
}
|
||||
|
||||
func (r *RepositoryService) Forget(bsl *velerov1api.BackupStorageLocation, repo *velerov1api.BackupRepository, snapshotID string) error {
|
||||
return r.exec(restic.ForgetCommand(repo.Spec.ResticIdentifier, snapshotID), bsl)
|
||||
}
|
||||
|
||||
func (r *RepositoryService) exec(cmd *restic.Command, bsl *velerov1api.BackupStorageLocation) error {
|
||||
file, err := r.credentialsFileStore.Path(repokey.RepoKeySelector())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// ignore error since there's nothing we can do and it's a temp file.
|
||||
defer os.Remove(file)
|
||||
|
||||
cmd.PasswordFile = file
|
||||
|
||||
// if there's a caCert on the ObjectStorage, write it to disk so that it can be passed to restic
|
||||
var caCertFile string
|
||||
if bsl.Spec.ObjectStorage != nil && bsl.Spec.ObjectStorage.CACert != nil {
|
||||
caCertFile, err = restic.TempCACertFile(bsl.Spec.ObjectStorage.CACert, bsl.Name, r.fileSystem)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating temp cacert file")
|
||||
}
|
||||
// ignore error since there's nothing we can do and it's a temp file.
|
||||
defer os.Remove(caCertFile)
|
||||
}
|
||||
cmd.CACertFile = caCertFile
|
||||
|
||||
env, err := restic.CmdEnv(bsl, r.credentialsFileStore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Env = env
|
||||
|
||||
// #4820: restrieve insecureSkipTLSVerify from BSL configuration for
|
||||
// AWS plugin. If nothing is return, that means insecureSkipTLSVerify
|
||||
// is not enable for Restic command.
|
||||
skipTLSRet := restic.GetInsecureSkipTLSVerifyFromBSL(bsl, r.log)
|
||||
if len(skipTLSRet) > 0 {
|
||||
cmd.ExtraFlags = append(cmd.ExtraFlags, skipTLSRet)
|
||||
}
|
||||
|
||||
stdout, stderr, err := veleroexec.RunCommand(cmd.Cmd())
|
||||
r.log.WithFields(logrus.Fields{
|
||||
"repository": cmd.RepoName(),
|
||||
"command": cmd.String(),
|
||||
"stdout": stdout,
|
||||
"stderr": stderr,
|
||||
}).Debugf("Ran restic command")
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error running command=%s, stdout=%s, stderr=%s", cmd.String(), stdout, stderr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
60
pkg/repository/udmrepo/kopialib/backend/azure.go
Normal file
60
pkg/repository/udmrepo/kopialib/backend/azure.go
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
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 backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/kopia/kopia/repo/blob"
|
||||
"github.com/kopia/kopia/repo/blob/azure"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
)
|
||||
|
||||
type AzureBackend struct {
|
||||
options azure.Options
|
||||
}
|
||||
|
||||
func (c *AzureBackend) Setup(ctx context.Context, flags map[string]string) error {
|
||||
var err error
|
||||
c.options.Container, err = mustHaveString(udmrepo.StoreOptionOssBucket, flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.options.StorageAccount, err = mustHaveString(udmrepo.StoreOptionAzureStorageAccount, flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.options.StorageKey, err = mustHaveString(udmrepo.StoreOptionAzureKey, flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.options.Prefix = optionalHaveString(udmrepo.StoreOptionPrefix, flags)
|
||||
c.options.SASToken = optionalHaveString(udmrepo.StoreOptionAzureToken, flags)
|
||||
c.options.StorageDomain = optionalHaveString(udmrepo.StoreOptionAzureDomain, flags)
|
||||
|
||||
c.options.Limits = setupLimits(ctx, flags)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *AzureBackend) Connect(ctx context.Context, isCreate bool) (blob.Storage, error) {
|
||||
return azure.New(ctx, &c.options)
|
||||
}
|
||||
102
pkg/repository/udmrepo/kopialib/backend/azure_test.go
Normal file
102
pkg/repository/udmrepo/kopialib/backend/azure_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
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 backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
|
||||
"github.com/kopia/kopia/repo/blob/azure"
|
||||
"github.com/kopia/kopia/repo/blob/throttling"
|
||||
)
|
||||
|
||||
func TestAzureSetup(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
flags map[string]string
|
||||
expected azure.Options
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "must have bucket name",
|
||||
flags: map[string]string{},
|
||||
expectedErr: "key " + udmrepo.StoreOptionOssBucket + " not found",
|
||||
},
|
||||
{
|
||||
name: "must have storage account",
|
||||
flags: map[string]string{
|
||||
udmrepo.StoreOptionOssBucket: "fake-bucket",
|
||||
},
|
||||
expected: azure.Options{
|
||||
Container: "fake-bucket",
|
||||
},
|
||||
expectedErr: "key " + udmrepo.StoreOptionAzureStorageAccount + " not found",
|
||||
},
|
||||
{
|
||||
name: "must have secret key",
|
||||
flags: map[string]string{
|
||||
udmrepo.StoreOptionOssBucket: "fake-bucket",
|
||||
udmrepo.StoreOptionAzureStorageAccount: "fake-account",
|
||||
},
|
||||
expected: azure.Options{
|
||||
Container: "fake-bucket",
|
||||
StorageAccount: "fake-account",
|
||||
},
|
||||
expectedErr: "key " + udmrepo.StoreOptionAzureKey + " not found",
|
||||
},
|
||||
{
|
||||
name: "with limits",
|
||||
flags: map[string]string{
|
||||
udmrepo.StoreOptionOssBucket: "fake-bucket",
|
||||
udmrepo.StoreOptionAzureStorageAccount: "fake-account",
|
||||
udmrepo.StoreOptionAzureKey: "fake-key",
|
||||
udmrepo.ThrottleOptionReadOps: "100",
|
||||
udmrepo.ThrottleOptionUploadBytes: "200",
|
||||
},
|
||||
expected: azure.Options{
|
||||
Container: "fake-bucket",
|
||||
StorageAccount: "fake-account",
|
||||
StorageKey: "fake-key",
|
||||
Limits: throttling.Limits{
|
||||
ReadsPerSecond: 100,
|
||||
UploadBytesPerSecond: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
azFlags := AzureBackend{}
|
||||
|
||||
err := azFlags.Setup(context.Background(), tc.flags)
|
||||
|
||||
require.Equal(t, tc.expected, azFlags.options)
|
||||
|
||||
if tc.expectedErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -14,17 +14,20 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
package backend
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"context"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/kopia/kopia/repo/blob"
|
||||
)
|
||||
|
||||
func TestRepoKeySelector(t *testing.T) {
|
||||
selector := RepoKeySelector()
|
||||
// Store defines the methods for Kopia to establish a connection to
|
||||
// the backend storage
|
||||
type Store interface {
|
||||
// Setup setups the variables to a specific backend storage
|
||||
Setup(ctx context.Context, flags map[string]string) error
|
||||
|
||||
require.Equal(t, credentialsSecretName, selector.Name)
|
||||
require.Equal(t, credentialsKey, selector.Key)
|
||||
// Connect connects to a specific backend storage with the storage variables
|
||||
Connect(ctx context.Context, isCreate bool) (blob.Storage, error)
|
||||
}
|
||||
83
pkg/repository/udmrepo/kopialib/backend/common.go
Normal file
83
pkg/repository/udmrepo/kopialib/backend/common.go
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
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 backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/kopia/kopia/repo"
|
||||
"github.com/kopia/kopia/repo/blob"
|
||||
"github.com/kopia/kopia/repo/blob/throttling"
|
||||
"github.com/kopia/kopia/repo/content"
|
||||
"github.com/kopia/kopia/repo/encryption"
|
||||
"github.com/kopia/kopia/repo/hashing"
|
||||
"github.com/kopia/kopia/repo/object"
|
||||
"github.com/kopia/kopia/repo/splitter"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
)
|
||||
|
||||
const (
|
||||
maxDataCacheMB = 2000
|
||||
maxMetadataCacheMB = 2000
|
||||
maxCacheDurationSecond = 30
|
||||
)
|
||||
|
||||
func setupLimits(ctx context.Context, flags map[string]string) throttling.Limits {
|
||||
return throttling.Limits{
|
||||
DownloadBytesPerSecond: optionalHaveFloat64(ctx, udmrepo.ThrottleOptionDownloadBytes, flags),
|
||||
ListsPerSecond: optionalHaveFloat64(ctx, udmrepo.ThrottleOptionListOps, flags),
|
||||
ReadsPerSecond: optionalHaveFloat64(ctx, udmrepo.ThrottleOptionReadOps, flags),
|
||||
UploadBytesPerSecond: optionalHaveFloat64(ctx, udmrepo.ThrottleOptionUploadBytes, flags),
|
||||
WritesPerSecond: optionalHaveFloat64(ctx, udmrepo.ThrottleOptionWriteOps, flags),
|
||||
}
|
||||
}
|
||||
|
||||
// SetupNewRepositoryOptions setups the options when creating a new Kopia repository
|
||||
func SetupNewRepositoryOptions(ctx context.Context, flags map[string]string) repo.NewRepositoryOptions {
|
||||
return repo.NewRepositoryOptions{
|
||||
BlockFormat: content.FormattingOptions{
|
||||
Hash: optionalHaveStringWithDefault(udmrepo.StoreOptionGenHashAlgo, flags, hashing.DefaultAlgorithm),
|
||||
Encryption: optionalHaveStringWithDefault(udmrepo.StoreOptionGenEncryptAlgo, flags, encryption.DefaultAlgorithm),
|
||||
},
|
||||
|
||||
ObjectFormat: object.Format{
|
||||
Splitter: optionalHaveStringWithDefault(udmrepo.StoreOptionGenSplitAlgo, flags, splitter.DefaultAlgorithm),
|
||||
},
|
||||
|
||||
RetentionMode: blob.RetentionMode(optionalHaveString(udmrepo.StoreOptionGenRetentionMode, flags)),
|
||||
RetentionPeriod: optionalHaveDuration(ctx, udmrepo.StoreOptionGenRetentionPeriod, flags),
|
||||
}
|
||||
}
|
||||
|
||||
// SetupConnectOptions setups the options when connecting to an existing Kopia repository
|
||||
func SetupConnectOptions(ctx context.Context, repoOptions udmrepo.RepoOptions) repo.ConnectOptions {
|
||||
return repo.ConnectOptions{
|
||||
CachingOptions: content.CachingOptions{
|
||||
MaxCacheSizeBytes: maxDataCacheMB << 20,
|
||||
MaxMetadataCacheSizeBytes: maxMetadataCacheMB << 20,
|
||||
MaxListCacheDuration: content.DurationSeconds(time.Duration(maxCacheDurationSecond) * time.Second),
|
||||
},
|
||||
ClientOptions: repo.ClientOptions{
|
||||
Hostname: optionalHaveString(udmrepo.GenOptionOwnerDomain, repoOptions.GeneralOptions),
|
||||
Username: optionalHaveString(udmrepo.GenOptionOwnerName, repoOptions.GeneralOptions),
|
||||
ReadOnly: optionalHaveBool(ctx, udmrepo.StoreOptionGenReadOnly, repoOptions.GeneralOptions),
|
||||
Description: repoOptions.Description,
|
||||
},
|
||||
}
|
||||
}
|
||||
62
pkg/repository/udmrepo/kopialib/backend/file_system.go
Normal file
62
pkg/repository/udmrepo/kopialib/backend/file_system.go
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
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 backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/kopia/kopia/repo/blob"
|
||||
"github.com/kopia/kopia/repo/blob/filesystem"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
)
|
||||
|
||||
type FsBackend struct {
|
||||
options filesystem.Options
|
||||
}
|
||||
|
||||
const (
|
||||
defaultFileMode = 0o600
|
||||
defaultDirMode = 0o700
|
||||
)
|
||||
|
||||
func (c *FsBackend) Setup(ctx context.Context, flags map[string]string) error {
|
||||
path, err := mustHaveString(udmrepo.StoreOptionFsPath, flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prefix := optionalHaveString(udmrepo.StoreOptionPrefix, flags)
|
||||
|
||||
c.options.Path = filepath.Join(path, prefix)
|
||||
c.options.FileMode = defaultFileMode
|
||||
c.options.DirectoryMode = defaultDirMode
|
||||
|
||||
c.options.Limits = setupLimits(ctx, flags)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *FsBackend) Connect(ctx context.Context, isCreate bool) (blob.Storage, error) {
|
||||
if !filepath.IsAbs(c.options.Path) {
|
||||
return nil, errors.Errorf("filesystem repository path is not absolute, path: %s", c.options.Path)
|
||||
}
|
||||
|
||||
return filesystem.New(ctx, &c.options, isCreate)
|
||||
}
|
||||
54
pkg/repository/udmrepo/kopialib/backend/gcs.go
Normal file
54
pkg/repository/udmrepo/kopialib/backend/gcs.go
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
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 backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/kopia/kopia/repo/blob"
|
||||
"github.com/kopia/kopia/repo/blob/gcs"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
)
|
||||
|
||||
type GCSBackend struct {
|
||||
options gcs.Options
|
||||
}
|
||||
|
||||
func (c *GCSBackend) Setup(ctx context.Context, flags map[string]string) error {
|
||||
var err error
|
||||
c.options.BucketName, err = mustHaveString(udmrepo.StoreOptionOssBucket, flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.options.ServiceAccountCredentialsFile, err = mustHaveString(udmrepo.StoreOptionCredentialFile, flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.options.Prefix = optionalHaveString(udmrepo.StoreOptionPrefix, flags)
|
||||
c.options.ReadOnly = optionalHaveBool(ctx, udmrepo.StoreOptionGcsReadonly, flags)
|
||||
|
||||
c.options.Limits = setupLimits(ctx, flags)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GCSBackend) Connect(ctx context.Context, isCreate bool) (blob.Storage, error) {
|
||||
return gcs.New(ctx, &c.options)
|
||||
}
|
||||
61
pkg/repository/udmrepo/kopialib/backend/gcs_test.go
Normal file
61
pkg/repository/udmrepo/kopialib/backend/gcs_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
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 backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
)
|
||||
|
||||
func TestGcsSetup(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
flags map[string]string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "must have bucket name",
|
||||
flags: map[string]string{},
|
||||
expectedErr: "key " + udmrepo.StoreOptionOssBucket + " not found",
|
||||
},
|
||||
{
|
||||
name: "must have credential file",
|
||||
flags: map[string]string{
|
||||
udmrepo.StoreOptionOssBucket: "fake-bucket",
|
||||
},
|
||||
expectedErr: "key " + udmrepo.StoreOptionCredentialFile + " not found",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
gcsFlags := GCSBackend{}
|
||||
|
||||
err := gcsFlags.Setup(context.Background(), tc.flags)
|
||||
|
||||
if tc.expectedErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
65
pkg/repository/udmrepo/kopialib/backend/mocks/Logger.go
Normal file
65
pkg/repository/udmrepo/kopialib/backend/mocks/Logger.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Code generated by mockery v2.14.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
|
||||
// Logger is an autogenerated mock type for the Logger type
|
||||
type Logger struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Debugf provides a mock function with given fields: msg, args
|
||||
func (_m *Logger) Debugf(msg string, args ...interface{}) {
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, msg)
|
||||
_ca = append(_ca, args...)
|
||||
_m.Called(_ca...)
|
||||
}
|
||||
|
||||
// Debugw provides a mock function with given fields: msg, keyValuePairs
|
||||
func (_m *Logger) Debugw(msg string, keyValuePairs ...interface{}) {
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, msg)
|
||||
_ca = append(_ca, keyValuePairs...)
|
||||
_m.Called(_ca...)
|
||||
}
|
||||
|
||||
// Errorf provides a mock function with given fields: msg, args
|
||||
func (_m *Logger) Errorf(msg string, args ...interface{}) {
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, msg)
|
||||
_ca = append(_ca, args...)
|
||||
_m.Called(_ca...)
|
||||
}
|
||||
|
||||
// Infof provides a mock function with given fields: msg, args
|
||||
func (_m *Logger) Infof(msg string, args ...interface{}) {
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, msg)
|
||||
_ca = append(_ca, args...)
|
||||
_m.Called(_ca...)
|
||||
}
|
||||
|
||||
// Warnf provides a mock function with given fields: msg, args
|
||||
func (_m *Logger) Warnf(msg string, args ...interface{}) {
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, msg)
|
||||
_ca = append(_ca, args...)
|
||||
_m.Called(_ca...)
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewLogger interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewLogger creates a new instance of Logger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewLogger(t mockConstructorTestingTNewLogger) *Logger {
|
||||
mock := &Logger{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
185
pkg/repository/udmrepo/kopialib/backend/mocks/Storage.go
Normal file
185
pkg/repository/udmrepo/kopialib/backend/mocks/Storage.go
Normal file
@@ -0,0 +1,185 @@
|
||||
// Code generated by mockery v2.14.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
blob "github.com/kopia/kopia/repo/blob"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Storage is an autogenerated mock type for the Storage type
|
||||
type Storage struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Close provides a mock function with given fields: ctx
|
||||
func (_m *Storage) Close(ctx context.Context) error {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ConnectionInfo provides a mock function with given fields:
|
||||
func (_m *Storage) ConnectionInfo() blob.ConnectionInfo {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 blob.ConnectionInfo
|
||||
if rf, ok := ret.Get(0).(func() blob.ConnectionInfo); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(blob.ConnectionInfo)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteBlob provides a mock function with given fields: ctx, blobID
|
||||
func (_m *Storage) DeleteBlob(ctx context.Context, blobID blob.ID) error {
|
||||
ret := _m.Called(ctx, blobID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, blob.ID) error); ok {
|
||||
r0 = rf(ctx, blobID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DisplayName provides a mock function with given fields:
|
||||
func (_m *Storage) DisplayName() string {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// FlushCaches provides a mock function with given fields: ctx
|
||||
func (_m *Storage) FlushCaches(ctx context.Context) error {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetBlob provides a mock function with given fields: ctx, blobID, offset, length, output
|
||||
func (_m *Storage) GetBlob(ctx context.Context, blobID blob.ID, offset int64, length int64, output blob.OutputBuffer) error {
|
||||
ret := _m.Called(ctx, blobID, offset, length, output)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, blob.ID, int64, int64, blob.OutputBuffer) error); ok {
|
||||
r0 = rf(ctx, blobID, offset, length, output)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetCapacity provides a mock function with given fields: ctx
|
||||
func (_m *Storage) GetCapacity(ctx context.Context) (blob.Capacity, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
var r0 blob.Capacity
|
||||
if rf, ok := ret.Get(0).(func(context.Context) blob.Capacity); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
r0 = ret.Get(0).(blob.Capacity)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetMetadata provides a mock function with given fields: ctx, blobID
|
||||
func (_m *Storage) GetMetadata(ctx context.Context, blobID blob.ID) (blob.Metadata, error) {
|
||||
ret := _m.Called(ctx, blobID)
|
||||
|
||||
var r0 blob.Metadata
|
||||
if rf, ok := ret.Get(0).(func(context.Context, blob.ID) blob.Metadata); ok {
|
||||
r0 = rf(ctx, blobID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(blob.Metadata)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, blob.ID) error); ok {
|
||||
r1 = rf(ctx, blobID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ListBlobs provides a mock function with given fields: ctx, blobIDPrefix, cb
|
||||
func (_m *Storage) ListBlobs(ctx context.Context, blobIDPrefix blob.ID, cb func(blob.Metadata) error) error {
|
||||
ret := _m.Called(ctx, blobIDPrefix, cb)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, blob.ID, func(blob.Metadata) error) error); ok {
|
||||
r0 = rf(ctx, blobIDPrefix, cb)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// PutBlob provides a mock function with given fields: ctx, blobID, data, opts
|
||||
func (_m *Storage) PutBlob(ctx context.Context, blobID blob.ID, data blob.Bytes, opts blob.PutOptions) error {
|
||||
ret := _m.Called(ctx, blobID, data, opts)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, blob.ID, blob.Bytes, blob.PutOptions) error); ok {
|
||||
r0 = rf(ctx, blobID, data, opts)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewStorage interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewStorage(t mockConstructorTestingTNewStorage) *Storage {
|
||||
mock := &Storage{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
68
pkg/repository/udmrepo/kopialib/backend/mocks/Store.go
Normal file
68
pkg/repository/udmrepo/kopialib/backend/mocks/Store.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Code generated by mockery v2.14.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
blob "github.com/kopia/kopia/repo/blob"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Store is an autogenerated mock type for the Store type
|
||||
type Store struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Connect provides a mock function with given fields: ctx, isCreate
|
||||
func (_m *Store) Connect(ctx context.Context, isCreate bool) (blob.Storage, error) {
|
||||
ret := _m.Called(ctx, isCreate)
|
||||
|
||||
var r0 blob.Storage
|
||||
if rf, ok := ret.Get(0).(func(context.Context, bool) blob.Storage); ok {
|
||||
r0 = rf(ctx, isCreate)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(blob.Storage)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, bool) error); ok {
|
||||
r1 = rf(ctx, isCreate)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Setup provides a mock function with given fields: ctx, flags
|
||||
func (_m *Store) Setup(ctx context.Context, flags map[string]string) error {
|
||||
ret := _m.Called(ctx, flags)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, map[string]string) error); ok {
|
||||
r0 = rf(ctx, flags)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewStore interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewStore creates a new instance of Store. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewStore(t mockConstructorTestingTNewStore) *Store {
|
||||
mock := &Store{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
63
pkg/repository/udmrepo/kopialib/backend/s3.go
Normal file
63
pkg/repository/udmrepo/kopialib/backend/s3.go
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
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 backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/kopia/kopia/repo/blob"
|
||||
"github.com/kopia/kopia/repo/blob/s3"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
)
|
||||
|
||||
type S3Backend struct {
|
||||
options s3.Options
|
||||
}
|
||||
|
||||
func (c *S3Backend) Setup(ctx context.Context, flags map[string]string) error {
|
||||
var err error
|
||||
c.options.BucketName, err = mustHaveString(udmrepo.StoreOptionOssBucket, flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.options.AccessKeyID, err = mustHaveString(udmrepo.StoreOptionS3KeyId, flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.options.SecretAccessKey, err = mustHaveString(udmrepo.StoreOptionS3SecretKey, flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.options.Endpoint = optionalHaveString(udmrepo.StoreOptionS3Endpoint, flags)
|
||||
c.options.Region = optionalHaveString(udmrepo.StoreOptionOssRegion, flags)
|
||||
c.options.Prefix = optionalHaveString(udmrepo.StoreOptionPrefix, flags)
|
||||
c.options.DoNotUseTLS = optionalHaveBool(ctx, udmrepo.StoreOptionS3DisableTls, flags)
|
||||
c.options.DoNotVerifyTLS = optionalHaveBool(ctx, udmrepo.StoreOptionS3DisableTlsVerify, flags)
|
||||
c.options.SessionToken = optionalHaveString(udmrepo.StoreOptionS3Token, flags)
|
||||
|
||||
c.options.Limits = setupLimits(ctx, flags)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *S3Backend) Connect(ctx context.Context, isCreate bool) (blob.Storage, error) {
|
||||
return s3.New(ctx, &c.options)
|
||||
}
|
||||
69
pkg/repository/udmrepo/kopialib/backend/s3_test.go
Normal file
69
pkg/repository/udmrepo/kopialib/backend/s3_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
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 backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
)
|
||||
|
||||
func TestS3Setup(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
flags map[string]string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "must have bucket name",
|
||||
flags: map[string]string{},
|
||||
expectedErr: "key " + udmrepo.StoreOptionOssBucket + " not found",
|
||||
},
|
||||
{
|
||||
name: "must have access key Id",
|
||||
flags: map[string]string{
|
||||
udmrepo.StoreOptionOssBucket: "fake-bucket",
|
||||
},
|
||||
expectedErr: "key " + udmrepo.StoreOptionS3KeyId + " not found",
|
||||
},
|
||||
{
|
||||
name: "must have access key",
|
||||
flags: map[string]string{
|
||||
udmrepo.StoreOptionOssBucket: "fake-bucket",
|
||||
udmrepo.StoreOptionS3KeyId: "fake-key-id",
|
||||
},
|
||||
expectedErr: "key " + udmrepo.StoreOptionS3SecretKey + " not found",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s3Flags := S3Backend{}
|
||||
|
||||
err := s3Flags.Setup(context.Background(), tc.flags)
|
||||
|
||||
if tc.expectedErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
89
pkg/repository/udmrepo/kopialib/backend/utils.go
Normal file
89
pkg/repository/udmrepo/kopialib/backend/utils.go
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
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 backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/kopia/kopia/repo/logging"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func mustHaveString(key string, flags map[string]string) (string, error) {
|
||||
if value, exist := flags[key]; exist {
|
||||
return value, nil
|
||||
} else {
|
||||
return "", errors.New("key " + key + " not found")
|
||||
}
|
||||
}
|
||||
|
||||
func optionalHaveString(key string, flags map[string]string) string {
|
||||
return optionalHaveStringWithDefault(key, flags, "")
|
||||
}
|
||||
|
||||
func optionalHaveBool(ctx context.Context, key string, flags map[string]string) bool {
|
||||
if value, exist := flags[key]; exist {
|
||||
ret, err := strconv.ParseBool(value)
|
||||
if err == nil {
|
||||
return ret
|
||||
}
|
||||
|
||||
backendLog()(ctx).Errorf("Ignore %s, value [%s] is invalid, err %v", key, value, err)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func optionalHaveFloat64(ctx context.Context, key string, flags map[string]string) float64 {
|
||||
if value, exist := flags[key]; exist {
|
||||
ret, err := strconv.ParseFloat(value, 64)
|
||||
if err == nil {
|
||||
return ret
|
||||
}
|
||||
|
||||
backendLog()(ctx).Errorf("Ignore %s, value [%s] is invalid, err %v", key, value, err)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func optionalHaveStringWithDefault(key string, flags map[string]string, defValue string) string {
|
||||
if value, exist := flags[key]; exist {
|
||||
return value
|
||||
} else {
|
||||
return defValue
|
||||
}
|
||||
}
|
||||
|
||||
func optionalHaveDuration(ctx context.Context, key string, flags map[string]string) time.Duration {
|
||||
if value, exist := flags[key]; exist {
|
||||
ret, err := time.ParseDuration(value)
|
||||
if err == nil {
|
||||
return ret
|
||||
}
|
||||
|
||||
backendLog()(ctx).Errorf("Ignore %s, value [%s] is invalid, err %v", key, value, err)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func backendLog() func(ctx context.Context) logging.Logger {
|
||||
return logging.Module("kopialib-bd")
|
||||
}
|
||||
87
pkg/repository/udmrepo/kopialib/backend/utils_test.go
Normal file
87
pkg/repository/udmrepo/kopialib/backend/utils_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
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 backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/kopia/kopia/repo/logging"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
storagemocks "github.com/vmware-tanzu/velero/pkg/repository/udmrepo/kopialib/backend/mocks"
|
||||
)
|
||||
|
||||
func TestOptionalHaveBool(t *testing.T) {
|
||||
var expectMsg string
|
||||
testCases := []struct {
|
||||
name string
|
||||
key string
|
||||
flags map[string]string
|
||||
logger *storagemocks.Logger
|
||||
retFuncErrorf func(mock.Arguments)
|
||||
expectMsg string
|
||||
retValue bool
|
||||
}{
|
||||
{
|
||||
name: "key not exist",
|
||||
key: "fake-key",
|
||||
flags: map[string]string{},
|
||||
retValue: false,
|
||||
},
|
||||
{
|
||||
name: "value valid",
|
||||
key: "fake-key",
|
||||
flags: map[string]string{
|
||||
"fake-key": "true",
|
||||
},
|
||||
retValue: true,
|
||||
},
|
||||
{
|
||||
name: "value invalid",
|
||||
key: "fake-key",
|
||||
flags: map[string]string{
|
||||
"fake-key": "fake-value",
|
||||
},
|
||||
logger: new(storagemocks.Logger),
|
||||
retFuncErrorf: func(args mock.Arguments) {
|
||||
expectMsg = fmt.Sprintf(args[0].(string), args[1].(string), args[2].(string), args[3].(error))
|
||||
},
|
||||
expectMsg: "Ignore fake-key, value [fake-value] is invalid, err strconv.ParseBool: parsing \"fake-value\": invalid syntax",
|
||||
retValue: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.logger != nil {
|
||||
tc.logger.On("Errorf", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(tc.retFuncErrorf)
|
||||
}
|
||||
|
||||
ctx := logging.WithLogger(context.Background(), func(module string) logging.Logger {
|
||||
return tc.logger
|
||||
})
|
||||
|
||||
retValue := optionalHaveBool(ctx, tc.key, tc.flags)
|
||||
|
||||
require.Equal(t, retValue, tc.retValue)
|
||||
require.Equal(t, tc.expectMsg, expectMsg)
|
||||
})
|
||||
}
|
||||
}
|
||||
160
pkg/repository/udmrepo/kopialib/repo_init.go
Normal file
160
pkg/repository/udmrepo/kopialib/repo_init.go
Normal file
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
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 kopialib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/kopia/kopia/repo"
|
||||
"github.com/kopia/kopia/repo/blob"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo/kopialib/backend"
|
||||
)
|
||||
|
||||
type kopiaBackendStore struct {
|
||||
name string
|
||||
description string
|
||||
store backend.Store
|
||||
}
|
||||
|
||||
// backendStores lists the supported backend storages at present
|
||||
var backendStores []kopiaBackendStore = []kopiaBackendStore{
|
||||
{udmrepo.StorageTypeAzure, "an Azure blob storage", &backend.AzureBackend{}},
|
||||
{udmrepo.StorageTypeFs, "a filesystem", &backend.FsBackend{}},
|
||||
{udmrepo.StorageTypeGcs, "a Google Cloud Storage bucket", &backend.GCSBackend{}},
|
||||
{udmrepo.StorageTypeS3, "an S3 bucket", &backend.S3Backend{}},
|
||||
}
|
||||
|
||||
// CreateBackupRepo creates a Kopia repository and then connect to it.
|
||||
// The storage must be empty, otherwise, it will fail
|
||||
func CreateBackupRepo(ctx context.Context, repoOption udmrepo.RepoOptions) error {
|
||||
if repoOption.ConfigFilePath == "" {
|
||||
return errors.New("invalid config file path")
|
||||
}
|
||||
|
||||
backendStore, err := setupBackendStore(ctx, repoOption.StorageType, repoOption.StorageOptions)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error to setup backend storage")
|
||||
}
|
||||
|
||||
st, err := backendStore.store.Connect(ctx, true)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error to connect to storage")
|
||||
}
|
||||
|
||||
err = createWithStorage(ctx, st, repoOption)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error to create repo with storage")
|
||||
}
|
||||
|
||||
err = connectWithStorage(ctx, st, repoOption)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error to connect repo with storage")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConnectBackupRepo connects to an existing Kopia repository.
|
||||
// If the repository doesn't exist, it will fail
|
||||
func ConnectBackupRepo(ctx context.Context, repoOption udmrepo.RepoOptions) error {
|
||||
if repoOption.ConfigFilePath == "" {
|
||||
return errors.New("invalid config file path")
|
||||
}
|
||||
|
||||
backendStore, err := setupBackendStore(ctx, repoOption.StorageType, repoOption.StorageOptions)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error to setup backend storage")
|
||||
}
|
||||
|
||||
st, err := backendStore.store.Connect(ctx, false)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error to connect to storage")
|
||||
}
|
||||
|
||||
err = connectWithStorage(ctx, st, repoOption)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error to connect repo with storage")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findBackendStore(storage string) *kopiaBackendStore {
|
||||
for _, options := range backendStores {
|
||||
if strings.EqualFold(options.name, storage) {
|
||||
return &options
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupBackendStore(ctx context.Context, storageType string, storageOptions map[string]string) (*kopiaBackendStore, error) {
|
||||
backendStore := findBackendStore(storageType)
|
||||
if backendStore == nil {
|
||||
return nil, errors.New("error to find storage type")
|
||||
}
|
||||
|
||||
err := backendStore.store.Setup(ctx, storageOptions)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error to setup storage")
|
||||
}
|
||||
|
||||
return backendStore, nil
|
||||
}
|
||||
|
||||
func createWithStorage(ctx context.Context, st blob.Storage, repoOption udmrepo.RepoOptions) error {
|
||||
err := ensureEmpty(ctx, st)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error to ensure repository storage empty")
|
||||
}
|
||||
|
||||
options := backend.SetupNewRepositoryOptions(ctx, repoOption.GeneralOptions)
|
||||
|
||||
if err := repo.Initialize(ctx, st, &options, repoOption.RepoPassword); err != nil {
|
||||
return errors.Wrap(err, "error to initialize repository")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func connectWithStorage(ctx context.Context, st blob.Storage, repoOption udmrepo.RepoOptions) error {
|
||||
options := backend.SetupConnectOptions(ctx, repoOption)
|
||||
if err := repo.Connect(ctx, repoOption.ConfigFilePath, st, repoOption.RepoPassword, &options); err != nil {
|
||||
return errors.Wrap(err, "error to connect to repository")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureEmpty(ctx context.Context, s blob.Storage) error {
|
||||
hasDataError := errors.Errorf("has data")
|
||||
|
||||
err := s.ListBlobs(ctx, "", func(cb blob.Metadata) error {
|
||||
return hasDataError
|
||||
})
|
||||
|
||||
if errors.Is(err, hasDataError) {
|
||||
return errors.New("found existing data in storage location")
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "error to list blobs")
|
||||
}
|
||||
237
pkg/repository/udmrepo/kopialib/repo_init_test.go
Normal file
237
pkg/repository/udmrepo/kopialib/repo_init_test.go
Normal file
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
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 kopialib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
storagemocks "github.com/vmware-tanzu/velero/pkg/repository/udmrepo/kopialib/backend/mocks"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type comparableError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (ce *comparableError) Error() string {
|
||||
return ce.message
|
||||
}
|
||||
|
||||
func (ce *comparableError) Is(err error) bool {
|
||||
return err.Error() == ce.message
|
||||
}
|
||||
|
||||
func TestCreateBackupRepo(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
backendStore *storagemocks.Store
|
||||
repoOptions udmrepo.RepoOptions
|
||||
connectErr error
|
||||
setupError error
|
||||
returnStore *storagemocks.Storage
|
||||
storeListErr error
|
||||
getBlobErr error
|
||||
listBlobErr error
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "invalid config file",
|
||||
expectedErr: "invalid config file path",
|
||||
},
|
||||
{
|
||||
name: "storage setup fail, invalid type",
|
||||
repoOptions: udmrepo.RepoOptions{
|
||||
ConfigFilePath: "fake-file",
|
||||
},
|
||||
expectedErr: "error to setup backend storage: error to find storage type",
|
||||
},
|
||||
{
|
||||
name: "storage setup fail, backend store steup fail",
|
||||
repoOptions: udmrepo.RepoOptions{
|
||||
ConfigFilePath: "fake-file",
|
||||
StorageType: udmrepo.StorageTypeAzure,
|
||||
},
|
||||
backendStore: new(storagemocks.Store),
|
||||
setupError: errors.New("fake-setup-error"),
|
||||
expectedErr: "error to setup backend storage: error to setup storage: fake-setup-error",
|
||||
},
|
||||
{
|
||||
name: "storage connect fail",
|
||||
repoOptions: udmrepo.RepoOptions{
|
||||
ConfigFilePath: "fake-file",
|
||||
StorageType: udmrepo.StorageTypeAzure,
|
||||
},
|
||||
backendStore: new(storagemocks.Store),
|
||||
connectErr: errors.New("fake-connect-error"),
|
||||
expectedErr: "error to connect to storage: fake-connect-error",
|
||||
},
|
||||
{
|
||||
name: "create repository error, exist blobs",
|
||||
repoOptions: udmrepo.RepoOptions{
|
||||
ConfigFilePath: "fake-file",
|
||||
StorageType: udmrepo.StorageTypeAzure,
|
||||
},
|
||||
backendStore: new(storagemocks.Store),
|
||||
returnStore: new(storagemocks.Storage),
|
||||
listBlobErr: &comparableError{
|
||||
message: "has data",
|
||||
},
|
||||
expectedErr: "error to create repo with storage: error to ensure repository storage empty: found existing data in storage location",
|
||||
},
|
||||
{
|
||||
name: "create repository error, error list blobs",
|
||||
repoOptions: udmrepo.RepoOptions{
|
||||
ConfigFilePath: "fake-file",
|
||||
StorageType: udmrepo.StorageTypeAzure,
|
||||
},
|
||||
backendStore: new(storagemocks.Store),
|
||||
returnStore: new(storagemocks.Storage),
|
||||
listBlobErr: errors.New("fake-list-blob-error"),
|
||||
expectedErr: "error to create repo with storage: error to ensure repository storage empty: error to list blobs: fake-list-blob-error",
|
||||
},
|
||||
{
|
||||
name: "create repository error, initialize error",
|
||||
repoOptions: udmrepo.RepoOptions{
|
||||
ConfigFilePath: "fake-file",
|
||||
StorageType: udmrepo.StorageTypeAzure,
|
||||
},
|
||||
backendStore: new(storagemocks.Store),
|
||||
returnStore: new(storagemocks.Storage),
|
||||
getBlobErr: errors.New("fake-list-blob-error-01"),
|
||||
expectedErr: "error to create repo with storage: error to initialize repository: unexpected error when checking for format blob: fake-list-blob-error-01",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
backendStores = []kopiaBackendStore{
|
||||
{udmrepo.StorageTypeAzure, "fake store", tc.backendStore},
|
||||
{udmrepo.StorageTypeFs, "fake store", tc.backendStore},
|
||||
{udmrepo.StorageTypeGcs, "fake store", tc.backendStore},
|
||||
{udmrepo.StorageTypeS3, "fake store", tc.backendStore},
|
||||
}
|
||||
|
||||
if tc.backendStore != nil {
|
||||
tc.backendStore.On("Connect", mock.Anything, mock.Anything, mock.Anything).Return(tc.returnStore, tc.connectErr)
|
||||
tc.backendStore.On("Setup", mock.Anything, mock.Anything, mock.Anything).Return(tc.setupError)
|
||||
}
|
||||
|
||||
if tc.returnStore != nil {
|
||||
tc.returnStore.On("ListBlobs", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.listBlobErr)
|
||||
tc.returnStore.On("GetBlob", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.getBlobErr)
|
||||
}
|
||||
|
||||
err := CreateBackupRepo(context.Background(), tc.repoOptions)
|
||||
|
||||
if tc.expectedErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectBackupRepo(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
backendStore *storagemocks.Store
|
||||
repoOptions udmrepo.RepoOptions
|
||||
connectErr error
|
||||
setupError error
|
||||
returnStore *storagemocks.Storage
|
||||
getBlobErr error
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "invalid config file",
|
||||
expectedErr: "invalid config file path",
|
||||
},
|
||||
{
|
||||
name: "storage setup fail, invalid type",
|
||||
repoOptions: udmrepo.RepoOptions{
|
||||
ConfigFilePath: "fake-file",
|
||||
},
|
||||
expectedErr: "error to setup backend storage: error to find storage type",
|
||||
},
|
||||
{
|
||||
name: "storage setup fail, backend store steup fail",
|
||||
repoOptions: udmrepo.RepoOptions{
|
||||
ConfigFilePath: "fake-file",
|
||||
StorageType: udmrepo.StorageTypeAzure,
|
||||
},
|
||||
backendStore: new(storagemocks.Store),
|
||||
setupError: errors.New("fake-setup-error"),
|
||||
expectedErr: "error to setup backend storage: error to setup storage: fake-setup-error",
|
||||
},
|
||||
{
|
||||
name: "storage connect fail",
|
||||
repoOptions: udmrepo.RepoOptions{
|
||||
ConfigFilePath: "fake-file",
|
||||
StorageType: udmrepo.StorageTypeAzure,
|
||||
},
|
||||
backendStore: new(storagemocks.Store),
|
||||
connectErr: errors.New("fake-connect-error"),
|
||||
expectedErr: "error to connect to storage: fake-connect-error",
|
||||
},
|
||||
{
|
||||
name: "connect repository error",
|
||||
repoOptions: udmrepo.RepoOptions{
|
||||
ConfigFilePath: "fake-file",
|
||||
StorageType: udmrepo.StorageTypeAzure,
|
||||
},
|
||||
backendStore: new(storagemocks.Store),
|
||||
returnStore: new(storagemocks.Storage),
|
||||
getBlobErr: errors.New("fake-get-blob-error"),
|
||||
expectedErr: "error to connect repo with storage: error to connect to repository: unable to read format blob: fake-get-blob-error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
backendStores = []kopiaBackendStore{
|
||||
{udmrepo.StorageTypeAzure, "fake store", tc.backendStore},
|
||||
{udmrepo.StorageTypeFs, "fake store", tc.backendStore},
|
||||
{udmrepo.StorageTypeGcs, "fake store", tc.backendStore},
|
||||
{udmrepo.StorageTypeS3, "fake store", tc.backendStore},
|
||||
}
|
||||
|
||||
if tc.backendStore != nil {
|
||||
tc.backendStore.On("Connect", mock.Anything, mock.Anything, mock.Anything).Return(tc.returnStore, tc.connectErr)
|
||||
tc.backendStore.On("Setup", mock.Anything, mock.Anything, mock.Anything).Return(tc.setupError)
|
||||
}
|
||||
|
||||
if tc.returnStore != nil {
|
||||
tc.returnStore.On("GetBlob", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.getBlobErr)
|
||||
}
|
||||
|
||||
err := ConnectBackupRepo(context.Background(), tc.repoOptions)
|
||||
|
||||
if tc.expectedErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,9 @@ const (
|
||||
GenOptionMaintainFull = "full"
|
||||
GenOptionMaintainQuick = "quick"
|
||||
|
||||
GenOptionOwnerName = "username"
|
||||
GenOptionOwnerDomain = "domainname"
|
||||
|
||||
StoreOptionS3KeyId = "accessKeyID"
|
||||
StoreOptionS3Provider = "providerName"
|
||||
StoreOptionS3SecretKey = "secretAccessKey"
|
||||
@@ -56,6 +59,14 @@ const (
|
||||
StoreOptionPrefix = "prefix"
|
||||
StoreOptionPrefixName = "unified-repo"
|
||||
|
||||
StoreOptionGenHashAlgo = "hashAlgo"
|
||||
StoreOptionGenEncryptAlgo = "encryptAlgo"
|
||||
StoreOptionGenSplitAlgo = "splitAlgo"
|
||||
|
||||
StoreOptionGenRetentionMode = "retentionMode"
|
||||
StoreOptionGenRetentionPeriod = "retentionPeriod"
|
||||
StoreOptionGenReadOnly = "readOnly"
|
||||
|
||||
ThrottleOptionReadOps = "readOPS"
|
||||
ThrottleOptionWriteOps = "writeOPS"
|
||||
ThrottleOptionListOps = "listOPS"
|
||||
@@ -63,6 +74,11 @@ const (
|
||||
ThrottleOptionDownloadBytes = "downloadBytes"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultUsername = "default"
|
||||
defaultDomain = "default"
|
||||
)
|
||||
|
||||
type RepoOptions struct {
|
||||
// StorageType is a repository specific string to identify a backup storage, i.e., "s3", "filesystem"
|
||||
StorageType string
|
||||
@@ -80,59 +96,72 @@ type RepoOptions struct {
|
||||
Description string
|
||||
}
|
||||
|
||||
// PasswordGetter defines the method to get a repository password.
|
||||
type PasswordGetter interface {
|
||||
GetPassword(param interface{}) (string, error)
|
||||
}
|
||||
|
||||
// StoreOptionsGetter defines the methods to get the storage related options.
|
||||
type StoreOptionsGetter interface {
|
||||
GetStoreType(param interface{}) (string, error)
|
||||
GetStoreOptions(param interface{}) (map[string]string, error)
|
||||
}
|
||||
|
||||
func NewRepoOptions(options ...func(*RepoOptions) error) (*RepoOptions, error) {
|
||||
ro := &RepoOptions{}
|
||||
for _, o := range options {
|
||||
err := o(ro)
|
||||
// NewRepoOptions creates a new RepoOptions for different purpose
|
||||
func NewRepoOptions(optionFuncs ...func(*RepoOptions) error) (*RepoOptions, error) {
|
||||
options := &RepoOptions{
|
||||
GeneralOptions: make(map[string]string),
|
||||
StorageOptions: make(map[string]string),
|
||||
}
|
||||
|
||||
for _, optionFunc := range optionFuncs {
|
||||
err := optionFunc(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ro, nil
|
||||
return options, nil
|
||||
}
|
||||
|
||||
// WithPassword sets the RepoPassword to RepoOptions, the password is acquired through
|
||||
// the provided interface
|
||||
func WithPassword(getter PasswordGetter, param interface{}) func(*RepoOptions) error {
|
||||
return func(ro *RepoOptions) error {
|
||||
return func(options *RepoOptions) error {
|
||||
password, err := getter.GetPassword(param)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ro.RepoPassword = password
|
||||
options.RepoPassword = password
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithConfigFile sets the ConfigFilePath to RepoOptions
|
||||
func WithConfigFile(workPath string, repoID string) func(*RepoOptions) error {
|
||||
return func(ro *RepoOptions) error {
|
||||
ro.ConfigFilePath = getRepoConfigFile(workPath, repoID)
|
||||
return func(options *RepoOptions) error {
|
||||
options.ConfigFilePath = getRepoConfigFile(workPath, repoID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithGenOptions sets the GeneralOptions to RepoOptions
|
||||
func WithGenOptions(genOptions map[string]string) func(*RepoOptions) error {
|
||||
return func(ro *RepoOptions) error {
|
||||
return func(options *RepoOptions) error {
|
||||
for k, v := range genOptions {
|
||||
ro.GeneralOptions[k] = v
|
||||
options.GeneralOptions[k] = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithStoreOptions sets the StorageOptions to RepoOptions, the store options are acquired through
|
||||
// the provided interface
|
||||
func WithStoreOptions(getter StoreOptionsGetter, param interface{}) func(*RepoOptions) error {
|
||||
return func(ro *RepoOptions) error {
|
||||
return func(options *RepoOptions) error {
|
||||
storeType, err := getter.GetStoreType(param)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -143,23 +172,34 @@ func WithStoreOptions(getter StoreOptionsGetter, param interface{}) func(*RepoOp
|
||||
return err
|
||||
}
|
||||
|
||||
ro.StorageType = storeType
|
||||
options.StorageType = storeType
|
||||
|
||||
for k, v := range storeOptions {
|
||||
ro.StorageOptions[k] = v
|
||||
options.StorageOptions[k] = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDescription sets the Description to RepoOptions
|
||||
func WithDescription(desc string) func(*RepoOptions) error {
|
||||
return func(ro *RepoOptions) error {
|
||||
ro.Description = desc
|
||||
return func(options *RepoOptions) error {
|
||||
options.Description = desc
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetRepoUser returns the default username that is used to manipulate the Unified Repo
|
||||
func GetRepoUser() string {
|
||||
return defaultUsername
|
||||
}
|
||||
|
||||
// GetRepoDomain returns the default user domain that is used to manipulate the Unified Repo
|
||||
func GetRepoDomain() string {
|
||||
return defaultDomain
|
||||
}
|
||||
|
||||
func getRepoConfigFile(workPath string, repoID string) string {
|
||||
if workPath == "" {
|
||||
workPath = filepath.Join(os.Getenv("HOME"), "udmrepo")
|
||||
@@ -22,16 +22,8 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultUsername = "default"
|
||||
defaultDomain = "default"
|
||||
)
|
||||
|
||||
// Create creates an instance of BackupRepoService
|
||||
func Create(logger logrus.FieldLogger) udmrepo.BackupRepoService {
|
||||
///TODO: create from kopiaLib
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetRepoUser() (username, domain string) {
|
||||
return defaultUsername, defaultDomain
|
||||
}
|
||||
|
||||
@@ -20,9 +20,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -49,6 +51,14 @@ const (
|
||||
// DefaultVolumesToRestic specifies whether restic should be used, by default, to
|
||||
// take backup of all pod volumes.
|
||||
DefaultVolumesToRestic = false
|
||||
|
||||
// insecureSkipTLSVerifyKey is the flag in BackupStorageLocation's config
|
||||
// to indicate whether to skip TLS verify to setup insecure HTTPS connection.
|
||||
insecureSkipTLSVerifyKey = "insecureSkipTLSVerify"
|
||||
|
||||
// resticInsecureTLSFlag is the flag for Restic command line to indicate
|
||||
// skip TLS verify on https connection.
|
||||
resticInsecureTLSFlag = "--insecure-tls"
|
||||
)
|
||||
|
||||
// SnapshotIdentifier uniquely identifies a restic snapshot
|
||||
@@ -176,3 +186,22 @@ func CmdEnv(backupLocation *velerov1api.BackupStorageLocation, credentialFileSto
|
||||
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// GetInsecureSkipTLSVerifyFromBSL get insecureSkipTLSVerify flag from BSL configuration,
|
||||
// Then return --insecure-tls flag with boolean value as result.
|
||||
func GetInsecureSkipTLSVerifyFromBSL(backupLocation *velerov1api.BackupStorageLocation, logger logrus.FieldLogger) string {
|
||||
result := ""
|
||||
|
||||
if backupLocation == nil {
|
||||
logger.Info("bsl is nil. return empty.")
|
||||
return result
|
||||
}
|
||||
|
||||
if insecure, _ := strconv.ParseBool(backupLocation.Spec.Config[insecureSkipTLSVerifyKey]); insecure {
|
||||
logger.Debugf("set --insecure-tls=true for Restic command according to BSL %s config", backupLocation.Name)
|
||||
result = resticInsecureTLSFlag + "=true"
|
||||
return result
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
@@ -216,3 +217,98 @@ func TestTempCACertFile(t *testing.T) {
|
||||
|
||||
os.Remove(fileName)
|
||||
}
|
||||
|
||||
func TestGetInsecureSkipTLSVerifyFromBSL(t *testing.T) {
|
||||
log := logrus.StandardLogger()
|
||||
tests := []struct {
|
||||
name string
|
||||
backupLocation *velerov1api.BackupStorageLocation
|
||||
logger logrus.FieldLogger
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"Test with nil BSL. Should return empty string.",
|
||||
nil,
|
||||
log,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Test BSL with no configuration. Should return empty string.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "azure",
|
||||
},
|
||||
},
|
||||
log,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Test with AWS BSL's insecureSkipTLSVerify set to false.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "aws",
|
||||
Config: map[string]string{
|
||||
"insecureSkipTLSVerify": "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
log,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Test with AWS BSL's insecureSkipTLSVerify set to true.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "aws",
|
||||
Config: map[string]string{
|
||||
"insecureSkipTLSVerify": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
log,
|
||||
"--insecure-tls=true",
|
||||
},
|
||||
{
|
||||
"Test with Azure BSL's insecureSkipTLSVerify set to invalid.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "azure",
|
||||
Config: map[string]string{
|
||||
"insecureSkipTLSVerify": "invalid",
|
||||
},
|
||||
},
|
||||
},
|
||||
log,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Test with GCP without insecureSkipTLSVerify.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "gcp",
|
||||
Config: map[string]string{},
|
||||
},
|
||||
},
|
||||
log,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Test with AWS without config.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "aws",
|
||||
},
|
||||
},
|
||||
log,
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res := GetInsecureSkipTLSVerifyFromBSL(test.backupLocation, test.logger)
|
||||
|
||||
assert.Equal(t, test.expected, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
Copyright the Velero contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
|
||||
import (
|
||||
"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()
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
/*
|
||||
Copyright the Velero contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
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"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
|
||||
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"
|
||||
)
|
||||
|
||||
// RepositoryManager executes commands against restic repositories.
|
||||
type RepositoryManager interface {
|
||||
// InitRepo initializes a repo with the specified name and identifier.
|
||||
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.BackupRepository) error
|
||||
|
||||
// PruneRepo deletes unused data from a repo.
|
||||
PruneRepo(repo *velerov1api.BackupRepository) error
|
||||
|
||||
// UnlockRepo removes stale locks from a repo.
|
||||
UnlockRepo(repo *velerov1api.BackupRepository) error
|
||||
|
||||
// Forget removes a snapshot from the list of
|
||||
// available snapshots in a repo.
|
||||
Forget(context.Context, SnapshotIdentifier) error
|
||||
|
||||
podvolume.BackupperFactory
|
||||
|
||||
podvolume.RestorerFactory
|
||||
}
|
||||
|
||||
type repositoryManager struct {
|
||||
namespace string
|
||||
veleroClient clientset.Interface
|
||||
repoLister velerov1listers.BackupRepositoryLister
|
||||
repoInformerSynced cache.InformerSynced
|
||||
kbClient kbclient.Client
|
||||
log logrus.FieldLogger
|
||||
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 (
|
||||
// insecureSkipTLSVerifyKey is the flag in BackupStorageLocation's config
|
||||
// to indicate whether to skip TLS verify to setup insecure HTTPS connection.
|
||||
insecureSkipTLSVerifyKey = "insecureSkipTLSVerify"
|
||||
|
||||
// resticInsecureTLSFlag is the flag for Restic command line to indicate
|
||||
// skip TLS verify on https connection.
|
||||
resticInsecureTLSFlag = "--insecure-tls"
|
||||
)
|
||||
|
||||
// NewRepositoryManager constructs a RepositoryManager.
|
||||
func NewRepositoryManager(
|
||||
ctx context.Context,
|
||||
namespace string,
|
||||
veleroClient clientset.Interface,
|
||||
repoInformer velerov1informers.BackupRepositoryInformer,
|
||||
repoClient velerov1client.BackupRepositoriesGetter,
|
||||
kbClient kbclient.Client,
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter,
|
||||
pvClient corev1client.PersistentVolumesGetter,
|
||||
credentialFileStore credentials.FileStore,
|
||||
log logrus.FieldLogger,
|
||||
) (RepositoryManager, error) {
|
||||
rm := &repositoryManager{
|
||||
namespace: namespace,
|
||||
veleroClient: veleroClient,
|
||||
repoLister: repoInformer.Lister(),
|
||||
repoInformerSynced: repoInformer.Informer().HasSynced,
|
||||
kbClient: kbClient,
|
||||
pvcClient: pvcClient,
|
||||
pvClient: pvClient,
|
||||
credentialsFileStore: credentialFileStore,
|
||||
log: log,
|
||||
ctx: ctx,
|
||||
|
||||
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) InitRepo(repo *velerov1api.BackupRepository) error {
|
||||
// restic init requires an exclusive lock
|
||||
rm.repoLocker.LockExclusive(repo.Name)
|
||||
defer rm.repoLocker.UnlockExclusive(repo.Name)
|
||||
|
||||
return rm.exec(InitCommand(repo.Spec.ResticIdentifier), repo.Spec.BackupStorageLocation)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
snapshotsCmd := SnapshotsCommand(repo.Spec.ResticIdentifier)
|
||||
// use the '--latest=1' flag to minimize the amount of data fetched since
|
||||
// we're just validating that the repo exists and can be authenticated
|
||||
// to.
|
||||
// "--last" is replaced by "--latest=1" in restic v0.12.1
|
||||
snapshotsCmd.ExtraFlags = append(snapshotsCmd.ExtraFlags, "--latest=1")
|
||||
|
||||
return rm.exec(snapshotsCmd, repo.Spec.BackupStorageLocation)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return rm.exec(PruneCommand(repo.Spec.ResticIdentifier), repo.Spec.BackupStorageLocation)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return rm.exec(UnlockCommand(repo.Spec.ResticIdentifier), repo.Spec.BackupStorageLocation)
|
||||
}
|
||||
|
||||
func (rm *repositoryManager) Forget(ctx context.Context, snapshot SnapshotIdentifier) error {
|
||||
// We can't wait for this in the constructor, because this informer is coming
|
||||
// from the shared informer factory, which isn't started until *after* the repo
|
||||
// manager is instantiated & passed to the controller constructors. We'd get a
|
||||
// deadlock if we tried to wait for this in the constructor.
|
||||
if !cache.WaitForCacheSync(ctx.Done(), rm.repoInformerSynced) {
|
||||
return errors.New("timed out waiting for cache to sync")
|
||||
}
|
||||
|
||||
repo, err := rm.repoEnsurer.EnsureRepo(ctx, rm.namespace, snapshot.VolumeNamespace, snapshot.BackupStorageLocation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// restic forget requires an exclusive lock
|
||||
rm.repoLocker.LockExclusive(repo.Name)
|
||||
defer rm.repoLocker.UnlockExclusive(repo.Name)
|
||||
|
||||
return rm.exec(ForgetCommand(repo.Spec.ResticIdentifier, snapshot.SnapshotID), repo.Spec.BackupStorageLocation)
|
||||
}
|
||||
|
||||
func (rm *repositoryManager) exec(cmd *Command, backupLocation string) error {
|
||||
file, err := rm.credentialsFileStore.Path(repokey.RepoKeySelector())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// ignore error since there's nothing we can do and it's a temp file.
|
||||
defer os.Remove(file)
|
||||
|
||||
cmd.PasswordFile = file
|
||||
|
||||
loc := &velerov1api.BackupStorageLocation{}
|
||||
if err := rm.kbClient.Get(context.Background(), kbclient.ObjectKey{
|
||||
Namespace: rm.namespace,
|
||||
Name: backupLocation,
|
||||
}, loc); err != nil {
|
||||
return errors.Wrap(err, "error getting backup storage location")
|
||||
}
|
||||
|
||||
// if there's a caCert on the ObjectStorage, write it to disk so that it can be passed to restic
|
||||
var caCertFile string
|
||||
if loc.Spec.ObjectStorage != nil && loc.Spec.ObjectStorage.CACert != nil {
|
||||
caCertFile, err = TempCACertFile(loc.Spec.ObjectStorage.CACert, backupLocation, rm.fileSystem)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating temp cacert file")
|
||||
}
|
||||
// ignore error since there's nothing we can do and it's a temp file.
|
||||
defer os.Remove(caCertFile)
|
||||
}
|
||||
cmd.CACertFile = caCertFile
|
||||
|
||||
env, err := CmdEnv(loc, rm.credentialsFileStore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Env = env
|
||||
|
||||
// #4820: restrieve insecureSkipTLSVerify from BSL configuration for
|
||||
// AWS plugin. If nothing is return, that means insecureSkipTLSVerify
|
||||
// is not enable for Restic command.
|
||||
skipTLSRet := GetInsecureSkipTLSVerifyFromBSL(loc, rm.log)
|
||||
if len(skipTLSRet) > 0 {
|
||||
cmd.ExtraFlags = append(cmd.ExtraFlags, skipTLSRet)
|
||||
}
|
||||
|
||||
stdout, stderr, err := veleroexec.RunCommand(cmd.Cmd())
|
||||
rm.log.WithFields(logrus.Fields{
|
||||
"repository": cmd.RepoName(),
|
||||
"command": cmd.String(),
|
||||
"stdout": stdout,
|
||||
"stderr": stderr,
|
||||
}).Debugf("Ran restic command")
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error running command=%s, stdout=%s, stderr=%s", cmd.String(), stdout, stderr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInsecureSkipTLSVerifyFromBSL get insecureSkipTLSVerify flag from BSL configuration,
|
||||
// Then return --insecure-tls flag with boolean value as result.
|
||||
func GetInsecureSkipTLSVerifyFromBSL(backupLocation *velerov1api.BackupStorageLocation, logger logrus.FieldLogger) string {
|
||||
result := ""
|
||||
|
||||
if backupLocation == nil {
|
||||
logger.Info("bsl is nil. return empty.")
|
||||
return result
|
||||
}
|
||||
|
||||
if insecure, _ := strconv.ParseBool(backupLocation.Spec.Config[insecureSkipTLSVerifyKey]); insecure {
|
||||
logger.Debugf("set --insecure-tls=true for Restic command according to BSL %s config", backupLocation.Name)
|
||||
result = resticInsecureTLSFlag + "=true"
|
||||
return result
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
/*
|
||||
Copyright the Velero contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
)
|
||||
|
||||
func TestGetInsecureSkipTLSVerifyFromBSL(t *testing.T) {
|
||||
log := logrus.StandardLogger()
|
||||
tests := []struct {
|
||||
name string
|
||||
backupLocation *velerov1api.BackupStorageLocation
|
||||
logger logrus.FieldLogger
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"Test with nil BSL. Should return empty string.",
|
||||
nil,
|
||||
log,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Test BSL with no configuration. Should return empty string.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "azure",
|
||||
},
|
||||
},
|
||||
log,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Test with AWS BSL's insecureSkipTLSVerify set to false.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "aws",
|
||||
Config: map[string]string{
|
||||
"insecureSkipTLSVerify": "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
log,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Test with AWS BSL's insecureSkipTLSVerify set to true.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "aws",
|
||||
Config: map[string]string{
|
||||
"insecureSkipTLSVerify": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
log,
|
||||
"--insecure-tls=true",
|
||||
},
|
||||
{
|
||||
"Test with Azure BSL's insecureSkipTLSVerify set to invalid.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "azure",
|
||||
Config: map[string]string{
|
||||
"insecureSkipTLSVerify": "invalid",
|
||||
},
|
||||
},
|
||||
},
|
||||
log,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Test with GCP without insecureSkipTLSVerify.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "gcp",
|
||||
Config: map[string]string{},
|
||||
},
|
||||
},
|
||||
log,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Test with AWS without config.",
|
||||
&velerov1api.BackupStorageLocation{
|
||||
Spec: velerov1api.BackupStorageLocationSpec{
|
||||
Provider: "aws",
|
||||
},
|
||||
},
|
||||
log,
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
res := GetInsecureSkipTLSVerifyFromBSL(test.backupLocation, test.logger)
|
||||
|
||||
assert.Equal(t, test.expected, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -157,7 +157,7 @@ func GetKubeconfigContext() error {
|
||||
func TestE2e(t *testing.T) {
|
||||
// Skip running E2E tests when running only "short" tests because:
|
||||
// 1. E2E tests are long running tests involving installation of Velero and performing backup and restore operations.
|
||||
// 2. E2E tests require a kubernetes cluster to install and run velero which further requires ore configuration. See above referenced command line flags.
|
||||
// 2. E2E tests require a kubernetes cluster to install and run velero which further requires more configuration. See above referenced command line flags.
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping E2E tests")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user