mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-07-19 22:42:44 +00:00
Merge branch 'main' into rm-improvement
This commit is contained in:
@@ -48,7 +48,10 @@ jobs:
|
||||
version: latest
|
||||
|
||||
- name: Build
|
||||
run: make local
|
||||
run: |
|
||||
make local
|
||||
# Clean go cache to ease the build environment storage pressure.
|
||||
go clean -modcache -cache
|
||||
|
||||
- name: Test
|
||||
run: make test
|
||||
@@ -73,7 +76,7 @@ jobs:
|
||||
run: |
|
||||
sudo swapoff -a
|
||||
sudo rm -f /mnt/swapfile
|
||||
docker image prune -a --force
|
||||
docker system prune -a --force
|
||||
|
||||
# Build and push Velero image to docker registry
|
||||
docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
+6
-4
@@ -43,7 +43,8 @@ RUN mkdir -p /output/usr/bin && \
|
||||
go build -o /output/${BIN} \
|
||||
-ldflags "${LDFLAGS}" ${PKG}/cmd/${BIN} && \
|
||||
go build -o /output/velero-helper \
|
||||
-ldflags "${LDFLAGS}" ${PKG}/cmd/velero-helper
|
||||
-ldflags "${LDFLAGS}" ${PKG}/cmd/velero-helper && \
|
||||
go clean -modcache -cache
|
||||
|
||||
# Restic binary build section
|
||||
FROM --platform=$BUILDPLATFORM golang:1.20-bullseye as restic-builder
|
||||
@@ -65,10 +66,11 @@ COPY . /go/src/github.com/vmware-tanzu/velero
|
||||
|
||||
RUN mkdir -p /output/usr/bin && \
|
||||
export GOARM=$(echo "${GOARM}" | cut -c2-) && \
|
||||
/go/src/github.com/vmware-tanzu/velero/hack/build-restic.sh
|
||||
/go/src/github.com/vmware-tanzu/velero/hack/build-restic.sh && \
|
||||
go clean -modcache -cache
|
||||
|
||||
# Velero image packing section
|
||||
FROM gcr.io/distroless/base-nossl-debian11:nonroot
|
||||
FROM paketobuildpacks/run-jammy-tiny:latest
|
||||
|
||||
LABEL maintainer="Xun Jiang <jxun@vmware.com>"
|
||||
|
||||
@@ -76,5 +78,5 @@ COPY --from=velero-builder /output /
|
||||
|
||||
COPY --from=restic-builder /output /
|
||||
|
||||
USER nonroot:nonroot
|
||||
USER cnb:cnb
|
||||
|
||||
|
||||
+2
-3
@@ -6,7 +6,6 @@
|
||||
|
||||
| Maintainer | GitHub ID | Affiliation |
|
||||
|---------------------|---------------------------------------------------------------|--------------------------------------------------|
|
||||
| Dave Smith-Uchida | [dsu-igeek](https://github.com/dsu-igeek) | [Kasten](https://github.com/kastenhq/) |
|
||||
| Scott Seago | [sseago](https://github.com/sseago) | [OpenShift](https://github.com/openshift) |
|
||||
| Daniel Jiang | [reasonerjt](https://github.com/reasonerjt) | [VMware](https://www.github.com/vmware/) |
|
||||
| Wenkai Yin | [ywk253100](https://github.com/ywk253100) | [VMware](https://www.github.com/vmware/) |
|
||||
@@ -26,12 +25,12 @@
|
||||
* Carlisia Thompson ([carlisia](https://github.com/carlisia))
|
||||
* Bridget McErlean ([zubron](https://github.com/zubron))
|
||||
* JenTing Hsiao ([jenting](https://github.com/jenting))
|
||||
|
||||
* Dave Smith-Uchida ([dsu-igeek](https://github.com/dsu-igeek))
|
||||
|
||||
## Velero Contributors & Stakeholders
|
||||
|
||||
| Feature Area | Lead |
|
||||
|------------------------|:------------------------------------------------------------------------------------:|
|
||||
| Architect | Dave Smith-Uchida [dsu-igeek](https://github.com/dsu-igeek) |
|
||||
| Technical Lead | Daniel Jiang [reasonerjt](https://github.com/reasonerjt) |
|
||||
| Kubernetes CSI Liaison | |
|
||||
| Deployment | |
|
||||
|
||||
@@ -100,7 +100,7 @@ To fix CVEs and keep pace with Golang, Velero made changes as follows:
|
||||
* Enable staticcheck linter. (#5788, @blackpiglet)
|
||||
* Set Kopia IgnoreUnknownTypes in ErrorHandlingPolicy to True for ignoring backup unknown file type (#5786, @qiuming-best)
|
||||
* Bump up Restic version to 0.15.0 (#5784, @qiuming-best)
|
||||
* Add File system backup related matrics to Grafana dashboard
|
||||
* Add File system backup related metrics to Grafana dashboard
|
||||
- Add metrics backup_warning_total for record of total warnings
|
||||
- Add metrics backup_last_status for record of last status of the backup (#5779, @allenxu404)
|
||||
* Design for Handling backup of volumes by resources filters (#5773, @qiuming-best)
|
||||
|
||||
@@ -61,7 +61,7 @@ in progress for 1.9.
|
||||
* Add rbac and annotation test cases (#4455, @mqiu)
|
||||
* remove --crds-version in velero install command. (#4446, @jxun)
|
||||
* Upgrade e2e test vsphere plugin (#4440, @mqiu)
|
||||
* Fix e2e test failures for the inappropriate optimaze of velero install (#4438, @mqiu)
|
||||
* Fix e2e test failures for the inappropriate optimize of velero install (#4438, @mqiu)
|
||||
* Limit backup namespaces on test resource filtering cases (#4437, @mqiu)
|
||||
* Bump up Go to 1.17 (#4431, @reasonerjt)
|
||||
* Added `<backup name>`-itemsnapshots.json.gz to the backup format. This file exists
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Add `orLabelSelectors` for backup, restore commands
|
||||
@@ -0,0 +1 @@
|
||||
Delete PartiallyFailed orphaned backups as well as Completed ones
|
||||
@@ -0,0 +1 @@
|
||||
Add support for block volumes with Kopia
|
||||
@@ -0,0 +1 @@
|
||||
Remove schedule-related metrics on schedule delete
|
||||
@@ -0,0 +1 @@
|
||||
Proposal to support JSON Merge Patch and Strategic Merge Patch in Resource Modifiers
|
||||
@@ -0,0 +1 @@
|
||||
Fix #6861. Only Restic path requires repoIdentifier, so for non-restic path, set the repoIdentifier fields as empty in PVB and PVR and also remove the RepoIdentifier column in the get output of PVBs and PVRs
|
||||
@@ -0,0 +1 @@
|
||||
Fix issue #6859, move plugin depending podvolume functions to util pkg, so as to remove the dependencies to unnecessary repository packages like kopia, azure, etc.
|
||||
@@ -0,0 +1 @@
|
||||
Replace the base image with paketobuildpacks image
|
||||
@@ -0,0 +1 @@
|
||||
Set ParallelUploadAboveSize as MaxInt64 and flush repo after setting up policy so that policy is retrieved correctly by TreeForSource
|
||||
@@ -0,0 +1 @@
|
||||
Partially fix #6734, guide Kubernetes' scheduler to spread backup pods evenly across nodes as much as possible, so that data mover backup could achieve better parallelism
|
||||
@@ -35,10 +35,6 @@ spec:
|
||||
jsonPath: .spec.volume
|
||||
name: Volume
|
||||
type: string
|
||||
- description: Backup repository identifier for this backup
|
||||
jsonPath: .spec.repoIdentifier
|
||||
name: Repository ID
|
||||
type: string
|
||||
- description: The type of the uploader to handle data transfer
|
||||
jsonPath: .spec.uploaderType
|
||||
name: Uploader Type
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -49,6 +49,9 @@ spec:
|
||||
- mountPath: /host_pods
|
||||
mountPropagation: HostToContainer
|
||||
name: host-pods
|
||||
- mountPath: /var/lib/kubelet/plugins
|
||||
mountPropagation: HostToContainer
|
||||
name: host-plugins
|
||||
- mountPath: /scratch
|
||||
name: scratch
|
||||
- mountPath: /credentials
|
||||
@@ -60,6 +63,9 @@ spec:
|
||||
- hostPath:
|
||||
path: /var/lib/kubelet/pods
|
||||
name: host-pods
|
||||
- hostPath:
|
||||
path: /var/lib/kubelet/plugins
|
||||
name: host-plugins
|
||||
- emptyDir: {}
|
||||
name: scratch
|
||||
- name: cloud-credentials
|
||||
|
||||
@@ -175,7 +175,7 @@ If there are one or more, download the backup tarball from backup storage, untar
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
Another proposal for higher level `DeleteItemActions` was initially included, which would require implementors to individually download the backup tarball themselves.
|
||||
Another proposal for higher level `DeleteItemActions` was initially included, which would require implementers to individually download the backup tarball themselves.
|
||||
While this may be useful long term, it is not a good fit for the current goals as each plugin would be re-implementing a lot of boilerplate.
|
||||
See the deletion-plugins.md file for this alternative proposal in more detail.
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
# Proposal to Support JSON Merge Patch and Strategic Merge Patch in Resource Modifiers
|
||||
|
||||
- [Proposal to Support JSON Merge Patch and Strategic Merge Patch in Resource Modifiers](#proposal-to-support-json-merge-patch-and-strategic-merge-patch-in-resource-modifiers)
|
||||
- [Abstract](#abstract)
|
||||
- [Goals](#goals)
|
||||
- [Non Goals](#non-goals)
|
||||
- [User Stories](#user-stories)
|
||||
- [Scenario 1](#scenario-1)
|
||||
- [Scenario 2](#scenario-2)
|
||||
- [Detailed Design](#detailed-design)
|
||||
- [How to choose the right patch type](#how-to-choose-the-right-patch-type)
|
||||
- [New Field MergePatches](#new-field-mergepatches)
|
||||
- [New Field StrategicPatches](#new-field-strategicpatches)
|
||||
- [Conditional Patches in ALL Patch Types](#conditional-patches-in-all-patch-types)
|
||||
- [Wildcard Support for GroupResource](#wildcard-support-for-groupresource)
|
||||
- [Helper Command to Generate Merge Patch and Strategic Merge Patch](#helper-command-to-generate-merge-patch-and-strategic-merge-patch)
|
||||
- [Security Considerations](#security-considerations)
|
||||
- [Compatibility](#compatibility)
|
||||
- [Implementation](#implementation)
|
||||
- [Future Enhancements](#future-enhancements)
|
||||
- [Open Issues](#open-issues)
|
||||
|
||||
## Abstract
|
||||
Velero introduced the concept of Resource Modifiers in v1.12.0. This feature allows the user to specify a configmap with a set of rules to modify the resources during restore. The user can specify the filters to select the resources and then specify the JSON Patch to apply on the resource. This feature is currently limited to the operations supported by JSON Patch RFC.
|
||||
This proposal is to add support for JSON Merge Patch and Strategic Merge Patch in the Resource Modifiers. This will allow the user to use the same configmap to apply JSON Merge Patch and Strategic Merge Patch on the resources during restore.
|
||||
|
||||
## Goals
|
||||
- Allow the user to specify a JSON patch, JSON Merge Patch or Strategic Merge Patch for modification.
|
||||
- Allow the user to specify multiple JSON Patch, JSON Merge Patch or Strategic Merge Patch.
|
||||
- Allow the user to specify mixed JSON Patch, JSON Merge Patch and Strategic Merge Patch in the same configmap.
|
||||
|
||||
## Non Goals
|
||||
- Deprecating the existing RestoreItemAction plugins for standard substitutions(like changing the namespace, changing the storage class, etc.)
|
||||
|
||||
## User Stories
|
||||
|
||||
### Scenario 1
|
||||
- Alice has some Pods and part of them have an annotation `{"for": "bar"}`.
|
||||
- Alice wishes to restore these Pods to a different cluster without this annotation.
|
||||
- Alice can use this feature to remove this annotation during restore.
|
||||
|
||||
### Scenario 2
|
||||
- Bob has a Pod with several containers and one container with name nginx has an image `repo1/nginx`.
|
||||
- Bob wishes to restore this Pod to a different cluster, but new cluster can not access repo1, so he pushes the image to repo2.
|
||||
- Bob can use this feature to update the image of container nginx to `repo2/nginx` during restore.
|
||||
|
||||
## Detailed Design
|
||||
- The design and approach is inspired by kubectl patch command and [this doc](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/).
|
||||
- New fields `MergePatches` and `StrategicPatches` will be added to the `ResourceModifierRule` struct to support all three patch types.
|
||||
- Only one of the three patch types can be specified in a single `ResourceModifierRule`.
|
||||
- Add wildcard support for `groupResource` in `conditions` struct.
|
||||
- The workflow to create Resource Modifier ConfigMap and reference it in RestoreSpec will remain the same as described in document [Resource Modifiers](https://github.com/vmware-tanzu/velero/blob/main/site/content/docs/main/restore-resource-modifiers.md).
|
||||
|
||||
### How to choose the right patch type
|
||||
- [JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7386) is a naively simple format, with limited usability. Probably it is a good choice if you are building something small, with very simple JSON Schema.
|
||||
- [JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) is a more complex format, but it is applicable to any JSON documents. For a comparison of JSON patch and JSON merge patch, see [JSON Patch and JSON Merge Patch](https://erosb.github.io/post/json-patch-vs-merge-patch/).
|
||||
- Strategic Merge Patch is a Kubernetes defined patch type, mainly used to process resources of type list. You can replace/merge a list, add/remove items from a list by key, change the order of items in a list, etc. Strategic merge patch is not supported for custom resources. For more details, see [this doc](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/).
|
||||
|
||||
### New Field MergePatches
|
||||
MergePatches is a list to specify the merge patches to be applied on the resource. The merge patches will be applied in the order specified in the configmap. A subsequent patch is applied in order and if multiple patches are specified for the same path, the last patch will override the previous patches.
|
||||
|
||||
Example of MergePatches in ResourceModifierRule
|
||||
```yaml
|
||||
version: v1
|
||||
resourceModifierRules:
|
||||
- conditions:
|
||||
groupResource: pods
|
||||
namespaces:
|
||||
- ns1
|
||||
mergePatches:
|
||||
- patchData: |
|
||||
{
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"foo": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- The above configmap will apply the Merge Patch to all the pods in namespace ns1 and remove the annotation `foo` from the pods.
|
||||
- Both json and yaml format are supported for the patchData.
|
||||
|
||||
### New Field StrategicPatches
|
||||
StrategicPatches is a list to specify the strategic merge patches to be applied on the resource. The strategic merge patches will be applied in the order specified in the configmap. A subsequent patch is applied in order and if multiple patches are specified for the same path, the last patch will override the previous patches.
|
||||
|
||||
Example of StrategicPatches in ResourceModifierRule
|
||||
```yaml
|
||||
version: v1
|
||||
resourceModifierRules:
|
||||
- conditions:
|
||||
groupResource: pods
|
||||
resourceNameRegex: "^my-pod$"
|
||||
namespaces:
|
||||
- ns1
|
||||
strategicPatches:
|
||||
- patchData: |
|
||||
{
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "nginx",
|
||||
"image": "repo2/nginx"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
- The above configmap will apply the Strategic Merge Patch to the pod with name my-pod in namespace ns1 and update the image of container nginx to `repo2/nginx`.
|
||||
- Both json and yaml format are supported for the patchData.
|
||||
|
||||
### Conditional Patches in ALL Patch Types
|
||||
Since JSON Merge Patch and Strategic Merge Patch do not support conditional patches, we will use the `test` operation of JSON Patch to support conditional patches in all patch types by adding it to `Conditions` struct in `ResourceModifierRule`.
|
||||
|
||||
Example of test in conditions
|
||||
```yaml
|
||||
version: v1
|
||||
resourceModifierRules:
|
||||
- conditions:
|
||||
groupResource: persistentvolumeclaims.storage.k8s.io
|
||||
matches:
|
||||
- path: "/spec/storageClassName"
|
||||
value: "premium"
|
||||
mergePatches:
|
||||
- patchData: |
|
||||
{
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"foo": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- The above configmap will apply the Merge Patch to all the PVCs in all namespaces with storageClassName premium and remove the annotation `foo` from the PVCs.
|
||||
- You can specify multiple rules in the `matches` list. The patch will be applied only if all the matches are satisfied.
|
||||
|
||||
### Wildcard Support for GroupResource
|
||||
The user can specify a wildcard for groupResource in the conditions' struct. This will allow the user to apply the patches for all the resources of a particular group or all resources in all groups. For example, `*.apps` will apply to all the resources in the `apps` group, `*` will apply to all the resources in all groups.
|
||||
|
||||
### Helper Command to Generate Merge Patch and Strategic Merge Patch
|
||||
The patchData of Strategic Merge Patch is sometimes a bit complex for user to write. We can provide a helper command to generate the patchData for Strategic Merge Patch. The command will take the original resource and the modified resource as input and generate the patchData.
|
||||
It can also be used in JSON Merge Patch.
|
||||
|
||||
Here is a sample code snippet to achieve this:
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
pod := &corev1.Pod{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "web",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
newPod := pod.DeepCopy()
|
||||
patch := client.StrategicMergeFrom(pod)
|
||||
newPod.Spec.Containers[0].Image = "nginx1"
|
||||
|
||||
data, _ := patch.Data(newPod)
|
||||
fmt.Println(string(data))
|
||||
// Output:
|
||||
// {"spec":{"$setElementOrder/containers":[{"name":"web"}],"containers":[{"image":"nginx1","name":"web"}]}}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
No security impact.
|
||||
|
||||
## Compatibility
|
||||
Compatible with current Resource Modifiers.
|
||||
|
||||
## Implementation
|
||||
- Use "github.com/evanphx/json-patch" to support JSON Merge Patch.
|
||||
- Use "k8s.io/apimachinery/pkg/util/strategicpatch" to support Strategic Merge Patch.
|
||||
- Use glob to support wildcard for `groupResource` in `conditions` struct.
|
||||
- Use `test` operation of JSON Patch to calculate the `matches` in `conditions` struct.
|
||||
|
||||
## Future enhancements
|
||||
- add a Velero subcommand to generate/validate the patchData for Strategic Merge Patch and JSON Merge Patch.
|
||||
- add jq support for more complex conditions or patches, to meet the situations that the current conditions or patches can not handle. like [this issue](https://github.com/vmware-tanzu/velero/issues/6344)
|
||||
|
||||
## Open Issues
|
||||
N/A
|
||||
@@ -397,7 +397,7 @@ Target volume information includes PVC and PV that represents the volume and the
|
||||
The data mover information and backup repository information are the same with DataUpload CRD.
|
||||
DataDownload CRD defines the same status as DataUpload CRD with nearly the same meanings.
|
||||
|
||||
Below is the full spec of DataUpload CRD:
|
||||
Below is the full spec of DataDownload CRD:
|
||||
```
|
||||
apiVersion: apiextensions.k8s.io/v1alpha1
|
||||
kind: CustomResourceDefinition
|
||||
@@ -703,33 +703,38 @@ type Provider interface {
|
||||
In this case, we will extend the default kopia uploader to add the ability, when a given volume is for a block mode and is mapped as a device, we will use the [StreamingFile](https://pkg.go.dev/github.com/kopia/kopia@v0.13.0/fs#StreamingFile) to stream the device and backup to the kopia repository.
|
||||
|
||||
```go
|
||||
func getLocalBlockEntry(kopiaEntry fs.Entry, log logrus.FieldLogger) (fs.Entry, error) {
|
||||
path := kopiaEntry.LocalFilesystemPath()
|
||||
|
||||
fileInfo, err := os.Lstat(path)
|
||||
func getLocalBlockEntry(sourcePath string) (fs.Entry, error) {
|
||||
source, err := resolveSymlink(sourcePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Unable to get the source device information %s", path)
|
||||
return nil, errors.Wrap(err, "resolveSymlink")
|
||||
}
|
||||
|
||||
fileInfo, err := os.Lstat(source)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to get the source device information %s", source)
|
||||
}
|
||||
|
||||
if (fileInfo.Sys().(*syscall.Stat_t).Mode & syscall.S_IFMT) != syscall.S_IFBLK {
|
||||
return nil, errors.Errorf("Source path %s is not a block device", path)
|
||||
return nil, errors.Errorf("source path %s is not a block device", source)
|
||||
}
|
||||
device, err := os.Open(path)
|
||||
|
||||
device, err := os.Open(source)
|
||||
if err != nil {
|
||||
if os.IsPermission(err) || err.Error() == ErrNotPermitted {
|
||||
return nil, errors.Wrapf(err, "No permission to open the source device %s, make sure that node agent is running in privileged mode", path)
|
||||
return nil, errors.Wrapf(err, "no permission to open the source device %s, make sure that node agent is running in privileged mode", source)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "Unable to open the source device %s", path)
|
||||
return nil, errors.Wrapf(err, "unable to open the source device %s", source)
|
||||
}
|
||||
return virtualfs.StreamingFileFromReader(kopiaEntry.Name(), device), nil
|
||||
|
||||
sf := virtualfs.StreamingFileFromReader(source, device)
|
||||
return virtualfs.NewStaticDirectory(source, []fs.Entry{sf}), nil
|
||||
}
|
||||
```
|
||||
|
||||
In the `pkg/uploader/kopia/snapshot.go` this is used in the Backup call like
|
||||
|
||||
```go
|
||||
if volMode == PersistentVolumeFilesystem {
|
||||
if volMode == uploader.PersistentVolumeFilesystem {
|
||||
// to be consistent with restic when backup empty dir returns one error for upper logic handle
|
||||
dirs, err := os.ReadDir(source)
|
||||
if err != nil {
|
||||
@@ -742,15 +747,17 @@ In the `pkg/uploader/kopia/snapshot.go` this is used in the Backup call like
|
||||
source = filepath.Clean(source)
|
||||
...
|
||||
|
||||
sourceEntry, err := getLocalFSEntry(source)
|
||||
if err != nil {
|
||||
return nil, false, errors.Wrap(err, "Unable to get local filesystem entry")
|
||||
}
|
||||
var sourceEntry fs.Entry
|
||||
|
||||
if volMode == PersistentVolumeBlock {
|
||||
sourceEntry, err = getLocalBlockEntry(sourceEntry, log)
|
||||
if volMode == uploader.PersistentVolumeBlock {
|
||||
sourceEntry, err = getLocalBlockEntry(source)
|
||||
if err != nil {
|
||||
return nil, false, errors.Wrap(err, "Unable to get local block device entry")
|
||||
return nil, false, errors.Wrap(err, "unable to get local block device entry")
|
||||
}
|
||||
} else {
|
||||
sourceEntry, err = getLocalFSEntry(source)
|
||||
if err != nil {
|
||||
return nil, false, errors.Wrap(err, "unable to get local filesystem entry")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -766,6 +773,8 @@ We only need to extend two functions the rest will be passed through.
|
||||
```go
|
||||
type BlockOutput struct {
|
||||
*restore.FilesystemOutput
|
||||
|
||||
targetFileName string
|
||||
}
|
||||
|
||||
var _ restore.Output = &BlockOutput{}
|
||||
@@ -773,30 +782,15 @@ var _ restore.Output = &BlockOutput{}
|
||||
const bufferSize = 128 * 1024
|
||||
|
||||
func (o *BlockOutput) WriteFile(ctx context.Context, relativePath string, remoteFile fs.File) error {
|
||||
|
||||
targetFileName, err := filepath.EvalSymlinks(o.TargetPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Unable to evaluate symlinks for %s", targetFileName)
|
||||
}
|
||||
|
||||
fileInfo, err := os.Lstat(targetFileName)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Unable to get the target device information for %s", targetFileName)
|
||||
}
|
||||
|
||||
if (fileInfo.Sys().(*syscall.Stat_t).Mode & syscall.S_IFMT) != syscall.S_IFBLK {
|
||||
return errors.Errorf("Target file %s is not a block device", targetFileName)
|
||||
}
|
||||
|
||||
remoteReader, err := remoteFile.Open(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed to open remote file %s", remoteFile.Name())
|
||||
return errors.Wrapf(err, "failed to open remote file %s", remoteFile.Name())
|
||||
}
|
||||
defer remoteReader.Close()
|
||||
|
||||
targetFile, err := os.Create(targetFileName)
|
||||
targetFile, err := os.Create(o.targetFileName)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed to open file %s", targetFileName)
|
||||
return errors.Wrapf(err, "failed to open file %s", o.targetFileName)
|
||||
}
|
||||
defer targetFile.Close()
|
||||
|
||||
@@ -807,7 +801,7 @@ func (o *BlockOutput) WriteFile(ctx context.Context, relativePath string, remote
|
||||
bytesToWrite, err := remoteReader.Read(buffer)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
return errors.Wrapf(err, "Failed to read data from remote file %s", targetFileName)
|
||||
return errors.Wrapf(err, "failed to read data from remote file %s", o.targetFileName)
|
||||
}
|
||||
readData = false
|
||||
}
|
||||
@@ -819,7 +813,7 @@ func (o *BlockOutput) WriteFile(ctx context.Context, relativePath string, remote
|
||||
bytesToWrite -= bytesWritten
|
||||
offset += bytesWritten
|
||||
} else {
|
||||
return errors.Wrapf(err, "Failed to write data to file %s", targetFileName)
|
||||
return errors.Wrapf(err, "failed to write data to file %s", o.targetFileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -829,42 +823,43 @@ func (o *BlockOutput) WriteFile(ctx context.Context, relativePath string, remote
|
||||
}
|
||||
|
||||
func (o *BlockOutput) BeginDirectory(ctx context.Context, relativePath string, e fs.Directory) error {
|
||||
targetFileName, err := filepath.EvalSymlinks(o.TargetPath)
|
||||
var err error
|
||||
o.targetFileName, err = filepath.EvalSymlinks(o.TargetPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Unable to evaluate symlinks for %s", targetFileName)
|
||||
return errors.Wrapf(err, "unable to evaluate symlinks for %s", o.targetFileName)
|
||||
}
|
||||
|
||||
fileInfo, err := os.Lstat(targetFileName)
|
||||
fileInfo, err := os.Lstat(o.targetFileName)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Unable to get the target device information for %s", o.TargetPath)
|
||||
return errors.Wrapf(err, "unable to get the target device information for %s", o.TargetPath)
|
||||
}
|
||||
|
||||
if (fileInfo.Sys().(*syscall.Stat_t).Mode & syscall.S_IFMT) != syscall.S_IFBLK {
|
||||
return errors.Errorf("Target file %s is not a block device", o.TargetPath)
|
||||
return errors.Errorf("target file %s is not a block device", o.TargetPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
Of note, we do need to add root access to the daemon set node agent to access the new mount.
|
||||
Additional mount is required in the node-agent specification to resolve symlinks to the block devices from /host_pods/POD_ID/volumeDevices/kubernetes.io~csi directory.
|
||||
|
||||
```yaml
|
||||
...
|
||||
- mountPath: /var/lib/kubelet/plugins
|
||||
mountPropagation: HostToContainer
|
||||
name: host-plugins
|
||||
|
||||
....
|
||||
- hostPath:
|
||||
path: /var/lib/kubelet/plugins
|
||||
name: host-plugins
|
||||
```
|
||||
|
||||
...
|
||||
Privileged mode is required to access the block devices in /var/lib/kubelet/plugins/kubernetes.io/csi/volumeDevices/publish directory as confirmed by testing on EKS and Minikube.
|
||||
|
||||
```yaml
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: &c.privilegedAgent,
|
||||
Privileged: &c.privilegedNodeAgent,
|
||||
},
|
||||
|
||||
```
|
||||
|
||||
## Plugin Data Movers
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ else
|
||||
fi
|
||||
|
||||
if [[ -z "$BUILDX_PLATFORMS" ]]; then
|
||||
BUILDX_PLATFORMS="linux/amd64,linux/arm64,linux/arm/v7,linux/ppc64le"
|
||||
BUILDX_PLATFORMS="linux/amd64,linux/arm64"
|
||||
fi
|
||||
|
||||
# Debugging info
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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 resourcemodifiers
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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 resourcemodifiers
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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 resourcemodifiers
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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 resourcemodifiers
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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 resourcepolicies
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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 resourcepolicies
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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 resourcepolicies
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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 resourcepolicies
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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 resourcepolicies
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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 resourcepolicies
|
||||
|
||||
import (
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[build]
|
||||
base = "site/"
|
||||
command = "hugo --gc --minify"
|
||||
publish = "site/public"
|
||||
publish = "public"
|
||||
|
||||
[context.production.environment]
|
||||
HUGO_VERSION = "0.73.0"
|
||||
|
||||
@@ -89,6 +89,14 @@ const (
|
||||
|
||||
// ResourceUsageLabel is the label key to explain the Velero resource usage.
|
||||
ResourceUsageLabel = "velero.io/resource-usage"
|
||||
|
||||
// VolumesToBackupAnnotation is the annotation on a pod whose mounted volumes
|
||||
// need to be backed up using pod volume backup.
|
||||
VolumesToBackupAnnotation = "backup.velero.io/backup-volumes"
|
||||
|
||||
// VolumesToExcludeAnnotation is the annotation on a pod whose mounted volumes
|
||||
// should be excluded from pod volume backup.
|
||||
VolumesToExcludeAnnotation = "backup.velero.io/backup-volumes-excludes"
|
||||
)
|
||||
|
||||
type AsyncOperationIDPrefix string
|
||||
|
||||
@@ -114,7 +114,6 @@ type PodVolumeBackupStatus struct {
|
||||
// +kubebuilder:printcolumn:name="Namespace",type="string",JSONPath=".spec.pod.namespace",description="Namespace of the pod containing the volume to be backed up"
|
||||
// +kubebuilder:printcolumn:name="Pod",type="string",JSONPath=".spec.pod.name",description="Name of the pod containing the volume to be backed up"
|
||||
// +kubebuilder:printcolumn:name="Volume",type="string",JSONPath=".spec.volume",description="Name of the volume to be backed up"
|
||||
// +kubebuilder:printcolumn:name="Repository ID",type="string",JSONPath=".spec.repoIdentifier",description="Backup repository identifier for this backup"
|
||||
// +kubebuilder:printcolumn:name="Uploader Type",type="string",JSONPath=".spec.uploaderType",description="The type of the uploader to handle data transfer"
|
||||
// +kubebuilder:printcolumn:name="Storage Location",type="string",JSONPath=".spec.backupStorageLocation",description="Name of the Backup Storage Location where this backup should be stored"
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
|
||||
|
||||
@@ -56,7 +56,7 @@ type DataDownloadSpec struct {
|
||||
OperationTimeout metav1.Duration `json:"operationTimeout"`
|
||||
}
|
||||
|
||||
// TargetPVCSpec is the specification for a target PVC.
|
||||
// TargetVolumeSpec is the specification for a target PVC.
|
||||
type TargetVolumeSpec struct {
|
||||
// PVC is the name of the target PVC that is created by Velero restore
|
||||
PVC string `json:"pvc"`
|
||||
|
||||
@@ -1,34 +1,17 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright the Velero contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by deepcopy-gen. DO NOT EDIT.
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package v2alpha1
|
||||
|
||||
import (
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CSISnapshotSpec) DeepCopyInto(out *CSISnapshotSpec) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CSISnapshotSpec.
|
||||
@@ -48,7 +31,6 @@ func (in *DataDownload) DeepCopyInto(out *DataDownload) {
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataDownload.
|
||||
@@ -81,7 +63,6 @@ func (in *DataDownloadList) DeepCopyInto(out *DataDownloadList) {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataDownloadList.
|
||||
@@ -114,7 +95,6 @@ func (in *DataDownloadSpec) DeepCopyInto(out *DataDownloadSpec) {
|
||||
}
|
||||
}
|
||||
out.OperationTimeout = in.OperationTimeout
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataDownloadSpec.
|
||||
@@ -139,7 +119,6 @@ func (in *DataDownloadStatus) DeepCopyInto(out *DataDownloadStatus) {
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
out.Progress = in.Progress
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataDownloadStatus.
|
||||
@@ -159,7 +138,6 @@ func (in *DataUpload) DeepCopyInto(out *DataUpload) {
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataUpload.
|
||||
@@ -192,7 +170,6 @@ func (in *DataUploadList) DeepCopyInto(out *DataUploadList) {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataUploadList.
|
||||
@@ -227,7 +204,6 @@ func (in *DataUploadResult) DeepCopyInto(out *DataUploadResult) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataUploadResult.
|
||||
@@ -260,7 +236,6 @@ func (in *DataUploadSpec) DeepCopyInto(out *DataUploadSpec) {
|
||||
}
|
||||
}
|
||||
out.OperationTimeout = in.OperationTimeout
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataUploadSpec.
|
||||
@@ -296,7 +271,6 @@ func (in *DataUploadStatus) DeepCopyInto(out *DataUploadStatus) {
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
out.Progress = in.Progress
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataUploadStatus.
|
||||
@@ -312,7 +286,6 @@ func (in *DataUploadStatus) DeepCopy() *DataUploadStatus {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *TargetVolumeSpec) DeepCopyInto(out *TargetVolumeSpec) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetVolumeSpec.
|
||||
|
||||
@@ -52,6 +52,7 @@ import (
|
||||
vsv1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/volumesnapshotter/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/podvolume"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
pdvolumeutil "github.com/vmware-tanzu/velero/pkg/util/podvolume"
|
||||
"github.com/vmware-tanzu/velero/pkg/volume"
|
||||
)
|
||||
|
||||
@@ -200,7 +201,7 @@ func (ib *itemBackupper) backupItemInternal(logger logrus.FieldLogger, obj runti
|
||||
// Get the list of volumes to back up using pod volume backup from the pod's annotations. Remove from this list
|
||||
// any volumes that use a PVC that we've already backed up (this would be in a read-write-many scenario,
|
||||
// where it's been backed up from another pod), since we don't need >1 backup per PVC.
|
||||
includedVolumes, optedOutVolumes := podvolume.GetVolumesByPod(pod, boolptr.IsSetToTrue(ib.backupRequest.Spec.DefaultVolumesToFsBackup))
|
||||
includedVolumes, optedOutVolumes := pdvolumeutil.GetVolumesByPod(pod, boolptr.IsSetToTrue(ib.backupRequest.Spec.DefaultVolumesToFsBackup))
|
||||
for _, volume := range includedVolumes {
|
||||
// track the volumes that are PVCs using the PVC snapshot tracker, so that when we backup PVCs/PVs
|
||||
// via an item action in the next step, we don't snapshot PVs that will have their data backed up
|
||||
|
||||
@@ -95,6 +95,12 @@ func (b *PersistentVolumeBuilder) StorageClass(name string) *PersistentVolumeBui
|
||||
return b
|
||||
}
|
||||
|
||||
// VolumeMode sets the PersistentVolume's volume mode.
|
||||
func (b *PersistentVolumeBuilder) VolumeMode(volMode corev1api.PersistentVolumeMode) *PersistentVolumeBuilder {
|
||||
b.object.Spec.VolumeMode = &volMode
|
||||
return b
|
||||
}
|
||||
|
||||
// NodeAffinityRequired sets the PersistentVolume's NodeAffinity Requirement.
|
||||
func (b *PersistentVolumeBuilder) NodeAffinityRequired(req *corev1api.NodeSelector) *PersistentVolumeBuilder {
|
||||
b.object.Spec.NodeAffinity = &corev1api.VolumeNodeAffinity{
|
||||
|
||||
@@ -95,6 +95,7 @@ type CreateOptions struct {
|
||||
ExcludeNamespaceScopedResources flag.StringArray
|
||||
Labels flag.Map
|
||||
Selector flag.LabelSelector
|
||||
OrSelector flag.OrLabelSelector
|
||||
IncludeClusterResources flag.OptionalBool
|
||||
Wait bool
|
||||
StorageLocation string
|
||||
@@ -130,6 +131,7 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
|
||||
flags.StringVar(&o.StorageLocation, "storage-location", "", "Location in which to store the backup.")
|
||||
flags.StringSliceVar(&o.SnapshotLocations, "volume-snapshot-locations", o.SnapshotLocations, "List of locations (at most one per provider) where volume snapshots should be stored.")
|
||||
flags.VarP(&o.Selector, "selector", "l", "Only back up resources matching this label selector.")
|
||||
flags.Var(&o.OrSelector, "or-selector", "Backup resources matching at least one of the label selector from the list. Label selectors should be separated by ' or '. For example, foo=bar or app=nginx")
|
||||
flags.StringVar(&o.OrderedResources, "ordered-resources", "", "Mapping Kinds to an ordered list of specific resources of that Kind. Resource names are separated by commas and their names are in format 'namespace/resourcename'. For cluster scope resource, simply use resource name. Key-value pairs in the mapping are separated by semi-colon. Example: 'pods=ns1/pod1,ns1/pod2;persistentvolumeclaims=ns1/pvc4,ns1/pvc8'. Optional.")
|
||||
flags.DurationVar(&o.CSISnapshotTimeout, "csi-snapshot-timeout", o.CSISnapshotTimeout, "How long to wait for CSI snapshot creation before timeout.")
|
||||
flags.DurationVar(&o.ItemOperationTimeout, "item-operation-timeout", o.ItemOperationTimeout, "How long to wait for async plugin operations before timeout.")
|
||||
@@ -168,9 +170,8 @@ func (o *CreateOptions) Validate(c *cobra.Command, args []string, f client.Facto
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := f.KubebuilderWatchClient()
|
||||
if err != nil {
|
||||
return err
|
||||
if o.Selector.LabelSelector != nil && o.OrSelector.OrLabelSelectors != nil {
|
||||
return fmt.Errorf("either a 'selector' or an 'or-selector' can be specified, but not both")
|
||||
}
|
||||
|
||||
// Ensure that unless FromSchedule is set, args contains a backup name
|
||||
@@ -191,7 +192,7 @@ func (o *CreateOptions) Validate(c *cobra.Command, args []string, f client.Facto
|
||||
|
||||
if o.StorageLocation != "" {
|
||||
location := &velerov1api.BackupStorageLocation{}
|
||||
if err := client.Get(context.Background(), kbclient.ObjectKey{
|
||||
if err := o.client.Get(context.Background(), kbclient.ObjectKey{
|
||||
Namespace: f.Namespace(),
|
||||
Name: o.StorageLocation,
|
||||
}, location); err != nil {
|
||||
@@ -201,7 +202,7 @@ func (o *CreateOptions) Validate(c *cobra.Command, args []string, f client.Facto
|
||||
|
||||
for _, loc := range o.SnapshotLocations {
|
||||
snapshotLocation := new(velerov1api.VolumeSnapshotLocation)
|
||||
if err := o.client.Get(context.TODO(), kbclient.ObjectKey{Namespace: f.Namespace(), Name: loc}, snapshotLocation); err != nil {
|
||||
if err := o.client.Get(context.Background(), kbclient.ObjectKey{Namespace: f.Namespace(), Name: loc}, snapshotLocation); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -365,6 +366,7 @@ func (o *CreateOptions) BuildBackup(namespace string) (*velerov1api.Backup, erro
|
||||
IncludedNamespaceScopedResources(o.IncludeNamespaceScopedResources...).
|
||||
ExcludedNamespaceScopedResources(o.ExcludeNamespaceScopedResources...).
|
||||
LabelSelector(o.Selector.LabelSelector).
|
||||
OrLabelSelector(o.OrSelector.OrLabelSelectors).
|
||||
TTL(o.TTL).
|
||||
StorageLocation(o.StorageLocation).
|
||||
VolumeSnapshotLocations(o.SnapshotLocations...).
|
||||
|
||||
@@ -47,6 +47,15 @@ func TestCreateOptions_BuildBackup(t *testing.T) {
|
||||
orders, err := ParseOrderedResources(o.OrderedResources)
|
||||
o.CSISnapshotTimeout = 20 * time.Minute
|
||||
o.ItemOperationTimeout = 20 * time.Minute
|
||||
orLabelSelectors := []*metav1.LabelSelector{
|
||||
{
|
||||
MatchLabels: map[string]string{"k1": "v1", "k2": "v2"},
|
||||
},
|
||||
{
|
||||
MatchLabels: map[string]string{"a1": "b1", "a2": "b2"},
|
||||
},
|
||||
}
|
||||
o.OrSelector.OrLabelSelectors = orLabelSelectors
|
||||
assert.NoError(t, err)
|
||||
|
||||
backup, err := o.BuildBackup(cmdtest.VeleroNameSpace)
|
||||
@@ -58,6 +67,7 @@ func TestCreateOptions_BuildBackup(t *testing.T) {
|
||||
SnapshotVolumes: o.SnapshotVolumes.Value,
|
||||
IncludeClusterResources: o.IncludeClusterResources.Value,
|
||||
OrderedResources: orders,
|
||||
OrLabelSelectors: orLabelSelectors,
|
||||
CSISnapshotTimeout: metav1.Duration{Duration: o.CSISnapshotTimeout},
|
||||
ItemOperationTimeout: metav1.Duration{Duration: o.ItemOperationTimeout},
|
||||
}, backup.Spec)
|
||||
|
||||
@@ -69,6 +69,7 @@ func TestNewDescribeCommand(t *testing.T) {
|
||||
|
||||
if err == nil {
|
||||
assert.Contains(t, stdout, "Velero-Native Snapshots: <none included>")
|
||||
assert.Contains(t, stdout, "Or label selector: <none>")
|
||||
assert.Contains(t, stdout, fmt.Sprintf("Name: %s", backupName))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ type Options struct {
|
||||
BackupStorageConfig flag.Map
|
||||
VolumeSnapshotConfig flag.Map
|
||||
UseNodeAgent bool
|
||||
PrivilegedNodeAgent bool
|
||||
//TODO remove UseRestic when migration test out of using it
|
||||
UseRestic bool
|
||||
Wait bool
|
||||
@@ -110,6 +111,7 @@ func (o *Options) BindFlags(flags *pflag.FlagSet) {
|
||||
flags.BoolVar(&o.RestoreOnly, "restore-only", o.RestoreOnly, "Run the server in restore-only mode. Optional.")
|
||||
flags.BoolVar(&o.DryRun, "dry-run", o.DryRun, "Generate resources, but don't send them to the cluster. Use with -o. Optional.")
|
||||
flags.BoolVar(&o.UseNodeAgent, "use-node-agent", o.UseNodeAgent, "Create Velero node-agent daemonset. Optional. Velero node-agent hosts Velero modules that need to run in one or more nodes(i.e. Restic, Kopia).")
|
||||
flags.BoolVar(&o.PrivilegedNodeAgent, "privileged-node-agent", o.PrivilegedNodeAgent, "Use privileged mode for the node agent. Optional. Required to backup block devices.")
|
||||
flags.BoolVar(&o.Wait, "wait", o.Wait, "Wait for Velero deployment to be ready. Optional.")
|
||||
flags.DurationVar(&o.DefaultRepoMaintenanceFrequency, "default-repo-maintain-frequency", o.DefaultRepoMaintenanceFrequency, "How often 'maintain' is run for backup repositories by default. Optional.")
|
||||
flags.DurationVar(&o.GarbageCollectionFrequency, "garbage-collection-frequency", o.GarbageCollectionFrequency, "How often the garbage collection runs for expired backups.(default 1h)")
|
||||
@@ -198,6 +200,7 @@ func (o *Options) AsVeleroOptions() (*install.VeleroOptions, error) {
|
||||
SecretData: secretData,
|
||||
RestoreOnly: o.RestoreOnly,
|
||||
UseNodeAgent: o.UseNodeAgent,
|
||||
PrivilegedNodeAgent: o.PrivilegedNodeAgent,
|
||||
UseVolumeSnapshots: o.UseVolumeSnapshots,
|
||||
BSLConfig: o.BackupStorageConfig.Data(),
|
||||
VSLConfig: o.VolumeSnapshotConfig.Data(),
|
||||
|
||||
@@ -92,6 +92,7 @@ type CreateOptions struct {
|
||||
StatusExcludeResources flag.StringArray
|
||||
NamespaceMappings flag.Map
|
||||
Selector flag.LabelSelector
|
||||
OrSelector flag.OrLabelSelector
|
||||
IncludeClusterResources flag.OptionalBool
|
||||
Wait bool
|
||||
AllowPartiallyFailed flag.OptionalBool
|
||||
@@ -124,6 +125,7 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
|
||||
flags.Var(&o.StatusIncludeResources, "status-include-resources", "Resources to include in the restore status, formatted as resource.group, such as storageclasses.storage.k8s.io.")
|
||||
flags.Var(&o.StatusExcludeResources, "status-exclude-resources", "Resources to exclude from the restore status, formatted as resource.group, such as storageclasses.storage.k8s.io.")
|
||||
flags.VarP(&o.Selector, "selector", "l", "Only restore resources matching this label selector.")
|
||||
flags.Var(&o.OrSelector, "or-selector", "Restore resources matching at least one of the label selector from the list. Label selectors should be separated by ' or '. For example, foo=bar or app=nginx")
|
||||
flags.DurationVar(&o.ItemOperationTimeout, "item-operation-timeout", o.ItemOperationTimeout, "How long to wait for async plugin operations before timeout.")
|
||||
f := flags.VarPF(&o.RestoreVolumes, "restore-volumes", "", "Whether to restore volumes from snapshots.")
|
||||
// this allows the user to just specify "--restore-volumes" as shorthand for "--restore-volumes=true"
|
||||
@@ -185,6 +187,10 @@ func (o *CreateOptions) Validate(c *cobra.Command, args []string, f client.Facto
|
||||
return errors.New("Velero client is not set; unable to proceed")
|
||||
}
|
||||
|
||||
if o.Selector.LabelSelector != nil && o.OrSelector.OrLabelSelectors != nil {
|
||||
return errors.New("either a 'selector' or an 'or-selector' can be specified, but not both")
|
||||
}
|
||||
|
||||
if len(o.ExistingResourcePolicy) > 0 && !isResourcePolicyValid(o.ExistingResourcePolicy) {
|
||||
return errors.New("existing-resource-policy has invalid value, it accepts only none, update as value")
|
||||
}
|
||||
@@ -304,6 +310,7 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
|
||||
ExistingResourcePolicy: api.PolicyType(o.ExistingResourcePolicy),
|
||||
NamespaceMapping: o.NamespaceMappings.Data(),
|
||||
LabelSelector: o.Selector.LabelSelector,
|
||||
OrLabelSelectors: o.OrSelector.OrLabelSelectors,
|
||||
RestorePVs: o.RestoreVolumes.Value,
|
||||
PreserveNodePorts: o.PreserveNodePorts.Value,
|
||||
IncludeClusterResources: o.IncludeClusterResources.Value,
|
||||
|
||||
@@ -145,6 +145,7 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
|
||||
ExcludedNamespaceScopedResources: o.BackupOptions.ExcludeNamespaceScopedResources,
|
||||
IncludeClusterResources: o.BackupOptions.IncludeClusterResources.Value,
|
||||
LabelSelector: o.BackupOptions.Selector.LabelSelector,
|
||||
OrLabelSelectors: o.BackupOptions.OrSelector.OrLabelSelectors,
|
||||
SnapshotVolumes: o.BackupOptions.SnapshotVolumes.Value,
|
||||
TTL: metav1.Duration{Duration: o.BackupOptions.TTL},
|
||||
StorageLocation: o.BackupOptions.StorageLocation,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2017 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 flag
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// OrLabelSelector is a Cobra-compatible wrapper for defining
|
||||
// a Kubernetes or-label-selector flag.
|
||||
type OrLabelSelector struct {
|
||||
OrLabelSelectors []*metav1.LabelSelector
|
||||
}
|
||||
|
||||
// String returns a string representation of the or-label
|
||||
// selector flag.
|
||||
func (ls *OrLabelSelector) String() string {
|
||||
orLabels := []string{}
|
||||
for _, v := range ls.OrLabelSelectors {
|
||||
orLabels = append(orLabels, metav1.FormatLabelSelector(v))
|
||||
}
|
||||
return strings.Join(orLabels, " or ")
|
||||
}
|
||||
|
||||
// Set parses the provided string and assigns the result
|
||||
// to the or-label-selector receiver. It returns an error if
|
||||
// the string is not parseable.
|
||||
func (ls *OrLabelSelector) Set(s string) error {
|
||||
orItems := strings.Split(s, " or ")
|
||||
ls.OrLabelSelectors = make([]*metav1.LabelSelector, 0)
|
||||
for _, orItem := range orItems {
|
||||
parsed, err := metav1.ParseToLabelSelector(orItem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ls.OrLabelSelectors = append(ls.OrLabelSelectors, parsed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns a string representation of the
|
||||
// OrLabelSelector type.
|
||||
func (ls *OrLabelSelector) Type() string {
|
||||
return "orLabelSelector"
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package flag
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestStringOfOrLabelSelector(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
orLabelSelector *OrLabelSelector
|
||||
expectedStr string
|
||||
}{
|
||||
{
|
||||
name: "or between two labels",
|
||||
orLabelSelector: &OrLabelSelector{
|
||||
OrLabelSelectors: []*metav1.LabelSelector{
|
||||
{
|
||||
MatchLabels: map[string]string{"k1": "v1"},
|
||||
},
|
||||
{
|
||||
MatchLabels: map[string]string{"k2": "v2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedStr: "k1=v1 or k2=v2",
|
||||
},
|
||||
{
|
||||
name: "or between two label groups",
|
||||
orLabelSelector: &OrLabelSelector{
|
||||
OrLabelSelectors: []*metav1.LabelSelector{
|
||||
{
|
||||
MatchLabels: map[string]string{"k1": "v1", "k2": "v2"},
|
||||
},
|
||||
{
|
||||
MatchLabels: map[string]string{"a1": "b1", "a2": "b2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedStr: "k1=v1,k2=v2 or a1=b1,a2=b2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expectedStr, test.orLabelSelector.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetOfOrLabelSelector(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inputStr string
|
||||
expectedSelector *OrLabelSelector
|
||||
}{
|
||||
{
|
||||
name: "or between two labels",
|
||||
inputStr: "k1=v1 or k2=v2",
|
||||
expectedSelector: &OrLabelSelector{
|
||||
OrLabelSelectors: []*metav1.LabelSelector{
|
||||
{
|
||||
MatchLabels: map[string]string{"k1": "v1"},
|
||||
},
|
||||
{
|
||||
MatchLabels: map[string]string{"k2": "v2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "or between two label groups",
|
||||
inputStr: "k1=v1,k2=v2 or a1=b1,a2=b2",
|
||||
expectedSelector: &OrLabelSelector{
|
||||
OrLabelSelectors: []*metav1.LabelSelector{
|
||||
{
|
||||
MatchLabels: map[string]string{"k1": "v1", "k2": "v2"},
|
||||
},
|
||||
{
|
||||
MatchLabels: map[string]string{"a1": "b1", "a2": "b2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
selector := &OrLabelSelector{}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
require.Nil(t, selector.Set(test.inputStr))
|
||||
assert.Equal(t, len(test.expectedSelector.OrLabelSelectors), len(selector.OrLabelSelectors))
|
||||
assert.Equal(t, test.expectedSelector.String(), selector.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeOfOrLabelSelector(t *testing.T) {
|
||||
selector := &OrLabelSelector{}
|
||||
assert.Equal(t, "orLabelSelector", selector.Type())
|
||||
}
|
||||
@@ -199,6 +199,18 @@ func DescribeBackupSpec(d *Describer, spec velerov1api.BackupSpec) {
|
||||
}
|
||||
d.Printf("Label selector:\t%s\n", s)
|
||||
|
||||
d.Println()
|
||||
if len(spec.OrLabelSelectors) == 0 {
|
||||
s = emptyDisplay
|
||||
} else {
|
||||
orLabelSelectors := []string{}
|
||||
for _, v := range spec.OrLabelSelectors {
|
||||
orLabelSelectors = append(orLabelSelectors, metav1.FormatLabelSelector(v))
|
||||
}
|
||||
s = strings.Join(orLabelSelectors, " or ")
|
||||
}
|
||||
d.Printf("Or label selector:\t%s\n", s)
|
||||
|
||||
d.Println()
|
||||
d.Printf("Storage Location:\t%s\n", spec.StorageLocation)
|
||||
|
||||
|
||||
@@ -91,6 +91,8 @@ Resources:
|
||||
|
||||
Label selector: <none>
|
||||
|
||||
Or label selector: <none>
|
||||
|
||||
Storage Location: backup-location
|
||||
|
||||
Velero-Native Snapshot PVs: auto
|
||||
@@ -153,6 +155,8 @@ Resources:
|
||||
|
||||
Label selector: <none>
|
||||
|
||||
Or label selector: <none>
|
||||
|
||||
Storage Location: backup-location
|
||||
|
||||
Velero-Native Snapshot PVs: auto
|
||||
@@ -208,6 +212,8 @@ Resources:
|
||||
|
||||
Label selector: <none>
|
||||
|
||||
Or label selector: <none>
|
||||
|
||||
Storage Location: backup-location
|
||||
|
||||
Velero-Native Snapshot PVs: auto
|
||||
|
||||
@@ -146,6 +146,18 @@ func DescribeRestore(ctx context.Context, kbClient kbclient.Client, restore *vel
|
||||
}
|
||||
d.Printf("Label selector:\t%s\n", s)
|
||||
|
||||
d.Println()
|
||||
if len(restore.Spec.OrLabelSelectors) == 0 {
|
||||
s = emptyDisplay
|
||||
} else {
|
||||
orLabelSelectors := []string{}
|
||||
for _, v := range restore.Spec.OrLabelSelectors {
|
||||
orLabelSelectors = append(orLabelSelectors, metav1.FormatLabelSelector(v))
|
||||
}
|
||||
s = strings.Join(orLabelSelectors, " or ")
|
||||
}
|
||||
d.Printf("Or label selector:\t%s\n", s)
|
||||
|
||||
d.Println()
|
||||
d.Printf("Restore PVs:\t%s\n", BoolPointerString(restore.Spec.RestorePVs, "false", "true", "auto"))
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ Backup Template:
|
||||
|
||||
Label selector: <none>
|
||||
|
||||
Or label selector: <none>
|
||||
|
||||
Storage Location:
|
||||
|
||||
Velero-Native Snapshot PVs: auto
|
||||
@@ -82,6 +84,8 @@ Backup Template:
|
||||
|
||||
Label selector: <none>
|
||||
|
||||
Or label selector: <none>
|
||||
|
||||
Storage Location:
|
||||
|
||||
Velero-Native Snapshot PVs: auto
|
||||
|
||||
@@ -540,8 +540,8 @@ func (b *backupReconciler) validateAndGetSnapshotLocations(backup *velerov1api.B
|
||||
if len(errors) > 0 {
|
||||
return nil, errors
|
||||
}
|
||||
allLocations := &velerov1api.VolumeSnapshotLocationList{}
|
||||
err := b.kbClient.List(context.Background(), allLocations, &kbclient.ListOptions{Namespace: backup.Namespace, LabelSelector: labels.Everything()})
|
||||
volumeSnapshotLocations := &velerov1api.VolumeSnapshotLocationList{}
|
||||
err := b.kbClient.List(context.Background(), volumeSnapshotLocations, &kbclient.ListOptions{Namespace: backup.Namespace, LabelSelector: labels.Everything()})
|
||||
if err != nil {
|
||||
errors = append(errors, fmt.Sprintf("error listing volume snapshot locations: %v", err))
|
||||
return nil, errors
|
||||
@@ -549,8 +549,8 @@ func (b *backupReconciler) validateAndGetSnapshotLocations(backup *velerov1api.B
|
||||
|
||||
// build a map of provider->list of all locations for the provider
|
||||
allProviderLocations := make(map[string][]*velerov1api.VolumeSnapshotLocation)
|
||||
for i := range allLocations.Items {
|
||||
loc := allLocations.Items[i]
|
||||
for i := range volumeSnapshotLocations.Items {
|
||||
loc := volumeSnapshotLocations.Items[i]
|
||||
allProviderLocations[loc.Spec.Provider] = append(allProviderLocations[loc.Spec.Provider], &loc)
|
||||
}
|
||||
|
||||
|
||||
@@ -570,7 +570,7 @@ func (r *backupDeletionReconciler) patchDeleteBackupRequest(ctx context.Context,
|
||||
}
|
||||
|
||||
func (r *backupDeletionReconciler) patchBackup(ctx context.Context, backup *velerov1api.Backup, mutate func(*velerov1api.Backup)) (*velerov1api.Backup, error) {
|
||||
//TODO: The patchHelper can't be used here because the `backup/xxx/status` does not exist, until the bakcup resource is refactored
|
||||
//TODO: The patchHelper can't be used here because the `backup/xxx/status` does not exist, until the backup resource is refactored
|
||||
|
||||
// Record original json
|
||||
oldData, err := json.Marshal(backup)
|
||||
|
||||
@@ -331,7 +331,7 @@ func (b *backupSyncReconciler) deleteOrphanedBackups(ctx context.Context, locati
|
||||
|
||||
for i, backup := range backupList.Items {
|
||||
log = log.WithField("backup", backup.Name)
|
||||
if backup.Status.Phase != velerov1api.BackupPhaseCompleted || backupStoreBackups.Has(backup.Name) {
|
||||
if !(backup.Status.Phase == velerov1api.BackupPhaseCompleted || backup.Status.Phase == velerov1api.BackupPhasePartiallyFailed) || backupStoreBackups.Has(backup.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -557,7 +557,7 @@ var _ = Describe("Backup Sync Reconciler", func() {
|
||||
k8sBackups: []*velerov1api.Backup{
|
||||
baseBuilder("backupA").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backupB").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backupC").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backupC").Phase(velerov1api.BackupPhasePartiallyFailed).Result(),
|
||||
},
|
||||
expectedDeletes: sets.NewString("backupA", "backupB", "backupC"),
|
||||
},
|
||||
@@ -568,9 +568,10 @@ var _ = Describe("Backup Sync Reconciler", func() {
|
||||
k8sBackups: []*velerov1api.Backup{
|
||||
baseBuilder("backup-1").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backup-2").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backup-C").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backup-B").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backup-C").Phase(velerov1api.BackupPhasePartiallyFailed).Result(),
|
||||
},
|
||||
expectedDeletes: sets.NewString("backup-C"),
|
||||
expectedDeletes: sets.NewString("backup-B", "backup-C"),
|
||||
},
|
||||
{
|
||||
name: "all overlapping backups",
|
||||
@@ -579,7 +580,7 @@ var _ = Describe("Backup Sync Reconciler", func() {
|
||||
k8sBackups: []*velerov1api.Backup{
|
||||
baseBuilder("backup-1").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backup-2").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backup-3").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backup-3").Phase(velerov1api.BackupPhasePartiallyFailed).Result(),
|
||||
},
|
||||
expectedDeletes: sets.NewString(),
|
||||
},
|
||||
@@ -589,13 +590,14 @@ var _ = Describe("Backup Sync Reconciler", func() {
|
||||
cloudBackups: sets.NewString("backup-1", "backup-2", "backup-3"),
|
||||
k8sBackups: []*velerov1api.Backup{
|
||||
baseBuilder("backupA").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backupB").Phase(velerov1api.BackupPhasePartiallyFailed).Result(),
|
||||
baseBuilder("Deleting").Phase(velerov1api.BackupPhaseDeleting).Result(),
|
||||
baseBuilder("Failed").Phase(velerov1api.BackupPhaseFailed).Result(),
|
||||
baseBuilder("FailedValidation").Phase(velerov1api.BackupPhaseFailedValidation).Result(),
|
||||
baseBuilder("InProgress").Phase(velerov1api.BackupPhaseInProgress).Result(),
|
||||
baseBuilder("New").Phase(velerov1api.BackupPhaseNew).Result(),
|
||||
},
|
||||
expectedDeletes: sets.NewString("backupA"),
|
||||
expectedDeletes: sets.NewString("backupA", "backupB"),
|
||||
},
|
||||
{
|
||||
name: "all overlapping backups and all backups that are not complete",
|
||||
@@ -616,12 +618,13 @@ var _ = Describe("Backup Sync Reconciler", func() {
|
||||
baseBuilder("backup-1").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backup-2").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backup-C").Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backup-D").Phase(velerov1api.BackupPhasePartiallyFailed).Result(),
|
||||
|
||||
baseBuilder("backup-4").ObjectMeta(builder.WithLabels(velerov1api.StorageLocationLabel, "alternate")).Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backup-5").ObjectMeta(builder.WithLabels(velerov1api.StorageLocationLabel, "alternate")).Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backup-6").ObjectMeta(builder.WithLabels(velerov1api.StorageLocationLabel, "alternate")).Phase(velerov1api.BackupPhaseCompleted).Result(),
|
||||
baseBuilder("backup-6").ObjectMeta(builder.WithLabels(velerov1api.StorageLocationLabel, "alternate")).Phase(velerov1api.BackupPhasePartiallyFailed).Result(),
|
||||
},
|
||||
expectedDeletes: sets.NewString("backup-C"),
|
||||
expectedDeletes: sets.NewString("backup-C", "backup-D"),
|
||||
},
|
||||
{
|
||||
name: "some overlapping backups",
|
||||
@@ -646,8 +649,14 @@ var _ = Describe("Backup Sync Reconciler", func() {
|
||||
).
|
||||
Phase(velerov1api.BackupPhaseCompleted).
|
||||
Result(),
|
||||
builder.ForBackup("ns-1", "backup-D").
|
||||
ObjectMeta(
|
||||
builder.WithLabels(velerov1api.StorageLocationLabel, "the-really-long-location-name-that-is-much-more-than-63-c69e779"),
|
||||
).
|
||||
Phase(velerov1api.BackupPhasePartiallyFailed).
|
||||
Result(),
|
||||
},
|
||||
expectedDeletes: sets.NewString("backup-C"),
|
||||
expectedDeletes: sets.NewString("backup-C", "backup-D"),
|
||||
useLongBSLName: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -177,7 +177,10 @@ func (r *DataUploadReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
exposeParam := r.setupExposeParam(du)
|
||||
exposeParam, err := r.setupExposeParam(du)
|
||||
if err != nil {
|
||||
return r.errorOut(ctx, du, err, "failed to set exposer parameters", log)
|
||||
}
|
||||
|
||||
// Expose() will trigger to create one pod whose volume is restored by a given volume snapshot,
|
||||
// but the pod maybe is not in the same node of the current controller, so we need to return it here.
|
||||
@@ -735,18 +738,33 @@ func (r *DataUploadReconciler) closeDataPath(ctx context.Context, duName string)
|
||||
r.dataPathMgr.RemoveAsyncBR(duName)
|
||||
}
|
||||
|
||||
func (r *DataUploadReconciler) setupExposeParam(du *velerov2alpha1api.DataUpload) interface{} {
|
||||
func (r *DataUploadReconciler) setupExposeParam(du *velerov2alpha1api.DataUpload) (interface{}, error) {
|
||||
if du.Spec.SnapshotType == velerov2alpha1api.SnapshotTypeCSI {
|
||||
pvc := &corev1.PersistentVolumeClaim{}
|
||||
err := r.client.Get(context.Background(), types.NamespacedName{
|
||||
Namespace: du.Spec.SourceNamespace,
|
||||
Name: du.Spec.SourcePVC,
|
||||
}, pvc)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get PVC %s/%s", du.Spec.SourceNamespace, du.Spec.SourcePVC)
|
||||
}
|
||||
|
||||
accessMode := exposer.AccessModeFileSystem
|
||||
if pvc.Spec.VolumeMode != nil && *pvc.Spec.VolumeMode == corev1.PersistentVolumeBlock {
|
||||
accessMode = exposer.AccessModeBlock
|
||||
}
|
||||
|
||||
return &exposer.CSISnapshotExposeParam{
|
||||
SnapshotName: du.Spec.CSISnapshot.VolumeSnapshot,
|
||||
SourceNamespace: du.Spec.SourceNamespace,
|
||||
StorageClass: du.Spec.CSISnapshot.StorageClass,
|
||||
HostingPodLabels: map[string]string{velerov1api.DataUploadLabel: du.Name},
|
||||
AccessMode: exposer.AccessModeFileSystem,
|
||||
AccessMode: accessMode,
|
||||
Timeout: du.Spec.OperationTimeout.Duration,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *DataUploadReconciler) setupWaitExposePara(du *velerov2alpha1api.DataUpload) interface{} {
|
||||
|
||||
@@ -306,6 +306,7 @@ func TestReconcile(t *testing.T) {
|
||||
name string
|
||||
du *velerov2alpha1api.DataUpload
|
||||
pod *corev1.Pod
|
||||
pvc *corev1.PersistentVolumeClaim
|
||||
snapshotExposerList map[velerov2alpha1api.SnapshotType]exposer.SnapshotExposer
|
||||
dataMgr *datapath.Manager
|
||||
expectedProcessed bool
|
||||
@@ -345,11 +346,21 @@ func TestReconcile(t *testing.T) {
|
||||
}, {
|
||||
name: "Dataupload should be accepted",
|
||||
du: dataUploadBuilder().Result(),
|
||||
pod: builder.ForPod(velerov1api.DefaultNamespace, dataUploadName).Volumes(&corev1.Volume{Name: "dataupload-1"}).Result(),
|
||||
pod: builder.ForPod("fake-ns", dataUploadName).Volumes(&corev1.Volume{Name: "test-pvc"}).Result(),
|
||||
pvc: builder.ForPersistentVolumeClaim("fake-ns", "test-pvc").Result(),
|
||||
expectedProcessed: false,
|
||||
expected: dataUploadBuilder().Phase(velerov2alpha1api.DataUploadPhaseAccepted).Result(),
|
||||
expectedRequeue: ctrl.Result{},
|
||||
},
|
||||
{
|
||||
name: "Dataupload should fail to get PVC information",
|
||||
du: dataUploadBuilder().Result(),
|
||||
pod: builder.ForPod("fake-ns", dataUploadName).Volumes(&corev1.Volume{Name: "wrong-pvc"}).Result(),
|
||||
expectedProcessed: true,
|
||||
expected: dataUploadBuilder().Phase(velerov2alpha1api.DataUploadPhaseFailed).Result(),
|
||||
expectedRequeue: ctrl.Result{},
|
||||
expectedErrMsg: "failed to get PVC",
|
||||
},
|
||||
{
|
||||
name: "Dataupload should be prepared",
|
||||
du: dataUploadBuilder().SnapshotType(fakeSnapshotType).Result(),
|
||||
@@ -448,6 +459,11 @@ func TestReconcile(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
if test.pvc != nil {
|
||||
err = r.client.Create(ctx, test.pvc)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
if test.dataMgr != nil {
|
||||
r.dataPathMgr = test.dataMgr
|
||||
} else {
|
||||
|
||||
@@ -144,7 +144,7 @@ func (r *downloadRequestReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
||||
log.WithError(err).Error("fail to get restore for DownloadRequest")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
log.Warnf("Fail to get restore for DownloadRequest %s. Retry later.", err.Error())
|
||||
log.Warnf("fail to get restore for DownloadRequest %s. Retry later.", err.Error())
|
||||
return ctrl.Result{}, errors.WithStack(err)
|
||||
}
|
||||
backupName = restore.Spec.BackupName
|
||||
@@ -172,7 +172,7 @@ func (r *downloadRequestReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
||||
log.Errorf("BSL for DownloadRequest cannot be found")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
log.Warnf("Fail to get BSL for DownloadRequest: %s", err.Error())
|
||||
log.Warnf("fail to get BSL for DownloadRequest: %s", err.Error())
|
||||
return ctrl.Result{}, errors.WithStack(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ func (c *scheduleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
|
||||
if err := c.Get(ctx, req.NamespacedName, schedule); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
log.WithError(err).Error("schedule not found")
|
||||
c.metrics.RemoveSchedule(req.Name)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, errors.Wrapf(err, "error getting schedule %s", req.String())
|
||||
|
||||
@@ -133,10 +133,10 @@ func (fs *fileSystemBR) StartBackup(source AccessPoint, realSource string, paren
|
||||
if !fs.initialized {
|
||||
return errors.New("file system data path is not initialized")
|
||||
}
|
||||
volMode := getPersistentVolumeMode(source)
|
||||
|
||||
go func() {
|
||||
snapshotID, emptySnapshot, err := fs.uploaderProv.RunBackup(fs.ctx, source.ByPath, realSource, tags, forceFull, parentSnapshot, volMode, fs)
|
||||
snapshotID, emptySnapshot, err := fs.uploaderProv.RunBackup(fs.ctx, source.ByPath, realSource, tags, forceFull,
|
||||
parentSnapshot, source.VolMode, fs)
|
||||
|
||||
if err == provider.ErrorCanceled {
|
||||
fs.callbacks.OnCancelled(context.Background(), fs.namespace, fs.jobName)
|
||||
@@ -155,10 +155,8 @@ func (fs *fileSystemBR) StartRestore(snapshotID string, target AccessPoint) erro
|
||||
return errors.New("file system data path is not initialized")
|
||||
}
|
||||
|
||||
volMode := getPersistentVolumeMode(target)
|
||||
|
||||
go func() {
|
||||
err := fs.uploaderProv.RunRestore(fs.ctx, snapshotID, target.ByPath, volMode, fs)
|
||||
err := fs.uploaderProv.RunRestore(fs.ctx, snapshotID, target.ByPath, target.VolMode, fs)
|
||||
|
||||
if err == provider.ErrorCanceled {
|
||||
fs.callbacks.OnCancelled(context.Background(), fs.namespace, fs.jobName)
|
||||
@@ -172,13 +170,6 @@ func (fs *fileSystemBR) StartRestore(snapshotID string, target AccessPoint) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPersistentVolumeMode(source AccessPoint) uploader.PersistentVolumeMode {
|
||||
if source.ByBlock != "" {
|
||||
return uploader.PersistentVolumeBlock
|
||||
}
|
||||
return uploader.PersistentVolumeFilesystem
|
||||
}
|
||||
|
||||
// UpdateProgress which implement ProgressUpdater interface to update progress status
|
||||
func (fs *fileSystemBR) UpdateProgress(p *uploader.Progress) {
|
||||
if fs.callbacks.OnProgress != nil {
|
||||
|
||||
@@ -53,7 +53,7 @@ type Callbacks struct {
|
||||
// AccessPoint represents an access point that has been exposed to a data path instance
|
||||
type AccessPoint struct {
|
||||
ByPath string
|
||||
ByBlock string
|
||||
VolMode uploader.PersistentVolumeMode
|
||||
}
|
||||
|
||||
// AsyncBR is the interface for asynchronous data path methods
|
||||
|
||||
@@ -233,9 +233,12 @@ func (e *csiSnapshotExposer) CleanUp(ctx context.Context, ownerObject corev1.Obj
|
||||
}
|
||||
|
||||
func getVolumeModeByAccessMode(accessMode string) (corev1.PersistentVolumeMode, error) {
|
||||
if accessMode == AccessModeFileSystem {
|
||||
switch accessMode {
|
||||
case AccessModeFileSystem:
|
||||
return corev1.PersistentVolumeFilesystem, nil
|
||||
} else {
|
||||
case AccessModeBlock:
|
||||
return corev1.PersistentVolumeBlock, nil
|
||||
default:
|
||||
return "", errors.Errorf("unsupported access mode %s", accessMode)
|
||||
}
|
||||
}
|
||||
@@ -356,6 +359,13 @@ func (e *csiSnapshotExposer) createBackupPod(ctx context.Context, ownerObject co
|
||||
}
|
||||
|
||||
var gracePeriod int64 = 0
|
||||
volumeMounts, volumeDevices := kube.MakePodPVCAttachment(volumeName, backupPVC.Spec.VolumeMode)
|
||||
|
||||
if label == nil {
|
||||
label = make(map[string]string)
|
||||
}
|
||||
|
||||
label[podGroupLabel] = podGroupSnapshot
|
||||
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -373,16 +383,26 @@ func (e *csiSnapshotExposer) createBackupPod(ctx context.Context, ownerObject co
|
||||
Labels: label,
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
TopologySpreadConstraints: []corev1.TopologySpreadConstraint{
|
||||
{
|
||||
MaxSkew: 1,
|
||||
TopologyKey: "kubernetes.io/hostname",
|
||||
WhenUnsatisfiable: corev1.ScheduleAnyway,
|
||||
LabelSelector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
podGroupLabel: podGroupSnapshot,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: containerName,
|
||||
Image: podInfo.image,
|
||||
ImagePullPolicy: corev1.PullNever,
|
||||
Command: []string{"/velero-helper", "pause"},
|
||||
VolumeMounts: []corev1.VolumeMount{{
|
||||
Name: volumeName,
|
||||
MountPath: "/" + volumeName,
|
||||
}},
|
||||
VolumeMounts: volumeMounts,
|
||||
VolumeDevices: volumeDevices,
|
||||
},
|
||||
},
|
||||
ServiceAccountName: podInfo.serviceAccount,
|
||||
|
||||
@@ -82,7 +82,7 @@ func (e *genericRestoreExposer) Expose(ctx context.Context, ownerObject corev1.O
|
||||
return errors.Errorf("Target PVC %s/%s has already been bound, abort", sourceNamespace, targetPVCName)
|
||||
}
|
||||
|
||||
restorePod, err := e.createRestorePod(ctx, ownerObject, hostingPodLabels, selectedNode)
|
||||
restorePod, err := e.createRestorePod(ctx, ownerObject, targetPVC, hostingPodLabels, selectedNode)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error to create restore pod")
|
||||
}
|
||||
@@ -247,7 +247,8 @@ func (e *genericRestoreExposer) RebindVolume(ctx context.Context, ownerObject co
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *genericRestoreExposer) createRestorePod(ctx context.Context, ownerObject corev1.ObjectReference, label map[string]string, selectedNode string) (*corev1.Pod, error) {
|
||||
func (e *genericRestoreExposer) createRestorePod(ctx context.Context, ownerObject corev1.ObjectReference, targetPVC *corev1.PersistentVolumeClaim,
|
||||
label map[string]string, selectedNode string) (*corev1.Pod, error) {
|
||||
restorePodName := ownerObject.Name
|
||||
restorePVCName := ownerObject.Name
|
||||
|
||||
@@ -260,6 +261,7 @@ func (e *genericRestoreExposer) createRestorePod(ctx context.Context, ownerObjec
|
||||
}
|
||||
|
||||
var gracePeriod int64 = 0
|
||||
volumeMounts, volumeDevices := kube.MakePodPVCAttachment(volumeName, targetPVC.Spec.VolumeMode)
|
||||
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -283,10 +285,8 @@ func (e *genericRestoreExposer) createRestorePod(ctx context.Context, ownerObjec
|
||||
Image: podInfo.image,
|
||||
ImagePullPolicy: corev1.PullNever,
|
||||
Command: []string{"/velero-helper", "pause"},
|
||||
VolumeMounts: []corev1.VolumeMount{{
|
||||
Name: volumeName,
|
||||
MountPath: "/" + volumeName,
|
||||
}},
|
||||
VolumeMounts: volumeMounts,
|
||||
VolumeDevices: volumeDevices,
|
||||
},
|
||||
},
|
||||
ServiceAccountName: podInfo.serviceAccount,
|
||||
|
||||
@@ -26,11 +26,13 @@ import (
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/datapath"
|
||||
"github.com/vmware-tanzu/velero/pkg/uploader"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
)
|
||||
|
||||
var getVolumeDirectory = kube.GetVolumeDirectory
|
||||
var getVolumeMode = kube.GetVolumeMode
|
||||
var singlePathMatch = kube.SinglePathMatch
|
||||
|
||||
// GetPodVolumeHostPath returns a path that can be accessed from the host for a given volume of a pod
|
||||
@@ -45,7 +47,17 @@ func GetPodVolumeHostPath(ctx context.Context, pod *corev1.Pod, volumeName strin
|
||||
|
||||
logger.WithField("volDir", volDir).Info("Got volume dir")
|
||||
|
||||
pathGlob := fmt.Sprintf("/host_pods/%s/volumes/*/%s", string(pod.GetUID()), volDir)
|
||||
volMode, err := getVolumeMode(ctx, logger, pod, volumeName, cli)
|
||||
if err != nil {
|
||||
return datapath.AccessPoint{}, errors.Wrapf(err, "error getting volume mode for volume %s in pod %s", volumeName, pod.Name)
|
||||
}
|
||||
|
||||
volSubDir := "volumes"
|
||||
if volMode == uploader.PersistentVolumeBlock {
|
||||
volSubDir = "volumeDevices"
|
||||
}
|
||||
|
||||
pathGlob := fmt.Sprintf("/host_pods/%s/%s/*/%s", string(pod.GetUID()), volSubDir, volDir)
|
||||
logger.WithField("pathGlob", pathGlob).Debug("Looking for path matching glob")
|
||||
|
||||
path, err := singlePathMatch(pathGlob, fs, logger)
|
||||
@@ -56,6 +68,7 @@ func GetPodVolumeHostPath(ctx context.Context, pod *corev1.Pod, volumeName strin
|
||||
logger.WithField("path", path).Info("Found path matching glob")
|
||||
|
||||
return datapath.AccessPoint{
|
||||
ByPath: path,
|
||||
ByPath: path,
|
||||
VolMode: volMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -29,17 +29,19 @@ import (
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
"github.com/vmware-tanzu/velero/pkg/uploader"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
)
|
||||
|
||||
func TestGetPodVolumeHostPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
getVolumeDirFunc func(context.Context, logrus.FieldLogger, *corev1.Pod, string, ctrlclient.Client) (string, error)
|
||||
pathMatchFunc func(string, filesystem.Interface, logrus.FieldLogger) (string, error)
|
||||
pod *corev1.Pod
|
||||
pvc string
|
||||
err string
|
||||
name string
|
||||
getVolumeDirFunc func(context.Context, logrus.FieldLogger, *corev1.Pod, string, ctrlclient.Client) (string, error)
|
||||
getVolumeModeFunc func(context.Context, logrus.FieldLogger, *corev1.Pod, string, ctrlclient.Client) (uploader.PersistentVolumeMode, error)
|
||||
pathMatchFunc func(string, filesystem.Interface, logrus.FieldLogger) (string, error)
|
||||
pod *corev1.Pod
|
||||
pvc string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "get volume dir fail",
|
||||
@@ -55,6 +57,9 @@ func TestGetPodVolumeHostPath(t *testing.T) {
|
||||
getVolumeDirFunc: func(context.Context, logrus.FieldLogger, *corev1.Pod, string, ctrlclient.Client) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
getVolumeModeFunc: func(context.Context, logrus.FieldLogger, *corev1.Pod, string, ctrlclient.Client) (uploader.PersistentVolumeMode, error) {
|
||||
return uploader.PersistentVolumeFilesystem, nil
|
||||
},
|
||||
pathMatchFunc: func(string, filesystem.Interface, logrus.FieldLogger) (string, error) {
|
||||
return "", errors.New("fake-error-2")
|
||||
},
|
||||
@@ -62,6 +67,18 @@ func TestGetPodVolumeHostPath(t *testing.T) {
|
||||
pvc: "fake-pvc-1",
|
||||
err: "error identifying unique volume path on host for volume fake-pvc-1 in pod fake-pod-2: fake-error-2",
|
||||
},
|
||||
{
|
||||
name: "get block volume dir success",
|
||||
getVolumeDirFunc: func(context.Context, logrus.FieldLogger, *corev1.Pod, string, ctrlclient.Client) (
|
||||
string, error) {
|
||||
return "fake-pvc-1", nil
|
||||
},
|
||||
pathMatchFunc: func(string, filesystem.Interface, logrus.FieldLogger) (string, error) {
|
||||
return "/host_pods/fake-pod-1-id/volumeDevices/kubernetes.io~csi/fake-pvc-1-id", nil
|
||||
},
|
||||
pod: builder.ForPod(velerov1api.DefaultNamespace, "fake-pod-1").Result(),
|
||||
pvc: "fake-pvc-1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -70,12 +87,18 @@ func TestGetPodVolumeHostPath(t *testing.T) {
|
||||
getVolumeDirectory = test.getVolumeDirFunc
|
||||
}
|
||||
|
||||
if test.getVolumeModeFunc != nil {
|
||||
getVolumeMode = test.getVolumeModeFunc
|
||||
}
|
||||
|
||||
if test.pathMatchFunc != nil {
|
||||
singlePathMatch = test.pathMatchFunc
|
||||
}
|
||||
|
||||
_, err := GetPodVolumeHostPath(context.Background(), test.pod, test.pvc, nil, nil, velerotest.NewLogger())
|
||||
assert.EqualError(t, err, test.err)
|
||||
if test.err != "" || err != nil {
|
||||
assert.EqualError(t, err, test.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ import (
|
||||
|
||||
const (
|
||||
AccessModeFileSystem = "by-file-system"
|
||||
AccessModeBlock = "by-block-device"
|
||||
podGroupLabel = "velero.io/exposer-pod-group"
|
||||
podGroupSnapshot = "snapshot-exposer"
|
||||
)
|
||||
|
||||
// ExposeResult defines the result of expose.
|
||||
|
||||
@@ -86,6 +86,14 @@ func DaemonSet(namespace string, opts ...podTemplateOption) *appsv1.DaemonSet {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "host-plugins",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
HostPath: &corev1.HostPathVolumeSource{
|
||||
Path: "/var/lib/kubelet/plugins",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "scratch",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
@@ -102,13 +110,20 @@ func DaemonSet(namespace string, opts ...podTemplateOption) *appsv1.DaemonSet {
|
||||
"/velero",
|
||||
},
|
||||
Args: daemonSetArgs,
|
||||
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: &c.privilegedNodeAgent,
|
||||
},
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "host-pods",
|
||||
MountPath: "/host_pods",
|
||||
MountPropagation: &mountPropagationMode,
|
||||
},
|
||||
{
|
||||
Name: "host-plugins",
|
||||
MountPath: "/var/lib/kubelet/plugins",
|
||||
MountPropagation: &mountPropagationMode,
|
||||
},
|
||||
{
|
||||
Name: "scratch",
|
||||
MountPath: "/scratch",
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestDaemonSet(t *testing.T) {
|
||||
|
||||
ds = DaemonSet("velero", WithSecret(true))
|
||||
assert.Equal(t, 7, len(ds.Spec.Template.Spec.Containers[0].Env))
|
||||
assert.Equal(t, 3, len(ds.Spec.Template.Spec.Volumes))
|
||||
assert.Equal(t, 4, len(ds.Spec.Template.Spec.Volumes))
|
||||
|
||||
ds = DaemonSet("velero", WithFeatures([]string{"foo,bar,baz"}))
|
||||
assert.Len(t, ds.Spec.Template.Spec.Containers[0].Args, 3)
|
||||
|
||||
@@ -47,6 +47,7 @@ type podTemplateConfig struct {
|
||||
serviceAccountName string
|
||||
uploaderType string
|
||||
defaultSnapshotMoveData bool
|
||||
privilegedNodeAgent bool
|
||||
}
|
||||
|
||||
func WithImage(image string) podTemplateOption {
|
||||
@@ -149,6 +150,12 @@ func WithServiceAccountName(sa string) podTemplateOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithPrivilegedNodeAgent() podTemplateOption {
|
||||
return func(c *podTemplateConfig) {
|
||||
c.privilegedNodeAgent = true
|
||||
}
|
||||
}
|
||||
|
||||
func Deployment(namespace string, opts ...podTemplateOption) *appsv1.Deployment {
|
||||
// TODO: Add support for server args
|
||||
c := &podTemplateConfig{
|
||||
|
||||
@@ -240,6 +240,7 @@ type VeleroOptions struct {
|
||||
SecretData []byte
|
||||
RestoreOnly bool
|
||||
UseNodeAgent bool
|
||||
PrivilegedNodeAgent bool
|
||||
UseVolumeSnapshots bool
|
||||
BSLConfig map[string]string
|
||||
VSLConfig map[string]string
|
||||
@@ -374,6 +375,9 @@ func AllResources(o *VeleroOptions) *unstructured.UnstructuredList {
|
||||
if len(o.Features) > 0 {
|
||||
dsOpts = append(dsOpts, WithFeatures(o.Features))
|
||||
}
|
||||
if o.PrivilegedNodeAgent {
|
||||
dsOpts = append(dsOpts, WithPrivilegedNodeAgent())
|
||||
}
|
||||
ds := DaemonSet(o.Namespace, dsOpts...)
|
||||
if err := appendUnstructured(resources, ds); err != nil {
|
||||
fmt.Printf("error appending DaemonSet %s: %s\n", ds.GetName(), err.Error())
|
||||
|
||||
@@ -505,6 +505,88 @@ func (m *ServerMetrics) InitSchedule(scheduleName string) {
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveSchedule removes metrics associated with a specified schedule.
|
||||
func (m *ServerMetrics) RemoveSchedule(scheduleName string) {
|
||||
if g, ok := m.metrics[backupTarballSizeBytesGauge].(*prometheus.GaugeVec); ok {
|
||||
g.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[backupAttemptTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[backupSuccessTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[backupPartialFailureTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[backupFailureTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[backupValidationFailureTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if h, ok := m.metrics[backupDurationSeconds].(*prometheus.HistogramVec); ok {
|
||||
h.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[backupDeletionAttemptTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[backupDeletionSuccessTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[backupDeletionFailureTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if g, ok := m.metrics[backupLastSuccessfulTimestamp].(*prometheus.GaugeVec); ok {
|
||||
g.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[backupItemsTotalGauge].(*prometheus.GaugeVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[backupItemsErrorsGauge].(*prometheus.GaugeVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[backupWarningTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[backupLastStatus].(*prometheus.GaugeVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[restoreAttemptTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[restorePartialFailureTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[restoreFailedTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[restoreSuccessTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[restoreValidationFailedTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[volumeSnapshotSuccessTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[volumeSnapshotAttemptTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[volumeSnapshotFailureTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName)
|
||||
}
|
||||
if c, ok := m.metrics[csiSnapshotAttemptTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName, "")
|
||||
}
|
||||
if c, ok := m.metrics[csiSnapshotSuccessTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName, "")
|
||||
}
|
||||
if c, ok := m.metrics[csiSnapshotFailureTotal].(*prometheus.CounterVec); ok {
|
||||
c.DeleteLabelValues(scheduleName, "")
|
||||
}
|
||||
}
|
||||
|
||||
// InitSchedule initializes counter metrics for a node.
|
||||
func (m *ServerMetrics) InitMetricsForNode(node string) {
|
||||
if c, ok := m.metrics[podVolumeBackupEnqueueTotal].(*prometheus.CounterVec); ok {
|
||||
|
||||
@@ -200,10 +200,11 @@ func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.
|
||||
b.resultsLock.Unlock()
|
||||
|
||||
var (
|
||||
errs []error
|
||||
podVolumeBackups []*velerov1api.PodVolumeBackup
|
||||
podVolumes = make(map[string]corev1api.Volume)
|
||||
mountedPodVolumes = sets.String{}
|
||||
errs []error
|
||||
podVolumeBackups []*velerov1api.PodVolumeBackup
|
||||
podVolumes = make(map[string]corev1api.Volume)
|
||||
mountedPodVolumes = sets.String{}
|
||||
attachedPodDevices = sets.String{}
|
||||
)
|
||||
pvcSummary := NewPVCBackupSummary()
|
||||
|
||||
@@ -233,6 +234,14 @@ func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.
|
||||
for _, volumeMount := range container.VolumeMounts {
|
||||
mountedPodVolumes.Insert(volumeMount.Name)
|
||||
}
|
||||
for _, volumeDevice := range container.VolumeDevices {
|
||||
attachedPodDevices.Insert(volumeDevice.Name)
|
||||
}
|
||||
}
|
||||
|
||||
repoIdentifier := ""
|
||||
if repositoryType == velerov1api.BackupRepositoryTypeRestic {
|
||||
repoIdentifier = repo.Spec.ResticIdentifier
|
||||
}
|
||||
|
||||
var numVolumeSnapshots int
|
||||
@@ -263,6 +272,15 @@ func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.
|
||||
continue
|
||||
}
|
||||
|
||||
// check if volume is a block volume
|
||||
if attachedPodDevices.Has(volumeName) {
|
||||
msg := fmt.Sprintf("volume %s declared in pod %s/%s is a block volume. Block volumes are not supported for fs backup, skipping",
|
||||
volumeName, pod.Namespace, pod.Name)
|
||||
log.Warn(msg)
|
||||
pvcSummary.addSkipped(volumeName, msg)
|
||||
continue
|
||||
}
|
||||
|
||||
// volumes that are not mounted by any container should not be backed up, because
|
||||
// its directory is not created
|
||||
if !mountedPodVolumes.Has(volumeName) {
|
||||
@@ -283,7 +301,7 @@ func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.
|
||||
}
|
||||
}
|
||||
|
||||
volumeBackup := newPodVolumeBackup(backup, pod, volume, repo.Spec.ResticIdentifier, b.uploaderType, pvc)
|
||||
volumeBackup := newPodVolumeBackup(backup, pod, volume, repoIdentifier, b.uploaderType, pvc)
|
||||
if _, err = b.veleroClient.VeleroV1().PodVolumeBackups(volumeBackup.Namespace).Create(context.TODO(), volumeBackup, metav1.CreateOptions{}); err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
|
||||
@@ -157,6 +157,12 @@ func (r *restorer) RestorePodVolumes(data RestoreData) []error {
|
||||
for _, podVolume := range data.Pod.Spec.Volumes {
|
||||
podVolumes[podVolume.Name] = podVolume
|
||||
}
|
||||
|
||||
repoIdentifier := ""
|
||||
if repositoryType == velerov1api.BackupRepositoryTypeRestic {
|
||||
repoIdentifier = repo.Spec.ResticIdentifier
|
||||
}
|
||||
|
||||
for volume, backupInfo := range volumesToRestore {
|
||||
volumeObj, ok := podVolumes[volume]
|
||||
var pvc *corev1api.PersistentVolumeClaim
|
||||
@@ -170,7 +176,7 @@ func (r *restorer) RestorePodVolumes(data RestoreData) []error {
|
||||
}
|
||||
}
|
||||
|
||||
volumeRestore := newPodVolumeRestore(data.Restore, data.Pod, data.BackupLocation, volume, backupInfo.snapshotID, repo.Spec.ResticIdentifier, backupInfo.uploaderType, data.SourceNamespace, pvc)
|
||||
volumeRestore := newPodVolumeRestore(data.Restore, data.Pod, data.BackupLocation, volume, backupInfo.snapshotID, repoIdentifier, backupInfo.uploaderType, data.SourceNamespace, pvc)
|
||||
|
||||
if err := errorOnly(r.veleroClient.VeleroV1().PodVolumeRestores(volumeRestore.Namespace).Create(context.TODO(), volumeRestore, metav1.CreateOptions{})); err != nil {
|
||||
errs = append(errs, errors.WithStack(err))
|
||||
|
||||
@@ -37,14 +37,6 @@ const (
|
||||
// TODO(2.0): remove
|
||||
podAnnotationPrefix = "snapshot.velero.io/"
|
||||
|
||||
// VolumesToBackupAnnotation is the annotation on a pod whose mounted volumes
|
||||
// need to be backed up using pod volume backup.
|
||||
VolumesToBackupAnnotation = "backup.velero.io/backup-volumes"
|
||||
|
||||
// VolumesToExcludeAnnotation is the annotation on a pod whose mounted volumes
|
||||
// should be excluded from pod volume backup.
|
||||
VolumesToExcludeAnnotation = "backup.velero.io/backup-volumes-excludes"
|
||||
|
||||
// DefaultVolumesToFsBackup specifies whether pod volume backup should be used, by default, to
|
||||
// take backup of all pod volumes.
|
||||
DefaultVolumesToFsBackup = false
|
||||
@@ -216,85 +208,3 @@ func getPodSnapshotAnnotations(obj metav1.Object) map[string]string {
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// GetVolumesToBackup returns a list of volume names to backup for
|
||||
// the provided pod.
|
||||
// Deprecated: Use GetVolumesByPod instead.
|
||||
func GetVolumesToBackup(obj metav1.Object) []string {
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
backupsValue := annotations[VolumesToBackupAnnotation]
|
||||
if backupsValue == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(backupsValue, ",")
|
||||
}
|
||||
|
||||
func getVolumesToExclude(obj metav1.Object) []string {
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(annotations[VolumesToExcludeAnnotation], ",")
|
||||
}
|
||||
|
||||
func contains(list []string, k string) bool {
|
||||
for _, i := range list {
|
||||
if i == k {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetVolumesByPod returns a list of volume names to backup for the provided pod.
|
||||
func GetVolumesByPod(pod *corev1api.Pod, defaultVolumesToFsBackup bool) ([]string, []string) {
|
||||
// tracks the volumes that have been explicitly opted out of backup via the annotation in the pod
|
||||
optedOutVolumes := make([]string, 0)
|
||||
|
||||
if !defaultVolumesToFsBackup {
|
||||
return GetVolumesToBackup(pod), optedOutVolumes
|
||||
}
|
||||
|
||||
volsToExclude := getVolumesToExclude(pod)
|
||||
podVolumes := []string{}
|
||||
for _, pv := range pod.Spec.Volumes {
|
||||
// cannot backup hostpath volumes as they are not mounted into /var/lib/kubelet/pods
|
||||
// and therefore not accessible to the node agent daemon set.
|
||||
if pv.HostPath != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup volumes mounting secrets. Secrets will be backed up separately.
|
||||
if pv.Secret != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup volumes mounting config maps. Config maps will be backed up separately.
|
||||
if pv.ConfigMap != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup volumes mounted as projected volumes, all data in those come from kube state.
|
||||
if pv.Projected != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup DownwardAPI volumes, all data in those come from kube state.
|
||||
if pv.DownwardAPI != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup volumes that are included in the exclude list.
|
||||
if contains(volsToExclude, pv.Name) {
|
||||
optedOutVolumes = append(optedOutVolumes, pv.Name)
|
||||
continue
|
||||
}
|
||||
// don't include volumes that mount the default service account token.
|
||||
if strings.HasPrefix(pv.Name, "default-token") {
|
||||
continue
|
||||
}
|
||||
podVolumes = append(podVolumes, pv.Name)
|
||||
}
|
||||
return podVolumes, optedOutVolumes
|
||||
}
|
||||
|
||||
@@ -17,12 +17,10 @@ limitations under the License.
|
||||
package podvolume
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
@@ -303,322 +301,3 @@ func TestVolumeHasNonRestorableSource(t *testing.T) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVolumesToBackup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
annotations map[string]string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "nil annotations",
|
||||
annotations: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "no volumes to backup",
|
||||
annotations: map[string]string{"foo": "bar"},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "one volume to backup",
|
||||
annotations: map[string]string{"foo": "bar", VolumesToBackupAnnotation: "volume-1"},
|
||||
expected: []string{"volume-1"},
|
||||
},
|
||||
{
|
||||
name: "multiple volumes to backup",
|
||||
annotations: map[string]string{"foo": "bar", VolumesToBackupAnnotation: "volume-1,volume-2,volume-3"},
|
||||
expected: []string{"volume-1", "volume-2", "volume-3"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
pod := &corev1api.Pod{}
|
||||
pod.Annotations = test.annotations
|
||||
|
||||
res := GetVolumesToBackup(pod)
|
||||
|
||||
// sort to ensure good compare of slices
|
||||
sort.Strings(test.expected)
|
||||
sort.Strings(res)
|
||||
|
||||
assert.Equal(t, test.expected, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVolumesByPod(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
pod *corev1api.Pod
|
||||
expected struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}
|
||||
defaultVolumesToFsBackup bool
|
||||
}{
|
||||
{
|
||||
name: "should get PVs from VolumesToBackupAnnotation when defaultVolumesToFsBackup is false",
|
||||
defaultVolumesToFsBackup: false,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToBackupAnnotation: "pvbPV1,pvbPV2,pvbPV3",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should get all pod volumes when defaultVolumesToFsBackup is true and no PVs are excluded",
|
||||
defaultVolumesToFsBackup: true,
|
||||
pod: &corev1api.Pod{
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// PVB Volumes
|
||||
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should get all pod volumes except ones excluded when defaultVolumesToFsBackup is true",
|
||||
defaultVolumesToFsBackup: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// PVB Volumes
|
||||
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
|
||||
/// Excluded from PVB through annotation
|
||||
{Name: "nonPvbPV1"}, {Name: "nonPvbPV2"}, {Name: "nonPvbPV3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{"nonPvbPV1", "nonPvbPV2", "nonPvbPV3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should exclude default service account token from pod volume backup",
|
||||
defaultVolumesToFsBackup: true,
|
||||
pod: &corev1api.Pod{
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// PVB Volumes
|
||||
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
|
||||
/// Excluded from PVB because colume mounting default service account token
|
||||
{Name: "default-token-5xq45"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should exclude host path volumes from pod volume backups",
|
||||
defaultVolumesToFsBackup: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// PVB Volumes
|
||||
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
|
||||
/// Excluded from pod volume backup through annotation
|
||||
{Name: "nonPvbPV1"}, {Name: "nonPvbPV2"}, {Name: "nonPvbPV3"},
|
||||
// Excluded from pod volume backup because hostpath
|
||||
{Name: "hostPath1", VolumeSource: corev1api.VolumeSource{HostPath: &corev1api.HostPathVolumeSource{Path: "/hostpathVol"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{"nonPvbPV1", "nonPvbPV2", "nonPvbPV3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should exclude volumes mounting secrets",
|
||||
defaultVolumesToFsBackup: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// PVB Volumes
|
||||
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
|
||||
/// Excluded from pod volume backup through annotation
|
||||
{Name: "nonPvbPV1"}, {Name: "nonPvbPV2"}, {Name: "nonPvbPV3"},
|
||||
// Excluded from pod volume backup because hostpath
|
||||
{Name: "superSecret", VolumeSource: corev1api.VolumeSource{Secret: &corev1api.SecretVolumeSource{SecretName: "super-secret"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{"nonPvbPV1", "nonPvbPV2", "nonPvbPV3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should exclude volumes mounting config maps",
|
||||
defaultVolumesToFsBackup: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// PVB Volumes
|
||||
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
|
||||
/// Excluded from pod volume backup through annotation
|
||||
{Name: "nonPvbPV1"}, {Name: "nonPvbPV2"}, {Name: "nonPvbPV3"},
|
||||
// Excluded from pod volume backup because hostpath
|
||||
{Name: "appCOnfig", VolumeSource: corev1api.VolumeSource{ConfigMap: &corev1api.ConfigMapVolumeSource{LocalObjectReference: corev1api.LocalObjectReference{Name: "app-config"}}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{"nonPvbPV1", "nonPvbPV2", "nonPvbPV3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should exclude projected volumes",
|
||||
defaultVolumesToFsBackup: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
|
||||
{
|
||||
Name: "projected",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
Projected: &corev1api.ProjectedVolumeSource{
|
||||
Sources: []corev1api.VolumeProjection{{
|
||||
Secret: &corev1api.SecretProjection{
|
||||
LocalObjectReference: corev1api.LocalObjectReference{},
|
||||
Items: nil,
|
||||
Optional: nil,
|
||||
},
|
||||
DownwardAPI: nil,
|
||||
ConfigMap: nil,
|
||||
ServiceAccountToken: nil,
|
||||
}},
|
||||
DefaultMode: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should exclude DownwardAPI volumes",
|
||||
defaultVolumesToFsBackup: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
|
||||
{
|
||||
Name: "downwardAPI",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
DownwardAPI: &corev1api.DownwardAPIVolumeSource{
|
||||
Items: []corev1api.DownwardAPIVolumeFile{
|
||||
{
|
||||
Path: "labels",
|
||||
FieldRef: &corev1api.ObjectFieldSelector{
|
||||
APIVersion: "v1",
|
||||
FieldPath: "metadata.labels",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualIncluded, actualOptedOut := GetVolumesByPod(tc.pod, tc.defaultVolumesToFsBackup)
|
||||
|
||||
sort.Strings(tc.expected.included)
|
||||
sort.Strings(actualIncluded)
|
||||
assert.Equal(t, tc.expected.included, actualIncluded)
|
||||
|
||||
sort.Strings(tc.expected.optedOut)
|
||||
sort.Strings(actualOptedOut)
|
||||
assert.Equal(t, tc.expected.optedOut, actualOptedOut)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kopia/kopia/repo"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
@@ -188,11 +189,13 @@ func (urp *unifiedRepoProvider) PrepareRepo(ctx context.Context, param RepoParam
|
||||
log.Debug("Repo has already been initialized remotely")
|
||||
return nil
|
||||
}
|
||||
log.Infof("failed to connect to the repo: %v, will try to create it", err)
|
||||
if !errors.Is(err, repo.ErrRepositoryNotInitialized) {
|
||||
return errors.Wrap(err, "error to connect to backup repo")
|
||||
}
|
||||
|
||||
err = urp.repoService.Init(ctx, *repoOption, true)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error to init backup repo")
|
||||
return errors.Wrap(err, "error to create backup repo")
|
||||
}
|
||||
|
||||
log.Debug("Prepare repo complete")
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"testing"
|
||||
|
||||
awscredentials "github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/kopia/kopia/repo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -648,7 +649,28 @@ func TestPrepareRepo(t *testing.T) {
|
||||
}
|
||||
return errors.New("fake-error-2")
|
||||
},
|
||||
expectedErr: "error to init backup repo: fake-error-2",
|
||||
expectedErr: "error to connect to backup repo: fake-error-1",
|
||||
},
|
||||
{
|
||||
name: "not initialize",
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
},
|
||||
repoService: new(reposervicenmocks.BackupRepoService),
|
||||
retFuncInit: func(ctx context.Context, repoOption udmrepo.RepoOptions, createNew bool) error {
|
||||
if !createNew {
|
||||
return repo.ErrRepositoryNotInitialized
|
||||
}
|
||||
return errors.New("fake-error-2")
|
||||
},
|
||||
expectedErr: "error to create backup repo: fake-error-2",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
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 kopia
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/kopia/kopia/fs"
|
||||
"github.com/kopia/kopia/fs/virtualfs"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const ErrNotPermitted = "operation not permitted"
|
||||
|
||||
func getLocalBlockEntry(sourcePath string) (fs.Entry, error) {
|
||||
source, err := resolveSymlink(sourcePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "resolveSymlink")
|
||||
}
|
||||
|
||||
fileInfo, err := os.Lstat(source)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to get the source device information %s", source)
|
||||
}
|
||||
|
||||
if (fileInfo.Sys().(*syscall.Stat_t).Mode & syscall.S_IFMT) != syscall.S_IFBLK {
|
||||
return nil, errors.Errorf("source path %s is not a block device", source)
|
||||
}
|
||||
|
||||
device, err := os.Open(source)
|
||||
if err != nil {
|
||||
if os.IsPermission(err) || err.Error() == ErrNotPermitted {
|
||||
return nil, errors.Wrapf(err, "no permission to open the source device %s, make sure that node agent is running in privileged mode", source)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "unable to open the source device %s", source)
|
||||
}
|
||||
|
||||
sf := virtualfs.StreamingFileFromReader(source, device)
|
||||
return virtualfs.NewStaticDirectory(source, []fs.Entry{sf}), nil
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
Copyright The Velero Contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package kopia
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/kopia/kopia/fs"
|
||||
"github.com/kopia/kopia/snapshot/restore"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type BlockOutput struct {
|
||||
*restore.FilesystemOutput
|
||||
|
||||
targetFileName string
|
||||
}
|
||||
|
||||
var _ restore.Output = &BlockOutput{}
|
||||
|
||||
const bufferSize = 128 * 1024
|
||||
|
||||
func (o *BlockOutput) WriteFile(ctx context.Context, relativePath string, remoteFile fs.File) error {
|
||||
remoteReader, err := remoteFile.Open(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open remote file %s", remoteFile.Name())
|
||||
}
|
||||
defer remoteReader.Close()
|
||||
|
||||
targetFile, err := os.Create(o.targetFileName)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open file %s", o.targetFileName)
|
||||
}
|
||||
defer targetFile.Close()
|
||||
|
||||
buffer := make([]byte, bufferSize)
|
||||
|
||||
readData := true
|
||||
for readData {
|
||||
bytesToWrite, err := remoteReader.Read(buffer)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
return errors.Wrapf(err, "failed to read data from remote file %s", o.targetFileName)
|
||||
}
|
||||
readData = false
|
||||
}
|
||||
|
||||
if bytesToWrite > 0 {
|
||||
offset := 0
|
||||
for bytesToWrite > 0 {
|
||||
if bytesWritten, err := targetFile.Write(buffer[offset:bytesToWrite]); err == nil {
|
||||
bytesToWrite -= bytesWritten
|
||||
offset += bytesWritten
|
||||
} else {
|
||||
return errors.Wrapf(err, "failed to write data to file %s", o.targetFileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *BlockOutput) BeginDirectory(ctx context.Context, relativePath string, e fs.Directory) error {
|
||||
var err error
|
||||
o.targetFileName, err = filepath.EvalSymlinks(o.TargetPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to evaluate symlinks for %s", o.targetFileName)
|
||||
}
|
||||
|
||||
fileInfo, err := os.Lstat(o.targetFileName)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to get the target device information for %s", o.TargetPath)
|
||||
}
|
||||
|
||||
if (fileInfo.Sys().(*syscall.Stat_t).Mode & syscall.S_IFMT) != syscall.S_IFBLK {
|
||||
return errors.Errorf("target file %s is not a block device", o.TargetPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -68,6 +68,11 @@ func newOptionalInt(b int) *policy.OptionalInt {
|
||||
return &ob
|
||||
}
|
||||
|
||||
func newOptionalInt64(b int64) *policy.OptionalInt64 {
|
||||
ob := policy.OptionalInt64(b)
|
||||
return &ob
|
||||
}
|
||||
|
||||
func newOptionalBool(b bool) *policy.OptionalBool {
|
||||
ob := policy.OptionalBool(b)
|
||||
return &ob
|
||||
@@ -88,7 +93,7 @@ func getDefaultPolicy() *policy.Policy {
|
||||
},
|
||||
UploadPolicy: policy.UploadPolicy{
|
||||
MaxParallelFileReads: newOptionalInt(runtime.NumCPU()),
|
||||
ParallelUploadAboveSize: nil,
|
||||
ParallelUploadAboveSize: newOptionalInt64(math.MaxInt64),
|
||||
},
|
||||
SchedulingPolicy: policy.SchedulingPolicy{
|
||||
Manual: true,
|
||||
@@ -106,6 +111,11 @@ func setupDefaultPolicy(ctx context.Context, rep repo.RepositoryWriter, sourceIn
|
||||
return nil, errors.Wrap(err, "error to set policy")
|
||||
}
|
||||
|
||||
err = rep.Flush(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error to flush repo")
|
||||
}
|
||||
|
||||
// retrieve policy from repo
|
||||
policyTree, err := treeForSourceFunc(ctx, rep, sourceInfo)
|
||||
if err != nil {
|
||||
@@ -121,25 +131,22 @@ func Backup(ctx context.Context, fsUploader SnapshotUploader, repoWriter repo.Re
|
||||
if fsUploader == nil {
|
||||
return nil, false, errors.New("get empty kopia uploader")
|
||||
}
|
||||
|
||||
if volMode == uploader.PersistentVolumeBlock {
|
||||
return nil, false, errors.New("unable to handle block storage")
|
||||
}
|
||||
|
||||
dir, err := filepath.Abs(sourcePath)
|
||||
source, err := filepath.Abs(sourcePath)
|
||||
if err != nil {
|
||||
return nil, false, errors.Wrapf(err, "Invalid source path '%s'", sourcePath)
|
||||
}
|
||||
|
||||
// to be consistent with restic when backup empty dir returns one error for upper logic handle
|
||||
dirs, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, false, errors.Wrapf(err, "Unable to read dir in path %s", dir)
|
||||
} else if len(dirs) == 0 {
|
||||
return nil, true, nil
|
||||
if volMode == uploader.PersistentVolumeFilesystem {
|
||||
// to be consistent with restic when backup empty dir returns one error for upper logic handle
|
||||
dirs, err := os.ReadDir(source)
|
||||
if err != nil {
|
||||
return nil, false, errors.Wrapf(err, "Unable to read dir in path %s", source)
|
||||
} else if len(dirs) == 0 {
|
||||
return nil, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
dir = filepath.Clean(dir)
|
||||
source = filepath.Clean(source)
|
||||
|
||||
sourceInfo := snapshot.SourceInfo{
|
||||
UserName: udmrepo.GetRepoUser(),
|
||||
@@ -147,16 +154,25 @@ func Backup(ctx context.Context, fsUploader SnapshotUploader, repoWriter repo.Re
|
||||
Path: filepath.Clean(realSource),
|
||||
}
|
||||
if realSource == "" {
|
||||
sourceInfo.Path = dir
|
||||
sourceInfo.Path = source
|
||||
}
|
||||
|
||||
rootDir, err := getLocalFSEntry(dir)
|
||||
if err != nil {
|
||||
return nil, false, errors.Wrap(err, "Unable to get local filesystem entry")
|
||||
var sourceEntry fs.Entry
|
||||
|
||||
if volMode == uploader.PersistentVolumeBlock {
|
||||
sourceEntry, err = getLocalBlockEntry(source)
|
||||
if err != nil {
|
||||
return nil, false, errors.Wrap(err, "unable to get local block device entry")
|
||||
}
|
||||
} else {
|
||||
sourceEntry, err = getLocalFSEntry(source)
|
||||
if err != nil {
|
||||
return nil, false, errors.Wrap(err, "unable to get local filesystem entry")
|
||||
}
|
||||
}
|
||||
|
||||
kopiaCtx := kopia.SetupKopiaLog(ctx, log)
|
||||
snapID, snapshotSize, err := SnapshotSource(kopiaCtx, repoWriter, fsUploader, sourceInfo, rootDir, forceFull, parentSnapshot, tags, log, "Kopia Uploader")
|
||||
snapID, snapshotSize, err := SnapshotSource(kopiaCtx, repoWriter, fsUploader, sourceInfo, sourceEntry, forceFull, parentSnapshot, tags, log, "Kopia Uploader")
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -338,7 +354,8 @@ func findPreviousSnapshotManifest(ctx context.Context, rep repo.Repository, sour
|
||||
}
|
||||
|
||||
// Restore restore specific sourcePath with given snapshotID and update progress
|
||||
func Restore(ctx context.Context, rep repo.RepositoryWriter, progress *Progress, snapshotID, dest string, log logrus.FieldLogger, cancleCh chan struct{}) (int64, int32, error) {
|
||||
func Restore(ctx context.Context, rep repo.RepositoryWriter, progress *Progress, snapshotID, dest string, volMode uploader.PersistentVolumeMode,
|
||||
log logrus.FieldLogger, cancleCh chan struct{}) (int64, int32, error) {
|
||||
log.Info("Start to restore...")
|
||||
|
||||
kopiaCtx := kopia.SetupKopiaLog(ctx, log)
|
||||
@@ -360,7 +377,7 @@ func Restore(ctx context.Context, rep repo.RepositoryWriter, progress *Progress,
|
||||
return 0, 0, errors.Wrapf(err, "Unable to resolve path %v", dest)
|
||||
}
|
||||
|
||||
output := &restore.FilesystemOutput{
|
||||
fsOutput := &restore.FilesystemOutput{
|
||||
TargetPath: path,
|
||||
OverwriteDirectories: true,
|
||||
OverwriteFiles: true,
|
||||
@@ -368,11 +385,18 @@ func Restore(ctx context.Context, rep repo.RepositoryWriter, progress *Progress,
|
||||
IgnorePermissionErrors: true,
|
||||
}
|
||||
|
||||
err = output.Init(ctx)
|
||||
err = fsOutput.Init(ctx)
|
||||
if err != nil {
|
||||
return 0, 0, errors.Wrap(err, "error to init output")
|
||||
}
|
||||
|
||||
var output restore.Output = fsOutput
|
||||
if volMode == uploader.PersistentVolumeBlock {
|
||||
output = &BlockOutput{
|
||||
FilesystemOutput: fsOutput,
|
||||
}
|
||||
}
|
||||
|
||||
stat, err := restoreEntryFunc(kopiaCtx, rep, output, rootEntry, restore.Options{
|
||||
Parallel: runtime.NumCPU(),
|
||||
RestoreDirEntryAtDepth: math.MaxInt32,
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/kopia/kopia/fs"
|
||||
"github.com/kopia/kopia/fs/virtualfs"
|
||||
"github.com/kopia/kopia/repo"
|
||||
"github.com/kopia/kopia/repo/manifest"
|
||||
"github.com/kopia/kopia/snapshot"
|
||||
@@ -594,11 +595,11 @@ func TestBackup(t *testing.T) {
|
||||
expectedError: errors.New("Unable to read dir"),
|
||||
},
|
||||
{
|
||||
name: "Unable to handle block mode",
|
||||
name: "Source path is not a block device",
|
||||
sourcePath: "/",
|
||||
tags: nil,
|
||||
volMode: uploader.PersistentVolumeBlock,
|
||||
expectedError: errors.New("unable to handle block storage"),
|
||||
expectedError: errors.New("source path / is not a block device"),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -660,6 +661,7 @@ func TestRestore(t *testing.T) {
|
||||
expectedBytes int64
|
||||
expectedCount int32
|
||||
expectedError error
|
||||
volMode uploader.PersistentVolumeMode
|
||||
}
|
||||
|
||||
// Define test cases
|
||||
@@ -697,6 +699,46 @@ func TestRestore(t *testing.T) {
|
||||
snapshotID: "snapshot-123",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Expect block volume successful",
|
||||
filesystemEntryFunc: func(ctx context.Context, rep repo.Repository, rootID string, consistentAttributes bool) (fs.Entry, error) {
|
||||
return snapshotfs.EntryFromDirEntry(rep, &snapshot.DirEntry{Type: snapshot.EntryTypeFile}), nil
|
||||
},
|
||||
restoreEntryFunc: func(ctx context.Context, rep repo.Repository, output restore.Output, rootEntry fs.Entry, options restore.Options) (restore.Stats, error) {
|
||||
return restore.Stats{}, nil
|
||||
},
|
||||
snapshotID: "snapshot-123",
|
||||
expectedError: nil,
|
||||
volMode: uploader.PersistentVolumeBlock,
|
||||
},
|
||||
{
|
||||
name: "Unable to evaluate symlinks for block volume",
|
||||
filesystemEntryFunc: func(ctx context.Context, rep repo.Repository, rootID string, consistentAttributes bool) (fs.Entry, error) {
|
||||
return snapshotfs.EntryFromDirEntry(rep, &snapshot.DirEntry{Type: snapshot.EntryTypeFile}), nil
|
||||
},
|
||||
restoreEntryFunc: func(ctx context.Context, rep repo.Repository, output restore.Output, rootEntry fs.Entry, options restore.Options) (restore.Stats, error) {
|
||||
err := output.BeginDirectory(ctx, "fake-dir", virtualfs.NewStaticDirectory("fake-dir", nil))
|
||||
return restore.Stats{}, err
|
||||
},
|
||||
snapshotID: "snapshot-123",
|
||||
expectedError: errors.New("unable to evaluate symlinks for"),
|
||||
volMode: uploader.PersistentVolumeBlock,
|
||||
dest: "/wrong-dest",
|
||||
},
|
||||
{
|
||||
name: "Target file is not a block device",
|
||||
filesystemEntryFunc: func(ctx context.Context, rep repo.Repository, rootID string, consistentAttributes bool) (fs.Entry, error) {
|
||||
return snapshotfs.EntryFromDirEntry(rep, &snapshot.DirEntry{Type: snapshot.EntryTypeFile}), nil
|
||||
},
|
||||
restoreEntryFunc: func(ctx context.Context, rep repo.Repository, output restore.Output, rootEntry fs.Entry, options restore.Options) (restore.Stats, error) {
|
||||
err := output.BeginDirectory(ctx, "fake-dir", virtualfs.NewStaticDirectory("fake-dir", nil))
|
||||
return restore.Stats{}, err
|
||||
},
|
||||
snapshotID: "snapshot-123",
|
||||
expectedError: errors.New("target file /tmp is not a block device"),
|
||||
volMode: uploader.PersistentVolumeBlock,
|
||||
dest: "/tmp",
|
||||
},
|
||||
}
|
||||
|
||||
em := &manifest.EntryMetadata{
|
||||
@@ -706,6 +748,10 @@ func TestRestore(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.volMode == "" {
|
||||
tc.volMode = uploader.PersistentVolumeFilesystem
|
||||
}
|
||||
|
||||
if tc.invalidManifestType {
|
||||
em.Labels[manifest.TypeLabelKey] = ""
|
||||
} else {
|
||||
@@ -725,7 +771,7 @@ func TestRestore(t *testing.T) {
|
||||
repoWriterMock.On("OpenObject", mock.Anything, mock.Anything).Return(em, nil)
|
||||
|
||||
progress := new(Progress)
|
||||
bytesRestored, fileCount, err := Restore(context.Background(), repoWriterMock, progress, tc.snapshotID, tc.dest, logrus.New(), nil)
|
||||
bytesRestored, fileCount, err := Restore(context.Background(), repoWriterMock, progress, tc.snapshotID, tc.dest, tc.volMode, logrus.New(), nil)
|
||||
|
||||
// Check if the returned error matches the expected error
|
||||
if tc.expectedError != nil {
|
||||
|
||||
@@ -128,11 +128,6 @@ func (kp *kopiaProvider) RunBackup(
|
||||
return "", false, errors.New("path is empty")
|
||||
}
|
||||
|
||||
// For now, error on block mode
|
||||
if volMode == uploader.PersistentVolumeBlock {
|
||||
return "", false, errors.New("unable to currently support block mode")
|
||||
}
|
||||
|
||||
log := kp.log.WithFields(logrus.Fields{
|
||||
"path": path,
|
||||
"realSource": realSource,
|
||||
@@ -214,10 +209,6 @@ func (kp *kopiaProvider) RunRestore(
|
||||
"volumePath": volumePath,
|
||||
})
|
||||
|
||||
if volMode == uploader.PersistentVolumeBlock {
|
||||
return errors.New("unable to currently support block mode")
|
||||
}
|
||||
|
||||
repoWriter := kopia.NewShimRepo(kp.bkRepo)
|
||||
progress := new(kopia.Progress)
|
||||
progress.InitThrottle(restoreProgressCheckInterval)
|
||||
@@ -235,7 +226,7 @@ func (kp *kopiaProvider) RunRestore(
|
||||
// We use the cancel channel to control the restore cancel, so don't pass a context with cancel to Kopia restore.
|
||||
// Otherwise, Kopia restore will not response to the cancel control but return an arbitrary error.
|
||||
// Kopia restore cancel is not designed as well as Kopia backup which uses the context to control backup cancel all the way.
|
||||
size, fileCount, err := RestoreFunc(context.Background(), repoWriter, progress, snapshotID, volumePath, log, restoreCancel)
|
||||
size, fileCount, err := RestoreFunc(context.Background(), repoWriter, progress, snapshotID, volumePath, volMode, log, restoreCancel)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed to run kopia restore")
|
||||
|
||||
@@ -94,12 +94,12 @@ func TestRunBackup(t *testing.T) {
|
||||
notError: false,
|
||||
},
|
||||
{
|
||||
name: "error on vol mode",
|
||||
name: "success to backup block mode volume",
|
||||
hookBackupFunc: func(ctx context.Context, fsUploader kopia.SnapshotUploader, repoWriter repo.RepositoryWriter, sourcePath string, realSource string, forceFull bool, parentSnapshot string, volMode uploader.PersistentVolumeMode, tags map[string]string, log logrus.FieldLogger) (*uploader.SnapshotInfo, bool, error) {
|
||||
return nil, true, nil
|
||||
return &uploader.SnapshotInfo{}, false, nil
|
||||
},
|
||||
volMode: uploader.PersistentVolumeBlock,
|
||||
notError: false,
|
||||
notError: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
@@ -125,31 +125,31 @@ func TestRunRestore(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
hookRestoreFunc func(ctx context.Context, rep repo.RepositoryWriter, progress *kopia.Progress, snapshotID, dest string, log logrus.FieldLogger, cancleCh chan struct{}) (int64, int32, error)
|
||||
hookRestoreFunc func(ctx context.Context, rep repo.RepositoryWriter, progress *kopia.Progress, snapshotID, dest string, volMode uploader.PersistentVolumeMode, log logrus.FieldLogger, cancleCh chan struct{}) (int64, int32, error)
|
||||
notError bool
|
||||
volMode uploader.PersistentVolumeMode
|
||||
}{
|
||||
{
|
||||
name: "normal restore",
|
||||
hookRestoreFunc: func(ctx context.Context, rep repo.RepositoryWriter, progress *kopia.Progress, snapshotID, dest string, log logrus.FieldLogger, cancleCh chan struct{}) (int64, int32, error) {
|
||||
hookRestoreFunc: func(ctx context.Context, rep repo.RepositoryWriter, progress *kopia.Progress, snapshotID, dest string, volMode uploader.PersistentVolumeMode, log logrus.FieldLogger, cancleCh chan struct{}) (int64, int32, error) {
|
||||
return 0, 0, nil
|
||||
},
|
||||
notError: true,
|
||||
},
|
||||
{
|
||||
name: "failed to restore",
|
||||
hookRestoreFunc: func(ctx context.Context, rep repo.RepositoryWriter, progress *kopia.Progress, snapshotID, dest string, log logrus.FieldLogger, cancleCh chan struct{}) (int64, int32, error) {
|
||||
hookRestoreFunc: func(ctx context.Context, rep repo.RepositoryWriter, progress *kopia.Progress, snapshotID, dest string, volMode uploader.PersistentVolumeMode, log logrus.FieldLogger, cancleCh chan struct{}) (int64, int32, error) {
|
||||
return 0, 0, errors.New("failed to restore")
|
||||
},
|
||||
notError: false,
|
||||
},
|
||||
{
|
||||
name: "failed to restore block mode",
|
||||
hookRestoreFunc: func(ctx context.Context, rep repo.RepositoryWriter, progress *kopia.Progress, snapshotID, dest string, log logrus.FieldLogger, cancleCh chan struct{}) (int64, int32, error) {
|
||||
return 0, 0, errors.New("failed to restore")
|
||||
name: "normal block mode restore",
|
||||
hookRestoreFunc: func(ctx context.Context, rep repo.RepositoryWriter, progress *kopia.Progress, snapshotID, dest string, volMode uploader.PersistentVolumeMode, log logrus.FieldLogger, cancleCh chan struct{}) (int64, int32, error) {
|
||||
return 0, 0, nil
|
||||
},
|
||||
volMode: uploader.PersistentVolumeBlock,
|
||||
notError: false,
|
||||
notError: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -212,6 +212,9 @@ func TestResticRunRestore(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.volMode == "" {
|
||||
tc.volMode = uploader.PersistentVolumeFilesystem
|
||||
}
|
||||
resticRestoreCMDFunc = tc.hookResticRestoreFunc
|
||||
if tc.volMode == "" {
|
||||
tc.volMode = uploader.PersistentVolumeFilesystem
|
||||
|
||||
@@ -316,3 +316,23 @@ func WaitPVBound(ctx context.Context, pvGetter corev1client.CoreV1Interface, pvN
|
||||
func IsPVCBound(pvc *corev1api.PersistentVolumeClaim) bool {
|
||||
return pvc.Spec.VolumeName != ""
|
||||
}
|
||||
|
||||
// MakePodPVCAttachment returns the volume mounts and devices for a pod needed to attach a PVC
|
||||
func MakePodPVCAttachment(volumeName string, volumeMode *corev1api.PersistentVolumeMode) ([]corev1api.VolumeMount, []corev1api.VolumeDevice) {
|
||||
var volumeMounts []corev1api.VolumeMount = nil
|
||||
var volumeDevices []corev1api.VolumeDevice = nil
|
||||
|
||||
if volumeMode != nil && *volumeMode == corev1api.PersistentVolumeBlock {
|
||||
volumeDevices = []corev1api.VolumeDevice{{
|
||||
Name: volumeName,
|
||||
DevicePath: "/" + volumeName,
|
||||
}}
|
||||
} else {
|
||||
volumeMounts = []corev1api.VolumeMount{{
|
||||
Name: volumeName,
|
||||
MountPath: "/" + volumeName,
|
||||
}}
|
||||
}
|
||||
|
||||
return volumeMounts, volumeDevices
|
||||
}
|
||||
|
||||
+59
-20
@@ -35,6 +35,7 @@ import (
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/uploader"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
)
|
||||
|
||||
@@ -50,6 +51,8 @@ const (
|
||||
KubeAnnSelectedNode = "volume.kubernetes.io/selected-node"
|
||||
)
|
||||
|
||||
var ErrorPodVolumeIsNotPVC = errors.New("pod volume is not a PVC")
|
||||
|
||||
// NamespaceAndName returns a string in the format <namespace>/<name>
|
||||
func NamespaceAndName(objMeta metav1.Object) string {
|
||||
if objMeta.GetNamespace() == "" {
|
||||
@@ -122,6 +125,57 @@ func EnsureNamespaceExistsAndIsReady(namespace *corev1api.Namespace, client core
|
||||
// where the specified volume lives.
|
||||
// For volumes with a CSIVolumeSource, append "/mount" to the directory name.
|
||||
func GetVolumeDirectory(ctx context.Context, log logrus.FieldLogger, pod *corev1api.Pod, volumeName string, cli client.Client) (string, error) {
|
||||
pvc, pv, volume, err := GetPodPVCVolume(ctx, log, pod, volumeName, cli)
|
||||
if err != nil {
|
||||
// This case implies the administrator created the PV and attached it directly, without PVC.
|
||||
// Note that only one VolumeSource can be populated per Volume on a pod
|
||||
if err == ErrorPodVolumeIsNotPVC {
|
||||
if volume.VolumeSource.CSI != nil {
|
||||
return volume.Name + "/mount", nil
|
||||
}
|
||||
return volume.Name, nil
|
||||
}
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Most common case is that we have a PVC VolumeSource, and we need to check the PV it points to for a CSI source.
|
||||
// PV's been created with a CSI source.
|
||||
isProvisionedByCSI, err := isProvisionedByCSI(log, pv, cli)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
if isProvisionedByCSI {
|
||||
if pv.Spec.VolumeMode != nil && *pv.Spec.VolumeMode == corev1api.PersistentVolumeBlock {
|
||||
return pvc.Spec.VolumeName, nil
|
||||
}
|
||||
return pvc.Spec.VolumeName + "/mount", nil
|
||||
}
|
||||
|
||||
return pvc.Spec.VolumeName, nil
|
||||
}
|
||||
|
||||
// GetVolumeMode gets the uploader.PersistentVolumeMode of the volume.
|
||||
func GetVolumeMode(ctx context.Context, log logrus.FieldLogger, pod *corev1api.Pod, volumeName string, cli client.Client) (
|
||||
uploader.PersistentVolumeMode, error) {
|
||||
_, pv, _, err := GetPodPVCVolume(ctx, log, pod, volumeName, cli)
|
||||
|
||||
if err != nil {
|
||||
if err == ErrorPodVolumeIsNotPVC {
|
||||
return uploader.PersistentVolumeFilesystem, nil
|
||||
}
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if pv.Spec.VolumeMode != nil && *pv.Spec.VolumeMode == corev1api.PersistentVolumeBlock {
|
||||
return uploader.PersistentVolumeBlock, nil
|
||||
}
|
||||
return uploader.PersistentVolumeFilesystem, nil
|
||||
}
|
||||
|
||||
// GetPodPVCVolume gets the PVC, PV and volume for a pod volume name.
|
||||
// Returns pod volume in case of ErrorPodVolumeIsNotPVC error
|
||||
func GetPodPVCVolume(ctx context.Context, log logrus.FieldLogger, pod *corev1api.Pod, volumeName string, cli client.Client) (
|
||||
*corev1api.PersistentVolumeClaim, *corev1api.PersistentVolume, *corev1api.Volume, error) {
|
||||
var volume *corev1api.Volume
|
||||
|
||||
for i := range pod.Spec.Volumes {
|
||||
@@ -132,41 +186,26 @@ func GetVolumeDirectory(ctx context.Context, log logrus.FieldLogger, pod *corev1
|
||||
}
|
||||
|
||||
if volume == nil {
|
||||
return "", errors.New("volume not found in pod")
|
||||
return nil, nil, nil, errors.New("volume not found in pod")
|
||||
}
|
||||
|
||||
// This case implies the administrator created the PV and attached it directly, without PVC.
|
||||
// Note that only one VolumeSource can be populated per Volume on a pod
|
||||
if volume.VolumeSource.PersistentVolumeClaim == nil {
|
||||
if volume.VolumeSource.CSI != nil {
|
||||
return volume.Name + "/mount", nil
|
||||
}
|
||||
return volume.Name, nil
|
||||
return nil, nil, volume, ErrorPodVolumeIsNotPVC // There is a pod volume but it is not a PVC
|
||||
}
|
||||
|
||||
// Most common case is that we have a PVC VolumeSource, and we need to check the PV it points to for a CSI source.
|
||||
pvc := &corev1api.PersistentVolumeClaim{}
|
||||
err := cli.Get(ctx, client.ObjectKey{Namespace: pod.Namespace, Name: volume.VolumeSource.PersistentVolumeClaim.ClaimName}, pvc)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
return nil, nil, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
pv := &corev1api.PersistentVolume{}
|
||||
err = cli.Get(ctx, client.ObjectKey{Name: pvc.Spec.VolumeName}, pv)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
return nil, nil, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
// PV's been created with a CSI source.
|
||||
isProvisionedByCSI, err := isProvisionedByCSI(log, pv, cli)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
if isProvisionedByCSI {
|
||||
return pvc.Spec.VolumeName + "/mount", nil
|
||||
}
|
||||
|
||||
return pvc.Spec.VolumeName, nil
|
||||
return pvc, pv, volume, nil
|
||||
}
|
||||
|
||||
// isProvisionedByCSI function checks whether this is a CSI PV by annotation.
|
||||
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
"github.com/vmware-tanzu/velero/pkg/uploader"
|
||||
)
|
||||
|
||||
func TestNamespaceAndName(t *testing.T) {
|
||||
@@ -164,6 +165,13 @@ func TestGetVolumeDirectorySuccess(t *testing.T) {
|
||||
pv: builder.ForPersistentVolume("a-pv").CSI("csi.test.com", "provider-volume-id").Result(),
|
||||
want: "a-pv/mount",
|
||||
},
|
||||
{
|
||||
name: "Block CSI volume with a PVC/PV does not append '/mount' to the volume name",
|
||||
pod: builder.ForPod("ns-1", "my-pod").Volumes(builder.ForVolume("my-vol").PersistentVolumeClaimSource("my-pvc").Result()).Result(),
|
||||
pvc: builder.ForPersistentVolumeClaim("ns-1", "my-pvc").VolumeName("a-pv").Result(),
|
||||
pv: builder.ForPersistentVolume("a-pv").CSI("csi.test.com", "provider-volume-id").VolumeMode(corev1.PersistentVolumeBlock).Result(),
|
||||
want: "a-pv",
|
||||
},
|
||||
{
|
||||
name: "CSI volume mounted without a PVC appends '/mount' to the volume name",
|
||||
pod: builder.ForPod("ns-1", "my-pod").Volumes(builder.ForVolume("my-vol").CSISource("csi.test.com").Result()).Result(),
|
||||
@@ -211,6 +219,54 @@ func TestGetVolumeDirectorySuccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetVolumeModeSuccess tests the GetVolumeMode function
|
||||
func TestGetVolumeModeSuccess(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pod *corev1.Pod
|
||||
pvc *corev1.PersistentVolumeClaim
|
||||
pv *corev1.PersistentVolume
|
||||
want uploader.PersistentVolumeMode
|
||||
}{
|
||||
{
|
||||
name: "Filesystem PVC volume",
|
||||
pod: builder.ForPod("ns-1", "my-pod").Volumes(builder.ForVolume("my-vol").PersistentVolumeClaimSource("my-pvc").Result()).Result(),
|
||||
pvc: builder.ForPersistentVolumeClaim("ns-1", "my-pvc").VolumeName("a-pv").Result(),
|
||||
pv: builder.ForPersistentVolume("a-pv").VolumeMode(corev1.PersistentVolumeFilesystem).Result(),
|
||||
want: uploader.PersistentVolumeFilesystem,
|
||||
},
|
||||
{
|
||||
name: "Block PVC volume",
|
||||
pod: builder.ForPod("ns-1", "my-pod").Volumes(builder.ForVolume("my-vol").PersistentVolumeClaimSource("my-pvc").Result()).Result(),
|
||||
pvc: builder.ForPersistentVolumeClaim("ns-1", "my-pvc").VolumeName("a-pv").Result(),
|
||||
pv: builder.ForPersistentVolume("a-pv").VolumeMode(corev1.PersistentVolumeBlock).Result(),
|
||||
want: uploader.PersistentVolumeBlock,
|
||||
},
|
||||
{
|
||||
name: "Pod volume without a PVC",
|
||||
pod: builder.ForPod("ns-1", "my-pod").Volumes(builder.ForVolume("my-vol").Result()).Result(),
|
||||
want: uploader.PersistentVolumeFilesystem,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
clientBuilder := fake.NewClientBuilder()
|
||||
|
||||
if tc.pvc != nil {
|
||||
clientBuilder = clientBuilder.WithObjects(tc.pvc)
|
||||
}
|
||||
if tc.pv != nil {
|
||||
clientBuilder = clientBuilder.WithObjects(tc.pv)
|
||||
}
|
||||
|
||||
// Function under test
|
||||
mode, err := GetVolumeMode(context.Background(), logrus.StandardLogger(), tc.pod, tc.pod.Spec.Volumes[0].Name, clientBuilder.Build())
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsV1Beta1CRDReady(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
Copyright the Velero contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package podvolume
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
)
|
||||
|
||||
// GetVolumesByPod returns a list of volume names to backup for the provided pod.
|
||||
func GetVolumesByPod(pod *corev1api.Pod, defaultVolumesToFsBackup bool) ([]string, []string) {
|
||||
// tracks the volumes that have been explicitly opted out of backup via the annotation in the pod
|
||||
optedOutVolumes := make([]string, 0)
|
||||
|
||||
if !defaultVolumesToFsBackup {
|
||||
return GetVolumesToBackup(pod), optedOutVolumes
|
||||
}
|
||||
|
||||
volsToExclude := getVolumesToExclude(pod)
|
||||
podVolumes := []string{}
|
||||
for _, pv := range pod.Spec.Volumes {
|
||||
// cannot backup hostpath volumes as they are not mounted into /var/lib/kubelet/pods
|
||||
// and therefore not accessible to the node agent daemon set.
|
||||
if pv.HostPath != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup volumes mounting secrets. Secrets will be backed up separately.
|
||||
if pv.Secret != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup volumes mounting config maps. Config maps will be backed up separately.
|
||||
if pv.ConfigMap != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup volumes mounted as projected volumes, all data in those come from kube state.
|
||||
if pv.Projected != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup DownwardAPI volumes, all data in those come from kube state.
|
||||
if pv.DownwardAPI != nil {
|
||||
continue
|
||||
}
|
||||
// don't backup volumes that are included in the exclude list.
|
||||
if contains(volsToExclude, pv.Name) {
|
||||
optedOutVolumes = append(optedOutVolumes, pv.Name)
|
||||
continue
|
||||
}
|
||||
// don't include volumes that mount the default service account token.
|
||||
if strings.HasPrefix(pv.Name, "default-token") {
|
||||
continue
|
||||
}
|
||||
podVolumes = append(podVolumes, pv.Name)
|
||||
}
|
||||
return podVolumes, optedOutVolumes
|
||||
}
|
||||
|
||||
// GetVolumesToBackup returns a list of volume names to backup for
|
||||
// the provided pod.
|
||||
// Deprecated: Use GetVolumesByPod instead.
|
||||
func GetVolumesToBackup(obj metav1.Object) []string {
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
backupsValue := annotations[velerov1api.VolumesToBackupAnnotation]
|
||||
if backupsValue == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(backupsValue, ",")
|
||||
}
|
||||
|
||||
func getVolumesToExclude(obj metav1.Object) []string {
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(annotations[velerov1api.VolumesToExcludeAnnotation], ",")
|
||||
}
|
||||
|
||||
func contains(list []string, k string) bool {
|
||||
for _, i := range list {
|
||||
if i == k {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
Copyright the Velero contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package podvolume
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
)
|
||||
|
||||
func TestGetVolumesToBackup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
annotations map[string]string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "nil annotations",
|
||||
annotations: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "no volumes to backup",
|
||||
annotations: map[string]string{"foo": "bar"},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "one volume to backup",
|
||||
annotations: map[string]string{"foo": "bar", velerov1api.VolumesToBackupAnnotation: "volume-1"},
|
||||
expected: []string{"volume-1"},
|
||||
},
|
||||
{
|
||||
name: "multiple volumes to backup",
|
||||
annotations: map[string]string{"foo": "bar", velerov1api.VolumesToBackupAnnotation: "volume-1,volume-2,volume-3"},
|
||||
expected: []string{"volume-1", "volume-2", "volume-3"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
pod := &corev1api.Pod{}
|
||||
pod.Annotations = test.annotations
|
||||
|
||||
res := GetVolumesToBackup(pod)
|
||||
|
||||
// sort to ensure good compare of slices
|
||||
sort.Strings(test.expected)
|
||||
sort.Strings(res)
|
||||
|
||||
assert.Equal(t, test.expected, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVolumesByPod(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
pod *corev1api.Pod
|
||||
expected struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}
|
||||
defaultVolumesToFsBackup bool
|
||||
}{
|
||||
{
|
||||
name: "should get PVs from VolumesToBackupAnnotation when defaultVolumesToFsBackup is false",
|
||||
defaultVolumesToFsBackup: false,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
velerov1api.VolumesToBackupAnnotation: "pvbPV1,pvbPV2,pvbPV3",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should get all pod volumes when defaultVolumesToFsBackup is true and no PVs are excluded",
|
||||
defaultVolumesToFsBackup: true,
|
||||
pod: &corev1api.Pod{
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// PVB Volumes
|
||||
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should get all pod volumes except ones excluded when defaultVolumesToFsBackup is true",
|
||||
defaultVolumesToFsBackup: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
velerov1api.VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// PVB Volumes
|
||||
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
|
||||
/// Excluded from PVB through annotation
|
||||
{Name: "nonPvbPV1"}, {Name: "nonPvbPV2"}, {Name: "nonPvbPV3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{"nonPvbPV1", "nonPvbPV2", "nonPvbPV3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should exclude default service account token from pod volume backup",
|
||||
defaultVolumesToFsBackup: true,
|
||||
pod: &corev1api.Pod{
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// PVB Volumes
|
||||
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
|
||||
/// Excluded from PVB because colume mounting default service account token
|
||||
{Name: "default-token-5xq45"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should exclude host path volumes from pod volume backups",
|
||||
defaultVolumesToFsBackup: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
velerov1api.VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// PVB Volumes
|
||||
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
|
||||
/// Excluded from pod volume backup through annotation
|
||||
{Name: "nonPvbPV1"}, {Name: "nonPvbPV2"}, {Name: "nonPvbPV3"},
|
||||
// Excluded from pod volume backup because hostpath
|
||||
{Name: "hostPath1", VolumeSource: corev1api.VolumeSource{HostPath: &corev1api.HostPathVolumeSource{Path: "/hostpathVol"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{"nonPvbPV1", "nonPvbPV2", "nonPvbPV3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should exclude volumes mounting secrets",
|
||||
defaultVolumesToFsBackup: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
velerov1api.VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// PVB Volumes
|
||||
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
|
||||
/// Excluded from pod volume backup through annotation
|
||||
{Name: "nonPvbPV1"}, {Name: "nonPvbPV2"}, {Name: "nonPvbPV3"},
|
||||
// Excluded from pod volume backup because hostpath
|
||||
{Name: "superSecret", VolumeSource: corev1api.VolumeSource{Secret: &corev1api.SecretVolumeSource{SecretName: "super-secret"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{"nonPvbPV1", "nonPvbPV2", "nonPvbPV3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should exclude volumes mounting config maps",
|
||||
defaultVolumesToFsBackup: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
velerov1api.VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
// PVB Volumes
|
||||
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
|
||||
/// Excluded from pod volume backup through annotation
|
||||
{Name: "nonPvbPV1"}, {Name: "nonPvbPV2"}, {Name: "nonPvbPV3"},
|
||||
// Excluded from pod volume backup because hostpath
|
||||
{Name: "appCOnfig", VolumeSource: corev1api.VolumeSource{ConfigMap: &corev1api.ConfigMapVolumeSource{LocalObjectReference: corev1api.LocalObjectReference{Name: "app-config"}}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{"nonPvbPV1", "nonPvbPV2", "nonPvbPV3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should exclude projected volumes",
|
||||
defaultVolumesToFsBackup: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
velerov1api.VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
|
||||
{
|
||||
Name: "projected",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
Projected: &corev1api.ProjectedVolumeSource{
|
||||
Sources: []corev1api.VolumeProjection{{
|
||||
Secret: &corev1api.SecretProjection{
|
||||
LocalObjectReference: corev1api.LocalObjectReference{},
|
||||
Items: nil,
|
||||
Optional: nil,
|
||||
},
|
||||
DownwardAPI: nil,
|
||||
ConfigMap: nil,
|
||||
ServiceAccountToken: nil,
|
||||
}},
|
||||
DefaultMode: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should exclude DownwardAPI volumes",
|
||||
defaultVolumesToFsBackup: true,
|
||||
pod: &corev1api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
velerov1api.VolumesToExcludeAnnotation: "nonPvbPV1,nonPvbPV2,nonPvbPV3",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PodSpec{
|
||||
Volumes: []corev1api.Volume{
|
||||
{Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"},
|
||||
{
|
||||
Name: "downwardAPI",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
DownwardAPI: &corev1api.DownwardAPIVolumeSource{
|
||||
Items: []corev1api.DownwardAPIVolumeFile{
|
||||
{
|
||||
Path: "labels",
|
||||
FieldRef: &corev1api.ObjectFieldSelector{
|
||||
APIVersion: "v1",
|
||||
FieldPath: "metadata.labels",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: struct {
|
||||
included []string
|
||||
optedOut []string
|
||||
}{
|
||||
included: []string{"pvbPV1", "pvbPV2", "pvbPV3"},
|
||||
optedOut: []string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualIncluded, actualOptedOut := GetVolumesByPod(tc.pod, tc.defaultVolumesToFsBackup)
|
||||
|
||||
sort.Strings(tc.expected.included)
|
||||
sort.Strings(actualIncluded)
|
||||
assert.Equal(t, tc.expected.included, actualIncluded)
|
||||
|
||||
sort.Strings(tc.expected.optedOut)
|
||||
sort.Strings(actualOptedOut)
|
||||
assert.Equal(t, tc.expected.optedOut, actualOptedOut)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,24 @@ toc: "true"
|
||||
|
||||
When opening a pull request, please fill out the checklist supplied the template. This will help others properly categorize and review your pull request.
|
||||
|
||||
### PR title
|
||||
|
||||
Make sure that the pull request title summarizes the change made (and not just "fixes issue #xxxx"):
|
||||
|
||||
Example PR titles:
|
||||
|
||||
- "Check for nil when validating foo"
|
||||
- "Issue #1234: Check for nil when validating foo"
|
||||
|
||||
### Cherry-pick PRs
|
||||
|
||||
When a PR to main needs to be cherry-picked to a release branch, please wait until the main PR is merged first before creating the CP PR. If the CP PR is made before the main PR is merged, there is a risk that PR modifications in response to review comments will not make it into the CP PR.
|
||||
|
||||
The Cherry-pick PR title should reference the branch it's cherry-picked to and the fact that it's a CP of a commit to main:
|
||||
|
||||
- "[release-1.13 CP] Issue #1234: Check for nil when validating foo"
|
||||
|
||||
|
||||
## Adding a changelog
|
||||
|
||||
Authors are expected to include a changelog file with their pull requests. The changelog file
|
||||
@@ -108,7 +126,7 @@ Signed-off-by: Joe Beda <joe@heptio.com>
|
||||
|
||||
This can easily be done with the `--signoff` option to `git commit`.
|
||||
|
||||
By doing this you state that you can certify the following (from https://developercertificate.org/):
|
||||
By doing this you state that you can certify the following (from [https://developercertificate.org/](https://developercertificate.org/)):
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
|
||||
@@ -41,7 +41,7 @@ velero install --use-node-agent
|
||||
### Configure Node Agent DaemonSet spec
|
||||
|
||||
After installation, some PaaS/CaaS platforms based on Kubernetes also require modifications the node-agent DaemonSet spec.
|
||||
The steps in this section are only needed if you are installing on RancherOS, OpenShift, VMware Tanzu Kubernetes Grid
|
||||
The steps in this section are only needed if you are installing on RancherOS, Nutanix, OpenShift, VMware Tanzu Kubernetes Grid
|
||||
Integrated Edition (formerly VMware Enterprise PKS), or Microsoft Azure.
|
||||
|
||||
|
||||
@@ -63,6 +63,22 @@ hostPath:
|
||||
path: /opt/rke/var/lib/kubelet/pods
|
||||
```
|
||||
|
||||
**Nutanix**
|
||||
|
||||
Update the host path for volumes in the node-agent DaemonSet in the Velero namespace from `/var/lib/kubelet/pods` to
|
||||
`/var/nutanix/var/lib/kubelet`.
|
||||
|
||||
```yaml
|
||||
hostPath:
|
||||
path: /var/lib/kubelet/pods
|
||||
```
|
||||
|
||||
to
|
||||
|
||||
```yaml
|
||||
hostPath:
|
||||
path: /var/nutanix/var/lib/kubelet
|
||||
```
|
||||
|
||||
**OpenShift**
|
||||
|
||||
@@ -75,24 +91,10 @@ To mount the correct hostpath to pods volumes, run the node-agent pod in `privil
|
||||
oc adm policy add-scc-to-user privileged -z velero -n velero
|
||||
```
|
||||
|
||||
2. Modify the DaemonSet yaml to request a privileged mode:
|
||||
|
||||
```diff
|
||||
@@ -67,3 +67,5 @@ spec:
|
||||
value: /credentials/cloud
|
||||
- name: VELERO_SCRATCH_DIR
|
||||
value: /scratch
|
||||
+ securityContext:
|
||||
+ privileged: true
|
||||
2. Install Velero with the '--privileged-node-agent' option to request a privileged mode:
|
||||
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```shell
|
||||
oc patch ds/node-agent \
|
||||
--namespace velero \
|
||||
--type json \
|
||||
-p '[{"op":"add","path":"/spec/template/spec/containers/0/securityContext","value": { "privileged": true}}]'
|
||||
velero install --use-node-agent --privileged-node-agent
|
||||
```
|
||||
|
||||
If node-agent is not running in a privileged mode, it will not be able to access snapshot volumes within the mounted
|
||||
@@ -324,7 +326,7 @@ Velero backs up resources for CSI snapshot data movement backup in the same way
|
||||
- CSI plugin checks if a data movement is required, if so it creates a `DataUpload` CR and then returns to Velero backup.
|
||||
- Velero now is able to back up other resources, including other PVC objects.
|
||||
- Velero backup controller periodically queries the data movement status from CSI plugin, the period is configurable through the Velero server parameter `--item-operation-sync-frequency`, by default it is 10s. On the call, CSI plugin turns to check the phase of the `DataUpload` CRs.
|
||||
- When all the `DataUpload` CRs come to a terminal state (i.e., `Completed`, `Failed` or `Cancelled`), Velero backup perists all the necessary information and finish the backup.
|
||||
- When all the `DataUpload` CRs come to a terminal state (i.e., `Completed`, `Failed` or `Cancelled`), Velero backup persists all the necessary information and finish the backup.
|
||||
|
||||
- CSI plugin expects a data mover to handle the `DataUpload` CR. If no data mover is configured for the backup, Velero built-in data mover will handle it.
|
||||
- If the `DataUpload` CR does not reach to the terminal state with in the given time, the `DataUpload` CR will be cancelled. You can set the timeout value per backup through the `--item-operation-timeout` parameter, the default value is `4 hours`.
|
||||
|
||||
@@ -59,6 +59,11 @@ Velero supports the following kinds of plugins:
|
||||
- **Restore Item Action** - executes arbitrary logic for individual items prior to restoring them into a cluster
|
||||
- **Delete Item Action** - executes arbitrary logic based on individual items within a backup prior to deleting the backup
|
||||
|
||||
Plugin binaries are discovered by recursively reading a directory in no particular order. Hence no guarantee is provided for the
|
||||
order in which item action plugins are invoked. However, if a single binary implements multiple item action plugins,
|
||||
they may be invoked in the order in which they are registered but it is best to not depend on this
|
||||
implementation. This is not guaranteed officially and the implementation can change at any time.
|
||||
|
||||
## Plugin Logging
|
||||
|
||||
Velero provides a [logger][2] that can be used by plugins to log structured information to the main Velero server log or
|
||||
|
||||
@@ -23,6 +23,14 @@ By default, `velero install` does not install Velero's [File System Backup][3].
|
||||
|
||||
If you've already run `velero install` without the `--use-node-agent` flag, you can run the same command again, including the `--use-node-agent` flag, to add the file system backup to your existing install.
|
||||
|
||||
## CSI Snapshot Data Movement
|
||||
|
||||
Velero node-agent is required by CSI snapshot data movement when Velero built-in data mover is used. By default, `velero install` does not install Velero's node-agent. To enable it, specify the `--use-node-agent` flag.
|
||||
|
||||
For some use cases, Velero node-agent requires to run under privileged mode. For example, when backing up block volumes, it is required to allow the node-agent to access the block device. To enable it set velero install flags `--privileged-node-agent`.
|
||||
|
||||
If you've already run `velero install` without the `--use-node-agent` or `--privileged-node-agent` flag, you can run the same command again, including the `--use-node-agent` or `--privileged-node-agent` flag, to add CSI snapshot data movement to your existing install.
|
||||
|
||||
## Default Pod Volume backup to file system backup
|
||||
|
||||
By default, `velero install` does not enable the use of File System Backup (FSB) to take backups of all pod volumes. You must apply an [annotation](file-system-backup.md/#using-opt-in-pod-volume-backup) to every pod which contains volumes for Velero to use FSB for the backup.
|
||||
|
||||
@@ -39,10 +39,6 @@ You can also override the default list of linters by running the command
|
||||
|
||||
To run unit tests, use `make test`.
|
||||
|
||||
## Vendor dependencies
|
||||
|
||||
If you need to add or update the vendored dependencies, see [Vendoring dependencies][11].
|
||||
|
||||
## Using the main branch
|
||||
|
||||
If you are developing or using the main branch, note that you may need to update the Velero CRDs to get new changes as other development work is completed.
|
||||
@@ -53,4 +49,3 @@ velero install --crds-only --dry-run -o yaml | kubectl apply -f -
|
||||
|
||||
**NOTE:** You could change the default CRD API version (v1beta1 _or_ v1) if Velero CLI can't discover the Kubernetes preferred CRD API version. The Kubernetes version < 1.16 preferred CRD API version is v1beta1; the Kubernetes version >= 1.16 preferred CRD API version is v1.
|
||||
|
||||
[11]: vendoring-dependencies.md
|
||||
|
||||
@@ -77,7 +77,7 @@ backup which created the backup repository, then Velero will not be able to conn
|
||||
### Configure Node Agent DaemonSet spec
|
||||
|
||||
After installation, some PaaS/CaaS platforms based on Kubernetes also require modifications the node-agent DaemonSet spec.
|
||||
The steps in this section are only needed if you are installing on RancherOS, OpenShift, VMware Tanzu Kubernetes Grid
|
||||
The steps in this section are only needed if you are installing on RancherOS, Nutanix, OpenShift, VMware Tanzu Kubernetes Grid
|
||||
Integrated Edition (formerly VMware Enterprise PKS), or Microsoft Azure.
|
||||
|
||||
|
||||
@@ -99,6 +99,23 @@ hostPath:
|
||||
path: /opt/rke/var/lib/kubelet/pods
|
||||
```
|
||||
|
||||
**Nutanix**
|
||||
|
||||
|
||||
Update the host path for volumes in the node-agent DaemonSet in the Velero namespace from `/var/lib/kubelet/pods` to
|
||||
`/var/nutanix/var/lib/kubelet`.
|
||||
|
||||
```yaml
|
||||
hostPath:
|
||||
path: /var/lib/kubelet/pods
|
||||
```
|
||||
|
||||
to
|
||||
|
||||
```yaml
|
||||
hostPath:
|
||||
path: /var/nutanix/var/lib/kubelet
|
||||
```
|
||||
|
||||
**OpenShift**
|
||||
|
||||
@@ -111,24 +128,10 @@ To mount the correct hostpath to pods volumes, run the node-agent pod in `privil
|
||||
oc adm policy add-scc-to-user privileged -z velero -n velero
|
||||
```
|
||||
|
||||
2. Modify the DaemonSet yaml to request a privileged mode:
|
||||
|
||||
```diff
|
||||
@@ -67,3 +67,5 @@ spec:
|
||||
value: /credentials/cloud
|
||||
- name: VELERO_SCRATCH_DIR
|
||||
value: /scratch
|
||||
+ securityContext:
|
||||
+ privileged: true
|
||||
2. Install Velero with the '--privileged-node-agent' option to request a privileged mode:
|
||||
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```shell
|
||||
oc patch ds/node-agent \
|
||||
--namespace velero \
|
||||
--type json \
|
||||
-p '[{"op":"add","path":"/spec/template/spec/containers/0/securityContext","value": { "privileged": true}}]'
|
||||
velero install --use-node-agent --privileged-node-agent
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ By default Velero runs with an RBAC policy of ClusterRole `cluster-admin`. This
|
||||
|
||||
For more information about RBAC and access control generally in Kubernetes, see the Kubernetes documentation about [access control][1], [managing service accounts][2], and [RBAC authorization][3].
|
||||
|
||||
## Set up Roles and RoleBindings
|
||||
## Set up with restricted RBAC permissions
|
||||
|
||||
Here's a sample Role and RoleBinding pair.
|
||||
Here's a sample of restricted permission setting.
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
@@ -28,13 +28,12 @@ rules:
|
||||
- "*"
|
||||
resources:
|
||||
- "*"
|
||||
```
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: ROLEBINDING_NAME_HERE
|
||||
namespace: YOUR_NAMESPACE_HERE
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: YOUR_SERVICEACCOUNT_HERE
|
||||
@@ -42,9 +41,55 @@ roleRef:
|
||||
kind: Role
|
||||
name: ROLE_NAME_HERE
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: velero-clusterrole
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- persistentvolumes
|
||||
- namespaces
|
||||
verbs:
|
||||
- '*'
|
||||
- apiGroups:
|
||||
- '*'
|
||||
resources:
|
||||
- '*'
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- 'apiextensions.k8s.io'
|
||||
resources:
|
||||
- 'customresourcedefinitions'
|
||||
verbs:
|
||||
- get
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: velero-clusterrolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: velero-clusterrole
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: YOUR_SERVICEACCOUNT_HERE
|
||||
namespace: YOUR_NAMESPACE_HERE
|
||||
```
|
||||
|
||||
You can add more permissions into the `Role` setting according to the need.
|
||||
`velero-clusterrole` ClusterRole is verified to work in most cases.
|
||||
`Namespaces` resource permission is needed to create namespace during restore. If you don't need that, the `create` permission can be removed, but `list` and `get` permissions of `Namespaces` resource is still needed, because Velero needs to know whether the namespace it's assigned exists in the cluster.
|
||||
`PersistentVolumes` resource permission is needed for back up and restore volumes. If that is not needed, it can be removed too.
|
||||
`CustomResourceDefinitions` resource permission is needed to backup CR instances' CRD. It's better to keep them.
|
||||
It's better to have the `list` permission for all resources, because Velero needs to read some resources during backup, for example, `ClusterRoles` is listed for backing `ServiceAccount` up, and `VolumeSnapshotContent` for CSI `PersistentVolumeClaim`. If you just enable `list` permissions for the resources you want to back up and restore, it's possible that backup or restore end with failure.
|
||||
|
||||
[1]: https://kubernetes.io/docs/reference/access-authn-authz/controlling-access/
|
||||
[2]: https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/
|
||||
[3]: https://kubernetes.io/docs/reference/access-authn-authz/rbac/
|
||||
[4]: namespace.md
|
||||
|
||||
|
||||
@@ -101,6 +101,24 @@ Includes cluster-scoped resources. Cannot work with `--include-cluster-scoped-re
|
||||
|
||||
For more information read the [Kubernetes label selector documentation](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors)
|
||||
|
||||
### --or-selector
|
||||
|
||||
To include the resources that match at least one of the label selectors from the list. Separate the selectors with ` or `. The ` or ` is used as a separator to split label selectors, and it is not an operator.
|
||||
|
||||
This option cannot be used together with `--selector`.
|
||||
|
||||
* Include resources matching any one of the label selector, `foo=bar` or `baz=qux`
|
||||
|
||||
```bash
|
||||
velero backup create backup1 --or-selector "foo=bar or baz=qux"
|
||||
```
|
||||
|
||||
* Include resources that are labeled `environment=production` or `env=prod` or `env=production` or `environment=prod`.
|
||||
|
||||
```bash
|
||||
velero restore create restore-prod --from-backup=prod-backup --or-selector "env in (prod,production) or environment in (prod, production)"
|
||||
```
|
||||
|
||||
### --include-cluster-scoped-resources
|
||||
Kubernetes cluster-scoped resources to include in the backup, formatted as resource.group, such as `storageclasses.storage.k8s.io`(use '*' for all resources). Cannot work with `--include-resources`, `--exclude-resources` and `--include-cluster-resources`. This parameter only works for backup, not for restore.
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ kubectl edit deployment/velero -n velero
|
||||
...
|
||||
```
|
||||
|
||||
**Note:** Velero plugins are started as separate processes and once the Velero operation is done (either successfully or not), they exit. So, if you see **received EOF, stopping recv loop** messages in debug logs, that does not mean an error occurred, just that a plugin finished executing.
|
||||
|
||||
## Known issue with restoring LoadBalancer Service
|
||||
|
||||
Because of how Kubernetes handles Service objects of `type=LoadBalancer`, when you restore these objects you might encounter an issue with changed values for Service UIDs. Kubernetes automatically generates the name of the cloud resource based on the Service UID, which is different when restored, resulting in a different name for the cloud load balancer. If the DNS CNAME for your application points to the DNS name of your cloud load balancer, you'll need to update the CNAME pointer when you perform a Velero restore.
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
title: "Vendoring dependencies"
|
||||
layout: docs
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
We are using [dep][0] to manage dependencies. You can install it by following [these
|
||||
instructions][1].
|
||||
|
||||
## Adding a new dependency
|
||||
|
||||
Run `dep ensure`. If you want to see verbose output, you can append `-v` as in
|
||||
`dep ensure -v`.
|
||||
|
||||
## Updating an existing dependency
|
||||
|
||||
Run `dep ensure -update <pkg> [<pkg> ...]` to update one or more dependencies.
|
||||
|
||||
[0]: https://github.com/golang/dep
|
||||
[1]: https://golang.github.io/dep/docs/installation.html
|
||||
@@ -6,7 +6,7 @@ layout: docs
|
||||
## When is it appropriate to use Ark instead of etcd's built in backup/restore?
|
||||
|
||||
Etcd's backup/restore tooling is good for recovering from data loss in a single etcd cluster. For
|
||||
example, it is a good idea to take a backup of etcd prior to upgrading etcd istelf. For more
|
||||
example, it is a good idea to take a backup of etcd prior to upgrading etcd itself. For more
|
||||
sophisticated management of your Kubernetes cluster backups and restores, we feel that Ark is
|
||||
generally a better approach. It gives you the ability to throw away an unstable cluster and restore
|
||||
your Kubernetes resources and data into a new cluster, which you can't do easily just by backing up
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user