mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-07-30 03:52:42 +00:00
Merge branch 'main' into issue-fix-6663
This commit is contained in:
@@ -0,0 +1 @@
|
||||
Support JSON Merge Patch and Strategic Merge Patch in Resource Modifiers
|
||||
@@ -0,0 +1 @@
|
||||
Add the PV backup information design document.
|
||||
@@ -0,0 +1 @@
|
||||
Fix unified repository (kopia) s3 credentials profile selection
|
||||
@@ -0,0 +1 @@
|
||||
restore: Use warning when Create IsAlreadyExist and Get error
|
||||
@@ -0,0 +1 @@
|
||||
Fix inconsistent behavior of Backup and Restore hook execution
|
||||
@@ -0,0 +1 @@
|
||||
Add HealthCheckNodePort deletion logic for Service restore.
|
||||
@@ -0,0 +1 @@
|
||||
Add description markers for dataupload and datadownload CRDs
|
||||
@@ -0,0 +1 @@
|
||||
Read information from the credential specified by BSL
|
||||
@@ -0,0 +1 @@
|
||||
Fix issue #7027, data mover backup exposer should not assume the first volume as the backup volume in backup pod
|
||||
@@ -0,0 +1 @@
|
||||
Remove the Velero generated client.
|
||||
@@ -0,0 +1 @@
|
||||
Remove dependency of generated client part 3.
|
||||
@@ -186,6 +186,12 @@ spec:
|
||||
- Continue
|
||||
- Fail
|
||||
type: string
|
||||
waitForReady:
|
||||
description: WaitForReady ensures command will
|
||||
be launched when container is Ready instead
|
||||
of Running.
|
||||
nullable: true
|
||||
type: boolean
|
||||
waitTimeout:
|
||||
description: WaitTimeout defines the maximum amount
|
||||
of time Velero should wait for the container
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -48,6 +48,8 @@ spec:
|
||||
name: v2alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: DataDownload acts as the protocol between data mover plugins
|
||||
and data mover controller for the datamover restore operation
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
|
||||
@@ -49,6 +49,8 @@ spec:
|
||||
name: v2alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: DataUpload acts as the protocol between data mover plugins and
|
||||
data mover controller for the datamover backup operation
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -88,10 +88,10 @@ Example of StrategicPatches in ResourceModifierRule
|
||||
version: v1
|
||||
resourceModifierRules:
|
||||
- conditions:
|
||||
groupResource: pods
|
||||
resourceNameRegex: "^my-pod$"
|
||||
namespaces:
|
||||
- ns1
|
||||
groupResource: pods
|
||||
resourceNameRegex: "^my-pod$"
|
||||
namespaces:
|
||||
- ns1
|
||||
strategicPatches:
|
||||
- patchData: |
|
||||
{
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
# PersistentVolume backup information design
|
||||
|
||||
## Abstract
|
||||
Create a new metadata file in the backup repository's backup name sub-directory to store the backup-including PVC and PV information. The information includes the way of backing up the PVC and PV data, snapshot information, and status. The needed snapshot status can also be recorded there, but the Velero-Native snapshot plugin doesn't provide a way to get the snapshot size from the API, so it's possible that not all snapshot size information is available.
|
||||
|
||||
This new additional metadata file is needed when:
|
||||
* Get a summary of the backup's PVC and PV information, including how the data in them is backed up, or whether the data in them is skipped from backup.
|
||||
* Find out how the PVC and PV should be restored in the restore process.
|
||||
* Retrieve the PV's snapshot information for backup.
|
||||
|
||||
## Background
|
||||
There is already a [PR](https://github.com/vmware-tanzu/velero/pull/6496) to track the skipped PVC in the backup. This design will depend on it and go further to get a summary of PVC and PV information, then persist into a metadata file in the backup repository.
|
||||
|
||||
In the restore process, the Velero server needs to decide how the PV resource should be restored according to how the PV is backed up. The current logic is to check whether it's backed up by Velero-native snapshot, by file-system backup, or having `DeletionPolicy` set as `Delete`.
|
||||
|
||||
The checks are made by the backup-generated PVBs or Snapshots. There is no generic way to find this information, and the CSI backup and Snapshot data movement backup are not covered.
|
||||
|
||||
Another thing that needs noticing is when describing the backup, there is no generic way to find the PV's snapshot information.
|
||||
|
||||
## Goals
|
||||
- Create a new metadata file to store backup's PVCs and PVs information and volume data backing up method. The file can be used to let downstream consumers generate a summary.
|
||||
- Create a generic way to let the Velero server know how the PV resources are backed up.
|
||||
- Create a generic way to let the Velero server find the PV corresponding snapshot information.
|
||||
|
||||
## Non Goals
|
||||
- Unify how to get snapshot size information for all PV backing-up methods, and all other currently not ready PVs' information.
|
||||
|
||||
## High-Level Design
|
||||
Create _backup-name_-volumes-info.json metadata file in the backup's repository. This file will be encoded to contain all the PVC and PV information included in the backup. The information covers whether the PV or PVC's data is skipped during backup, how its data is backed up, and the backed-up detail information.
|
||||
|
||||
Please notice that the new metadata file includes all skipped volume information. This is used to address [the second phase needs of skipped volumes information](https://github.com/vmware-tanzu/velero/issues/5834#issuecomment-1526624211).
|
||||
|
||||
The `restoreItem` function can decode the _backup-name_-volumes-info.json file to determine how to handle the PV resource.
|
||||
|
||||
## Detailed Design
|
||||
|
||||
### The VolumeInfo structure
|
||||
_backup-name_-volumes-info.json file is a structure that contains an array of structure `VolumeInfo` and a VolumeInfo version parameter. The version is used to support multiple VolumeInfo version, and make the backward-compatible support possible. The current version is `1`.
|
||||
|
||||
The 1 version of `VolumeInfo` definition is:
|
||||
``` golang
|
||||
type VolumeInfoV1 struct {
|
||||
PVCName string // The PVC's name. The format should be <namespace-name>/<PVC-name>
|
||||
PVName string // The PV name.
|
||||
BackupMethod string // The way the volume data is backed up. The valid value includes `VeleroNativeSnapshot`, `PodVolumeBackup`, `CSISnapshot` and `Skipped`.
|
||||
SnapshotDataMovement bool // Whether the volume's snapshot data is moved to specified storage.
|
||||
|
||||
SkippedReason string // The reason for the volume is skipped in the backup.
|
||||
StartTimestamp *metav1.Time // Snapshot starts timestamp.
|
||||
|
||||
CSISnapshotInfo CSISnapshotInfo
|
||||
SnapshotDataMoveInfo SnapshotDataMoveInfo
|
||||
NativeSnapshotInfo VeleroNativeSnapshotInfo
|
||||
PVBInfo PodVolumeBackupInfo
|
||||
}
|
||||
|
||||
// CSISnapshotInfo is used for displaying the CSI snapshot status
|
||||
type CSISnapshotInfo struct {
|
||||
SnapshotHandle string // The actual snapshot ID. It can be the cloud provider's snapshot ID or the file-system uploader's snapshot.
|
||||
Size int64 // The snapshot corresponding volume size. Some of the volume backup methods cannot retrieve the data by current design, for example, the Velero native snapshot.
|
||||
|
||||
Driver string // The name of the CSI driver.
|
||||
VSCName string // The name of the VolumeSnapshotContent.
|
||||
}
|
||||
|
||||
// SnapshotDataMoveInfo is used for displaying the snapshot data mover status.
|
||||
type SnapshotDataMoveInfo struct {
|
||||
DataMover string // The data mover used by the backup. The valid values are `velero` and ``(equals to `velero`).
|
||||
UploaderType string // The type of the uploader that uploads the snapshot data. The valid values are `kopia` and `restic`. It's useful for file-system backup and snapshot data mover.
|
||||
RetainedSnapshot string // The name or ID of the snapshot associated object(SAO).
|
||||
}
|
||||
|
||||
// VeleroNativeSnapshotInfo is used for displaying the Velero native snapshot status.
|
||||
type VeleroNativeSnapshotInfo struct {
|
||||
SnapshotHandle string // The actual snapshot ID. It can be the cloud provider's snapshot ID or the file-system uploader's snapshot.
|
||||
Size int64 // The snapshot corresponding volume size. Some of the volume backup methods cannot retrieve the data by current design, for example, the Velero native snapshot.
|
||||
|
||||
VolumeType string // The cloud provider snapshot volume type.
|
||||
VolumeAZ string // The cloud provider snapshot volume's availability zones.
|
||||
IOPS string // The cloud provider snapshot volume's IOPS.
|
||||
}
|
||||
|
||||
// PodVolumeBackupInfo is used for displaying the PodVolumeBackup snapshot status.
|
||||
type PodVolumeBackupInfo struct {
|
||||
SnapshotHandle string // The actual snapshot ID. It can be the cloud provider's snapshot ID or the file-system uploader's snapshot.
|
||||
Size int64 // The snapshot corresponding volume size. Some of the volume backup methods cannot retrieve the data by current design, for example, the Velero native snapshot.
|
||||
|
||||
UploaderType string // The type of the uploader that uploads the data. The valid values are `kopia` and `restic`. It's useful for file-system backup and snapshot data mover.
|
||||
VolumeName string // The PVC's corresponding volume name used by Pod
|
||||
PodName string // The Pod name mounting this PVC. The format should be <namespace-name>/<pod-name>.
|
||||
}
|
||||
```
|
||||
|
||||
To make Velero support multiple versions of VolumeInfo, Velero needs to create a structure to include the version and the versioned volume information together. The information is persisted in the metadata file during backup creation. When reading the VolumeInfo metadata file, Velero reads the version information first by un-marshalling the metadata file by structure `VolumeInfoVersion`, then decide to use which version of the VolumeInfos according to the version value.
|
||||
|
||||
If there is need to introduce a non compatible change to the VolumeInfo, a new version of `VolumeInfos` and `VolumeInfo` are needed. For example, version 2 is created, then `VolumeInfosV2` and `VolumeInfoV2` structures are needed.
|
||||
|
||||
Only when there a non-backward-compatible change introduced in the `VolumeInfo` structure, the `version`'s version will be incremented.
|
||||
|
||||
``` golang
|
||||
type VolumeInfoVersion struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
type VolumeInfosV2 struct {
|
||||
Infos []VolumeInfoV2
|
||||
Version string // VolumeInfo structure's version information.
|
||||
}
|
||||
|
||||
type VolumeInfoV2 struct {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### How the VolumeInfo array is generated.
|
||||
The function `persistBackup` has `backup *pkgbackup.Request` in parameters.
|
||||
From it, the `VolumeSnapshots`, `PodVolumeBackups`, `CSISnapshots`, `itemOperationsList`, and `SkippedPVTracker` can be read. All of them will be iterated and merged into the `VolumeInfo` array, and then persisted into backup repository in function `persistBackup`.
|
||||
|
||||
Please notice that the change happened in async operations are not reflected in the new metadata file. The file only covers the volume changes happen in the Velero server process scope.
|
||||
|
||||
A new methods are added to BackupStore to download the VolumeInfo metadata file.
|
||||
Uploading the metadata file is covered in the exiting `PutBackup` method.
|
||||
|
||||
``` golang
|
||||
type BackupStore interface {
|
||||
...
|
||||
GetVolumeInfos(name string) ([]*VolumeInfo, error)
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### How the VolumeInfo array is used.
|
||||
|
||||
#### Generate the PVC backed-up information summary
|
||||
The downstream tools can use this VolumeInfo array to format and display their volume information. This is in the scope of this feature.
|
||||
|
||||
#### Retrieve volume backed-up information for `velero backup describe` command
|
||||
The `velero backup describe` can also use this VolumeInfo array structure to display the volume information. The snapshot data mover volume should use this structure at first, then the Velero native snapshot, CSI snapshot, and PodVolumeBackup can also use this structure. The detailed implementation is also not in this feature's scope.
|
||||
|
||||
#### Let restore know how to restore the PV
|
||||
In the function `restoreItem`, it will determine whether to restore the PV resource by checking it in the Velero native snapshots list, PodVolumeBackup list, and its DeletionPolicy. This logic is still kept. The logic will be used when the new `VolumeInfo` metadata cannot be found to support backward compatibility.
|
||||
|
||||
``` golang
|
||||
if groupResource == kuberesource.PersistentVolumes {
|
||||
switch {
|
||||
case hasSnapshot(name, ctx.volumeSnapshots):
|
||||
...
|
||||
case hasPodVolumeBackup(obj, ctx):
|
||||
...
|
||||
case hasDeleteReclaimPolicy(obj.Object):
|
||||
...
|
||||
default:
|
||||
...
|
||||
```
|
||||
|
||||
After introducing the VolumeInfo array, the following logic will be added.
|
||||
``` golang
|
||||
if groupResource == kuberesource.PersistentVolumes {
|
||||
volumeInfo := GetVolumeInfo(pvName)
|
||||
switch volumeInfo.BackupMethod {
|
||||
case VeleroNativeSnapshot:
|
||||
...
|
||||
case PodVolumeBackup:
|
||||
...
|
||||
case CSISnapshot:
|
||||
...
|
||||
case Skipped:
|
||||
// Check whether the Velero server should restore the PV depending on the DeletionPolicy setting.
|
||||
default:
|
||||
// Need to check whether the volume is backed up by the SnapshotDataMover.
|
||||
if volumeInfo.SnapshotDataMovement:
|
||||
```
|
||||
|
||||
### How the VolumeInfo metadata file is deleted
|
||||
_backup-name_-volumes-info.json file is deleted during backup deletion.
|
||||
|
||||
## Alternatives Considered
|
||||
The restore process needs more information about how the PVs are backed up to determine whether this PV should be restored. The released branches also need a similar function, but backporting a new feature into previous releases may not be a good idea, so according to [Anshul Ahuja's suggestion](https://github.com/vmware-tanzu/velero/issues/6595#issuecomment-1731081580), adding more cases here to support checking PV backed-up by CSI plugin and CSI snapshot data mover: https://github.com/vmware-tanzu/velero/blob/5ff5073cc3f364bafcfbd26755e2a92af68ba180/pkg/restore/restore.go#L1206-L1324.
|
||||
|
||||
## Security Considerations
|
||||
There should be no security impact introduced by this design.
|
||||
|
||||
## Compatibility
|
||||
After this design is implemented, there should be no impact on the existing [skipped PVC summary feature](https://github.com/vmware-tanzu/velero/pull/6496).
|
||||
|
||||
To support older version backup, which doesn't have the VolumeInfo metadata file, the old logic, which is checking the Velero native snapshots list, PodVolumeBackup list, and PVC DeletionPolicy, is still kept, and supporting CSI snapshots and snapshot data mover logic will be added too.
|
||||
|
||||
## Implementation
|
||||
This will be implemented in the Velero v1.13 development cycle.
|
||||
|
||||
## Open Issues
|
||||
There are no open issues identified by now.
|
||||
@@ -64,6 +64,7 @@ require (
|
||||
k8s.io/metrics v0.25.6
|
||||
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed
|
||||
sigs.k8s.io/controller-runtime v0.12.2
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd
|
||||
sigs.k8s.io/yaml v1.3.0
|
||||
)
|
||||
|
||||
@@ -184,7 +185,6 @@ require (
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/component-base v0.24.2 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -1428,8 +1428,8 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lR
|
||||
sigs.k8s.io/controller-runtime v0.12.2 h1:nqV02cvhbAj7tbt21bpPpTByrXGn2INHRsi39lXy9sE=
|
||||
sigs.k8s.io/controller-runtime v0.12.2/go.mod h1:qKsk4WE6zW2Hfj0G4v10EnNB2jMG1C+NTb8h+DwCoU0=
|
||||
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY=
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k=
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/kustomize/api v0.8.11/go.mod h1:a77Ls36JdfCWojpUqR6m60pdGY1AYFix4AH83nJtY1g=
|
||||
sigs.k8s.io/kustomize/api v0.11.4/go.mod h1:k+8RsqYbgpkIrJ4p9jcdPqe8DprLxFUUO0yNOq8C+xI=
|
||||
sigs.k8s.io/kustomize/kyaml v0.11.0/go.mod h1:GNMwjim4Ypgp/MueD3zXHLRJEjz7RvtPae0AwlvEMFM=
|
||||
|
||||
@@ -19,7 +19,7 @@ HACK_DIR=$(dirname "${BASH_SOURCE}")
|
||||
${HACK_DIR}/update-3generated-crd-code.sh
|
||||
|
||||
# ensure no changes to generated CRDs
|
||||
if [! git diff --exit-code config/crd/v1/crds/crds.go config/crd/v2alpha1/crds/crds.go >/dev/null]; then
|
||||
if ! git diff --exit-code config/crd/v1/crds/crds.go config/crd/v2alpha1/crds/crds.go &> /dev/null; then
|
||||
# revert changes to state before running CRD generation to stay consistent
|
||||
# with code-generator `--verify-only` option which discards generated changes
|
||||
git checkout config/crd
|
||||
|
||||
@@ -50,6 +50,11 @@ type DefaultListWatchFactory struct {
|
||||
PodsGetter cache.Getter
|
||||
}
|
||||
|
||||
type HookErrInfo struct {
|
||||
Namespace string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (d *DefaultListWatchFactory) NewListWatch(namespace string, selector fields.Selector) cache.ListerWatcher {
|
||||
return cache.NewListWatchFromClient(d.PodsGetter, "pods", namespace, selector)
|
||||
}
|
||||
@@ -158,8 +163,8 @@ func (e *DefaultWaitExecHookHandler) HandleHooks(
|
||||
if hook.Hook.WaitTimeout.Duration != 0 && time.Since(waitStart) > hook.Hook.WaitTimeout.Duration {
|
||||
err := fmt.Errorf("hook %s in container %s expired before executing", hook.HookName, hook.Hook.Container)
|
||||
hookLog.Error(err)
|
||||
errors = append(errors, err)
|
||||
if hook.Hook.OnError == velerov1api.HookErrorModeFail {
|
||||
errors = append(errors, err)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
@@ -172,8 +177,9 @@ func (e *DefaultWaitExecHookHandler) HandleHooks(
|
||||
}
|
||||
if err := e.PodCommandExecutor.ExecutePodCommand(hookLog, podMap, pod.Namespace, pod.Name, hook.HookName, eh); err != nil {
|
||||
hookLog.WithError(err).Error("Error executing hook")
|
||||
err = fmt.Errorf("hook %s in container %s failed to execute, err: %v", hook.HookName, hook.Hook.Container, err)
|
||||
errors = append(errors, err)
|
||||
if hook.Hook.OnError == velerov1api.HookErrorModeFail {
|
||||
errors = append(errors, err)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
@@ -204,10 +210,9 @@ func (e *DefaultWaitExecHookHandler) HandleHooks(
|
||||
podWatcher.Run(ctx.Done())
|
||||
|
||||
// There are some cases where this function could return with unexecuted hooks: the pod may
|
||||
// be deleted, a hook with OnError mode Fail could fail, or it may timeout waiting for
|
||||
// be deleted, a hook could fail, or it may timeout waiting for
|
||||
// containers to become ready.
|
||||
// Each unexecuted hook is logged as an error but only hooks with OnError mode Fail return
|
||||
// an error from this function.
|
||||
// Each unexecuted hook is logged as an error and this error will be returned from this function.
|
||||
for _, hooks := range byContainer {
|
||||
for _, hook := range hooks {
|
||||
if hook.executed {
|
||||
@@ -222,9 +227,7 @@ func (e *DefaultWaitExecHookHandler) HandleHooks(
|
||||
},
|
||||
)
|
||||
hookLog.Error(err)
|
||||
if hook.Hook.OnError == velerov1api.HookErrorModeFail {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -209,10 +209,10 @@ func TestWaitExecHandleHooks(t *testing.T) {
|
||||
Result(),
|
||||
},
|
||||
},
|
||||
expectedErrors: []error{errors.New("pod hook error")},
|
||||
expectedErrors: []error{errors.New("hook <from-annotation> in container container1 failed to execute, err: pod hook error")},
|
||||
},
|
||||
{
|
||||
name: "should return no error when hook from annotation fails with on error mode continue",
|
||||
name: "should return error when hook from annotation fails with on error mode continue",
|
||||
initialPod: builder.ForPod("default", "my-pod").
|
||||
ObjectMeta(builder.WithAnnotations(
|
||||
podRestoreHookCommandAnnotationKey, "/usr/bin/foo",
|
||||
@@ -278,7 +278,7 @@ func TestWaitExecHandleHooks(t *testing.T) {
|
||||
Result(),
|
||||
},
|
||||
},
|
||||
expectedErrors: nil,
|
||||
expectedErrors: []error{errors.New("hook <from-annotation> in container container1 failed to execute, err: pod hook error")},
|
||||
},
|
||||
{
|
||||
name: "should return no error when hook from annotation executes after 10ms wait for container to start",
|
||||
@@ -422,7 +422,7 @@ func TestWaitExecHandleHooks(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should return no error when spec hook with wait timeout expires with OnError mode Continue",
|
||||
name: "should return error when spec hook with wait timeout expires with OnError mode Continue",
|
||||
groupResource: "pods",
|
||||
initialPod: builder.ForPod("default", "my-pod").
|
||||
Containers(&v1.Container{
|
||||
@@ -435,7 +435,7 @@ func TestWaitExecHandleHooks(t *testing.T) {
|
||||
},
|
||||
}).
|
||||
Result(),
|
||||
expectedErrors: nil,
|
||||
expectedErrors: []error{errors.New("hook my-hook-1 in container container1 in pod default/my-pod not executed: context deadline exceeded")},
|
||||
byContainer: map[string][]PodExecRestoreHook{
|
||||
"container1": {
|
||||
{
|
||||
@@ -515,8 +515,8 @@ func TestWaitExecHandleHooks(t *testing.T) {
|
||||
sharedHooksContextTimeout: time.Millisecond,
|
||||
},
|
||||
{
|
||||
name: "should return no error when shared hooks context is canceled before spec hook with OnError mode Continue executes",
|
||||
expectedErrors: nil,
|
||||
name: "should return error when shared hooks context is canceled before spec hook with OnError mode Continue executes",
|
||||
expectedErrors: []error{errors.New("hook my-hook-1 in container container1 in pod default/my-pod not executed: context deadline exceeded")},
|
||||
groupResource: "pods",
|
||||
initialPod: builder.ForPod("default", "my-pod").
|
||||
Containers(&v1.Container{
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package resourcemodifiers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type JSONMergePatch struct {
|
||||
PatchData string `json:"patchData,omitempty"`
|
||||
}
|
||||
|
||||
type JSONMergePatcher struct {
|
||||
patches []JSONMergePatch
|
||||
}
|
||||
|
||||
func (p *JSONMergePatcher) Patch(u *unstructured.Unstructured, _ logrus.FieldLogger) (*unstructured.Unstructured, error) {
|
||||
objBytes, err := u.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in marshaling object %s", err)
|
||||
}
|
||||
|
||||
for _, patch := range p.patches {
|
||||
patchBytes, err := yaml.YAMLToJSON([]byte(patch.PatchData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in converting YAML to JSON %s", err)
|
||||
}
|
||||
|
||||
objBytes, err = jsonpatch.MergePatch(objBytes, patchBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in applying JSON Patch: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
updated := &unstructured.Unstructured{}
|
||||
err = updated.UnmarshalJSON(objBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in unmarshalling modified object %s", err.Error())
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package resourcemodifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
)
|
||||
|
||||
func TestJsonMergePatchFailure(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data string
|
||||
}{
|
||||
{
|
||||
name: "patch with bad yaml",
|
||||
data: "a: b:",
|
||||
},
|
||||
{
|
||||
name: "patch with bad json",
|
||||
data: `{"a"::1}`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
err := clientgoscheme.AddToScheme(scheme)
|
||||
assert.NoError(t, err)
|
||||
pt := &JSONMergePatcher{
|
||||
patches: []JSONMergePatch{{PatchData: tt.data}},
|
||||
}
|
||||
|
||||
u := &unstructured.Unstructured{}
|
||||
_, err = pt.Patch(u, logrus.New())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package resourcemodifiers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
type JSONPatch struct {
|
||||
Operation string `json:"operation"`
|
||||
From string `json:"from,omitempty"`
|
||||
Path string `json:"path"`
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (p *JSONPatch) ToString() string {
|
||||
if addQuotes(p.Value) {
|
||||
return fmt.Sprintf(`{"op": "%s", "from": "%s", "path": "%s", "value": "%s"}`, p.Operation, p.From, p.Path, p.Value)
|
||||
}
|
||||
return fmt.Sprintf(`{"op": "%s", "from": "%s", "path": "%s", "value": %s}`, p.Operation, p.From, p.Path, p.Value)
|
||||
}
|
||||
|
||||
func addQuotes(value string) bool {
|
||||
if value == "" {
|
||||
return true
|
||||
}
|
||||
// if value is null, then don't add quotes
|
||||
if value == "null" {
|
||||
return false
|
||||
}
|
||||
// if value is a boolean, then don't add quotes
|
||||
if _, err := strconv.ParseBool(value); err == nil {
|
||||
return false
|
||||
}
|
||||
// if value is a json object or array, then don't add quotes.
|
||||
if strings.HasPrefix(value, "{") || strings.HasPrefix(value, "[") {
|
||||
return false
|
||||
}
|
||||
// if value is a number, then don't add quotes
|
||||
if _, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type JSONPatcher struct {
|
||||
patches []JSONPatch `yaml:"patches"`
|
||||
}
|
||||
|
||||
func (p *JSONPatcher) Patch(u *unstructured.Unstructured, logger logrus.FieldLogger) (*unstructured.Unstructured, error) {
|
||||
modifiedObjBytes, err := p.applyPatch(u)
|
||||
if err != nil {
|
||||
if errors.Is(err, jsonpatch.ErrTestFailed) {
|
||||
logger.Infof("Test operation failed for JSON Patch %s", err.Error())
|
||||
return u.DeepCopy(), nil
|
||||
}
|
||||
return nil, fmt.Errorf("error in applying JSON Patch %s", err.Error())
|
||||
}
|
||||
|
||||
updated := &unstructured.Unstructured{}
|
||||
err = updated.UnmarshalJSON(modifiedObjBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in unmarshalling modified object %s", err.Error())
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (p *JSONPatcher) applyPatch(u *unstructured.Unstructured) ([]byte, error) {
|
||||
patchBytes := p.patchArrayToByteArray()
|
||||
jsonPatch, err := jsonpatch.DecodePatch(patchBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in decoding json patch %s", err.Error())
|
||||
}
|
||||
|
||||
objBytes, err := u.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in marshaling object %s", err.Error())
|
||||
}
|
||||
|
||||
return jsonPatch.Apply(objBytes)
|
||||
}
|
||||
|
||||
func (p *JSONPatcher) patchArrayToByteArray() []byte {
|
||||
var patches []string
|
||||
for _, patch := range p.patches {
|
||||
patches = append(patches, patch.ToString())
|
||||
}
|
||||
patchesStr := strings.Join(patches, ",\n\t")
|
||||
return []byte(fmt.Sprintf(`[%s]`, patchesStr))
|
||||
}
|
||||
@@ -18,16 +18,16 @@ package resourcemodifiers
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/util/collections"
|
||||
@@ -38,11 +38,9 @@ const (
|
||||
ResourceModifierSupportedVersionV1 = "v1"
|
||||
)
|
||||
|
||||
type JSONPatch struct {
|
||||
Operation string `json:"operation"`
|
||||
From string `json:"from,omitempty"`
|
||||
Path string `json:"path"`
|
||||
Value string `json:"value,omitempty"`
|
||||
type MatchRule struct {
|
||||
Path string `json:"path,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type Conditions struct {
|
||||
@@ -50,11 +48,14 @@ type Conditions struct {
|
||||
GroupResource string `json:"groupResource"`
|
||||
ResourceNameRegex string `json:"resourceNameRegex,omitempty"`
|
||||
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
|
||||
Matches []MatchRule `json:"matches,omitempty"`
|
||||
}
|
||||
|
||||
type ResourceModifierRule struct {
|
||||
Conditions Conditions `json:"conditions"`
|
||||
Patches []JSONPatch `json:"patches"`
|
||||
Conditions Conditions `json:"conditions"`
|
||||
Patches []JSONPatch `json:"patches,omitempty"`
|
||||
MergePatches []JSONMergePatch `json:"mergePatches,omitempty"`
|
||||
StrategicPatches []StrategicMergePatch `json:"strategicPatches,omitempty"`
|
||||
}
|
||||
|
||||
type ResourceModifiers struct {
|
||||
@@ -83,10 +84,10 @@ func GetResourceModifiersFromConfig(cm *v1.ConfigMap) (*ResourceModifiers, error
|
||||
return resModifiers, nil
|
||||
}
|
||||
|
||||
func (p *ResourceModifiers) ApplyResourceModifierRules(obj *unstructured.Unstructured, groupResource string, log logrus.FieldLogger) []error {
|
||||
func (p *ResourceModifiers) ApplyResourceModifierRules(obj *unstructured.Unstructured, groupResource string, scheme *runtime.Scheme, log logrus.FieldLogger) []error {
|
||||
var errs []error
|
||||
for _, rule := range p.ResourceModifierRules {
|
||||
err := rule.Apply(obj, groupResource, log)
|
||||
err := rule.apply(obj, groupResource, scheme, log)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
@@ -95,13 +96,22 @@ func (p *ResourceModifiers) ApplyResourceModifierRules(obj *unstructured.Unstruc
|
||||
return errs
|
||||
}
|
||||
|
||||
func (r *ResourceModifierRule) Apply(obj *unstructured.Unstructured, groupResource string, log logrus.FieldLogger) error {
|
||||
namespaceInclusion := collections.NewIncludesExcludes().Includes(r.Conditions.Namespaces...)
|
||||
if !namespaceInclusion.ShouldInclude(obj.GetNamespace()) {
|
||||
return nil
|
||||
func (r *ResourceModifierRule) apply(obj *unstructured.Unstructured, groupResource string, scheme *runtime.Scheme, log logrus.FieldLogger) error {
|
||||
ns := obj.GetNamespace()
|
||||
if ns != "" {
|
||||
namespaceInclusion := collections.NewIncludesExcludes().Includes(r.Conditions.Namespaces...)
|
||||
if !namespaceInclusion.ShouldInclude(ns) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if r.Conditions.GroupResource != groupResource {
|
||||
g, err := glob.Compile(r.Conditions.GroupResource, '.')
|
||||
if err != nil {
|
||||
log.Errorf("Bad glob pattern of groupResource in condition, groupResource: %s, err: %s", r.Conditions.GroupResource, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !g.Match(groupResource) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -125,87 +135,82 @@ func (r *ResourceModifierRule) Apply(obj *unstructured.Unstructured, groupResour
|
||||
}
|
||||
}
|
||||
|
||||
patches, err := r.PatchArrayToByteArray()
|
||||
match, err := matchConditions(obj, r.Conditions.Matches, log)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !match {
|
||||
log.Info("Conditions do not match, skip it")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("Applying resource modifier patch on %s/%s", obj.GetNamespace(), obj.GetName())
|
||||
err = ApplyPatch(patches, obj, log)
|
||||
err = r.applyPatch(obj, scheme, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PatchArrayToByteArray converts all JsonPatch to string array with the format of jsonpatch.Patch and then convert it to byte array
|
||||
func (r *ResourceModifierRule) PatchArrayToByteArray() ([]byte, error) {
|
||||
var patches []string
|
||||
for _, patch := range r.Patches {
|
||||
patches = append(patches, patch.ToString())
|
||||
func matchConditions(u *unstructured.Unstructured, rules []MatchRule, _ logrus.FieldLogger) (bool, error) {
|
||||
if len(rules) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
patchesStr := strings.Join(patches, ",\n\t")
|
||||
return []byte(fmt.Sprintf(`[%s]`, patchesStr)), nil
|
||||
}
|
||||
|
||||
func (p *JSONPatch) ToString() string {
|
||||
if addQuotes(p.Value) {
|
||||
return fmt.Sprintf(`{"op": "%s", "from": "%s", "path": "%s", "value": "%s"}`, p.Operation, p.From, p.Path, p.Value)
|
||||
}
|
||||
return fmt.Sprintf(`{"op": "%s", "from": "%s", "path": "%s", "value": %s}`, p.Operation, p.From, p.Path, p.Value)
|
||||
}
|
||||
var fixed []JSONPatch
|
||||
for _, rule := range rules {
|
||||
if rule.Path == "" {
|
||||
return false, fmt.Errorf("path is required for match rule")
|
||||
}
|
||||
|
||||
func ApplyPatch(patch []byte, obj *unstructured.Unstructured, log logrus.FieldLogger) error {
|
||||
jsonPatch, err := jsonpatch.DecodePatch(patch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in decoding json patch %s", err.Error())
|
||||
fixed = append(fixed, JSONPatch{
|
||||
Operation: "test",
|
||||
Path: rule.Path,
|
||||
Value: rule.Value,
|
||||
})
|
||||
}
|
||||
objBytes, err := obj.MarshalJSON()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in marshaling object %s", err.Error())
|
||||
}
|
||||
modifiedObjBytes, err := jsonPatch.Apply(objBytes)
|
||||
|
||||
p := &JSONPatcher{patches: fixed}
|
||||
_, err := p.applyPatch(u)
|
||||
if err != nil {
|
||||
if errors.Is(err, jsonpatch.ErrTestFailed) {
|
||||
log.Infof("Test operation failed for JSON Patch %s", err.Error())
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
return fmt.Errorf("error in applying JSON Patch %s", err.Error())
|
||||
return false, err
|
||||
}
|
||||
err = obj.UnmarshalJSON(modifiedObjBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in unmarshalling modified object %s", err.Error())
|
||||
}
|
||||
return nil
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func unmarshalResourceModifiers(yamlData []byte) (*ResourceModifiers, error) {
|
||||
resModifiers := &ResourceModifiers{}
|
||||
err := yaml.UnmarshalStrict(yamlData, resModifiers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode yaml data into resource modifiers %v", err)
|
||||
return nil, fmt.Errorf("failed to decode yaml data into resource modifiers, err: %s", err)
|
||||
}
|
||||
return resModifiers, nil
|
||||
}
|
||||
|
||||
func addQuotes(value string) bool {
|
||||
if value == "" {
|
||||
return true
|
||||
type patcher interface {
|
||||
Patch(u *unstructured.Unstructured, logger logrus.FieldLogger) (*unstructured.Unstructured, error)
|
||||
}
|
||||
|
||||
func (r *ResourceModifierRule) applyPatch(u *unstructured.Unstructured, scheme *runtime.Scheme, logger logrus.FieldLogger) error {
|
||||
var p patcher
|
||||
if len(r.Patches) > 0 {
|
||||
p = &JSONPatcher{patches: r.Patches}
|
||||
} else if len(r.MergePatches) > 0 {
|
||||
p = &JSONMergePatcher{patches: r.MergePatches}
|
||||
} else if len(r.StrategicPatches) > 0 {
|
||||
p = &StrategicMergePatcher{patches: r.StrategicPatches, scheme: scheme}
|
||||
} else {
|
||||
return fmt.Errorf("no patch data found")
|
||||
}
|
||||
// if value is null, then don't add quotes
|
||||
if value == "null" {
|
||||
return false
|
||||
|
||||
updated, err := p.Patch(u, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in applying patch %s", err)
|
||||
}
|
||||
// if value is a boolean, then don't add quotes
|
||||
if _, err := strconv.ParseBool(value); err == nil {
|
||||
return false
|
||||
}
|
||||
// if value is a json object or array, then don't add quotes.
|
||||
if strings.HasPrefix(value, "{") || strings.HasPrefix(value, "[") {
|
||||
return false
|
||||
}
|
||||
// if value is a number, then don't add quotes
|
||||
if _, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
u.SetUnstructuredContent(updated.Object)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
)
|
||||
|
||||
func TestGetResourceModifiersFromConfig(t *testing.T) {
|
||||
@@ -131,6 +135,128 @@ func TestGetResourceModifiersFromConfig(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cm5 := &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap",
|
||||
Namespace: "test-namespace",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: pods\n namespaces:\n - ns1\n matches:\n - path: /metadata/annotations/foo\n value: bar\n mergePatches:\n - patchData: |\n metadata:\n annotations:\n foo: null",
|
||||
},
|
||||
}
|
||||
|
||||
rules5 := &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "pods",
|
||||
Namespaces: []string{
|
||||
"ns1",
|
||||
},
|
||||
Matches: []MatchRule{
|
||||
{
|
||||
Path: "/metadata/annotations/foo",
|
||||
Value: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
MergePatches: []JSONMergePatch{
|
||||
{
|
||||
PatchData: "metadata:\n annotations:\n foo: null",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cm6 := &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap",
|
||||
Namespace: "test-namespace",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: pods\n namespaces:\n - ns1\n strategicPatches:\n - patchData: |\n spec:\n containers:\n - name: nginx\n image: repo2/nginx",
|
||||
},
|
||||
}
|
||||
|
||||
rules6 := &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "pods",
|
||||
Namespaces: []string{
|
||||
"ns1",
|
||||
},
|
||||
},
|
||||
StrategicPatches: []StrategicMergePatch{
|
||||
{
|
||||
PatchData: "spec:\n containers:\n - name: nginx\n image: repo2/nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cm7 := &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap",
|
||||
Namespace: "test-namespace",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: pods\n namespaces:\n - ns1\n mergePatches:\n - patchData: |\n {\"metadata\":{\"annotations\":{\"foo\":null}}}",
|
||||
},
|
||||
}
|
||||
|
||||
rules7 := &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "pods",
|
||||
Namespaces: []string{
|
||||
"ns1",
|
||||
},
|
||||
},
|
||||
MergePatches: []JSONMergePatch{
|
||||
{
|
||||
PatchData: `{"metadata":{"annotations":{"foo":null}}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cm8 := &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-configmap",
|
||||
Namespace: "test-namespace",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"sub.yml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: pods\n namespaces:\n - ns1\n strategicPatches:\n - patchData: |\n {\"spec\":{\"containers\":[{\"name\": \"nginx\",\"image\": \"repo2/nginx\"}]}}",
|
||||
},
|
||||
}
|
||||
|
||||
rules8 := &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "pods",
|
||||
Namespaces: []string{
|
||||
"ns1",
|
||||
},
|
||||
},
|
||||
StrategicPatches: []StrategicMergePatch{
|
||||
{
|
||||
PatchData: `{"spec":{"containers":[{"name": "nginx","image": "repo2/nginx"}]}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type args struct {
|
||||
cm *v1.ConfigMap
|
||||
}
|
||||
@@ -180,6 +306,38 @@ func TestGetResourceModifiersFromConfig(t *testing.T) {
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "complex yaml data with json merge patch",
|
||||
args: args{
|
||||
cm: cm5,
|
||||
},
|
||||
want: rules5,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "complex yaml data with strategic merge patch",
|
||||
args: args{
|
||||
cm: cm6,
|
||||
},
|
||||
want: rules6,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "complex json data with json merge patch",
|
||||
args: args{
|
||||
cm: cm7,
|
||||
},
|
||||
want: rules7,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "complex json data with strategic merge patch",
|
||||
args: args{
|
||||
cm: cm8,
|
||||
},
|
||||
want: rules8,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -502,6 +660,38 @@ func TestResourceModifiers_ApplyResourceModifierRules(t *testing.T) {
|
||||
wantErr: false,
|
||||
wantObj: deployNginxTwoReplica.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "nginx deployment: Empty Resource Regex",
|
||||
fields: fields{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "deployments.apps",
|
||||
Namespaces: []string{"foo"},
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "test",
|
||||
Path: "/spec/replicas",
|
||||
Value: "1",
|
||||
},
|
||||
{
|
||||
Operation: "replace",
|
||||
Path: "/spec/replicas",
|
||||
Value: "2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
obj: deployNginxOneReplica.DeepCopy(),
|
||||
groupResource: "deployments.apps",
|
||||
},
|
||||
wantErr: false,
|
||||
wantObj: deployNginxTwoReplica.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "nginx deployment: Empty Resource Regex and namespaces list",
|
||||
fields: fields{
|
||||
@@ -719,7 +909,7 @@ func TestResourceModifiers_ApplyResourceModifierRules(t *testing.T) {
|
||||
Version: tt.fields.Version,
|
||||
ResourceModifierRules: tt.fields.ResourceModifierRules,
|
||||
}
|
||||
got := p.ApplyResourceModifierRules(tt.args.obj, tt.args.groupResource, logrus.New())
|
||||
got := p.ApplyResourceModifierRules(tt.args.obj, tt.args.groupResource, nil, logrus.New())
|
||||
|
||||
assert.Equal(t, tt.wantErr, len(got) > 0)
|
||||
assert.Equal(t, *tt.wantObj, *tt.args.obj)
|
||||
@@ -727,6 +917,633 @@ func TestResourceModifiers_ApplyResourceModifierRules(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
var podYAMLWithNginxImage = `
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: pod1
|
||||
namespace: fake
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx
|
||||
name: nginx
|
||||
`
|
||||
|
||||
var podYAMLWithNginx1Image = `
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: pod1
|
||||
namespace: fake
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx1
|
||||
name: nginx
|
||||
`
|
||||
|
||||
var podYAMLWithNFSVolume = `
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: pod1
|
||||
namespace: fake
|
||||
spec:
|
||||
containers:
|
||||
- image: fake
|
||||
name: fake
|
||||
volumeMounts:
|
||||
- mountPath: /fake1
|
||||
name: vol1
|
||||
- mountPath: /fake2
|
||||
name: vol2
|
||||
volumes:
|
||||
- name: vol1
|
||||
nfs:
|
||||
path: /fake2
|
||||
- name: vol2
|
||||
emptyDir: {}
|
||||
`
|
||||
|
||||
var podYAMLWithPVCVolume = `
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: pod1
|
||||
namespace: fake
|
||||
spec:
|
||||
containers:
|
||||
- image: fake
|
||||
name: fake
|
||||
volumeMounts:
|
||||
- mountPath: /fake1
|
||||
name: vol1
|
||||
- mountPath: /fake2
|
||||
name: vol2
|
||||
volumes:
|
||||
- name: vol1
|
||||
persistentVolumeClaim:
|
||||
claimName: pvc1
|
||||
- name: vol2
|
||||
emptyDir: {}
|
||||
`
|
||||
|
||||
var svcYAMLWithPort8000 = `
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: svc1
|
||||
namespace: fake
|
||||
spec:
|
||||
ports:
|
||||
- name: fake1
|
||||
port: 8001
|
||||
protocol: TCP
|
||||
targetPort: 8001
|
||||
- name: fake
|
||||
port: 8000
|
||||
protocol: TCP
|
||||
targetPort: 8000
|
||||
- name: fake2
|
||||
port: 8002
|
||||
protocol: TCP
|
||||
targetPort: 8002
|
||||
`
|
||||
|
||||
var svcYAMLWithPort9000 = `
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: svc1
|
||||
namespace: fake
|
||||
spec:
|
||||
ports:
|
||||
- name: fake1
|
||||
port: 8001
|
||||
protocol: TCP
|
||||
targetPort: 8001
|
||||
- name: fake
|
||||
port: 9000
|
||||
protocol: TCP
|
||||
targetPort: 9000
|
||||
- name: fake2
|
||||
port: 8002
|
||||
protocol: TCP
|
||||
targetPort: 8002
|
||||
`
|
||||
|
||||
var cmYAMLWithLabelAToB = `
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: cm1
|
||||
namespace: fake
|
||||
labels:
|
||||
a: b
|
||||
c: d
|
||||
`
|
||||
|
||||
var cmYAMLWithLabelAToC = `
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: cm1
|
||||
namespace: fake
|
||||
labels:
|
||||
a: c
|
||||
c: d
|
||||
`
|
||||
|
||||
var cmYAMLWithoutLabelA = `
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: cm1
|
||||
namespace: fake
|
||||
labels:
|
||||
c: d
|
||||
`
|
||||
|
||||
func TestResourceModifiers_ApplyResourceModifierRules_StrategicMergePatch(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
unstructuredSerializer := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
|
||||
o1, _, err := unstructuredSerializer.Decode([]byte(podYAMLWithNFSVolume), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
podWithNFSVolume := o1.(*unstructured.Unstructured)
|
||||
|
||||
o2, _, err := unstructuredSerializer.Decode([]byte(podYAMLWithPVCVolume), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
podWithPVCVolume := o2.(*unstructured.Unstructured)
|
||||
|
||||
o3, _, err := unstructuredSerializer.Decode([]byte(svcYAMLWithPort8000), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
svcWithPort8000 := o3.(*unstructured.Unstructured)
|
||||
|
||||
o4, _, err := unstructuredSerializer.Decode([]byte(svcYAMLWithPort9000), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
svcWithPort9000 := o4.(*unstructured.Unstructured)
|
||||
|
||||
o5, _, err := unstructuredSerializer.Decode([]byte(podYAMLWithNginxImage), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
podWithNginxImage := o5.(*unstructured.Unstructured)
|
||||
|
||||
o6, _, err := unstructuredSerializer.Decode([]byte(podYAMLWithNginx1Image), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
podWithNginx1Image := o6.(*unstructured.Unstructured)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rm *ResourceModifiers
|
||||
obj *unstructured.Unstructured
|
||||
groupResource string
|
||||
wantErr bool
|
||||
wantObj *unstructured.Unstructured
|
||||
}{
|
||||
{
|
||||
name: "update image",
|
||||
rm: &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "pods",
|
||||
Namespaces: []string{"fake"},
|
||||
},
|
||||
StrategicPatches: []StrategicMergePatch{
|
||||
{
|
||||
PatchData: `{"spec":{"containers":[{"name":"nginx","image":"nginx1"}]}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
obj: podWithNginxImage.DeepCopy(),
|
||||
groupResource: "pods",
|
||||
wantErr: false,
|
||||
wantObj: podWithNginx1Image.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "update image with yaml format",
|
||||
rm: &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "pods",
|
||||
Namespaces: []string{"fake"},
|
||||
},
|
||||
StrategicPatches: []StrategicMergePatch{
|
||||
{
|
||||
PatchData: `spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx1`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
obj: podWithNginxImage.DeepCopy(),
|
||||
groupResource: "pods",
|
||||
wantErr: false,
|
||||
wantObj: podWithNginx1Image.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "replace nfs with pvc in volume",
|
||||
rm: &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "pods",
|
||||
Namespaces: []string{"fake"},
|
||||
},
|
||||
StrategicPatches: []StrategicMergePatch{
|
||||
{
|
||||
PatchData: `{"spec":{"volumes":[{"nfs":null,"name":"vol1","persistentVolumeClaim":{"claimName":"pvc1"}}]}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
obj: podWithNFSVolume.DeepCopy(),
|
||||
groupResource: "pods",
|
||||
wantErr: false,
|
||||
wantObj: podWithPVCVolume.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "replace any other volume source with pvc in volume",
|
||||
rm: &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "pods",
|
||||
Namespaces: []string{"fake"},
|
||||
},
|
||||
StrategicPatches: []StrategicMergePatch{
|
||||
{
|
||||
PatchData: `{"spec":{"volumes":[{"$retainKeys":["name","persistentVolumeClaim"],"name":"vol1","persistentVolumeClaim":{"claimName":"pvc1"}}]}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
obj: podWithNFSVolume.DeepCopy(),
|
||||
groupResource: "pods",
|
||||
wantErr: false,
|
||||
wantObj: podWithPVCVolume.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "update a service port",
|
||||
rm: &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "services",
|
||||
Namespaces: []string{"fake"},
|
||||
},
|
||||
StrategicPatches: []StrategicMergePatch{
|
||||
{
|
||||
PatchData: `{"spec":{"$setElementOrder/ports":[{"port":8001},{"port":9000},{"port":8002}],"ports":[{"name":"fake","port":9000,"protocol":"TCP","targetPort":9000},{"$patch":"delete","port":8000}]}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
obj: svcWithPort8000.DeepCopy(),
|
||||
groupResource: "services",
|
||||
wantErr: false,
|
||||
wantObj: svcWithPort9000.DeepCopy(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.rm.ApplyResourceModifierRules(tt.obj, tt.groupResource, scheme, logrus.New())
|
||||
|
||||
assert.Equal(t, tt.wantErr, len(got) > 0)
|
||||
assert.Equal(t, *tt.wantObj, *tt.obj)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceModifiers_ApplyResourceModifierRules_JSONMergePatch(t *testing.T) {
|
||||
unstructuredSerializer := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
|
||||
o1, _, err := unstructuredSerializer.Decode([]byte(cmYAMLWithLabelAToB), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
cmWithLabelAToB := o1.(*unstructured.Unstructured)
|
||||
|
||||
o2, _, err := unstructuredSerializer.Decode([]byte(cmYAMLWithLabelAToC), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
cmWithLabelAToC := o2.(*unstructured.Unstructured)
|
||||
|
||||
o3, _, err := unstructuredSerializer.Decode([]byte(cmYAMLWithoutLabelA), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
cmWithoutLabelA := o3.(*unstructured.Unstructured)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rm *ResourceModifiers
|
||||
obj *unstructured.Unstructured
|
||||
groupResource string
|
||||
wantErr bool
|
||||
wantObj *unstructured.Unstructured
|
||||
}{
|
||||
{
|
||||
name: "update labels",
|
||||
rm: &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "configmaps",
|
||||
Namespaces: []string{"fake"},
|
||||
},
|
||||
MergePatches: []JSONMergePatch{
|
||||
{
|
||||
PatchData: `{"metadata":{"labels":{"a":"c"}}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
obj: cmWithLabelAToB.DeepCopy(),
|
||||
groupResource: "configmaps",
|
||||
wantErr: false,
|
||||
wantObj: cmWithLabelAToC.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "update labels in yaml format",
|
||||
rm: &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "configmaps",
|
||||
Namespaces: []string{"fake"},
|
||||
},
|
||||
MergePatches: []JSONMergePatch{
|
||||
{
|
||||
PatchData: `metadata:
|
||||
labels:
|
||||
a: c`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
obj: cmWithLabelAToB.DeepCopy(),
|
||||
groupResource: "configmaps",
|
||||
wantErr: false,
|
||||
wantObj: cmWithLabelAToC.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "delete labels",
|
||||
rm: &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "configmaps",
|
||||
Namespaces: []string{"fake"},
|
||||
},
|
||||
MergePatches: []JSONMergePatch{
|
||||
{
|
||||
PatchData: `{"metadata":{"labels":{"a":null}}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
obj: cmWithLabelAToB.DeepCopy(),
|
||||
groupResource: "configmaps",
|
||||
wantErr: false,
|
||||
wantObj: cmWithoutLabelA.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "add labels",
|
||||
rm: &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "configmaps",
|
||||
Namespaces: []string{"fake"},
|
||||
},
|
||||
MergePatches: []JSONMergePatch{
|
||||
{
|
||||
PatchData: `{"metadata":{"labels":{"a":"b"}}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
obj: cmWithoutLabelA.DeepCopy(),
|
||||
groupResource: "configmaps",
|
||||
wantErr: false,
|
||||
wantObj: cmWithLabelAToB.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "delete non-existing labels",
|
||||
rm: &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "configmaps",
|
||||
Namespaces: []string{"fake"},
|
||||
},
|
||||
MergePatches: []JSONMergePatch{
|
||||
{
|
||||
PatchData: `{"metadata":{"labels":{"a":null}}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
obj: cmWithoutLabelA.DeepCopy(),
|
||||
groupResource: "configmaps",
|
||||
wantErr: false,
|
||||
wantObj: cmWithoutLabelA.DeepCopy(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.rm.ApplyResourceModifierRules(tt.obj, tt.groupResource, nil, logrus.New())
|
||||
|
||||
assert.Equal(t, tt.wantErr, len(got) > 0)
|
||||
assert.Equal(t, *tt.wantObj, *tt.obj)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceModifiers_wildcard_in_GroupResource(t *testing.T) {
|
||||
unstructuredSerializer := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
|
||||
o1, _, err := unstructuredSerializer.Decode([]byte(cmYAMLWithLabelAToB), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
cmWithLabelAToB := o1.(*unstructured.Unstructured)
|
||||
|
||||
o2, _, err := unstructuredSerializer.Decode([]byte(cmYAMLWithLabelAToC), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
cmWithLabelAToC := o2.(*unstructured.Unstructured)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rm *ResourceModifiers
|
||||
obj *unstructured.Unstructured
|
||||
groupResource string
|
||||
wantErr bool
|
||||
wantObj *unstructured.Unstructured
|
||||
}{
|
||||
{
|
||||
name: "match all groups and resources",
|
||||
rm: &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "*",
|
||||
Namespaces: []string{"fake"},
|
||||
},
|
||||
MergePatches: []JSONMergePatch{
|
||||
{
|
||||
PatchData: `{"metadata":{"labels":{"a":"c"}}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
obj: cmWithLabelAToB.DeepCopy(),
|
||||
groupResource: "configmaps",
|
||||
wantErr: false,
|
||||
wantObj: cmWithLabelAToC.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "match all resources in group apps",
|
||||
rm: &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "*.apps",
|
||||
Namespaces: []string{"fake"},
|
||||
},
|
||||
MergePatches: []JSONMergePatch{
|
||||
{
|
||||
PatchData: `{"metadata":{"labels":{"a":"c"}}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
obj: cmWithLabelAToB.DeepCopy(),
|
||||
groupResource: "fake.apps",
|
||||
wantErr: false,
|
||||
wantObj: cmWithLabelAToC.DeepCopy(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.rm.ApplyResourceModifierRules(tt.obj, tt.groupResource, nil, logrus.New())
|
||||
|
||||
assert.Equal(t, tt.wantErr, len(got) > 0)
|
||||
assert.Equal(t, *tt.wantObj, *tt.obj)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceModifiers_conditional_patches(t *testing.T) {
|
||||
unstructuredSerializer := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
|
||||
o1, _, err := unstructuredSerializer.Decode([]byte(cmYAMLWithLabelAToB), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
cmWithLabelAToB := o1.(*unstructured.Unstructured)
|
||||
|
||||
o2, _, err := unstructuredSerializer.Decode([]byte(cmYAMLWithLabelAToC), nil, nil)
|
||||
assert.NoError(t, err)
|
||||
cmWithLabelAToC := o2.(*unstructured.Unstructured)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rm *ResourceModifiers
|
||||
obj *unstructured.Unstructured
|
||||
groupResource string
|
||||
wantErr bool
|
||||
wantObj *unstructured.Unstructured
|
||||
}{
|
||||
{
|
||||
name: "match conditions and apply patches",
|
||||
rm: &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "*",
|
||||
Namespaces: []string{"fake"},
|
||||
Matches: []MatchRule{
|
||||
{
|
||||
Path: "/metadata/labels/a",
|
||||
Value: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
MergePatches: []JSONMergePatch{
|
||||
{
|
||||
PatchData: `{"metadata":{"labels":{"a":"c"}}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
obj: cmWithLabelAToB.DeepCopy(),
|
||||
groupResource: "configmaps",
|
||||
wantErr: false,
|
||||
wantObj: cmWithLabelAToC.DeepCopy(),
|
||||
},
|
||||
{
|
||||
name: "mismatch conditions and skip patches",
|
||||
rm: &ResourceModifiers{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "*",
|
||||
Namespaces: []string{"fake"},
|
||||
Matches: []MatchRule{
|
||||
{
|
||||
Path: "/metadata/labels/a",
|
||||
Value: "c",
|
||||
},
|
||||
},
|
||||
},
|
||||
MergePatches: []JSONMergePatch{
|
||||
{
|
||||
PatchData: `{"metadata":{"labels":{"a":"c"}}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
obj: cmWithLabelAToB.DeepCopy(),
|
||||
groupResource: "configmaps",
|
||||
wantErr: false,
|
||||
wantObj: cmWithLabelAToB.DeepCopy(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.rm.ApplyResourceModifierRules(tt.obj, tt.groupResource, nil, logrus.New())
|
||||
|
||||
assert.Equal(t, tt.wantErr, len(got) > 0)
|
||||
assert.Equal(t, *tt.wantObj, *tt.obj)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONPatch_ToString(t *testing.T) {
|
||||
type fields struct {
|
||||
Operation string
|
||||
|
||||
@@ -24,6 +24,21 @@ func (r *ResourceModifierRule) Validate() error {
|
||||
if err := r.Conditions.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, size := range []int{
|
||||
len(r.Patches),
|
||||
len(r.MergePatches),
|
||||
len(r.StrategicPatches),
|
||||
} {
|
||||
if size != 0 {
|
||||
count++
|
||||
}
|
||||
if count >= 2 {
|
||||
return fmt.Errorf("only one of patches, mergePatches, strategicPatches can be specified")
|
||||
}
|
||||
}
|
||||
|
||||
for _, patch := range r.Patches {
|
||||
if err := patch.Validate(); err != nil {
|
||||
return err
|
||||
|
||||
@@ -129,6 +129,32 @@ func TestResourceModifiers_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "More than one patch type in a rule",
|
||||
fields: fields{
|
||||
Version: "v1",
|
||||
ResourceModifierRules: []ResourceModifierRule{
|
||||
{
|
||||
Conditions: Conditions{
|
||||
GroupResource: "*",
|
||||
},
|
||||
Patches: []JSONPatch{
|
||||
{
|
||||
Operation: "test",
|
||||
Path: "/spec/storageClassName",
|
||||
Value: "premium",
|
||||
},
|
||||
},
|
||||
MergePatches: []JSONMergePatch{
|
||||
{
|
||||
PatchData: `{"metadata":{"labels":{"a":null}}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package resourcemodifiers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
kubejson "sigs.k8s.io/json"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type StrategicMergePatch struct {
|
||||
PatchData string `json:"patchData,omitempty"`
|
||||
}
|
||||
|
||||
type StrategicMergePatcher struct {
|
||||
patches []StrategicMergePatch
|
||||
scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
func (p *StrategicMergePatcher) Patch(u *unstructured.Unstructured, _ logrus.FieldLogger) (*unstructured.Unstructured, error) {
|
||||
gvk := u.GetObjectKind().GroupVersionKind()
|
||||
schemaReferenceObj, err := p.scheme.New(gvk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
origin := u.DeepCopy()
|
||||
updated := u.DeepCopy()
|
||||
for _, patch := range p.patches {
|
||||
patchBytes, err := yaml.YAMLToJSON([]byte(patch.PatchData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in converting YAML to JSON %s", err)
|
||||
}
|
||||
|
||||
err = strategicPatchObject(origin, patchBytes, updated, schemaReferenceObj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in applying Strategic Patch %s", err.Error())
|
||||
}
|
||||
|
||||
origin = updated.DeepCopy()
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
// strategicPatchObject applies a strategic merge patch of `patchBytes` to
|
||||
// `originalObject` and stores the result in `objToUpdate`.
|
||||
// It additionally returns the map[string]interface{} representation of the
|
||||
// `originalObject` and `patchBytes`.
|
||||
// NOTE: Both `originalObject` and `objToUpdate` are supposed to be versioned.
|
||||
func strategicPatchObject(
|
||||
originalObject runtime.Object,
|
||||
patchBytes []byte,
|
||||
objToUpdate runtime.Object,
|
||||
schemaReferenceObj runtime.Object,
|
||||
) error {
|
||||
originalObjMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(originalObject)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
patchMap := make(map[string]interface{})
|
||||
var strictErrs []error
|
||||
strictErrs, err = kubejson.UnmarshalStrict(patchBytes, &patchMap)
|
||||
if err != nil {
|
||||
return apierrors.NewBadRequest(err.Error())
|
||||
}
|
||||
|
||||
if err := applyPatchToObject(originalObjMap, patchMap, objToUpdate, schemaReferenceObj, strictErrs); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyPatchToObject applies a strategic merge patch of <patchMap> to
|
||||
// <originalMap> and stores the result in <objToUpdate>.
|
||||
// NOTE: <objToUpdate> must be a versioned object.
|
||||
func applyPatchToObject(
|
||||
originalMap map[string]interface{},
|
||||
patchMap map[string]interface{},
|
||||
objToUpdate runtime.Object,
|
||||
schemaReferenceObj runtime.Object,
|
||||
strictErrs []error,
|
||||
) error {
|
||||
patchedObjMap, err := strategicpatch.StrategicMergeMapPatch(originalMap, patchMap, schemaReferenceObj)
|
||||
if err != nil {
|
||||
return interpretStrategicMergePatchError(err)
|
||||
}
|
||||
|
||||
// Rather than serialize the patched map to JSON, then decode it to an object, we go directly from a map to an object
|
||||
converter := runtime.DefaultUnstructuredConverter
|
||||
if err := converter.FromUnstructuredWithValidation(patchedObjMap, objToUpdate, true); err != nil {
|
||||
strictError, isStrictError := runtime.AsStrictDecodingError(err)
|
||||
switch {
|
||||
case !isStrictError:
|
||||
// disregard any sttrictErrs, because it's an incomplete
|
||||
// list of strict errors given that we don't know what fields were
|
||||
// unknown because StrategicMergeMapPatch failed.
|
||||
// Non-strict errors trump in this case.
|
||||
return apierrors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{
|
||||
field.Invalid(field.NewPath("patch"), fmt.Sprintf("%+v", patchMap), err.Error()),
|
||||
})
|
||||
//case validationDirective == metav1.FieldValidationWarn:
|
||||
// addStrictDecodingWarnings(requestContext, append(strictErrs, strictError.Errors()...))
|
||||
default:
|
||||
strictDecodingError := runtime.NewStrictDecodingError(append(strictErrs, strictError.Errors()...))
|
||||
return apierrors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{
|
||||
field.Invalid(field.NewPath("patch"), fmt.Sprintf("%+v", patchMap), strictDecodingError.Error()),
|
||||
})
|
||||
}
|
||||
} else if len(strictErrs) > 0 {
|
||||
switch {
|
||||
//case validationDirective == metav1.FieldValidationWarn:
|
||||
// addStrictDecodingWarnings(requestContext, strictErrs)
|
||||
default:
|
||||
return apierrors.NewInvalid(schema.GroupKind{}, "", field.ErrorList{
|
||||
field.Invalid(field.NewPath("patch"), fmt.Sprintf("%+v", patchMap), runtime.NewStrictDecodingError(strictErrs).Error()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// interpretStrategicMergePatchError interprets the error type and returns an error with appropriate HTTP code.
|
||||
func interpretStrategicMergePatchError(err error) error {
|
||||
switch err {
|
||||
case mergepatch.ErrBadJSONDoc, mergepatch.ErrBadPatchFormatForPrimitiveList, mergepatch.ErrBadPatchFormatForRetainKeys, mergepatch.ErrBadPatchFormatForSetElementOrderList, mergepatch.ErrUnsupportedStrategicMergePatchFormat:
|
||||
return apierrors.NewBadRequest(err.Error())
|
||||
case mergepatch.ErrNoListOfLists, mergepatch.ErrPatchContentNotMatchRetainKeys:
|
||||
return apierrors.NewGenericServerResponse(http.StatusUnprocessableEntity, "", schema.GroupResource{}, "", err.Error(), 0, false)
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package resourcemodifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
)
|
||||
|
||||
func TestStrategicMergePatchFailure(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data string
|
||||
kind string
|
||||
}{
|
||||
{
|
||||
name: "patch with unknown kind",
|
||||
data: "{}",
|
||||
kind: "BadKind",
|
||||
},
|
||||
{
|
||||
name: "patch with bad yaml",
|
||||
data: "a: b:",
|
||||
kind: "Pod",
|
||||
},
|
||||
{
|
||||
name: "patch with bad json",
|
||||
data: `{"a"::1}`,
|
||||
kind: "Pod",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
err := clientgoscheme.AddToScheme(scheme)
|
||||
assert.NoError(t, err)
|
||||
pt := &StrategicMergePatcher{
|
||||
patches: []StrategicMergePatch{{PatchData: tt.data}},
|
||||
scheme: scheme,
|
||||
}
|
||||
|
||||
u := &unstructured.Unstructured{}
|
||||
u.SetGroupVersionKind(schema.GroupVersionKind{Version: "v1", Kind: tt.kind})
|
||||
_, err = pt.Patch(u, logrus.New())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,8 @@ import (
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
"github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/scheme"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
"github.com/vmware-tanzu/velero/pkg/util"
|
||||
)
|
||||
|
||||
func TestIsReadyToValidate(t *testing.T) {
|
||||
@@ -163,7 +163,7 @@ func TestListBackupStorageLocations(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
client := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithRuntimeObjects(tt.backupLocations).Build()
|
||||
client := fake.NewClientBuilder().WithScheme(util.VeleroScheme).WithRuntimeObjects(tt.backupLocations).Build()
|
||||
if tt.expectError {
|
||||
_, err := ListBackupStorageLocations(context.Background(), client, "ns-1")
|
||||
g.Expect(err).NotTo(BeNil())
|
||||
|
||||
@@ -261,12 +261,12 @@ type ExecHook struct {
|
||||
type HookErrorMode string
|
||||
|
||||
const (
|
||||
// HookErrorModeContinue means that an error from a hook is acceptable, and the backup can
|
||||
// proceed.
|
||||
// HookErrorModeContinue means that an error from a hook is acceptable and the backup/restore can
|
||||
// proceed with the rest of hooks' execution. This backup/restore should be in `PartiallyFailed` status.
|
||||
HookErrorModeContinue HookErrorMode = "Continue"
|
||||
|
||||
// HookErrorModeFail means that an error from a hook is problematic, and the backup should be in
|
||||
// error.
|
||||
// HookErrorModeFail means that an error from a hook is problematic and Velero should stop executing following hooks.
|
||||
// This backup/restore should be in `PartiallyFailed` status.
|
||||
HookErrorModeFail HookErrorMode = "Fail"
|
||||
)
|
||||
|
||||
|
||||
@@ -784,6 +784,11 @@ func (in *ExecRestoreHook) DeepCopyInto(out *ExecRestoreHook) {
|
||||
}
|
||||
out.ExecTimeout = in.ExecTimeout
|
||||
out.WaitTimeout = in.WaitTimeout
|
||||
if in.WaitForReady != nil {
|
||||
in, out := &in.WaitForReady, &out.WaitForReady
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecRestoreHook.
|
||||
|
||||
@@ -131,6 +131,7 @@ type DataDownloadStatus struct {
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since this DataDownload was created"
|
||||
// +kubebuilder:printcolumn:name="Node",type="string",JSONPath=".status.node",description="Name of the node where the DataDownload is processed"
|
||||
|
||||
// DataDownload acts as the protocol between data mover plugins and data mover controller for the datamover restore operation
|
||||
type DataDownload struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ type DataUploadStatus struct {
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since this DataUpload was created"
|
||||
// +kubebuilder:printcolumn:name="Node",type="string",JSONPath=".status.node",description="Name of the node where the DataUpload is processed"
|
||||
|
||||
// DataUpload acts as the protocol between data mover plugins and data mover controller for the datamover backup operation
|
||||
type DataUpload struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ import (
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
velerov2alpha1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1"
|
||||
clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
|
||||
)
|
||||
|
||||
//go:generate mockery --name Factory
|
||||
@@ -42,9 +41,6 @@ import (
|
||||
type Factory interface {
|
||||
// BindFlags binds common flags (--kubeconfig, --namespace) to the passed-in FlagSet.
|
||||
BindFlags(flags *pflag.FlagSet)
|
||||
// Client returns a VeleroClient. It uses the following priority to specify the cluster
|
||||
// configuration: --kubeconfig flag, KUBECONFIG environment variable, in-cluster configuration.
|
||||
Client() (clientset.Interface, error)
|
||||
// KubeClient returns a Kubernetes client. It uses the following priority to specify the cluster
|
||||
// configuration: --kubeconfig flag, KUBECONFIG environment variable, in-cluster configuration.
|
||||
KubeClient() (kubernetes.Interface, error)
|
||||
@@ -115,19 +111,6 @@ func (f *factory) ClientConfig() (*rest.Config, error) {
|
||||
return Config(f.kubeconfig, f.kubecontext, f.baseName, f.clientQPS, f.clientBurst)
|
||||
}
|
||||
|
||||
func (f *factory) Client() (clientset.Interface, error) {
|
||||
clientConfig, err := f.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
veleroClient, err := clientset.NewForConfig(clientConfig)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return veleroClient, nil
|
||||
}
|
||||
|
||||
func (f *factory) KubeClient() (kubernetes.Interface, error) {
|
||||
clientConfig, err := f.ClientConfig()
|
||||
if err != nil {
|
||||
|
||||
@@ -112,11 +112,6 @@ func TestFactory(t *testing.T) {
|
||||
assert.Equal(t, test.burst, clientConfig.Burst)
|
||||
strings.Contains(clientConfig.UserAgent, test.baseName)
|
||||
|
||||
client, _ := f.Client()
|
||||
_, e := client.Discovery().ServerGroups()
|
||||
assert.Contains(t, e.Error(), fmt.Sprintf("Get \"%s/api?timeout=", test.expectedHost))
|
||||
assert.NotNil(t, client)
|
||||
|
||||
kubeClient, _ := f.KubeClient()
|
||||
group := kubeClient.NodeV1().RESTClient().APIVersion().Group
|
||||
assert.NotNil(t, kubeClient)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.30.1. DO NOT EDIT.
|
||||
// Code generated by mockery v2.20.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
@@ -13,8 +13,6 @@ import (
|
||||
pkgclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
rest "k8s.io/client-go/rest"
|
||||
|
||||
versioned "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
|
||||
)
|
||||
|
||||
// Factory is an autogenerated mock type for the Factory type
|
||||
@@ -27,32 +25,6 @@ func (_m *Factory) BindFlags(flags *pflag.FlagSet) {
|
||||
_m.Called(flags)
|
||||
}
|
||||
|
||||
// Client provides a mock function with given fields:
|
||||
func (_m *Factory) Client() (versioned.Interface, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 versioned.Interface
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (versioned.Interface, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() versioned.Interface); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(versioned.Interface)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ClientConfig provides a mock function with given fields:
|
||||
func (_m *Factory) ClientConfig() (*rest.Config, error) {
|
||||
ret := _m.Called()
|
||||
@@ -212,12 +184,13 @@ func (_m *Factory) SetClientQPS(_a0 float32) {
|
||||
_m.Called(_a0)
|
||||
}
|
||||
|
||||
// NewFactory creates a new instance of Factory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewFactory(t interface {
|
||||
type mockConstructorTestingTNewFactory interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Factory {
|
||||
}
|
||||
|
||||
// NewFactory creates a new instance of Factory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewFactory(t mockConstructorTestingTNewFactory) *Factory {
|
||||
mock := &Factory{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
|
||||
+2
-8
@@ -25,20 +25,14 @@ import (
|
||||
)
|
||||
|
||||
func CreateRetryGenerateName(client kbclient.Client, ctx context.Context, obj kbclient.Object) error {
|
||||
return CreateRetryGenerateNameWithFunc(obj, func() error {
|
||||
return client.Create(ctx, obj, &kbclient.CreateOptions{})
|
||||
})
|
||||
}
|
||||
|
||||
func CreateRetryGenerateNameWithFunc(obj kbclient.Object, createFn func() error) error {
|
||||
retryCreateFn := func() error {
|
||||
// needed to ensure that the name from the failed create isn't left on the object between retries
|
||||
obj.SetName("")
|
||||
return createFn()
|
||||
return client.Create(ctx, obj, &kbclient.CreateOptions{})
|
||||
}
|
||||
if obj.GetGenerateName() != "" && obj.GetName() == "" {
|
||||
return retry.OnError(retry.DefaultRetry, apierrors.IsAlreadyExists, retryCreateFn)
|
||||
} else {
|
||||
return createFn()
|
||||
return client.Create(ctx, obj, &kbclient.CreateOptions{})
|
||||
}
|
||||
}
|
||||
|
||||
+16
-11
@@ -30,9 +30,12 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/vmware-tanzu/crash-diagnostics/exec"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/cmd"
|
||||
)
|
||||
@@ -110,30 +113,32 @@ func (o *option) complete(f client.Factory, fs *pflag.FlagSet) error {
|
||||
}
|
||||
|
||||
func (o *option) validate(f client.Factory) error {
|
||||
kubeClient, err := f.KubeClient()
|
||||
crClient, err := f.KubebuilderClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l, err := kubeClient.AppsV1().Deployments(o.namespace).List(context.TODO(), metav1.ListOptions{
|
||||
LabelSelector: "component=velero",
|
||||
deploymentList := new(appsv1.DeploymentList)
|
||||
selector, err := labels.Parse("component=velero")
|
||||
cmd.CheckError(err)
|
||||
err = crClient.List(context.TODO(), deploymentList, &ctrlclient.ListOptions{
|
||||
Namespace: o.namespace,
|
||||
LabelSelector: selector,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to check velero deployment")
|
||||
}
|
||||
if len(l.Items) == 0 {
|
||||
if len(deploymentList.Items) == 0 {
|
||||
return fmt.Errorf("velero deployment does not exist in namespace: %s", o.namespace)
|
||||
}
|
||||
veleroClient, err := f.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(o.backup) > 0 {
|
||||
if _, err := veleroClient.VeleroV1().Backups(o.namespace).Get(context.TODO(), o.backup, metav1.GetOptions{}); err != nil {
|
||||
backup := new(velerov1api.Backup)
|
||||
if err := crClient.Get(context.TODO(), ctrlclient.ObjectKey{Namespace: o.namespace, Name: o.backup}, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(o.restore) > 0 {
|
||||
if _, err := veleroClient.VeleroV1().Restores(o.namespace).Get(context.TODO(), o.restore, metav1.GetOptions{}); err != nil {
|
||||
restore := new(velerov1api.Restore)
|
||||
if err := crClient.Get(context.TODO(), ctrlclient.ObjectKey{Namespace: o.namespace, Name: o.restore}, restore); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,7 +490,7 @@ func (s *nodeAgentServer) markInProgressPVRsFailed(client ctrlclient.Client) {
|
||||
}
|
||||
|
||||
func (s *nodeAgentServer) getDataPathConcurrentNum(defaultNum int, logger logrus.FieldLogger) int {
|
||||
configs, err := nodeagent.GetConfigs(s.ctx, s.namespace, s.kubeClient.CoreV1())
|
||||
configs, err := nodeagent.GetConfigs(s.ctx, s.namespace, s.kubeClient)
|
||||
if err != nil {
|
||||
logger.WithError(err).Warn("Failed to get node agent configs")
|
||||
return defaultNum
|
||||
|
||||
+11
-3
@@ -21,6 +21,8 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
@@ -38,19 +40,25 @@ func NewGetCommand(f client.Factory, use string) *cobra.Command {
|
||||
err := output.ValidateFlags(c)
|
||||
cmd.CheckError(err)
|
||||
|
||||
veleroClient, err := f.Client()
|
||||
crClient, err := f.KubebuilderClient()
|
||||
cmd.CheckError(err)
|
||||
|
||||
var repos *api.BackupRepositoryList
|
||||
if len(args) > 0 {
|
||||
repos = new(api.BackupRepositoryList)
|
||||
for _, name := range args {
|
||||
repo, err := veleroClient.VeleroV1().BackupRepositories(f.Namespace()).Get(context.TODO(), name, metav1.GetOptions{})
|
||||
repo := new(api.BackupRepository)
|
||||
err := crClient.Get(context.TODO(), ctrlclient.ObjectKey{Namespace: f.Namespace(), Name: name}, repo)
|
||||
cmd.CheckError(err)
|
||||
repos.Items = append(repos.Items, *repo)
|
||||
}
|
||||
} else {
|
||||
repos, err = veleroClient.VeleroV1().BackupRepositories(f.Namespace()).List(context.TODO(), listOptions)
|
||||
selector := labels.NewSelector()
|
||||
if listOptions.LabelSelector != "" {
|
||||
selector, err = labels.Parse(listOptions.LabelSelector)
|
||||
cmd.CheckError(err)
|
||||
}
|
||||
err = crClient.List(context.TODO(), repos, &ctrlclient.ListOptions{LabelSelector: selector})
|
||||
cmd.CheckError(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
@@ -49,13 +49,11 @@ func NewLogsCommand(f client.Factory) *cobra.Command {
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
restoreName := args[0]
|
||||
|
||||
veleroClient, err := f.Client()
|
||||
cmd.CheckError(err)
|
||||
|
||||
kbClient, err := f.KubebuilderClient()
|
||||
cmd.CheckError(err)
|
||||
|
||||
restore, err := veleroClient.VeleroV1().Restores(f.Namespace()).Get(context.TODO(), restoreName, metav1.GetOptions{})
|
||||
restore := new(velerov1api.Restore)
|
||||
err = kbClient.Get(context.TODO(), ctrlclient.ObjectKey{Namespace: f.Namespace(), Name: restoreName}, restore)
|
||||
if apierrors.IsNotFound(err) {
|
||||
cmd.Exit("Restore %q does not exist.", restoreName)
|
||||
} else if err != nil {
|
||||
|
||||
@@ -115,7 +115,7 @@ func (o *CreateOptions) Complete(args []string, f client.Factory) error {
|
||||
func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
|
||||
var orders map[string]string
|
||||
|
||||
veleroClient, err := f.Client()
|
||||
crClient, err := f.KubebuilderClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -171,7 +171,7 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = veleroClient.VeleroV1().Schedules(schedule.Namespace).Create(context.TODO(), schedule, metav1.CreateOptions{})
|
||||
err = crClient.Create(context.TODO(), schedule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
@@ -36,19 +38,25 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command {
|
||||
Use: use + " [NAME1] [NAME2] [NAME...]",
|
||||
Short: "Describe schedules",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
veleroClient, err := f.Client()
|
||||
crClient, err := f.KubebuilderClient()
|
||||
cmd.CheckError(err)
|
||||
|
||||
var schedules *v1.ScheduleList
|
||||
if len(args) > 0 {
|
||||
schedules = new(v1.ScheduleList)
|
||||
for _, name := range args {
|
||||
schedule, err := veleroClient.VeleroV1().Schedules(f.Namespace()).Get(context.TODO(), name, metav1.GetOptions{})
|
||||
schedule := new(v1.Schedule)
|
||||
err := crClient.Get(context.TODO(), ctrlclient.ObjectKey{Namespace: f.Namespace(), Name: name}, schedule)
|
||||
cmd.CheckError(err)
|
||||
schedules.Items = append(schedules.Items, *schedule)
|
||||
}
|
||||
} else {
|
||||
schedules, err = veleroClient.VeleroV1().Schedules(f.Namespace()).List(context.TODO(), listOptions)
|
||||
selector := labels.NewSelector()
|
||||
if listOptions.LabelSelector != "" {
|
||||
selector, err = labels.Parse(listOptions.LabelSelector)
|
||||
cmd.CheckError(err)
|
||||
}
|
||||
err = crClient.List(context.TODO(), schedules, &ctrlclient.ListOptions{LabelSelector: selector})
|
||||
cmd.CheckError(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
@@ -38,19 +40,25 @@ func NewGetCommand(f client.Factory, use string) *cobra.Command {
|
||||
err := output.ValidateFlags(c)
|
||||
cmd.CheckError(err)
|
||||
|
||||
veleroClient, err := f.Client()
|
||||
crClient, err := f.KubebuilderClient()
|
||||
cmd.CheckError(err)
|
||||
|
||||
var schedules *api.ScheduleList
|
||||
if len(args) > 0 {
|
||||
schedules = new(api.ScheduleList)
|
||||
for _, name := range args {
|
||||
schedule, err := veleroClient.VeleroV1().Schedules(f.Namespace()).Get(context.TODO(), name, metav1.GetOptions{})
|
||||
schedule := new(api.Schedule)
|
||||
err := crClient.Get(context.TODO(), ctrlclient.ObjectKey{Name: name, Namespace: f.Namespace()}, schedule)
|
||||
cmd.CheckError(err)
|
||||
schedules.Items = append(schedules.Items, *schedule)
|
||||
}
|
||||
} else {
|
||||
schedules, err = veleroClient.VeleroV1().Schedules(f.Namespace()).List(context.TODO(), listOptions)
|
||||
selector := labels.NewSelector()
|
||||
if listOptions.LabelSelector != "" {
|
||||
selector, err = labels.Parse(listOptions.LabelSelector)
|
||||
cmd.CheckError(err)
|
||||
}
|
||||
err := crClient.List(context.TODO(), schedules, &ctrlclient.ListOptions{LabelSelector: selector})
|
||||
cmd.CheckError(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
kubeerrs "k8s.io/apimachinery/pkg/util/errors"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
@@ -63,7 +64,7 @@ func NewPauseCommand(f client.Factory, use string) *cobra.Command {
|
||||
}
|
||||
|
||||
func runPause(f client.Factory, o *cli.SelectOptions, paused bool) error {
|
||||
client, err := f.Client()
|
||||
crClient, err := f.KubebuilderClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -75,7 +76,8 @@ func runPause(f client.Factory, o *cli.SelectOptions, paused bool) error {
|
||||
switch {
|
||||
case len(o.Names) > 0:
|
||||
for _, name := range o.Names {
|
||||
schedule, err := client.VeleroV1().Schedules(f.Namespace()).Get(context.TODO(), name, metav1.GetOptions{})
|
||||
schedule := new(velerov1api.Schedule)
|
||||
err := crClient.Get(context.TODO(), ctrlclient.ObjectKey{Name: name, Namespace: f.Namespace()}, schedule)
|
||||
if err != nil {
|
||||
errs = append(errs, errors.WithStack(err))
|
||||
continue
|
||||
@@ -83,11 +85,16 @@ func runPause(f client.Factory, o *cli.SelectOptions, paused bool) error {
|
||||
schedules = append(schedules, schedule)
|
||||
}
|
||||
default:
|
||||
selector := labels.Everything().String()
|
||||
selector := labels.Everything()
|
||||
if o.Selector.LabelSelector != nil {
|
||||
selector = o.Selector.String()
|
||||
convertedSelector, err := metav1.LabelSelectorAsSelector(o.Selector.LabelSelector)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
selector = convertedSelector
|
||||
}
|
||||
res, err := client.VeleroV1().Schedules(f.Namespace()).List(context.TODO(), metav1.ListOptions{
|
||||
res := new(velerov1api.ScheduleList)
|
||||
err := crClient.List(context.TODO(), res, &ctrlclient.ListOptions{
|
||||
LabelSelector: selector,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -113,7 +120,7 @@ func runPause(f client.Factory, o *cli.SelectOptions, paused bool) error {
|
||||
continue
|
||||
}
|
||||
schedule.Spec.Paused = paused
|
||||
if _, err := client.VeleroV1().Schedules(schedule.Namespace).Update(context.TODO(), schedule, metav1.UpdateOptions{}); err != nil {
|
||||
if err := crClient.Update(context.TODO(), schedule); err != nil {
|
||||
return errors.Wrapf(err, "failed to update schedule %s", schedule.Name)
|
||||
}
|
||||
fmt.Printf("Schedule %s %s successfully\n", schedule.Name, msg)
|
||||
|
||||
@@ -124,12 +124,12 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := f.Client()
|
||||
client, err := f.KubebuilderClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := client.VeleroV1().VolumeSnapshotLocations(volumeSnapshotLocation.Namespace).Create(context.TODO(), volumeSnapshotLocation, metav1.CreateOptions{}); err != nil {
|
||||
if err := client.Create(context.TODO(), volumeSnapshotLocation); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
@@ -36,18 +37,19 @@ func NewGetCommand(f client.Factory, use string) *cobra.Command {
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
err := output.ValidateFlags(c)
|
||||
cmd.CheckError(err)
|
||||
veleroClient, err := f.Client()
|
||||
client, err := f.KubebuilderClient()
|
||||
cmd.CheckError(err)
|
||||
var locations *api.VolumeSnapshotLocationList
|
||||
locations := new(api.VolumeSnapshotLocationList)
|
||||
|
||||
if len(args) > 0 {
|
||||
locations = new(api.VolumeSnapshotLocationList)
|
||||
for _, name := range args {
|
||||
location, err := veleroClient.VeleroV1().VolumeSnapshotLocations(f.Namespace()).Get(context.TODO(), name, metav1.GetOptions{})
|
||||
location := new(api.VolumeSnapshotLocation)
|
||||
err := client.Get(context.TODO(), kbclient.ObjectKey{Namespace: f.Namespace(), Name: name}, location)
|
||||
cmd.CheckError(err)
|
||||
locations.Items = append(locations.Items, *location)
|
||||
}
|
||||
} else {
|
||||
locations, err = veleroClient.VeleroV1().VolumeSnapshotLocations(f.Namespace()).List(context.TODO(), listOptions)
|
||||
err = client.List(context.TODO(), locations, &kbclient.ListOptions{Namespace: f.Namespace()})
|
||||
cmd.CheckError(err)
|
||||
}
|
||||
_, err = output.PrintWithFormat(c, locations)
|
||||
|
||||
@@ -152,12 +152,12 @@ func newPodVolumeRestoreItemAction(f client.Factory) plugincommon.HandlerInitial
|
||||
return nil, err
|
||||
}
|
||||
|
||||
veleroClient, err := f.Client()
|
||||
crClient, err := f.KubebuilderClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return restore.NewPodVolumeRestoreAction(logger, client.CoreV1().ConfigMaps(f.Namespace()), veleroClient.VeleroV1().PodVolumeBackups(f.Namespace())), nil
|
||||
return restore.NewPodVolumeRestoreAction(logger, client.CoreV1().ConfigMaps(f.Namespace()), crClient), nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+27
-17
@@ -66,7 +66,6 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/controller"
|
||||
velerodiscovery "github.com/vmware-tanzu/velero/pkg/discovery"
|
||||
"github.com/vmware-tanzu/velero/pkg/features"
|
||||
clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
|
||||
"github.com/vmware-tanzu/velero/pkg/itemoperationmap"
|
||||
"github.com/vmware-tanzu/velero/pkg/metrics"
|
||||
"github.com/vmware-tanzu/velero/pkg/nodeagent"
|
||||
@@ -250,12 +249,12 @@ type server struct {
|
||||
metricsAddress string
|
||||
kubeClientConfig *rest.Config
|
||||
kubeClient kubernetes.Interface
|
||||
veleroClient clientset.Interface
|
||||
discoveryClient discovery.DiscoveryInterface
|
||||
discoveryHelper velerodiscovery.Helper
|
||||
dynamicClient dynamic.Interface
|
||||
csiSnapshotClient *snapshotv1client.Clientset
|
||||
csiSnapshotLister snapshotv1listers.VolumeSnapshotLister
|
||||
crClient ctrlclient.Client
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
logger logrus.FieldLogger
|
||||
@@ -295,12 +294,12 @@ func newServer(f client.Factory, config serverConfig, logger *logrus.Logger) (*s
|
||||
return nil, err
|
||||
}
|
||||
|
||||
veleroClient, err := f.Client()
|
||||
dynamicClient, err := f.DynamicClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dynamicClient, err := f.DynamicClient()
|
||||
crClient, err := f.KubebuilderClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -375,14 +374,20 @@ func newServer(f client.Factory, config serverConfig, logger *logrus.Logger) (*s
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var discoveryClient *discovery.DiscoveryClient
|
||||
if discoveryClient, err = discovery.NewDiscoveryClientForConfig(clientConfig); err != nil {
|
||||
cancelFunc()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &server{
|
||||
namespace: f.Namespace(),
|
||||
metricsAddress: config.metricsAddress,
|
||||
kubeClientConfig: clientConfig,
|
||||
kubeClient: kubeClient,
|
||||
veleroClient: veleroClient,
|
||||
discoveryClient: veleroClient.Discovery(),
|
||||
discoveryClient: discoveryClient,
|
||||
dynamicClient: dynamicClient,
|
||||
crClient: crClient,
|
||||
ctx: ctx,
|
||||
cancelFunc: cancelFunc,
|
||||
logger: logger,
|
||||
@@ -727,6 +732,11 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
|
||||
s.logger.Fatal(err, "unable to create controller", "controller", controller.BackupStorageLocation)
|
||||
}
|
||||
|
||||
pvbInformer, err := s.mgr.GetCache().GetInformer(s.ctx, &velerov1api.PodVolumeBackup{})
|
||||
if err != nil {
|
||||
s.logger.Fatal(err, "fail to get controller-runtime informer from manager for PVB")
|
||||
}
|
||||
|
||||
if _, ok := enabledRuntimeControllers[controller.Backup]; ok {
|
||||
backupper, err := backup.NewKubernetesBackupper(
|
||||
s.mgr.GetClient(),
|
||||
@@ -736,10 +746,8 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
|
||||
podvolume.NewBackupperFactory(
|
||||
s.repoLocker,
|
||||
s.repoEnsurer,
|
||||
s.veleroClient,
|
||||
s.kubeClient.CoreV1(),
|
||||
s.kubeClient.CoreV1(),
|
||||
s.kubeClient.CoreV1(),
|
||||
s.crClient,
|
||||
pvbInformer,
|
||||
s.logger,
|
||||
),
|
||||
s.config.podVolumeOperationTimeout,
|
||||
@@ -818,10 +826,8 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
|
||||
podvolume.NewBackupperFactory(
|
||||
s.repoLocker,
|
||||
s.repoEnsurer,
|
||||
s.veleroClient,
|
||||
s.kubeClient.CoreV1(),
|
||||
s.kubeClient.CoreV1(),
|
||||
s.kubeClient.CoreV1(),
|
||||
s.crClient,
|
||||
pvbInformer,
|
||||
s.logger,
|
||||
),
|
||||
s.config.podVolumeOperationTimeout,
|
||||
@@ -909,6 +915,11 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
|
||||
}
|
||||
}
|
||||
|
||||
pvrInformer, err := s.mgr.GetCache().GetInformer(s.ctx, &velerov1api.PodVolumeRestore{})
|
||||
if err != nil {
|
||||
s.logger.Fatal(err, "fail to get controller-runtime informer from manager for PVR")
|
||||
}
|
||||
|
||||
if _, ok := enabledRuntimeControllers[controller.Restore]; ok {
|
||||
restorer, err := restore.NewKubernetesRestorer(
|
||||
s.discoveryHelper,
|
||||
@@ -918,10 +929,9 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
|
||||
podvolume.NewRestorerFactory(
|
||||
s.repoLocker,
|
||||
s.repoEnsurer,
|
||||
s.veleroClient,
|
||||
s.kubeClient.CoreV1(),
|
||||
s.kubeClient.CoreV1(),
|
||||
s.kubeClient,
|
||||
s.crClient,
|
||||
pvrInformer,
|
||||
s.logger,
|
||||
),
|
||||
s.config.podVolumeOperationTimeout,
|
||||
|
||||
@@ -30,7 +30,6 @@ import (
|
||||
clientgotesting "k8s.io/client-go/testing"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
discoverymocks "github.com/vmware-tanzu/velero/pkg/discovery/mocks"
|
||||
"github.com/vmware-tanzu/velero/pkg/features"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/logging"
|
||||
@@ -548,39 +547,31 @@ func TestHelper_refreshServerPreferredResources(t *testing.T) {
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
isGetResError bool
|
||||
name string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "success get preferred resources",
|
||||
name: "success get preferred resources",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "failed to get preferred resources",
|
||||
isGetResError: true,
|
||||
name: "failed to get preferred resources",
|
||||
expectedErr: errors.New("Failed to discover preferred resources"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fakeClient := discoverymocks.NewServerResourcesInterface(t)
|
||||
|
||||
if tc.isGetResError {
|
||||
fakeClient.On("ServerPreferredResources").Return(nil, errors.New("Failed to discover preferred resources"))
|
||||
} else {
|
||||
fakeClient.On("ServerPreferredResources").Return(apiList, nil)
|
||||
}
|
||||
fakeClient := velerotest.NewFakeServerResourcesInterface(apiList, []*metav1.APIGroup{}, map[schema.GroupVersion]error{}, tc.expectedErr)
|
||||
|
||||
resources, err := refreshServerPreferredResources(fakeClient, logrus.New())
|
||||
|
||||
if tc.isGetResError {
|
||||
if tc.expectedErr != nil {
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, resources)
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, resources)
|
||||
}
|
||||
|
||||
fakeClient.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -612,41 +603,31 @@ func TestHelper_refreshServerGroupsAndResources(t *testing.T) {
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
isGetResError bool
|
||||
name string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "success get service groups and resouorces",
|
||||
},
|
||||
{
|
||||
name: "failed to service groups and resouorces",
|
||||
isGetResError: true,
|
||||
name: "failed to service groups and resouorces",
|
||||
expectedErr: errors.New("Failed to discover service groups and resouorces"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fakeClient := discoverymocks.NewServerResourcesInterface(t)
|
||||
|
||||
if tc.isGetResError {
|
||||
fakeClient.On("ServerGroupsAndResources").Return(nil, nil, errors.New("Failed to discover service groups and resouorces"))
|
||||
} else {
|
||||
fakeClient.On("ServerGroupsAndResources").Return(apiGroup, apiList, nil)
|
||||
}
|
||||
fakeClient := velerotest.NewFakeServerResourcesInterface(apiList, apiGroup, map[schema.GroupVersion]error{}, tc.expectedErr)
|
||||
|
||||
serverGroups, serverResources, err := refreshServerGroupsAndResources(fakeClient, logrus.New())
|
||||
|
||||
if tc.isGetResError {
|
||||
if tc.expectedErr != nil {
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, serverGroups)
|
||||
assert.Nil(t, serverResources)
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, serverGroups)
|
||||
assert.NotNil(t, serverResources)
|
||||
}
|
||||
|
||||
fakeClient.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.30.1. DO NOT EDIT.
|
||||
// Code generated by mockery v2.20.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
@@ -140,12 +140,13 @@ func (_m *Helper) ServerVersion() *version.Info {
|
||||
return r0
|
||||
}
|
||||
|
||||
// NewHelper creates a new instance of Helper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewHelper(t interface {
|
||||
type mockConstructorTestingTNewHelper interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Helper {
|
||||
}
|
||||
|
||||
// NewHelper creates a new instance of Helper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewHelper(t mockConstructorTestingTNewHelper) *Helper {
|
||||
mock := &Helper{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
@@ -153,84 +154,3 @@ func NewHelper(t interface {
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// serverResourcesInterface is an autogenerated mock type for the serverResourcesInterface type
|
||||
type serverResourcesInterface struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ServerGroupsAndResources provides a mock function with given fields:
|
||||
func (_m *serverResourcesInterface) ServerGroupsAndResources() ([]*v1.APIGroup, []*v1.APIResourceList, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*v1.APIGroup
|
||||
var r1 []*v1.APIResourceList
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(0).(func() ([]*v1.APIGroup, []*v1.APIResourceList, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []*v1.APIGroup); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*v1.APIGroup)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() []*v1.APIResourceList); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).([]*v1.APIResourceList)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(2).(func() error); ok {
|
||||
r2 = rf()
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// ServerPreferredResources provides a mock function with given fields:
|
||||
func (_m *serverResourcesInterface) ServerPreferredResources() ([]*v1.APIResourceList, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*v1.APIResourceList
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]*v1.APIResourceList, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []*v1.APIResourceList); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*v1.APIResourceList)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
type mockConstructorTestingTnewServerResourcesInterface interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewServerResourcesInterface creates a new instance of serverResourcesInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewServerResourcesInterface(t mockConstructorTestingTnewServerResourcesInterface) *serverResourcesInterface {
|
||||
mock := &serverResourcesInterface{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
@@ -204,6 +204,7 @@ func (e *csiSnapshotExposer) GetExposed(ctx context.Context, ownerObject corev1.
|
||||
|
||||
backupPodName := ownerObject.Name
|
||||
backupPVCName := ownerObject.Name
|
||||
volumeName := string(ownerObject.UID)
|
||||
|
||||
curLog := e.log.WithFields(logrus.Fields{
|
||||
"owner": ownerObject.Name,
|
||||
@@ -232,7 +233,20 @@ func (e *csiSnapshotExposer) GetExposed(ctx context.Context, ownerObject corev1.
|
||||
|
||||
curLog.WithField("backup pvc", backupPVCName).Info("Backup PVC is bound")
|
||||
|
||||
return &ExposeResult{ByPod: ExposeByPod{HostingPod: pod, VolumeName: pod.Spec.Volumes[0].Name}}, nil
|
||||
i := 0
|
||||
for i = 0; i < len(pod.Spec.Volumes); i++ {
|
||||
if pod.Spec.Volumes[i].Name == volumeName {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if i == len(pod.Spec.Volumes) {
|
||||
return nil, errors.Errorf("backup pod %s doesn't have the expected backup volume", pod.Name)
|
||||
}
|
||||
|
||||
curLog.WithField("pod", pod.Name).Infof("Backup volume is found in pod at index %v", i)
|
||||
|
||||
return &ExposeResult{ByPod: ExposeByPod{HostingPod: pod, VolumeName: volumeName}}, nil
|
||||
}
|
||||
|
||||
func (e *csiSnapshotExposer) CleanUp(ctx context.Context, ownerObject corev1.ObjectReference, vsName string, sourceNamespace string) {
|
||||
|
||||
@@ -37,6 +37,8 @@ import (
|
||||
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
|
||||
clientFake "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
type reactor struct {
|
||||
@@ -442,3 +444,180 @@ func TestExpose(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetExpose(t *testing.T) {
|
||||
backup := &velerov1.Backup{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: velerov1.SchemeGroupVersion.String(),
|
||||
Kind: "Backup",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: velerov1.DefaultNamespace,
|
||||
Name: "fake-backup",
|
||||
UID: "fake-uid",
|
||||
},
|
||||
}
|
||||
|
||||
backupPod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: backup.Namespace,
|
||||
Name: backup.Name,
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: "fake-volume",
|
||||
},
|
||||
{
|
||||
Name: "fake-volume-2",
|
||||
},
|
||||
{
|
||||
Name: string(backup.UID),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
backupPodWithoutVolume := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: backup.Namespace,
|
||||
Name: backup.Name,
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: "fake-volume-1",
|
||||
},
|
||||
{
|
||||
Name: "fake-volume-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
backupPVC := &corev1.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: backup.Namespace,
|
||||
Name: backup.Name,
|
||||
},
|
||||
Spec: corev1.PersistentVolumeClaimSpec{
|
||||
VolumeName: "fake-pv-name",
|
||||
},
|
||||
}
|
||||
|
||||
backupPV := &corev1.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "fake-pv-name",
|
||||
},
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
corev1.AddToScheme(scheme)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
kubeClientObj []runtime.Object
|
||||
ownerBackup *velerov1.Backup
|
||||
exposeWaitParam CSISnapshotExposeWaitParam
|
||||
Timeout time.Duration
|
||||
err string
|
||||
expectedResult *ExposeResult
|
||||
}{
|
||||
{
|
||||
name: "backup pod is not found",
|
||||
ownerBackup: backup,
|
||||
exposeWaitParam: CSISnapshotExposeWaitParam{
|
||||
NodeName: "fake-node",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wait pvc bound fail",
|
||||
ownerBackup: backup,
|
||||
exposeWaitParam: CSISnapshotExposeWaitParam{
|
||||
NodeName: "fake-node",
|
||||
},
|
||||
kubeClientObj: []runtime.Object{
|
||||
backupPod,
|
||||
},
|
||||
Timeout: time.Second,
|
||||
err: "error to wait backup PVC bound, fake-backup: error to wait for rediness of PVC: error to get pvc velero/fake-backup: persistentvolumeclaims \"fake-backup\" not found",
|
||||
},
|
||||
{
|
||||
name: "backup volume not found in pod",
|
||||
ownerBackup: backup,
|
||||
exposeWaitParam: CSISnapshotExposeWaitParam{
|
||||
NodeName: "fake-node",
|
||||
},
|
||||
kubeClientObj: []runtime.Object{
|
||||
backupPodWithoutVolume,
|
||||
backupPVC,
|
||||
backupPV,
|
||||
},
|
||||
Timeout: time.Second,
|
||||
err: "backup pod fake-backup doesn't have the expected backup volume",
|
||||
},
|
||||
{
|
||||
name: "succeed",
|
||||
ownerBackup: backup,
|
||||
exposeWaitParam: CSISnapshotExposeWaitParam{
|
||||
NodeName: "fake-node",
|
||||
},
|
||||
kubeClientObj: []runtime.Object{
|
||||
backupPod,
|
||||
backupPVC,
|
||||
backupPV,
|
||||
},
|
||||
Timeout: time.Second,
|
||||
expectedResult: &ExposeResult{
|
||||
ByPod: ExposeByPod{
|
||||
HostingPod: backupPod,
|
||||
VolumeName: string(backup.UID),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...)
|
||||
|
||||
fakeClientBuilder := clientFake.NewClientBuilder()
|
||||
fakeClientBuilder = fakeClientBuilder.WithScheme(scheme)
|
||||
|
||||
fakeClient := fakeClientBuilder.WithRuntimeObjects(test.kubeClientObj...).Build()
|
||||
|
||||
exposer := csiSnapshotExposer{
|
||||
kubeClient: fakeKubeClient,
|
||||
log: velerotest.NewLogger(),
|
||||
}
|
||||
|
||||
var ownerObject corev1.ObjectReference
|
||||
if test.ownerBackup != nil {
|
||||
ownerObject = corev1.ObjectReference{
|
||||
Kind: test.ownerBackup.Kind,
|
||||
Namespace: test.ownerBackup.Namespace,
|
||||
Name: test.ownerBackup.Name,
|
||||
UID: test.ownerBackup.UID,
|
||||
APIVersion: test.ownerBackup.APIVersion,
|
||||
}
|
||||
}
|
||||
|
||||
test.exposeWaitParam.NodeClient = fakeClient
|
||||
|
||||
result, err := exposer.GetExposed(context.Background(), ownerObject, test.Timeout, &test.exposeWaitParam)
|
||||
if test.err == "" {
|
||||
assert.NoError(t, err)
|
||||
|
||||
if test.expectedResult == nil {
|
||||
assert.Nil(t, result)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.expectedResult.ByPod.VolumeName, result.ByPod.VolumeName)
|
||||
assert.Equal(t, test.expectedResult.ByPod.HostingPod.Name, result.ByPod.HostingPod.Name)
|
||||
}
|
||||
} else {
|
||||
assert.EqualError(t, err, test.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,13 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -76,12 +76,18 @@ func IsRunning(ctx context.Context, kubeClient kubernetes.Interface, namespace s
|
||||
}
|
||||
|
||||
// IsRunningInNode checks if the node agent pod is running properly in a specified node. If not, return the error found
|
||||
func IsRunningInNode(ctx context.Context, namespace string, nodeName string, podClient corev1client.PodsGetter) error {
|
||||
func IsRunningInNode(ctx context.Context, namespace string, nodeName string, crClient ctrlclient.Client) error {
|
||||
if nodeName == "" {
|
||||
return errors.New("node name is empty")
|
||||
}
|
||||
|
||||
pods, err := podClient.Pods(namespace).List(ctx, metav1.ListOptions{LabelSelector: fmt.Sprintf("name=%s", daemonSet)})
|
||||
pods := new(v1.PodList)
|
||||
parsedSelector, err := labels.Parse(fmt.Sprintf("name=%s", daemonSet))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "fail to parse selector")
|
||||
}
|
||||
|
||||
err = crClient.List(ctx, pods, &ctrlclient.ListOptions{LabelSelector: parsedSelector})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to list daemonset pods")
|
||||
}
|
||||
@@ -108,8 +114,8 @@ func GetPodSpec(ctx context.Context, kubeClient kubernetes.Interface, namespace
|
||||
return &ds.Spec.Template.Spec, nil
|
||||
}
|
||||
|
||||
func GetConfigs(ctx context.Context, namespace string, cmClient corev1client.ConfigMapsGetter) (*Configs, error) {
|
||||
cm, err := cmClient.ConfigMaps(namespace).Get(ctx, configName, metav1.GetOptions{})
|
||||
func GetConfigs(ctx context.Context, namespace string, kubeClient kubernetes.Interface) (*Configs, error) {
|
||||
cm, err := kubeClient.CoreV1().ConfigMaps(namespace).Get(ctx, configName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil, nil
|
||||
|
||||
@@ -27,13 +27,14 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
kerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/credentials"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/scheme"
|
||||
"github.com/vmware-tanzu/velero/pkg/itemoperation"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
"github.com/vmware-tanzu/velero/pkg/util"
|
||||
"github.com/vmware-tanzu/velero/pkg/volume"
|
||||
)
|
||||
|
||||
@@ -302,7 +303,9 @@ func (s *objectBackupStore) GetBackupMetadata(name string) (*velerov1api.Backup,
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
decoder := scheme.Codecs.UniversalDecoder(velerov1api.SchemeGroupVersion)
|
||||
codecFactory := serializer.NewCodecFactory(util.VeleroScheme)
|
||||
|
||||
decoder := codecFactory.UniversalDecoder(velerov1api.SchemeGroupVersion)
|
||||
obj, _, err := decoder.Decode(data, nil, nil)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
|
||||
+22
-35
@@ -26,13 +26,13 @@ import (
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
veleroclient "github.com/vmware-tanzu/velero/pkg/client"
|
||||
clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
|
||||
"github.com/vmware-tanzu/velero/pkg/label"
|
||||
"github.com/vmware-tanzu/velero/pkg/nodeagent"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
@@ -50,10 +50,7 @@ type backupper struct {
|
||||
ctx context.Context
|
||||
repoLocker *repository.RepoLocker
|
||||
repoEnsurer *repository.Ensurer
|
||||
veleroClient clientset.Interface
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter
|
||||
pvClient corev1client.PersistentVolumesGetter
|
||||
podClient corev1client.PodsGetter
|
||||
crClient ctrlclient.Client
|
||||
uploaderType string
|
||||
|
||||
results map[string]chan *velerov1api.PodVolumeBackup
|
||||
@@ -103,32 +100,31 @@ func newBackupper(
|
||||
ctx context.Context,
|
||||
repoLocker *repository.RepoLocker,
|
||||
repoEnsurer *repository.Ensurer,
|
||||
podVolumeBackupInformer cache.SharedIndexInformer,
|
||||
veleroClient clientset.Interface,
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter,
|
||||
pvClient corev1client.PersistentVolumesGetter,
|
||||
podClient corev1client.PodsGetter,
|
||||
pvbInformer ctrlcache.Informer,
|
||||
crClient ctrlclient.Client,
|
||||
uploaderType string,
|
||||
backup *velerov1api.Backup,
|
||||
log logrus.FieldLogger,
|
||||
) *backupper {
|
||||
b := &backupper{
|
||||
ctx: ctx,
|
||||
repoLocker: repoLocker,
|
||||
repoEnsurer: repoEnsurer,
|
||||
veleroClient: veleroClient,
|
||||
pvcClient: pvcClient,
|
||||
pvClient: pvClient,
|
||||
podClient: podClient,
|
||||
crClient: crClient,
|
||||
uploaderType: uploaderType,
|
||||
|
||||
results: make(map[string]chan *velerov1api.PodVolumeBackup),
|
||||
}
|
||||
|
||||
podVolumeBackupInformer.AddEventHandler(
|
||||
pvbInformer.AddEventHandler(
|
||||
cache.ResourceEventHandlerFuncs{
|
||||
UpdateFunc: func(_, obj interface{}) {
|
||||
pvb := obj.(*velerov1api.PodVolumeBackup)
|
||||
|
||||
if pvb.GetLabels()[velerov1api.BackupUIDLabel] != string(backup.UID) {
|
||||
return
|
||||
}
|
||||
|
||||
if pvb.Status.Phase == velerov1api.PodVolumeBackupPhaseCompleted || pvb.Status.Phase == velerov1api.PodVolumeBackupPhaseFailed {
|
||||
b.resultsLock.Lock()
|
||||
defer b.resultsLock.Unlock()
|
||||
@@ -153,7 +149,8 @@ func resultsKey(ns, name string) string {
|
||||
|
||||
func (b *backupper) getMatchAction(resPolicies *resourcepolicies.Policies, pvc *corev1api.PersistentVolumeClaim, volume *corev1api.Volume) (*resourcepolicies.Action, error) {
|
||||
if pvc != nil {
|
||||
pv, err := b.pvClient.PersistentVolumes().Get(context.TODO(), pvc.Spec.VolumeName, metav1.GetOptions{})
|
||||
pv := new(corev1api.PersistentVolume)
|
||||
err := b.crClient.Get(context.TODO(), ctrlclient.ObjectKey{Name: pvc.Spec.VolumeName}, pv)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error getting pv for pvc %s", pvc.Spec.VolumeName)
|
||||
}
|
||||
@@ -173,7 +170,7 @@ func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.
|
||||
}
|
||||
log.Infof("pod %s/%s has volumes to backup: %v", pod.Namespace, pod.Name, volumesToBackup)
|
||||
|
||||
err := nodeagent.IsRunningInNode(b.ctx, backup.Namespace, pod.Spec.NodeName, b.podClient)
|
||||
err := nodeagent.IsRunningInNode(b.ctx, backup.Namespace, pod.Spec.NodeName, b.crClient)
|
||||
if err != nil {
|
||||
return nil, nil, []error{err}
|
||||
}
|
||||
@@ -213,7 +210,8 @@ func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.
|
||||
for _, podVolume := range pod.Spec.Volumes {
|
||||
podVolumes[podVolume.Name] = podVolume
|
||||
if podVolume.PersistentVolumeClaim != nil {
|
||||
pvc, err := b.pvcClient.PersistentVolumeClaims(pod.Namespace).Get(context.TODO(), podVolume.PersistentVolumeClaim.ClaimName, metav1.GetOptions{})
|
||||
pvc := new(corev1api.PersistentVolumeClaim)
|
||||
err := b.crClient.Get(context.TODO(), ctrlclient.ObjectKey{Namespace: pod.Namespace, Name: podVolume.PersistentVolumeClaim.ClaimName}, pvc)
|
||||
if err != nil {
|
||||
errs = append(errs, errors.Wrap(err, "error getting persistent volume claim for volume"))
|
||||
continue
|
||||
@@ -263,7 +261,7 @@ func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.
|
||||
|
||||
// hostPath volumes are not supported because they're not mounted into /var/lib/kubelet/pods, so our
|
||||
// daemonset pod has no way to access their data.
|
||||
isHostPath, err := isHostPathVolume(&volume, pvc, b.pvClient.PersistentVolumes())
|
||||
isHostPath, err := isHostPathVolume(&volume, pvc, b.crClient)
|
||||
if err != nil {
|
||||
errs = append(errs, errors.Wrap(err, "error checking if volume is a hostPath volume"))
|
||||
continue
|
||||
@@ -303,11 +301,7 @@ func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api.
|
||||
}
|
||||
|
||||
volumeBackup := newPodVolumeBackup(backup, pod, volume, repoIdentifier, b.uploaderType, pvc)
|
||||
// TODO: once backupper is refactored to use controller-runtime, just pass client instead of anonymous func
|
||||
if err := veleroclient.CreateRetryGenerateNameWithFunc(volumeBackup, func() error {
|
||||
_, err := b.veleroClient.VeleroV1().PodVolumeBackups(volumeBackup.Namespace).Create(context.TODO(), volumeBackup, metav1.CreateOptions{})
|
||||
return err
|
||||
}); err != nil {
|
||||
if err := veleroclient.CreateRetryGenerateName(b.crClient, b.ctx, volumeBackup); err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
@@ -339,13 +333,9 @@ ForEachVolume:
|
||||
return podVolumeBackups, pvcSummary, errs
|
||||
}
|
||||
|
||||
type pvGetter interface {
|
||||
Get(ctx context.Context, name string, opts metav1.GetOptions) (*corev1api.PersistentVolume, error)
|
||||
}
|
||||
|
||||
// isHostPathVolume returns true if the volume is either a hostPath pod volume or a persistent
|
||||
// volume claim on a hostPath persistent volume, or false otherwise.
|
||||
func isHostPathVolume(volume *corev1api.Volume, pvc *corev1api.PersistentVolumeClaim, pvGetter pvGetter) (bool, error) {
|
||||
func isHostPathVolume(volume *corev1api.Volume, pvc *corev1api.PersistentVolumeClaim, crClient ctrlclient.Client) (bool, error) {
|
||||
if volume.HostPath != nil {
|
||||
return true, nil
|
||||
}
|
||||
@@ -354,7 +344,8 @@ func isHostPathVolume(volume *corev1api.Volume, pvc *corev1api.PersistentVolumeC
|
||||
return false, nil
|
||||
}
|
||||
|
||||
pv, err := pvGetter.Get(context.TODO(), pvc.Spec.VolumeName, metav1.GetOptions{})
|
||||
pv := new(corev1api.PersistentVolume)
|
||||
err := crClient.Get(context.TODO(), ctrlclient.ObjectKey{Name: pvc.Spec.VolumeName}, pv)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
@@ -421,7 +412,3 @@ func newPodVolumeBackup(backup *velerov1api.Backup, pod *corev1api.Pod, volume c
|
||||
|
||||
return pvb
|
||||
}
|
||||
|
||||
func errorOnly(_ interface{}, err error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -18,17 +18,14 @@ package podvolume
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
|
||||
velerov1informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
)
|
||||
|
||||
@@ -41,48 +38,31 @@ type BackupperFactory interface {
|
||||
func NewBackupperFactory(
|
||||
repoLocker *repository.RepoLocker,
|
||||
repoEnsurer *repository.Ensurer,
|
||||
veleroClient clientset.Interface,
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter,
|
||||
pvClient corev1client.PersistentVolumesGetter,
|
||||
podClient corev1client.PodsGetter,
|
||||
crClient ctrlclient.Client,
|
||||
pvbInformer ctrlcache.Informer,
|
||||
log logrus.FieldLogger,
|
||||
) BackupperFactory {
|
||||
return &backupperFactory{
|
||||
repoLocker: repoLocker,
|
||||
repoEnsurer: repoEnsurer,
|
||||
veleroClient: veleroClient,
|
||||
pvcClient: pvcClient,
|
||||
pvClient: pvClient,
|
||||
podClient: podClient,
|
||||
log: log,
|
||||
repoLocker: repoLocker,
|
||||
repoEnsurer: repoEnsurer,
|
||||
crClient: crClient,
|
||||
pvbInformer: pvbInformer,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
type backupperFactory struct {
|
||||
repoLocker *repository.RepoLocker
|
||||
repoEnsurer *repository.Ensurer
|
||||
veleroClient clientset.Interface
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter
|
||||
pvClient corev1client.PersistentVolumesGetter
|
||||
podClient corev1client.PodsGetter
|
||||
log logrus.FieldLogger
|
||||
repoLocker *repository.RepoLocker
|
||||
repoEnsurer *repository.Ensurer
|
||||
crClient ctrlclient.Client
|
||||
pvbInformer ctrlcache.Informer
|
||||
log logrus.FieldLogger
|
||||
}
|
||||
|
||||
func (bf *backupperFactory) NewBackupper(ctx context.Context, backup *velerov1api.Backup, uploaderType string) (Backupper, error) {
|
||||
informer := velerov1informers.NewFilteredPodVolumeBackupInformer(
|
||||
bf.veleroClient,
|
||||
backup.Namespace,
|
||||
0,
|
||||
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
|
||||
func(opts *metav1.ListOptions) {
|
||||
opts.LabelSelector = fmt.Sprintf("%s=%s", velerov1api.BackupUIDLabel, backup.UID)
|
||||
},
|
||||
)
|
||||
b := newBackupper(ctx, bf.repoLocker, bf.repoEnsurer, bf.pvbInformer, bf.crClient, uploaderType, backup, bf.log)
|
||||
|
||||
b := newBackupper(ctx, bf.repoLocker, bf.repoEnsurer, informer, bf.veleroClient, bf.pvcClient, bf.pvClient, bf.podClient, uploaderType, bf.log)
|
||||
|
||||
go informer.Run(ctx.Done())
|
||||
if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced) {
|
||||
if !cache.WaitForCacheSync(ctx.Done(), bf.pvbInformer.HasSynced) {
|
||||
return nil, errors.New("timed out waiting for caches to sync")
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -32,17 +31,15 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrlfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
||||
"k8s.io/client-go/kubernetes"
|
||||
kubefake "k8s.io/client-go/kubernetes/fake"
|
||||
clientTesting "k8s.io/client-go/testing"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
"github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
|
||||
velerofake "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/fake"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
)
|
||||
|
||||
func TestIsHostPathVolume(t *testing.T) {
|
||||
@@ -101,15 +98,14 @@ func TestIsHostPathVolume(t *testing.T) {
|
||||
VolumeName: "pv-1",
|
||||
},
|
||||
}
|
||||
pvGetter := &fakePVGetter{
|
||||
pv: &corev1api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pv-1",
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeSpec{},
|
||||
pv := &corev1api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pv-1",
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeSpec{},
|
||||
}
|
||||
isHostPath, err = isHostPathVolume(vol, pvc, pvGetter)
|
||||
crClient1 := velerotest.NewFakeControllerRuntimeClient(t, pv)
|
||||
isHostPath, err = isHostPathVolume(vol, pvc, crClient1)
|
||||
assert.Nil(t, err)
|
||||
assert.False(t, isHostPath)
|
||||
|
||||
@@ -130,35 +126,23 @@ func TestIsHostPathVolume(t *testing.T) {
|
||||
VolumeName: "pv-1",
|
||||
},
|
||||
}
|
||||
pvGetter = &fakePVGetter{
|
||||
pv: &corev1api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pv-1",
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeSpec{
|
||||
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
||||
HostPath: &corev1api.HostPathVolumeSource{},
|
||||
},
|
||||
pv = &corev1api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pv-1",
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeSpec{
|
||||
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
||||
HostPath: &corev1api.HostPathVolumeSource{},
|
||||
},
|
||||
},
|
||||
}
|
||||
isHostPath, err = isHostPathVolume(vol, pvc, pvGetter)
|
||||
crClient2 := velerotest.NewFakeControllerRuntimeClient(t, pv)
|
||||
|
||||
isHostPath, err = isHostPathVolume(vol, pvc, crClient2)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, isHostPath)
|
||||
}
|
||||
|
||||
type fakePVGetter struct {
|
||||
pv *corev1api.PersistentVolume
|
||||
}
|
||||
|
||||
func (g *fakePVGetter) Get(ctx context.Context, name string, opts metav1.GetOptions) (*corev1api.PersistentVolume, error) {
|
||||
if g.pv != nil {
|
||||
return g.pv, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("item not found")
|
||||
}
|
||||
|
||||
func Test_backupper_BackupPodVolumes_log_test(t *testing.T) {
|
||||
type args struct {
|
||||
backup *velerov1api.Backup
|
||||
@@ -322,6 +306,7 @@ func createPVBObj(fail bool, withSnapshot bool, index int, uploaderType string)
|
||||
func TestBackupPodVolumes(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
velerov1api.AddToScheme(scheme)
|
||||
corev1api.AddToScheme(scheme)
|
||||
|
||||
ctxWithCancel, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -509,40 +494,6 @@ func TestBackupPodVolumes(t *testing.T) {
|
||||
uploaderType: "kopia",
|
||||
bsl: "fake-bsl",
|
||||
},
|
||||
{
|
||||
name: "create PVB fail",
|
||||
volumes: []string{
|
||||
"fake-volume-1",
|
||||
"fake-volume-2",
|
||||
},
|
||||
sourcePod: createPodObj(true, true, true, 2),
|
||||
kubeClientObj: []runtime.Object{
|
||||
createNodeAgentPodObj(true),
|
||||
createPVCObj(1),
|
||||
createPVCObj(2),
|
||||
createPVObj(1, false),
|
||||
createPVObj(2, false),
|
||||
},
|
||||
ctlClientObj: []runtime.Object{
|
||||
createBackupRepoObj(),
|
||||
},
|
||||
veleroReactors: []reactor{
|
||||
{
|
||||
verb: "create",
|
||||
resource: "podvolumebackups",
|
||||
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, nil, errors.New("fake-create-error")
|
||||
},
|
||||
},
|
||||
},
|
||||
runtimeScheme: scheme,
|
||||
uploaderType: "kopia",
|
||||
bsl: "fake-bsl",
|
||||
errs: []string{
|
||||
"fake-create-error",
|
||||
"fake-create-error",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "context cancelled",
|
||||
ctx: ctxWithCancel,
|
||||
@@ -630,23 +581,28 @@ func TestBackupPodVolumes(t *testing.T) {
|
||||
fakeClientBuilder = fakeClientBuilder.WithScheme(test.runtimeScheme)
|
||||
}
|
||||
|
||||
fakeCtlClient := fakeClientBuilder.WithRuntimeObjects(test.ctlClientObj...).Build()
|
||||
objList := append(test.ctlClientObj, test.veleroClientObj...)
|
||||
objList = append(objList, test.kubeClientObj...)
|
||||
fakeCtrlClient := fakeClientBuilder.WithRuntimeObjects(objList...).Build()
|
||||
|
||||
fakeKubeClient := kubefake.NewSimpleClientset(test.kubeClientObj...)
|
||||
var kubeClient kubernetes.Interface = fakeKubeClient
|
||||
|
||||
fakeVeleroClient := velerofake.NewSimpleClientset(test.veleroClientObj...)
|
||||
for _, reactor := range test.veleroReactors {
|
||||
fakeVeleroClient.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorFunc)
|
||||
fakeCRWatchClient := velerotest.NewFakeControllerRuntimeWatchClient(t, test.kubeClientObj...)
|
||||
lw := kube.InternalLW{
|
||||
Client: fakeCRWatchClient,
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
ObjectList: new(velerov1api.PodVolumeBackupList),
|
||||
}
|
||||
var veleroClient versioned.Interface = fakeVeleroClient
|
||||
|
||||
ensurer := repository.NewEnsurer(fakeCtlClient, velerotest.NewLogger(), time.Millisecond)
|
||||
pvbInformer := cache.NewSharedIndexInformer(&lw, &velerov1api.PodVolumeBackup{}, 0, cache.Indexers{})
|
||||
|
||||
go pvbInformer.Run(ctx.Done())
|
||||
require.True(t, cache.WaitForCacheSync(ctx.Done(), pvbInformer.HasSynced))
|
||||
|
||||
ensurer := repository.NewEnsurer(fakeCtrlClient, velerotest.NewLogger(), time.Millisecond)
|
||||
|
||||
backupObj := builder.ForBackup(velerov1api.DefaultNamespace, "fake-backup").Result()
|
||||
backupObj.Spec.StorageLocation = test.bsl
|
||||
|
||||
factory := NewBackupperFactory(repository.NewRepoLocker(), ensurer, veleroClient, kubeClient.CoreV1(), kubeClient.CoreV1(), kubeClient.CoreV1(), velerotest.NewLogger())
|
||||
factory := NewBackupperFactory(repository.NewRepoLocker(), ensurer, fakeCtrlClient, pvbInformer, velerotest.NewLogger())
|
||||
bp, err := factory.NewBackupper(ctx, backupObj, test.uploaderType)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
+23
-27
@@ -27,12 +27,12 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
veleroclient "github.com/vmware-tanzu/velero/pkg/client"
|
||||
clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
|
||||
"github.com/vmware-tanzu/velero/pkg/label"
|
||||
"github.com/vmware-tanzu/velero/pkg/nodeagent"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
@@ -54,13 +54,11 @@ type Restorer interface {
|
||||
}
|
||||
|
||||
type restorer struct {
|
||||
ctx context.Context
|
||||
repoLocker *repository.RepoLocker
|
||||
repoEnsurer *repository.Ensurer
|
||||
veleroClient clientset.Interface
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter
|
||||
podClient corev1client.PodsGetter
|
||||
kubeClient kubernetes.Interface
|
||||
ctx context.Context
|
||||
repoLocker *repository.RepoLocker
|
||||
repoEnsurer *repository.Ensurer
|
||||
kubeClient kubernetes.Interface
|
||||
crClient ctrlclient.Client
|
||||
|
||||
resultsLock sync.Mutex
|
||||
results map[string]chan *velerov1api.PodVolumeRestore
|
||||
@@ -72,30 +70,30 @@ func newRestorer(
|
||||
ctx context.Context,
|
||||
repoLocker *repository.RepoLocker,
|
||||
repoEnsurer *repository.Ensurer,
|
||||
podVolumeRestoreInformer cache.SharedIndexInformer,
|
||||
veleroClient clientset.Interface,
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter,
|
||||
podClient corev1client.PodsGetter,
|
||||
pvrInformer ctrlcache.Informer,
|
||||
kubeClient kubernetes.Interface,
|
||||
crClient ctrlclient.Client,
|
||||
restore *velerov1api.Restore,
|
||||
log logrus.FieldLogger,
|
||||
) *restorer {
|
||||
r := &restorer{
|
||||
ctx: ctx,
|
||||
repoLocker: repoLocker,
|
||||
repoEnsurer: repoEnsurer,
|
||||
veleroClient: veleroClient,
|
||||
pvcClient: pvcClient,
|
||||
podClient: podClient,
|
||||
kubeClient: kubeClient,
|
||||
ctx: ctx,
|
||||
repoLocker: repoLocker,
|
||||
repoEnsurer: repoEnsurer,
|
||||
kubeClient: kubeClient,
|
||||
crClient: crClient,
|
||||
|
||||
results: make(map[string]chan *velerov1api.PodVolumeRestore),
|
||||
log: log,
|
||||
}
|
||||
|
||||
podVolumeRestoreInformer.AddEventHandler(
|
||||
pvrInformer.AddEventHandler(
|
||||
cache.ResourceEventHandlerFuncs{
|
||||
UpdateFunc: func(_, obj interface{}) {
|
||||
pvr := obj.(*velerov1api.PodVolumeRestore)
|
||||
if pvr.GetLabels()[velerov1api.RestoreUIDLabel] != string(restore.UID) {
|
||||
return
|
||||
}
|
||||
|
||||
if pvr.Status.Phase == velerov1api.PodVolumeRestorePhaseCompleted || pvr.Status.Phase == velerov1api.PodVolumeRestorePhaseFailed {
|
||||
r.resultsLock.Lock()
|
||||
@@ -169,7 +167,8 @@ func (r *restorer) RestorePodVolumes(data RestoreData) []error {
|
||||
var pvc *corev1api.PersistentVolumeClaim
|
||||
if ok {
|
||||
if volumeObj.PersistentVolumeClaim != nil {
|
||||
pvc, err = r.pvcClient.PersistentVolumeClaims(data.Pod.Namespace).Get(context.TODO(), volumeObj.PersistentVolumeClaim.ClaimName, metav1.GetOptions{})
|
||||
pvc := new(corev1api.PersistentVolumeClaim)
|
||||
err := r.crClient.Get(context.TODO(), ctrlclient.ObjectKey{Namespace: data.Pod.Namespace, Name: volumeObj.PersistentVolumeClaim.ClaimName}, pvc)
|
||||
if err != nil {
|
||||
errs = append(errs, errors.Wrap(err, "error getting persistent volume claim for volume"))
|
||||
continue
|
||||
@@ -179,10 +178,7 @@ func (r *restorer) RestorePodVolumes(data RestoreData) []error {
|
||||
|
||||
volumeRestore := newPodVolumeRestore(data.Restore, data.Pod, data.BackupLocation, volume, backupInfo.snapshotID, repoIdentifier, backupInfo.uploaderType, data.SourceNamespace, pvc)
|
||||
|
||||
// TODO: once restorer is refactored to use controller-runtime, just pass client instead of anonymous func
|
||||
if err := veleroclient.CreateRetryGenerateNameWithFunc(volumeRestore, func() error {
|
||||
return errorOnly(r.veleroClient.VeleroV1().PodVolumeRestores(volumeRestore.Namespace).Create(context.TODO(), volumeRestore, metav1.CreateOptions{}))
|
||||
}); err != nil {
|
||||
if err := veleroclient.CreateRetryGenerateName(r.crClient, r.ctx, volumeRestore); err != nil {
|
||||
errs = append(errs, errors.WithStack(err))
|
||||
continue
|
||||
}
|
||||
@@ -214,7 +210,7 @@ func (r *restorer) RestorePodVolumes(data RestoreData) []error {
|
||||
} else if err != nil {
|
||||
r.log.WithError(err).Error("Failed to check node-agent pod status, disengage")
|
||||
} else {
|
||||
err = nodeagent.IsRunningInNode(checkCtx, data.Restore.Namespace, nodeName, r.podClient)
|
||||
err = nodeagent.IsRunningInNode(checkCtx, data.Restore.Namespace, nodeName, r.crClient)
|
||||
if err != nil {
|
||||
r.log.WithField("node", nodeName).WithError(err).Error("node-agent pod is not running in node, abort the restore")
|
||||
r.nodeAgentCheck <- errors.Wrapf(err, "node-agent pod is not running in node %s", nodeName)
|
||||
|
||||
@@ -18,18 +18,15 @@ package podvolume
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
|
||||
velerov1informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
)
|
||||
|
||||
@@ -41,47 +38,33 @@ type RestorerFactory interface {
|
||||
|
||||
func NewRestorerFactory(repoLocker *repository.RepoLocker,
|
||||
repoEnsurer *repository.Ensurer,
|
||||
veleroClient clientset.Interface,
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter,
|
||||
podClient corev1client.PodsGetter,
|
||||
kubeClient kubernetes.Interface,
|
||||
crClient ctrlclient.Client,
|
||||
pvrInformer ctrlcache.Informer,
|
||||
log logrus.FieldLogger) RestorerFactory {
|
||||
return &restorerFactory{
|
||||
repoLocker: repoLocker,
|
||||
repoEnsurer: repoEnsurer,
|
||||
veleroClient: veleroClient,
|
||||
pvcClient: pvcClient,
|
||||
podClient: podClient,
|
||||
kubeClient: kubeClient,
|
||||
log: log,
|
||||
repoLocker: repoLocker,
|
||||
repoEnsurer: repoEnsurer,
|
||||
kubeClient: kubeClient,
|
||||
crClient: crClient,
|
||||
pvrInformer: pvrInformer,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
type restorerFactory struct {
|
||||
repoLocker *repository.RepoLocker
|
||||
repoEnsurer *repository.Ensurer
|
||||
veleroClient clientset.Interface
|
||||
pvcClient corev1client.PersistentVolumeClaimsGetter
|
||||
podClient corev1client.PodsGetter
|
||||
kubeClient kubernetes.Interface
|
||||
log logrus.FieldLogger
|
||||
repoLocker *repository.RepoLocker
|
||||
repoEnsurer *repository.Ensurer
|
||||
kubeClient kubernetes.Interface
|
||||
crClient ctrlclient.Client
|
||||
pvrInformer ctrlcache.Informer
|
||||
log logrus.FieldLogger
|
||||
}
|
||||
|
||||
func (rf *restorerFactory) NewRestorer(ctx context.Context, restore *velerov1api.Restore) (Restorer, error) {
|
||||
informer := velerov1informers.NewFilteredPodVolumeRestoreInformer(
|
||||
rf.veleroClient,
|
||||
restore.Namespace,
|
||||
0,
|
||||
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
|
||||
func(opts *metav1.ListOptions) {
|
||||
opts.LabelSelector = fmt.Sprintf("%s=%s", velerov1api.RestoreUIDLabel, restore.UID)
|
||||
},
|
||||
)
|
||||
r := newRestorer(ctx, rf.repoLocker, rf.repoEnsurer, rf.pvrInformer, rf.kubeClient, rf.crClient, restore, rf.log)
|
||||
|
||||
r := newRestorer(ctx, rf.repoLocker, rf.repoEnsurer, informer, rf.veleroClient, rf.pvcClient, rf.podClient, rf.kubeClient, rf.log)
|
||||
|
||||
go informer.Run(ctx.Done())
|
||||
if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced) {
|
||||
if !cache.WaitForCacheSync(ctx.Done(), rf.pvrInformer.HasSynced) {
|
||||
return nil, errors.New("timed out waiting for cache to sync")
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
appv1 "k8s.io/api/apps/v1"
|
||||
@@ -31,15 +30,14 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
kubefake "k8s.io/client-go/kubernetes/fake"
|
||||
clientTesting "k8s.io/client-go/testing"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
ctrlfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
"github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
|
||||
velerofake "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/fake"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
)
|
||||
|
||||
func TestGetVolumesRepositoryType(t *testing.T) {
|
||||
@@ -162,6 +160,7 @@ type expectError struct {
|
||||
func TestRestorePodVolumes(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
velerov1api.AddToScheme(scheme)
|
||||
corev1api.AddToScheme(scheme)
|
||||
|
||||
ctxWithCancel, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -264,42 +263,6 @@ func TestRestorePodVolumes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create pvb fail",
|
||||
pvbs: []*velerov1api.PodVolumeBackup{
|
||||
createPVBObj(true, true, 1, "kopia"),
|
||||
createPVBObj(true, true, 2, "kopia"),
|
||||
},
|
||||
kubeClientObj: []runtime.Object{
|
||||
createNodeAgentDaemonset(),
|
||||
createPVCObj(1),
|
||||
createPVCObj(2),
|
||||
},
|
||||
ctlClientObj: []runtime.Object{
|
||||
createBackupRepoObj(),
|
||||
},
|
||||
veleroReactors: []reactor{
|
||||
{
|
||||
verb: "create",
|
||||
resource: "podvolumerestores",
|
||||
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, nil, errors.New("fake-create-error")
|
||||
},
|
||||
},
|
||||
},
|
||||
restoredPod: createPodObj(true, true, true, 2),
|
||||
sourceNamespace: "fake-ns",
|
||||
bsl: "fake-bsl",
|
||||
runtimeScheme: scheme,
|
||||
errs: []expectError{
|
||||
{
|
||||
err: "fake-create-error",
|
||||
},
|
||||
{
|
||||
err: "fake-create-error",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create pvb fail",
|
||||
ctx: ctxWithCancel,
|
||||
@@ -407,22 +370,32 @@ func TestRestorePodVolumes(t *testing.T) {
|
||||
fakeClientBuilder = fakeClientBuilder.WithScheme(test.runtimeScheme)
|
||||
}
|
||||
|
||||
fakeCtlClient := fakeClientBuilder.WithRuntimeObjects(test.ctlClientObj...).Build()
|
||||
objClient := append(test.ctlClientObj, test.kubeClientObj...)
|
||||
objClient = append(objClient, test.veleroClientObj...)
|
||||
|
||||
fakeCRClient := velerotest.NewFakeControllerRuntimeClient(t, objClient...)
|
||||
|
||||
fakeKubeClient := kubefake.NewSimpleClientset(test.kubeClientObj...)
|
||||
var kubeClient kubernetes.Interface = fakeKubeClient
|
||||
|
||||
fakeVeleroClient := velerofake.NewSimpleClientset(test.veleroClientObj...)
|
||||
for _, reactor := range test.veleroReactors {
|
||||
fakeVeleroClient.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorFunc)
|
||||
fakeCRWatchClient := velerotest.NewFakeControllerRuntimeWatchClient(t, test.kubeClientObj...)
|
||||
lw := kube.InternalLW{
|
||||
Client: fakeCRWatchClient,
|
||||
Namespace: velerov1api.DefaultNamespace,
|
||||
ObjectList: new(velerov1api.PodVolumeRestoreList),
|
||||
}
|
||||
var veleroClient versioned.Interface = fakeVeleroClient
|
||||
|
||||
ensurer := repository.NewEnsurer(fakeCtlClient, velerotest.NewLogger(), time.Millisecond)
|
||||
pvrInformer := cache.NewSharedIndexInformer(&lw, &velerov1api.PodVolumeBackup{}, 0, cache.Indexers{})
|
||||
|
||||
go pvrInformer.Run(ctx.Done())
|
||||
require.True(t, cache.WaitForCacheSync(ctx.Done(), pvrInformer.HasSynced))
|
||||
|
||||
ensurer := repository.NewEnsurer(fakeCRClient, velerotest.NewLogger(), time.Millisecond)
|
||||
|
||||
restoreObj := builder.ForRestore(velerov1api.DefaultNamespace, "fake-restore").Result()
|
||||
|
||||
factory := NewRestorerFactory(repository.NewRepoLocker(), ensurer, veleroClient, kubeClient.CoreV1(), kubeClient.CoreV1(), kubeClient, velerotest.NewLogger())
|
||||
factory := NewRestorerFactory(repository.NewRepoLocker(), ensurer, kubeClient,
|
||||
fakeCRClient, pvrInformer, velerotest.NewLogger())
|
||||
rs, err := factory.NewRestorer(ctx, restoreObj)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -62,7 +62,7 @@ func GetS3ResticEnvVars(config map[string]string) (map[string]string, error) {
|
||||
result[awsSecretKeyEnvVar] = creds.SecretAccessKey
|
||||
result[awsSessTokenEnvVar] = creds.SessionToken
|
||||
result[awsCredentialsFileEnvVar] = ""
|
||||
result[awsProfileEnvVar] = ""
|
||||
result[awsProfileEnvVar] = "" // profile is not needed since we have the credentials from profile via GetS3Credentials
|
||||
result[awsConfigFileEnvVar] = ""
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ func GetS3Credentials(config map[string]string) (*aws.Credentials, error) {
|
||||
// as credentials of a BSL
|
||||
awsconfig.WithSharedConfigFiles([]string{credentialsFile}))
|
||||
}
|
||||
opts = append(opts, awsconfig.WithSharedConfigProfile(config[awsProfileKey]))
|
||||
|
||||
cfg, err := awsconfig.LoadDefaultConfig(context.Background(), opts...)
|
||||
if err != nil {
|
||||
|
||||
@@ -17,8 +17,11 @@ limitations under the License.
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -63,3 +66,81 @@ func TestGetS3ResticEnvVars(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetS3CredentialsCorrectlyUseProfile(t *testing.T) {
|
||||
type args struct {
|
||||
config map[string]string
|
||||
secretFileContents string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *aws.Credentials
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Test GetS3Credentials use profile correctly",
|
||||
args: args{
|
||||
config: map[string]string{
|
||||
"profile": "some-profile",
|
||||
},
|
||||
secretFileContents: `[default]
|
||||
aws_access_key_id = default-access-key-id
|
||||
aws_secret_access_key = default-secret-access-key
|
||||
[profile some-profile]
|
||||
aws_access_key_id = some-profile-access-key-id
|
||||
aws_secret_access_key = some-profile-secret-access-key
|
||||
`,
|
||||
},
|
||||
want: &aws.Credentials{
|
||||
AccessKeyID: "some-profile-access-key-id",
|
||||
SecretAccessKey: "some-profile-secret-access-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test GetS3Credentials default to default profile",
|
||||
args: args{
|
||||
config: map[string]string{},
|
||||
secretFileContents: `[default]
|
||||
aws_access_key_id = default-access-key-id
|
||||
aws_secret_access_key = default-secret-access-key
|
||||
[profile some-profile]
|
||||
aws_access_key_id = some-profile-access-key-id
|
||||
aws_secret_access_key = some-profile-secret-access-key
|
||||
`,
|
||||
},
|
||||
want: &aws.Credentials{
|
||||
AccessKeyID: "default-access-key-id",
|
||||
SecretAccessKey: "default-secret-access-key",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "velero-test-aws-credentials")
|
||||
defer os.Remove(tmpFile.Name())
|
||||
if err != nil {
|
||||
t.Errorf("GetS3Credentials() error = %v", err)
|
||||
return
|
||||
}
|
||||
// write the contents of the secret file to the temp file
|
||||
_, err = tmpFile.WriteString(tt.args.secretFileContents)
|
||||
if err != nil {
|
||||
t.Errorf("GetS3Credentials() error = %v", err)
|
||||
return
|
||||
}
|
||||
tt.args.config["credentialsFile"] = tmpFile.Name()
|
||||
got, err := GetS3Credentials(tt.args.config)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetS3Credentials() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got.AccessKeyID, tt.want.AccessKeyID) {
|
||||
t.Errorf("GetS3Credentials() got = %v, want %v", got.AccessKeyID, tt.want.AccessKeyID)
|
||||
}
|
||||
if !reflect.DeepEqual(got.SecretAccessKey, tt.want.SecretAccessKey) {
|
||||
t.Errorf("GetS3Credentials() got = %v, want %v", got.SecretAccessKey, tt.want.SecretAccessKey)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ var getGCPCredentials = repoconfig.GetGCPCredentials
|
||||
var getS3BucketRegion = repoconfig.GetAWSBucketRegion
|
||||
|
||||
type localFuncTable struct {
|
||||
getStorageVariables func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error)
|
||||
getStorageVariables func(*velerov1api.BackupStorageLocation, string, string, credentials.FileStore) (map[string]string, error)
|
||||
getStorageCredentials func(*velerov1api.BackupStorageLocation, credentials.FileStore) (map[string]string, error)
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ func (urp *unifiedRepoProvider) GetStoreOptions(param interface{}) (map[string]s
|
||||
return map[string]string{}, errors.Errorf("invalid parameter, expect %T, actual %T", RepoParam{}, param)
|
||||
}
|
||||
|
||||
storeVar, err := funcTable.getStorageVariables(repoParam.BackupLocation, urp.repoBackend, repoParam.BackupRepo.Spec.VolumeNamespace)
|
||||
storeVar, err := funcTable.getStorageVariables(repoParam.BackupLocation, urp.repoBackend, repoParam.BackupRepo.Spec.VolumeNamespace, urp.credentialGetter.FromFile)
|
||||
if err != nil {
|
||||
return map[string]string{}, errors.Wrap(err, "error to get storage variables")
|
||||
}
|
||||
@@ -447,7 +447,8 @@ func getStorageCredentials(backupLocation *velerov1api.BackupStorageLocation, cr
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getStorageVariables(backupLocation *velerov1api.BackupStorageLocation, repoBackend string, repoName string) (map[string]string, error) {
|
||||
func getStorageVariables(backupLocation *velerov1api.BackupStorageLocation, repoBackend string, repoName string,
|
||||
credentialFileStore credentials.FileStore) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
backendType := repoconfig.GetBackendType(backupLocation.Spec.Provider, backupLocation.Spec.Config)
|
||||
@@ -459,6 +460,13 @@ func getStorageVariables(backupLocation *velerov1api.BackupStorageLocation, repo
|
||||
if config == nil {
|
||||
config = map[string]string{}
|
||||
}
|
||||
if backupLocation.Spec.Credential != nil {
|
||||
credsFile, err := credentialFileStore.Path(backupLocation.Spec.Credential)
|
||||
if err != nil {
|
||||
return map[string]string{}, errors.WithStack(err)
|
||||
}
|
||||
config[repoconfig.CredentialsFileKey] = credsFile
|
||||
}
|
||||
|
||||
bucket := strings.Trim(config["bucket"], "/")
|
||||
prefix := strings.Trim(config["prefix"], "/")
|
||||
|
||||
@@ -437,11 +437,12 @@ func TestGetStorageVariables(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
credFileStore := new(credmock.FileStore)
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
getS3BucketRegion = tc.getS3BucketRegion
|
||||
|
||||
actual, err := getStorageVariables(&tc.backupLocation, tc.repoBackend, tc.repoName)
|
||||
actual, err := getStorageVariables(&tc.backupLocation, tc.repoBackend, tc.repoName, credFileStore)
|
||||
|
||||
require.Equal(t, tc.expected, actual)
|
||||
|
||||
@@ -530,7 +531,7 @@ func TestGetStoreOptions(t *testing.T) {
|
||||
BackupRepo: &velerov1api.BackupRepository{},
|
||||
},
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, errors.New("fake-error-2")
|
||||
},
|
||||
},
|
||||
@@ -544,7 +545,7 @@ func TestGetStoreOptions(t *testing.T) {
|
||||
BackupRepo: &velerov1api.BackupRepository{},
|
||||
},
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
@@ -604,7 +605,7 @@ func TestPrepareRepo(t *testing.T) {
|
||||
repoService: new(reposervicenmocks.BackupRepoService),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, errors.New("fake-store-option-error")
|
||||
},
|
||||
},
|
||||
@@ -615,7 +616,7 @@ func TestPrepareRepo(t *testing.T) {
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
@@ -635,7 +636,7 @@ func TestPrepareRepo(t *testing.T) {
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
@@ -656,7 +657,7 @@ func TestPrepareRepo(t *testing.T) {
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
@@ -733,7 +734,7 @@ func TestForget(t *testing.T) {
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
@@ -757,7 +758,7 @@ func TestForget(t *testing.T) {
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
@@ -785,7 +786,7 @@ func TestForget(t *testing.T) {
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
@@ -877,7 +878,7 @@ func TestInitRepo(t *testing.T) {
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
@@ -895,7 +896,7 @@ func TestInitRepo(t *testing.T) {
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
@@ -965,7 +966,7 @@ func TestConnectToRepo(t *testing.T) {
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
@@ -983,7 +984,7 @@ func TestConnectToRepo(t *testing.T) {
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
@@ -1057,7 +1058,7 @@ func TestBoostRepoConnect(t *testing.T) {
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
@@ -1084,7 +1085,7 @@ func TestBoostRepoConnect(t *testing.T) {
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
@@ -1110,7 +1111,7 @@ func TestBoostRepoConnect(t *testing.T) {
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
@@ -1197,7 +1198,7 @@ func TestPruneRepo(t *testing.T) {
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
@@ -1215,7 +1216,7 @@ func TestPruneRepo(t *testing.T) {
|
||||
getter: new(credmock.SecretStore),
|
||||
credStoreReturn: "fake-password",
|
||||
funcTable: localFuncTable{
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string) (map[string]string, error) {
|
||||
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, velerocredentials.FileStore) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
},
|
||||
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
|
||||
|
||||
@@ -27,11 +27,11 @@ import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
veleroimage "github.com/vmware-tanzu/velero/internal/velero"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
velerov1client "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/typed/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/label"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
@@ -47,16 +47,16 @@ const (
|
||||
)
|
||||
|
||||
type PodVolumeRestoreAction struct {
|
||||
logger logrus.FieldLogger
|
||||
client corev1client.ConfigMapInterface
|
||||
podVolumeBackupClient velerov1client.PodVolumeBackupInterface
|
||||
logger logrus.FieldLogger
|
||||
client corev1client.ConfigMapInterface
|
||||
crClient ctrlclient.Client
|
||||
}
|
||||
|
||||
func NewPodVolumeRestoreAction(logger logrus.FieldLogger, client corev1client.ConfigMapInterface, podVolumeBackupClient velerov1client.PodVolumeBackupInterface) *PodVolumeRestoreAction {
|
||||
func NewPodVolumeRestoreAction(logger logrus.FieldLogger, client corev1client.ConfigMapInterface, crClient ctrlclient.Client) *PodVolumeRestoreAction {
|
||||
return &PodVolumeRestoreAction{
|
||||
logger: logger,
|
||||
client: client,
|
||||
podVolumeBackupClient: podVolumeBackupClient,
|
||||
logger: logger,
|
||||
client: client,
|
||||
crClient: crClient,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,9 +86,11 @@ func (a *PodVolumeRestoreAction) Execute(input *velero.RestoreItemActionExecuteI
|
||||
|
||||
log := a.logger.WithField("pod", kube.NamespaceAndName(&pod))
|
||||
|
||||
opts := label.NewListOptionsForBackup(input.Restore.Spec.BackupName)
|
||||
podVolumeBackupList, err := a.podVolumeBackupClient.List(context.TODO(), opts)
|
||||
if err != nil {
|
||||
opts := &ctrlclient.ListOptions{
|
||||
LabelSelector: label.NewSelectorForBackup(input.Restore.Spec.BackupName),
|
||||
}
|
||||
podVolumeBackupList := new(velerov1api.PodVolumeBackupList)
|
||||
if err := a.crClient.List(context.TODO(), podVolumeBackupList, opts); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ limitations under the License.
|
||||
package restore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
@@ -25,7 +24,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
@@ -34,7 +32,6 @@ import (
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
"github.com/vmware-tanzu/velero/pkg/buildinfo"
|
||||
velerofake "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/fake"
|
||||
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
@@ -131,7 +128,7 @@ func TestPodVolumeRestoreActionExecute(t *testing.T) {
|
||||
name string
|
||||
pod *corev1api.Pod
|
||||
podFromBackup *corev1api.Pod
|
||||
podVolumeBackups []*velerov1api.PodVolumeBackup
|
||||
podVolumeBackups []runtime.Object
|
||||
want *corev1api.Pod
|
||||
}{
|
||||
{
|
||||
@@ -179,7 +176,7 @@ func TestPodVolumeRestoreActionExecute(t *testing.T) {
|
||||
builder.WithAnnotations("snapshot.velero.io/not-used", "")).
|
||||
InitContainers(builder.ForContainer("first-container", "").Result()).
|
||||
Result(),
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
podVolumeBackups: []runtime.Object{
|
||||
builder.ForPodVolumeBackup(veleroNs, "pvb-1").
|
||||
PodName("my-pod").
|
||||
PodNamespace("ns-1").
|
||||
@@ -225,7 +222,7 @@ func TestPodVolumeRestoreActionExecute(t *testing.T) {
|
||||
builder.ForVolume("vol-2").PersistentVolumeClaimSource("pvc-2").Result(),
|
||||
).
|
||||
Result(),
|
||||
podVolumeBackups: []*velerov1api.PodVolumeBackup{
|
||||
podVolumeBackups: []runtime.Object{
|
||||
builder.ForPodVolumeBackup(veleroNs, "pvb-1").
|
||||
PodName("my-pod").
|
||||
PodNamespace("original-ns").
|
||||
@@ -259,12 +256,7 @@ func TestPodVolumeRestoreActionExecute(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset()
|
||||
clientsetVelero := velerofake.NewSimpleClientset()
|
||||
|
||||
for _, podVolumeBackup := range tc.podVolumeBackups {
|
||||
_, err := clientsetVelero.VeleroV1().PodVolumeBackups(veleroNs).Create(context.TODO(), podVolumeBackup, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
crClient := velerotest.NewFakeControllerRuntimeClient(t, tc.podVolumeBackups...)
|
||||
|
||||
unstructuredPod, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.pod)
|
||||
require.NoError(t, err)
|
||||
@@ -294,7 +286,7 @@ func TestPodVolumeRestoreActionExecute(t *testing.T) {
|
||||
a := NewPodVolumeRestoreAction(
|
||||
logrus.StandardLogger(),
|
||||
clientset.CoreV1().ConfigMaps(veleroNs),
|
||||
clientsetVelero.VeleroV1().PodVolumeBackups(veleroNs),
|
||||
crClient,
|
||||
)
|
||||
|
||||
// method under test
|
||||
|
||||
@@ -28,8 +28,6 @@ import (
|
||||
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
"github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/fake"
|
||||
informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions"
|
||||
providermocks "github.com/vmware-tanzu/velero/pkg/plugin/velero/mocks/volumesnapshotter/v1"
|
||||
vsv1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/volumesnapshotter/v1"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
@@ -116,11 +114,6 @@ func TestExecutePVAction_NoSnapshotRestores(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var (
|
||||
client = fake.NewSimpleClientset()
|
||||
snapshotLocationInformer = informers.NewSharedInformerFactory(client, 0).Velero().V1().VolumeSnapshotLocations()
|
||||
)
|
||||
|
||||
r := &pvRestorer{
|
||||
logger: velerotest.NewLogger(),
|
||||
restorePVs: tc.restore.Spec.RestorePVs,
|
||||
@@ -132,7 +125,7 @@ func TestExecutePVAction_NoSnapshotRestores(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, loc := range tc.locations {
|
||||
require.NoError(t, snapshotLocationInformer.Informer().GetStore().Add(loc))
|
||||
require.NoError(t, r.kbclient.Create(context.TODO(), loc))
|
||||
}
|
||||
|
||||
res, err := r.executePVAction(tc.obj)
|
||||
|
||||
+14
-14
@@ -311,7 +311,7 @@ func (kr *kubernetesRestorer) RestoreWithResolvers(
|
||||
discoveryHelper: kr.discoveryHelper,
|
||||
resourcePriorities: kr.resourcePriorities,
|
||||
resourceRestoreHooks: resourceRestoreHooks,
|
||||
hooksErrs: make(chan error),
|
||||
hooksErrs: make(chan hook.HookErrInfo),
|
||||
waitExecHookHandler: waitExecHookHandler,
|
||||
hooksContext: hooksCtx,
|
||||
hooksCancelFunc: hooksCancelFunc,
|
||||
@@ -360,7 +360,7 @@ type restoreContext struct {
|
||||
discoveryHelper discovery.Helper
|
||||
resourcePriorities Priorities
|
||||
hooksWaitGroup sync.WaitGroup
|
||||
hooksErrs chan error
|
||||
hooksErrs chan hook.HookErrInfo
|
||||
resourceRestoreHooks []hook.ResourceRestoreHook
|
||||
waitExecHookHandler hook.WaitExecHookHandler
|
||||
hooksContext go_context.Context
|
||||
@@ -655,8 +655,8 @@ func (ctx *restoreContext) execute() (results.Result, results.Result) {
|
||||
ctx.hooksWaitGroup.Wait()
|
||||
close(ctx.hooksErrs)
|
||||
}()
|
||||
for err := range ctx.hooksErrs {
|
||||
errs.Velero = append(errs.Velero, err.Error())
|
||||
for errInfo := range ctx.hooksErrs {
|
||||
errs.Add(errInfo.Namespace, errInfo.Err)
|
||||
}
|
||||
ctx.log.Info("Done waiting for all post-restore exec hooks to complete")
|
||||
|
||||
@@ -1474,7 +1474,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
|
||||
}
|
||||
|
||||
if ctx.resourceModifiers != nil {
|
||||
if errList := ctx.resourceModifiers.ApplyResourceModifierRules(obj, groupResource.String(), ctx.log); errList != nil {
|
||||
if errList := ctx.resourceModifiers.ApplyResourceModifierRules(obj, groupResource.String(), ctx.kbClient.Scheme(), ctx.log); errList != nil {
|
||||
for _, err := range errList {
|
||||
errs.Add(namespace, err)
|
||||
}
|
||||
@@ -1532,18 +1532,17 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
|
||||
}
|
||||
|
||||
if restoreErr != nil {
|
||||
// check for the existence of the object in cluster, if no error then it implies that object exists
|
||||
// and if err then we want to judge whether there is an existing error in the previous creation.
|
||||
// if so, we will return the 'get' error.
|
||||
// otherwise, we will return the original creation error.
|
||||
// check for the existence of the object that failed creation due to alreadyExist in cluster, if no error then it implies that object exists.
|
||||
// and if err then itemExists remains false as we were not able to confirm the existence of the object via Get call or creation call.
|
||||
// We return the get error as a warning to notify the user that the object could exist in cluster and we were not able to confirm it.
|
||||
if !ctx.disableInformerCache {
|
||||
fromCluster, err = ctx.getResource(groupResource, obj, namespace, name)
|
||||
} else {
|
||||
fromCluster, err = resourceClient.Get(name, metav1.GetOptions{})
|
||||
}
|
||||
if err != nil && isAlreadyExistsError {
|
||||
ctx.log.Errorf("Error retrieving in-cluster version of %s: %v", kube.NamespaceAndName(obj), err)
|
||||
errs.Add(namespace, err)
|
||||
ctx.log.Warnf("Unable to retrieve in-cluster version of %s: %v, object won't be restored by velero or have restore labels, and existing resource policy is not applied", kube.NamespaceAndName(obj), err)
|
||||
warnings.Add(namespace, err)
|
||||
return warnings, errs, itemExists
|
||||
}
|
||||
}
|
||||
@@ -1899,10 +1898,11 @@ func (ctx *restoreContext) waitExec(createdObj *unstructured.Unstructured) {
|
||||
// on the ctx.podVolumeErrs channel.
|
||||
defer ctx.hooksWaitGroup.Done()
|
||||
|
||||
podNs := createdObj.GetNamespace()
|
||||
pod := new(v1.Pod)
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(createdObj.UnstructuredContent(), &pod); err != nil {
|
||||
ctx.log.WithError(err).Error("error converting unstructured pod")
|
||||
ctx.hooksErrs <- err
|
||||
ctx.hooksErrs <- hook.HookErrInfo{Namespace: podNs, Err: err}
|
||||
return
|
||||
}
|
||||
execHooksByContainer, err := hook.GroupRestoreExecHooks(
|
||||
@@ -1912,7 +1912,7 @@ func (ctx *restoreContext) waitExec(createdObj *unstructured.Unstructured) {
|
||||
)
|
||||
if err != nil {
|
||||
ctx.log.WithError(err).Errorf("error getting exec hooks for pod %s/%s", pod.Namespace, pod.Name)
|
||||
ctx.hooksErrs <- err
|
||||
ctx.hooksErrs <- hook.HookErrInfo{Namespace: podNs, Err: err}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1922,7 +1922,7 @@ func (ctx *restoreContext) waitExec(createdObj *unstructured.Unstructured) {
|
||||
|
||||
for _, err := range errs {
|
||||
// Errors are already logged in the HandleHooks method.
|
||||
ctx.hooksErrs <- err
|
||||
ctx.hooksErrs <- hook.HookErrInfo{Namespace: podNs, Err: err}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -66,6 +66,9 @@ func (a *ServiceAction) Execute(input *velero.RestoreItemActionExecuteInput) (*v
|
||||
if err := deleteNodePorts(service); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := deleteHealthCheckNodePort(service); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
res, err := runtime.DefaultUnstructuredConverter.ToUnstructured(service)
|
||||
@@ -76,6 +79,72 @@ func (a *ServiceAction) Execute(input *velero.RestoreItemActionExecuteInput) (*v
|
||||
return velero.NewRestoreItemActionExecuteOutput(&unstructured.Unstructured{Object: res}), nil
|
||||
}
|
||||
|
||||
func deleteHealthCheckNodePort(service *corev1api.Service) error {
|
||||
// Check service type and external traffic policy setting,
|
||||
// if the setting is not applicable for HealthCheckNodePort, return early.
|
||||
if service.Spec.ExternalTrafficPolicy != corev1api.ServiceExternalTrafficPolicyTypeLocal ||
|
||||
service.Spec.Type != corev1api.ServiceTypeLoadBalancer {
|
||||
return nil
|
||||
}
|
||||
|
||||
// HealthCheckNodePort is already 0, return.
|
||||
if service.Spec.HealthCheckNodePort == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search HealthCheckNodePort from server's last-applied-configuration
|
||||
// annotation(HealthCheckNodePort is specified by `kubectl apply` command)
|
||||
lastAppliedConfig, ok := service.Annotations[annotationLastAppliedConfig]
|
||||
if ok {
|
||||
appliedServiceUnstructured := new(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(lastAppliedConfig), appliedServiceUnstructured); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
healthCheckNodePort, exist, err := unstructured.NestedFloat64(*appliedServiceUnstructured, "spec", "healthCheckNodePort")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Found healthCheckNodePort in lastAppliedConfig annotation,
|
||||
// and the value is not 0. No need to delete, return.
|
||||
if exist && healthCheckNodePort != 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Search HealthCheckNodePort from ManagedFields(HealthCheckNodePort
|
||||
// is specified by `kubectl apply --server-side` command).
|
||||
for _, entry := range service.GetManagedFields() {
|
||||
if entry.FieldsV1 == nil {
|
||||
continue
|
||||
}
|
||||
fields := new(map[string]interface{})
|
||||
if err := json.Unmarshal(entry.FieldsV1.Raw, fields); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
_, exist, err := unstructured.NestedMap(*fields, "f:spec", "f:healthCheckNodePort")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
// Because the format in ManagedFields is `f:healthCheckNodePort: {}`,
|
||||
// cannot get the value, check whether exists is enough.
|
||||
// Found healthCheckNodePort in ManagedFields.
|
||||
// No need to delete. Return.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cannot find HealthCheckNodePort from Annotation and
|
||||
// ManagedFields, which means it's auto-generated. Delete it.
|
||||
service.Spec.HealthCheckNodePort = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteNodePorts(service *corev1api.Service) error {
|
||||
if service.Spec.Type == corev1api.ServiceTypeExternalName {
|
||||
return nil
|
||||
|
||||
@@ -36,7 +36,8 @@ import (
|
||||
func svcJSON(ports ...corev1api.ServicePort) string {
|
||||
svc := corev1api.Service{
|
||||
Spec: corev1api.ServiceSpec{
|
||||
Ports: ports,
|
||||
HealthCheckNodePort: 8080,
|
||||
Ports: ports,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -486,6 +487,164 @@ func TestServiceActionExecute(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "If PreserveNodePorts is True in restore spec then HealthCheckNodePort always preserved.",
|
||||
obj: corev1api.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "svc-1",
|
||||
},
|
||||
Spec: corev1api.ServiceSpec{
|
||||
HealthCheckNodePort: 8080,
|
||||
ExternalTrafficPolicy: corev1api.ServiceExternalTrafficPolicyTypeLocal,
|
||||
Type: corev1api.ServiceTypeLoadBalancer,
|
||||
Ports: []corev1api.ServicePort{
|
||||
{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
NodePort: 8080,
|
||||
},
|
||||
{
|
||||
Name: "hepsiburada",
|
||||
NodePort: 9025,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
restore: builder.ForRestore(api.DefaultNamespace, "").PreserveNodePorts(true).Result(),
|
||||
expectedRes: corev1api.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "svc-1",
|
||||
},
|
||||
Spec: corev1api.ServiceSpec{
|
||||
HealthCheckNodePort: 8080,
|
||||
ExternalTrafficPolicy: corev1api.ServiceExternalTrafficPolicyTypeLocal,
|
||||
Type: corev1api.ServiceTypeLoadBalancer,
|
||||
Ports: []corev1api.ServicePort{
|
||||
{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
NodePort: 8080,
|
||||
},
|
||||
{
|
||||
Name: "hepsiburada",
|
||||
NodePort: 9025,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "If PreserveNodePorts is False in restore spec then HealthCheckNodePort should be cleaned.",
|
||||
obj: corev1api.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "svc-1",
|
||||
},
|
||||
Spec: corev1api.ServiceSpec{
|
||||
HealthCheckNodePort: 8080,
|
||||
ExternalTrafficPolicy: corev1api.ServiceExternalTrafficPolicyTypeLocal,
|
||||
Type: corev1api.ServiceTypeLoadBalancer,
|
||||
},
|
||||
},
|
||||
restore: builder.ForRestore(api.DefaultNamespace, "").PreserveNodePorts(false).Result(),
|
||||
expectedRes: corev1api.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "svc-1",
|
||||
},
|
||||
Spec: corev1api.ServiceSpec{
|
||||
HealthCheckNodePort: 0,
|
||||
ExternalTrafficPolicy: corev1api.ServiceExternalTrafficPolicyTypeLocal,
|
||||
Type: corev1api.ServiceTypeLoadBalancer,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "If PreserveNodePorts is false in restore spec, but service is not expected, then HealthCheckNodePort should be kept.",
|
||||
obj: corev1api.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "svc-1",
|
||||
},
|
||||
Spec: corev1api.ServiceSpec{
|
||||
HealthCheckNodePort: 8080,
|
||||
ExternalTrafficPolicy: corev1api.ServiceExternalTrafficPolicyTypeCluster,
|
||||
Type: corev1api.ServiceTypeLoadBalancer,
|
||||
},
|
||||
},
|
||||
restore: builder.ForRestore(api.DefaultNamespace, "").PreserveNodePorts(false).Result(),
|
||||
expectedRes: corev1api.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "svc-1",
|
||||
},
|
||||
Spec: corev1api.ServiceSpec{
|
||||
HealthCheckNodePort: 8080,
|
||||
ExternalTrafficPolicy: corev1api.ServiceExternalTrafficPolicyTypeCluster,
|
||||
Type: corev1api.ServiceTypeLoadBalancer,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "If PreserveNodePorts is false in restore spec, but HealthCheckNodePort can be found in Annotation, then it should be kept.",
|
||||
obj: corev1api.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "svc-1",
|
||||
Annotations: map[string]string{annotationLastAppliedConfig: svcJSON()},
|
||||
},
|
||||
Spec: corev1api.ServiceSpec{
|
||||
HealthCheckNodePort: 8080,
|
||||
ExternalTrafficPolicy: corev1api.ServiceExternalTrafficPolicyTypeLocal,
|
||||
Type: corev1api.ServiceTypeLoadBalancer,
|
||||
},
|
||||
},
|
||||
restore: builder.ForRestore(api.DefaultNamespace, "").PreserveNodePorts(false).Result(),
|
||||
expectedRes: corev1api.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "svc-1",
|
||||
Annotations: map[string]string{annotationLastAppliedConfig: svcJSON()},
|
||||
},
|
||||
Spec: corev1api.ServiceSpec{
|
||||
HealthCheckNodePort: 8080,
|
||||
ExternalTrafficPolicy: corev1api.ServiceExternalTrafficPolicyTypeLocal,
|
||||
Type: corev1api.ServiceTypeLoadBalancer,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "If PreserveNodePorts is false in restore spec, but HealthCheckNodePort can be found in ManagedFields, then it should be kept.",
|
||||
obj: corev1api.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "svc-1",
|
||||
ManagedFields: []metav1.ManagedFieldsEntry{
|
||||
{
|
||||
FieldsV1: &metav1.FieldsV1{
|
||||
Raw: []byte(`{"f:spec":{"f:healthCheckNodePort":{}}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Spec: corev1api.ServiceSpec{
|
||||
HealthCheckNodePort: 8080,
|
||||
ExternalTrafficPolicy: corev1api.ServiceExternalTrafficPolicyTypeLocal,
|
||||
Type: corev1api.ServiceTypeLoadBalancer,
|
||||
},
|
||||
},
|
||||
restore: builder.ForRestore(api.DefaultNamespace, "").PreserveNodePorts(false).Result(),
|
||||
expectedRes: corev1api.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "svc-1",
|
||||
ManagedFields: []metav1.ManagedFieldsEntry{
|
||||
{
|
||||
FieldsV1: &metav1.FieldsV1{
|
||||
Raw: []byte(`{"f:spec":{"f:healthCheckNodePort":{}}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Spec: corev1api.ServiceSpec{
|
||||
HealthCheckNodePort: 8080,
|
||||
ExternalTrafficPolicy: corev1api.ServiceExternalTrafficPolicyTypeLocal,
|
||||
Type: corev1api.ServiceTypeLoadBalancer,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -24,14 +24,11 @@ import (
|
||||
discoveryfake "k8s.io/client-go/discovery/fake"
|
||||
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||
kubefake "k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
"github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/fake"
|
||||
)
|
||||
|
||||
// APIServer contains in-memory fakes for all of the relevant
|
||||
// Kubernetes API server clients.
|
||||
type APIServer struct {
|
||||
VeleroClient *fake.Clientset
|
||||
KubeClient *kubefake.Clientset
|
||||
DynamicClient *dynamicfake.FakeDynamicClient
|
||||
DiscoveryClient *DiscoveryClient
|
||||
@@ -43,7 +40,6 @@ func NewAPIServer(t *testing.T) *APIServer {
|
||||
t.Helper()
|
||||
|
||||
var (
|
||||
veleroClient = fake.NewSimpleClientset()
|
||||
kubeClient = kubefake.NewSimpleClientset()
|
||||
dynamicClient = dynamicfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(),
|
||||
map[schema.GroupVersionResource]string{
|
||||
@@ -65,7 +61,6 @@ func NewAPIServer(t *testing.T) *APIServer {
|
||||
)
|
||||
|
||||
return &APIServer{
|
||||
VeleroClient: veleroClient,
|
||||
KubeClient: kubeClient,
|
||||
DynamicClient: dynamicClient,
|
||||
DiscoveryClient: discoveryClient,
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
appsv1api "k8s.io/api/apps/v1"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -38,6 +39,8 @@ func NewFakeControllerRuntimeClientBuilder(t *testing.T) *k8sfake.ClientBuilder
|
||||
require.NoError(t, err)
|
||||
err = corev1api.AddToScheme(scheme)
|
||||
require.NoError(t, err)
|
||||
err = appsv1api.AddToScheme(scheme)
|
||||
require.NoError(t, err)
|
||||
err = snapshotv1api.AddToScheme(scheme)
|
||||
require.NoError(t, err)
|
||||
return k8sfake.NewClientBuilder().WithScheme(scheme)
|
||||
@@ -51,7 +54,13 @@ func NewFakeControllerRuntimeClient(t *testing.T, initObjs ...runtime.Object) cl
|
||||
require.NoError(t, err)
|
||||
err = corev1api.AddToScheme(scheme)
|
||||
require.NoError(t, err)
|
||||
err = appsv1api.AddToScheme(scheme)
|
||||
require.NoError(t, err)
|
||||
err = snapshotv1api.AddToScheme(scheme)
|
||||
require.NoError(t, err)
|
||||
return k8sfake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(initObjs...).Build()
|
||||
}
|
||||
|
||||
func NewFakeControllerRuntimeWatchClient(t *testing.T, initObjs ...runtime.Object) client.WithWatch {
|
||||
return NewFakeControllerRuntimeClientBuilder(t).WithRuntimeObjects(initObjs...).Build()
|
||||
}
|
||||
|
||||
@@ -35,12 +35,12 @@ import (
|
||||
"github.com/vmware-tanzu/velero/internal/credentials"
|
||||
"github.com/vmware-tanzu/velero/internal/credentials/mocks"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/scheme"
|
||||
"github.com/vmware-tanzu/velero/pkg/repository"
|
||||
udmrepo "github.com/vmware-tanzu/velero/pkg/repository/udmrepo"
|
||||
udmrepomocks "github.com/vmware-tanzu/velero/pkg/repository/udmrepo/mocks"
|
||||
"github.com/vmware-tanzu/velero/pkg/uploader"
|
||||
"github.com/vmware-tanzu/velero/pkg/uploader/kopia"
|
||||
"github.com/vmware-tanzu/velero/pkg/util"
|
||||
)
|
||||
|
||||
type FakeBackupProgressUpdater struct {
|
||||
@@ -64,7 +64,7 @@ func (f *FakeRestoreProgressUpdater) UpdateProgress(p *uploader.Progress) {}
|
||||
func TestRunBackup(t *testing.T) {
|
||||
var kp kopiaProvider
|
||||
kp.log = logrus.New()
|
||||
updater := FakeBackupProgressUpdater{PodVolumeBackup: &velerov1api.PodVolumeBackup{}, Log: kp.log, Ctx: context.Background(), Cli: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()}
|
||||
updater := FakeBackupProgressUpdater{PodVolumeBackup: &velerov1api.PodVolumeBackup{}, Log: kp.log, Ctx: context.Background(), Cli: fake.NewClientBuilder().WithScheme(util.VeleroScheme).Build()}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -121,7 +121,7 @@ func TestRunBackup(t *testing.T) {
|
||||
func TestRunRestore(t *testing.T) {
|
||||
var kp kopiaProvider
|
||||
kp.log = logrus.New()
|
||||
updater := FakeRestoreProgressUpdater{PodVolumeRestore: &velerov1api.PodVolumeRestore{}, Log: kp.log, Ctx: context.Background(), Cli: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()}
|
||||
updater := FakeRestoreProgressUpdater{PodVolumeRestore: &velerov1api.PodVolumeRestore{}, Log: kp.log, Ctx: context.Background(), Cli: fake.NewClientBuilder().WithScheme(util.VeleroScheme).Build()}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/internal/credentials"
|
||||
"github.com/vmware-tanzu/velero/internal/credentials/mocks"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/scheme"
|
||||
"github.com/vmware-tanzu/velero/pkg/util"
|
||||
)
|
||||
|
||||
type NewUploaderProviderTestCase struct {
|
||||
@@ -42,7 +42,7 @@ type NewUploaderProviderTestCase struct {
|
||||
func TestNewUploaderProvider(t *testing.T) {
|
||||
// Mock objects or dependencies
|
||||
ctx := context.Background()
|
||||
client := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
|
||||
client := fake.NewClientBuilder().WithScheme(util.VeleroScheme).Build()
|
||||
repoIdentifier := "repoIdentifier"
|
||||
bsl := &velerov1api.BackupStorageLocation{}
|
||||
backupRepo := &velerov1api.BackupRepository{}
|
||||
|
||||
@@ -33,9 +33,9 @@ import (
|
||||
"github.com/vmware-tanzu/velero/internal/credentials"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
"github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/scheme"
|
||||
"github.com/vmware-tanzu/velero/pkg/restic"
|
||||
"github.com/vmware-tanzu/velero/pkg/uploader"
|
||||
"github.com/vmware-tanzu/velero/pkg/util"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
)
|
||||
|
||||
@@ -148,7 +148,7 @@ func TestResticRunBackup(t *testing.T) {
|
||||
tc.volMode = uploader.PersistentVolumeFilesystem
|
||||
}
|
||||
if !tc.nilUpdater {
|
||||
updater := FakeBackupProgressUpdater{PodVolumeBackup: &velerov1api.PodVolumeBackup{}, Log: tc.rp.log, Ctx: context.Background(), Cli: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()}
|
||||
updater := FakeBackupProgressUpdater{PodVolumeBackup: &velerov1api.PodVolumeBackup{}, Log: tc.rp.log, Ctx: context.Background(), Cli: fake.NewClientBuilder().WithScheme(util.VeleroScheme).Build()}
|
||||
_, _, err = tc.rp.RunBackup(context.Background(), "var", "", map[string]string{}, false, parentSnapshot, tc.volMode, &updater)
|
||||
} else {
|
||||
_, _, err = tc.rp.RunBackup(context.Background(), "var", "", map[string]string{}, false, parentSnapshot, tc.volMode, nil)
|
||||
@@ -221,7 +221,7 @@ func TestResticRunRestore(t *testing.T) {
|
||||
}
|
||||
var err error
|
||||
if !tc.nilUpdater {
|
||||
updater := FakeBackupProgressUpdater{PodVolumeBackup: &velerov1api.PodVolumeBackup{}, Log: tc.rp.log, Ctx: context.Background(), Cli: fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()}
|
||||
updater := FakeBackupProgressUpdater{PodVolumeBackup: &velerov1api.PodVolumeBackup{}, Log: tc.rp.log, Ctx: context.Background(), Cli: fake.NewClientBuilder().WithScheme(util.VeleroScheme).Build()}
|
||||
err = tc.rp.RunRestore(context.Background(), "", "var", tc.volMode, &updater)
|
||||
} else {
|
||||
err = tc.rp.RunRestore(context.Background(), "", "var", tc.volMode, nil)
|
||||
|
||||
@@ -25,9 +25,10 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
|
||||
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/scheme"
|
||||
"github.com/vmware-tanzu/velero/pkg/util"
|
||||
)
|
||||
|
||||
// Encode converts the provided object to the specified format
|
||||
@@ -56,8 +57,11 @@ func To(obj runtime.Object, format string, w io.Writer) error {
|
||||
// Only objects registered in the velero scheme, or objects with their TypeMeta set will have valid encoders.
|
||||
func EncoderFor(format string, obj runtime.Object) (runtime.Encoder, error) {
|
||||
var encoder runtime.Encoder
|
||||
|
||||
codecFactory := serializer.NewCodecFactory(util.VeleroScheme)
|
||||
|
||||
desiredMediaType := fmt.Sprintf("application/%s", format)
|
||||
serializerInfo, found := runtime.SerializerInfoForMediaType(scheme.Codecs.SupportedMediaTypes(), desiredMediaType)
|
||||
serializerInfo, found := runtime.SerializerInfoForMediaType(codecFactory.SupportedMediaTypes(), desiredMediaType)
|
||||
if !found {
|
||||
return nil, errors.Errorf("unable to locate an encoder for %q", desiredMediaType)
|
||||
}
|
||||
@@ -69,7 +73,7 @@ func EncoderFor(format string, obj runtime.Object) (runtime.Encoder, error) {
|
||||
if !obj.GetObjectKind().GroupVersionKind().Empty() {
|
||||
return encoder, nil
|
||||
}
|
||||
encoder = scheme.Codecs.EncoderForVersion(encoder, v1.SchemeGroupVersion)
|
||||
encoder = codecFactory.EncoderForVersion(encoder, v1.SchemeGroupVersion)
|
||||
return encoder, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
|
||||
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1"
|
||||
)
|
||||
|
||||
var VeleroScheme = runtime.NewScheme()
|
||||
|
||||
func init() {
|
||||
localSchemeBuilder := runtime.SchemeBuilder{
|
||||
v1.AddToScheme,
|
||||
v2alpha1.AddToScheme,
|
||||
}
|
||||
utilruntime.Must(localSchemeBuilder.AddToScheme(VeleroScheme))
|
||||
}
|
||||
@@ -17,7 +17,7 @@ You can deploy Velero on Tencent [TKE](https://cloud.tencent.com/document/produc
|
||||
|
||||
Create an object bucket for Velero to store backups in the Tencent Cloud COS console. For how to create, please refer to Tencent Cloud COS [Create a bucket](https://cloud.tencent.com/document/product/436/13309) usage instructions.
|
||||
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315.E5.8D.95.E4.B8.AA.E6.8E.88.E6.9D.83) Tencent user instructions.
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315) Tencent user instructions.
|
||||
|
||||
## Get bucket access credentials
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ repository on AWS S3, the full backup repo path for namespace1 would be `https:/
|
||||
for namespace2 would be `https://s3-us-west-2.amazonaws.com/bucket/kopia/ns2`.
|
||||
|
||||
There may be additional installation steps depending on the cloud provider plugin you are using. You should refer to the
|
||||
[plugin specific documentation](supported-providers.md) for the must up to date information.
|
||||
[plugin specific documentation](supported-providers.md) for the most up to date information.
|
||||
|
||||
**Note:** Currently, Velero creates a secret named `velero-repo-credentials` in the velero install namespace, containing a default backup repository password.
|
||||
You can update the secret with your own password encoded as base64 prior to the first backup (i.e., FS Backup, data mover) targeting to the backup repository. The value of the key to update is
|
||||
|
||||
@@ -280,21 +280,23 @@ There are two ways to delete a Restore object:
|
||||
1. Deleting with `velero restore delete` will delete the Custom Resource representing the restore, along with its individual log and results files. It will not delete any objects that were created by the restore in your cluster.
|
||||
2. Deleting with `kubectl -n velero delete restore` will delete the Custom Resource representing the restore. It will not delete restore log or results files from object storage, or any objects that were created during the restore in your cluster.
|
||||
|
||||
## What happens to NodePorts when restoring Services
|
||||
## What happens to NodePorts and HealthCheckNodePort when restoring Services
|
||||
|
||||
During a restore, Velero deletes **Auto assigned** NodePorts by default and Services get new **auto assigned** nodePorts after restore.
|
||||
During a restore, Velero deletes **Auto assigned** NodePorts and HealthCheckNodePort by default and Services get new **auto assigned** nodePorts and healthCheckNodePort after restore.
|
||||
|
||||
Velero auto detects **explicitly specified** NodePorts using **`last-applied-config`** annotation and they are **preserved** after restore. NodePorts can be explicitly specified as `.spec.ports[*].nodePort` field on Service definition.
|
||||
Velero auto detects **explicitly specified** NodePorts using **`last-applied-config`** annotation and **`managedFields`**. They are **preserved** after restore. NodePorts can be explicitly specified as `.spec.ports[*].nodePort` field on Service definition.
|
||||
|
||||
### Always Preserve NodePorts
|
||||
Velero will do the same to the `HealthCheckNodePort` as `NodePorts`.
|
||||
|
||||
It is not always possible to set nodePorts explicitly on some big clusters because of operational complexity. As the Kubernetes [NodePort documentation](https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport) states, "if you want a specific port number, you can specify a value in the `nodePort` field. The control plane will either allocate you that port or report that the API transaction failed. This means that you need to take care of possible port collisions yourself. You also have to use a valid port number, one that's inside the range configured for NodePort use.""
|
||||
### Always Preserve NodePorts and HealthCheckNodePort
|
||||
|
||||
It is not always possible to set nodePorts and healthCheckNodePort explicitly on some big clusters because of operational complexity. As the Kubernetes [NodePort documentation](https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport) states, "if you want a specific port number, you can specify a value in the `nodePort` field. The control plane will either allocate you that port or report that the API transaction failed. This means that you need to take care of possible port collisions yourself. You also have to use a valid port number, one that's inside the range configured for NodePort use.""
|
||||
|
||||
The clusters which are not explicitly specifying nodePorts may still need to restore original NodePorts in the event of a disaster. Auto assigned nodePorts are typically defined on Load Balancers located in front of cluster. Changing all these nodePorts on Load Balancers is another operation complexity you are responsible for updating after disaster if nodePorts are changed.
|
||||
|
||||
Use the `velero restore create ` command's `--preserve-nodeports` flag to preserve Service nodePorts always, regardless of whether nodePorts are explicitly specified or not. This flag is used for preserving the original nodePorts from a backup and can be used as `--preserve-nodeports` or `--preserve-nodeports=true`. If this flag is present, Velero will not remove the nodePorts when restoring a Service, but will try to use the nodePorts from the backup.
|
||||
Use the `velero restore create ` command's `--preserve-nodeports` flag to preserve Service nodePorts and healthCheckNodePort always, regardless of whether nodePorts are explicitly specified or not. This flag is used for preserving the original nodePorts and healthCheckNodePort from a backup and can be used as `--preserve-nodeports` or `--preserve-nodeports=true`. If this flag is present, Velero will not remove the nodePorts and healthCheckNodePort when restoring a Service, but will try to use the nodePorts from the backup.
|
||||
|
||||
Trying to preserve nodePorts may cause port conflicts when restoring on situations below:
|
||||
Trying to preserve nodePorts and healthCheckNodePort may cause port conflicts when restoring on situations below:
|
||||
|
||||
- If the nodePort from the backup is already allocated on the target cluster then Velero prints error log as shown below and continues the restore operation.
|
||||
|
||||
|
||||
@@ -105,3 +105,83 @@ resourceModifierRules:
|
||||
- Update a container's image using a json patch with positional arrays
|
||||
kubectl patch pod valid-pod -type='json' -p='[{"op": "replace", "path": "/spec/containers/0/image", "value":"new image"}]'
|
||||
- Before creating the resource modifier yaml, you can try it out using kubectl patch command. The same commands should work as it is.
|
||||
|
||||
#### JSON Merge Patch
|
||||
You can modify a resource using JSON Merge Patch
|
||||
```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.
|
||||
- For more details, please refer to [this doc](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/)
|
||||
|
||||
#### Strategic Merge Patch
|
||||
You can modify a resource using Strategic Merge Patch
|
||||
```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.
|
||||
- For more details, please refer to [this doc](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/)
|
||||
|
||||
|
||||
### Conditional Patches in ALL Patch Types
|
||||
A new field `matches` is added in conditions to support conditional patches.
|
||||
|
||||
Example of matches 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 core group, `*.*` will apply to all the resources in all groups.
|
||||
- If both `*.groupName` and `namespaces` are specified, the patches will be applied to all the namespaced resources in this group in the specified namespaces and all the cluster resources in this group.
|
||||
@@ -17,7 +17,7 @@ You can deploy Velero on Tencent [TKE](https://cloud.tencent.com/document/produc
|
||||
|
||||
Create an object bucket for Velero to store backups in the Tencent Cloud COS console. For how to create, please refer to Tencent Cloud COS [Create a bucket](https://cloud.tencent.com/document/product/436/13309) usage instructions.
|
||||
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315.E5.8D.95.E4.B8.AA.E6.8E.88.E6.9D.83) Tencent user instructions.
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315) Tencent user instructions.
|
||||
|
||||
## Get bucket access credentials
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ You can deploy Velero on Tencent [TKE](https://cloud.tencent.com/document/produc
|
||||
|
||||
Create an object bucket for Velero to store backups in the Tencent Cloud COS console. For how to create, please refer to Tencent Cloud COS [Create a bucket](https://cloud.tencent.com/document/product/436/13309) usage instructions.
|
||||
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315.E5.8D.95.E4.B8.AA.E6.8E.88.E6.9D.83) Tencent user instructions.
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315) Tencent user instructions.
|
||||
|
||||
## Get bucket access credentials
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ You can deploy Velero on Tencent [TKE](https://cloud.tencent.com/document/produc
|
||||
|
||||
Create an object bucket for Velero to store backups in the Tencent Cloud COS console. For how to create, please refer to Tencent Cloud COS [Create a bucket](https://cloud.tencent.com/document/product/436/13309) usage instructions.
|
||||
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315.E5.8D.95.E4.B8.AA.E6.8E.88.E6.9D.83) Tencent user instructions.
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315) Tencent user instructions.
|
||||
|
||||
## Get bucket access credentials
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ You can deploy Velero on Tencent [TKE](https://cloud.tencent.com/document/produc
|
||||
|
||||
Create an object bucket for Velero to store backups in the Tencent Cloud COS console. For how to create, please refer to Tencent Cloud COS [Create a bucket](https://cloud.tencent.com/document/product/436/13309) usage instructions.
|
||||
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315.E5.8D.95.E4.B8.AA.E6.8E.88.E6.9D.83) Tencent user instructions.
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315) Tencent user instructions.
|
||||
|
||||
## Get bucket access credentials
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ You can deploy Velero on Tencent [TKE](https://cloud.tencent.com/document/produc
|
||||
|
||||
Create an object bucket for Velero to store backups in the Tencent Cloud COS console. For how to create, please refer to Tencent Cloud COS [Create a bucket](https://cloud.tencent.com/document/product/436/13309) usage instructions.
|
||||
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315.E5.8D.95.E4.B8.AA.E6.8E.88.E6.9D.83) Tencent user instructions.
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315) Tencent user instructions.
|
||||
|
||||
## Get bucket access credentials
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ You can deploy Velero on Tencent [TKE](https://cloud.tencent.com/document/produc
|
||||
|
||||
Create an object bucket for Velero to store backups in the Tencent Cloud COS console. For how to create, please refer to Tencent Cloud COS [Create a bucket](https://cloud.tencent.com/document/product/436/13309) usage instructions.
|
||||
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315.E5.8D.95.E4.B8.AA.E6.8E.88.E6.9D.83) Tencent user instructions.
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315) Tencent user instructions.
|
||||
|
||||
## Get bucket access credentials
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ You can deploy Velero on Tencent [TKE](https://cloud.tencent.com/document/produc
|
||||
|
||||
Create an object bucket for Velero to store backups in the Tencent Cloud COS console. For how to create, please refer to Tencent Cloud COS [Create a bucket](https://cloud.tencent.com/document/product/436/13309) usage instructions.
|
||||
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315.E5.8D.95.E4.B8.AA.E6.8E.88.E6.9D.83) Tencent user instructions.
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315) Tencent user instructions.
|
||||
|
||||
## Get bucket access credentials
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ You can deploy Velero on Tencent [TKE](https://cloud.tencent.com/document/produc
|
||||
|
||||
Create an object bucket for Velero to store backups in the Tencent Cloud COS console. For how to create, please refer to Tencent Cloud COS [Create a bucket](https://cloud.tencent.com/document/product/436/13309) usage instructions.
|
||||
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315.E5.8D.95.E4.B8.AA.E6.8E.88.E6.9D.83) Tencent user instructions.
|
||||
Set access to the bucket through the object storage console, the bucket needs to be **read** and **written**, so the account is granted data reading, data writing permissions. For how to configure, see the [permission access settings](https://cloud.tencent.com/document/product/436/13315) Tencent user instructions.
|
||||
|
||||
## Get bucket access credentials
|
||||
|
||||
|
||||
@@ -34,16 +34,12 @@ import (
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
velerov2alpha1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1"
|
||||
clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
|
||||
)
|
||||
|
||||
// Factory knows how to create a VeleroClient and Kubernetes client.
|
||||
type Factory interface {
|
||||
// BindFlags binds common flags (--kubeconfig, --namespace) to the passed-in FlagSet.
|
||||
BindFlags(flags *pflag.FlagSet)
|
||||
// Client returns a VeleroClient. It uses the following priority to specify the cluster
|
||||
// configuration: --kubeconfig flag, KUBECONFIG environment variable, in-cluster configuration.
|
||||
Client() (clientset.Interface, error)
|
||||
// KubeClient returns a Kubernetes client. It uses the following priority to specify the cluster
|
||||
// configuration: --kubeconfig flag, KUBECONFIG environment variable, in-cluster configuration.
|
||||
KubeClient() (kubernetes.Interface, error)
|
||||
@@ -114,19 +110,6 @@ func (f *factory) ClientConfig() (*rest.Config, error) {
|
||||
return Config(f.kubeconfig, f.kubecontext, f.baseName, f.clientQPS, f.clientBurst)
|
||||
}
|
||||
|
||||
func (f *factory) Client() (clientset.Interface, error) {
|
||||
clientConfig, err := f.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
veleroClient, err := clientset.NewForConfig(clientConfig)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return veleroClient, nil
|
||||
}
|
||||
|
||||
func (f *factory) KubeClient() (kubernetes.Interface, error) {
|
||||
clientConfig, err := f.ClientConfig()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user